feat(types,cli,core,sdui-parser): declare per-type node slots once and walk them in the gate, core validation and the SDUI parser (objectui#11170) - #11637
Conversation
…d walk them in all three readers (objectui#11170) `NODE_SLOT_DECLARATIONS` / `nodeSlotsFor` on `@object-ui/types`, beside `BaseSchema.children`: the keys other than `children` through which a renderer hands authored nodes back to `SchemaRenderer`, per registry type, spelled as key paths (`trigger`, `items[].content`, `regions[].components`). The `page:*` rows are the spec's `pageComponentSlotPositions()` by reference, pinned both ways; every other row is pinned against the live renderer. `body` stays retired and enters only on the four `page:*` types measured still painting it. The `objectui check` gate, core's `validateChildren` and the SDUI parser (via `ManifestComponent.slots`, projected by `manifestFromConfigs`'s `slotsFor`) walk the declaration; none keeps a list of its own. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ
✅ 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
|
Contract reviewServed-tier: At-tier review for the The merge. Check-runs on the head, six readings. 2026-10-05T02:50Z: 44 runs, 27 success, 3 skipped, 14 in progress (the eight test shards, Type Check, Lint, Build Docs, Doc Snippet Type Check, Bundle Analysis, Spec Main Shape Gate). 2026-10-05T02:54Z: 8 in progress, 0 failures. 2026-10-05T02:58Z: 7 in progress, 0 failures. 2026-10-05T02:59Z: 1 in progress (shard 1); Spec Main Shape Gate completed ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11170
Clause-②: yes (narrowing)
One published declaration of the per-type NODE SLOTS on
@object-ui/types, besideBaseSchema.children, and three readers that walk it: theobjectui checkunevaluated-expression gate (findUnbindableTextExpressions), core'svalidateChildrenand the SDUI parser'svalidateTree. None keeps a slot list of its own.Premise readings, on
mainat9dfaca65The slot list, re-measured. Every renderer read that hands authored nodes back to
SchemaRendererthrough a key other thanchildren— the census grep forrenderChildren(,renderNodeSlot(,renderTriggerSlot(and a directSchemaRenderer schema=acrosspackages/components, everyplugin-*,layout,reactandapp-shell— gives, per registry key:alert-dialogtrigger/content;context-menutrigger;dialogtrigger/content/footer;drawertrigger/content;dropdown-menutrigger;hover-cardtrigger/content;popovertrigger/content;sheettrigger/content/footer;tooltiptriggeronly (itscontentis placed RAW in a React child position, text only — objectui#10295);collapsibletrigger/content;cardheader/footer;tablefooter;data-tableemptyAction;emptyaction;header-baractions/rightContent;tabsitems[].content;accordionitems[].content;listitems[].content;resizablepanels[].content;carouselitems[](the renderer hands EACH item torenderChildren);page/app/utility/home/record, onePageRenderer):regions[].components;page:*family:page:tabsandpage:accordionitems[].children;page:cardfooterand the retiredbody(schema.body ?? schema.children);page:section,page:footer,page:sidebarthe retiredbody(children || body).page:headerreads no node slot (itsactionsare action ids);detail/detail-view(oneDetailView)header,footer,actions,tabs[].content,sections[].fields[].render,fields[].render;detail-sectionfields[].render;report-viewerreport.sections[].content;plugin-timeline:timelineitems[].content(the baretimelineisview:timeline, no slot);view-switcherviews[].schema;dashboardwidgets[].component.The card's list was a starting point and is superseded:
page:card'sbodyis a retired read, not a live slot;carouselhas notrigger/header/footerand readsitems[]directly;panels[].contentisresizable's, not a tab's; and twenty-odd further positions were not on it.The spec export.
@objectstack/spec17.6.0's./uisubpath exportspageComponentSlotPositions()andPageComponentSlotPosition({ key, panelKey?, retired }): derived from theComponentPropsMaprows that declare a slot, it answers four positions —children,body(retired),footer,items[].children— by SHAPE over the wholepage:*family, not per type. It exists identically in objectstackmain(component.zod.ts). Nothing in the installed spec declares a slot for any non-page type, andCOMPONENT_CHILD_KEYS/mapComponentTreeare not on any exported subpath.The registrations. Thirty-five
type: 'slot'inputs other thanchildrenexist (the overlays'trigger/content/footer,collapsible,empty.action,page:card.footer), andComponentInputhas no vocabulary for a nested position:items[].content,regions[].components,sections[].fields[].rendercannot be declared as inputs.isKnownSchemaTypeis a generated snapshot from the registration calls because the CLI cannot ask the registry (the header ofscripts/regenerate-known-schema-types.mjs).The three walks, as they stood. The gate:
visitjudged the node then followednode.children(array or single node). Core:validateChildrenrecursed throughschema.childrenonly. The parser:visitdescendednode.childrenonly;CHILD_LIST_KEYischildren, andRETIRED_CHILD_LIST_KEY(body) is answered by name in the prop walk.Derivation route: (c), with (a) by reference for the page family
page:*rows — they are placed on the types whose renderer reads each position, andnode-slots-11170.test.tsholds them againstpageComponentSlotPositions()in both directions (every row position is a spec position with the spec's retirement flag; every non-childrenspec position is placed on at least onepage:*type).card.header,table.footer,data-table.emptyAction,header-bar.actions, every plugin slot). The registrations are instead one PIN direction: every registered slot input has a row (node-slot-declaration-11170.test.tsx, direction 2).NODE_SLOT_DECLARATIONSinpackages/types/src/node-slots.ts, one row per renderer under every registry spelling that resolves to it, positions spelled as key paths (trigger,items[].content,items[],regions[].components,report.sections[].content),nodeSlotsFor(type)the verbatim lookup,nodeSlotValues(node, path)the one walk. Pinned both ways against the renderers (below).bodyis never a generic key: it enters only on the fourpage:*types measured reading it, markedretired; the parser's refusals of the spelling are verbatim.The readers
packages/cli): afterchildren,visitwalksnodeSlotsFor(node.type)throughnodeSlotValues; a retired position is walked too (the renderer paints it). Path printed as the command spells it:items → 0 → content → value.validateChildrenrecurses through the declared positions afterchildren, path spelledschema.items[0].content.ManifestComponent.slots?: readonly string[], projected bymanifestFromConfigs(configs, { slotsFor: nodeSlotsFor })— non-retired positions only, as the spec's authoring walks skip tombstoned spellings — andvalidateTreedescends them with a dependency-free walk of the same grammar. Producers wired:getJsxManifestinpage.tsx(kind:'html'pages),packages/sdui-parser/scripts/gen-manifest.ts(the shippedsdui.manifest.json),apps/console/dev/manifest-dump.tsx. A manifest built without the option serialises byte-identically.@object-ui/typesjoins the parser's devDependencies for the generator script.Before/after over the repository's own schemas (
node packages/cli/dist/cli.js checkat the root, the CLI built frommainand then from this branch;validateSchemaandfindUnbindableTextExpressionsover the 479 JSON documents with a string roottypeunderexamples/,apps/,packages/,content/,docs/, and over the 237 JSON fences incontent/docs,docs, package READMEs that parse as node documents): identical readings —287 validated, 146 recognised but not validated, 0 did not validate, 0 refusals, 0 warnings before and after; core 0 findings before and after; fences 0 and 0. No example needed a fix, and no false refusal appeared. The parser's in-repo authored corpus is the onekind:'html'preview source inapps/console/src/sdui-tiers-preview.tsx; the console html-tier suites pass on the merged head.Tests and ablations
packages/types/src/__tests__/node-slots-11170.test.ts— the table's shape, the grammar and the walk, thepage:*rows ⇔pageComponentSlotPositions()both ways.packages/components/src/renderers/__tests__/node-slot-declaration-11170.test.tsx— over the live registry: (1) a node authored at every declared position of every key registered here reaches the DOM throughSchemaRenderer, in the context the renderer needs; (2) every registeredslotinput is a declared position; (3) registry twins answer one list and every listed key is registered; (4) the projected manifest carries the positions andvalidateTree's reach equalsnodeSlotValues' at every position. Controls: a key with no row renderschildrenand not an undeclared key.nodeSlotDeclaration-11170.test.tsxinplugin-detail,plugin-report,plugin-timeline,plugin-view,plugin-dashboard— the plugin rows the same way, each asserting the row lists exactly the positions it exercises.check-unbindable-text-expression-4795.test.ts(new block; the ablation-2 rowsfields[],columns[],{ "type": "multiple" }stay verbatim and green beside it),packages/core/src/validation/__tests__/node-slot-walk-11170.test.ts,packages/sdui-parser/src/__tests__/node-slot-walk-11170.test.ts.contentfrom thedialogrow (committed tree,node scripts-free anchored replace, on-disk anchor counts 1/0 → 0/1): gate pin, two core pins and two census directions red (5 failed, 3 files); restore fromHEAD, blob6798a9ab…equal before and after, 49 passed. Ablation B — turn the parser's slotvisitinto a no-op: three parser pins and census direction 4 red (4 failed, 2 files); restore, blob72a08ad1…equal, 15 passed.Verification on head
7f5eb1af(merged withorigin/main1c2e2c46)type-check(both legs) fortypes,cli,core,sdui-parser,components,plugin-detail,plugin-report,plugin-timeline,plugin-view,plugin-dashboard: all exit 0.packages/types/ packages/sdui-parser/ packages/cli/ packages/core/src/validation/→ 413 files, 10328 tests passed; the console manifest / html-tier suites,packages/components/src/renderers/__tests__/(ratchet included), the six census suites and the four reader pins on the merged head → 23 files, 342 tests passed.pnpm exec eslintover every touched file: 0 errors (33 pre-existingno-explicit-anywarnings inbase.ts/schema-validator.ts, none on an added line).check:doc-snippets(777/777 blocks compile against the built tree),check:new-line-citations(0 new),check:control-bytes,check-changeset-presence,changeset:check,check:changeset-claims,check:readme-exports,check:component-surface-parity,check:phantom-deps,check:unused-deps,check:unreferenced-sources,check:self-import,check:test-path-roots,check:doc-types,check:doc-fences,check:esm-specifiers,check:pending-changeset-literals.Acceptance notes
propertiesbag is a stated boundary, unchanged. All three walks read a slot off the node, afterSchemaRenderer's hoist; a document authored in the spec's page-component shape ({ type, properties: { children } }) is not walked into by any of them today, as before.carousel: renderer and contract disagree. The renderer hands eachitems[]entry torenderChildren(and its owndefaultPropsauthor lists of nodes per item), whileCarouselItemSchemaand the TypeScript face declareitems[].content. The row follows the measured read; the disagreement is reported for a card of its own, not fixed here.tooltip.content(raw text);dashboardwidget entries on the component arm;app-schema-renderer'sschema(a stored page document, rendered structurally);report's unwrappedreportobject (routed to data renderers, no authored node). Each is asserted where a reader could have guessed otherwise.check:component-surface-parity→ the TS face → the zod mirror) is the place it surfaces first.Session:
session_01CPvhwGcirXqBGEdPSb72TZ.Generated by Claude Code