You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Carried out of #3843 (PR #3972) rather than left buried in it. #3843 asked for the guard to be shared and the four drifting modules converted; both are done. What it did not ask for — and what the conversion makes visible — is that the builders are still copied per module.
Six route modules emit the declared envelope. Each defines its own two-line pair:
Module
builders
service-storage/storage-routes.ts
local sendOk / sendError
service-settings/settings-routes.ts
local sendOk / sendError
service-datasource/admin-routes.ts
local sendOk / sendError
rest/external-datasource-routes.ts
local sendOk / sendError
rest/package-routes.ts
local sendOk / sendError
service-i18n/i18n-service-plugin.ts
local sendError; success built inline in 4 routes
So the envelope's shape now lives in eleven places rather than one. Nothing is wrong — the shared conformance guard (@objectstack/route-envelope-conformance) proves all six agree today, which is exactly why this is a cleanup rather than a bug. But the guard proves agreement; it does not create it. A seventh module starts by copying the pair again.
This is the same argument #3843 made about the scan one layer over: three copies was the signal it wanted lifting. Six copies of the builder is the same signal.
The i18n half of it
i18n-service-plugin.ts is the one module still building success bodies inline, once per read route:
Those bodies are correct — packages/runtime/src/i18n-success-envelope.conformance.test.ts drives them — so this is not the envelope drift #3843 was about. It is a weaker guard: a fifth read route could get the shape wrong and only a driven test would notice. #3843 pinned it as an explicit ratchet in service-i18n/src/error-envelope.conformance.test.ts:
which fails on any new inline body and drops to the default 2 / 1 the moment the four are consolidated.
What needs deciding
Consolidate i18n only. Add a sendOk to i18n-service-plugin.ts, delete the ratchet, all six modules then pass the shared guard on its defaults. Small, mechanical, no cross-package movement.
One shared runtime builder for all six. The envelope is declared once in packages/spec; the thing that writes it could be too. sendOk / sendError need nothing but a { status(n): this; json(body): unknown } shape, so a structurally-typed pair has no dependencies at all and every module can import it. The blocker is placement, not design: packages/spec is schemas-only (Prime Directive ✨ Set up Copilot instructions #2), and the callers span rest + four services/*, which rules out anything that depends on them.
Leave it. The guard already fails any module that disagrees, and six two-line copies are cheap. Costs nothing today; pays the copy again per new module.
Recommendation: 1, then 2. (1) is worth doing regardless — it retires a ratchet and makes the guard uniform. (2) is the real consolidation, and it wants the placement question answered deliberately rather than by whoever writes the seventh module. Worth sequencing after #3841/#3842, since a shared builder is the natural place to enforce a code vocabulary once one is chosen.
Adjacent, one line, noted here rather than filed twice
admin-routes.ts answers datasource_admin_unavailable on three routes whose missing dependency is the external-datasource service, not the admin one (GET /:name/remote-tables, POST /:name/test, POST /:name/object-draft all resolve externalService() and then call the shared unavailable() helper). Pre-existing, untouched by #3843 — which deliberately carried every code string over verbatim — and a natural pickup for whoever does #3841's sweep.
Carried out of #3843 (PR #3972) rather than left buried in it. #3843 asked for the guard to be shared and the four drifting modules converted; both are done. What it did not ask for — and what the conversion makes visible — is that the builders are still copied per module.
The state after #3843
Six route modules emit the declared envelope. Each defines its own two-line pair:
service-storage/storage-routes.tssendOk/sendErrorservice-settings/settings-routes.tssendOk/sendErrorservice-datasource/admin-routes.tssendOk/sendErrorrest/external-datasource-routes.tssendOk/sendErrorrest/package-routes.tssendOk/sendErrorservice-i18n/i18n-service-plugin.tssendError; success built inline in 4 routesSo the envelope's shape now lives in eleven places rather than one. Nothing is wrong — the shared conformance guard (
@objectstack/route-envelope-conformance) proves all six agree today, which is exactly why this is a cleanup rather than a bug. But the guard proves agreement; it does not create it. A seventh module starts by copying the pair again.This is the same argument #3843 made about the scan one layer over: three copies was the signal it wanted lifting. Six copies of the builder is the same signal.
The i18n half of it
i18n-service-plugin.tsis the one module still building success bodies inline, once per read route:Those bodies are correct —
packages/runtime/src/i18n-success-envelope.conformance.test.tsdrives them — so this is not the envelope drift #3843 was about. It is a weaker guard: a fifth read route could get the shape wrong and only a driven test would notice. #3843 pinned it as an explicit ratchet inservice-i18n/src/error-envelope.conformance.test.ts:which fails on any new inline body and drops to the default
2 / 1the moment the four are consolidated.What needs deciding
sendOktoi18n-service-plugin.ts, delete the ratchet, all six modules then pass the shared guard on its defaults. Small, mechanical, no cross-package movement.packages/spec; the thing that writes it could be too.sendOk/sendErrorneed nothing but a{ status(n): this; json(body): unknown }shape, so a structurally-typed pair has no dependencies at all and every module can import it. The blocker is placement, not design:packages/specis schemas-only (Prime Directive ✨ Set up Copilot instructions #2), and the callers spanrest+ fourservices/*, which rules out anything that depends on them.Recommendation: 1, then 2. (1) is worth doing regardless — it retires a ratchet and makes the guard uniform. (2) is the real consolidation, and it wants the placement question answered deliberately rather than by whoever writes the seventh module. Worth sequencing after #3841/#3842, since a shared builder is the natural place to enforce a
codevocabulary once one is chosen.Adjacent, one line, noted here rather than filed twice
admin-routes.tsanswersdatasource_admin_unavailableon three routes whose missing dependency is the external-datasource service, not the admin one (GET /:name/remote-tables,POST /:name/test,POST /:name/object-draftall resolveexternalService()and then call the sharedunavailable()helper). Pre-existing, untouched by #3843 — which deliberately carried every code string over verbatim — and a natural pickup for whoever does #3841's sweep.