Skip to content

ADR-0112 batch 2: sweep the ~100 lowercase snake_case error-code emitters to the SCREAMING catalog #4003

Description

@os-zhuang

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 = StandardErrorCodeERROR_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

  • cloud-connection (53)
  • plugin-auth (52) + @objectstack/hono (1)
  • metadata-protocol thrown codes (27) + the spec lock evaluators feeding them
  • service-automation (2) + the runtime branch, spec contract docstring and contract type reading them
  • service-messaging (4) + the plugin-webhooks route branching on them
  • rest top-level (6) and error:-slot codes (4)
  • trigger-api (6) — converted to the declared envelope; three branches gained the message they never had
  • batch_abortedBATCH_ABORTED (it rode in the same failed[] array as the causal item's catalog code, so one array carried two vocabularies)
  • objectui consumer branches — 11 sites, merged as objectui#2977 (see below; far worse than "dead branches")
  • Diagnostics vocabularies (build-probes.ts, metadata-diagnostics.ts) — decided out of scope, recorded as ADR-0112 D6c
  • CLI emitJson({ error: '…' }) (8) and code-like message: 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 already VALIDATION_FAILED

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions