Skip to content

fix(rest)!: an async import job whose every row failed ends failed, not succeeded - #27795

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-27676-all-rows-failed-job
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-27676-all-rows-failed-job

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Requested by Sales · project thread
Fixes #27676

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no hand-written docs page or published skill states the async import job's terminal-status rule (git grep of content/docs outside references//releases/ and of skills/ for the job status vocabulary: 0 hits; import-mappings.mdx documents per-row outcomes only). The ImportJobStatus docblock and the generated reference are the spec seat's card #27734.

维护者速读

  • 改了什么:后台导入作业(POST /data/OBJ/import/jobs)在未取消、有行、且没有任何一行被新建/更新/跳过时,终态记为 failed(原来是 succeeded),error 写一句话说明每行都失败、去看 results。
  • 为什么改:按你的裁决 B(记录 6509036166):一行都没导入的作业不能对只看 status 的客户端说「成功」。
  • 风险与回滚:只改这一种终态;部分失败、全部跳过、零行作业仍是 succeeded,取消仍优先;撤销不受影响(这种作业本来就不可撤销)。回滚即删掉 data-action-routes.ts 里的一个判断。
  • 先看:packages/rest/src/data-action-routes.ts 里 everyRowFailed 那段;packages/rest/src/import-job-integration.test.ts 末尾新增的 describe;packages/rest/src/import-row-cell-errors.test.ts 的轮询期望。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/import-job-integration.test.ts,预期 21/21 通过。
  • 需要你做的:无,席位落地。

Ruling (binding)

Maintainer ruling B, record 6509036166 on #27676, execution parameters quoted:

  1. Predicate. At the terminal-status line of the job worker (packages/rest/src/data-action-routes.ts, the finalStatus computation): !cancelled && total > 0 && created + updated + skipped === 0 ⇒ failed, else today's rule. Not ok === 0: a skipped row is not counted in ok. error is set to one sentence saying every row failed and pointing at results, with no tracker number.
  2. Kept as succeeded on purpose: a zero-row job, an all-skipped job, and a partly-failed job. The batch door's "any failure ⇒ false" rule is not carried over. Cancellation still wins.
  3. Dry-run: the same rule (an all-error dry-run ends failed).
  4. Server PR (domain:server, after import: POST /data/OBJ/import/jobs accepts an unregistered object with 201 and the job then reads succeeded, where the data door answers 404 #27501 (a) PR fix(rest)!: an async import job for an unregistered object is refused with 404 OBJECT_NOT_FOUND before any job is created #27711 merges; same file), with five pins: all rows fail → failed, errors === total, error present; 2 ok + 1 fail → succeeded (the existing partial pin is unchanged); all skipped → succeeded; cancellation still wins; all-error dry-run → failed.
  5. Spec docblock (packages/spec/src/api/export.zod.ts, the ImportJobStatus / ImportJobProgressSchema.error sentences, plus gen:docs) goes to the domain:spec seat as its own PR, not in the server PR.

What changes

  • packages/rest/src/data-action-routes.ts, the async import worker's terminal status. The existing cancel resolution runs first and is unchanged: summary.cancelled, then the in-memory cancelledImportJobs flag, then the durable row re-read. Only a status still succeeded after all three is tested by the new predicate, so a cancel always wins, including a late one.
  • The predicate, mapped onto the runner's real summary (ImportRunSummary extends ImportProgress, packages/core/src/utils/import-runner.ts): ruling cancelled = the resolved status is not cancelled (summary.cancelled plus the two late re-checks); total = summary.total (rows.length); created / updated / skipped = summary.created / summary.updated / summary.skipped. summary.ok is not read: it counts created and updated rows only.
  • When the predicate holds, the final patch writes status: 'failed' and error: "Every row of this import failed and nothing was imported; each row's reason is in the job's results." Counts, results and completed_at are written exactly as before. A dry run takes the same path (its valid rows count as created / updated in the runner's preview).
  • No packages/spec change: the ImportJobStatus / error docblocks are spec: ImportJobStatus and ImportJobProgressSchema.error docblocks say failed is only a fatal abort; after #27676 B an all-rows-failed job also ends failed #27734.
  • Changeset: @objectstack/rest patch, BREAKING note, ADR-0087 not-required (no-migration-prescription) marker.

Readers of failed vs succeeded in this lane, measured

  • Undo (importJobUndoable, rest-server.ts): admits succeeded / cancelled only, and also needs a non-empty undo log. A job that created and updated nothing has an empty log, so it was not undoable before and is not now. Pinned: the all-failed job reads undoable: false and undo answers 422 NOT_UNDOABLE.
  • Cancel route: acts on pending / running only; unaffected.
  • History filter ?status=: passes the string through; pinned that the all-failed job lists under status=failed.
  • Console Import Wizard (ImportWizard.tsx importResultOutcome): already reads "nothing imported and something errored" as failed, and jobStatus === 'failed' as failed; the synthetic row at the prog.status === 'failed' branch fires only when no per-row error arrived, which this change never produces. failed is already in its closed terminal set. No UI change.

Tests

packages/rest/src/import-job-integration.test.ts, a new describe on the file's harness (real RestServer route, real ObjectQL engine + ObjectStackProtocolImplementation, SQLite :memory:, real sys_import_job):

  • every row fails → failed, errors === total (3), error present and naming results; results has 3 failed rows; nothing landed; undoable: false, undo 422 NOT_UNDOABLE; listed under status=failed.
  • control: every row skipped (writeMode: 'update', no match) → succeeded, no error.
  • control: a header-only CSV (0 rows) → succeeded, no error.
  • every row of a dry run fails → failed, dryRun: true.
  • control: a valid dry run → succeeded.
  • control: a cancel that lands after the last row of a job whose every row fails at the driver → cancelled, with errors: 3 (it waits for the worker's own final write of results before reading).
  • 2 ok + 1 fail → succeeded: the existing first case of the file, unchanged.

packages/rest/src/import-row-cell-errors.test.ts: its async poll helper asserted succeeded for every job; the two one-row jobs whose only row fails (NO_NAME) now expect failed, per the ruling. A stale fixture, a direct consequence of this change.

Acceptance notes

  • The pins drive the route handlers registered on a real RestServer, not an HTTP transport, the harness the file's existing cases use.
  • The first full-suite run showed the late-cancel pin racing a fixed 50 ms wait under load (it read the cancel route's write before the worker's final patch); the pin now waits on the worker's results write instead. Re-run green in the full suite.
  • The sync door (POST /data/:object/import) has no status and is untouched.

Verification

Readings are at head 2fa11634 unless a line names another sha.

Red before the fix (e536326, tests only)
× every row fails: the job ends failed, errors === total, and error is set
× every row of a dry run fails: the job ends failed
-   "status": "failed",
+   "status": "succeeded",
Tests  2 failed | 19 passed (21)
Ablations (scripts/ablation-replace.mjs; each restored blob == HEAD, `git diff HEAD` empty)

The suites import ./rest-server from source, so no dist rebuild was involved.

  1. Delete if (everyRowFailed) finalStatus = 'failed'; → red: the two all-failed pins plus the two import-row-cell-errors polls (Tests 4 failed | 23 passed (27)).
  2. Predicate spelled summary.ok === 0 → red: "every row skipped keeps succeeded" (expected "succeeded", received "failed").
  3. Cancel guard spelled !summary.cancelled (ignoring the late re-checks) → red: the late-cancel control.
  4. && summary.total > 0 deleted → red: "a job with no rows keeps succeeded". (A first attempt with an empty --replacement was refused by the tool before running anything; re-run with --delete.)
Package suites and gates
  • pnpm --filter @objectstack/rest test: lock VERDICT command-exit 0, 342/342 files, 6701 passed, 326 skipped.
  • pnpm --filter @objectstack/rest typecheck: exit 0, 0 test-layer errors (at 138a620; the commit after it touches two test files that the full suite above compiled and ran).
  • eslint packages/rest --no-inline-config: exit 0.
  • dispatch-gates --commands: 59 families derived; results in the os-dev-report on rest: an async import job whose every row failed ends failed, not succeeded (ruled B; split from #27501 (b)) #27676. check:dual-build-cjs-loads exits 3 PREREQUISITE NOT MET (it reads every package's dist): NOT MEASURED locally, carried by CI Lint & Repo Gates.
  • No open PR touches data-action-routes.ts, import-job-integration.test.ts or import-row-cell-errors.test.ts (REST read of all 35 open PRs' files).
  • Dogfood battery ((narrowing) arm) at the final head 2c10836a: os-verify-lock VERDICT exit 0 (held 31m29s), 355 files passed and 3 skipped (358), 2867 tests passed and 20 skipped (2887), no failures (os-dev-report 6512268257). An earlier run at 2fa11634 had 2 failures, both the stale dogfood poll in packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts, which 2c10836a updates (it waited for succeeded on an all-rows-failed job); that file is added to the claim by amendment.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e00374a3488b2f517f6efcf86a097a9dda16ac8a → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json e00374a3488b2f517f6efcf86a097a9dda16ac8a

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head 2c10836 · 2026-10-30T01:00Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2c10836a274fe9447b920426a47ebc9490a6c599
Local-runs: none
Scope: packages/rest/src/data-action-routes.ts, packages/rest/src/import-job-integration.test.ts, packages/rest/src/import-row-cell-errors.test.ts, packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts, .changeset/27676-import-job-every-row-failed.md + packages/core/src/utils/import-runner.ts, packages/rest/src/rest-server.ts, packages/spec/src/api/export.zod.ts, packages/platform-objects/src/audit/sys-import-job.object.ts, ui/packages/plugin-grid/src/ImportWizard.tsx, packages/qa/dogfood/test/import-user-field-target.dogfood.test.ts, packages/rest/src/import-undo-controls.test.ts, packages/rest/src/import-control-opt-out-gate.test.ts, content/docs/data-modeling/import-mappings.mdx

Reviewed 2026-10-30T01:13Z against maintainer ruling B (record 6509036166, ruled 2026-10-29T21:42Z), the claim 6509513333 and its amendment 6512284153, the os-dev-report 6512268257, the PR body, the file list, the net diff against main at the head, and the head's check-runs. Read-only: nothing built, run or re-run.

① Derived judgments

  • Accept set: unchanged — right. No request shape, query parameter or enum moves. ListImportJobsRequestSchema.status and the sys_import_job.status select already carry failed; packages/spec and ui/ are untouched in the net diff (0 lines), so the docblock half stays spec: ImportJobStatus and ImportJobProgressSchema.error docblocks say failed is only a fatal abort; after #27676 B an all-rows-failed job also ends failed #27734's as the ruling's item 5 orders.
  • Public surface: one already-declared value is emitted in one more case — right. GET /data/import/jobs/:jobId, /results, the history list and the sys_import_job row now read failed, with error populated, for a job that was not cancelled, had rows, and created, updated or skipped none. Zero-row, all-skipped and partly-failed jobs stay succeeded; cancel still wins. This is the ruling's letter B and nothing wider.
  • Predicate fidelity — right. finalStatus === 'succeeded' && summary.total is positive && summary.created + summary.updated + summary.skipped === 0. Before that line finalStatus is one of cancelled / succeeded, so finalStatus === 'succeeded' is exactly the ruling's !cancelled after all three cancel resolutions (summary.cancelled, the in-memory cancelledImportJobs flag, the durable row re-read). summary.total is rows.length in the runner's snapshot; summary.ok is not read. Not ok === 0, as ruled.
  • The summary's field names mean what the ruling meant — verified in import-runner.ts. Every row lands in exactly one of created / updated / skipped / failed: skipped is NO_MATCH in update mode and BLANK_MATCH_KEY when skipping is asked, both ok: true; an upsert row is created or updated, never neither; a duplicate match key inside the file (DUPLICATE_MATCH_KEY) and an ambiguous match are failed, not skipped, which is the right side of the line (nothing written). The dry run's reportPreview counts a valid row as created / updated, so an all-invalid dry run reads failed and a valid one succeeded (ruling item 3). The runner's whole-batch refusal path (multiValueMatchKeyRefusal, every row refused before any read) also reads failed, correctly. Residual, pre-existing and not introduced here: a bulk write whose uncertain outcome exhausts its retries marks its rows failed although some may have landed — the counts already say created: 0 in that case, and the new error sentence states what the counts state.
  • error sentence — right. One sentence, points at results, no tracker number, written only when the predicate holds (the spread leaves the column null otherwise); importJobToProgress maps row.error onto the DTO's error, which the spec already declares for status = failed.
  • Readers, measured at the head. Undo: importJobUndoable admits succeeded / cancelled only, so a failed job is not undoable — and the same job was not undoable before either, since created + updated = 0 leaves the undo log empty; pinned (undoable: false, 422 NOT_UNDOABLE). Cancel route: acts on pending / running only. ?status=: pass-through on a declared member; pinned. Console wizard: importResultOutcome already answered failed for "nothing imported and something errored", and now also via jobStatus === 'failed' — same headline; the synthetic row fires only when importResult.errors.length === 0, and capImportResults keeps failures first under its 500-row cap, so an all-failed job's sample always carries a failure and no synthetic row is added; failed is in the closed terminal set and isSettledJobRead settles on the first failed read, which is complete because the worker writes status, counts and results in one patch; the history badge turns destructive — the intended signal; the wizard's isImportJobUndoable reads the server's undoable. No branch on succeeded in ui/apps, the objectui adapter or packages/client (typed pass-through). Other pollers: import-undo-controls.test.ts:197 and import-control-opt-out-gate.test.ts:166 import ok rows (Test Core green); import-user-field-target.dogfood.test.ts:90 returns only on succeeded but imports single rows it asserts ok: true, so it is unaffected.
  • Dogfood poll's strict assertion (some(r.ok) ? 'succeeded' : 'failed') is exact for that file's 4–6-row fixtures: results is complete below the cap and skipped rows are ok: true. As a general predicate it would be cap-sensitive on a job past 500 rows; not this file's case.

② Semver level

  • @objectstack/rest (published, 18.2.0) patch with the BREAKING banner and ! in the title — right. Metadata-protocol: none (no authorable key moves) takes patch; a non-protocol change, widening or breaking, ships patch and keeps its banner and ADR-0087 disposition (Post-Task Checklist step 3).
  • Clause-②: no (narrowing) — right. No schema moves (no); a consumer's succeeded promise narrows ((narrowing), BREAKING). Both declaration lines are line-initial in the PR body and the changeset body.
  • ADR-0087 not-required (no-migration-prescription) — honest. Nothing authorable or exported is removed, renamed or re-shaped; the changeset's remedy paragraph is reading guidance, not a FROM → TO rewrite under migration framing. Check Changeset and Lint & Repo Gates are green on the head.
  • Skills-docs: none — REASON — holds. No hand-written docs page or skills/** file states the async job's terminal-status vocabulary; import-mappings.mdx names the async route in one table row and documents per-row outcomes only; the generated reference carries the enum and is spec: ImportJobStatus and ImportJobProgressSchema.error docblocks say failed is only a fatal abort; after #27676 B an all-rows-failed job also ends failed #27734's.
  • Narrowing dogfood battery at the FINAL head: on record in os-dev-report dogfood_battery — 2c10836, os-verify-lock VERDICT exit 0, Test Files 355 passed / 3 skipped, Tests 2867 passed / 20 skipped, no failures, after the red at 2fa1163 (2 failures, the stale dogfood poll this head fixes). CI's Dogfood Regression Gate (n/3) shards are skipped on pull_request by design (ci.yml deny-list; dogfood-gate concludes success with zero attestations), so the local run is the battery of record for this head and the merge-group / main runs carry the full battery.

③ Boundary flags

  • Dev deviation 1 (two test files outside the claim's File surface): accepted — a red that is this change's consequence is a stale fixture fixed in the same PR (os-dev.md definition of done); the seat's amendment 6512284153 adds both files to the claim.
  • Dev deviation 2 (PR body opened at 2fa1163): the body now names the dogfood file, the battery line and the amendment — patched by the seat; nothing left stale.
  • Dev deviation 3 (late-cancel pin's wait moved from a fixed 50 ms to the worker's results write): judged in ④ — sound, one should-fix hardening.
  • Dev deviation 4 (model-free commit trailers): no bearing on the review.
  • open_questions: none declared; none found.
  • Out-of-scope, carried: the ImportJobStatus / ImportJobProgressSchema.error docblocks in export.zod.ts still say succeeded admits per-row errors and failed is a fatal error — spec: ImportJobStatus and ImportJobProgressSchema.error docblocks say failed is only a fatal abort; after #27676 B an all-rows-failed job also ends failed #27734 by the ruling's item 5; until it lands the generated reference lags the runtime, so the spec seat should land it in the same release train. Observed beside it, not blocking: sys-import-job.object.ts:70 labels the error column "Fatal Error", so the Setup grid titles this sentence "Fatal Error" — a label for the spec / platform-objects seat alongside spec: ImportJobStatus and ImportJobProgressSchema.error docblocks say failed is only a fatal abort; after #27676 B an all-rows-failed job also ends failed #27734.
  • Check-runs on the head, re-read by this posting act: 53 runs, 38 success, 15 skipped, 0 failed, none in progress — Governed Surface Queue Guard, Check Changeset (both runs, the second triggered by the body edit), Lint & Repo Gates, TypeScript Type Check, Build Core, Test Core (8/8), Spec property liveness, PR-body guards all success; the skipped set is the per-PR deny-list and the UI legs no ui/ path woke.
  • No fork head (head repo = base repo); no governed path in the file list; no packages/spec or ui/ path slipped in.

④ Design judgment

The narrowing itself, against the published spec on main: ImportJobStatus already declares failed and ImportJobProgressSchema.error already exists "when status = failed"; nothing declared is taken away at a consumer, and no consumer gains a fallback. What changes is the producer's rule at the one point where a one-field reader was told succeeded for a job that wrote nothing — the spec's own failed made real for that case. The spec's succeeded sentence ("rows may still have per-row errors") stays true because partial keeps succeeded; the failed sentence becomes narrower than the runtime and is #27734's to widen, as ruled. Not an undelivered capability narrowed at the consumer.

  • packages/rest/src/data-action-routes.ts — holds. The predicate sits after all three cancel resolutions so a late cancel wins (pinned); finalStatus is type-narrowed to the three terminals; error is written only when the predicate holds; counts, results, completed_at and the undo log are written as before; the thrown-worker catch is untouched; the sentence constant carries no tracker number (check:doc-authoring). Observation only: a dry run shares the sentence "nothing was imported", which is true of every dry run and not misleading.
  • packages/rest/src/import-job-integration.test.ts — holds. The ruling's five pins are present (all failed → failed with errors === total and error naming results; the existing 2 ok + 1 fail pin untouched; all skipped → succeeded; late cancel → cancelled; all-error dry run → failed) plus zero-row, valid dry-run, undo 422 and ?status=failed controls, on the file's real-engine harness. The late-cancel control is sound: the worker's IIFE runs synchronously to its first await inside the handler, before callCreate resolves, so the cancel always lands after the pending check and the worker runs; a 3-row job has no mid-loop checkpoint, so the in-memory flag's final re-check decides; waiting on the results write is the right signal because only the worker's final patch writes it; the dev's ablation (guard spelled !summary.cancelled) shows the pin bites. Should-fix (non-blocking): after the 400 × 5 ms wait, assert that the results read reached 3 rows, so an exhausted wait fails loudly instead of letting the cancel route's own cancelled write and the onProgress flush satisfy the assertions vacuously — the window is negligible today, the assertion makes the precondition explicit.
  • packages/rest/src/import-row-cell-errors.test.ts — holds. jobRows defaults to succeeded; the two single-NO_NAME-row jobs now expect failed; the [BAD_ROW, GOOD_ROW] callers keep succeeded, as the rule says.
  • packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts — holds. The poll returns on succeeded or failed, asserts the status matches whether any row is ok, and still refuses any other read that is not pending / running (a cancelled read fails as before).
  • .changeset/27676-import-job-every-row-failed.md — holds. patch, both declaration lines, BREAKING banner naming the ruling, ADR-0087 marker, before / now / unchanged-on-purpose / who sees it / remedy.

Must-fix: none. Should-fix: one (the late-cancel pin's loud-exhaustion assertion), non-blocking.

Implemented-by: claude/issue-27676-all-rows-failed-job
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

席位意见: ACCEPT。谓词逐字按裁定(不是 ok === 0),五条钉子齐全并多钉了零行作业;一条测试健壮性建议另开卡。

席位核对

ACCEPT · head 2c10836a274fe9447b920426a47ebc9490a6c599 · 2026-10-30T01:16Z

  • 形态: 5 files, +175/−7: the terminal-status predicate (data-action-routes.ts +21/−1), the ruling's pins, two stale pins updated (one rest test, one dogfood poll, added by claim amendment 6512284153), one changeset. No packages/spec, no ui/, no governed path; Tier S.
  • Clause-②: no (narrowing), Metadata-protocol: none, @objectstack/rest patch + BREAKING + !, ADR-0087 not-required (no-migration-prescription). Ruling-ref 6509036166 (maintainer, letter B). Contract review at CONTRACT_REVIEW_TIER (claim 6512295312), record 6512470441: PASS, no must-fix, 1 should-fix.
  • 前提实测: the two all-failed pins red before the fix (received succeeded); four ablations (predicate removed; ok === 0; guard spelled !summary.cancelled; total > 0 removed) each red on the pin it targets, restored to the HEAD blob.
  • 抽查读数: the review read every row path of the import runner into exactly one of created/updated/skipped/failed, and dry-run's preview counts, against the ruling's predicate.
  • 门禁 check-runs: 53 on this head: 38 success, 15 skipped, 0 other (read 2026-10-30T01:16Z); mergeable_state: clean. Derived gates 63 (62 exit 0, check:dual-build-cjs-loads carried by CI); rest 342/342 files; dogfood battery at 2c10836a 355 files / 2867 tests green (os-dev-report 6512268257).
  • 接受的偏差: the two stale test files outside the original claim (amended); the late-cancel pin now waits for the worker's own results write instead of a fixed 50 ms; the PR body was written before the dogfood fix and patched by the seat.
  • 范围外去向: should-fix 1 (assert the late-cancel wait completed) → rest: the async import late-cancel pin should assert its wait for the worker's results write actually completed #27839; the export.zod.ts docblocks → spec: ImportJobStatus and ImportJobProgressSchema.error docblocks say failed is only a fatal abort; after #27676 B an all-rows-failed job also ends failed #27734, with the sys_import_job.error "Fatal Error" label noted there (6512498465).
  • 散文面: changeset and the error sentence checked against the ruling by the review.
  • 落地条款: relay pr_ready + automerge_enable through the merge queue.

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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rest: an async import job whose every row failed ends failed, not succeeded (ruled B; split from #27501 (b))

1 participant