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
messaging: NotificationTemplateStore's locale ladder (named tag, primary subtag, en) never reads the stack's declared i18n.fallbackLocale #28919
Filing gate: ① the #28880 dev's out-of-scope finding (os-dev-report 6568876497), filed by domain:server seat 5. Graded by this seat: p3. The sibling ladder already folds the primary subtag, so only a declared fallback outside the recipient's language is missed. Graded under 「定级你可以自己决定」 (Leehom, 2026-11-01).
Reach: in-app, SMS and push notifications rendered from sys_notification_template for a recipient whose locale has no row, on a stack whose i18n.fallbackLocale names a locale other than en.
Reader who acts: a domain:server seat (packages/services/service-messaging).
What happens
NotificationTemplateStore loads (topic, channel, locale) and walks a fixed candidate list: the named tag, then its primary subtag (zh-CN → zh), then DEFAULT_LOCALE (en). See the comment at notification-template.object.ts ~line 130. It never reads the stack's declared fallback (i18n.fallbackLocale, else defaultLocale, through the i18n service's getFallbackLocale). After PR #28917 (#28880), the email ladder does read it, so the two template families disagree for the same recipient and stack.
Wanted
Measure: does the store see the i18n service, and does any shipped stack declare a non-en fallback?
If so, the declared fallback joins the ladder before en, folded by resolveBundleLocale if the store can read its locale list. ⛔ No third copy of the BCP-47 ladder.
Tests red first, with an ablation. Update the comment at ~line 130 to the new two-resolver reading.
Blocked-by: #28880 (PR #28917 lands the email side this aligns with).
Claim: PM loop round 1 · card thread · 2026-11-02T05:20Z
Session: session_01FEkQ5oc56xChD6Wrc4cGSt (the seat); the card thread is session_01FEkQ5pQMBN4w3aJaA84U6X
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-j938he (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#5
Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z) with the card's grade and this seat's rulings in the order; card assigned to seat 5 by seat 1's queue split after Leehom's 「队列进了很多任务,抓紧派发」 (2026-11-02T05:15Z).
File surface: packages/services/service-messaging/src/ and the composition that hands it the i18n reading, changesets. ⛔ No packages/spec/**, ⛔ no plugin-email.
Container & model: S, mode:thread, model and effort as the thread was opened
Clause-②: no
Metadata-protocol: none
Responsibility: the code is the notification template locale ladder · a recipient whose locale has no row skips the stack declared fallback and renders en · reached by in-app, SMS and push notifications on a stack declaring a non-en fallback
Thread-read: 6569438085
Premise: holds (read at origin/main135054eb: NotificationTemplateStore walks named tag, primary subtag, DEFAULT_LOCALE; no read of getFallbackLocale)
Serial constraints cleared: none needed.
Resume-from: none — fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28919.md in the project.
domain:server#5 · os-project-manager · session_01FEkQ5pQMBN4w3aJaA84U6X
os-dev-report
{
"issue": 28919,
"status": "done",
"branch": "claude/project-thread-j938he",
"pr": "#29048",
"session": "session_01FEkQ5pQMBN4w3aJaA84U6X",
"premise_still_valid": true,
"head": "1c8f2f69",
"summary": "NotificationTemplateStore's ladder for a named recipient locale is now: named tag, primary subtag, the stack's declared fallback (i18n getFallbackLocale, read at each resolve) and its primary subtag, then DEFAULT_LOCALE 'en'. MessagingServicePlugin hands the store the reader under a module-private symbol key (TEMPLATE_DECLARED_FALLBACK_LOCALE, not re-exported), the same placement #28917 used. NotificationTemplateStoreOptions and the class declaration are unchanged, and the built d.ts differs only in the store docblock. Ruling 2 reading: the store holds no locale list (one findOne per rung), so no resolveBundleLocale fold; the fallback takes the same named-then-primary steps (tagThenPrimary), with no third ladder copy. A no-locale call, no i18n service, or a store built outside the package answer as before. Ruling 3 measurement: no shipped stack declares a non-en fallback (app-crm, app-showcase, app-todo and hotcrm src/sales declare 'en'; app-multi-package declares no i18n). The plugin already reads the i18n service (getDefaultTemplateLocale), so no new published member was needed. The ~line 130 comment and the email-channel comment give the new reading.",
"tests": "New src/template-declared-fallback.test.ts, 12 cases (store ladder plus composition through MessagingServicePlugin, with i18n registered after kernel:ready, and with no i18n). Red first on main: 5 failed / 7 passed (the 5 fallback cases red; today-ladder cases green). After the fix: 12/12. pnpm --filter @objectstack/service-messaging test at 1c8f2f6: Test Files 63/63, Tests 771/771, lock VERDICT command-exit 0. typecheck exit 0 (test file in tsc --listFiles). Ablation through scripts/ablation-replace.mjs, each landed (anchor 1 to 0, blob moved) and restored to a byte-identical HEAD; tests import src by relative path, so no dist is in the path: A delete fallback rung, 5 failed; B fallback without primary subtag, 3 failed; C fallback before named tag, 5 failed; D drop reader throw guard, 1 failed (the first attempt was refused by the tool as a no-op, since the replacement was a substring of the anchor, and ran nothing; rerun with a distinct replacement); E drop the plugin wiring key, 1 failed. d.ts: service-messaging built at the base and at the fix; index.d.ts and index.d.cts differ only in the NotificationTemplateStore docblock.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 1c8f2f6: 71 derived, 71 run, all exit 0; --ran: 71 accounted, 0 NOT-MEASURED, 0 UNRUN (record: pm-orders/server/28919-gates-ran.txt in the project). First pass at 4b97a0b: check:engine-double-contract exit 1 (the new double's update/delete were not on the dispatch predicates), fixed in d00dbd1 with assertEngineUpdateDispatch/assertEngineDeleteDispatch plus --write of engine-double-contract.pinned.json (3 rows for the new file only). check:i18n, check:dual-build-cjs-loads and check-plugin-teardown-shape --self-test were exit 3 (prerequisite) until the build closure was built and the fixture commit 621a487 fetched; all exit 0 on rerun and in the final pass. eslint --no-inline-config packages/services/service-messaging/src exit 0. CI check-runs on PR #29048: in_progress at report time.",
"line_budget": "7 files, +319/-17 vs origin/main: template-renderer.ts +61/-, messaging-service-plugin.ts +25, email-channel.ts +3/-1, notification-template.object.ts +19/-, new test 198+, changeset +10, engine-double-contract.pinned.json +15",
"files_changed": [
".changeset/28919-notification-template-declared-fallback.md",
"packages/services/service-messaging/src/email-channel.ts",
"packages/services/service-messaging/src/messaging-service-plugin.ts",
"packages/services/service-messaging/src/objects/notification-template.object.ts",
"packages/services/service-messaging/src/template-declared-fallback.test.ts",
"packages/services/service-messaging/src/template-renderer.ts",
"scripts/engine-double-contract.pinned.json"
],
"deviations": [
"scripts/engine-double-contract.pinned.json is outside the order's file surface: the gate's own --write records the new test file's pinned double (3 rows, no other row moved).",
"The PR body was written once at pr_create and opens with the harness attribution block, then Fixes #28919. It carries no seat signature line."
],
"declarations": "Clause-②: no · Metadata-protocol: none · patch changeset for @objectstack/service-messaging",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "3 — mcp__github__issue_read x3 (get, get_comments x2); no write tools",
"api_writes": "3 — git push (branch claude/project-thread-j938he), fleet-write pr_create (run 41399775980, PR #29048, no labels, no assignees), this os-dev-report comment via post-stamped",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed — the store tries exact rows, so a zh-TW declared fallback over a store holding only zh-CN lands zh or en, while the email side folds it with resolveBundleLocale. This is by ruling 2; recorded in PR Acceptance notes."
]
}
ACCEPT → PR #29048 comment 6570671797 · head 1c8f2f69 · 2026-11-02T06:29Z. No contract review owed. Landing: pr_ready + automerge_enable through the relay; the merge queue closes this card.
Release: session_01FEkQ5oc56xChD6Wrc4cGSt · seat 5 signs off at the maintainer's wrap-up · back to pm:queue for the next domain:server seat
Resume-from: claude/project-thread-j938he@1c8f2f69
The maintainer's words, verbatim: 「尽快提交代码,然后收工」 (2026-11-02T07:10Z) and 「自动合并中的也交给下一任跟进」 (2026-11-02T07:23Z). This is the default hand-back; seat 5 keeps no stay-behind.
Next: PR #29048 is ACCEPTed (6570671797) and in the merge queue. The next seat posts Landed after merge, clears labels, and asks the coordinator to resolve the card thread.
Claim: PM loop round 1 · take-over of the predecessor's hand-back · 2026-11-02T08:09Z
Session: session_01ADHkmQKwKFCp1m6Pb6Q8c1 (the seat; no new dev: PR #29048 is already written and ACCEPTed)
Account: os-steve (the seat's linked user; the card's assignee)
Branch: claude/project-thread-j938he (unchanged; the predecessor's card-thread branch)
Worktree: none (landing watch only)
Domain: domain:server
Seat: domain:server#5
Ruling-ref: the maintainer, in the os-steve project "objectstack spec": 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」; the predecessor's Release 6571739906 names this card for the next seat-5 holder.
File surface: as PR #29048's file list (unchanged: packages/services/service-messaging/src/, changeset)
Container & model: S, landing watch only, no dispatch
Clause-②: no
Metadata-protocol: none
Responsibility: as the predecessor's claim on this card (unchanged)
Thread-read: 6571739906
Premise: holds (PR #29048 is ACCEPTed on head 1c8f2f69; nothing has moved the head since the Release)
Serial constraints cleared: none new; the PR is past review.
Resume-from: claude/project-thread-j938he@1c8f2f69
State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim. Next: the merge queue removed PR #29048 at 07:47Z on an unrelated cli test timeout; the seat records the signature on the PR and re-queues it once.
MERGED by the merge queue at 2026-11-02T09:23:14Z, merge commit dedc350519 (re-queued once at 08:15Z after a timeout kick on the cli validate-build-gate-parity shard; signed on the PR)
this card
closed completed by the PR's Fixes line
State: pm:dispatched removed; domain:server, area:i18n and the priority label stay. Nothing remains on this card.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: unblocked ·
pm:queue(p3 kept) · 2026-11-02T04:53Z⛔ Not a claim.
Blocked-by: #28880is closed as completed. PR fix(plugin-email): a templated email to a named locale folds it against the template's locales and tries the stack's declared fallback before en-US #28917, the email side this card aligns with, merged. The card waited only for that ladder to land, so it can now align the notification store to it.pm:blocked→pm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Unblocked · 2026-11-02T04:58Z
Blocked-by #28880 is closed: PR #28917 merged as
e0dc0771ab.pm:blocked→pm:queue. Read the merged PR for the ladder as landed.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Claim: PM loop round 1 · card thread · 2026-11-02T05:20Z
Session:
session_01FEkQ5oc56xChD6Wrc4cGSt(the seat); the card thread issession_01FEkQ5pQMBN4w3aJaA84U6XAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-j938he(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z) with the card's grade and this seat's rulings in the order; card assigned to seat 5 by seat 1's queue split after Leehom's 「队列进了很多任务,抓紧派发」 (2026-11-02T05:15Z).
File surface:
packages/services/service-messaging/src/and the composition that hands it the i18n reading, changesets. ⛔ Nopackages/spec/**, ⛔ noplugin-email.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: no
Metadata-protocol: none
Responsibility: the code is the notification template locale ladder · a recipient whose locale has no row skips the stack declared fallback and renders
en· reached by in-app, SMS and push notifications on a stack declaring a non-en fallbackThread-read: 6569438085
Premise: holds (read at
origin/main135054eb:NotificationTemplateStorewalks named tag, primary subtag,DEFAULT_LOCALE; no read ofgetFallbackLocale)Serial constraints cleared: none needed.
Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28919.mdin the project.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5pQMBN4w3aJaA84U6X
os-dev-report
{
"issue": 28919,
"status": "done",
"branch": "claude/project-thread-j938he",
"pr": "#29048",
"session": "session_01FEkQ5pQMBN4w3aJaA84U6X",
"premise_still_valid": true,
"head": "1c8f2f69",
"summary": "NotificationTemplateStore's ladder for a named recipient locale is now: named tag, primary subtag, the stack's declared fallback (i18n getFallbackLocale, read at each resolve) and its primary subtag, then DEFAULT_LOCALE 'en'. MessagingServicePlugin hands the store the reader under a module-private symbol key (TEMPLATE_DECLARED_FALLBACK_LOCALE, not re-exported), the same placement #28917 used. NotificationTemplateStoreOptions and the class declaration are unchanged, and the built d.ts differs only in the store docblock. Ruling 2 reading: the store holds no locale list (one findOne per rung), so no resolveBundleLocale fold; the fallback takes the same named-then-primary steps (tagThenPrimary), with no third ladder copy. A no-locale call, no i18n service, or a store built outside the package answer as before. Ruling 3 measurement: no shipped stack declares a non-en fallback (app-crm, app-showcase, app-todo and hotcrm src/sales declare 'en'; app-multi-package declares no i18n). The plugin already reads the i18n service (getDefaultTemplateLocale), so no new published member was needed. The ~line 130 comment and the email-channel comment give the new reading.",
"tests": "New src/template-declared-fallback.test.ts, 12 cases (store ladder plus composition through MessagingServicePlugin, with i18n registered after kernel:ready, and with no i18n). Red first on main: 5 failed / 7 passed (the 5 fallback cases red; today-ladder cases green). After the fix: 12/12. pnpm --filter @objectstack/service-messaging test at 1c8f2f6: Test Files 63/63, Tests 771/771, lock VERDICT command-exit 0. typecheck exit 0 (test file in tsc --listFiles). Ablation through scripts/ablation-replace.mjs, each landed (anchor 1 to 0, blob moved) and restored to a byte-identical HEAD; tests import src by relative path, so no dist is in the path: A delete fallback rung, 5 failed; B fallback without primary subtag, 3 failed; C fallback before named tag, 5 failed; D drop reader throw guard, 1 failed (the first attempt was refused by the tool as a no-op, since the replacement was a substring of the anchor, and ran nothing; rerun with a distinct replacement); E drop the plugin wiring key, 1 failed. d.ts: service-messaging built at the base and at the fix; index.d.ts and index.d.cts differ only in the NotificationTemplateStore docblock.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 1c8f2f6: 71 derived, 71 run, all exit 0; --ran: 71 accounted, 0 NOT-MEASURED, 0 UNRUN (record: pm-orders/server/28919-gates-ran.txt in the project). First pass at 4b97a0b: check:engine-double-contract exit 1 (the new double's update/delete were not on the dispatch predicates), fixed in d00dbd1 with assertEngineUpdateDispatch/assertEngineDeleteDispatch plus --write of engine-double-contract.pinned.json (3 rows for the new file only). check:i18n, check:dual-build-cjs-loads and check-plugin-teardown-shape --self-test were exit 3 (prerequisite) until the build closure was built and the fixture commit 621a487 fetched; all exit 0 on rerun and in the final pass. eslint --no-inline-config packages/services/service-messaging/src exit 0. CI check-runs on PR #29048: in_progress at report time.",
"line_budget": "7 files, +319/-17 vs origin/main: template-renderer.ts +61/-, messaging-service-plugin.ts +25, email-channel.ts +3/-1, notification-template.object.ts +19/-, new test 198+, changeset +10, engine-double-contract.pinned.json +15",
"files_changed": [
".changeset/28919-notification-template-declared-fallback.md",
"packages/services/service-messaging/src/email-channel.ts",
"packages/services/service-messaging/src/messaging-service-plugin.ts",
"packages/services/service-messaging/src/objects/notification-template.object.ts",
"packages/services/service-messaging/src/template-declared-fallback.test.ts",
"packages/services/service-messaging/src/template-renderer.ts",
"scripts/engine-double-contract.pinned.json"
],
"deviations": [
"scripts/engine-double-contract.pinned.json is outside the order's file surface: the gate's own --write records the new test file's pinned double (3 rows, no other row moved).",
"The PR body was written once at pr_create and opens with the harness attribution block, then Fixes #28919. It carries no seat signature line."
],
"declarations": "Clause-②: no · Metadata-protocol: none · patch changeset for @objectstack/service-messaging",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "3 — mcp__github__issue_read x3 (get, get_comments x2); no write tools",
"api_writes": "3 — git push (branch claude/project-thread-j938he), fleet-write pr_create (run 41399775980, PR #29048, no labels, no assignees), this os-dev-report comment via post-stamped",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed — the store tries exact rows, so a zh-TW declared fallback over a store holding only zh-CN lands zh or en, while the email side folds it with resolveBundleLocale. This is by ruling 2; recorded in PR Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
ACCEPT → PR #29048 comment 6570671797 · head
1c8f2f69· 2026-11-02T06:29Z. No contract review owed. Landing:pr_ready+automerge_enablethrough the relay; the merge queue closes this card.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Release:
session_01FEkQ5oc56xChD6Wrc4cGSt· seat 5 signs off at the maintainer's wrap-up · back topm:queuefor the nextdomain:serverseatResume-from:
claude/project-thread-j938he@1c8f2f69The maintainer's words, verbatim: 「尽快提交代码,然后收工」 (2026-11-02T07:10Z) and 「自动合并中的也交给下一任跟进」 (2026-11-02T07:23Z). This is the default hand-back; seat 5 keeps no stay-behind.
Next: PR #29048 is ACCEPTed (6570671797) and in the merge queue. The next seat posts Landed after merge, clears labels, and asks the coordinator to resolve the card thread.
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop round 1 · take-over of the predecessor's hand-back · 2026-11-02T08:09Z
Session:
session_01ADHkmQKwKFCp1m6Pb6Q8c1(the seat; no new dev: PR #29048 is already written and ACCEPTed)Account:
os-steve(the seat's linked user; the card's assignee)Branch:
claude/project-thread-j938he(unchanged; the predecessor's card-thread branch)Worktree: none (landing watch only)
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: the maintainer, in the
os-steveproject "objectstack spec": 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」; the predecessor's Release 6571739906 names this card for the next seat-5 holder.File surface: as PR #29048's file list (unchanged: packages/services/service-messaging/src/, changeset)
Container & model:
S, landing watch only, no dispatchClause-②: no
Metadata-protocol: none
Responsibility: as the predecessor's claim on this card (unchanged)
Thread-read: 6571739906
Premise: holds (PR #29048 is ACCEPTed on head
1c8f2f69; nothing has moved the head since the Release)Serial constraints cleared: none new; the PR is past review.
Resume-from:
claude/project-thread-j938he@1c8f2f69State:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim. Next: the merge queue removed PR #29048 at 07:47Z on an unrelated cli test timeout; the seat records the signature on the PR and re-queues it once.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Landed · #28919 · 2026-11-02T09:45Z
dedc350519(re-queued once at 08:15Z after a timeout kick on the cli validate-build-gate-parity shard; signed on the PR)completedby the PR'sFixeslineState:
pm:dispatchedremoved;domain:server,area:i18nand the priority label stay. Nothing remains on this card.