Repository navigation
feat(types)!: the widget slot's metric-card arm declares MetricCard's registered inputs, and the widget component slot takes the arm (objectui#11467) - #11482
Conversation
… registered inputs, and the widget `component` slot takes the arm (objectui#11467) `DashboardWidgetSlotComponentSchema` declares `title`, `value`, `icon`, `trend` and `trendValue` (the sixth input, `description`, is the base's member) with the types of the props `MetricCard` reads, on the TypeScript face and on the zod twin. `value` is required, as the registration marks it. The widget's `component` slot takes the arm first, then any other node, on both faces. The objectui#11022 side table of input names is retired with its walker arm: every name it held is now a member. Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude <noreply@anthropic.com>
…valued card; `value` is refused at its key when absent (objectui#11467) Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude <noreply@anthropic.com>
…n both READMEs and the mdx twin; changeset (objectui#11467) 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: Inputs: card objectui#11467 (body, the claim and the os-dev report), PR objectui#11482 (body, 11-file list, net diff +572/-382), the head's check-runs, and REST reads of files at the head where a diff claim needed its source ( ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named:
② Semver level
What fails ②. A
The Changeset Claim Re-read gate is report-only by design and its went-false reading keys on a pending body naming a FILE this change touches, which neither entry does, so green there is not a verdict on this. Remedy, one commit on the branch: append a dated note to the two entries, retiring the quoted readings and naming ③ Boundary flags
Implemented-by: VERDICT: FAIL |
…ctui#11467 falsifies Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude <noreply@anthropic.com>
✅ 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: card objectui#11467 (body, the claim 5954392494 and the os-dev report 5956160782; the card carries no ruling comment), PR objectui#11482 (body, 13-file list, net diff +576/-382 against Delta from the earlier head: ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named:
② Semver level
What fails ② on this head. The same class, on a third pending entry the earlier record did not name and this head leaves untouched: ③ Boundary flags
Implemented-by: VERDICT: FAIL |
… accept-set readings objectui#11467 falsifies; scope the README's validate clause to a card in widgets[] Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude <noreply@anthropic.com>
✅ 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: card objectui#11467 (body, the claim 5954392494 and the os-dev report 5956160782; the card carries no ruling comment and no sweep list), PR objectui#11482 (body, 14-file list, net diff +581/-382 against Delta from the earlier heads: ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named:
② Semver level
So ② holds on this head: the changeset says what the diff publishes, and every pending reading this head falsifies now carries its dated note. ③ Boundary flags
Implemented-by: VERDICT: PASS |
Brings in objectui#11485, #11486 and #11482; the parity ledger merged without a conflict (#11485 moved rows in other blocks). Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude <noreply@anthropic.com>
…ypeScript twin declares (objectui#6152 round 6) (objectstack-ai#11492) Part of objectstack-ai#6152 (round 6). The card stays open: `ObjectGridSchema.resizableColumns` stops here with an open question (below), the three spec-derived pairs route to objectui#2231, and the two chatbot `displayMode` entries stay by decision (objectui#7654 ruling B). Clause-②: yes Why yes (mixed): the strict face widens on an `object-view`'s `table` slot, where nine keys it refused as unknown are now declared. The tolerant face narrows there and on the flat mirror: a wrongly typed value it kept unexamined is now refused at the key. No TypeScript declaration changes shape. ## Round 6 in one paragraph `ObjectGridSchema`'s `UnmirroredDeclared` entry held eleven keys. Each was measured first: authored writers in both repositories, readers by a type-checker census and a runtime probe through the real `SchemaRenderer` and registry, and the member of the spec's `ComponentPropsMap['object-grid']` row. Ten are MIRRORED on the flat zod `ObjectGridSchema`: `aggregations`, `bulkActionDefs`, `conditionalFormatting`, `grouping`, `navigation`, `operations`, `reorderableColumns`, `rowColor`, `rowHeight` and `singleClickEdit`. The eleventh, `resizableColumns`, is NOT retired: the seat's answer A predates objectui#11276, which moved the authored node into the spec's `properties` bag, and executing it now would leave that bag accepting a key nothing reads (open question below). `UnmirroredDeclared` goes from 5 entries / 16 keys to 5 / 6 (spec-derived 3 / 14 to 3 / 4; local 2 / 2 unchanged). ## Measured first (base `50c73fed0`) **H1 held.** A TypeScript-parser count of `interface UnmirroredDeclared` at the base reads 5 entries / 16 keys, with 11 on `objectql.zod.ts#ObjectGridSchema`, the eleven the claim names. `KnownDrift` 49 / 86, `RuntimeOnlyDeclared` 5 / 30 and `RuntimeOnlyNamedAllowList` 4 / 23 read the same at the base, at `main` `401611b2` and at this head. **H2: the authoring side is the spec's for all eleven.** A one-off probe through the built faces (`AnyComponentSchema`, `StrictAnyComponentSchema`, the spec row, the flat mirror): - written flat on an `object-grid` node, every one of the eleven is refused by name on both faces, toward `properties.KEY` (`flatPropRefusals`); - in the bag, the spec row judges it: `reorderableColumns`, `resizableColumns` and `singleClickEdit` as booleans, `bulkActionDefs` as an array of unknown entries, and the other seven as `unknown` (a `properties.grouping: 42` parses on both faces); - in an `object-view`'s `table` slot, which is built from the flat mirror: nine keys were kept unexamined by the tolerant face and refused as unknown keys by the strict face, while `navigation` and `resizableColumns` are refused by name (withheld). So the remaining question per key is the hoisted node's parity (the TypeScript `ObjectGridSchema`, which `ObjectGrid` reads) and the `table` slot that inherits the mirror. **H3: every key is read.** Type-checker census over `packages/*/src` and `apps/*/src` (5697 files; controls `objectName` 58 reads, `columns` 13, `resizable` 2): every key has a src read in `ObjectGrid`, and `ObjectView` reads `table.operations` and relays six more through `tableRelay`. The first census run read 0 for the `objectName` control: its file list had dropped every file directly under a `src/` root. That run was discarded and the enumeration fixed. Runtime probe (one-off, deleted): an authored node `{ type: 'object-grid', properties: { … } }` through the real registry, varying one key, with a fetch double recording every request. Every key changed the drawn markup or the adapter calls: | key | runtime reading (base) | | --- | --- | | `aggregations` | the group header query gains a `sum` of `amount`, and the header draws it | | `bulkActionDefs` | after a row is selected, the bulk bar draws the def's button | | `conditionalFormatting` | the matching row gains its `background-color` | | `grouping` | the grid asks `queryGroupHeaders` and draws grouped sections | | `navigation` | clicking a row opens the drawer overlay | | `operations` | `export: false` removes the export button that `exportOptions` draws | | `reorderableColumns` | header cells become `draggable="true"` | | `resizableColumns` | `false` removes the column resize handle (no `resizable` written) | | `rowColor` | the row's background class changes | | `rowHeight` | the row-height toolbar appears | | `singleClickEdit` | absent, one click opens the cell editor; `false`, it does not | Controls: the same document rendered twice is byte-identical; `columns` changed (the lit control) differs. A first run was stopped by the repository's network-escape guard (a permission read reached a real socket) and its readings were discarded; the recorded run stubs `fetch` and records each request in the compared log. **H4: `singleClickEdit` on `object-grid` is authored, not host-paired.** Unlike `DataTableSchema` (round 5), the spec's `object-grid` row declares it ("Enter cell edit on single click (default true when editable)"), and the grid supplies its own save path. So it is mirrored here, by reference to the row member, and round 5's runtime-only filing was not copied across. **Authored census.** objectui: an AST and JSON walk over 497 JSON files, 1561 Markdown fences and 6083 TS/TSX files, with controls `object-grid` bag `objectName` 31 and `table.columns` 15. It found no document writing any of the eleven in an `object-view` `table` slot (two typed tests only). In an `object-grid` bag, three doc fences write keys this round mirrors: `operations` in `README.md` and `content/docs/api/schema-reference.md`, `singleClickEdit` in `content/docs/plugins/plugin-grid.mdx`. The in-code producers are `ObjectView` (the relayed keys), `FieldDesigner` / `ObjectManager` (`operations`) and the plugin-grid demo. `resizableColumns` is written nowhere. objectstack `main` `3a6d92f7` (read by `git grep` on a self-named ref): its two authored `object-grid` nodes, the showcase work queues, write `objectName` / `columns` / `filter` only, and nothing in it authors an `object-view`. ## What changed `packages/types/src/zod/objectql.zod.ts`, the flat `ObjectGridSchema` mirror: | key | member | before | after | | --- | --- | --- | --- | | `grouping` | the spec's `GroupingConfigSchema`, by reference | kept unexamined | judged; a padded field name is refused by the spec's own rule | | `navigation` | the spec's `NavigationConfigSchema`, by reference | kept | judged (the `table` slot still refuses it by name) | | `rowColor` | the spec's `RowColorConfigSchema`, by reference | kept | judged | | `rowHeight` | the spec's `RowHeightSchema`, by reference | kept | the five presets only | | `reorderableColumns`, `singleClickEdit` | the row's own boolean members, by reference | kept | boolean | | `aggregations` | strict `{ field, type }` entry, restating the twin | kept | judged; unknown member named | | `operations` | strict six-toggle object, restating the twin | kept | judged; a misspelt toggle named | | `bulkActionDefs` | strict entry restating `BulkActionDef`; `operation` and `visible`'s second arm by reference to the spec | kept | judged | | `conditionalFormatting` | the two-dialect rule, now one private declaration shared with `ListViewSchema`'s mirror | kept | judged | - The ten members are those the TypeScript twin declares, so `NarrowerThanDeclared` and `WiderThanDeclaredKeys` measure nothing new, and no `KnownDrift` or `WiderThanDeclared` row was added. The spec row types seven of the ten as `unknown`. Binding those would have mirrored nothing, so each member takes the spec schema the twin names, or the twin's own shape. - `bulkActionDefs` is NOT the spec's `BulkActionDefSchema` by reference. Measured against the twin, that schema is narrower on `params` (a strict entry with a closed `type`) and on `visible` (no `ExpressionWire` arm), so binding it would have needed a `KnownDrift` row. - `ListViewSchema`'s `conditionalFormatting` now reads the hoisted rule. Its accept set is unchanged, and the zod `ListViewSchema` member is the same union. `packages/types/src/objectql.ts`: only `singleClickEdit`'s comment. It said `@default false`, but `ObjectGrid` reads `schema.singleClickEdit ?? true` and the spec row says the default is true. `packages/types/src/__tests__/zod-mirror-parity.test.ts`: the `ObjectGridSchema` entry goes from eleven keys to `resizableColumns`, with the round's route written on it. The four pinned figure sites and the history sentences moved with it (header bullet 5 / 6; both ledger docstrings 6 keys; totals line 5 / 6 with 3 / 4 and 2 / 2; the spec-derived bullet 3 / 4). **Doors, before and after:** | door | before | after | | --- | --- | --- | | authored `object-grid` (`properties` bag) | the spec row judges | unchanged | | `object-grid` prop written flat on the node | refused by name | unchanged | | `object-view` `table`, strict face (`objectui validate`) | the nine refused as unknown keys | a declared value parses; a wrong one is refused at the key | | `object-view` `table`, tolerant face (`safeValidateSchema`) | any value kept unexamined | a wrong value refused at the key | | `ObjectGridSchema.safeParse` directly | kept | judged | ## Pins - New `packages/types/src/__tests__/unmirrored-declared-round6-6152.test.ts` (49 rows). For each key it checks that the mirror accepts the declared value and refuses a wrong one at the key, then drives the nine slot keys through the `table` slot on both faces. It holds that `table.navigation` / `table.resizableColumns` stay refused by name, and that the authored node does not move (flat refused by name, bag parses). It pins the two row members by identity, the spec's padded-name rule reaching `grouping`, the shared conditional-formatting rule by identity with `ListViewSchema`'s, and the strict entries naming unknown members. It also holds that `resizableColumns` is not a member. - `grouping-field-padded-refused-7347.test.ts`: the census now finds `table.grouping` on `object-view`, as the 2026-09-30 evidence comment on objectstack-ai#6152 predicted for the first round that mirrored this key. The table slot gets its own row and so carries the padded-field refusal on every face (the row type gained an optional `type` so one arm can own two paths). - `imported-defaults-8317.test.ts`: the four new spec crossings are registered, and the mirror reads the row's members through the boundary (`stripImportedDefaults(SpecObjectGridPropsSchema).shape.KEY`). ## Corpus and pending changesets re-judged - The three doc fences above write their keys in the `object-grid` bag, which the spec judges and this round does not touch. `content/docs/plugins/plugin-view.mdx`'s `table` section lists the relayed keys as declared, and the validator now agrees with it. No corpus document needed a change. - `.changeset/11276-object-grid-properties-bag.md` (pending) said the row declares eleven keys "the flat mirror does not". This round makes that false for ten, so it gets a dated, append-only correction note (+2 / -0, frontmatter and every prior line byte-identical, checked by `cmp`). `check:changeset-claims` named 41 more pending changesets. Their paragraphs about the grid, the table slot and the ledgers were re-read, and none is made false. ## Reverse verification (predictions written first; objectstack `scripts/ablation-replace.mjs`, wrap mode, on committed `437408ce`; each restore proven by blob == HEAD and an empty `git diff HEAD`) - **A1**, the `grouping` member deleted from the mirror (anchor x1 to x0, blob `36a2f818` to `dd46877a`): `tsc -p tsconfig.test.json` exit 2 with exactly one TS2322, `"objectql.zod.ts#ObjectGridSchema"` not assignable to `never` at the ledger reconciliation. vitest 22 failed, exactly as predicted: 4 in the round-6 pin, and 18 in the 7347 file (the census plus the table row on four faces). - **A2**, `grouping` put back in the ledger entry with the mirror intact (blob `a93e249d` to `d0959519`): `tsc` exit 2 at the same reconciliation. The parity file had 2 runtime failures, the objectui#7279 split/totals pin and the objectui#8243 docstring pin. The prediction named objectstack-ai#7279 and "possibly" objectstack-ai#8222; the second one to fire was objectstack-ai#8243. No dist leg: every subject here resolves to source (relative imports in the types tests, and the root vitest alias for consumers). ## Tests and gates (final head `437408ce`; heavy runs through `os-verify-lock.sh`; each exit code captured before any pipe) - `pnpm --filter @object-ui/types run type-check` (`tsc --noEmit`, `-p tsconfig.examples.json`, `-p tsconfig.test.json`, which judges the ledgers): exit 0. - `pnpm exec vitest run packages/types/`: `Test Files 336 passed (336)`, `Tests 8904 passed (8904)`. - Zod-face consumers outside `packages/types`: every test file that imports `@object-ui/types/zod` or names `safeValidateSchema` / `AnyComponentSchema` / `StrictAnyComponentSchema`. That is 115 files (schema-catalog, cli, app-shell previews, apps/console, components, scripts and twelve plugin packages), run in two halves: `58 passed (58)` / `1286 passed`, then `57 passed (57)` / `1015 passed`. - The doc-snippet build closure (`turbo run build --concurrency=2`, the filter `check-doc-snippet-types.mjs --build-filter` prints): `Tasks: 35 successful, 35 total`. Then `check:doc-snippets` ("776 of 776 block(s) judged, 0 failed"), `check:doc-examples`, and `check:readme-exports`: all exit 0. - All exit 0: `check-changeset-presence` ("6 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"), `check-changeset-no-major`, `check-changeset-fixed`, `check-changeset-overwrite` (report-only; the one appended note), `check:changeset-claims` (report-only), `check:pending-changeset-literals`, `check:control-bytes`, `check:new-line-citations` ("0 new citation(s)"), `check:spec-symbols`, `check:handler-key-reads`, `check:element-data-source-declaration`, `check:test-path-roots`, `check:unreferenced-sources`, `check-type-check-coverage` ("43/43"), `check:doc-types`, `check:doc-fences`, `check:component-surface-parity` (report-only), `check-governed-queue-guard --test` over the diff (NOT GOVERNED). - Lint, a declared narrowing: `eslint --no-inline-config --format json` over the 6 changed TS files. The JSON judged 6 files with no ignore notice: 0 errors, and 38 warnings, every one on a pre-existing line (0 on an added line). `eslint.config.js` sets no `parserOptions` / `projectService`, and no `eslint-rules/` file reads the disk, so the diff cannot move an untouched file's verdict. The repo-wide `pnpm lint` is CI's. - `main` was merged once before this PR (`54be4dda`, bringing in objectui#11485, objectstack-ai#11486 and objectstack-ai#11482); the parity file merged with no conflict. NOT MEASURED: `check:sdui-registration-pins` (it needs a console build, and no registration input moved); the full `pnpm test` farm and the repo-wide lint (CI's); `plugin-grid` / `plugin-view` behaviour suites (no runtime file changed). The contract record is the seat's to owe on the landing head. ## Open question: `resizableColumns` The seat's answer A on objectstack-ai#6152 ("`resizable` is canonical; the local half tombstones `resizableColumns`; its spec half forks to objectstack") was given while the flat mirror was the authored `object-grid` arm. Since objectui#11276 the authored node's props live in the spec's `properties` bag, which is `ComponentPropsMap['object-grid']` by reference and still declares `resizableColumns` ("Alternate spelling of `resizable`"). Pinned by objectui#11276: "the bag is the spec row by reference". So the local half as written would tombstone the TypeScript member and the mirror, and drop `ObjectGrid`'s `?? schema.resizableColumns` read. That leaves `objectui validate` and `os validate` both accepting `properties.resizableColumns`, with the flat refusal pointing at it, and nothing reading it. Measured: no document in either repository writes the key. The options, with four axes, are in the report on the card; this PR leaves the key in the ledger. ## Acceptance notes (noted, not filed here) - The spec's `object-grid` row types seven of these keys as `unknown`, so the authored bag accepts `properties: { rowHeight: 'comfortable' }` or `grouping: 42` on both faces. `ObjectGrid` then falls back silently (for `rowHeight`, to `compact`). objectui#11276's PR already reported the row's `unknown` members as a spec-row finding. The report on the card names it for the seat with its measured reach. - `BulkActionDef` / `BulkActionParam` (the TypeScript twin) are wider than the spec's `BulkActionDefSchema`: open params with any `type`, `ExpressionWire` predicates, and the runtime `actionDef`. The mirror follows the twin. A narrowing of the twin would be its own question. - The strict face closes undeclared keys at every depth, including a `bulkActionDefs` param's catch-all that the twin and the tolerant face keep. That is the strict face's own construction, not this member's. --- _Generated by [Claude Code](https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…alue; metric-card leaves the widget type vocabulary (objectui#11483) (objectstack-ai#11498) Fixes objectstack-ai#11483 Clause-②: no (narrowing) Builds on objectui#11467 (PR objectstack-ai#11482, landed as `401611b21`), which declared the widget slot's `metric-card` component arm with a required `value`. ## What this does This executes triage's ruling `5957113843` on its vocabulary arm. The measurement below found no widget-arm data binding for `metric-card`, so `metric-card` leaves the widget arm's `type` vocabulary. The component arm's required `value` is now the one reading of a `metric-card` in `widgets[]`, and the requirement has one spelling. - **Zod face:** `DashboardWidgetTypeSchema` is the spec's chart families plus `list` and `custom`. It no longer spreads `DASHBOARD_COMPONENT_WIDGET_TYPES`. - **TypeScript face:** `DashboardWidgetTypeName`, the twin, drops `DashboardComponentWidgetType`. `DashboardWidgetSchema['type']` keeps it (see "Why the TypeScript widget interface still reads `metric-card`"). - **What stays:** `DASHBOARD_COMPONENT_WIDGET_TYPES` still lists `metric-card`. It is the component arm's `type`, and nothing else reads it in `src`. - **What is not added:** no renderer fallback, no refinement, no second required-key declaration. `MetricCard`, `DashboardRenderer` and every renderer file are untouched. ## The accept set, before and after Measured on both validator faces at base `f560ded1` and at this head. The tolerant face is `DashboardComponentSchema` / `AnyComponentSchema`; the strict face is `StrictAnyComponentSchema`. Each entry sits in a dashboard's `widgets[]`. | entry | tolerant, before | tolerant, after | strict, before | strict, after | | --- | --- | --- | --- | --- | | `{ type: 'metric-card', title }` (the card's measured node) | accept | **refuse** | accept | **refuse** | | the same plus `description`, `id`, `layout` | accept | **refuse** | accept | **refuse** | | `{ type: 'metric-card', title, dataset, values }` | accept | **refuse** | accept | **refuse** | | `{ type: 'metric-card', title, options: { value } }` | accept | **refuse** | refuse | refuse | | `{ type: 'metric-card', title, value }` | accept | accept | accept | accept | | `{ type: 'metric', dataset, values }` (control) | accept | accept | accept | accept | | a card in the legacy `component` envelope, no `value` | accept | accept (unchanged; objectui#8347's corner) | refuse | refuse | Standalone, the zod `DashboardWidgetSchema` now refuses `{ id, type: 'metric-card' }` at `type`. Each refusal is one `invalid_union` at `widgets.0`. The component arm answers `invalid_union` at `value`, and the widget arm answers `invalid_value` at `type`. `objectui validate` was run on the valueless card through `packages/cli`'s `validate` command at this head. It exits 1 and names `widgets → 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 no `value` ever draws a figure, and through which read. The probe was a throwaway vitest file, never committed. It rendered through the real `DashboardRenderer` and `DashboardGridLayout`, with `@object-ui/components`, `@object-ui/plugin-charts` and the plugin barrel registered. A `queryDataset` double returned `revenue: 4217`. For each case the probe read three things: the text of `MetricCard`'s figure element, whether `4217` appeared anywhere, and the dataset query calls. It read them at 0, 150 and 300 ms. Both surfaces gave the same readings. - **Lit control:** `{ type: 'metric-card', title, value: '4217' }`. `MetricCard`'s figure reads `4217`, so the probe can see a figure. - **Measured node:** `{ 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. - **Inert-key control:** adding `description` leaves the figure empty, so a change to some key is not mistaken for a binding. - **`dataset` + `values`, no `dimensions`:** `MetricCard` never mounts. `4217` is drawn by `DatasetWidget`, and the query runs once. The read in `DashboardRenderer` (and the same in `DashboardGridLayout`) is `const datasetBound = !!widget.dataset`, which routes to `DatasetWidget`. That route is chosen before the type is consulted. - Inside `DatasetWidget`, the figure comes from `isMetric = METRIC_TYPES.has(widgetType) || (dimensions.length === 0 && …)`. `metric-card` is not in `METRIC_TYPES`, so the tile comes from the dimensionless clause, which ignores the type. - **Type-agnostic control:** `{ type: 'bar', dataset, values }` draws the same `4217`. - With `dimensions`, a `metric-card` takes the chart branch instead (`CHART_TYPE_MAP[widgetType] ?? 'bar'`). That last point is a code reading; jsdom draws no chart. - **Verdict:** `dataset` is the generic widget binding. It is not a binding the renderer resolves into the card's `value`. - **`options: { value: '4217' }`:** the figure reads `4217`. The read is the slot-component passthrough's `return toDashboardNodeType({ ...widget, ...options })`. That is a literal spread onto the card's props: a second spelling of `value`, not a data binding. The ruling forbids a second spelling. - **No figure at all:** `options.data` with an object provider, legacy `object` / `valueField` / `aggregate`, inline `data`, an element `dataSource`, and `filter` all 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-card` On the TypeScript face, `DashboardWidgetSchema` is also the read type of every `widgets[]` 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-shell` closure: - **Drop the component type from `DashboardWidgetSchema['type']` too.** `tsc` fails 10 lines in `plugin-dashboard`: 7 in `DashboardRenderer.tsx` and 3 in `DashboardGridLayout.tsx`. All are slot-entry callbacks annotated `DashboardWidgetSchema` (TS2345, TS2322, TS2769), and the build stops there. - **Keep it on the interface but narrow the slot's element type.** `tsc` fails once, in `DashboardWithConfig.tsx`, which writes `DashboardWidgetSchema` values back into `widgets`. The build stops at `plugin-dashboard`, so `plugin-designer` and `app-shell` went unmeasured on that variant. So `DashboardWidgetSchema['type']` is `DashboardWidgetTypeName | DashboardComponentWidgetType`, and the slot's element type is unchanged. One measured limit follows: a `metric-card` literal with no `value` still compiles directly in `widgets[]`, through that read type, while both validator faces refuse it. It is pinned two-faced in `metric-card-needs-value-11483.test.ts` in the same style as `envelopeStray`. The parity ledger records the divergence by measurement. `zod-mirror-parity.test.ts`'s `KnownDrift` gains `type` on `complex.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.getSymbolAtLocation` resolved through aliases to the declaration in `complex.ts` / `zod/complex.zod.ts`, with `@object-ui/types` mapped to source. - **`DASHBOARD_COMPONENT_WIDGET_TYPES`:** 21 resolved sites before and 20 after. The site removed is `DashboardWidgetTypeSchema`'s spread. Its `src` readers outside `packages/types` number zero, before and after. Its readers inside `packages/types` are the arm's own zod `type` enum and `DashboardComponentWidgetType`. Its test readers are `dashboard-widget-slot-component-arm-7952`, `dashboard-widget-strict-6002`, `report-chart-query-spec-parity`, `plugin-dashboard`'s `metricCardRegisteredInputsStrictFace-11022` and the schema-catalog gate. - ⇒ It now means the component arm's closed `type` set and nothing else. `metric-card` stays its only member, and the arm's `satisfies readonly ['metric-card']` check still holds. - **`DashboardWidgetTypeSchema`:** readers are `report-chart-query-spec-parity` and the schema-catalog gate. No `src` reader. - **`DashboardWidgetTypeName`:** `src` readers are `app-shell`'s `DashboardWidgetInspector` and `plugin-designer`'s `DashboardEditor`. Both are type-picker lists, and neither offers `metric-card`. Narrowing the type makes it a compile error for either to offer the card as a widget. - **The strict walker** (`strict-authoring-face.ts`) names `metric-card` only in a comment. It keys on nothing here. - **`measureRefusal`** (`plugin-dashboard`) asks the zod `DashboardWidgetSchema`. For a `metric-card` probe it returned `undefined` before, because `metric-card` is not in the spec's metric family. After, the `type` issue aborts the measure check, and it still returns `undefined`. No picker passes it a `metric-card`. - **Structural readers of `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. - **Schema catalog:** `basic-dashboard.json`, `e-commerce-dashboard.json` and `support-dashboard.json` hold 11 cards between them, and every one has `value`. Left alone. - **`content/docs/plugins/plugin-dashboard.mdx`, "TypeScript Support":** `const widget: DashboardWidgetSchema = { id: 'revenue', title: 'Revenue', type: 'metric-card', layout }` had no `value`. It is a plain authoring example, so it is **rewritten**. It now holds a `metric` widget with `dataset` and `values`, plus a `metric-card` with its `value` annotated `DashboardWidgetSlotComponentSchema`. - **`packages/types/README.md`:** `card({ type: 'metric-card', bogus: 1 })` taught "`bogus` is named". It is **amended** to carry `value: 42`, so it teaches only that, and a valueless line is added. - **Every other doc fence and README `metric-card`** carries `value`. Left alone. - **Test fixtures:** - The `valueless` literals in `dashboard-widget-slot-component-arm-7952` and `strict-widget-slot-registered-inputs-11022` are deliberate negatives, and they still refuse. Left alone. - `node-recursion-point-8344`'s `metric-card` fixtures are root-level or nested "unmirrored" controls, refused because `metric-card` is no `AnyComponentSchema` arm. This change does not touch that. Left alone. - **`scripts/measure-strict-authoring-face.mjs` at this head:** the `dashboard` component 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 mdx `type: 'card'` widget. That comparison is across different bases, so it is supporting, not decisive. ## H4: `envelopeStray` It still compiles, and the zod face still refuses it. A `type`-less envelope is not discriminated by `type`, so this change cannot reach it. Left untouched. ## Pins - **`packages/types/src/__tests__/metric-card-needs-value-11483.test.ts` (new):** - The measured node is refused on all three faces. The envelope is `invalid_union` at `widgets.0`, with the component arm at `value` and the widget arm at `type`. The bare type and a card carrying `description` / `id` / `layout` are refused too. - A card with `value` passes. - No widget key stands in for `value`: a dataset-bound card and an `options.value` card are refused, and the `metric` + dataset control passes. - Vocabulary on both faces. Disjointness is a type-level `Equal` check. - The TypeScript measured limit, recorded two-faced. - **`plugin-dashboard`'s `metricCardRegisteredInputsStrictFace-11022.test.ts` (extended):** the registration's `required` and 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. - **Updated:** - `report-chart-query-spec-parity`: the "admits `metric-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 names `type`. - The schema-catalog gate's check 2 counts the slot's component set as known. - `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 with `value` keeps its props, and the routing sentence holds. - `7952-dashboard-widgets-component-arm.md`: the arm is still assignable to `DashboardWidgetSchema`. - `8344-node-recursion-point-redirect.md`: the envelope's `component` slot still admits `metric-card`. - `11348-dashboard-widget-reads.md`, `11478-anyschema-declared-node-types.md`, `8499-node-slot-registered-arms.md`, `9659-node-recursion-point-inert-clause.md` and `9256-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.md` and `6442-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-claims` listed 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 current `KnownDrift` total or anything about the widget vocabulary. ## Verification Every run below is at head `821d9993` unless it says otherwise. **Build and type-check** - Build of `pnpm --filter '@object-ui/app-shell...' run build`: 30 projects, exit 0. - `type-check`, each script name echoed: `@object-ui/types` exit 0 (all three programs), `@object-ui/plugin-dashboard` exit 0, `@object-ui/plugin-designer` exit 0, `@object-ui/app-shell` exit 0. - `tsc --listFilesOnly` confirms 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. - The schema-catalog gate file: 30 passed. - `packages/cli`'s `check-validity-recogniser` and `registered-types-validate-ratchet-10859`: 37 passed. - `app-shell`'s `widget-dom-leak-sweep`: 215 passed. **Gates**, all exit 0: - Doc gates: `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`. - Changeset gates: `scripts/check-changeset-no-major.mjs`, `scripts/check-changeset-presence.mjs`, `check:changeset-claims`, `check:pending-changeset-literals`. - Other gates: `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 --test` over the diff: NOT GOVERNED. **Not run.** `check:sdui-registration-pins` was not run: no registration input moves. It needs a console build. NOT MEASURED, declared. **Reverse verification**, after the fix was committed: - **Mutation:** `...DASHBOARD_COMPONENT_WIDGET_TYPES,` is put back into `DashboardWidgetTypeSchema`. This was done through `ablation-replace.mjs`: the anchor went from 1 to 0 hits, and the blob went from `73f5fd3b` to `b31a539a`. - **Mutated run** of the five pin files: 17 failed, 117 passed (4 files red). The red tests are every new valueless / dataset / options pin on the faces that accepted them before, the slot-level required-input walk, the inverted parity pins, and the 11022 widget-arm shape. - **Restored:** the blob equals HEAD and `git diff HEAD` is empty. The restored run passed 134 of 134. - **Direction:** red, as expected. The strict face's `options.value` pin stayed green under the mutation, as expected, because that face already refused `options` on a card. ## Acceptance notes (out of scope, not filed) - **The legacy envelope corner.** `{ id, component: { type: 'metric-card', title } }` still passes the tolerant face through the `component` slot's `BaseSchema` fallback. `objectui validate` exits 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's `BaseSchema` fallback. That card is outside this one's file surface. - **The TypeScript measured limit above.** Closing it means retyping `plugin-dashboard`'s slot-entry callbacks in `DashboardRenderer`, `DashboardGridLayout` and `DashboardWithConfig`, which this card's surface forbids. Carrier candidate: objectui#8347, which already holds the sibling `envelopeStray` corner. PM to confirm. - **An observation, no producer.** On the tolerant face the component arm keeps an `options` bag through its passthrough. The renderer's `{ ...widget, ...options }` would then let `options.value` override the card's `value`. Nothing in the corpus writes `options` on a card. Carrier: none. ## Deviations - **Node version.** This container's Node is v22.22.0, below jsdom's `^22.22.2` engine floor. Node v22.23.3 was taken from nodejs.org, SHA256-verified against `SHASUMS256.txt`, and put on `PATH` from 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 objectstack-ai#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 of `DashboardWidgetTypeSchema` that 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](https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #11467
Clause-②: yes
Parent: objectui#8347, which keeps the index-signature removal. This PR is its F2 half and leaves the signature in place: every commit compiles with
BaseSchema's signature present.What this does
Seat answer A, executed.
DashboardWidgetSlotComponentSchemais the widget slot's component arm. It now declaresmetric-card's registered inputs as members, typed as the propsMetricCardreads. The zod twin declares the same members. The widget's legacycomponentslot takes the arm first, then any other node, on both faces.titlestring | I18nLabel, optionalI18nLabelSchema(defaults stripped), optionalMetricCardProps.titlevaluestring | number, requiredz.unionof string and number, requiredMetricCardProps.value; the registration marks itrequirediconstring, optionalz.string(), optionalMetricCardProps.icontrend'up' | 'down' | 'neutral', optionalz.enumof the three, optionalMetricCardProps.trendtrendValuestring, optionalz.string(), optionalMetricCardProps.trendValuedescriptionBaseSchemamember, not restatedstring | I18nLabelon both facescomponentchanges too. On the TypeScript face it isDashboardWidgetSlotComponentSchema | SchemaNode. On the zod face it isz.unionof the arm andBaseSchema."By reference" across a package boundary.
@object-ui/typescannot import the registration orMetricCard, so the members are transcribed. The registering package holds them to both sources inmetricCardRegisteredInputsStrictFace-11022.test.ts:BaseSchemaandlayoutis a registered input;valueis required, as the registration marks it;trendis exactly the registered enum;MetricCardPropsfor the six inputs.No second spelling. objectui#11022's side table of input names (
DASHBOARD_WIDGET_SLOT_REGISTERED_INPUTS, throughdeclareRegisteredInputs) recorded these same names so the strict walker could admit them. Every name it held is now a member, so the table,declareRegisteredInputs/registeredInputsOf, and the walker's branch for them are removed. They were internal and not exported, and the arm was their only caller.A guard instead of the table's
satisfies. The arm's zodtypeisz.enumoverDASHBOARD_COMPONENT_WIDGET_TYPESwith asatisfiescheck against the one-member listmetric-card. A second member of the closed set is therefore a compile error until the arm is split per component. Without the check, the new member would inherit the card's props.Measured first
H1. Which inputs
MetricCardreads. Two instruments, both at based93e53f5.MetricCardPropsmember it looked up the references to the member symbol, then the uses of the destructured binding in the render body.title,value,icon,trend,trendValueanddescriptionare each destructured and read. A standalonetscprogram over the same config: 0 errors.DashboardRendererdrew the card through the realSchemaRendererandComponentRegistry, one key removed at a time from a full card. The probe was a one-shot test file, deleted after the run.title,valueordescriptionchanged the text.iconchanged the drawn svg count.trendortrendValuechanged the text, the svg count and the trend colour class. Each is drawn only together with the other.trendfrom up to down or neutral moved the colour class from green to red or yellow.MetricCard's rest spread (objectui#4426, pre-existing).All five keys the card named are read.
descriptionis the sixth input and is already declared onBaseSchema.H2. What each zod face judged at base.
description(throughBaseSchema). The other five passed through the catchall unjudged.unknown), sotrend: 'sideways',title: 7andvalue: {}all parsed on both faces.componentslot wasBaseSchemaalone. The strict face therefore refused a card'stitle,value,icon,trendandtrendValuethere asunrecognized_keys. That includes the plugin-dashboard README's own legacy-envelope example.H3. PR objectui#11460 had landed (
9d1c0bff) before this branch was cut fromd93e53f5.H4.
zod-mirror-parity.test.tsdid not move. The file is untouched, and the types suite is green with it.componentstays inKnownDrift(the primitive arms ofSchemaNode), and so doeswidgets.Accept-set change (Clause-② yes)
widgets[]with all inputs well-typedwidgets[],trend: 'sideways'/title: 7/icon: 5/trendValue: 3/value: {}widgets[]withicon,trendortrendValueand novaluewidgets[]with onlytitle/description/layoutand novaluecomponentslot with its inputs (README example)componentwithtrend: 'sideways'BaseSchemafallback)trend)component({ type: 'chart' })someProp)The TypeScript face gains no accepted key while the signature stands. The read type of
titleoff awidgets[]entry moves fromanytostring | I18nLabel. Six consumer packages and two examples type-check unchanged; see Gates.The card's own reading, two legs
The probe used the parent's mutation: only
BaseSchema's documented index signature deleted, with@object-ui/typesrebuilt so thatdist/base.d.tskeeps 1 signature. Thentsc -p packages/types/tsconfig.test.jsonandpnpm check:doc-snippetsran. The mutation went throughscripts/ablation-replace.mjsfrom the objectstack checkout: anchor 1 → 0, blobce2bc9e31dc6→93c3221b8301, restored to blob == HEAD,git diff HEADempty. Both legs ran in this container, in two worktrees.d93e53f5(control)35a89136valuex3 (README usage copy;componentslot x2),titlex1 (9256 accepting literal), plus probesomePropsomeProp, probebogusonlycheck:doc-snippetspackages/plugin-dashboard/README.mdThe
bogusline at head is the arm-7952 file's "MEASURED LIMIT" constant. That corner shuts once thecomponentcard'svalueis declared, and the constant says to delete it then. This is noted for objectui#8347's last PR. The restore leg rebuilt types:dist/base.d.tssignatures 2, src 2,git diff HEAD0.Reverse verification of the new pins
Each leg deleted the members with
ablation-replace.mjson the committed tree (anchor 1 → 0, blob moved), ran, and restored (blob == HEAD,git diff HEADempty). The direction was predicted before each run: red.value-required row, the arm-shape pin, the README-literal strict pin, plugin-dashboard UNDER-admission x6, DECLARED, typed-members, and the saved-layout strict parse.Equalpins went false: the five members and the zod twin. Three@ts-expect-errordirectives went unused (TS2578). Thetitleread pin went false.Corpus re-judged
scripts/measure-strict-authoring-face.mjsran at base and at head over the schema catalog, the docs fences, the apps and the types sources: 610 documents, 2244 nodes.documentsRedTodayWholeDocumentis 23 → 23 andnodesRedToday30 → 30.metric-cardin the corpus carries a stringvalueand atrendfrom the three, so no document's verdict moved on the shipped faces.type: 'card'widget withchildren, in the plugin-dashboard mdx) also lists the component arm's missingvalueamong its union errors now. Its verdict is unchanged; only the error shape moved.Pins moved
strict-widget-slot-registered-inputs-11022.test.ts. Several blocks are rewritten:layoutand the five inputs";bogusandvalue-required fixtures now carry avalue.metricCardRegisteredInputsStrictFace-11022.test.ts. The OVER-admission test, which read "strict keys minus tolerant keys", is replaced by DECLARED-both-directions on both faces. Typed-member andMetricCardPropspins are added. The UNDER-admission andlayoutfixtures carry the requiredvalue.dashboard-widget-slot-component-arm-7952.test.ts.valueis added to two existing literals so each pin keeps refusing for its stated reason. A new objectui#11467 block holds the member types, the twin parity, the TypeScript refusals, thecomponentslot type, the README's two literals on both faces, and thecomponent-slot strict judgment.Files outside the claimed surface (declared)
The claim's surface did not list five files. Each one is the same accept-set change, not a side quest:
packages/types/src/zod/node-derivation.tsandpackages/types/src/strict-authoring-face.ts: the side table and its walker branch are removed, because every name in it is now a member.strict-widget-slot-registered-inputs-11022.test.ts(types) andmetricCardRegisteredInputsStrictFace-11022.test.ts(plugin-dashboard): they pinned the passthrough-plus-side-table state that the ruling replaces.packages/types/README.md: its strict-face paragraph described the side table.The renderer, the index signature and
SchemaNodeare untouched.Gates (head
35a89136)pnpm --filter PKG run type-check(each echoedtype-check): exit 0 for types, plugin-dashboard, core, sdui-parser, plugin-list, plugin-designer, app-shell, example-schema-catalog and example-console-starter. All 40 packages were built first, with turboTasks: 40 successful.packages/types/: 330 files, 8770 tests passed;packages/plugin-dashboard/: 160 files, 1532 passed, 6 skipped (pre-existing);examples/schema-catalog/: 39 files, 2235 passed;packages/sdui-parser/: 20 files, 289 passed;check:doc-snippets: 776 of 776 judged, 0 failed.check:doc-examples: 87 fail, all 87 declared.check:doc-types: green.check:spec-symbols,check:component-surface-parity,check:new-line-citations,check:control-bytes,check:changeset-claims,check:pending-changeset-literals,check:doc-fences,check:readme-exports,check:test-path-rootsandcheck:handler-key-reads: exit 0.node scripts/check-changeset-no-major.mjsandnode scripts/check-changeset-presence.mjs: exit 0.check:sdui-registration-pins: NOT MEASURED. It exits 2 with "No console build to weigh", a prerequisite the run did not meet. No registration orsideEffectsarray moved.node scripts/check-governed-queue-guard.mjs --teston the 11 paths: NOT GOVERNED.Overlap
mainadvanced 6 commits afterd93e53f5, objectui#11477 (F3) among them.git merge-treeagainst it is clean. The one shared file ispackages/types/README.md, in a different section. Nothing onmaintouchescomplex.ts,complex.zod.tsorzod-mirror-parity.test.ts.SchemaNode.componentisDashboardWidgetSlotComponentSchema | SchemaNode, so it composes with whateverSchemaNodebecomes, and the card stays typed by the arm.Acceptance notes
valuerequired is enforced on the arm, not on everymetric-card. A{ type: 'metric-card', title }entry with no card-only input still parses on both faces through the widget arm. The widget arm parses it becauseDashboardWidgetTypeSchemaalso carries the closed component types (objectui#4600). Reported to the seat as a finding.componentslot. A card that fails the arm still parses on the tolerant face through theBaseSchemafallback, and the TypeScript union has the same corner. Only the strict face refuses it. This is recorded as a MEASURED LIMIT in the arm-7952 file, not ruled.type: 'card'widget withchildrenis refused by the validator today. The README twin was repaired by objectui#7035; the mdx was not. Reported to the seat as a finding.The session that made this change is
https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC.Generated by Claude Code