Skip to content

feat(spec,app-shell)!: a form view's type is its layout only — simple, tabbed or wizard; split, drawer and modal are retired with pane, splitSize and splitResizable - #27229

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-9k357q
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-9k357q

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #27155
Ruling-ref: 6464210455
Refs #27153

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up owed — #27235 (hotcrm src/sales/views/lead.view.ts)

维护者速读

  • 改了什么:表单视图的 type 只表示版式,可选 simple(默认)、tabbed、wizard。split、drawer、modal 退役,写了会被拒并附改法。只服务分栏表单的 pane、splitSize、splitResizable 一起退役。Studio 新建视图不再提供这三个值。
  • 为什么改:落实 ADR-0050 裁决 A′。表单在哪里打开(抽屉或弹窗)由打开它的一方决定,不是表单自己声明;分栏已经在 feat(spec,ui)!: a record opens one way per object — navigation.mode keeps drawer (the default), page, new_window and none; modal, split and popover are retired; the split pane becomes a reader preference #26804 变成用户个人偏好。
  • 风险与回滚:这是收窄,旧元数据里的这三个值和三个键会被拒。已存的 sys_metadata 行在读取时自动转换,os migrate meta --from 17 改写源码。本仓只有 showcase 用到,已改写;hotcrm 的 lead.view.ts 由 hotcrm 席位跟进。回滚就是 revert 本 PR。
  • 先看:① packages/spec/src/ui/view.zod.ts,FORM_VIEW_TYPE_RETIRED 的三条改法文案和 type 枚举;② packages/spec/src/conversions/registry.ts,formViewPresentationTypesRemoved 的 apply;③ ui/packages/app-shell/src/views/metadata-admin/package-schema.ts,modal 改为 simple。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run --project repo src/ui/form-view-type-layout-retirement.test.ts,预期 31 个测试全部通过。
  • 需要你做的:无,席位落地。

What changes

Spec (@objectstack/spec)

  • FormViewSchema.type is simple | tabbed | wizard, still defaulting to simple. It is built with enumWithRetiredValues, so split, drawer and modal are refused by name, each with its own prescription. The prescriptions name live replacements only:
    • split → tabbed or the simple default, with subforms for editing child records beside the parent. Opening a record beside its list is the reader's Display-menu preference.
    • drawer / modal → the caller's open mode: the list's navigation.mode, the console's New/Edit dialogs (sized by modalSize), or an action type: 'modal' opening a page.
  • The split-only keys FormSectionSchema.pane (and the legacy groups[].pane), FormViewSchema.splitSize and FormViewSchema.splitResizable are retiredKey() tombstones, refused on every layout. The superRefine check that refused pane off a split form is gone, because the tombstone refuses it everywhere.
  • Kept, as a stated boundary: splitDirection, drawerSide, drawerWidth and modalSize. See Acceptance notes.
  • ADR-0087 kit (spec-property-retirement, value-level route plus key tombstones):
    • D2 conversion form-view-presentation-types-removed (retiredFromLoadPath: true, retiredAfter: '18.2.0', MAJOR_18 order 78). It deletes a retired type, so the form lands on simple, and strips splitSize, splitResizable and every section's or group's pane. It covers a view container's form and formViews.*, a form view item's config, and a flattened form overlay, both in stored rows and in existing sources. Its fixture has 9 notices.
    • D3 semantic entry ui-form-view-type-layout-only (its conversionIds names the conversion), plus a STEP18_RATIONALE fragment.
    • Three RETIRED_KEYS_BY_MAJOR rows: ui/FormSection:pane, ui/FormView:splitSize, ui/FormView:splitResizable.
    • Liveness (liveness/view.json): splitSize and splitResizable are dead with REMOVED notes; the type row is noted NARROWED; splitDirection, drawerSide and drawerWidth are noted inert on a form view; the sections note records the pane removal.
    • Removal-version pin: one JOIN row (three value refusals plus three tombstones).
  • Regenerated: authorable-surface/ui.json (three [RETIRED] rows), content/docs/references/**, liveness/state-counts/view.md. api-surface, react-blocks.md and skills/** are byte-identical (check:react-blocks and check:skill-refs are in sync).
  • Docs: content/docs/ui/views.mdx drops the three rows from the Form Types table and adds a paragraph on where a form opens. protocol/objectui/layout-dsl.mdx drops the pane mention.

Showcase (examples/app-showcase, private)

  • In task.view.ts, formViews.split is now type: 'tabbed' with its panes removed, and formViews.quick is now type: 'simple'. The default list declares navigation: { mode: 'drawer' } to show the open mode where it now lives.
  • coverage.ts has FORM_VIEW_TYPES = ['simple', 'tabbed', 'wizard']. Comments are updated in translations/index.ts and new-project-wizard.page.ts.
  • The lint package's frozen showcase snapshot (packages/lint/src/showcase-shape.fixtures.ts, test-only, not shipped) follows, with a header note on the protocol-forced edit.

Studio (@object-ui/app-shell): a defect this PR would otherwise create

The precedent is #26804, which fixed its own Studio inspector in the same PR. Claim amendments 2 and 3 cover this.

  • metadata-admin/anchors.ts: the New View form's formType picker offers simple | tabbed | wizard. It offered six values, and the three retired ones would now build a body the server refuses at save.
  • metadata-admin/default-schemas.ts: the view type enum drops the same three values.
  • metadata-admin/package-schema.ts: getPackageForm() returns type: 'simple'. It returned type: 'modal', a spec-typed literal that fails tsc (TS2322) after the narrowing. SectionedSchemaForm reads only tabbed, so the render is unchanged. PackageFormDialog is what opens it as a dialog.
  • ui test pins whose assertion the narrowing made false are re-pinned to the refusal, never deleted:
    • view-create-body.test.ts: the three layouts are accepted; a new case shows each retired type refused at config.type.
    • formSectionGroupReference-7051.test.tsx: pane beside group and the three retired types are refused.
    • form-spec.containers.test.tsx and FormPage.viewSpec.test.ts: pane leaves the "was undeclarable" literals, and a @ts-expect-error pins it as refused.

Why

ADR-0050 was Accepted by ruling 6464210455 on #27032 (letter A′, maintainer 「其他同意」). type is the form's layout. drawer and modal described where a form is opened, which the caller already models. split was a list-beside-record view mode, not a layout. The ruling mapped split to the list's navigation.mode: 'split', but #26804 (17b5152) retired that the day before and made the side-by-side pane a reader's preference. Following the seat's ruling, the prescriptions apply the ruling's intent to the current tree and name live values only.

Tests

  • New pin packages/spec/src/ui/form-view-type-layout-retirement.test.ts (31 tests, repo project, added to vitest.repo-tests.json):
    • five doors refuse each retired type with invalid_value at type, plus the prescription's first sentence and migrate sentence. The doors are FormViewSchema, view.form, view.formViews.*, a ViewItem form record and the flattened form overlay;
    • the five doors accept all three layouts;
    • a misspelt value keeps zod's own message;
    • defineStack refuses with STACK_SCHEMA_INVALID / 422 at views.0.formViews.quick.type;
    • pane is refused on every layout, in sections and in groups; splitSize and splitResizable are refused too;
    • the kept keys still parse;
    • @ts-expect-error checks at the authoring site;
    • the conversion over a stored view container and a stored form view item, its idempotence, its absence from the load path, its step-18 wiring and its D3 entry;
    • a tree-scoped absence walk with an anti-vacuity matcher test. As a probe, I put a copy of the pre-change task.view.ts in the tree; the walk named it, and I removed the copy.
  • Updated spec tests:
    • view.test.ts: the pane block now pins the tombstone;
    • view-form-features-root.test.ts: tabbed/wizard replace split;
    • section-group-reference.test.ts;
    • view-union-diagnostics.test.ts: three form types, plus a new case where a retired type settles nothing on its own and viewKind still does;
    • view-metadata-schema.test.ts: reads the vocabulary through the kept splitDirection;
    • retired-key-removal-version.pin.test.ts: JOIN row.
  • objectstack build of the showcase with formViews.quick changed to type: 'modal' (a temporary edit, restored with a trap) exits 2. The refusal sits at formViews.quick.type and names os migrate meta --from 17. The unmodified showcase builds with exit 0.

Acceptance notes

  • Kept keys. splitDirection, drawerSide and drawerWidth stay declared because the React ObjectForm block contract (react-blocks.ts) takes its dataProps from FormViewSchema. Tombstoning them would regenerate skills/objectstack-ui/references/react-blocks.md and make this PR Tier H. They are ui(plugin-form): ObjectForm drops the split / drawer / modal branches and SplitForm retires; showcase task views re-spelled (ADR-0050 cutover, renderer half of #27155) #27159's, together with the renderer branches (ObjectForm.tsx, ModalForm.tsx, SplitForm), the object-form formType in component.zod.ts, and the block-contract keys. modalSize stays because the console's record dialog reads it on every layout.
  • Survey of stored and authored metadata for the retired values, measured on this tree at base:
  • selectViewMetadataBranch reads its form vocabulary off FormViewSchema.type, so a bare { type: 'modal' } body now settles no branch, as an unknown type does. With viewKind: 'form' it still reaches the form overlay, whose parse names the prescription. This is diagnostic routing only, and it is pinned.
  • No skills/** edit, so the PR is not Tier H. docs/adr/** is untouched.

Verification

Head: edd4570f5. The final-head gate union and full dogfood ran on cd275780d. The last commit (edd4570f5) changes two lines in one ui test file, and the ratchet families plus plugin-form type-check were re-run on it (listed under Derived gates).

Type-checks and build

Each was run separately, with its exit code read before any pipe:

  • pnpm build --concurrency=2 (whole workspace, under os-verify-lock): VERDICT command-exit 0, 116/116 tasks. The first two runs exposed the package-schema.ts TS2322 and the console test literals; both are fixed in this PR.
  • pnpm --filter @objectstack/spec typecheck (tsc, scripts and the test layer): exit 0. check:test-typecheck is OK, with 132 pinned signatures held.
  • type-check for @object-ui/types, @object-ui/plugin-form, @object-ui/app-shell (including tsconfig.test.json) and @object-ui/console: exit 0 each. plugin-form was re-run on edd4570f5: exit 0.
  • pnpm --filter @objectstack/example-showcase typecheck: exit 0.
  • pnpm --filter @objectstack/example-showcase build: exit 0. With formViews.quick changed to type: 'modal': exit 2, with an invalid_value at formViews.quick.type whose message is the prescription, ending in the os migrate meta --from 17 sentence.
  • check:react-blocks (1 generated file in sync) and check:skill-refs (9 generated files in sync): react-blocks.md and skills/** are unchanged.
Tests
  • @objectstack/spec pnpm test (local project): 740 files, 22234 passed and 1 failed. The failure was view-metadata-schema.test.ts, which read the vocabulary through splitSize; I re-pointed it to the kept splitDirection, and the re-run passed 50/50.
  • @objectstack/spec pnpm test:repo: 82 files, 1355 tests passed.
  • The new pin, on its own: 31/31. With a copy of the pre-change task.view.ts placed in the tree, the absence walk names it.
  • @objectstack/lint validate-translation-references and validate-translatable-sections: 2 files, 252 tests passed.
  • @objectstack/example-showcase test: 41 files, 505 tests passed.
  • ui, from ui/: every test file that reads a spec view or form schema (371 files, chosen by grep for FormViewSchema|FormSectionSchema|ViewItemSchema|ViewSchema|ViewMetadataSchema|defineStack|FormView|FormSection). 370 passed and 1 skipped; 7978 tests passed and 9 skipped; VERDICT command-exit 0.
  • pnpm --dir ui exec vitest run scripts/: 175 files passed and 2 skipped; 5019 tests passed.
Derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 112 commands on cd275780d. Each ran with eval "$cmd" > log 2>&1; status=$?, and all 112 recorded exit 0.
  • dispatch-gates --ran: ✓ 112 derived famil(ies) accounted for — 112 run, 0 NOT-MEASURED (a DERIVED zero — all 112 recorded an exit code and none of them is 3).
  • An earlier run, before the full build, had 11 gates exit 3 with PREREQUISITE NOT MET because they read built output. After pnpm build all 11 exited 0, and the final union above includes them.
  • Re-run on edd4570f5, all exit 0: check:type-check-debt, check:type-check-coverage, check:nul-bytes, check:test-source-alias, check:undeclared-dep-imports, check-comment-mask-adoption.mjs and check:issue-citations.
  • eslint on the changed files: root exit 0. ui exit 0, with warnings only; formSectionGroupReference-7051.test.tsx carries the same 18 warnings as at base.
Full dogfood
  • pnpm --filter @objectstack/dogfood test under os-verify-lock, on cd275780d, after a whole-workspace build: VERDICT command-exit 0.
  • 351 files: 348 passed and 3 skipped. 2824 tests: 2804 passed and 20 skipped. No failures.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufziVeBj5ENSWqsDTaYe

…r wizard; split, drawer and modal retired with pane, splitSize and splitResizable

FormViewSchema.type is now `simple | tabbed | wizard` (default `simple`),
built with enumWithRetiredValues: `split`, `drawer` and `modal` are refused
by name with prescriptions that name live replacements only. The split-only
keys `sections[].pane`, `splitSize` and `splitResizable` become retiredKey()
tombstones. `splitDirection`, `drawerSide`, `drawerWidth` and `modalSize`
stay declared (console dialog sizing and the React ObjectForm block contract).

ADR-0087 kit: D2 conversion form-view-presentation-types-removed (retired
from the load path, retiredAfter 18.2.0), D3 entry
ui-form-view-type-layout-only, STEP18_RATIONALE fragment, three
RETIRED_KEYS_BY_MAJOR rows, liveness rows, removal-version JOIN row, and a
pin test with a tree-scoped absence walk. Showcase task views re-spelled to
tabbed / simple, with the drawer open mode shown on the list's
navigation.mode. Reference docs, authorable surface and liveness counts
regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziVeBj5ENSWqsDTaYe
…llow the narrowed form view type

The view-create form and the default view schema offered split, drawer and
modal, which the spec now refuses at save. The package form spells its
layout simple (its dialog is the caller's open mode). ui tests that pinned
spec acceptance of the retired members are re-pinned to their refusal.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziVeBj5ENSWqsDTaYe
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 22 documentable anchor(s). ⚠️ 8 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/state-counts/view.md, packages/spec/liveness/view.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 8 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/state-counts/view.md, packages/spec/liveness/view.json, …) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 56 of 208 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8f298a4addc1ff9ba464b4f30c91c23678ae519b — the merge of head 2c75635eb43d820fc57b9fbc13b94cd3bbc3ec0f into base ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8f298a4addc1ff9ba464b4f30c91c23678ae519b && git checkout 8f298a4addc1ff9ba464b4f30c91c23678ae519b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31 2c75635eb43d820fc57b9fbc13b94cd3bbc3ec0f && git checkout -B drift-repro ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31 && git merge --no-ff 2c75635eb43d820fc57b9fbc13b94cd3bbc3ec0f

node scripts/docs-audit/affected-docs.mjs --json ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2c75635eb43d820fc57b9fbc13b94cd3bbc3ec0f
Local-runs: none
Scope: .changeset/27155-form-view-type-layout-only.md, content/docs/protocol/objectui/index.mdx, content/docs/protocol/objectui/layout-dsl.mdx, content/docs/references/api/protocol.mdx, content/docs/references/ui/view.mdx, content/docs/ui/views.mdx, examples/app-showcase/src/coverage.ts, examples/app-showcase/src/system/translations/index.ts, examples/app-showcase/src/ui/pages/new-project-wizard.page.ts, examples/app-showcase/src/ui/views/task.view.ts, packages/lint/src/showcase-shape.fixtures.ts, packages/spec/authorable-surface/ui.json, packages/spec/liveness/state-counts/view.md, packages/spec/liveness/view.json, packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/retired-keys/18.ui__FormSection__pane.ts, packages/spec/src/migrations/entries/retired-keys/18.ui__FormView__splitResizable.ts, packages/spec/src/migrations/entries/retired-keys/18.ui__FormView__splitSize.ts, packages/spec/src/migrations/entries/semantic/18.ui-form-view-type-layout-only.ts, packages/spec/src/migrations/registry.ts.template, packages/spec/src/shared/retired-key-removal-version.pin.test.ts, packages/spec/src/ui/form-view-type-layout-retirement.test.ts, packages/spec/src/ui/section-group-reference.test.ts, packages/spec/src/ui/view-form-features-root.test.ts, packages/spec/src/ui/view-metadata-schema.test.ts, packages/spec/src/ui/view-union-diagnostics.test.ts, packages/spec/src/ui/view.test.ts, packages/spec/src/ui/view.zod.ts, packages/spec/vitest.repo-tests.json, ui/apps/console/src/components/FormPage.viewSpec.test.ts, ui/packages/app-shell/src/views/metadata-admin/anchors.ts, ui/packages/app-shell/src/views/metadata-admin/default-schemas.ts, ui/packages/app-shell/src/views/metadata-admin/form-spec.containers.test.tsx, ui/packages/app-shell/src/views/metadata-admin/package-schema.ts, ui/packages/app-shell/src/views/metadata-admin/view-create-body.test.ts, ui/packages/plugin-form/src/tests/formSectionGroupReference-7051.test.tsx + packages/spec/src/shared/retired-key.ts (retiredKey, enumWithRetiredValues), packages/spec/src/ui/react-blocks.ts, packages/spec/src/ui/component.zod.ts (objectFormSection, object-form formType), packages/spec/src/ui/action.zod.ts (ActionType), packages/spec/src/kernel/metadata-type-schemas.ts, packages/spec/src/conversions/apply.ts, packages/spec/src/conversions/stored.ts, packages/metadata/src/loaders/database-loader.ts, ui/packages/plugin-form/src/ObjectForm.tsx, ui/packages/plugin-form/src/DrawerForm.tsx, ui/packages/plugin-form/src/SplitForm.tsx, ui/packages/app-shell/src/views/RecordFormPage.tsx, ui/packages/app-shell/src/hooks/useActionModal.tsx, ui/packages/app-shell/src/utils/recordFormNavigation.ts, ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx, ui/packages/app-shell/src/views/metadata-admin/previews/block-config.ts, ui/packages/app-shell/src/views/metadata-admin/previews/form-preview.ts, ui/apps/console/src/components/FormPage.tsx, scripts/check-adr-0087-registration.mjs, .changeset/config.json, docs/adr/0087-metadata-protocol-upgrade-contract.md, docs/adr/0050-form-layout-vs-presentation.md, .claude/skills/pm-dispatch/SKILL.md (the Skills-docs: spelling)

Head note. The brief named edd4570f59. At read time (2026-10-28T11:33Z) /pulls/27229 reported head 2c75635eb43d820fc57b9fbc13b94cd3bbc3ec0f, one commit on top of edd4570f59 (docs(objectui): the form view example lists the three layouts, one line in content/docs/protocol/objectui/index.mdx, author session session_01WGufziVeBj5ENSWqsDTaYe, committed 2026-10-28T11:32Z), so the net diff is 36 paths, not 35. This record reviews the newest head. The code delta between the dev's measured heads (cd275780d for the gate union and dogfood, edd4570f5 for the ratchet re-run) and this head is two lines in one ui test file plus one docs comment line; the dev's local measurements therefore carry, and the check-runs on 2c75635e are the gate verdicts of record.

Inputs read: card #27155 body and all five comments (claim 6465227713, amendments 6465299594 / 6465901449 / 6466295596, os-dev-report 6467838432); ruling 6464210455 on #27032; #26804 (17b5152) and #24692; #27159 with scope note 6465248799 (edited) and #27153 with seat note 6465263085 (edited); ADR-0087 and ADR-0050 on origin/main ae16687a; the PR body, 36-path file list, both PR comments (docs-drift bot only), the three-dot diff at the head; check-runs on edd4570f59 (2026-10-28T11:32Z) and on 2c75635e (2026-10-28T11:37Z, 2026-10-28T11:43Z, and the final reading in judgment 7).

① Derived judgments

1. Refusals and prescriptions — hold. FormViewSchema.type is enumWithRetiredValues(['simple','tabbed','wizard'], FORM_VIEW_TYPE_RETIRED).default('simple') (view.zod.ts ≈L4614). The helper (retired-key.ts:402) returns a plain ZodEnum whose error map fires only on the retired member, so a misspelt value keeps zod's message and an omitted key materialises simple — the default is unchanged and parse(parse(x)) is stable (a parsed form carries no retired member and no tombstoned key). The three prescriptions name live values only: tabbed / the simple default / subforms (declared on FormViewSchema), navigation.mode with drawer as the default (#26804 made drawer the default and retired split), the console's New/Edit dialogs sized by modalSize, and action type: 'modal' (ActionType enum, action.zod.ts:585). No prescription names navigation.mode: 'split'; the ruling's mapping is applied to the current tree exactly as the seat's claim ruling (1) directs, and the PR body says so. The split-only keys are retiredKey() tombstones: FormSectionSchema.pane (≈L4053, so the legacy groups[] alias is covered too — groups folds onto sections after the refinement, and the author's path is pinned in view.test.ts), splitSize and splitResizable (≈L4697–4699). The old superRefine that refused pane off a split form is removed; that is correct, since the tombstone now refuses the key on every layout at the section itself, and the remaining refinement (features.* root) still runs on both buckets. Doors: FormViewSchema, ViewSchema.form / formViews.*, ViewItemSchema form arms and the flattened formOverlay all build on FormViewSchema via .extend (≈L4177, L6698); defineStack → STACK_SCHEMA_INVALID / 422; the /meta write door and Studio save parse view through getMetadataTypeSchema('view') = ViewMetadataSchema (kernel/metadata-type-schemas.ts:114), whose arms are those same members. All five schema doors plus defineStack are pinned in the new test for each of the three values, and the three keys are pinned on each of the three layouts, in sections and in groups.

2. ADR-0087 disposition — true in code and pinned. D2 form-view-presentation-types-removed (conversions/registry.ts, MAJOR_18 order 78, retiredFromLoadPath: true, retiredAfter: '18.2.0') deletes a retired type (the form lands on the simple default), strips splitSize / splitResizable and every sections[].pane / groups[].pane, over mapViewPayloads (a container's form, formViews.*, a form view item's config, the stored-row { views: [row] } wrap) and the assembled viewItems channel. It is retired from the authoring funnel (apply.ts:157 skips it unless includeRetired), and applyConversionsToStoredItem pins includeRetired to true (stored.ts header) — that primitive is what packages/metadata/src/loaders/database-loader.ts:884 calls per stored row, so a sys_metadata view row carrying type: 'split' | 'drawer' | 'modal', pane, splitSize or splitResizable converts on read to a simple form, exactly as the changeset says. Pinned over a stored container (five notices, ViewSchema.parse of the output, copy-on-write), a stored form view item, idempotence, load-path absence, step-18 wiring and the D3 entry ui-form-view-type-layout-only (conversionIds names the D2; acceptanceCriteria present); the fixture's 9 notices are accounted for. Three RETIRED_KEYS_BY_MAJOR rows (ui/FormSection:pane, ui/FormView:splitSize, ui/FormView:splitResizable), a STEP18_RATIONALE fragment (order 112, unique in the template) and a JOIN row in retired-key-removal-version.pin.test.ts (three value refusals + three tombstones). Stripping the keys on every form, not only split forms, is sound: pane off a split form was already a parse error on main, and splitSize / splitResizable were accepted on any type before, so a stored tabbed form carrying them now rehydrates clean. Survey count: I re-measured on origin/main with git grep — pane: authors are exactly task.view.ts:424–425 and the lint fixture :334–335; retired type authorings in form-view shape are task.view.ts:421 (split), :431 (drawer), the lint fixture's copy, and the spec-typed package-schema.ts:118 (modal); the two Studio enums (anchors.ts:305, default-schemas.ts:223); plus one docs comment the newest commit fixes (content/docs/protocol/objectui/index.mdx:234); no JSON fixture or seed. The PR's numbers match. The survey is repo-scoped by construction; live sys_metadata rows are not surveyable from here and the PR does not claim they are — the conversion above is the disposition for them, and that is the right answer to the question.

3. The kept keys — true, and no sentence overclaims. react-blocks.ts:240–245: the React ObjectForm block's schema is FormViewSchema, its dataProps include drawerSide, drawerWidth, modalSize, splitDirection, and its formType binding keeps 'split' | 'drawer' | 'modal' (untouched here, #27159's). Live readers: ObjectForm.tsx:559 (split arm → SplitForm.tsx:474), :597–598 (drawer arm → DrawerForm.tsx:304, 710–715). The diff's claim that the three are inert on a form view is accurate in-repo: the only form-view routes into those arms are RecordFormPage.tsx:324–339 (pageFormType is derived from formDef.type, so 'split' is now unreachable; it still compiles because ['tabbed','wizard','split'].includes(formDef.type) is a string-array read and the ternary's result narrows into the declared union) and useActionModal.tsx:280 (formType: 'modal' → ModalForm, which reads modalSize only). No sentence in the diff says they are retired; view.zod.ts JSDoc and liveness/view.json both say "stays declared … leaves on #27159". modalSize "read on every layout" is true: recordFormNavigation.ts:236–240 copies the form view's modalSize onto the record dialog irrespective of type. The liveness rows: splitSize / splitResizable → dead with REMOVED notes, type NARROWED, kept keys live with reasons; counts 76 / 7 / 83 consistent (two rows moved). Tombstoning the three would regenerate skills/objectstack-ui/references/react-blocks.md (Tier H) — the claim amendment's reason to keep them is confirmed by the generator's dataProps source.

4. ui breakage — the form-view surfaces are closed; the remaining carriers are the object-form block's own, by design. Fixed here: Studio's New View picker (anchors.ts), the view default schema (default-schemas.ts), and package-schema.ts (modal → simple; its only reader SchemaForm.tsx:1561 branches on formType === 'tabbed', so the render is unchanged and PackageFormDialog keeps the dialog). view.form.ts (the spec-side Studio form for view) hardcodes no form-type list. MCP / AI authoring tools, packages/create-objectstack, templates and seeds: no form-type vocabulary found. Surfaces that can still emit split / drawer / modal are all the page block object-form's formType, whose schema is component.zod.ts:6847 with its own section shape (objectFormSection, its own pane enum at ≈L6800 — not derived from FormSectionSchema, so this PR leaks no narrowing onto it): the page-block inspector previews/block-config.ts:188–196, plugin-form/src/index.tsx:295, types/src/zod/objectql.zod.ts:1017, the console reader set FormPage.tsx:259, form-preview.ts:20, and ui/content/docs/api/schema-reference.md:898. Those produce a block, never a form view, and are #27159's per scope note 6465248799; the dev's report names most of them, I add block-config.ts and schema-reference.md:898 (see ③). The renderer's type-check: RecordFormPage.tsx compiles as reasoned above; ObjectForm.tsx reads its own ObjectFormSchema (types/src/objectql.ts:1833), untouched. The showcase views render as tabbed / simple; navigation: { mode: 'drawer' } on the default list is a live value. The dev's ui type-checks (types, plugin-form, app-shell incl. tsconfig.test.json, console) exited 0 on edd4570f5; the CI Type Check UI, Build & E2E UI and Test UI runs on this head are the record (judgment 7).

5. Tests — re-pinned, nothing weakened. ui: view-create-body.test.ts keeps the three layouts accepted and adds the refusal at config.type plus the enum equality; formSectionGroupReference-7051.test.tsx turns the two acceptances into refusals with path and message pinned (['pane'], was removed in @objectstack/spec 18.3.0; ['invalid_value:type']); form-spec.containers.test.tsx and FormPage.viewSpec.test.ts move pane out of the "was undeclarable" literals into a @ts-expect-error pin. One render assertion was deleted — expect(screen.queryByText('secondary')).not.toBeInTheDocument() — which asserted the inertness of a key the spec no longer admits; the tsc pin in the same file is the closer-to-code replacement, so this is a re-pin, not a loss. Spec: view.test.ts replaces the three split-acceptance tests with refusal pins carrying the tombstone's first sentence (sections, groups, defaulted type) and adds the split refusal; view-form-features-root.test.ts becomes it.each(['tabbed','wizard']) (wider than before); section-group-reference.test.ts keeps columns beside group; view-union-diagnostics.test.ts adds the retired-type routing case; view-metadata-schema.test.ts swaps splitSize for splitDirection (same vocabulary question). The new pin (31 tests) covers every refusal: 5 doors × 3 values with code, path, first sentence and migrate sentence; defineStack envelope; tsc at the authoring site for a value and for both tombstones; pane on 3 layouts × 2 buckets; splitSize / splitResizable on 3 layouts; the stated-boundary acceptance of the four kept keys; conversion, idempotence, load-path absence, wiring, D3; and a tree-scoped absence walk with an anti-vacuity matcher test and visited/mentioning floors. Its regexes judge only type: literals in form-view shape — a stated bound; the index.mdx:234 comment it could not see is fixed by the last commit.

6. Declarations. .changeset/27155-form-view-type-layout-only.md: @objectstack/spec: minor, @object-ui/app-shell: minor; Clause-②: no (narrowing); Metadata-protocol: narrowed (…); exactly one ADR-0087 disposition marker, an HTML comment reading adr-0087: registered form-view-presentation-types-removed, ui-form-view-type-layout-only (the registered ID[, ID] shape check-adr-0087-registration.mjs:62 reads); **BREAKING** banner; FROM → TO and the one-line fix in the body; the kept keys stated. @object-ui/plugin-form, @object-ui/console and @objectstack/lint are touched only in test / fixture files that no dist carries (packages/lint/tsup.config.ts entries: index, runtime, rule-explanations; the fixture is imported by two tests only), so they publish nothing and owe no changeset (AGENTS.md Post-Task step 3). The ADR-0087: disposition is also written into the liveness notes and the view.zod.ts comments. Regenerated and consistent: authorable-surface/ui.json (three [RETIRED]), content/docs/references/** (every six-value type Enum row on main becomes three), liveness/state-counts/view.md. api-surface (export names), react-blocks.md and skills/** are unchanged by construction — retired-key.ts §Registration says a value narrowing is byte-invisible to the ratchets, and the tombstones keep each key in the shape. sdui.manifest.json is untouched and owes nothing here: it enumerates the ui registry (the block's own schema), and the dev's out-of-scope finding that its object-form inputs carry splitSize / splitResizable is the block's, #27159's. Docs: views.mdx Form Types table and the new paragraph, layout-dsl.mdx pane mention, index.mdx example comment — all three handwritten mentions on main are now gone; pages.mdx "split pane" is the reader preference (#26804), not this surface. Skills-docs: follow-up owed — hotcrm src/sales/views/lead.view.ts is the exact spelling pm-dispatch/SKILL.md:313 prescribes for an owed follow-up; skills/** teaches no retired form-view value (its only six-value list is the block's formType in react-blocks.md:21, which survives). Fixes #27155, Refs #27153 (not Part of, correct since the drawer keys stay), Ruling-ref: 6464210455, and #27159 named in Acceptance notes.

7. Gates (check-runs on 2c75635e, final reading 2026-10-28T11:44Z; 61 runs). Conclusions ARE the verdicts; an in_progress run is recorded as in progress, not as a pass. 28 success: Build Core, Build UI Deps, Build & E2E UI, Build Docs, Check Changeset, Spec property liveness, Governed Surface Queue Guard, PR-body guards (claim · duplicate · Part-of · single-writer path), Check PR Size, Check Documentation Links, Flag docs affected by code changes, Dogfood Regression Gate, Lint & Repo Gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI release & engine gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Type Check · source gates, Type Check · debt ledger, Type Check · consumer gates, Plan Test Core shards, Test Core (8/8), Auto Label, filter. 7 skipped by path filter or opt-in (UI Published Artifacts, Dogfood shards, Dogfood Verify CLI, Temporal Conformance, Packed-tarball smoke, 18.x upgrade harness, Schedule gate). 0 failed. 26 still in progress at this reading: Test Core (1/8–7/8), Test UI (1/16–16/16), Lint UI, Type Check UI, Type Check · workspace — these are the ui type-check and the test shards the renderer question in judgment 4 rests on; the dev's local runs of the same surfaces (ui type-checks exit 0; 371 spec-reading ui test files 370 passed / 1 skipped; spec test 22234 passed; test:repo 1355 passed; 112/112 derived gates exit 0, 0 NOT-MEASURED; full dogfood 348 passed / 3 skipped, 2804 tests, on cd275780d) are the dev's measurements, not CI's. This record's contract verdict stands on the code reading above; the Tier S landing condition "every check green" is not yet met at this reading, and the owning seat re-reads the head's check-runs before landing — a red in any of the 26 reopens this record.

② Semver level

@objectstack/spec: minor — correct. The kind is narrowed and the arm (narrowing), which AGENTS.md step 3 prices at least minor on a package the diff moves; the BREAKING banner and the ADR-0087 disposition ride with it; ruling 6464210455 (letter A′) and the #24692 B′ / #26804 precedents fix that a narrowing ships as a minor because this repository has no spec-major window, so ADR-0050's own "spec-major" wording does not bind the level. The prescriptions and retiredAfter: '18.2.0' name 18.3.0, the next minor from the current 18.2.0, consistently with #26804. @object-ui/app-shell: minor — a private package (privatePackages.version: true, not in the fixed group), moved by the diff with the same narrowed kind; the #26804 precedent priced its ui consumers minor too. No changeset for plugin-form, console, lint (test-only, nothing published). check-changeset-no-major exit 0 per the dev; Check Changeset green on this head.

③ Boundary flags

Implemented-by: claude/project-thread-9k357q
Reviewed-by: session_01WGufzgbJhFoXYsLSe5WSPP

VERDICT: PASS

Posted 2026-10-28T11:45Z · an isolated subagent of the reviewing session; one GitHub write (this comment), no MCP writes.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 (席位终稿)

  • 改了什么:表单视图的 type 只剩三种布局:simple、tabbed、wizard。
    • split、drawer、modal 发布时会被拒绝,拒绝信息给出迁移方法,只指向仍然有效的写法。
    • 随 split 一起退役的 section.pane、splitSize、splitResizable 也一样被拒绝。
    • 已存的元数据行在读取时自动转换(ADR-0087)。
    • Studio 新建视图时只再提供三种布局。包表单原来写的 modal 改成 simple,显示效果不变。
  • 为什么改:按裁决 A′(6464210455,ADR-0050 生效)。一个字段混了布局和打开方式两层意思;“在弹窗里放分页表单”早已能做,这三个值没有业务在用。
  • 风险与回滚:这是收窄,写了旧值的元数据会被拒,但已存的行会自动转换。本仓只有 showcase 用到,已改写。hotcrm 的 lead.view.ts 另立卡 hotcrm: lead.view.ts authors form views with the retired types split / drawer / modal — re-spell to layouts when hotcrm takes the spec release that carries #27229 #27235,跟 hotcrm 升级 spec 时一起改。回滚 = revert 本 PR。
  • 先看:packages/spec/src/ui/view.zod.ts 的 FormViewSchema,以及转换 form-view-presentation-types-removed。
  • 两分钟验证:写一个 type: 'modal' 的表单视图跑 objectstack build,预期被拒,且信息写明迁移方法。
  • 需要你做的:无,席位落地。

席位意见:接受。契约复核 PASS(6468131018),确认:各个入口都拒绝、迁移方法只指向有效写法、已存行在读取时转换、保留的三个键确实还有 React 组件在读。渲染器那一半(ObjectForm 的分支、页面块的 formType、复核新找到的两处)在子卡 #27159。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-28T11:55Z

Head: 2c75635eb43d820fc57b9fbc13b94cd3bbc3ec0f · card #27155 · claim 6465227713 with amendments 1–3 · Ruling-ref 6464210455 (letter A′)

  1. 形态: a draft PR to main carrying Fixes #27155 and Refs #27153. 36 paths, +1072 / −177, inside the amended claim surface: spec, the conversion, the liveness ledger, regenerated artifacts, the showcase, and the Studio choosers, package form and re-pinned ui tests from amendments 2 and 3. check-governed-merges --pr 27229 reports NOT governed (1249 lines), so the ordinary queue landing applies. The assignee is os-support-ai.
  2. 前提实测: the premise holds on main: six type values, and the three presentation values are used only in demo views. The review read head 2c75635e, which is one docs commit past the dev's edd4570f, and confirmed four things:
  3. 门禁 check-runs (on this head): 68 runs read at 2026-10-28T11:53Z: 59 success, 9 skipped, none red or pending. All required contexts are green. Dev-side readings: 112/112 gates and 0 NOT-MEASURED; full dogfood 348 passed and 3 skipped; full build and ui type-checks green.
  4. 接受的偏差:
  5. 范围外去向:
  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body (with hotcrm: lead.view.ts authors form views with the retired types split / drawer / modal — re-spell to layouts when hotcrm takes the spec release that carries #27229 #27235 in its Skills-docs: line, applied this round), the changesets, the tombstone messages and the D2/D3 disposition text. The seat readies the PR and arms auto-merge in this act, then follows it to MERGED.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/xl tests tooling

Projects

None yet

2 participants