Skip to content

Commit 642da7f

Browse files
committed
docs(spec): scope the surviving one-rung claims to a call that names a locale
Text-only remediation on the contract-review record for this PR. No logic line moves: both .ts files reprint byte-identical through the TypeScript printer with removeComments (stripped sha256 equal before and after), and a dark control on a pair that really does move code reads DIFFERENT, so the instrument is not blind. - .changeset/email-template-locale-floor.md (pending and unreleased, from #17884): "retries exactly one rung - the literal en-US" and "the resolver's sole retry rung" were unscoped, and would have compiled verbatim into the published packages/spec CHANGELOG.md beside this PR's correction of them -- the erratum-in-a-later-entry form AGENTS.md forbids. Both are now scoped to a call that NAMES a locale, with a pointer to SendTemplateInput.locale for the full ladder. The same file's "the single literal en-US rung" is scoped too. - stack-email-template-locale-floor.test.ts: the header sentence now scopes the one-rung claim and names the no-locale case; the title line's bare "no fallback floor" is scoped the same way. - email-template-floor-locale-parity.pin.test.ts: "its single retry rung" now names both rungs and which call shape reaches each. - .changeset/18056-email-template-locale-rungs.md: states that the guard's emitted warning TEXT changed and now names both call shapes, keeping "control flow is unchanged" and dropping any byte-for-byte claim. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 708595d commit 642da7f

4 files changed

Lines changed: 36 additions & 24 deletions

File tree

.changeset/18056-email-template-locale-rungs.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ best match and no folding, only the ladder above.
4040
`defineStack`'s `warnEmailTemplateLocaleFloor` gains a declaration of the two
4141
shapes it deliberately does NOT examine (a stack whose `i18n.supportedLocales`
4242
is absent or empty; a bundle whose tags all fall outside `supportedLocales`) —
43-
both can still ship a floorless bundle. Its logic is unchanged and its warning
44-
stays advisory; whether either shape should warn is the ADR-0049
45-
enforce-or-remove question and is not answered here. Both shapes are now pinned
46-
against a warning discriminator so neither can change without a test saying so.
43+
both can still ship a floorless bundle. Its control flow is unchanged — the same
44+
bundles warn, once each, and the warning stays advisory — but the emitted warning
45+
TEXT did change, and now names BOTH call shapes: it says the bundle has no
46+
fallback floor *for a send that names a locale*, and adds that a send naming NO
47+
locale does not fail but drops to that bundle's lowest tag and renders it
48+
silently. A test asserting on the old wording needs updating. Whether either
49+
undeclared shape should warn is the ADR-0049 enforce-or-remove question and is
50+
not answered here. Both shapes are now pinned against a warning discriminator so
51+
neither can change without a test saying so.

.changeset/email-template-locale-floor.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44

55
Email templates: say where the `en-US` fallback floor is, and report a bundle that has none.
66

7-
`IEmailService.sendTemplate` matches `(name, locale)` exactly and retries exactly one rung —
8-
the literal `en-US`. There is no language-subtag folding, so a bundle whose English row is
9-
tagged `en` is unreachable from `en-US` and from every other tag it does not itself carry;
10-
each such delivery raises `TEMPLATE_NOT_FOUND`, which classifies permanent, so it dead-letters
11-
with no retry. An app declaring `i18n.defaultLocale: 'en'` and authoring `locale: 'en'` has
12-
done the consistent thing throughout and still shipped a bundle with no floor — and it
13-
validated, built and installed clean.
7+
`IEmailService.sendTemplate` matches `(name, locale)` exactly and, for a call that NAMES a
8+
locale, retries exactly one rung — the literal `en-US` — and stops. There is no language-subtag
9+
folding, so a bundle whose English row is tagged `en` is unreachable from `en-US` and from every
10+
other tag it does not itself carry; each such delivery raises `TEMPLATE_NOT_FOUND`, which
11+
classifies permanent, so it dead-letters with no retry. An app declaring
12+
`i18n.defaultLocale: 'en'` and authoring `locale: 'en'` has done the consistent thing throughout
13+
and still shipped a bundle with no floor for those calls — and it validated, built and installed
14+
clean.
1415

1516
- `EmailTemplateDefinitionSchema.locale`'s `describe` and TSDoc now state the exact match, the
16-
single literal `en-US` rung, the absence of folding, and that the stack's own declared default
17-
locale is the wrong tag whenever it is not spelled `en-US`.
17+
one literal `en-US` rung a call that NAMES a locale gets, the absence of folding, and that the
18+
stack's own declared default locale is the wrong tag whenever it is not spelled `en-US`.
1819
- New exported `EMAIL_TEMPLATE_FLOOR_LOCALE` names that tag once: it is both the schema default
19-
and the resolver's sole retry rung.
20+
and the rung `sendTemplate` retries for a call that NAMES a locale. The full ladder — including
21+
the lowest-tag rung reachable only by a call that names NO locale — is on
22+
`SendTemplateInput.locale` in `packages/spec/src/contracts/email-service.ts`.
2023
- `defineStack` now reports (advisory `console.warn`, warn-once per bundle) an `emailTemplates`
2124
bundle that carries rows for the stack's own `i18n.supportedLocales` but none tagged `en-US`.
2225

packages/spec/src/stack-email-template-locale-floor.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22
//
33
// #17614 — an `emailTemplates` bundle tagged with the stack's OWN
4-
// `i18n.defaultLocale` has no fallback floor.
4+
// `i18n.defaultLocale` has no fallback floor for a send that names a locale.
55
//
66
// Measured before this landed, on an unmodified tree: a stack declaring
77
// `defaultLocale: 'en'`, `supportedLocales: ['en','zh-CN','ja-JP','es-ES']`
@@ -10,10 +10,13 @@
1010
// PERMANENT, so the delivery dead-letters with no retry — for `de-DE` and for
1111
// the literal `en-US`. The identical bundle with its English row tagged
1212
// `en-US` delivered for `de-DE`. `sendTemplate` matches `(name, locale)`
13-
// exactly and retries exactly one rung, the literal `en-US`; there is no
14-
// language-subtag folding, and that ladder's shape is a settled ruling this
15-
// change deliberately does not touch. The remedy is the bundle, so the
16-
// diagnostic is where the author is standing.
13+
// exactly and, for a call that NAMES a locale, retries exactly one rung — the
14+
// literal `en-US` — and stops; there is no language-subtag folding. A call that
15+
// names NO locale is the other case and does NOT dead-letter: it starts at
16+
// `en-US` by name and, when the bundle carries no `en-US` row, drops to that
17+
// bundle's lowest locale tag and renders it silently. That ladder's shape is a
18+
// settled ruling this change deliberately does not touch. The remedy is the
19+
// bundle, so the diagnostic is where the author is standing.
1720
//
1821
// These pin the diagnostic ADVISORY: every case asserts the parse still
1922
// succeeds and the stack comes back unchanged. The diagnostic narrows what

packages/spec/src/system/email-template-floor-locale-parity.pin.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@
2929
* and turbo's input hashing both see the escape.
3030
*
3131
* ⛔ Scope: the VALUE, not the resolver. This asserts two literals agree. It
32-
* asserts nothing about the ladder's shape — the exact `(name, locale)` match
33-
* and its single retry rung are a settled ruling, and changing them is not this
34-
* pin's business. Renaming either constant turns this red on purpose: the
35-
* docblock names `DEFAULT_TEMPLATE_LOCALE` specifically, so a rename is an edit
36-
* to the published claim and has to be made in both places.
32+
* asserts nothing about the ladder's shape — the exact `(name, locale)` match,
33+
* the one retry rung a call that NAMES a locale gets, and the lowest-tag rung
34+
* reachable only by a call that names NO locale are a settled ruling, and
35+
* changing them is not this pin's business. Renaming either constant turns this
36+
* red on purpose: the docblock names `DEFAULT_TEMPLATE_LOCALE` specifically, so
37+
* a rename is an edit to the published claim and has to be made in both places.
3738
*
3839
* ⛔ A missing or unreadable declaration is a FAILURE, never a silent pass —
3940
* that is the whole failure mode a text-reading pin has to defend against.

0 commit comments

Comments
 (0)