ADR 0105 — The error-code map is partial, and the registry is complete
Context
ADR 0026 introduced STITCH_ERROR_STATUS and, with it, a promise: a consumer’s
map is Record<StitchErrorCode, AppCode>, so “a missing or renamed code becomes
a TypeScript error, not a silent 500”.
That promise has the wrong sign. The set of framework codes grows in ordinary
releases — seven FILE_* codes in 0.56.0, APPLICATION_NOT_ACCEPTING after
that. Under an exhaustive map every one of those additions stops a consumer’s
build on an additive release, which is precisely what the caret is supposed
to make safe. 0.56.1 changed codeMap to Partial<…> for that reason and the
reversal was recorded only in a changelog line: ADR 0026 still promised the
compile error, and the shipped JSDoc on codeMap still said “Exhaustive”
directly above the Partial type — visible to any consumer on hover, because
JSDoc travels in the .d.ts.
Meanwhile the registry itself was incomplete, which is the failure that
actually costs something. Five codes the framework throws — WAIT_TIMEOUT,
WAIT_FAILED, DOWNLOAD_NOT_FOUND, VIEW_HTTP_ERROR,
OPERATION_NOT_SUCCEEDED — were absent from it. For those isStitchErrorCode
answered false, so createErrorHook skipped both the codeMap lookup and the
unmappedCode fallback: the code reached the wire in stitchkit’s spelling as
though the project had thrown it. A consumer who had mapped “every framework
code” was silently missing five and had no way to find out.
Decision
The map a consumer writes is partial; the registry the framework keeps is complete.
ErrorHookConfig.codeMapisPartial<Record<StitchErrorCode, TWireCode>>. A code left out is not an error — it falls through tounmappedCode, which is where a project decides what an unknown framework code should look like on its wire. Adding a framework code is therefore additive for every consumer.STITCH_ERROR_STATUScontains every code the framework throws, including the managed runtime-tool codes that reach a caller through a tool result rather than an HTTP response. They are still codes stitchkit authored, and remapping them is exactly what the registry is for.- Including the codes of adapters stitchkit ships for optional peers —
GRAMMY_WEBHOOK_NOT_ACCEPTINGis in the registry. ADR 0002 keeps a domain model out of the core; the registry is not one. It is the list of codes this framework authors, and its only consumer-facing use is deciding what each one looks like on someone’s wire. Leaving an adapter’s code out does not keep a provider name out of a consumer’s concern: it makesisStitchErrorCodeanswerfalse, socreateErrorHookskipscodeMapandunmappedCodeand the code reaches the wire spelledGRAMMY_WEBHOOK_NOT_ACCEPTING— more visible out than in, and unmappable besides. - Completeness is held by a check, not by review:
packages/core/tests/error-registry-completeness.test.tsscans everynew AppError('CODE'inpackages/core/src— and everysuper('CODE'inside a class that extends it, which is how the two branded adapter errors are written — and fails on one the registry does not carry. It foundOPERATION_NOT_SUCCEEDED, which a manual audit of the same question had missed. - The one place that should break on a new code is the framework’s own
fixture:
packages/core/tests/error-hook.test.tskeepssatisfies Record<StitchErrorCode, string>, so adding a code without deciding its wire spelling fails our build rather than a consumer’s.
Consequences
- Supersedes the exhaustiveness clause of ADR 0026. The rest of 0026 — one
registry,
isStitchErrorCode, thecode → statusmap — stands unchanged. - Adding a framework error code is a patch-level, additive change: it widens
StitchErrorCode, and nothing in a consumer’s tree has to move. It is still named in the changelog. - A consumer who wants the compile error can write
satisfies Record<StitchErrorCode, AppCode>on their own object and accept that an additive release will stop their build — an informed choice rather than the default. - An enumeration of the codes in prose is a copy that goes stale; the guide now
says so and points at
Object.keys(STITCH_ERROR_STATUS).
Alternatives considered
- Keep the exhaustive map and never add codes. Rejected: the framework grew seven codes in one release for good reasons, and freezing the set to protect a type shape is the tail wagging the dog.
- Split “HTTP codes” from “tool codes” into two registries. Rejected: the consumer question is “what did the framework throw and what should it look like on my wire”, and that question does not care which transport carried it. Two registries would put the answer in two places, which is what ADR 0026 exists to prevent.