Repository navigation
fix(plugin-grid,app-shell): a CSV file reaches the import door as CSV text - #13316
Conversation
…and the server's CSV door Measurement for objectui#13315 item 2, before the wizard sends a CSV file source to the server's CSV import door. The browser half (decode, parse, re-spelled CSV, the default path's real request) runs live; the server's parseCsvToRows is recorded at objectstack main abc25a3536 and at the head of objectstack-ai/objectstack#25130 (bf8244cb20). Recorded inputs are the ones whose difference changes an imported value. No behaviour changes. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
… text The import wizard sends a CSV file source (.csv, .tsv, .txt: the files parseSpreadsheetFile reads as delimited text) as format csv with sourceRowsToCsv(rows), the cells the preview parsed, on the hand-mapped path and on a named mapping not declared json. A json-declared mapping keeps JSON rows (the door refuses CSV for it); an .xlsx sheet and a paste are unchanged. The source kind travels as wizard state set where the grid is loaded, and the CSV-file test is delimitedFileDelimiter, the predicate parseSpreadsheetFile now picks its text arm with. The identity import's adapter takes the same change: a CSV request is cut at record boundaries into batches of at most 500 rows, each posted as the header record plus its data records, and each batch's rows are still read to name every row's identity in the result. The measurement test now pins the shipped route and keeps the recorded B1-B3 shapes; the changeset states them (objectui#13315, maintainer ruling A). Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…dapter's authFetch Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
席位核对 · ACCEPTobjectstack
Generated by Claude Code |
Fixes #13315
Clause-②: no
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'withsourceRowsToCsv(rows), the cells the preview parsed. The identity import's adapter takes the same change. Neither parser changes:parseDelimitedis byte-identical, and objectstack'sparseCsvToRowsis not touched.Dispatched by objectstack
domain:cliseat 2 on objectui#13315's claim (comment 6327882289), sessionhttps://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.What changed
assembleImportRequest,ImportWizard.tsx). A newcsvFileoption. With it, the hand-mapped path sends{ format: 'csv', csv: sourceRowsToCsv(rows) }, and a named mapping sends CSV text unless it declaressourceFormat: 'json', which the door refuses CSV for (MAPPING_FORMAT_MISMATCH). Acsv-declared mapping sends CSV text from any source, as before (console: the import wizard, given a shipped CSV mapping, sends JSON rows that name the csv mapping and fails MAPPING_FORMAT_MISMATCH; the REST door with the same mapping succeeds #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 requestbuildImportRequestassembles.assembleImportRequestcannot 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 theinitialFileeffect answerdelimitedFileDelimiter(file.name) !== null, a paste answers false, and reset clears it.delimitedFileDelimiter(importParsers.ts) is the name testparseSpreadsheetFilealready 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,.tsvand.txt, the delimited-text inputs the card's item 2 measured as CSV.parseSpreadsheetFilebehaves exactly as before.identityImport.ts, H2). The adapter batched the request'srows, and a CSV request has none, so it would have thrown.batchIdentityImportRequestnow 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 exactlysourceRowsToCsvof 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.Why 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 onDataSource.importRecordswould 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.Measured (H2, H3)
parseCsvToRowson every batch body, atmainabc25a3536and at fix(rest)!: the CSV export writes a text cell in a form its consumer reads as text, and the CSV import reverses it (#25078) objectstack#25130's headbf8244cb20(read from git objects, transpiled unchanged). Both refs read the same: every batch body equalssourceRowsToCsvof 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.'=Region, auto-mapped toregion) reaches the server asregion. Pinned.Pins
ImportWizard.csvFileSource-13315.test.tsxdrives the real wizard to its real request. A.csvfile on the default path sendsformat: 'csv'with the corrected preview cells, and the dry run sends the same payload. A.tsvor.txtfile picked in the upload step is a CSV file too. A marked header still maps. An undeclared named mapping sends CSV text withmappingName. CONTROLS: ajson-declared mapping, an .xlsx sheet and a paste send JSON rows.identityImport.csvBatches-13315.test.tsxcovers 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.csvfile: three CSV batches cut from the wizard's text, results renumbered 1 to 1001, and a one-time password on row 777 nameduser777@example.test. CONTROL: a paste goes out as JSON rows.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.Ablation, one-shot, with objectstack's
scripts/ablation-replace.mjson the committed headba5cc54; each mutation was proven on disk and restored to the HEAD blob:csvFile: 6 red (the .csv, .tsv, .txt and marked-header pins, the parity B1 case, the 1001-row identity case), 16 green.The shapes the changeset states (B1 to B3)
Stated 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.Acceptance notes
Observations, not filed and 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. Not fixed, because the ruling leaves both parsers unchanged. Carrier: none.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.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.Gates (local; final head
ec515a4unless a line says otherwise; each exit code captured before any pipe)dispatch-gates.mjsrefuses a non-objectstack tree, so the list is hand-derived from this repository'spackage.jsonand AGENTS.md section 9:pnpm --workspace-concurrency=2 --filter "@object-ui/app-shell^..." build(29 packages,plugin-gridincluded), atba5cc54: VERDICT command-exit 0.pnpm --filter @object-ui/plugin-grid type-check, atba5cc54(noplugin-gridfile changed after it): exit 0.tsc -p tsconfig.test.json --listFilesnames both 13315 test files.pnpm --filter @object-ui/app-shell type-check: exit 2 atba5cc54(TS2322, the new test's endpoint parameter type), fixed inec515a4, then exit 0.--listFilesnames the new test once.pnpm --filter @object-ui/plugin-grid lintandpnpm --filter @object-ui/app-shell lint, atba5cc54: exit 0, 0 errors. eslint on the changed files atec515a4: exit 0, warning counts unchanged from the merge base (ImportWizard.tsx9 and 9,identityImport.ts0 and 0).pnpm exec vitest run packages/plugin-grid/: Test Files 240 passed (240), Tests 2168 passed (2168).identityImport*,importTargetFields,ObjectView.importPredicates,ObjectView.headerFold-12705, the twoExcelImportBarfiles): Test Files 10 passed (10), Tests 78 passed (78). The full app-shell suite is left to CI's Test shards.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.check:readme-exportsexited 1 with "the population COLLAPSED", because 28 packages are unbuilt in this worktree. CI's Repo Gates runs it.CI 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).Landing order, unchanged from the card: this PR, then objectstack's
.objectui-shabump, then objectstack-ai/objectstack#25130.Generated by Claude Code