Skip to content

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

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21229-object-grid-export-options
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21229-object-grid-export-options

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 feat(spec)!: record:line_items gets its ComponentPropsMap row, its columns are the inline grid column contract, and the showcase Tasks grid binds by name (#21142) #21244) and 58 (PR feat(spec)!: an analytics dataset dimension's and measure's field is a column reference (#21220) #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 docs(spec): ComponentPropsMap['object-grid'].exportOptions's describe names 2 of the 5 members the renderer reads #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

claude added 4 commits October 2, 2026 00:40
…ptions object by identity (#21229)

The page-component row was z.unknown(), so a bare ['csv'] passed every
door and ObjectGrid exported its csv/json default. The row now takes
ListViewExportOptionsSchema - the strict five-member object, moved
verbatim with its format enum and 'pdf' prescription into the non-barrel
ui/list-view-export-options.ts (VIEW_HISTORY into ui/view-history.ts) -
by identity, not the list view's lifting union. A bare array is refused
with the object form named.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…tionale and changeset (#21229)

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 2, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:ui tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 14 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via invalid_type (literal, a string literal in ListViewExportOptionsSchema))
  • content/docs/api/error-handling-server.mdx (via invalid_type (literal, a string literal in ListViewExportOptionsSchema))
  • content/docs/automation/flows.mdx (via invalid_type (literal, a string literal in ListViewExportOptionsSchema))
  • content/docs/deployment/cli.mdx (via invalid_type (literal, a string literal in ListViewExportOptionsSchema))
  • content/docs/protocol/objectui/concept.mdx (via invalid_type (literal, a string literal in ListViewExportOptionsSchema))
  • content/docs/ui/views.mdx (via fileNamePrefix (symbol, a field of const object LIST_VIEW_EXPORT_OPTIONS_SHAPE; a field of const object ListViewExportOptionsSchema), includeHeaders (symbol, a field of const object LIST_VIEW_EXPORT_OPTIONS_SHAPE; a field of const object ListViewExportOptionsSchema), maxRecords (symbol, a field of const object LIST_VIEW_EXPORT_OPTIONS_SHAPE; a field of const object ListViewExportOptionsSchema))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via maxRecords (symbol, a field of const object LIST_VIEW_EXPORT_OPTIONS_SHAPE; a field of const object ListViewExportOptionsSchema))
  • content/docs/releases/v17/17-0.mdx (via invalid_type (literal, a string literal in ListViewExportOptionsSchema))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 748b2407235a6a32d2cd7f61f36e1f69f95d775e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 22bdd03121e88757b3a36bee9dd2aa16c006873f — the merge of head 032865c93cd45e071663e062a385af5f0b1b50c2 into base 748b2407235a6a32d2cd7f61f36e1f69f95d775e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 22bdd03121e88757b3a36bee9dd2aa16c006873f && git checkout 22bdd03121e88757b3a36bee9dd2aa16c006873f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 748b2407235a6a32d2cd7f61f36e1f69f95d775e 032865c93cd45e071663e062a385af5f0b1b50c2 && git checkout -B drift-repro 748b2407235a6a32d2cd7f61f36e1f69f95d775e && git merge --no-ff 032865c93cd45e071663e062a385af5f0b1b50c2

node scripts/docs-audit/affected-docs.mjs --json 748b2407235a6a32d2cd7f61f36e1f69f95d775e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 748b2407235a6a32d2cd7f61f36e1f69f95d775e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 032865c93cd45e071663e062a385af5f0b1b50c2
Local-runs: none

Isolated contract review of PR #21287 for card #21229, on the triage ruling 5939380297 (object form by identity, bare array refused and not lifted, no ADR-0087 lift). Inputs: the card and its three comments, the PR body and file list, the net diff origin/main...032865c93c (10 files, +442/-178), the head's check-runs, and reads of origin/main and the pinned objectui 31971ff1e28f. Nothing was built, run or re-run.

① Derived judgments

Accept set of ComponentPropsMap['object-grid'].exportOptions, before → after (component.zod.ts:4047, exportOptions: ListViewExportOptionsSchema.optional()):

  • absent → accepted → accepted. RIGHT (.optional()).
  • the object form, any subset of the five members, {} included → accepted → accepted. RIGHT.
  • a bare array ['csv'] → accepted → refused, invalid_type at exportOptions, message naming { formats: ['csv', 'xlsx'] }. RIGHT, and it is the ruling's shape: the prescription comes from the object's own error map (list-view-export-options.ts, issue.code === 'invalid_type' && Array.isArray(issue.input)), the only map a type failure at that position consults; no lift, no conversion.
  • a format outside the enum → accepted → refused, invalid_value at exportOptions.formats.N; 'pdf' keeps its retirement text (the enum's error map moved with the enum). RIGHT.
  • an undeclared key → accepted → refused, unrecognized_keys at exportOptions, surface this export options block, near-miss rename (maxRecord → maxRecords). RIGHT.
  • null, true, a string → accepted → refused, invalid_type (the block is .optional(), not nullable). RIGHT.
  • wrong member types (formats: 'csv', maxRecords: -1, streaming: 'false') → accepted → refused at the member path. RIGHT.
  • By identity, not the union. The row holds the very const the list view's union holds as its object arm (view.zod.ts:2832–2835: z.union([z.array(ListViewExportFormatSchema).transform(...), ListViewExportOptionsSchema], { error: exportOptionsPdfUnionError })). The pin asserts toBe against the arm read off the union and options === undefined. RIGHT per the ruling.

The sharing change.

  • ui/list-view-export-options.ts: I diffed the moved text against the deleted view.zod.ts block. LIST_VIEW_EXPORT_PDF_RETIRED is byte-identical; the enum and its error map are identical; the five member declarations with their describes are byte-identical; the options (surface: 'this export options block', history: VIEW_HISTORY) are the same. Construction changed from strictObject(options, shape) to closedObject(z.object(shape, { error }).strict()), which is strictObject's own body (shared/strict-object.ts, last function) with the error map wrapped: an invalid_type on an array input answers the new prescription; every other issue goes to strictObjectError(options, shape), which answers only unrecognized_keys and returns undefined otherwise (suggestions.zod.ts:368), so every other message is zod's default exactly as before. prime is forwarded on the wrapper, so closedObject's terminal-refusal path still primes the unknown-key map. Verbatim with one addition, as stated. RIGHT, and needed: the row and the list view share one declaration.
  • ui/view-history.ts: the sentence is byte-identical. It had to move: the shared block's strictObjectError needs VIEW_HISTORY, and view.zod.ts now imports the shared module, so importing the constant back from view.zod.ts would close an import cycle, while exporting it from view.zod.ts would publish it through ui/index.ts's export * from './view.zod'. A non-barrel module is the only placement that keeps the refusal sentence and publishes nothing. RIGHT. view.zod.ts keeps 51 history: VIEW_HISTORY uses, every one unchanged; the 52nd moved with the block (the PR body's "53 uses" counts the old declaration line as well; substance true, count imprecise).
  • Reachability. ui/index.ts reaches view.zod and component.zod by export *; view.zod.ts imports the three moved names and re-exports none (its only export { … } is the two HTTP schemas); component.zod.ts imports one name. packages/spec/package.json exports names domain subpaths only; no path reaches ui/list-view-export-options or ui/view-history. api-surface/ui.json records name (kind) per entry point, so neither the unchanged export set nor the member-type change moves it; json-schema.manifest/ui.json lists schema names; authorable-surface/ui.json lists top-level keys only (ui/ObjectGridProps:exportOptions already there, no nested row in the shard). The diff touches none of the four ratchets, and Lint & Repo Gates is green on the head. RIGHT.
  • The list view's accept set. ListViewSchema.exportOptions is untouched: the same two-arm union with the same union-level map and the same top-level 'pdf' refine. The object arm's map cannot change which arm succeeds, so the accept set and the lift (['csv'] → { formats: ['csv'] }, asserted as the pin's control) are unchanged. Only inside an invalid_union (both arms failing, e.g. ['docx']) does the object branch's nested message read the object-form prescription instead of zod's expected object, received array; the union's own message and the pdf refine are as before. The disclosure is TRUE at the code.

ADR-0087 disposition (D3 only, entry ui-object-grid-export-options-closed, no D2). Re-read at the code, because a sibling review failed on this question:

  • page.zod.ts:324: properties: z.record(z.string(), z.unknown()).optional().default({}). The save door's per-type parse of a page never reaches the props row; the kernel's lintUnknownKeysAgainstSchema docblock (kernel/metadata-authoring-lint.ts) says the walk "stops dead at the carrier".
  • validateComponentProps (lint/src/authoring-rules.ts:1049–1057): tier: 'advisory', commands: ALL, surfaces: CLI_ONLY. It never emits error and never runs at runtime-publish.
  • runtimeAuthoringRulesFor('page') (lint/src/runtime-gate.ts:550) keeps rules with the runtime-publish surface whose runtimeTypes names page; the only such rule is validatePresetComparands (:817–831), which does not read exportOptions.
  • defineStack parses through the same page schema. So no door on the save or load path refuses any of the shapes the row now refuses; there is no load-path refusal for a retiredFromLoadPath conversion to pre-empt, the bare array has no single lossless target ({} keeps today's menu, { formats } honours the author), and the ruling forbids a lift. Nothing stops loading, so D3-only is RIGHT. The changeset carries exactly one marker, adr-0087: registered ui-object-grid-export-options-closed, naming an id that resolves and is new in the diff.

Registry order. The STEP18 rationale fragment takes order: 59; origin/main holds 57 (registry.ts:6032) and 58 (:5372) as its highest. The fragment sits in its id-sorted gap (before ui-object-master-detail-form-details-closed), and the generated semantic entry sits before ui-object-grid-page-size-positive-integer-refused, which is the sort build-migration-registry.ts:258 emits. RIGHT. (Two fragments already share order: 56 on main; pre-existing, not this PR's.)

Regenerated pages and ledgers. component.mdx: the row's type is the object, the "Unvalidated here" sentence is gone, and the new nested-shape table's five rows match the shape's describes. ui.md: 189 → 188 sites, 179 → 178 strict, view.zod.ts 61 → 60, strip 7 unchanged. scripts/lib/strictness-ledger.ts:222 filters f.endsWith('.zod.ts'), so a .strict() site that now lives in list-view-export-options.ts leaves the count; the B3 ratchet reports only new .zod.ts files, so no hand-written ledger row is owed. The dev's explanation is TRUE; the moved site is still CLOSED, and alias-integrity.test.ts scans every .ts under src, so the moved strictObjectError call is still audited.

The pin test (component-object-grid-export-options-members.pin.test.ts, rewritten): identity (toBe the union's object arm; not the union), bare array refused at exportOptions with the object form named plus the list-view lift as control, object form accepted (all five members, {}, absent), out-of-enum refused at exportOptions.formats.1 and 'pdf' with its retirement text, undeclared key refused with keys: ['maxRecord'], the surface name and the rename. Every assertion follows from the schema as written. Replacing the #17166 pin is right: that pin existed so this change would red and be decided deliberately, and the ruling is that decision.

Published prose, sentence by sentence. Changeset: title TRUE; Clause-②: yes (narrowing) TRUE; BREAKING at minor "under the repo's launch-window convention" TRUE (ADR-0087 addendum 2026-09-13: pre-GA, a metadata-facing retirement ships minor with the banner and its disposition entry); "What reads the row: the component-props gate on objectstack validate, objectstack build and objectstack lint, … advisory component-props-invalid / component-props-unknown-key" TRUE (commands: ALL, tier: 'advisory', codes at validate-component-props.ts:113/115); "A stored page still saves and loads …" TRUE; the four refusal bullets TRUE; ObjectGridProps['exportOptions'] and ObjectGridPropsParsed are the object type TRUE (component.zod.ts:4069/4077, z.input/z.infer of the closed object); the list-view nested-message sentence TRUE; the FROM → TO table and the one-line fix TRUE; the census paragraph is the dev's reading on f148852752 with a lit control, not re-run here, and says so ("Deployed metadata was not measured"). D3 entry: surface, replacement (maxRecords a non-negative integer, includeHeaders / streaming booleans, formats from csv/xlsx/json), reason and acceptanceCriteria TRUE against the schema. The row's describe TRUE. The pin file's describe TRUE.

objectui at the pin 31971ff1e28f. No source imports any moved name (ListViewExportOptionsSchema, VIEW_HISTORY, LIST_VIEW_EXPORT_PDF_RETIRED, ListViewExportFormatSchema were all module-private before). packages/types/src/__tests__/export-options-spec-parity.test.ts:147 asserts specUi.ListViewExportOptionsSchema is undefined: still TRUE. Its peel (packages/types/src/zod/objectql.zod.ts:208) reads the union's object arm by .shape: still present, and closedObject leaves _zod.def untouched; its other assertions (two arms, five keys, the refusals and the lift) still hold. No pinned test parses the spec row with a bare-array exportOptions; the 10885 test reads Object.keys(shape) and renders. Console Pin Gate is skipped on this head because the console path filter (ci.yml:117–126) does not include packages/spec/src, so Post-Task Checklist #4 is judged here by read: nothing the pin imports is removed or renamed. RIGHT.

Check-runs on 032865c93c (last read 2026-10-02T02:09Z): 35 check-runs, every one completed. 33 success: Build Core, Build Docs, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Governed Surface Queue Guard, Lint & Repo Gates, Spec property liveness, Temporal Conformance (live PG + MySQL), Test Core and its six shards, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace, TypeScript Type Check, the four single-writer and card-claim checks, Auto Label, filter, Flag docs affected by code changes. 2 skipped by design: Console Pin Gate (the console path filter does not name packages/spec/src; judged by read above) and Packed-tarball smoke (opt-in). None failed; none in progress at this read.

② Semver level

The diff narrows the accept set of a published authoring surface (ComponentPropsMap['object-grid'] through @objectstack/spec/ui) and narrows the published type ObjectGridProps['exportOptions'] from unknown to an object; it adds no export, removes no def, and moves no ratchet. The changeset .changeset/21229-object-grid-export-options-closed.md declares '@objectstack/spec': minor with the BREAKING banner, the ! summary, Clause-②: yes (narrowing) and the FROM → TO migration. That is the level the Post-Task Checklist and ADR-0087's pre-GA rule require for (narrowing) (BREAKING, at least minor, carrying its migration and its disposition). Matches. Check Changeset and Lint & Repo Gates (which carries check:adr-0087-registration) are green on the head. The PR body's own line reads Clause-②: yes without the arm; the changeset carries the full declaration where the gate reads it, and the two are consistent. Clause-②: yes (narrowing).

③ Boundary flags

  • open_questions: none declared. None found.
  • Deviation 1 (last gate chunk moved to background, waited on its PID): process only; the head's check-runs are the gate verdicts here. Answered.
  • Deviation 2 (the export-measurement build dirtied json-schema.manifest/ui.json and authorable-surface/ui.json, restored): the PR's file list carries neither, so the restore holds in the diff. Answered.
  • Deviation 3 (first ablation attempt refused as a no-op, re-anchored): measured nothing; no effect on the diff. Answered.
  • Note 1, objectui's two stale comments: confirmed at the pin, packages/plugin-grid/src/__tests__/ObjectGrid.exportOptionsKeys.test.ts:43 and the object-grid.exportOptions row of MEMBER_PINS in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts:2782; both are prose, neither asserts. Carrier: objectui's next spec pin-bump PR, as the dev says. The claim forbids an objectui edit here. Accepted.
  • Note 2, the strictness-ledger count: the generator counts .zod.ts files only (verified), the move is arithmetic the generator owns, and the strip ratchet is unchanged at 7. Accepted. Residual, stated: the ledger's ui/ site count no longer sees one CLOSED site that still exists, a scope limit of the instrument rather than a posture change.
  • Census not re-run (read-only review): accepted as the dev's reading, stated as such in the D3 entry and the changeset.
  • PR body count: "its 53 VIEW_HISTORY uses are unchanged" counts the declaration line; 51 uses remain unchanged and one moved with the block. Prose only, not a published surface; no action.
  • Draft, auto_merge null, no governed path in the file list; this record is the Tier S record the lane asked for and does not by itself land anything.

Implemented-by: claude/issue-21229-object-grid-export-options
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 02:13
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 02:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 5a9292e Oct 2, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21229-object-grid-export-options branch October 2, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

2 participants