Skip to content

Commit c35cfdb

Browse files
os-zhuangclaude
andauthored
refactor(service-i18n): collapse the four inline success builders behind a sendOk, retire the ratchet (#3973) (#4023)
#3973 option 1. No wire shape changes — this is about the guard, not the bodies. #3636 put the declared `{ success: true, data }` envelope on all three i18n read routes but built it inline in each, so one envelope half had four call sites while the error half had already been consolidated behind `sendError` (#3675). Those bodies were never wrong, which is exactly why this needed saying out loud rather than being left alone. `scripts/check-route-envelope.mjs` counts response write sites per module: a consolidated module sits at a fixed two however many routes it grows. This one sat at five with a declared ratchet, because a fifth read route could have hand-rolled a fourth-dialect body and only a driven test would have caught it — and a driven test only covers the routes that existed when it was written. The four builders now collapse into one `sendOk` and the module declares the same 2 / 1 / 1 as the other five. Guard goes 5/2/1 → 6/1/1 conformant/ratcheted/exempt. Also corrects a claim #3843 left in six suite headers. They said the repo-wide scan "found two immediately (#3973, #3983)" — but #3973 was not one of them: i18n's unconsolidated builder was already known from #3843's own survey. The two the scan actually surfaced were share-link-routes.ts and the dev-only hmr-routes.ts. The guard header carried the same conflation and is fixed alongside. Verified: guard 6/1/1 + self-test, spec tsc, check:docs (251 in sync), check:skill-docs, check-console-sha, pnpm lint, and — the one that matters for "no wire change" — runtime 901 passed, which includes i18n-success-envelope.conformance.test.ts driving those exact three routes. Plus i18n 62, rest 491, storage 220, settings 189, datasource 154. Empty changeset: releases nothing. Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1ea6bce commit c35cfdb

9 files changed

Lines changed: 73 additions & 24 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
---
3+
4+
Consolidate `service-i18n`'s four inline success builders behind a `sendOk`, and
5+
retire the route-envelope ratchet that pinned them (#3973 option 1). Deliberately
6+
empty frontmatter: **no wire shape changes**, so there is nothing for a consumer to
7+
read in a CHANGELOG.
8+
9+
#3636 put the declared `{ success: true, data }` envelope on all three i18n read
10+
routes, but built it inline in each — four call sites for one envelope half, while
11+
the error half had already been consolidated behind `sendError` (#3675). Those
12+
bodies were never wrong; the problem was the guard.
13+
14+
`scripts/check-route-envelope.mjs` counts response write sites per module, so a
15+
consolidated module sits at a fixed two however many routes it grows. This one sat
16+
at five with a declared ratchet, because a fifth read route could have hand-rolled
17+
a fourth-dialect body and only a driven test would have caught it — and a driven
18+
test only covers the routes that existed when it was written. The four builders now
19+
collapse into one, and the module declares the same `2 / 1 / 1` as the other five.
20+
21+
Guard: **6 conformant / 1 ratcheted / 1 exempt**, down from 5 / 2 / 1. The remaining
22+
ratchet is `share-link-routes.ts` (#3983), which needs its own consumer sweep.
23+
24+
Also corrects a claim #3843 left in six suite headers: it said the repo-wide scan
25+
"found two immediately (#3973, #3983)". #3973 was not one of them — i18n's
26+
unconsolidated builder was already known — the two it actually surfaced were
27+
`share-link-routes.ts` and the dev-only `hmr-routes.ts`.

packages/rest/src/external-datasource-envelope.conformance.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
* `pnpm check:route-envelope` in CI. It sits outside any package on purpose: the
2626
* three predecessors of that scan were per-package, which structurally cannot
2727
* notice a route module nobody thought to convert, and two such modules turned up
28-
* the moment it went repo-wide (#3973, #3983).
28+
* the moment it went repo-wide: `share-link-routes.ts` (#3983) and the dev-only
29+
* `hmr-routes.ts`, neither of them in #3843's hand-written survey.
2930
*
3031
* What stays here is the half that has to live next to the routes it drives:
3132
* every branch driven, every body parsed against the real spec schemas.

packages/services/service-datasource/src/__tests__/envelope.conformance.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
* `pnpm check:route-envelope` in CI. It sits outside any package on purpose: the
2828
* three predecessors of that scan were per-package, which structurally cannot
2929
* notice a route module nobody thought to convert, and two such modules turned up
30-
* the moment it went repo-wide (#3973, #3983).
30+
* the moment it went repo-wide: `share-link-routes.ts` (#3983) and the dev-only
31+
* `hmr-routes.ts`, neither of them in #3843's hand-written survey.
3132
*
3233
* What stays here is the half that has to live next to the routes it drives:
3334
* every branch driven, every body parsed against the real spec schemas.

packages/services/service-i18n/src/error-envelope.conformance.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
*
2323
* That move mattered more than deduplication. A per-package scan structurally
2424
* cannot notice a module nobody thought to convert, and going repo-wide found two
25-
* immediately (#3973, #3983). It also dropped the regex: the old block stripped
25+
* immediately — `share-link-routes.ts` (#3983) and the dev-only `hmr-routes.ts`,
26+
* neither of them in #3843's hand-written survey. It also dropped the regex: the old block stripped
2627
* comments with `String.replace`, which ate `//` inside string literals and
2728
* truncated the rest of that line — response writes included — and counted
2829
* `c.req.json()` (a request READ) as an unenveloped response. The AST has neither

packages/services/service-i18n/src/i18n-service-plugin.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ function sendError(res: IHttpResponse, status: number, code: string, message: st
3030
res.status(status).json({ success: false, error: { code, message } });
3131
}
3232

33+
/**
34+
* Emit a success body in the DECLARED envelope — `BaseResponseSchema`
35+
* (`packages/spec/src/api/contract.zod.ts`), i.e. `{ success: true, data }`.
36+
*
37+
* #3636 put the right shape on all three read routes, but built it inline in each
38+
* one — four call sites for one envelope half, while the error half had already
39+
* been consolidated behind `sendError` (#3675). Those bodies were never wrong;
40+
* this is about the GUARD, not the wire.
41+
*
42+
* `scripts/check-route-envelope.mjs` counts response write sites per module, so a
43+
* consolidated module sits at a fixed two no matter how many routes it grows. This
44+
* one sat at five with a declared ratchet (#3973) precisely because a fifth read
45+
* route could have hand-rolled a fourth-dialect body and only a driven test would
46+
* have caught it — and a driven test only covers the routes that existed when it
47+
* was written. Collapsing the four inline builders into this one retires that
48+
* ratchet: the module now declares the same `2 / 1 / 1` as the other five.
49+
*/
50+
function sendOk(res: IHttpResponse, data: unknown): void {
51+
res.json({ success: true, data });
52+
}
53+
3354
/**
3455
* Configuration options for the I18nServicePlugin.
3556
*/
@@ -187,7 +208,7 @@ export class I18nServicePlugin implements Plugin {
187208
// copy is what let them answer in different shapes (#3833's lesson,
188209
// one route over).
189210
const locales = toLocaleDescriptors(i18n.getLocales(), i18n.getDefaultLocale?.() ?? 'en');
190-
res.json({ success: true, data: { locales } });
211+
sendOk(res, { locales });
191212
} catch (error: any) {
192213
sendError(res, 500, 'INTERNAL', error?.message ?? 'Internal error');
193214
}
@@ -202,7 +223,7 @@ export class I18nServicePlugin implements Plugin {
202223
return;
203224
}
204225
const translations = i18n.getTranslations(locale);
205-
res.json({ success: true, data: { locale, translations } });
226+
sendOk(res, { locale, translations });
206227
} catch (error: any) {
207228
sendError(res, 500, 'INTERNAL', error?.message ?? 'Internal error');
208229
}
@@ -223,7 +244,7 @@ export class I18nServicePlugin implements Plugin {
223244
if (hasGetFieldLabels) {
224245
const labels = (i18n as II18nService & { getFieldLabels(obj: string, loc: string): Record<string, string> })
225246
.getFieldLabels(objectName, locale);
226-
res.json({ success: true, data: { object: objectName, locale, labels } });
247+
sendOk(res, { object: objectName, locale, labels });
227248
} else {
228249
// Fallback: read field labels out of the locale's translation data.
229250
// That data is NESTED (`objects.<obj>.fields.<field>.label`) — the
@@ -234,7 +255,7 @@ export class I18nServicePlugin implements Plugin {
234255
// drift out of shape again the way it did after that fix (#3833).
235256
const data = i18n.getTranslations(locale) as TranslationData | undefined;
236257
const labels = resolveObjectFieldLabels(data, objectName);
237-
res.json({ success: true, data: { object: objectName, locale, labels } });
258+
sendOk(res, { object: objectName, locale, labels });
238259
}
239260
} catch (error: any) {
240261
sendError(res, 500, 'INTERNAL', error?.message ?? 'Internal error');

packages/services/service-settings/src/envelope.conformance.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
* `pnpm check:route-envelope` in CI. It sits outside any package on purpose: the
2525
* three predecessors of that scan were per-package, which structurally cannot
2626
* notice a route module nobody thought to convert, and two such modules turned up
27-
* the moment it went repo-wide (#3973, #3983).
27+
* the moment it went repo-wide: `share-link-routes.ts` (#3983) and the dev-only
28+
* `hmr-routes.ts`, neither of them in #3843's hand-written survey.
2829
*
2930
* What stays here is the half that has to live next to the routes it drives:
3031
* every branch driven, every body parsed against the real spec schemas.

packages/services/service-storage/src/error-envelope.conformance.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
*
2727
* That move mattered more than deduplication. A per-package scan structurally
2828
* cannot notice a module nobody thought to convert, and going repo-wide found two
29-
* immediately (#3973, #3983). It also dropped the regex: the old block stripped
29+
* immediately — `share-link-routes.ts` (#3983) and the dev-only `hmr-routes.ts`,
30+
* neither of them in #3843's hand-written survey. It also dropped the regex: the old block stripped
3031
* comments with `String.replace`, which ate `//` inside string literals and
3132
* truncated the rest of that line — response writes included — and counted
3233
* `c.req.json()` (a request READ) as an unenveloped response. The AST has neither

packages/services/service-storage/src/success-envelope.conformance.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
*
4444
* That move mattered more than deduplication. A per-package scan structurally
4545
* cannot notice a module nobody thought to convert, and going repo-wide found two
46-
* immediately (#3973, #3983). It also dropped the regex: the old block stripped
46+
* immediately — `share-link-routes.ts` (#3983) and the dev-only `hmr-routes.ts`,
47+
* neither of them in #3843's hand-written survey. It also dropped the regex: the old block stripped
4748
* comments with `String.replace`, which ate `//` inside string literals and
4849
* truncated the rest of that line — response writes included — and counted
4950
* `c.req.json()` (a request READ) as an unenveloped response. The AST has neither

scripts/check-route-envelope.mjs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030
* #3675 / #3689 shipped this as a regex block copied into each converted
3131
* package. Three copies was the signal it wanted lifting (#3843 option 3), and
3232
* lifting it to a repo-wide scan buys the thing per-package copies structurally
33-
* cannot: **a module nobody thought to convert still gets audited.** Both
34-
* modules in the RATCHET table below were found that way — neither is in
35-
* #3843's hand-written survey.
33+
* cannot: **a module nobody thought to convert still gets audited.** Two modules
34+
* in the table below were found exactly that way, neither of them in #3843's
35+
* hand-written survey — `share-link-routes.ts` (ratcheted, #3983) and
36+
* `hmr-routes.ts` (exempt).
3637
*
3738
* A module discovered by the scan but absent from the table is an ERROR, not a
3839
* default: silently applying `2 / 1 / 1` to an unknown module would let a new
@@ -97,6 +98,10 @@ const MODULES = {
9798
'packages/services/service-datasource/src/admin-routes.ts': { responses: 2, ok: 1, err: 1 },
9899
'packages/rest/src/external-datasource-routes.ts': { responses: 2, ok: 1, err: 1 },
99100
'packages/rest/src/package-routes.ts': { responses: 2, ok: 1, err: 1 },
101+
// Consolidated by #3973: #3636 put the right envelope on its three read routes
102+
// but built it inline in four places, so this module carried a ratchet at 5 / 4 / 1
103+
// until those collapsed behind a `sendOk`.
104+
'packages/services/service-i18n/src/i18n-service-plugin.ts': { responses: 2, ok: 1, err: 1 },
100105

101106
// ── Exempt ──────────────────────────────────────────────────────────────
102107

@@ -114,17 +119,7 @@ const MODULES = {
114119
exempt: 'dev-only SSE endpoint (/api/v1/dev/*), not on the SDK surface',
115120
},
116121

117-
// ── Ratchets: real, tracked, NOT blessed ────────────────────────────────
118-
119-
// The error half IS consolidated behind `sendError` (#3675). The success half
120-
// is not: each of four read routes builds `{ success: true, data }` inline.
121-
// Those bodies are CORRECT — `packages/runtime/src/i18n-success-envelope
122-
// .conformance.test.ts` drives them — so this is not envelope drift. It is an
123-
// unconsolidated builder, i.e. a weaker guard: a fifth read route could get
124-
// the shape wrong and only a driven test would notice.
125-
'packages/services/service-i18n/src/i18n-service-plugin.ts': {
126-
responses: 5, ok: 4, err: 1, ratchet: '#3973',
127-
},
122+
// ── Ratchet: real, tracked, NOT blessed ─────────────────────────────────
128123

129124
// Found BY THIS SCAN, absent from #3843's survey — the fifth drifting module.
130125
// `sendError` already nests `{ code, message }` (that is why #3675's changeset

0 commit comments

Comments
 (0)