Skip to content

feat(spec): declare ResumeFailureReport once and carry it on ApprovalRecallResult / ApprovalDecisionResult (batch #76) - #16640

Draft
os-trump wants to merge 2 commits into
mainfrom
claude/issue-16559-stranded-resume-details-contract
Draft

feat(spec): declare ResumeFailureReport once and carry it on ApprovalRecallResult / ApprovalDecisionResult (batch #76)#16640
os-trump wants to merge 2 commits into
mainfrom
claude/issue-16559-stranded-resume-details-contract

Conversation

@os-trump

@os-trump os-trump commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

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 runId of the run that is actually stranded, and repairable; 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 in packages/spec and 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's 400 FLOW_FAILED details) already landed its half in #16587.

What changed

Premise re-measured (rule 6)

The card measured on 0344f40 that the structure was not in spec. That was true then and is false on origin/main now: ResumeFailureDetailsSchema landed in 68437d4d9 (#16587, 12:15Z), which is NOT an ancestor of 0344f40 (merge-base --is-ancestor exit 1; control leg 621a487600344f40 exit 0, so the negative is sound). So the "declare fresh in contracts/ or promote packages/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 resumeError predicate — a search, not an enumeration

Search: grep -rn for resumeError and for resumed over the whole tree (.ts/.md/.mdx, excluding node_modules, dist, .turbo, .cache, CHANGELOG.md), plus not resumed / resumed: false / resumed is false spellings. Sites asserting or implying "resumeError only beside resumed: false": 12.

  • In spec (5): the four resumeError docblocks the card listed (recall, send-back, resubmit, decision) and the adjacent ApprovalDecisionResult.resumed docblock ("false here means … see resumeError"). Edited (4): recall and decision resumeError + both resumed docblocks — the two carriers the ruling names. Left (2): ApprovalSendBackResult / ApprovalResubmitResult resumeError — the ruling names no carrier on those doors, and their sentence stays true of the door's own run (see 验收备注).
  • Outside spec (7), all left, each true of what it describes: plugin-approvals/src/approval-service.ts:451 (plugin-local ApprovalContinuationResult.resumeError), :3009-3012 (the private resumeRecordedOutcome helper'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 buildVERDICT 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-error is real.
  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 on resume-failure-report.pin.test.ts, approval-service.test.ts, automation-api.zod.test.ts, automation-result-status.pin.test.tsTest Files 4 passed (4) · Tests 66 passed (66).
  • Reverse verification against the rebuilt dist (a scratch file under packages/types/src, tsc --noEmit --ignoreConfig, importing from @objectstack/spec/contracts): RED leg code: '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 2 TS2305, so the module resolved to the built .d.ts. Scratch file removed; git status clean.
  • Mutation leg for the prose pin (fix committed first): re-inserted "when resumed is 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 with git checkout HEAD -- path, git diff HEAD empty, git hash-object equals the HEAD blob (ea350ea7…) — byte-identical.
  • pnpm --filter @objectstack/spec check:generated✓ All 15 generated artifacts are up to date after --fix regenerated exactly the two it proved stale.
  • Gates: 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; --ran reconciliation: 75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN. 72 exit 0. check:doc-formula-expressions first answered exit 3 (PREREQUISITE NOT MET, formula/lint unbuilt), then exit 0 after building @objectstack/formula + @objectstack/lint. Two remain NOT MEASURED locally, exit 3 PREREQUISITE NOT METcheck:dual-build-cjs-loads (needs every package's dist/) and check:type-check-debt (--re-measure needs the 30-package built closure) — the full-farm runs are CI's.
  • pnpm check:nul-bytes exit 0 at the final commit; control-byte self-scan of the changed files: no hits.

维护者速读(草稿)

改了什么:在 packages/spec 的审批契约里新增一个导出结构 ResumeFailureReport(注册过的错误码 + 真正搁浅的 runId + repairable,status 沿用),并把它作为可选字段 resumeFailure 挂到 ApprovalRecallResultApprovalDecisionResult 上;同时把这两处 resumeError 的说明从「仅当 resumed 为 false」改成「只要告知了失败就有,与 resumed 无关」。结构不是新造的:它继承 #16587 已落地的 ResumeFailureDetails(自动化 resume 门 400 详情用的那份),只补一个成功信封上没有别处可放的 code

为什么改:决策批次 #76 已裁定(「同意」):resume 失败必须以机器可读的形状到达调用方,门的状态码不变。今天 recall 只给一段散文,decide 在子流程父 run 搁浅时什么都不说;#15556#15970 两张 services 卡要落地都先得有这个契约字段。本 PR 只做契约,不改任何门的行为。

风险与代价(含回滚):纯加法 —— 新导出 + 两个可选字段 + 文档说明;不铸新错误码(code 的类型就是 ADR-0112 台账,拼错编译即红)。对已有生产者与消费者零破坏(反向验证:未注册码被 .d.ts 拒绝、不带该字段的字面量照常通过)。@objectstack/spec minor。回滚 = revert 本 PR;此前没有任何门写这个字段,回滚无数据面影响。

席位意见:

你要做的:契约复审(Clause-②: yes,卡上已挂 needs:contract-review)后按常规入队;若认为 send-back / resubmit 两道门也该带同一字段(裁决未点名),请在卡上补一句裁决,我会另开一张卡承接。

验收备注

🤖 Generated with Claude Code


Generated by Claude Code

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

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 130 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 4d728680c0cd5fd1660aa1a57271bc7f084df559packageMentionDocs.

Which tree this was computed on

This run read content/docs from 99b2ab15a2e74de0020658bcb9c71c76fc0b6ecb — the merge of head 44eb05bf562b4d35a2d492054b0e4a35fac5c720 into base 4d728680c0cd5fd1660aa1a57271bc7f084df559, 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 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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant