Uh oh!
There was an error while loading. Please reload this page.
docs(services): state the notify template locale the delivery path resolves, not a per-recipient one - #12505
Conversation
…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
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Uh oh!
There was an error while loading. Please reload this page.
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
notifynode'stemplatepath resolves(name, locale)againstsys_email_templatewith one locale for the whole notification:payload.locale— interpolated once, before fan-out — else the deployment default (II18nService.getDefaultLocale()).sys_usercarries 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/specwas corrected to say exactly that, and that correction is already onmain. The same retired promise survived outside the spec file. This PR sweeps it, mirroring the honest wording that already lives atservice-messaging/src/email-channel.ts:86-99and in the correctedio-node-config.zod.ts.The sites
service-automation/src/builtin/notify-node.ts— thetemplatefield'sconfigSchemadescriptioncontent/docs/automation/email-templates.mdxservice-messaging/src/messaging-service-plugin.tsservice-automation/src/builtin/notify-node.ts— the execute-time guard commentservice-automation/src/builtin/notify-node.test.ts— the sibling commentA sixth site the card did not enumerate, fixed here and called out rather than slipped in:
notify-node.ts(themessaging.emitpayload 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 Flownotifynodes cannot be localized:title/messageare 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.tsgains one test asserting the form description namespayload.localeand 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 withexpected '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 blob3b7f6056andgit diff HEADcame back empty. The mutation is intra-package source, so nodistand 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-changesetwould 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 commitExit codes captured before any pipe, and each verdict quoted from the gate's own output line.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack— 5 paths vs merge basefe72aa5c1, 38 path-matched families plus the convention-triggered ones. 44 of 44 attempted gates green, includingcheck: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, sincecontent/docs/**is in scope), and the convention-triggeredcheck:query-options-erasure,check:engine-double-contract,check:where-matcher,check:i18n,check:i18n-stale-fill,check:type-check-coverage.check:skill-examples("Build first, then re-run") andcheck:i18n("Nothing was checked: no bundle was compared"). Both prerequisites were built (@objectstack/cli,@objectstack/client-reactclosures) 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)".@objectstack/service-automation—Test Files 91 passed (91),Tests 1083 passed (1083);@objectstack/service-messaging—Test Files 29 passed (29),Tests 295 passed (295), plus itstypecheckclean. 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.check:type-check-debt --re-measureneeds 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 --noEmitonservice-automationreports 3 diagnostics, all innested-region-parity.test.ts— a file this diff never touches — and zero namingnotify-node.tsornotify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself.Generated by Claude Code