You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
spec: NotifyConfigSchema template doc/description promises per-recipient locale resolution that the delivery path deliberately does not do (deployment default, ruled 2026-08-13) #12178
Found while implementing objectstack-ai/hotcrm#1185 (localize 16 notify nodes). The wording below is what made that card look actionable; the implementation is correct and the spec doc block is what drifted, so filing against packages/spec.
The drift
packages/spec/src/automation/io-node-config.zod.ts states, in the NotifyConfigSchema doc block (line ~135) and again in the template field .describe() (line ~188):
L135: "the delivery path resolves (name, recipient locale)per recipient at delivery time via IEmailService.sendTemplate({ template, locale })"
L188: "the delivery path resolves (name, recipient locale) at delivery time and renders subject/body per recipient"
Read plainly, both say the recipient's own locale selects the template row.
The delivery path's own source says the opposite, and says it honestly — packages/services/service-messaging/src/email-channel.ts L86-99:
"The measured source, and its limits, spelled out: the platform has no per-user locale today (sys_user carries no locale column; the 2026-08-13 ruling defers one until measured pull) … So 'recipient locale' resolves to the deployment default — II18nService.getDefaultLocale()."
So "recipient locale" is a term of art here meaning deployment default, and the spec block is the one place it is not qualified.
Measured on the published 17.1.0 artifacts
Two recipients, different locales, one notification, real createEmailChannel / createInboxChannel, stub email service capturing what reaches sendTemplate / renderTemplate:
The only lever is payload.locale, which is interpolated once before fan-out, so it is one value for the whole notification (verified at all three channel.send call sites in service-messaging: fanOut, the outbox single-delivery path, and processDigestGroup — each passes the same payload to every recipient).
Also confirmed: sys_user has no locale field (and carries protection.lock: "full"), and sys_notification_preference has none either — so there is no per-recipient source to read even if a channel wanted one.
service-messaging@17.2.0 is byte-identical on the two locale-resolution lines, so this is not a "fixed in a later release" situation.
Why it is worth a wording fix rather than a shrug
The doc block is the contract a consuming app reads. Quoted at face value it licenses exactly one conclusion — "convert the nodes and non-English users get non-English notifications" — which is false, and the conversion is a net regression when made on that belief (TEMPLATE_* failures classify permanent and dead-letter, and the inbox channel starts requiring an email service with renderTemplate() where inline text needed none).
One nearby log line has the same issue — service-messaging/src/plugin.ts: "notify template refs resolve sys_email_template per recipient locale".
Suggested fix
Qualify the three sites the way email-channel.ts already does — name the resolved value the deployment default (II18nService.getDefaultLocale()), and note that a per-user locale is deferred by the 2026-08-13 ruling and layers in as an override when it lands. No behaviour change.
Note packages/spec/src/automation/io-node-config.test.ts L277/L303 assert /recipient locale/ against these strings, so a reword touches those two assertions.
Not filing the per-user-locale capability itself here — that is the deferred half of the 2026-08-13 ruling and belongs to whoever weighs the pull; hotcrm#1185 is one such datapoint and says so.
Claim: PM loop round R7, wave 3
Session: session_01E5LFCYBJ3q2s6yW6oMLxwy
Branch: claude/issue-12178-notify-locale-wording
Worktree: objectstack-issue-12178
Domain: domain:spec
File surface: packages/spec/src/automation/io-node-config.zod.ts (all "recipient locale" sites — re-measured on origin/main at fire: :135, :181, :188, :251, :272 — the card names two; the sweep must be consistent across all of them), packages/spec/src/automation/io-node-config.test.ts (:277/:303 assertions on /recipient locale/), regenerated projections, changeset (stop on breach; explain in the report). ⛔ NOT in this card: the service-messaging/src/plugin.ts log line (another lane's package — dev files it as an unassigned finding with a back-link instead)
Container & model: S, mode:subagent, model: opus — text-only doc/describe alignment to a recorded ruling; no accept/reject change
Clause-②: no
Serial constraints cleared: no in-flight PR or claim touches automation/io-node-config.* — wave-1/2 PRs and wave-3 sibling #12174 (ui/view.zod.ts) all disjoint; H17 on-hold trigger-file index: no hit.
Dev claim (os-dev subagent, dispatched by the domain:spec PM seat, R7 wave 3).
Session: session_01E5LFCYBJ3q2s6yW6oMLxwy
Branch: claude/issue-12178-notify-locale-wording (pushed, empty, at base af56546)
Worktree: ../objectstack-issue-12178 from fresh origin/main
Starting work now: re-grep every "recipient locale" site in packages/spec/src/automation/io-node-config.zod.ts and rewrite the prose to the ruled deployment-default semantics (2026-08-13 ruling), moving the io-node-config.test.ts assertions onto the new truthful text. No delivery-behaviour change. The service-messaging/src/plugin.ts log line stays out of scope and will be filed as an unassigned finding with a back-link.
{
"issue": 12178,
"status": "done",
"branch": "claude/issue-12178-notify-locale-wording",
"pr": "https://github.com/objectstack-ai/objectstack/pull/12447",
"premise_still_valid": true,
"summary": "Rewrote every 'recipient locale' site in packages/spec/src/automation/io-node-config.zod.ts to state the ruled deployment-default semantics (2026-08-13): the locale is payload.locale if the producer set one, else II18nService.getDefaultLocale(), and it is ONE value per notification rather than one per recipient, with the deferral dated in place. The card named two sites; a re-grep at claim found five (:135 doc block, :181 field JSDoc, :188 describe, :251 and :272 superRefine refusal strings) and all five were corrected, so grep -c 'recipient locale' on the file is now 0. No delivery behaviour touched, no accept/reject change; the generated doc projection was regenerated via gen:docs, never hand-edited. Two corrections for the record: the card's out-of-scope path service-messaging/src/plugin.ts does not exist (it is messaging-service-plugin.ts:250), and the same promise survives at three further non-spec sites, all filed together as #12446.",
"tests": "Gate union run at final commit 4823268 on a clean tree (git status empty). Gate families derived mechanically: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` -> 4-path changeset, 43 path-matched families + convention kinds. 47 gates run, ALL GREEN, exit codes captured before any pipe. Key verdicts, quoted from the gates' own output: spec vitest full package exit=0 ('Test Files 1 passed (1) / Tests 28 passed (28)' on the targeted run); spec typecheck exit=0 (includes check:test-typecheck, so the edited test file IS covered); check:docs exit=0; check:generated exit=0; check:authorable-surface exit=0; check:skill-examples '260 prose examples type-check across 3 surface(s)'; check:liveness, check:empty-state, check:variant-docs, check:strictness-ledger, check:nul-bytes, check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:docs-redirects, check:docs-audit-scope, check:role-word, check:quick-reference-counts, check:published-files, check:published-readme-links, check:merge-driver, check:test-source-alias, check:cross-package-test-inputs, check:type-source-resolution, check:slot-lookup, check:page-declaration-shape, check:spec-parsed-alias, check:changeset-gate-self-tests, check:objectui-changeset, check:engine-double-contract, check:where-matcher, check:query-options-erasure, lint check:doc-formula-expressions, lint check:doc-security-posture, and 12 node scripts/check-*.mjs families all exit=0. SIX gates first reported exit 1 as PREREQUISITE NOT MET on an unbuilt workspace, each printing its own 'Nothing was measured ... it is NOT a finding'; they were re-run as real measurements after building spec, formula, lint, client-react and client, and all five re-runnable ones went green (spec check:docs, check:generated, check:skill-examples, lint check:doc-formula-expressions, lint check:doc-security-posture). Also ran a control-byte self-scan (grep -naP over all four changed files, clean). REVERSE-VERIFICATION, expected direction RED, observed RED: fix committed FIRST so restore had a recovery point, then the pre-fix .describe() text was restored on disk with the new assertions in place. Mutation proved on disk before measuring, not by the editor's exit code -- anchor hits before=1, after=0, old-text hits after=1, hash moved b8e2344f -> 5211ac23, printing 'MUTATION CONFIRMED ON DISK'. Result: vitest exit 1, 'Tests 1 failed | 27 passed (28)', failing on exactly the intended pin ('AssertionError: expected ... to match /\\(name, locale\\)/ at io-node-config.test.ts:321'). Only one test fails because only the describe was mutated, not the refusal messages. RESTORE leg proved, not assumed: `git checkout HEAD -- <absolute path>` (never bare `--`, which reads the index), then hash back to the HEAD blob b8e2344f AND `git diff HEAD` empty -- script carried a trap on EXIT/INT/TERM with absolute paths, but the hash comparison is the proof, not the trap. NO REBUILD LEG APPLIES: the test imports './io-node-config.zod.js' sibling-relative from src, so nothing resolves through exports/dist -- the ablation-dist-preflight condition is not met here.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #12446: the 'per recipient locale' promise survives in 4 sites outside packages/spec -- notify-node.ts:197 (the Studio form field description, highest stakes), content/docs/automation/email-templates.mdx:164-166 ('one node mails each person in their own language'), messaging-service-plugin.ts:250 (the log line the card named, at a different path than stated), and notify-node.ts:288 (comment). Unassigned, unlabeled for PM triage, dedup-searched first, back-linked to #12178 and PR #12447."
],
"deviations": [
"Dedup search used MCP search_issues instead of the REST list endpoints the standing rule prefers: REST is 403 from this dev seat (probed: `GET /repos/objectstack-ai/objectstack/issues` -> HTTP 403), exactly as the dispatch stated. GraphQL pool spend: 2 search calls.",
"Declared narrowing -- check-dev-prereqs is NOT in the green union. It reports '67 of 67 workspace packages declare an entry point under dist/ that is not on disk', i.e. a statement about an unbuilt checkout requiring a full `pnpm build`; that is a repo-scale run CI owns and its output says nothing about this diff. Same for check:type-check-debt --re-measure, which refuses outright without the built workspace closure. The cheap half is covered: spec typecheck (including check:test-typecheck over the edited test file) is green.",
"The card named 2 'recipient locale' sites; the re-grep found 5 and all 5 were corrected, per the dispatch's instruction to sweep the whole file rather than trust either list.",
"The out-of-scope path in the card body and triage comment (service-messaging/src/plugin.ts) does not exist; the real file is messaging-service-plugin.ts. Recorded in #12446 so the next reader is not sent to a missing path."
],
"files_changed": [
"packages/spec/src/automation/io-node-config.zod.ts (5 wording sites)",
"packages/spec/src/automation/io-node-config.test.ts (the :277/:303 assertions moved onto the new text and tightened)",
"content/docs/references/automation/io-node-config.mdx (regenerated via gen:docs, 1 insertion / 1 deletion)",
".changeset/notify-template-locale-is-deployment-default.md (@objectstack/spec: patch, per the describe-only precedent already in the tree)"
]
}
Contract review (clause-② enqueue gate: path limb packages/spec/src/** hit; declaration limb no, confirmed from the diff — same accepts/refusals, only the prose an author reads changes) — PASS, and ACCEPT — reviewer of record: spec seat R7, session_01E5LFCYBJ3q2s6yW6oMLxwy, at CONTRACT_REVIEW_TIER.
Verified against the PR #12447 diff, read in full (4 files, +123/−16):
All FIVE "recipient locale" sites corrected (the card named two; the at-claim re-grep found five and the sweep covered them all — file now greps 0 for the phrase). The wording states exactly the ruled semantics: locale = payload.locale if set (interpolated once, before fan-out) else II18nService.getDefaultLocale(); one value per notification, not per recipient; the 2026-08-13 deferral dated in place so the limitation carries provenance.
The test pins were moved AND inverted into anti-regression guards: they now assert the qualification ((name, locale), deployment default, not per recipient) and REFUSE a bare recipient locale — a future edit cannot quietly restore the promise. Reverse-verification ran with mutation-proven-on-disk and a proven restore leg.
Generated io-node-config.mdx moved by gen:docs only; changeset patch per the describe-only precedent; no delivery behaviour touched.
No governed face ⇒ normal queue path; deviations (MCP dedup fallback over 403'd REST, declared farm-scale narrowings, sanitizer-eaten placeholder respelled via read-back) accepted as recorded.
Landing: ready-flip + enqueue when EVERY check on PR #12447 is green. Card stays pm:dispatched until MERGED.
Activity
huangyiirene commentedon Aug 26, 2026
Claim: PM loop round R7, wave 3
Session:
session_01E5LFCYBJ3q2s6yW6oMLxwyBranch:
claude/issue-12178-notify-locale-wordingWorktree:
objectstack-issue-12178Domain:
domain:specFile surface:
packages/spec/src/automation/io-node-config.zod.ts(all "recipient locale" sites — re-measured onorigin/mainat fire: :135, :181, :188, :251, :272 — the card names two; the sweep must be consistent across all of them),packages/spec/src/automation/io-node-config.test.ts(:277/:303 assertions on/recipient locale/), regenerated projections, changeset (stop on breach; explain in the report). ⛔ NOT in this card: theservice-messaging/src/plugin.tslog line (another lane's package — dev files it as an unassigned finding with a back-link instead)Container & model: S,
mode:subagent,model: opus— text-only doc/describe alignment to a recorded ruling; no accept/reject changeClause-②: no
Serial constraints cleared: no in-flight PR or claim touches
automation/io-node-config.*— wave-1/2 PRs and wave-3 sibling #12174 (ui/view.zod.ts) all disjoint; H17 on-hold trigger-file index: no hit.Generated by Claude Code
huangyiirene commentedon Aug 26, 2026
Dev claim (os-dev subagent, dispatched by the
domain:specPM seat, R7 wave 3).Session:
session_01E5LFCYBJ3q2s6yW6oMLxwyBranch:
claude/issue-12178-notify-locale-wording(pushed, empty, at baseaf56546)Worktree:
../objectstack-issue-12178from freshorigin/mainStarting work now: re-grep every "recipient locale" site in
packages/spec/src/automation/io-node-config.zod.tsand rewrite the prose to the ruled deployment-default semantics (2026-08-13 ruling), moving theio-node-config.test.tsassertions onto the new truthful text. No delivery-behaviour change. Theservice-messaging/src/plugin.tslog line stays out of scope and will be filed as an unassigned finding with a back-link.Generated by Claude Code
Generated by Claude Code
huangyiirene commentedon Aug 26, 2026
os-dev-report
{ "issue": 12178, "status": "done", "branch": "claude/issue-12178-notify-locale-wording", "pr": "https://github.com/objectstack-ai/objectstack/pull/12447", "premise_still_valid": true, "summary": "Rewrote every 'recipient locale' site in packages/spec/src/automation/io-node-config.zod.ts to state the ruled deployment-default semantics (2026-08-13): the locale is payload.locale if the producer set one, else II18nService.getDefaultLocale(), and it is ONE value per notification rather than one per recipient, with the deferral dated in place. The card named two sites; a re-grep at claim found five (:135 doc block, :181 field JSDoc, :188 describe, :251 and :272 superRefine refusal strings) and all five were corrected, so grep -c 'recipient locale' on the file is now 0. No delivery behaviour touched, no accept/reject change; the generated doc projection was regenerated via gen:docs, never hand-edited. Two corrections for the record: the card's out-of-scope path service-messaging/src/plugin.ts does not exist (it is messaging-service-plugin.ts:250), and the same promise survives at three further non-spec sites, all filed together as #12446.", "tests": "Gate union run at final commit 4823268 on a clean tree (git status empty). Gate families derived mechanically: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` -> 4-path changeset, 43 path-matched families + convention kinds. 47 gates run, ALL GREEN, exit codes captured before any pipe. Key verdicts, quoted from the gates' own output: spec vitest full package exit=0 ('Test Files 1 passed (1) / Tests 28 passed (28)' on the targeted run); spec typecheck exit=0 (includes check:test-typecheck, so the edited test file IS covered); check:docs exit=0; check:generated exit=0; check:authorable-surface exit=0; check:skill-examples '260 prose examples type-check across 3 surface(s)'; check:liveness, check:empty-state, check:variant-docs, check:strictness-ledger, check:nul-bytes, check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:docs-redirects, check:docs-audit-scope, check:role-word, check:quick-reference-counts, check:published-files, check:published-readme-links, check:merge-driver, check:test-source-alias, check:cross-package-test-inputs, check:type-source-resolution, check:slot-lookup, check:page-declaration-shape, check:spec-parsed-alias, check:changeset-gate-self-tests, check:objectui-changeset, check:engine-double-contract, check:where-matcher, check:query-options-erasure, lint check:doc-formula-expressions, lint check:doc-security-posture, and 12 node scripts/check-*.mjs families all exit=0. SIX gates first reported exit 1 as PREREQUISITE NOT MET on an unbuilt workspace, each printing its own 'Nothing was measured ... it is NOT a finding'; they were re-run as real measurements after building spec, formula, lint, client-react and client, and all five re-runnable ones went green (spec check:docs, check:generated, check:skill-examples, lint check:doc-formula-expressions, lint check:doc-security-posture). Also ran a control-byte self-scan (grep -naP over all four changed files, clean). REVERSE-VERIFICATION, expected direction RED, observed RED: fix committed FIRST so restore had a recovery point, then the pre-fix .describe() text was restored on disk with the new assertions in place. Mutation proved on disk before measuring, not by the editor's exit code -- anchor hits before=1, after=0, old-text hits after=1, hash moved b8e2344f -> 5211ac23, printing 'MUTATION CONFIRMED ON DISK'. Result: vitest exit 1, 'Tests 1 failed | 27 passed (28)', failing on exactly the intended pin ('AssertionError: expected ... to match /\\(name, locale\\)/ at io-node-config.test.ts:321'). Only one test fails because only the describe was mutated, not the refusal messages. RESTORE leg proved, not assumed: `git checkout HEAD -- <absolute path>` (never bare `--`, which reads the index), then hash back to the HEAD blob b8e2344f AND `git diff HEAD` empty -- script carried a trap on EXIT/INT/TERM with absolute paths, but the hash comparison is the proof, not the trap. NO REBUILD LEG APPLIES: the test imports './io-node-config.zod.js' sibling-relative from src, so nothing resolves through exports/dist -- the ablation-dist-preflight condition is not met here.", "open_questions": [], "out_of_scope_findings": [ "filed as #12446: the 'per recipient locale' promise survives in 4 sites outside packages/spec -- notify-node.ts:197 (the Studio form field description, highest stakes), content/docs/automation/email-templates.mdx:164-166 ('one node mails each person in their own language'), messaging-service-plugin.ts:250 (the log line the card named, at a different path than stated), and notify-node.ts:288 (comment). Unassigned, unlabeled for PM triage, dedup-searched first, back-linked to #12178 and PR #12447." ], "deviations": [ "Dedup search used MCP search_issues instead of the REST list endpoints the standing rule prefers: REST is 403 from this dev seat (probed: `GET /repos/objectstack-ai/objectstack/issues` -> HTTP 403), exactly as the dispatch stated. GraphQL pool spend: 2 search calls.", "Declared narrowing -- check-dev-prereqs is NOT in the green union. It reports '67 of 67 workspace packages declare an entry point under dist/ that is not on disk', i.e. a statement about an unbuilt checkout requiring a full `pnpm build`; that is a repo-scale run CI owns and its output says nothing about this diff. Same for check:type-check-debt --re-measure, which refuses outright without the built workspace closure. The cheap half is covered: spec typecheck (including check:test-typecheck over the edited test file) is green.", "The card named 2 'recipient locale' sites; the re-grep found 5 and all 5 were corrected, per the dispatch's instruction to sweep the whole file rather than trust either list.", "The out-of-scope path in the card body and triage comment (service-messaging/src/plugin.ts) does not exist; the real file is messaging-service-plugin.ts. Recorded in #12446 so the next reader is not sent to a missing path." ], "files_changed": [ "packages/spec/src/automation/io-node-config.zod.ts (5 wording sites)", "packages/spec/src/automation/io-node-config.test.ts (the :277/:303 assertions moved onto the new text and tightened)", "content/docs/references/automation/io-node-config.mdx (regenerated via gen:docs, 1 insertion / 1 deletion)", ".changeset/notify-template-locale-is-deployment-default.md (@objectstack/spec: patch, per the describe-only precedent already in the tree)" ] }Generated by Claude Code
Generated by Claude Code
huangyiirene commentedon Aug 26, 2026
Contract review (clause-② enqueue gate: path limb
packages/spec/src/**hit; declaration limbno, confirmed from the diff — same accepts/refusals, only the prose an author reads changes) — PASS, and ACCEPT — reviewer of record: spec seat R7,session_01E5LFCYBJ3q2s6yW6oMLxwy, atCONTRACT_REVIEW_TIER.Verified against the PR #12447 diff, read in full (4 files, +123/−16):
payload.localeif set (interpolated once, before fan-out) elseII18nService.getDefaultLocale(); one value per notification, not per recipient; the 2026-08-13 deferral dated in place so the limitation carries provenance.(name, locale),deployment default,not per recipient) and REFUSE a barerecipient locale— a future edit cannot quietly restore the promise. Reverse-verification ran with mutation-proven-on-disk and a proven restore leg.io-node-config.mdxmoved bygen:docsonly; changesetpatchper the describe-only precedent; no delivery behaviour touched.packages/spec— one of them a Studio form description, one a doc that says "each person in their own language" #12446 verified filed (4 remaining sites outside spec, including the Studio form-field description — highest stakes — and the messaging-service-plugin.ts log line at its CORRECTED path; the card's stated path did not exist, which is itself recorded so the next reader is not sent to a missing file). Unassigned/unlabeled for triage, as required.Landing: ready-flip + enqueue when EVERY check on PR #12447 is green. Card stays
pm:dispatcheduntil MERGED.Generated by Claude Code