Repository navigation
rest: an async import job whose every row failed ends failed, not succeeded (ruled B; split from #27501 (b)) #27676
Description
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Blocked-by: #27711
Ruled B by the maintainer (record 6509036166): execution card now. All rows failed ⇒ failed; the predicate, the five pins and the split spec docblock (#27734) are in the record. Built after #27711 merges (same file).
Filing gate: ② a product decision only the maintainer takes. Split out of #27501 ask (b) by triage answer 6498786930 ("changes a status that undo, the UI and clients read today, and no ruling covers it"), after this seat's retriage 6497991662 asked for the split. ⛔ Not a claim; (b) is not built until ruled. #27501 (a) — refuse an unregistered object with 404 before any job row exists — stays in scope there and does not wait on this card.
Business reason: an automated loader (a script, an agent, a migration) that polls a background import to its terminal state and branches on status === 'succeeded' is told "succeeded" today even when not one row was written. Who benefits from a ruling: every client of POST /api/v1/data/:object/import/jobs; without one, the (a) fix removes one producer of the case (a mistyped object) and leaves the ordinary one (every row refused by validation or mapping).
Reader who acts: the maintainer rules; then the domain:server seat (A/B/D) or the domain:spec seat (C) executes per "After the ruling".
Dedupe: the board's search endpoint is closed to this session, so the board was not searched; read instead: the parent thread (#27501, three records) and the tree corpus (check-prior-rulings.mjs --terms, 4 ADR hits, none on the job's terminal status). The triage answer states no ruling covers it.
维护者速读
- 改了什么:不改代码,问你一个口径。后台导入作业(
POST /data/OBJ/import/jobs)里每一行都失败、一条也没写进去时,作业状态该不该还叫succeeded。 - 为什么改:今天只要 worker 没抛错、没被取消,作业就记
succeeded,哪怕 0 行落库;唯一的失败迹象是行级错误计数。QA 探针(QA run · release-18.3.0-w3-tier1-b (89/89) · c4c515ad · 2026-10-29 · 79 PASS / 6 PARTIAL / 4 FAIL / 0 BLOCKED / 0 NOT-RUN #27438)把对象名写错,拿到 201 和一个succeeded的作业。自动化加载器只看状态就会信。 - 风险与回滚:A/D 零风险;B 只改「全部失败」这一种终态,部分失败照旧
succeeded,撤销、界面、现有 pin 都量过不受影响,回滚即还原一条规则;C 要加协议枚举值,老版 Console 的轮询是闭集、会等不到终态,客户端的 switch 也会漏。 - 先看:下面「Options」表和「Readers of the status, measured」表。
- 两分钟验证:
git show origin/main:packages/rest/src/data-action-routes.ts | sed -n 675,677p(终态只分 cancelled / succeeded);git show origin/main:packages/spec/src/api/export.zod.ts | sed -n 483,489p(枚举注释写明succeeded允许行级错误)。 - 需要你做的:选 A / B / C / D(推荐 B)。
Background
The async import door persists a sys_import_job, answers 201, and a background worker runs runImport (per-row; a failing row never stops the batch, as content/docs/data-modeling/import-mappings.mdx:268-283 documents). When the worker returns, the terminal status is computed in one line, packages/rest/src/data-action-routes.ts:675 on origin/main 85663a5:
let finalStatus = summary.cancelled ? 'cancelled' : 'succeeded';
failed is written only by the catch at :696, i.e. when the worker threw. The per-row counts (error_count from summary.errors, :689) are written beside the status either way. The synchronous door (POST /data/:object/import) has no status at all: its response is counts (ok, errors, created, …; ImportResponseSchema, packages/spec/src/api/export.zod.ts:454-465).
The contract declares today's behaviour on purpose. ImportJobStatus, packages/spec/src/api/export.zod.ts:483-489:
'succeeded', // Finished (rows may still have per-row errors)
'failed', // Aborted on a fatal error
The same five values are the sys_import_job.status select (packages/platform-objects/src/audit/sys-import-job.object.ts:48), and ImportJobProgressSchema.error is described as "Fatal error message (when status = failed)".
Governing text:
packages/spec/src/api/export.zod.ts:486(above): the declaration sayssucceededadmits per-row errors. Under 〈升级与决策〉"协议为基准", the current runtime matches its declaration, so this is a request to change the contract's meaning, not a defect.- AGENTS.md › Route & surface ownership §4: "Machine-readable surfaces must not lie … a wrong answer here propagates into everything built on top of it." Prime Directive chore: version packages #10: never advertise what the runtime does not deliver.
- AGENTS.md › Degradation log levels, the "third legal answer": "a batch whose contract IS a per-item outcome report, does not look normal from the outside — the requester was told." This is the text that defends today's design: the per-row report is the delivery.
- ADR-0119 D4 (nearest principle, not on point): "A response claiming
success: truefor a row that was rolled back is the bug, not merely a missing transaction." git grep -n -iE 'import job|ImportJobStatus|sys_import_job' origin/main -- AGENTS.md docs/adr packages/spec/src→ only ADR-0103 D5 (who may write the row) and the spec docblocks above; no ADR rules on the terminal status.
Protocol statement: A, B, D: Clause-②: no, Metadata-protocol: none (B changes when an already-declared value is emitted; no shape moves). C: Clause-②: yes (widening) on a published schema (ImportJobStatus, ListImportJobsRequestSchema.status, the platform object's select), Metadata-protocol: none (a DTO, not an authorable key); under the protocol-class rule the card re-grades to p1 + the in-development major's target: and moves to the spec lane.
Premises (each with its re-check)
- The terminal status is
succeededunless cancelled or thrown, with counts written beside it. Re-check:git show origin/main:packages/rest/src/data-action-routes.ts | sed -n 675,700p. - The declaration says so. Re-check:
git show origin/main:packages/spec/src/api/export.zod.ts | sed -n 483,489p. - Undo accepts only
succeededorcancelledjobs with a non-empty undo log. Re-check:git show origin/main:packages/rest/src/rest-server.ts | sed -n 977,983p. - The Console already derives a four-way outcome (
success | partial | failed | cancelled) from the counts plusjobStatus === 'failed', and keeps a closed terminal set. Re-check:git show origin/main:ui/packages/plugin-grid/src/ImportWizard.tsx | sed -n 494,504pand| sed -n 1127p. - Three in-repo pollers treat
succeededas "it worked". Re-check:git grep -n "status === 'succeeded'\|toBe('succeeded')" origin/main -- packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts packages/rest/src/import-undo-controls.test.ts packages/rest/src/import-control-opt-out-gate.test.ts.
The question
When a customer's background import finishes and not a single row was imported, should the job's one-word status still say it succeeded?
Options
| What it does | What the client sees | Effort (lane · PRs · precedent) | |
|---|---|---|---|
A Keep succeeded; make the outcome loud without a status change |
Sharpen the succeeded / failed / error docblocks and the import docs to say "status is the worker's lifecycle; the verdict is ok / errors / results" |
Unchanged wire. A one-field reader is still told succeeded on zero rows |
spec seat (docblock is a packages/spec touch; gen:docs) · 1 docs-only PR · no precedent needed |
B All rows failed → failed; partial stays succeeded |
At :675, a job with total_rows > 0 and every row ok:false ends failed, with error set to a sentence that says every row failed and points at results (no tracker number); cancel still wins; partial failure keeps today's succeeded |
A zero-effect job reads failed with per-row errors attached; a partly-successful job reads as today |
domain:server · 1 PR after #27501 (a) lands (same file) · nearest landed precedent on this family's semantics #24431 (+584/−48, 17 files, which also carried spec docblocks + UI + docs); B needs no UI change (reading 4) |
C Add a third terminal, e.g. partial, for "some rows failed"; all-failed → failed |
Widen ImportJobStatus, the list filter, the platform-object select; emit it at :675; admit it in importJobUndoable, the wizard's terminal set / badge map / 10 locale files / the satisfies pin, the dogfood poll, tests, docs |
Three honest terminals on the wire; every closed-set reader built before the widening breaks (table below) | domain:spec lead + server + ui in ONE PR (Type Check UI reds the spec widening until the UI member lands) · contract review CONTRACT_REVIEW_TIER · no measured enum-widening precedent in this clone (shallow; the history question is refused here) |
| D Close not planned | Nothing; #27501 (a) removes the producer that was observed | As today | zero |
Business meaning: A is a courier that reports "the run is finished" and staples the list of undelivered parcels to it; B reports the run "failed" only when not one parcel was delivered; C puts three stamps on the run — delivered / partly delivered / nothing delivered; D leaves the courier as is.
Readers of the status, measured on origin/main 85663a5
| Reader | What it does with succeeded vs a failure status |
A | B | C |
|---|---|---|---|---|
Undo route, packages/rest/src/rest-server.ts:977-983 (importJobUndoable), data-action-routes.ts:770 (422 NOT_UNDOABLE) |
Undoable only when succeeded or cancelled AND the undo log has entries |
— | — : an all-failed job wrote nothing, its log is empty, it is already not undoable | breaks unless partial is added here: every partly-successful job would lose Undo |
Cancel route, data-action-routes.ts:730-740 |
Acts only on pending / running |
— | — | — |
List filter ?status=, data-action-routes.ts:898-902; ListImportJobsRequestSchema.status enum, export.zod.ts:564 |
Passes the string through | — | — | enum widens too |
Console Import Wizard result screen, ui/packages/plugin-grid/src/ImportWizard.tsx:494-504 |
Derives its own outcome from counts: zero imported + errors → headline "Import failed" (ui/packages/i18n/src/locales/en.ts:839); some imported + errors → "Import finished with errors" (:838); jobStatus === 'failed' → failed |
— | — : it already says "failed" for this case; :2696 adds a synthetic row only when failed arrives with no per-row errors, which B does not produce |
the derivation stays (it also serves the sync door) |
Console history list, ImportWizard.tsx:2059-2065 (IMPORT_JOB_STATUS_VARIANT: Record<ImportJobStatus,…>), badge label grid.import.jobStatus.* (en.ts:905-911 and 9 more locales), error count already drawn in red beside it |
Badge colour and word per status | — | the badge turns red for an all-failed job | Type Check UI reds (missing record key) until the UI member lands; 10 locale files |
Console poll loop, ImportWizard.tsx:1127 (IMPORT_JOB_TERMINAL_STATUSES, closed set), :1159-1170 (isSettledJobRead), :2742-2770 (for (;;) until settled) |
Any status outside the closed set is "not finished" | — | — | an older Console polls a partial job every 800 ms forever and never shows a result |
UI type pin, ui/packages/types/src/__tests__/spec-derived-unions.test.ts:171-172 |
Pins the five members satisfies ImportJobStatus |
— | — | must be rewritten in the same PR |
Client SDK, packages/client/src/index.ts:7203-7256, :8076-8112; objectui adapter ui/packages/data-objectstack/src/index.ts:4936-4971 |
Typed pass-through, no branch on status | — | — | type only |
Tests: packages/rest/src/import-job-integration.test.ts:176 pins succeeded with errors: 1 (partial); :147, import-undo-controls.test.ts:197-198, import-control-opt-out-gate.test.ts:166 poll the closed set and assert succeeded |
Partial = succeeded is pinned |
— | — : partial unchanged; one new pin for all-failed | :176 flips; the three pollers need the member |
Dogfood, packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts:84-88 |
Returns results only on succeeded; asserts every other read is pending / running |
— | — in practice (its fixtures import cleanly); the assertion would fire on an all-failed fixture | fires on any partial job |
QA checklist records-forms.import-job-undo-cancel (docs/qa/platform-checklist/areas/records-forms.json) |
acceptance[0] "reaches a terminal state", unnamed; acceptance[2] already says "the job reports the cancelled/partial state" for the cancel case | — | — | wording already fits |
Docs / skills: content/docs/data-modeling/import-mappings.mdx:268-283, :316; generated content/docs/references/api/export.mdx:242; skills/objectui/guides/data-integration.md:69 |
No hand-written page states the status vocabulary; the generated reference carries the enum | one sentence | one sentence | regenerate + one sentence |
Outside this repo (cloud, hotcrm, customer loaders, older Console builds in packages/console/dist) |
not measurable here | — | a loader that treated zero-row succeeded as success now sees failed — the intended change |
closed-set clients break silently |
四维分析
os-decision-facets
- ① 项目长远合理性:两年后的样子是「导入是逐行作业(文档已定:一行失败不停整批),作业的一字状态是一个字段就能信的整体裁定,行级细节留在计数与
results」。参照:Salesforce Bulk API 2.0 的作业有失败记录仍到JobComplete,Failed只给作业级失败;BigQuery load 的坏行超过maxBadRecords容忍即整作业FAILED。B 只在「一行都没落」这一个一字段读者真会被骗的点改口径,不增枚举、不增键,特例最小;C 把界面自造的四态搬进协议,是契约增生,且界面的推导还得留着(同步门没有状态),所以 C 不消灭任何特例;A 把「成功」留给生命周期、把裁定推给读计数的人,与仓内三处读者的实际读法相反。 - ② 实际业务拉动:今天撞上的是 QA 探针 QA run · release-18.3.0-w3-tier1-b (89/89) · c4c515ad · 2026-10-29 · 79 PASS / 6 PARTIAL / 4 FAIL / 0 BLOCKED / 0 NOT-RUN #27438(对象名写错 → 每行
OBJECT_NOT_FOUND→succeeded),(a) 会关掉这个生产者;常态生产者是映射错列、必填全空这类「每行都被拒」的导入。板上无具名客户拉动(本会话搜不了看板,父线程无);仓内实测三处轮询者把succeeded读作「成功」(dogfood:86、undo-controls:198、opt-out gate:166)。零具名客户拉动按规则默认 D;拉动来自 ③。 - ③ 防 AI 犯错:AI 加载器的典型写法就是轮询到终态、
status === 'succeeded'即继续。B 让「零效果」响亮(failed+ 一句指向results的error);A 是消费端宽容形,出错时谁也看不到;C 最响亮,但新枚举值本身对每个 switch 闭集的生成客户端是陷阱(老 Console 永远轮询);D 保留静默形。 - ④ 创业阶段不扩散:B 一条规则、一个文件、一个 pin、一句 docblock,无新键、无新门禁;C 的枚举扩张是永久义务(spec、平台对象、server、UI 十个 locale、文档、测试)且破坏闭集读者;A 只动文档;D 最省。不荐新增门禁。
Prior rulings read: import job,importjobstatus,sys_import_job,succeeded,per-row → 4 hits; ADR-0027 Decision §13, ADR-0077 Decision §2, ADR-0103 D5, ADR-0119 D4 (none rules on the job's terminal status; ADR-0119 D4 is the nearest principle); thread: 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 (6495291748, 6497991662, 6498786930); repo: objectstack-ai/objectstack — board search unresolved (endpoint closed to this session).
Recommendation
B — recommendation only; the ruling is the maintainer's. One line: it is the single point where a one-field reader is actually misled (nothing landed), and it costs no shape change and breaks no measured reader — partial jobs keep succeeded, so the :176 pin, Undo, the wizard and the dogfood poll all stand.
Not A: it leaves the silent shape, and the three in-repo pollers show readers do treat succeeded as "it worked". Not C: a widening that breaks three measured closed-set readers and ships a permanent member for a state every reader can already derive from counts (the Console does). Not D: (a) removes one producer of the case, not the case.
Confidence gaps, stated: (1) the board could not be searched from this session for a prior ruling; the parent thread and the tree corpus were read. (2) External clients (cloud, hotcrm, customer loaders) are not measurable here. (3) Dry-run jobs: under B a validate-only job whose every row is invalid would also read failed (dryRun: true stays visible on the DTO); recommend the same rule, to be ruled in the same ruling. (4) The predicate: "every row failed" = total_rows > 0 and created + updated + skipped === 0 (equivalently errors === total, since the counts partition the rows); the PR names it.
Self-check: 只看①选 B;②③④ 是否翻转:否(② 零具名客户拉动指向 D,但 ③ 的三处仓内读者实测把 succeeded 读作成功,维持 B;④ 不新增门禁)。
After the ruling
- A: the spec seat lands a docs-only PR (the
succeeded/failed/errordescribes +gen:docs, one sentence inimport-mappings.mdx);Clause-②: no,Metadata-protocol: none; this card closes on merge. - B:
domain:serverdispatches it 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) lands (same file, behind this seat's rest: GET /data/OBJ/export silently answers an unknown format (pdf, xml, empty) with a 200 CSV instead of refusing it #27500 / data: POST /api/v1/data/:object/:id/clone accepts anoverridesthat is not an object (a string) and answers 201 with the overrides silently ignored #27489 fold). The PR: the rule atdata-action-routes.ts:675plus theerrorsentence (no tracker number in the runtime string); a pin inimport-job-integration.test.ts— all rows failed →failed,errors === total,errorpresent; CONTROL: 2 ok + 1 failed stayssucceeded(:176untouched), cancel still wins; the docblock sentences inexport.zod.ts(apackages/spectouch → the spec seat co-signs or it is split per the server lane's red line) and one sentence inimport-mappings.mdx. Changesetpatch,Clause-②: no,Metadata-protocol: none, the semantic change stated in the changeset body; contract reviewCONTRACT_REVIEW_TIERif the spec docblock moves, elseTIER_DEFAULT. Ruled in the same ruling: dry-run jobs follow the same rule (recommended), and the predicate in gap (4). - C: the card moves to
domain:spec,priority:p1+ the in-development major'starget:(protocol class); one PR spec + server + ui (Type Check UIforces it): the three enums,:675,importJobUndoable,IMPORT_JOB_TERMINAL_STATUSES,IMPORT_JOB_STATUS_VARIANT, 10 locale files, thesatisfiespin, the dogfood poll,:176, docs regenerated;Clause-②: yes (widening),Metadata-protocol: none, contract reviewCONTRACT_REVIEW_TIER; the changeset names the closed-set break for older clients. - D: this card closes not planned; 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) continues unchanged.
Related
#27501 (parent; (a) in scope there) · triage 6495291748 · retriage 6497991662 · answer 6498786930 · QA run #27438 (anchor #27377) · #27500 / #27489 (same-file fold ahead of (a)) · #24431 (precedent: last semantics change on the import-job family) · ADR-0103 D5 · ADR-0119 D4.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Independent review (fable)
Read at
origin/main33ab543; the card's pinned 85663a5 is reachable in this clone and every cited file is byte-identical between the two. Chinese review file:/mnt/project-files/issue-27676/director-review-27676.md. Recorded 2026-10-29T17:33Z. ⛔ Not a ruling; a check and a recommendation.Verified readings
data-action-routes.ts:675, cancel wins:676-682, counts:689,catch→failed:694-698; enum commentsexport.zod.ts:483-489,errordescribe:533, list filter enum:564; platform selectsys-import-job.object.ts:47-48;importJobUndoablerest-server.ts:977-983; the 422 text atdata-action-routes.ts:771already names "nothing was written".ImportWizard.tsx:494-504(:490-491: all rows deliberately skipped = success), closed set:1127,isSettledJobRead:1159-1170,for (;;):2742at 800 ms (:525) with only a consecutive-error cap, so an unknown non-terminal status does poll forever;:2696synthetic row; badge map:2059-2065;satisfiespinspec-derived-unions.test.ts:171-172; three in-repo pollers readsucceededas success (dogfood:84-88, undo-controls:197-198, opt-out gate:166);:176pins partial =succeeded; SDK and objectui adapter pass the string through.packages/console/distis gitignored, so "older Console builds" means the published npm package: not measurable here, implied by the source loop.total(import-runner.ts:1142-1147), sototal > 0 && created + updated + skipped === 0⇔errors === total. But the aggregateokcounts only created / updated (:1359,:1516,:1534) and NOT skipped rows, which areok: true(:1459-1472). ⛔summary.ok === 0would mark an all-skippedupdate-mode jobfailed, against the wizard's own rule; pin "all rows skipped →succeeded" as a CONTROL. Dry run:reportPreviewcounts valid rows as created / updated (:1101-1124), so an all-invalid dry run readsfailedunder B, consistent with gap (3). Cancel:summary.cancelledis tested first, so the rule runs only on the non-cancelled branch. Zero rows:import-prepare.tsrefuses only> maxRows(:668) and a body with no payload (:580); a header-only CSV builds atotal_rows: 0job (data-action-routes.ts:586) that endssucceeded0/0, and B'stotal > 0guard keeps it so. Rule it in the same stroke (recommend: keep).filing-gate.md: a spec acceptance / declaration surface change, wider or narrower, is p1 + the in-development major'starget:) holds; fix(rest)!: an import undo runs under the object's declared controls for every caller but the system principal #24431 is +584/−48, 17 files, merged; B'sClause-②: noholds under the mechanical boundary test (no wider accept set, no wider public surface).Corrections
en.tslines: "Import failed" is:847, "Import finished with errors" is:846(card: 839 / 838);jobStatus.*is:913-919(card: 905-911). Same lines at 85663a5. The strings themselves are as described.lanes/server.md:6red line reads "触packages/spec的卡一律归 spec 席", andSKILL.md:163gives describe / JSDoc prose todomain:spec. "The spec seat co-signs" is not a form the rule offers. B's threeexport.zod.tssentences (succeeded,failedcomments;errordescribe) are a separate spec-seat PR (+gen:docs), or the whole card moves todomain:spec.degradedto the job status vocabulary (#7072) #7340 (Job status vocabulary has nodegradedvalue — #5548's services half cannot compile or persist without wideningJobExecutionStatus+ the twosys_job*selects #7072) widenedJobExecutionStatus+ the twosys_job*selects + translations, +148/−12, 10 files, with the UI label / colour / filter as a separate objectui follow-up (nowui/is in-tree, so the card's one-PR claim for C stands); ADR-0044 D1 addedApprovalStatus: 'returned'with "dual-source enums, all updated together". Both are C's measured shape.content/docs/references/api/export.mdx:242-250lists the five members without the comments, and no hand-written page states thatsucceededadmits per-row errors. B retracts no published statement.JobCompletewith every record failed (an A shape); BigQuery fails the whole load abovemaxBadRecords, default 0 (stricter than B). B is the minimal threshold form (100 %). Not re-checked online in this session.Governing texts the card did not cite
success? Ruled no: "an audit face that reports success for a run that accomplished nothing is the declared ≠ enforced shape applied to observability". The option "document that status means did-not-throw" (this card's A) was rejected: "legalizes silence on a machine-readable audit surface, against the standing rule that fail-open or fail-closed can be ruled but silence cannot". The option "make it a failure" (this card's B) was rejected there only becauseIJobServicethird-party implementations key retries on throw. Chosen: one additional outcome, "Vocabulary stays minimal … ⛔ Do not open an enum family". Landed via spec: JobHandler 增加可选的「跑完但没干成」回报通道 —— #5548 裁决(B-minimal)的契约半边,可加性,Promise<void> handler 零改动 #6617, Job status vocabulary has nodegradedvalue — #5548's services half cannot compile or persist without wideningJobExecutionStatus+ the twosys_job*selects #7072 / PR feat(spec,platform-objects): adddegradedto the job status vocabulary (#7072) #7340, PR fix(service-job,service-automation): map a degraded job outcome to sys_job_run.status instead of success (#5548) #7446; text atpackages/spec/src/contracts/job-service.ts:83-85,packages/spec/src/system/job.zod.ts:476-509,sys-job-run.object.ts:59-60. Bearing here: A and D are the shape DbJobAdapter 把「handler 没抛错」记成 sys_job_run.status='success' —— 内部自行降级的 job(如 wait 唤醒打空)在作业审计面上仍显示成功 #5548 rejected; B is DbJobAdapter 把「handler 没抛错」记成 sys_job_run.status='success' —— 内部自行降级的 job(如 wait 唤醒打空)在作业审计面上仍显示成功 #5548's verdict applied to a family whosefailedhas no retry coupling (terminal, not undoable,errorcarries text, the wizard already distinguishes at:2696), so the one reason that forced a third member there is absent;degraded's "reason rides the existingerrorcolumn" matches B'serrorsentence.check-prior-rulings.mjssearches ADR decision lines only and cannot see this ruling (re-run withdegraded,terminal stateadded: 20 hits, all term recall).buildBatchDataResponsesetssuccess: failed === 0(protocol-batch-data.ts:528-537); the spec example "Partial Success Response (atomic=false)" carriessuccess: false(batch.zod.ts:253-256). So the repo has three per-item verdict conventions today (batch: any failure = not success; sync import: counts, no verdict; async import: lifecyclesucceeded). B does not unify them and fixes only the one lie; the ruling should say that partial-as-succeededis kept on purpose, so the batch precedent does not reopen this.Recommendation (a recommendation; the ruling is the maintainer's)
B. Axis ① first: a job's one-word status should be a verdict one field can be trusted on; #5548 already ruled that "ran, nothing accomplished" may not read as success; the import family's
failedis free of the retry coupling that forced a third value there; C pays a permanent enum member and three measured closed-set breaks for a state every reader can derive from counts; A / D keep the shape #5548 rejected. Three lines for the ruling so nothing comes back: (1) predicate!cancelled && total > 0 && created + updated + skipped === 0⇒failedwith anerrorsentence pointing atresults; dry run same rule; zero rows staysucceeded; partial stayssucceeded, on purpose. (2) Execution: a server PR (the rule, the evaluation order, five pins: all failed →failedwitherrors === totalanderrorset; 2 ok + 1 failed →succeeded,:176untouched; all skipped →succeeded; cancel still wins; all-invalid dry run →failed) plus a spec-seat PR for the three docblock sentences andgen:docs, or the card moves todomain:specwhole. (3) The ruling comment names #5548 as its precedent.Facets, independently re-derived
os-decision-facets
results;DbJobAdapter 把「handler 没抛错」记成 sys_job_run.status='success' —— 内部自行降级的 job(如 wait 唤醒打空)在作业审计面上仍显示成功 #5548 已为同族裁定「跑完但什么都没干成」不得读作成功。B 只在这一个一字段读者真会被骗的点改口径,不加枚举、不加键;C 为一个所有读者都能从计数推出的状态付出永久枚举 + 三处实测闭集读者破坏;A/D 是 DbJobAdapter 把「handler 没抛错」记成 sys_job_run.status='success' —— 内部自行降级的 job(如 wait 唤醒打空)在作业审计面上仍显示成功 #5548 否掉的「记成文档、留着静默」形状。OBJECT_NOT_FOUND→succeeded),(a) 关掉这个生产者;常态生产者是映射错列、必填全空的整批被拒。板上无具名客户拉动;仓内实测三处轮询者把succeeded读作成功(dogfood:86、undo-controls:198、opt-out gate:166),批量门的裁定已是「任一失败即 false」。status === 'succeeded'即继续。B 让零效果响亮(failed+ 一句指向results的error),与 DbJobAdapter 把「handler 没抛错」记成 sys_job_run.status='success' —— 内部自行降级的 job(如 wait 唤醒打空)在作业审计面上仍显示成功 #5548「silence cannot be ruled」同向;A 是消费端宽容形;C 最响亮但新枚举值对每个闭集 switch 的客户端是陷阱(老 Console 永远轮询);D 保留静默形。degradedto the job status vocabulary (#7072) #7340 +148/−12 10 文件再加 UI 半边);A 只动文档;D 最省。不荐新增门禁。Prior rulings read: import job,importjobstatus,sys_import_job,succeeded,per-row,degraded,terminal state,partial → 20 hits; ADR-0044 D1 (third-terminal form only), ADR-0119 D4 (nearest principle), none rules on this job's terminal status; thread: 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 (6495291748, 6497991662, 6498786930) · DbJobAdapter 把「handler 没抛错」记成 sys_job_run.status='success' —— 内部自行降级的 job(如 wait 唤醒打空)在作业审计面上仍显示成功 #5548 ruling (5224023201, 5225117584) · Job status vocabulary has no
degradedvalue — #5548's services half cannot compile or persist without wideningJobExecutionStatus+ the twosys_job*selects #7072 / PR feat(spec,platform-objects): adddegradedto the job status vocabulary (#7072) #7340; repo: objectstack-ai/objectstackReply line for the maintainer: 「#27676 选 B」(含上面三条);or 「#27676 选 C」(then in #5548's form: one value, minimal vocabulary; not recommended); or A / D (contradicts #5548; say why the import family is the exception).
Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Ruling: needs-user-decision · letter B · maintainer 「27676 B」 (2026-10-29T21:42Z, project ruling thread 「待你裁决的问题」) · recorded by the seat 2026-10-29T21:44Z
The maintainer (Sales) ruled B in the project's ruling thread (https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuAAoT2LLBar7yUSvgs2Mr7J). The ruling thread wrote nothing to GitHub; this comment is its record, posted by the
domain:server#2seat through the relay.Marker: maintainer-ruled. The recommendation (B) and the independent review6503865890(agrees B) are on this card above.The question
A job of the async import door (
POST /data/:object/import/jobs) whose every row failed readssucceededtoday.succeeded; sharpen the docs only.failed; partial failure stayssucceeded. ← ruledpartial).Execution parameters (settled in one pass)
packages/rest/src/data-action-routes.ts, thefinalStatuscomputation):!cancelled && total > 0 && created + updated + skipped === 0⇒failed, else today's rule. Notok === 0: a skipped row is not counted inok.erroris set to one sentence saying every row failed and pointing atresults, with no tracker number.succeededon 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.failed).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,errorpresent; 2 ok + 1 fail →succeeded(the existing partial pin is unchanged); all skipped →succeeded; cancellation still wins; all-error dry-run →failed.packages/spec/src/api/export.zod.ts, theImportJobStatus/ImportJobProgressSchema.errorsentences, plusgen:docs) goes to thedomain:specseat as its own PR, not in the server PR.5224023201,5225117584), the batch-door convention the review named.四维分析
os-decision-facets
succeeded读作成功。failed和指向results的一句话。Prior rulings read: import job,importjobstatus,sys_import_job,succeeded,per-row → 4 hits; ADR-0027 Decision §13, ADR-0077 Decision §2, ADR-0103 D5, ADR-0119 D4 (none rules on the job's terminal status); thread: 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 (6498786930); repo: objectstack-ai/objectstack.
Next: the seat files the spec docblock card and moves this card to execution (
pm:queue,Blocked-by: #27711).[-][Decision] async import job: a job whose every row failed reads `succeeded` today — keep it, flip only that case to `failed`, or add a `partial` status (split from #27501 (b))[/-][+]rest: an async import job whose every row failed ends `failed`, not `succeeded` (ruled B; split from #27501 (b))[/+]objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Claim: PM loop round 4
Session:
session_01L6bhgUCizoYWQVJWjhdR6FAccount:
os-salesBranch:
claude/issue-27676-all-rows-failed-jobWorktree:
objectstack-issue-27676Domain:
domain:serverSeat:
domain:server#2File surface:
packages/rest/src/data-action-routes.ts(the async import job's terminal-status computation only),packages/rest/src/import-job-integration.test.ts,.changeset/*; ⛔ nopackages/spec(the docblock is #27734) — stop on breach and explain in the reportContainer & model:
S,mode:subagent,model: default (dispatch-gates --tier: no path-derived mandate),effort: mediumClause-②: no (narrowing)
Metadata-protocol: none
Responsibility:
rest async import job terminal status (this lane) | maintainer ruling B 6509036166 | every client that polls an import job to its terminal statusThread-read: 6509036166
Ruling-ref: 6509036166 (maintainer, letter B)
Premise: holds (read on main
81d00a4e2026-10-29T22:11Z: the terminal status issummary.cancelled ? 'cancelled' : 'succeeded'atdata-action-routes.ts:765;failedis written only when the worker throws)Serial constraints cleared: #27711 (same file) merged; no open PR edits these files.
objectstack-fleet commentedon Oct 30, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
os-dev-report
{ "issue": 27676, "status": "done", "branch": "claude/issue-27676-all-rows-failed-job", "pr": "https://github.com/objectstack-ai/objectstack/pull/27795", "head": "2c10836a", "session": "session_01L6bhgUCizoYWQVJWjhdR6F", "premise_still_valid": true, "summary": "Ruling B (record 6509036166) built at the async import worker's terminal status in packages/rest/src/data-action-routes.ts. The existing cancel resolution is unchanged and runs first (summary.cancelled, then the in-memory cancelledImportJobs flag, then the durable row re-read). Only a status still 'succeeded' after all three is tested: summary.total > 0 && summary.created + summary.updated + summary.skipped === 0 makes it 'failed', and the final patch then also writes error = one sentence that every row failed and that each row's reason is in the job's results (no tracker number). Field mapping: ruling cancelled = the resolved status (summary.cancelled + the two late re-checks); total = summary.total (rows.length); created/updated/skipped = summary.created/updated/skipped (ImportRunSummary extends ImportProgress, packages/core/src/utils/import-runner.ts); summary.ok is not read. Dry runs take the same path. No packages/spec change (#27734). Readers measured: importJobUndoable needs a non-empty undo log, so a zero-write job was and stays not undoable (pinned: undoable false, undo 422 NOT_UNDOABLE); the cancel route acts on pending/running only; the ?status= filter passes through (pinned: listed under status=failed); the Console wizard already reads nothing-imported-plus-errors and jobStatus failed as 'failed', and its synthetic row needs a failed job with no per-row errors, which this never produces. No docs page states the old rule (git grep of content/docs outside references/releases and skills/: 0 hits), so Skills-docs: none. Two stale fixtures, direct consequences, were updated in this PR: import-row-cell-errors.test.ts (its job poll asserted succeeded for one-row all-failed jobs) and the dogfood import-reference-exposure poll (a withheld-target job fails every row); the dogfood one is now strict: failed exactly when no row is ok.", "mechanism_assumptions": "Held. The terminal line was at :765 on 81d00a4e as the claim said; ImportRunSummary carries total/created/updated/skipped/errors/ok/cancelled, and ok excludes skipped rows, as the review said.", "tests": "Red first at e5363269 (tests only): the all-failed and all-failed dry-run pins red (status expected failed, received succeeded), Tests 2 failed | 19 passed (21). Green: import-job-integration + import-row-cell-errors 27/27 at 2fa11634. Ablations through scripts/ablation-replace.mjs, suites import ./rest-server from source (no dist involved), each restored blob == HEAD (2e6727ca4e85) with git diff HEAD empty: (1) delete `if (everyRowFailed) finalStatus = 'failed';` -> 4 red (both all-failed pins + both import-row-cell-errors polls), Tests 4 failed | 23 passed (27); (2) predicate spelled summary.ok === 0 -> 'every row skipped keeps succeeded' red; (3) guard spelled !summary.cancelled (ignores the late re-checks) -> the late-cancel control red; (4) `&& summary.total > 0` deleted -> 'a job with no rows keeps succeeded' red. The first attempt at (4) used an empty --replacement; the tool refused it before running anything (no-op, declared), re-run with --delete. Package: pnpm --filter @objectstack/rest test at 2fa11634 (lock VERDICT command-exit 0): 342/342 files, 6701 passed, 326 skipped; the later commit 2c10836a touches only the dogfood test. pnpm --filter @objectstack/rest typecheck and pnpm --filter @objectstack/dogfood typecheck at 2c10836a: exit 0 (lock VERDICT command-exit 0, with the dogfood import-reference-exposure file 11/11). eslint packages/rest --no-inline-config: exit 0.", "gates": "dispatch-gates --commands at 2c10836a: 63 families. 61 exit 0 in the loop; check:dts-closure exited 1 once while the dogfood closure build was rewriting packages/metadata/dist (declarations missing mid-build), re-run after the build: exit 0, 219/219 declaration files; check:type-check-debt timed out in the loop waiting on the lock, re-run under os-verify-lock: VERDICT command-exit 0, OK 123/126 packages, re-measure 0 entries; check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs every package's dist), NOT MEASURED locally, carried by CI Lint & Repo Gates. --ran: 63 derived, 62 run, 1 NOT-MEASURED, 0 UNRUN. PR check-runs at 2c10836a: 49 runs, 36 success, 13 skipped, 0 failure; Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Build Core success, Governed Surface Queue Guard success, Check Changeset success. Open-PR overlap: none of the 35 open PRs touches data-action-routes.ts, import-job-integration.test.ts or import-row-cell-errors.test.ts (REST read of each PR's files), so the merge-tree check has no pair. origin/main merged once (138a6202) before pr_create.", "dogfood_battery": "2c10836a · os-verify-lock VERDICT command-exit 0 (held 31m29s, waited 6m03s; closure build + pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 355 passed | 3 skipped (358) · Tests 2867 passed | 20 skipped (2887) · no failures. Earlier run at 2fa11634 was red: 2 failures, both test/import-reference-exposure.dogfood.test.ts 'jobs: a withheld target answers a cell naming its record as one naming none' (admin, member): its poll returned only on succeeded and the all-failed job now reads failed. Fixed in 2c10836a (stale fixture, consequence of this change), then the full battery re-ran at the final head as above.", "line_budget": "5 files, +175 / -7 vs the merge base with origin/main (data-action-routes.ts +21/-1, tests +134/-6, changeset +19); under the 3000-line human-merge threshold", "files_changed": [ "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" ], "deviations": [ "File surface: the claim names data-action-routes.ts, import-job-integration.test.ts and .changeset/*; two stale fixtures outside it were updated because this change made them red (packages/rest/src/import-row-cell-errors.test.ts, packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts). The seat should add both to the claim's file surface. No packages/spec touch.", "PR body is written once and was opened at 2fa11634, before the dogfood fixture fix: it does not list import-reference-exposure.dogfood.test.ts and its Verification section points at this report for the battery. Suggested seat edit: add under What changes / Tests: 'packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts: the async poll now returns on succeeded or failed and asserts failed exactly when no row is ok (stale fixture, consequence of this change).' and the battery line from dogfood_battery.", "The late-cancel pin first raced a fixed 50 ms settle under full-suite load; it now waits on the worker's own results write (2fa11634). Declared in the PR body.", "Harness-written Co-Authored-By trailer with a model name was not used; commits carry the model-free pair." ], "mcp_calls": "0", "api_writes": "2 — relay pr_create (repository_dispatch -> fleet-write run 40973934714 -> POST /repos/objectstack-ai/objectstack/pulls, draft, no assignees, no reviewers, no labels sent); this os-dev-report via scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/27676/comments). git push is not counted.", "open_questions": [], "out_of_scope_findings": [ "carrier: #27734 · the ImportJobStatus / ImportJobProgressSchema.error docblocks in packages/spec/src/api/export.zod.ts still say succeeded admits per-row errors and failed means a fatal error; spec seat card, noted, not filed", "none other" ] }objectstack-fleet commentedon Oct 30, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Claim amendment to
6509513333· 2026-10-30T00:59Z · File surface increment, declaredThe dev's os-dev-report
6512268257names two test files outside the claim's File surface that this change made red, updated in the same PR as.claude/agents/os-dev.mdrequires (stale pins are fixed where the behaviour changes). The claim's File surface adds them:packages/rest/src/import-row-cell-errors.test.ts(an existing pin that readsucceededfor an all-rows-failed job)packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts(a dogfood poll that waited forsucceededon an all-rows-failed job)Everything else in the claim is unchanged.
objectstack-fleet commentedon Oct 30, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
ACCEPT → PR #27795 · head
2c10836a· 2026-10-30T01:17Z · landing sent (ruling B6509036166; contract review PASS6512470441). Follow-ups: #27839 (test robustness), #27734 (spec docblocks, spec lane).