fix(spec): register plugin-approvals as a second emitter of RESUME_FAILED - #17924
Conversation
…ILED `ResumeFailureReport.code` is a REQUIRED `ErrorCode` whose docblock prescribes `RESUME_FAILED`, but the provenance ledger listed that code only under `@objectstack/rest`. The first producer to fill the slot therefore stamps a registered code its own owner key does not list, and `check:error-code-provenance` refuses it — measured on the stamp site at `packages/plugins/plugin-approvals/src/approval-service.ts:3370`, which the guard names verbatim at exit 1. A ROW, not a waiver. The waiver precedent's predicate is "the door stamps this code itself for every throw and never reads the producer's declaration". Both halves fail here: `resumeFailure` rides a SUCCESS answer, which the REST approvals door serves with `res.json(out)` verbatim, and `packages/rest/src` spells `resumeFailure` nowhere. The producer's literal is the wire value. One code, not the three the docblock names: `RESUME_TARGET_LOST` is a thrown message prefix served under rest's row, and `RESUME_IN_PROGRESS` is compared but never constructed in this package and is emitted by `@objectstack/service-automation`, which carries its own row. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 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 523125478f956c994040d392e520c7fa87f5d974 && git checkout 523125478f956c994040d392e520c7fa87f5d974
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7135cf057e90dbe6d9177aef17e525da7277f68b 316703591d8b90a75083f7ac667dc9449ce2d84b && git checkout -B drift-repro 7135cf057e90dbe6d9177aef17e525da7277f68b && git merge --no-ff 316703591d8b90a75083f7ac667dc9449ce2d84b
node scripts/docs-audit/affected-docs.mjs --json 7135cf057e90dbe6d9177aef17e525da7277f68b
|
Fixes #17909
Clause-②: yes
packages/specdeclaredResumeFailureReport.code: ErrorCodeas required and prescribedRESUME_FAILEDin its docblock, but the provenance ledger listed that code only under@objectstack/rest. So the only correct implementation stamps a registered code its own owner key does not list, andcheck:error-code-provenancerefuses it. This adds the missing provenance row under@objectstack/plugin-approvals.The measurement the card declared NOT MEASURED
The card recorded its causal link as derived from the guard's stated rule, not from the guard's own output — a fresh worktree answered
tsx: not found. Dependencies installed, the guard run, and here is its own text.Before — branch base
5741ff10c30, ledger unchanged, PR #17908's stamp site injected from its head109c9ce13:VERDICT guard-exit=1. Exactly one violation, exactly the site the card names.After — this PR's row, same injected stamp site:
VERDICT guard-exit=0, and the listed count moves 318 to 319 — the site is admitted as listed, not waived.The card frames this as one code; it is three, and only one is stamped
The member's docblock names
RESUME_FAILED,RESUME_TARGET_LOSTandRESUME_IN_PROGRESS. Measured on the tree, not inherited:RESUME_FAILED@objectstack/rest(:287)approval-service.ts:3370(new in #17908). The row this PR adds.RESUME_TARGET_LOST@objectstack/rest(:288)approval-service.ts:3189); rest's catch maps that prefix. No row owed.RESUME_IN_PROGRESS@objectstack/service-automation(:1044) — not rest, correcting the dispatch's open premiseservice-automation/src/engine.ts:5795and:6165, which its own owner key already lists. plugin-approvals only compares it (:3304). No row owed.Why a row and not a waiver — the precedent's own predicate, applied
The
EXTERNAL_IMPORT_ERRORwaiver states the test plainly: the door's catch "stamps this code itself for EVERY importObject throw and never reads the producer's declaration — the door names the wire vocabulary." Both halves fail here, and the second decisively:resumeFailurerides a success answer. The REST approvals door servesdecide/recallwithres.json(out)(rest-server.ts), which copies the service's result verbatim — there is no envelope for the door to stamp on that path.git grep resumeFailure packages/rest/srcmatches nothing. The door neither stamps nor reads it. The producer's literal is the wire value.The door's
handleApprovalErrorcatch is a real second moment, and it is the throw path — already covered by rest's existing row. This PR does not touch it.⛔ The gate is not evaded. The literal stays exactly where it is; nothing is hoisted into a constant to dodge the textual scan.
Ablation and cost direction
Each leg mutates on disk, proves the mutation landed by occurrence count, runs the guard, and restores under a
trapverified bygit hash-objectagainst theHEADblob plus an emptygit diff HEAD. The guard is atsxscript importing../src/api/error-code-ledger.zod— source, nodistinterposed, so no build sits between the edit and the reading.exit=1,@objectstack/plugin-approvals stamps 'RESUME_FAILED' … at …:3370, listed back to 318RESUME_TARGET_LOSTinsteadexit=1, namesRESUME_TARGET_LOSTat:3290. The row admits exactly the one code measuredRESUME_FAILEDat two sitesexit=0, 320 listedDoes this unblock #15970?
Yes, conditionally, and the condition is measured rather than asserted. The guard reconciles on
(package, code), not on line number, so any number ofRESUME_FAILEDsites inside@objectstack/plugin-approvalsare admitted by this one row — the third leg above proves it with two sites.ApprovalRecallResult.resumeFailureis the same member type in the same package, so an implementation that stampsRESUME_FAILEDis covered with no further ledger edit.RESUME_TARGET_LOSTfor a lost run: the cost-direction leg is exactly that case, and it reds. That is the intended narrowness, not a gap — that code's wire emission belongs to rest's catch, and if a producer ever stamps it on a success answer it owes its own reading, not a pre-emptive row.Clause ② and the changeset level
Declared
Clause-②: yes, measured rather than argued:node scripts/pm/check-widening-tells.mjs --declaration noanswersexit=4with✗ T4 … a new registration in a registry / catalogaterror-code-ledger.zod.ts:1084; with--declaration yesit answersexit=0. The ledger's own header agrees — "Registering a code widens this face and is therefore a Clause-② change, door or no door." The changeset isminoraccordingly.RESUME_FAILEDwas already in the registered union under rest, so no response can carry a code it could not carry before — the per-package rows are provenance, not identity, as this file's header says. Theyesis the conservative direction, not a claim that the accept set grew.needs:contract-reviewis hung on this PR.Verification
pnpm --filter @objectstack/spec check:error-code-provenanceexit=0pnpm --filter @objectstack/spec test(--project local)exit=0— 473 files passed, 1 skipped; 13483 tests passed, 1 skippedpnpm --filter @objectstack/spec test:repo(--project repo)exit=0— 31 files, 523 tests passedpnpm --filter @objectstack/spec typecheckexit=0pnpm check:nul-bytesexit=0— 8541 text files scannedpnpm check:error-code-casingexit=0pnpm check:dispatcher-error-vocabularyexit=0check:liveness,check:duration-unit-keys,check:empty-state,check:variant-docs,check:llms-txt,check:skill-refs,check:strictness-ledger,check:yaml-examples,check:objectui-pin-citations,check:export-origins)exit=0|
pnpm --filter @objectstack/spec build+check:api-surface/check:authorable-surface/check:docs/check:exported-any/check:dual-source-exports/check:entry-nameability/check:browser-reachable-entries/check:generated| allexit=0— no generated artifact moves || derived gate sweep (
node scripts/pm/dispatch-gates.mjs --commands, all 78 families, reconciled with--ran) | 75exit=0, 3exit=3NOT MEASURED |The three NOT-MEASURED families are
check:doc-formula-expressions,check:dual-build-cjs-loadsandcheck:lean-entry-closure— each exits 3 withPREREQUISITE NOT MET, wanting a repo-widepnpm buildthis diff does not otherwise need. CI's Build Core supplies it; recorded as not measured, ⛔ not as green.Heavy steps ran through
scripts/pm/os-verify-lock.shon a shared box; verdicts are read from each command's own exit code captured before any pipe.维护者速读(草稿)
改了什么。
packages/spec/src/api/error-code-ledger.zod.ts里,在'@objectstack/plugin-approvals'的 owner key 下加了一行'RESUME_FAILED',附带记录 wire 路径的注释;外加一份minorchangeset。一个数据表条目,没有新导出符号,没有已发布载荷上的新键。为什么改。 spec 自己把
ResumeFailureReport.code声明成必填并在 docblock 里点名RESUME_FAILED,却没给会填它的那个包登记 provenance 行 —— 于是"照声明实现"这件事本身会被姊妹门禁check:error-code-provenance拒收。第一个来填槽的实现者只是第一个发现的人。这一行把声明补齐。风险与代价(含回滚)。 风险低:
RESUME_FAILED本来就在已注册并集里(挂在 rest 名下),这一行只改 provenance 归属,不让任何响应能带上它此前带不了的码。代价是一条minorchangeset(条款② 的机械判据命中 T4,按保守方向申报yes)。回滚 = 删掉这一行加这份 changeset,单文件 revert,无迁移、无数据、无 wire 影响。席位意见。 (留空,待席位定稿)
你要做的。 确认两件事:(1) 同意这里走"加行"而不是"加 waiver" —— 判据是先例那句可测的话("门自己 stamp 且从不读生产者的声明")在成功路径上两半都不成立,证据在上面的
res.json(out)与packages/rest/src零命中;(2) 条款② 申报为yes属于保守方向,而非真的放宽了接受集 —— 若你认为这类"已注册码的第二 emitter 行"不该触发契约复核,那是门禁 T4 的判据问题,值得单独立卡,⛔ 不在本 PR 处理。Unblocks
bubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556) — blocked on this; its only failing step was Error-code provenance guard.#15556 is not addressed here.recallwhose resume strands reports it as an ordinary non-failure — norepairablediscriminator, where the identical strand throughdecidecarries one #15970 — covered for aRESUME_FAILEDstamp, measured above.#15970 remains open.Generated by Claude Code