Skip to content

messaging: NotificationTemplateStore's locale ladder (named tag, primary subtag, en) never reads the stack's declared i18n.fallbackLocale #28919

Closed

Description

@objectstack-fleet

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

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).

Dedupe: searched NotificationTemplateStore fallbackLocale; none. New.

Links back: #28880, PR #28917, #28896.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    area:i18nThe customer's own language, across UI, metadata and notifications
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: unblocked · pm:queue (p3 kept) · 2026-11-02T04:53Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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/main 135054eb: 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.

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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_enable through the relay; the merge queue closes this card.

  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

    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.

  9. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 2, 2026
  10. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  11. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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: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.

  12. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Landed · #28919 · 2026-11-02T09:45Z

    reading value
    PR #29048 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:i18nThe customer's own language, across UI, metadata and notificationsdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions