Skip to content

fix(core): an import row reports the fields its write dropped, on the dry run and on the commit - #21203

Merged
objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-20701-import-reads-drops
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-20701-import-reads-drops

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

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/import on SQLite :memory: at origin/main f3b16fc2f (real ObjectQL, the real protocol, the real RestServer routes):

column POST /data/:object import dry run, before import commit, before
formula doubled (the card's title case) 201 + droppedFields computed ok, created, no drop signal ok, created, no drop signal
static readonly ro 201 + droppedFields readonly ok, created, no drop signal ok, created, no drop signal
only a formula key 201, an empty record + computed ok, created ok, created (an empty record)
writable title 201 ok, created ok, created

The 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 ok row carries the engine's own per-row droppedFields, 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

  • One helper, rowDrops, in packages/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:
    • the dry run: the validateData verdict for the row (results[0].droppedFields);
    • a create through insertManyData: the row's own outcome;
    • a create through createData (the inline path, and bulkWrite's per-row degradation) and an update through updateData: 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.ts is not touched.
  • ⛔ The import keeps no list of non-writable types and never reads a 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 optional droppedFields it already answered with. That is the step the engine PR's acceptance notes left to this one.
  • REST: no edit. The sync route answers summary.results whole. The job worker persists capImportResults(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:

  1. A create batched through createManyData (a protocol without insertManyData). 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.
  2. An upsert row matched to a record whose readonlyWhen is true. The preview asks validateData in update mode, which runs no readonlyWhen or primary-key strip, because it has no prior record. So the dry run names nothing for that field, while the commit reports readonly_when, as PATCH does. 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.droppedFields describe 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 neighbouring warnings bullet now names the runner's current path. content/docs/references/api/export.mdx is regenerated (check:generated --fix, only the stale check: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-changeset is not applied. Check Changeset stays red until someone confirms the correction here.

Hypotheses, measured

  • H1: at f3b16fc2f the formula row succeeds on both halves; neither half reports the drop (table above).
  • H2: holds. validateData answers results[].droppedFields, and insertManyData answers outcomes[i].droppedFields. createManyData answers only a batch union. createData and updateData answer per call. Before this PR, runImport read none of them.
  • H3: a row whose only payload key is dropped: the dry run says created, the commit creates an empty record, and the door answers 201 with the same empty record. The two halves agree, and now both report the drop. The one remaining difference is cell 2 above.
  • H4: all three REST surfaces pass the row whole; no REST edit.
  • H5: objectui (origin/main edfcf5a). ImportWizard.tsx reads row, ok, field, error and code off import rows; importRecords returns the result untouched. The exhaustive reason consumer (writeWarningToast, STRIPPED_LINE) is fed only from create and update responses. So nothing in objectui reads ImportRowResult.droppedFields today.

Tests

All readings are on SQLite :memory:, with heavy steps under os-verify-lock. The final head is 345905c5f8, a merge of origin/main whose incoming commits touch none of the import paths.

  • New packages/core/src/utils/import-runner-dropped-fields.test.ts (11 cases, ImportProtocolLike doubles, one channel per case): the dry run (create and update), the insertManyData outcomes, createData, a failed batch degraded to createData, a summary recompute failure after the write, updateData, the createManyData union (no key), verbatim copy of an unknown reason, and the controls: a clean row, a refused row, a failed outcome.
  • New 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's droppedFields. Also: a row whose only key is dropped; per-row attribution in a mixed file; upsert updates equal to PATCH's report; the readonlyWhen limit; and the async job's results route, commit and dry run.
  • Flipped on purpose: 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, at 345905c5f8: Test Files 75 passed (75), Tests 2114 passed (2114).
  • pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2, the whole package at ca1af119d3: Test Files 256 passed (256), Tests 4844 passed | 301 skipped (5145).
    • The last merge (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 typecheck and pnpm --filter @objectstack/rest run typecheck, at 345905c5f8: exit 0. tsc -p tsconfig.test.json --listFilesOnly lists the new core pin.
  • Spec, at 0bfd86f84f: export.test.ts and validate-data.test.ts (53 passed), and pnpm --filter @objectstack/spec run typecheck exit 0.
  • Import-side consumers, after building their closures: plugin-auth's four import suites (140 passed) and service-automation's connector-pull suites (19 passed). tsc --noEmit and check:test-typecheck pass in both packages.
  • Reverse check of the cross-package type: a throwaway rest file typed against core's rebuilt .d.ts. A valid ImportRowResult.droppedFields and a valid insertManyData outcome compiled. A bad reason gave exactly one TS2322. 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 to blob == HEAD (10ea4ddf19e3) with an empty git diff HEAD.

  • Core leg: rowDrops returns {}. 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.
  • REST leg (dist-routed): rest resolves @objectstack/core through dist/. The same mutation, with a globalThis.__ABLATION_20701__ marker, was rebuilt into core.
    • ablation-dist-preflight found the marker in dist/. tsup's DTS step refused the mutated file (unused parameter), but the JS it reads was emitted.
    • Result: Tests 11 failed | 8 passed (19) over the new pin and the flipped file; the greens are the controls.
    • Restore leg: core rebuilt, and ablation-dist-preflight … --absent printed "marker absent from all 14 built files". The two files re-ran green (19 passed).

Gates

node scripts/pm/dispatch-gates.mjs --commands was re-derived at 345905c5f8 and named 110 commands. Each ran with its exit code recorded before any pipe. --ran reconciles: "110 derived famil(ies) accounted for — 110 run, 0 NOT-MEASURED".

  • 109 exited 0.
  • 1 exited 1 by design: check-empty-changeset --base origin/main, the DELIBERATE CORRECTION above.
  • check:driver-conformance read the same before and after: "OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt".

Lint, narrowed with proof (at 345905c5f8)

  1. The population comes from eslint's own config. isPathIgnored is true for the three changesets and the generated .mdx. calculateConfigForFile lints the six .ts files.
  2. The counts come from eslint's JSON. eslint --no-inline-config --format json over those six: files=6 errors=0 warnings=0.
  3. Invariance. parserOptions.project and projectService are 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;
    • a spec test comment;
    • the generated export.mdx;
    • the two predecessor changesets.

    Each says something this PR makes false. The engine seat named the docblock bullet and the insertManyData outcome type as this PR's inputs.

  • Line 2 declares a widening, per the seat's answer 5935693776 on the card. @objectstack/core exports ImportRowResult and ImportProtocolLike, and both gain an optional droppedFields, an additive widening of its published TypeScript surface. So the changeset grades @objectstack/core minor. @objectstack/spec stays patch, for the describe text. No BREAKING marker is owed: nothing is removed, renamed or refused.

  • The createManyData-only path and the update-mode readonlyWhen preview are the two named absences above. Neither is a new defect.


Generated by Claude Code

claude added 11 commits October 1, 2026 13:46
…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>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/spec, touching 14 documentable anchor(s).

8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/api/data-api.mdx (via droppedFields (symbol, a field of interface ImportRowResult, an authorable key of api/ImportRowResult))
  • content/docs/api/wire-format.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/data-modeling/fields.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/data-modeling/import-mappings.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it), /:object/import/jobs (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/kernel/contracts/data-engine.mdx (via droppedFields (symbol, a field of interface ImportRowResult, an authorable key of api/ImportRowResult), insertManyData (symbol, a method of interface ImportProtocolLike))
  • content/docs/protocol/kernel/http-protocol.mdx (via droppedFields (symbol, a field of interface ImportRowResult, an authorable key of api/ImportRowResult))
  • content/docs/protocol/objectql/state-machine.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it), /:object/import/jobs (route, bridged from symbol runImport — its route source's handler names it))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/releases/v17/17-0.mdx (via droppedFields (symbol, a field of interface ImportRowResult, an authorable key of api/ImportRowResult))
  • content/docs/releases/v17/17-4.mdx (via droppedFields (symbol, a field of interface ImportRowResult, an authorable key of api/ImportRowResult), insertManyData (symbol, a method of interface ImportProtocolLike))
  • content/docs/releases/v17/17-5.mdx (via droppedFields (symbol, a field of interface ImportRowResult, an authorable key of api/ImportRowResult), insertManyData (symbol, a method of interface ImportProtocolLike))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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 — 142 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 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0b5588cfc4cafd88ad22526f7fd81cf1256a41db — the merge of head 9e26304a5c78e9429672ddf864768e7e6500081a into base 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 2 commits October 1, 2026 16:25
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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9e26304a5c78e9429672ddf864768e7e6500081a
Local-runs: none

Read-only review of PR #21203 (Fixes #20701, line 2 Clause-②: yes (widening); draft; head repo equals base repo; 10 files, +617 / −39) against main at the merge base 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1. Inputs: card #20701's body and all 34 comments (through the takeover claim 5936863307), the PR body, its file list and net diff, the check-runs on the head, and files read at origin/main with git show / git grep. Every path this PR touches, plus bulk-write.ts, protocol.ts, protocol.zod.ts, data-engine.zod.ts, the gate script and the two workflows named below, is byte-identical between the merge base and origin/main; the only moved file among those consulted is rest-server.ts, one description string far from the import routes. Nothing was built, run or re-run. One attempt to read the red job's log was refused by the proxy, so that job's signature below is its failing STEP from the jobs API, with no first error line.

① Derived judgments

Each accept-set or public-surface change the diff implies, named and judged:

  1. @objectstack/core published TypeScript surface widens — RIGHT. packages/core/src/index.ts re-exports ./utils/import-runner.js whole, so ImportRowResult (gains optional droppedFields, typed DroppedFieldsEvent[] from @objectstack/spec/data) and ImportProtocolLike (its insertManyData outcome element gains optional droppedFields) are published. Both additions are optional keys: an existing implementor of insertManyData still type-checks, an existing reader of ImportRowResult is unaffected, and the new outcome type is spelled exactly as the real protocol's own signature at packages/metadata-protocol/src/protocol.ts:13744. Additive widening; nothing removed, renamed, narrowed or refused. createData / updateData stay typed any on the interface, as before.

  2. The dry run now carries the engine's per-row drops — RIGHT. previewVerdict already returned the first element of validateData's results, the per-row verdict, and ValidateDataResponseSchema.results[] declares droppedFields since spec: declare the write-drop signal on the validate and import answers (an optional per-row dropped-fields key on ImportRowResultSchema and on ValidateDataResponseSchema) and declare the served ImportRowResult.warnings: #20701 item 1, spec half #20921 (protocol.zod.ts:2146). The diff spreads rowDrops(verdict?.droppedFields) only into the two ok arms; a refused dry-run row and a skipped row are untouched. rowDrops adds the key only when the report is a non-empty array, so a clean row grows no empty key, matching the describe's "present only when at least one field was dropped".

  3. The commit through insertManyData — RIGHT. flushPendingCreates hands bulkWrite the buffered rows' data objects themselves, bulkWrite slices that array without copying, splitByExisting pushes the same objects into toCreate, and writeOne receives the batch element; so a Map keyed by the row object and read back through batch[res.index].data resolves to the right row. Outcome k is toCreate[k]'s, and the recovered-outcomes path (recoverSummaryStale) keeps that order, so the summary-stale case is attributed per row too (pinned). A row a retry attempt found already committed gets no entry and therefore no key, which the code comment states: that is an omission, never a false report.

  4. The commit through createData (the inline path and bulkWrite's per-row degradation) and through updateData — RIGHT. Both responses are one row's; responseDrops reads only the key CreateDataResponseSchema and UpdateDataResponseSchema declare. On the update path the read sits inside the try, so a recovered record (summary stale) carries no key rather than a guessed one.

  5. Verbatim copy, no second vocabulary, no reason read — RIGHT, and triage's ⛔ holds (5906762084, 5910777625: no import-side list of non-writable types; the engine's report is the only source). rowDrops copies the array and never inspects reason; one core pin drives a reason outside today's enum through unchanged.

  6. The two named absences — RIGHT, and both are stated where a reader looks (the core header, the spec describe, the changeset). (a) createManyData answers a batch-level union whose own docblock says a name there means "at least one row dropped this field" and forbids a per-row reconstruction (protocol.ts, the docblock above :13744), so those rows carry no key. (b) An update-mode preview runs no readonlyWhen or primary-key strip, so an upsert row matched to a locked record names nothing on the dry run and readonly_when on the commit; pinned as the limit (the pin flips on purpose when the engine closes it). Reach of (a), measured on origin/main: the four in-tree runImport callers are the two REST routes and service-automation's connector pull, which hand in the real protocol service (it has insertManyData), and plugin-auth's admin user import, whose hand-built double deliberately offers no bulk primitive at all, so its rows take the inline createData path. No in-tree caller takes the createManyData-only path; the absence is a declared limit of the published contract, not a reachable gap today.

  7. Published spec shape ImportRowResultSchema — RIGHT: the accept set does not move. export.zod.ts changes a TSDoc block, one describe string and a path in the neighbouring warnings bullet (the runner moved to core in PR feat(service-automation,core,types): the connector sync executor pulls a mapping's connectorSource through the import runner, moved beside bulkWrite #21084); no key, type, refinement or default changes. The new describe is true of the runner: a committed row reports what its write made; a dry-run row reports what its preview runs, excluding the update-mode readonlyWhen / primary-key strips; absence does not prove nothing was dropped. The old sentence "a dry-run row reports the strips the write would make" would be false for exactly that update-mode cell, so correcting it is owed. export.test.ts changes a comment only.

  8. content/docs/references/api/export.mdx — RIGHT. AUTO-GEN, not hand-edited: the three changed droppedFields rows are byte-equal to the new describe, and check:docs (inside the required TypeScript Type Check job) is green on this head, which is the gate that reds on a hand edit or a stale regeneration.

  9. The REST wire — RIGHT, no REST source edit needed. On origin/main the sync route answers results: summary.results (rest-server.ts:9268), the job worker persists capImportResults(summary.results) (:9459), which filters and slices rows whole, and the results route returns the stored items; so an ok row's new key reaches all three. The new REST pin measures the three at the wire against a real engine (sync dry run and commit, upsert, the async job's results route with dryRun true and false), including equality with the create door's and PATCH's own droppedFields.

  10. Tests — RIGHT in direction. The flipped import-row-report-field-20701.test.ts cells, which pinned only that both halves succeed, now also assert the reported strip and its reason; the header's "not pinned here" paragraph is replaced accordingly. No pin was weakened.

  11. Not governed. None of the 10 paths is a governed surface; Governed Surface Queue Guard is green. No ADR surface is touched, and ADR-0087 is not engaged (nothing breaking).

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 readonlyWhen fork's "revisit B" trigger (the #20922 seat ruling) was measured by the dev against objectui edfcf5a with no consumer found, and that reading is the dev's, not re-measured here.

② Semver level

  • Clause-②: yes (widening) — RIGHT. Item 1 above is an additive widening of @objectstack/core's published TypeScript surface, and the served REST import row gains a key that was declared but never produced. Nothing narrows: no accept set shrinks, no key is removed, renamed or refused. The arm is the widening arm, so no BREAKING marker and no ADR-0087 disposition are owed. This is seat answer B on the card (5935693776, open to veto, none recorded through comment 34), consistent with the lane's grading of the same act on engine: validateData forwards engine.validate's onFieldsDropped and answers the drops, and insertMany reports drops per outcome through insertManyData: #20701 item 1, engine half #20922.
  • .changeset/20701-import-row-dropped-fields.md: @objectstack/core: minor, @objectstack/spec: patch — RIGHT. yes takes at least minor on the package whose surface widens, and core is that package. Spec's change is text only (TSDoc, one describe, and the generated docs page it feeds), with the schema's accept set unchanged: patch. @objectstack/rest publishes nothing from this diff (two test files only), so no rest changeset is owed and skip-changeset does not apply. The changeset body carries the Clause-②: yes (widening) line, and its claims match the diff: the four reason arms are the engine's enum, the three REST surfaces are pinned, the two absences are the ones in item 6, and the insertManyData outcome type is the one in item 1. The gate's level axis accepted it: the Check Changeset job's steps "Require a changeset", "Require an ADR-0087 disposition on a declared-breaking changeset" and "Guard against accidental major bumps" are green on this head; its one red step is the deliberate correction below.

The deliberate correction: two changesets this PR edits but did not add

check-empty-changeset (the Check Changeset job's step "Reject an empty-frontmatter changeset added by this PR") refuses both edits by name. Its source at the merge base (identical on origin/main) names the two classes and gives the DELIBERATE CORRECTION class the remedy "do NOT restore it -- say so on the PR and get it confirmed". Both notes are still pending on origin/main (neither has been consumed by a release), so each would ship in the same release as this PR.

  • .changeset/20921-import-row-dropped-fields.md — removed sentence: "The key is declared ahead of its producer." Replacement: none (pure removal; the surrounding line is otherwise byte-identical). Judgment: once this PR lands, runImport produces ImportRowResult.droppedFields and the engine produces ValidateDataResponse.results[].droppedFields (engine: validateData forwards engine.validate's onFieldsDropped and answers the drops, and insertMany reports drops per outcome through insertManyData: #20701 item 1, engine half #20922), so the key ships alongside its producer and the sentence would be false. The edit is a correction and nothing more. Every other sentence of the note stays true after landing: the schema additions, the engine's reason vocabulary with no second enum, unchanged ok / action / valid, "a server that does not produce the report omits the key" (still true for the two absences in ① item 6), the warnings paragraph, the capped-sample paragraph and the exhaustive-consumer paragraph.

  • .changeset/20922-per-row-dropped-fields.md — removed sentence: "The REST import route does not copy the per-row report onto its row results yet." Replacement: none (pure removal; the rest of the line is byte-identical). Judgment: once this PR lands, the import runner the REST import route calls copies the per-row report onto each ok row and the route serves it (① item 9), so the sentence would be false. Correction and nothing more. Every other sentence stays true: the per-row answers of validate / validateData and insertMany / insertManyData, recorded at the strips, the hook exemption, "a row the write does not complete carries none", the update-mode preview limit (which this PR relies on and names), the unchanged batch-level union, and the minor grading in both packages.

  • No other pending note is made false by this diff: the three earlier 20701-* REST notes, 20805-formula-write-strip.md and 20963-not-null-hint-declared-column.md were read on origin/main, and none states that the import row lacks the drop signal.

The three ride-the-red conditions (SKILL.md), each read on the merge base, which the head carries unchanged:

  1. The gate's own source declares the class red by design, with the confirmation remedy quoted above (scripts/check-empty-changeset.mjs).
  2. The job has no queue leg: pr-automation.yml triggers on pull_request only (no merge_group anywhere in the file), and lint.yml's unconditional changeset-family step runs the --self-test halves only, by its own comment, so the queue's rebuilt generation never runs the real scan.
  3. This record is the PR comment that names the gate, the two notes, each removed sentence and why it would be false. Per the landing rules, a same-head contract-review PASS record is the confirmation; nothing further is awaited.

③ Boundary flags

Every dev flag and open_questions entry from the two reports on this head's branch (5935656110, 5935832005), the seat answer (5935693776), the release (5936634032) and the takeover claim (5936863307):

  • Open question 1 (round 0): which Clause-② line. Answered by seat answer B; applied in round 1 (commit ff2ea1c7f1; the head carries '@objectstack/core': minor, the changeset's Clause-②: yes (widening) line and the PR body's line 2). Judged RIGHT in ②. Closed.
  • Deviation: file surface beyond the claim (spec text, the generated page, a spec test comment, two predecessor changesets). Each edit corrects text the change makes false (① items 7 and 8, the correction subsection), and the takeover claim names all 10 files as the surface. The spec touch owes this at-tier review; this record is it. Closed.
  • Deviation: changeset levels. Settled with the open question. Closed.
  • Deviation: merges of origin/main. The last, 0d421041d1, is the merge base of this head; main has since moved only on paths outside this diff's files (one rest-server.ts description string). The landing-time merge-tree is the seat's. Noted.
  • Deviation: harness attribution. The PR body ends with the session-URL footer and carries no model identifier; the changeset carries none. Closed.
  • Deviations: throwaway probes deleted; worktree cleanup; the body's "Line 2 declares a widening" wording. Nothing to answer; the file list carries no probe file. Closed.
  • Out-of-scope finding: the update-mode preview fork ("revisit B"). The dev measured no consumer acting on update-row previews or on ImportRowResult.droppedFields in objectui; nothing is filed. The limit is pinned and documented. Taken as noted, not escalated.
  • Out-of-scope finding: createManyData-only protocols NOT MEASURED. Measured here on origin/main (① item 6): no in-tree caller takes that path. Closed; nothing to file.
  • Release 5936634032: the at-tier review record was refused by a permission layer and never posted. This record is the re-run on 9e26304a5, written through the fleet relay.
  • Release 5936634032: the three ride-the-red conditions. The seat verified two; all three are verified above, the third being this comment.
  • Dispatch ⛔s (claim 5932483988): no change to the async job's results cap (capImportResults untouched; the describe still names the capped, failures-first sample) and no storage.notNull judgment in the preview (the preview reads validateData only). Both hold.
  • Nothing is escalated.

Check-runs on the head (read at 2026-10-01T17:31Z)

42 check-runs on 9e26304a5c78e9429672ddf864768e7e6500081a, all completed. The seven required contexts all read success: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Also success: Build Docs, Dogfood Verify CLI, Spec property liveness, Check Documentation Links, Flag docs affected by code changes, Check PR Size, Auto Label, the four Type Check · jobs, the six Test Core (n/6) and three Dogfood Regression Gate (n/3) shards, filter, and the five PR-automation claim and part-of guards. skipped: Console Pin Gate, Packed-tarball smoke (opt-in), and one earlier Auto Label / Check PR Size pair. The only failure: Check Changeset, twice (job ids 110470696709 and 110471069144, the second triggered by the body edit); in both, the one failing step is "Reject an empty-frontmatter changeset added by this PR", the step that runs check-empty-changeset.mjs, with every other step of that job success or skipped as designed. That red is the deliberate correction judged above. The job's log could not be read through the proxy, so the first error line is not quoted here; the dev's round-1 report quotes the gate naming exactly the two notes.

Implemented-by: claude/issue-20701-import-reads-drops
Reviewed-by: session_017xfMoEjKUuSh2xYB8sCozp

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 17:47
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 17:47
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit bbcd20c Oct 1, 2026
42 of 44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20701-import-reads-drops branch October 1, 2026 18:09
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

3 participants