Skip to content

Commit 5a9292e

Browse files
feat(spec)!: an object-grid block's exportOptions is the list view's export options object, and a bare format array is refused (#21229) (#21287)
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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: an `object-grid` page block's `exportOptions` is the list view's export options object, and a bare format array is refused (#21229)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered ui-object-grid-export-options-closed -->
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 row: 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+
- **`ComponentPropsMap['object-grid'].exportOptions`** was `z.unknown()`, so any value passed. The console's `ObjectGrid` reads `exportOptions.formats`, `.maxRecords`, `.includeHeaders`, `.fileNamePrefix` and `.streaming`, and lifts nothing: a bare format array — legal on a list view, which lifts it to `{ formats }` at parse — showed the export menu with its csv/json default and dropped the author's list without a report. The row now takes the list view's own five-member export options object, by identity and not the list view's union, so the legacy spelling does not spread to the grid:
16+
- a bare array is refused with the object form named (`{ formats: ['csv', 'xlsx'] }`);
17+
- a format outside `csv` / `xlsx` / `json` is refused at its index, and `pdf` keeps its retirement text;
18+
- a key the object does not declare is named, with the rename a near-miss gets (`maxRecord` → `maxRecords`);
19+
- `null` and other non-object values are refused.
20+
- **`ObjectGridProps['exportOptions']`** (and `ObjectGridPropsParsed`) is the object type `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }` instead of `unknown`.
21+
- The list view's `exportOptions` accepts and lifts exactly what it did. One message changed there, nested only: when a bare array also fails the array arm (a format outside the enum), the object arm's branch of the union now names the object form instead of zod's `expected object, received array`.
22+
23+
## FROM → TO
24+
25+
| you wrote on an `object-grid` | write instead |
26+
|:--|:--|
27+
| `exportOptions: ['csv', 'xlsx']` | `exportOptions: { formats: ['csv', 'xlsx'] }` — the grid now offers exactly those formats; write `{}` to keep the csv/json default it has been offering |
28+
| `exportOptions: { formats: ['csv', 'pdf'] }` | `exportOptions: { formats: ['csv'] }` |
29+
| `exportOptions: { formats: ['csv'], maxRecord: 100 }` | `exportOptions: { formats: ['csv'], maxRecords: 100 }` |
30+
| `exportOptions: null` | omit `exportOptions` |
31+
32+
The one-line fix: write `exportOptions` on an `object-grid` as the object `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`, with `formats` drawn from `csv`, `xlsx` and `json`.
33+
34+
## Who is affected, measured
35+
36+
On `origin/main` `f148852752`: zero `object-grid` blocks authoring `exportOptions` in the examples, the package fixtures, the documentation and the published skills, against ten authored `object-grid` blocks through the same census (nine in TypeScript, one in a YAML documentation example) and four list-view `exportOptions` authorings as the key's control. No conversion is registered: nothing on the metadata load path refuses the shape, and a bare array has no rewrite that both keeps what the grid shows today and honours the author's list. Deployed metadata was not measured.

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ Sort field and direction pair
724724
| **reorderableColumns** | `boolean` | optional | Allow column drag-reorder |
725725
| **frozenColumns** | `number` | optional | How many leading columns stay frozen (default 1) |
726726
| **showColumnTypeIcons** | `boolean` | optional | Show field-type icons in column headers |
727-
| **exportOptions** | `any` | optional | Export config (`{ formats, maxRecords, includeHeaders, fileNamePrefix, streaming }`). Unvalidated here (`z.unknown()`), so this list is the whole account of the shape; `ListViewSchema.exportOptions` declares the same five members with their per-member contract |
727+
| **exportOptions** | `{ formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export config — the object `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`, the same block a list view's `exportOptions` declares, with `formats` drawn from `csv`, `xlsx` and `json`. A bare format array is refused: the grid reads `exportOptions.formats`, so write `{ formats: ['csv', 'xlsx'] }` |
728728
| **operations** | `any` | optional | Operation toggles (`{ export: false, … }`) |
729729
| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }` | optional | Data source binding (ViewDataSchema — discriminated on `provider`: object \| api \| value \| schema). Static inline rows live at `{ provider: 'value', items: [...] }`; the bare-array shortcut is refused — see migration `object-grid-data-view-data-converged` |
730730
| **staticData** | `any[]` | optional | Deprecated bare-array static-rows shortcut the renderer still reads. Prefer `data: { provider: 'value', items: [...] }` |
@@ -772,6 +772,16 @@ Sort field and direction pair
772772
| :--- | :--- | :--- | :--- |
773773
| **fields** | `{ field: string; order: Enum<'asc' \| 'desc'>; collapsed: boolean }[]` | ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy` |
774774

775+
### Nested Shape: `ObjectGridProps.exportOptions`
776+
777+
| Property | Type | Required | Description |
778+
| :--- | :--- | :--- | :--- |
779+
| **formats** | `Enum<'csv' \| 'xlsx' \| 'json'>[]` | optional | Formats offered in the export menu (default: ['csv', 'json']). XLSX is delivered by the server stream only. |
780+
| **maxRecords** | `integer` | optional | Maximum number of records to export; 0 or absent = unlimited |
781+
| **includeHeaders** | `boolean` | optional | Include column headers in the exported file (default true) |
782+
| **fileNamePrefix** | `string` | optional | Download file name prefix — replaces the object label and suppresses the view label in the generated file name |
783+
| **streaming** | `boolean` | optional | Set false to force the client-side export path (csv/json only) instead of the server stream |
784+
775785
### Nested Shape: `ObjectGridProps.data[provider='object']`
776786

777787
| Property | Type | Required | Description |

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

Lines changed: 6 additions & 6 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/` | 189 | 179 | 3 | 0 | 7 |
24+
| `ui/` | 188 | 178 | 3 | 0 | 7 |
2525

2626
## `ui/` — sites
2727

@@ -44,25 +44,25 @@ classify and is not listed (it becomes reportable the day it grows its first sit
4444
| `report.zod.ts` | 3 |
4545
| `responsive.zod.ts` | 1 |
4646
| `sharing.zod.ts` | 1 |
47-
| `view.zod.ts` | 61 |
47+
| `view.zod.ts` | 60 |
4848
| `widget.zod.ts` | 1 |
49-
| **total** | **189** |
49+
| **total** | **188** |
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 189**, in 4 file(s).
57+
**7 strip of 188**, in 4 file(s).
5858

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

6767
| Bucket | Sites |
6868
|---|---|
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// #21229 — an `object-grid` page block's `exportOptions` was `z.unknown()`, so a
6+
// bare format array (the list view's legacy spelling, which the list view lifts
7+
// to `{ formats }`) was accepted on the grid, whose renderer reads
8+
// `exportOptions.formats` and lifts nothing. The row now takes the list view's
9+
// five-member export options OBJECT by identity, not the list view's union. D3
10+
// only: page-component `properties` is not parsed on the metadata save or load
11+
// path, so a stored page is never refused and there is no load-path refusal for
12+
// a conversion to pre-empt; the bare array never worked here, and lifting it
13+
// would change the export menu a deployed grid shows today; the authored census
14+
// found nothing to respell.
15+
export const entry: SemanticMigration = {
16+
id: 'ui-object-grid-export-options-closed',
17+
surface: 'page `object-grid` components — `properties.exportOptions` (which used to accept any value)',
18+
replacement: 'the export options object a list view\'s `exportOptions` declares: `{ formats?, '
19+
+ 'maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`, with `formats` drawn from '
20+
+ '`csv`, `xlsx` and `json`, `maxRecords` a non-negative integer, and `includeHeaders` / '
21+
+ '`streaming` booleans. Where a bare format array was written, write `{ formats: [...] }` to '
22+
+ 'offer the formats you listed — the grid will now offer exactly those — or `{}` to keep the '
23+
+ 'csv/json default the grid has been offering. Delete `pdf` from `formats`, and any key the '
24+
+ 'object does not declare; delete an `exportOptions: null` (it never enabled the menu).',
25+
reason: 'The grid reads one export options block — `exportOptions.formats`, `.maxRecords`, '
26+
+ '`.includeHeaders`, `.fileNamePrefix` and `.streaming` — the block a list view declares, '
27+
+ 'but the page-component row declared the key `z.unknown()`, so any value passed the '
28+
+ 'component-props gate. The trap was the list view\'s legacy spelling: a bare format array is '
29+
+ 'legal on a list view, which lifts it to `{ formats }` at parse, and was accepted on the grid, '
30+
+ 'which lifts nothing — the export menu appeared, offering the csv/json default, and the '
31+
+ 'author\'s list was dropped without a report. The row now takes the list view\'s export '
32+
+ 'options object itself rather than its union, so a legacy spelling does not spread to a '
33+
+ 'surface that never read it: a bare array is refused with the object form named, a format '
34+
+ 'outside the enum is refused at its index (`pdf` with its retirement text), and a key the '
35+
+ 'object does not declare is named. It is read where every page component\'s props are: the '
36+
+ 'component-props gate reports these as an advisory `component-props-invalid` / '
37+
+ '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and '
38+
+ '`objectstack lint`, and a stored page still saves and loads, because a page component\'s '
39+
+ '`properties` is not parsed on the metadata save or load path. No conversion is registered: '
40+
+ 'nothing on the load path refuses the shape; a bare array has no rewrite that both keeps '
41+
+ 'what the grid shows today and honours what the author wrote, which is the judgment this '
42+
+ 'entry leaves to the upgrader; and the authored census found nothing to respell. Population '
43+
+ 'measured at the change, on origin/main f148852752: zero `object-grid` blocks authoring '
44+
+ '`exportOptions` in the examples, the package fixtures, the documentation and the published '
45+
+ 'skills, against ten authored `object-grid` blocks through the same matcher (nine in '
46+
+ 'TypeScript, one in a YAML documentation example) and four list-view `exportOptions` '
47+
+ 'authorings as the key\'s control. Deployed metadata NOT MEASURED.',
48+
acceptanceCriteria: 'Every `object-grid` node validates: `objectstack validate` reports no '
49+
+ '`component-props-invalid` / `component-props-unknown-key` finding on a '
50+
+ '`properties.exportOptions` path. Every `exportOptions` on an `object-grid` is an object '
51+
+ 'carrying only the five declared keys, with every `formats` entry `csv`, `xlsx` or `json`, '
52+
+ 'and the grid\'s export menu offers the declared formats the active export path delivers '
53+
+ '(`xlsx` on the server stream only).',
54+
};

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6008,6 +6008,23 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [
60086008
+ '`columns` untouched) on `object-form` page components, on every form payload a view '
60096009
+ 'carries, and on the assembled-manifest `viewItems` channel.',
60106010
},
6011+
{
6012+
id: 'ui-object-grid-export-options-closed',
6013+
order: 59,
6014+
text:
6015+
'It also closes the export options of an `object-grid` page block (#21229). `exportOptions` '
6016+
+ 'was `z.unknown()`, so a bare format array — the list view\'s legacy spelling, which the list '
6017+
+ 'view lifts to `{ formats }` — was accepted on the grid, whose renderer reads '
6018+
+ '`exportOptions.formats` and lifts nothing: the export menu offered its csv/json default and '
6019+
+ 'the author\'s list was dropped. The row now takes the list view\'s five-member export '
6020+
+ 'options object by identity, not the list view\'s union, and refuses a bare array with the '
6021+
+ 'object form named, a format outside the enum and an undeclared key. Page-component '
6022+
+ '`properties` is read by the component-props gate, which reports these as advisory '
6023+
+ 'findings, and is not parsed on the metadata save or load path, so a stored page still saves '
6024+
+ 'and loads and no conversion is registered: the bare array never worked here, and lifting it '
6025+
+ 'would change the menu a deployed grid shows. The authored census found nothing to respell. '
6026+
+ 'Its D3 record is the semantic entry `ui-object-grid-export-options-closed`.',
6027+
},
60116028
{
60126029
id: 'ui-object-master-detail-form-details-closed',
60136030
order: 56,
@@ -18698,6 +18715,56 @@ const step18: MigrationStep = {
1869818715
+ 'group per value of that field, and a board that showed one swimlane shows one swimlane per value — '
1869918716
+ 'check that this is the grouping you meant.',
1870018717
},
18718+
// #21229 — an `object-grid` page block's `exportOptions` was `z.unknown()`, so a
18719+
// bare format array (the list view's legacy spelling, which the list view lifts
18720+
// to `{ formats }`) was accepted on the grid, whose renderer reads
18721+
// `exportOptions.formats` and lifts nothing. The row now takes the list view's
18722+
// five-member export options OBJECT by identity, not the list view's union. D3
18723+
// only: page-component `properties` is not parsed on the metadata save or load
18724+
// path, so a stored page is never refused and there is no load-path refusal for
18725+
// a conversion to pre-empt; the bare array never worked here, and lifting it
18726+
// would change the export menu a deployed grid shows today; the authored census
18727+
// found nothing to respell.
18728+
{
18729+
id: 'ui-object-grid-export-options-closed',
18730+
surface: 'page `object-grid` components — `properties.exportOptions` (which used to accept any value)',
18731+
replacement: 'the export options object a list view\'s `exportOptions` declares: `{ formats?, '
18732+
+ 'maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`, with `formats` drawn from '
18733+
+ '`csv`, `xlsx` and `json`, `maxRecords` a non-negative integer, and `includeHeaders` / '
18734+
+ '`streaming` booleans. Where a bare format array was written, write `{ formats: [...] }` to '
18735+
+ 'offer the formats you listed — the grid will now offer exactly those — or `{}` to keep the '
18736+
+ 'csv/json default the grid has been offering. Delete `pdf` from `formats`, and any key the '
18737+
+ 'object does not declare; delete an `exportOptions: null` (it never enabled the menu).',
18738+
reason: 'The grid reads one export options block — `exportOptions.formats`, `.maxRecords`, '
18739+
+ '`.includeHeaders`, `.fileNamePrefix` and `.streaming` — the block a list view declares, '
18740+
+ 'but the page-component row declared the key `z.unknown()`, so any value passed the '
18741+
+ 'component-props gate. The trap was the list view\'s legacy spelling: a bare format array is '
18742+
+ 'legal on a list view, which lifts it to `{ formats }` at parse, and was accepted on the grid, '
18743+
+ 'which lifts nothing — the export menu appeared, offering the csv/json default, and the '
18744+
+ 'author\'s list was dropped without a report. The row now takes the list view\'s export '
18745+
+ 'options object itself rather than its union, so a legacy spelling does not spread to a '
18746+
+ 'surface that never read it: a bare array is refused with the object form named, a format '
18747+
+ 'outside the enum is refused at its index (`pdf` with its retirement text), and a key the '
18748+
+ 'object does not declare is named. It is read where every page component\'s props are: the '
18749+
+ 'component-props gate reports these as an advisory `component-props-invalid` / '
18750+
+ '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and '
18751+
+ '`objectstack lint`, and a stored page still saves and loads, because a page component\'s '
18752+
+ '`properties` is not parsed on the metadata save or load path. No conversion is registered: '
18753+
+ 'nothing on the load path refuses the shape; a bare array has no rewrite that both keeps '
18754+
+ 'what the grid shows today and honours what the author wrote, which is the judgment this '
18755+
+ 'entry leaves to the upgrader; and the authored census found nothing to respell. Population '
18756+
+ 'measured at the change, on origin/main f148852752: zero `object-grid` blocks authoring '
18757+
+ '`exportOptions` in the examples, the package fixtures, the documentation and the published '
18758+
+ 'skills, against ten authored `object-grid` blocks through the same matcher (nine in '
18759+
+ 'TypeScript, one in a YAML documentation example) and four list-view `exportOptions` '
18760+
+ 'authorings as the key\'s control. Deployed metadata NOT MEASURED.',
18761+
acceptanceCriteria: 'Every `object-grid` node validates: `objectstack validate` reports no '
18762+
+ '`component-props-invalid` / `component-props-unknown-key` finding on a '
18763+
+ '`properties.exportOptions` path. Every `exportOptions` on an `object-grid` is an object '
18764+
+ 'carrying only the five declared keys, with every `formats` entry `csv`, `xlsx` or `json`, '
18765+
+ 'and the grid\'s export menu offers the declared formats the active export path delivers '
18766+
+ '(`xlsx` on the server stream only).',
18767+
},
1870118768
{
1870218769
id: 'ui-object-grid-page-size-positive-integer-refused',
1870318770
surface: '`object-grid` page-component page sizes '

0 commit comments

Comments
 (0)