Skip to content

Commit 72f3c74

Browse files
feat(spec)!: object-metric drillDown and compareTo take the shape the tile reads; object-grid columns exits its hold (#21464, stage 5) (#21673)
Part of #21464 Clause-②: yes (narrowing) ## What this does Stage 5 of the `ComponentPropsMap` `z.unknown()` close-out: the S-metric remainder and the exit of the `object-grid` `columns` hold, per the claim `5975220557`, the seat's S-metric answer `5972917488` (§1 `drillDown`: B; §2 `compareTo`: B), the staging answer `5963787404`, the holds' carrier answer `5969669440`, stage 4's landing `5973565761` and `domain:devx`'s pointer `5974144504`. Three narrowings, one ADR-0087 D3 entry each. Read points are at the `.objectui-sha` pin `ab1879721595` (this repo's pin), under `packages/plugin-dashboard/src/` or `packages/plugin-grid/src/` unless named. | member | was | now | read point at `ab1879721595` | |:--|:--|:--|:--| | `object-metric` `compareTo` | `z.unknown()` | strict `{ kind }` (module-private `ObjectMetricCompareToSchema`): `kind` is `DashboardWidgetSchema.shape.compareTo`'s own `kind` by reference; `dimension` refused by name with the prescription | `ObjectMetricWidget.tsx:522-523` (`shiftFilterByCompareTo`) and `:675` (`compareToTrendLabelKey`); both dispatch on `kind === 'previousYear'` alone (objectui `core/src/utils/compare-to.ts:104`, `:128`), and the file's header records `dimension` as never read on this path (`:28-33`) | | `object-metric` `drillDown` | `z.unknown()` | strict `{ enabled?, title?, target?, columns?, maxRows?, report? }` (module-private `ObjectMetricDrillDownSchema`): the first five are `ChartDrillDownSchema.shape`'s members by reference; `filter` and `mode` refused by name with objectui's prescriptions; `report` HELD at `z.unknown()` | `ObjectMetricWidget.tsx:696` (`isDrillEnabled`), `:708` (`resolveDrillTitle` against an empty click event), `:732-744` (the shared `DrillDownDrawer`: `target`, `columns`, `maxRows`, `report`); objectui's own `ObjectMetricDrillDownConfig` refuses `filter` and `mode` by name (`types/src/data-display.ts:2628`) | | `object-grid` `columns` | `z.array(z.unknown())`, held at stage 2 | `ListViewSchema.shape.columns` by reference (the reference stage 2 held it from) | `ObjectGrid.tsx:819` (`normalizeColumns`), `:3367` / `:3451` (the draw path keeps only an entry with a string `field`, reading `ListColumn`'s own members); `groupValueFormatter` (`:2979`) now reads the column's `type` alone, typed as `ListColumn` (`:3004-3007`), and the labels from `objectDefField?.options` only (`:3010`) | `enabled` and `title` keep the chart drill-down's defs (`_zod.def` identity, pinned) with describes of their own, because the chart's speak of a clicked segment and its `${event.*}` tokens, and a metric tile has no click context. No new member carries a default or a transform, so `ObjectMetricPropsParsed` still differs from the authored state on `filter` alone; a parsed grid column's `prefix.type` carries the list view's `'text'` default, as stage 2 originally recorded. No new export: both new schemas are module-private. ## The holds, re-read at `ab1879721595` - **`object-grid` `columns`: exit condition met.** Stage 2's records held the member on one read (`colOverride?.options || objectDefField?.options` at `89cad75d55`), and the writer test (`5966636964`) held it only if a renderer path at the pin reads `editable` or `options` off an authored column. objectui `b0bf413` (objectstack-ai/objectui#11544) retired the `options` read, and no read takes `editable` (or `reference`) off an authored column: the emitted columns are built key by key from `ListColumn` members (`:3676-3702`), and every object spread at `:4384`, `:4483`, `:4628`, `:4728` is of an EMITTED column. The three records that said the grid reads `columns[].options` are corrected: the docblock in `component.zod.ts`, the enumeration pin's held line (removed) and the list-family pin's header. One finding beside the exit, see Acceptance notes: the footer summary still reads four undeclared column keys. - **`object-metric` `drillDown.report`: stays held**, as ruled. Its docblock is re-anchored: the drawer draws a dataset-bound report when `isDatasetBoundReport` holds (`DrillDownDrawer.tsx:92`, used at `:115`; objectui#11506, `8366acc`), and objectui#11517 (`9ed8d0f`) refuses the `{ name }` arm. Expressed in the house shape: the enumeration pin's walk reaches it as `object-metric drillDown.report`, a `staged('objectui-held', …)` ledger line, and the `object-metric` stage leaves `STAGES` (its four members are typed). No fork was needed. ## The census A writer is a value written on the block: a page-component node (an object literal naming the type, a literal annotated or asserted with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside an object-literal `schema`, or the argument of a local helper that mounts one, with positional helper parameters resolved at every call site. Values resolve through same-file constants. Instrument: a TypeScript-AST walk over every `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.json`, `.yaml`, `.md` and `.mdx` file (fenced code in documents parsed too). Each static value was parsed through the row at this branch's source, and the grid's refused set was cross-checked against `ListViewSchema.shape.columns` directly (identical). | corpus | `compareTo` | `drillDown` | `object-grid` `columns` | |:--|:--|:--|:--| | objectstack `6ec54f00ba` (`examples/`, `packages/` with `packages/apps/`, `content/`, `skills/`, `apps/`, `docs/`) | 0 | 0 | 5 · 5 static · 5 parse | | objectui `ab1879721595` (whole tree, `git archive` of the pin) | 5 · 5 (3 distinct) · 4 parse · 1 refused | 26 · 25 static (13) · 23 parse · 2 refused · 1 not static | 362 · 353 static (147) · 312 parse · 41 refused · 9 not static | **objectstack writers.** The showcase's two grids (`examples/app-showcase/src/ui/pages/my-work.page.ts:65`, `command-center.page.ts:172`) and three test copies (`packages/lint/src/validate-component-props.test.ts:553`, `:577`, `packages/spec/src/ui/component.test.ts:3433`): all field-name strings, all parse, all drawn. `origin/main` moved four commits since the base (to `fea67065a3`); none touches `packages/spec`, and none adds a line naming `object-grid`, `object-metric`, `drillDown` or `compareTo`. **objectui writers, under the writer test.** Every value is a test fixture or a run-time hand-off. Each refused value: | refused value | where at the pin | drawn or probe | |:--|:--|:--| | `compareTo: { kind: 'previousYear', dimension: 'close_date' }` | `plugin-dashboard/src/__tests__/ObjectMetricWidget.compareTo.test.tsx:109` | probe: "carries a dimension without letting it touch the query" | | `drillDown: { enabled: true, filter: { stage: 'won' } }` and `{ enabled: true, mode: 'record' }` | `plugin-dashboard/src/__tests__/ObjectMetricWidget.drillDownRefusal-9002.test.tsx:56`, `:58` | compile-time refusal probes (`@ts-expect-error`) | | 17 columns keyed `accessorKey` / `header` | `columnDeclaredSpellingOnly` ×4, `columnSpellingDiagnosticRender` ×9, `ObjectGrid.malformedFilter-9050` ×3, `authoredColumnsFls-6799:409` | not drawn: `resolvesToDataColumn` needs `field`; the spelling diagnostic names them | | 5 columns keyed `name` | `projectionFls-6898:206`, `authoredColumnsFls-6799:333`, `columnIdentity` ×3 | not drawn by that key: the draw path reads `field` (the identity fold reads `name` for the projection and field security only, `field` winning) | | 14 columns carrying `editable: false` | `gridDependentValues-7165`, `lookupPickerKeys-7154` ×5, `relationalMetaCopySet-7166` ×4, `inlineEditExitMode` ×2, `inlineEditPersistence`, `inlineEditLookupRepro` | the column draws; no read takes `editable` off an authored column | | 1 column carrying `reference` | `ObjectGrid.linkCellAnchor.test.tsx:252` | the column draws; relational metadata comes from the object field (`applyRelationalMeta`), and the test asserts the cell stays plain | | 2 columns carrying `options` | `gridGroupingMembers-8071.test.tsx:293`, `:312` | probes: the tests assert the column `options` are NOT read | | 1 column with no `field` | `serverGroupedSelectIdentity-11105.test.tsx:162` | not drawn (`resolvesToDataColumn`) | | `columns: 5` | `types/src/__tests__/element-data-source-objectname-waiver-11117.test.ts:154` | a refusal probe through objectui's own mirror | The not-static values: one `drillDown` `it.each` over two report probes (both parse: `report` is held open); for `columns`, the run-time hand-offs in `plugin-view/src/ObjectView.tsx:2487`, `plugin-designer/src/ObjectManager.tsx:116` and `FieldDesigner.tsx:188` (the designers' lists are `{ field, label, width }`), and six test lists built from `{ field, label[, type] }` entries, which parse by construction. Stage 2 counted 40 refused `columns` values with a different instrument; the 41st here, the `reference` column, was present at `89cad75d55` too. ## Changes - `packages/spec/src/ui/component.zod.ts`: `ObjectMetricDrillDownSchema` and `ObjectMetricCompareToSchema` (module-private) with their docblocks and prescriptions; the two members; `columns` back to `ListViewSchema.shape.columns` with the exit docblock; the grid row and `ObjectGridPropsParsed` docblocks; imports of `ChartDrillDownSchema` (beside `ChartGroupBySchema`) and `DashboardWidgetSchema` (`dashboard.zod.ts` imports nothing that reaches `component.zod.ts`; the eager `gen:schema` build is green). - `packages/spec/src/ui/component-props-unknown-members.pin.test.ts`: the `drillDown`, `compareTo` and `columns[]` lines leave the ledger; `drillDown.report` joins as `objectui-held`; the `object-metric` stage leaves `STAGES`, whose `objectui-held` text now names the drill report. - `packages/spec/src/ui/component-metric-family-typed-members.pin.test.ts`: §1 seven new byte-identical parses (objectui's drill and comparison writers), §2 ten new refusals by `code` and path plus three prescription checks, §3 def identity for the five drill members and `kind`, and each shape's exact member set, §4 the two new D3 ids. - `packages/spec/src/ui/component-list-family-typed-members.pin.test.ts`: the `columns` cases stage 2's fix round removed come back (§1 two parses and the `prefix` default, §2 nine union refusals including `options`, `reference` and a footer `precision` hint, §3 def identity), and the header no longer says the grid reads a column's `options`. - `packages/spec/src/migrations/entries/semantic/18.ui-object-metric-compare-to-typed.ts`, `18.ui-object-metric-drill-down-typed.ts`, `18.ui-object-grid-columns-typed.ts` (new) and `packages/spec/src/migrations/registry.ts`: three hand-written step-18 rationale fragments at orders 71, 72 and 73 (70, #21632's, is the last taken on `main`), and the semantic region regenerated by `gen:migration-registry`. No D2 conversion: nothing stored needs rewriting, since page-component `properties` is not parsed on the save or load path. No `RETIRED_KEYS_BY_MAJOR` row: the refused keys are nested in member shapes, not top-level row keys. - Regenerated: `content/docs/references/ui/component.mdx` (`gen:docs`) and `docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md` (`gen:strictness-ledger`: `ui/` 194 → 196 sites, +2 strict). - `.changeset/21464-component-props-metric-rest-and-grid-columns-typed.md`: `@objectstack/spec` `minor`, BREAKING banner, the `Clause-②` line, the FROM → TO table, the census, and the ADR-0087 marker registering the three ids. ## Measurements All on the head `a10f94d7c4` (base `6ec54f00ba`; 11 files, +765 / −119, 884 changed lines, under the 5000 threshold). Heavy runs went through `scripts/pm/os-verify-lock.sh`, and every exit code was written to a file before it was read. - **`pnpm --filter @objectstack/spec test`**: exit 0 — Test Files 609 passed (609); Tests 18095 passed, 1 todo. - **`pnpm --filter @objectstack/spec typecheck`**: exit 0 — `check:test-typecheck: OK` (52 files / 246 errors / 135 signatures held, unmoved); `tsc -p tsconfig.test.json --listFilesOnly` names all three pins. - **`pnpm --filter @objectstack/spec build` and `check:generated --fix`**: the build is green (the eager `gen:schema` pass included); `--fix` regenerated only what it proved stale (`gen:docs`, `gen:strictness-ledger`), and the derived `check:generated` then exits 0. - **Consumers** (the row's import side and the one example that authors these blocks): `pnpm --filter @objectstack/lint exec vitest run src/validate-component-props.test.ts` — 51 passed (after building lint's closure; the first try answered `Failed to resolve entry for package "@objectstack/formula"`, an unbuilt dependency, not a measurement); `pnpm --filter @objectstack/lint test` — Test Files 119 passed, Tests 5627 passed; `pnpm --filter @objectstack/example-showcase validate` (`objectstack validate`) — "Validation passed". - **The public door, lit both ways** (`validateComponentProps` from `@objectstack/lint` over the built spec, one block per page): a drill with `enabled` / `target` / `columns` / `maxRows`, a held `report`, `compareTo: { kind: 'previousYear' }`, the showcase's string columns and a `{ field, label, width }` column report nothing; a drill `filter` reports `component-props-unknown-key` at `properties.drillDown.filter`, a comparison `dimension` at `properties.compareTo.dimension`, a bare `compareTo: 'previousYear'` `component-props-invalid` at `properties.compareTo`, an `accessorKey` column `component-props-invalid` at `properties.columns`, and a column `options` `component-props-unknown-key`. - **Ablation**, one leg per narrowing, on the committed tree (`node scripts/ablation-replace.mjs` in wrap mode inside a script with its own `EXIT INT TERM` restore trap on absolute paths, HEAD-blob comparison, an empty hash read as failure). Each leg reverts one member to its former `z.unknown()` form and runs the three pins (175 tests): - `compareTo` → `z.unknown().optional()`: anchor x1 → x0, blob `b3bba43a35` → `4ef63a8559`. Red: 8 failed, 167 passed. The enumeration pin's §1 received exactly `[ "object-metric compareTo" ]` and its census-equals-ledger control read 92 against 91; the companion pin failed its four comparison refusals, the `dimension` prescription and the `kind` def identity. - `drillDown` → `z.unknown().optional()`: blob → `51d55b8e7b`. Red: 12 failed, 163 passed. §1 received `[ "object-metric drillDown" ]`, and its second half `[ "object-metric drillDown.report" ]` (a ledger line outliving its member); the companion pin failed its six drill refusals, the two prescriptions and both §3 cases. - `columns` → `z.array(z.unknown()).optional()`: blob → `8f09c64675`. Red: 13 failed, 162 passed. §1 received `[ "object-grid columns[]" ]`, 92 against 91; the list-family pin failed the `prefix`-default parse, its nine `columns` union refusals and the def identity. - Restore after every leg and in the trap: blob `b3bba43a35` equals the HEAD blob and `git diff HEAD` is empty. Green control after the three legs: 175 passed (175). The pins import `./component.zod` from source, so no build or dist preflight sits between mutation and run. - **Derived gates**: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) — 114 commands; `--ran` — "Run reconciliation — 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN", exit 0, all 114 exit 0. Six first answered PREREQUISITE NOT MET (exit 3) in the fresh worktree (`@objectstack/lint` `check:doc-formula-expressions` and `check:doc-security-posture`, spec `check:skill-examples`, `check:docs-transcript-drift`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`); after one `turbo run build` (72 tasks, 71 cached) each exits 0, and those reruns are what the reconciliation records. - **Changeset gates with this body as the `pull_request` payload**: `check-changeset-no-major` exit 0 — "LEVEL AXIS: this PR declares clause-② `yes (narrowing)`, and no package whose `packages/**/src/**` it moves is graded `patch`"; `check-adr-0087-registration` exit 0 — "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition"; `check-empty-changeset` exit 0. - **Narrowed lint**: `eslint --no-inline-config --format json` over the 8 changed TypeScript files — 8 files, 0 errors, 0 warnings. Population from eslint's own `--print-config` (the 8 resolve a config; the changeset, `component.mdx` and the strictness counts page resolve `undefined`); count from the JSON output; invariance: `eslint.config.mjs` never enables type-aware linting (its own text, about lines 326-328), so this diff cannot move a verdict on an untouched file. The full `pnpm lint` is CI's. - **NOT MEASURED**: the Console Pin Gate, the Dogfood Regression Gate and the full `pnpm lint`, reason: CI-owned; objectui was read at the pin, not built or run against this spec. ## Acceptance notes - **objectui's grid footer still reads four column keys `ListColumn` does not declare.** `plugin-grid/src/useColumnSummary.ts:558-568` at the pin formats a column's summary with `(col as any).currency`, `.defaultCurrency`, `.precision` and `.scale` ahead of the field's (the same file reads `currencyConfig` and `max` off the field only, saying a column-level read "would be a second, undeclared spelling"). The read was there at `89cad75d55` too. No measured writer authors one on an `object-grid` column, so under the writer test it holds nothing, and a list view's columns (the same entry) already refuse all four; with this PR the grid row refuses them as well (pinned: the `precision` case in the list-family pin). Reported for the seat as an objectui finding, the shape objectstack-ai/objectui#11544 had. - **The drill `report`'s contract may be cheaper than the hold suggests.** At the pin objectui types `DrillDownConfig.report` as this spec's own `ReportSchema` author input (`types/src/data-display.ts:23-24`, `:2592`, `SpecReportInput`), and the drawer applies the spec report's `runtimeFilter`. So the S-objectui-held step for `report` may reduce to the spec declaring the member by reference to `ReportSchema`. Not done here, as ruled; raised for the seat. - **objectui's next `@objectstack/spec` bump:** its block schemas take these rows by reference (`ObjectMetricBlockSchema`, `ObjectGridBlockSchema`, through `propsBag`), but no objectui fixture that authors a refused value parses through them: the refused metric values mount the widget directly, and the refused grid columns mount `ObjectGrid` directly. objectui source does not import the narrowed `ObjectMetricProps` / `ObjectGridProps` types. Not built here: the Console Pin Gate builds objectui at the pin against `main`, and its filter does not list `packages/spec/src/**`. - **`check-widening-tells --declaration no` exits 4 with seven T1 tells**, every one a key inside a former `z.unknown()` bag (the drill's six members, `compareTo.kind`), the shape the gate rules a true refusal; `--declaration yes` exits 0. The claim, this body and the changeset declare `yes (narrowing)`. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8ad9694 commit 72f3c74

11 files changed

Lines changed: 765 additions & 119 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: an `object-metric` page block's `drillDown` and `compareTo`, and an `object-grid` page block's `columns`, take the shape each block reads instead of any value (#21464)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered ui-object-metric-compare-to-typed, ui-object-metric-drill-down-typed, ui-object-grid-columns-typed -->
10+
11+
**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path.
12+
13+
**`@objectstack/spec`**
14+
15+
- **`object-metric` `compareTo` takes the tile's read, `{ kind }`.** It was `z.unknown()`, although the tile reads `kind` alone: a bare `'previousYear'` or a kind outside the two compared against the previous period, and a `dimension` was carried and never read. `kind` is the dashboard widget comparison's own vocabulary by reference (`previousPeriod`, `previousYear`). `dimension` is refused by name, with the prescription: this inline tile shifts the date macros in its own `filter` and never reads a dataset time dimension, so state the window on the tile's `filter`.
16+
- **`object-metric` `drillDown` takes the tile's read.** It was `z.unknown()`: a drill `filter`, a `mode` or a misspelled member passed and was ignored. Its five list members — `enabled`, `title`, `target` (`drawer`, `dialog`, `navigate`), `columns` (field names) and `maxRows` (a positive whole number) — are the chart drill-down's own by reference. `filter` and `mode` are refused by name: a metric tile has no click event for a drill filter to resolve against (the drilled list is scoped by the metric's own `filter`), and no row for `mode` to open as a record. The chart's drill-down shape is not taken whole, because it declares `filter`.
17+
- **The drill-down's `report` is not narrowed** and still accepts any value. The tile draws a dataset-bound report through the shared drill drawer, but no spec drill shape declares a `report` member yet; it is typed once the spec declares the drill report.
18+
- **`object-grid` `columns` takes the list view's own `columns`**: all field-name strings, or all column entries `{ field, label?, width?, align?, hidden?, sortable?, resizable?, wrap?, type?, pinned?, summary?, prefix?, link?, action? }`. It was an array of `z.unknown()`, held at the second stage because the grid's group headers drew a column's `options`, which the column entry does not declare. The renderer has since retired that read (the group-header labels come from the object field's `options` only), so the hold is lifted. A column keyed `accessorKey` / `header` or `name`, a column with no `field`, a list mixing strings and entries, or a column key the entry does not declare (`editable`, `options`, `reference`, or a footer number hint such as `currency` or `precision`) is refused.
19+
- **`ObjectMetricProps` and `ObjectGridProps`** carry these types on the three members instead of `unknown`. A parsed column's `prefix.type` now carries the list view's `'text'` default.
20+
21+
## FROM → TO
22+
23+
| you wrote | write instead |
24+
|:--|:--|
25+
| `object-metric` `compareTo: 'previousYear'` | `compareTo: { kind: 'previousYear' }` |
26+
| `object-metric` `compareTo: { kind: 'previousYear', dimension: 'close_date' }` | `compareTo: { kind: 'previousYear' }`, with the window stated on the tile's own `filter` (date macros such as `{current_quarter_start}`) |
27+
| `object-metric` `compareTo: { kind: 'previousQuarter' }` | `previousPeriod` (the equal-length window before the one the filter resolves to) or `previousYear` |
28+
| `object-metric` `drillDown: { filter: { stage: 'won' } }` | delete it, and scope the metric with its own `filter` (the drilled list follows it) |
29+
| `object-metric` `drillDown: { mode: 'record' }` | delete it — a metric always lists the records behind its number |
30+
| `object-metric` `drillDown: { limit: 50 }` | `drillDown: { maxRows: 50 }` |
31+
| `object-grid` `columns: [{ accessorKey: 'amount', header: 'Amount' }]` | `columns: [{ field: 'amount', label: 'Amount' }]` |
32+
| `object-grid` `columns: [{ name: 'salary' }]` | `columns: [{ field: 'salary' }]` |
33+
| `object-grid` `columns: ['name', { field: 'amount', width: 120 }]` | all entries: `[{ field: 'name' }, { field: 'amount', width: 120 }]` |
34+
| `object-grid` a column `editable`, `options`, `reference`, `currency` or `precision` | delete the key: inline editing is the grid's own `editable`, and option labels, relational metadata and number formats are the object field's |
35+
36+
The one-line fix: write each member as the table above shows. No conversion is registered, because a refused value has no rewrite that both keeps what the block shows today and honours what the author wrote; the D3 entries `ui-object-metric-compare-to-typed`, `ui-object-metric-drill-down-typed` and `ui-object-grid-columns-typed` carry that judgment.
37+
38+
## Who is affected, measured
39+
40+
A writer is a value written on the block: a page-component node (an object literal naming the type, a literal annotated with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside `schema={{…}}`, or the argument of a local test helper that mounts one (positional helper parameters resolved at every call site). Values resolve through same-file constants. Each static value was parsed through the row.
41+
42+
- **objectstack** at `6ec54f00ba`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/`, `apps/` and `docs/`: 5 `object-grid` `columns` values, all field-name strings (the showcase's `my-work.page.ts` and `command-center.page.ts` grids, and three test copies), all parse. No `object-metric` `drillDown` or `compareTo` is authored.
43+
- **objectui** at the `.objectui-sha` pin `ab1879721595`, every one a test fixture or a run-time hand-off:
44+
- `compareTo`: 5 values, 4 parse. The refused one is the test that asserts a `dimension` never touches the query (`ObjectMetricWidget.compareTo.test.tsx`).
45+
- `drillDown`: 26 values, 25 static; 23 parse. The two refused are the compile-time refusal probes for `filter` and `mode` (`ObjectMetricWidget.drillDownRefusal-9002.test.tsx`). The one that is not static carries a report probe, which parses, since `report` stays open.
46+
- `object-grid` `columns`: 362 values, 353 static (147 distinct); 312 parse. Each of the 41 refused is a test fixture whose refused key or entry the grid does not draw: 17 columns keyed `accessorKey` / `header` and 5 keyed `name` (the column-spelling diagnostic, identity and field-security tests); 14 columns carrying `editable: false` and 1 carrying `reference`, keys no read takes off an authored column; 2 carrying `options`, the tests asserting that the group headers no longer read them; 1 column with no `field`; and 1 numeric `columns` refused by objectui's own mirror. The 9 values that are not static are 3 run-time hand-offs (the object view and two designer grids) and 6 test lists built from `{ field, label, type }` entries, which parse.
47+
- **Deployed metadata** was not measured.

‎content/docs/references/ui/component.mdx‎

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ Sort field and direction pair
710710
| **title** | `string \| Record<string, string>` | optional | Fallback for `label` (the renderer reads `label \|\| title`) |
711711
| **description** | `string \| Record<string, string>` | optional | One line of help text drawn above the grid's rows — a string, or an inline locale map resolved against the display locale |
712712
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string }` | optional | What the grid draws instead of an empty table: `{ title, message, icon }` — the list view's own empty-state shape |
713-
| **columns** | `any[]` | optional | Columns: field names or column definition objects |
713+
| **columns** | `string[] \| { field: string; label?: string \| Record<string, string>; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Columns — all field-name strings, or all column entries `{ field, label?, width?, align?, hidden?, sortable?, … }`, the same union a list view's `columns` declares. One spelling per list: an array mixing strings and column objects is refused |
714714
| **fields** | `string[]` | optional | Field-name fallback the grid reads when `columns` is absent — bare field names (`['name', 'amount']`); write column decoration such as `label` or `width` on `columns` |
715715
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. THE key, singular — not the plural misspelling. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` |
716716
| **defaultFilters** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Legacy base-filter fallback, read only when `filter` is absent — the SAME ViewFilterRule array form `[{ field, operator, value }, ...]` as `filter`, lowered through the same sink. Prefer `filter`. The MongoDB-style record form, a bare string and an ObjectQL AST tuple array are refused — see migration `object-grid-default-filters-rule-array` |
@@ -754,6 +754,25 @@ Sort field and direction pair
754754
| **message** | `string \| Record<string, string>` | optional | Line of text below the heading |
755755
| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph |
756756

757+
### Nested Shape: `ObjectGridProps.columns[number]`
758+
759+
| Property | Type | Required | Description |
760+
| :--- | :--- | :--- | :--- |
761+
| **field** | `string` | ✅ | Field name (snake_case) |
762+
| **label** | `string \| Record<string, string>` | optional | Display label override |
763+
| **width** | `number` | optional | Column width in pixels |
764+
| **align** | `Enum<'left' \| 'center' \| 'right'>` | optional | Text alignment |
765+
| **hidden** | `boolean` | optional | Hide column by default |
766+
| **sortable** | `boolean` | optional | Allow sorting by this column |
767+
| **resizable** | `boolean` | optional | Allow resizing this column |
768+
| **wrap** | `boolean` | optional | Allow text wrapping |
769+
| **type** | `string` | optional | Renderer type override (e.g., "currency", "date") |
770+
| **pinned** | `Enum<'left' \| 'right'>` | optional | Pin/freeze column to left or right side |
771+
| **summary** | `Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …> \| { type: Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …>; field?: string }` | optional | Footer aggregation for this column — the function alone, or `{ type, field }` to aggregate another field |
772+
| **prefix** | `{ field: string; type?: Enum<'badge' \| 'text'> }` | optional | Field rendered inline before this cell value |
773+
| **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) |
774+
| **action** | `string` | optional | Registered Action ID to execute when clicked |
775+
757776
### Nested Shape: `ObjectGridProps.filter[number]`
758777

759778
View filter rule
@@ -1116,8 +1135,8 @@ Sort field and direction pair
11161135
| **variant** | `Enum<'card' \| 'bare'>` | optional | Layout variant |
11171136
| **fallbackValue** | `string \| number` | optional | Static value shown when no data source is available |
11181137
| **trend** | `{ value: number; label?: string \| Record<string, string>; direction?: Enum<'up' \| 'down' \| 'neutral'> }` | optional | Static trend badge `{ value, label?, direction? }` — `value` painted as a percentage, `direction` up / down / neutral. A `compareTo`-derived trend replaces it |
1119-
| **drillDown** | `any` | optional | Click-through drill config — opens the underlying records |
1120-
| **compareTo** | `any` | optional | Period-over-period comparison (`{ kind: 'previousPeriod' \| 'previousYear' }`) |
1138+
| **drillDown** | `{ enabled?: boolean; title?: string; target?: Enum<'drawer' \| 'dialog' \| 'navigate'>; columns?: string[]; … }` | optional | Click-through drill config `{ enabled?, title?, target?, columns?, maxRows?, report? }` — opens the records behind the number, scoped by the metric's own `filter`; a present block is on unless `enabled: false`. `filter` and `mode` are refused: a metric tile has no click context and no row |
1139+
| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'> }` | optional | Period-over-period comparison `{ kind }` — `previousPeriod` or `previousYear`, shifting the date macros in the tile's own `filter`. `dimension` is refused: this tile never reads a dataset time dimension |
11211140

11221141
### Nested Shape: `ObjectMetricProps.aggregate`
11231142

@@ -1145,6 +1164,23 @@ View filter rule
11451164
| **label** | `string \| Record<string, string>` | optional | Badge caption — a string or an inline locale map. The tile-level `description` outranks it in the one caption slot they share |
11461165
| **direction** | `Enum<'up' \| 'down' \| 'neutral'>` | optional | Arrow beside the value; omit it for no arrow |
11471166

1167+
### Nested Shape: `ObjectMetricProps.drillDown`
1168+
1169+
| Property | Type | Required | Description |
1170+
| :--- | :--- | :--- | :--- |
1171+
| **enabled** | `boolean` | optional | Turn the tile's drill on or off; the block being present already means on, so this is only needed to force it off |
1172+
| **title** | `string` | optional | Drill drawer/dialog heading; defaults to the tile's own `title`, then its `label`. A metric tile has no click context, so an `${event.*}` token here resolves to nothing |
1173+
| **target** | `Enum<'drawer' \| 'dialog' \| 'navigate'>` | optional | Where the drilled list opens: 'drawer' (default, side sheet), 'dialog' (centered modal), or 'navigate' (skip the in-place view and open the object's full list page; needs host drill navigation, else falls back to 'drawer') |
1174+
| **columns** | `string[]` | optional | Field names to show as columns in the drilled list (default: the table's own columns) |
1175+
| **maxRows** | `integer` | optional | Rows per page in the drilled list |
1176+
| **report** | `any` | optional | Drill into a report instead of the record list — not typed on this row yet: the tile draws a dataset-bound report here, but no spec drill shape declares a `report` member yet (the chart's drill-down refuses it) |
1177+
1178+
### Nested Shape: `ObjectMetricProps.compareTo`
1179+
1180+
| Property | Type | Required | Description |
1181+
| :--- | :--- | :--- | :--- |
1182+
| **kind** | `Enum<'previousPeriod' \| 'previousYear'>` | ✅ | Comparison window: previousPeriod (equal-length, immediately before) or previousYear (−1 calendar year) |
1183+
11481184

11491185
---
11501186

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The `strict` column is the one the campaign schedules against; it counts both th
2121

2222
| Dir | Sites | strict | passthrough | catchall | strip |
2323
|---|---|---|---|---|---|
24-
| `ui/` | 194 | 183 | 4 | 0 | 7 |
24+
| `ui/` | 196 | 185 | 4 | 0 | 7 |
2525

2626
## `ui/` — sites
2727

@@ -36,7 +36,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit
3636
| `app.zod.ts` | 19 |
3737
| `bulk-action.zod.ts` | 4 |
3838
| `chart.zod.ts` | 8 |
39-
| `component.zod.ts` | 64 |
39+
| `component.zod.ts` | 66 |
4040
| `dashboard.zod.ts` | 11 |
4141
| `dataset.zod.ts` | 4 |
4242
| `i18n.zod.ts` | 1 |
@@ -46,23 +46,23 @@ classify and is not listed (it becomes reportable the day it grows its first sit
4646
| `sharing.zod.ts` | 1 |
4747
| `view.zod.ts` | 60 |
4848
| `widget.zod.ts` | 1 |
49-
| **total** | **194** |
49+
| **total** | **196** |
5050

5151
## `ui/` — open
5252

5353
Per file, how many of its sites still silently discard unknown keys. The `Class`
5454
column that decides the bucket split is hand-written in the ledger; the arithmetic
5555
over it is here.
5656

57-
**7 strip of 194**, in 4 file(s).
57+
**7 strip of 196**, in 4 file(s).
5858

5959
| File | Strip | Sites |
6060
|---|---|---|
6161
| `action-params.zod.ts` | 1 | 1 |
6262
| `app.zod.ts` | 1 | 19 |
6363
| `view.zod.ts` | 4 | 60 |
6464
| `widget.zod.ts` | 1 | 1 |
65-
| **total** | **7** | **194** |
65+
| **total** | **7** | **196** |
6666

6767
| Bucket | Sites |
6868
|---|---|

0 commit comments

Comments
 (0)