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
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
SpecBridge hands ObjectGrid a bare exportOptions array, so a spec-authored view's declared formats are silently ignored #4585
Found while landing objectui#4535 (PR #4584). Out of that card's surface — filed rather than fixed there.
What happens
packages/react/src/spec-bridge/bridges/list-view.ts builds a renderer node of type: 'object-grid' (:125) and copies the spec's export options across verbatim:
That node is rendered by ObjectGridRenderer (ComponentRegistry.register('object-grid', …)), i.e. by ObjectGrid. ObjectGrid reads the OBJECT form and only that:
At objectui's pinned @objectstack/spec@17.0.0-rc.6, ListView.exportOptions is a bare format ARRAY — ('csv' | 'xlsx' | 'json' | 'pdf')[] — not an object. So .formats on it is undefined, the default ['csv', 'json'] wins, and the view's declared formats are dropped with no error, no warning and no console line.
Net effect for an author: a spec-canonical list view declaring exportOptions: ['csv', 'xlsx'] and routed through SpecBridge renders an export menu offering csv and json. The declared xlsx never appears; an undeclared json does. !!schema.exportOptions is still truthy (a non-empty array), so the export button itself shows — the failure is silent rather than absent.
Why the spec's array lift does not save it
objectstack#8010 gave ListViewExportOptionsSchema a parse-time lift: a stored bare array becomes { formats: [...] } when the spec schema parses it. That lift never runs on this path. The bridge's input is a TypeScript type — type ListViewSpec = Partial< ListView > — not a parsed value, and there is no parse or safeParse anywhere under packages/react/src/spec-bridge/. So the bridge forwards whatever its host handed it. A host that parses first is fine; a host that passes raw metadata is not, and nothing in the bridge distinguishes them.
This is why bumping the spec pin alone will not close it.
Pinned in the current tests, so it reads as intended
packages/react/src/spec-bridge/__tests__/P1SpecBridge.test.ts:390-397 asserts the passthrough:
it('should pass through exportOptions string[] format',()=>{// …expect(node.exportOptions).toEqual(['csv','xlsx']);});
The assertion is about the bridge's output shape and never renders it, so the bridge is green while the grid downstream cannot read what it produced.
Repro
Build a spec ListView with exportOptions: ['csv', 'xlsx'].
Run it through SpecBridge and render the resulting node.
Open the export menu: it offers CSV and JSON. Expected: CSV and XLSX (XLSX subject to the server-stream gate).
Direction (for triage, not a decision)
The producer is where this belongs — a consumer-side Array.isArray fallback in ObjectGrid would be a second de-facto contract for one spec key, which is the shape objectstack#8010 was filed against. Two candidates worth weighing:
Lift in the bridge: normalize a bare array to { formats: [...] } at :158, mirroring the spec's own parse-time lift, so the bridge emits one shape regardless of what the host hands it. Cheap, local, and keeps the renderer strict.
Parse in the bridge: run the spec schema over the input so the lift and every other spec-side coercion apply. Larger change and a behavior change for hosts currently passing fragments (the input is deliberately Partial).
ListView (plugin-list) already normalizes both spellings for its own toolbar (ListView.tsx:1104-1113), so whichever lands, the bridge path is the one that lacks it.
CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4585-bridge-export-lift. Dispatching a dev agent now.
PM ruling (delegated decision authority; maintainer veto window open — record objections here):lift in the bridge, not parse-in-the-bridge:
The bridge normalizes a bare exportOptions array to { formats: [...] } at the assignment site, mirroring EXACTLY the spec's own parse-time lift and nothing more — this is the SAME contract applied where parse cannot reach, not a second one (the consumer-side fallback the card rightly rejects would be). The object form passes through verbatim (pinned unchanged). Full-parse is rejected: the input is deliberately Partial, and running the spec schema over host fragments is a behavior change out of proportion to one key's coercion.
Red-first at BOTH levels: bridge-output (node.exportOptions currently equals the bare array — the P1SpecBridge passthrough pin is an AUTHORIZED pin move: it pins the broken passthrough today and moves to pin the lifted shape, declared in pins_moved) and end-to-end (the card's repro — a spec view declaring ['csv','xlsx'] rendered through the bridge offers csv+json today; post-fix csv with xlsx subject to the stream gate — reuse ObjectGrid's exportGate harness idiom).
Changeset: '@object-ui/react' per its measured .d.ts (the bridge is internal — expected patch; if the node type surface moves, grade with reasoning). Never major.
Sequencing: gate worktree creation on PR #4584 reaching origin/main (armed on green CI — the type you consume lands there). Mutual exclusion: ⛔ #4550 in flight owns packages/react's navigation files (disjoint — spec-bridge only for you); #4576 in flight owns core/i18n number policy (disjoint). ObjectGrid.tsx is read-only landed context.
Activity
yinlianghui commentedon Aug 13, 2026
CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch
claude/issue-4585-bridge-export-lift. Dispatching a dev agent now.PM ruling (delegated decision authority; maintainer veto window open — record objections here): lift in the bridge, not parse-in-the-bridge:
exportOptionsarray to{ formats: [...] }at the assignment site, mirroring EXACTLY the spec's own parse-time lift and nothing more — this is the SAME contract applied where parse cannot reach, not a second one (the consumer-side fallback the card rightly rejects would be). The object form passes through verbatim (pinned unchanged). Full-parse is rejected: the input is deliberatelyPartial, and running the spec schema over host fragments is a behavior change out of proportion to one key's coercion.ListViewExportOptionsPR fix(types): exportOptions matches the spec's object form — pdf retired, streaming typed (#4535) #4584 landed in @object-ui/types — one spelling of the five-key shape, no third copy.description, retiredefaultValue, pin thenamedelivery #8324, an equivalence test (bridge lift === spec parse output) becomes possible — record the intent in the test header; do not block on it.Sequencing: gate worktree creation on PR #4584 reaching origin/main (armed on green CI — the type you consume lands there). Mutual exclusion: ⛔ #4550 in flight owns packages/react's navigation files (disjoint — spec-bridge only for you); #4576 in flight owns core/i18n number policy (disjoint). ObjectGrid.tsx is read-only landed context.
Generated by Claude Code
Generated by Claude Code
fix(react): SpecBridge lifts a bare exportOptions array to the spec's…