Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/17909-approvals-resume-failed-provenance.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 20 additions & 0 deletions packages/spec/src/api/error-code-ledger.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading