diff --git a/.changeset/delivery-not-eligible-gloss-both-surfaces.md b/.changeset/delivery-not-eligible-gloss-both-surfaces.md new file mode 100644 index 0000000000..9cfc404898 --- /dev/null +++ b/.changeset/delivery-not-eligible-gloss-both-surfaces.md @@ -0,0 +1,36 @@ +--- +'@objectstack/spec': patch +--- + +`DELIVERY_NOT_ELIGIBLE`'s ledger gloss now describes every surface that raises it, not just `redeliver` + +The `ERROR_CODE_LEDGER` entry for `DELIVERY_NOT_ELIGIBLE` glossed the code as +*"delivery row is in a non-terminal state"*. That named one refusal on one +surface, and the code has since been reused on a second: `INotificationOutbox.ack` +refuses any row that is not `in_flight`, which covers an unclaimed `pending` row +**and** an already-terminal one. So the old wording was not merely incomplete — +it was backwards for half the code's uses, describing terminal rows as the +acceptable ones when `ack` refuses exactly those. + +The reuse itself is the ruled shape, not a defect: one concept — *this delivery +row's state does not permit the requested operation* — on two delivery surfaces, +with a second near-synonym code rejected for the vocabulary sprawl ADR-0112 +exists to prevent. Only the comment lagged. + +The gloss is now stated per surface, because the two refuse opposite halves of +the state space and no single status predicate covers both: + +- **`IHttpOutbox.redeliver`** (`HttpRedeliverError`) refuses a row that is NOT + terminal — `redeliver` means send this again, so it wants + `success`/`failed`/`dead`. It also raises the same code when the producer's + `RedeliverGuard` refuses or itself throws (fail-closed: "we could not check" + must never read as "allowed"), and when the terminal re-check at the write + misses because a dispatcher tick re-claimed the row mid-call. +- **`INotificationOutbox.ack`** (`NotificationAckError`) refuses a row that is + not `in_flight` — both the unclaimed `pending` row (the ack-as-cancel trap) + and the already-terminal one — plus the `SqlNotificationOutbox` compare-and-set + read-back that shows the claim was lost mid-ack. + +Comment only. No code is registered or removed, no wire value changes, and no +acceptance or refusal behaviour moves — `packages/spec` publishes +`src/**/*.zod.ts`, so the corrected gloss ships to consumers reading the ledger. diff --git a/packages/spec/src/api/error-code-ledger.zod.ts b/packages/spec/src/api/error-code-ledger.zod.ts index 84659682c9..ebf10d2d1a 100644 --- a/packages/spec/src/api/error-code-ledger.zod.ts +++ b/packages/spec/src/api/error-code-ledger.zod.ts @@ -510,7 +510,34 @@ export const ERROR_CODE_LEDGER = { ], '@objectstack/service-messaging': [ 'DELIVERY_NEVER_SENT', // [#8069] terminal delivery row with 0 attempts — a PARKED record of a delivery that could never be prepared, not one that failed. Redelivering it would be a FIRST send, and the row carries no HMAC signature because the secret that would have produced one is exactly what went missing, so it would go out unsigned (#7799). Distinct from DELIVERY_NOT_ELIGIBLE: that one says "wrong state, try when it settles"; this one says "never, fix the configuration instead" - 'DELIVERY_NOT_ELIGIBLE', // delivery row is in a non-terminal state + // "this delivery row's state does not permit the requested operation" — + // ONE concept on TWO delivery surfaces of this package, deliberately + // sharing one spelling (PR #11858's contract-review PASS ruled option B; + // a second near-synonym code was rejected for the vocabulary sprawl + // ADR-0112 exists to prevent). Stated per-surface because the two refuse + // OPPOSITE halves of the state space — no single status predicate glosses + // both, and the older "non-terminal state" wording named only the first: + // - `IHttpOutbox.redeliver` (`HttpRedeliverError`; the operator + // redeliver button) refuses a row that is NOT terminal — + // `redeliver` means send this AGAIN, so it wants `success`/`failed`/ + // `dead` (`assertHttpRedeliverable`). Also raised when the producer's + // `RedeliverGuard` refuses, or itself throws (fail-closed on purpose: + // "we could not check" must never read as "allowed"), and when the + // terminal re-check AT THE WRITE misses because a dispatcher tick + // re-claimed the row mid-call — both `SqlHttpOutbox` and + // `MemoryHttpOutbox` report that miss instead of a false success + // (#11009). + // - `INotificationOutbox.ack` (`NotificationAckError`; #11453) refuses a + // row that is not `in_flight` — which covers BOTH an unclaimed + // `pending` row (the ack-as-cancel trap) and an already-terminal one, + // because `ack` records the outcome of a delivery the caller CLAIMED. + // Also raised by `SqlNotificationOutbox` when its compare-and-set + // read-back shows the claim was lost mid-ack (a slow send outrunning + // `claimTtlMs`), so nothing was written. + // Distinct from DELIVERY_NEVER_SENT: this one says "wrong state for THIS + // operation, try when it settles"; that one says "never, fix the + // configuration instead". + 'DELIVERY_NOT_ELIGIBLE', ], '@objectstack/trigger-api': [ 'ENQUEUE_FAILED', // queue accepted the call but publish threw