Skip to content

feat(spec)!: form layout accepts only vertical | horizontal — the inline and grid arms retired (#20221) - #20262

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-20221-form-layout-inline-grid-retired
Sep 28, 2026
Merged

hotlong merged 6 commits into
mainfrom
claude/issue-20221-form-layout-inline-grid-retired

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20221
Clause-②: no (narrowing)

Retires the inline and grid arms of form layout (ADR-0049 enforce-or-remove), per the triage direction on the card (comment 5855767378) under the maintainer's family criterion on #18900: the capability grid names already exists under another key (columns), and inline is not a record-form layout, so both are redundant vocabulary and retire with no alias window.

What changed

  • Both schemas narrowed to 'vertical' | 'horizontal'. ObjectFormPropsSchema.layout (the object-form page component) and FormViewSchema.layout (view.form, view.formViews.*, a form view item's config). Each enum carries a per-value error map keyed on the input (the record:chatter position precedent): 'grid' and 'inline' are refused with invalid_value at layout and a prescription (write 'vertical' or omit layout; for multi-column set columns), while a never-vocabulary value keeps zod's own enum refusal.
  • ADR-0087 D2 conversion form-layout-inline-grid-to-vertical (protocol 18, retiredFromLoadPath: true): rewrites both values to 'vertical' and leaves columns untouched, on object-form page components (regions, slots, nesting), on every form payload a views entry carries (mapViewPayloads, form kind only), and on the assembled-manifest viewItems channel (record config or flattened form overlay). Wired into the protocol-18 chain step; the step's rationale is extended.
  • One D3 entry for the family, ui-form-layout-inline-grid-retired (ruling B on [Decision] 一次退役,要写一条记录还是两条?—— 迁移条目的 D2/D3 约定,两处成文相互矛盾 #17152: one semantic entry per retirement family even when D2 is lossless). It carries the one judgement the chain cannot make: whether a form that said grid without columns wanted more than one column. registry.ts regenerated with gen:migration-registry.
  • Descriptions: the columns describe on object-form no longer says "grid layout"; both layout describes name columns as where multi-column lives.
  • Regenerated, never hand-edited: content/docs/references/{api/protocol,ui/component,ui/view}.mdx and skills/objectstack-ui/references/react-blocks.md (the layout row of the ObjectForm block).
  • Hand-written: content/docs/protocol/objectui/layout-dsl.mdx (one sentence listed the four-arm enum), the form.layout liveness row in packages/spec/liveness/view.json (still live; note re-anchored at the pin, verifiedAt stamped), and the changeset.
  • Pins: packages/spec/src/ui/form-layout-inline-grid-retired.test.ts (29 tests).

Mechanism hypotheses, measured

  • H1 — only these two sites declare the four-arm form enum: CONFIRMED. git grep over packages/spec/src at the merge base ab820016b3 for a layout enum with 'inline' / 'grid' arms: exactly ui/component.zod.ts (ObjectFormPropsSchema.layout) and ui/view.zod.ts (FormViewSchema.layout). The other 'grid' hits are the view-type vocabulary (ListView.type, inlineEdit, the object designer), left untouched.
  • H1, the flattened overlay: CONFIRMED, it inherits the narrowing. VIEW_METADATA_MEMBERS.formOverlay is built from FormViewSchema (.extend at the base; .safeExtend after spec(ui): a flattened viewKind: 'list' view overlay with no columns is refused by the list overlay member, then ACCEPTED by the form overlay member, so its list keys are stripped unjudged and a wrong 200 stores it #20186 landed and was merged into this branch), so the flattened form overlay refuses both values with the same prescription. Pinned through VIEW_METADATA_MEMBERS.formOverlay; the union and its members were not edited.
  • H2 — producers: zero, re-measured with lit controls. Matcher: layout followed by grid or inline as a value, quoted or bare, across every tracked file (TS, JSON, YAML, Markdown), tests excluded. At ab820016b3: 0 hits. Controls on the same tree: the same matcher with vertical|horizontal hits 2 (conversion fixtures); layout: under examples/** hits 53; formViews under examples/** hits 35; the one authored object-form in examples/ and apps/ (the showcase create wizard) authors no layout. At the merged head 2d0b7a6bcb the only subject hits are this PR's own prose and fixture.
  • Renderer read points, re-measured at the .objectui-sha pin f8a9d0fb0 (recorded beside the prescriptions): the simple arm folds both values to vertical (ObjectForm.tsx:1406-1410); the drawer/modal hand-off (:463, :499), DrawerForm.tsx:575 and ModalForm.tsx:597 pass only vertical / horizontal; TabbedForm.tsx:556, SplitForm.tsx:445, WizardForm.tsx:1075 hard-code vertical. The pinned sibling imports neither enum for a value assignment, so the pin build is unaffected.

Verification (merged head 2d0b7a6bcb)

  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 550 files passed, 16177 tests passed, 2 todo.
  • pnpm --filter @objectstack/spec typecheck (tsc, scripts-typecheck, test-typecheck): exit 0; test layer "53 file(s) / 255 error(s) / 142 pinned signature(s) held".
  • pnpm --filter @objectstack/spec build then check:generated: "All 15 generated artifacts are up to date". authorable-surface/, api-surface/ and json-schema.manifest/ are byte-unchanged, as expected for an enum-value narrowing (the ratchets record key and export existence, not value sets). spec-changes.json and the upgrade guide are unchanged because the protocol-18 step is not yet cut.
  • Gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 2d0b7a6bcb (117 families, the same set as before the merge): 115 run, all exit 0; 2 NOT MEASURED; 0 unrun (--ran reconciliation). Among them: check:adr-0087-registration ("registered form-layout-inline-grid-to-vertical, ui-form-layout-inline-grid-retired (new here: both)"), check:changeset-no-major, check:empty-changeset, check:doc-authoring, check:liveness, check:objectui-pin-citations, check:react-blocks, check:skill-examples, check:nul-bytes, check:cross-package-test-inputs.
    • NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt. Both refuse with their own prerequisite (exit 3): every workspace package's built output, which this worktree did not build on the shared box. The diff adds no import, export or entry point, and no ledgered package reads or writes the retired values (census above). CI builds that closure.
  • Ablation (one-time proof, taken at 41ceb7e5d6 before the merge, which touched neither enum nor the pin file): both enums re-admitted 'inline' and 'grid' through scripts/ablation-replace.mjs (anchor 1 to 0 on each file, blob b6d256b45a8a to 0787a83d3806 and 33d6fd43be7c to 8514b7afecb3), then the pin file ran: 14 failed, 15 passed. Red: all 10 refusal pins (2 values times 5 doors), the two-surface vocabulary pin, and the 3 conversion pins that assert the input is refused before the rewrite. Green: the 5 vertical/horizontal control pairs, the 5 never-vocabulary pins, the load-path, non-carrier and registration pins. Restore proven: both blobs equal HEAD and git diff HEAD is empty.
  • skills/** readings: skills/objectstack-ui/references/react-blocks.md 115 lines before and after (one row regenerated in place); the published catalog's SKILL.md files, 10 files, 4402 lines before and after.

Acceptance notes

  • objectui follow-through (on the objectui pin bump, not filed here): the page-designer inspector's object-form layout select (still offers Inline and Grid), the object-form registry inputs enum (plugin-form/src/index.tsx:238 at the pin), ObjectFormSchema.layout and its docblock, the fold sites that become plain pass-through, the FormSchema mirror's 'grid' (its WiderThanDeclared row), and the docs listing four arms. This is objectui#7759 group C.
  • Rewrite, not strip. The card's own option R proposed stripping the key; the triage direction and the dispatch rule a rewrite to 'vertical'. Both are behaviour-preserving ('vertical' is the renderer default); the rewrite was followed.
  • Clause-② line. Triage's note 4 wrote yes; the claim wrote no, since the accept set only narrows. This PR and its changeset carry no (narrowing).
  • Governed path. skills/objectstack-ui/references/react-blocks.md is regenerated output under skills/** (Tier H), so this PR waits on a maintainer approval. It is not split out: CI's react-blocks gate re-derives it from the schema.
  • Hand-written docs page beyond the claim's file list: content/docs/protocol/objectui/layout-dsl.mdx, one sentence that enumerated the four arms.
  • Observation, no carrier: horizontal renders byte-identical to vertical on the drawer, modal and tabbed presentations, and on the simple form changes only the action row's justification (the card's "Not in this card" measurement). Not in scope here.

维护者速读(草稿)

改了什么:表单的 layout(字段布局)只保留 vertical(纵向)和 horizontal(横向)两个取值,删掉 inline 和 grid。两处都改:页面上的 object-form 组件,以及视图里的表单定义。写了这两个值的元数据现在会在保存/校验时被明确拒绝,报错里直接告诉作者该怎么写:改成 vertical,想要多列就设 columns。已存的旧数据会被自动改写成 vertical(columns 原样保留),不会因此加载失败。

为什么改:这两个值从来没有生效过。前端所有表单形态都把它们当成 vertical 渲染,效果完全一样。也就是说,作者(尤其是 AI)写了一个看起来有意义、实际被丢掉的配置,还能通过校验。多列表单本来就由 columns 控制,而 inline 是工具栏/筛选行的形态,不是记录表单的布局。按您在 #18900 定下的判据,这属于重复词汇,直接退役,不留过渡期。

风险与代价(含回滚):仓库内与示例应用里没有任何地方写过这两个值(已实测,并有对照),所以对现有应用没有影响。外部如有人写了,会在校验时看到明确的修改提示;已存数据由转换层自动改写,渲染结果与之前完全相同。代价是前端(objectui)的设计器下拉框和组件注册里还列着这两个选项,要在下次同步 objectui 版本时一并删掉,本 PR 已在 Acceptance notes 里写明。回滚就是 revert 本 PR,没有数据迁移需要撤销。

席位意见:

你要做的:批准此 PR。


Generated by Claude Code

Narrow ObjectFormPropsSchema.layout and FormViewSchema.layout to
vertical | horizontal with a per-value prescription naming `columns`;
add the ADR-0087 D2 conversion form-layout-inline-grid-to-vertical and
the family's D3 entry ui-form-layout-inline-grid-retired; pin tests.

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
…layout narrowing (wip)

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
…for the two-arm form layout

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

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (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/ai/actions-as-tools.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/ai/agents.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/api/client-sdk.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/api/error-catalog.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/data-modeling/index.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/deployment/validating-metadata.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/permissions/authorization.mdx (via crm_lead (literal, a string literal in fixture))
  • 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))
  • content/docs/ui/index.mdx (via crm_lead (literal, a string literal in fixture))

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

  • content/docs/releases/v13.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/releases/v17/17-3.mdx (via FormViewSchema (symbol, a top-level const))

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
  • 1 changed file(s) yielded no anchor (packages/spec/liveness/view.json) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 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 — 136 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 8af914a30d1dab62fd7b73d1d847076b639cb2ab → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 28e0107e7e2308846084462074b41bd062327f0c — the merge of head 1d496f5c98fb1a332bc7d1e074a437183a9401f5 into base 8af914a30d1dab62fd7b73d1d847076b639cb2ab, 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 28e0107e7e2308846084462074b41bd062327f0c && git checkout 28e0107e7e2308846084462074b41bd062327f0c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8af914a30d1dab62fd7b73d1d847076b639cb2ab 1d496f5c98fb1a332bc7d1e074a437183a9401f5 && git checkout -B drift-repro 8af914a30d1dab62fd7b73d1d847076b639cb2ab && git merge --no-ff 1d496f5c98fb1a332bc7d1e074a437183a9401f5

node scripts/docs-audit/affected-docs.mjs --json 8af914a30d1dab62fd7b73d1d847076b639cb2ab

⚠️ 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 8af914a30d1dab62fd7b73d1d847076b639cb2ab → 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: 72/72 CONTRACT_REVIEW_TIER
Head-sha: 2d0b7a6bcb4b6fae1f81d07135edd5221a9ba685

① Derived judgments

  1. Enums. ObjectFormPropsSchema.layout (component.zod.ts:3600) and FormViewSchema.layout (view.zod.ts, schema at :4085) are z.enum(['vertical','horizontal']) with an error map keyed on issue.input (the CHATTER_POSITION_RETIRED precedent at component.zod.ts:1555). All five doors reach one of them without re-declaring layout: ComponentPropsMap['object-form'] (:4637) is the same schema object; ViewSchema.form/formViews (:4668/:4670), both ViewItemSchema form arms (:5026/:5078) and the flattened overlay (FormViewSchema.safeExtend, :5857 — it re-declares columns, not layout) all embed FormViewSchema; defineForm (:6579) parses through it too. Every grid prescription names columns. git grep of packages/spec/src finds no other inline/grid layout enum; ListView.type 'grid' untouched (protocol.mdx still prints it). Repo-wide producer grep: only this PR's fixtures and changeset.
  2. D2. Rewrites both to 'vertical' by spreading the payload, so columns survives. Reach: mapPageComponents (walk.ts:380 — regions, slots, children/body/footer/items[].children), mapViewPayloads (walk.ts:565 — record config, container slots form and formViews, both kind:'form' at :505/:507, flattened overlay), plus its own mapCollection(ASSEMBLED_VIEW_ITEMS_KEY) arm mirroring view-item-owner-hidden-removed. No carrier missed: the two schemas are embedded nowhere else in spec src. Registered in CONVERSIONS_BY_MAJOR[18] and step18.conversionIds; retiredFromLoadPath: true matches both protocol-18 precedents (record-chatter-position-vocabulary, view-item-owner-hidden-removed); applyConversionsToStoredItem forces includeRetired (stored.ts:100). Fixture expectedNotices: 6 = 2 components + form + formViews.quick + record + overlay. Converted output parses green on the props door, ViewSchema and the overlay (pinned).
  3. D3. 18.ui-form-layout-inline-grid-retired.ts registered; the registry block is byte-identical to the entry file after the generator's 4-space indent (diff empty) and sits in sorted position between ui-form-field-precision-scale-integer-refused and ui-form-view-predicate-features-root-refused (build-migration-registry.ts:253). The refusal prescriptions carry no tracker number. The entry's reason cites #18900, and the D2 summary/step-18 rationale cite #20221: the same convention as 231 of 266 existing semantic entries and the step-17 summaries (#3713, …); the enforced rule (check:doc-authoring) scans the md corpora and refusal prose, not these fields. Non-blocking. Every replacement/acceptanceCriteria sentence is true at head (item 5).
  4. Skills hunk. build-react-blocks-contract.ts derives the ObjectForm layout row from FormViewSchema (react-blocks.ts:247) via z.toJSONSchema, renders enum as 'vertical' | 'horizontal' and clip(description, 160); the new describe is 127 chars, so the row is the describe verbatim, as the diff shows. columns row unchanged because that describe is unchanged. Only one row of one file under skills/** moves (2-line diff). Mechanical proof (check:react-blocks, in Lint & Repo Gates) still running at review time.
  5. Text. At objectui f8a9d0fb0596… (the untouched .objectui-sha): ObjectForm.tsx:1407 comment "Map 'grid' and 'inline' to 'vertical' as fallback", :1408-1410 fold; :463/:499 two-value hand-off; DrawerForm.tsx:575, ModalForm.tsx:597 fold to 'vertical'; TabbedForm.tsx:556, SplitForm.tsx:445, WizardForm.tsx:1075 layout: 'vertical' as const; index.tsx:238 four-arm inputs. At eb7f586b: fold comment at :937 and four-arm inputs at :103, so the docblock claim holds. Liveness row is props.form.children.layout, live, note re-anchored at the same lines; verifiedAt has 28 precedents in the file; Spec property liveness CI green. layout-dsl.mdx sentence true. 17.5.0 in the prescriptions matches 7 other unreleased refusals at head (spec at 17.4.0; every pending spec changeset is minor). authorable-surface//api-surface/ record no enum values; spec-changes.json/upgrade guide carry no step-18 text — correctly unchanged. The changeset's "the JSON Schema" is the json-schema/ build output (check-generated.ts:405), not a committed file.
  6. Pins. 10 refusal pins and the vocabulary pin depend on the enums; the 3 rewrite pins assert refusal-before-rewrite; D2/D3 registration pins depend on the registrations; the load-path pin depends on retiredFromLoadPath. Only the non-carrier control and the load-path pin would stay green with the conversion absent, and both are paired with the registration pin. issues[0] in the vocabulary pin is safe: objectName is optional (:3594).

② Semver level

"@objectstack/spec": minor with feat(spec)!: title, **BREAKING** banner, the adr-0087 marker registered form-layout-inline-grid-to-vertical, ui-form-layout-inline-grid-retired (both ids resolve at head), and Clause-②: no (narrowing) on PR body line 2 and in the changeset — the exact spelling clause2-line.mjs:95 documents for a breaking narrowing (CLAUSE2_ARMS :101). Check Changeset CI green; check-changeset-no-major and check-adr-0087-registration run in Lint & Repo Gates, in progress at review time (30/34 checks completed, 0 failed, 2 skipped).

③ Boundary flags

  • objectui follow-through (objectui#7759 group C): owed on the pin bump — objectui origin/main plugin-form/src/index.tsx:238 still offers four arms; correctly not filed here.
  • Rewrite, not strip: behaviour-preserving at the pin (fold target is 'vertical'); follows the triage note.
  • Clause-② no (narrowing) over triage's yes: the correct spelling per clause2-line.mjs.
  • Governed path: draft PR, mergeable_state: blocked; awaits the maintainer's approval for skills/**.
  • layout-dsl.mdx hand edit beyond the file list: one true sentence; new line 98 is left unwrapped (cosmetic).
  • horizontal observation: no carrier; out of scope.
  • Context, not a defect: the pinned objectui commit exists in the objectui repo but is not reachable from objectui origin/main; the pin is inherited and untouched by this PR.

Implemented-by: claude/issue-20221-form-layout-inline-grid-retired
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么:表单的 layout(字段布局)只保留 vertical(纵向)和 horizontal(横向),删去 inline 和 grid。改了两处:页面里的 object-form 组件,以及视图里的表单定义(含 formViews、视图项、平铺表单覆盖体,共五个入口)。写了这两个值的元数据在校验或保存时会被明确拒绝,报错直接告诉作者怎么改:写 vertical;想要多列,设 columns。数据库里已存的旧视图和页面,读取时由转换层自动改成 vertical,columns 原样保留。

为什么改:这两个值从来没有生效过。前端每一种表单形态都把它们当 vertical 渲染,效果完全一样。作者、尤其是 AI,会写出一个看似有意义、实际被丢弃的配置,而且还能通过校验。多列本来就由 columns 控制;inline 是工具栏、筛选行的形态,不是记录表单的布局。按您在 #18900 定下的判据(主流平台有这个能力,但已经由别的键表达),它们属于重复词汇,直接退役,不留过渡期。

风险与代价(含回滚):

  • 本仓与示例应用中没有任何地方写过这两个值(已实测,有对照),现有应用不受影响。
  • 外部若有人写了,会看到带改法的明确报错;已存数据自动改写,渲染结果与之前一致。
  • 本 PR 触及受管路径 skills/**:只有生成的 react-blocks.md 里的一行(layout 行由生成器按新 schema 重写),这就是需要您批准的原因。
  • 剩余代价:objectui 设计器的下拉框和组件注册里还列着这两个选项,要在下次同步 objectui 版本时一并删去(objectui#7759 C 组),已写入 PR 的 Acceptance notes。
  • 回滚:revert 本 PR 即可,没有需要撤销的数据迁移。

席位意见:建议批准。

  • 契约复审档复核 PASS(72/72),其中逐项核对了受管的那一行生成内容,以及转换层覆盖的每一个存储位置。
  • 长远看,消灭「声明了却不兑现」的词汇,让声明即强制。
  • 防 AI 写错:把静默折叠改成响亮拒绝并给出处方。
  • 实际需求:零生产者,实测。
  • 不扩散:立即退役,无别名。

你要做的:批准此 PR(os-zhuang 或 hotlong 任一账号的 APPROVED 即可);批准后由本席完成入队落地。

This was referenced Sep 27, 2026
@hotlong
hotlong marked this pull request as ready for review September 28, 2026 02:12
@hotlong
hotlong enabled auto-merge September 28, 2026 02:12
…rm-layout-inline-grid-retired

Conflicts resolved by hand, both intents kept:
- packages/spec/src/conversions/registry.ts: main's report-joined-chart-removed
  and view-overlay-owner-hidden-removed kept; form-layout-inline-grid-to-vertical
  appended after them (definition and CONVERSIONS_BY_MAJOR[18] list).
- packages/spec/src/migrations/registry.ts: step18 rationale takes main's text
  (including its rewrite of the overlay sentence) and appends this branch's
  form-layout paragraph; conversionIds append form-layout-inline-grid-to-vertical
  after main's two. Generated regions are regenerated in the next commit.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…rion in words, not its tracker number

`os migrate meta` prints this D3 entry's `reason` to the author, and since
the ui-* family was brought to the tracker-free line the CLI pin
(packages/cli/test/migrate-meta-engine-guidance.test.ts) holds every ui-*
entry's printed block free of a tracker id. The reason cited the
maintainer's ADR-0049 family criterion by its card number; it now says what
that criterion decided: judge a family by whether mainstream platforms have
the capability (build the consumer once, correctly, if they do; retire the
key if they do not), not by whether anything in this repository reads it.
Multi-column is such a capability and the spec already carries it as
`columns`, so `grid` / `inline` are redundant vocabulary, not a missing
consumer. Text only: no id, conversion, refusal or rewrite moves.
registry.ts regenerated with gen:migration-registry.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Director seat note · the APPROVED click has landed, the head is dirty against main · 2026-09-28T02:30Z

Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3), check-in #9. A hand-off to the owning seat, not a dispatch; nothing else written on this PR.

  • The maintainer approved this PR as hotlong at 2026-09-28T02:12Z, marked it ready for review and armed auto-merge. That is the click the 速读 (5857885725) asked for — the decision is in.
  • The head 2d0b7a6bcb4b6fae1f81d07135edd5221a9ba685 no longer merges cleanly: git merge-tree --write-tree origin/main (main at 6704717188) reports content conflicts in packages/spec/src/conversions/registry.ts and packages/spec/src/migrations/registry.ts. Auto-merge is armed but the merge queue cannot take a dirty head, so it will sit until the branch carries main.
  • For the owning seat (domain:spec seat 1): merge origin/main into the branch with a merge commit (no rebase), regenerate the two registries with the repo's tooling, push; then a fresh at-tier record at the new head (the PASS 5857879278 is bound to 2d0b7a6bcb), and clear needs-user-decision (the click has landed). If the push dismisses the APPROVED review, re-request os-zhuang / hotlong via the relay rather than asking in chat.
  • The director writes no code and does not resolve the conflict.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Post-landing audit · merged two commits past the recorded head, no fresh record · marker stripped · 2026-09-28T03:40Z

Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3), duty one (record audit). Read-only on the code; one label write.

  • Merged from the queue at 2026-09-28T03:31Z at 1d496f5c98. The at-tier PASS record 5857879278 is bound to 2d0b7a6bcb. Between them: b2f3921ce0 (merge of origin/main, resolving the two registry conflicts) and 1d496f5c98 (fix(spec): the D3 entry's reason string restated in words, mirrored in migrations/registry.ts; 7 / 4 lines, prose only, read through the API).
  • Reading: the record's ① judgments, ② minor and ③ flags hold at the merged head — the second commit changes no key, enum, conversion or test, only the sentence os migrate meta prints. So the landing is sound, and the gap is procedural: the owning seat pushed past the recorded head without a fresh or adopting record, and the queue guard (APPROVED by a governed approver) does not check record-vs-head. Logged on the seat ledger as a 漏网 of the record discipline, not of the contract.
  • needs-user-decision was still on the merged PR (the click landed at 2026-09-28T02:12Z); stripped now. Nothing else changes.

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…JsonSchema (objectstack-ai#20304)

Fixes objectstack-ai#19100

Clause-②: no

## What this does

`packages/spec/scripts/build-react-blocks-contract.ts` read each block's
spec schema through a bare `z.toJSONSchema(schema, { unrepresentable:
'any' })`. That call sat outside the published-projection choke point,
so the refinement override that every published JSON Schema artifact
carries was not applied to the schemas behind
`skills/objectstack-ui/references/react-blocks.md`. The generator now
projects through `projectPublishedJsonSchema(schema, { unrepresentable:
'any' })`. Its allowance row in
`published-projection-choke-point.test.ts` and the prose naming it are
removed.

**Measured outcome: branch (A), byte-identical.** The shipped
`react-blocks.md` is not in this diff, and no `skills/**` path is
touched.

## Measurement (taken before any repair, on `3f86dc52f2`)

### The shipped file, regenerated with the package's own generator
(`pnpm --filter @objectstack/spec gen:react-blocks`)

```
blob before: 573f51b
blob after : 573f51b
cmp before after: exit 0

$ git status --porcelain      (after gen:react-blocks, generator edited)
 M packages/spec/scripts/build-react-blocks-contract.ts
$ git diff --stat
 packages/spec/scripts/build-react-blocks-contract.ts | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)
```

The shipped file has no hunk. `check:react-blocks` on the final tree: `1
generated files in sync with packages/spec`.

### JSON projection of the 3 spec-backed block schemas, bare call vs
`projectPublishedJsonSchema`

| block | schema | before | after | identical |
|---|---|---|---|---|
| ObjectForm | `FormViewSchema` | 17923 B, sha256 `caec4524119dbe87` |
17923 B, sha256 `caec4524119dbe87` | yes |
| ListView | `ListViewSchema` | 93656 B, sha256 `7dad3d8b8b3dd51c` |
94666 B, sha256 `773bae792a7d435e` | **no** |
| ObjectChart | `ChartConfigSchema` | 13619 B, sha256 `b1974a1cf703a528`
| 13619 B, sha256 `b1974a1cf703a528` | yes |

The fourth block (`Block`) has no spec schema; it is overlay only.

**The divergent schema is `ListViewSchema`, at exactly two JSON paths:**

- `properties/conditionalFormatting/items/properties/condition/anyOf/1`
- `properties/bulkActionDefs/items/properties/visible/anyOf/1`

Both are the object branch of `EvaluatedExpressionInputSchema`, which is
`EvaluatedExpressionSchema`. The bare call dropped two declared
refinements there:

1. `NON_BLANK_STRING` on `source`
(`packages/spec/src/shared/expression.zod.ts:172`). Through the override
it projects as `"minLength": 1, "pattern": "\\S"`.
2. `requiredOneOf(['source', 'ast'])` on `ExpressionSchema`
(`expression.zod.ts:105`), which `EvaluatedExpressionSchema` inherits
via `safeExtend`. Through the override it projects as `allOf: [{ anyOf:
[{ required: ["source"] }, { required: ["ast"] }] }]`.

ObjectForm and ObjectChart are identical because their projections reach
no Expression envelope at all: zero `dialect` and zero `source` nodes,
so the override has nothing to write.

**Why the markdown does not move:** the generator keeps only the props
named in the block's `dataProps` allow-list
(`build-react-blocks-contract.ts:93`). ListView's list
(`packages/spec/src/ui/react-blocks.ts:305`) is `type, data, columns,
sort, searchableFields, userFilters, pagination, grouping, rowHeight,
selection, rowActions, inlineEdit`. It names neither
`conditionalFormatting` nor `bulkActionDefs`, so both divergent subtrees
are filtered out before any row is rendered. So no sentence on the
AI-facing page is wrong today. The change makes the generator project
the way the other published producers do; it does not alter what the
page says.

Full ListView projection diff (only these hunks):

```diff
@@ -1861,7 +1861,9 @@   (conditionalFormatting[].condition, object branch)
                   "source": {
-                    "type": "string"
+                    "type": "string",
+                    "minLength": 1,
+                    "pattern": "\\S"
                   },
@@ -1881,7 +1883,23 @@
-                "additionalProperties": false
+                "additionalProperties": false,
+                "allOf": [
+                  { "anyOf": [ { "required": ["source"] }, { "required": ["ast"] } ] }
+                ]
@@ -1933,7 +1951,9 @@   (bulkActionDefs[].visible, object branch: same two additions)
@@ -1953,7 +1973,23 @@
```

(The `allOf` body is shown compacted; the emitted JSON is the same
structure, pretty-printed.)

## The diff

- `packages/spec/scripts/build-react-blocks-contract.ts`: import
`projectPublishedJsonSchema`, drop the `zod` import, route the one
projection through the helper, and update the header comment.
- `packages/spec/scripts/published-projection-choke-point.test.ts`:
- remove the `build-react-blocks-contract.ts` row from
`DECLARED_DIRECT_CALLS`, so the allowance goes from 3 rows to 2 (the
choke point itself plus the representability probe);
  - rewrite the docblock that named it;
- add the generator to `PUBLISHED_PRODUCERS`, so the pin also asserts
that it imports the helper, the same hold `build-schemas.ts` and
`build-openapi.ts` are under.
- `packages/spec/scripts/lib/refinement-projection.ts`, **comment
only**: the choke point's docblock listed this generator as a direct
`z.toJSONSchema` caller outside the helper, which this change makes
false. It is now producer 5 of 5, and the "outside" bullet names only
the CLI's `os generate`.

## Governed-surface routing

`node scripts/pm/check-governed-merges.mjs --branch
claude/issue-19100-react-blocks-published-projection` gives exit 0: `0
of 3 path(s) hit the register ... NOT governed`, size 58 changed lines.
No `skills/**` path is in the diff.

About the generated-artifact exception: the same predicate, run with
`skills/objectstack-ui/references/react-blocks.md` added to the list,
gives exit 3, and the exception does **not** lift the path: `the tree
under test modifies the generator this exception trusts — the path stays
governed ... land the generator change and the artifact regeneration as
separate PRs`. So if the bytes had changed, this PR would have been Tier
H, with no exception route available. They did not change, so the
question does not arise.

## Changeset

None. `skip-changeset` applies because nothing here ships:
`@objectstack/spec`'s `files[]` is `dist, json-schema, liveness,
prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md,
api-surface, spec-changes.json`, and `scripts/` is not in it.

## Verification (final tree `c64a07ed94`)

- `pnpm --filter @objectstack/spec exec vitest run --project repo
scripts/published-projection-choke-point.test.ts`: 6/6 passed, VERDICT
command-exit 0.
- `vitest run --project local` on
`scripts/refinement-projection.test.ts` and
`src/ui/react-blocks.test.ts`: 2 files, 90 tests passed.
- `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit`,
`check:scripts-typecheck`, `check:test-typecheck`): VERDICT command-exit
0.
- `pnpm --filter @objectstack/spec test` (`--project local`): 551 files
passed and 1 skipped; 16253 tests passed, 1 skipped, 1 todo; VERDICT
command-exit 0.
- `check:react-blocks`: exit 0, in sync.
- Derived gate set (`node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands`, 61 commands): 56 exit 0,
including `check:react-blocks`, `check:authorable-surface`,
`check:liveness`, `check:pm-governed-merges`, `check:pm-dispatch-gates`,
`check:nul-bytes`, `check:comment-mask-adoption` and
`check:test-source-alias`. 5 are **NOT MEASURED** (exit 3, PREREQUISITE
NOT MET): `check:dts-closure`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:sourcemap-no-sources-content` and
`check:type-check-debt`. Each needs a whole-workspace `dist/` build,
which this diff cannot move, because no changed file ships. They are
declared to CI. `dispatch-gates --ran` reconciliation: 61 derived, 56
run, 5 NOT-MEASURED (derived from the recorded exit 3), 0 unrun.
- Targeted lint (`eslint --no-inline-config --format json` on the 3
changed files): 3 files linted, 0 errors, 0 warnings. The narrowing
excludes nothing: the ESLint config never enables type-aware linting (no
`parserOptions.project`, no typed rules, as stated at
`eslint.config.mjs:326-328`), so this diff cannot change the verdict on
any untouched file. The full `pnpm lint` run is CI's.
- NOT MEASURED locally: the rest of the spec `repo` vitest project (32
other files). It is outside the package's `pnpm test` script, and my one
local attempt was cut off by my own timeout after a 256 s lock wait. Its
one file related to this diff, the choke-point pin, ran green on its own
(above).

### Reverse verification (one-time, not kept as test files)

Both runs used `node scripts/ablation-replace.mjs` against the committed
tree `c64a07ed94`. Each restore was proven by blob equality with HEAD
(`224f9f42639e`) and an empty `git diff HEAD`.

1. Replacing the helper call with the old bare call makes the pin go red
as predicted: `no producer in scripts/ reaches z.toJSONSchema directly`
fails with `"build-react-blocks-contract.ts: 1 direct call(s), declared
0"` (1 failed, 5 passed).
2. Deleting the helper import makes the pin go red as predicted: `every
published producer imports the helper it is required to project through`
fails on `build-react-blocks-contract.ts` (1 failed, 5 passed).

## Acceptance notes

- ADR-0082 (line 39) describes the generator as reading the spec schemas
through `z.toJSONSchema`. That is still true at the mechanism level,
because the helper is the call that reaches it, so the ADR is not edited
here. It is a governed surface and outside this card.
- The ListView divergence is real but has no rendered effect today. If
`conditionalFormatting` or `bulkActionDefs` ever join ListView's
`dataProps`, the row type still renders from the top-level node
(`object[]`), which the override does not change.
- PR objectstack-ai#20262 also regenerates one row of `react-blocks.md`. This PR does
not touch that file, so the two cannot conflict.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…on entries states each lesson in words, not tracker numbers (stage 2) (objectstack-ai#20324)

Part of objectstack-ai#20233

Clause-②: no

**Stage 2 of a staged card.** The card stays open for later stages; this
PR carries no closing keyword. Text only: no entry id, `from` / `to`,
conversion or matching logic moves, and the chain rewrites exactly what
it rewrote before.

## What this does

`os migrate meta` prints every ADR-0087 semantic entry it crosses as one
block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's
`reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's
runtime-string rule applies to all of it: 「Runtime strings — refusal
prose, prescriptions, anything an author is shown — carry no tracker
number (`pnpm check:doc-authoring`): the lesson goes into the text.」
Form **D** of ruling C+D on the parent card sets the shape: the lesson
in words, and no number, dead or alive.

This stage covers the next two families by site count, `ui-` and
`plugin-`: **111 sites → 0** in the three prose fields, plus the
`surface` field of the two entries that carried an id there (ruling A in
the stage-1 ACCEPT, `5858839916`). Each site now says what the cited
ruling, measurement or fix decided. ADR ids stay. `registry.ts`,
`spec-changes.json` and `docs/protocol-upgrade-guide.md` are regenerated
from the entries (`gen:migration-registry`, `gen:spec-changes`,
`gen:upgrade-guide`), never hand-edited. The stage-1 pin is widened to
hold `engine-`, `ui-` and `plugin-`.

## Census — tracker ids in the author-shown fields

**Instrument.** The stage-1 instrument, re-implemented: a TypeScript-AST
walk over every `packages/spec/src/migrations/entries/**/*.ts`. For each
`entry` object literal it evaluates the string value of `replacement`,
`reason`, `acceptanceCriteria` and (counted separately) `surface`,
joining string literals with `+`, then counts `#` followed by 4 or 5
digits at a word boundary. **Tree:** `objectstack-ai/objectstack` at
`2aa25efb4e` (this branch's base, the stage-1 merge). Unevaluable
fields: 0.

**Controls, same run.**
- **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites
(replacement 1, reason 6), the same reading stage 1 took.
- **Dark (comment lines):** 734 `//` lines in entry files carry a
tracker id, and none is counted — for example
`18.client-envelope-convergence-analytics-automation.ts` has 5 such
lines and counts 0. Comment lines belong to the sibling card, and ⛔ this
PR touches none (the count is 734 before and after).
- **Dark (field boundary):**
`17.authoring-schemas-strict-unknown-keys.ts` carries one id in
`surface`; it counts 0 in the three-field total and 1 in the `surface`
column.

**Re-measured on the base, matching the stage-1 census:** `ui-` 17
entries, **65** sites (replacement 6 / reason 58 / acceptanceCriteria
1), 42 distinct ids; `plugin-` 11 entries, **46** sites (1 / 39 / 6), 31
distinct ids. `surface`: 1 site each. `engine-`: 0 (stage 1). Whole
tree: 267 entries, **950** sites, 9 `surface` sites.

**After this PR:** `ui-` 0, `plugin-` 0, `engine-` 0; whole tree **950 →
839** sites and `surface` **9 → 7**. The next family by site count is
`driver-` / `kernel-` / `system-` (44 each).

| entry | sites (replacement / reason / acceptanceCriteria) | `surface`
|
|---|---|---:|
| `17.plugin-activation-events-retired` | 5 (0 / 4 / 1) | 0 |
| `18.plugin-auto-restart-never-reinitialised` | 11 (0 / 7 / 4) | 0 |
| `18.plugin-manifest-contributes-dead-members-retired` | 3 (0 / 2 / 1)
| 0 |
| `18.plugin-manifest-contributes-routes-retired` | 6 (1 / 5 / 0) | 1 |
| `18.plugin-manifest-dead-containers-retired` | 3 (0 / 3 / 0) | 0 |
| `18.plugin-manifest-kind-globs-retired` | 2 (0 / 2 / 0) | 0 |
| `17.plugin-manifest-loading-retired` | 2 (0 / 2 / 0) | 0 |
| `17.plugin-runtime-family-retired` | 5 (0 / 5 / 0) | 0 |
| `18.plugin-security-scan-result-surface-retired` | 6 (0 / 6 / 0) | 0 |
| `18.plugin-security-scanner-retired` | 3 (0 / 3 / 0) | 0 |
| `18.ui-cloud-connection-widgets-unknown-keys-refused` | 3 (0 / 3 / 0)
| 0 |
| `18.ui-form-field-length-malformed-refused` | 8 (2 / 6 / 0) | 0 |
| `18.ui-form-field-precision-scale-integer-refused` | 4 (1 / 3 / 0) | 0
|
| `18.ui-form-view-predicate-features-root-refused` | 2 (0 / 2 / 0) | 0
|
| `17.ui-interaction-config-family-retired` | 7 (1 / 6 / 0) | 0 |
| `18.ui-list-view-groupbyfield-padded-refused` | 1 (0 / 1 / 0) | 0 |
| `18.ui-list-view-grouping-field-padded-refused` | 2 (0 / 2 / 0) | 0 |
| `18.ui-mcp-connect-agent-unknown-keys-refused` | 5 (0 / 5 / 0) | 0 |
| `17.ui-notification-action-embed-config-retired` | 8 (0 / 8 / 0) | 0 |
| `18.ui-object-grid-page-size-positive-integer-refused` | 4 (0 / 4 / 0)
| 0 |
| `18.ui-react-list-view-binding-aliases-retired` | 2 (0 / 2 / 0) | 1 |
| `18.ui-record-blocks-unknown-keys-refused` | 3 (0 / 3 / 0) | 0 |
| `18.ui-reference-rail-unknown-keys-refused` | 2 (0 / 2 / 0) | 0 |
| `17.ui-widget-i18n-family-retired` | 14 (2 / 11 / 1) | 0 |
| four entries with no site: `plugin-version-semver-2-0-0`,
`ui-action-undoable-unfulfillable-refused`,
`ui-bulk-action-param-unknown-keys-refused`,
`ui-report-joined-container-selection-refused` | 0 | 0 |
| **total, 28 entries** | **111 (7 / 97 / 7)** | **2** |

## Every citation read, and what the text now says

I read each cited issue or PR myself with single-card REST reads: the
body, and the comments where a ruling or a measurement lives. Ids are in
code spans so this body posts no cross-references. `objectui#N` ids were
read from `objectstack-ai/objectui`; bare ids from this repository.

| cited | what it decided (read) | how the text now carries it |
|---|---|---|
| `objectstack-ai#3733` | The pruned `cached` field key: the parse succeeded and the
removed key was dropped without a word; the orphan schema was deleted. |
"an earlier field-key prune measured exactly that — the parse succeeded
and the removed key was dropped without a word" |
| `objectstack-ai#3950` | Removed the plugin sandboxing / integrity / approval config
nothing read: an exported schema with no consumer is read as a
capability. | "the lesson of the unwired plugin sandboxing / integrity /
approval config …: an exported schema with no consumer is read as a
capability", and the plugin-runtime "earlier removal of this module's
discovery/sandbox config island" |
| `objectstack-ai#4001` | Maintainer, 2026-08-03: every authorable surface refuses an
unknown key (strict), in the v17 window, measured file by file for an
authoring door. | "the component-props unknown-key gate (an authorable
surface refuses a key it does not declare …)"; "the v17 unknown-key
strictness sweep (its ui/ batch 14)"; "the batch of the v17 unknown-key
strictness sweep that measured this file as having no authoring door" |
| `objectstack-ai#4115` | Ruling A: an objectui symbol named like a spec export must
import it, or take a name of its own (or an allowlist row), enforced by
a CI guard. | "renamed off the spec's names under objectui's rule that a
symbol named like a spec export must import it or take a name of its
own" |
| `objectstack-ai#4484` | `findStream` removed with no tombstone: a TS/API surface
nothing parses, so tsc at the call site carries the ban. |
"`contracts.IDataDriver.findStream` (removed with no tombstone, because
nothing parses a driver object)" |
| `objectstack-ai#4535` | The dual-source cleanup: 52 names declared twice across
entry points, taken to 0. | "the dual-source cleanup removed the `./ui`
copies …" |
| `objectstack-ai#4583` | The datasource capability flags were dead; `readOnly` was
precisely validated and inert, and the CRM example called a datasource a
read replica while writes went through. The strictness ledger records
the "more convincing lie" lesson there. | "(the lesson of the datasource
capability flags: `readOnly` was precisely validated and read by
nothing, while a shipped example called a datasource a read replica and
wrote through it)" |
| `objectstack-ai#4610` | Removed the `./ui` `Notification` / `NotificationConfig`
copies (zero import sites measured); the `./api` inbox row stayed live.
| "the dual-source cleanup removed the `./ui` copies of
`NotificationSchema` / `NotificationConfigSchema` (the same names
declared differently on other entry points)" |
| `objectstack-ai#4653` | Maintainer ruling A, 2026-08-02: converge `activationEvents`
on the kernel's structured `{ type, pattern }` shape, re-exported from
studio. | "once the kernel and studio copies had converged on the
kernel's structured `{ type, pattern }` shape" |
| `objectstack-ai#4657` | Retire both `activationEvents` keys (REMOVE, v17 window):
kernel tombstone, studio strict refusal, the orphan schema deleted. |
"Both keys took ADR-0049's REMOVE answer, not ENFORCE, while protocol 17
was still unreleased"; its id sentence in plugin-runtime is covered by
the entry id `plugin-activation-events-retired` |
| `objectstack-ai#4834` | Maintainer, 2026-08-03: REMOVE the rest of the
plugin-runtime family; hot loading returns with its implementation, if
ever. | "The maintainer's ruling of 2026-08-03 is that decision,
answered REMOVE: …"; "the maintainer's REMOVE ruling on the rest of the
plugin-runtime family"; `plugin-runtime-family-retired` by id elsewhere
|
| `objectstack-ai#4875` | The health-check timeout guard is cleared when the race
settles, and deliberately not `unref`'d (an unref'd guard can swallow
the timeout). | "its guard timer (kept ref'd while the race is
undecided, cleared the moment it settles)" |
| `objectstack-ai#4910` | Inbound rate limiting was built from a new seam: a `server:`
key that carries only the keys its executor consumes. | "the way inbound
rate limiting came back, as a new key carrying only what its executor
consumes" |
| `objectstack-ai#4914` | Maintainer, 2026-08-04: REMOVE `manifest.loading`, with a
hard precondition of a clean cloud and objectui bare-name sweep. | "the
maintainer ruled REMOVE on 2026-08-04, on condition that a bare-name
sweep of cloud and objectui came back clean first" |
| `objectstack-ai#4938` | Maintainer, 2026-08-04: retire `HttpServerConfig`'s seven
unreachable keys with their container. | "the `HttpServerConfig`
retirement (seven keys no runtime read and no authoring door reached,
retired with their container)" |
| `objectstack-ai#4988` | Maintainer, 2026-08-04: retire the five ui interaction
files; touch, dnd, keyboard and motion are renderer built-in behaviour,
offline belongs to a sync engine. | "The 2026-08-04 ruling retired the
family — touch, drag-and-drop, keyboard and motion are renderer built-in
behaviour and offline belongs to a sync engine …";
`ui-interaction-config-family-retired` by id in the widget entry |
| `objectstack-ai#5015` | REMOVE `NotificationActionSchema` / `EmbedConfigSchema`,
2026-08-04: a no-door dead surface retires implementation-first, as
three same-shape rulings that week had decided. | "left the disposition
to ADR-0049's enforce-or-remove, which came back REMOVE on 2026-08-04: a
dead surface with no authoring door retires implementation-first, …" |
| `objectstack-ai#5021` | Maintainer, 2026-08-04: retire all nine unconsumed theme
token groups; theme-driven typography is not near-term. | "the
theme-token retirement (theme-driven typography is not a near-term
capability, so nine token groups nothing consumed were retired)" |
| `objectstack-ai#5040` | Build the declarative ApiEndpoint executor in 17.x; the v17
loud refusal of a non-empty `apis:` becomes execution. | "live since
protocol 17, once the declarative endpoint executor was built and the
loud refusal of a non-empty `apis:` became execution" |
| `objectstack-ai#5055` | Maintainer, 2026-08-06: retire the doorless widget and i18n
shapes (8 of 9 widget sites; `FieldWidgetProps` kept). | already stated
by the entry ("The 2026-08-06 ruling weighed …"); the trailing id is
dropped |
| `objectstack-ai#5068` | Maintainer, 2026-08-05, direction A: parse a component's
`properties` against its `ComponentPropsMap` row by `type`, at publish
and lint; a type with no row is skipped because the type union is open.
| "the props gate's dispatch (it parses `properties` against the type's
row at publish and lint time, and skips a type with no row because the
type union is open)" |
| `objectstack-ai#5781` | Correction: objectui did re-export the `./ui` notification
names; the removal stands. | "falsified for objectui, which re-exported
both names, … the removal itself stands" |
| `objectstack-ai#6011` | Maintainer: close the `ctx.user` `roles` alias now, no
window. | "`actor-user-roles-to-positions` (the `ctx.user` `roles`
alias, closed at once on the maintainer's word rather than given a
window)" |
| `objectstack-ai#7751` | Maintainer, 2026-08-12, direction A: the `object-*` blocks
get `ComponentPropsMap` rows, key sets from renderer read points. | "the
shape it was given when the `object-*` blocks first got
`ComponentPropsMap` rows measured from their read points" |
| `objectstack-ai#8321` | `Field.scale` / `precision` refuse non-integer and negative
values (`int().min(0)`). | "(that surface tightened first, to a
non-negative integer)"; "after that surface converged on
`z.number().int().min(0)`" |
| `objectstack-ai#8691` | A strict `record:reference_rail` row, key set from the
renderer's read points. | "the class already closed for
`record:reference_rail` …"; "after the rail was given its strict row" |
| `objectstack-ai#8744` | Strict rows for `record:alert` / `record:quick_actions` /
`record:history`. | "… and then for `record:alert` /
`record:quick_actions` / `record:history`, each by declaring a strict
`ComponentPropsMap` row" |
| `objectstack-ai#11168` | The kind-registration log names the declared `id`; the
`kind` bucket is reachable through `GET /metadata/:type`; `globs` had
zero readers. | "Measured by the engine-lane fix that made kind
registration log its declared `id` (which also found the `kind` bucket
itself reachable …)" |
| `objectstack-ai#11169` | Maintainer, 2026-08-24 (「接受你的建议。」): remove `globs` through
the full ADR-0049 ceremony. | "maintainer ruling 2026-08-24 (「接受你的建议。」)
…: remove, through the full ADR-0049 ceremony" |
| `objectstack-ai#11327` | The doc-correction half of the routes ruling (2026-08-22,
「接受所有」, Option B): four author-facing sites redirected to the imperative
`http.server` mount. | "the author-facing corrections landed FIRST (the
skill's decision table, the dispatcher protocol doc, ADR-0088:40 and
app.mdx, each redirected to the imperative mount)"; the ruling sentence
states Option B's content |
| `objectstack-ai#11566` | Maintainer, 2026-08-24: `maxLength` →
`z.number().int().min(1)`. | "`maxLength` by the maintainer's 2026-08-24
ruling"; "tightened first, to a positive integer, by maintainer rulings"
|
| `objectstack-ai#11575` | Strict, empty rows for `cloud-connection:panel` /
`marketplace:installed-list`. | "the strict, empty
`cloud-connection:panel` / `marketplace:installed-list` rows closed the
previous two" |
| `objectstack-ai#11825` | Maintainer, 2026-08-25: retire the declarative
`AdvancedPluginLifecycleConfig` container; the classes stay a
host-driven library. | "which is why the maintainer retired its
declarative config container on 2026-08-25 and kept the classes as a
host-driven library"; "The maintainer's 2026-08-25 keep of the
host-driven library still stands" |
| `objectstack-ai#11852` | Both failure routes (returned, thrown or timed out) funnel
into one failure step: one counter, one threshold comparison. | "the two
failure routes — a returned failure and a thrown or timed-out check —
sharing one failure counter and one threshold comparison" |
| `objectstack-ai#11949` | Maintainer, 2026-08-25, option B: `minLength` →
`int().min(1)`, zero refused. | "`minLength` by the 2026-08-25 one,
which refused zero too" |
| `objectstack-ai#11955` | `successThreshold` binds from every status that records a
failure. | "The fix that made `successThreshold` bind from every status
that records a failure made that MORE convincing" |
| `objectstack-ai#12174` | The form-field row keys are live, so they were
shape-tightened in place on the object-field templates. | "The
form-field row still carried the object field's old shape … The row keys
are LIVE … The schema now refuses …" (unchanged tail) |
| `objectstack-ai#12269` | Closure A: `packages/mcp` gets its own canonical-envelope
gate. | "door 3 of the canonical-envelope gate `@objectstack/mcp` was
given for its shipped page" |
| `objectstack-ai#12340` | Maintainer, 2026-08-26: retire the `'disk'` /
`'distributed'` state strategies (silent memory fallbacks) and
`distributedConfig`; "a vocabulary of nothing is not a vocabulary". |
"the `'disk'` / `'distributed'` state strategies that fell back to
memory in silence"; "(ruled 2026-08-26: a vocabulary of nothing is not a
vocabulary)" |
| `objectstack-ai#12400` | The cloud leg for `capabilities` / `configuration` /
`extensions` measured clean at cloud `15f55df`. | "dispatched once the
cloud half of the census below came back clean" (the census clause names
`15f55df`) |
| `objectstack-ai#12428` | Refuse/retire: `startWatching` throws instead of logging
success; `watchPatterns` is tombstoned because a key leaving a surviving
def has no route-3 exit. | "the file-watching placeholder whose
`startWatching` logged success while watching nothing"; "for the reason
the file-watching retirement recorded" |
| `objectstack-ai#12665` | Implement the maintainer's 2026-08-27 option B: a form view
may not name `features.*` in a predicate; refused at authoring. | "Ruled
by the maintainer on 2026-08-27 (option B — vocabulary narrowing: a form
view may not name `features.*` in a predicate, and the authoring door
refuses it loudly)" |
| `objectstack-ai#14791` | Maintainer, 2026-09-07: retire the `objectName` /
`viewType` aliases now, no window. | "(2026-09-07)" in the sentence that
states the retirement |
| `objectstack-ai#15513` | Maintainer, 2026-09-05: retire the incident-response,
training and change-management families whole; not roadmapped. |
"retired whole, with the training and change-management families
(maintainer ruling 2026-09-05: not roadmapped, so retired rather than
marked experimental …)" |
| `objectstack-ai#15930` | Retired `PluginSecurityScanner`; no replacement, repair
refused. | "the scanner retirement recorded as
plugin-security-scanner-retired. That retirement removed
PluginSecurityScanner …" |
| `objectstack-ai#15932` | Maintainer, 2026-09-07 (「同意」): retire the scan-result
family and `securityScan`. | "maintainer ruling 2026-09-07 (adopted
verbatim 「同意」): retire the scan-result family and its securityScan
sibling, because once the scanner was gone nothing so much as imported
their types" |
| `objectstack-ai#16526` | Maintainer, 2026-09-07, option A: cloud does not re-host
the consumer-less control-plane files; they are deleted. | "(ruled
2026-09-07: cloud does not re-host the control-plane files it never
consumed)" |
| `objectstack-ai#17360` | Ruling C: refuse a padded grouping field name at the
producer (not a trim). | "Ruled by the maintainer on 2026-09-10
(「其他同意」): refuse at the producer." |
| `objectstack-ai#17499` | Refuse a padded `groupByField` on kanban, gantt and
timeline. | "The same padded-name defect the grouping-level narrowing …
refused, on the axis that one scoped out by name, and given the same
refusal." |
| `objectstack-ai#19046` | Bound the grid component arm's page sizes to positive
integers. | the sentence now opens "This door still carried the shape
…"; the declaration half is stated in the entry |
| `objectui#3161` | Batch 7/8 of the objectui burn-down under the
`objectstack-ai#4115` rule (renames). | folded into the `objectstack-ai#4115` sentence |
| `objectui#3169` | objectui stopped declaring symbols under names the
spec owns; its rename tripwire fails both ways. | "the rename tripwire
objectui added when it stopped declaring symbols under names the spec
owns" |
| `objectui#3289` (PR) | `@object-ui/fields`' validation slot renamed
onto the spec's `error` and connected to its producer. | "an objectui
fix of 2026-08-03, made to follow the spec, renamed …" |
| `objectui#5595` | The console FormPage honours the form's own
`maxLength` override. | "(fixed so that a form's own bound wins over the
object's, as its docstring promised)" |
| `objectui#5898` | The form-view bridge maps every spec key or explains
why not. | "mapField, which maps every spec key or explains why it does
not" |
| `objectui#6262` | The measured `features.*` asymmetry, and the ruling
record. | folded into the `objectstack-ai#12665` sentence |
| `objectui#7347` | The measured padded-grouping failure, and ruling C's
record. | folded into the `objectstack-ai#17360` sentence |
| `objectui#9853` | Measured `pagination.pageSize: 0` reaching
`ObjectGrid`. | "an objectui grid measurement found that …" |
| `objectui#9896` (PR) | A non-positive `pageSize` is refused at all
three grid read points. | "objectui's grid plugin repaired the consumer
half — it now refuses a non-positive page size at all three read points
…" |

The eight dead ids, and the two ids whose page does not say what the
text claimed (`objectstack-ai#2561`, `objectstack-ai#3896`), are in **Acceptance notes**. No
call-shaped token moves: a `name(` census over `registry.ts` is
identical before and after, so textual call-spelling ratchets read the
same.

## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`,
widened

The stage-1 pin now selects every entry whose id starts with a covered
prefix: `engine-`, `ui-` or `plugin-`. It spawns the real CLI (`os
migrate meta --from 16 --to 18`) once, locates each covered block
**verbatim** in stdout, and asserts the printed block carries no `#`
plus 4 or 5 digits. That block includes `surface`. Anti-vacuity:
- the derived set must contain all 29 rewritten entries (5 `engine-`, 10
`plugin-`, 14 `ui-`), and every covered prefix must select at least one
entry;
- presence in stdout is asserted before cleanliness;
- the detector is exercised on both sides first (lit on 4 and 5 digits,
dark on 3, 6 and `ADR-0112`).

The chain reports every semantic entry of every crossed hop, whatever
the stack authors (`applyMetaMigrations` maps `step.semantic` straight
to TODOs), so the fixture is kept as-is and the header now says so. The
file keeps its stage-1 name; a rename is left to the stage that covers
the last family. An entry added later to a covered family is held on
arrival — see Acceptance notes for the one known in-flight case.

## Ablation — the widened pin can fail on a `ui-` block and on `surface`

From committed state, HEAD `8a1076b0a6`, with
`scripts/ablation-replace.mjs` in wrap mode and
`scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is
built from the generated `registry.ts`, so that is the file mutated
(stage 1's attempt 2 records why the entry file is the wrong target).
- **Mutation.** In `registry.ts`, the `surface` of
`ui-react-list-view-binding-aliases-retired`: anchor `(the react-tier
overlay aliases published as deprecated` → `(the react-tier overlay
aliases objectstack-ai#11284 published as deprecated`. The tool read anchor 1 → 0 and
replacement 0 → 1, blob `ab26922f` → `9a200491`.
- **Mutate leg.** Spec build under the lock: command-exit 0. Preflight:
marker present in 4 built files. Pin: **red**, `1 failed | 2 passed` —
`ui-react-list-view-binding-aliases-retired: the printed guidance cites
a tracker id: expected 'objectstack-ai#11284' to be undefined`.
- **Restore.** Tool-proven: blob `ab26922f` == HEAD, `git diff HEAD`
empty.
- **Restore leg.** Spec build under the lock: command-exit 0. The
`--absent` preflight found the marker in none of 222 built files, with
the working tree clean against HEAD. Pin: **green**, `3 passed`.

## Verification

Final head **`071dc7fc1d`** unless a line says otherwise.

- **Pin and its neighbour:** `pnpm --filter @objectstack/cli exec vitest
run --project integration --maxWorkers=2
test/migrate-meta-engine-guidance.test.ts
test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`,
`Tests 10 passed | 1 skipped` (the skip is the default-range file's own
pre-existing `skipIf`).
- **Spec tests that read these entries or the registry:** `pnpm --filter
@objectstack/spec exec vitest run --project local --maxWorkers=2
src/migrations` plus the 14 other spec test files that read
`MIGRATIONS_BY_MAJOR` or one of these entries' text
(`plugin-runtime-tier-truthful-text`, `interaction-config-retirement`,
`widget-i18n-retirement`, …) and
`scripts/build-schemas-check-mode.test.ts`: `Test Files 15 passed`,
`Tests 438 passed`.
- ⚠️ The first `src/migrations` run, at `8a1076b0a6`, went **red, 2
failed**: `migrations.test.ts` pinned the two tracker numbers in
`ui-notification-action-embed-config-retired`'s `reason`. The second
commit re-pins the same guard on the sentences that now carry the lesson
(see Acceptance notes).
- **CLI unit:** `src/utils/spec-release-changes.test.ts` 6 passed;
`test/vitest-tiers-partition.test.ts` 22 passed (at `8a1076b0a6`; no CLI
file changed after it).
- **Call-spelling census that reads `registry.ts`:** `pnpm --filter
@objectstack/driver-sql exec vitest run --maxWorkers=2
src/sql-driver-query-signature.test.ts` gives 15 passed.
- **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exits 0.
`pnpm --filter @objectstack/cli typecheck` exits 0 (at `8a1076b0a6`),
and its test layer holds the recorded 3 files / 28 errors, unchanged.
- **Build:** `pnpm exec turbo run build --filter="@objectstack/cli^..."
--concurrency=2` gives 55/55 (at `8a1076b0a6`); `@objectstack/spec`
rebuilt at the final head, command-exit 0.
- **Gate families:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derives **88** families at
`071dc7fc1d` (the same set as at `8a1076b0a6`). `--ran` over the
recorded exit codes reads **88 derived, 88 run, 0 NOT-MEASURED, 0
UNRUN**, all exit 0. They include `check:migration-registry`,
`check:spec-changes`, `check:upgrade-guide`, `check:generated` (15
artifacts current), `check:doc-authoring`, `check:issue-citations`,
`check:nul-bytes`, `check:adr-0087-registration`,
`check:changeset-no-major` and `check:dual-build-cjs-loads` (104 require
entry points across 66 packages load).
- Union discipline: the first pass started before the second commit, so
the 21 families that started before that edit were re-run at the final
head, and the 6 that refused on a spec `dist` stamp made stale by that
edit (5 × exit 3, `check:generated` exit 1 naming `api-surface` stale by
stamp, `check:dual-build-cjs-loads` exit 3) were re-run after rebuilding
spec at the final head. The reconciled list takes each family's latest
run.
- **Lint (a proven narrowing, not the repo-wide run, which is CI's):**
`eslint --no-inline-config --format json` over the 27 changed `.ts`
files reports 27 files, 0 errors, 0 warnings.
- The population is read from `eslint.config.mjs`:
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 27
are in it (no file-ignored warning).
- Invariance: the config enables no type-aware linting (no
`parserOptions.project`, no typed rules), so a text edit cannot move the
verdict on a file it does not touch.
- **Mergeability:** a driver-free bare clone's `merge-tree --write-tree`
of this head against `origin/main` `7b1e4a4871` (six commits past the
base, one of them adding 25 semantic entries) exits 0 with no conflict.
The census over that merged tree reads `engine-` 0, `plugin-` 0 and
`ui-` 0 across 18 entries: main's new `ui-report-joined-chart-retired`
carries tracker ids only in comment lines.

## Acceptance notes

- **Dead ids, rewritten from the code on `main`.** Eight cited numbers
answer 404 on both the issues and the pulls endpoint, re-probed with a
200 control (`objectstack-ai#11327`): `objectstack-ai#10627`, `objectstack-ai#10724`, `objectstack-ai#10726`, `objectstack-ai#10812`,
`objectstack-ai#11284`, `objectstack-ai#11328`, `objectstack-ai#11332`, `objectstack-ai#14919`. Each sentence was rewritten from
what `main` records, and anything it could not confirm was dropped:
- `objectstack-ai#10627` / `objectstack-ai#10724` / `objectstack-ai#10726` / `objectstack-ai#10812` / `objectstack-ai#11328` (contributes
routes and dead members): `packages/spec/src/kernel/manifest.zod.ts`
carries all ten `retiredKey()` tombstones and the census comment;
`packages/objectql/src/engine.ts` (`manifest.contributes?.kinds`) is
re-measured as the only non-spec read; `plugin-rest-api.zod.ts` and
`metadata-plugin.zod.ts` point at the imperative `http.server` mount.
Dropped: "triage graded 2026-08-21" and the 2026-08-24 date on the
routes entry's cloud sentence (the cloud sha `5b5925a` is kept:
`objectstack-ai#12400`'s body corroborates it). The routes ruling's 「接受所有」 and Option
B are kept: `objectstack-ai#11327`'s body records them.
- `objectstack-ai#11332` (dead containers): the three `manifest.capabilities` /
`configuration` / `extensions` tombstones on `main`. Dropped: "triage
graded 2026-08-23".
- `objectstack-ai#11284` (react-tier convergence):
`packages/spec/src/ui/react-blocks.ts` records the 2026-08-23 maintainer
ruling that the react tier converges on the metadata-tier vocabulary,
deprecating first.
- `objectstack-ai#14919` (scanner): `packages/core/src/security/index.ts`'s tombstone
and `security-scanner-retirement.pin.test.ts` record the 2026-09-05
ruling, the removed class and types, and repair refused. Dropped: "ruled
A: retire in three surfaces" and the batch numbers, which `main` does
not state.
- **A bare id that names the wrong card.** The widget / interaction
entries' "(`objectstack-ai#2561`)" resolves here to an unrelated security-lifecycle
umbrella. The claim ("objectui holds TYPE re-exports … never validators,
and says so") is objectui's own decision on `objectui#2561`: keep the
`@objectstack/spec/ui` re-exports type-only. It was rewritten from
objectui's `packages/types/src/__tests__/p2-spec-exports.test.ts` at
objectui `main`, which records that decision.
- **`objectstack-ai#3896`, cited as "follow-up" and "close-out".** `objectstack-ai#3896` itself is
the sharing-rule `criteria` REST bypass and says neither. The
"follow-up" is PR `objectstack-ai#3950` (its title says so); the "close-out" is the
inert-key sweep recorded on `main` in `docs/protocol-upgrade-guide.md`
and the strictness ledger. Both sentences now say what those decided.
- **Cross-repo ids.** Ten sites are spelled `objectui#N` (nine) or
"objectui PR " plus a number (one). The stage-1 census counted them by
number with the rest; they were read from `objectstack-ai/objectui` (all
200), not from this repository, where the same numbers name unrelated
cards.
- **One bare-number spelling went too.** The scan-result entry spelled a
deleted card as "issue" plus its number, twice, without `#`. The
instrument cannot see it, but it is a tracker number shown to the
author, and it sat in a rewritten sentence.
- **In-flight entry the widened pin will hold.** Open PR objectstack-ai#20262 adds
`18.ui-form-layout-inline-grid-retired.ts` with one tracker id in its
entry text (read from the PR's file list: one `#` plus five digits). It
is not on `main`, so it is untouched here. Once this lands, `ui-` is
covered: objectstack-ai#20262 must rewrite that site before it lands, or the pin goes
red on its merge ref.
- **Comment lines are untouched.**
`18.ui-list-view-groupbyfield-padded-refused.ts` keeps its `//` comment
citing a number; comment and docblock lines are the sibling card's
surface.
- **Generated projections** (`spec-changes.json`,
`docs/protocol-upgrade-guide.md`) are regenerated, as in stage 1; their
`--check` legs are green.
- **One file beyond the claim's surface:
`packages/spec/src/migrations/migrations.test.ts`.** Its guard on
`ui-notification-action-embed-config-retired` (the entry must keep
explaining its orphaning and name the objectui correction) matched the
two tracker numbers by regex, so the rewrite turned it red. The same two
assertions now match the sentences that carry the lesson (`dual-source
cleanup removed the ./ui copies`, `objectui, which re-exported both
names`); the negative assertion beside them is unchanged. No other test
pins a covered entry's text. A `git grep` of test files for the 24 ids
finds four besides the pin and this one: three
(`plugin-runtime-tier-truthful-text`, `interaction-config-retirement`,
`widget-i18n-retirement`) were run above and pass, and the fourth
(`packages/core`'s `granted-permissions-not-enforced.pin.test.ts`) names
the loading entry's file only inside a failure message.

## Line budget

Entry files: **333 changed lines** (+210 / −123) across 24 files,
against the stage-1 ≈400 budget. The whole diff is **830 lines** (+533 /
−297) in 30 files. Of the rest, `registry.ts` is 333, the two
projections are 52 (`spec-changes.json` 32, the upgrade guide 20), the
widened pin is 85, `migrations.test.ts` is 6 and the changeset is 21.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…(ADR-0049) (objectstack-ai#20398)

Fixes objectstack-ai#20323

Clause-②: yes

## What this does

Retires `action.aria` under ADR-0049 enforce-or-remove, following the
triage direction on the card (RETIRE, on the chart-config precedent
`2bf6ef18d`). No maintainer word reversed it to ENFORCE.

- **Schema.** `ActionSchema.aria` becomes a `retiredKey()` tombstone. It
is a `tsc` error (the input type is `never`) and a parse error that
carries the prescription. The accessible name that IS applied is the
action's required `label`, and the placing node's `aria` block
(`page.components[].aria` or the list view `aria`) names the region.
`AriaPropsSchema` is untouched: this is a key retirement, not a def
retirement.
- **ADR-0087.** The D2 conversion `action-aria-removed` (protocol 18,
`retiredFromLoadPath`) strips the key from `actions[]` and
`objects[].actions[]` as a lossless delete. The D3 semantic entry
`action-aria-retired` is its own family, per the one-entry-per-family
rule. The retired key `ui/Action:aria` is registered under 18, the major
of the chart-config sibling.
- **Ledger.** `liveness/action.json` regrades `aria` from `live` to
`dead`. Its `REMOVED` note records that the uncited 「PARTIAL — honored
by a few objectui renderers」 claim had no reader behind it. The
undrilled-container row `action/aria` goes, and `state-counts.md` is
regenerated.
- **Docs.** Two hand-written pages taught `aria` on an action and are
corrected: `protocol/objectui/actions.mdx` and `widget-contract.mdx`.
The three reference pages that render `ActionSchema` are regenerated.
- **Changeset.** `minor`, BREAKING, with `Clause-②: yes`, the FROM → TO
table and the ADR-0087 `registered` marker.

## Premise, re-measured on this checkout

- **objectui at the `.objectui-sha` pin `f8a9d0fb05`.** A reader grep
for an action's `aria` (`action`, `actionDef`, `def`, `spec`, `btn`,
`a`, `act`, `item` followed by `.aria`) over `packages/**` and `apps/**`
non-test sources hits **0** lines. The control, the same grep for
`.variant`, hits **18** files. Every `schema.aria` reader there is a
placing node: the `record:*` components, `ListView`, `ObjectView` and
`element:button`'s props.
- **Icon-only reversal condition.** Triage named "icon-only actions have
no accessible name" as the condition that would reverse this to ENFORCE.
It does not hold. `action-icon.tsx:243` renders
`aria-label={schema.label || schema.name}`, `action-menu.tsx:341`
renders `aria-label={schema.label || moreActionsLabel}`, and
`action-button.tsx:346` renders `{schema.label}` as the visible text.
- **Framework.** A grep for `.aria` over `packages/**` non-test TS
outside `packages/spec` hits **0** lines. `action.form.ts` has 0 `aria`
rows; its one hit is the `variant` substring.
- **Authors in this repo.** Across `examples/**`, the published
`skills/**` and `packages/**` fixtures, **0** actions author `aria`. The
control is **15** `variant:` lines in `examples/**`.
- **Authors in HotCRM `2f7b2326`.** **0** actions author `aria`. Its 6
`aria:` blocks are all page-level `page.aria`, which stays live. The
control: 7 files under `src/**/actions/` declare `locations:`, 17 times.
- **Served schema.** `metadata-protocol` drops a tombstone's `{ not: {}
}` node from the served JSON Schema, via `stripUnauthorableProperties`.
So the Studio "More fields" form stops offering `aria` once this ships.
- **Pinned sibling.** objectui's `ActionRunner.ts:410` mirrors the key
as `aria?: SpecActionInput['aria']`. A type probe against this branch's
built `dist` compiles that mirror at exit 0, because it evaluates to
`undefined`. An authored block on it is refused with TS2322, and the
control leg without `@ts-expect-error` exits 2. So the Console Pin
Gate's objectui build is not broken by this change.

## Hand-over review (the stopped run's six commits, read hunk by hunk)

| commit | verdict |
| --- | --- |
| `0c5dbbff` sources, ledger, tests, changeset | **kept, with
corrections.** The prescription said "removed in @objectstack/spec 17";
it now says `17.5.0`, the spelling every sibling retirement on this line
uses. The refusal pin in `action.test.ts` now also asserts the
tombstone's own issue kind (`invalid_type` at `aria`, and no root
`unrecognized_keys`, which is what a bare deletion would answer
instead). The changeset's BREAKING sentence now names the replacement.
The HotCRM control is re-measured: 7 files and 17 lines, not "5 action
files". |
| `6334c137` regenerated artifacts | kept, and re-derived after both
merges |
| `bfc86375` the stored-row pin uses a parseable script action | kept |
| `0f20c62f` the `action/aria` undrilled-container row goes | kept;
`check:liveness` is green |
| `8cbcfa46` merge of `main` | kept |
| `90b8fcdc` regenerated state counts | kept; superseded by the
post-merge regeneration |

Added in this round:

- `33407119` and `c38c18ae` merge `origin/main` through
`scripts/pm/os-regen-merge.sh`. Both merges stopped on the two
registries. Each was settled by stacking both sides: main's conversions
and rationale paragraphs first, then this branch's. The merges brought
in objectstack-ai#20262, objectstack-ai#20352, objectstack-ai#20251 and objectstack-ai#20353.
- `2b15085d` and `7faf0e9f` are the deferred regenerations.
- `68b038ef` carries the corrections above, plus two more:
- a new pin that the stored-row seam reaches an `object` row's nested
action (the changeset's second at-rest coordinate);
- one sentence on the `action` row of `liveness/README.md`. That row
said "makes the dead set three", which this change makes false in a
published file (`liveness` is in `@objectstack/spec`'s `files`).
- `8dd3a2ce` regenerates the reference pages for the 17.5.0
prescription.

## Verification

Head `7faf0e9f`, on `origin/main` `15bf186f`:

- `pnpm --filter @objectstack/spec test`: `Test Files 558 passed (558)`
· `Tests 16509 passed | 1 todo (16510)`.
- `pnpm --filter @objectstack/spec test:repo`: `Test Files 35 passed
(35)` · `Tests 634 passed (634)`.
- `pnpm --filter @objectstack/spec check:generated`: all 15 generated
artifacts up to date, measured over a spec build made on this head.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands` derives 114
commands. All 114 exit 0, and `--ran` reconciles them as `114 derived,
114 run, 0 NOT-MEASURED, 0 UNRUN`. The run covers every package's build
closure, rebuilt on this head, so the gates that read `dist` measured
it.

Head `8dd3a2ce`, before the second `main` merge, on `origin/main`
`862b6ce8`. The second merge touched none of these packages' interaction
with this diff; the incoming commits retire other keys and touch no
action surface.

| package | command | Test Files | Tests |
| --- | --- | --- | --- |
| `@objectstack/lint` | `vitest run` | 112 passed | 4640 passed |
| `@objectstack/cli` | `vitest run --project unit` | 231 passed | 3309
passed |
| `@objectstack/runtime` | `vitest run --project local` | 282 passed |
4059 passed, 1 skipped |
| `@objectstack/metadata-protocol` | `vitest run` | 189 passed, 3
skipped | 2736 passed, 19 skipped |
| `@objectstack/metadata-core` | `vitest run` | 16 passed | 285 passed |
| `@objectstack/objectql` | `vitest run --project local` | 322 passed |
5857 passed |

`pnpm --filter @objectstack/spec typecheck` was green at the same head.
The cli `integration` layer is declared to CI: this diff touches no
spawn entry and no integration file.

**Ablation**, from the committed state at `8dd3a2ce`. The blobs of
`action.zod.ts` and of the three pin files are byte-identical at
`7faf0e9f`.

- The mutation goes through `scripts/ablation-replace.mjs`: the anchor `
aria: retiredKey(` becomes ` aria: z.any().optional().describe(`, so the
key is accepted again. The tool reports the anchor going from 1 to 0,
the replacement from 0 to 1, and the blob from `2e0a17b14b06` to
`45e6bd9b1156`. The wrapper also arms a `trap` that restores the file.
- The control leg runs the three pin files on the committed state:
`Tests 166 passed (166)`.
- The mutant leg: `Tests 4 failed | 162 passed (166)`. The four red
tests:
  - `action.test.ts` · refuses an action carrying `aria`;
- `aria-carrier-tombstones.test.ts` · the action tombstone fires and
prescribes;
  - `aria-carrier-tombstones.test.ts` · the object-nested coordinate;
  - `action-aria-removed.test.ts` · the stored-row seam.
- The restore is proven by content, not by an exit code. The file's blob
equals HEAD's blob `2e0a17b14b06`, `git diff HEAD` is empty, and the
porcelain status has 0 lines.
- The pins import `./action.zod` and `../ui/action.zod.js` relatively.
They read `src`, not `dist`, so this ablation has no dist leg.

## Acceptance notes

- **objectui, owned by seat 4 after landing; not in this PR.**
`ActionDefaultInspector` should list `aria` in its `RETIRED_FIELDS`, per
triage note 3. `ActionRunner.ts:410` should drop its `aria?:
SpecActionInput['aria']` mirror, which evaluates to `undefined` once
this ships and still compiles (the probe above).
- **objectstack-ai#19332, owned by seat 4 after landing.** The disposition of
`action.aria` goes to objectstack-ai#19332's item that waits on this card.
- **Advisory lint.** No `lint-liveness-properties` non-warn pin is
added. The `aria` row never carried `authorWarn`, so the advisory lint's
behaviour is unchanged: it was silent before and is silent now.
- **Aliases.** `ActionSchema` never aliased `accessibility` or
`ariaProps` onto `aria`, unlike the chart config. A probe shows both
already refused as `unrecognized_keys`, so no alias refusal pin is owed.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(ui): layout: 'inline' | 'grid' on object-form / FormView parse green and render as vertical — enforce-or-remove (ADR-0049)

3 participants