Batch 2 of ADR-0112's rollout (#3841, decided in the ADR; batch 1 landed in #3988). Framework PR: #4021. Console PR: objectui#2977 (merged).
State after batch 1
ApiErrorSchema.code is now the closed set ErrorCode = StandardErrorCode ∪ ERROR_CODE_LEDGER (packages/spec/src/api/error-code-ledger.zod.ts).
Correction to an earlier draft of this issue: the surviving lowercase codes were not ledger-registered exemptions — the ledger is SCREAMING-only, lint-enforced. They survived because nothing validated them: flat envelopes, thrown Error payloads, or fields the envelope schema never covered. A blind spot, not a tracked exception.
Survey (replaces the ADR's ~100 estimate)
208 lowercase code-position literals across 10 packages, plus 23 where a bare code string sat in the error: message slot. ~35 of the 208 are field-level ({ field, code }) and belong to #3977 under D6.
Progress
Findings that changed the plan
The ADR's "no consumer branches on the lowercase dialect" was wrong — six times over. Each was renamed atomically with its emitter, and each was found by a different method, which is the real lesson:
| # |
Consumer |
Found by |
| 1–3 |
runtime/domains/automation.ts (forbidden, invalid_signal), metadata-protocol/protocol.ts:4353 (destructive_change), spec contract docstring |
grepping for code === '…' |
| 4 |
spec/contracts/automation-service.ts literal-union type |
a consumer's dts build, two packages away — tsc --noEmit on the declaring package passed |
| 5 |
plugin-approvals test (the approval node is the service-gated case) |
running the full suite |
| 6 |
qa/dogfood package-first authoring |
the dogfood shards, which the full-suite run excludes by design |
The ADR now carries the correction plus a sweep checklist of the seven literal forms a code: '…' grep misses. Two are worth repeating: a missed comparison fails silently rather than loudly, and a missed literal-union type surfaces in a package the sweep never touched (it took Build Core, Test Core and both dogfood shards red at once, reading like four unrelated failures).
objectui was actively broken, not merely dead-branched (objectui#2977). Eleven === comparisons on the old spelling: the destructive-change confirm dialog, the writable-package hint, field-scoped validation issues, the all-or-nothing publish summary, unknown-object tolerance in the header and record search, three marketplace install messages, and isNotFoundError in the data layer. RECORD_NOT_FOUND had been renamed in batch 1, so that branch was already dead a release before anyone noticed. Fixed with a case-insensitive errorCodeIs in @object-ui/types — the console ships separately from its server, so both spellings are live peers until old servers are unsupported.
Persisted code columns are a third vocabulary (ADR-0112 D6b). sys_metadata_audit.code keeps its lowercase values: it is audit history (old rows keep their spelling forever) and the same column holds non-errors (ok, lock_override).
Diagnostics are a fourth (D6c). They ship as payload of a 200, describe an artifact rather than the request, and carry a severity that can be warning.
The line, now written down once: the catalog governs the code a failing request answers with. Field-addressed (D6), persisted (D6b), or inside a successful response (D6c) means a different vocabulary with different consumers.
One unbounded code source closed. plugin-auth's OAuth client registration put better-auth's arbitrary body.error straight into error.code. A closed set cannot hold with an unbounded source feeding it; the code is ours now and the upstream discriminator moved to details.upstreamError.
Remaining
Refs #3841, #3988, #4021, objectui#2977.
Batch 2 of ADR-0112's rollout (#3841, decided in the ADR; batch 1 landed in #3988). Framework PR: #4021. Console PR: objectui#2977 (merged).
State after batch 1
ApiErrorSchema.codeis now the closed setErrorCode=StandardErrorCode∪ERROR_CODE_LEDGER(packages/spec/src/api/error-code-ledger.zod.ts).Correction to an earlier draft of this issue: the surviving lowercase codes were not ledger-registered exemptions — the ledger is SCREAMING-only, lint-enforced. They survived because nothing validated them: flat envelopes, thrown
Errorpayloads, or fields the envelope schema never covered. A blind spot, not a tracked exception.Survey (replaces the ADR's ~100 estimate)
208 lowercase code-position literals across 10 packages, plus 23 where a bare code string sat in the
error:message slot. ~35 of the 208 are field-level ({ field, code }) and belong to #3977 under D6.Progress
cloud-connection(53)plugin-auth(52) +@objectstack/hono(1)metadata-protocolthrown codes (27) + thespeclock evaluators feeding themservice-automation(2) + theruntimebranch,speccontract docstring and contract type reading themservice-messaging(4) + theplugin-webhooksroute branching on themresttop-level (6) anderror:-slot codes (4)trigger-api(6) — converted to the declared envelope; three branches gained the message they never hadbatch_aborted→BATCH_ABORTED(it rode in the samefailed[]array as the causal item's catalog code, so one array carried two vocabularies)build-probes.ts,metadata-diagnostics.ts) — decided out of scope, recorded as ADR-0112 D6cemitJson({ error: '…' })(8) and code-likemessage:strings (5) — decided out of scope (terminal output contract read by shell scripts, not a request response)spec/ui/action-params.zod.ts(4),plugin-sharing(1) — param/field-addressed, D6 territory; plugin-sharing's own top-level code was alreadyVALIDATION_FAILEDFindings that changed the plan
The ADR's "no consumer branches on the lowercase dialect" was wrong — six times over. Each was renamed atomically with its emitter, and each was found by a different method, which is the real lesson:
runtime/domains/automation.ts(forbidden,invalid_signal),metadata-protocol/protocol.ts:4353(destructive_change),speccontract docstringcode === '…'spec/contracts/automation-service.tsliteral-union typedtsbuild, two packages away —tsc --noEmiton the declaring package passedplugin-approvalstest (the approval node is the service-gated case)qa/dogfoodpackage-first authoringThe ADR now carries the correction plus a sweep checklist of the seven literal forms a
code: '…'grep misses. Two are worth repeating: a missed comparison fails silently rather than loudly, and a missed literal-union type surfaces in a package the sweep never touched (it took Build Core, Test Core and both dogfood shards red at once, reading like four unrelated failures).objectui was actively broken, not merely dead-branched (objectui#2977). Eleven
===comparisons on the old spelling: the destructive-change confirm dialog, the writable-package hint, field-scoped validation issues, the all-or-nothing publish summary, unknown-object tolerance in the header and record search, three marketplace install messages, andisNotFoundErrorin the data layer.RECORD_NOT_FOUNDhad been renamed in batch 1, so that branch was already dead a release before anyone noticed. Fixed with a case-insensitiveerrorCodeIsin@object-ui/types— the console ships separately from its server, so both spellings are live peers until old servers are unsupported.Persisted code columns are a third vocabulary (ADR-0112 D6b).
sys_metadata_audit.codekeeps its lowercase values: it is audit history (old rows keep their spelling forever) and the same column holds non-errors (ok,lock_override).Diagnostics are a fourth (D6c). They ship as payload of a 200, describe an artifact rather than the request, and carry a severity that can be
warning.The line, now written down once: the catalog governs the code a failing request answers with. Field-addressed (D6), persisted (D6b), or inside a successful response (D6c) means a different vocabulary with different consumers.
One unbounded code source closed.
plugin-auth's OAuth client registration put better-auth's arbitrarybody.errorstraight intoerror.code. A closed set cannot hold with an unbounded source feeding it; the code is ours now and the upstream discriminator moved todetails.upstreamError.Remaining
Refs #3841, #3988, #4021, objectui#2977.