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
{{ message }}
Repository navigation
Auth emails are always en-US: no send names a locale and sys_user has no locale column, so localized template rows can never be selected #8195
Found while implementing #8019 (old-address change-email notice). Filed unassigned; triage grades it. Not a defect in that PR — it is the surface that PR landed on.
Measured
EmailService.sendTemplate resolves templates by (name, locale) and honours an explicit input.locale; with none named it resolves DEFAULT_TEMPLATE_LOCALE (en-US) — deliberately, per #7731. Two facts make every other locale row unreachable through the platform's own send path:
No auth send names a locale. Every sendTemplate call in packages/plugins/plugin-auth/src/auth-manager.ts (password reset, verification, MCP, and the new change-email notice) omits locale.
There is no per-recipient locale to name.packages/platform-objects/src/identity/sys-user.object.ts declares no locale / language field, so nothing on the user row expresses a preference.
Consequence: a zh-CN deployment receives English password-reset, verification and invitation mail. Before #8019 this was invisible because every built-in auth template was en-US-only — packages/plugins/plugin-email/src/templates/auth-templates.ts had exactly one row per template, and in fact no template anywhere in the repo shipped a non-en-US row.
Why it surfaces now
The #8019 ruling scoped its new template to all four supported locales, so auth.email_change_notice ships as four rows (en-US, zh-CN, ja-JP, es-ES). Three of them are correct, seeded, and currently unselectable by the platform itself — they become live the moment a caller or a tenant overlay names a locale. That PR deliberately did not wire a locale resolution, because making one template locale-aware while the other five stay en-US-only manufactures exactly the mixed-language artefact II18nService.setSupportedLocales' docstring describes: a user would get a Chinese "your email is being changed" notice and an English "verify your email" in the same flow.
Two halves, gradeable separately
Dormant surface (observation-class): three locale rows the platform cannot select today.
User-visible gap (concrete): outbound auth mail is English-only on a platform that supports four locales and whose UI already switches.
What a fix would need to decide
Where recipient locale comes from — a sys_user column, the deployment default (II18nService.getDefaultLocale(), reachable from the plugin layer), or the request's Accept-Language. This is a product decision, not a lookup.
Whether the other five auth templates get localized in the same change. Localizing the resolution without localizing the templates just moves the mixed-language problem.
Related
#8019 / the change-email notice PR (the four-locale template that exposed this) · #7731 (why a no-locale send resolves en-US rather than an arbitrary row) · #7801 (row locale vs. render-filter locale) · #5419 (the console's mirror of this on the UI side)
Triage ⇒ needs-user-decision, type Feature, routed domain:identity (the send calls live in plugin-auth/src/auth-manager.ts; the column option would land in platform-objects; templates in plugin-email — split per lane after the ruling). This is a product decision, not a lookup: every option either expands the public face or accepts a user-visible gap.
Background
Measured in the card: no auth send names a locale; sys_user has no locale column; so the four-locale template rows #8019 shipped are unselectable by the platform itself, and a zh-CN deployment gets English password-reset/verification mail while the UI switches languages.
Premises (re-check before ruling if the card sits)
git grep -n "sendTemplate" origin/main -- packages/plugins/plugin-auth/src/auth-manager.ts — still no locale argument at any call site (non-zero hits expected; zero hits ⇒ premise moved).
git grep -n -i "locale\|language" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts — still no per-user locale field (neighbour-check the file with git grep -n "email" … sys-user.object.ts).
Question
Where does the recipient locale for outbound auth mail come from — and are the five remaining en-US-only auth templates localized in the same change?
Options
A — per-user column on sys_user (locale/language). Most correct long-term; expands a shipped schema, needs a precedence design (user > org > deployment), permanent maintenance obligation.
B — deployment default via II18nService.getDefaultLocale() at the plugin layer. No schema change; fixes the whole class for single-locale deployments (the common case today); per-user granularity deferred.
C — request Accept-Language. No schema change, but auth mail is often sent outside the triggering user's request context (invitations, admin-initiated resets), and locale flaps per device.
D — defer entirely (leave English-only).
Recommendation
B now, and localize the remaining five auth templates in the same change — a partial localization manufactures the mixed-language flow II18nService.setSupportedLocales' docstring warns about. Park A as a direction card (v18-class) with the precedence design, rather than adding a schema column ad hoc.
Four-prong:
Platform long-term coherence — B consumes an already-declared service value; A adds a durable schema key that deserves a one-time precedence design, not a rider.
Measured business pull — real and user-visible (localized deployments receive English credential mail; UI already switches), but no measured pull for per-user granularity yet.
AI-agent error-resistance — B is a closed single-source read; A creates a new per-send seam every future template author can forget. Localizing all six templates together removes the mixed-language trap.
Startup scope discipline — B is the smallest change that closes the user-visible gap; A/C expand scope; D leaves a shipped four-locale surface permanently dormant.
Downstream note: no open card carries a Blocked-by: pointing here today.
Maintainer ruling — deployment default locale, all templates in one batch
Ruled by the maintainer in a live PM session, 2026-08-13 (session session_015XyMgCSMGGn9oSwbQEeWYg), verbatim: 「其他全部接受你的建议。」 — accepting the recommendation as presented.
Ruling:
Recipient locale resolves to the deployment default (II18nService.getDefaultLocale(), reachable from the plugin layer) — a zh-CN deployment sends zh-CN auth mail across the board. Accept-Language is rejected (mail is async; the request context is often absent or wrong).
A per-user sys_user.locale column is deferred until measured pull (a real deployment with a mixed-locale user population). It is a new public surface and a standing obligation on every write path; when it comes, it layers on top of this resolution as an override, so nothing shipped now is thrown away.
Claiming this card and starting implementation against the 2026-08-13 ruling (deployment-default locale via II18nService.getDefaultLocale(); all auth templates localized in the same change; no sys_user.locale column).
Session:1454f3a3-b771-5eef-8695-7fcac193a629
Branch:claude/issue-8195-auth-mail-locale
File surface (intended):packages/plugins/plugin-auth/src/auth-manager.ts (locale resolution + the five sendTemplate call sites), packages/plugins/plugin-email/src/templates/auth-templates.ts (the remaining auth templates localized to the four supported locales), plus tests and a changeset.
Premise re-checks against origin/main, run before any edit — both hold:
git grep -n "sendTemplate" origin/main -- packages/plugins/plugin-auth/src/auth-manager.ts → 6 hits (5 real call sites + 1 comment mention at :1192); no call site passes a locale argument.
git grep -n -i "locale\|language" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts → zero hits. Neighbour-check with email returns 30+ hits, so the file path is right and simply has no locale field.
Dev report — STOPPED at the cross-lane wall, no code written
Status: blocked (governance/routing, not product). The ruling is not in doubt and I am not re-adjudicating it. I stopped because implementing it as ruled requires editing a package in another seat's lane, and the PM instruction on this dispatch was explicit: stop and report rather than absorb it.
origin/main:packages/plugins/plugin-auth/src/auth-manager.ts:1140: const result = await email.sendTemplate({
origin/main:packages/plugins/plugin-auth/src/auth-manager.ts:1192: // Let send failures propagate (see above): sendTemplate THROWS on
origin/main:packages/plugins/plugin-auth/src/auth-manager.ts:1196: const result = await email.sendTemplate({
origin/main:packages/plugins/plugin-auth/src/auth-manager.ts:2415: await emailService.sendTemplate({
origin/main:packages/plugins/plugin-auth/src/auth-manager.ts:2546: await emailService.sendTemplate({
origin/main:packages/plugins/plugin-auth/src/auth-manager.ts:3061: await email.sendTemplate({
Non-zero as expected: 6 hits = 5 real call sites plus one prose mention at :1192. I read all five call-site option objects in full; none passes a locale argument. Templates named: auth.password_reset (1140), auth.verify_email (1196), auth.invitation (2415), auth.magic_link (2546), auth.email_change_notice (3061).
Zero hits, as expected. Neighbour-check git grep -n "email" … sys-user.object.ts returns 30+ hits (email field at :512, email_verified at :520, the change_my_email action at :330, unique index at :757), so the path is correct and the file simply carries no locale field. No sys_user schema change was made — nothing was written at all.
The cross-lane wall — hit, not absorbed
The lane map in .claude/skills/pm-dispatch/SKILL.md (the anchoring rule table, lines 184-190) assigns packages to exactly one domain each:
package
lane
plugin-auth
domain:identity (this card's label)
plugin-email
domain:services
The anchoring rule is "Every package belongs to exactly one domain", and the only boundary-crossing channel is spelled out as 「跨域例外路径(唯一的越界通道)」 — a cross-domain single PR designated by the triage seat (「由分诊座位指定一个车道 PM 认领」), whose claim comment declares the full file surface and which runs a targeted in-flight check. A dev agent cannot place itself on that path.
Template ownership is exclusive and confirmed, so the split cannot be dodged:
git grep -ln "AUTH_PASSWORD_RESET_TEMPLATE\|auth-templates" resolves only into plugin-email (templates/auth-templates.ts, email-plugin.ts, index.ts, one test).
plugin-auth's only hit is auth-manager.ts, which names templates as strings at the send sites; it defines none. plugin-auth/package.json does not even depend on @objectstack/plugin-email — it reaches the email service through the service registry.
So the ruled change is irreducibly two-lane:
domain:identity (mine): packages/plugins/plugin-auth/src/auth-manager.ts — resolve II18nService.getDefaultLocale() at the plugin layer, pass locale at the 5 call sites.
domain:services (not mine): packages/plugins/plugin-email/src/templates/auth-templates.ts — localize the 5 remaining en-US-only templates to 4 locales each (15 new rows).
Six auth templates exist; auth.email_change_notice already has all four locales (en-US/zh-CN/ja-JP/es-ES, from #8019). The other five — password_reset, verify_email, magic_link, invitation, two_factor_otp — are en-US-only. That matches the ruling's "remaining five" exactly.
Measured evidence that splitting is worse than the card assumed
Triage suggested "split per lane"; the ruling forbids it. I checked what a split would actually produce, and the ruling is right — with a sharper failure than the card describes. The ladder in packages/plugins/plugin-email/src/email-service.ts:1145-1160:
So shipping the identity half alone on a zh-CN deployment gives: auth.email_change_notice renders a genuine zh-CN row, while the other five miss, fall back to the en-US row body, yet still hand zh-CN to the render filters. The result is not merely mixed language across a flow — it is English prose carrying zh-CN-formatted dates and numbers inside one message, the precise artefact the comment at :1185-1190 says the row-locale authority exists to prevent. Resolution-without-templates is therefore actively harmful, not just incomplete. I did not ship it.
What I did NOT do — stated plainly, not filled in
No worktree was created, no file was edited, no branch pushed, no PR opened. Consequently the following dispatch asks have no result to report, and I am not fabricating them:
Ablation prediction-vs-reality table — not run (nothing to mutate).
Per-locale rendered-text pins + the negative case — not written. The design is sound and I recommend keeping it for whoever lands this: pin subject/body literals per locale from the template file as independent literals, plus a negative assertion that a zh-CN default renders none of the en-US subject text. It cannot be authored without the plugin-email half.
check:i18n — not run, nothing regenerated. It would have an opinion only once template rows change.
Gates run: none. Skipped in full — check:i18n, check:engine-double-contract, check:type-check-coverage, check:nul-bytes, pnpm test, pnpm typecheck, ESLint battery. All skipped because there is no diff to test.
Changeset — none.
Everything above is read-only inspection of origin/main via git show / git grep.
For the PM
The ruling and the lane map are both binding and they conflict on routing only, not on substance. This needs the cross-domain exception path designated by the triage seat, with a claim comment declaring the full two-package file surface above. In-flight input, offered with its limitation stated: a title-scoped search of open PRs found none naming plugin-email — that is not a file-surface in-flight check, and the real one (reading claim-comment file surfaces across domain:identity + domain:services) still needs running on that path.
Serial constraint noted for whoever lands it: PR #8432 (#8243) is in the merge queue on auth-manager.ts (import block, one after-hook call, one request-seam block) and PR #8417 (#8317) on auth-plugin.ts / index.ts. The five sendTemplate call sites are a different region, so a textual conflict is unlikely but origin/main should be merged before finalising.
Triage designation: cross-domain exception path (option A). One PR, claimed by the domain:services seat, spanning both packages. pm:blocked ⇒ pm:queue; lane label re-routed domain:identity ⇒ domain:services so the designated lane's inventory query finds it. Triage seat (whole-repo Routine), scheduled fire ~01:08Z 2026-08-16.
This card has been parked since 2026-08-13 waiting for exactly this designation — the dev stopped correctly at the lane wall, and the deadlock analysis above is adopted as-is:
The 2026-08-13 maintainer ruling forbids splitting (all templates localized in the same change as the resolution wiring).
The lane map forbids a dev crossing packages without a triage-designated exception.
Option C (split per lane) is disqualified by measurement, not by preference: shipping the identity half alone produces English bodies carrying zh-CN-formatted dates inside one message (the fallback ladder at email-service.ts:1145-1160 + :1200).
Designation (the only boundary-crossing channel, per protocol)
Claiming lane: domain:services — the larger surface is plugin-email (15 new template rows); the identity side is 5 call sites plus one resolution helper. Whole card, one PR, one claim.
Full file surface (must be declared verbatim in the claim comment — mandatory for a cross-domain exception card):
packages/plugins/plugin-email/src/templates/auth-templates.ts — localize password_reset, verify_email, magic_link, invitation, two_factor_otp to the four supported locales (the sixth, email_change_notice, already has all four).
packages/plugins/plugin-auth/src/auth-manager.ts — resolve II18nService.getDefaultLocale() at the plugin layer and pass locale at the 5 sendTemplate call sites (:1140, :1196, :2415, :2546, :3061 as of the 2026-08-13 reading — re-anchor on symbols, not line numbers).
Plus tests for both packages and a changeset.
Targeted in-flight check is required at claim time (this is the exception path's price): read the claim-comment file surfaces of any in-flight domain:identity AND domain:services cards; if either package intersects, yield and requeue. The serial notes above (auth-manager PRs from 08-13) are stale leads, not current readings — re-run the check against live claims.
Ruling carried (binding, not re-adjudicable): deployment-default locale via II18nService.getDefaultLocale(); all auth templates localized in the same change; Accept-Language rejected; no sys_user.locale column (deferred until measured pull).
Suggested acceptance pins from the stopped dev's report, worth keeping: per-locale rendered-text pins from independent literals, plus a negative assertion that a zh-CN default renders none of the en-US subject text.
Size/model suggestion: M, opus (implementation of a settled ruling; the template prose itself is the judgment surface).
⛔ This designation names the lane, not a dispatch — claim discipline (assign + Claim: comment + race re-read) applies unchanged when the services seat picks it up.
Claim — cross-domain exception path, per the triage designation of 2026-08-16 01:20Z. Implementing the settled 2026-08-13 maintainer ruling; ⛔ not re-adjudicating any of it.
Session:session_01Y26DJEHSBhhAQ6wwfsHNza
Branch:claude/issue-8195-auth-mail-locale (cut from origin/main @ f2f09e4e3, pushed empty before the first edit)
Re-read every comment on this card before starting. The earlier claim (1454f3a3-b771-5eef-8695-7fcac193a629, 2026-08-13) is not live: that session stopped at the lane wall with zero diff and zero branch and reported blocked, and the triage designation above is what released it. No other session holds this card.
Full file surface — declared verbatim, as the exception path requires
packages/plugins/plugin-auth/src/auth-manager.ts — resolve II18nService.getDefaultLocale() at the plugin layer, pass locale at the 5 sendTemplate call sites. Re-anchoring on symbols, not the card's 2026-08-13 line numbers.
Tests in both packages, plus a changeset.
Targeted in-flight check — run at claim time, result: no intersection
Read against live claims rather than the stale 08-13 serial notes, as the designation instructs.
packages/plugins/plugin-email/** — clear. No domain:services card carries pm:dispatched; every open one is pm:on-hold, needs-user-decision, or unclaimed pm:queue. None of the 7 open feature PRs touches the package.
packages/plugins/plugin-auth/** — clear, three cards checked individually:
claim D merged as PR #9069 at 11:19Z today; claims B/C are explicitly not dispatched (「⛔ 仍不派」), and their residual 18 sites do not include auth-manager.ts
File-surface amendment — same round, declared before the report. Session session_01Y26DJEHSBhhAQ6wwfsHNza, branch claude/issue-8195-auth-mail-locale, PR #9134 (draft).
One file was added to the surface I declared at claim time:
packages/plugins/plugin-auth/src/auth-plugin.ts — the plugin-layer read of II18nService.getDefaultLocale().
Why it was unavoidable rather than convenient
The triage designation folded the resolution into auth-manager.ts (「resolve II18nService.getDefaultLocale() at the plugin layer, pass locale at the 5 sendTemplate call sites」). Those are two different files in this package, and the ruling's own words are what split them: the plugin layer is where ctx.getService('i18n') is reachable, and AuthManager holds no kernel context.
The precedent is in the same two files already, and I followed it rather than inventing a shape — #2815's auth SMS locale: the setter setDefaultSmsLocale() lives in auth-manager.ts:3329, and the read that feeds it lives in auth-plugin.ts:729. The email locale now sits beside it in both places.
Against the in-place-fix conditions
Same package, same defect class as the card, mechanical, form pinned by an existing landed declaration, and — the one that matters for parallel serialization — my claim-time in-flight check was already run at package granularity, not file granularity: I checked packages/plugins/plugin-auth/** as a whole against every live claim, so the addition does not reach into anything the check did not already cover. No claim intersects it; re-verified just now, unchanged since claim time.
⛔ Nothing else moved. packages/platform-objects is untouched (no sys_user.locale column — the ruling defers it), and content/docs/releases/ is untouched.
Full amended surface:
packages/plugins/plugin-email/src/templates/auth-templates.ts — 15 new locale rows
packages/plugins/plugin-email/src/index.ts — exports for the new rows
packages/plugins/plugin-email/src/auth-templates-locales.test.ts — new
{
"issue": 8195,
"status": "done",
"branch": "claude/issue-8195-auth-mail-locale",
"pr": "https://github.com/objectstack-ai/objectstack/pull/9134",
"premise_still_valid": true,
"summary": "Both halves landed in one PR on the designated cross-domain exception path, implementing the 2026-08-13 ruling without re-adjudicating it. plugin-email: password_reset, verify_email, magic_link, invitation and two_factor_otp each gain zh-CN/ja-JP/es-ES rows (15 new rows), composed into per-template lists and seeded through BUILTIN_AUTH_TEMPLATES — seeding is what makes a row selectable, an exported-but-unseeded row silently falls back to en-US. Each localized row carries a LOCALIZED FOOTER: wrap() supplies an English one by default, so a row that forgets it renders translated prose under an English sign-off, which is the mixed-language artefact in miniature and the likeliest slip in that file. plugin-auth: new setDefaultEmailLocale() mirroring the setDefaultSmsLocale() precedent (#2815) already in the same file, plus a locale argument at all 5 sendTemplate call sites, re-anchored on symbols — every line number on the card had moved (:1140⇒:1151, :1196⇒:1207, :2415⇒:2438, :2546⇒:2569, :3061⇒:3136); confirmed exactly 5 real sites, the 6th hit is comment prose as the card said. AuthPlugin reads II18nService.getDefaultLocale() on kernel:ready. An unconfigured deployment is unchanged: no locale key is passed at all (not `locale: undefined`) and the ladder resolves its documented en-US default. origin/main merged (24206416a) before finalising; it moved packages/spec, so the closure was rebuilt and everything re-run at the merged head.",
"premise_recheck": "Both premises re-measured on origin/main before editing, not taken from the card. (1) `grep -n sendTemplate auth-manager.ts` → 6 hits, 5 real call sites, none passing a locale — held. (2) sys-user.object.ts still carries no locale/language field — held, and untouched, per the ruling deferring the column.",
"file_surface_amendment": "packages/plugins/plugin-auth/src/auth-plugin.ts was NOT in the verbatim surface the triage designation listed. Declared on the issue (comment 5307636415) and named in the PR body rather than absorbed silently. Unavoidable: the ruling says 'resolved at the PLUGIN LAYER', which is where ctx.getService('i18n') is reachable, and AuthManager holds no kernel context — the designation folded two files into one name. The #2815 SMS precedent proves the split (setter at auth-manager.ts:3329, read at auth-plugin.ts:729). Same package, same defect class, mechanical, and my claim-time in-flight check was run at PACKAGE granularity, so the addition is inside what was already checked.",
"ruling_gap_flagged": "One place where the ruling's named source and the runtime's values did not line up, resolved as an implementation detail rather than a change of source. II18nService.getDefaultLocale() carries the message CATALOG language, whose English spelling is the bare 'en' (FileI18nAdapter: `options.defaultLocale ?? 'en'`), while sys_email_template rows are keyed 'en-US' and SendTemplateInput.locale is documented as matched exactly, with 'no language-only prefix matching' stated in the contract. Passed through raw, the commonest deployment of all would miss every row and lean on the en-US fallback while telling the render filters 'en' — #7801's split reopened one notch narrower. normalizeAuthEmailLocale promotes a BARE language subtag to the regional row we ship (en⇒en-US, zh⇒zh-CN, …) and passes everything else through UNTOUCHED, because an unshipped regional tag like en-GB or fr-FR may be a tenant's own overlay row and swallowing it would re-create this exact bug from the fifth locale onward. Noted for the record: the localization.locale SETTING is a select whose four options are byte-identical to the template locale set and whose default is en-US — a second axis that exists and differs; the ruling named the i18n service, so that is what this uses.",
"tests": "All figures at 8e32236e6 (= PR head), the post-merge commit; the union was run AFTER the final commit. BUILD CLOSURE first (fresh worktree): pnpm --filter '@objectstack/plugin-email^...' --filter '@objectstack/plugin-auth^...' build — green, and rebuilt again after the origin/main merge because that merge moved packages/spec. TESTS: plugin-email 'Test Files 25 passed (25) / Tests 409 passed (409)'; plugin-auth 'Test Files 55 passed (55) / Tests 1261 passed (1261)'. TYPECHECK: both packages `tsc --noEmit` Done — script name echoed in the output, so not the zero-match pnpm --filter no-op. NEW COVERAGE: plugin-email/src/auth-templates-locales.test.ts (34 cases) drives the REAL EmailService over the real seeded rows and asserts rendered output, every expectation an independent literal (deriving them from the template constants would make the file agree with any edit, including pasting the English body under a zh-CN tag) — per-locale subject, a distinctive body phrase, the footer, and that the link/OTP hole survived translation; plus the NEGATIVE case, a zh-CN send rendering none of the en-US subject/body/footer, and that a no-locale send still resolves en-US. plugin-auth/src/auth-email-locale.test.ts (11 cases) drives all five send callbacks off the captured better-auth config: each names the locale; an unconfigured deployment names none (asserted via hasOwnProperty, since absence and undefined are different facts to the ladder's contract); the rest of the payload is undisturbed; and the normalizer in both directions. REVERSE VERIFICATION, direction predicted BEFORE running, twice, and both matched exactly. Ablation A — neutralize emailLocaleArg() to return {}: PREDICTED 4 red / 7 pass (three 'stamps the locale' cases plus 'the normalizer is actually applied'; the 'unconfigured names NO locale' case and the 5 pure-function cases stay green because they do not depend on the spread). MEASURED 'Tests 4 failed | 7 passed (11)', exactly those four. Ablation B — drop FOOTER_ZH_CN from one row, i.e. the 'forgot the footer' slip itself: PREDICTED 2 red / 32 pass (the zh-CN password-reset render pin and its negative twin). MEASURED 'Tests 2 failed | 32 passed (34)', exactly those two. Both restored with `git checkout <branch> -- the file` from the COMMITTED branch (fix committed first; never git stash), and `git diff --quiet HEAD` then confirmed the tree byte-identical to the commit. No dogfood ablation is involved, so no dist/ rebuild claim is made.",
"gates": "GREEN at 8e32236e6: check:nul-bytes (5992 text files, no raw ASCII control bytes), check:where-matcher (247 matchers, 0 silently-wrong, baseline key set verified — no files added), check:query-options-erasure (67 unswept non-test sites, none new; test surface at ceiling), check:engine-double-contract (311 pinned, no new double — the new plugin-auth test deliberately uses the captured-config harness instead of a fake data engine, so no ledger entry was needed and none was added), check:type-check-coverage, check:test-source-alias, check:type-source-resolution, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset. GATE LIST RE-DERIVED from the actual changed paths via scripts/pm/dispatch-gates.mjs rather than taken from the dispatch prompt; it added the changeset family (adr-0087 / no-major / empty-changeset / gate-self-tests / objectui-changeset) and the convention-triggered test-surface family (query-options-erasure, type-check-coverage, engine-double-contract, where-matcher) — all run above. NOT run locally and stated plainly: check:type-check-debt --re-measure, which requires a FULL workspace turbo build closure; both touched packages typecheck clean including their test files, so no ledger count can rise, but I did not measure the repo-wide ratchet — CI owns it. Control-character self-scan beyond the gate over every touched file (grep -naP over the C0 range + DEL): no hits, which matters here because the diff is dense with non-ASCII prose. skip-changeset deliberately NOT applied: this PR ships src changes plus a changeset, so it is not a tests-only PR. content/docs/releases/ untouched.",
"in_flight_check": "Run at claim time as the exception path requires, against LIVE claims rather than the stale 2026-08-13 serial notes. plugin-email: no domain:services card carries pm:dispatched; every open one is pm:on-hold, needs-user-decision, or unclaimed pm:queue; none of the 7 open feature PRs touches the package. plugin-auth: three cards read individually — #8692 declares packages/plugins/plugin-security/src/** (different package); #8193 is labelled pm:dispatched but its PR #8221 merged 2026-08-12 and the residual is a test-pin question with no live branch; #8531's claim D merged as PR #9069 at 11:19Z the same day, and its claims B/C are explicitly NOT dispatched (「⛔ 仍不派」) with a residual site list that does not include auth-manager.ts. No intersection ⇒ proceeded rather than yielding.",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
hotlong commentedon Aug 12, 2026
Triage ⇒
needs-user-decision, type Feature, routeddomain:identity(the send calls live inplugin-auth/src/auth-manager.ts; the column option would land inplatform-objects; templates inplugin-email— split per lane after the ruling). This is a product decision, not a lookup: every option either expands the public face or accepts a user-visible gap.Background
Measured in the card: no auth send names a
locale;sys_userhas no locale column; so the four-locale template rows #8019 shipped are unselectable by the platform itself, and azh-CNdeployment gets English password-reset/verification mail while the UI switches languages.Premises (re-check before ruling if the card sits)
git grep -n "sendTemplate" origin/main -- packages/plugins/plugin-auth/src/auth-manager.ts— still nolocaleargument at any call site (non-zero hits expected; zero hits ⇒ premise moved).git grep -n -i "locale\|language" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts— still no per-user locale field (neighbour-check the file withgit grep -n "email" … sys-user.object.ts).Question
Where does the recipient locale for outbound auth mail come from — and are the five remaining
en-US-only auth templates localized in the same change?Options
sys_user(locale/language). Most correct long-term; expands a shipped schema, needs a precedence design (user > org > deployment), permanent maintenance obligation.II18nService.getDefaultLocale()at the plugin layer. No schema change; fixes the whole class for single-locale deployments (the common case today); per-user granularity deferred.Accept-Language. No schema change, but auth mail is often sent outside the triggering user's request context (invitations, admin-initiated resets), and locale flaps per device.Recommendation
B now, and localize the remaining five auth templates in the same change — a partial localization manufactures the mixed-language flow
II18nService.setSupportedLocales' docstring warns about. Park A as a direction card (v18-class) with the precedence design, rather than adding a schema column ad hoc.Four-prong:
Downstream note: no open card carries a
Blocked-by:pointing here today.Generated by Claude Code
hotlong commentedon Aug 13, 2026
Maintainer ruling — deployment default locale, all templates in one batch
Ruled by the maintainer in a live PM session, 2026-08-13 (session
session_015XyMgCSMGGn9oSwbQEeWYg), verbatim: 「其他全部接受你的建议。」 — accepting the recommendation as presented.Ruling:
II18nService.getDefaultLocale(), reachable from the plugin layer) — a zh-CN deployment sends zh-CN auth mail across the board.Accept-Languageis rejected (mail is async; the request context is often absent or wrong).sys_user.localecolumn is deferred until measured pull (a real deployment with a mixed-locale user population). It is a new public surface and a standing obligation on every write path; when it comes, it layers on top of this resolution as an override, so nothing shipped now is thrown away.Label swap:
needs-user-decision→pm:queue.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
Claiming this card and starting implementation against the 2026-08-13 ruling (deployment-default locale via
II18nService.getDefaultLocale(); all auth templates localized in the same change; nosys_user.localecolumn).1454f3a3-b771-5eef-8695-7fcac193a629claude/issue-8195-auth-mail-localepackages/plugins/plugin-auth/src/auth-manager.ts(locale resolution + the fivesendTemplatecall sites),packages/plugins/plugin-email/src/templates/auth-templates.ts(the remaining auth templates localized to the four supported locales), plus tests and a changeset.Premise re-checks against
origin/main, run before any edit — both hold:git grep -n "sendTemplate" origin/main -- packages/plugins/plugin-auth/src/auth-manager.ts→ 6 hits (5 real call sites + 1 comment mention at :1192); no call site passes alocaleargument.git grep -n -i "locale\|language" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts→ zero hits. Neighbour-check withemailreturns 30+ hits, so the file path is right and simply has no locale field.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
Dev report — STOPPED at the cross-lane wall, no code written
Status:
blocked(governance/routing, not product). The ruling is not in doubt and I am not re-adjudicating it. I stopped because implementing it as ruled requires editing a package in another seat's lane, and the PM instruction on this dispatch was explicit: stop and report rather than absorb it.Premise re-checks — both HOLD
Run against
origin/mainbefore any edit.1.
git grep -n "sendTemplate" origin/main -- packages/plugins/plugin-auth/src/auth-manager.tsNon-zero as expected: 6 hits = 5 real call sites plus one prose mention at :1192. I read all five call-site option objects in full; none passes a
localeargument. Templates named:auth.password_reset(1140),auth.verify_email(1196),auth.invitation(2415),auth.magic_link(2546),auth.email_change_notice(3061).2.
git grep -n -i "locale\|language" origin/main -- packages/platform-objects/src/identity/sys-user.object.tsZero hits, as expected. Neighbour-check
git grep -n "email" … sys-user.object.tsreturns 30+ hits (emailfield at :512,email_verifiedat :520, thechange_my_emailaction at :330, unique index at :757), so the path is correct and the file simply carries no locale field. Nosys_userschema change was made — nothing was written at all.The cross-lane wall — hit, not absorbed
The lane map in
.claude/skills/pm-dispatch/SKILL.md(the anchoring rule table, lines 184-190) assigns packages to exactly one domain each:plugin-authdomain:identity(this card's label)plugin-emaildomain:servicesThe anchoring rule is "Every package belongs to exactly one domain", and the only boundary-crossing channel is spelled out as 「跨域例外路径(唯一的越界通道)」 — a cross-domain single PR designated by the triage seat (「由分诊座位指定一个车道 PM 认领」), whose claim comment declares the full file surface and which runs a targeted in-flight check. A dev agent cannot place itself on that path.
Template ownership is exclusive and confirmed, so the split cannot be dodged:
git grep -ln "AUTH_PASSWORD_RESET_TEMPLATE\|auth-templates"resolves only intoplugin-email(templates/auth-templates.ts,email-plugin.ts,index.ts, one test).plugin-auth's only hit isauth-manager.ts, which names templates as strings at the send sites; it defines none.plugin-auth/package.jsondoes not even depend on@objectstack/plugin-email— it reaches the email service through the service registry.So the ruled change is irreducibly two-lane:
domain:identity(mine):packages/plugins/plugin-auth/src/auth-manager.ts— resolveII18nService.getDefaultLocale()at the plugin layer, passlocaleat the 5 call sites.domain:services(not mine):packages/plugins/plugin-email/src/templates/auth-templates.ts— localize the 5 remainingen-US-only templates to 4 locales each (15 new rows).Six auth templates exist;
auth.email_change_noticealready has all four locales (en-US/zh-CN/ja-JP/es-ES, from #8019). The other five —password_reset,verify_email,magic_link,invitation,two_factor_otp— areen-US-only. That matches the ruling's "remaining five" exactly.Measured evidence that splitting is worse than the card assumed
Triage suggested "split per lane"; the ruling forbids it. I checked what a split would actually produce, and the ruling is right — with a sharper failure than the card describes. The ladder in
packages/plugins/plugin-email/src/email-service.ts:1145-1160:and then at :1200, deliberately:
So shipping the identity half alone on a zh-CN deployment gives:
auth.email_change_noticerenders a genuine zh-CN row, while the other five miss, fall back to the en-US row body, yet still handzh-CNto the render filters. The result is not merely mixed language across a flow — it is English prose carrying zh-CN-formatted dates and numbers inside one message, the precise artefact the comment at :1185-1190 says the row-locale authority exists to prevent. Resolution-without-templates is therefore actively harmful, not just incomplete. I did not ship it.What I did NOT do — stated plainly, not filled in
No worktree was created, no file was edited, no branch pushed, no PR opened. Consequently the following dispatch asks have no result to report, and I am not fabricating them:
plugin-emailhalf.check:i18n— not run, nothing regenerated. It would have an opinion only once template rows change.check:i18n,check:engine-double-contract,check:type-check-coverage,check:nul-bytes,pnpm test,pnpm typecheck, ESLint battery. All skipped because there is no diff to test.Everything above is read-only inspection of
origin/mainviagit show/git grep.For the PM
The ruling and the lane map are both binding and they conflict on routing only, not on substance. This needs the cross-domain exception path designated by the triage seat, with a claim comment declaring the full two-package file surface above. In-flight input, offered with its limitation stated: a title-scoped search of open PRs found none naming
plugin-email— that is not a file-surface in-flight check, and the real one (reading claim-comment file surfaces acrossdomain:identity+domain:services) still needs running on that path.Serial constraint noted for whoever lands it: PR #8432 (#8243) is in the merge queue on
auth-manager.ts(import block, one after-hook call, one request-seam block) and PR #8417 (#8317) onauth-plugin.ts/index.ts. The fivesendTemplatecall sites are a different region, so a textual conflict is unlikely butorigin/mainshould be merged before finalising.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
PM:转
pm:blocked,等分诊指定跨域通道。⛔ 这条路由不归执行席位定,也不归我定。domain:identity执行席位,sessionsession_01PEVB6w7D7uCszR9Mw1BL73。派发后 dev 在第一次编辑前停下并上报,零 diff、零分支。这是派发令明确要求的行为,不是失败。
死结的形状
pm-dispatch/SKILL.md:184-190plugin-email=domain:services,唯一的越界通道必须由分诊席位指定,dev agent 不能自行进入两条同时成立,而这张卡的交付物必然横跨两个包。dev 两个出口都没走。
这是本轮真正的产出。dev 读了
plugin-email/src/email-service.ts的回退阶梯::1145-1160—— 未命中的 locale 回退到 en-US 那一行;:1200——const locale = preferred || row.locale,仍然把调用者的 locale 交给渲染过滤器。所以在 zh-CN 部署上只上身份侧那一半,得到的是:已有四语言的
email_change_notice是真中文,而其余五封是英文正文里嵌着 zh-CN 格式的日期和数字——在同一封邮件内部。这比卡片原本描述的「跨流程混合语言」更尖锐,而且正是
:1185-1190注释里说 row-locale 权威存在就是为了防止的那件事。结论:C 不是「较差的部分修复」,是比今天「统一英文」更坏的状态。 它被证据取消资格,不只是被裁定文字取消资格。
不可再分的两 lane 面(供分诊指定时直接用)
domain:identity——packages/plugins/plugin-auth/src/auth-manager.ts:在插件层解析II18nService.getDefaultLocale(),并在 5 个sendTemplate调用点传 locale。domain:services——packages/plugins/plugin-email/src/templates/auth-templates.ts:把 5 封仅 en-US 的模板(password_reset、verify_email、magic_link、invitation、two_factor_otp)各补 4 个 locale = 15 行新增。git grep -ln找AUTH_PASSWORD_RESET_TEMPLATE/auth-templates只落到plugin-email;plugin-auth在发送点只以字符串命名模板、一个都没定义,而且根本不依赖@objectstack/plugin-email(它通过服务注册表拿到邮件服务)。前提复核(裁定后卡片搁置一天,已按分诊写下的两条复核)
git grep -n "sendTemplate" … auth-manager.ts→ 非零,6 处命中(:1140、:1192、:1196、:2415、:2546、:3061)。其中:1192是注释里的散文提及,5 处是真实调用点,逐个读过 option 对象,无一传 locale。✅ 前提成立。git grep -i "locale\|language" … sys-user.object.ts→ 无输出,exit 1。邻居对照git grep "email"同文件返回 30+ 命中(email字段:512、email_verified:520、change_my_emailaction:330、唯一索引:757),确认路径正确且确实没有 locale 字段。✅ 前提成立。顺带一条与裁定文字的吻合:总共 6 封认证模板,
auth.email_change_notice已随 #8019 带齐四语言 —— 与裁定说的「remaining five」完全对上。路由选项(⛔ 我不替分诊选)
domain:identity/domain:services的在飞检查。domain:services席位(模板面更大,身份侧只有 5 个调用点加一个解析 helper)。dev 建议 A,理由是它是 lane map 唯一承认的双 lane 通道,且精确保住裁定的「一次改动」约束;并指出 B 可行且比 A 更容易安排,但把身份 lane 的接缝(插件层解析 + 5 个认证发送点)交给一个不拥有
auth-manager.ts的席位,是同一个边界问题掉个头。⛔ 我把它呈上,不选边 —— SKILL.md 明说越界通道由分诊指定。
一条 dev 自己标注的证据强度
它对在飞检查的说法是:按标题搜 PR 没找到提及
plugin-email的,「这不是例外通道要求的文件面在飞检查,那个仍需运行」。据实标为弱证据,没有当成已完成。串行提示(给最终落地的人)
PR #8432(#8243)已合并、PR #8417(#8317)在队列,都在同一个包。5 个
sendTemplate调用点与它们的区域不同,文本冲突不太可能,但收尾前应先合origin/main。Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
Triage designation: cross-domain exception path (option A). One PR, claimed by the
domain:servicesseat, spanning both packages.pm:blocked⇒pm:queue; lane label re-routeddomain:identity⇒domain:servicesso the designated lane's inventory query finds it. Triage seat (whole-repo Routine), scheduled fire ~01:08Z 2026-08-16.This card has been parked since 2026-08-13 waiting for exactly this designation — the dev stopped correctly at the lane wall, and the deadlock analysis above is adopted as-is:
email-service.ts:1145-1160+:1200).Designation (the only boundary-crossing channel, per protocol)
domain:services— the larger surface isplugin-email(15 new template rows); the identity side is 5 call sites plus one resolution helper. Whole card, one PR, one claim.packages/plugins/plugin-email/src/templates/auth-templates.ts— localizepassword_reset,verify_email,magic_link,invitation,two_factor_otpto the four supported locales (the sixth,email_change_notice, already has all four).packages/plugins/plugin-auth/src/auth-manager.ts— resolveII18nService.getDefaultLocale()at the plugin layer and passlocaleat the 5sendTemplatecall sites (:1140,:1196,:2415,:2546,:3061as of the 2026-08-13 reading — re-anchor on symbols, not line numbers).domain:identityANDdomain:servicescards; if either package intersects, yield and requeue. The serial notes above (auth-manager PRs from 08-13) are stale leads, not current readings — re-run the check against live claims.II18nService.getDefaultLocale(); all auth templates localized in the same change;Accept-Languagerejected; nosys_user.localecolumn (deferred until measured pull).Size/model suggestion: M,
opus(implementation of a settled ruling; the template prose itself is the judgment surface).⛔ This designation names the lane, not a dispatch — claim discipline (assign +
Claim:comment + race re-read) applies unchanged when the services seat picks it up.Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
Claim — cross-domain exception path, per the triage designation of 2026-08-16 01:20Z. Implementing the settled 2026-08-13 maintainer ruling; ⛔ not re-adjudicating any of it.
session_01Y26DJEHSBhhAQ6wwfsHNzaclaude/issue-8195-auth-mail-locale(cut fromorigin/main@f2f09e4e3, pushed empty before the first edit)Re-read every comment on this card before starting. The earlier claim (
1454f3a3-b771-5eef-8695-7fcac193a629, 2026-08-13) is not live: that session stopped at the lane wall with zero diff and zero branch and reportedblocked, and the triage designation above is what released it. No other session holds this card.Full file surface — declared verbatim, as the exception path requires
packages/plugins/plugin-email/src/templates/auth-templates.ts— localizepassword_reset,verify_email,magic_link,invitation,two_factor_otpto the four supported locales (en-US/zh-CN/ja-JP/es-ES). The sixth,email_change_notice, already has all four from [finding] change-email verifies only the NEW address, so a hijacked session can move the account identity without the old mailbox ever being told #8019 — leave it alone. That is 15 new rows.packages/plugins/plugin-auth/src/auth-manager.ts— resolveII18nService.getDefaultLocale()at the plugin layer, passlocaleat the 5sendTemplatecall sites. Re-anchoring on symbols, not the card's 2026-08-13 line numbers.Targeted in-flight check — run at claim time, result: no intersection
Read against live claims rather than the stale 08-13 serial notes, as the designation instructs.
packages/plugins/plugin-email/**— clear. Nodomain:servicescard carriespm:dispatched; every open one ispm:on-hold,needs-user-decision, or unclaimedpm:queue. None of the 7 open feature PRs touches the package.packages/plugins/plugin-auth/**— clear, three cards checked individually:packages/plugins/plugin-security/src/**— different packagepm:dispatched, but its PR #8221 merged 2026-08-12; the residual is a test-pin question with no live branchauth-manager.tsProceeding.
Generated by Claude Code
Generated by Claude Code
fix(auth): auth emails follow the deployment locale (#8195)
os-project-manager commentedon Aug 16, 2026
File-surface amendment — same round, declared before the report. Session
session_01Y26DJEHSBhhAQ6wwfsHNza, branchclaude/issue-8195-auth-mail-locale, PR #9134 (draft).One file was added to the surface I declared at claim time:
packages/plugins/plugin-auth/src/auth-plugin.ts— the plugin-layer read ofII18nService.getDefaultLocale().Why it was unavoidable rather than convenient
The triage designation folded the resolution into
auth-manager.ts(「resolveII18nService.getDefaultLocale()at the plugin layer, passlocaleat the 5sendTemplatecall sites」). Those are two different files in this package, and the ruling's own words are what split them: the plugin layer is wherectx.getService('i18n')is reachable, andAuthManagerholds no kernel context.The precedent is in the same two files already, and I followed it rather than inventing a shape — #2815's auth SMS locale: the setter
setDefaultSmsLocale()lives inauth-manager.ts:3329, and the read that feeds it lives inauth-plugin.ts:729. The email locale now sits beside it in both places.Against the in-place-fix conditions
Same package, same defect class as the card, mechanical, form pinned by an existing landed declaration, and — the one that matters for parallel serialization — my claim-time in-flight check was already run at package granularity, not file granularity: I checked
packages/plugins/plugin-auth/**as a whole against every live claim, so the addition does not reach into anything the check did not already cover. No claim intersects it; re-verified just now, unchanged since claim time.⛔ Nothing else moved.
packages/platform-objectsis untouched (nosys_user.localecolumn — the ruling defers it), andcontent/docs/releases/is untouched.Full amended surface:
packages/plugins/plugin-email/src/templates/auth-templates.ts— 15 new locale rowspackages/plugins/plugin-email/src/index.ts— exports for the new rowspackages/plugins/plugin-email/src/auth-templates-locales.test.ts— newpackages/plugins/plugin-auth/src/auth-manager.ts— setter, normalizer, 5 call sitespackages/plugins/plugin-auth/src/auth-plugin.ts— the amendmentpackages/plugins/plugin-auth/src/auth-email-locale.test.ts— new.changeset/auth-email-deployment-locale.mdGenerated by Claude Code
Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
{ "issue": 8195, "status": "done", "branch": "claude/issue-8195-auth-mail-locale", "pr": "https://github.com/objectstack-ai/objectstack/pull/9134", "premise_still_valid": true, "summary": "Both halves landed in one PR on the designated cross-domain exception path, implementing the 2026-08-13 ruling without re-adjudicating it. plugin-email: password_reset, verify_email, magic_link, invitation and two_factor_otp each gain zh-CN/ja-JP/es-ES rows (15 new rows), composed into per-template lists and seeded through BUILTIN_AUTH_TEMPLATES — seeding is what makes a row selectable, an exported-but-unseeded row silently falls back to en-US. Each localized row carries a LOCALIZED FOOTER: wrap() supplies an English one by default, so a row that forgets it renders translated prose under an English sign-off, which is the mixed-language artefact in miniature and the likeliest slip in that file. plugin-auth: new setDefaultEmailLocale() mirroring the setDefaultSmsLocale() precedent (#2815) already in the same file, plus a locale argument at all 5 sendTemplate call sites, re-anchored on symbols — every line number on the card had moved (:1140⇒:1151, :1196⇒:1207, :2415⇒:2438, :2546⇒:2569, :3061⇒:3136); confirmed exactly 5 real sites, the 6th hit is comment prose as the card said. AuthPlugin reads II18nService.getDefaultLocale() on kernel:ready. An unconfigured deployment is unchanged: no locale key is passed at all (not `locale: undefined`) and the ladder resolves its documented en-US default. origin/main merged (24206416a) before finalising; it moved packages/spec, so the closure was rebuilt and everything re-run at the merged head.", "premise_recheck": "Both premises re-measured on origin/main before editing, not taken from the card. (1) `grep -n sendTemplate auth-manager.ts` → 6 hits, 5 real call sites, none passing a locale — held. (2) sys-user.object.ts still carries no locale/language field — held, and untouched, per the ruling deferring the column.", "file_surface_amendment": "packages/plugins/plugin-auth/src/auth-plugin.ts was NOT in the verbatim surface the triage designation listed. Declared on the issue (comment 5307636415) and named in the PR body rather than absorbed silently. Unavoidable: the ruling says 'resolved at the PLUGIN LAYER', which is where ctx.getService('i18n') is reachable, and AuthManager holds no kernel context — the designation folded two files into one name. The #2815 SMS precedent proves the split (setter at auth-manager.ts:3329, read at auth-plugin.ts:729). Same package, same defect class, mechanical, and my claim-time in-flight check was run at PACKAGE granularity, so the addition is inside what was already checked.", "ruling_gap_flagged": "One place where the ruling's named source and the runtime's values did not line up, resolved as an implementation detail rather than a change of source. II18nService.getDefaultLocale() carries the message CATALOG language, whose English spelling is the bare 'en' (FileI18nAdapter: `options.defaultLocale ?? 'en'`), while sys_email_template rows are keyed 'en-US' and SendTemplateInput.locale is documented as matched exactly, with 'no language-only prefix matching' stated in the contract. Passed through raw, the commonest deployment of all would miss every row and lean on the en-US fallback while telling the render filters 'en' — #7801's split reopened one notch narrower. normalizeAuthEmailLocale promotes a BARE language subtag to the regional row we ship (en⇒en-US, zh⇒zh-CN, …) and passes everything else through UNTOUCHED, because an unshipped regional tag like en-GB or fr-FR may be a tenant's own overlay row and swallowing it would re-create this exact bug from the fifth locale onward. Noted for the record: the localization.locale SETTING is a select whose four options are byte-identical to the template locale set and whose default is en-US — a second axis that exists and differs; the ruling named the i18n service, so that is what this uses.", "tests": "All figures at 8e32236e6 (= PR head), the post-merge commit; the union was run AFTER the final commit. BUILD CLOSURE first (fresh worktree): pnpm --filter '@objectstack/plugin-email^...' --filter '@objectstack/plugin-auth^...' build — green, and rebuilt again after the origin/main merge because that merge moved packages/spec. TESTS: plugin-email 'Test Files 25 passed (25) / Tests 409 passed (409)'; plugin-auth 'Test Files 55 passed (55) / Tests 1261 passed (1261)'. TYPECHECK: both packages `tsc --noEmit` Done — script name echoed in the output, so not the zero-match pnpm --filter no-op. NEW COVERAGE: plugin-email/src/auth-templates-locales.test.ts (34 cases) drives the REAL EmailService over the real seeded rows and asserts rendered output, every expectation an independent literal (deriving them from the template constants would make the file agree with any edit, including pasting the English body under a zh-CN tag) — per-locale subject, a distinctive body phrase, the footer, and that the link/OTP hole survived translation; plus the NEGATIVE case, a zh-CN send rendering none of the en-US subject/body/footer, and that a no-locale send still resolves en-US. plugin-auth/src/auth-email-locale.test.ts (11 cases) drives all five send callbacks off the captured better-auth config: each names the locale; an unconfigured deployment names none (asserted via hasOwnProperty, since absence and undefined are different facts to the ladder's contract); the rest of the payload is undisturbed; and the normalizer in both directions. REVERSE VERIFICATION, direction predicted BEFORE running, twice, and both matched exactly. Ablation A — neutralize emailLocaleArg() to return {}: PREDICTED 4 red / 7 pass (three 'stamps the locale' cases plus 'the normalizer is actually applied'; the 'unconfigured names NO locale' case and the 5 pure-function cases stay green because they do not depend on the spread). MEASURED 'Tests 4 failed | 7 passed (11)', exactly those four. Ablation B — drop FOOTER_ZH_CN from one row, i.e. the 'forgot the footer' slip itself: PREDICTED 2 red / 32 pass (the zh-CN password-reset render pin and its negative twin). MEASURED 'Tests 2 failed | 32 passed (34)', exactly those two. Both restored with `git checkout <branch> -- the file` from the COMMITTED branch (fix committed first; never git stash), and `git diff --quiet HEAD` then confirmed the tree byte-identical to the commit. No dogfood ablation is involved, so no dist/ rebuild claim is made.", "gates": "GREEN at 8e32236e6: check:nul-bytes (5992 text files, no raw ASCII control bytes), check:where-matcher (247 matchers, 0 silently-wrong, baseline key set verified — no files added), check:query-options-erasure (67 unswept non-test sites, none new; test surface at ceiling), check:engine-double-contract (311 pinned, no new double — the new plugin-auth test deliberately uses the captured-config harness instead of a fake data engine, so no ledger entry was needed and none was added), check:type-check-coverage, check:test-source-alias, check:type-source-resolution, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset. GATE LIST RE-DERIVED from the actual changed paths via scripts/pm/dispatch-gates.mjs rather than taken from the dispatch prompt; it added the changeset family (adr-0087 / no-major / empty-changeset / gate-self-tests / objectui-changeset) and the convention-triggered test-surface family (query-options-erasure, type-check-coverage, engine-double-contract, where-matcher) — all run above. NOT run locally and stated plainly: check:type-check-debt --re-measure, which requires a FULL workspace turbo build closure; both touched packages typecheck clean including their test files, so no ledger count can rise, but I did not measure the repo-wide ratchet — CI owns it. Control-character self-scan beyond the gate over every touched file (grep -naP over the C0 range + DEL): no hits, which matters here because the diff is dense with non-ASCII prose. skip-changeset deliberately NOT applied: this PR ships src changes plus a changeset, so it is not a tests-only PR. content/docs/releases/ untouched.", "in_flight_check": "Run at claim time as the exception path requires, against LIVE claims rather than the stale 2026-08-13 serial notes. plugin-email: no domain:services card carries pm:dispatched; every open one is pm:on-hold, needs-user-decision, or unclaimed pm:queue; none of the 7 open feature PRs touches the package. plugin-auth: three cards read individually — #8692 declares packages/plugins/plugin-security/src/** (different package); #8193 is labelled pm:dispatched but its PR #8221 merged 2026-08-12 and the residual is a test-pin question with no live branch; #8531's claim D merged as PR #9069 at 11:19Z the same day, and its claims B/C are explicitly NOT dispatched (「⛔ 仍不派」) with a residual site list that does not include auth-manager.ts. No intersection ⇒ proceeded rather than yielding.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
fix(auth): auth emails follow the deployment locale (objectstack-ai#8195
merge: origin/main into claude/issue-14319-accept-language