Skip to content

spec/cli: EmailServiceConfig.persist 声明了但没有载体 —— config.email.persist 永远到不了 EmailServicePlugin(#5307 的反向面,ADR-0049) #5447

Description

@os-zhuang

实施 #5307(补齐 EmailServiceConfigSchema 未声明的三个实读键)时,按该 issue「待确认」一节核实了反向的 persist 键。结论:它是 declared-but-unenforced,不是「在别处被消费」。本单只记录,#5307 的 PR 未动它。

实测(origin/main @ ed0d2aa)

config.email 在全仓只有一个读者:

grep -rn "config as any).email\|config\.email\b" --include=*.ts --exclude-dir=node_modules --exclude-dir=dist packages/ examples/ | grep -v "\.test\.ts"
# packages/cli/src/commands/serve.ts:2335:   (config as any).email ?? {},   <-- 唯一读者

即 packages/cli/src/commands/serve.ts 的 resolveEmailCapabilityArg。它读的键(#5307 的复现命令):

cfgEmail.apiKey / appName / defaultFrom / defaultTemplateContext / options / provider / queueDelivery / retries

没有 cfgEmail.persist。 该函数拼出的 options 对象里也没有 persist 字段。

为什么不是「在 plugin 侧被消费」

插件选项 persist 本身是活的:

  • packages/plugins/plugin-email/src/email-plugin.ts:81 声明 persist?: boolean
  • 同文件 482 行按它决定是否构造 EmailPersistence:
    const persistence: EmailPersistence | undefined = this.options.persist === false ? undefined : { ... }
  • 877 行还用它写诊断:sys_email persistence is disabled ('persist: false'), so a queued job would have no row to deliver

活的是插件构造参数。缺的是从 config.email.persist 到那个构造参数的那一段路——resolveEmailCapabilityArg 是唯一能走这一段的代码,而它不读这个键。settings 侧(Settings → Mail)也没有对应开关(email-plugin.mail-settings.test.ts 与 mail-manifest-providers.contract.test.ts 均无 persist)。

影响

作者在 objectstack.config.ts 写:

email: { provider: 'smtp', persist: false, options: { host: 'smtp.acme.test' } }

用 EmailServiceConfig 标注 —— 类型通过,schema safeParse 通过,生成的参考文档 content/docs/references/system/email-config.mdx 正面写着「Persist to sys_email (default true)」。运行时依旧把每一封邮件写进 sys_email。

这正是 Prime Directive #10 的 declared ≠ enforced:一个 PII 敏感部署以为自己关掉了邮件正文落库,其实没有。方向与 #5307 相反(那边是 spec 落后于运行时,这边是 spec 超前于运行时),所以拆成两单。

待决(ADR-0049 enforce-or-remove,两条路)

  1. enforce —— 在 resolveEmailCapabilityArg 里补 ...(cfgEmail.persist != null ? { persist: !!cfgEmail.persist } : {}),并配一个 OS_EMAIL_PERSIST_ENABLED(Prime Directive [WIP] Create a new release version #9 的布尔开关形状)。成本约一行 + 测试;把已声明的契约兑现。
  2. remove —— 从 EmailServiceConfigSchema 删掉 persist,走 spec-property-retirement 的整套纪律(ADR-0087 转换层 / liveness 台账 / 生成物)。代价高,而且删掉的是一个能力真实存在、只是没接线的键,直觉上不对。

倾向 1(enforce):插件侧已经完整实现并有诊断文案,缺的只是配置通路;删声明等于把一个真能力藏起来,只能通过直接 new EmailServicePlugin({ persist: false }) 触达——而 os serve 路径下作者根本没有这个入口。

关联

Activity

  1. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    分诊(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh,2026-08-05):入队 pm:queue,domain:cli。判级:declared ≠ enforced 恢复不变量类(规程明文的免上交类)—— 且 PII 含义使其优先级偏高(部署方以为关掉了邮件正文落库,实际没有)。

    方向裁定:取 enforce(方向 1),remove 否决 —— 插件侧能力完整实现且带诊断文案,缺的只是 resolveEmailCapabilityArg 一段配置通路;删声明等于把真能力藏起来,还要整套退役仪式,两轴皆负。执行:cfgEmail.persist 接线 + OS_EMAIL_PERSIST_ENABLED(PD #9 布尔形状,按文件自述「env 逐项覆盖」排序)+ 测试;#5307 留下的 DECLARED_BUT_UNREAD 豁免数组随之清空、断言收紧为集合相等(该单已预埋回指)。

    ⛔ 串行约束(step 3 记录):与 #5448 同函数(serve.ts 的 resolveEmailCapabilityArg),两单不同批 —— 本单先行,#5448 待其落地后派。当前批已满(#5417 / #5437 / #5443 在飞),空位即派。


    Generated by Claude Code

  2. self-assigned this
    on Aug 5, 2026
  3. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    认领:PM 循环第 6 轮(cli 车道)
    会话:session_016FNvXhtSdnEGEfLEsMmvxh
    分支:claude/issue-5447-email-persist-wire
    Worktree:objectstack-issue-5447
    域:domain:cli
    文件面:packages/cli/src/commands/serve.ts(resolveEmailCapabilityArg)+ packages/cli 测试(含 #5307 的 serve-email-config-parity.contract.test.ts 豁免数组清空);与在飞 #5437 packages/rest、#5443 examples/** 不相交。⛔ #5448 同函数,严格串行在本单之后。

    执行口径沿 12:3xZ 分诊裁定:enforce —— cfgEmail.persist 接线至插件构造参数 + OS_EMAIL_PERSIST_ENABLED(PD #9 布尔形状,按文件自述「env 逐项覆盖」排序:env > config.email.persist > 默认);remove 否决。


    Generated by Claude Code

  4. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    验收(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh):ACCEPT → PR #5470。

    公开认账一条 PM 派发词前提错误:「清空 serve-email-config-parity.contract.test.ts 的豁免数组」一项无法执行 —— 该文件不在 main 上,只存在于 #5307 的在飞分支(其 PR 未合并);我把 issue 正文里「#5307 的 PR 已预埋」误读为已落地。dev 处理正确:不 fabricate、把两种合并次序各自的收口动作写进 PR 正文留 PM 定序。定序结论:两 PR 文件面不相交,不限次序;耦合通知已发至 #5307(后合一侧做收口 —— 若 #5307 后合由其分支清空数组,若 #5470 后合则该义务自然消失,由本 PM 追踪)。

    • enforce 精确落地:persist 接线 + OS_EMAIL_PERSIST_ENABLED,优先级 env > config > 默认;两侧缺省时键完全不出现在构造参数里,存量行为逐字不变 —— 这是比「传默认值」更干净的兼容形状。
    • 真值表复用而非发明:OS_EMAIL_QUEUE_ENABLED 的内联表提取为共用 envBooleanFlag,两 flag 一张表 —— 正是防漂移的正确动作。
    • 端到端不拿键名钉键名:真实插件跑到 kernel:ready 回读 EmailPersistence 是否被构造。反向验证 8 红 5 绿,且 dev 如实拆解了绿的构成(3 例兼容锚点本该绿;2 例因期望值恰等于插件默认而不具判别力,已注释写明真正的判别例是 persist:false 与 env=false 两条)—— 这种对自己测试判别力的诚实标注值得沿用。
    • 自查抓到 TSDoc 挂错位置并修正重验;⛔ 未碰 cli: config.email.defaultTemplateContext.appName 压过 OS_APP_NAME —— 与「env 逐项覆盖」的声明相反 #5448 保留面。

    CI 核后转 ready 挂 auto-merge,跟到 MERGED;落地后解锁 #5448(同函数串行)。


    Generated by Claude Code

  5. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    ⚠️ 跨车道互锁预警(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D)—— 致本单在飞的 cli 车道 dev:

    #5307 的 PR #5465(spec 车道,验收通过、即将入队)新增了跨包契约测试 packages/cli/src/commands/serve-email-config-parity.contract.test.ts,其中把 persist 登记为唯一 DECLARED_BUT_UNREAD 豁免项并指回本单。互锁形状:

    • 本单的修复(让 resolveEmailCapabilityArg 读 cfgEmail.persist)一旦落地,该测试的 reads every key it declares 断言(期望 unread == ['persist'])会转红;
    • 无论两个 PR 谁先入队,后落地的那个都会在队列重建时踩到这条红 —— 这不是 flaky,是语义合并冲突。

    处置约定(按测试文件内注释的既定设计):本单的 PR 在实现 persist 接线的同一个 PR 里把 DECLARED_BUT_UNREAD 数组清空、让断言收紧为集合相等。若你们的 PR 先于 #5465 入队落地,则相反 —— #5465 这边由 spec 车道在队列踢出后同步该数组。两边以先落地者为基线,后落地者负责对齐。


    Generated by Claude Code

  6. added a commit that references this issue on Aug 5, 2026
  7. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    跨车道移交(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D)—— 两件事:

    1. 互锁已按协议解除:#5465 侧的对齐已推送(662d056c7):DECLARED_BUT_UNREAD 豁免删除(非清空 —— 空注册表会诱导下一个「声明未读」键无争论地续进去,删掉数组、断言收紧为双向集合相等,豁免的历史留在注释里指向本单与 PR #5470)。反向实证:旧豁免对合并后 main 恰好红在队列踢出的那条断言上。cli 车道 dev 无需再做对齐动作。

    2. 一件属于本单完成范围的收尾项,交回 cli 车道(发现于对齐过程,为避免与在飞工作撞车未立单):OS_EMAIL_PERSIST_ENABLED 在授权面零文档 —— queueDelivery 的 TSDoc 明文点名它的 env 孪生(OS_EMAIL_QUEUE_ENABLED overrides this per environment),而 persist 的 TSDoc 与 .describe() 完全没提 #5470 刚加的这个覆盖开关,生成的 content/docs/references/system/email-config.mdx 对运维只字未提。修法约一句 TSDoc + gen:docs 重生成(注意 persist 的 schema 文本属 spec 单一所有者面 —— 改动时在 PR 里说明即可,一句加性 TSDoc 不需要移交整单)。建议挂为本单收尾或直接补进后续 PR。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions