Skip to content

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

Description

@os-warren

Measured on origin/main@b03ba3a (2026-08-31) while closing objectui#4535, whose declared file surface was packages/types/src/objectql.ts + packages/plugin-grid/src/ObjectGrid.tsx. The defect below sits outside that surface, so it was reported rather than fixed in that PR.

Note on spelling: generic type arguments below are written with a space after the opening bracket (z.input< typeof X >). GitHub's body sanitizer silently eats short tag-shaped fragments, and it ate one of these on the first publish of this card.

What landed, and what did not

objectui#4535 / objectstack#8010 reconciled exportOptions with the spec's five-key object form. The reconciliation landed for ObjectGrid: ListViewExportOptions in packages/types/src/objectql.ts declares exactly formats, maxRecords, includeHeaders, fileNamePrefix, streaming; 'pdf' is out of ListViewExportFormat; ObjectGrid.tsx reads streaming with no cast; two guards pin both directions.

The ListView leg did not. ListView is the component ObjectView actually renders for a saved view, and it carries a second, unreconciled declaration of the same key.

1. The zod mirror still declares the pre-#8010 shape

packages/types/src/zod/objectql.zod.ts:511-518:

exportOptions: z.union([
  z.array(z.enum(['csv', 'xlsx', 'json', 'pdf'])),
  z.object({
    formats: z.array(z.enum(['csv', 'xlsx', 'json', 'pdf'])).optional(),
    maxRecords: z.number().optional(),
    includeHeaders: z.boolean().optional(),
    fileNamePrefix: z.string().optional(),
  }),
]).optional().describe('Export options'),

Three separate drifts against @objectstack/spec@17.2.0 (the installed pin), each measured against the spec's own ListViewSchema.shape.exportOptions:

  • 'pdf' is accepted here, in both spellings. The spec's enum is ["csv","xlsx","json"] and a declared 'pdf' is a parse-time refusal carrying an os migrate meta --from 16 prescription. objectui validates a value the platform then refuses at publish — the author sees green locally and a refusal upstream.
  • streaming is declared nowhere. It is the fifth spec key and the renderer honours it (below). This is precisely the undeclared-but-read shape objectstack#8010 was filed for, still open on this surface.
  • The object branch is not strict. The spec's is strictObject (catchall never), so a sixth key is refused upstream and silently stripped here.

This is not cosmetic: packages/types/src/objectql.ts:2121 is export type ListViewSchema = ListViewInferred & ListViewRuntimeProps, and ListViewInferred = z.input< typeof ListViewSchema > (the zod one) reads from this declaration. So ListViewSchema['exportOptions'] — the TYPE the ListView renderer is written against — is the legacy union with 'pdf' and without streaming, while its sibling ObjectGridSchema['exportOptions'] is the clean five-key ListViewExportOptions. Two authoring surfaces for one spec key, disagreeing.

Same failure class as objectui#4605 (the zod BaseSchema mirror still declares visible/disabled as boolean ... after the TS declarations widened), and it lives under the objectui#2231 umbrella.

2. Both as any streaming reads survive in ListView

packages/plugin-list/src/ListView.tsx:

  • :1324&& (resolvedExportOptions as any)?.streaming !== false;
  • :2705&& (exportConfig as any)?.streaming !== false;

These are objectui#4535 item 3's exact defect, in the other renderer. The casts are not gratuitous — they are load-bearing because of item 1: the key is genuinely absent from the type these reads go through. Fixing the zod mirror is what lets them be deleted, so the two halves are one change.

:1304 carries the same 'pdf' in its cast:

return { formats: schema.exportOptions as Array<'csv' | 'xlsx' | 'json' | 'pdf'> };

3. Keep the array tolerance — measured, not assumed

:1299-1308's resolvedExportOptions fold (bare array to { formats }) must STAY. objectui#4535 item 4 asked whether raw un-parsed metadata can still reach the renderer, and it can:

  • normalizeListViewSchema (packages/core/src/utils/normalize-list-view.ts) does not touch exportOptions — zero occurrences in the file.
  • Nothing on the render path runs .parse()/.safeParse() on a view schema; the only such calls in the repo are in tests. That is exactly why ListViewInferred is z.input and not z.infer, as its own doc comment records.
  • packages/app-shell/src/views/ObjectView.tsx:2116 forwards the stored value verbatim: exportOptions: viewDef.allowExport === false ? undefined : (viewDef.exportOptions ?? listSchema.exportOptions).

So the spec's parse-time array lift never runs before the renderer, and a stored bare-array declaration still arrives as an array. The tolerance is load-bearing, not legacy. Note ObjectGrid has none — a bare array reaching it degrades to the ['csv','json'] default, silently ignoring the author's declared formats. Worth deciding deliberately rather than inheriting.

Suggested shape

  1. Rebuild zod/objectql.zod.ts's exportOptions as the spec's two-branch union: z.array(SpecFormatEnum).transform((formats) => ({ formats })) union a z.strictObject of the five keys, 'pdf' gone from both. Ideally derived from ListViewSchema.shape.exportOptions in @objectstack/spec/ui rather than restated — it is a live export, and packages/types already depends on the spec.
  2. Delete both as any casts in ListView.tsx and the 'pdf' in the :1304 cast; keep the array fold.
  3. Extend the pin: packages/types/src/__tests__/export-options-spec-parity.test.ts (landed with objectui#4535) explicitly scopes itself to the TS declaration and states the zod mirror is unmeasured. Widen it once this is fixed.

Also worth folding in

ListViewExportOptions is entry 1 in the @object-ui/types list of CLAIM_DEBT in scripts/check-spec-symbol-derivation.mjs:665 — the shrink-only ledger of unbacked spec-alignment claims. Its claim is now backed by a real parity test, and the mirror's reason is measured (ListViewExportOptionsSchema is internal to the spec bundle, not a public export — only ListViewSchema is). That makes it a clean burn-down via the ledger's own route 2: move it to CLAIM_ALLOW with that reason. Not done in objectui#4535's PR because scripts/** was outside its file surface and editing a gate script pulls in that script's own test suite.

Re-check commands

git grep -n "exportOptions" -- packages/types/src/zod/objectql.zod.ts packages/plugin-list/src/ListView.tsx
git grep -n "as any)?.streaming" -- packages/plugin-list/src/ListView.tsx
git grep -n "exportOptions" -- packages/core/src/utils/normalize-list-view.ts   # expect zero hits

Spec side, against the installed pin rather than a working tree:

import { ListViewSchema } from '@objectstack/spec/ui';
const eo = ListViewSchema.shape.exportOptions;
eo.safeParse(['csv','xlsx']);            // { formats: ['csv','xlsx'] } — the lift
eo.safeParse(['csv','pdf']).success;     // false, with the migration prescription
eo.safeParse({ formats:['csv'], compression:'gzip' }).success;  // false — strict

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanefindingpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions