Repository navigation
Commit 5a9292e
Fixes #21229
Clause-②: yes
Dispatched by the PM claim `5943166878` (PM loop round 1, `domain:spec`
seat 1), on the triage ruling `5939380297`.
`ComponentPropsMap['object-grid'].exportOptions` was `z.unknown()`. It
now takes `ListViewExportOptionsSchema`, the list view's strict
five-member export options object, **by identity**. It does not take the
list view's union, whose legacy bare-array arm lifts to `{ formats }`. A
bare array is refused with the object form named. The changeset carries
the `(narrowing)` arm, the BREAKING banner at `minor`, and the ADR-0087
marker `registered ui-object-grid-export-options-closed`. It is D3 only;
the reading is below.
## What changed
- **New non-barrel module
`packages/spec/src/ui/list-view-export-options.ts`.** It holds the
export options block, moved verbatim out of `view.zod.ts`: the
retired-`'pdf'` prescription, the `csv` / `xlsx` / `json` format enum,
and the strict five-member object.
- There is one addition. The object's own error map answers an
`invalid_type` on an array input with a prescription naming `{ formats:
['csv', 'xlsx'] }`.
- It is the object's map because that is the only map a type failure at
this position consults. A wrapper's or the enclosing row's map is never
reached, and an object-level refinement never runs once a property has
failed its type.
- The object is built exactly as `strictObject()` builds one:
`closedObject(z.object(shape, { error }).strict())` with the same
registered `strictObjectError` declaration. The `prime` handle is
forwarded, so `closedObject`'s terminal unknown-key contract is kept.
- **New non-barrel module `packages/spec/src/ui/view-history.ts`.**
`VIEW_HISTORY` moved here, verbatim, so the moved block keeps the
refusal sentence it has always carried. `view.zod.ts` imports both
modules, and its 53 `VIEW_HISTORY` uses are unchanged.
- **`component.zod.ts`:** `exportOptions:
ListViewExportOptionsSchema.optional()`. The "Unvalidated here" describe
text is gone. A docblock records the ruling and the door reading.
- **D3 entry** `18.ui-object-grid-export-options-closed.ts`, regenerated
into `migrations/registry.ts` by `gen:migration-registry`.
- **`STEP18_RATIONALE` fragment**
`ui-object-grid-export-options-closed`, `order: 59`. `main` holds 57 (PR
#21244) and 58 (PR #21240) at the merge `b91e40bc89`. It is inserted at
its sorted position.
- **Pin file `component-object-grid-export-options-members.pin.test.ts`,
rewritten.** The #17166 version asserted the key was still unvalidated,
so that this change would red there and be decided deliberately; it did.
The new file holds the triage pins:
- **identity:** the row's inner schema is the very instance the list
view's union holds as its object arm, read from the union rather than
imported by name, and it is not the union;
- **bare array refused:** `invalid_type` at `exportOptions`, with the
object form named. The control is the same array on a list view, which
still lifts to `{ formats: ['csv'] }`;
- **object form accepted:** all five members, `{}` and absent;
- **a format outside the enum refused:** `invalid_value` at
`exportOptions.formats.1`, and `pdf` with its retirement text;
- **an undeclared key refused:** `unrecognized_keys` at `exportOptions`,
naming `this export options block` and the `maxRecord` → `maxRecords`
rename.
- **Regenerated:** `content/docs/references/ui/component.mdx` (the row's
type, and a new nested-shape table) and
`docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md` (see
the strictness-ledger note under Acceptance notes).
## Measured: why a non-public module, not an export (Zone 2 item 1)
- `ui/index.ts` is `export * from './view.zod'`, so any export from
`view.zod.ts` is public.
- **Measured.** I added `export { ListViewExportOptionsSchema }` to
`view.zod.ts` through `scripts/ablation-replace.mjs` in wrap mode, then
rebuilt spec. The first attempt was refused by the tool as a no-op,
because the anchor was inside the replacement; it was re-anchored and
re-run. Results:
- `check:api-surface` turned red with `./ui +
ListViewExportOptionsSchema (const)` ("0 breaking, 1 added"), and
`check:export-origins` turned red;
- the build also wrote `ui/ListViewExportOptions` into
`json-schema.manifest/ui.json` (a new published JSON Schema def) and 5
`ui/ListViewExportOptions:*` rows into `authorable-surface/ui.json`.
- The file was restored, blob equal to HEAD and `git diff HEAD` empty.
The two dirtied artifacts were restored with `git checkout HEAD`, and
after a clean rebuild `check:api-surface` reads "unchanged ✓".
- **objectui.** At the pin `31971ff1e28f`,
`packages/types/src/__tests__/export-options-spec-parity.test.ts`
asserts `expect(specUi.ListViewExportOptionsSchema).toBeUndefined()`. An
export would red that test at objectui's next spec bump.
- **With the non-barrel module**, `check:api-surface`,
`check:export-origins`, `check:declaration-map` and
`check:authorable-surface` are unchanged. One declaration; zero public
surface. This is the `analytics-column-reference.ts` /
`analytics-carrier-filter.ts` / `section-group-reference.ts` precedent.
## Measured: the ADR-0087 disposition is D3 only (Zone 2 item 2)
Every pre-PR shape that the pinned renderer handles and the PR refuses
was put through every door, before (`f148852752`) and after
(`8b2c2bb558`, the schema commit, same `src/ui` as HEAD). The reading
uses the built `dist`, `getMetadataTypeSchema('page')` (the save door's
per-type parse), `defineStack`, the props gate `validateComponentProps`,
and `runtimeAuthoringRulesFor('page')`.
| shape (on an `object-grid` node's `properties`) | at the pinned
renderer (`ObjectGrid.tsx` at `31971ff1e28f`) | row before → after |
save door | `defineStack` | props gate after |
|:--|:--|:--|:--|:--|:--|
| `['csv']` | `!!exportOptions` is true, so the menu shows the csv/json
default; the list is dropped | accept → `invalid_type` | ok | accepted |
warning `component-props-invalid` |
| `{ formats: ['csv'], foo: 1 }` | renders; `foo` is never read | accept
→ `unrecognized_keys` | ok | accepted | warning
`component-props-unknown-key` |
| `{ formats: ['pdf'] }` / `['xml']` | the value is hidden from the menu
with a `console.warn` | accept → `invalid_value` | ok | accepted |
warning `component-props-invalid` |
| `null` | `!!null` is false, so no menu, the same as absent | accept →
`invalid_type` | ok | accepted | warning `component-props-invalid` |
| `true`, `'csv'`, `{ formats: 'csv' }`, `{ maxRecords: -1 }`, `{
streaming: 'false' }` | renders, with the default or a misread | accept
→ refused | ok | accepted | warning `component-props-invalid` |
- **Lit controls, same run.** An undeclared `object-grid` prop gives the
props-gate warning `component-props-unknown-key`. An undeclared page key
is REFUSED at the save door and THROWS in `defineStack`. The accepted
object forms (`{ formats: ['csv','xlsx'] }`, `{}`, all five, absent)
give no finding anywhere.
- **The runtime publish gate for `page`** runs one rule,
`validatePresetComparands`. The props gate is `tier: 'advisory'`,
`surfaces: CLI_ONLY`.
- **The renderer.** At the pin, objectui runs its zod mirror only in the
`objectui validate`/`check` CLI. `SchemaRenderer` runs a structural
`validateSchema` in dev only.
- **So nothing on the save or load path refuses any of these shapes.** A
stored page saves and loads, and no conversion has a load-path refusal
to pre-empt.
- **Lossless rewrites.** The undeclared key, `pdf` and `null` have one
(delete it), but nothing stops loading. The bare array has none that
both keeps today's menu (`{}`) and honours the author's list (`{ formats
}`), and that choice is the upgrader's, which the D3 entry states.
Triage also ruled out a lift.
## Census (Zone 2 item 3), on `f148852752`
- **Zero `object-grid` blocks author `exportOptions`** in `examples/**`,
the package fixtures, `content/docs/**` and `skills/**`.
- **Control:** the same census finds 10 authored `object-grid` blocks.
Nine are TypeScript nodes (two showcase pages and seven package-test
fixtures). One is the YAML example in
`content/docs/protocol/objectui/layout-dsl.mdx`. The `exportOptions`
matcher is lit on the 4 list-view authorings: `app-crm` ×2, the showcase
task view, and the lint showcase fixture.
- **`skills/**`:** nothing teaches `exportOptions`.
`skills/objectstack-ui/rules/pages.md` names `object-grid` in prose
only, so no Tier H follow-up is owed.
- Nothing needed respelling.
## objectui (Zone 2 item 4, Post-Task Checklist #4)
Nothing the pinned objectui imports moves:
- `check:api-surface` is unchanged, and no export was removed or
renamed.
- `ListViewSchema.shape.exportOptions` is still a two-arm union with one
five-key object arm, which is what objectui's
`SPEC_EXPORT_OPTIONS_OBJECT_SHAPE` peel reads.
- `ListViewExportOptionsSchema` is still not exported, so objectui's
floor test holds.
- No pinned objectui test parses the row with `exportOptions` and
expects a bare array to pass. Six pinned tests mention `exportOptions`
near the row; two of them carry only prose that will go stale (see
Acceptance notes).
## Changes outside the row, stated
- **The list view's nested message.** The list view's `exportOptions`
accepts and lifts exactly what it did, and its top-level messages are
unchanged. Only when a bare array also fails the array arm (`['docx']`)
does the object arm's nested branch message read the new prescription
instead of `Invalid input: expected object, received array`. Measured on
`dist`. Both carriers read one declaration, and the text is worded to be
true on both. The changeset says so.
## Tests and gates: all on `032865c93c` (HEAD, the merge of
`origin/main` `b91e40bc89` through `os-regen-merge.sh`)
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: **597 files passed, 17475 passed, 1 todo**.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
`check:test-typecheck` is OK (52 files / 246 errors / 135 pinned
signatures, unchanged), so the rewritten pin compiles under
`tsconfig.test.json`.
- **Contract-face fixture triage.** These are consumers of the spec, the
downstream (`...@objectstack/spec`) direction, limited to the three the
dispatch names:
- `@objectstack/lint` (the props gate): **119 files / 5515 tests
passed**;
- `@objectstack/metadata-protocol`: **200 passed + 3 skipped files /
2973 passed + 19 skipped tests**;
- `@objectstack/spec`: as above.
- No fixture in any of them needed a change.
- `pnpm --filter @objectstack/spec check:generated`: 15 of 15 up to date
after `--fix` regenerated the 2 it proved stale (`check:docs`,
`check:strictness-ledger`).
- **`dispatch-gates.mjs --commands`** (no paths) derived 112 commands;
all 112 ran and exited 0.
- Six first exited 3 with PREREQUISITE NOT MET, because `lint`,
`client-react` and `objectql` had no `dist`:
`check:doc-formula-expressions`, `check:doc-security-posture`,
`check:skill-examples`, `check:docs-transcript-drift`,
`check:dual-build-cjs-loads` and `check:lean-entry-closure`. Each re-ran
green once the dists existed.
- `--ran` with `cmd :: exit N`: "112 derived, 112 run, 0 NOT-MEASURED, 0
UNRUN (a DERIVED zero)".
- **Reverse verification, cross-package type.** A scratch `probe.ts`
typed against the rebuilt `dist/ui/index.d.mts`, using
`ObjectGridProps`, was compiled with `tsc`:
- `exportOptions: ['csv']` gives `TS2559`;
- `{ formats: ['xml'] }` gives `TS2322`;
- `{ formats: ['csv'], maxRecord: 1 }` gives `TS2561`;
- the control `{ formats: ['csv','xlsx'], maxRecords: 10 }` compiles.
- At the base the key was `unknown` (the reference page rendered `any`).
- **NOT MEASURED, declared to CI:** the 6 path-scheduled CI jobs (Test
Core shards, Temporal Conformance, Dogfood Regression and Verify, Build
Core, Build Docs), the 4 workspace type-check lanes, and the 54
artifact-roster families the derivation lists outside its total.
## Acceptance notes
- **The strictness ledger's scope.** The ledger
(`check:strictness-ledger`) counts `.zod.ts` files only. The moved block
is one CLOSED site and now lives in a non-`.zod.ts` module, like the
other non-barrel helpers, so the regenerated `ui/` counts read 189 → 188
sites and 179 → 178 strict. The strip count, which is the ratchet's
target, is unchanged at 7. Naming the module `.zod.ts` would have kept
the site counted, at the price of a hand-written ledger row and of
opting a non-public module into the `.zod.ts` generators' discovery.
- **Stale prose in objectui.** At the pin, two objectui test files say
the spec row is `z.unknown()`: the
`ObjectGrid.exportOptionsKeys.test.ts` docblock, and the
`object-grid.exportOptions` row in
`registry-inputs-spec-parity.test.ts`'s `MEMBER_PINS`. Neither asserts
it, so nothing goes red. Once the spec version carrying this lands, both
describe a past state. Carrier: objectui's next spec pin-bump PR. Noted,
not filed.
- **objectui's `properties` bag.** On objectui `origin/main`, the bag
arm from objectui#11399 judges the bag by
`ComponentPropsMap['object-grid']` by reference. Once objectui installs
this spec, its `objectui validate` refuses a bare array in the bag, as
its flat mirror has since objectui#7762. No objectui change is owed
beyond the pin bump.
- **A public export, if objectui later wants one.** objectui's
export-options parity test says "When upstream exports the symbol,
derive from it and delete both the mirror". Publishing
`ListViewExportOptionsSchema` stays possible as its own decision. It
moves `api-surface` (+1 const), `json-schema.manifest` (+1 def) and
`authorable-surface` (+5 rows), as measured above. It is not done here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent dcc5ef4 commit 5a9292e
10 files changed
Lines changed: 442 additions & 178 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
724 | 724 | | |
725 | 725 | | |
726 | 726 | | |
727 | | - | |
| 727 | + | |
728 | 728 | | |
729 | 729 | | |
730 | 730 | | |
| |||
772 | 772 | | |
773 | 773 | | |
774 | 774 | | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
775 | 785 | | |
776 | 786 | | |
777 | 787 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
44 | 44 | | |
45 | 45 | | |
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 | | |
| |||
| 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 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6008 | 6008 | | |
6009 | 6009 | | |
6010 | 6010 | | |
| 6011 | + | |
| 6012 | + | |
| 6013 | + | |
| 6014 | + | |
| 6015 | + | |
| 6016 | + | |
| 6017 | + | |
| 6018 | + | |
| 6019 | + | |
| 6020 | + | |
| 6021 | + | |
| 6022 | + | |
| 6023 | + | |
| 6024 | + | |
| 6025 | + | |
| 6026 | + | |
| 6027 | + | |
6011 | 6028 | | |
6012 | 6029 | | |
6013 | 6030 | | |
| |||
18698 | 18715 | | |
18699 | 18716 | | |
18700 | 18717 | | |
| 18718 | + | |
| 18719 | + | |
| 18720 | + | |
| 18721 | + | |
| 18722 | + | |
| 18723 | + | |
| 18724 | + | |
| 18725 | + | |
| 18726 | + | |
| 18727 | + | |
| 18728 | + | |
| 18729 | + | |
| 18730 | + | |
| 18731 | + | |
| 18732 | + | |
| 18733 | + | |
| 18734 | + | |
| 18735 | + | |
| 18736 | + | |
| 18737 | + | |
| 18738 | + | |
| 18739 | + | |
| 18740 | + | |
| 18741 | + | |
| 18742 | + | |
| 18743 | + | |
| 18744 | + | |
| 18745 | + | |
| 18746 | + | |
| 18747 | + | |
| 18748 | + | |
| 18749 | + | |
| 18750 | + | |
| 18751 | + | |
| 18752 | + | |
| 18753 | + | |
| 18754 | + | |
| 18755 | + | |
| 18756 | + | |
| 18757 | + | |
| 18758 | + | |
| 18759 | + | |
| 18760 | + | |
| 18761 | + | |
| 18762 | + | |
| 18763 | + | |
| 18764 | + | |
| 18765 | + | |
| 18766 | + | |
| 18767 | + | |
18701 | 18768 | | |
18702 | 18769 | | |
18703 | 18770 | | |
| |||
0 commit comments