Repository navigation
fix(core): an import row reports the fields its write dropped, on the dry run and on the commit - #21203
Conversation
…n the dry run and the commit runImport copies the engine's own per-row droppedFields onto each ok ImportRowResult: from validateData's verdict on the dry run, from the row's insertManyData outcome, and from the createData / updateData response a single-row write answers with. The events are taken verbatim; the import reads no reason and holds no list of non-writable types. A create batched through createManyData answers only the batch-level union, which names no row, so those rows carry no key. ImportProtocolLike.insertManyData's outcome type names droppedFields. Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…the runner reads Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…the async job and the create door Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…rt on both halves The cells used to pin only that both halves succeed, with a note that the drop signal could not be read yet. They now assert the reported strip and its reason on the dry run and on the commit. Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…who produces it, and where it cannot
The ImportRowResultSchema docblock said the key was declared ahead of
its producer and that no server set it. The import runner now copies
the engine's per-row report onto each row, so the bullet names the
producer and the two places it cannot: a create batched through
createManyData (a batch-level union) and the readonlyWhen / primary-key
strips of a row the import updates, which an update-mode preview does
not run. The describe's "a dry-run row reports the strips the write
would make" says the same. The warnings bullet names the runner's
current path.
The two pending notes for the predecessor cards each lose the sentence
this change makes false ("declared ahead of its producer", "the REST
import route does not copy the per-row report onto its row results
yet").
Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
…ds describe Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 142 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0b5588cfc4cafd88ad22526f7fd81cf1256a41db && git checkout 0b5588cfc4cafd88ad22526f7fd81cf1256a41db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1 9e26304a5c78e9429672ddf864768e7e6500081a && git checkout -B drift-repro 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1 && git merge --no-ff 9e26304a5c78e9429672ddf864768e7e6500081a
node scripts/docs-audit/affected-docs.mjs --json 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1
|
The exported ImportRowResult and ImportProtocolLike gain an optional droppedFields, an additive widening of the published TypeScript surface; the seat's answer reads it as yes (widening). Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read-only review of PR #21203 ( ① Derived judgmentsEach accept-set or public-surface change the diff implies, named and judged:
Residues, acceptance notes only, none blocking: the retry-found-existing row's missing key (item 3) is stated in a code comment and covered by the describe's generic "a server or write path that does not produce this report omits the key", but is not named in the changeset; the ② Semver level
The deliberate correction: two changesets this PR edits but did not add
The three ride-the-red conditions (SKILL.md), each read on the merge base, which the head carries unchanged:
③ Boundary flagsEvery dev flag and
Check-runs on the head (read at 2026-10-01T17:31Z)42 check-runs on Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20701
Clause-②: yes (widening)
What changes
An import row now says which of its fields the write dropped, on the dry run and on the commit, in the words the create door already uses.
The card's own case, measured through
POST /api/v1/data/:object/importon SQLite:memory:atorigin/mainf3b16fc2f(real ObjectQL, the real protocol, the real RestServer routes):POST /data/:objectdoubled(the card's title case)201+droppedFieldscomputedreadonlyro201+droppedFieldsreadonly201, an empty record +computedtitle201The commit no longer fails the formula row: the engine strips the value before the driver sees it, and reports the strip per row. What was left was the report. The door said what it dropped, and the import row said nothing on either half, so a file whose formula column was ignored read exactly like a file that wrote it.
After this PR, each
okrow carries the engine's own per-rowdroppedFields, equal to the create door's. The dry run's report equals the commit's. The sync route, the async job's results and the async dry run all carry it.How
rowDrops, inpackages/core/src/utils/import-runner.ts. It copies the engine's per-row report onto the row verbatim, from whichever single-row channel that row's write answered on:validateDataverdict for the row (results[0].droppedFields);insertManyData: the row's own outcome;createData(the inline path, andbulkWrite's per-row degradation) and an update throughupdateData: the response, which is one row's.bulkWrite's per-row result carries only the record, so a buffered create's report is recorded by the write call that wrote the row, keyed by the row object itself, and read back when the row's result is built.bulk-write.tsis not touched.reason. The engine's report is the only source, so a reason the engine adds later reaches the row unchanged. One core case uses a reason outside today's enum to prove the copy is verbatim.ImportProtocolLike.insertManyData's declared outcome now names the optionaldroppedFieldsit already answered with. That is the step the engine PR's acceptance notes left to this one.summary.resultswhole. The job worker persistscapImportResults(summary.results), which keeps each row whole. The results route returns the stored items whole. The new REST pins measure all three.Where the key stays absent, and what the spec now says
Two named places, both stated in the core header and in the spec:
createManyData(a protocol withoutinsertManyData). Its report is a batch-level union that names no row, and its own docblock forbids resolving it to rows, so those rows carry no key. That is not a guessed share.readonlyWhenis true. The preview asksvalidateDatainupdatemode, which runs noreadonlyWhenor primary-key strip, because it has no prior record. So the dry run names nothing for that field, while the commit reportsreadonly_when, asPATCHdoes. This is the engine seat's ruling on that fork (keep the named limit; the commit still reports the drop). It is pinned as the limit, so that closing it flips the pin on purpose.The
ImportRowResultSchema.droppedFieldsdescribe said "A dry-run row reports the strips the write would make". Cell 2 makes that false once the key is produced, so the describe now says a dry-run row reports the strips its preview runs, and names the update-mode limit. The docblock bullet that said the key was "DECLARED AHEAD of its producer … no server sets the key" now names the producer and both places. The neighbouringwarningsbullet now names the runner's current path.content/docs/references/api/export.mdxis regenerated (check:generated --fix, only the stalecheck:docs).Two pending release notes corrected: please confirm
This PR edits two changesets it did not add, because it makes one sentence in each false in the same release. That is the DELIBERATE CORRECTION class of
check-empty-changeset, which exits 1 by design:.changeset/20921-import-row-dropped-fields.md: it loses "The key is declared ahead of its producer.".changeset/20922-per-row-dropped-fields.md: it loses "The REST import route does not copy the per-row report onto its row results yet."No other sentence in either note is changed.
skip-changesetis not applied.Check Changesetstays red until someone confirms the correction here.Hypotheses, measured
f3b16fc2fthe formula row succeeds on both halves; neither half reports the drop (table above).validateDataanswersresults[].droppedFields, andinsertManyDataanswersoutcomes[i].droppedFields.createManyDataanswers only a batch union.createDataandupdateDataanswer per call. Before this PR,runImportread none of them.201with the same empty record. The two halves agree, and now both report the drop. The one remaining difference is cell 2 above.origin/mainedfcf5a).ImportWizard.tsxreadsrow,ok,field,errorandcodeoff import rows;importRecordsreturns the result untouched. The exhaustivereasonconsumer (writeWarningToast,STRIPPED_LINE) is fed only from create and update responses. So nothing in objectui readsImportRowResult.droppedFieldstoday.Tests
All readings are on SQLite
:memory:, with heavy steps underos-verify-lock. The final head is345905c5f8, a merge oforigin/mainwhose incoming commits touch none of the import paths.packages/core/src/utils/import-runner-dropped-fields.test.ts(11 cases,ImportProtocolLikedoubles, one channel per case): the dry run (create and update), theinsertManyDataoutcomes,createData, a failed batch degraded tocreateData, a summary recompute failure after the write,updateData, thecreateManyDataunion (no key), verbatim copy of an unknown reason, and the controls: a clean row, a refused row, a failed outcome.packages/rest/src/import-row-dropped-fields-20701.test.ts(11 cases, real engine). The card's formula case, readonly, and both together: dry run and commit each equal the create door'sdroppedFields. Also: a row whose only key is dropped; per-row attribution in a mixed file; upsert updates equal toPATCH's report; thereadonlyWhenlimit; and the async job's results route, commit and dry run.import-row-report-field-20701.test.ts, whose formula and readonly cells pinned only that both halves succeed. They now also assert the reported strip and its reason, on both halves.pnpm --filter @objectstack/core exec vitest run --project local --maxWorkers=2, at345905c5f8:Test Files 75 passed (75),Tests 2114 passed (2114).pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2, the whole package atca1af119d3:Test Files 256 passed (256),Tests 4844 passed | 301 skipped (5145).345905c5f8) brought objectql, driver-memory, service-analytics and one spec contract file. On it, rest's closure was rebuilt and its 18 import suites re-ran:Tests 413 passed | 21 skipped (434).pnpm --filter @objectstack/core run typecheckandpnpm --filter @objectstack/rest run typecheck, at345905c5f8: exit 0.tsc -p tsconfig.test.json --listFilesOnlylists the new core pin.0bfd86f84f:export.test.tsandvalidate-data.test.ts(53 passed), andpnpm --filter @objectstack/spec run typecheckexit 0.140 passed) and service-automation'sconnector-pullsuites (19 passed).tsc --noEmitandcheck:test-typecheckpass in both packages..d.ts. A validImportRowResult.droppedFieldsand a validinsertManyDataoutcome compiled. A badreasongave exactly oneTS2322. The file was then deleted.Ablation
Each leg ran with the fix committed first, through
scripts/ablation-replace.mjs(WRAP mode), and each was restored toblob == HEAD (10ea4ddf19e3)with an emptygit diff HEAD.rowDropsreturns{}. The anchor went from 1 hit to 0 and the blob changed. Result:Tests 9 failed | 2 passed (11); the two greens are the no-drop controls.@objectstack/corethroughdist/. The same mutation, with aglobalThis.__ABLATION_20701__marker, was rebuilt into core.ablation-dist-preflightfound the marker indist/. tsup's DTS step refused the mutated file (unused parameter), but the JS it reads was emitted.Tests 11 failed | 8 passed (19)over the new pin and the flipped file; the greens are the controls.ablation-dist-preflight … --absentprinted "marker absent from all 14 built files". The two files re-ran green (19 passed).Gates
node scripts/pm/dispatch-gates.mjs --commandswas re-derived at345905c5f8and named 110 commands. Each ran with its exit code recorded before any pipe.--ranreconciles: "110 derived famil(ies) accounted for — 110 run, 0 NOT-MEASURED".check-empty-changeset --base origin/main, the DELIBERATE CORRECTION above.check:driver-conformanceread the same before and after: "OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt".Lint, narrowed with proof (at
345905c5f8)isPathIgnoredis true for the three changesets and the generated.mdx.calculateConfigForFilelints the six.tsfiles.eslint --no-inline-config --format jsonover those six:files=6 errors=0 warnings=0.parserOptions.projectandprojectServiceare null on all six, so type-aware linting is off and this diff cannot move eslint's verdict on any file it does not touch.Acceptance notes
The claim's surface named the core runner, REST and
.changeset/20701-*.md. This PR also edits:packages/spec/src/api/export.zod.ts: a TSDoc docblock and one describe;export.mdx;Each says something this PR makes false. The engine seat named the docblock bullet and the
insertManyDataoutcome type as this PR's inputs.Line 2 declares a widening, per the seat's answer 5935693776 on the card.
@objectstack/coreexportsImportRowResultandImportProtocolLike, and both gain an optionaldroppedFields, an additive widening of its published TypeScript surface. So the changeset grades@objectstack/coreminor.@objectstack/specstayspatch, for the describe text. No BREAKING marker is owed: nothing is removed, renamed or refused.The
createManyData-only path and the update-modereadonlyWhenpreview are the two named absences above. Neither is a new defect.Generated by Claude Code