Skip to content

fix(rest)!: the CSV export writes a text cell in a form its consumer reads as text, and the CSV import reverses it (#25078) - #25130

Draft
objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/issue-25078-export-value-context
Draft

objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/issue-25078-export-value-context

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25078

Clause-②: no (narrowing)
Metadata-protocol: none
Blocked-by: objectstack-ai/objectui#13315

The CSV export now writes a text cell its consumer would interpret in a form that consumer reads as text, and the CSV import reverses exactly that. This is this repository's half of the maintainer's choice B on #25078 (comment 6319255545): the rule is defined once, server-side, and both directions read it.

This PR stays a draft until two things are on main: objectstack-ai/objectui#13315's PR, and this repository's .objectui-sha bump over that commit. This PR does not move the pin.

Contract

Served answers that change

  • GET /api/v1/data/:object/export?format=csv, on the metadata path and on the raw path (no field metadata). A CSV text cell is served with one apostrophe (U+0027) in front of its content, before the RFC-4180 quoting, when both of these hold:
    • its first character is one of a small fixed set that the file's consumer interprets;
    • it is not made only of digits, spaces and the characters a signed number or a phone number is written with.
    • A text cell is a string in a column whose field is not declared numeric, boolean, date or datetime. The declared type decides, never the JS type. Header labels and reference display titles are text cells too. On the raw path every string cell is one.
    • Every other byte of the file is unchanged.
  • parseCsvToRows, exported from @objectstack/rest, and through it the CSV payload of three doors:
    • POST /api/v1/data/:object/import;
    • POST /api/v1/data/:object/import/jobs;
    • POST /api/v1/auth/admin/import-users.
    • Every cell, header row included, loses exactly one leading apostrophe when the rest of the cell is one the export would mark. Cells without that shape are unchanged.
    • The reversal runs at the parse, before an inline or a named mapping, and it reads the cell text only. With a named mapping, rows keep their source headers until core's applyMappingToRows, so no target field type is known at that point.

Unchanged by construction: phone numbers and signed amounts; every cell that does not begin with one of the set; numeric, boolean, date and datetime cells; the xlsx and json export formats; json and xlsx import payloads.

Stated consequences

  • A user-authored CSV cell that is an apostrophe followed by a cell the export would mark now imports without that apostrophe. A cell that begins with an apostrophe followed by anything else imports unchanged.
  • A stored text value of that same shape is written unchanged by the export, so it comes back without its leading apostrophe. Measured with a one-shot script against this branch: it is the one stored shape the round trip does not return unchanged. It is raised in the report as an open question for the seat.
  • A value beginning with TAB or CR is marked, and the import removes the mark. Core's default trimWhitespace: true then trims the leading TAB or CR, exactly as it did before this change. With trimWhitespace: false the value returns exactly. Both cases are pinned in csv-text-cell-round-trip.test.ts.

The rule lives in packages/rest/src/export-format.ts (csvTextCellNeedsMark, markCsvTextCell, unmarkCsvTextCell, isCsvTextColumn). The writer is formatCsvCell / rowsToCsv in rest-server.ts. The reversal is in parseCsvToRows in import-prepare.ts.

Files

  • packages/rest/src/export-format.ts: the rule, defined once. The set of non-text types is composed from the spec's NUMERIC_VALUE_TYPES, BOOLEAN_VALUE_TYPES, CALENDAR_DATE_TYPES and INSTANT_TYPES, not re-listed.
  • packages/rest/src/rest-server.ts: formatCsvCell takes a text-column flag, and rowsToCsv derives that flag per column from the declared type.
  • packages/rest/src/import-prepare.ts: parseCsvToRows reverses every cell, and reverses header cells before trimming and mapping.
  • packages/rest/src/data-action-routes.ts: comment only. The export route's doc said the raw path streams stored values unchanged, which this change makes false for csv.
  • Pins:
    • packages/rest/src/csv-text-cell.test.ts (new);
    • packages/rest/src/csv-text-cell-round-trip.test.ts (new);
    • one new describe in packages/plugins/plugin-auth/src/admin-import-users.test.ts, on that file's existing engine double, so the engine-double ledger does not move.
  • .changeset/25078-csv-text-cell.md: @objectstack/rest patch. It carries the BREAKING banner and one ADR-0087 marker, not-required (no-migration-prescription).

Tests

All runs are on the final head bf8244cb20.

  • The predicate: one table of 56 cases, with the boundary characters on both halves of the rule.
    • The code-point neighbours of each trigger as a first character.
    • Every character of the numeric-text set, after each trigger character the rule's second half applies to.
    • A letter or a trailing word outside that set.
  • The writer table, per value, on the metadata path and on the raw path:
    • plain words after a trigger character are marked;
    • two phone numbers, a signed amount, a company name and an unsigned amount are byte-identical;
    • number, currency and percent fields holding a negative amount stay numeric and unprefixed;
    • a string in a numeric, boolean, date or datetime column is never marked;
    • header labels and reference titles that start with a trigger character are marked.
  • The round trip, through each CSV door:
    • the sync import, with an inline mapping and with a named mapping;
    • the async import job;
    • the admin user import, whose file comes from the real export route of RestServer.
    • Each case asserts that the file carried the marks, and that every stored value came back exactly, including a value that begins with an apostrophe followed by a plain word.
  • Suites:
    • @objectstack/rest local project: 313 files / 6318 tests passed, 326 skipped.
    • @objectstack/plugin-auth: 181 files / 3423 tests passed, 10 skipped.
    • Both packages' typecheck, the test layer included, exit 0.

Ablations. Each leg goes through scripts/ablation-replace.mjs. The anchor must hit, and the restore is proven by blob equal to HEAD and an empty git diff HEAD. The whole tree's porcelain is empty after every leg.

  • Writer ablated (formatCsvCell never marks): 20 red of 133.
  • Header marking ablated: 5 red.
  • Declared-type decision ablated (isCsvTextColumn always true): 12 red.
  • The predicate's second half removed: 36 red.
  • Cell reversal removed: 14 red.
  • Header reversal removed: 6 red.
  • Admin door, which resolves @objectstack/rest through dist/:
    • The reversal was removed and rest was rebuilt. ablation-dist-preflight then reported the marker absent from all 6 built files, and the admin round trip went red, 1 of 1.
    • After the restore and a rebuild, the preflight reported the marker present in 2 built files and a clean tree, and the pin was green again.

Gates

All readings are at bf8244cb20, with each exit code captured before any pipe.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 71 commands. Every one of them exited 0, and so did the 10 families listed under "Declared WIDE population".
  • The reconciliation printed: ✓ dispatch-gates --ran: 71 derived famil(ies) accounted for — 71 run, 0 NOT-MEASURED.
  • node scripts/check-changeset-no-major.mjs --base origin/main --event with this body: exit 0, LEVEL AXIS ... a non-protocol change at patch.
  • node scripts/check-adr-0087-registration.mjs --base at the merge base: exit 0.
  • pnpm lint over the whole repository: exit 0.
  • CI runs the full farm.

main at fccbd9d0d5 was merged once into this branch. The incoming commits touch no file this PR edits. One of them adds the NOT NULL tenant column, and that is why the round-trip harness calls installDefaultOrganization.

Acceptance notes

  • The changeset level is patch, with Metadata-protocol: none. That follows AGENTS.md Post-Task Checklist step 3 as it reads on main since f2b6cf230b: a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and its ADR-0087 disposition.
  • For import wizard: send a CSV file's own CSV to the server's CSV import door (format csv) on every path, so the server applies the one cell rule its CSV export pairs with (objectstack#25078) objectui#13315: an inline mapping sent with CSV text must name a column by its header as this parse reads it, that is after the reversal. This only matters for a column whose label begins with one of the set. At the pin cf153eb2e5, the wizard's default path applies the mapping in the browser, and the mapping is keyed by field name.
  • The round-trip harness calls installDefaultOrganization. The async job door's sys_import_job write needs the Default Organization on the NOT NULL tenant column that landed on main in 5e9b100743. Without that call one test is red, measured.

Generated by Claude Code

…reads as text; the CSV import reverses it (#25078)

The CSV writer marks a text cell its consumer would interpret, by one rule
defined once in export-format.ts and keyed on the field's declared type; the
CSV import parse removes exactly that mark, so an export to import round trip
through the sync, async-job and admin user import doors returns the stored
value. xlsx, json and the json import door are unchanged.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…s existing engine double (#25078)

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…not return unchanged (#25078)

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…port-value-context

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…the tenant column needs (#25078)

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

12 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 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 — 19 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 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e

⚠️ 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: bf8244cb20dcaa155853344a15baad129cef7e0e
Local-runs: none
Scope: packages/rest/src/export-format.ts, packages/rest/src/rest-server.ts, packages/rest/src/import-prepare.ts, packages/rest/src/data-action-routes.ts, packages/rest/src/csv-text-cell.test.ts, packages/rest/src/csv-text-cell-round-trip.test.ts, packages/plugins/plugin-auth/src/admin-import-users.test.ts + packages/spec/src/data/field-value.zod.ts, packages/spec/src/data/index.ts, packages/core/src/utils/import-field-meta.ts, packages/core/src/utils/import-mapping.ts, packages/core/src/utils/import-coerce.ts, packages/plugins/plugin-auth/src/admin-import-users.ts, packages/rest/src/index.ts, packages/rest/package.json, packages/rest/src/default-organization.testkit.ts, .objectui-sha, content/docs/references/api/export.mdx, AGENTS.md (Post-Task Checklist step 3 as it stands on main 4e2c42f363); the prose face .changeset/25078-csv-text-cell.md is judged in ②.

① Derived judgments

Read on the net diff of bf8244cb20 against its merge base with main (fccbd9d0d5; main has since moved to 4e2c42f363 without touching any file this PR edits): 8 files, +740/−10. Inputs: card #25078's body and all ten comments, the PR body and file list, the head's check-runs.

  1. The rule, defined once — right. csvTextCellNeedsMark (export-format.ts) is the only definition: the text's first character is one of the six leads AND the text holds at least one character outside the numeric-text set (digits, the space, and the six punctuation marks a signed amount with a group separator or a phone number with parentheses is written with; the first character counts). markCsvTextCell prefixes one mark (U+0027); unmarkCsvTextCell removes exactly one mark when and only when the remainder satisfies the predicate. No second copy of the predicate exists under packages/ outside the pins (git grep at the head). The character class is a literal set with one digit range and an escaped hyphen, so it holds exactly those seventeen characters and nothing else.

    • The 56-case table — no boundary mis-sorted. Counted: 6 leads before a plain word; 6 leads alone (the four that are themselves outside the set marked, the two signs alone not); 9 + 9 set members after each sign; 5 outside-set cases; 8 ordinary business values including the empty cell; 11 code-point neighbours of the six leads (both sides of each, the neighbour the two signs share counted once), every one unmarked exactly as the code sorts it; a leading space and the mark itself as first character, unmarked. Each expected value agrees with the predicate as written, and the table is round-tripped through mark then unmark for every row.
  2. Served answer GET /api/v1/data/:object/export?format=csv — right, and exactly as the PR body states it. rowsToCsv derives textColumns once per export from isCsvTextColumn(metaMap.get(f)); formatCsvCell(value, textCell) marks a value only when it is a string AND the column is a text column, before the RFC-4180 quoting (/[",\r\n]/), so the quote wraps the mark. The header row is marked with the flag fixed true; a reference display title and an option label arrive from formatRowCells as strings in a text column and are marked by the same rule. The raw path: the route starts from an empty map and the schema-read catch leaves it empty, so every column is a text column and every string cell goes through the rule. rowsToCsv has two call sites (data rows; the zero-row header), both in this route; no other CSV writer exists in packages/rest/src.

  3. Text columns by declared type, never by JS typeof — right. CSV_NON_TEXT_TYPES is composed from the spec's NUMERIC_VALUE_TYPES (number, currency, percent, rating, slider, progress, summary), BOOLEAN_VALUE_TYPES (boolean, toggle), CALENDAR_DATE_TYPES (date) and INSTANT_TYPES (datetime), reached through packages/spec/src/data/index.ts; packages/spec/** is untouched. A string in any of those columns is never marked (pinned for six types); a number, boolean, bigint, Date or serialised object is never marked in any column (the typeof string arm is the only one that calls the rule). time, formula, autonumber, json, the option and reference types are text columns, and a string cell in them is governed by the predicate alone.

  4. xlsx and json export — byte-unchanged, right. The xlsx arm writes headerLabel and formatRowCells straight into the worksheet; the json arm writes formatRowForJson. Neither calls rowsToCsv, formatCsvCell or the rule, and the diff touches neither arm nor formatCellValue, formatRowCells or formatRowForJson.

  5. parseCsvToRows (published from packages/rest/src/index.ts) — right. Every data cell is read through unmarkCsvTextCell; each header cell is unmarked BEFORE .trim() and before the inline mapping[h] lookup, so an inline mapping names the label as the export labelled it (pinned). The reversal reads the cell text alone and sits before any mapping: the inline rename happens inside this parse by header; a named mapping's applyMappingToRows (core) runs after the parse and reads row[source] by that unmarked header (pinned through the sync door with a named mapping whose source label begins with a lead).

    • All three CSV doors — right. prepareImportRequest's CSV branch is the only caller of parseCsvToRows; the sync door (data-action-routes.ts about 369), the async import-job door (about 483) and plugin-auth's runAdminImportUsers (admin-import-users.ts 550, which forwards the csv body) all parse through it. The json branches (format: 'json' with rows, a bare array) call only the inline rename; the xlsx branch calls parseXlsxToRows. Both untouched: the JSON and xlsx import doors read no mark.
  6. Round trip — every pinned value returns exactly, right. Ten values (four marked, five ordinary business values, one user-authored cell that is the mark before a plain word) through the sync door with an inline mapping, the sync door with a named mapping and the async job door (real routes over ObjectQL with better-sqlite3 in memory), and ten names through the admin door whose file comes from the real export route; each case first asserts the file carried the marks on cells and on the header label that begins with a lead, so none passes by the writer never marking.

    • TAB and CR leaders — stated honestly. Marked by the writer; a CR cell was RFC-4180-quoted before this change too, so the parse keeps it; unmarkCsvTextCell restores the leading character; core's text fall-through (import-coerce.ts) then trims under the default trimWhitespace, which is what it did to the same two values before this change. With trimWhitespace: false the stored value returns exactly. Both pinned in csv-text-cell-round-trip.test.ts; both stated in the changeset and the PR body.
    • The one stored shape — stated honestly. A stored text value that is one mark followed by a remainder satisfying the predicate: markCsvTextCell leaves it (its first character is the mark, not a lead), the parse removes the mark, so it does not return. The changeset, the PR body and the unmarkCsvTextCell docblock all state it; the dev measured it one-shot and the reading here agrees. A stored value with two leading marks before such a remainder does round-trip (after one mark the remainder begins with the mark, not a lead), and the parse pin fixes that case, so "that shape" in the changeset is exactly one shape.
  7. Public surface of @objectstack/rest — right: nothing added, one published behaviour changed and declared. parseCsvToRows is the only published name whose behaviour moves, and the PR body declares it. The five new exports of export-format.ts are not re-exported by index.ts, the package's exports map serves only . from dist/, and files ships dist only, so they are module-internal; formatCsvCell is module-private; rowsToCsv keeps its signature and is not published. The CSV doors' accept set is unchanged (every cell they accepted they still accept); what they store for one cell shape changes, which is the narrowing ② records.

  8. data-action-routes.ts — comment only, right. The eight added and one removed lines are the route docblock; the sentence it corrects ("the raw stored values stream through unchanged") would have been false for csv after this diff. No doc under content/docs or docs/ states the cell-level contract (references/api/export.mdx describes the payload shape and trimWhitespace only), so the claim's "docs the change makes false" resolves to that one comment; Flag docs affected by code changes concluded success.

  9. Landing hold — right. Blocked-by: objectstack-ai/objectui#13315 is line-initial in the PR body; the PR is a draft; .objectui-sha is not in the file list and its blob at the head equals main's (cf153eb2e5).

  10. Control bytes — none. TAB, CR and the mark are spelled as escapes or by code point in all eight files at the head (grep over the committed blobs: 0 hits in each).

  11. Check-runs on bf8244cb20 (read 2026-10-21, latest run per name, 40 names): 28 success, 7 skipped, 5 in_progress. Success includes Build Core, TypeScript Type Check and its four lanes, Lint & Repo Gates and its lanes, Check Changeset, Governed Surface Queue Guard, Test Core (1/6), and the claim and single-writer gates. Test Core (2/6) through (6/6) are still in_progress: the required Test Core context is NOT MEASURED, not a pass. This record judges on the diff; the seat that adopts it re-reads the head's check-runs and lands nothing until all six shards conclude success. Skipped: Console Pin Gate, Temporal Conformance, Build Docs, the dogfood matrix and Verify CLI, the two opt-in lanes (path-scheduled; not a verdict either way).

② Semver level

  • @objectstack/rest: patch — matches what the diff publishes. AGENTS.md Post-Task Checklist step 3 as it stands on main (4e2c42f363): the level follows the Metadata-protocol kind alone, none takes patch, and "a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition". The changeset carries the BREAKING banner, exactly one ADR-0087 marker (not-required (no-migration-prescription), in the body's HTML-comment form, with its reasoning), and both line-initial declarations. The dispatch order's "BREAKING shipped as minor" was the pre-f2b6cf230b rule; the dev's deviation to patch is the rule on main. Check Changeset concluded success on this head. The package is released (18.0.0, files: dist), so a changeset is owed and skip-changeset would be wrong.
  • Clause-②: no (narrowing) — right, on the PR body and the changeset, line-initial in both. no: no new key on any published payload and no new published name (①.7). (narrowing): the CSV export serves different bytes for one class of text cells, and the CSV doors store a user-authored cell of one shape without its leading mark where they stored it verbatim before — BREAKING for a consumer that reads the file other than through these doors, and exactly the arm step 3's closed pair admits. Step 3's migration clause is not owed: nothing an author can write is removed or renamed, so no FROM → TO mapping; the ADR-0087 marker's reasoning (no metadata, declaration or stored shape moves) is correct on the diff.
  • Metadata-protocol: none — right. No spec key, accepted value, metadata type, meaning or default moves; packages/spec/** is untouched; the declared-type decision only reads the spec's existing classes.
  • Level of the prose. The changeset describes the change at the PR's own level (the lead set is "a small fixed set that its consumer interprets", the mark is named by code point, the one lossy shape and the TAB/CR trim are stated, the unchanged formats and doors are listed) and its title line carries fix(rest)!:. One imprecision: "What to do" tells an outside reader to apply csvTextCellNeedsMark (packages/rest/src/export-format.ts), but that name is not published by the package (①.7) and src/ does not ship, so the pointer is to repository source. Flagged in ③; not a level question.

③ Boundary flags

  1. Open question (report 6322329836): keep the rule as chosen (A) or extend it to an escape over leading marks (B). The maintainer decides; the seat puts it to them. It does not block this record. The rule under review is the one the maintainer chose (6317629287, the option labelled A′), the lossy shape is its direct consequence, and the PR states it in three places (①.6). The dev's recommendation of A is accurately argued: no producer of that stored shape was measured, B serves an unmeasured case, and B would not touch the objectui half. If the maintainer chooses B, the predicate and the pins change — a new head and a new record, before the hold lifts either way.
  2. The numeric-text set's membership against the private order — escalated to the seat. The inputs this record may read describe the set only as "digits, spaces and the characters a signed number or a phone number is written with" (PR body, changeset); the code's set holds digits, the space and six punctuation marks, and the table pins each of the six unmarked after a sign. The maintainer's exact set lives in the dispatch order, which this record did not read, and the reviewer's brief counts five punctuation marks (a loose count with the parentheses as one, or a different set). The seat holding the order compares before adopting: a set there that differs from the code's makes this verdict FAIL on the first judge-with-care item, since the rule would then not be exactly the maintainer's choice.
  3. Dev deviations (report 6322329836), each answered:
    • changeset level patch against the dispatch's minor — right, step 3 on main governs (②);
    • AGENTS.md read first at 795ff708d8 then at 03966ceae0, differing only in step 3 — right, and that step was read on the newer tree;
    • worktree on the existing branch, --ff-only, the fast-forward pushed as the write probe — process only, no diff effect; accepted;
    • origin/main fccbd9d0d5 merged once; installDefaultOrganization added to the harness for the NOT NULL tenant column — test-only; the testkit exists at the head (packages/rest/src/default-organization.testkit.ts); accepted;
    • two edits outside the round-25 claim's named surface: data-action-routes.ts comment-only (①.8) and the admin pin in plugin-auth — the claim's "pins cover all three doors" names that door, and its pin belongs in the door's own package; accepted;
    • the admin pin moved onto the file's existing engine double so the engine-double ledger does not move — no ledger file in the diff; accepted;
    • the mark spelled by code point after the Write tool turned an escape into a literal — no raw control byte in any changed file at the head (①.10); accepted;
    • first gate run exit 3 (built output missing) then exit 0; check-changeset-no-major driven with a synthesized payload; long runs under nohup — the head's CI runs are the gate readings, not these (①.11); accepted.
  4. Out-of-scope note for import wizard: send a CSV file's own CSV to the server's CSV import door (format csv) on every path, so the server applies the one cell rule its CSV export pairs with (objectstack#25078) objectui#13315 (noted, not filed — right not to file here): an inline mapping sent with CSV text must key a column by its header as this parse reads it, after the reversal — confirmed in the code (unmarkCsvTextCell(h).trim() precedes mapping[h]). It matters only for a label beginning with a lead and belongs to the objectui PR's author; the seat carries it there.
  5. The changeset's "What to do" names an unpublished function (②). A consumer following it reads this repository's source at the path the changeset names; the rule is otherwise stated in words at the disclosure level. The seat decides whether to reword it as the predicate in this repository's packages/rest/src/export-format.ts before landing (a prose-only push, which re-opens this record on a new head); publishing the predicate would be a surface widening and a different claim. Not blocking.
  6. Check-runs incomplete (①.11): five Test Core shards in_progress at the time of this record; NOT MEASURED, never a pass. Adopt only on a re-read with the required contexts concluded.
  7. The PR body counts plugin-auth at 181 test files and the report at 180 for the same local run; immaterial, the CI reading is the gate.

Implemented-by: claude/issue-25078-export-value-context
Reviewed-by: session_01LmjU2x1faLZ39DVTFyVca6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT (landing held)

domain:cli seat 2 (#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-21T17:49Z · head bf8244cb20dcaa155853344a15baad129cef7e0e · claim 6311298793 (amended 6313433222, 6317629287, 6319726645) → report 6322329836 → review of record 6322695804 (CONTRACT_REVIEW_TIER, PASS).

Check Reading
Shape Draft, base main. First line Fixes #25078. Clause-②: no (narrowing), Metadata-protocol: none and Blocked-by: objectstack-ai/objectui#13315 are line-initial. One changeset, @objectstack/rest: patch, with the BREAKING banner and one ADR-0087 marker. .objectui-sha is untouched.
The rule The record's ③ 2 escalation is answered here. The seat compared the code against its dispatch order (round 25), and the two match exactly: the leads are the six named; the "numeric text" set is the ten digits, the space and the six marks + - ( ) . ,, which is exactly /^[0-9 +\-().,]*$/; the trigger requires both halves; and one mark comes off on import only when the remainder satisfies the trigger. The brief's "five" was the seat's miscount. The PASS stands.
Surface 8 files, +740 / −10. check-governed-merges.mjs --pr 25130 is read again at landing.
Evidence 133 new pins (56-case predicate table, writer, parse and round trip through the sync door with inline and named mappings, and through the async job door), plus the admin door's pin in plugin-auth. Seven ablations each red the pins they own. @objectstack/rest: 313 files / 6318 tests. @objectstack/plugin-auth: 180 files / 3423 tests.
Gates Local: 71/71 derived and the 10 Declared WIDE population families exit 0; pnpm lint exit 0; both changeset gates exit 0.

Accepted deviations: the changeset is patch under ruling A, not the dispatch's minor; data-action-routes.ts gets a comment-only edit; the admin door's pin sits in plugin-auth on its existing engine double; main was merged once, and the harness seeds the default organization.

Carried to the pre-landing push (this PR merges main before landing anyway, because PR #25124 shares rest-server.ts, and that is a new head owing a new record): the changeset's "What to do" line names csvTextCellNeedsMark, which the package does not publish (record ③ 5). It is reworded there to point at the rule in this repository's packages/rest/src/export-format.ts.

Open with the maintainer: report 6322329836's question about the one stored shape that does not round-trip (keep the rule, or extend it to an escape). It does not block, and either answer lands before the hold lifts.

Landing: held until objectstack-ai/objectui#13315's PR lands and objectstack's .objectui-sha bump over it is on main, and then until every check on the landing head reads success.


Generated by Claude Code

This branch has not been deployed

No deployments
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/l tests tooling

Projects

None yet

2 participants