You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding(plugin-grid/types): a bare exportOptions array on a directly-authored object-grid node passes the zod mirror unvalidated and silently degrades to the csv/json default #7762
Found while landing objectui#6956 (the ListView leg of the exportOptions reconciliation). Out of that card's surface — packages/plugin-grid/** was excluded by the dispatch — so it is filed with the measurement rather than fixed there. Refs #6956. Refs #4585 (closed by PR #4587, which covers the SpecBridge route only; this card is about what that fix does not reach).
Measured on origin/main = 83c77dc3 (2026-09-05), through the installed @objectstack/spec@17.2.0. Generic types below are written in words or with a space inside the brackets: the body sanitizer eats short tag-shaped fragments.
What happens
packages/plugin-grid/src/ObjectGrid.tsx:2956 reads the OBJECT form and only that:
A bare format array has no .formats, so the default wins. The useEffect at :2964 that warns about dropped formats reads .formats too and returns early — no error, no warning, no console line. !!schema.exportOptions (:4690) is truthy for a non-empty array, so the export button still shows, offering csv and json regardless of what the author declared. The failure is silent rather than absent.
A directly-authored object-grid node — OPEN. Three readings:
TS face: ObjectGridSchema['exportOptions'] is ListViewExportOptions, object only (packages/types/src/objectql.ts:953) — the array is a compile-time refusal for TS-authored metadata.
Zod mirror: ObjectGridSchema in packages/types/src/zod/objectql.zod.ts (:111 to :151) declares NO exportOptions member at all — 'exportOptions' in ObjectGridSchema.shape is false, and the key sits in the objectql.zod.ts#ObjectGridSchema LOCAL list of zod-mirror-parity.test.ts (declared in TS, absent from the mirror). BaseSchema is .passthrough() (base.zod.ts:241), so ObjectGridSchema.safeParse({ type: 'object-grid', objectName: 'accounts', exportOptions: ['csv', 'xlsx'] }) succeeds with data.exportOptions equal to ["csv","xlsx"], verbatim. The same call with { formats: ['csv', 'pdf'], compression: 'gzip' } also succeeds, verbatim — the retired 'pdf' and a sixth key pass this mirror too, where the list-view mirror (after finding(types/plugin-list): the ListView leg of the exportOptions reconciliation never landed — zod mirror still takes 'pdf' and declares no streaming, and both as any reads survive #6956) refuses both.
Render path: nothing parses; SchemaRenderer hands the node to ObjectGridRenderer as authored (no parse / safeParse in plugin-grid/src/index.tsx or react/src/SchemaRenderer.tsx).
Controls for the readings above: the list-view mirror on the same bare array lifts it to { formats: ['csv', 'xlsx'] } (same session, same installed spec), and 'bulkActions' in ObjectGridSchema.shape is true — the in reading distinguishes a declared key from an undeclared one.
Two candidate remedies (for triage, not a decision)
A. The same fold as ListView, at ObjectGrid's read site — Array.isArray(schema.exportOptions) ? { formats: schema.exportOptions } : schema.exportOptions. Cheap and local. Against it: #4585's own direction rejected a consumer-side fallback as "a second de-facto contract for one spec key" (the shape objectstack#8010 was filed against), and it leaves the zod mirror admitting 'pdf' and sixth keys on this node.
B. A named refusal on the object-grid zod mirror — declare exportOptions there as the spec's strict five-key object and refuse the bare array by name (message along the lines of: a bare format array is the list-view spelling; object-grid reads the object form — write { formats: [...] }). This keeps the TS face and the zod face agreeing (object only), turns the silent degrade into a loud refusal at validation, and closes the 'pdf' / sixth-key gap on this node in the same move. Note the spec's object branch is not an exported symbol (measured in export-options-spec-parity.test.ts), so the declaration either restates the five keys (a copy, pinned by the existing key-set test) or unwraps the object arm of ListViewSchema.shape.exportOptions at module load.
Either way: packages/plugin-grid/src/index.tsx:248, the designer description for exportOptions, lists four keys and omits streaming — worth correcting in the same PR.
Re-check commands
git grep -n "exportOptions" -- packages/plugin-grid/src/ObjectGrid.tsx | head -5
git grep -n "exportOptions" -- packages/types/src/zod/objectql.zod.ts # no member between :111 and :151
# from packages/plugin-grid, after `pnpm --filter @object-ui/types build`:
node -e "import('@object-ui/types/zod').then(m => console.log(m.ObjectGridSchema.safeParse({ type: 'object-grid', objectName: 'a', exportOptions: ['csv','xlsx'] })))"
Filed unlabelled and unassigned for triage. Written by the dev seat of session_01BAZFhALsQsGqxui8sNqM8s's dispatch for objectui#6956.
Found while landing objectui#6956 (the ListView leg of the
exportOptionsreconciliation). Out of that card's surface —packages/plugin-grid/**was excluded by the dispatch — so it is filed with the measurement rather than fixed there. Refs #6956. Refs #4585 (closed by PR #4587, which covers the SpecBridge route only; this card is about what that fix does not reach).Measured on
origin/main=83c77dc3(2026-09-05), through the installed@objectstack/spec@17.2.0. Generic types below are written in words or with a space inside the brackets: the body sanitizer eats short tag-shaped fragments.What happens
packages/plugin-grid/src/ObjectGrid.tsx:2956reads the OBJECT form and only that:A bare format array has no
.formats, so the default wins. TheuseEffectat:2964that warns about dropped formats reads.formatstoo and returns early — no error, no warning, no console line.!!schema.exportOptions(:4690) is truthy for a non-empty array, so the export button still shows, offering csv and json regardless of what the author declared. The failure is silent rather than absent.Which routes still hand the renderer the array
SpecBridge (
packages/react/src/spec-bridge/bridges/list-view.ts) — CLOSED: SpecBridge hands ObjectGrid a bareexportOptionsarray, so a spec-authored view's declared formats are silently ignored #4585 / PR fix(react): SpecBridge lifts a bare exportOptions array to the spec's object form (#4585) #4587 lift a bare array to{ formats }at the assignment site.A saved view (
packages/app-shell/src/views/ObjectView.tsx:2266) forwardsviewDef.exportOptions ?? listSchema.exportOptionsverbatim into alist-viewnode.ListViewfolds a bare array to{ formats }for its own toolbar (ListView.tsx:1299to:1308, measured load-bearing on finding(types/plugin-list): the ListView leg of theexportOptionsreconciliation never landed — zod mirror still takes'pdf'and declares nostreaming, and bothas anyreads survive #6956) and does NOT forwardexportOptionsto theobject-gridnode it renders (the:2365block carries zeroexportOptionskeys; the file's onlyexportOptionsreads are the fold andhandleExport). Not a route.A directly-authored
object-gridnode — OPEN. Three readings:ObjectGridSchema['exportOptions']isListViewExportOptions, object only (packages/types/src/objectql.ts:953) — the array is a compile-time refusal for TS-authored metadata.ObjectGridSchemainpackages/types/src/zod/objectql.zod.ts(:111to:151) declares NOexportOptionsmember at all —'exportOptions' in ObjectGridSchema.shapeisfalse, and the key sits in theobjectql.zod.ts#ObjectGridSchemaLOCAL list ofzod-mirror-parity.test.ts(declared in TS, absent from the mirror).BaseSchemais.passthrough()(base.zod.ts:241), soObjectGridSchema.safeParse({ type: 'object-grid', objectName: 'accounts', exportOptions: ['csv', 'xlsx'] })succeeds withdata.exportOptionsequal to["csv","xlsx"], verbatim. The same call with{ formats: ['csv', 'pdf'], compression: 'gzip' }also succeeds, verbatim — the retired'pdf'and a sixth key pass this mirror too, where thelist-viewmirror (after finding(types/plugin-list): the ListView leg of theexportOptionsreconciliation never landed — zod mirror still takes'pdf'and declares nostreaming, and bothas anyreads survive #6956) refuses both.SchemaRendererhands the node toObjectGridRendereras authored (noparse/safeParseinplugin-grid/src/index.tsxorreact/src/SchemaRenderer.tsx).So a JSON
object-griddocument — a stored page, a catalog entry, an AI-authored app, anything not typed through TS — that writesexportOptions: ['csv', 'xlsx']validates through@object-ui/typesand renders an export menu offering csv and json. The two authoring faces disagree in the direction opposite to finding(types/plugin-list): the ListView leg of theexportOptionsreconciliation never landed — zod mirror still takes'pdf'and declares nostreaming, and bothas anyreads survive #6956's: TS refuses, zod admits.Controls for the readings above: the
list-viewmirror on the same bare array lifts it to{ formats: ['csv', 'xlsx'] }(same session, same installed spec), and'bulkActions' in ObjectGridSchema.shapeistrue— theinreading distinguishes a declared key from an undeclared one.Two candidate remedies (for triage, not a decision)
A. The same fold as ListView, at ObjectGrid's read site —
Array.isArray(schema.exportOptions) ? { formats: schema.exportOptions } : schema.exportOptions. Cheap and local. Against it: #4585's own direction rejected a consumer-side fallback as "a second de-facto contract for one spec key" (the shape objectstack#8010 was filed against), and it leaves the zod mirror admitting'pdf'and sixth keys on this node.B. A named refusal on the
object-gridzod mirror — declareexportOptionsthere as the spec's strict five-key object and refuse the bare array by name (message along the lines of: a bare format array is thelist-viewspelling;object-gridreads the object form — write{ formats: [...] }). This keeps the TS face and the zod face agreeing (object only), turns the silent degrade into a loud refusal at validation, and closes the'pdf'/ sixth-key gap on this node in the same move. Note the spec's object branch is not an exported symbol (measured inexport-options-spec-parity.test.ts), so the declaration either restates the five keys (a copy, pinned by the existing key-set test) or unwraps the object arm ofListViewSchema.shape.exportOptionsat module load.Either way:
packages/plugin-grid/src/index.tsx:248, the designer description forexportOptions, lists four keys and omitsstreaming— worth correcting in the same PR.Re-check commands
Filed unlabelled and unassigned for triage. Written by the dev seat of
session_01BAZFhALsQsGqxui8sNqM8s's dispatch for objectui#6956.