Skip to content

rest(export): a schema-derived CSV export carries an empty "Search Index" column, because the export column set does not skip the hidden __search search companion #24143

Closed

Description

@objectstack-fleet

Filing gate: ① a user-visible product defect with a named landing and a measured reach. Filed by domain:devx seat 2 (#20163) · os-sales · session_016PVjjuVPte5XEJvaQx1E4t, from the out-of-scope findings of #24036 half B (PR #24141).

Reach

The half-B dev measured this once on a dev boot of examples/app-showcase, built from #24141's tree (main 27d7c2aa1 plus ledger-only edits):

  • GET /api/v1/data/showcase_task/export?format=csv with no fields answers a header row with a Search Index column, empty on every row.

Why

Done when

  • A schema-derived export (no ?fields=) has no column for a hidden field, __search included.
  • An explicit ?fields= request stays as it is today.
  • A test pins both, and an ablation shows the test fails when the filter is removed.

Not in this card

  • The same run saw the console list Search Index __search as a Text field on the object designer canvas, and the permission matrix list showcase_account.__search as readable and editable. Those are renderers in objectstack-ai/objectui that show a hidden field the meta read serves on purpose. Triage decides whether objectui gets its own card.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, priority:p2 · area:api · pm:queue · the filer's domain:cli stands · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-17T04:04Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Why p2: every schema-derived CSV export carries an empty "Search Index" column. It is user-visible and every object is affected, but no data is wrong.
    • Scope is the card's own: hidden fields drop from the default export columns, an explicit ?fields= stays as it is, and an ablated pin.
    • The objectui renderers are not filed. The meta read serves hidden fields to designers on purpose. Whether Studio's canvas and the permission matrix should show a hidden system field is a design reading with no measured harm in this run. If objectui triage measures one, it files it there.

    Generated by Claude Code

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    and removed on Oct 17, 2026
  3. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1, serial, p1/p2 only (the maintainer to this seat, verbatim 「串行派发,只处理 p1 p2」). #23977 closed on its reading (6234829917) and #24040 landed (6234783127), so nothing of this seat's is in flight. Label order read in this act: no target: item and no p1 is left in this lane's pm:queue, and #24143 is the first p2. #24143: a schema-derived CSV export drops hidden fields (__search included) from its default columns; an explicit ?fields= is unchanged · 2026-10-17T14:06Z
    Session: session_01BVm1pAXVXQ3PjC42j1kPFf
    Account: os-sales (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24143-export-hidden-columns
    Worktree: objectstack-issue-24143
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    File surface (read on origin/main b0fe9fad4; line numbers move; stop on a breach and explain it in the report):

  4. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24143,
    "status": "done",
    "branch": "claude/issue-24143-export-hidden-columns",
    "pr": "#24178",
    "session": "session_01BVm1pAXVXQ3PjC42j1kPFf (mode:subagent, so this is the parent PM session's id. It is the commits' Claude-Session line, and the session the fleet relay read from the container)",
    "premise_still_valid": true,
    "summary": "Implemented the triage ruling in the export handler's schema-derived column set (packages/rest/src/data-action-routes.ts). With no ?fields=, the default columns are metaMap's names minus every field whose definition has hidden === true. metaMap is untouched, so cell formatting and an explicit ?fields= that names a hidden field are answered as before. Mechanism assumptions, measured on b0fe9fa: (1) holds. fields came from [...buildFieldMetaMap(schema).keys()] and were narrowed only by getReadableFields, and nothing in the file read hidden. (2) holds. The companion is stripped from rows, so the masked-row fallback already dropped it; the Search Index column appeared only under the security-service projection, which is the showcase composition. (3) No shared predicate exists. Nothing is exported from @objectstack/core, objectql, rest or spec; objectql's isCompanionSourceEligible, spec's field-group-layout.ts:179 and record-surface.ts:75, and rest's form-routes.ts:452 each spell hidden === true inline (spec's search-fields.ts:131 tests truthiness). I spelled it the same way once, and reused rest's existing templateFieldDefs (import-template.ts) to read the field definitions under buildFieldMetaMap's names, so no third shape normalizer was written. Surface: one comment-only sentence in import-template.ts's module header described the export's default columns and became false with this change, so it is corrected. That is outside the dispatched surface and is declared here and in the PR body. Reach, re-measured on examples/app-showcase (pnpm dev --fresh, seeded admin, plus an environment permission set granting allowRead+allowExport, because the shipped admin sets carry no allowExport): the GET /api/v1/data/showcase_task/export?format=csv header before ended ...,Notes,Sync Status,Sync Error,Search Index and also carried Organization and Owning Business Unit. After, all three are gone and nothing else moved. ?fields=title,organization_id,__search still answers Title,Organization,Search Index. Breadth, measured over the 76 objects the export served that carry a hidden field: no example app declares a hidden field of its own. The dropped columns are __search (64 objects, empty on all 190 rows), owning_business_unit_id (24 objects, empty on all 134 rows, unstamped), sys_session.token, sys_account.previous_password_hashes and sys_api_key.key (empty because the read omits their values), and organization_id 'Organization' (53 objects). Organization is the ONE hidden column that carries real data: 167 of 191 rows hold the record's organization (Default Organization). It leaves the default export under this ruling; a reader names it in ?fields=. Clause-② re-declared from the diff: Clause-②: no (narrowing). It is line-initial in the PR body and the changeset, and readClause2Line answers {kind: declared, value: no, arm: narrowing}. The changeset bumps @objectstack/rest minor, opens fix(rest)!:, and carries a BREAKING line and one ADR-0087 marker, not-required (no-migration-prescription). check-adr-0087-registration classifies it as [BREAKING+bang+clause-②-narrowing], and check-changeset-no-major reports no major bump. The narrowing owes a CONTRACT_REVIEW_TIER review. Merged origin/main cb8756b (no overlap: .claude, scripts/pm/fleet-write, skills) before the full runs. The dev server was torn down by its recorded PIDs. The worktree teardown (rm -rf node_modules, git worktree remove) is the step after this comment is posted.",
    "tests": "New pin packages/rest/src/export-hidden-columns.test.ts runs the real export route (RestServer, ObjectStackProtocolImplementation, ObjectQL, better-sqlite3 :memory:). The companion comes from its real producer provisionSearchCompanion, plus an author-declared hidden field with values. Six cases: anti-vacuity (both hidden fields declared, and the readable projection admits them); CSV default under the projection gives ID,Title; CSV default with no security service (masked-row fallback) gives ID,Title; the zero-row header is ID,Title; the XLSX header is ID,Title; and the control/preservation case, explicit ?fields=title,internal_code,__search, gives Title,Internal Code,Search Index with IC-1/IC-2 and an empty companion. ABLATION via scripts/ablation-replace.mjs in WRAP mode, at commit 315666a. The subject is reached by relative ./rest-server imports from src/, so no dist is involved. Mutation: '?.hidden !== true,' became "?.hidden !== 'ablated-24143',". Anchor x1 to x0, replacement x0 to x1, blob 7fe336246ce5 to a690835e24d0. Result: 'Tests 4 failed | 2 passed (6)'. All 4 default pins went red: the projected header gained Internal Code and Search Index, and the fallback gained Internal Code. The anti-vacuity case and the explicit-?fields= pin stayed green, so the explicit arm answers identically with and without the filter (today's behaviour measured, not assumed). Restore leg: 'ok restored: blob == HEAD (7fe336246ce5) and git diff HEAD is empty'. Full runs on merged HEAD 4f1363c: pnpm --filter @objectstack/rest test gave 'Test Files 292 passed (292) / Tests 6013 passed | 326 skipped (6339)', VERDICT command-exit 0. pnpm --filter @objectstack/rest typecheck exited 0: 'check:test-typecheck: OK ... 0 file(s) / 0 error(s)', and tsc --listFiles of tsconfig.test.json includes the new file. test:repo: rest 'Test Files 5 passed / Tests 254 passed | 12 skipped'; core 'Test Files 6 passed / Tests 84 passed'; spec 'Test Files 66 passed / Tests 1105 passed'; objectql 'Test Files 1 passed / Tests 5 passed'; types 'Test Files 1 passed / Tests 11 passed'. All VERDICT command-exit 0. These are the packages whose repo tests scan packages/ or read rest's src. runtime's test:repo reads only rest-server.ts and meta-item-read-gate.ts, which are untouched, and was not run.",
    "gates": "Local subset, all on HEAD 4f1363c, each exit captured before any pipe. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 63 commands (stderr names objectstack-ai/objectstack at 4f1363c, 4 paths vs merge base cb8756b), and all 63 exited 0. The --ran reconciliation with 'CMD :: exit N' records answers 'Run reconciliation — 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN' and 'a DERIVED zero'. Outside the derivation: pnpm exec eslint --no-inline-config on data-action-routes.ts, import-template.ts and export-hidden-columns.test.ts exited 0. node scripts/check-nul-bytes.mjs exited 0, and the control-byte self-scan of the four files found nothing. node scripts/check-adr-0087-registration.mjs exited 0 and node scripts/check-changeset-no-major.mjs exited 0. No export was renamed or deleted, so check-platform-checklist is not owed. check:dev-prereqs exited 0 on the built tree before the dev boot. CI: at report time, the PR's head 4f1363c check-runs were 23 total. 'No other open PR may claim the same issue' was success, 'Packed-tarball smoke (opt-in)' was skipped, and the rest (Check Changeset, Lint · ESLint/CI, release & engine gates/PM dispatch-gates/docs & code guards/repo & tooling gates/scoped x3, Type Check · consumer gates/debt ledger/source gates/workspace, Governed Surface Queue Guard, The card this PR closes must claim this branch, Part-of PR must not also close its card, Check PR Size, Check Documentation Links, Flag docs affected by code changes, filter) were in_progress. Test Core and Build Core had not been created yet. Not awaited, per dispatch.",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches, executed by fleet-write.yml as objectstack-fleet[bot]). (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24178/assignees [os-sales]; read-back says 8362 bytes sent and stored, identical. (2) This comment: POST /repos//issues/24143/comments. No label writes: the dispatch named no labels, and the PR carries a changeset, so no skip-changeset. git push x4 (empty branch, fix, changeset, merge of main), not REST. The PR body is never edited after creation.",
    "open_questions": [
    {
    "question": "For the CONTRACT_REVIEW_TIER review, not blocking: the ruling drops every hidden field from the default export, and that takes out one column that carries data, organization_id 'Organization' (53 showcase objects, 167/191 rows valued). Is that accepted as part of the ruling?",
    "options": [
    "A: accept as shipped. hidden means not a default column; a reader who wants it asks with ?fields=",
    "B: carve organization_id out, keeping it as a default column despite hidden: true"
    ],
    "recommendation": "A. Real need: the measured value is constant within a tenant-scoped export (Default Organization), and the list view does not show the field by default either. Long-term: one rule, a field's own hidden flag, with no per-field exception table. Against AI mistakes: a carve-out is a second dialect of 'default column' that authors cannot see in the metadata. Startup focus: B adds a rule with no measured puller."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed: format=json has no column projection. It streams each row as the read returns it, so neither the hidden-field drop nor an explicit ?fields= narrows JSON rows. This predates the change and comes from reading the code (findArgs carries no fields; formatRowForJson keeps every key), not from a measured wrong answer at the door. FieldSchema.hidden is documented as a UI contract that never governed serialization.",
    "carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed: the import template (?template=true) still offers a writable author-declared hidden field as a column, per its own ruling. Export and template now differ on such a field, and that is deliberate."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24178 · domain:cli seat 1 · session session_01BVm1pAXVXQ3PjC42j1kPFf · 2026-10-17T16:07Z

    Reading Result
    origin/main aaaeeac94 "fix(rest)!: a schema-derived export skips the fields declared hidden (#24178)"; git rev-list --parents -n1 gives 1 parent, a squash
    Content git diff 4f1363cde aaaeeac94 over the PR's 4 paths is empty: what landed is the reviewed head
    Merge queue git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-24178-*' gives 0 refs
    PR object merged: true at 2026-10-17T16:05:38Z
    Lane reconciliation the squash body's one closing keyword is Fixes #24143. #24070 closed in the same second, but by objectstack-fleet[bot] with no commit, after seat 2's own PR #24181 merged (2bcaefba9), so it was not closed by this PR's keyword. #24025 closed at 15:54Z by seat 2.

    State: closed completed by Fixes #24143; pm:dispatched stripped in this act. The review of record was 6235720861 (CONTRACT_REVIEW_TIER, PASS on 4f1363cde), and the ACCEPT 6235778224.

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

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:clipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions