Repository navigation
docs(email): three shipped carriers said 'best-matching locale'; the resolver matches (name, locale) exactly - #19504
Conversation
…resolver matches (name, locale) exactly Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 3 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3c2fdc15de404cbeec0110c028e203f98769ad19 && git checkout 3c2fdc15de404cbeec0110c028e203f98769ad19
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cac3636cab1cecef8f7a0453e4963dd1d01fb21 380636ac44cfa984e26d71c332acd74da2da612a && git checkout -B drift-repro 2cac3636cab1cecef8f7a0453e4963dd1d01fb21 && git merge --no-ff 380636ac44cfa984e26d71c332acd74da2da612a
node scripts/docs-audit/affected-docs.mjs --json 2cac3636cab1cecef8f7a0453e4963dd1d01fb21 |
Fixes #18499
Clause-②: no
Three shipped carriers still told readers that the EmailService "picks the best-matching locale" — the exact sentence
packages/plugins/plugin-email/src/template-loader.tsenumerates as a false declaration. This PR replaces that prose with what the resolver does. No resolution code changes;packages/specis zero files in this diff.1. The resolver, read at this branch's base (
4045781fa) — the criterion for every rewriteCard line numbers were not trusted; every reading below was taken on today's head.
packages/plugins/plugin-email/src/template-loader.ts— the loader (base lines 110-118, this branch's lines 119-124):with the two order lists it pins the row by (lines 80, 83-86):
packages/plugins/plugin-email/src/email-service.ts—resolveAndRenderTemplate, the ladder above the loader (lines 1317-1332, unchanged by this PR):DEFAULT_TEMPLATE_LOCALEis the literal'en-US'(email-service.ts:463).So, mechanically:
{ name, locale }match, then exactly one retry at the literal'en-US', thenTEMPLATE_NOT_FOUND.if (!row && !preferred)gates the third rung out for it;'en-US'(the ladder passeswanted, and the loader's own no-locale branch also queries'en-US'first), and only when the bundle carries noen-USrow at all does it take the bundle's lowest locale tag, ordered;2. Carrier by carrier: before, after, and the line that decides it
(a)
packages/platform-objects/src/audit/sys-email-template.object.ts, base lines 10-11Before: "Resolved by
(name, locale); the EmailService picks the best-matching locale for the recipient, falling back toen-US."After (head lines 10-18): resolved by an EXACT
(name, locale)match, no language-subtag folding, one literalen-USrung for a call that names a locale and thenTEMPLATE_NOT_FOUND; a call naming none starts aten-USand falls to the bundle's lowest tag only when noen-USrow exists.Decided by:
template-loader.ts:120(if (locale) return first({ name, locale }, BY_ID)) plusemail-service.ts:1320-1323(the single retry, gated off the third rung by!preferred).(b)
packages/services/service-messaging/src/objects/notification-template.object.ts, base line 65Before: "both resolve a template by best-matching locale."
After (head lines 63-71): the 16-char BCP-47 BOUND is shared with
sys_email_template; the RESOLUTION is not, and neither side picks a best match.NotificationTemplateStorewalks a fixed candidate list;sys_email_templatematches exactly and folds nothing.Decided by:
packages/services/service-messaging/src/template-renderer.ts:131-140,localeCandidates— it pushes the named tag, thenlocale.split('-')[0], thenDEFAULT_LOCALE(template-renderer.ts:9, the literal'en'). That sentence was false twice over: not only is there no best match, the two resolvers do not behave alike — this one DOES fold a primary subtag, and the email one never does.(c)
docs/qa/platform-checklist/areas/integration-system.json, line 818 (itemintegration-system.email-template-render, acceptance clause 4)Before: "(name, locale) resolution picks the best locale row and falls back to en-US — two rows with one name are an i18n bundle, both reachable by recipient locale".
After: an EXACT match with no language-subtag folding; each bundle row reachable only from its own exact tag; a named locale with no row retries the single literal rung en-US and then raises TEMPLATE_NOT_FOUND; a call naming no locale starts at en-US. The clause's
verifyline was corrected with it — "the unmatched recipient gets the fallback" now names what unmatched means and what must NOT render (azhrecipient gets the en-US row, not the zh-CN one), because the old wording would have ticked either outcome. Itemrevision4 goes to 5 with ahistoryentry, perdocs/qa/platform-checklist/README.md.Decided by: the same two code sites as (a).
3. A fourth file is edited, and why it is not scope creep
packages/plugins/plugin-email/src/template-loader.ts(the authority text) quoted carrier (a) verbatim in its "What was wrong" block, so correcting (a) would have stranded that quotation — the exact defect the card records againstpackages/metadata-core/src/item-key-discriminators.ts. Measured while making the edit: one of the block's three bullets was ALREADY stale at this base. It attributes toEmailTemplateDefinitionSchema.localethe sentence "the service picks the best match for the recipient's locale, falling back toen-US", andgrep -nE "best match|best-matching" packages/spec/src/system/email-template.zod.tsexits 1 — zero occurrences. Both bullets are now cited rather than quoted at length, so a later rewording cannot strand them again. Nopackages/specfile is touched.Note for the seat: the dispatch named
template-loader.tsas one of "the three carriers" and did not namedocs/qa/platform-checklist/areas/integration-system.json. The card body and the triage comment name the other way round — the checklist JSON is a carrier,template-loader.tsis the contract text. This PR follows the card and the triage comment, and editstemplate-loader.tsas a consequence of (a) rather than as a carrier, which is why the diff is four files rather than three.4. Verification
Base
4045781faff6ef4811b7438aee009be750d7cdde; readings below taken on this branch.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackover the real change set, reconciled with--ran: 58 derived, 57 run and green, 1 NOT MEASURED, 0 unrun. The one NOT MEASURED ispnpm check:dual-build-cjs-loads, exit 3 (PREREQUISITE NOT MET — it needs a whole-repopnpm build; 43-plus packages outside this diff's closure have nodist/). That is CI's run, not a finding and not a pass.pnpm check:platform-checklist— exit 0 (the gate that ratchets the checklist item'srevisionagainst itshistory).pnpm check:i18nandpnpm check:i18n-stale-fill— exit 0, after building the closure the gate names. Nolabel,descriptionorhelpstring is touched by this diff, so no translation bundle moves.turbo run buildover@objectstack/platform-objects,@objectstack/plugin-email,@objectstack/service-messagingand their dependency closure — 19 tasks successful; then the i18n gate's own closure — 57 tasks successful.turbo run typecheck test— 23 tasks successful. platform-objects 47 files / 668 tests, service-messaging 44 files / 479 tests, plugin-email 30 files / 468 tests, all passed.pnpm lint(the repo-wideeslint . --no-inline-config) — exit 0. Run whole, so no narrowing needs declaring.grep -naPover the C0 set plus DEL, exit 1 (no match).scripts/pm/os-verify-lock.shwith a stable slot; every verdict read off itsVERDICT command-exitline.Pin tests on the three sentences: there are none. Probed
best-matching locale|picks the bestrepo-wide (12 hits, of which 6 are CHANGELOG history and one is the prior round's changeset) and grepped the test files that name these two object modules —sys-email-template.organization-unique.test.ts,managed-by-system-data.test.ts,notification-keyed-text-bounds.test.ts— none asserts docblock or comment text.docs/qa/platform-checklist/coverage.jsonreferences the item by id only, and the id is unchanged. No new verification surface was invented to stand in for the absent pins.5. Acceptance notes
packages/metadata-core/src/item-key-discriminators.ts:7-11still quotesEmailTemplateDefinitionSchemaas saying the service "picks the best match for the recipient's locale, falling back toen-US" — a sentence with zero occurrences inpackages/spec/src/system/email-template.zod.tstoday. The card records this deliberately as a stale quotation and explicitly not a second instance of the false claim, so it is left untouched here rather than widening the diff intodomain:engine's lane. Reported for the seat.packages/plugins/plugin-email/src/email-service.ts:465-467, theTemplateLoaderinterface docblock, opens with "Returns the best-matching row for(name, locale)" and then, two lines later, states the exact-match rule correctly. It is the same false phrase family; it escaped the card's probe only because it reads "best-matching row" rather than "best-matching locale". Left out of this diff because the card enumerated its carrier set from that probe and this file is not in it. Reported for the seat.variants[0]already read "exact (name, locale) match / en-US fallback", i.e. the item contradicted itself between its variants list and its acceptance clause. Only the clause was wrong; the variant is left as it stands.Generated by Claude Code