Skip to content

refactor(types,rest,services,plugin-sharing): one shared writer for the response envelope, and error.code is enforced at compile time (#3973) - #4229

Merged
os-zhuang merged 5 commits into
mainfrom
claude/duplicate-envelope-builders-802cd8
Jul 31, 2026
Merged

refactor(types,rest,services,plugin-sharing): one shared writer for the response envelope, and error.code is enforced at compile time (#3973)#4229
os-zhuang merged 5 commits into
mainfrom
claude/duplicate-envelope-builders-802cd8

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3973.

BaseResponseSchema declares one envelope for every REST body the platform emits. It declared it once; the code that wrote it was copied per route module. After #3843 and #3983 converted the last drifting one, seven modules each carried their own two-line sendOk / sendError pair — the envelope's shape in fourteen places rather than one.

#3973's option 1 (consolidating i18n's four inline success builders) had already shipped. This is option 2, whose stated blocker was placement, not design.

Placement, answered by precedent

Candidate Verdict
packages/spec Ruled out — schemas-only (Prime Directive #2)
packages/core All seven depend on it, but writing an HTTP envelope is not kernel business
@objectstack/types ✅ Depends on nothing but @objectstack/spec, so every caller can reach it

@objectstack/types is already where the repo puts a helper the HTTP boundaries share: error-leak.ts (#3867) sits one file over and made the identical argument first — "do not ship driver internals to clients" is a property of the HTTP boundary, not of one router. Writing the declared envelope is the same kind of property.

The builders take a structural { status(n), json(body) }, so the package imports no HTTP contract at allIHttpResponse satisfies it, and so does the any-typed res the three older modules carry.

error.code is now checked by the compiler

All seven copies typed the parameter code: string. ADR-0112 (#3841) closed the vocabulary — ErrorCode is StandardErrorCode ∪ ERROR_CODE_LEDGER — but an invented code was still caught only at runtime, by a conformance suite parsing a driven body, i.e. only on routes some test happened to drive.

sendError(res, 400, 'NOT_A_REGISTERED_CODE', 'invented');
// Argument of type '"NOT_A_REGISTERED_CODE"' is not assignable to parameter of type 'ErrorCode'.

Verified with a probe rather than inferred from a silent typecheck: TS2345 on an unregistered code, and TS2578 on a valid code under @ts-expect-error (proving the directive-detector is active, so the test's suppression is real). This cost zero call-site churn — every code the seven modules emit was already registered.

The guard got stronger, not just updated

scripts/check-route-envelope.mjs counts response write sites per module. A module routing everything through the shared pair builds none itself:

before after
each of the seven modules 2 / 1 / 1 0 / 0 / 0
the shared pair 2 / 1 / 1, pinned

So the invariant is now total for the surface rather than per-module, and what the count asserts changed from "your two builders are the enveloped ones" to "you have no builders". A new route that hand-rolls a body still moves it off zero and fails. A SHARED_BUILDER check makes deleting or relocating the one writer a loud failure instead of silently leaving nothing pinned. Two self-test cases were added for the consolidated shape.

Nothing changes on the wire

The seven pairs were identical modulo the optional status and extra parameters this one unions. Every module's driven conformance suite still parses its real bodies against the real spec schemas. One internal call site was rewritten: package-routes passed details positionally and now passes { details }, producing the same error.details it always did.

Per-module rationale (what each emitted pre-#3843, why its codes were minted, why /external/validate keeps its computed ok) was preserved and lifted into each module header, not deleted with the builders.

Verification

  • pnpm test132/132 turbo tasks green
  • 1455 tests across the seven affected packages, including every driven envelope conformance suite
  • pnpm check:route-envelope + self-test, pnpm build, pnpm lint, pnpm check:error-code-casing
  • origin/main merged and everything re-run after (per AGENTS.md multi-agent discipline [WIP] Create a new release version #9/chore: version packages #10)

Two earlier full-suite runs failed on unrelated load-induced 5s timeoutsservice-settings once, then rest's import-integration.test.ts (xlsx parsing) under import 823s of contention. Both pass standalone, neither touches changed code, and a different package failed each run. Re-running at --concurrency=3 was clean.

Out-of-scope findings filed rather than folded in (Prime Directive #10)

🤖 Generated with Claude Code

os-zhuang and others added 2 commits July 31, 2026 00:41
…he response envelope, and error.code is enforced at compile time (#3973)

`BaseResponseSchema` declares one envelope for every REST body the platform
emits. It declared it once; the code that *wrote* it was copied per route
module. After #3843 and #3983 converted the last drifting one, seven modules
each carried their own two-line `sendOk` / `sendError` pair — the envelope's
shape in fourteen places rather than one.

`check:route-envelope` proved those copies agreed, which is why this is a
cleanup rather than a bug fix. But a guard proves agreement; it does not create
it. An eighth module starts by copying the pair again — not hypothetically:
`share-link-routes.ts` was found already drifting by the repo-wide scan, and
its drift had broken `client.shareLinks.create()` / `.list()` (#3983).

Placement was #3973's open question, not design. `packages/spec` is
schemas-only (Prime Directive #2), and the callers span `rest`, four
`services/*` and one `plugins/*`. `@objectstack/types` depends on nothing but
`@objectstack/spec`, so every caller can reach it, and it is already where the
repo puts a helper the HTTP boundaries share — `looksLikeInternalErrorLeak`
(#3867) sits one file over and made the same argument first. The builders take
a structural `{ status(n), json(body) }`, so the package imports no HTTP
contract at all.

`error.code` is now checked by the compiler. All seven copies typed it
`string`, so an invented code was caught only at runtime, by a conformance
suite parsing a driven body — i.e. only on routes some test happened to drive.
The shared `sendError` types it as ADR-0112's closed `ErrorCode` union, so an
unregistered code fails to compile at every call site at once. This cost no
call-site churn: every code the seven modules emit was already registered.

Nothing changes on the wire. The seven pairs were identical modulo the optional
`status` and `extra` parameters this one unions, and each module's driven
conformance suite still parses its real bodies against the real spec schemas.
One internal call site was rewritten: `package-routes` passed `details`
positionally and now passes `{ details }`, producing the same `error.details`.

The guard got stronger. A module routing everything through the shared pair
builds none itself, so the seven now declare `0 / 0 / 0` where they declared
`2 / 1 / 1`, and the pair is pinned separately at `2 / 1 / 1` so the invariant
is total for the surface rather than per-module. What the count asserts is no
longer "your two builders are the enveloped ones" but "you have no builders".

Two out-of-scope findings filed rather than folded in (Prime Directive #10):
#4224 (settings passes four keys `ApiErrorSchema` does not declare — the reason
`extra` stays `Record<string, unknown>`) and #4225 (three admin routes answer
503 naming the wrong service — carried out of #3973 so closing it does not bury
the note).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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 31, 2026 1:52am

Request Review

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/plugin-sharing, @objectstack/rest, @objectstack/service-datasource, @objectstack/service-i18n, @objectstack/service-settings, @objectstack/service-storage, @objectstack/types.

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

  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest)
  • content/docs/api/plugin-endpoints.mdx (via @objectstack/service-storage)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services/service-settings)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services/service-settings)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services/service-settings)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-i18n)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-sharing)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-sharing)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-sharing, @objectstack/rest, @objectstack/service-i18n, @objectstack/service-settings, @objectstack/service-storage, @objectstack/types)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/service-i18n)
  • content/docs/protocol/objectql/security.mdx (via packages/plugins/plugin-sharing)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-sharing, @objectstack/rest, @objectstack/service-i18n, @objectstack/service-settings, @objectstack/service-storage)
  • content/docs/releases/v12.mdx (via @objectstack/rest)
  • content/docs/releases/v9.mdx (via @objectstack/service-settings)

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.

os-zhuang and others added 3 commits July 31, 2026 08:26
Two of main's commits landed in files this branch rewrites, and both are the
follow-ups this branch's own issues asked for:

  #4237 (#4224) — settings error bodies moved onto the declared `details` slot,
                  and its LOCAL sendError tightened `extra` to ApiError's own
                  optional fields.
  #4234 (#4225) — the datasource-admin 503 became `resolve(res, service, method)`,
                  so the name that performs the lookup is the name that writes
                  the message.

Resolution keeps main's semantics and this branch's consolidation: main's call
sites and its `resolve` helper survive verbatim; the local sendOk/sendError
copies both commits still carried are deleted in favour of the shared pair.

`extra` is tightened at the SHARED builder rather than in the one module that
was fixed — `Pick<ApiError, 'category'|'httpStatus'|'details'|'requestId'>`.
#4224's fix removed the only reason it was `Record<string, unknown>`, and doing
it here makes an undeclared sibling of `code` a compile error in all seven
modules at once instead of only the module someone happened to fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@os-zhuang
os-zhuang merged commit d5749d7 into main Jul 31, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/duplicate-envelope-builders-802cd8 branch July 31, 2026 02:05
os-zhuang pushed a commit that referenced this pull request Jul 31, 2026
Resolves the #3973/#4229 overlap: the per-module sendOk/sendError pairs this
branch had annotated were consolidated into @objectstack/types on main, so the
local definitions go and the #4249 notes move to the module docs. With the
shared sendError typing `code` as the closed ErrorCode union, SERVICE_ERROR_CODE
is now Record<ServiceName, ErrorCode> — the mis-attribution class this branch
fixes is a compile error going forward.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Di9hefxJMFD3rxiP2G2Eug
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Six route modules now each carry their own copy of the envelope builders, and i18n's four are still inline

1 participant