Repository navigation
Commit 72f3c74
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
File tree
- .changeset
- content/docs/references/ui
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages/spec/src
- migrations
- entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
710 | 710 | | |
711 | 711 | | |
712 | 712 | | |
713 | | - | |
| 713 | + | |
714 | 714 | | |
715 | 715 | | |
716 | 716 | | |
| |||
754 | 754 | | |
755 | 755 | | |
756 | 756 | | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
757 | 776 | | |
758 | 777 | | |
759 | 778 | | |
| |||
1116 | 1135 | | |
1117 | 1136 | | |
1118 | 1137 | | |
1119 | | - | |
1120 | | - | |
| 1138 | + | |
| 1139 | + | |
1121 | 1140 | | |
1122 | 1141 | | |
1123 | 1142 | | |
| |||
1145 | 1164 | | |
1146 | 1165 | | |
1147 | 1166 | | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
| 1181 | + | |
| 1182 | + | |
| 1183 | + | |
1148 | 1184 | | |
1149 | 1185 | | |
1150 | 1186 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
| 49 | + | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
65 | | - | |
| 65 | + | |
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
| |||
0 commit comments