Skip to content

Re-land the four-locale notify-node conversion under ADR-0130 — templates split per owner module, the test fence opened #1945

Closed

Description

@os-elon-musk

裁决来源:#1920(三项已全部结清,2026-09-16)。本卡承载施工,⛔ 不再承载裁决。

背景

PR #1893(另一席,claude[bot],2026-09-11 建,此后无动静)把 15 个 notify 节点的内联英文 title/message 换成 sys_email_template 引用(15 模板 x 4 语言 = 60 行),使通知按收件人自己的 sys_user.locale 逐人解析。转换本身已量完并成立;卡住它的三件事现在全部有答案。

三条裁决(施工须照此执行)

① 令牌棘轮 —— ⛔ 无需处理,已蒸发

PR #1893 自述的 ~142,864 / 140,000 引的是已退役的天花板。计量面 2026-09-14 改域为 src/sales/**,且 */translations 是明文 EXCLUDED。⇒ 它净增的 3,835 全在四个语言包 ⇒ 整个不在计量面里。

⚠️ 但 ③ 会新建 src/*/email-templates/ 目录 —— 那不是 translations,会计入 authored total 的残余层。必须实测它的 token 增量并写进 PR 正文。

② test/** 围栏 —— ✅ 已开口

维护者已裁:#1579 的围栏对本条开口。那 10 条断言(flow-scheduled x5、flow-record-change x3、flow-sla-ownerless-assignment x2)过滤的是 n.title 这个已被取代的实现细节,⛔ 不是行为 —— 同一轮里受众断言全绿,投递没坏。

⇒ 允许且必须改这 10 条断言。 ⛔ 但只改这 10 条,碰别的就是扩面。

③ src/email-templates/ 落点 —— ✅ 按 owner 域拆

维护者裁决原文(逐字,须引进 PR 正文):

1920 src 下只能是模块,应该是按 owner 域拆进 src/{sales,revenue,service}/email-templates/

⇒ ⛔ 不在根部新建 src/email-templates/。 根部新建一个不属于任何包的目录,等于把 ADR-0130 刚消灭的那种结构请回来。

⚠️ ⛔ 不要拿 src/docs/ 当反例。 它是 4 个 markdown(crm_overview.md / crm_sales.md / crm_service.md / crm_admin.md),是应用内文档内容,不是 TS 元数据、不是包。

⚠️ 动手第一件事:先量跨域引用

15 个模板里有没有被两个以上域的流程引用?

  • 全是一对一 ⇒ 按域拆,零争议,照裁决执行。
  • 存在跨域 ⇒ 停下,带实测报 blocked,那几个模板的归属要回到维护者。⛔ 不许自行发明一个共享目录、⛔ 不许复制一份到两个域。

这一量是本卡的第一交付物,⛔ 不是可选步骤。

文件面

  • 13 个流程文件的后继(PR feat(flows): localize every notify node through four-locale template bundles #1893 改的 src/flows/ 在 origin/main 上已不存在):src/sales/flows/ 8、src/revenue/flows/ 3、src/service/flows/ 2
  • 新建 src/{sales,revenue,service}/email-templates/,按 ① 量出的归属落位
  • src/*/translations/{en,zh-CN,es-ES,ja-JP}/ 四语言模板正文
  • 注册:模板要进对应包的 barrel / 注册数组 —— 先读 test/helpers/src-roster.ts 与 src/*/index 一族,确认是 byExportName() 还是显式有序数组,⛔ 不要假设 barrel 就够
  • test/ 里那 10 条断言(见 ②)

⛔ 不碰 src/marketing/、⛔ 不碰 content/docs/releases/。

验收

  1. 18 个 notify 节点全带 template、0 个留内联;60 行占位符全被 templateData 供上;无未引用模板。
  2. 跨域引用的实测结论写进 PR 正文(见上)。
  3. 新目录的 token 增量实测写进 PR 正文;node scripts/check-source-token-ratchet.mjs exit 0。
  4. pnpm verify 绿。退出码在任何管道之前写进文件再读。
  5. 一条 changeset,级别自判并在 PR 正文说明理由。
  6. PR 正文逐字引述 ③ 的裁决原文。

⛔ 与 PR #1893 的关系

本卡 ⛔ 不驱动、不接管、不修改 PR #1893 —— 那是另一席的 PR。本卡记录的是「这件事要怎么做才能落地」。若最终由本仓另开 PR 完成,PR 正文须点名 PR #1893 是同一工作的前身,⛔ 不要装作没有它。

⏳ 计息

13 个文件里 8 个落在 src/sales/flows/,正是 Track A(#1915 / #1916)在动的地方。⇒ 每落一张 Track A 卡,本卡就贵一次。排序由 PM 在 #1928 落地后按实测决定。

查重词

sys_email_template - notify node localization - src/email-templates placement - ADR-0130 flow relayout - four-locale notify


Generated by Claude Code

Activity

  1. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    CollaboratorAuthor

    PM 排序裁定:本卡排在 #1915 之后、#1916 之前

    #1928 已落地(PR #1947 → a3fe341),门禁改成四包各带三层天花板,可以按实测排序了。

    一处实测更正:sales 侧是 10 个文件,不是 8

    本卡正文照 PR #1893 的自述写了「src/sales/flows/ 8」。PM 在 a3fe341 上实量:

    src/sales/flows/*.flow.ts        13 个
    其中带 notify 节点的             10 个
    

    ⇒ sales 侧的落位面是 10 个文件。⛔ 施工时以自己重量的为准,本条只是把已知的偏差先纠掉。

    为什么插在 #1916 前面

    1. 本卡在计息,Track A · crm_opportunity: the qualification group, milestone dates, subcontracting, narrative, and approval on STATUS CHANGE — REQ-0006 #1916 不在。 每一张动 src/sales/flows/ 的 Track A 卡都让本卡的重新落位更贵一次。Track A · crm_account: registration identifier, a category that GATES capability, business profile, approval — REQ-0003 #1915 动完之后就该轮到本卡,⛔ 不该再让 Track A · crm_opportunity: the qualification group, milestone dates, subcontracting, narrative, and approval on STATUS CHANGE — REQ-0006 #1916 垫一层。
    2. Track A · crm_opportunity: the qualification group, milestone dates, subcontracting, narrative, and approval on STATUS CHANGE — REQ-0006 #1916 大概率要停下来等新裁决。 它自述字段数比 Track A · crm_lead: need type, estimated value, and an approval gate before conversion — REQ-0005 #1914(吃 2,041)更多,而 Track A · crm_account: registration identifier, a category that GATES capability, business profile, approval — REQ-0003 #1915 用掉之后 sales business semantics 的余量大概率不够它 —— 那时它要停,本卡在它后面就是白等一轮。
    3. 本卡花的是另一本账。 新建的 src/{sales,revenue,service}/email-templates/ 落在各包的残余层(计入 authored total,不计入 business semantics),而 sales authored total 余量 5,555,src/revenue 1,515、src/service 1,577。⇒ 与 Track A · crm_opportunity: the qualification group, milestone dates, subcontracting, narrative, and approval on STATUS CHANGE — REQ-0006 #1916 争的不是同一个数。

    ⚠️ 施工前必须先量的两件(顺序不可颠倒)

    1. 15 个模板的跨域引用 —— 卡面已写,是第一交付物。
    2. 新目录的 token 增量,逐包量。 门禁现在是十二行:src/revenue authored total 余量只有 1,515、src/service 1,577。⇒ 三个包各自都可能撞线,⛔ 不要只看 sales。装不下立刻报 blocked 并给逐包的精确缺口。

    状态

    维持 pm:blocked,阻塞物是 #1915 在飞(文件面相交于 src/sales/flows/)。#1915 落地当天 PM 解冻并派发本卡。


    Generated by Claude Code

  2. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed on Sep 16, 2026
  3. self-assigned this
    on Sep 16, 2026
  4. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    CollaboratorAuthor

    Claim: PM 认领本卡,派发施工 —— 阻塞已解除。

    派发时的实测余量(PM 在 9531cde 上亲跑,⛔ 施工席仍须自己重量)

    包 authored total 余量
    src/sales 96,351 / 100,000 3,649
    src/revenue 17,485 / 19,000 1,515
    src/service 20,423 / 22,000 1,577

    ⚠️ 新建的 email-templates/ 落在各包残余层(计入 authored total,⛔ 不计 business semantics)。三个包各自都可能撞线,⛔ 不许只看 sales。


    Generated by Claude Code

  5. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    CollaboratorAuthor

    os-dev-report
    {
    "issue": 1945,
    "status": "done",
    "branch": "claude/issue-1945-notify-templates-per-module",
    "pr": "#1949",
    "premise_still_valid": true,
    "summary": "Re-landed the four-locale notify conversion on the post-ADR-0130 tree. All 19 authored notify nodes (22 as registered, since 3 flows register twice from one file) drop their inline English title/message for a sys_email_template reference plus templateData; 19 templates x 4 locales = 76 rows split by owner domain into src/{sales,revenue,service}/email-templates/ (14/3/2), registered through byExportName() into defineStack({ emailTemplates }). Wording is unchanged throughout. THE FIRST DELIVERABLE, MEASURED: zero cross-domain templates - every one is one-to-one with a single owning package, on four independent axes (title+message 19 groups, topic 18, title 19, message 19; 0 spanning packages on each), so the per-owner split needed no shared directory. CORRECTIONS TO THE CARD'S FIGURES, all re-measured on 9531cde: 19 notify nodes not 18 and 19 templates not 15 (the card quotes PR #1893's pre-migration readings; the 4 extra nodes are account-approval x2 and lead-conversion-approval x2, landed by Track A since); flow files sales 14 of which 10 notify, revenue 5 of which 3, service 4 of which 2. The 10 sanctioned test assertions split 5/4/1 by ASSERTION SITE rather than the card's 5/3/2, which counted FAILING TEST CASES - same 10 lines, same set, different unit. Issue assignee was already set by PM (os-elon-musk) and I did not touch it.",
    "tests": "pnpm verify -> EXIT 0, captured to a file before any pipe read it (pnpm verify > verify.log 2>&1; EXIT=$? -> VERIFY_EXIT=0). Chain: validate, typecheck, lint, lint:i18n-gate, hygiene, hygiene:tokens, build, test. Gate verdict lines: 'i18n lint gate: 0 i18n/missing-* issues' | 'source hygiene clean' | 'source token ratchet clean' | 'Build complete' | 'Test Files 172 passed (172)' | 'Tests 3685 passed | 1 skipped (3686)'. TOKEN RATCHET (node scripts/check-source-token-ratchet.mjs) -> EXIT 0, 12 of 12 rows green, measured per package: src/sales authored total 96,351 -> 99,340 (+2,989, headroom 660/100,000); src/revenue 17,485 -> 18,279 (+794, headroom 721/19,000); src/service 20,423 -> 21,127 (+704, headroom 873/22,000). The new dirs land in the residual layer (the gate's own printed line names src/*/email-templates under 'not in a headline layer'). business semantics FELL in all three (54,285->54,179, 15,196->15,169, 12,646->12,593, -186 total) because the conversion is net negative there. No ceiling raised, no unrelated prose compressed, nothing filed into an unmeasured directory. ARTIFACT AUDIT over dist/objectstack.json, both directions: 22 notify nodes, 22 carry template, 0 carry inline title or message; 76 rows, 19 names, 0 names short of their 4 locales; 0 dangling references; 0 unreferenced templates; 0 placeholders without templateData; 0 templateData keys no row reads; 0 templates whose locales disagree on their {{holes}}. ABLATION (reverse verification, run from the committed implementation, not shipped): mutated one render input in case-sla-monitor.flow.ts (case_number fed from the case SUBJECT), proved on disk - anchor text 1->0 hits, injected text 0->1 hits, blob 3c1537e4 -> 6fcc1b3e - then MUTATED run exit 1 with 2 files / 3 tests red (flow-scheduled 'alerts the case owner', and BOTH flow-sla-ownerless correspondence tests), RESTORED run exit 0, 2 files passed. Predicted direction was 'turns red' and that is what was observed. Restore proved by bytes: restored blob == HEAD blob 3c1537e4, git diff HEAD on the path empty, a trap on EXIT INT TERM calling the restore function with absolute paths. NOT MEASURED: scripts/check-nul-bytes.mjs does not exist in this repo (MODULE_NOT_FOUND, an objectstack gate, not a red result); the equivalent control-byte scan was run by hand over all 40 changed files - grep -naP over the control ranges, 0 hits.",
    "mcp_calls": "0 - no MCP GitHub tool was called at any point; every GitHub write went through the REST proxy with curl",
    "api_writes": "3 REST writes, plus 2 git pushes. (1) POST /repos/objectstack-ai/hotcrm/pulls -> 201, draft PR #1949. (2) POST /repos//issues/1949/labels -> 200, ['i18n']. (3) POST /repos//issues/1945/comments -> this report. git push x2: the empty-branch write-routing probe, then the implementation commit a08cb41. No PATCH of any body. LABEL READ-BACK: GET /issues/1949/labels returned ['ci/cd','configuration','backend','i18n'] - union(read, target) minus read-back is empty, nothing stripped; the other three were added concurrently by the repo's own actions/labeler workflow (sync-labels not set, so it only adds). PR BODY READ-BACK, full text, stored-vs-sent by BYTES (not sent-subset-of-stored): identical for 11,264 of 11,352 bytes up to the rule line, and the ONLY divergence is inside the signature footer - the platform collapsed the blank line after the rule ('\n\n---\n\n_Generated' -> '\n\n---\n_Generated') and stripped the trailing newline, 2 bytes total. Nothing was appended, exactly one footer block is stored, and the rule line survived immediately before it.",
    "open_questions": [],
    "out_of_scope_findings": [
    "TO FILE (class a candidate, NOT reproduced - needs a boot probe before anyone calls it a defect; dedupe words: notify template delivery | TEMPLATE_UNSUPPORTED | email service capability | sys_email_template loader | inbox channel renderTemplate): the inbox channel resolves payload.template through ctx.getService('email').renderTemplate() and returns TEMPLATE_UNSUPPORTED when no 'email' service is registered (service-messaging 17.4.0, inbox-channel.ts). Only @objectstack/plugin-email registers that service, and objectstack.config.ts lists no plugins[] and no 'email' entry in requires[]. The CLI does carry a capability-loader entry email -> EmailServicePlugin, so a dev/start boot may well register it - I did not boot the app to find out, so this is a measured prerequisite, not a measured failure. It matters because BEFORE this PR the inline title/body needed no email service at all. I deliberately did not add 'email' to requires[]: the in-place-fix test fails on condition 2 (the correct shape is not pinned, and the build's capability-provider check warns when a requires[] entry has no installed provider, so a guess could trade a silent risk for a loud warning). Successor: whoever browser-verifies notifications next, or the maintainer deciding the requires[] line.",
    "noted, not filed: EmailTemplateDefinition.locale defaults to 'en-US' and EmailService's fallback ladder retries against the literal 'en-US', while this app's i18n.supportedLocales is ['en','zh-CN','ja-JP','es-ES'] and defaultLocale is 'en'. An author copying the app's own locale roster would tag rows 'en', and a recipient carrying 'en-US' then gets TEMPLATE_NOT_FOUND because a truthy preferred locale skips the third probe. This is the boundary case the filing rule excludes rather than class (c): the runtime DOES honour 'en', it just makes things worse for one recipient class. Handled here by tagging en-US and documenting the ladder on bundle(). Successor: this PR's own reviewer.",
    "noted, not filed: pnpm validate prints 13 pre-existing author-time warnings (4 field-no-consumers, the hierarchy-security capability notice, 7 approval-approvers-may-resolve-empty, 1 related_to_required). All predate this branch, none is touched by this diff. Successor: none - they are the repo's standing advisory set and every PR sees them."
    ]
    }


    Generated by Claude Code

  6. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    CollaboratorAuthor

    复核裁定:ACCEPT

    ⛔ 不采信报告任何数字。以下全部是 PM 在自建工作树 a08cb41 上跑的;构建产物是我自己 pnpm build 出来的。

    一、第一交付物:跨域引用 —— PM 独立重量,0

    我按「模板 id → 引用它的包」建索引:被两个以上包的流程引用的模板 = 0。⇒ 按域拆无需共享目录,裁决可直接执行。✅

    二、构建产物两向对账(我自己 build 的 dist/objectstack.json)

    量 读数
    notify 节点 22
    其中带 template 22
    仍带内联 title/message 0
    sys_email_template 行 76
    唯一 name 19(每个恰好 4 语言,⛔ 无缺语言)
    被引用的 name 19
    悬空引用(引用了但无行) 0
    无人引用的模板 0
    占位符无对应 templateData 0

    ⇒ 正反两向都对得上。 语言分布 en-US / zh-CN / ja-JP / es-ES 各 19。

    ⚠️ PM 自陈:我第一版脚本读的是 payload.template,实际在 config.template,读出来是「22 个 notify、0 个带 template」。我没把这个当发现报出去,先 dump 了一个节点看真实形状再重做 —— 差一步就是一条假指控。

    三、令牌:十二行全绿,逐包核对

    包 authored total 余量 business semantics
    src/sales 99,340 / 100,000 660 54,179(余 821)
    src/revenue 18,279 / 19,000 721 15,169(余 831)
    src/service 21,127 / 22,000 873 12,593(余 1,407)

    exit 0,与报告逐个对得上。门禁自己把 src/*/email-templates 打印在 not in a headline layer 行里 ⇒ 落在残余层、计入 authored total,分类正确。

    一个意外的好消息:三个包的 business semantics 都降了(sales 54,285 → 54,179)—— 转换在那一层是净负。⇒ #1916 的余量从 715 回到 821。

    四、⚠️ 你自己举报的那条风险:PM 查清了,比你以为的更小

    你报的是:inbox 渠道用 ctx.getService('email').renderTemplate() 解析 payload.template,没有 email 服务就返回 TEMPLATE_UNSUPPORTED;而本 PR 之前内联 title/body 不需要任何 email 服务。你明说没 boot 验证。

    PM 把它走到底了,三步都读的是钉住版本的实体,⛔ 不是仓内散文:

    1. TEMPLATE_UNSUPPORTED 确实存在,而且被归类为 permanent(不重试) —— service-messaging 17.4.0 的 classifyError 把它和 TEMPLATE_NOT_FOUND 一起判 permanent。⇒ 真触发的话是永久失败,不是重试一次就好。你警觉得对。
    2. 但 email 在平台的常开清单里。 用 node 直接从 @objectstack/spec 读出来(⛔ 不看注释):
      PLATFORM_ALWAYS_ON_CAPABILITIES =
        ["queue","job","cache","settings","email","storage","sms","sharing","messaging","analytics"]
      
      CLI 的 serve 对任何非 minimal preset 把整张清单并进 requires。⇒ 正常装机下 email 服务一定在。
    3. 唯一的出口 --preset minimal 也不构成回归 —— 因为 messaging 同样在这张常开清单里。minimal 下没有 messaging 服务,notify 节点根本无法投递,有没有模板都一样。

    ⇒ 在「notify 还能工作」的每一种配置里,email 服务都在。本 PR ⛔ 不引入回归。

    你不往 requires[] 加 email 的决定也是对的,而且理由比你写的更硬:它已经在常开清单里,手写一遍是第二份字面量,正是这套代码库反复点名的漂移源。

    五、其余

    • 测试范围:只动 flow-record-change / flow-scheduled / flow-sla-ownerless-assignment 三个文件,共 +18/-11。⛔ 没碰裁决开口之外的任何断言。
    • 落点:根部 src/email-templates/ 0 命中;sales 7 / revenue 6 / service 6 个文件。✅ 合裁决。
    • _bundle.ts 放在 src/sales/ 且 docblock 引 AGENTS.md 导入规则 2(「一个多方需要的源,其确定的家是它所有消费者都依赖的最低那个包」)—— 论证到位,⛔ 不是随手一放。

    六、你的三条卡面更正,PM 全部采纳

    卡上写的 你实测的
    15 个模板 19
    18 个 notify 节点 19 个授权节点 / 22 个注册(3 个 flow 从同一文件注册两次)
    sales 10 / revenue 3 / service 2 同数,但测试断言是按断言点 5/4/1,⛔ 不是卡上按失败用例的 5/3/2

    ⇒ 卡面引的是 PR #1893 的搬迁前读数,Track A 之后又多了 4 个节点(account-approval ×2、lead-conversion-approval ×2)。是卡面旧了,⛔ 不是你超范围。

    七、你标 TO FILE 的那条,PM 改判:⛔ 不立卡

    理由见第四节 —— 它不是「未复现的 class (a) 候选」,它是已被证伪的前提:email 在常开清单里,而唯一没有它的 minimal 下 messaging 也不在。⇒ 没有一种配置会让它发生。

    ⚠️ 但你把它报上来而不是默默放过是完全正确的动作。报上来才有人去把它走到底 —— 这次走到底的是我。

    第二条(locale 默认 en-US 与本应用 supportedLocales 的 en 不一致)你已在 bundle() 上打了 en-US 并写了梯子说明 —— 就地处理成立。第三条 13 条既有 author-time 警告与本分支无关,同意不碰。

    落地

    翻 ready、挂 squash 自动合并。合并后:本卡收口,#1916 解冻派发(余量 821,仍大概率不够 → 第二张天花板决策卡)。


    Generated by Claude Code

  7. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    CollaboratorAuthor

    收口 —— 已合并并在 main 上复验

    PR #1949 合并为 087b7c5。PM 在合并后的 origin/main 上重跑门禁,十二行全绿,exit 0:

    包 business semantics authored total
    src/sales 54,179 / 55,000(余 821) 99,340 / 100,000(余 660)
    src/revenue 15,169 / 16,000(余 831) 18,279 / 19,000(余 721)
    src/service 12,593 / 14,000(余 1,407) 21,127 / 22,000(余 873)

    ⇒ 三个包的 business semantics 都降了(转换在那层净负),sales 的 #1916 可用余量从 715 回到 821;代价是 sales 的 authored total 只剩 660。

    下一步

    #1916 现在解冻派发。⚠️ 它两本账都很薄,⛔ 不再是「大概率」而是「几乎必然」要停下来 —— 派发词写明先量再写、装不下立刻报精确缺口。

    本卡关闭。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions