Skip to content

Commit 024d964

Browse files
committed
docs(spec): name all five exportOptions members on object-grid, not two
`ComponentPropsMap['object-grid'].exportOptions` is `z.unknown()`, so nothing about the key is parsed, refused or stripped and the `.describe()` string is the entire account of its shape. It named `formats` and `streaming`; the only renderer reads five, and `maxRecords`, `includeHeaders` and `fileNamePrefix` had no authoring surface at all. Measured at the `.objectui-sha` pin `53ded82bf7a494f54e344e19099dbf00854b8694` with objectui's own scanner: formats 2 read sites, streaming 2, maxRecords 1, includeHeaders 1, fileNamePrefix 1, absent-name control 0. The key is unchanged and no accept set moves. A pin holds the describe string's member enumeration equal to the members `ListViewExportOptionsSchema` declares, so the list is derived rather than restated, and records that the key is unvalidated today. `content/docs/references/ui/component.mdx` is regenerated. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
1 parent fd62a66 commit 024d964

4 files changed

Lines changed: 182 additions & 2 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`ComponentPropsMap['object-grid'].exportOptions` names all five members the renderer reads, not two
6+
7+
The entry is `z.unknown()`, so nothing about this key is parsed, refused or
8+
stripped: a member that does not exist draws no error and has no effect, and a
9+
member that does exist cannot be discovered from the schema. That makes the
10+
`.describe()` string the entire account of the key's shape rather than a summary
11+
of an enforced one — and it projects straight into
12+
`content/docs/references/ui/component.mdx`, which is what an author (or a
13+
generating model, ADR-0033) reads.
14+
15+
It named two members, `formats` and `streaming`. The only renderer reads five.
16+
17+
Measured at the `.objectui-sha` pin `53ded82bf7a494f54e344e19099dbf00854b8694`
18+
— objectui `packages/plugin-grid/src/ObjectGrid.tsx`, through the
19+
`schema.exportOptions` expression and the `exportConfig` local bound to it, with
20+
objectui's own scanner (`ObjectGrid.exportOptionsKeys.test.ts`, whose
21+
comment/string stripping is what stops a prose mention of a key being counted as
22+
a read): `formats` 2 read sites, `streaming` 2, `maxRecords` 1,
23+
`includeHeaders` 1, `fileNamePrefix` 1, and an absent-name control
24+
(`zzzNotAMember`) 0 on the same instrument — which is what makes those five
25+
counts readings rather than a matcher that matches anything. The same instrument
26+
answers the same five, with the same per-member counts, at objectui
27+
`3fbdd4a2dae1`, so the set is not an artefact of the pin's age.
28+
29+
The three missing members are `maxRecords`, `includeHeaders` and
30+
`fileNamePrefix`. An author reading the old string learned that
31+
`exportOptions` takes `{ formats, streaming }` and had no way to reach the other
32+
three short of reading the renderer's source — the shape objectstack#8010
33+
closed for this same key one layer out, when `streaming` was read for releases
34+
while no schema declared it.
35+
36+
⛔ The key is unchanged: it stays `z.unknown()` and no accept set moves in either
37+
direction. Giving `exportOptions` a real shape is a separate and much larger
38+
change with its own review requirements; this is the docs half only.
39+
40+
The new list is not restated in prose that can drift on its own. A pin holds the
41+
describe string's member enumeration equal to the members
42+
`ListViewExportOptionsSchema` declares — the spec's own five-key declaration of
43+
this same authoring block, reached through `ListViewSchema.exportOptions`'s
44+
object branch and itself derived from that same read set. Both spellings reach
45+
one renderer, so narrowing or widening the declared block now reds the
46+
`z.unknown()` prose instead of leaving it quietly behind: the declared side has
47+
parse failures to catch drift, this side had nothing. The pin also records that
48+
the key is unvalidated today, so the day it grows an accept set is a deliberate
49+
decision rather than a silent one.
50+
51+
`content/docs/references/ui/component.mdx` is regenerated from the string
52+
(`gen:schema` then `gen:docs`) and carries the same one-line change.

content/docs/references/ui/component.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ Sort field and direction pair
428428
| **reorderableColumns** | `boolean` | optional | Allow column drag-reorder |
429429
| **frozenColumns** | `number` | optional | How many leading columns stay frozen (default 1) |
430430
| **showColumnTypeIcons** | `boolean` | optional | Show field-type icons in column headers |
431-
| **exportOptions** | `any` | optional | Export config (`{ formats, streaming }`) |
431+
| **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 |
432432
| **operations** | `any` | optional | Operation toggles (`{ export: false, … }`) |
433433
| **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` |
434434
| **staticData** | `any[]` | optional | Deprecated bare-array static-rows shortcut the renderer still reads. Prefer `data: { provider: 'value', items: [...] }` |
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#17166] `ComponentPropsMap['object-grid'].exportOptions`'s `.describe()` must
5+
* name every member the renderer reads — because here the prose IS the shape.
6+
*
7+
* ## Why this key is different from its neighbours
8+
*
9+
* The entry is `z.unknown()`. Nothing is parsed, nothing is refused, nothing is
10+
* stripped: an author who writes a member that does not exist gets no error and
11+
* no effect, and an author who omits one that does exist has no way to discover
12+
* it. So the `.describe()` string is not a SUMMARY of an enforced shape — it is
13+
* the entire account of the shape that exists at this position, and it projects
14+
* straight into `content/docs/references/ui/component.mdx`, which is what an
15+
* author (or a generating model, ADR-0033) reads.
16+
*
17+
* Until #17166 that string named two members, `formats` and `streaming`, of the
18+
* five the only renderer reads.
19+
*
20+
* ## What was measured, and on which tree
21+
*
22+
* Measured at the `.objectui-sha` pin `53ded82bf7a494f54e344e19099dbf00854b8694`
23+
* — objectui `packages/plugin-grid/src/ObjectGrid.tsx`, through the
24+
* `schema.exportOptions` expression and the `exportConfig` local bound to it,
25+
* with objectui's own scanner (`ObjectGrid.exportOptionsKeys.test.ts`, whose
26+
* comment/string stripping is what keeps a prose mention of a key from being
27+
* counted as a read):
28+
*
29+
* formats 2 · streaming 2 · maxRecords 1 · includeHeaders 1 · fileNamePrefix 1
30+
*
31+
* and an absent-name control (`zzzNotAMember`) reading 0 on the same instrument,
32+
* which is what makes those five counts readings rather than a matcher that
33+
* matches anything. ⚠️ Those counts are a dated observation and belong to that
34+
* tree; this pin does NOT re-derive them, and ⛔ must not be read as asserting
35+
* them today.
36+
*
37+
* ## Why the list is DERIVED here and not restated
38+
*
39+
* A restated list is a third copy of the contract, and the copy is what drifts —
40+
* which is the whole defect this file closes. So the expected member list is
41+
* read from `ListViewSchema.exportOptions`'s object branch
42+
* (`ListViewExportOptionsSchema`), the spec's OWN five-key declaration of this
43+
* same authoring block, itself derived from that same read set at #8010. Both
44+
* spellings — the page-component `object-grid` props and the list view's
45+
* `exportOptions` — reach one renderer, so the two surfaces describe one block.
46+
*
47+
* ⇒ Narrowing or widening the declared block reds this pin instead of leaving
48+
* the `z.unknown()` prose quietly behind, which is the direction of rot that has
49+
* no other guard: the declared side has parse failures, this side has nothing.
50+
*
51+
* ⛔ This pin does NOT ask the key to stop being `z.unknown()`. Giving it a real
52+
* shape is an accept-set change with its own review requirements; the last test
53+
* below records that it is unvalidated TODAY, so that change reds here and is
54+
* made deliberately rather than by accident.
55+
*/
56+
57+
import { describe, it, expect } from 'vitest';
58+
import { ComponentPropsMap } from './component.zod';
59+
import { ListViewSchema } from './view.zod';
60+
61+
/**
62+
* The member names enumerated inside the first `({ … })` group of a describe
63+
* string, e.g. `Export config ({ formats, streaming })` -> `['formats', 'streaming']`.
64+
*
65+
* Deliberately anchored to the parenthesised group rather than "any identifier
66+
* in the sentence": the prose around it names `z.unknown()` and
67+
* `ListViewSchema.exportOptions`, and a scan that read those as members would
68+
* pass for the wrong reason. The self-test below is what proves the anchor
69+
* discriminates instead of matching anything.
70+
*/
71+
function describedMembers(description: string): string[] {
72+
const group = /\(\{([^}]*)\}\)/.exec(description);
73+
if (!group) return [];
74+
return group[1]!.split(',').map((k) => k.trim()).filter(Boolean);
75+
}
76+
77+
/** The spec's own declaration of this block: the object branch of the list view's union. */
78+
function declaredMembers(): string[] {
79+
const optional = (ListViewSchema as unknown as { shape: Record<string, unknown> })
80+
.shape.exportOptions as { unwrap(): { options: Array<{ shape?: Record<string, unknown> }> } };
81+
const branches = optional.unwrap().options;
82+
const objectBranch = branches.find((b) => b.shape !== undefined);
83+
return objectBranch === undefined ? [] : Object.keys(objectBranch.shape!);
84+
}
85+
86+
const gridProps = ComponentPropsMap['object-grid'] as unknown as {
87+
shape: Record<string, { description?: string }>;
88+
safeParse(v: unknown): { success: boolean };
89+
};
90+
const description = gridProps.shape.exportOptions?.description ?? '';
91+
92+
describe('object-grid `exportOptions` — the describe names every declared member (#17166)', () => {
93+
it('the parser discriminates: it reads a member group and does not invent one', () => {
94+
// Lit control — a planted group is read back exactly.
95+
expect(describedMembers('Export config ({ alpha, beta })')).toEqual(['alpha', 'beta']);
96+
// Dark control — a sentence with no member group yields nothing, so a
97+
// green equality below can never come from a matcher that matches anything.
98+
expect(describedMembers('Export config, unvalidated.')).toEqual([]);
99+
// And a name absent from the group is not produced by prose that mentions it.
100+
expect(describedMembers('Export config ({ alpha }) — zzzNotAMember is not a member'))
101+
.toEqual(['alpha']);
102+
});
103+
104+
it('scans something: both sides are non-empty and the authority is the five-key block', () => {
105+
// Non-vacuity floor. The assertion below is an equality, and an equality
106+
// between two empty lists passes for the worst possible reason.
107+
expect(description).not.toBe('');
108+
expect(describedMembers(description).length).toBeGreaterThanOrEqual(5);
109+
expect(declaredMembers().length).toBeGreaterThanOrEqual(5);
110+
expect(declaredMembers()).toContain('formats');
111+
});
112+
113+
it('names exactly the members `ListViewExportOptionsSchema` declares', () => {
114+
// Named rather than counted: a failure must say WHICH member the prose is
115+
// short of, because the fix is to name it — the reader gets nothing else.
116+
expect([...describedMembers(description)].sort()).toEqual([...declaredMembers()].sort());
117+
});
118+
119+
it('is still unvalidated, which is why the prose carries the whole account', () => {
120+
// The premise of this file, asserted rather than assumed: an undeclared
121+
// member is neither refused nor honoured here. If this ever goes red the
122+
// key grew an accept set and the describe's "Unvalidated here" sentence —
123+
// and this pin's reason to exist — need re-deciding, deliberately.
124+
expect(gridProps.safeParse({ exportOptions: { zzzNotAMember: 1 } }).success).toBe(true);
125+
expect(description).toContain('Unvalidated here');
126+
});
127+
});

packages/spec/src/ui/component.zod.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2580,7 +2580,8 @@ export const ObjectGridPropsSchema = lazySchema(() => strictObject({
25802580
reorderableColumns: z.boolean().optional().describe('Allow column drag-reorder'),
25812581
frozenColumns: z.number().optional().describe('How many leading columns stay frozen (default 1)'),
25822582
showColumnTypeIcons: z.boolean().optional().describe('Show field-type icons in column headers'),
2583-
exportOptions: z.unknown().optional().describe('Export config ({ formats, streaming })'),
2583+
exportOptions: z.unknown().optional()
2584+
.describe('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'),
25842585
operations: z.unknown().optional().describe('Operation toggles ({ export: false, … })'),
25852586
/**
25862587
* Data source binding — `ViewDataSchema`, the #5090-pinned authority the

0 commit comments

Comments
 (0)