Skip to content

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) #13315

Closed

Description

@objectstack-fleet

Filing gate ④: the objectui half of a two-repo fix. Filed by objectstack domain:cli seat 2 (seat post objectstack-ai/objectstack#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6. ⛔ Not a claim. The objectstack side leads, by the seam rule: objectstack-ai/objectstack#25078 (p1).

Disclosure: the maintainer's ruling on objectstack-ai/objectstack#9387 (5323310017) applies. This card, its PR and every comment describe the change at this card's level only.

Reader, and what they do: objectui's domain:ui execution seat dispatches it. It has no blocker: it works against the current server.

Why

The maintainer chose the fix for objectstack-ai/objectstack#25078 in objectstack domain:cli seat 2's session chat (the option labelled 「B」 in the seat's picker):

  • objectstack's CSV export writes certain text cells in a form its own CSV import door reverses exactly.
  • The rule is defined once, server-side, at that door.

The console's import wizard does not reach that door on its default path:

What

  1. When the wizard's source is a CSV file, the request is format: 'csv' with CSV text on every path, not only for a csv-declared mapping. Either the file's own text or sourceRowsToCsv(rows) as the mapping path does now; say which and why.
    • The preview and the column mapping stay client-side, as now.
    • xlsx and pasted sources are unchanged.
  2. Measure parity first: for every input the wizard accepts as CSV (delimiters, BOM, quoted newlines, encodings, header handling), does the server's CSV parse (objectstack packages/rest/src/import-prepare.ts, parseCsvToRows) give the same cells as the browser's parseDelimited? If any difference would change an imported value, stop and report it. Do not paper over it in either parser.
  3. Read every other console import that reads a CSV file and sends rows to a server import door (for example packages/app-shell/src/views/IdentityImportPanels.tsx). It gets the same change, or a measured reason it does not need it.
  4. Pins: a CSV file source sends format: 'csv' on the default path and on the named-mapping path. xlsx and paste send rows as before.

Landing order (the seam rule; the pin bump is never a rider)

  1. This card's PR lands on objectui main.
  2. objectstack's .objectui-sha bump covers that commit, through the dedicated bump script.
  3. rest: one record export format may not neutralize a stored value for the context its consumer reads it in (reach not measured; detail withheld under the disclosure ruling) objectstack#25078's PR lands last, Blocked-by: this card.

Dedupe

One MCP search_issues query in objectui returned 3 hits, all closed: #13156 (the csv-mapping path, the precedent here), #13230 and #2163. New.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    on Oct 21, 2026
  2. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer from objectstack domain:cli seat 2 · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-21T17:32Z. ⛔ Not a claim, and no label change.

    The objectstack half is built: objectstack-ai/objectstack#25130 (draft, held at landing, Blocked-by: this card). One reading from its dev (report 6322329836 on objectstack-ai/objectstack#25078) bears on this card's build:

    • The server's CSV parse (packages/rest/src/import-prepare.ts, parseCsvToRows) applies the reversal to header cells too, before the inline-mapping lookup.
    • So when the wizard sends CSV with an inline mapping, the mapping must name a column by its header as the server reads it, after the reversal. This matters only for a column whose label the export rule marks.
    • At the pin cf153eb2e5, the wizard's default path keys rows by field name after its own mapping, so a build that keeps that keying is unaffected. Pin whichever you choose.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · objectstack domain:cli seat 2 · p1 · claimed as objectstack-ai/objectstack#25078's unowned blocker, on the maintainer's direct order · 2026-10-21T22:23Z
    Session: session_01AFrKwndmrCLtF5V9hNtrw3
    Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-13315-import-wizard-csv-door
    Worktree: objectui-issue-13315
    Domain: domain:ui
    Seat: domain:cli#2 (objectstack seat post objectstack-ai/objectstack#22648)
    Authority: the maintainer's direct order in the chat of session session_01AFrKwndmrCLtF5V9hNtrw3, verbatim 「objectui#13315 你也接手」. It is also the blocked seat's own unowned blocker: objectstack-ai/objectstack#25078's PR objectstack-ai/objectstack#25130 is held at landing with Blocked-by: this card, and this card was pm:queue with no assignee at this claim. Only this card is taken; no second objectui card rides on it.
    File surface: packages/plugin-grid/src/ImportWizard.tsx (assembleImportRequest), packages/plugin-grid/src/importParsers.ts (read; edited only if the parity measurement needs no parser change, which the card forbids), the CSV-reading import panels the card's item 3 names (for example packages/app-shell/src/views/IdentityImportPanels.tsx), their tests, and a changeset. Stop on a breach and explain it in the report.
    Container & model: M, mode:subagent, model: default tier (dispatch-gates.mjs --tier --repo objectstack-ai/objectui at objectstack 60b42cd0b: "no path-derived mandate")
    Clause-②: no
    Basis for the Clause-② line: the wizard moves a CSV file's request onto the server's existing CSV import door (format: 'csv'), which a csv-declared mapping already uses (#13156). No published accept set or public surface widens.
    Responsibility: objectui's packages/plugin-grid import wizard sends browser-parsed rows; the platform path that already covers the shape is the server's CSV import door, whose cell rule pairs with its CSV export (objectstack-ai/objectstack#25078, ruling B); it reaches every console user who imports a CSV file.
    Thread-read: 6322387032
    Serial constraints cleared: none of objectui's 5 open PRs touches packages/plugin-grid/src/ImportWizard.tsx, importParsers.ts or packages/app-shell/src/views/IdentityImportPanels.tsx (their file lists read in this act). The landing order is the card's own: this PR, then objectstack's .objectui-sha bump, then objectstack-ai/objectstack#25130.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 13315,
      "status": "needs_decision",
      "branch": "claude/issue-13315-import-wizard-csv-door",
      "pr": "https://github.com/objectstack-ai/objectui/pull/13316",
      "session": "session_01AFrKwndmrCLtF5V9hNtrw3",
      "premise_still_valid": true,
      "summary": "Item 2's stop was taken, as the card and the dispatch require. The premise holds: H1 is confirmed, and at 7e9708e the default path sends format json rows. The parity measurement found differences that change an imported value on both routes the card allows, so no format switch shipped and items 1, 3 and 4 are not built. Draft PR #13316 (Refs #13315) adds one test recording those differences and an empty-frontmatter changeset. FILE TEXT, where the server parses the file's own decoded text, breaks on four classes even without the cell rule: a .tsv file, blank lines, a bare CR and a repeated header. RE-SPELLED, sourceRowsToCsv(rows) as Zone 3 suggests, reads back the preview's cells at objectstack main for 20 of 21 classes and differs on three shapes. B1: a last row whose sent cells are all empty is dropped by the server's CSV parse. B2: the wizard trims a cell before the PR-head rule reads it, so an exported cell starting with + or - that has trailing TAB or LF keeps its mark. B3: when a space comes before a mark, the mark is removed. Item 3: the identity import goes through the same wizard. Its adapter (identityImport.ts) posts JSON rows to a door that parses through prepareImportRequest. That door has the CSV form and the same rule, so the adapter needs the same change, and the change must land with the wizard's.",
      "tests": "At d997d58, every run through os-verify-lock.sh. (1) The new file: pnpm exec vitest run packages/plugin-grid/src/ImportWizard.csvDoorParity-13315.test.tsx gave 'Test Files 1 passed (1) / Tests 8 passed (8)', VERDICT command-exit 0. (2) The whole package: pnpm exec vitest run packages/plugin-grid/ gave 'Test Files 239 passed (239) / Tests 2160 passed (2160)', VERDICT command-exit 0. (3) The one-shot measurement: the measurement script in the scratchpad read objectstack's parseCsvToRows from git objects at main abc25a3536 and at #25130's head bf8244cb20. At the head it also read unmarkCsvTextCell, its predicate and formatCsvCell. It took this branch's decodeSpreadsheetText, parseDelimited, csvCell, sourceRowsToCsv and buildSourceRows. Each was extracted by source text and transpiled unchanged with the worktree's TypeScript, then run over 21 input classes on both routes. Per-class verdicts printed raw parity DIFF on tsv, whitespace, blank-lines, lone-cr, export-marked-trailing-ws and user-space-before-mark. Re-spelled default DIFF was trailing-projected-empty only, and re-spelled named was same on all 21. A follow-up run showed the named path reaches B1 through an unnamed column: 'name,' / 'Ada,x' / ',y' is re-spelled as 'name CRLF Ada CRLF' and read back as one row. No ablation: the diff changes no tested behaviour.",
      "gates": "Local, at d997d58. Each exit code was captured before any pipe. node /home/user/objectstack/scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectui exited 2 ('REFUSING ... this checkout is objectstack-ai/objectstack'), so the list was hand-derived from objectui package.json and AGENTS.md section 9. pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-grid^...' build: VERDICT command-exit 0. pnpm --filter @object-ui/plugin-grid type-check (tsc --noEmit && tsc -p tsconfig.test.json): VERDICT command-exit 0, and tsc -p tsconfig.test.json --listFiles names the new test once. pnpm --filter @object-ui/plugin-grid lint: exit 0 (0 errors), and eslint on the new file alone: exit 0 with no output. node scripts/check-changeset-presence.mjs: exit 0 ('Every one of them has an EMPTY frontmatter'). node scripts/check-changeset-no-major.mjs: exit 0. pnpm check:new-line-citations: exit 0 ('0 new citation(s)'). pnpm check:control-bytes, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check:vi-mock-specifiers and check:unreferenced-sources: each exit 0. grep -naP for control bytes over the test, the changeset and the PR body: no match. CI on PR #13316 at d997d58, 35 check-runs when this was written, none waited on. success: Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, Live E2E (informational), label. in_progress: Build & E2E, Build Docs, Bundle Analysis, Lint, Repo Gates, Test (dist pins), Test (shard 1/16 to 16/16), Type Check. skipped: the coverage matrix and its shards, dependabot. Declared to CI and not run locally: repo-wide lint and the full test matrix.",
      "dogfood_battery": "n/a: no ! in the PR title and the Clause-② line is no, with no (narrowing) arm.",
      "mcp_calls": "0",
      "api_writes": "2 REST writes, both relayed as objectstack-fleet[bot]. (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches answered 204. Run 39689695764 executed POST /repos/objectstack-ai/objectui/pulls (draft, #13316) and POST /repos/objectstack-ai/objectui/issues/13316/assignees (os-bill). The read-back gave 9024 bytes sent and 9024 stored, identical, and an API read of the body showed no first differing byte. (2) This comment: POST /repos/objectstack-ai/objectui/issues/13315/comments through scripts/pm/post-stamped.mjs. The two git pushes are not REST writes. Reads only otherwise: gh api GETs of the card, its comments, objectstack#25078 comment 6322329836, PR #13316 and its check-runs.",
      "line_budget": "n/a: no skills/** file and no ratcheted ledger touched. The diff against merge base 7e9708e is 2 files, +191/-0.",
      "files_changed": [
        ".changeset/13315-import-wizard-csv-door.md",
        "packages/plugin-grid/src/ImportWizard.csvDoorParity-13315.test.tsx"
      ],
      "deviations": [
        "Item 2's stop was taken: there is no format switch, and items 1, 3 and 4 are not built. PR #13316 is a measurement PR that refers to #13315 without closing it, as the dispatch prescribes. Its committed test records the differing inputs only. The holding classes are in the PR body.",
        "dispatch-gates.mjs refused objectui (exit 2), so the local gate list was hand-derived from objectui's package.json and AGENTS.md section 9.",
        "objectstack-ai/objectstack#25130's head was fetched into a self-named ref, refs/os-dev-13315/pr25130, instead of read through FETCH_HEAD, following objectui AGENTS.md's FETCH_HEAD rule. The same fetch also moved the shared objectstack checkout's remote-tracking refs origin/main and origin/claude/issue-25078-export-value-context. Nothing in that working tree changed: HEAD is 60b42cd0b and porcelain is empty. The self-named ref was deleted after the measurement.",
        "The committed test's server half is a set of recorded literals. @objectstack/rest is outside objectui's pin closure, so no objectui test can run the server's parse. The test header says so and names the two refs and the function, per AGENTS.md #9.",
        "Commit trailers follow objectui AGENTS.md: the model-free pair, Claude-Session plus Co-authored-by: Claude. The harness's attribution reminder named a model in Co-Authored-By, and its own precedence sentence yields to the repository rule. The PR body ends with the os-dev session-URL footer, not the harness's line.",
        "The relay needs an actions file. It was written as 13315-pr-actions.json in the issue-scoped scratchpad directory, read back, used once and deleted.",
        "The plugin and tests labels on #13316 were set by the repository's labeler, not by this run.",
        "The worktree was removed after the PR opened. node_modules went first, and git worktree remove ran without --force on a clean, pushed tree."
      ],
      "open_questions": [
        {
          "question": "Item 2's stop: neither route the card allows hands the server the wizard's cells for every CSV input. Which way should a CSV file source reach the server's CSV import door? Measured shapes are in PR #13316's body. B1: input 'name,note' / 'Ada,x' / ',only a note' with note unmapped. Today's JSON rows are [{name:'Ada'},{name:''}]. Re-spelled they are 'name CRLF Ada CRLF', and the server reads [{name:'Ada'}] at main and at the PR head. B2: the export writes stored '+1 555 0100' + LF as a quoted cell, and the preview shows the mark plus '+1 555 0100'. Re-spelled, the PR head keeps the mark. Read as file text, the server imports '+1 555 0100'. B3: input ' MARK=x', a space before a mark. Re-spelled imports '=x'. Read as file text, the server imports 'MARK=x', and so does today's JSON.",
          "options": [
            "A: RE-SPELLED as measured, the Zone 3 route. Every CSV-file source sends format csv with sourceRowsToCsv(rows): the default path and an undeclared named mapping. A json-declared mapping keeps JSON rows, because the door refuses csv for it. xlsx and paste are unchanged. The identity adapter sends per-batch CSV and lands in the same PR. B1 to B3 are stated in the changeset. Cost of B1: a last row, or a run of last rows, whose sent cells are all empty no longer reaches the runner. Today such a row creates a record with defaults or fails with a required-field error, and after the change the result counts it one row fewer than the preview. Inner rows of that shape are kept, so row numbering stays aligned. On the named path, B1 needs data under an unnamed column. Cost of B2: an exported cell starting with + or - and made only of digits, spaces and ()+-., followed by trailing non-space whitespace, keeps its mark through the console, exactly as today. Cost of B3: a typed space before a mark is trimmed and the mark is removed, where today the mark is kept. Effort: S for the wizard, M for the identity adapter (per-batch CSV, plus identity enrichment that still reads rows).",
            "B: FILE TEXT. Send the file's decoded text, with an inline mapping keyed by each header as the server reads it. Cost: a .tsv file becomes one column, a leading blank line empties every row, inner blank lines become rows, a bare CR is dropped and a repeated header loses a column. Each of these needs a change to the server's parse, which the card forbids, and the inline mapping would also have to name every unmapped header to keep it out. Effort: L, across both repositories.",
            "C: A, plus the browser hands the server each cell as written, untrimmed, for the request while the preview stays trimmed. The server's rule then reads the raw cell and its default trimWhitespace trims after, which closes B2 and B3; B1 stays. Cost: a change to the browser parser, which the card forbids without a ruling, and two forms of every cell in the wizard that corrections must keep in step. Effort: M.",
            "D: No switch; CSV files keep JSON rows. Cost: an export followed by a console import keeps the mark on every marked cell, so ruling B holds only through the API's CSV door. objectstack-ai/objectstack#25130 stays held behind this card, or drops its Blocked-by. Effort: none."
          ],
          "recommendation": "A. Actual business need: A carries ruling B's round trip through the console for every export-produced marked shape measured except B2, and today's JSON path already imports B2 with the mark. This round measured no producer of B1 (a last row with data only in unmapped or unnamed columns), of B2 (stored text starting with + or - in the numeric set, followed by trailing non-space whitespace) or of B3 (a typed space before a mark). Long-term soundness: one parse, the browser's, decides the cells. The csv-mapping path from #13156 already sends that parse, and the rule stays defined once at the server's door. B needs changes to the server's parse, and C adds a second cell form. Preventing AI authoring errors: no option changes what a metadata author writes, and the rule keeps one owner. Startup focus: A adds no rule or parser mode. C adds one for shapes with no measured producer, and D leaves the card's goal undone."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted in PR #13316 Acceptance notes, not filed. parseDelimited's doc comment says trailing whitespace is trimmed, but the code trims both ends of every cell, and B3 rests on the leading half.",
        "carrier: objectui#13315's build round · noted, not filed. Once a CSV switch ships and objectstack-ai/objectstack#25130 lands, the preview still checks cells as the file spells them: checkImportCell for the preview, and savedMappingToDisplayIndexMap for a named mapping's columns. A marked select value or a marked header would then show as a preview finding or an unmatched column while the server accepts it. Dormant until both land.",
        "carrier: none (承接者:无) · noted in PR #13316 Acceptance notes, not filed. This is a function-level reading of objectstack's parseCsvToRows at abc25a3536 and bf8244cb20, with no public door measured, so it carries no reach line. A leading blank line becomes the header row and empties every row. An inner blank line becomes a row. A repeated header keeps the last column. The function's doc comment says the first non-empty line is the header. Dedupe words: parseCsvToRows leading blank line header; csv import empty first line; import-prepare header row."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    决策待裁 · objectstack domain:cli seat 2 · os-bill · session session_01AFrKwndmrCLtF5V9hNtrw3 · 2026-10-21T22:59Z。⛔ 不是新认领;本卡仍由本席认领(6327882289),PR #13316 在手。

    一句话问题: 控制台导入 CSV 文件时,无论走哪条路把文件交给服务端的 CSV 导入入口,都有少数几种输入的导入结果会和预览看到的不一样;本卡第 2 条要求遇到这种差异就停下报告、不许改任何一方的解析器,所以要你定走哪条路。

    背景: 实测报告 6328404991(dev,d997d58),量测 PR #13316(只加一条记录差异的测试,Refs #13315,不关卡)。本席在 origin/main 上核对了服务端解析器 packages/rest/src/import-prepare.ts parseCsvToRows:末尾整行全空的行确实会被丢弃(源码注释「Drop fully-empty trailing rows」),B1 成立。

    Governing text: 本卡第 2 条原文 "If any difference would change an imported value, stop and report it. Do not paper over it in either parser.";objectstack-ai/objectstack#25078 的裁决 B(规则只在服务端 CSV 入口定义一次)。协议声明:不改协议,不碰 packages/spec。

    前提(带复核命令): ① 服务端丢弃末尾全空行:git show origin/main:packages/rest/src/import-prepare.ts | grep -n "fully-empty trailing"(正控:同文件 grep -n "parseCsvToRows" 必中)。② 向导默认路径今天发 JSON 行:objectui git grep -n "format: 'json'" origin/main -- packages/plugin-grid/src/ImportWizard.tsx(正控:同文件 grep -n "assembleImportRequest")。

    选项 做什么 客户可感知后果 工作量
    A 浏览器解析后重写成 CSV 发送 所有 CSV 文件来源都用预览那份解析结果重写成 CSV 发给服务端;身份导入同改、同 PR 落地;xlsx 和粘贴不变 三种边角形态与预览不一致:B1 文件末尾「只在未映射列有数据」的行会少导入一行;B2 带尾随制表/换行的某类导出单元格仍保留标记(与今天相同);B3 用户手打的「空格 + 标记」会被去掉标记 objectui · 1 PR · 向导 S,身份导入适配 M
    B 直接发送文件原文 服务端按自己的方式解析原始文件 .tsv 变成一列、空行把整份数据弄空或多出空行、单独 CR 丢失、重复表头丢一列;每一条都要改服务端解析器(本卡禁止) 两仓 · L
    C A + 发未修剪的原始单元格 预览仍修剪,请求里带原样单元格 关掉 B2、B3,B1 仍在;向导里每个单元格有两份,纠错时要两份同步 M,且要改浏览器解析器(本卡禁止,需另裁)
    D 不切换 CSV 文件继续发 JSON 行 从控制台导出再从控制台导入,标记全都留着;裁决 B 只在 API 的 CSV 入口成立。#25130 要么继续挂着,要么去掉它的 Blocked-by 无

    业务含义: A = 「以用户在预览里看到的为准」,只在三种几乎没人写的形态上有偏差;B = 「把原件交给后台重新读」,常见文件格式都会出错;C = 在 A 上再给极少数情况打补丁;D = 控制台这条路放弃导出 → 导入往返。

    四轴(业务立场):

    os-decision-facets
    ① 项目长远合理性:A 缩小特例(一份解析定单元格,规则只在服务端一处);B、C 扩大特例。
    ② 实际业务拉动:控制台导出 → 导入往返今天就有人用;B1–B3 零实测来源,默认不为它们加东西。
    ③ 防 AI 犯错:A 的偏差(B1)表现为结果计数少一行,可见;不引入静默容错。
    ④ 创业阶段不扩散:A 不新增模式或门禁;C 新增一种单元格形态,D 留下未完成的目标。
    Prior rulings read: "import wizard CSV format csv server import door" → 5 hits (objectui #13315, #13156, #13230, #13124, #2163), none a ruling on this question; ADR none; thread: 0

    推荐:A。 只看①选 A;②③④ 是否翻转:否。置信缺口:B1–B3 的真实来源本轮没有找;若将来出现,C 是 M 级的补法,不影响服务端那一半。不取 B:常见文件格式都会坏且要改服务端;不取 C:为零来源形态加第二种单元格;不取 D:放弃本卡目标。

    裁后执行: 选 A ⇒ 本席在同一分支续派开发(向导 + 身份导入同 PR,changeset 写明 B1–B3,PR 改为 Fixes #13315),落地后 objectstack 做 .objectui-sha bump,再落 objectstack-ai/objectstack#25130。选 D ⇒ 关 PR #13316,本卡按 D 关闭,#25130 去掉 Blocked-by 后落地。选 B/C ⇒ 本席另立服务端或浏览器解析器的卡并重排落地顺序。


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruled: A · the maintainer's ruling recorded · objectstack domain:cli seat 2 · os-bill · session session_01AFrKwndmrCLtF5V9hNtrw3 · 2026-10-21T23:33Z. ⛔ Not a new claim; this seat's claim 6327882289 continues on the same branch and PR #13316.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 13315,
      "status": "done",
      "branch": "claude/issue-13315-import-wizard-csv-door",
      "pr": "https://github.com/objectstack-ai/objectui/pull/13316",
      "head": "ec515a44a98458e0ed0fb4cc00c769d8a9af338e",
      "session": "session_01AFrKwndmrCLtF5V9hNtrw3 (the seat's session; this subagent shares the parent's id)",
      "premise_still_valid": true,
      "summary": "Ruling A is built on PR #13316's branch, and its head is ec515a4. A CSV file source now sends format csv with sourceRowsToCsv(rows), the cells the preview parsed. That holds on the hand-mapped path and on a named mapping not declared json. A json-declared mapping keeps JSON rows, and xlsx and paste are unchanged. The dry run, the background job and the sync import send the same request. H1: assembleImportRequest takes a new csvFile option, and no new prop was needed. The wizard keeps one boolean state, set where the grid is loaded, from delimitedFileDelimiter(file.name). That is the name test parseSpreadsheetFile already picked its text arm with, now a named function in importParsers.ts that both read. So .csv, .tsv and .txt count as CSV files, the delimited-text inputs card item 2 measured as CSV. parseDelimited is byte-identical. Identity import: the adapter got the same change in the same PR. A CSV request is cut at record boundaries into batches of at most 500 rows. Each batch is posted as the header record plus its data records, which for the wizard's text is exactly sourceRowsToCsv of that batch's rows. The enrichment still reads each batch's rows, now read from the same records. The adapter reads the wizard's text back because the wizard calls one importRecords for every data source. The alternatives would double the generic door's payload or widen DataSource.importRecords. H2 was measured on a multi-batch file against objectstack's real parseCsvToRows at main and at #25130's head. Numbering stays aligned with 0 misaligned rows, but B1 also applies at every batch end: preview row 500, the last of batch 1, with all mapped cells empty, is never read. The changeset states B1 to B3 at user level, including that per-batch case. H3: on the default path a marked header reaches the server as its field name. This is pinned. The PR title and body still read as the measurement PR. os-dev.md forbids a dev PATCH of the PR body, so the replacement text is in pr_title_proposed and pr_body_proposed for the seat to write (see deviations).",
      "tests": "Targeted at ba5cc54, through os-verify-lock.sh: vitest run on csvFileSource-13315, csvDoorParity-13315, mappingSourceFormat-13156, identityImport.csvBatches-13315 and identityImport.test gave 'Test Files 5 passed (5) / Tests 45 passed (45)', VERDICT command-exit 0. At ec515a4, through the lock: pnpm exec vitest run packages/plugin-grid/ gave 'Test Files 240 passed (240) / Tests 2168 passed (2168)', exit 0. The 10 app-shell files that read the import surfaces (identityImport.test, identityImport.csvBatches-13315, identityImportFields.emailRule-11913, identityImportSavedMapping, identityImportWriteModes-12697, importTargetFields, ObjectView.importPredicates, ObjectView.headerFold-12705, and ExcelImportBar importGrant-12082 and sharedSelect-11865) gave 'Test Files 10 passed (10) / Tests 78 passed (78)', exit 0. The whole lock run ended VERDICT command-exit 0. Ablation was one-shot at committed head ba5cc54 with objectstack's scripts/ablation-replace.mjs in WRAP mode. Each anchor hit once, the blob changed and the file was restored to the HEAD blob with an empty git diff HEAD. The tests import source by relative path or the vitest alias, so no dist leg was needed. A: replacing ': opts.csvFile === true;' with ': false;' gave 6 red (the .csv, .tsv, .txt and marked-header pins, parity B1, identity 1001-row) and 16 green. B: replacing the identity CSV branch condition with false gave 2 red (the batching case, the 1001-row case) and 4 green. Both matched the predicted sets. H2 measurement (one-shot, scratchpad): tsx ran the adapter's batchIdentityImportRequest on the wizard's sourceRowsToCsv of a 1203-line file. Then parseCsvToRows ran on each batch at objectstack abc25a3536 and bf8244cb20, read by git show, plus unmarkCsvTextCell at the head. Both refs: each batch body equals sourceRowsToCsv(batch rows): true; the adapter's rows equal the preview's rows: true; 0 misaligned rows. The server reads 499, 500 and 202 rows for batches of 500, 500 and 202, and the one row never read is preview row 500. The re-run at ec515a4 printed identical output.",
      "gates": "Local; each exit code captured before any pipe. dispatch-gates.mjs refuses objectui, so the list was hand-derived from package.json and AGENTS.md section 9. pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build (29 packages, plugin-grid included) at ba5cc54: VERDICT command-exit 0. pnpm --filter @object-ui/plugin-grid type-check at ba5cc54: exit 0. No plugin-grid file changed after it, and tsc -p tsconfig.test.json --listFiles counts 2 for the two 13315 tests. pnpm --filter @object-ui/app-shell type-check: exit 2 at ba5cc54 (TS2322 in the new test). It was fixed in ec515a4, and the re-run gave exit 0, with --listFiles counting the new test once. pnpm --filter @object-ui/plugin-grid lint and pnpm --filter @object-ui/app-shell lint at ba5cc54: exit 0, 0 errors. eslint on the 6 changed source and test files at ec515a4: exit 0, and warnings did not change against merge base 7e9708e (ImportWizard.tsx 9 to 9, identityImport.ts 0 to 0). At ec515a4, each exit 0: node scripts/check-changeset-presence.mjs ('6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'), node scripts/check-changeset-no-major.mjs, pnpm check:new-line-citations ('0 new citation(s)'), check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:unreferenced-sources, check:changeset-claims and check:pending-changeset-literals. At ba5cc54, each exit 0: check:vi-mock-inherit, check:vi-mock-override-shape and check:shell-escape-residue. NOT MEASURED: pnpm check:readme-exports exited 1, 'the population COLLAPSED -- this run proves nothing', because 28 packages are unbuilt in this worktree. Its reason is a prerequisite and not this diff; CI's Repo Gates runs it. CI on PR #13316 at ec515a44a9 had 36 check-runs, and none was waited on. success (33): Build & E2E, Build Docs, Bundle Analysis, Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, Lint, Live E2E (informational), Repo Gates, Test, Test (dist pins), Test (shard 1/16 to 16/16), Type Check, label. skipped (3): Test (coverage), the coverage shard matrix, dependabot. Declared to CI: repo-wide lint and the full app-shell suite (1681 files), which CI's 16 Test shards ran green.",
      "dogfood_battery": "n/a: an objectui card with no ! in the PR title, and the claim's Clause-② line is 'no' with no (narrowing) arm.",
      "mcp_calls": "0",
      "api_writes": "1: POST /repos/objectstack-ai/objectui/issues/13315/comments (this report) through scripts/pm/post-stamped.mjs, relayed as objectstack-fleet[bot]. Two git pushes (d997d58 to ba5cc54, then ba5cc54 to ec515a4) are not REST writes. No PR title or body write; see deviations. Reads only otherwise: REST GETs of the card, its comments, PR #13316 and its check-runs, plus git ls-remote and git show in the shared objectstack checkout.",
      "line_budget": "n/a: no skills/** file and no ratcheted ledger touched. Against merge base 7e9708e the diff is 7 files, +884/-34; this round, against d997d58, it is +708/-49.",
      "files_changed": [
        ".changeset/13315-import-wizard-csv-door.md",
        "packages/plugin-grid/src/ImportWizard.tsx",
        "packages/plugin-grid/src/importParsers.ts",
        "packages/plugin-grid/src/ImportWizard.csvFileSource-13315.test.tsx",
        "packages/plugin-grid/src/ImportWizard.csvDoorParity-13315.test.tsx",
        "packages/app-shell/src/views/identityImport.ts",
        "packages/app-shell/src/views/__tests__/identityImport.csvBatches-13315.test.tsx"
      ],
      "deviations": [
        "CONFLICT, named and not chosen silently: the dispatch's write budget includes a PR title and body update through the relay. os-dev.md says a dev writes the PR body only once, in the stroke that opens the PR, and never PATCHes it; a later change is named in the report and the seat writes it. The same file says it wins on a conflict with the dispatch. So the PR is still titled and described as the measurement PR, with 'Refs #13315'. The replacement is pr_title_proposed and pr_body_proposed below, ready for one relay issue_patch on #13316 with title and body. Its first line is 'Fixes #13315', its second the claim's 'Clause-②: no', and it ends with the session-URL footer. It was scanned for angle-bracket shapes (none) and for closing keywords (only 'Fixes #13315' next to an issue number).",
        "No fetch in the shared objectstack checkout. The #25130 head bf8244cb20 was already in the object store from the measurement round. git ls-remote showed the remote branch still at bf8244cb20, so every read was git show by sha or origin/main abc25a3536. No ref was created, moved or deleted there, and porcelain stays empty.",
        "importParsers.ts was edited, which the claim's file surface lists as 'read; edited only if...'. The edit moves parseSpreadsheetFile's existing extension test into delimitedFileDelimiter, which both now read. parseDelimited and decodeSpreadsheetText are byte-identical, and parseSpreadsheetFile's behaviour is unchanged. Neither package entry exports the new function.",
        "Reading of 'CSV-file source': .csv, .tsv and .txt, the files parseSpreadsheetFile reads as delimited text. This follows card item 2, which lists delimiters among the inputs the wizard accepts as CSV. A .tsv is re-spelled with commas; the measurement round found that re-spelling reads back the preview's cells for the tsv class.",
        "The identity adapter reads the wizard's CSV text back (readCsvRequestRecords) to batch it and to name identities, instead of receiving rows. The scan splits records the way the door's CSV parse does, posts each record's bytes as written, and decides no imported value. Rejected alternatives: rows beside the text, which doubles the generic door's payload, and a new parameter on DataSource.importRecords, which widens a published interface.",
        "Narrowed, and declared: the app-shell suite (1681 test files) was not run in full locally. Only the 10 files that read the import surfaces ran. CI's Test shards 1/16 to 16/16 ran the full suite green at ec515a4.",
        "The build closure, plugin-grid type-check and both package lints ran at ba5cc54. The only later commit, ec515a4, retypes one parameter in the new app-shell test. app-shell type-check, eslint on the changed files, the plugin-grid suite, the app-shell subset and the light gates were re-run at ec515a4.",
        "objectui has no ablation-replace.mjs, so the ablation ran objectstack's copy against objectui files, with cwd in the objectui worktree.",
        "Commit trailers follow objectui AGENTS.md: the model-free pair, Claude-Session plus Co-authored-by: Claude. The harness's attribution reminder named a model, and its own precedence sentence yields to the repository rule.",
        "Cleanup: the worktree was removed after the final push. node_modules went first, and git worktree remove ran without --force on a clean tree whose head equals the remote's (ec515a4). The scratchpad files for this issue, including the PR body draft and the H2 measurement script, were deleted after this report was posted."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted in Acceptance notes, not filed, not fixed here: parseDelimited's doc comment says trailing whitespace is trimmed, but the code trims both ends of every cell, and B3 rests on the leading half. The ruling leaves both parsers unchanged.",
        "carrier: none (承接者:无) · noted in Acceptance notes, not filed: once objectstack-ai/objectstack#25130 lands, the preview still checks cells as the file spells them (checkImportCell) and matches a named mapping's columns by the header as written (savedMappingToDisplayIndexMap). A marked select value or a marked header would then show as a preview finding or an unmatched column while the server accepts it. Dormant until #25130 lands.",
        "carrier: none (承接者:无) · noted in Acceptance notes, not filed: objectstack's parseCsvToRows, read as a function only with no public door measured. A leading blank line becomes the header, an inner blank line becomes a row, and a repeated header keeps the last column, while its doc comment says the first non-empty line is the header. This PR never sends the file's own text."
      ],
      "pr_title_proposed": "fix(plugin-grid,app-shell): a CSV file reaches the import door as CSV text",
      "pr_body_proposed": "Fixes #13315\nClause-②: no\n\n**Build PR, on the maintainer's ruling A** (the option labelled 「A 重写成 CSV 发送 (Recommended)」, recorded on objectui#13315 in comment 6328933548). A CSV file source now reaches the server's CSV import door: the import wizard sends `format: 'csv'` with `sourceRowsToCsv(rows)`, the cells the preview parsed. The identity import's adapter takes the same change. Neither parser changes: `parseDelimited` is byte-identical, and objectstack's `parseCsvToRows` is not touched.\n\nDispatched by objectstack `domain:cli` seat 2 on objectui#13315's claim (comment 6327882289), session `https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3`. The disclosure ruling on objectstack-ai/objectstack#9387 applies: this PR describes the change at the card's level only.\n\n## What changed\n\n- **The request (`assembleImportRequest`, `ImportWizard.tsx`).** A new `csvFile` option. With it, the hand-mapped path sends `{ format: 'csv', csv: sourceRowsToCsv(rows) }`, and a named mapping sends CSV text unless it declares `sourceFormat: 'json'`, which the door refuses CSV for (`MAPPING_FORMAT_MISMATCH`). A `csv`-declared mapping sends CSV text from any source, as before (#13156). An .xlsx sheet and a paste keep JSON rows. The dry-run pre-check, the background job and the sync import all send the one request `buildImportRequest` assembles.\n- **What carries \"this is a CSV file\" (H1).** `assembleImportRequest` cannot see the source, and no new prop was needed. The wizard keeps one boolean state, `csvFile`, set where the grid is loaded: the upload step's file path and the `initialFile` effect answer `delimitedFileDelimiter(file.name) !== null`, a paste answers false, and reset clears it. `delimitedFileDelimiter` (`importParsers.ts`) is the name test `parseSpreadsheetFile` already picked its text arm with, moved into a named function that both now read, so \"a CSV file\" means the same thing to the parse and to the request: `.csv`, `.tsv` and `.txt`, the delimited-text inputs the card's item 2 measured as CSV. `parseSpreadsheetFile` behaves exactly as before.\n- **The identity import (`identityImport.ts`, H2).** The adapter batched the request's `rows`, and a CSV request has none, so it would have thrown. `batchIdentityImportRequest` now cuts a CSV request at record boundaries (`readCsvRequestRecords`): each batch of at most 500 rows is posted as the header record plus its data records, joined by CRLF, which for the wizard's text is exactly `sourceRowsToCsv` of that batch's rows. Each batch's rows are read from the same records, so the one-time-password reveal still names every row by its email or phone. JSON rows are batched as before.\n\nWhy the adapter reads the wizard's text back instead of receiving rows: the wizard calls one `importRecords(object, request)` for every data source and cannot tell the identity adapter from the generic one, which posts the request as it is. Carrying the rows beside the text would double the generic door's payload, and a new parameter on `DataSource.importRecords` would widen a published interface. The scan splits records the way the door's CSV parse does, keeps each record's bytes as written, and decides no imported value.\n\n## Measured (H2, H3)\n\n- **H2, numbering across batches.** A one-shot script ran the adapter's batching on a 1203-line file (a quoted comma, a quoted LF, a quoted CRLF, doubled quotes, an export-marked cell, a blank line, and rows whose mapped cells are all empty at preview rows 250 and 500), then objectstack's `parseCsvToRows` on every batch body, at `main` `abc25a3536` and at objectstack-ai/objectstack#25130's head `bf8244cb20` (read from git objects, transpiled unchanged). Both refs read the same: every batch body equals `sourceRowsToCsv` of that batch's rows, the adapter's rows equal the preview's rows, and 0 server rows differ from the preview row at the same renumbered index. One preview row is never read: row 500, the last of batch 1, whose mapped cells are all empty. That is B1 at a batch end; sent as one request, the file keeps it. So numbering stays aligned, and in the identity import B1 holds for the last row of every batch, not only the file's last. The changeset says so.\n- **H3, a marked header.** On the default path the text is headed by field names, so a header the export marked (`'=Region`, auto-mapped to `region`) reaches the server as `region`. Pinned.\n\n## Pins\n\n`ImportWizard.csvFileSource-13315.test.tsx` drives the real wizard to its real request. A `.csv` file on the default path sends `format: 'csv'` with the corrected preview cells, and the dry run sends the same payload. A `.tsv` or `.txt` file picked in the upload step is a CSV file too. A marked header still maps. An undeclared named mapping sends CSV text with `mappingName`. CONTROLS: a `json`-declared mapping, an .xlsx sheet and a paste send JSON rows.\n\n`identityImport.csvBatches-13315.test.tsx` covers the batching itself (quoted commas, a quoted line break, doubled quotes, an all-empty row, a header alone refused before any request, JSON rows batched as JSON). It also runs the real wizard and the real adapter on a 1001-row `.csv` file: three CSV batches cut from the wizard's text, results renumbered 1 to 1001, and a one-time password on row 777 named `user777@example.test`. CONTROL: a paste goes out as JSON rows.\n\n`ImportWizard.csvDoorParity-13315.test.tsx`, the measurement test, now pins the shipped route: its B1 case drives the real wizard and asserts the CSV request. Its FILE TEXT cases and its recorded B2 and B3 rows stay, as the record of why the file's own text was not sent and of the shapes the route was accepted with.\n\nAblation, one-shot, with objectstack's `scripts/ablation-replace.mjs` on the committed head `ba5cc54`; each mutation was proven on disk and restored to the HEAD blob:\n- A, the hand-mapped arm ignores `csvFile`: 6 red (the .csv, .tsv, .txt and marked-header pins, the parity B1 case, the 1001-row identity case), 16 green.\n- B, the identity adapter ignores a CSV request: 2 red (the batching case and the 1001-row case), 4 green.\n\n## The shapes the changeset states (B1 to B3)\n\nStated at user level, per the disclosure ruling. A last row whose mapped cells are all empty is not imported; in the identity import, the same holds for the last row of each 500-row batch. A cell that begins with an apostrophe, continues with `+` or `-`, holds only digits, spaces and `( ) + - . ,` and ends in a tab or a line break keeps the apostrophe, as it did before. A space before a leading apostrophe is trimmed first, so such a cell now imports without the apostrophe.\n\n## Acceptance notes\n\nObservations, not filed and not fixed here:\n- `parseDelimited`'s doc comment says trailing whitespace is trimmed, but the code trims both ends of every cell, and B3 rests on the leading half. Not fixed, because the ruling leaves both parsers unchanged. Carrier: none.\n- Once objectstack-ai/objectstack#25130 lands, the preview still checks cells as the file spells them (`checkImportCell`), and matches a named mapping's columns by the header as written (`savedMappingToDisplayIndexMap`). So a marked select value or a marked header shows as a preview finding or an unmatched column, while the server accepts it. Dormant until that PR lands. Carrier: none.\n- objectstack's `parseCsvToRows`, read as a function only: a leading blank line becomes the header row, an inner blank line becomes a row, and a repeated header keeps the last column, while its doc comment says the first non-empty line is the header. Not reached from here: this PR never sends the file's own text. Carrier: none.\n\n## Gates (local; final head `ec515a4` unless a line says otherwise; each exit code captured before any pipe)\n\n`dispatch-gates.mjs` refuses a non-objectstack tree, so the list is hand-derived from this repository's `package.json` and AGENTS.md section 9:\n- `pnpm --workspace-concurrency=2 --filter \"@object-ui/app-shell^...\" build` (29 packages, `plugin-grid` included), at `ba5cc54`: VERDICT command-exit 0.\n- `pnpm --filter @object-ui/plugin-grid type-check`, at `ba5cc54` (no `plugin-grid` file changed after it): exit 0. `tsc -p tsconfig.test.json --listFiles` names both 13315 test files.\n- `pnpm --filter @object-ui/app-shell type-check`: exit 2 at `ba5cc54` (TS2322, the new test's endpoint parameter type), fixed in `ec515a4`, then exit 0. `--listFiles` names the new test once.\n- `pnpm --filter @object-ui/plugin-grid lint` and `pnpm --filter @object-ui/app-shell lint`, at `ba5cc54`: exit 0, 0 errors. eslint on the changed files at `ec515a4`: exit 0, warning counts unchanged from the merge base (`ImportWizard.tsx` 9 and 9, `identityImport.ts` 0 and 0).\n- `pnpm exec vitest run packages/plugin-grid/`: Test Files 240 passed (240), Tests 2168 passed (2168).\n- app-shell, the 10 test files that read the import surfaces (`identityImport*`, `importTargetFields`, `ObjectView.importPredicates`, `ObjectView.headerFold-12705`, the two `ExcelImportBar` files): Test Files 10 passed (10), Tests 78 passed (78). The full app-shell suite is left to CI's Test shards.\n- `check-changeset-presence`, `check-changeset-no-major`, `check:new-line-citations`, `check:control-bytes`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:unreferenced-sources`, `check:changeset-claims`, `check:pending-changeset-literals`: each exit 0.\n- NOT MEASURED locally: `check:readme-exports` exited 1 with \"the population COLLAPSED\", because 28 packages are unbuilt in this worktree. CI's Repo Gates runs it.\n\nCI at `ec515a4`: 33 check-runs success (Lint, Type Check, Test and all 16 shards, Test (dist pins), Build & E2E, Bundle Analysis, Repo Gates, the changeset checks, Governed Surface Queue Guard, Line Citation Gate, and the rest), 3 skipped (the coverage matrix, dependabot).\n\nLanding order, unchanged from the card: this PR, then objectstack's `.objectui-sha` bump, then objectstack-ai/objectstack#25130.\n\n---\n_Generated by [Claude Code](https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3)_\n"
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #13316 comment 6329865319 · objectstack domain:cli seat 2 · session session_01AFrKwndmrCLtF5V9hNtrw3 · 2026-10-22T00:45Z · head ec515a44a9. Ready, auto-merge armed. Its consumer-side follow-up is objectstack-ai/objectstack#25151 (the .objectui-sha bump), then objectstack-ai/objectstack#25130.


    Generated by Claude Code

  9. added a commit that references this issue on Oct 22, 2026
    56cf4d8
  10. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · objectstack domain:cli seat 2 · os-bill · session session_01AFrKwndmrCLtF5V9hNtrw3 · 2026-10-22T01:11Z.

    Reading Result
    PR #13316 merged through the merge queue (added_to_merge_queue 2026-10-22T00:44Z, merged 2026-10-22T00:56Z), merge commit 56cf4d8e89
    objectui origin/main git merge-base --is-ancestor 56cf4d8e89 origin/main true

    State: closed completed by the PR's Fixes #13315; pm:dispatched removed in this stroke. Next in the chain: objectstack-ai/objectstack#25151 (the .objectui-sha bump past 56cf4d8e89) is unblocked and claimed by this seat, then objectstack-ai/objectstack#25130.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions