Skip to content

feat(spec)!: retire FormView drawerSide / drawerWidth — no form view opens in a drawer - #29287

Merged
os-zhuang merged 6 commits into
mainfrom
claude/project-thread-6xhq0v
Nov 2, 2026
Merged

os-zhuang merged 6 commits into
mainfrom
claude/project-thread-6xhq0v

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Requested by Steve · project thread
Fixes #29012

Clause-②: no (narrowing)
Metadata-protocol: narrowed

维护者速读

  • 改了什么:表单视图(form view)上的 drawerSide / drawerWidth 两个键退役,写了就在保存时被拒并给出修法;存量数据读取时自动删掉这两个键。React 页面里 ObjectForm 组件的同名属性保留不变。
  • 为什么改:表单视图早已不能以抽屉方式打开(type: 'drawer' 已在本版本退役),控制台、记录表单页和 Studio 都不读这两个键,作者写了能存能发布却毫无效果,属于“声明了但不生效”。
  • 风险与回滚:只收窄表单视图的可写键;已存的表单视图读取时被转换,界面上看到的完全一样。回滚即还原本 PR。
  • 先看:packages/spec/src/ui/view.zod.ts(两个墓碑和提示文案)、packages/spec/src/ui/react-blocks.ts(两个属性改由 ObjectForm 自己声明)、packages/spec/src/conversions/registry.ts 的 form-view-drawer-keys-removed。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run --project repo src/ui/form-view-drawer-keys-retirement.test.ts,应看到 29 个用例全部通过。
  • 需要你做的:无,席位落地。

What changes

  • FormViewSchema.drawerSide / drawerWidth are retiredKey() tombstones (packages/spec/src/ui/view.zod.ts). Each is refused at every form-view door (a view's form, formViews.*, a form view item record, a flattened form overlay, defineStack as STACK_SCHEMA_INVALID / 422) with a prescription that says why it was inert, what to do instead (navigation.mode / navigation.size for a list's record drawer; the React ObjectForm block's own props for a page drawer) and the os migrate meta --from 17 sentence. TypeScript rejects both at a FormView position. The describe that deferred the question to closed spec(ui): FormViewSchema declares drawerSide / drawerWidth and buttons.reset, but no console entry point renders a drawer form or a reset button; implement or retire them (ADR-0049) #27153 is gone.
  • The React ObjectForm block keeps both props (packages/spec/src/ui/react-blocks.ts). They leave the block's dataProps (which read FormViewSchema) and move to its overlay as binding props with the same types; drawerSide keeps its description, and drawerWidth's drops the form view's [DEPRECATED → size buckets] tag (the block's drawer has no size bucket) and says what DrawerForm does with the value: the drawer's width from the left or right edge, its height from the top or bottom, the [finding] ADR-0082 D4 ratchet: react-blocks <ObjectForm> gains 17 registry-only props after objectui#4648/#4901 — spec has not caught up #9392 rule for a prop only the react tier reads. The overlay entry shares the tombstone's name, so REACT_OVERLAY_SHADOWS.ObjectForm ledgers it with that reason. The regenerated skills/objectstack-ui/references/react-blocks.md moves the two rows from the data group to the binding group; validate-react-page-props reads the overlay, so a React page's drawerSide="right" stays known.
  • ADR-0087 kit. D2 conversion form-view-drawer-keys-removed (toMajor: 18, retiredFromLoadPath, retiredAfter: '18.2.0', stripKeys on every form payload and the assembled viewItems channel; never walks page components, so the object-form page block keeps its keys). D3 semantic entry ui-form-view-drawer-keys-retired. RETIRED_KEYS_BY_MAJOR[18] entries ui/FormView:drawerSide and ui/FormView:drawerWidth. Step-18 rationale fragment. Removal-version pin rows.
  • Ledger and generated artifacts. liveness/view.json: both rows dead with the tombstone note (view counts 75/8 → 73/10). Regenerated authorable-surface/ui.json (two [RETIRED] rows), content/docs/references/{api/protocol,ui/view}.mdx, liveness/state-counts/view.md, the react-blocks contract.
  • Changeset .changeset/29012-form-view-drawer-keys-retired.md: BREAKING, minor on @objectstack/spec and patch on @objectstack/skills (the regenerated skills/objectstack-ui/references/react-blocks.md ships there, as in 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 changeset), FROM → TO, the one-line fix, registered ADR-0087 marker.

Forced test edits

  • src/ui/form-view-type-layout-retirement.test.ts: the "STATED BOUNDARY: drawerSide, drawerWidth and modalSize stay declared" pin now holds modalSize only, and its stored-row case uses modalSize: 'lg' as the bystander instead of drawerSide: 'left' (the new conversion strips that). The header's bookkeeping line points at the new pin.
  • src/conversions/registry.ts form-view-presentation-types-removed fixture: the quick form's bystander is title: 'Quick' instead of drawerSide: 'left', because fixtures must stay disjoint under whole-table replay. Its docblock now says the drawer keys left later in the same release.
  • src/ui/view-metadata-schema.test.ts: the "a FormView key nobody would think to allow-list" probe uses showStepIndicator instead of drawerSide.
  • src/system/metadata-form-zod-reconciliation.test.ts: the omit row for view.drawerWidth is dropped, as the ledger asks for a key that became a tombstone.

New pin

src/ui/form-view-drawer-keys-retirement.test.ts (repo project, listed in vitest.repo-tests.json): both keys refused at five doors with the prescription's first sentence and the migrate sentence; a modalSize control at each door; defineStack envelope; RETIRED_KEYS_BY_MAJOR; @ts-expect-error at the authoring site; the block overlay publishes both and the object-form page block still accepts them; the conversion on a stored container and a form view item record, never on a page block, idempotent, off the load path, wired into step 18, with its D3 entry; tree-scoped absence over the five roots spec#test already declares.

Why

Triage ruling on the card (comment 6571544001): retire under ADR-0049. Measured at origin/main 104cd1a:

  • No form-view surface reads either key. The only code that hands them to DrawerForm is ui/packages/plugin-form/src/ObjectFormContainer.tsx, which serves the object-form block's formType='drawer'; a form view's type cannot be drawer since spec(view): narrow FormView.type to layout only (simple | tabbed | wizard) — retire split, drawer, modal with tombstones (ADR-0050 Accepted, #27032 letter A′) #27155. ui/packages/plugin-designer/src/FieldDesigner.tsx composes its own DrawerFormSchema in code. The console form page, the New / Edit dialogs (which read modalSize only) and the record form page never read them.
  • The ui prop types are the block's own. DrawerForm.tsx declares drawerSide? / drawerWidth? on DrawerFormSchema, and @object-ui/types ObjectFormSchema declares its own; neither derives from FormViewSchema, so ui/ needs no edit (checklist 4 measured, below).
  • drawerWidth's size-bucket deprecation had no conversion. The only conversion text naming it was the presentation-types docblock saying it stays. This entry deletes it; the prescription names navigation.size.
  • Authored uses on a form view: 0. examples/: 0 form views (2 hits are ObjectForm formType="drawer" drawerSide="right" JSX on React pages, the block, unaffected); skills/: 0 (the generated block contract only). Control leg: the live form-view key submitBehavior reads 8 in examples/ on the same tree. HotCRM fixtures are not in this repository: NOT MEASURED.

Tests

All runs went through os-verify-lock (each VERDICT command-exit 0 unless stated). @objectstack/spec, @objectstack/lint and the spec repo project ran at 0dc8fcc3, whose tree differs from the head 8c7bfc30 only by the changeset file; ui/ plugin-form, the gates and dogfood ran at 8c7bfc30.

Round 2 re-ran at the head ffc3fe89, under os-verify-lock: turbo run build --filter='./packages/**' exit 0; @objectstack/spec test Test Files 790 passed (790), Tests 23373 passed; @objectstack/lint test Test Files 189 passed (189), Tests 7830 passed; dispatch-gates --commands 112 derived, 112 run, reconciled with --ran as a DERIVED zero, every exit 0 except check-empty-changeset --base origin/main (exit 1 by design, above). The spec repo project, ui plugin-form and dogfood readings are from 8c7bfc30; since then the code delta is one description string in react-blocks.ts plus its regenerated row, and the rest is changeset prose.

  • pnpm --filter @objectstack/spec typecheck: exit 0 (compiles the new pin's two @ts-expect-error lines).
  • pnpm --filter @objectstack/spec test (local project): Test Files 789 passed, 1 skipped (790); Tests 23371 passed, 1 skipped, 1 todo.
  • pnpm --filter @objectstack/spec test:repo (repo project, run locally because this diff adds and edits repo-project tests): Test Files 88 passed (88); Tests 1477 passed.
  • Consumer @objectstack/lint (validate-react-page-props reads the block overlay; lint-liveness-properties reads the ledger): pnpm --filter @objectstack/lint test: Test Files 189 passed (189); Tests 7825 passed, 5 skipped.
  • Consumer ui/packages/plugin-form (run from ui/): pnpm --filter @object-ui/plugin-form test: Test Files 217 passed (217); Tests 2407 passed, 1 skipped. The first run failed 3 files on a prerequisite (@objectstack/client unbuilt in a fresh container: "Failed to resolve import"); green after building it.
  • ui/ type-check: NOT MEASURED locally (prerequisite: @object-ui/plugin-form type-check needs the ui workspace's own packages built, TS2307 on @object-ui/types and siblings). By source: no ui/ type derives drawerSide / drawerWidth from FormView (DrawerFormSchema and ObjectFormSchema declare their own; git grep of ui/ for FormView[ indexes names only modalSize, defaultTab, tabPosition, layout, buttons, defaults). Type Check UI on this PR is the reading.
  • packages/metadata: no tombstone or conversion pin there names these keys (git grep drawerSide drawerWidth -- packages/ outside packages/spec is empty), so no run is owed.
Ablations (each restored, blob == HEAD and git diff HEAD empty, by scripts/ablation-replace.mjs)
  • Tombstone: drawerSide: retiredKey(FORM_VIEW_DRAWER_SIDE_RETIRED) replaced by the old z.enum([...]).optional(). The new pin went red: Tests 7 failed, 22 passed (29) — the five door refusals, the defineStack envelope and the @ts-expect-error case. Restored.
  • Conversion: the stripKeys(form, ['drawerSide', 'drawerWidth'], emit, path) call replaced by a pass-through. conversions.test.ts fixture replay for form-view-drawer-keys-removed and the pin's two stored-row cases went red: Tests 3 failed, 235 passed (238). Restored. (An earlier attempt used a replacement that threw instead of passing through and reddened every form-view fixture; it was discarded and rerun as the pass-through above.)
  • Dogfood (title carries ! and the (narrowing) arm) at 8c7bfc30, after turbo run build --filter='./packages/**' (78/78): pnpm --filter @objectstack/dogfood test under the lock, VERDICT command-exit 0; Test Files 388 passed, 3 skipped (391); Tests 3208 passed, 28 skipped. No failures.

Acceptance notes

Verification

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 8c7bfc30: 112 derived families, all run; reconciled with --ran: "112 derived famil(ies) accounted for — 112 run, 0 NOT-MEASURED (a DERIVED zero)". First pass had six prerequisite exits in the fresh container (check-plugin-teardown-shape --self-test needed its pinned commit fetched; check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:dts-closure, check:dual-build-cjs-loads needed built packages); each exit 0 after turbo run build --filter='./packages/**' (78/78).
  • pnpm --filter @objectstack/spec check:generated after regeneration: in the gate list, exit 0 (first pass named react-blocks, docs and liveness counts stale; regenerated with gen:react-blocks, gen:docs, gen:liveness-counts).
  • node scripts/check-adr-0087-registration.mjs --base origin/main: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered form-view-drawer-keys-removed, ui-form-view-drawer-keys-retired (new here: both)".
  • node scripts/check-changeset-no-major.mjs --base origin/main --event on this body: LEVEL AXIS ✓ for no (narrowing) + narrowed, minor.
  • node scripts/check-empty-changeset.mjs --base origin/main: exit 1 by design at ffc3fe89 (DELIBERATE CORRECTION of the pending 27155 note, see Acceptance notes; the same red is Check Changeset on CI). --self-test exit 0.
  • node scripts/pm/check-governed-merges.mjs --test on the 20-path list: "0 of 20 path(s) hit the register after 1 generated-artifact lift(s) … NOT governed"; react-blocks.md is a pure regeneration.
  • Every family CI runs is read from this PR's check-runs.

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 (CI-owned run).
  • 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 by Claude Code

…opens in a drawer

Tombstone both keys on FormViewSchema (ADR-0049 enforce-or-remove), move
them to the React ObjectForm block's overlay (the drawer that reads them),
add the form-view-drawer-keys-removed D2 conversion, its D3 entry, the
RETIRED_KEYS_BY_MAJOR entries, the step-18 rationale fragment, the liveness
rows and the pins. Generated artifacts follow in the next commit.

Claude-Session: https://claude.ai/code/session_01ADHkmSVA1CYQ5wh8VMGZ5r
Co-authored-by: Claude <noreply@anthropic.com>
authorable-surface (two [RETIRED] rows), reference docs, the react-blocks
contract (drawerSide / drawerWidth now published from the ObjectForm
overlay) and the liveness counts.

Claude-Session: https://claude.ai/code/session_01ADHkmSVA1CYQ5wh8VMGZ5r
Co-authored-by: Claude <noreply@anthropic.com>
…drawerWidth

The reconciliation ledger excuses a tombstoned key automatically and refuses
an entry for a key that is no longer authorable.

Claude-Session: https://claude.ai/code/session_01ADHkmSVA1CYQ5wh8VMGZ5r
Co-authored-by: Claude <noreply@anthropic.com>
…ING, minor on spec)

Claude-Session: https://claude.ai/code/session_01ADHkmSVA1CYQ5wh8VMGZ5r
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 21 documentable anchor(s). ⚠️ 7 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.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via ObjectForm (symbol, a field of const object REACT_OVERLAY_SHADOWS))
  • content/docs/frontend/plugins/plugin-form.mdx (via ObjectForm (symbol, a field of const object REACT_OVERLAY_SHADOWS), drawerSide (literal, a string literal in ObjectForm; a string literal in apply; a string literal in dataProps; a string literal in interactions), drawerWidth (literal, a string literal in ObjectForm; a string literal in apply; a string literal in dataProps; a string literal in interactions), modalSize (literal, a string literal in dataProps))
  • content/docs/permissions/field-level-security.mdx (via ObjectForm (symbol, a field of const object REACT_OVERLAY_SHADOWS))
  • content/docs/protocol/objectui/actions.mdx (via submitBehavior (literal, a string literal in dataProps))
  • content/docs/protocol/objectui/concept.mdx (via FormViewSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/index.mdx (via FormViewSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/layout-dsl.mdx (via FormViewSchema (symbol, a top-level const), ObjectForm (symbol, a field of const object REACT_OVERLAY_SHADOWS), tabPosition (literal, a string literal in dataProps))
  • content/docs/ui/forms.mdx (via submitBehavior (literal, a string literal in dataProps))
  • content/docs/ui/public-data-collection.mdx (via submitBehavior (literal, a string literal in dataProps))
  • content/docs/ui/react-pages.mdx (via ObjectForm (symbol, a field of const object REACT_OVERLAY_SHADOWS), REACT_BLOCKS (symbol, a top-level const object), drawerSide (literal, a string literal in ObjectForm; a string literal in apply; a string literal in dataProps; a string literal in interactions))
  • content/docs/ui/translations.mdx (via submitBehavior (literal, a string literal in dataProps))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via drawerWidth (literal, a string literal in ObjectForm; a string literal in apply; a string literal in dataProps; a string literal in interactions))
  • content/docs/releases/v17/17-1.mdx (via submitBehavior (literal, a string literal in dataProps))
  • content/docs/releases/v17/17-3.mdx (via FormViewSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object formViewDrawerKeysRemoved), retiredFromLoadPath (symbol, a field of const object formViewDrawerKeysRemoved))
  • content/docs/releases/v17/17-7.mdx (via submitBehavior (literal, a string literal in dataProps))
  • content/docs/releases/v17/index.mdx (via submitBehavior (literal, a string literal in dataProps))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 7 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
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 168 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 504ba5108effc72f5b798070e73c8b0a699c63b1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 84b9f533d051118b740bd20e4be9bacb7aacefea — the merge of head ffc3fe89618b871f7bd587b607da9e42cad6244f into base 504ba5108effc72f5b798070e73c8b0a699c63b1, 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 84b9f533d051118b740bd20e4be9bacb7aacefea && git checkout 84b9f533d051118b740bd20e4be9bacb7aacefea
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 504ba5108effc72f5b798070e73c8b0a699c63b1 ffc3fe89618b871f7bd587b607da9e42cad6244f && git checkout -B drift-repro 504ba5108effc72f5b798070e73c8b0a699c63b1 && git merge --no-ff ffc3fe89618b871f7bd587b607da9e42cad6244f

node scripts/docs-audit/affected-docs.mjs --json 504ba5108effc72f5b798070e73c8b0a699c63b1

⚠️ 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 504ba5108effc72f5b798070e73c8b0a699c63b1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… retirement

The pending 27155 note said drawerSide / drawerWidth stay declared on the
form view; this branch retires both in the same 18.3.0 release. Drop them
from the sentence and say they retire in the same release. splitDirection
and modalSize are left as written. Deliberate correction:
check-empty-changeset exits 1 on a modified pending changeset by design.

Claude-Session: https://claude.ai/code/session_01ADHkmSVA1CYQ5wh8VMGZ5r
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: 8a8c90a767b0b890e7752864a1aabf6f46b1c2af · read 2026-11-02T11:36Z

维护者速读

席位意见:可落地,等契约复审。

席位核对 — ACCEPT · domain:spec#1 · head 8a8c90a7

  1. Shape: draft, base main (includes feat(spec): list views offer Hide Fields by default — userActions.hideFields defaults to true #29083, 104cd1a58d), Fixes #29012, Clause-②: no (narrowing) / Metadata-protocol: narrowed at line start in the body and the changeset (BREAKING minor, migration, one ADR-0087 marker). Within claim 6573656255's surface. Governed: the diff touches skills/objectstack-ui/references/react-blocks.md, and the CI Governed Surface Queue Guard on this head LIFTED it as a byte-equal regeneration of gen:react-blocks ([finding] skills/*/references/_index.md is generator-owned (gen:skill-refs) yet sits inside the skills/** governed-merge fence — every spec PR touching an indexed headline is forced to carry a governed-surface file #11705 register), CLEAR. So it is not Tier H.
  2. Diff, read on the head: FormViewSchema.drawerSide / drawerWidth become retiredKey() tombstones. The React ObjectForm block keeps both as its own overlay props, and REACT_OVERLAY_SHADOWS.ObjectForm ledgers the name collision. ADR-0087 kit: D2 conversion form-view-drawer-keys-removed (retiredFromLoadPath, retiredAfter: '18.2.0'), D3 ui-form-view-drawer-keys-retired, two retired-key entries, and the step-18 template fragment. Liveness view 75/8 → 73/10. Authorable surface and references regenerated.
  3. Seat ruling, applied at 8a8c90a7: the pending .changeset/27155-form-view-type-layout-only.md "Unchanged" bullet no longer says drawerSide / drawerWidth stay, and it names their retirement in the same release. This is the deliberate-correction class, so check-empty-changeset exits 1 and the 'Check Changeset' red on this head is by design. The check is not required; nothing was restored and no skip-changeset was added.
  4. Forced test edit (1, named): in form-view-presentation-types-removed, the quick bystander drawerSide became title.
  5. Readings (dev, at 8c7bfc30; 8a8c90a7 differs by that one changeset sentence): 112/112 derived gates, DERIVED zero; dogfood 388 passed + 3 skipped; spec 789 files; test:repo 88/88; lint 189/189; ui plugin-form 217/217; adr-0087, no-major --event and empty-changeset self-test exit 0. ui type-check was not measured locally; CI Type Check UI is the reading. Check-runs on 8a8c90a7 at this reading: 52 success, 8 skipped, 1 failure (Check Changeset, by design), 3 still running.
  6. PR body: stale on the head (names 8c7bfc30, not the 27155 correction). The dev declined to patch it per os-dev.md L63. This record is the current-head reading. The process gap is pm relay: no door can rewrite a PR body after round 1; the dev writes it once (os-dev.md), and a seat issue_patch must open with its signature, which breaks the Fixes-first body shape #29145.
  7. Landing: one CONTRACT_REVIEW_TIER review (a narrowing), which the seat runs itself (claim line follows). Then the seat readies and queues. After it lands, spec: retire the two inert personal declarations — a view marked personal is visible to everyone, and the docs promise personal views that do not exist #29037 (same file) is next.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head 8a8c90a · 2026-11-02T11:38Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8a8c90a767b0b890e7752864a1aabf6f46b1c2af
Local-runs: none
Scope: packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/retired-keys/18.ui__FormView__drawerSide.ts, packages/spec/src/migrations/entries/retired-keys/18.ui__FormView__drawerWidth.ts, packages/spec/src/migrations/entries/semantic/18.ui-form-view-drawer-keys-retired.ts, packages/spec/src/migrations/registry.ts.template, packages/spec/src/ui/react-blocks.ts, packages/spec/src/ui/view.zod.ts, skills/objectstack-ui/references/react-blocks.md + ui/packages/plugin-form/src/DrawerForm.tsx, ui/packages/plugin-form/src/ObjectFormContainer.tsx, ui/packages/plugin-form/src/index.tsx, ui/packages/types/src/zod/objectql.zod.ts, packages/spec/src/ui/component.zod.ts (ObjectFormPropsSchema), packages/spec/src/conversions/walk.ts (mapViewPayloads), packages/spec/src/shared/retired-key.ts, packages/spec/src/ui/react-blocks.test.ts (overlay-shadow ledger), packages/spec/scripts/build-react-blocks-contract.ts, packages/lint/src/validate-react-page-props.ts, packages/spec/src/migrations/entries/README.md, .claude/skills/spec-property-retirement/SKILL.md, docs/adr/0049-no-unenforced-security-properties.md, docs/adr/0087-metadata-protocol-upgrade-contract.md (2026-10-26 addendum), .changeset/27155-form-view-type-layout-only.md, .changeset/27159-*.md, .changeset/29012-form-view-drawer-keys-retired.md, packages/skills/scripts/sync-catalog.mjs, scripts/check-changeset-no-major.mjs (packagesTouched), scripts/check-empty-changeset.mjs (via the Check Changeset log). Narrowing-face floor: every non-prose path of the diff was read, the four edited pins and the new pin packages/spec/src/ui/form-view-drawer-keys-retirement.test.ts included.

① Derived judgments

  • FormViewSchema.drawerSide / drawerWidth become retiredKey() tombstones: the accept set of every form-view door narrows (the schema, view.form, view.formViews.*, a ViewItem form record's config, the flattened form overlay, defineStack as STACK_SCHEMA_INVALID / 422), and the FormView input type refuses both at a TypeScript authoring site. Right — the triage ruling (6571544001) is retire under ADR-0049; type: 'drawer' left the enum in spec(view): narrow FormView.type to layout only (simple | tabbed | wizard) — retire split, drawer, modal with tombstones (ADR-0050 Accepted, #27032 letter A′) #27155, so no form view can reach DrawerForm, and the only path that hands the keys to DrawerForm.tsx (L313 side, L718-724 width) is ObjectFormContainer.tsx L149-179 behind the object-form block's formType === 'drawer'.
  • React ObjectForm block contract: the two names leave dataProps (which are read off FormViewSchema.shape by build-react-blocks-contract.ts dataProps(schema, allow)) and are redeclared as binding interactions with the same types ('top' | 'bottom' | 'left' | 'right', string) and descriptions; REACT_OVERLAY_SHADOWS.ObjectForm = ['drawerSide', 'drawerWidth'] ledgers the name collision with the tombstones. Right — the shape test in react-blocks.test.ts holds the ledger equal to the computed collision set in both directions, so the row is required, and the meaning is preserved (same type, same prose). Consequence: validate-react-page-props builds knownProps from interactions only (L174), so the two props are now known there and a near-miss on a React page is reported; before, as data props, they were outside its known set. A gain, not a loss.
  • ObjectFormPropsSchema (component.zod.ts L7076-7077) keeps drawerSide / drawerWidth; ui/packages/types ObjectFormSchema declares its own (objectql.zod.ts L1035-1036, objectql.ts L2243-2249); the block's registered inputs (plugin-form/src/index.tsx L343-344) are the block's own. Right — no ui/ type derives the two from FormView, which is why Type Check UI stays green with no ui/ edit (AGENTS.md checklist 4 measured, not assumed).
  • D2 form-view-drawer-keys-removed (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.2.0', inserted in identifier order at order: 81, one more than the highest) strips both keys from every form payload mapViewPayloads reaches (ViewItem config, container form / formViews.*, flattened overlay) and the assembled viewItems channel; it never walks page components. Right — parallel to form-view-buttons-reset-removed, idempotent by stripKeys' construction, expectedNotices: 5 = 2 + 1 + 1 + 1 as the fixture authors it, and 18.2.0 is the latest stable (@objectstack/spec@18.2.0 per the Check Changeset log).
  • form-view-presentation-types-removed fixture: the quick bystander drawerSide: 'left' → title: 'Quick'. Right — whole-table replay demands disjoint fixtures (skill §3) and title is a live FormViewSchema key; the docblock's "stays" sentence is corrected to point at the later entry.
  • RETIRED_KEYS_BY_MAJOR[18] +ui/FormView:drawerSide, +ui/FormView:drawerWidth (file names per entries/README.md, values spelled as authorable-surface/ui.json spells them); MIGRATIONS_BY_MAJOR[18].semantic +ui-form-view-drawer-keys-retired; STEP18_RATIONALE +fragment at order: 116 (one more than 115) in id-sorted position. Right.
  • Generated artifacts: authorable-surface/ui.json two rows → [RETIRED]; liveness/view.json both rows live → dead with the house-style tombstone note; state-counts/view.md 75/8 → 73/10; content/docs/references/** and react-blocks.md regenerated; api-surface/ unchanged. Right — a key-level tombstone on a def that still exists is invisible to the api-surface ratchet by design (skill §2), so zero change there is the expected reading.
  • metadata-form-zod-reconciliation.test.ts: the omit row for view.drawerWidth is dropped. Right — a tombstone is not a deliberately-unoffered live key.
  • .changeset/27155-form-view-type-layout-only.md Unchanged bullet rewritten. Wrong as written — see ② and ④.

② Semver level

③ Boundary flags

④ Design judgment

  • packages/spec/src/ui/view.zod.ts — holds. Both prescriptions follow the five guidance conventions: fully-qualified key in backticks, was removed in @objectstack/spec 18.3.0 (ADR-0049), a dash clause stating why it was inert, an imperative fix naming the live mechanism (navigation.mode for a list's record drawer, navigation.size for its width, the ObjectForm block's own props for a React page), and the standard migrate sentence via FORM_VIEW_TYPE_RETIRED_FIX with --from 17 (toMajor 18 − 1). Judged against the card's repro: PUT /meta/view … config: { drawerSide: 'left', drawerWidth: '480px' } now meets the ViewItem form-record door and is refused with the prescription (pinned at that door); a row already stored is stripped on read by applyConversionsToStoredItem (pinned with includeRetired semantics). The comment block names the one real reader and why it is unreachable from a form view. No #nnnn leaks into the runtime string.
  • packages/spec/src/ui/react-blocks.ts — holds. Does the React ObjectForm block still receive what DrawerForm reads? Yes: DrawerFormSchema.drawerSide?: 'top' | 'bottom' | 'left' | 'right' and drawerWidth?: string (DrawerForm.tsx L173-183) are fed by ObjectFormContainer.tsx L178-179 from the block's own ObjectFormSchema (ui/packages/types), whose declarations do not derive from FormViewSchema; the spec-side contract now publishes the same two names with the same types as binding props, so validate-react-page-props keeps drawerSide="right" known. The REACT_OVERLAY_SHADOWS row is required by the collision test and its comment states the reason (tombstone name, same meaning). Tried to break it: a React page writing drawerWidth={480} — the overlay says string, the ui ObjectFormSchema says string, DrawerForm applies it as a CSS value, consistent; only ObjectFormPropsSchema (page block, component.zod.ts L7077) admits string | number, a pre-existing divergence this diff neither creates nor widens. Should-fix 5: the new overlay description for drawerWidth carries the form view's [DEPRECATED → size buckets] tag verbatim, but the block's drawer has no size bucket to migrate to (modalSize is the modal container's; navigation.size is a list's), so the published contract now deprecates a block prop toward a replacement that does not exist on the block. Drop the tag, or name a real replacement; ObjectFormPropsSchema's own describe for the same prop carries no deprecation.
  • packages/spec/src/conversions/registry.ts — holds. apply is structurally the form-view-buttons-reset-removed shape: mapViewPayloads (three arms in walk.ts L590-640: ViewItem config, container slots by presence, flattened overlay only when config is absent) plus mapCollection over ASSEMBLED_VIEW_ITEMS_KEY with the same three-way guard; list payloads and page components are never visited, so the object-form block's keys survive (pinned). stripKeys returns the input reference when nothing is stripped, so untouched forms ride through by reference (pinned) and replay is idempotent by construction. Fixture: disjoint from every other 18 fixture (the only overlap, the presentation-types quick form, was moved off drawerSide in the same diff); expectedNotices: 5 is the key count, not the entry count. retiredFromLoadPath: true keeps the authoring funnel on the prescription; retiredAfter: '18.2.0' is the latest stable, inside the census window. Insertion is in identifier order (formViewDrawerKeysRemoved sorts after formViewButtonsResetRemoved and before formViewOptionDefaultRemoved) with order: 81. Tried to break it: a stored view row with viewKind: 'form' and a non-dict config falls to arm 1's guard and is left for the parse (documented in the walker and in this entry's viewItems arm); a container whose form slot is absent but formViews present is handled per present slot. Nothing found.
  • packages/spec/src/migrations/entries/retired-keys/18.ui__FormView__drawerSide.ts, 18.ui__FormView__drawerWidth.ts — hold. Names per entries/README.md (the protocol major, a dot, then the entry id with / and : spelled __), values are the exact ${defKey}:${name} strings authorable-surface/ui.json prints without the [RETIRED] tag, which is what build gate (b) matches as set members; landed in the same diff as the tombstones, so gate (b2) ("registration nobody consumes") cannot fire. Each comment names its D2 and D3.
  • packages/spec/src/migrations/entries/semantic/18.ui-form-view-drawer-keys-retired.ts — holds. One D3 entry per retirement family; surface is prose with the / joiner; replacement names the two live mechanisms; reason states the judgment the upgrader still owes (was the form meant to open in a drawer somewhere) and names the D2 conversion in backticks, which the new pin requires; acceptanceCriteria non-empty; conversionIds: ['form-view-drawer-keys-removed'] matches the D2 id, so chain replay reaches the fixture's after.
  • packages/spec/src/migrations/registry.ts.template — holds. One STEP18_RATIONALE fragment, id = the D3 id, inserted where that id sorts (between …buttons-reset-retired and …type-layout-only), order: 116 = highest + 1, text consistent with the tombstones and the block's retained props; the merge test's sorted-list discipline is respected.
  • skills/objectstack-ui/references/react-blocks.md — holds as a pure regeneration of react-blocks.ts: the two rows leave the data group and appear in the binding group with the types and descriptions the overlay declares; nothing hand-written. (Should-fix 5's description text lands here through the generator; it is fixed in react-blocks.ts, then regenerated.)
  • Retirement kit completeness against spec-property-retirement §4: schema tombstones ✓; orphan value schema n/a ✓; conversion + chain step + exact RETIRED_KEYS_BY_MAJOR rows + one D3 entry ✓; liveness ledger rows dead with verifiedAt and the house-style note, counts regenerated ✓; generated baselines (authorable-surface, docs, react-blocks) ✓, api-surface correctly unchanged ✓; forms — no *.form.ts names either key ✓; i18n n/a ✓; CLI advisory lint is ledger-driven, no test named the keys ✓; pin tests — refusal at five doors + defineStack, stored-row conversion, page-block non-touch, idempotence, load-path retirement, chain wiring, D3, @ts-expect-error in a compiled repo-project file, and a tree-scoped absence walk over the five declared roots with an anti-vacuity probe ✓; examples — the two examples/ hits are ObjectForm formType="drawer" drawerSide="right" JSX, the block, unaffected ✓; published skills — only the generated contract and rules/record-page.md's navigation width note, which is about a list's drawer ✓; docs — content/docs/ui/components/plugins.mdx L144-145 and frontend/plugins/plugin-form.mdx L89 describe the block's props, not a form view's ✓; changeset ✓ (with should-fix 2 and the must-fix on the sibling changeset). Removal-version pin rows added ✓.
  • The narrowing itself against the published spec on main: it takes no declared capability away at any consumer — no metadata form-view surface read either key, and the one reader (DrawerForm through the object-form block) keeps its inputs on both sides — and it makes the spec's own refusal real where before a valid, inert save stood. Verdict on the narrowing: sound. The verdict on the PR fails on the changeset correction alone.

Implemented-by: claude/project-thread-6xhq0v
Reviewed-by: session_01ADHkmPi25XXKx92Xmg4viS

VERDICT: FAIL

…size-bucket tag, bump skills

- 27155's Unchanged bullet: only modalSize stays declared; splitDirection
  (#27159) and drawerSide / drawerWidth retire from the form view in the
  same release, and the React ObjectForm block keeps the drawer pair as its
  own props. Deliberate correction of a pending note: check-empty-changeset
  exits 1 by design.
- The ObjectForm overlay drawerWidth no longer carries the form view's
  [DEPRECATED → size buckets] tag: the block's drawer has no size bucket.
  Its description now says what DrawerForm does with the value.
  react-blocks.md regenerated.
- The drawer-keys changeset bumps @objectstack/skills (patch): the
  regenerated react-blocks.md ships in that package.

Claude-Session: https://claude.ai/code/session_01ADHkmSVA1CYQ5wh8VMGZ5r
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head ffc3fe8 · 2026-11-02T12:49Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ffc3fe89618b871f7bd587b607da9e42cad6244f
Local-runs: none
Scope: packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/retired-keys/18.ui__FormView__drawerSide.ts, packages/spec/src/migrations/entries/retired-keys/18.ui__FormView__drawerWidth.ts, packages/spec/src/migrations/entries/semantic/18.ui-form-view-drawer-keys-retired.ts, packages/spec/src/migrations/registry.ts.template, packages/spec/src/ui/react-blocks.ts, packages/spec/src/ui/view.zod.ts, skills/objectstack-ui/references/react-blocks.md + ui/packages/plugin-form/src/DrawerForm.tsx, ui/packages/plugin-form/src/ObjectFormContainer.tsx, ui/packages/plugin-form/src/index.tsx, ui/packages/types/src/zod/objectql.zod.ts, packages/spec/src/ui/component.zod.ts (ObjectFormPropsSchema), packages/spec/src/conversions/walk.ts (mapViewPayloads), packages/spec/src/shared/retired-key.ts, packages/spec/src/ui/react-blocks.test.ts (overlay-shadow ledger), packages/spec/scripts/build-react-blocks-contract.ts, packages/lint/src/validate-react-page-props.ts, packages/spec/src/migrations/entries/README.md, .claude/skills/spec-property-retirement/SKILL.md, docs/adr/0049-no-unenforced-security-properties.md, docs/adr/0087-metadata-protocol-upgrade-contract.md (2026-10-26 addendum), .changeset/27155-form-view-type-layout-only.md, .changeset/27159-*.md, .changeset/29012-form-view-drawer-keys-retired.md, packages/skills/scripts/sync-catalog.mjs, scripts/check-changeset-no-major.mjs (packagesTouched), scripts/check-empty-changeset.mjs (via the Check Changeset log). Narrowing-face floor: every non-prose path of the diff against main, the four edited pins and the new pin packages/spec/src/ui/form-view-drawer-keys-retirement.test.ts included. Round 2 of the review of this PR: the previous record (FAIL on 8a8c90a7) is carried forward on every file the one-commit delta 8a8c90a7..ffc3fe89 leaves byte-identical; the delta touches .changeset/27155-form-view-type-layout-only.md, .changeset/29012-form-view-drawer-keys-retired.md, packages/spec/src/ui/react-blocks.ts and skills/objectstack-ui/references/react-blocks.md only, and those four are re-read whole.

① Derived judgments

  • FormViewSchema.drawerSide / drawerWidth are retiredKey() tombstones: the accept set narrows at every form-view door (the schema, view.form, view.formViews.*, a ViewItem form record's config, the flattened form overlay, defineStack as STACK_SCHEMA_INVALID / 422) and the FormView input type refuses both at a TypeScript authoring site. Right — triage ruling 6571544001 (retire under ADR-0049); type: 'drawer' left the enum in spec(view): narrow FormView.type to layout only (simple | tabbed | wizard) — retire split, drawer, modal with tombstones (ADR-0050 Accepted, #27032 letter A′) #27155, so no form view reaches DrawerForm, and the one path that hands the keys to DrawerForm.tsx (L313 side, L718-724 width) is ObjectFormContainer.tsx L149-179 behind the object-form block's formType === 'drawer'. Unchanged since round 1.
  • React ObjectForm block contract: the two names leave dataProps (read off FormViewSchema.shape by build-react-blocks-contract.ts dataProps(schema, allow)) and are redeclared as binding interactions with the same types ('top' | 'bottom' | 'left' | 'right', string); REACT_OVERLAY_SHADOWS.ObjectForm = ['drawerSide', 'drawerWidth'] ledgers the collision with the tombstones' names. Right — react-blocks.test.ts holds the ledger equal to the computed collision set in both directions, so the row is required and the meaning is preserved. validate-react-page-props builds knownProps from interactions only (L174), so a near-miss of either prop on a React page is now reported where before, as data props, they were outside its known set. Round 2 changes only the drawerWidth description (see ④).
  • ObjectFormPropsSchema (component.zod.ts L7076-7077) keeps both keys; ui/packages/types ObjectFormSchema declares its own (objectql.zod.ts L1035-1036, objectql.ts L2243-2249); the registered block's inputs (plugin-form/src/index.tsx L343-344) are the block's own. Right — no ui/ type derives the pair from FormView, so Type Check UI is green with no ui/ edit (AGENTS.md checklist 4 measured).
  • D2 form-view-drawer-keys-removed (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.2.0', identifier order, order: 81) strips both keys from every form payload mapViewPayloads reaches and from the assembled viewItems channel; never walks page components. Right — parallel to form-view-buttons-reset-removed, idempotent by stripKeys' construction, fixture expectedNotices: 5 = 2 + 1 + 1 + 1, 18.2.0 is the latest stable per the Check Changeset log.
  • form-view-presentation-types-removed fixture: quick bystander drawerSide: 'left' → title: 'Quick' (a live FormViewSchema key), docblock corrected. Right — whole-table fixture disjointness (skill §3).
  • RETIRED_KEYS_BY_MAJOR[18] +ui/FormView:drawerSide, +ui/FormView:drawerWidth; MIGRATIONS_BY_MAJOR[18].semantic +ui-form-view-drawer-keys-retired; STEP18_RATIONALE +fragment at order: 116 in id-sorted position. Right.
  • Generated artifacts: authorable-surface/ui.json two rows [RETIRED]; liveness/view.json both rows dead with the house-style note; state-counts/view.md 75/8 → 73/10; content/docs/references/** and react-blocks.md regenerated; api-surface/ unchanged — expected, a key-level tombstone is invisible to that ratchet (skill §2). Right.
  • metadata-form-zod-reconciliation.test.ts: the omit row for view.drawerWidth dropped. Right.
  • .changeset/27155-form-view-type-layout-only.md Unchanged bullet, round 2 text: "modalSize stays declared: the console reads it for its record dialog. splitDirection (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) and drawerSide / drawerWidth (spec(view): FormViewSchema drawerSide / drawerWidth are accepted on a form view and read by no form-view surface; the describe says the question "stays open on #27153", which is closed #29012) retire from the form view in the same release; the React ObjectForm block keeps the drawer pair as its own props." Right — every clause is true on this tree: FormViewSchema.splitDirection, drawerSide, drawerWidth are tombstones (view.zod.ts L5150, L5195-5197), modalSize is live, the block's overlay declares the drawer pair. Round 1's must-fix 1 is closed.
  • .changeset/29012-form-view-drawer-keys-retired.md gains '@objectstack/skills': patch. Right — skills/objectstack-ui/references/react-blocks.md ships in that package (packages/skills/scripts/sync-catalog.mjs copies skills/** into dist/skills/), matching the 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 precedent. Round 1's should-fix 2 is closed.

② Semver level

③ Boundary flags

④ Design judgment

  • packages/spec/src/ui/view.zod.ts — unchanged since round 1; holds. Both prescriptions follow the five guidance conventions (fully-qualified backticked key; was removed in @objectstack/spec 18.3.0 (ADR-0049); a dash clause stating why it was inert; an imperative fix naming the live mechanism — navigation.mode / navigation.size for a list's record drawer, the ObjectForm block's own props for a React page; the standard migrate sentence via FORM_VIEW_TYPE_RETIRED_FIX with --from 17). Against the card's repro: PUT /meta/view … config: { drawerSide: 'left', drawerWidth: '480px' } now meets the ViewItem form-record door and is refused with the prescription (pinned); a row already stored is stripped on read by applyConversionsToStoredItem (pinned). No tracker number in the runtime string.
  • packages/spec/src/ui/react-blocks.ts — holds; round 2 changes one string. Does the React ObjectForm block still receive what DrawerForm reads? Yes: DrawerFormSchema.drawerSide?: 'top' | 'bottom' | 'left' | 'right' and drawerWidth?: string (DrawerForm.tsx L173-183) are fed by ObjectFormContainer.tsx L178-179 from the block's own ObjectFormSchema; the spec overlay publishes the same two names with the same types as binding props; REACT_OVERLAY_SHADOWS.ObjectForm is required by the collision test and its comment states the reason. The new drawerWidth description — a CSS length such as "480px", the drawer's width when drawerSide is left or right, its height when top or bottom, default size when unset — is read against DrawerForm.tsx L718-724: if (!schema.drawerWidth) return undefined (unset → no inline style → the Sheet's default), isHorizontal = side === 'left' || side === 'right', width/maxWidth on a horizontal side, height/maxHeight on a vertical one, where side = schema.drawerSide || 'right' (L313). The description states exactly what the reader does, drops the [DEPRECATED → size buckets] tag that pointed at a bucket the block does not have, and still contains the formType 'drawer' phrase the new pin asserts on both props. Tried to break it: drawerWidth={480} on a React page — overlay string, ui ObjectFormSchema string, DrawerForm applies a CSS value; only ObjectFormPropsSchema (component.zod.ts L7077) admits string | number, a pre-existing divergence this diff neither creates nor widens. Round 1's should-fix 5 is closed.
  • packages/spec/src/conversions/registry.ts — unchanged since round 1; holds. apply is the form-view-buttons-reset-removed shape over mapViewPayloads (walk.ts L590-640: ViewItem config, container slots by presence, flattened overlay only when config is absent) plus mapCollection over ASSEMBLED_VIEW_ITEMS_KEY with the same three-way guard; list payloads and page components are never visited (pinned). stripKeys returns the input reference when nothing is stripped, so untouched forms ride through by reference and replay is idempotent by construction (pinned). Fixture disjoint from every other 18 fixture; expectedNotices: 5 is the key count. retiredFromLoadPath: true, retiredAfter: '18.2.0' inside the census window; inserted where formViewDrawerKeysRemoved sorts, order: 81. A stored form row with a non-dict config is left for the parse, as the walker documents. Nothing found.
  • packages/spec/src/migrations/entries/retired-keys/18.ui__FormView__drawerSide.ts, 18.ui__FormView__drawerWidth.ts — unchanged; hold. Names per entries/README.md (the protocol major, a dot, then the entry id with / and : spelled __); values are the exact ${defKey}:${name} strings authorable-surface/ui.json prints without the [RETIRED] tag, the set members build gate (b) reads; landed with the tombstones, so gate (b2) cannot fire; each comment names its D2 and D3.
  • packages/spec/src/migrations/entries/semantic/18.ui-form-view-drawer-keys-retired.ts — unchanged; holds. One D3 entry per family; surface prose with the / joiner; replacement names both live mechanisms; reason states the judgment the upgrader still owes and names the D2 id in backticks (the new pin requires it); acceptanceCriteria non-empty; conversionIds matches the D2 id, so chain replay reaches the fixture's after.
  • packages/spec/src/migrations/registry.ts.template — unchanged; holds. One STEP18_RATIONALE fragment, id = the D3 id, at its sorted position, order: 116 = highest + 1, text consistent with the tombstones and the retained block props.
  • skills/objectstack-ui/references/react-blocks.md — holds as a pure regeneration of react-blocks.ts: the round-2 diff is the one drawerWidth row, identical to the overlay's new description, and check:react-blocks inside the green TypeScript Type Check job proves the file current.
  • Retirement kit completeness against spec-property-retirement §4 — unchanged since round 1: schema tombstones ✓; conversion + chain step + exact RETIRED_KEYS_BY_MAJOR rows + one D3 entry ✓; liveness rows dead with verifiedAt and the house-style note, counts regenerated ✓; generated baselines ✓, api-surface correctly unchanged ✓; no *.form.ts names either key ✓; i18n n/a ✓; CLI advisory lint ledger-driven ✓; pins — refusal at five doors + defineStack, stored-row conversion, page-block non-touch, idempotence, load-path retirement, chain wiring, D3, @ts-expect-error in a compiled repo-project file, tree-scoped absence over the five declared roots with an anti-vacuity probe ✓; examples/ hits are the block's JSX ✓; published skills: only the generated contract and rules/record-page.md's list-drawer navigation note ✓; content/docs/** prose describes the block's props, not a form view's ✓; changeset ✓ (skills patch line now present).
  • The narrowing itself against the published spec on main: it takes no declared capability away at any consumer — no metadata form-view surface read either key, and the one reader (DrawerForm through the object-form block) keeps its inputs on both sides — and it makes the spec's own refusal real where a valid, inert save stood. Sound.

Implemented-by: claude/project-thread-6xhq0v
Reviewed-by: session_01ADHkmPi25XXKx92Xmg4viS

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 12:56
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit b5e7ba5 Nov 2, 2026
70 of 73 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-6xhq0v branch November 2, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants