Skip to content

refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) - #4021

Merged
os-zhuang merged 11 commits into
mainfrom
claude/error-code-vocabulary-mismatch-iscrkw
Jul 30, 2026
Merged

refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003)#4021
os-zhuang merged 11 commits into
mainfrom
claude/error-code-vocabulary-mismatch-iscrkw

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Batch 2 of ADR-0112's rollout, tracked in #4003. Batch 1 was #3988. Console side: objectui#2977 (merged).

What

Converts the lowercase snake_case emitters to the declared vocabulary — the standard catalog where the condition is generic, a registered ledger code where it is domain-specific — and adds four packages to ERROR_CODE_LEDGER (cloud-connection, hono, service-messaging, trigger-api) plus entries under rest, plugin-auth, metadata-protocol, service-automation.

Cluster Swept
cloud-connection 53
plugin-auth (+ hono) 53
metadata-protocol thrown codes (+ the spec lock evaluators feeding them) 27
rest top-level + error:-slot codes 10
trigger-api (converted to the declared envelope) 6
service-messaging redeliver (+ the plugin-webhooks route) 4
service-automation resume refusals (+ runtime branch, spec docstring and contract type) 2

Generic conditions collapse onto the standard catalog rather than keeping a synonym: unauthorized/unauthenticatedUNAUTHENTICATED, forbiddenPERMISSION_DENIED, not_foundRESOURCE_NOT_FOUND, internalINTERNAL_ERROR, unavailableSERVICE_UNAVAILABLE, not_supportedNOT_IMPLEMENTED, bad_requestINVALID_REQUEST (a status name is not a semantic code).

Four routes also had a bare code string in the error: message slot, so error was a code and the declared object envelope was nowhere. They now emit error: { code, message }, and three trigger-api branches gained the message they never had. Renaming alone would have left them unvalidated — which is how the lowercase dialect survived in the first place.

Also included: D9a from the ADR's ride-along list — spec exported two mutually incompatible ErrorCategory types and two RetryStrategy types; the connector-side pair becomes ConnectorErrorCategory / ConnectorRetryStrategy. Side effect worth noting: the api-side ErrorCategory and RetryStrategy now appear in the generated API reference at all — the name collision had been silently dropping them.

New: a guard so this cannot regress

The ledger's admission test enforces casing on every code someone registers. Nothing enforced the ones nobody registers, and an unregistered lowercase literal is invisible twice over — to the ledger, which has nothing to check, and to the schema, on any route that never parses its own response. That is how 208 accumulated across 10 packages.

scripts/check-error-code-casing.mjs (wired into lint.yml) scans for the four positions batch 2 actually got bitten by: emission, property assignment, comparison, and literal-union type. The design constraint is that code is also an ordinary domain field name — a license plan code, a seed industry code, a locale — and a guard that flags those gets switched off within a week. So a hit counts only with an error-shaped neighbour nearby, and the D6 families are recognised structurally (a field/param/path/target key in the same record, over a window, because { code, field, message } is routinely spread over three lines). Escape hatches, in descending preference: structural, then // adr0112-ok: <reason> at the site (a bare marker without a reason does not count), then EXEMPT_FILES for a file that owns a non-catalog vocabulary.

--self-test runs 17 cases — every violating form, every exemption route, every known false-positive shape — before the scan, so a green run means the matcher was checked rather than merely quiet. Verified end-to-end by reintroducing code: 'object_not_found' into rest-server.ts: the guard fails, and passes again on revert.

Five things worth a reviewer's attention

1. The ADR's "no consumer branches on the lowercase dialect" was wrong — six times. Each was renamed atomically with its emitter, and each was found by a different method, which is the actual lesson:

Consumer Found by
runtime/domains/automation.ts (forbidden, invalid_signal), metadata-protocol/protocol.ts:4353 (destructive_change), the spec contract docstring grepping code === '…'
spec/contracts/automation-service.ts literal-union type a consumer's dts build two packages away — tsc --noEmit on the declaring package passed
plugin-approvals test (its approval node is the service-gated case) running the full suite
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 forms a code: '…' grep misses. Two deserve repeating: a missed comparison fails silently rather than loudly, and a missed literal-union type surfaces in a package the sweep never touched — that one took Build Core, Test Core and both dogfood shards red simultaneously, reading like four unrelated failures.

2. Three vocabularies are deliberately NOT swept, now recorded as decisions.

  • D6b — persisted. sys_metadata_audit.code spells its denials exactly like the thrown codes, so a naive sweep renames it too. But it is audit history — rows written before this ADR keep their spelling forever — and the same column holds outcomes that are not errors (ok, lock_override). Following the catalog buys a data migration or one column with two vocabularies mixed by write date. Recorded at both sites, so it reads as a decision rather than a missed rename.
  • D6c — diagnostics. build-probes.ts / metadata-diagnostics.ts records ship as payload of a 200, describe an artifact rather than the request, and carry a severity that can be warning.
  • CLI emitJson({ error: '…' }) is a terminal output contract read by shell scripts, not a request response; renaming it would break users' pipelines with neither a schema nor this ADR asking for it.

The line, written down once: the catalog governs the code a failing request answers with. Field-addressed (D6), persisted (D6b), or inside a successful response (D6c) is a different vocabulary with different consumers.

3. An unbounded code source is closed. plugin-auth's OAuth client registration put better-auth's arbitrary body.error string straight into error.code. A closed set cannot hold with an unbounded source feeding it; the code is ours now (OAUTH_REGISTER_FAILED) and the upstream discriminator moves to details.upstreamError.

4. mapDataError's translation is inverted, not renamed. It deliberately downcased the internal OBJECT_NOT_FOUND to object_not_found on the way out, and a test pinned exactly that ("does not let the generic 4xx passthrough ship the internal SCREAMING_CASE code") — correct when the data routes were believed to speak their own dialect, backwards once there is one vocabulary. The translation is gone; the test is rewritten to pin what still matters (no per-route dialect translation) rather than deleted.

5. objectui was actively broken, not merely dead-branched (fixed in objectui#2977, merged first on purpose). 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 read, since the console ships separately from its server and both spellings are live peers until old servers are unsupported.

Out of scope (deliberately)

Field-level codes ({ field, code } in import-coerce.ts, record-validator.ts, rule-validator.ts, action-params.zod.ts) stay as they are — ADR-0112 D6 puts them under #3977.

Test evidence

  • pnpm turbo run test (the same command CI runs): 129/129 package tasks green.
  • Dogfood suite run separately (the full-suite filter excludes it): 71 files / 410 tests green.
  • turbo run build across all 57 packages: green.
  • All 12 spec check gates green locally, plus the new check:error-code-casing (2398 files, 0 violations).

service-automation carries 6 failures in notify-node/screen-nodes/crud-config-aliases/engine that reproduce with these changes stashed — pre-existing, not from this PR.

Refs #3841, #3988, objectui#2977. #4003 can close with this.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK

claude added 4 commits July 30, 2026 02:54
…-0112 D9a)

packages/spec exported two mutually incompatible ErrorCategory types and two
RetryStrategy types — api/errors.zod.ts (HTTP error responses) and
integration/connector.zod.ts (connector normalisation). Separate subpath
exports kept TypeScript quiet, but the doc/schema generators key on the
stripped name, so a cross-reference to 'ErrorCategory' resolved to whichever
was scanned last.

Rename the connector-side pair to ConnectorErrorCategory / ConnectorRetryStrategy
and say in each docblock why the name is prefixed. Both are spec-internal (only
connector.zod.ts and its test referenced them), so no downstream break.

The json-schema ratchet caught the rename as an unpublish; the two manifest keys
are renamed alongside, per its instructions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
…owercase code clusters (ADR-0112 batch 2)

Converts 105 lowercase snake_case error codes to the declared vocabulary and
registers the service-specific ones in ERROR_CODE_LEDGER.

Generic conditions collapse onto the standard catalog rather than keeping a
synonym: 'unauthorized'/'unauthenticated' -> UNAUTHENTICATED, 'forbidden' ->
PERMISSION_DENIED, 'not_found' -> RESOURCE_NOT_FOUND, 'internal' ->
INTERNAL_ERROR, 'unavailable' -> SERVICE_UNAVAILABLE, 'not_supported' ->
NOT_IMPLEMENTED, and 'bad_request' -> INVALID_REQUEST (a status name is not a
semantic code). Domain conditions get registered codes, prefixed where the bare
name wouldn't carry meaning (MARKETPLACE_STORAGE_FAILED, PLUGIN_MANIFEST_INVALID,
ENVIRONMENT_BIND_FAILED, RESEED_SKIPPED, ...). New ledger entries:
@objectstack/cloud-connection and @objectstack/hono.

Also closes an unbounded code source found while sweeping: the OAuth client
registration route put better-auth's arbitrary `body.error` string straight into
`error.code`, so any upstream string became a code and the closed set could not
hold. The code is now ours (OAUTH_REGISTER_FAILED) and the upstream
discriminator moves to `details.upstreamError`.

Per-package consumer check (ADR-0112 batch-2 precondition): no reader outside
either package branches on the swept spellings; the in-package test assertions
move with the emitters.

Refs #4003, #3841.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
…n-error codes; keep the audit column out (ADR-0112 batch 2)

Three clusters, all of which reach `error.code` on the wire — a thrown error's
`.code` is copied straight into the response body (rest-server.ts:268), so these
were never merely internal.

- metadata-protocol: 24 thrown codes (ITEM_LOCKED, NOT_OVERRIDABLE,
  METADATA_CONFLICT, DESTRUCTIVE_CHANGE, ...) registered under the package, plus
  the two `evaluateLockFor*` helpers in spec that feed them.
- service-automation resume refusals: 'forbidden' -> PERMISSION_DENIED,
  'invalid_signal' -> INVALID_SIGNAL, together with the runtime branch that
  reads them and the contract docstring that documents them.

Two decisions worth keeping:

`sys_metadata_audit.code` is deliberately NOT swept. It spells its denial
reasons identically to the codes the protocol throws, so a naive sweep renames
it too — but the column is persisted audit history (rows predating ADR-0112 keep
their spelling forever) and it also holds non-error outcomes ('ok',
'lock_override'). Following the catalog would buy a data migration or one column
with two vocabularies mixed by write date. It stays lowercase and self-contained;
the divergence is recorded at both sites and as ADR-0112 D6b, so it reads as a
decision rather than a missed rename.

The ADR's "no consumer branches on the lowercase dialect" claim was wrong: the
per-service re-verification it mandated found three, each renamed atomically
here. The ADR now records the correction plus a sweep checklist of the six
literal forms a `code: '...'` grep misses (comparisons, property assignment,
computed ternaries, regex assertions, toBe assertions, docstrings) — every one
of them cost a red suite in this batch.

Refs #4003, #3841.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
…-level codes and un-nest three flat envelopes (ADR-0112 batch 2)

- rest: OBJECT_NOT_FOUND, UNAUTHENTICATED (it sat in the same if/else as an
  already-SCREAMING PERMISSION_DENIED), INVALID_REQUEST.
- service-messaging redeliver: RESOURCE_NOT_FOUND / DELIVERY_NOT_ELIGIBLE, with
  the plugin-webhooks route that branches on them.

Three routes also had a bare code string in the `error:` slot with a real
`message:` beside it, so `error` was a code and the declared object envelope was
nowhere. They now emit `error: { code, message }` — a rename alone would have
left them unvalidated, which is how the lowercase dialect survived the first
place.

`mapDataError`'s downcasing is inverted, not just renamed. It deliberately
translated the internal SCREAMING OBJECT_NOT_FOUND down to `object_not_found` on
the way out, and a test pinned exactly that ("does not let the generic 4xx
passthrough ship the internal SCREAMING_CASE code") — correct when the data
routes were believed to speak their own dialect, backwards once there is one
vocabulary. The translation is gone and the test is rewritten to pin what still
matters: no per-route dialect translation, what the gate threw is what ships.
Two comments that still taught the lowercase wire code are updated with it.

Field-level codes in `import-coerce.ts` are deliberately untouched (ADR-0112 D6,
#3977).

Refs #4003, #3841.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 4:17am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests labels Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 14 package(s): packages/adapters, @objectstack/cloud-connection, @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/plugin-approvals, @objectstack/plugin-auth, @objectstack/plugin-webhooks, packages/qa, @objectstack/rest, @objectstack/runtime, packages/services, @objectstack/spec, packages/triggers.

118 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via @objectstack/plugin-approvals, packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/plugins/plugin-webhooks, packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth, @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql, @objectstack/plugin-auth, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/qa, packages/runtime, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/plugin-auth, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via packages/adapters, @objectstack/objectql, @objectstack/plugin-approvals, @objectstack/plugin-auth, @objectstack/plugin-webhooks, @objectstack/rest, @objectstack/runtime, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/plugin-approvals, @objectstack/plugin-auth, @objectstack/plugin-webhooks, @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/metadata-core, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/plugin-approvals, @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 4 commits July 30, 2026 03:29
…lope; draw the catalog's boundary (ADR-0112 batch 2)

The API-trigger webhook endpoint returned `{ error: '<code>' }` — a bare code in
the message slot, six times, three of them with no message at all. It now answers
`{ success: false, error: { code, message } }` with catalog codes
(RESOURCE_NOT_FOUND, INVALID_REQUEST, SERVICE_UNAVAILABLE) plus two registered
ones (INVALID_SIGNATURE, ENQUEUE_FAILED). Every branch gains the message it was
missing.

Also records what batch 2 decided NOT to sweep, as ADR-0112 D6c, because three
lowercase clusters look like stragglers and are not:

- Diagnostics records (`build-probes.ts` RuntimeBuildIssue,
  `metadata-diagnostics.ts`) ship as the payload of a 200, describe an artifact
  rather than the request, and carry a severity that can be 'warning'.
- Param/field-addressed issues (`ActionParamIssue`, plugin-sharing's `fields[]`)
  are D6 territory; plugin-sharing's own top-level code is already
  VALIDATION_FAILED.
- CLI `emitJson({ error: '...' })` output is a terminal contract read by shell
  scripts, not a request response — renaming it would break users' pipelines
  with neither a schema nor this ADR asking for it.

The line, now written down once: the catalog governs the code a failing REQUEST
answers with. Field-addressed (D6), persisted (D6b), or shipped inside a
successful response (D6c) means a different vocabulary with different consumers.

Refs #4003, #3841.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
…name

The resume result's `code` is a literal-union TYPE in the service contract, not
just a value, and batch 2 renamed only the values plus the docstring beside it.
Nothing in spec failed: `tsc --noEmit` on the declaring package is happy with a
narrower union than its callers emit. The break surfaced two packages away — the
dts build of every consumer of @objectstack/spec/integration, which is what took
Build Core, Test Core and the dogfood shards red at once.

Added to the ADR's sweep checklist as its own row, since it is the one form whose
error appears in a package the sweep never touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
…s of the automation and publish codes

Four more consumers of codes batch 2 renamed, found by running the full suite
rather than grepping harder:

- plugin-approvals asserts the resume refusal code (its approval node IS the
  service-gated case), a package the sweep never touched.
- http-dispatcher.test mocks the automation service's result, so its two
  literals are the emitter's contract restated in a mock.
- batch_aborted -> BATCH_ABORTED: it rides in the same failed[] array as the
  causal item's catalog code, so leaving it lowercase made one array carry two
  vocabularies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
…name

Sixth consumer found, and the one my local verification could not have caught:
the full-suite run I used as the safety net excludes @objectstack/dogfood by
design, so the dogfood shards were the one gate I had not actually run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
The ledger's admission test enforces casing on every code someone REGISTERS.
Nothing enforced the ones nobody registers — and an unregistered lowercase
literal in a code position is invisible twice: to the ledger, which has nothing
to check, and to the schema, on any route that never parses its own response.
That is how 208 lowercase literals accumulated across 10 packages. Without a
guard, number 209 lands the same way.

`scripts/check-error-code-casing.mjs` scans packages/ for four code positions —
emission, property assignment, comparison, and literal-union type — which are
the forms batch 2 actually got bitten by, comparison and union-type being the two
that fail silently or two packages away.

The design problem is that `code` is also an ordinary domain field name: a
license plan code, a seed industry code, a locale. A guard that flags those gets
switched off within a week, so a hit only counts when an error-shaped neighbour
(error/message/throw/status/issues/...) appears within a few lines, and the
D6 families are recognised structurally — a `field`/`param`/`path`/`target` key
in the same record, checked over a window because `{ code, field, message }` is
routinely spread across three lines.

Three escape hatches, in descending preference:
  - structural: the D6 shapes above, no annotation needed;
  - `// adr0112-ok: <reason>` at the site, for a file that legitimately holds
    both vocabularies (protocol.ts throws a catalog code and writes an audit row
    beside it) — the reason is required, a bare marker does not count;
  - EXEMPT_FILES, for a file that OWNS a non-catalog vocabulary (a diagnostics
    producer, OData's foreign error vocabulary), each line carrying its reason.

`--self-test` runs 17 cases (each violating form, each exemption route, each
known false-positive shape) before the scan, so a green run means the matcher
was checked, not just quiet. End-to-end verified by reintroducing
`code: 'object_not_found'` in rest-server.ts: the guard fails, and passes again
when reverted.

Two real fixes fell out of turning it on: the `'locked'` fixtures in two
publish tests now spell what the protocol actually throws (ITEM_LOCKED), and the
audit-row sites carry their D6b marker so the divergence reads as a decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file labels Jul 30, 2026
…the new guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 04:29
@os-zhuang
os-zhuang merged commit f5a4ef0 into main Jul 30, 2026
19 checks passed
@os-zhuang
os-zhuang deleted the claude/error-code-vocabulary-mismatch-iscrkw branch July 30, 2026 04:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants