Skip to content

fix(rest)!: a schema-derived export skips the fields declared hidden - #24178

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24143-export-hidden-columns
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24143-export-hidden-columns

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24143

Clause-②: no (narrowing)

BREAKING (a served-set narrowing at a REST door): the default columns of GET /api/v1/data/:object/export lose every field declared hidden. Shipped as minor under the launch-window convention; the changeset carries the same Clause-② line and one ADR-0087 disposition (not-required (no-migration-prescription)). This narrowing owes one CONTRACT_REVIEW_TIER review of record before the queue.

What changed

  • packages/rest/src/data-action-routes.ts, the export handler's schema-derived column set (no ?fields=): the column list is metaMap's names minus every field whose definition has hidden === true. metaMap itself is untouched, so cell formatting and an explicit ?fields= that names a hidden field are answered as before. The field definitions are read through the existing templateFieldDefs (import-template.ts), which names fields exactly as buildFieldMetaMap does, so no shape normalizer is spelled a third time.
  • The route's own doc comment for fields=, and one sentence of the import-template.ts module header that described the export's default columns, now say what the export does. That header sentence is a comment-only edit outside the dispatched file surface; my change made it false, so it is corrected here rather than left stale.
  • Untouched, as ruled: the explicit ?fields= arm, the FLS narrowing (跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391, security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547) and the headerless no-projection path, format=json (it streams rows as the read returns them and has no column set), ?template=true (its own column rule), and everything in packages/objectql and packages/spec.

Shared predicate

None exists. No package exports an "is this field hidden" or "shown by default" predicate: @objectstack/core has none, and objectql (search-companion.ts isCompanionSourceEligible), spec (field-group-layout.ts, record-surface.ts, search-fields.ts) and rest (form-routes.ts) each spell hidden === true inline. This change spells it the same way, once, at the export's column selection.

Reach, re-measured on examples/app-showcase

A pnpm dev -- --fresh boot of this worktree, signed in as the seeded admin. The shipped admin sets carry no allowExport, so an environment permission set granting allowRead + allowExport was saved and assigned to the admin for the measurement. GET /api/v1/data/showcase_task/export?format=csv, no fields:

before (b0fe9fad4): Organization,Created By,Last Modified At,Last Modified By,Owner,Owning Business Unit,Title,Project,Assignee,Status,Priority,Estimate (h),Progress,Done,Due Date,Start Date,End Date,Created,Work Location,Cover,Labels,Notes,Sync Status,Sync Error,Search Index
after  (this PR):   Created By,Last Modified At,Last Modified By,Owner,Title,Project,Assignee,Status,Priority,Estimate (h),Progress,Done,Due Date,Start Date,End Date,Created,Work Location,Cover,Labels,Notes,Sync Status,Sync Error

After the fix, ?fields=title,organization_id,__search still answers the header Title,Organization,Search Index, with Default Organization in the Organization cells and an empty Search Index.

Breadth of the narrowing

On the same boot I exported every served object that carries a hidden field: 80 objects, of which 76 answered 200. For each one I compared the header before and after, and counted non-empty cells in each hidden column of the before export. The only labels that left any header are the hidden fields below. No other column moved, and no column appeared.

hidden field objects whose default header carried it value in the before export
__search (Search Index) 64 empty on all 190 rows
organization_id (Organization) 53 carries data: 167 of 191 rows, the record's organization (Default Organization here)
owning_business_unit_id (Owning Business Unit) 24 empty on all 134 rows (nothing stamps it yet, ADR-0117 D2)
sys_session.token (Session Token) 1 empty (the read omits it)
sys_account.previous_password_hashes 1 empty (the read omits it)
sys_api_key.key (Hashed Key) 1 header only, no rows

No example app (app-crm, app-showcase, app-todo, app-multi-package, embed-objectql) declares a hidden field of its own. The hidden fields are the companion, the two injected scope anchors (organization_id, owning_business_unit_id), and the three platform-object fields above. organization_id is the one column with real data that leaves the default export. A reader who wants it names it in ?fields=.

Tests

packages/rest/src/export-hidden-columns.test.ts drives the real export route: a real RestServer on a real ObjectStackProtocolImplementation on a real ObjectQL, with a better-sqlite3 :memory: driver. The companion is declared by its real producer (provisionSearchCompanion), next to an author-declared hidden field that carries a value. The security service answers getReadableFields the way plugin-security does for an unrestricted caller: every schema field, the companion included.

  • Anti-vacuity: the companion is declared hidden, and the readable projection the route asked for admits both hidden fields.
  • CSV default, with the security projection: header ID,Title; rows unchanged.
  • CSV default, without a security service (the masked-row fallback): header ID,Title.
  • CSV default, zero rows: the projected header is ID,Title.
  • XLSX default: header row ID,Title.
  • Control and preservation: explicit ?fields=title,internal_code,__search answers Title,Internal Code,Search Index, with IC-1/IC-2 in the hidden field's cells and an empty companion.

Ablation, through scripts/ablation-replace.mjs (the anchor must hit, and restore is checked against the blob). The subject is data-action-routes.ts, which the test reaches through relative ./rest-server imports, so it runs from src/ and no dist/ is involved. Mutation: ?.hidden !== true, became ?.hidden !== 'ablated-24143',, so the filter keeps every field. The anchor count went from 1 to 0, and the blob went from 7fe336246ce5 to a690835e24d0. Result: 4 failed, 2 passed. All four default pins went red: under the projection the header gained Internal Code and Search Index, and on the fallback path it gained Internal Code. The anti-vacuity case and the explicit-?fields= pin stayed green, which shows the explicit arm answers the same with the filter and without it. Restore leg: blob after restore 7fe336246ce5 equals HEAD, and git diff HEAD is empty.

Suite runs (on the merged tree 4f1363cde):

  • pnpm --filter @objectstack/rest test: 292 files passed; 6013 tests passed, 326 skipped.
  • pnpm --filter @objectstack/rest typecheck: exit 0. The test layer compiles, and --listFiles includes the new file.
  • test:repo for every package whose repo tests can read the changed files (each one scans packages/ or reads rest's sources): rest (5 files, 254 passed / 12 skipped), core (6 files, 84 passed), spec (66 files, 1105 passed), objectql (1 file, 5 passed) and types (1 file, 11 passed). Runtime's test:repo reads only rest-server.ts and meta-item-read-gate.ts from rest, neither of which this diff touches, so it was not run.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 4f1363cde: 63 derived commands, all exit 0. The --ran reconciliation answers 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN.
  • pnpm exec eslint --no-inline-config on the three changed source files: exit 0.

Acceptance notes

  • format=json has no column projection. It writes each row as the read returns it, so a hidden author field's value stays in a JSON export, and an explicit ?fields= does not narrow JSON rows either. That was already true before this change. FieldSchema.hidden is documented as a UI contract that has never governed serialization, and JSON is the serialization door, so JSON is left as is.
  • The import template (?template=true) still offers a writable author-declared hidden field as a column (ruling Q2 C on the template). The export and the template now disagree on such a field on purpose: the export answers "what do I see by default", and the template answers "what can I fill".

Generated by Claude Code

A schema-derived export (no ?fields=) took every field of the object, so the
hidden __search search companion became an empty "Search Index" column. The
default column set now drops every field declared `hidden`; an explicit
?fields= list is answered as before.

Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/import-template.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/src/import-template.ts) — pages documenting those are invisible to this run
  • 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 — 18 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 cb8756b005a2d77c7c07574dffe4f99b8e91f8d1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3042aae41dcba8f50dff167ef223d2e5ad77197f — the merge of head 4f1363cde882d0edb51c73a775f65a3f6292e071 into base cb8756b005a2d77c7c07574dffe4f99b8e91f8d1, 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 3042aae41dcba8f50dff167ef223d2e5ad77197f && git checkout 3042aae41dcba8f50dff167ef223d2e5ad77197f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cb8756b005a2d77c7c07574dffe4f99b8e91f8d1 4f1363cde882d0edb51c73a775f65a3f6292e071 && git checkout -B drift-repro cb8756b005a2d77c7c07574dffe4f99b8e91f8d1 && git merge --no-ff 4f1363cde882d0edb51c73a775f65a3f6292e071

node scripts/docs-audit/affected-docs.mjs --json cb8756b005a2d77c7c07574dffe4f99b8e91f8d1

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4f1363cde882d0edb51c73a775f65a3f6292e071
Local-runs: none
Scope: packages/rest/src/data-action-routes.ts, packages/rest/src/export-hidden-columns.test.ts, packages/rest/src/import-template.ts + .changeset/24143-export-hidden-columns.md, packages/rest/src/export-format.ts, packages/rest/src/rest-server.ts, packages/rest/src/form-routes.ts, packages/rest/src/index.ts, packages/rest/package.json, packages/rest/tsconfig.test.json, packages/core/src/utils/import-field-meta.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/data/injected-system-column-provenance.ts, packages/objectql/src/search-companion.ts, packages/objectql/src/registry.ts, packages/objectql/src/index.ts, packages/plugins/plugin-security/src/security-plugin.ts, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, scripts/pm/clause2-line.mjs, docs/adr/0087-metadata-protocol-upgrade-contract.md, .github/workflows/pr-automation.yml, .github/workflows/lint.yml, .claude/skills/pm-dispatch/references/filing-gate.md

Narrowing face: the PR body and the changeset both carry Clause-②: no (narrowing) line-initial, and the diff holds no packages/spec schema and no governed path. Net diff read as origin/main cb8756b00 to 4f1363cde (the head sits on that main; 3 commits, 4 files, +228/−6). Every file named after the + was read at the head.

① Derived judgments

  1. Served set of GET /api/v1/data/:object/export with no ?fields=, format=csv|xlsx — narrows. Right, and (narrowing) is the right arm. The default columns go from "every schema field, intersected with the FLS-readable set" to "every schema field whose definition has hidden !== true, intersected with the FLS-readable set". Read in the cited code: templateFieldDefs (import-template.ts:119-137) and buildFieldMetaMap (core utils/import-field-meta.ts) normalise schema.fields (array or map) under one name rule (def.name when it is a string, else the key), so defs.get(name) resolves for every metaMap key; the filter runs before fieldsFromSchema = true, and the two narrowings downstream — the security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547 readable projection and the 跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391 masked-row fallback — only ever intersect fields and never widen it, so the drop survives both paths; the zero-row header writes the already-narrowed fields when readableProjected || !fieldsFromSchema. The measured breadth follows from declarations, not from the run alone: the companion is hidden: true (search-companion.ts:320); TENANT_SCOPE_FIELD_DEF (organization_id, "Organization") and OWNING_BUSINESS_UNIT_FIELD_DEF are hidden: true, readonly: true, system: true (spec injected-system-column-provenance.ts:105-152); OWNER_FIELD_DEF is not hidden and stays.
  2. Accept set of the route — unchanged. Right; Clause-②: no is true. No query parameter is added, the handler's refuseUnknownQueryParams call is untouched, and the explicit ?fields= arm sits outside the edited branch (if (!fields || fields.length === 0)); the preservation pin names both hidden fields and gets both columns, the companion's cells empty because the read strips its value.
  3. Public surface — unchanged. Right. templateFieldDefs was already exported from import-template.ts and is not re-exported by packages/rest/src/index.ts; the diff adds an intra-package import only. No packages/spec path moves, so no generated artifact is owed and no api-surface gate applies.
  4. format=json — unchanged, by the diff and in fact. Right. formatRowForJson(row, metaMap, timezone) (export-format.ts:404) takes no column list and findArgs carries no fields, so a JSON export carries every key the read returns, with or without ?fields=. The defect this exposes is answered in ③.
  5. ?template=true — unchanged. Right. templateColumns (import-template.ts:154) reads its own TEMPLATE_COLUMN_EXCLUSIONS and never the export's fields.
  6. The import-template.ts header edit — comment-only, outside the dispatched surface, declared in the report and in the PR body; the sentence had become false and leaving it stale would have been the worse act. Accepted. One non-blocking nit: the next sentence's example list of injected columns the export carries ("created_at, created_by, organization_id") now names one the export no longer carries by default; its point still holds for the other two. A follow-up comment edit, not a verdict factor.
  7. The pin (export-hidden-columns.test.ts) reaches the real route. Right. The fourteen undefined positional arguments put securityServiceProvider at RestServer's 18th parameter (rest-server.ts:2104-2131), and the anti-vacuity case proves the provider was consulted (readableAsked has length 1). The stub's answer — every schema field, the companion included — is what plugin-security's computeReadableFields (security-plugin.ts:5645-5666) returns for an unrestricted caller: it filters on fieldPerms[f].readable !== false and never reads hidden, which is why the showcase measurement saw the column. withSecurity: false exercises the masked-row fallback. ./rest-server without .js, and better-sqlite3 through SqlDriver, each match 86 sibling tests and tsconfig.test.json's module semantics; provisionSearchCompanion is on objectql's index and every package the test imports is in rest's dependency set. The ablation (4 of 6 red with the filter neutralised, restore proven against the blob) is read from the report, not re-run.
  8. The hidden === true spelling and the cast. Right. TemplateFieldDef (import-template.ts:81-92) declares no hidden member, so as { hidden?: unknown } is load-bearing. The spelling matches isCompanionSourceEligible (search-companion.ts:136) and form-routes.ts:452; no package exports a hidden or shown-by-default predicate, so none is owed.
  9. Edge, noted and not a finding: an object whose every field is hidden leaves names empty, so fieldsFromSchema stays false and the handler falls to first-row derivation — the pre-existing behaviour for an empty metaMap, unreachable while the primary key and the audit columns are never hidden.

② Semver level

  • Changeset .changeset/24143-export-hidden-columns.md: '@objectstack/rest': minor; summary fix(rest)!:; a **BREAKING** line; Clause-②: no (narrowing) line-initial; one ADR-0087 marker, an HTML comment reading adr-0087: not-required (no-migration-prescription) with its reason.
  • What the diff publishes: a served-set narrowing at a REST door of a published package (@objectstack/rest 18.0.0-next.3). That is BREAKING, so at least minor; minor and not major under the launch-window convention (check-changeset-no-major.mjs header: "During the launch window we ship breaking changes as minor"). Not patch, not skip-changeset. Right.
  • Disposition: no-migration-prescription is the honest one of the closed CATEGORIES (check-adr-0087-registration.mjs:536-543). registered and already-registered need an ADR-0087 registry id and the diff touches no registry; unpublished fails on a published package; runtime-interface-only, type-surface-only and runtime-export-retired each name a path-qualified TS declaration and none moved; the catch-all is refused only by a body carrying a migration prescription, and the body's one consumer sentence ("names the field in ?fields=") is a usage note with no FROM → TO rewrite, arrow or table. Nothing exists for objectstack migrate meta to rewrite — no spec key, export, config field or stored shape moves — so the "breaking changesets carry their migration" rule has no FROM → TO to demand. Right. The gates agree: Check Changeset (pr-automation.yml:244, runs both scripts) and Lint · CI, release & engine gates (lint.yml:3137) are success on the head.
  • The declaration line: PR body line 3 and changeset line 7, one spelling, both carriers agree. The dispatch's provisional Clause-②: no was re-declared from the diff with the arm, as the dispatch asked. Right.
  • The changeset's "Served less now" and "Unchanged" lists match the code read in ①.

③ Boundary flags

  • Open question (organization_id leaves the default export; A accept as shipped, B carve it out): A, accepted — not escalated. (i) The card's Done-when is "no column for a hidden field, __search included" and the triage ruling (6229423208) is "hidden fields drop from the default export columns": every hidden field, no exception named. (ii) organization_id is hidden: true by the platform's own declaration (TENANT_SCOPE_FIELD_DEF), the same flag the default UI projection reads ("Hidden from default UI", field.zod.ts:2074), so the export now agrees with the list — which is the card's own "Why". (iii) A carve-out is a second dialect of "default column" that no author can see in the metadata (Prime Directive Add comprehensive test suite for Zod schema validation #12: one strict contract, no consumer-side accommodation); if the tenant anchor should show by default, the change belongs on its declaration, not in the export.
  • Out-of-scope finding 1 (format=json ignores ?fields= and has no column projection): the reading is right; accepted as outside this card; escalated to the dispatching seat as a filing candidate. It predates the diff and sits outside the dispatched surface, so "carrier: none" is right for this PR. It is more than a note, though: the route's own doc comment (data-action-routes.ts:752) declares fields=a,b,c with no format caveat, and a declared query parameter that one format silently drops is the shape AGENTS.md's route rule 5 names (a dropped projection returns the full set and no status distinguishes it) and Prime Directive chore: version packages #10's declared-but-not-enforced. Evidence for the card: that contract text plus the code read; the repro is one request, ?format=json&fields=title.
  • Out-of-scope finding 2 (?template=true still offers a writable author-declared hidden field): right and deliberate — accepted, no card. The template's rule is keyed on writability (TEMPLATE_COLUMN_EXCLUSIONS) under its own ruling; the export's on visibility. They answer different questions.
  • Dev flag, the comment-only edit outside the dispatched file surface: accepted (① item 6).
  • Dev flag, mode:subagent: Implemented-by: therefore names the branch.
  • Observation for the PM seat, not a verdict factor: filing-gate.md 定级判据 counts a REST/data-door narrowing into the protocol class ("REST/数据门收窄亦算"), which that section grades "p1 + 在研大版 target:"; the card carries priority:p2 and no target: label, graded as a defect before the narrowing face was known. Re-grading is the triage/PM seat's call under "维护者裁决"; it does not touch this PR's contract.

Check-runs on the head, as read: 38 in all; 24 success (among them Lint & Repo Gates, Build Core, Governed Surface Queue Guard, Check Changeset, every Lint · shard, Type Check · source gates and Type Check · debt ledger), 6 skipped (Build Docs, Console Pin Gate, the dogfood matrix shard, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance — full-battery or opt-in lanes), 8 in_progress (Test Core 1 to 6, Type Check · consumer gates, Type Check · workspace); no failure. The TypeScript Type Check and Test Core summary contexts had not been created yet. Not awaited; the queue guard reads them.

Implemented-by: claude/issue-24143-export-hidden-columns
Reviewed-by: session_01BVm1pAXVXQ3PjC42j1kPFf

VERDICT: PASS

Inputs read at 2026-10-17T15:24Z: card #24143 (body and its 3 comments), PR #24178 (body, 4-file list, net diff against main), and the 38 check-runs on the head; nothing built, run or re-run.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · domain:cli seat 1 · os-sales · session session_01BVm1pAXVXQ3PjC42j1kPFf · 2026-10-17T15:28Z

Head 4f1363cde. This is the seat's review of record for #24143 (claim 6234867555). The narrowing face is judged by the at-tier record 6235720861 on this head (Served-tier: CONTRACT_REVIEW_TIER, PASS, from an isolated read-only agent given the card, the PR and the check-runs only). This record covers what the review leaves to the seat: the form, the prose faces, the seat's own read of the diff, the routing, and the landing.

  • Form:
    • Draft → main, titled fix(rest)!: ….
    • The body's first line is Fixes #24143, its only closing keyword (scanned).
    • Clause-②: no (narrowing) is line-initial in the body and the changeset, re-declared from the claim's bare no as the claim asked. The default export serves fewer columns, no query parameter is added, and the explicit ?fields= arm is outside the edited branch.
  • The ruling executed as ruled: triage 6229423208: "hidden fields drop from the default export columns, an explicit ?fields= stays as it is, and an ablated pin."
  • The seat's own read of the code: the field definitions are read through rest's existing templateFieldDefs, under the same name rule buildFieldMetaMap uses, so no third shape normalizer is written.
    • No package exports a hidden or shown-by-default predicate. The inline hidden === true matches form-routes.ts:452 and isCompanionSourceEligible, and the dev measured that instead of assuming it.
    • The import-template.ts header edit is comment-only and makes a false sentence true. The review's nit on the next sentence does not hold: that sentence says the registry injects organization_id onto every object and that a value typed into it never lands, which is about the template's write path and is still true. No edit is owed.
  • Prose faces, sentence by sentence against the diff:
    • The changeset's Served less now lists Search Index, Organization, Owning Business Unit, Session Token, Previous Password Hashes and Hashed Key. Each is a field declared hidden: true: the companion at search-companion.ts:320, and the injected tenant and business-unit anchors at injected-system-column-provenance.ts:105-152, per the review. ✓
    • "No other column moved": owner_id is not hidden and stays. ✓
    • Unchanged: explicit ?fields=, FLS narrowing, zero-row header rules, format=json and ?template=true. Each matches the code the review read. ✓
    • The ADR-0087 marker not-required (no-migration-prescription) is honest: no spec key, export, option or stored shape moves, and the consumer note ("names the field in ?fields=") is a usage note, not a FROM → TO rewrite. Check Changeset is green. ✓
  • The dev's open question and findings, routed:
    • Open question (organization_id leaves the default export): A, accepted as shipped. The card's done-when and triage's ruling name every hidden field, with no exception. The platform declares the tenant anchor hidden, the same flag the default UI projection reads, so the export now agrees with the list. Its value is one organization across a tenant-scoped export, and ?fields=organization_id still serves it. A carve-out would be a second, invisible rule for what "default column" means. If the anchor should show by default, the change belongs on its declaration, not in this door. The review answers A as well.
    • Out-of-scope finding 1 (format=json ignores ?fields=): filed as rest(export): format=json ignores ?fields=, so a list view's JSON export carries every readable field instead of the view's columns #24183 in this act. The review escalated it as a filing candidate, and it now has reach: objectui's list-view handleExport sends format=json with the view's visible columns as fields (packages/plugin-list/src/ListView.tsx, through data-objectstack's exportDownload), and the server drops that list.
    • Out-of-scope finding 2 (?template=true still offers a writable hidden field): a deliberate difference. The template's rule is keyed on writability, the export's on visibility. Acceptance notes, carrier: none.
    • The review's grading observation (filing-gate.md 定级判据 counts a REST-door narrowing as protocol class): grading is triage's, and the card was graded before the narrowing face was known. It closes on this landing, so no re-grade is asked for.
  • Landing: not governed (check-governed-merges --pr 24178: 0 of 4 paths on the register; 234 changed lines ≤ 3000). With the review of record PASS on this head, the PR goes ready with auto-merge armed once every check on this head is green. Test Core 3/6 and 4/6 are the last ones pending.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 17, 2026 15:37
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 17, 2026 15:37
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit aaaeeac Oct 17, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24143-export-hidden-columns branch October 17, 2026 16:05
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 size/m tests tooling

Projects

None yet

2 participants