fix(types): the strict authoring face admits the widget-slot metric-card's registered inputs (objectui#11022) - #11023
Conversation
…t's registered inputs (objectui#11022) `StrictAnyComponentSchema` refused every `metric-card` widget carrying `value`, its required registration input: the slot arm admits the card's registry `inputs` through `BaseSchema`'s passthrough by ruling (objectstack#8593), and the strict walker closes that catchall. The slot arm now records its registration's input names in a side table (`declareRegisteredInputs`, `zod/node-derivation.ts`), and the walker's `object` arm admits exactly those, each judged by the arm's own catchall, before closing the object. A key no registration declares is still refused by name; the content channels objectui#9256 refused stay refused; the tolerant arm's shape, catchall and accept set do not move. Pins: `strict-widget-slot-registered-inputs-11022.test.ts` (types) and `metricCardRegisteredInputsStrictFace-11022.test.ts` (plugin-dashboard, against the live ComponentRegistry, both directions). Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…egistered inputs (objectui#11022) Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…t it uses (objectui#11022) Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ 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: Inputs, and nothing else: card objectui#11022 (body; triage ① Derived judgments
② Semver level
③ Boundary flags(a) File surface beyond the claim's wording (claim
(b) The route — derivation or hand list, and the drift guard.
(c) The census — "metric-card is the only same-position member".
Remaining dev flags. Deviation 1 (PR opened at Governed surface and size. None of the eight paths is in Check-runs on this head (read at 2026-09-28T22:00:53Z, none
Implemented-by: VERDICT: PASS Generated by Claude Code |
…trict-slot-registry-inputs Brings in the Spec Main Shape Gate sparse-cone repair so the required gate runs against this head. No conflicts; main moved only by #11025, which touches none of this branch's files. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
|
Regen-provenance: 5879599128 · 9fe80db → c357955 · git merge origin/main (12a6688) → (empty) From the
|
✅ 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 #11022
Clause-②: yes — the strict face's accept set widens: a correctly authored
metric-card(and any other card whose registered inputs the closed arm refused) now parses; an undeclared key is still refused.The widening, named.
StrictAnyComponentSchemanow accepts ametric-cardin a dashboard's widget slot carrying the inputs its registration declares (title,value,icon,trend,trendValue,description). Before this change it refusedvalue,icon,trendandtrendValueasunrecognized_keys. Nothing that parsed before is refused now. The tolerant face (AnyComponentSchemaand every named mirror) does not move. The changeset is@object-ui/typesminor, with no BREAKING note, because nothing narrows.Implemented by a dispatched
os-devrun under thedomain:specseat's claim5878477986(sessionhttps://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm). Grade: triage5878357772.What changed
zod/node-derivation.ts: a side table,declareRegisteredInputs(schema, names)/registeredInputsOf(schema), keyed by node identity (a plainWeakMap, not.meta(): a registry entry would reachz.toJSONSchemaoutput and be inherited down zod's clone chain). It only accepts a passthrough object, one with a catchall, and throws otherwise. A stripping object has no tolerant judgment for the strict face to copy.strict-authoring-face.ts, the walker'sobjectarm: for a node that carries a record, each recorded name the shape does not already declare is added asz.optional(catchall), judged by the node's own catchall exactly as the tolerant face judges it. The object is then closed withcatchall: z.never()like every other object. A key no registration declares is still refused by name, and a name the shape already declares (such asdescription, aBaseSchemamember) keeps its declared member. The module docblock gains a "Registered inputs" section, and theStrictAnyComponentSchema/deriveStrictAuthoringSchemadocblocks say what counts as declared.zod/complex.zod.ts: a privateDASHBOARD_WIDGET_SLOT_REGISTERED_INPUTStable, one row per member of the closedDASHBOARD_COMPONENT_WIDGET_TYPES. Itssatisfiesclause types it as aRecordfromDashboardComponentWidgetTypetoreadonly string[], so a member added without a row does not compile (ablation A5). The slot arm is wrapped indeclareRegisteredInputs(...). The arm's shape, catchall,describetext and accept set are unchanged. The GlobalFilterSchema docblock that objectui#10930 edits is not touched.complex.ts: docblock only. TheDASHBOARD_COMPONENT_WIDGET_TYPESnote now says a new member needs its row.packages/types/README.md: one paragraph and a snippet in "The strict authoring face".packages/types/src/__tests__/strict-widget-slot-registered-inputs-11022.test.ts(15 tests) andpackages/plugin-dashboard/src/__tests__/metricCardRegisteredInputsStrictFace-11022.test.ts(9 tests). The second is the registry parity: it runs against the liveComponentRegistryand checks both directions, and it holds no copy of either list..changeset/11022-strict-widget-slot-registered-inputs.md.Where the grade's pin sits:
metric-cardis deliberately not a root arm ofAnyComponentSchema(objectstack#8593). So a root{ type: 'metric-card', value: 42 }is refused attypeon both faces, before and after this change, and the pin puts the grade's widget in a dashboard'swidgetsslot. A control test records the root behaviour.The mechanism, measured on
origin/main797a30f48before any editconst DashboardWidgetSlotComponentSchema = BaseSchema.extend({ type: z.enum(DASHBOARD_COMPONENT_WIDGET_TYPES), body: retirementTombstone(...), children: retirementTombstone(...) })lives inpackages/types/src/zod/complex.zod.ts. It is the first option ofDashboardComponentSchema.widgets, which isz.array(z.union([DashboardWidgetSlotComponentSchema, DashboardWidgetSchema])).StrictAnyComponentSchemais az.lazyoverfaceWalker(AnyComponentSchema)inpackages/types/src/strict-authoring-face.ts.BaseSchema's loose catchall, inherited through.extend(measured:def.catchallisunknown). The walker's genericcase 'object'arm (out = cloneWithDef(schema, { shape, catchall: z.never() })) replaces that catchall on every object it reaches. The derived slot arm therefore admits exactlyBaseSchema's members, withtype/body/childrenoverridden.@object-ui/typesholds no registry-input source for the passthrough to "use". The package has no dependency on any registry. The inputs exist only in the registration:ComponentRegistry.register('metric-card', MetricCard, { inputs: [...] })in@object-ui/plugin-dashboard. So the repair records the names on the arm and holds them to the live registration in a test inside that package.Reproduction (strict face, BASE
797a30f48)widgets){ type: 'metric-card', value: 42 }invalid_unionatwidgets.0; arm 1:unrecognized_keysat path[], keys["value"]; arm 2:unrecognized_keysat[], keys["value"]{ type: 'metric-card' }{ type: 'metric-card', bogus: 1 }invalid_unionatwidgets.0; both armsunrecognized_keysat[], keys["bogus"]title,value,icon,trend,trendValue; arm 2 refusesvalue,icon,trend,trendValueAfter the change: the first and fourth rows parse on the strict face. The
bogusrow is refused exactly as before, with arm 1 and arm 2 bothunrecognized_keys["bogus"].children/bodystill get the objectui#9256 by-name refusal inside the union'serrors.Census: who else sits in the same position
Query. This was a throwaway vitest file in
apps/console, deleted after the run and not committed. It eagerly imported every registering package (the console'sregister-plugins,components,fields,layoutand 19 plugins) and readComponentRegistry.getAllConfigs(): 508 configs, 286 unique registrations, 0 lazy stubs left. For each registration × authored type (namespaced key and, unlessskipFallback, the bare fallback) × position, it parsed{ type, INPUT: probe }underStrictAnyComponentSchemaand recorded every input named by anunrecognized_keysissue anywhere in the error tree, unionerrorswalked. The positions were: root, when an arm claims the type; and widget slot, when the type is inDASHBOARD_COMPONENT_WIDGET_TYPES. That came to 911 probes over 179 rows that have an arm. An arm census overAnyComponentSchema(139 object arms) found the arms built the same way, with no member beyondBaseSchema's:div,box, the seven semantic tags (aside,main,header,nav,footer,section,article) and the retiredkanban. The widget-slot arm is the fifth such shape.Controls. Positive: the widget-slot
metric-cardvaluewas reported refused (["value"]from both arms) at BASE. Negative:buttonlabelwas admitted andbuttonboguswas refused, in the same run.Population in the same position: exactly one card,
metric-card. Four of its six registered inputs were refused (value,icon,trend,trendValue).titlewas admitted through the strict widget arm, which declares the spec'stitle, anddescriptionis aBaseSchemamember. The bare-shaped root armsdiv,boxand the seven semantic tags each register two inputs, and none of them is refused. No registration claimskanban. So there is no second member to pin. The registry pin walks the closed set, not a list, so a later member is covered, andtscrefuses a member without a row.A different position, not this card, is listed under Acceptance notes: declared root arms that do not declare some registered input.
Consumers
The strict face has no non-test consumer, re-checked (matches triage). A search of every
ts/tsx/mjs/jsfile outsidenode_modulesanddistforStrictAnyComponentSchema,StrictSchemaNodeSchemaandderiveStrictAuthoringSchemafinds:zod/index.zod.ts;packages/types/src/__tests__;scripts/measure-strict-authoring-face.mjs, which keeps its own walker and imports none of them.Red on base, then green: ablation both ways
Predictions were written to a file before any mutation. Every leg went through
ablation-replace.mjsin wrap mode against committed HEAD9fe80dbbc. The anchor count and a blob change were proven on disk, and the restore was proven by blob equal to HEAD and an emptygit diff HEAD. The tree was clean after each leg. The subjects are read fromsrc: the types pin imports relatively, and vitest aliases@object-ui/typesand@object-ui/coretosrcfor the dashboard pin, so no leg needed a rebuild.Object.values(...).flat()→[])value/icon/trend/trendValue, over-admission non-vacuitybogusrefused by name, hand-built "any other key refused"loading)expected [ 'loading' ] to deeply equal []tsc --noEmitonpackages/types: exactly 1 × TS1360 at thesatisfiesAll five legs matched the predictions exactly.
Gates (final HEAD
9fe80dbbc; heavy runs through the shared verify lock; exit codes captured by redirecting first)vitest run packages/types/+ the dashboard pin +examples/schema-catalog/test/plugin-dashboard-component-schema.test.ts+scripts/__tests__/check-readme-exports.test.ts9fe80dbbc)vitest run packages/plugin-dashboard/+ the files outsidepackages/typesthat read a touched file (kanban-column-cards-6939, clicheck-validity-recogniser, scriptscheck-handler-key-read-sites,check-readme-exports,component-node-vocabulary-7434)067461304; the later commit touches onlypackages/types/README.md, and the README readers were re-run at HEAD in the row above)@object-ui/typesbuild(tsc + vite + dist completeness) andtype-check(three programs)@object-ui/plugin-dashboardtype-check, after building its dependency closure--listFilesOnlyshows both new test files are in their packages' test programscheck:control-bytes·check:new-line-citations(0 new) ·check:test-path-roots·changeset:check·check-changeset-presence·check:changeset-claims·check:pending-changeset-literals·check:doc-types·check:doc-fences--testover the 8 pathsAGENTS.md: exit 3)no-explicit-anyon untouched lines. The single rooteslint.config.jssets noparserOptions.project/projectService, so no type-aware rule runs and no untouched file's verdict can move--strictagainst the builtpackages/types/distNOT MEASURED locally, left to CI:
pnpm testshards beyond the suites above.type-check. The public type surface is unchanged:declareRegisteredInputsreturns its argument's type, soDashboardComponentSchema's inferred type does not move, and the two newnode-derivationexports are not reachable through the package'sexportsmap.check:doc-snippets,check:doc-examplesandcheck:readme-exportsexited with PREREQUISITE NOT MET: their 34-package build closure was not built locally. That is not a verdict about the documents.pnpm check, which needs the CLI build.check:eager-closure. The main@object-ui/typesentry imports these modules type-only, so the new runtime code is reachable only through@object-ui/types/zod.pnpm lint.Serial constraints
origin/mainhas not moved since797a30f48. It was fetched into a private ref and read by commit.git merge-tree --write-treeof this head against objectui#10930's head exits 0 (clean).GlobalFilterSchemadocblock inzod/complex.zod.ts. This PR's edits there sit above it, in the new table const and the slot arm's wrapper and docblock.Acceptance notes: out of scope, not changed here
{ id, component: { type: 'metric-card', value: '1' } }in a widget slot parses on the tolerant face and is refused on the strict face:unrecognized_keys["value"]atcomponent. That member is plainBaseSchemaby design (the objectui#8344 note), and the walker closes it the same way. The grade ruled the related observation on that member out of this card, so it is untouched. The strict face is dormant (no consumer). Noted, not filed.propertiesbag (objectui#10872's flat-props batch). Fourteen namedataSource, which the registry emits throughwithElementDataSourceInput. The rest are hand-declared arms missing a key: for example,object-form19,object-grid11,calendar9. In each case the arm declares its keys by hand or by spec row, and the tolerant face leaves those inputs undeclared too. Which side is right is objectui#4631's authority order, instrumented bycheck:component-surface-parity(report-only). A registry record would be the wrong repair there. Noted, not filed.MetricCardalso readsloadinganderror(its props interface), and no registration declares them. So the strict face refuses them by the grade ("It still refuses a key that no registration declares"), and the tolerant face admits them. Noted.scripts/measure-strict-authoring-face.mjskeeps its own walker by design, and that walker does not read the registered-inputs record. On a corpus holding widget-slotmetric-cardnodes it will now namevalueand the other inputs as undeclared where the shipped face admits them. That is the redder direction, the opposite of objectui#10076's. It is a throwaway measurement script. Noted, not filed.content/docspage describes the strict face's per-slot behaviour. The schema-referencewidgetsrow says a component node's other keys are that component's own props, and that is now true on both faces.Generated by Claude Code