Skip to content

feat(spec)!: EnhancedApiError.fieldErrors → fields, tombstoned (ADR-0114 D4, #3977) - #4055

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

feat(spec)!: EnhancedApiError.fieldErrors → fields, tombstoned (ADR-0114 D4, #3977)#4055
os-zhuang merged 2 commits into
mainfrom
claude/error-code-vocabulary-mismatch-iscrkw

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Executes ADR-0114 D4, the one thing #4035 decided but deliberately left undone because retiring an authorable key needs its own sequence.

The rename

EnhancedApiError.fieldErrorsfields. The array and its element shape are unchanged — only the property name.

The wire has always carried fields: the validators, import coercion, validation-failure.ts, @objectstack/client and the console's field-error extractor all say it. fieldErrors was declared and emitted by nobody, so anyone reading error.fieldErrors was reading a field no server sent — ADR-0078's silently-inert declaration, sitting on the error envelope.

Tombstoned, not deleted

EnhancedApiErrorSchema is not .strict(), so a plain removal would let a producer still writing fieldErrors parse clean and lose the array — a validation failure that mentions no field. retiredKey() (ADR-0104) turns that into a rejection carrying the rename, so the failure is loud at the write side instead of silent at the read side. Two tests pin both halves: the old name throws with fields in the message, the new name parses.

A semantic chain entry, not a conversion — and that's the point

The ADR-0104 guard requires a registered retirement surface so the change reaches the documentation channel. The obvious move is a D2 conversion, and it would have been wrong here.

A conversion's job is rewriting author metadata. This is a response envelope: no stack, example or template has ever carried the key (checked across packages/, examples/, templates/, apps/), so os migrate meta has no source to rewrite. A conversion with an identity fixture and expectedNotices: 0 would claim a rewrite that does not exist.

The precedent is one step earlier in the same major: analytics-query-request-query / -format are HTTP-only surfaces whose own migration step says "No stored metadata carries this shape (it was HTTP-only), so the change is two semantic TODOs for API callers rather than a stack conversion." Same situation, same treatment — a semantic entry with a reason and an acceptance criterion, which is what actually reaches spec-changes.json (ADR-0087 D4), the generated upgrade guide, and the spec_changes MCP tool. The guard accepts semantic surfaces for exactly this reason.

The generated guide entry now reads:

enhanced-api-error-field-errors-renamedapi.enhancedApiError.fieldErrors → fields

  • Why not automatic: … This is a RESPONSE surface: no stack, example or template carries the key, so there is no source for the chain to rewrite — the schema tombstones it via retiredKey() and consumers move their read themselves.
  • Done when: No consumer reads error.fieldErrors; per-field validation detail is read from error.fields, and constructing an EnhancedApiError with fieldErrors fails to parse with the rename prescription instead of silently losing the array.

Two doc examples were teaching the opposite of this ADR

Both fixed rather than left for the drift advisory to re-flag:

  • error-handling-server.mdx hand-rolled a Zod → fields[] mapping with code: 'invalid_field' as const — a top-level code in a field position, and a private reimplementation of the translation ADR-0114 D3 centralised. It now calls zodIssuesToFields, the mapping the platform's own routes use, with a comment naming what that buys (too_small splitting by origin, a missing property becoming required).
  • error-handling-client.mdx's error class carried the dead name through its constructor, field and iteration.

Also gone: error-catalog.mdx's "widen the cast until the two names converge" workaround, since they have now converged.

Test evidence

Suite / gate Result
pnpm turbo run test (CI's command) 123/126 tasks ✅
dogfood (excluded from the above by design) 71 files / 410 ✅
spec errors + conversions + migrations 99 ✅
check:docs / check:spec-changes / check:upgrade-guide
check:authorable-surface (the guard that demanded this)
check:api-surface / check:skill-refs / check:skill-docs
check:skill-examples ✅ 198 examples
check:error-code-casing ✅ 2416 files, 0 violations

@objectstack/runtime fails one test, datasource-autoconnect.test.ts — the same pre-existing failure as #4035, which did not reproduce on CI there (17/17 green) and which I reproduced on that PR's parent commit. My diff touches spec and docs only.

Refs #3977, #4035. Completes ADR-0114.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MaSQn77TT5fUgHK9CesaDK


Generated by Claude Code

…0114 D4, #3977)

Completes the one thing #4035 decided but did not execute. The wire has always
carried `fields`; `fieldErrors` was declared and emitted by nobody, so a reader
keying on it was reading a field no server sent — ADR-0078's silently-inert
declaration, on the error envelope.

Tombstoned rather than deleted, because the schema is not .strict(): a plain
removal would let a producer still writing the old name parse clean and lose the
per-field detail, answering a validation failure that mentions no field.
retiredKey() turns that into a rejection carrying the rename.

Registered as a SEMANTIC chain entry, not a conversion, and the distinction is the
point. A conversion rewrites author metadata; this is a response envelope, and no
stack, example or template carries the key (checked across packages/, examples/,
templates/, apps/). A no-op conversion with an identity fixture would claim a
rewrite that does not exist. The analytics-query-request-* entries set the
precedent one step earlier in the same major: an HTTP-only surface with nothing
stored to rewrite is a semantic entry with a reason and an acceptance criterion —
which is what actually reaches spec-changes.json, the upgrade guide and the
spec_changes MCP tool.

Docs followed, and two examples were teaching the opposite of this ADR: the
server guide hand-rolled a Zod mapping with code: 'invalid_field' — a top-level
code in a field position — and the client guide's error class carried the dead
name. The server example now calls zodIssuesToFields, which is the mapping the
platform's own routes use.

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 7:17am

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

105 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 @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/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @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 packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @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 @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/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • 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/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/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • 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/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • 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/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @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/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @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/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/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @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/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.

…ntries it was missing

The v17 page documents each breaking change under its own heading, and the whole
ADR-0112 line had none — the largest wire-visible change in it appeared only as a
passing mention 600 lines down. That is the worst gap to leave, because a missed
error-code branch fails SILENTLY: nothing throws, the affordance it guarded just
disappears. Eleven console branches broke that way without one test failing.

Two entries: ADR-0112's vocabulary unification (with the generic-condition
collapse table, the four routes that stopped putting a code in the message slot,
and the case-insensitive advice for consumers spanning the upgrade), and
ADR-0114's field-level catalog plus this PR's fieldErrors -> fields rename.

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 07:29
@os-zhuang
os-zhuang merged commit 5dc4d02 into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/error-code-vocabulary-mismatch-iscrkw branch July 30, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants