feat(spec): EmailServiceConfigSchema 补齐 CLI 实读的 queueDelivery / appName / defaultTemplateContext (#5307) - #5465
Conversation
…me / defaultTemplateContext (#5307) config.email 在全仓只有一个读者:packages/cli/src/commands/serve.ts 的 resolveEmailCapabilityArg。它读八个键,schema 只声明五个,差集三个已经被 运行时消费多时 —— queueDelivery(#5160 的耐久队列开关)、appName(模板 产品名 + 兜底发件人来源)、defaultTemplateContext(自由渲染上下文)。 与 #5104 完全同型的 declared != implemented,spec 在落后的一侧:用 EmailServiceConfig 标注 objectstack.config.ts 的作者写 queueDelivery: true 会拿到类型错误,而同一份配置起得来、也确实走队列。 - 三个键均为 optional 且不带 .default():默认值由 resolveEmailCapabilityArg 对着 env 与顶层 config 解析,schema 再造一个只会多出一个谁也不赢的答案 - defaultTemplateContext 保持自由 record,读侧原样透传,不发明约束 - TSDoc 写清语义、默认值与优先级,含 defaultTemplateContext.appName 压过 OS_APP_NAME 这一处实测到的例外(另立 #5448) - 新增跨包契约测试把 issue 的手工 grep 机械化:读侧新增未声明键即变红, persist 作为唯一 DECLARED_BUT_UNREAD 豁免登记并指回 #5447 运行时零改动。
- authorable-surface.json 记入三个新可授权键(gen:schema;check:authorable-surface
是删除棘轮,新增键不记入即"对该棘轮永久隐形")
- content/docs/references/system/email-config.mdx 重新生成,属性表出现三行
- appName 的 .describe() 不再写 {{appName}}:生成器会把双花括号转义成
`{{x}` 加一个游离的 },main 上已有 3 处同样的破损(已另立 #5452),
源码留注释说明为何这里绕开
check:generated 九个 gate 全绿;gen:strictness-ledger 整体重算零 diff
(台账只分诊 ui/data/automation/security/studio,system/ 不在其内)。
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31010154371 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
分诊(spec 车道 PM,
Generated by Claude Code |
`serve-email-config-parity.contract.test.ts` 曾注册唯一一条 `DECLARED_BUT_UNREAD` 豁免:`persist` 被 schema 声明却无人读取(#5447)。cli 车道的 PR #5470 (`cd2efe62a`)已合入 main —— `resolveEmailCapabilityArg` 现在经由新的 `envBooleanFlag` / `OS_EMAIL_PERSIST_ENABLED`(三态,默认仍为 ON)读取 `cfgEmail.persist`。 于是本分支的断言在 merge queue 里必然变红:它 expect `unread == ['persist']`, 而合并后的 main 上 `unread == []`。跨车道协议(#5447 评论)约定后落地方对齐, 即本分支。 对齐做法:删除豁免数组,而非留一个空数组 —— 空注册表是一种邀请,下一个 declared-but-unread 键会被直接追加进去而不必辩论,正是该条目当初要防止的 「静默豁免」。断言随之收紧为两个方向都为空,即 declared 集与 read 集相等, 也就是文件注释当初许诺的 plain set equality。豁免的来龙去脉保留在注释中。 反向验证(方向预先判定为 red,结果一致):保留旧豁免数组对合并后的 main 运行, `AssertionError: expected [] to deeply equal [ 'persist' ]` —— 这正是本次预先 规避的队列失败;撤销豁免后该文件 6 个用例全绿。 另:changeset 里「它读八个键」是 #5470 之前的读侧计数(现为九个),补时间 限定词「本次改动时」,以免这段 CHANGELOG 文案落地后失真。schema 中 `persist` 的 TSDoc「Persist to sys_email (default true)」经核对在 #5470 之后依然成立 (默认仍为 ON),故不改。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
跨车道对齐:撤销
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31015301925 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
分诊(spec 车道 PM,
给 merge-queue-triage 检索者:本 PR 24h 内两次队列失败都是已定因的跨车道语义合并冲突(persist 接线 / appName 优先级),零 flaky 成分。 Generated by Claude Code |
#5448 已裁 direction B 并由 PR #5498 落地:`resolveEmailCapabilityArg` 现在把 `appName` 放在 context 展开之后解析,五级链为 `OS_APP_NAME` > `config.email.appName` > `defaultTemplateContext.appName` > 顶层 `appName` > `'ObjectStack'`。本分支上写于旧序之上的三处东西随之收口: - `serve-email-config-parity.contract.test.ts` 那条 pin 原本钉的是旧序 (context.appName 压过 env),现改为钉新序。它保留本文件自己的角度而非 重述 #5498 的用例:配置先过真正的 `EmailServiceConfigSchema.parse()` 再喂 读侧,因此钉住的是 #5307 新加的两个契约键既能存活 parse、又确实落在 schema 文案承诺的档位上。 - `email-config.zod.ts` 中 `appName` / `defaultTemplateContext` 的 TSDoc 与 两处 `.describe()`:旧文案写的是「写在 context 里的 appName 压过 appName 键与 OS_APP_NAME、是否合理 filed as #5448」,该事实已不成立。 `email-config.mdx` 由 `gen:docs` 整体重生成(未手改),9 个 generated 门全绿。 运行时零改动 —— `serve.ts` 未被本次改动触碰。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
对齐轮 2:收口到 #5448 已裁定的新序(70ad09d)已 merge #5448 的结局裁 direction B,由 PR #5498 落地:
本次改了三处
先证红(方向先定,再跑)预测:把 实测与预测一致: rung 2 因断言在 rung 1 即中止而未跑到,单独探针补证它同样具判别力(旧序下无 env 时也答 三个值刻意互不相同 —— 断言一个三者一致的值在任何序下都会绿。随后 验证未改 draft/ready 状态,未合并。 Generated by Claude Code |
Fixes #5307
前提复核(先于实施)
issue 的前提在 origin/main @ ed0d2aa 上仍然成立,#5308(#5104)昨日动过同一文件但只加了 provider 枚举值:
EmailServiceConfigSchema当时声明:provider/apiKey/defaultFrom/retries/persist/options。差集正是 issue 说的三个。改了什么
三个键都是运行时已经在读的,本次是把契约追平既成事实,运行时零改动:
queueDeliveryz.boolean().optional()OS_EMAIL_QUEUE_ENABLED覆盖;retries复用为队列尝试预算;无 queue 服务或persist: false时在kernel:ready硬失败appNamez.string().optional()OS_APP_NAME→ 本键 → 顶层config.appName→'ObjectStack';无defaultFrom时兼作兜底发件人来源(no-reply@acme-crm.local)defaultTemplateContextz.record(z.string(), z.unknown()).optional()sendTemplate()的渲染上下文。读侧原样透传两个刻意的克制:
.default()。 默认值由resolveEmailCapabilityArg对着 env 与顶层 config 解析,schema 再造一个只会多出一个谁也不赢的答案。defaultTemplateContext保持自由 record。 读侧只做透传,声明一套读侧没有的约束等于发明契约(PM notes 的要求,也是 Prime Directive Add comprehensive test suite for Zod schema validation #12 的方向)。新增的守护:把 issue 那条手工 grep 机械化
packages/cli/src/commands/serve-email-config-parity.contract.test.ts—— 这一族缺陷(#5104、本单)两次都是人肉 grep 发现的。该测试把读侧源码里的cfgEmail.<key>与EmailServiceConfigSchema.shape对齐,两个方向都断言:persist一项,登记在DECLARED_BUT_UNREAD并指回 spec/cli:EmailServiceConfig.persist声明了但没有载体 ——config.email.persist永远到不了 EmailServicePlugin(#5307 的反向面,ADR-0049) #5447,spec/cli:EmailServiceConfig.persist声明了但没有载体 ——config.email.persist永远到不了 EmailServicePlugin(#5307 的反向面,ADR-0049) #5447 落地后该数组清空、断言收紧为集合相等。另加行为半边:用真 schema
parse()一份作者配置,把结果喂给真resolveEmailCapabilityArg,断言三个值确实抵达插件选项 —— 只比名字不够,schema 可能用一个读侧根本不认的形状声明同名键。反向验证(方向先定,再跑)
预期:恢复到 origin/main 的 schema 后,新增的钉子全部转红。实测符合,并多出一条没预料到的信号:
expected undefined to be true(键被 strip 掉了)等。第 6 条leaves all three absent when unwritten恢复后仍绿 —— 它守的是「将来别加.default()」,不是本次修复的红/绿检测器,这里如实记下而不是凑成 6/6。declares every config.email key the resolver reads报的正是expected [ 'appName', …(2) ] to deeply equal [],与注释里写的三个键一字不差。仍绿的那条是persist豁免断言 —— 它守 spec/cli:EmailServiceConfig.persist声明了但没有载体 ——config.email.persist永远到不了 EmailServicePlugin(#5307 的反向面,ADR-0049) #5447,与本单无关。authorable-surface.json,pnpm --filter @objectstack/spec build直接失败 ——check:authorable-surface是删除棘轮,已记录的三个键突然消失即触发它的 tombstone 流程。生成物这一半也是被钉住的,不只是随行产物。生成物(#4001 纪律)
packages/spec/authorable-surface.json+3(gen:schema)。注:该 gate 是删除棘轮,新增键若不记入就「对它永久隐形」,所以必须随行。content/docs/references/system/email-config.mdx重新生成,属性表出现三行。pnpm gen:strictness-ledger整体重算,零 diff —— 台账只分诊ui/data/automation/security/studio,system/不在其内,且往既有z.object(加键不改变 site 数。如实报告为「重算过、无变化」,没有手改任何数字。check:generated九个 gate 全绿。顺带发现(均已另立单,本 PR 不修)
EmailServiceConfig.persist声明了但没有载体 ——config.email.persist永远到不了 EmailServicePlugin(#5307 的反向面,ADR-0049) #5447 ——persist是本单的反向面:插件选项活着(email-plugin.ts:482按它决定是否构造EmailPersistence),但config.email全仓唯一的读者resolveEmailCapabilityArg从不读这个键,所以config.email.persist: false永远到不了插件。ADR-0049 territory,按 PM 要求只核实与报告。config.email.defaultTemplateContext.appName压过OS_APP_NAME—— 与「env 逐项覆盖」的声明相反 #5448 ——config.email.defaultTemplateContext.appName压过OS_APP_NAME,与本文件头部声明的「env 逐项覆盖」相反。本 PR 只把实测到的现状写进 TSDoc 并用测试钉住,不改行为。.describe()里的{{var}}在生成的参考文档里被转义成 `{{var}+ 游离的}`(main 上现存 3 处) #5452 ——.describe()里的双花括号被文档生成器切成`{{x}`加游离},main 上现存 3 处。appName的 describe 已绕开并在源码留注释。验证
已 merge origin/main(61fde5e),与来件零文件重叠;合并后
check:generated复跑仍全绿,origin/main...HEAD的 delta 恰为 6 个文件、394 行纯新增、零删除。Generated by Claude Code