diff --git a/packages/rest/src/analytics-filter-refusal-envelope.test.ts b/packages/rest/src/analytics-filter-refusal-envelope.test.ts index df2c1f8a5a..d8009ac1c5 100644 --- a/packages/rest/src/analytics-filter-refusal-envelope.test.ts +++ b/packages/rest/src/analytics-filter-refusal-envelope.test.ts @@ -444,7 +444,10 @@ describe('[#5352] reading the envelope did not turn every failure into a 400', ( }); it('a HALF envelope (4xx status, no code) is not honoured — this route invents no code', async () => { - // ADR-0112's point is that the PRODUCER names the condition. A status with + // ADR-0112 D4's point is the semantic-CODE channel: `error.code` is closed + // at every door and the producer's own spelling rides `declaredCode` + // beside it — the producer names the condition ON THE CODE AXIS. The ADR + // rules no HTTP status for an undeclared throw. A status with // no code is a producer bug; answering it with a code chosen here would be // the consumer-side leniency the ADR exists to remove, and would hide the // bug behind a plausible wire shape. diff --git a/packages/rest/src/error-response.ts b/packages/rest/src/error-response.ts index 3336302fc0..9d29f08fb8 100644 --- a/packages/rest/src/error-response.ts +++ b/packages/rest/src/error-response.ts @@ -391,8 +391,19 @@ function declaredHttpStatus(error: any): number | undefined { * of `declaredCode` means demotion, exactly as `ApiErrorSchema.declaredCode` * documents for the nested envelope. * - the producer spelled NO string code → `{}`. Nothing is invented: ADR-0112 - * says the PRODUCER names the condition, so a half-declaration is honoured - * for the half that was declared. That is the answer this file already gave + * D4 governs the semantic-CODE channel — `error.code` closed at every door, + * the producer's own spelling honoured beside it as `declaredCode` — so a + * half-declaration is honoured for the half that was DECLARED. + * + * ⚠️ Axis, stated once here because the other sites echo this file: the + * ADR rules the CODE. It does not rule what HTTP status an undeclared + * throw deserves, and "the PRODUCER names the condition" is this file's + * own prose for the code rule — the sentence does not appear in + * ADR-0112 and must not be cited as though it ruled the status axis. + * Read unqualified it was taken for a status ruling once already, and + * argued for a `500` on a contract question the ADR is silent on. + * + * That is the answer this file already gave * (see the 5xx arm of {@link mapDataError}) and it is deliberately preserved * — narrowing the vocabulary must not start ADDING codes to bodies that * carried none. @@ -920,9 +931,14 @@ function classifyDataError(error: any, object?: string): { status: number; body: // empty string from landing on the wire as an ADR-0112 code. // // A 5xx with NO code passes its status through carrying no code at all, - // deliberately: ADR-0112 says the PRODUCER names the condition, so a + // deliberately: ADR-0112 D4 governs the semantic-CODE channel — the + // producer names the condition on the CODE axis (see the + // `resolveThrownHttpError` docblock above for why the phrase is this + // file's own prose and not an ADR quotation) — so a // half-declaration is honoured for the half that was declared and - // nothing is invented for the half that was not. That is the answer + // nothing is invented for the half that was not. The status half is + // kept on #5582/#7525's rule, not the ADR's: ADR-0112 rules no HTTP + // status here. That is the answer // `resolveErrorResponse` already gives the same shape // (`rest-5xx-message-sanitization.test.ts` §"a dynamically-assigned // status is treated identically"), and inventing `INTERNAL_ERROR` here diff --git a/packages/rest/src/rest-5xx-message-sanitization.test.ts b/packages/rest/src/rest-5xx-message-sanitization.test.ts index 2b08febac0..25945dc756 100644 --- a/packages/rest/src/rest-5xx-message-sanitization.test.ts +++ b/packages/rest/src/rest-5xx-message-sanitization.test.ts @@ -379,8 +379,10 @@ describe('[#5437] the 5xx envelope: status and code survive, the prose does not' expect(res.statusCode).toBe(500); expect(res.body.error).toBe(INTERNAL_ERROR_MESSAGE); - // No `code` was declared, so none is invented here — ADR-0112 says the - // PRODUCER names the condition. + // No `code` was declared, so none is invented here — ADR-0112 D4 + // governs the semantic-CODE channel (the producer names the condition + // on the CODE axis; the ADR rules no HTTP status for an undeclared + // throw). expect(res.body.code).toBeUndefined(); expect(loggedText('does not exist')).toBe(true); }, 60_000); diff --git a/packages/rest/src/rest-5xx-status-passthrough.test.ts b/packages/rest/src/rest-5xx-status-passthrough.test.ts index f4512f59c9..ba865d8387 100644 --- a/packages/rest/src/rest-5xx-status-passthrough.test.ts +++ b/packages/rest/src/rest-5xx-status-passthrough.test.ts @@ -214,9 +214,12 @@ describe('[#5582] a 5xx that declares no code passes its status and invents noth // This is the exact shape the two pins this issue named were written // on, and the exact shape `resolveErrorResponse` already answers this // way ("a dynamically-assigned status is treated identically (no code - // declared)", `rest-5xx-message-sanitization.test.ts`): ADR-0112 says - // the PRODUCER names the condition, so the half that was declared is - // honoured and the half that was not is left empty. Inventing + // declared)", `rest-5xx-message-sanitization.test.ts`): ADR-0112 D4 + // governs the semantic-CODE channel — the producer names the condition + // on the CODE axis — so the half that was declared is + // honoured and the half that was not is left empty. The 502 itself + // passes through on #5437/#5582's rule, NOT the ADR's: ADR-0112 rules + // no HTTP status for an undeclared throw. Inventing // `INTERNAL_ERROR` here would put a code on the wire nobody wrote — // and overwriting the 502 with a 500 would re-derive a declared status // from message text, which is what #5437 ruled against one door over. diff --git a/packages/rest/src/rest-hook-refusal-code-parity.test.ts b/packages/rest/src/rest-hook-refusal-code-parity.test.ts index f2f530d416..3ad54a1dcb 100644 --- a/packages/rest/src/rest-hook-refusal-code-parity.test.ts +++ b/packages/rest/src/rest-hook-refusal-code-parity.test.ts @@ -343,7 +343,9 @@ describe('[#10345] the sandbox unwrap is the branch, and it was never status-sha describe('[#10345] the pinned defaults the fix must not disturb', () => { it('a refusal that declares NO code still carries none — nothing is invented', () => { - // ADR-0112: the PRODUCER names the condition. Narrowing or widening the + // ADR-0112 D4 governs the semantic-CODE channel — the producer names + // the condition on the CODE axis, and the ADR rules no HTTP status for + // an undeclared throw. Narrowing or widening the // vocabulary must never start ADDING codes to bodies that carried none. const r = mapDataError(sandboxRefusal('month-end close is in progress'), 'crm_account'); expect(r.status).toBe(400); diff --git a/packages/rest/src/rest-hook-refusal-status-passthrough.test.ts b/packages/rest/src/rest-hook-refusal-status-passthrough.test.ts index 80faf20194..195a044dc4 100644 --- a/packages/rest/src/rest-hook-refusal-status-passthrough.test.ts +++ b/packages/rest/src/rest-hook-refusal-status-passthrough.test.ts @@ -380,9 +380,18 @@ describe('[#7525] a hook that refuses WITHOUT a status is unchanged', () => { expect(r.status).toBe(500); expect(r.body.code).toBe('INTERNAL_ERROR'); - // Its own `code` is NOT promoted to the wire by this fix: ADR-0112 says - // the producer names the condition, and this producer named a code but - // no status. Inventing a 4xx from the code alone would be the + // Its own `code` is NOT promoted to the wire by this fix: ADR-0112 D4 + // governs the semantic-CODE channel — the producer names the condition + // on the CODE axis — and this producer named a code but + // no status. + // + // ⚠️ Do not read the 500 above as an ADR-0112 ruling. It is the + // classifiers' default (#7525); ADR-0112 rules no HTTP status for an + // undeclared throw. The unqualified form of this sentence was read as + // ruling exactly that, and argued for a `500` on the sibling + // `/analytics/dataset/query` question the ADR is silent on. + // + // Inventing a 4xx from the code alone would be the // consumer-side leniency Prime Directive #12 removes — the follow-up // that belongs with #7463, not here. expect(r.body.code).not.toBe('CLOSE_PERIOD_LOCKED'); diff --git a/packages/rest/src/rest-thrown-code-vocabulary.test.ts b/packages/rest/src/rest-thrown-code-vocabulary.test.ts index 2cd0b1488b..dd619492ab 100644 --- a/packages/rest/src/rest-thrown-code-vocabulary.test.ts +++ b/packages/rest/src/rest-thrown-code-vocabulary.test.ts @@ -250,7 +250,9 @@ describe('#9232 §3 — the halves that must not move', () => { expect(status).toBe(arm.status); expect(body.code).toBeUndefined(); expect(body.declaredCode).toBeUndefined(); - // ADR-0112 says the PRODUCER names the condition, so a + // ADR-0112 D4 governs the semantic-CODE channel — the producer + // names the condition on the CODE axis, and the ADR rules no HTTP + // status for an undeclared throw — so a // half-declaration is honoured for the half that was declared. // Narrowing the vocabulary must not start ADDING codes to bodies // that carried none. diff --git a/packages/spec/src/shared/external-errors.ts b/packages/spec/src/shared/external-errors.ts index 33e3900b92..81b92068a8 100644 --- a/packages/spec/src/shared/external-errors.ts +++ b/packages/spec/src/shared/external-errors.ts @@ -44,8 +44,13 @@ export type ExternalErrorCode = * * ## Why HERE and not in a REST error map * - * ADR-0112: the PRODUCER names the condition. This repo's HTTP exits already - * agree on how to read one — `status` then `statusCode`, 400-599 — in + * The producer that knows the condition declares its own wire shape. For the + * STATUS half — which is what this table decides — that is this repo's + * convergent HTTP-exit convention, NOT an ADR-0112 ruling: ADR-0112 governs + * the semantic-CODE channel (cited correctly above for the ledgered `code`, + * D3) and rules no HTTP status for a throw. This repo's HTTP exits already + * agree on how to read a declared status — `status` then `statusCode`, + * 400-599 — in * `mapDataError`'s `declaredHttpStatus` (#7525), `resolveErrorResponse` * (#5437/#5582), `HttpDispatcher.errorFromThrown` (#3867), * `dispatcher-plugin.errorResponseBase`, `endpoint-executor`,