feat(types)!: a metric-card in the widget slot parses only with its value; metric-card leaves the widget type vocabulary (objectui#11483) - #11498
Conversation
…alue; metric-card leaves the widget type vocabulary DashboardWidgetTypeSchema named the component type metric-card, so a card carrying only keys both slot arms accept parsed as a widget and the component arm's required value governed nothing. The widget vocabulary is now the spec families plus list/custom on both faces (DashboardWidgetTypeName follows); the slot reads a metric-card through its component arm alone. DashboardWidgetSchema['type'] on the TypeScript face keeps the component type: it is the read type of every slot entry, and the renderers annotate slot entries with it. Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude <noreply@anthropic.com>
…#11483 measured The TS DashboardWidgetSchema keeps the component type in `type` (the read type of every slot entry); the mirror's widget vocabulary dropped it. Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC 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: PR objectui#11498 (card objectui#11483), head Check-runs on the head, read three times, last at 2026-10-02T19:04Z: 43 check-runs, 40 ① Derived judgmentsJudged against triage's ruling
Nothing in the diff widens: no face accepts anything it refused at base. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #11483
Clause-②: no (narrowing)
Builds on objectui#11467 (PR #11482, landed as
401611b21), which declared the widget slot'smetric-cardcomponent arm with a requiredvalue.What this does
This executes triage's ruling
5957113843on its vocabulary arm. The measurement below found no widget-arm data binding formetric-card, sometric-cardleaves the widget arm'stypevocabulary. The component arm's requiredvalueis now the one reading of ametric-cardinwidgets[], and the requirement has one spelling.DashboardWidgetTypeSchemais the spec's chart families pluslistandcustom. It no longer spreadsDASHBOARD_COMPONENT_WIDGET_TYPES.DashboardWidgetTypeName, the twin, dropsDashboardComponentWidgetType.DashboardWidgetSchema['type']keeps it (see "Why the TypeScript widget interface still readsmetric-card").DASHBOARD_COMPONENT_WIDGET_TYPESstill listsmetric-card. It is the component arm'stype, and nothing else reads it insrc.MetricCard,DashboardRendererand every renderer file are untouched.The accept set, before and after
Measured on both validator faces at base
f560ded1and at this head. The tolerant face isDashboardComponentSchema/AnyComponentSchema; the strict face isStrictAnyComponentSchema. Each entry sits in a dashboard'swidgets[].{ type: 'metric-card', title }(the card's measured node)description,id,layout{ type: 'metric-card', title, dataset, values }{ type: 'metric-card', title, options: { value } }{ type: 'metric-card', title, value }{ type: 'metric', dataset, values }(control)componentenvelope, novalueStandalone, the zod
DashboardWidgetSchemanow refuses{ id, type: 'metric-card' }attype. Each refusal is oneinvalid_unionatwidgets.0. The component arm answersinvalid_unionatvalue, and the widget arm answersinvalid_valueattype.objectui validatewas run on the valueless card throughpackages/cli'svalidatecommand at this head. It exits 1 and nameswidgets → 0 → value. The valued card exits 0.H1: the binding measurement (
binding_measured: no)The question was whether a
widgets[]entry{ type: 'metric-card', … }with widget-arm keys and novalueever draws a figure, and through which read. The probe was a throwaway vitest file, never committed. It rendered through the realDashboardRendererandDashboardGridLayout, with@object-ui/components,@object-ui/plugin-chartsand the plugin barrel registered. AqueryDatasetdouble returnedrevenue: 4217.For each case the probe read three things: the text of
MetricCard's figure element, whether4217appeared anywhere, and the dataset query calls. It read them at 0, 150 and 300 ms. Both surfaces gave the same readings.{ type: 'metric-card', title, value: '4217' }.MetricCard's figure reads4217, so the probe can see a figure.{ type: 'metric-card', title }. The figure is empty, and it stays empty from 150 to 300 ms. That is the stability control: the empty figure is not a loading transient.descriptionleaves the figure empty, so a change to some key is not mistaken for a binding.dataset+values, nodimensions:MetricCardnever mounts.4217is drawn byDatasetWidget, and the query runs once. The read inDashboardRenderer(and the same inDashboardGridLayout) isconst datasetBound = !!widget.dataset, which routes toDatasetWidget. That route is chosen before the type is consulted.DatasetWidget, the figure comes fromisMetric = METRIC_TYPES.has(widgetType) || (dimensions.length === 0 && …).metric-cardis not inMETRIC_TYPES, so the tile comes from the dimensionless clause, which ignores the type.{ type: 'bar', dataset, values }draws the same4217.dimensions, ametric-cardtakes the chart branch instead (CHART_TYPE_MAP[widgetType] ?? 'bar'). That last point is a code reading; jsdom draws no chart.datasetis the generic widget binding. It is not a binding the renderer resolves into the card'svalue.options: { value: '4217' }: the figure reads4217. The read is the slot-component passthrough'sreturn toDashboardNodeType({ ...widget, ...options }). That is a literal spread onto the card's props: a second spelling ofvalue, not a data binding. The ruling forbids a second spelling.options.datawith an object provider, legacyobject/valueField/aggregate, inlinedata, an elementdataSource, andfilterall leave the figure empty. The legacy keys get the retired-widget placeholder.⇒ No widget-arm key binds the card's figure. The ruling's vocabulary arm applies. The dataset-bound single figure keeps its own spelling:
{ type: 'metric', dataset, values }draws the same tile and parses on both faces (the control row in the table).Why the TypeScript widget interface still reads
metric-cardOn the TypeScript face,
DashboardWidgetSchemais also the read type of everywidgets[]entry. The component arm is assignable to it, the dashboard docs teach that, and the renderers annotate slot entries with it. The dispatch rules out moving any renderer file, and both stricter spellings would move one. Each was measured with a real build of the@object-ui/app-shellclosure:DashboardWidgetSchema['type']too.tscfails 10 lines inplugin-dashboard: 7 inDashboardRenderer.tsxand 3 inDashboardGridLayout.tsx. All are slot-entry callbacks annotatedDashboardWidgetSchema(TS2345, TS2322, TS2769), and the build stops there.tscfails once, inDashboardWithConfig.tsx, which writesDashboardWidgetSchemavalues back intowidgets. The build stops atplugin-dashboard, soplugin-designerandapp-shellwent unmeasured on that variant.So
DashboardWidgetSchema['type']isDashboardWidgetTypeName | DashboardComponentWidgetType, and the slot's element type is unchanged. One measured limit follows: ametric-cardliteral with novaluestill compiles directly inwidgets[], through that read type, while both validator faces refuse it. It is pinned two-faced inmetric-card-needs-value-11483.test.tsin the same style asenvelopeStray.The parity ledger records the divergence by measurement.
zod-mirror-parity.test.ts'sKnownDriftgainstypeoncomplex.zod.ts#DashboardWidgetSchema, and its header figure moves from 49 / 86 to 49 / 87. The file's own pin checks that figure.H2: who reads the vocabulary
A census with the TypeScript type checker, at base and at this head. Root files were prefiltered by name. Each verdict is
checker.getSymbolAtLocationresolved through aliases to the declaration incomplex.ts/zod/complex.zod.ts, with@object-ui/typesmapped to source.DASHBOARD_COMPONENT_WIDGET_TYPES: 21 resolved sites before and 20 after. The site removed isDashboardWidgetTypeSchema's spread. Itssrcreaders outsidepackages/typesnumber zero, before and after. Its readers insidepackages/typesare the arm's own zodtypeenum andDashboardComponentWidgetType. Its test readers aredashboard-widget-slot-component-arm-7952,dashboard-widget-strict-6002,report-chart-query-spec-parity,plugin-dashboard'smetricCardRegisteredInputsStrictFace-11022and the schema-catalog gate.typeset and nothing else.metric-cardstays its only member, and the arm'ssatisfies readonly ['metric-card']check still holds.DashboardWidgetTypeSchema: readers arereport-chart-query-spec-parityand the schema-catalog gate. Nosrcreader.DashboardWidgetTypeName:srcreaders areapp-shell'sDashboardWidgetInspectorandplugin-designer'sDashboardEditor. Both are type-picker lists, and neither offersmetric-card. Narrowing the type makes it a compile error for either to offer the card as a widget.strict-authoring-face.ts) namesmetric-cardonly in a comment. It keys on nothing here.measureRefusal(plugin-dashboard) asks the zodDashboardWidgetSchema. For ametric-cardprobe it returnedundefinedbefore, becausemetric-cardis not in the spec's metric family. After, thetypeissue aborts the measure check, and it still returnsundefined. No picker passes it ametric-card.DashboardWidgetSchema['type']are the renderers measured above. They are unchanged, because the interface keeps the type.H3: corpus re-judged
Every tracked JSON document was walked for a
type: 'metric-card'object. Every TS/TSX/JS file and every ts/tsx/js/json fence in tracked md/mdx was parsed for the same literal.basic-dashboard.json,e-commerce-dashboard.jsonandsupport-dashboard.jsonhold 11 cards between them, and every one hasvalue. Left alone.content/docs/plugins/plugin-dashboard.mdx, "TypeScript Support":const widget: DashboardWidgetSchema = { id: 'revenue', title: 'Revenue', type: 'metric-card', layout }had novalue. It is a plain authoring example, so it is rewritten. It now holds ametricwidget withdatasetandvalues, plus ametric-cardwith itsvalueannotatedDashboardWidgetSlotComponentSchema.packages/types/README.md:card({ type: 'metric-card', bogus: 1 })taught "bogusis named". It is amended to carryvalue: 42, so it teaches only that, and a valueless line is added.metric-cardcarriesvalue. Left alone.valuelessliterals indashboard-widget-slot-component-arm-7952andstrict-widget-slot-registered-inputs-11022are deliberate negatives, and they still refuse. Left alone.node-recursion-point-8344'smetric-cardfixtures are root-level or nested "unmirrored" controls, refused becausemetric-cardis noAnyComponentSchemaarm. This change does not touch that. Left alone.scripts/measure-strict-authoring-face.mjsat this head: thedashboardcomponent reads 15 documents, 1 strict-refused and 1 red today. That matches the reading objectui#11467's dev took at that PR's head. The one red is the known mdxtype: 'card'widget. That comparison is across different bases, so it is supporting, not decisive.H4:
envelopeStrayIt still compiles, and the zod face still refuses it. A
type-less envelope is not discriminated bytype, so this change cannot reach it. Left untouched.Pins
packages/types/src/__tests__/metric-card-needs-value-11483.test.ts(new):invalid_unionatwidgets.0, with the component arm atvalueand the widget arm attype. The bare type and a card carryingdescription/id/layoutare refused too.valuepasses.value: a dataset-bound card and anoptions.valuecard are refused, and themetric+ dataset control passes.Equalcheck.plugin-dashboard'smetricCardRegisteredInputsStrictFace-11022.test.ts(extended): the registration'srequiredand the schema agree in the slot. Each required input is read off the live registration and dropped from an otherwise complete node. The slot must refuse that node on both faces, at that input, through the component arm. The walk has a non-empty control and a complete-node control.report-chart-query-spec-parity: the "admitsmetric-card" pin is inverted, and the closed-set equality drops the component set.strict-widget-slot-registered-inputs-11022: the widget arm's refusal of{ value, bogus }now also namestype.zod-mirror-parity: the ledger row and the header figure.Pending changesets swept
Each pending entry that names a moved symbol, file or behaviour was read.
A dated note was appended where a standing reading is now false:
dashboard-widget-type-closed-enum.md: the vocabulary was the families plus two objectui sets.11467-metric-card-arm-inputs.md: a valueless card was refused only with a card-only input.11070-dashboard-keys-round11.md: "What does not move: the widget arm".10859-dashboard-node-keys.md: "The dashboard WIDGET vocabulary is unchanged".Left alone, each read and still true:
11022-strict-widget-slot-registered-inputs.md: its one affected reading is already marked false, and that note points to the 11467 entry, which now carries this change's note.6002-dashboard-widget-strict.md: a card withvaluekeeps its props, and the routing sentence holds.7952-dashboard-widgets-component-arm.md: the arm is still assignable toDashboardWidgetSchema.8344-node-recursion-point-redirect.md: the envelope'scomponentslot still admitsmetric-card.11348-dashboard-widget-reads.md,11478-anyschema-declared-node-types.md,8499-node-slot-registered-arms.md,9659-node-recursion-point-inert-clause.mdand9256-public-blocks-content-channels.md: root, nested and content-channel readings that this change does not reach.10859-console-phase-2b-stubs.md,10859-known-types-phase-2b.mdand6442-catalog-blocks-claimant-list.md: registration keys.9165-metric-percent-converged.md: tile rendering.dashboard-readme-card-widget-7035.md: the README examples still parse.pnpm check:changeset-claimslisted 30 more entries that name a touched file. Each was read: they describe other symbols in those files, or ledger figures recorded at a named past change. None states a currentKnownDrifttotal or anything about the widget vocabulary.Verification
Every run below is at head
821d9993unless it says otherwise.Build and type-check
pnpm --filter '@object-ui/app-shell...' run build: 30 projects, exit 0.type-check, each script name echoed:@object-ui/typesexit 0 (all three programs),@object-ui/plugin-dashboardexit 0,@object-ui/plugin-designerexit 0,@object-ui/app-shellexit 0.tsc --listFilesOnlyconfirms the new and edited test files are inside the test programs.Tests, run from the repo root
pnpm exec vitest run packages/types/: 336 files, 8848 tests passed.pnpm exec vitest run packages/plugin-dashboard/: 161 files, 1542 passed and 6 skipped.packages/cli'scheck-validity-recogniserandregistered-types-validate-ratchet-10859: 37 passed.app-shell'swidget-dom-leak-sweep: 215 passed.Gates, all exit 0:
check:doc-snippets(776 of 776 blocks judged, 0 failed, after building the six packages it needs),check:doc-examples,check:doc-types,check:doc-fences,check:doc-example-ids,check:doc-example-readers,check:docs-route-closure.scripts/check-changeset-no-major.mjs,scripts/check-changeset-presence.mjs,check:changeset-claims,check:pending-changeset-literals.check:component-surface-parity,check:new-line-citations(0 new),check:control-bytes,check:spec-symbols,check:readme-exports,check:test-path-roots,check:unreferenced-sources.check-governed-queue-guard --testover the diff: NOT GOVERNED.Not run.
check:sdui-registration-pinswas not run: no registration input moves. It needs a console build. NOT MEASURED, declared.Reverse verification, after the fix was committed:
...DASHBOARD_COMPONENT_WIDGET_TYPES,is put back intoDashboardWidgetTypeSchema. This was done throughablation-replace.mjs: the anchor went from 1 to 0 hits, and the blob went from73f5fd3btob31a539a.git diff HEADis empty. The restored run passed 134 of 134.options.valuepin stayed green under the mutation, as expected, because that face already refusedoptionson a card.Acceptance notes (out of scope, not filed)
{ id, component: { type: 'metric-card', title } }still passes the tolerant face through thecomponentslot'sBaseSchemafallback.objectui validateexits 0 on it, and the dashboard draws an empty figure: the probe's envelope case reads an empty figure, stable, on both surfaces. This is the same defect class, and the dispatch names its carrier: objectui#8347'sBaseSchemafallback. That card is outside this one's file surface.plugin-dashboard's slot-entry callbacks inDashboardRenderer,DashboardGridLayoutandDashboardWithConfig, which this card's surface forbids. Carrier candidate: objectui#8347, which already holds the siblingenvelopeStraycorner. PM to confirm.optionsbag through its passthrough. The renderer's{ ...widget, ...options }would then letoptions.valueoverride the card'svalue. Nothing in the corpus writesoptionson a card. Carrier: none.Deviations
Node version. This container's Node is v22.22.0, below jsdom's
^22.22.2engine floor. Node v22.23.3 was taken from nodejs.org, SHA256-verified againstSHASUMS256.txt, and put onPATHfrom this session's scratch directory only.No line addresses. The dispatch asked for file:line on the binding read. Reads are cited by file and quoted code instead, because AGENTS.md rule [WIP] Update documentation for project #11 bans line addresses in PR and issue bodies.
Two files outside the claim's file surface:
examples/schema-catalog/test/plugin-dashboard-component-schema.test.ts, a reader ofDashboardWidgetTypeSchemathat the census found.packages/types/src/__tests__/zod-mirror-parity.test.ts, the parity ledger.Both are test-only.
The session that made this change is
https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC.Generated by Claude Code