diff --git a/.changeset/17909-approvals-resume-failed-provenance.md b/.changeset/17909-approvals-resume-failed-provenance.md new file mode 100644 index 0000000000..3c0f172539 --- /dev/null +++ b/.changeset/17909-approvals-resume-failed-provenance.md @@ -0,0 +1,15 @@ +--- +"@objectstack/spec": minor +--- + +`@objectstack/plugin-approvals` is now registered as a second emitter of the already-registered `RESUME_FAILED` in `ERROR_CODE_LEDGER`, so the only correct implementation of `ResumeFailureReport.code` stops being refused by `check:error-code-provenance`. + +**The contradiction this closes.** `ResumeFailureReport` (`contracts/approval-service.ts`) declares `code: ErrorCode` as **required** — "a success answer has no envelope `code` to fall back on" — and its docblock prescribes `RESUME_FAILED` for a run that could not be advanced. But the ledger listed that code only under `@objectstack/rest`, so the first producer to fill the slot stamped a registered code its own owner key did not list, which the provenance gate refuses. The declaration shipped in a state where satisfying it tripped a sibling gate. + +**Measured, not derived.** With PR #17908's stamp site present and the ledger unchanged, the guard answers exit 1 and names it: `@objectstack/plugin-approvals stamps 'RESUME_FAILED' (objlit) at packages/plugins/plugin-approvals/src/approval-service.ts:3370 — not listed under its own owner key`. With this row, the same tree answers exit 0 with the site counted as listed. + +**A row, not a waiver — the precedent's own predicate decides it.** The `EXTERNAL_IMPORT_ERROR` waiver records "the door stamps this code itself for every throw and never reads the producer's declaration". Both halves fail for `resumeFailure`: it 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, so the door names no vocabulary to waive it under. + +**One code, not the three the docblock names.** `RESUME_TARGET_LOST` is a thrown message prefix mapped by rest's catch and stays under rest's row; `RESUME_IN_PROGRESS` is compared and never constructed in this package, and is emitted by `@objectstack/service-automation`, which carries its own row. A row for a code the package does not stamp would be the dead weight this file's gate refuses. + +⛔ **No wire byte moves and no accept set widens.** `RESUME_FAILED` was already in the registered union, so no response can now carry a code it could not carry before; the per-package rows are provenance, not identity. No exported symbol is added and no published payload gains a key. diff --git a/packages/spec/src/api/error-code-ledger.zod.ts b/packages/spec/src/api/error-code-ledger.zod.ts index 1f0992e9a6..c19c00ba9e 100644 --- a/packages/spec/src/api/error-code-ledger.zod.ts +++ b/packages/spec/src/api/error-code-ledger.zod.ts @@ -1062,6 +1062,26 @@ export const ERROR_CODE_LEDGER = { '@objectstack/plugin-approvals': [ 'FORBIDDEN', 'RECORD_LOCKED', + // [#17909] Second EMITTER of the code — `@objectstack/rest` already + // registers it for the THROW path, where the approvals door's + // `handleApprovalError` catch maps a `RESUME_FAILED:`-prefixed message to + // a 500 whose body names that same code, and the producer's declaration is + // never read. This row is the OTHER moment, and it is not that one: the + // `resumeFailure` member `ResumeFailureReport` declares + // (`contracts/approval-service.ts`) rides a SUCCESS answer, where the door + // has no envelope to stamp — `decide` / `recall` are served by + // `res.json(out)` (rest-server.ts), which copies the service's result + // verbatim, and `packages/rest/src` spells `resumeFailure` nowhere. So the + // wire value IS this package's own literal, reachable end to end (the + // #8035 test), and the door names no vocabulary here to waive it under. + // One code, not three: the member's docblock also names + // `RESUME_TARGET_LOST` and `RESUME_IN_PROGRESS`, and this package stamps + // neither in a scanned position — `RESUME_TARGET_LOST` is a thrown message + // prefix served under rest's row, `RESUME_IN_PROGRESS` is compared, never + // constructed, and is emitted by `@objectstack/service-automation`, which + // carries its own row. A row for a code this package does not stamp would + // be the dead weight this file's gate refuses. + 'RESUME_FAILED', ], '@objectstack/plugin-security': [ // [#7474] `controlled_by_parent` declared with no `master_detail` relation.