From 2019d7330ca003954ddb516a88d4a88e31c81c88 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 04:16:50 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(adr):=20ADR-0112=20amendment=20?= =?UTF-8?q?=E2=80=94=20error.code=20is=20closed=20at=20every=20door,=20dec?= =?UTF-8?q?laredCode=20is=20the=20open=20channel=20(#9106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the maintainer ruling of 2026-08-16 on #9106 as an amendment on D4, plus a Consequences bullet for the wire change it implies. The gap it closes is the one the card names as the deciding axis: an agent reading ADR-0112 today concludes error.code is closed and may write a consumer that switches exhaustively on it — wrong at exactly one door, in a way no gate reports. The amendment states the closure holds everywhere, names declaredCode as the open author-authored channel, fixes 'presence means demotion' as its semantics, records that #7867's sandbox passthrough is preserved rather than retired, names where the rule is pinned, and fences DUPLICATE off from the ledger. ⛔ Maintainer-merged per Prime Directive #14 — split into its own PR so the implementation can land through the normal path. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza --- docs/adr/0112-error-code-vocabulary-and-ledger.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/adr/0112-error-code-vocabulary-and-ledger.md b/docs/adr/0112-error-code-vocabulary-and-ledger.md index 20a9f9de97..19eba3c5d4 100644 --- a/docs/adr/0112-error-code-vocabulary-and-ledger.md +++ b/docs/adr/0112-error-code-vocabulary-and-ledger.md @@ -107,6 +107,20 @@ Nine rulings, D1–D9. **D4 — `ApiErrorSchema.code` stops being `z.string()`.** It becomes the generated union of the standard catalog and the registered ledger (`ErrorCode`, generated at build time — Zod-first per PD#1, enum generated from ledger + `StandardErrorCode`). Conformance suites thereby assert values for free. There is **no regex escape hatch** (see Alternatives — a casing regex passes hallucinated codes, which is precisely the AI failure mode this ADR exists to prevent). +> **Amendment (2026-08-17, [#9106](https://github.com/objectstack-ai/objectstack/issues/9106)) — `error.code` is closed at EVERY door, and `declaredCode` is the open, author-authored channel beside it.** D4 closed the *schema*; it did not say what a door does with a throw whose `.code` is not a member, and the two doors answered differently. The REST package door narrowed (an unregistered code fell to the code the status derives); the runtime dispatcher door put the producer's string on the wire verbatim. #8087 first ruled the verbatim spelling stays and delivered a gate over the platform producers (`pnpm check:dispatcher-error-vocabulary`) — and that gate's own first derivation measured the limb no registration can ever close: `SandboxError` carries a metadata app's **own** `.code` across the QuickJS boundary on purpose ([#7867](https://github.com/objectstack-ai/objectstack/issues/7867)), so `domains/actions.ts` served a code **authored by tenants, at runtime**, into `error.code`. `ERROR_CODE_LEDGER` cannot enumerate strings that do not exist when CI runs. +> +> **Ruled (maintainer, 2026-08-16).** `error.code` stays a closed vocabulary at every door. A thrown code that is not a member of `StandardErrorCode` union the serving side's ledger is **demoted** to a declared sibling field, `ApiError.declaredCode`, and `error.code` carries the member the HTTP status derives. One rule platform-wide, and the same one hotcrm#1075 states from the app side: **apps branch on enum codes; app-specific spellings ride `declaredCode`.** +> +> **The boundary, named.** `code` is closed — a consumer may switch on it exhaustively and that is now true at every door, which is exactly what it was not before this amendment. `declaredCode` is **open**: any string a producer spells, platform or tenant, and no ledger governs it. **Presence means demotion** — the field is absent when the producer's code IS a vocabulary member (it is already in `code`, and repeating it would put two spellings of one fact on every refusal) and absent when the producer declared none. So a consumer that reads `declaredCode` at all knows the serving side's ledger did not recognise the spelling. +> +> **The #7867 capability is preserved, not retired.** Narrowing the sandbox boundary — rejecting or namespacing an author's code before it reaches the wire — was the rejected alternative: it withdraws a capability this platform deliberately granted, to protect a property nothing had yet needed at that door. The author's code still crosses the sandbox and still reaches the wire; it lands in the open channel rather than the closed one. +> +> **Where it is pinned.** `resolveThrownHttpError` and `demotedDeclaredCode` (`packages/types/src/thrown-http-error.ts`, anchored in `scripts/adr-anchors/`) are the ONE definition of both spellings; all three dispatcher exits (`HttpDispatcher.errorFromThrown`, `dispatcher-plugin`'s `errorResponseBase`, `endpoint-executor`'s `endpointErrorAnswer`) read them rather than restating the rule. The demote is pinned end to end in `packages/runtime/src/domains/actions-validation-envelope.test.ts` (the actions door, with the tenant witness `DUPLICATE`), `packages/runtime/src/package-door-error-parity.test.ts` and `packages/runtime/src/error-envelope.conformance.test.ts` (which asserts, for every body the door emits, that a present `declaredCode` is never a vocabulary member and never a copy of `code`). +> +> ⛔ **`DUPLICATE` is deliberately NOT registered.** It is the pinned witness to the tenant-authored limb, re-homed under this rule rather than added to the ledger: registering one tenant spelling closes nothing (the next app picks a different string) and would promote a single app's vocabulary into the platform catalog every consumer branches on. It stays fenced off from the ledger-registration hand-off (#8846). +> +> **Scope.** This amendment rules the doors served by `resolveThrownHttpError` — the runtime dispatcher exits (the actions door among them) and the direct-mount REST package registrar. `packages/rest`'s flat `sendThrownError` dialect, which puts `code` at the body's **top level** rather than in `error.code`, is a different envelope position and is out of scope here; its convergence belongs to the envelope line (#7035), and `check:dispatcher-error-vocabulary` keeps its platform producers swept meanwhile. + **D5 — One location, eventually: `error.code` carries the semantic code.** Target end-state, recorded here so the follow-ups have a fixed destination: the HTTP status lives on the transport and (optionally) `error.httpStatus`; `error.code` is always the semantic string; `error.details.code` and `error.type` are retired as code carriers. The dispatcher-occupation fix (#3689 sibling) and `ROUTE_NOT_FOUND`-in-`type` retirement land as follow-ups (Rollout, batch 3). The client's three-location probe is deleted only after both. > **Amendment (2026-07-30, [#4007](https://github.com/objectstack-ai/objectstack/issues/4007)) — ruled and done.** With batch 3 landed, the client's parking-spot read (`error.details.code`) is deleted: SDK and server ship as a changesets fixed group, so the "newer SDK, older server" pairing it served is not a supported deployment — and batches 1–2 renamed the code *values* anyway, so a code dug out of an old server's parking spot would match no branch written against the current catalog; location-compat without value-compat protects nothing. The client's two remaining reads are the two *live* envelopes' declared spots (flat top-level `code`, wrapped `error.code`) — a present-tense fact, not a fallback chain; retiring the flat shape itself belongs to the envelope-convergence line (#3843 family), not this ADR. The D9b nesting fix (`category`/`retryable`, [#4006](https://github.com/objectstack-ai/objectstack/issues/4006)) landed in the same change. @@ -148,6 +162,7 @@ The line that decides all three of D6/D6b/D6c: **the catalog governs the code a - **New-code friction is introduced deliberately:** an unregistered code fails CI. This is the entropy gate; the ≈240-literal status quo is what its absence produces. - **Unblocked follow-ups:** the `error.code`-occupied-by-status fix; `error.type` retirement; deletion of the client's three-location probe and its explanatory comment block; the field-level vocabulary decision (D6's issue). - **Docs debt retired:** `error-catalog.mdx` regenerated; the two error-code items in the 2026-06 docs-accuracy audit close. +- **Breaking on the wire, for author-thrown codes only (added by the 2026-08-17 amendment):** a metadata app throwing its own `.code` from an action body used to see it echoed at `error.code`; it now arrives at `error.declaredCode`, with `error.code` carrying the closed member the status derives. The binding precondition the ruling attached — measure whether any existing consumer of the actions door branches on author-authored strings in `error.code` — was measured before landing and came back empty, twice, by two independent search shapes (the second widened to every SCREAMING_SNAKE literal precisely because a comparison-shape scan cannot see helper-mediated branches such as objectui's `errorCodeIs`). Registered platform codes are unaffected: they were, and remain, in `error.code` verbatim. ## Rollout From cafc7e7b55adeef7e586296bb42b8903f0fd7438 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 04:28:36 +0000 Subject: [PATCH 2/2] docs(adr): point the #9106 amendment's scope note at the live card (#9232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #7035 — cited by #9098's prose as the open envelope-position finding — is closed (PR #7293, three /meta 501 handlers). The flat sendThrownError door's remaining vocabulary question is filed as #9232; the amendment now names it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza --- docs/adr/0112-error-code-vocabulary-and-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0112-error-code-vocabulary-and-ledger.md b/docs/adr/0112-error-code-vocabulary-and-ledger.md index 19eba3c5d4..43433a38a5 100644 --- a/docs/adr/0112-error-code-vocabulary-and-ledger.md +++ b/docs/adr/0112-error-code-vocabulary-and-ledger.md @@ -119,7 +119,7 @@ Nine rulings, D1–D9. > > ⛔ **`DUPLICATE` is deliberately NOT registered.** It is the pinned witness to the tenant-authored limb, re-homed under this rule rather than added to the ledger: registering one tenant spelling closes nothing (the next app picks a different string) and would promote a single app's vocabulary into the platform catalog every consumer branches on. It stays fenced off from the ledger-registration hand-off (#8846). > -> **Scope.** This amendment rules the doors served by `resolveThrownHttpError` — the runtime dispatcher exits (the actions door among them) and the direct-mount REST package registrar. `packages/rest`'s flat `sendThrownError` dialect, which puts `code` at the body's **top level** rather than in `error.code`, is a different envelope position and is out of scope here; its convergence belongs to the envelope line (#7035), and `check:dispatcher-error-vocabulary` keeps its platform producers swept meanwhile. +> **Scope.** This amendment rules the doors served by `resolveThrownHttpError` — the runtime dispatcher exits (the actions door among them) and the direct-mount REST package registrar. `packages/rest`'s flat `sendThrownError` dialect, which puts `code` at the body's **top level** rather than in `error.code`, is a different envelope position and is out of scope here; it is measured and filed as [#9232](https://github.com/objectstack-ai/objectstack/issues/9232), and `check:dispatcher-error-vocabulary` keeps its platform producers swept meanwhile. Read that card before quoting "every door" at the flat dialect: this amendment rules the field `ApiError.code`, and #9232 is where the remaining question about the flat top-level `code` is recorded. **D5 — One location, eventually: `error.code` carries the semantic code.** Target end-state, recorded here so the follow-ups have a fixed destination: the HTTP status lives on the transport and (optionally) `error.httpStatus`; `error.code` is always the semantic string; `error.details.code` and `error.type` are retired as code carriers. The dispatcher-occupation fix (#3689 sibling) and `ROUTE_NOT_FOUND`-in-`type` retirement land as follow-ups (Rollout, batch 3). The client's three-location probe is deleted only after both.