feat(spec): declare ResumeFailureReport once and carry it on ApprovalRecallResult / ApprovalDecisionResult (batch #76) - #16640
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
…ureReport (#16559) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 130 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 99b2ab15a2e74de0020658bcb9c71c76fc0b6ecb && git checkout 99b2ab15a2e74de0020658bcb9c71c76fc0b6ecb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4d728680c0cd5fd1660aa1a57271bc7f084df559 44eb05bf562b4d35a2d492054b0e4a35fac5c720 && git checkout -B drift-repro 4d728680c0cd5fd1660aa1a57271bc7f084df559 && git merge --no-ff 44eb05bf562b4d35a2d492054b0e4a35fac5c720
node scripts/docs-audit/affected-docs.mjs --json 4d728680c0cd5fd1660aa1a57271bc7f084df559 |
Fixes #16559
Clause-②: yes
The contract half of the #16472 family ruling (director seat, decision batch #76, 2026-09-07, maintainer verbatim 「同意」): a resume failure told to the caller is told as a registered error code, the
runIdof the run that is actually stranded, andrepairable; the door's status code does not move, so a success answer carries the failure behind it as an optional, additive member. This PR declares that structure once inpackages/specand adds the optional carrier member to the two approval results the ruling names. No door changes here — the producer halves are #15556 (decide, the subflow bubble-up) and #15970 (recall); #15221 (the resume door's400 FLOW_FAILEDdetails) already landed its half in #16587.What changed
ResumeFailureReport(packages/spec/src/contracts/approval-service.ts) — new exported interface, declared once by reuse: itextends ResumeFailureDetails(api/automation-api.zod.ts, the structure feat(runtime, spec): the resume door's 400 FLOW_FAILED details carry the engine's stranded verdict #16587 landed for the resume door's400 FLOW_FAILEDdetails, whose own docblock says it is "the structure the ruling asks to be declared ONCE … reused by the other carriers").runId,statusandrepairableare inherited, never re-spelled, so the carriers cannot drift and a consumer holdingResumeFailureDetailsSchemaparses the member and reads the same three facts. The report adds exactly the one member a success envelope cannot leave to its envelope:code.code: ErrorCode— the ADR-0112 ledger vocabulary, so an unregistered spelling failstsc. ⛔ No new error code is minted here (batch 🔗 Broken links detected in documentation #76: a new code is a ledger event per [Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landedno, #15963 landsyes, and they are the same class #16404); the fenced files (api/error-code-ledger.zod.ts,api/contract.zod.ts,api/dispatcher*) are read, never edited —git diff --statabove them is empty.ApprovalRecallResult.resumeFailure?andApprovalDecisionResult.resumeFailure?— the optional, additive carrier on both ruled approval results, with the absence rule explicit in each docblock: an absent member means no report was made, never that no run is stranded (the service-automation: a throw betweenjournalConsumedSuspensionand thestatus: 'stranded'stamp makes a REPAIRABLE strand reportrepairable: false— the operator is told not to attempt a repair that works #15555 false-negative class; the misreading [Decision]inspectStrandedRequestsnow over-reports: it keys onstatus === 'failed'while the platform gained an authoritative strand discriminator — a cascade-failed run the engine calls NOT stranded is reported as one #15358 measured).resumeErrorwidened on both carriers: no longer "whenresumedis false" — presence is decided by whether a failure was TOLD, never byresumed, andresumeFailureis named as the machine-readable half of the same telling. Bothresumeddocblocks now saytruespeaks for this door's own resume, not for every run behind it (service-automation: a subflow'sbubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556's parent strand rides aresumed: trueanswer).packages/spec/src/contracts/resume-failure-report.pin.test.ts: type-level identities compiled bycheck:test-typecheck(the report minuscodeISResumeFailureDetails;codeis exactlyErrorCodeand required; both carriers declare the member at the exact optional shape; an unmintedFLOW_STRANDEDis refused via a compiled@ts-expect-error), plus runtime pins (the wire schema parses a report and hands the three shared members back out; the docblock's example codes are registered today; bothresumeFailuredocblocks carry the absence sentence; both widenedresumeErrordocblocks say "never byresumed"; the retired predicate is gone from the file).api-surface/contracts.jsonandexport-origins/contracts.json(one added row each, viacheck:generated --fix); changeset@objectstack/specminor per the ruling.StrandedDecisionDetails(@objectstack/types, the error-envelope carrier ofPOST /api/v1/approvals/requests/{id}/rejectreturns 500 while its effect lands AND strands the workflow run — three inconsistent outcomes from one call #13807'sdecide-door strand) is untouched: it is the 500-arm's carrier and stays; the two share therunId/repairablevocabulary through the spec declaration, aspackages/runtime/src/domains/automation.tsalready documents.Premise re-measured (rule 6)
The card measured on
0344f40that the structure was not in spec. That was true then and is false onorigin/mainnow:ResumeFailureDetailsSchemalanded in68437d4d9(#16587, 12:15Z), which is NOT an ancestor of0344f40(merge-base --is-ancestorexit 1; control leg621a48760→0344f40exit 0, so the negative is sound). So the "declare fresh incontracts/or promotepackages/types" fork is superseded by a third answer: reuse the spec declaration that already exists. The card's ask (declare once, add the optional field to both approval carriers, widen the docblocks) stands unchanged.The
resumeErrorpredicate — a search, not an enumerationSearch:
grep -rnforresumeErrorand forresumedover the whole tree (.ts/.md/.mdx, excludingnode_modules,dist,.turbo,.cache,CHANGELOG.md), plusnot resumed/resumed: false/resumed is falsespellings. Sites asserting or implying "resumeErroronly besideresumed: false": 12.resumeErrordocblocks the card listed (recall, send-back, resubmit, decision) and the adjacentApprovalDecisionResult.resumeddocblock ("falsehere means … seeresumeError"). Edited (4): recall and decisionresumeError+ bothresumeddocblocks — the two carriers the ruling names. Left (2):ApprovalSendBackResult/ApprovalResubmitResultresumeError— the ruling names no carrier on those doors, and their sentence stays true of the door's own run (see 验收备注).plugin-approvals/src/approval-service.ts:451(plugin-localApprovalContinuationResult.resumeError),:3009-3012(the privateresumeRecordedOutcomehelper's own throw posture),packages/types/src/stranded-decision.ts:14(the 500-arm carrier), and four plugin test pins of today's behaviour (subflow-hosted-approval-strand.test.ts:257,decision-strand-envelope.test.ts:263,stranded-run-repairability.test.ts:270,restored-approval-continuation.test.ts:339) that the producer cards move when they land.Verification (final commit
44eb05bf5)pnpm --filter @objectstack/spec build—VERDICT command-exit 0(lock, 2m15s).pnpm --filter @objectstack/spec typecheck(tsc + scripts +check:test-typecheck) — exit 0;check:test-typecheck: OK — 54 file(s) / 261 error(s) / 145 pinned signature(s)unchanged, i.e. the new pin file compiles with zero debt and its@ts-expect-erroris real.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2onresume-failure-report.pin.test.ts,approval-service.test.ts,automation-api.zod.test.ts,automation-result-status.pin.test.ts—Test Files 4 passed (4) · Tests 66 passed (66).dist(a scratch file underpackages/types/src,tsc --noEmit --ignoreConfig, importing from@objectstack/spec/contracts): RED legcode: 'FLOW_STRANDED'→ exit 2,TS2322: Type '"FLOW_STRANDED"' is not assignable to type 'ErrorCode'; GREEN leg (RESUME_FAILED,status: 'stranded', a recall literal WITHOUT the member, a decision literal WITH it,ResumeFailureDetailsSchema.parse(report)) → exit 0; CONTROL leg (a symbol that does not exist) → exit 2TS2305, so the module resolved to the built.d.ts. Scratch file removed;git statusclean.resumedis false" into the recall docblock (on-disk count 1, anchor count 1), ran the pin file under the lock →VERDICT command-exit 1,× 4. the retired predicate is gone from the contract file; restored withgit checkout HEAD -- path,git diff HEADempty,git hash-objectequals the HEAD blob (ea350ea7…) — byte-identical.pnpm --filter @objectstack/spec check:generated—✓ All 15 generated artifacts are up to dateafter--fixregenerated exactly the two it proved stale.node scripts/pm/dispatch-gates.mjs --commands(no paths; derived from the merge base) → 75 families; every command run with its exit captured before any pipe;--ranreconciliation:75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN. 72 exit 0.check:doc-formula-expressionsfirst answered exit 3 (PREREQUISITE NOT MET, formula/lint unbuilt), then exit 0 after building@objectstack/formula+@objectstack/lint. Two remain NOT MEASURED locally, exit 3PREREQUISITE NOT MET—check:dual-build-cjs-loads(needs every package'sdist/) andcheck:type-check-debt(--re-measureneeds the 30-package built closure) — the full-farm runs are CI's.pnpm check:nul-bytesexit 0 at the final commit; control-byte self-scan of the changed files: no hits.维护者速读(草稿)
改了什么:在
packages/spec的审批契约里新增一个导出结构ResumeFailureReport(注册过的错误码 + 真正搁浅的runId+repairable,status沿用),并把它作为可选字段resumeFailure挂到ApprovalRecallResult与ApprovalDecisionResult上;同时把这两处resumeError的说明从「仅当resumed为 false」改成「只要告知了失败就有,与resumed无关」。结构不是新造的:它继承 #16587 已落地的ResumeFailureDetails(自动化 resume 门 400 详情用的那份),只补一个成功信封上没有别处可放的code。为什么改:决策批次 #76 已裁定(「同意」):resume 失败必须以机器可读的形状到达调用方,门的状态码不变。今天
recall只给一段散文,decide在子流程父 run 搁浅时什么都不说;#15556、#15970 两张 services 卡要落地都先得有这个契约字段。本 PR 只做契约,不改任何门的行为。风险与代价(含回滚):纯加法 —— 新导出 + 两个可选字段 + 文档说明;不铸新错误码(
code的类型就是 ADR-0112 台账,拼错编译即红)。对已有生产者与消费者零破坏(反向验证:未注册码被.d.ts拒绝、不带该字段的字面量照常通过)。@objectstack/specminor。回滚 = revert 本 PR;此前没有任何门写这个字段,回滚无数据面影响。席位意见:
你要做的:契约复审(
Clause-②: yes,卡上已挂needs:contract-review)后按常规入队;若认为 send-back / resubmit 两道门也该带同一字段(裁决未点名),请在卡上补一句裁决,我会另开一张卡承接。验收备注
ApprovalSendBackResult/ApprovalResubmitResultkeep "on the paths that tolerate it" — the ruling names no carrier on those doors, but the service-automation: a subflow'sbubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556 shape (child resumed, parent stranded) can occur through any door that resumes a subflow-hosted approval. 承接者: service-automation: a subflow'sbubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556's implementer, who drives that shape end to end; a ruling line is needed before those two results grow the member.resumed: truecarries noresumeError"; they are the producer cards' to move (service-automation: a subflow'sbubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556 / approvals: arecallwhose resume strands reports it as an ordinary non-failure — norepairablediscriminator, where the identical strand throughdecidecarries one #15970), not a defect.ApprovalContinuationResult.resumeError(plugin-approvals/src/approval-service.ts:451) is a plugin-local shape with the pre-ruling "set only on the tolerated non-failure" sentence; true today forcontinueRestoredRun. 承接者: none until a card touches that verb.🤖 Generated with Claude Code
Generated by Claude Code