fix(types): arm AnyComponentSchema for seven registered node-slot renderers (objectui#8499) - #8763
Conversation
…derers (objectui#8499) Nine `type` spellings sat at declared node slots in this repo's own corpora and resolved in no arm of `AnyComponentSchema`. Eight were registered, live renderers with fixtures; the ninth (`my-component`) carries a written exemption. A reader following `content/docs/utilities/runner.mdx` got a document that renders in the browser and is refused by `objectui check` — the direction that trains authors to distrust the validator. `check:doc-types` judges type literals against the renderer registry (656 keys), not against `AnyComponentSchema` (107 arm literals), so the two faces disagreed by construction and nothing compared them at a node slot. Four arms, 47 literals, 107 -> 154: - SemanticElementSchema — the 7 sectioning tags `layout/semantic.tsx` registers - HtmlElementSchema — the 37 safe tags `basic/html-elements.tsx` registers - InputShorthandSchema — `email` / `password`, `form/input.tsx`'s two aliases - UiCalendarSchema — `ui:calendar`, `form/calendar.tsx`'s date primitive Every arm declares only keys its renderer demonstrably reads. `line-chart` is deliberately NOT armed: measured here it is a lazy stub in `apps/console` that `@object-ui/plugin-charts` never fulfils, so an arm would invent a capability rather than name one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…r the seven `LAYOUT_VOCABULARY` in `apps/console/src/__tests__/public-contract.test.ts` read each arm's `type` through `.value` alone. That accessor exists on `z.literal` and not on `z.enum`, so the two enum arms armed for objectui#8499 resolved nothing: 19 arms yielded 17 literals and the file's anti-vacuity case fired, exactly as it was built to. The derivation now switches on zod's own `def.type` discriminator — `.value` for a literal arm, `.options` for an enum arm — rather than trying accessors in turn, so accessor drift still surfaces as an arm resolving nothing instead of being absorbed by a fallback chain. The count assertion moves with it: one enum arm contributes 37 spellings, so literals-equals-arms is no longer the invariant; no-arm-contributes-zero is, and that is what is asserted. Fixing the derivation reveals a second declared contract. The seven sectioning tags have registered `isContainer: true` since objectui#6764, so becoming arms of `LayoutSchema` pulls them into this file's derived `LAYOUT_VOCABULARY` ∩ `isContainer` population, where an uncurated container lands by ABSENCE. They are ledgered in `UNCURATED_LAYOUT_CONTAINERS` against objectui#8775, which holds the decision. None is promoted into `PUBLIC_BLOCKS`: curating an `isContainer` tag makes `react-page.tsx` drop it from the `kind:'react'` JSX scope, so promotion is not a roster edit and is not this card's to take. Housing the arm outside `LayoutSchema` was refused for the opposite reason — it would remove seven real layout containers from the population without changing what they are. Also: the registered-but-unarmed control in `node-slot-registered-arms-8499.test.ts` compared an array with itself plus an element, which passes whatever the schema says. It now runs the real instrument and asserts the source reader fails closed. The `~30 nested slots` figure in the changeset is corrected to the measured 36, and 249 is labelled as the before figure against 257 after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012W3vMLTFY9SPr2LyxhSeYi
…e-slot-unresolved-arms
…erately untouched ⛔ NOT a finished change. The public export `EventHandlersSchema` (`z.record(z.string(), z.function())`) and its barrel line are removed and the knowledge is preserved as a NOTE at the declaration site, per the maintainer ruling on objectui#6124 (decision batch #8, reconfirmed batch #25) recorded on objectui#6910. The third half of that ruling — deleting the parity-ledger row `'base.zod.ts#EventHandlersSchema'` in `packages/types/src/__tests__/zod-mirror-parity.test.ts` — is NOT in this commit. That file is held by the open PR #8763, and this seat's dispatch forbids editing it. So this tree is RED BY CONSTRUCTION: `zod mirror parity — the population is closed > no map entry names a const that no longer exists` fails with `['base.zod.ts#EventHandlersSchema']`. Measured, not predicted. No PR is opened from this commit. It exists so the NOTE and the measurements survive the hand-off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012W3vMLTFY9SPr2LyxhSeYi
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Director seat adoption record — summon #20, Contract review (
|
| Ruling / brief item | Source | Verdict |
|---|---|---|
| Arms only for things that already render at runtime (「运行时已经正确渲染」) | triage 5580341429 | Conforms. All 47 literals resolve to a ComponentRegistry.register site (below). line-chart correctly refused (see (a)). |
my-component is exempt, "8 not 9" |
triage, dispatch 5595407505 | Conforms. Written exemption scripts/check-doc-component-types.mjs:572; not armed. |
Measure family-arm vs slot-constrained shape, declare "everywhere" with Clause-②: yes |
triage §「子问题」 | Conforms. PR body measures one SchemaNodeSchema at every slot (base.zod.ts:263,480-489: nodeUnion is one z.union whose slot 0 is the wrapped component union); a discriminated union dispatches on the literal alone, so (b) is not reachable by adding arms. Declared in both Claims. |
| Do not reopen "arm or no arm" as a decision box | dispatch | Conforms. |
| Consider a registry ⊆ arms comparison instrument ("reminder, not prescription") | triage | Conforms, scoped: node-slot-registered-arms-8499.test.ts compares SemanticElementSchema/HtmlElementSchema literal sets for equality against the tags/TAGS arrays read from semantic.tsx/html-elements.tsx source, with non-vacuity floors (7/37) and a fail-closed reader. Full containment (505 unarmed keys) declined with the number stated. |
File surface: zod/objectql.zod.ts (#8221) and the nine @default JSDoc tags (#8318) untouched; packages/components/src/renderers/** read-only |
dispatch, takeover claim 5596240727 | Conforms. objectql 0 files; no @default line in the layout.ts/form.ts diff; 0 renderer files changed. |
Ceiling FAIL 5596236677 items 1–4: enum-aware LAYOUT_VOCABULARY, no PUBLIC_BLOCKS promotion, ledger the seven with a tracking issue, stop on fork |
ceiling review | Conforms. public-contract.test.ts:468 switches on def.type (literal→.value, enum→.options, else []); anti-vacuity re-stated as "no arm resolves zero"; seven entries :549-555 under SECTIONING_TAG_UNRULED naming #8775 (open). Nothing promoted. |
Ceiling errata (249→257 labelling; two false data-table examples) |
ceiling review | Fixed in body and changeset. |
Ceiling "Also found": MEDIUM (four zod mirrors unreachable by name), LOW-1 (changeset's "only keys its renderer demonstrably reads" false for UiCalendarSchema), LOW-2 (tautological control) |
ceiling review | LOW-2 fixed. MEDIUM and LOW-1 not addressed on 90fcf4f2, and the patch-round dev report (5596533764) neither takes nor declines them. → F1, F2. |
Derived judgments
(a) Arms added to the published AnyComponentSchema union (four arms, 47 literals, 107→154):
| Arm (source schema) | type literals |
Union entry |
|---|---|---|
SemanticElementSchema = BaseSchema.extend(...) — zod/layout.zod.ts:499 |
aside main header nav footer section article (7) |
LayoutSchema :588 |
HtmlElementSchema = BaseSchema.extend(...) — zod/layout.zod.ts:544 |
h1…h6 p a blockquote pre strong em b i u small mark sub sup del ins abbr ul ol li dl dt dd figure figcaption img hr br time address cite q (37) |
LayoutSchema :589 |
InputShorthandSchema = InputSchema.omit({type,inputType}).extend(...) — zod/form.zod.ts:716 |
email password (2) |
FormComponentSchema :805 |
UiCalendarSchema = CalendarSchema.extend(...) — zod/form.zod.ts:753 |
ui:calendar (1) |
FormComponentSchema :806 |
TS twins: layout.ts:942,968 (union :1023-1024); form.ts:1659,1680 (union :1706-1707). LayoutSchema/FormComponentSchema feed AnyComponentSchema (index.zod.ts:436-439).
Seven of the card's nine spellings resolve: footer header main nav (semantic arm), h1 (html arm), password (shorthand arm), ui:calendar. The other two: my-component — the reader's own plugin component, written exemption scripts/check-doc-component-types.mjs:572; no arm can exist. line-chart — apps/console/src/register-plugins.ts:77 registers it only as a registerLazy stub pointing at @object-ui/plugin-charts; that package's src/index.tsx registers bar-chart chart chart:bar pie-chart donut-chart radar-chart scatter-chart (+object-chart) and no line-chart; a git grep over packages/*/src and apps/*/src finds no real registration anywhere (only the stub loop and cli/src/utils/known-schema-types.ts:234). It does not render, so arming it would fail the ruling's criterion; filed #8760 (open) and pinned-with-reason in the test. Correct. Note the family arms also admit 40 spellings the card did not list (email, article section aside, 36 HTML tags) — inside the card's own "arm per family, not per tag" scope paragraph and triage's 「按族」.
(b) Registration parity (each literal is the registered spelling):
semantic.tsx:13const tags = ['aside','main','header','nav','footer','section','article'], registered:46-47withnamespace: 'ui'(bare key +ui:twin) — arm enum identical, same order.html-elements.tsx:45-50TAGS(37), registered:180-181namespace: 'ui'— arm enum identical.input.tsx:94register('email', …),:111register('password', …), bothnamespace: 'ui'; wrappers pininputTypelast, so the arm's omission ofinputTypeis right.calendar.tsx:13register('calendar', …)with:23 namespace: 'ui'and:27 skipFallback: true→Registry.ts:458composesui:calendar,:492skips the bare key. Literal'ui:calendar'is the only spelling that reaches this renderer. Correct.- No schema was invented from nothing: all four derive from
BaseSchema/InputSchema/CalendarSchema. They areexport constin their modules but not re-exported by either barrel (F1).
(c) Union order / discriminator effect on existing arms: the four are appended to two z.discriminatedUnion('type', …) lists; dispatch is by discriminator map, so order cannot change an existing arm's verdict as long as literals are unique — pinned by node-slot-registered-arms-8499.test.ts (new Set(all).size === all.length) and any-component-union-fanout.test.ts:93-94. All four arms are BaseSchema.extend (passthrough, base.zod.ts:449); no .strict() arm added; .omit() in zod 4 keeps the def's catchall. Existing arms' child slots gain acceptances only (the intended widening), never refusals. The runner.mdx:329-330 document { "type": "h1", "children": "Sales Dashboard" } parses: nodeUnionOptions includes z.string() (base.zod.ts:482). Probe not run — no packages/types/dist and no zod install anywhere in this container (searched); the judgment is structural, corroborated by the dev's whole-corpus reading (nodesRedToday 52→52, documentsRedTodayWholeDocument 46→46) and by the ceiling review's independent reproduction.
(d) Spec mirror: @objectstack/spec declares none of the 47 spellings (grep over packages/spec/src/**/*.ts excluding tests: zero hits for ui:calendar, h1, aside, …; email/password occur only as inputType values, component.zod.ts:2325). Spec's PageComponentSchema.type is z.union([PageComponentType, z.string()]) — an open string arm; RESERVED_COMPONENT_TYPE_NAMESPACES (derived from the enum) are page record app nav global ai element, so ui: is not spec-reserved and component-type-unknown will not refuse ui:calendar; ComponentPropsMap has no row for any of them (specCarried still [] in public-contract.test.ts:496). There is no spec node/component union for AnyComponentSchema to be narrower than; the objectui face was already 107 spellings wide against the spec's enum by design. Not a finding.
(e) New exports / keys: four TS interfaces reachable via the ./layout and ./form subpaths of @object-ui/types (exports map) but not from the root . barrel (src/index.ts layout block ends :147, form block :196 — the four are absent; control CalendarSchema :172). Four zod schemas reachable from no published entry: ./zod is the sole zod entry and zod/index.zod.ts lists neither (layout block ends :119, form block :152; control CalendarSchema :144, PageNodeSchema :118). New keys: HtmlElementSchema children href target rel title src alt width height dateTime cite; SemanticElementSchema children; InputShorthandSchema = InputSchema keys − type/inputType + wrapperClass (form.zod.ts:726, read at input.tsx:42; TS InputSchema already declares it, ledger row zod-mirror-parity.test.ts:1991 untouched); UiCalendarSchema = CalendarSchema keys. EXPECTED_MIRROR_PAIRS 158→162 with four MIRRORS/Declared rows.
Semver / changeset
Clause-②: adding union arms widens the accept set and the public type surface ⇒yesmandatory. Both Claims carry it verbatim (5595407505, takeover5596240727— the newest Claim names branchclaude/issue-8499-node-slot-unresolved-arms). Carriers:needs:contract-reviewpresent on card and PR (REST labels read). PR is draft..changeset/8499-node-slot-registered-arms.md:2→'@object-ui/types': minor. AGENTS.md (9. Operational Rules, line 162): a changeset is owed once anysrc/file of a released package moves — 10 files acrosstypes/cli/console, one declaration satisfies; lines 237–242: nevermajor, breaking isminor. Additive API (4 subpath-exported interfaces, 47 accepted literals, two widened union types) gradesminor;patchwould understate a published accept-set widening, as triage pre-warned.Changeset Declaration/Changeset Bump Policy/Changeset Fixed Group Checkgreen. semver_ok: true, with the changeset's one false sentence at F2.- Governed paths: none of the 10 paths matches
GOVERNED_SURFACES(scripts/check-governed-queue-guard.mjs:227-233:docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md). Not governed. The dispatch's "governed package (#0.1)" phrase is the contract-first commandment, not the human-merge surface — the dev's reading (option A) is correct.
Boundary flags
packages/components/src/renderers/**— 0 files (read-only surface respected).content/docs/releases/— untouched.zod/objectql.zod.ts— untouched.@defaultJSDoc — untouched.zod-mirror-parity.test.ts— in surface (correction 5596274383), touched minimally (8 rows + count).PUBLIC_BLOCKS— not widened; seven declared containers ledgered against finding(core/contract): the seven semantic sectioning tags are declared layout containers the curated vocabulary has never ruled on, either way #8775 (open,finding).packages/types/src/registry.ts— untouched (already non-exhaustive).- Follow-ups filed and open: bug(console,docs):
line-chart/area-chart/advanced-chartare lazy stubs@object-ui/plugin-chartsnever fulfils — the dashboard doc teaches one, and it paints OBJUI-001 #8760, bug(schema-catalog): twocomponents-form-labelfixtures authordirection: "column"on aflexnode — the renderer draws a ROW andobjectui checkrefuses the key #8761, finding(types,components):inputTypeauthored on{ type: 'email' }/{ type: 'password' }parses green and is OVERWRITTEN by the registration wrapper — a validated key the runtime discards #8762, finding(core/contract): the seven semantic sectioning tags are declared layout containers the curated vocabulary has never ruled on, either way #8775.
Findings
- F1 (blocking) — The four new schemas are published by the union but unreachable by name from any published entry.
packages/types/src/zod/index.zod.tslayout block (ends :119) and form block (ends :152) omitSemanticElementSchema,HtmlElementSchema,InputShorthandSchema,UiCalendarSchema;packages/types/src/index.ts(:147, :196) omits the four interfaces. Every sibling arm is barrel-exported on both faces (controlsCalendarSchemaindex.zod.ts:144/index.ts:172;PageNodeSchema:118 / :148), and the changeset names the four schemas as the deliverable. The ceiling review raised this as MEDIUM atcc307588; the patch round neither took nor declined it. Remedy: one additive commit on90fcf4f2adding the eight barrel lines plus a reachability pin through a published entry (the parity test imports../zod/form.zod.jsdirectly and cannot see this). No force-push; no re-housing. - F2 (non-blocking, fold into the F1 commit) —
.changeset/…:40"Every arm declares only keys its renderer demonstrably reads" is false forUiCalendarSchema: it inheritsCalendarSchema(form.zod.ts:401-409) includingminDate/maxDate, whichcalendar.tsx:14-19never reads (it readsmode,value,defaultValue,className). Pre-existing debt oncalendar, not new widening — but changesets are the input to centrally written release notes (CLAUDE.md), so the sentence ships unless corrected. - F3 (non-blocking, no action) —
HtmlElementSchema(layout.zod.ts:544-567) declares the per-tag DOM keys on all 37 tags and typeswidth/heightasstring|numberagainst the registration's per-tagnumberinputs (html-elements.tsx:53-71). Reasoned in-file (renderer forwards verbatim;BaseSchemapassthrough already accepted them); narrows nothing, invents no key the renderer drops. - F4 (non-blocking, no action) — The comparison instrument covers the two family arms (5 of the card's 8 rows), not the registry ⊆ arms containment triage floated; 505 registered keys remain unarmed. Triage called it a reminder, not a prescription, and the trade-off is stated with its number. The single node-slot vocabulary (one
SchemaNodeSchema, ~293 slots) is architecture this PR priced but did not create.
CI at read time
Head 90fcf4f2, 33 check runs, latest-per-name: 30 success, 3 skipped (Test (coverage shard ${{ matrix.shard }}/4), Test (coverage), dependabot — template/skip names, not failures), 0 failed, 0 pending. Test (shard 1/4) — red at cc307588 — is success (completed 06:06:01Z); Type Check, Lint, Build & E2E, Build Docs, Doc Component Type Check, README Export Check, Governed Surface Queue Guard all success. PR at read (~06:46Z): mergeable: true, mergeable_state: clean, draft true; the brief's behind at 05:50Z did not reproduce (origin/main has since moved to 6cc48c4e; merge-base with the PR is efbd566d).
Dev report answers. (i) 5595906713 Q1 (per-slot vocabulary card): B stands for this PR; A is a legitimate follow-up programme card seeded by the PR body's measurement — not blocking, not this PR's. Q2 (governed premise): A — confirmed above; governed=false. (ii) 5596533764 deviations: declared type-check/lint/build narrowing is covered by the full-farm CI now green; channel switch and PR-body footer are cosmetic; removing the two false examples rather than replacing them is the honest move. Its silence on MEDIUM/LOW-1 is the gap F1/F2 name. (iii) 5596906444 Q1 (how MEDIUM/LOW-1 land): A — one fresh additive commit on 90fcf4f2 carrying only the two barrel files, the reachability pin and the changeset correction, by the seat holding the newest Claim; never a merge of the competing patch-round implementations, never a force-push. Q2 (why two seats): process observation, outside this review's remit.
Implemented-by: branch claude/issue-8499-node-slot-unresolved-arms
Reviewed-by: director seat summon #20 (isolated fable subagent, transcript-verified before adoption)
{"pr":8763,"repo":"objectui","head":"90fcf4f2cc59277ff555b66ea39377e0d73122c4","verdict":"CHANGES REQUIRED","blocking":["F1"],"clause2":"yes","semver_ok":true,"governed":false,"ci":"33 latest-per-name: 30 success, 3 skipped (coverage template x2, dependabot), 0 failed; Test (shard 1/4) green; mergeable_state clean at read"}
Generated by Claude Code
…e-slot-unresolved-arms
…-tag ledger
The ledger string and its docblock said that curating one of the seven
sectioning tags into PUBLIC_BLOCKS would DELETE that tag from every react
page. Measured, it does not:
- react-page.tsx:75-77 skips EVERY isContainer config
(`if (!tag || cfg.isContainer) continue;`), and these seven have carried
isContainer: true since objectui#6764.
- Simulated promotion of `main`: 46 injected identifiers before, 46 after;
`Main` absent from both; set difference empty.
- react-runtime imports only `react` and `sucrase` (0 registry references,
control: 21 `React` references in the same file), so a lowercase `main`
in a react page is a DOM intrinsic either way.
The docblock the implementer read describes objectui#6764's direction --
declaring containment on an ALREADY-PUBLIC block removes an injected
identifier -- not promotion. The conclusion is unchanged (no tag is promoted,
and promotion is still not a roster edit); only the reason was wrong. What
promotion actually does is widen the AI-authoring vocabulary,
sdui.manifest.json and the intrinsics, and turn the census pin red by design.
Also in this commit:
- The changeset's "every arm declares only keys its renderer demonstrably
reads" is corrected: UiCalendarSchema INHERITS minDate/maxDate from
CalendarSchema and calendar.tsx reads neither (0 occurrences; controls
`mode` 3, `className` 4). Pre-existing debt, stated rather than fixed.
- The movement of the repo's own unexportedNodeSchemas measurement, from
[breadcrumb, object-tree] to 49 node types, is declared knowingly in the
changeset rather than left silent.
- A second accessor path pins the vocabulary derivation against zod's own
propValues.type index, scoped in-comment to what it does and does not
catch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012W3vMLTFY9SPr2LyxhSeYi
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #8499
Head
005409fc1, DRAFT. Every reading below was re-derived on this branch; nothing is inherited from the card'sc90395b2.My own
unresolvedNodeTypesreadingpnpm --filter @object-ui/types build, thenscripts/measure-strict-authoring-face.mjs --json:3fbdd4a2,corpusMatchesMain: true)footerh1headerline-chartmainmy-componentnavpasswordui:calendar— 9, one occurrence eachcc307588)line-chartmy-componentThe population is identical to the card's. Same nine spellings, same counts.
my-componentis exempt by the written carve-out inscripts/check-doc-component-types.mjs;line-chartis the one the card's premise fails for (below). Seven armed.unresolvedRootTypesmoved too, unasked: 28 to 20. The eight that left areui:calendararticlesectionasidefooterheadermainnav—article/section/asidewere root-only occurrences of the same sectioning family, so arming the family cleared them as a side effect rather than by a separate decision.The (a)-vs-(b) shape measurement — I took (a), and (b) is not expressible by adding arms
Triage asked which of two shapes to take and named the fear precisely: "一个扁平 arm 会让
h1在每一个节点槽位合法,包括它明显不该出现的地方".The fear is correct, and the magnitude is: every declared node slot, all of them. Walked from
AnyComponentSchemaon the built face:AccordionItemSchema.content,CarouselItemSchema.content,TabItemSchema.content,ListItemSchema.content,TimelineEventSchema.content,ResizablePanelSchema.content,DetailViewFieldSchema.render,PageNodeRegionSchema.components, …)249unlabelled — it is the before figure, and after is 257. It also filed several ARM-level slots under "nested":HeaderBarSchema.logo/.left/.center/.right/.rightContent/.actionsbelong to an arm of the union, as doDataTableSchema.emptyAction,CardSchema.header/.footer,DialogSchema.footer,SheetSchema.footer,SidebarSchema.footer/.contentandTableSchema.footer. The genuinely nested population is the 36 item/region sub-schemas, and the conclusion is unchanged: one vocabulary, ~293 slots.Every one of those slots is spelled with the SAME
SchemaNodeSchemaobject — the walk tests identity, not shape. There is exactly one node-slot vocabulary in the whole face, andAnyComponentSchemais a discriminated union: it selects its arm from the authoredtypeliteral alone. A slot has no input into that selection. So:DashboardWidgetSchema.componentis spelledBaseSchema, explicitly "BaseSchema, ⛔ NOTSchemaNodeSchema(objectui#8344)", because that slot's accept set had to differ. That one exception took a maintainer ruling (objectstack#8593) for a single slot.DashboardComponentSchema.widgets,FormSchema.fields,TableSchema.columns— are not node slots at all; they are typed by their own schemas and this change does not reach them.⇒ (b) is disproportionate for this card and I say so explicitly, with the measurement. It is a per-slot vocabulary programme, not a narrower way to add an arm.
What (a) newly admits that is absurd
All 47 new literals become legal at all ~280 slots. Concretely, each of these now parses and did not before:
{ "type": "header-bar", "logo": { "type": "footer" } }— afooteras a header's logo;{ "type": "dialog", "trigger": { "type": "hr" } }— a horizontal rule as a dialog trigger;{ "type": "main", "children": [{ "type": "main" }] }— nestedmain, which HTML permits at most one of per document;{ "type": "tooltip", "trigger": { "type": "ui:calendar" } }— a date-picker grid as a tooltip trigger.{ "type": "data-table", "emptyAction": { "type": "main" } }and argued the "already lax" half with{ "type": "data-table", "emptyAction": { "type": "kanban" } }. Both are false as written:data-tablerequiresdataandcolumns, so neither document parses — the first does not parse now, and the second did not parse before. The four above are the ones that genuinely refuse-before and accept-after, and they carry the point on their own.SchemaNodeSchema, one discriminated union, no slot input into arm selection. (a) enlarges an existing hole by 44%; it does not open one. The hole itself is the single node-slot vocabulary, and closing it is the programme above.Accept set, before and after, each acceptance paired with a refusal control
Root and nested (
{ type: 'div', children: [X] }) give the same verdict throughout; the nested column is the one objectui#8344 made meaningful.{ type: 'footer' }/header/main/nav{ type: 'h1' }{ type: 'password' }{ type: 'ui:calendar' }{ type: 'line-chart' }{ type: 'h1ZZ' }{ type: 'stat-card' }(registered nowhere){ type: 'metric-card' }(registered; ruled not an arm){ type: 'my-component' }{ type: 'area-chart' }(console stub, unfulfilled)Value-level controls, so no arm is a
z.any()in disguise — each pair is one green and one red reading on the same key of the same new arm:{ type: 'img', src: 'a.png', width: 100 }{ type: 'img', src: 'a.png', width: true }{ type: 'password', required: true }{ type: 'password', required: 'yes' }{ type: 'ui:calendar', mode: 'single' }{ type: 'ui:calendar', mode: 'agenda' }{ type: 'main', hidden: true }{ type: 'main', hidden: 42 }Nothing that was green went red. The whole-corpus reading says it without relying on my choice of examples:
totals.nodes2103 to 2160 (+57 — the previously unresolved values are now walked as nodes),nodesRedToday52 to 52,documentsRedTodayWholeDocument46 to 46. All 57 newly-walked nodes are green.The two faces, re-measured with
scripts/check-doc-component-types.mjs's ownderiveRegistryKeys:AnyComponentSchemaliteralsaction,crud-dialog,report-builder)That last row is the control that matters for a widening: the 47 literals added are all registered. This change names nothing that does not exist.
Cost, stated rather than buried:
componentTypesNeverSeenwent 13 to 50. 37 of the 47 new literals are not authored anywhere in the corpora — the price of one arm per family instead of one arm per defect. The card's own repair-scope paragraph asks for exactly that ("An arm per family, not per tag"), and the alternative leavesh2diverging identically tomorrow.The premise fails for one of the eight:
line-chartis NOT a live rendererThe card says "Every one except
my-componentis a REGISTERED, LIVE renderer" and justifiesh1, the sectioning family andui:calendarby name. It does not justifyline-chart, and measured here that claim is false for it.apps/console/src/register-plugins.ts:77registers ten chart variants as lazy stubs pointing at@object-ui/plugin-charts. That package registers eight keys —bar-chartchartchart:bardonut-chartobject-chartpie-chartradar-chartscatter-chart— andline-chartis not among them.Registry.loadLazy's own docblock: "Resolves once the loader completes (whether or not the loaded module actually registered the expected type)". So the key is KNOWN tocheck:doc-typesand resolves to nothing at render time. The repository already recorded the same reading in passing —scripts/check-doc-component-types.mjscalls a sibling defect "the same failure as theline-chartwidget objectui#7896 recorded inpackages/plugin-dashboard/README.md".Triage's ruling admits arms only for things that 「运行时已经正确渲染」.
line-chartdoes not render, so an arm would invent a capability rather than name one — outside what was ruled. It is filed instead: #8760. The absence is pinned WITH its reason innode-slot-registered-arms-8499.test.ts, reading both halves from source, so registering the key for real turns this red and the arm becomes owed.The comparison instrument — what I built, and the trade-off
Triage's reminder: without a
renderer registry ⊆ node-slot-resolvablecomparison, the next registered renderer diverges the same way.The full containment instrument is disproportionate here, and this is the number that decides it: 505 registered keys still have no arm (552 before). A containment gate needs a 505-entry ledger on day one — an artifact this card is not authorised to mint, and one that would bury the signal it exists to give. It would also need
packages/types/dist, which breaks the no-build property that letscheck:doc-typesrun as a cheap per-PR check.What I built instead is the same comparison, scoped to the two families this card armed.
node-slot-registered-arms-8499.test.tsreadspackages/components/src/renderers/layout/semantic.tsxand.../basic/html-elements.tsxfrom source and asserts each arm's literal set equals the array the registration site loops over — plus a floor assertion on each read so a moved declaration reads as a failure rather than a vacuous pass, and a firing control that a registered-but-unarmed tag is detected. Adding a tag to either array without an arm now goes red. That is not the whole containment, and I am not claiming it is; it closes the two families that produced 5 of this card's 8 rows, at zero ledger cost.Ablation — the pin fails when the arms are removed
One-off, restored,
trap-guarded. The four arms were removed from their unions inzod/layout.zod.ts/zod/form.zod.ts(declarations left in place, so the exports still resolve and only the UNION loses them). No rebuild was performed, which is also the proof that the pin resolves package SOURCE rather than a staledist/.The 9 that stayed green under ablation are: the
UNREGISTEREDrefusals, theline-chartabsence legs, and the three declaration-vs-registration legs.SemanticElementSchema/HtmlElementSchemadirectly, so they survive removing those arms fromAnyComponentSchemaby design — the property they hold is arm-vs-registration parity, not union membership. They are not controls for the union; theUNREGISTEREDrefusals are. The test file now says so in its own docblock, and the leg that was labelled a firing control has been rebuilt (below), because it was comparing an array with itself plus an element and would have passed whatever the schema said.Restore leg:
git checkout HEAD -- ..., thengit diff HEADempty (verified, not inferred from an exit code), then re-runRESTORED_EXIT=0,Tests 26 passed (26).Gates
cc307588run. It is kept because the ceiling review reproduced it, but it is NOT the current head — the patch-round table at the bottom is. All from the repository root, exit codes captured before any pipe.pnpm --filter @object-ui/types type-checktsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json— the third is where the compile-time mirror-parity ledger reconcilespnpm --filter @object-ui/cli type-checkpnpm exec vitest run packages/types packages/cli packages/core packages/react examples/schema-catalogpnpm exec turbo run lint --concurrency=2node scripts/check-changeset-presence.mjs90fcf4f2after the merge. 1 changeset throughoutnode scripts/check-changeset-no-major.mjspnpm check:control-bytespnpm check:doc-typespnpm checklint.yml:481. Ran against a builtpackages/cli/dist/cli.js; 628 files analysed, 3 candidates remain, none of them this card'spnpm check:readme-exportsnot on disk -- run pnpm build firstand three floor misses — PRECONDITION NOT MET, not a red. Zero complaints namedpackages/types/README. Green after a full buildpnpm check:spec-symbolsnode scripts/check-type-check-coverage.mjspnpm check:sdui-registration-pinspnpm check:published-distpnpm check:node-esm-load@object-ui/authand@object-ui/react-runtimefrom a SIBLING worktree's cache (/home/user/objectui-review-8723/...); the gate names its own remedy,--force-build.@object-ui/typeswas built by this tree and is in the 32 entries that imported and evaluated cleanlypnpm exec turbo run build --filter=!@object-ui/site --concurrency=2Self-scan beyond
check:control-bytes, as the standing clause requires:grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over all 9 changed files — no hits (grep exit 1).scripts/check-governed-queue-guard.mjs --testover the 9 paths: NOT GOVERNED — none of the 5 governed surfaces matched. The dispatch's phrase "packages/typesis a governed package (AGENTS.md #0.1)" is the CONTRACT-FIRST commandment, not the human-merge governed surface; what #0.1 forces on this change is that the repair lands on the declaration rather than as a tolerant read, which it does — no consumer gained a fallback, no renderer changed, and the four arms declare only keys their renderers demonstrably read. This PR stays draft regardless, per the dispatch and the standing 「无席内契约复审档 PASS 在案 ⛔ 禁止入队」.Changeset
.changeset/8499-node-slot-registered-arms.md,'@object-ui/types': minor.Graded against
AGENTS.md: the rule is the paragraph at 9. Operational Rules — a changeset is required once any file under a released package'ssrc/moves, and "别再按「feature 要写、bug 修复不用」来判断". Seven such files moved across two released packages, so it is owed, and one declaration satisfies it. The level isminorby the objectui major/minor paragraph: "changeset 里不要声明major" and "objectui 自身的破坏性变更也标minor", mechanically enforced bycheck-changeset-no-major.mjs(run, exit 0).patchwould be wrong here even though the card is typed Bug — this is a published accept-set widening, which triage flagged in advance ("承接者写 changeset 时仍应按公开面扩张处理级别").File-surface collisions
Neither hard collision fired.
sortclauseconvertSortToQueryParamshonours is declared by none of the sevensortinputs — every one istype: 'array', so the html tier answerstype-mismatch#8221 holdstypes/src/zod/objectql.zod.tsamong others. Not touched — this change lands inzod/layout.zod.tsandzod/form.zod.ts, which the dispatch confirms are free since fix(types): strip the imported defaults at the spec import boundary — this mirror authors no default, imported subschemas included (objectui#8317) #8721 merged.@defaultthat no registered renderer reads, and only 8 of 41 are pinned against the renderer #8318 holds the JSDoc@defaulttags of 9 keys. Not touched.CardSchema,PageNodeSchema) live inpackages/types/src/layout.ts, which this PR also edits — but in a different region (two new interfaces appended before the union) and on no@defaulttag. Textual merge conflict is unlikely; it is reported rather than assumed away.packages/types/src/registry.tswas deliberately NOT touched:SchemaRegistryis already non-exhaustive (71 keys against 107 arms before this change, 37 arms unlisted), so adding 47 tag rows would have been noise rather than a completeness repair.Collateral: two tests whose EXAMPLES this card retired
Both had chosen
h1-family types precisely because they were unarmed. Neither assertion was weakened; each was re-pointed at an input whose absence from the union is ruled rather than merely pending, so it cannot rot the same way.packages/types/src/__tests__/node-recursion-point-8344.test.ts— itsUNMIRROREDfixture was{ type: 'h1' }, with a docblock reading "⛔ Do not 'fix' this by adding anh1arm to make some other test green: that is the public-surface widening objectui#8344 routes into its own card." This IS that card, and the widening landed by ruling. Now{ type: 'metric-card' }, whose exclusionzod/complex.zod.tsrecords as "DELIBERATELY not an arm ofAnyComponentSchema" under objectstack#8593.packages/cli/src/__tests__/check-validity-recogniser.test.ts— its "registered but not modelled" fixture wasabbr, chosen on the reasoning that "the raw HTML primitives are the stable inhabitants of this bucket". They were not. Nowmetric-card, with the dated census re-measured in place: 658/102/558 becomes 656/154/505.Out-of-scope findings, filed
Searched first. The repo-scoped REST search endpoint returns 403 in this container (
sessions are bound to their configured re…), so the duplicate check switched to one targeted MCPsearch_issues— declared here as a channel switch. It returned 56 semantically related plugin-charts/types findings and none matching, so it is a live reader rather than a blind one.line-chart/area-chart/advanced-chartare lazy stubs@object-ui/plugin-chartsnever fulfils — the dashboard doc teaches one, and it paints OBJUI-001 #8760 —line-chart/area-chart/advanced-chartare lazy stubs@object-ui/plugin-chartsnever fulfils, andcontent/docs/plugins/plugin-dashboard.mdx:210teaches one.components-form-labelfixtures authordirection: "column"on aflexnode — the renderer draws a ROW andobjectui checkrefuses the key #8761 — twocomponents-form-labelfixtures authordirection: "column"on aflexnode;flex.tsxmaps onlycol/col-reverse, so the fixture draws a ROW andobjectui checkrefuses the key.inputTypeauthored on{ type: 'email' }/{ type: 'password' }parses green and is OVERWRITTEN by the registration wrapper — a validated key the runtime discards #8762 —inputTypeauthored on{ type: 'email' }/{ type: 'password' }parses green and is overwritten by the registration wrapper. Its repair is an accept-set NARROWING, deliberately kept off this PR so this change moves the accept set in one direction only.NOT MEASURED
Build Docs. Not run. It is one of the three the dispatch names as not covering the PR head.check:node-esm-loadunder clean provenance. Its exit 1 is a shared-turbo-cache condition, diagnosed but not cleared — clearing it needs--force-build, a second full build for a gate that does not gate this PR.catalog-gallery-render.test.tsx's OBJUI-001 assertion, not on my own screenshot.componentTypesNeverSeenandinputType-census numbers are this repository's corpora only. hotcrm and the objectstack corpus were not read.corpusMatchesMainon the AFTER run readsfalse— expected and not a defect:packages/types/srcis one of the measurement script'sCORPUS_PATHS, and this branch edits it. The BEFORE run readtrueat3fbdd4a2, so the before/after pair is anchored.types/cli/core/react/schema-catalog. Their suites were not run. The fullturbo run lintandturbo run buildcovered every package; the test sweep was scoped to the consumers ofAnyComponentSchema/safeValidateSchemathat carry assertions about it.CalendarSchema(barecalendar) mirrors the right renderer. Noticed while armingui:calendar:CalendarSchema's key set is the ui date-picker primitive's, while barecalendarresolves to plugin-calendar's CRUD view. Not verified againstObjectCalendarRenderer's read sites, so it is recorded as an observation and NOT filed.Patch round — what the ceiling FAIL (
5596236677) asked forRun by the
os-devseat in sessionsession_012W3vMLTFY9SPr2LyxhSeYi, on the same branch and the same PR — a patch round, not a re-dispatch.1. The green→red:
LAYOUT_VOCABULARYcould not read an enum armapps/console/src/__tests__/public-contract.test.tsderived the layout vocabulary asLayoutSchema.options.map(arm => arm.shape?.type?.value)..valueexists onz.literaland not onz.enum, so the two arms this PR added resolved nothing: 19 arms yielded 17 literals and the file's own anti-vacuity case fired — the gate working, not failing.The derivation now switches on zod's own
def.typediscriminator —.valuefor a literal arm,.optionsfor an enum arm — rather than trying accessors in turn. ⛔ A.value-else-.options-else-… fallback chain was deliberately NOT written: it would absorb exactly the accessor drift the anti-vacuity case exists to report. An arm whose declaration is neither shape resolves nothing, and that is still a failure.⛔ The vocabulary is not hand-restated — that is the defect the file exists to catch. What changed is the count invariant, because it had to: one enum arm contributes 37 spellings, so literals equals arms is no longer meaningful. No arm contributes zero is, and that is what is asserted:
The identity case also now pins one spelling out of each enum arm (
main,h1) alongsidebox/flex/container, so a reader that silently stopped resolving enums cannot pass it either.apps/console/src/__tests__/public-contract.test.tscc307588(before)Tests 1 failed | 18 passed (19);expected [...] to have a length of 19 but got 17at:50190fcf4f2(after)Tests 19 passed (19)2. The second contract: seven declared layout containers, LEDGERED — none promoted
Fixing the derivation reveals what the review predicted. Measured on this head:
LayoutSchemaarmsLAYOUT_VOCABULARY ∩ isContaineraspect-ratio)The seven new members are exactly the sectioning tags —
articleasidefooterheadermainnavsection. No HTML tag fromHtmlElementSchemadeclares containment, so the population grows by seven and not by forty-four.Disposition taken: ledgered in
UNCURATED_LAYOUT_CONTAINERS, tracked by objectui#8775. ⛔ Nothing was promoted intoPUBLIC_BLOCKS, ⛔ nothing was weakened, skipped or deleted.⛔ The re-housing option was refused, deliberately. Moving
SemanticElementSchemaout ofLayoutSchemawould have gone green just as cheaply and cost no accept-set change — but the seven have declaredisContainer: truesince objectui#6764 and are registeredcategory: 'layout'. Re-housing removes seven genuine layout containers from the forcing function's population without changing anything about what they are: a gate that stops looking, not a gate that passes. Ledgering is that file's own reviewable alternative, and it is what the mechanism was built for — they arrived by absence, exactly as designed.⭐ Why promotion is not this card's to take — the merits, restated in patch round 2 because the reason first published here was FALSE. ⛔ This paragraph used to say that curating one of the seven would delete that tag from every react page. That is refuted at the mechanism, and the refutation is reproduced in patch round 2 below:
react-page.tsx:75-77skips every container config, these seven have carriedisContainer: truesince objectui#6764, and simulating the promotion ofmainleaves the injected-identifier count at 46 before, 46 after withMainabsent from both. What promotion actually does is widen the AI-authoring vocabulary,sdui.manifest.jsonand the generated intrinsics, and turn thecontainer-declaration-census.test.tsxpin red by design — a deliberate re-opening, which is whatsemantic.tsxmeans by "re-opens the question HERE". ⭐ The disposition does not move: nothing is promoted, and promotion is still not a roster edit — it is a public-contract decision, and objectui#8775 carries it.The forcing function is satisfied, and proven load-bearing — one ledger entry ablated, restored:
The entry deleted was one of the seven, and the case that fired is the forcing function itself, by ABSENCE.
3. The tautological control, rebuilt
node-slot-registered-arms-8499.test.ts's "detects a tag that is registered and unarmed" compared[...armed]against[...armed, 'hgroup']— set algebra that passes whatever the schema says, never touching a registration. It now runs the real instrument on both halves: a genuine mismatch read from a real registration site (html-elements.tsx's 37 tags againstSemanticElementSchema's 7), and a proof that the source reader fails closed — a declaration name that is not in the file throws rather than quietly yielding[], which is what makes thetoBe(7)/toBe(37)non-vacuity guards real guards.4. Gates at head
90fcf4f2origin/mainmerged once before pushing (efbd566d2, clean, no conflicts; #8756/#8766 and others landed). ⛔scripts/pm/dispatch-gates.mjscannot serve objectui paths — it refuses cross-repo by design (exit 2, naming both repos), so this list is hand-derived from objectui's ownpackage.jsonscripts and.github/workflows/.pnpm exec vitest run packages/types packages/cli apps/consoleapps/consoleis in scope this time — not running it is how the green→red reached the ceiling reviewpnpm exec vitest run apps/console/src/__tests__/public-contract.test.tspnpm exec turbo run type-check lint --filter=@object-ui/types --filter=@object-ui/cli --filter=@object-ui/consoleno-explicit-anyinpackages/types, none from this diffpnpm exec turbo run build --filter=@object-ui/console --filter=@object-ui/clinode scripts/check-changeset-presence.mjspnpm checklint.yml:481, against a built clipnpm check:control-bytespnpm check:doc-typespnpm check:spec-symbolspnpm check:sdui-registration-pinspnpm check:readme-exports@object-ui/plugin-ai … not on disk — run pnpm build first, zero naming this diff = PRECONDITION NOT MET. Green after building that package: 0 unbuiltpnpm check:vi-mock-specifierspnpm check:shell-escape-residueDeclared narrowing.
type-check/lint/buildwere filtered totypes+cli+consolerather than run farm-wide, and packages outside those three had their suites left to CI. This is a declared deviation, not an omission: the patch round's own diff is four files in two of those packages plus a changeset, and CI runs the full farm.Self-scan beyond
check:control-bytes:grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the four changed files — no hits (grep exit 1).Files changed this round (4):
apps/console/src/__tests__/public-contract.test.ts·packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts·packages/types/src/zod/layout.zod.ts(docblock only — one false absurd-slot example swapped) ·.changeset/8499-node-slot-registered-arms.md(the 249/36 erratum). ⛔packages/components/src/renderers/**untouched — verified, 0 files.⛔ Still draft, ⛔ not enqueued, ⛔
needs:contract-reviewuntouched — it stays on until a fresh ceiling PASS on this moved head.Patch round 2 — the ceiling re-review FAIL (
5597738741), three itemsRound 2 was implemented by the
domain:specobjectui seat in sessionsession_012W3vMLTFY9SPr2LyxhSeYi, as commit005409fc1; that commit's ownClaude-Sessiontrailer carries the same reference. (The footers at the foot of this body name the round-1 seat and are left alone — a PR body normalises footers on every edit, so attribution that has to survive is written here, in prose.)That review passed everything it re-derived — the enum-aware derivation, the generalized invariant (proved equivalent, not weakened), the ledger disposition, and the re-housing refusal (which it found more correct than the dispatch had allowed). ⛔ None of that is re-litigated here. Three items failed; all three are answered in one commit,
005409fc1.1 · A false mechanism was baked into SHIPPED text — replaced in four places
The claim "curating a sectioning tag into
PUBLIC_BLOCKSwould DELETE that tag from every react page" is refuted. Re-measured here rather than taken on the review's word:react-page.tsx:75-77if (!tag || cfg.isContainer) continue;— skips every container config, whatever list it came fromgetMeta('main').isContainer/getConfig('main').isContainertrue/true— the flag is already theremainpromoted (simulated: pushmainontoPUBLIC_BLOCKS, re-run the scope loop)Mainin either set[]{"type":"main","isContainer":true}— so it is skipped on the same line an unlisted tag never reachesComponentRegistryreferences inpackages/react-runtime/srcReactreferences in the same file; its only imports arereactandsucrase)⇒ a lowercase
mainin a react page is a DOM intrinsic before and after. The docblock the first round read describes objectui#6764's direction — declaring containment on an already public block removes an injected identifier — and reading it forwards is how the inversion happened.⭐ The conclusion does not change: no tag is promoted, and "promotion is not a roster edit" survives. Only the reason was wrong. Replaced in all four places it lived:
SECTIONING_TAG_UNRULED(apps/console/src/__tests__/public-contract.test.ts) — now states what promotion does move (vocabulary /sdui.manifest.json/ intrinsics, and the census pin red by design) and names the refuted claim as an earlier revision's error;UNCURATED_LAYOUT_CONTAINERSdocblock in the same file — carries the cost of a ledger entry once, so no entry re-argues it, including the measured 46/46;2 ·
unexportedNodeSchemas— option (b), declared in the changesetThe four new zod schemas are unreachable by name from
@object-ui/types/zod(the barrel uses explicit export lists; control:CalendarSchemais present, the four are not). Re-measured withscripts/measure-strict-authoring-face.mjs --jsonafter building@object-ui/types: 49 node types, up from[breadcrumb, object-tree]— the same 2, plus this change's 47 new literals.Taken: (b) — leave them unexported and declare the movement knowingly. Three reasons, in the order they became decisive:
b7af6b52returns HTTP 422, "No commit found for SHA" from/repos/objectstack-ai/objectui/commits/b7af6b52. Positive controls on the same endpoint and the same 8-char short form:90fcf4f2→ 200,fb010227→ 200. So the 422 is a real absence, not a channel artifact.packages/types/src/zod/index.zod.tsis one of the three files in PR feat(types): re-export BreadcrumbSchema and ObjectTreeSchema from the zod barrel (objectui#7917) #8777 (objectui#7917, open and non-draft, heada5eaf49c) — the PR whose entire purpose is that barrel's export list. Editing it here is outside this PR's 9-file surface and lands on top of another seat's in-flight work.SemanticElementSchema/HtmlElementSchemapublishes a named authoring surface —z.enumfamilies, not per-tag schemas — that no consumer asked for and that no ruling covers.The changeset now says all of this, including the honest asymmetry: the 2 were data blocks a consumer had a standing reason to validate alone (which is why objectui#7917 exists), while the 47 are HTML primitives and two input aliases with no per-tag consumer.⚠️ And the interaction is stated rather than hidden: if PR #8777 lands first, the same movement reads
0to47.3 · The false changeset sentence, corrected
"Every arm declares only keys its renderer demonstrably reads" is untrue for
UiCalendarSchema. Re-measured inpackages/components/src/renderers/form/calendar.tsx:minDate0,maxDate0,onChange0 occurrences; controlsmode3,className4. The renderer readsmode,value,defaultValue,classNameand nothing else.The sentence now scopes itself to the arms this change authors, and states the inherited exception:
UiCalendarSchemaextendsCalendarSchema, so it carriesminDate/maxDate. ⛔ No schema was changed to make the sentence true — that is pre-existing debt onCalendarSchemaand a different card's accept-set movement. (onChangeis ahandlerKeyRefusal, i.e. declared-and-unwritable, so it is a named refusal rather than an unread authorable key — a different category, noted so the correction is not over-read.)4 · The optional strengthening — taken
The review's blind spot: dropping a single non-container spelling (
q) from the 37-enum left this file green.LAYOUT_VOCABULARYis now cross-checked againstLayoutSchema._zod.propValues.type— zod's own discriminator index, a second accessor path over the same union. Both read 61 literals over 19 arms and agree exactly (derived-only: [],propValues-only: []).Ablation, isolating the new assertion (the mutation is in the test file itself, so no build step is involved):
Leg B is the one that matters: exactly one case fails, and it is the new one. Both legs ran under
trap restore EXIT INT TERMwith absolute paths, and both restores were verified by an emptygit diff HEAD, ⛔ never by an exit code.5 · Gates at head
005409fc1origin/mainmerged once before pushing (5591f03bd; #8776, #8780, #8781, #8746, #8779 and others landed — clean, no conflicts). ⛔scripts/pm/dispatch-gates.mjscannot serve objectui paths (exit 2, cross-repo by design), so this list stays hand-derived from objectui'spackage.jsonand.github/workflows/. Every exit code below was captured ascmd > file 2>&1; EXIT=$?— ⛔ never through a pipe.pnpm exec vitest run apps/console/.../public-contract.test.ts packages/types/ packages/cli/.../check-validity-recogniser.test.ts packages/components/.../container-declaration-census.test.tsxpnpm build --concurrency=2pnpm --filter @object-ui/console run type-checktsc --noEmit --listFileslistspublic-contract.test.ts(1 hit of 3715 files), so this is not a suite that excludes*.test.tspnpm --filter @object-ui/console run lintnode scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjsnode scripts/check-changeset-overwrite.mjspnpm check:sdui-registration-pinspnpm check:readme-exports@object-ui/plugin-ainot on disk); green after the build — 0 unbuiltpnpm check:control-bytesDeclared narrowing. Farm-wide
turbo run lint/type-check/testwere not run; the affected package's ownlintandtype-checkwere run in full, and the rest is CI's. Evidence this narrowing measures rather than skips: the lint population is 198 files read from eslint's own--format jsonoutput (not estimated), the edited file is in it at 0/0, and type-aware linting is not enabled ineslint.config.js(zeroprojectService/project: true/tsconfigRootDiroccurrences) — so this diff cannot move the verdict on any file it did not touch.--no-inline-configto the console's eslint run surfaces 3react-hooks/static-componentserrors insrc/pages/settings/. They are not in this diff and not a regression —--no-inline-configis thelint:rootspelling, and the package's reallintscript (eslint .) is exit 0.Self-scan beyond
check:control-bytes:grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over both changed files — no hits (grep exit 1).Files changed in round 2 (2):
apps/console/src/__tests__/public-contract.test.ts·.changeset/8499-node-slot-registered-arms.md. ⛔packages/components/src/renderers/**untouched (0 files) — the react-page and census readings above are reads, not edits. ⛔packages/types/src/zod/index.zod.tsuntouched, and deliberately so (item 2).⛔ Still draft, ⛔ not enqueued, ⛔
needs:contract-reviewuntouched — a delta re-review runs first.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Generated by Claude Code
Generated by Claude Code