docs(services): state the notify template locale the delivery path resolves, not a per-recipient one - #12505

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep
Aug 26, 2026
Merged

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one#12505
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes#12446

Text only. No schema accepts or refuses anything it did not before, no delivery behaviour moves, and no wire value changes.

What this corrects

The notify node's template path resolves (name, locale) against sys_email_template with one locale for the whole notification: payload.locale — interpolated once, before fan-out — else the deployment default (II18nService.getDefaultLocale()). sys_user carries no locale column and no request exists at async delivery time, so there is no per-recipient source to read. A per-user locale is deferred until measured pull (maintainer ruling, 2026-08-13) and layers in as an override at that same seam when it lands.

packages/spec was corrected to say exactly that, and that correction is already on main. The same retired promise survived outside the spec file. This PR sweeps it, mirroring the honest wording that already lives at service-messaging/src/email-channel.ts:86-99 and in the corrected io-node-config.zod.ts.

The sites

#sitewhat it said
1service-automation/src/builtin/notify-node.ts — the template field's configSchema description"resolved by (name, recipient locale) at delivery time and rendered per recipient" — the text rendered in the Studio form an author fills in, i.e. the shortest path from wording to an authoring mistake
2content/docs/automation/email-templates.mdx"so one node mails each person in their own language" — the only site that stated the false conclusion outright rather than merely licensing it. That sentence is gone, not softened
3service-messaging/src/messaging-service-plugin.tsthe channel-registration log line, advertising "resolve sys_email_template per recipient locale"
4service-automation/src/builtin/notify-node.ts — the execute-time guard comment"resolved per recipient locale at delivery"
5service-automation/src/builtin/notify-node.test.ts — the sibling commentsame sentence, moves with #4

A sixth site the card did not enumerate, fixed here and called out rather than slipped in:notify-node.ts (the messaging.emit payload comment) described what the outbox snapshots as "the per-recipient-locale resolution happens at delivery time". Same defect class as #4, same file, same already-claimed surface, and its correct form was pinned by the merged spec wording — so it was corrected in place instead of being filed as a separate finding. It is named here because an unnamed drive-by fix is unreviewable scope creep. The card's count of four sites is therefore low by one; the tree, not the card, was the source.

Deliberately NOT touched

  • packages/spec/** — zero files. A different lane owned that half.
  • content/docs/references/automation/io-node-config.mdx — auto-generated from the spec .describe(), and already carries the corrected text.
  • content/docs/releases/v17.mdx:3507 — release-owned; never edited in a code PR. It is a historical record of what shipped under Flow notify nodes cannot be localized: title/message are raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205, not live guidance.
  • email-channel.ts:87-99 / :125-127, inbox-channel.ts:46-51 / :118-124, messaging-service-plugin.ts:138-147 — these use "recipient locale" after defining it as the deployment default in the same block, or describe rendering happening per delivery row, which is accurate. They are the honest exemplars this PR mirrors, so changing them would be churn.

A pin, so this cannot quietly come back

notify-node.test.ts gains one test asserting the form description names payload.locale and the deployment default, states "not one per recipient", and refuses a bare "recipient locale" — the same shape the spec lane used.

Reverse-verified rather than assumed. Restoring the retired sentence on the description was proven on disk by anchored occurrence counts (injected text 1, deleted text 0 — not a bare --stat), and the pin then failed with expected 'Email template name (sys_email_templa…' to match /not one per recipient/ (1 failed | 15 passed). Restoration was proven by state, not by an exit code: the blob hash returned to the HEAD blob 3b7f6056 and git diff HEAD came back empty. The mutation is intra-package source, so no dist and no rebuild is involved on either leg.

Changeset — argued, not defaulted

Shipped: .changeset/notify-node-template-locale-is-not-per-recipient.md, patch on @objectstack/service-automation + @objectstack/service-messaging. skip-changeset would have been wrong here: site 1 is not a comment but a shipped string rendered in the Studio form, and site 3 ships in an operator-visible log line — both leave the package and reach a reader. (An empty changeset was never an option; it stalls the release.)

Verification — everything below ran at d86c9682, the final commit

Exit codes captured before any pipe, and each verdict quoted from the gate's own output line.

  • Derived gate union, re-derived from the real change set rather than from the dispatch's path list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 5 paths vs merge base fe72aa5c1, 38 path-matched families plus the convention-triggered ones. 44 of 44 attempted gates green, including check:nul-bytes, check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract, spec check:docs, spec check:skill-examples, spec check:empty-state, spec check:liveness, spec check:strictness-ledger, spec check:variant-docs, the changeset family (check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, release-rehearsal-clone --self-test), the packages family (check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-comment-mask-adoption, check-plugin-teardown-shape, check:cross-package-test-inputs, check-ci-filter-parity), the docs-drift family (check-affected-docs, check-drift-comment — run rather than assumed inert, since content/docs/** is in scope), and the convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n, check:i18n-stale-fill, check:type-check-coverage.
  • Two gates first refused on a missing prerequisite, which reads as NOT MEASURED rather than redcheck:skill-examples ("Build first, then re-run") and check:i18n ("Nothing was checked: no bundle was compared"). Both prerequisites were built (@objectstack/cli, @objectstack/client-react closures) and both then ran clean: "✅ 260 prose examples type-check across 3 surface(s)" and "check-i18n-bundles: OK (9 package(s) — all bundles in sync)".
  • Tests:@objectstack/service-automationTest Files 91 passed (91), Tests 1083 passed (1083); @objectstack/service-messagingTest Files 29 passed (29), Tests 295 passed (295), plus its typecheck clean. The new pin was confirmed by name, not inferred from a total: ✓ notify (baseline node) > describes 'template' with the locale the delivery path actually resolves, not a per-recipient one.
  • One gate honestly NOT MEASURED locally:check:type-check-debt --re-measure needs the whole workspace closure built and refuses otherwise. Its structural half, check:type-check-coverage, is green, and the ratchet's question was answered directly instead: tsc --noEmit on service-automation reports 3 diagnostics, all in nested-region-parity.test.ts — a file this diff never touches — and zero naming notify-node.ts or notify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself.

Generated by Claude Code

…solves
The `notify` node's `template` path resolves `(name, locale)` with ONE locale
for the whole notification: `payload.locale`, interpolated once before fan-out,
else the deployment default (`II18nService.getDefaultLocale()`). `sys_user`
carries no locale column and no request exists at async delivery time, so there
is no per-recipient source to read; a per-user locale is deferred until measured
pull (maintainer ruling, 2026-08-13).
`packages/spec` was corrected to say so. The same retired promise survived in
five sites outside it, two of them the ones an app author actually reads:
- the `template` field's `configSchema` description — the Studio form text —
which said the row is "resolved by (name, recipient locale) ... and rendered
per recipient";
- `content/docs/automation/email-templates.mdx`, the only site to state the
conclusion outright: "so one node mails each person in their own language";
- the messaging channel-registration log line;
- two internal comments in `notify-node.ts` (the execute-time guard and the
payload the outbox snapshots) and the sibling comment in its test.
Text only — no schema, delivery behaviour or wire value moves. A new pin asserts
the form description names `payload.locale` and the deployment default and
refuses a bare "recipient locale".
Part of #12446
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157mMVAq9fjGe2kaSD2aJC8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/service-messaging, touching 2 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via MessagingServicePlugin (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 316d9ab5571ee8c80a8c6c35e67834cebc3d11db — the merge of head d86c968222b0c5377baea4ea7af71fb968bd52cc into base 76c18be8ad56400c85cde381e63c7755bbd6153a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 316d9ab5571ee8c80a8c6c35e67834cebc3d11db && git checkout 316d9ab5571ee8c80a8c6c35e67834cebc3d11db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76c18be8ad56400c85cde381e63c7755bbd6153a d86c968222b0c5377baea4ea7af71fb968bd52cc && git checkout -B drift-repro 76c18be8ad56400c85cde381e63c7755bbd6153a && git merge --no-ff d86c968222b0c5377baea4ea7af71fb968bd52cc
node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 76c18be8ad56400c85cde381e63c7755bbd6153a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 07:09
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit e577445Aug 26, 2026
35 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-12446-recipient-locale-promise-sweep branch August 26, 2026 07:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one - #12505

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep
Aug 26, 2026
Merged

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one#12505
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes#12446

Text only. No schema accepts or refuses anything it did not before, no delivery behaviour moves, and no wire value changes.

What this corrects

The notify node's template path resolves (name, locale) against sys_email_template with one locale for the whole notification: payload.locale — interpolated once, before fan-out — else the deployment default (II18nService.getDefaultLocale()). sys_user carries no locale column and no request exists at async delivery time, so there is no per-recipient source to read. A per-user locale is deferred until measured pull (maintainer ruling, 2026-08-13) and layers in as an override at that same seam when it lands.

packages/spec was corrected to say exactly that, and that correction is already on main. The same retired promise survived outside the spec file. This PR sweeps it, mirroring the honest wording that already lives at service-messaging/src/email-channel.ts:86-99 and in the corrected io-node-config.zod.ts.

The sites

#sitewhat it said
1service-automation/src/builtin/notify-node.ts — the template field's configSchema description"resolved by (name, recipient locale) at delivery time and rendered per recipient" — the text rendered in the Studio form an author fills in, i.e. the shortest path from wording to an authoring mistake
2content/docs/automation/email-templates.mdx"so one node mails each person in their own language" — the only site that stated the false conclusion outright rather than merely licensing it. That sentence is gone, not softened
3service-messaging/src/messaging-service-plugin.tsthe channel-registration log line, advertising "resolve sys_email_template per recipient locale"
4service-automation/src/builtin/notify-node.ts — the execute-time guard comment"resolved per recipient locale at delivery"
5service-automation/src/builtin/notify-node.test.ts — the sibling commentsame sentence, moves with #4

A sixth site the card did not enumerate, fixed here and called out rather than slipped in:notify-node.ts (the messaging.emit payload comment) described what the outbox snapshots as "the per-recipient-locale resolution happens at delivery time". Same defect class as #4, same file, same already-claimed surface, and its correct form was pinned by the merged spec wording — so it was corrected in place instead of being filed as a separate finding. It is named here because an unnamed drive-by fix is unreviewable scope creep. The card's count of four sites is therefore low by one; the tree, not the card, was the source.

Deliberately NOT touched

  • packages/spec/** — zero files. A different lane owned that half.
  • content/docs/references/automation/io-node-config.mdx — auto-generated from the spec .describe(), and already carries the corrected text.
  • content/docs/releases/v17.mdx:3507 — release-owned; never edited in a code PR. It is a historical record of what shipped under Flow notify nodes cannot be localized: title/message are raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205, not live guidance.
  • email-channel.ts:87-99 / :125-127, inbox-channel.ts:46-51 / :118-124, messaging-service-plugin.ts:138-147 — these use "recipient locale" after defining it as the deployment default in the same block, or describe rendering happening per delivery row, which is accurate. They are the honest exemplars this PR mirrors, so changing them would be churn.

A pin, so this cannot quietly come back

notify-node.test.ts gains one test asserting the form description names payload.locale and the deployment default, states "not one per recipient", and refuses a bare "recipient locale" — the same shape the spec lane used.

Reverse-verified rather than assumed. Restoring the retired sentence on the description was proven on disk by anchored occurrence counts (injected text 1, deleted text 0 — not a bare --stat), and the pin then failed with expected 'Email template name (sys_email_templa…' to match /not one per recipient/ (1 failed | 15 passed). Restoration was proven by state, not by an exit code: the blob hash returned to the HEAD blob 3b7f6056 and git diff HEAD came back empty. The mutation is intra-package source, so no dist and no rebuild is involved on either leg.

Changeset — argued, not defaulted

Shipped: .changeset/notify-node-template-locale-is-not-per-recipient.md, patch on @objectstack/service-automation + @objectstack/service-messaging. skip-changeset would have been wrong here: site 1 is not a comment but a shipped string rendered in the Studio form, and site 3 ships in an operator-visible log line — both leave the package and reach a reader. (An empty changeset was never an option; it stalls the release.)

Verification — everything below ran at d86c9682, the final commit

Exit codes captured before any pipe, and each verdict quoted from the gate's own output line.

  • Derived gate union, re-derived from the real change set rather than from the dispatch's path list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 5 paths vs merge base fe72aa5c1, 38 path-matched families plus the convention-triggered ones. 44 of 44 attempted gates green, including check:nul-bytes, check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract, spec check:docs, spec check:skill-examples, spec check:empty-state, spec check:liveness, spec check:strictness-ledger, spec check:variant-docs, the changeset family (check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, release-rehearsal-clone --self-test), the packages family (check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-comment-mask-adoption, check-plugin-teardown-shape, check:cross-package-test-inputs, check-ci-filter-parity), the docs-drift family (check-affected-docs, check-drift-comment — run rather than assumed inert, since content/docs/** is in scope), and the convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n, check:i18n-stale-fill, check:type-check-coverage.
  • Two gates first refused on a missing prerequisite, which reads as NOT MEASURED rather than redcheck:skill-examples ("Build first, then re-run") and check:i18n ("Nothing was checked: no bundle was compared"). Both prerequisites were built (@objectstack/cli, @objectstack/client-react closures) and both then ran clean: "✅ 260 prose examples type-check across 3 surface(s)" and "check-i18n-bundles: OK (9 package(s) — all bundles in sync)".
  • Tests:@objectstack/service-automationTest Files 91 passed (91), Tests 1083 passed (1083); @objectstack/service-messagingTest Files 29 passed (29), Tests 295 passed (295), plus its typecheck clean. The new pin was confirmed by name, not inferred from a total: ✓ notify (baseline node) > describes 'template' with the locale the delivery path actually resolves, not a per-recipient one.
  • One gate honestly NOT MEASURED locally:check:type-check-debt --re-measure needs the whole workspace closure built and refuses otherwise. Its structural half, check:type-check-coverage, is green, and the ratchet's question was answered directly instead: tsc --noEmit on service-automation reports 3 diagnostics, all in nested-region-parity.test.ts — a file this diff never touches — and zero naming notify-node.ts or notify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself.

Generated by Claude Code

…solves
The `notify` node's `template` path resolves `(name, locale)` with ONE locale
for the whole notification: `payload.locale`, interpolated once before fan-out,
else the deployment default (`II18nService.getDefaultLocale()`). `sys_user`
carries no locale column and no request exists at async delivery time, so there
is no per-recipient source to read; a per-user locale is deferred until measured
pull (maintainer ruling, 2026-08-13).
`packages/spec` was corrected to say so. The same retired promise survived in
five sites outside it, two of them the ones an app author actually reads:
- the `template` field's `configSchema` description — the Studio form text —
which said the row is "resolved by (name, recipient locale) ... and rendered
per recipient";
- `content/docs/automation/email-templates.mdx`, the only site to state the
conclusion outright: "so one node mails each person in their own language";
- the messaging channel-registration log line;
- two internal comments in `notify-node.ts` (the execute-time guard and the
payload the outbox snapshots) and the sibling comment in its test.
Text only — no schema, delivery behaviour or wire value moves. A new pin asserts
the form description names `payload.locale` and the deployment default and
refuses a bare "recipient locale".
Part of #12446
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157mMVAq9fjGe2kaSD2aJC8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/service-messaging, touching 2 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via MessagingServicePlugin (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 316d9ab5571ee8c80a8c6c35e67834cebc3d11db — the merge of head d86c968222b0c5377baea4ea7af71fb968bd52cc into base 76c18be8ad56400c85cde381e63c7755bbd6153a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 316d9ab5571ee8c80a8c6c35e67834cebc3d11db && git checkout 316d9ab5571ee8c80a8c6c35e67834cebc3d11db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76c18be8ad56400c85cde381e63c7755bbd6153a d86c968222b0c5377baea4ea7af71fb968bd52cc && git checkout -B drift-repro 76c18be8ad56400c85cde381e63c7755bbd6153a && git merge --no-ff d86c968222b0c5377baea4ea7af71fb968bd52cc
node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 76c18be8ad56400c85cde381e63c7755bbd6153a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 07:09
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit e577445Aug 26, 2026
35 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-12446-recipient-locale-promise-sweep branch August 26, 2026 07:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one - #12505

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep
Aug 26, 2026
Merged

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one#12505
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes#12446

Text only. No schema accepts or refuses anything it did not before, no delivery behaviour moves, and no wire value changes.

What this corrects

The notify node's template path resolves (name, locale) against sys_email_template with one locale for the whole notification: payload.locale — interpolated once, before fan-out — else the deployment default (II18nService.getDefaultLocale()). sys_user carries no locale column and no request exists at async delivery time, so there is no per-recipient source to read. A per-user locale is deferred until measured pull (maintainer ruling, 2026-08-13) and layers in as an override at that same seam when it lands.

packages/spec was corrected to say exactly that, and that correction is already on main. The same retired promise survived outside the spec file. This PR sweeps it, mirroring the honest wording that already lives at service-messaging/src/email-channel.ts:86-99 and in the corrected io-node-config.zod.ts.

The sites

#sitewhat it said
1service-automation/src/builtin/notify-node.ts — the template field's configSchema description"resolved by (name, recipient locale) at delivery time and rendered per recipient" — the text rendered in the Studio form an author fills in, i.e. the shortest path from wording to an authoring mistake
2content/docs/automation/email-templates.mdx"so one node mails each person in their own language" — the only site that stated the false conclusion outright rather than merely licensing it. That sentence is gone, not softened
3service-messaging/src/messaging-service-plugin.tsthe channel-registration log line, advertising "resolve sys_email_template per recipient locale"
4service-automation/src/builtin/notify-node.ts — the execute-time guard comment"resolved per recipient locale at delivery"
5service-automation/src/builtin/notify-node.test.ts — the sibling commentsame sentence, moves with #4

A sixth site the card did not enumerate, fixed here and called out rather than slipped in:notify-node.ts (the messaging.emit payload comment) described what the outbox snapshots as "the per-recipient-locale resolution happens at delivery time". Same defect class as #4, same file, same already-claimed surface, and its correct form was pinned by the merged spec wording — so it was corrected in place instead of being filed as a separate finding. It is named here because an unnamed drive-by fix is unreviewable scope creep. The card's count of four sites is therefore low by one; the tree, not the card, was the source.

Deliberately NOT touched

  • packages/spec/** — zero files. A different lane owned that half.
  • content/docs/references/automation/io-node-config.mdx — auto-generated from the spec .describe(), and already carries the corrected text.
  • content/docs/releases/v17.mdx:3507 — release-owned; never edited in a code PR. It is a historical record of what shipped under Flow notify nodes cannot be localized: title/message are raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205, not live guidance.
  • email-channel.ts:87-99 / :125-127, inbox-channel.ts:46-51 / :118-124, messaging-service-plugin.ts:138-147 — these use "recipient locale" after defining it as the deployment default in the same block, or describe rendering happening per delivery row, which is accurate. They are the honest exemplars this PR mirrors, so changing them would be churn.

A pin, so this cannot quietly come back

notify-node.test.ts gains one test asserting the form description names payload.locale and the deployment default, states "not one per recipient", and refuses a bare "recipient locale" — the same shape the spec lane used.

Reverse-verified rather than assumed. Restoring the retired sentence on the description was proven on disk by anchored occurrence counts (injected text 1, deleted text 0 — not a bare --stat), and the pin then failed with expected 'Email template name (sys_email_templa…' to match /not one per recipient/ (1 failed | 15 passed). Restoration was proven by state, not by an exit code: the blob hash returned to the HEAD blob 3b7f6056 and git diff HEAD came back empty. The mutation is intra-package source, so no dist and no rebuild is involved on either leg.

Changeset — argued, not defaulted

Shipped: .changeset/notify-node-template-locale-is-not-per-recipient.md, patch on @objectstack/service-automation + @objectstack/service-messaging. skip-changeset would have been wrong here: site 1 is not a comment but a shipped string rendered in the Studio form, and site 3 ships in an operator-visible log line — both leave the package and reach a reader. (An empty changeset was never an option; it stalls the release.)

Verification — everything below ran at d86c9682, the final commit

Exit codes captured before any pipe, and each verdict quoted from the gate's own output line.

  • Derived gate union, re-derived from the real change set rather than from the dispatch's path list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 5 paths vs merge base fe72aa5c1, 38 path-matched families plus the convention-triggered ones. 44 of 44 attempted gates green, including check:nul-bytes, check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract, spec check:docs, spec check:skill-examples, spec check:empty-state, spec check:liveness, spec check:strictness-ledger, spec check:variant-docs, the changeset family (check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, release-rehearsal-clone --self-test), the packages family (check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-comment-mask-adoption, check-plugin-teardown-shape, check:cross-package-test-inputs, check-ci-filter-parity), the docs-drift family (check-affected-docs, check-drift-comment — run rather than assumed inert, since content/docs/** is in scope), and the convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n, check:i18n-stale-fill, check:type-check-coverage.
  • Two gates first refused on a missing prerequisite, which reads as NOT MEASURED rather than redcheck:skill-examples ("Build first, then re-run") and check:i18n ("Nothing was checked: no bundle was compared"). Both prerequisites were built (@objectstack/cli, @objectstack/client-react closures) and both then ran clean: "✅ 260 prose examples type-check across 3 surface(s)" and "check-i18n-bundles: OK (9 package(s) — all bundles in sync)".
  • Tests:@objectstack/service-automationTest Files 91 passed (91), Tests 1083 passed (1083); @objectstack/service-messagingTest Files 29 passed (29), Tests 295 passed (295), plus its typecheck clean. The new pin was confirmed by name, not inferred from a total: ✓ notify (baseline node) > describes 'template' with the locale the delivery path actually resolves, not a per-recipient one.
  • One gate honestly NOT MEASURED locally:check:type-check-debt --re-measure needs the whole workspace closure built and refuses otherwise. Its structural half, check:type-check-coverage, is green, and the ratchet's question was answered directly instead: tsc --noEmit on service-automation reports 3 diagnostics, all in nested-region-parity.test.ts — a file this diff never touches — and zero naming notify-node.ts or notify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself.

Generated by Claude Code

…solves
The `notify` node's `template` path resolves `(name, locale)` with ONE locale
for the whole notification: `payload.locale`, interpolated once before fan-out,
else the deployment default (`II18nService.getDefaultLocale()`). `sys_user`
carries no locale column and no request exists at async delivery time, so there
is no per-recipient source to read; a per-user locale is deferred until measured
pull (maintainer ruling, 2026-08-13).
`packages/spec` was corrected to say so. The same retired promise survived in
five sites outside it, two of them the ones an app author actually reads:
- the `template` field's `configSchema` description — the Studio form text —
which said the row is "resolved by (name, recipient locale) ... and rendered
per recipient";
- `content/docs/automation/email-templates.mdx`, the only site to state the
conclusion outright: "so one node mails each person in their own language";
- the messaging channel-registration log line;
- two internal comments in `notify-node.ts` (the execute-time guard and the
payload the outbox snapshots) and the sibling comment in its test.
Text only — no schema, delivery behaviour or wire value moves. A new pin asserts
the form description names `payload.locale` and the deployment default and
refuses a bare "recipient locale".
Part of #12446
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157mMVAq9fjGe2kaSD2aJC8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/service-messaging, touching 2 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via MessagingServicePlugin (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 316d9ab5571ee8c80a8c6c35e67834cebc3d11db — the merge of head d86c968222b0c5377baea4ea7af71fb968bd52cc into base 76c18be8ad56400c85cde381e63c7755bbd6153a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 316d9ab5571ee8c80a8c6c35e67834cebc3d11db && git checkout 316d9ab5571ee8c80a8c6c35e67834cebc3d11db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76c18be8ad56400c85cde381e63c7755bbd6153a d86c968222b0c5377baea4ea7af71fb968bd52cc && git checkout -B drift-repro 76c18be8ad56400c85cde381e63c7755bbd6153a && git merge --no-ff d86c968222b0c5377baea4ea7af71fb968bd52cc
node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 76c18be8ad56400c85cde381e63c7755bbd6153a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 07:09
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit e577445Aug 26, 2026
35 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-12446-recipient-locale-promise-sweep branch August 26, 2026 07:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one - #12505

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep
Aug 26, 2026
Merged

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one#12505
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes#12446

Text only. No schema accepts or refuses anything it did not before, no delivery behaviour moves, and no wire value changes.

What this corrects

The notify node's template path resolves (name, locale) against sys_email_template with one locale for the whole notification: payload.locale — interpolated once, before fan-out — else the deployment default (II18nService.getDefaultLocale()). sys_user carries no locale column and no request exists at async delivery time, so there is no per-recipient source to read. A per-user locale is deferred until measured pull (maintainer ruling, 2026-08-13) and layers in as an override at that same seam when it lands.

packages/spec was corrected to say exactly that, and that correction is already on main. The same retired promise survived outside the spec file. This PR sweeps it, mirroring the honest wording that already lives at service-messaging/src/email-channel.ts:86-99 and in the corrected io-node-config.zod.ts.

The sites

#sitewhat it said
1service-automation/src/builtin/notify-node.ts — the template field's configSchema description"resolved by (name, recipient locale) at delivery time and rendered per recipient" — the text rendered in the Studio form an author fills in, i.e. the shortest path from wording to an authoring mistake
2content/docs/automation/email-templates.mdx"so one node mails each person in their own language" — the only site that stated the false conclusion outright rather than merely licensing it. That sentence is gone, not softened
3service-messaging/src/messaging-service-plugin.tsthe channel-registration log line, advertising "resolve sys_email_template per recipient locale"
4service-automation/src/builtin/notify-node.ts — the execute-time guard comment"resolved per recipient locale at delivery"
5service-automation/src/builtin/notify-node.test.ts — the sibling commentsame sentence, moves with #4

A sixth site the card did not enumerate, fixed here and called out rather than slipped in:notify-node.ts (the messaging.emit payload comment) described what the outbox snapshots as "the per-recipient-locale resolution happens at delivery time". Same defect class as #4, same file, same already-claimed surface, and its correct form was pinned by the merged spec wording — so it was corrected in place instead of being filed as a separate finding. It is named here because an unnamed drive-by fix is unreviewable scope creep. The card's count of four sites is therefore low by one; the tree, not the card, was the source.

Deliberately NOT touched

  • packages/spec/** — zero files. A different lane owned that half.
  • content/docs/references/automation/io-node-config.mdx — auto-generated from the spec .describe(), and already carries the corrected text.
  • content/docs/releases/v17.mdx:3507 — release-owned; never edited in a code PR. It is a historical record of what shipped under Flow notify nodes cannot be localized: title/message are raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205, not live guidance.
  • email-channel.ts:87-99 / :125-127, inbox-channel.ts:46-51 / :118-124, messaging-service-plugin.ts:138-147 — these use "recipient locale" after defining it as the deployment default in the same block, or describe rendering happening per delivery row, which is accurate. They are the honest exemplars this PR mirrors, so changing them would be churn.

A pin, so this cannot quietly come back

notify-node.test.ts gains one test asserting the form description names payload.locale and the deployment default, states "not one per recipient", and refuses a bare "recipient locale" — the same shape the spec lane used.

Reverse-verified rather than assumed. Restoring the retired sentence on the description was proven on disk by anchored occurrence counts (injected text 1, deleted text 0 — not a bare --stat), and the pin then failed with expected 'Email template name (sys_email_templa…' to match /not one per recipient/ (1 failed | 15 passed). Restoration was proven by state, not by an exit code: the blob hash returned to the HEAD blob 3b7f6056 and git diff HEAD came back empty. The mutation is intra-package source, so no dist and no rebuild is involved on either leg.

Changeset — argued, not defaulted

Shipped: .changeset/notify-node-template-locale-is-not-per-recipient.md, patch on @objectstack/service-automation + @objectstack/service-messaging. skip-changeset would have been wrong here: site 1 is not a comment but a shipped string rendered in the Studio form, and site 3 ships in an operator-visible log line — both leave the package and reach a reader. (An empty changeset was never an option; it stalls the release.)

Verification — everything below ran at d86c9682, the final commit

Exit codes captured before any pipe, and each verdict quoted from the gate's own output line.

  • Derived gate union, re-derived from the real change set rather than from the dispatch's path list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 5 paths vs merge base fe72aa5c1, 38 path-matched families plus the convention-triggered ones. 44 of 44 attempted gates green, including check:nul-bytes, check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract, spec check:docs, spec check:skill-examples, spec check:empty-state, spec check:liveness, spec check:strictness-ledger, spec check:variant-docs, the changeset family (check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, release-rehearsal-clone --self-test), the packages family (check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-comment-mask-adoption, check-plugin-teardown-shape, check:cross-package-test-inputs, check-ci-filter-parity), the docs-drift family (check-affected-docs, check-drift-comment — run rather than assumed inert, since content/docs/** is in scope), and the convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n, check:i18n-stale-fill, check:type-check-coverage.
  • Two gates first refused on a missing prerequisite, which reads as NOT MEASURED rather than redcheck:skill-examples ("Build first, then re-run") and check:i18n ("Nothing was checked: no bundle was compared"). Both prerequisites were built (@objectstack/cli, @objectstack/client-react closures) and both then ran clean: "✅ 260 prose examples type-check across 3 surface(s)" and "check-i18n-bundles: OK (9 package(s) — all bundles in sync)".
  • Tests:@objectstack/service-automationTest Files 91 passed (91), Tests 1083 passed (1083); @objectstack/service-messagingTest Files 29 passed (29), Tests 295 passed (295), plus its typecheck clean. The new pin was confirmed by name, not inferred from a total: ✓ notify (baseline node) > describes 'template' with the locale the delivery path actually resolves, not a per-recipient one.
  • One gate honestly NOT MEASURED locally:check:type-check-debt --re-measure needs the whole workspace closure built and refuses otherwise. Its structural half, check:type-check-coverage, is green, and the ratchet's question was answered directly instead: tsc --noEmit on service-automation reports 3 diagnostics, all in nested-region-parity.test.ts — a file this diff never touches — and zero naming notify-node.ts or notify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself.

Generated by Claude Code

…solves
The `notify` node's `template` path resolves `(name, locale)` with ONE locale
for the whole notification: `payload.locale`, interpolated once before fan-out,
else the deployment default (`II18nService.getDefaultLocale()`). `sys_user`
carries no locale column and no request exists at async delivery time, so there
is no per-recipient source to read; a per-user locale is deferred until measured
pull (maintainer ruling, 2026-08-13).
`packages/spec` was corrected to say so. The same retired promise survived in
five sites outside it, two of them the ones an app author actually reads:
- the `template` field's `configSchema` description — the Studio form text —
which said the row is "resolved by (name, recipient locale) ... and rendered
per recipient";
- `content/docs/automation/email-templates.mdx`, the only site to state the
conclusion outright: "so one node mails each person in their own language";
- the messaging channel-registration log line;
- two internal comments in `notify-node.ts` (the execute-time guard and the
payload the outbox snapshots) and the sibling comment in its test.
Text only — no schema, delivery behaviour or wire value moves. A new pin asserts
the form description names `payload.locale` and the deployment default and
refuses a bare "recipient locale".
Part of #12446
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157mMVAq9fjGe2kaSD2aJC8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/service-messaging, touching 2 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via MessagingServicePlugin (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 316d9ab5571ee8c80a8c6c35e67834cebc3d11db — the merge of head d86c968222b0c5377baea4ea7af71fb968bd52cc into base 76c18be8ad56400c85cde381e63c7755bbd6153a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 316d9ab5571ee8c80a8c6c35e67834cebc3d11db && git checkout 316d9ab5571ee8c80a8c6c35e67834cebc3d11db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76c18be8ad56400c85cde381e63c7755bbd6153a d86c968222b0c5377baea4ea7af71fb968bd52cc && git checkout -B drift-repro 76c18be8ad56400c85cde381e63c7755bbd6153a && git merge --no-ff d86c968222b0c5377baea4ea7af71fb968bd52cc
node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 76c18be8ad56400c85cde381e63c7755bbd6153a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 07:09
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit e577445Aug 26, 2026
35 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-12446-recipient-locale-promise-sweep branch August 26, 2026 07:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one - #12505

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep
Aug 26, 2026
Merged

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one#12505
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes#12446

Text only. No schema accepts or refuses anything it did not before, no delivery behaviour moves, and no wire value changes.

What this corrects

The notify node's template path resolves (name, locale) against sys_email_template with one locale for the whole notification: payload.locale — interpolated once, before fan-out — else the deployment default (II18nService.getDefaultLocale()). sys_user carries no locale column and no request exists at async delivery time, so there is no per-recipient source to read. A per-user locale is deferred until measured pull (maintainer ruling, 2026-08-13) and layers in as an override at that same seam when it lands.

packages/spec was corrected to say exactly that, and that correction is already on main. The same retired promise survived outside the spec file. This PR sweeps it, mirroring the honest wording that already lives at service-messaging/src/email-channel.ts:86-99 and in the corrected io-node-config.zod.ts.

The sites

#sitewhat it said
1service-automation/src/builtin/notify-node.ts — the template field's configSchema description"resolved by (name, recipient locale) at delivery time and rendered per recipient" — the text rendered in the Studio form an author fills in, i.e. the shortest path from wording to an authoring mistake
2content/docs/automation/email-templates.mdx"so one node mails each person in their own language" — the only site that stated the false conclusion outright rather than merely licensing it. That sentence is gone, not softened
3service-messaging/src/messaging-service-plugin.tsthe channel-registration log line, advertising "resolve sys_email_template per recipient locale"
4service-automation/src/builtin/notify-node.ts — the execute-time guard comment"resolved per recipient locale at delivery"
5service-automation/src/builtin/notify-node.test.ts — the sibling commentsame sentence, moves with #4

A sixth site the card did not enumerate, fixed here and called out rather than slipped in:notify-node.ts (the messaging.emit payload comment) described what the outbox snapshots as "the per-recipient-locale resolution happens at delivery time". Same defect class as #4, same file, same already-claimed surface, and its correct form was pinned by the merged spec wording — so it was corrected in place instead of being filed as a separate finding. It is named here because an unnamed drive-by fix is unreviewable scope creep. The card's count of four sites is therefore low by one; the tree, not the card, was the source.

Deliberately NOT touched

  • packages/spec/** — zero files. A different lane owned that half.
  • content/docs/references/automation/io-node-config.mdx — auto-generated from the spec .describe(), and already carries the corrected text.
  • content/docs/releases/v17.mdx:3507 — release-owned; never edited in a code PR. It is a historical record of what shipped under Flow notify nodes cannot be localized: title/message are raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205, not live guidance.
  • email-channel.ts:87-99 / :125-127, inbox-channel.ts:46-51 / :118-124, messaging-service-plugin.ts:138-147 — these use "recipient locale" after defining it as the deployment default in the same block, or describe rendering happening per delivery row, which is accurate. They are the honest exemplars this PR mirrors, so changing them would be churn.

A pin, so this cannot quietly come back

notify-node.test.ts gains one test asserting the form description names payload.locale and the deployment default, states "not one per recipient", and refuses a bare "recipient locale" — the same shape the spec lane used.

Reverse-verified rather than assumed. Restoring the retired sentence on the description was proven on disk by anchored occurrence counts (injected text 1, deleted text 0 — not a bare --stat), and the pin then failed with expected 'Email template name (sys_email_templa…' to match /not one per recipient/ (1 failed | 15 passed). Restoration was proven by state, not by an exit code: the blob hash returned to the HEAD blob 3b7f6056 and git diff HEAD came back empty. The mutation is intra-package source, so no dist and no rebuild is involved on either leg.

Changeset — argued, not defaulted

Shipped: .changeset/notify-node-template-locale-is-not-per-recipient.md, patch on @objectstack/service-automation + @objectstack/service-messaging. skip-changeset would have been wrong here: site 1 is not a comment but a shipped string rendered in the Studio form, and site 3 ships in an operator-visible log line — both leave the package and reach a reader. (An empty changeset was never an option; it stalls the release.)

Verification — everything below ran at d86c9682, the final commit

Exit codes captured before any pipe, and each verdict quoted from the gate's own output line.

  • Derived gate union, re-derived from the real change set rather than from the dispatch's path list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 5 paths vs merge base fe72aa5c1, 38 path-matched families plus the convention-triggered ones. 44 of 44 attempted gates green, including check:nul-bytes, check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract, spec check:docs, spec check:skill-examples, spec check:empty-state, spec check:liveness, spec check:strictness-ledger, spec check:variant-docs, the changeset family (check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, release-rehearsal-clone --self-test), the packages family (check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-comment-mask-adoption, check-plugin-teardown-shape, check:cross-package-test-inputs, check-ci-filter-parity), the docs-drift family (check-affected-docs, check-drift-comment — run rather than assumed inert, since content/docs/** is in scope), and the convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n, check:i18n-stale-fill, check:type-check-coverage.
  • Two gates first refused on a missing prerequisite, which reads as NOT MEASURED rather than redcheck:skill-examples ("Build first, then re-run") and check:i18n ("Nothing was checked: no bundle was compared"). Both prerequisites were built (@objectstack/cli, @objectstack/client-react closures) and both then ran clean: "✅ 260 prose examples type-check across 3 surface(s)" and "check-i18n-bundles: OK (9 package(s) — all bundles in sync)".
  • Tests:@objectstack/service-automationTest Files 91 passed (91), Tests 1083 passed (1083); @objectstack/service-messagingTest Files 29 passed (29), Tests 295 passed (295), plus its typecheck clean. The new pin was confirmed by name, not inferred from a total: ✓ notify (baseline node) > describes 'template' with the locale the delivery path actually resolves, not a per-recipient one.
  • One gate honestly NOT MEASURED locally:check:type-check-debt --re-measure needs the whole workspace closure built and refuses otherwise. Its structural half, check:type-check-coverage, is green, and the ratchet's question was answered directly instead: tsc --noEmit on service-automation reports 3 diagnostics, all in nested-region-parity.test.ts — a file this diff never touches — and zero naming notify-node.ts or notify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself.

Generated by Claude Code

…solves
The `notify` node's `template` path resolves `(name, locale)` with ONE locale
for the whole notification: `payload.locale`, interpolated once before fan-out,
else the deployment default (`II18nService.getDefaultLocale()`). `sys_user`
carries no locale column and no request exists at async delivery time, so there
is no per-recipient source to read; a per-user locale is deferred until measured
pull (maintainer ruling, 2026-08-13).
`packages/spec` was corrected to say so. The same retired promise survived in
five sites outside it, two of them the ones an app author actually reads:
- the `template` field's `configSchema` description — the Studio form text —
which said the row is "resolved by (name, recipient locale) ... and rendered
per recipient";
- `content/docs/automation/email-templates.mdx`, the only site to state the
conclusion outright: "so one node mails each person in their own language";
- the messaging channel-registration log line;
- two internal comments in `notify-node.ts` (the execute-time guard and the
payload the outbox snapshots) and the sibling comment in its test.
Text only — no schema, delivery behaviour or wire value moves. A new pin asserts
the form description names `payload.locale` and the deployment default and
refuses a bare "recipient locale".
Part of #12446
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157mMVAq9fjGe2kaSD2aJC8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/service-messaging, touching 2 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via MessagingServicePlugin (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 316d9ab5571ee8c80a8c6c35e67834cebc3d11db — the merge of head d86c968222b0c5377baea4ea7af71fb968bd52cc into base 76c18be8ad56400c85cde381e63c7755bbd6153a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 316d9ab5571ee8c80a8c6c35e67834cebc3d11db && git checkout 316d9ab5571ee8c80a8c6c35e67834cebc3d11db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76c18be8ad56400c85cde381e63c7755bbd6153a d86c968222b0c5377baea4ea7af71fb968bd52cc && git checkout -B drift-repro 76c18be8ad56400c85cde381e63c7755bbd6153a && git merge --no-ff d86c968222b0c5377baea4ea7af71fb968bd52cc
node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 76c18be8ad56400c85cde381e63c7755bbd6153a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 07:09
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit e577445Aug 26, 2026
35 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-12446-recipient-locale-promise-sweep branch August 26, 2026 07:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one - #12505

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep
Aug 26, 2026
Merged

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one#12505
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes#12446

Text only. No schema accepts or refuses anything it did not before, no delivery behaviour moves, and no wire value changes.

What this corrects

The notify node's template path resolves (name, locale) against sys_email_template with one locale for the whole notification: payload.locale — interpolated once, before fan-out — else the deployment default (II18nService.getDefaultLocale()). sys_user carries no locale column and no request exists at async delivery time, so there is no per-recipient source to read. A per-user locale is deferred until measured pull (maintainer ruling, 2026-08-13) and layers in as an override at that same seam when it lands.

packages/spec was corrected to say exactly that, and that correction is already on main. The same retired promise survived outside the spec file. This PR sweeps it, mirroring the honest wording that already lives at service-messaging/src/email-channel.ts:86-99 and in the corrected io-node-config.zod.ts.

The sites

#sitewhat it said
1service-automation/src/builtin/notify-node.ts — the template field's configSchema description"resolved by (name, recipient locale) at delivery time and rendered per recipient" — the text rendered in the Studio form an author fills in, i.e. the shortest path from wording to an authoring mistake
2content/docs/automation/email-templates.mdx"so one node mails each person in their own language" — the only site that stated the false conclusion outright rather than merely licensing it. That sentence is gone, not softened
3service-messaging/src/messaging-service-plugin.tsthe channel-registration log line, advertising "resolve sys_email_template per recipient locale"
4service-automation/src/builtin/notify-node.ts — the execute-time guard comment"resolved per recipient locale at delivery"
5service-automation/src/builtin/notify-node.test.ts — the sibling commentsame sentence, moves with #4

A sixth site the card did not enumerate, fixed here and called out rather than slipped in:notify-node.ts (the messaging.emit payload comment) described what the outbox snapshots as "the per-recipient-locale resolution happens at delivery time". Same defect class as #4, same file, same already-claimed surface, and its correct form was pinned by the merged spec wording — so it was corrected in place instead of being filed as a separate finding. It is named here because an unnamed drive-by fix is unreviewable scope creep. The card's count of four sites is therefore low by one; the tree, not the card, was the source.

Deliberately NOT touched

  • packages/spec/** — zero files. A different lane owned that half.
  • content/docs/references/automation/io-node-config.mdx — auto-generated from the spec .describe(), and already carries the corrected text.
  • content/docs/releases/v17.mdx:3507 — release-owned; never edited in a code PR. It is a historical record of what shipped under Flow notify nodes cannot be localized: title/message are raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205, not live guidance.
  • email-channel.ts:87-99 / :125-127, inbox-channel.ts:46-51 / :118-124, messaging-service-plugin.ts:138-147 — these use "recipient locale" after defining it as the deployment default in the same block, or describe rendering happening per delivery row, which is accurate. They are the honest exemplars this PR mirrors, so changing them would be churn.

A pin, so this cannot quietly come back

notify-node.test.ts gains one test asserting the form description names payload.locale and the deployment default, states "not one per recipient", and refuses a bare "recipient locale" — the same shape the spec lane used.

Reverse-verified rather than assumed. Restoring the retired sentence on the description was proven on disk by anchored occurrence counts (injected text 1, deleted text 0 — not a bare --stat), and the pin then failed with expected 'Email template name (sys_email_templa…' to match /not one per recipient/ (1 failed | 15 passed). Restoration was proven by state, not by an exit code: the blob hash returned to the HEAD blob 3b7f6056 and git diff HEAD came back empty. The mutation is intra-package source, so no dist and no rebuild is involved on either leg.

Changeset — argued, not defaulted

Shipped: .changeset/notify-node-template-locale-is-not-per-recipient.md, patch on @objectstack/service-automation + @objectstack/service-messaging. skip-changeset would have been wrong here: site 1 is not a comment but a shipped string rendered in the Studio form, and site 3 ships in an operator-visible log line — both leave the package and reach a reader. (An empty changeset was never an option; it stalls the release.)

Verification — everything below ran at d86c9682, the final commit

Exit codes captured before any pipe, and each verdict quoted from the gate's own output line.

  • Derived gate union, re-derived from the real change set rather than from the dispatch's path list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 5 paths vs merge base fe72aa5c1, 38 path-matched families plus the convention-triggered ones. 44 of 44 attempted gates green, including check:nul-bytes, check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract, spec check:docs, spec check:skill-examples, spec check:empty-state, spec check:liveness, spec check:strictness-ledger, spec check:variant-docs, the changeset family (check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, release-rehearsal-clone --self-test), the packages family (check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-comment-mask-adoption, check-plugin-teardown-shape, check:cross-package-test-inputs, check-ci-filter-parity), the docs-drift family (check-affected-docs, check-drift-comment — run rather than assumed inert, since content/docs/** is in scope), and the convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n, check:i18n-stale-fill, check:type-check-coverage.
  • Two gates first refused on a missing prerequisite, which reads as NOT MEASURED rather than redcheck:skill-examples ("Build first, then re-run") and check:i18n ("Nothing was checked: no bundle was compared"). Both prerequisites were built (@objectstack/cli, @objectstack/client-react closures) and both then ran clean: "✅ 260 prose examples type-check across 3 surface(s)" and "check-i18n-bundles: OK (9 package(s) — all bundles in sync)".
  • Tests:@objectstack/service-automationTest Files 91 passed (91), Tests 1083 passed (1083); @objectstack/service-messagingTest Files 29 passed (29), Tests 295 passed (295), plus its typecheck clean. The new pin was confirmed by name, not inferred from a total: ✓ notify (baseline node) > describes 'template' with the locale the delivery path actually resolves, not a per-recipient one.
  • One gate honestly NOT MEASURED locally:check:type-check-debt --re-measure needs the whole workspace closure built and refuses otherwise. Its structural half, check:type-check-coverage, is green, and the ratchet's question was answered directly instead: tsc --noEmit on service-automation reports 3 diagnostics, all in nested-region-parity.test.ts — a file this diff never touches — and zero naming notify-node.ts or notify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself.

Generated by Claude Code

…solves
The `notify` node's `template` path resolves `(name, locale)` with ONE locale
for the whole notification: `payload.locale`, interpolated once before fan-out,
else the deployment default (`II18nService.getDefaultLocale()`). `sys_user`
carries no locale column and no request exists at async delivery time, so there
is no per-recipient source to read; a per-user locale is deferred until measured
pull (maintainer ruling, 2026-08-13).
`packages/spec` was corrected to say so. The same retired promise survived in
five sites outside it, two of them the ones an app author actually reads:
- the `template` field's `configSchema` description — the Studio form text —
which said the row is "resolved by (name, recipient locale) ... and rendered
per recipient";
- `content/docs/automation/email-templates.mdx`, the only site to state the
conclusion outright: "so one node mails each person in their own language";
- the messaging channel-registration log line;
- two internal comments in `notify-node.ts` (the execute-time guard and the
payload the outbox snapshots) and the sibling comment in its test.
Text only — no schema, delivery behaviour or wire value moves. A new pin asserts
the form description names `payload.locale` and the deployment default and
refuses a bare "recipient locale".
Part of #12446
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157mMVAq9fjGe2kaSD2aJC8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/service-messaging, touching 2 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via MessagingServicePlugin (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 316d9ab5571ee8c80a8c6c35e67834cebc3d11db — the merge of head d86c968222b0c5377baea4ea7af71fb968bd52cc into base 76c18be8ad56400c85cde381e63c7755bbd6153a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 316d9ab5571ee8c80a8c6c35e67834cebc3d11db && git checkout 316d9ab5571ee8c80a8c6c35e67834cebc3d11db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76c18be8ad56400c85cde381e63c7755bbd6153a d86c968222b0c5377baea4ea7af71fb968bd52cc && git checkout -B drift-repro 76c18be8ad56400c85cde381e63c7755bbd6153a && git merge --no-ff d86c968222b0c5377baea4ea7af71fb968bd52cc
node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 76c18be8ad56400c85cde381e63c7755bbd6153a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 07:09
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit e577445Aug 26, 2026
35 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-12446-recipient-locale-promise-sweep branch August 26, 2026 07:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one - #12505

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep
Aug 26, 2026
Merged

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one#12505
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes#12446

Text only. No schema accepts or refuses anything it did not before, no delivery behaviour moves, and no wire value changes.

What this corrects

The notify node's template path resolves (name, locale) against sys_email_template with one locale for the whole notification: payload.locale — interpolated once, before fan-out — else the deployment default (II18nService.getDefaultLocale()). sys_user carries no locale column and no request exists at async delivery time, so there is no per-recipient source to read. A per-user locale is deferred until measured pull (maintainer ruling, 2026-08-13) and layers in as an override at that same seam when it lands.

packages/spec was corrected to say exactly that, and that correction is already on main. The same retired promise survived outside the spec file. This PR sweeps it, mirroring the honest wording that already lives at service-messaging/src/email-channel.ts:86-99 and in the corrected io-node-config.zod.ts.

The sites

#sitewhat it said
1service-automation/src/builtin/notify-node.ts — the template field's configSchema description"resolved by (name, recipient locale) at delivery time and rendered per recipient" — the text rendered in the Studio form an author fills in, i.e. the shortest path from wording to an authoring mistake
2content/docs/automation/email-templates.mdx"so one node mails each person in their own language" — the only site that stated the false conclusion outright rather than merely licensing it. That sentence is gone, not softened
3service-messaging/src/messaging-service-plugin.tsthe channel-registration log line, advertising "resolve sys_email_template per recipient locale"
4service-automation/src/builtin/notify-node.ts — the execute-time guard comment"resolved per recipient locale at delivery"
5service-automation/src/builtin/notify-node.test.ts — the sibling commentsame sentence, moves with #4

A sixth site the card did not enumerate, fixed here and called out rather than slipped in:notify-node.ts (the messaging.emit payload comment) described what the outbox snapshots as "the per-recipient-locale resolution happens at delivery time". Same defect class as #4, same file, same already-claimed surface, and its correct form was pinned by the merged spec wording — so it was corrected in place instead of being filed as a separate finding. It is named here because an unnamed drive-by fix is unreviewable scope creep. The card's count of four sites is therefore low by one; the tree, not the card, was the source.

Deliberately NOT touched

  • packages/spec/** — zero files. A different lane owned that half.
  • content/docs/references/automation/io-node-config.mdx — auto-generated from the spec .describe(), and already carries the corrected text.
  • content/docs/releases/v17.mdx:3507 — release-owned; never edited in a code PR. It is a historical record of what shipped under Flow notify nodes cannot be localized: title/message are raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205, not live guidance.
  • email-channel.ts:87-99 / :125-127, inbox-channel.ts:46-51 / :118-124, messaging-service-plugin.ts:138-147 — these use "recipient locale" after defining it as the deployment default in the same block, or describe rendering happening per delivery row, which is accurate. They are the honest exemplars this PR mirrors, so changing them would be churn.

A pin, so this cannot quietly come back

notify-node.test.ts gains one test asserting the form description names payload.locale and the deployment default, states "not one per recipient", and refuses a bare "recipient locale" — the same shape the spec lane used.

Reverse-verified rather than assumed. Restoring the retired sentence on the description was proven on disk by anchored occurrence counts (injected text 1, deleted text 0 — not a bare --stat), and the pin then failed with expected 'Email template name (sys_email_templa…' to match /not one per recipient/ (1 failed | 15 passed). Restoration was proven by state, not by an exit code: the blob hash returned to the HEAD blob 3b7f6056 and git diff HEAD came back empty. The mutation is intra-package source, so no dist and no rebuild is involved on either leg.

Changeset — argued, not defaulted

Shipped: .changeset/notify-node-template-locale-is-not-per-recipient.md, patch on @objectstack/service-automation + @objectstack/service-messaging. skip-changeset would have been wrong here: site 1 is not a comment but a shipped string rendered in the Studio form, and site 3 ships in an operator-visible log line — both leave the package and reach a reader. (An empty changeset was never an option; it stalls the release.)

Verification — everything below ran at d86c9682, the final commit

Exit codes captured before any pipe, and each verdict quoted from the gate's own output line.

  • Derived gate union, re-derived from the real change set rather than from the dispatch's path list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 5 paths vs merge base fe72aa5c1, 38 path-matched families plus the convention-triggered ones. 44 of 44 attempted gates green, including check:nul-bytes, check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract, spec check:docs, spec check:skill-examples, spec check:empty-state, spec check:liveness, spec check:strictness-ledger, spec check:variant-docs, the changeset family (check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, release-rehearsal-clone --self-test), the packages family (check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-comment-mask-adoption, check-plugin-teardown-shape, check:cross-package-test-inputs, check-ci-filter-parity), the docs-drift family (check-affected-docs, check-drift-comment — run rather than assumed inert, since content/docs/** is in scope), and the convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n, check:i18n-stale-fill, check:type-check-coverage.
  • Two gates first refused on a missing prerequisite, which reads as NOT MEASURED rather than redcheck:skill-examples ("Build first, then re-run") and check:i18n ("Nothing was checked: no bundle was compared"). Both prerequisites were built (@objectstack/cli, @objectstack/client-react closures) and both then ran clean: "✅ 260 prose examples type-check across 3 surface(s)" and "check-i18n-bundles: OK (9 package(s) — all bundles in sync)".
  • Tests:@objectstack/service-automationTest Files 91 passed (91), Tests 1083 passed (1083); @objectstack/service-messagingTest Files 29 passed (29), Tests 295 passed (295), plus its typecheck clean. The new pin was confirmed by name, not inferred from a total: ✓ notify (baseline node) > describes 'template' with the locale the delivery path actually resolves, not a per-recipient one.
  • One gate honestly NOT MEASURED locally:check:type-check-debt --re-measure needs the whole workspace closure built and refuses otherwise. Its structural half, check:type-check-coverage, is green, and the ratchet's question was answered directly instead: tsc --noEmit on service-automation reports 3 diagnostics, all in nested-region-parity.test.ts — a file this diff never touches — and zero naming notify-node.ts or notify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself.

Generated by Claude Code

…solves
The `notify` node's `template` path resolves `(name, locale)` with ONE locale
for the whole notification: `payload.locale`, interpolated once before fan-out,
else the deployment default (`II18nService.getDefaultLocale()`). `sys_user`
carries no locale column and no request exists at async delivery time, so there
is no per-recipient source to read; a per-user locale is deferred until measured
pull (maintainer ruling, 2026-08-13).
`packages/spec` was corrected to say so. The same retired promise survived in
five sites outside it, two of them the ones an app author actually reads:
- the `template` field's `configSchema` description — the Studio form text —
which said the row is "resolved by (name, recipient locale) ... and rendered
per recipient";
- `content/docs/automation/email-templates.mdx`, the only site to state the
conclusion outright: "so one node mails each person in their own language";
- the messaging channel-registration log line;
- two internal comments in `notify-node.ts` (the execute-time guard and the
payload the outbox snapshots) and the sibling comment in its test.
Text only — no schema, delivery behaviour or wire value moves. A new pin asserts
the form description names `payload.locale` and the deployment default and
refuses a bare "recipient locale".
Part of #12446
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157mMVAq9fjGe2kaSD2aJC8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/service-messaging, touching 2 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via MessagingServicePlugin (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 316d9ab5571ee8c80a8c6c35e67834cebc3d11db — the merge of head d86c968222b0c5377baea4ea7af71fb968bd52cc into base 76c18be8ad56400c85cde381e63c7755bbd6153a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 316d9ab5571ee8c80a8c6c35e67834cebc3d11db && git checkout 316d9ab5571ee8c80a8c6c35e67834cebc3d11db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76c18be8ad56400c85cde381e63c7755bbd6153a d86c968222b0c5377baea4ea7af71fb968bd52cc && git checkout -B drift-repro 76c18be8ad56400c85cde381e63c7755bbd6153a && git merge --no-ff d86c968222b0c5377baea4ea7af71fb968bd52cc
node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 76c18be8ad56400c85cde381e63c7755bbd6153a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 07:09
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit e577445Aug 26, 2026
35 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-12446-recipient-locale-promise-sweep branch August 26, 2026 07:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one - #12505

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep
Aug 26, 2026
Merged

docs(services): state the notify template locale the delivery path resolves, not a per-recipient one#12505
os-support-ai merged 1 commit into
mainfrom
claude/issue-12446-recipient-locale-promise-sweep

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes#12446

Text only. No schema accepts or refuses anything it did not before, no delivery behaviour moves, and no wire value changes.

What this corrects

The notify node's template path resolves (name, locale) against sys_email_template with one locale for the whole notification: payload.locale — interpolated once, before fan-out — else the deployment default (II18nService.getDefaultLocale()). sys_user carries no locale column and no request exists at async delivery time, so there is no per-recipient source to read. A per-user locale is deferred until measured pull (maintainer ruling, 2026-08-13) and layers in as an override at that same seam when it lands.

packages/spec was corrected to say exactly that, and that correction is already on main. The same retired promise survived outside the spec file. This PR sweeps it, mirroring the honest wording that already lives at service-messaging/src/email-channel.ts:86-99 and in the corrected io-node-config.zod.ts.

The sites

#sitewhat it said
1service-automation/src/builtin/notify-node.ts — the template field's configSchema description"resolved by (name, recipient locale) at delivery time and rendered per recipient" — the text rendered in the Studio form an author fills in, i.e. the shortest path from wording to an authoring mistake
2content/docs/automation/email-templates.mdx"so one node mails each person in their own language" — the only site that stated the false conclusion outright rather than merely licensing it. That sentence is gone, not softened
3service-messaging/src/messaging-service-plugin.tsthe channel-registration log line, advertising "resolve sys_email_template per recipient locale"
4service-automation/src/builtin/notify-node.ts — the execute-time guard comment"resolved per recipient locale at delivery"
5service-automation/src/builtin/notify-node.test.ts — the sibling commentsame sentence, moves with #4

A sixth site the card did not enumerate, fixed here and called out rather than slipped in:notify-node.ts (the messaging.emit payload comment) described what the outbox snapshots as "the per-recipient-locale resolution happens at delivery time". Same defect class as #4, same file, same already-claimed surface, and its correct form was pinned by the merged spec wording — so it was corrected in place instead of being filed as a separate finding. It is named here because an unnamed drive-by fix is unreviewable scope creep. The card's count of four sites is therefore low by one; the tree, not the card, was the source.

Deliberately NOT touched

  • packages/spec/** — zero files. A different lane owned that half.
  • content/docs/references/automation/io-node-config.mdx — auto-generated from the spec .describe(), and already carries the corrected text.
  • content/docs/releases/v17.mdx:3507 — release-owned; never edited in a code PR. It is a historical record of what shipped under Flow notify nodes cannot be localized: title/message are raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205, not live guidance.
  • email-channel.ts:87-99 / :125-127, inbox-channel.ts:46-51 / :118-124, messaging-service-plugin.ts:138-147 — these use "recipient locale" after defining it as the deployment default in the same block, or describe rendering happening per delivery row, which is accurate. They are the honest exemplars this PR mirrors, so changing them would be churn.

A pin, so this cannot quietly come back

notify-node.test.ts gains one test asserting the form description names payload.locale and the deployment default, states "not one per recipient", and refuses a bare "recipient locale" — the same shape the spec lane used.

Reverse-verified rather than assumed. Restoring the retired sentence on the description was proven on disk by anchored occurrence counts (injected text 1, deleted text 0 — not a bare --stat), and the pin then failed with expected 'Email template name (sys_email_templa…' to match /not one per recipient/ (1 failed | 15 passed). Restoration was proven by state, not by an exit code: the blob hash returned to the HEAD blob 3b7f6056 and git diff HEAD came back empty. The mutation is intra-package source, so no dist and no rebuild is involved on either leg.

Changeset — argued, not defaulted

Shipped: .changeset/notify-node-template-locale-is-not-per-recipient.md, patch on @objectstack/service-automation + @objectstack/service-messaging. skip-changeset would have been wrong here: site 1 is not a comment but a shipped string rendered in the Studio form, and site 3 ships in an operator-visible log line — both leave the package and reach a reader. (An empty changeset was never an option; it stalls the release.)

Verification — everything below ran at d86c9682, the final commit

Exit codes captured before any pipe, and each verdict quoted from the gate's own output line.

  • Derived gate union, re-derived from the real change set rather than from the dispatch's path list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 5 paths vs merge base fe72aa5c1, 38 path-matched families plus the convention-triggered ones. 44 of 44 attempted gates green, including check:nul-bytes, check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract, spec check:docs, spec check:skill-examples, spec check:empty-state, spec check:liveness, spec check:strictness-ledger, spec check:variant-docs, the changeset family (check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, release-rehearsal-clone --self-test), the packages family (check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-comment-mask-adoption, check-plugin-teardown-shape, check:cross-package-test-inputs, check-ci-filter-parity), the docs-drift family (check-affected-docs, check-drift-comment — run rather than assumed inert, since content/docs/** is in scope), and the convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n, check:i18n-stale-fill, check:type-check-coverage.
  • Two gates first refused on a missing prerequisite, which reads as NOT MEASURED rather than redcheck:skill-examples ("Build first, then re-run") and check:i18n ("Nothing was checked: no bundle was compared"). Both prerequisites were built (@objectstack/cli, @objectstack/client-react closures) and both then ran clean: "✅ 260 prose examples type-check across 3 surface(s)" and "check-i18n-bundles: OK (9 package(s) — all bundles in sync)".
  • Tests:@objectstack/service-automationTest Files 91 passed (91), Tests 1083 passed (1083); @objectstack/service-messagingTest Files 29 passed (29), Tests 295 passed (295), plus its typecheck clean. The new pin was confirmed by name, not inferred from a total: ✓ notify (baseline node) > describes 'template' with the locale the delivery path actually resolves, not a per-recipient one.
  • One gate honestly NOT MEASURED locally:check:type-check-debt --re-measure needs the whole workspace closure built and refuses otherwise. Its structural half, check:type-check-coverage, is green, and the ratchet's question was answered directly instead: tsc --noEmit on service-automation reports 3 diagnostics, all in nested-region-parity.test.ts — a file this diff never touches — and zero naming notify-node.ts or notify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself.

Generated by Claude Code

…solves
The `notify` node's `template` path resolves `(name, locale)` with ONE locale
for the whole notification: `payload.locale`, interpolated once before fan-out,
else the deployment default (`II18nService.getDefaultLocale()`). `sys_user`
carries no locale column and no request exists at async delivery time, so there
is no per-recipient source to read; a per-user locale is deferred until measured
pull (maintainer ruling, 2026-08-13).
`packages/spec` was corrected to say so. The same retired promise survived in
five sites outside it, two of them the ones an app author actually reads:
- the `template` field's `configSchema` description — the Studio form text —
which said the row is "resolved by (name, recipient locale) ... and rendered
per recipient";
- `content/docs/automation/email-templates.mdx`, the only site to state the
conclusion outright: "so one node mails each person in their own language";
- the messaging channel-registration log line;
- two internal comments in `notify-node.ts` (the execute-time guard and the
payload the outbox snapshots) and the sibling comment in its test.
Text only — no schema, delivery behaviour or wire value moves. A new pin asserts
the form description names `payload.locale` and the deployment default and
refuses a bare "recipient locale".
Part of #12446
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157mMVAq9fjGe2kaSD2aJC8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/service-messaging, touching 2 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via MessagingServicePlugin (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 316d9ab5571ee8c80a8c6c35e67834cebc3d11db — the merge of head d86c968222b0c5377baea4ea7af71fb968bd52cc into base 76c18be8ad56400c85cde381e63c7755bbd6153a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 316d9ab5571ee8c80a8c6c35e67834cebc3d11db && git checkout 316d9ab5571ee8c80a8c6c35e67834cebc3d11db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76c18be8ad56400c85cde381e63c7755bbd6153a d86c968222b0c5377baea4ea7af71fb968bd52cc && git checkout -B drift-repro 76c18be8ad56400c85cde381e63c7755bbd6153a && git merge --no-ff d86c968222b0c5377baea4ea7af71fb968bd52cc
node scripts/docs-audit/affected-docs.mjs --json 76c18be8ad56400c85cde381e63c7755bbd6153a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 76c18be8ad56400c85cde381e63c7755bbd6153a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 07:09
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit e577445Aug 26, 2026
35 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-12446-recipient-locale-promise-sweep branch August 26, 2026 07:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude