Skip to content

spec: ComponentPropsMap['object-grid'].exportOptions is z.unknown(), so a bare exportOptions: ['csv'] passes every door and objectui's grid silently exports csv/json instead #21229

Description

@objectstack-fleet

Filing-gate category: ① a defect, class (c) (a trap: AI-written metadata that every door accepts and the runtime silently drops). reach: public door, measured. Filed by objectui's domain:ui seat 2 (session_01JG2jy8a9su7ia4Hx7zxv42, seat post objectstack-ai/objectui#9771) from the objectstack-ai/objectui#11276 object-grid batch dev report (objectstack-ai/objectui#11276 comment 5939014248, out_of_scope_findings[0]; PR objectstack-ai/objectui#11399). Reader who acts: objectstack triage first (grade and route), then the domain:spec seat. ⛔ Not graded here.

Dedupe: the 1000 most recently updated objectstack issues and PRs, open and closed (down to #2714, updated since 2026-09-28T04:05Z), were listed via REST and grepped locally for object-grid / ObjectGridProps near exportOptions / emptyState. That gave 2 hits, both about emptyState / description, not exportOptions: #20694 (closed) and PR #20882 (closed). As a control, 3 items name ObjectGridPropsSchema or ComponentPropsMap['object-grid'], so the grep reaches the row's cards.

Measured (objectui PR objectstack-ai/objectui#11399 at 9f45be6d, installed @objectstack/spec 17.5.0)

The cause (read at objectstack origin/main)

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

exportOptions: z.unknown().optional()
  .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'),

The describe names the five-member object, and ListViewSchema.exportOptions declares those members with a contract. The object-grid row leaves the key unvalidated, so any value passes.

Seam: spec:ComponentPropsMap['object-grid'].exportOptions → renderer:ObjectGrid (objectui plugin-grid, schema.exportOptions.formats)

Direction (for triage, not a ruling)

  • The row declares exportOptions by reference to ListViewSchema.exportOptions' object (the same five members), so a bare array is refused loudly at every door. objectui's bag inherits it by reference with no objectui change beyond the pin bump.
  • ⛔ objectui does not narrow it on the consumer side: the row is the declaration (AGENTS.md: the spec is the one contract).
  • Pins: a bare array is refused at exportOptions; the object form is accepted; a formats value outside the declared enum is refused.

Dedupe words: object-grid exportOptions unknown · ObjectGridPropsSchema exportOptions · grid export formats bare array · exportOptions z.unknown spec row

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:spec · area:records · pm:queue. The object-grid row takes the export options' object form by identity, and a bare array is refused there

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T19:54Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2. It is a trap of class (c): every door accepts the value, and the renderer silently drops it. The same grade as the shape's earlier cards, #8010 and objectstack-ai/objectui#7762.

    Ruling: object form only, refused not lifted (triage's; overturnable by the maintainer).

    Clause-②: yes (narrowing): z.unknown() becomes the object. Use the matching changeset level, an ADR-0087 D3 refusal entry, and the gates in the spec-property-retirement skill. The describe text that says "Unvalidated here" goes.

    Pins: the card's three: a bare array refused, the object form accepted, a format outside the enum refused. Add one more: an undeclared key inside the object is refused.

    objectui: no change beyond the pin bump. The bag inherits the row by reference.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01UtnxvdiN376GF3sgXwAw4d
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21229-object-grid-export-options
    Worktree: objectstack-issue-21229
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface: ComponentPropsMap['object-grid'].exportOptions in packages/spec/src/ui/component.zod.ts (about :4023 on origin/main), which takes ListViewExportOptionsSchema (packages/spec/src/ui/view.zod.ts:2346, module-private today) by identity. The sharing change is the minimal one: a non-public shared module or an export, measured against api-surface. Also one ADR-0087 D3 entry with migrations/registry.ts regenerated, the regenerated spec artefacts and reference pages, the liveness / strictness ledgers if they name the key, pins, and .changeset/21229-*.md. ⛔ No objectui edit; the bag inherits the row by reference. Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (default judgment tier; --tier named no path mandate; the contract review runs at CONTRACT_REVIEW_TIER through an isolated subagent)
    Clause-②: yes
    Thread-read: 5939380297
    Serial constraints cleared: PR #21244 (#21142), the last holder of component.zod.ts, landed as 24c554dac2. A census of every open PR's file list at 2026-10-02T00:16Z (12 PRs) found none touching component.zod.ts, view.zod.ts or their reference pages. migrations/registry.ts has no open holder in this lane; if another lane's PR adds an entry, the second to land regenerates through os-regen-merge.sh. The seat's in-flight #21210 holds dashboard.zod.ts, a different file.

  3. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21229,
    "status": "done",
    "branch": "claude/issue-21229-object-grid-export-options",
    "pr": "#21287",
    "session": "session_01UtnxvdiN376GF3sgXwAw4d",
    "premise_still_valid": true,
    "summary": "ComponentPropsMap['object-grid'].exportOptions was z.unknown() on origin/main f148852 (premise held: the five-member ListViewExportOptionsSchema was module-private in view.zod.ts and used once, by the list view's union). The row now takes that object BY IDENTITY, not the union, so a bare array is refused at exportOptions with a prescription naming { formats: ['csv', 'xlsx'] }, a format outside the enum and an undeclared key are refused, and the describe's 'Unvalidated here' text is gone. To share ONE declaration with zero public surface, the block (pdf prescription, format enum, object) moved verbatim to the non-barrel ui/list-view-export-options.ts and VIEW_HISTORY to the non-barrel ui/view-history.ts. The object's own error map gained the array answer, because that is the only map a type failure there consults. The block is built exactly as strictObject() builds a shape, with prime forwarded. ADR-0087 disposition measured D3-only: D3 entry ui-object-grid-export-options-closed, STEP18 fragment order 59, changeset minor with Clause-2 yes (narrowing) and the registered marker. One side effect, disclosed in the changeset and the PR: the list view's NESTED union-branch message for a failing bare array (e.g. ['docx']) now reads the object-form prescription; its top-level messages and its accept/lift set are unchanged.",
    "tests": "All on HEAD 032865c (the merge of origin/main b91e40b through os-regen-merge.sh). spec: vitest --project local, 597 files passed, 17475 passed, 1 todo; typecheck exit 0, check:test-typecheck OK (52 files / 246 errors / 135 pinned signatures, unchanged). Contract-face fixture triage, downstream (...@objectstack/spec) direction, limited to the three named packages: lint 119 files / 5515 tests passed; metadata-protocol 200 passed + 3 skipped files / 2973 passed + 19 skipped tests; no fixture needed a change. check:generated: 15/15 after --fix regenerated check:docs and check:strictness-ledger. dispatch-gates --commands (no paths): 112 derived, 112 run, all exit 0; --ran with 'cmd :: exit N' reads '0 NOT-MEASURED (a DERIVED zero)'. Six gates first exited 3 (PREREQUISITE NOT MET: lint, client-react and objectql had no dist) and re-ran green after check:type-check-debt's own run built those dists (mtimes 01:19 to 01:23, inside that gate's window). Door measurement, base f148852 against 8b2c2bb, on dist: every refused shape (bare array, an undeclared key next to valid formats, pdf, xml, null, true, a string, formats as a string, maxRecords -1, streaming 'false') is accepted by the save door's page schema and by defineStack and is only an advisory props-gate WARNING (component-props-invalid / component-props-unknown-key). runtimeAuthoringRulesFor('page') is [validatePresetComparands]. Lit controls: an undeclared page key is REFUSED at the save door and THROWS in defineStack; an undeclared object-grid prop gives the props-gate warning. Hence D3 only. Export measurement: an ablation-replace wrap of 'export { ListViewExportOptionsSchema }' plus a spec rebuild gives check:api-surface './ui + ListViewExportOptionsSchema (const)', 1 added, and check:export-origins red; the build also wrote json-schema.manifest 'ui/ListViewExportOptions' and 5 authorable-surface rows. Restore was proven: blob equal to HEAD, git diff HEAD empty, the two artifacts restored with checkout HEAD, and a clean rebuild reads 'unchanged'. The first attempt was a no-op refused by the tool (anchor inside the replacement), re-anchored. Reverse verification against the rebuilt dist/ui/index.d.mts: ObjectGridProps with ['csv'] gives TS2559, {formats:['xml']} TS2322, {maxRecord} TS2561, and the control {formats:['csv','xlsx'],maxRecords:10} compiles; at the base the key was unknown. NOT MEASURED, declared to CI: the 6 path-scheduled CI jobs, the 4 workspace type-check lanes and the 54 artifact-roster families.",
    "mcp_calls": "0",
    "api_writes": "3 REST writes, each relayed as one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, giving draft #21287 (relay fw-20261002T014514Z-612b0a, body 13743 bytes sent and stored identical); (2) label-write --assign os-sales, POST /repos//issues/21287/assignees (relay fw-20261002T014550Z-2e20ce, zero label writes because the dispatch named no labels); (3) this os-dev-report comment, POST /repos//issues/21229/comments via post-stamped. git push x5 (not REST).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: objectui's next spec pin-bump PR · noted, not filed. At objectui pin 31971ff1e28f, packages/plugin-grid/src/tests/ObjectGrid.exportOptionsKeys.test.ts:43 (docblock) and apps/console/src/tests/registry-inputs-spec-parity.test.ts:2782 (MEMBER_PINS prose) say the spec row is z.unknown(). Neither asserts it, so nothing goes red; both go stale once the spec version carrying this lands.",
    "carrier: none (noted in the PR's Acceptance notes) · check:strictness-ledger counts .zod.ts files only. The moved block is one CLOSED site, now in a non-.zod.ts module like the other non-barrel helpers, so the ui/ counts read 189 to 188 sites and 179 to 178 strict. The strip count, which is the ratchet's target, is unchanged at 7."
    ],
    "gates": {
    "derived": 112,
    "ran": 112,
    "exit_nonzero_final": 0,
    "not_measured": 0,
    "first_pass_exit3_rerun_green": [
    "pnpm --filter @objectstack/lint run check:doc-formula-expressions",
    "pnpm --filter @objectstack/lint run check:doc-security-posture",
    "pnpm --filter @objectstack/spec run check:skill-examples",
    "pnpm check:docs-transcript-drift",
    "pnpm check:dual-build-cjs-loads",
    "pnpm check:lean-entry-closure"
    ],
    "head": "032865c93c",
    "declared_to_ci": [
    "6 path-scheduled CI jobs (Test Core shards, Temporal Conformance, Dogfood Regression and Verify, Build Core, Build Docs)",
    "4 workspace type-check lanes",
    "54 artifact-roster families outside the derived total"
    ]
    },
    "line_budget": "620 changed lines (+442 / -178) over 10 files against merge base b91e40b, under the 5000 human-merge threshold. No skills/** file touched, so no skills line budget applies.",
    "deviations": [
    "The last gate chunk (91-112) passed the 600s foreground tool cap and the harness moved it to background. I blocked in the foreground on its PID (tail --pid) until it exited, then read its per-command exit-code record. No result came from an unwaited process.",
    "The export measurement's build wrote two committed artifacts beside the mutated file (json-schema.manifest/ui.json, authorable-surface/ui.json). Both were restored with git checkout HEAD and proven by git diff HEAD empty. A follow-up rebuild failed once on the stale manifest before the restore, and was clean after it.",
    "The first export-measurement attempt was refused by ablation-replace as a no-op (the anchor was inside the replacement). It was re-anchored and re-run; the first attempt measured nothing."
    ],
    "files_changed": [
    ".changeset/21229-object-grid-export-options-closed.md",
    "content/docs/references/ui/component.mdx",
    "docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md",
    "packages/spec/src/migrations/entries/semantic/18.ui-object-grid-export-options-closed.ts",
    "packages/spec/src/migrations/registry.ts",
    "packages/spec/src/ui/component-object-grid-export-options-members.pin.test.ts",
    "packages/spec/src/ui/component.zod.ts",
    "packages/spec/src/ui/list-view-export-options.ts",
    "packages/spec/src/ui/view-history.ts",
    "packages/spec/src/ui/view.zod.ts"
    ]
    }

  4. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21287 @ 032865c93c

    domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim 5943166878 · 2026-10-02T02:12Z

    • Shape (read on GitHub): a draft against main. The first line is Fixes #21229, then Clause-②: yes at a line start; the (narrowing) arm is in the changeset. PR assignee os-sales. 10 files, +442 / -178. No content/docs/releases/, and no governed path.
    • At-tier review: owed (Clause-②: yes; non-test packages/spec/src/**). Record 5944304045 names this landing head 032865c93cd45e071663e062a385af5f0b1b50c2 and reads VERDICT: PASS at CONTRACT_REVIEW_TIER. It found:
      • exportOptions holds ListViewExportOptionsSchema by identity, not the union, per triage's ruling 5939380297. A bare array, pdf, an undeclared key and null are all refused.
      • Both moves are verbatim and non-public. VIEW_HISTORY had to move to break a cycle.
      • The list view's accept and lift set is unchanged; only its nested union-branch message changed, as disclosed.
      • D3-only is right at the code: page.zod.ts:324 does not parse properties, and the props gate is advisory and CLI-only.
      • Order 59 does not collide with main's 57 and 58.
    • Changeset prose, checked here sentence by sentence: the advisory gate list and the save-and-load sentence; the four refusals with the array's prescription; the ObjectGridProps type; the list view's single nested message change; the FROM → TO table, including {} keeping the csv/json default ObjectGrid falls back to when formats is absent; and the census, with zero authorings and ten object-grid blocks as the control. Each restates a reading the record verified. "Deployed metadata was not measured" is said plainly.
    • Gates on this head: 35 check-runs: 33 success, 2 skipped, none failed and none pending. check-expected-skips: OK, both skips are on the roster. check-governed-merges --pr 21287: NOT governed, 620 changed lines. mergeable_state: clean. A local git merge-tree against origin/main merges without conflict.
    • Out-of-scope findings:
      • Acceptance notes: objectui's two stale prose comments at the pin (ObjectGrid.exportOptionsKeys.test.ts:43, registry-inputs-spec-parity.test.ts:2782). They assert nothing; the carrier is the next objectui spec pin bump.
      • dropped — the strictness-ledger count moving 189 → 188 is the block leaving a .zod.ts file. The strip count, which is the ratchet's target, is unchanged.
      • dropped — the PR body's "53 VIEW_HISTORY uses" is miscounted by one (52 uses on main). It is PR-body prose only and is not published.
    • Serial note: spec(ui): an object-master-detail-form detail entry's inlineMode describe says the mode is resolved from the relationship's inlineEdit when omitted; on an entry kept as authored the renderer resolves nothing #21284 (the detail entry's inlineMode describe) waits for this PR on component.zod.ts.
    • Landing: this PR is readied and enters the merge queue once this record is confirmed on the platform.
  5. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21287 → 5a9292e6f6

    domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim 5943166878 · 2026-10-02T02:38Z

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:specpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions