Skip to content

feat(spec): EmailProviderSchema 补上 'smtp',TSDoc 与 plugin-email 实际能力对齐 (#5104) - #5308

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5104-email-provider-types
Aug 4, 2026
Merged

feat(spec): EmailProviderSchema 补上 'smtp',TSDoc 与 plugin-email 实际能力对齐 (#5104)#5308
os-zhuang merged 3 commits into
mainfrom
claude/issue-5104-email-provider-types

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5104

先厘清派发中的矛盾:实测结论是 smtp,不是 ses

本单正文(04:39Z)说要加 'smtp';12:31Z 的释放认领评论却给了另一套派发包 ——「EmailConfigSchema 的 transport 判别联合加 ses 分支(region 必填、凭证可选走默认链)」。两者不能都当真,所以第一步是在 origin/main(@ 5993c5b)上实测 @objectstack/plugin-email 到底发布了什么。

实测结果(packages/plugins/plugin-email/src/transports/index.ts):

export const EMAIL_TRANSPORT_PROVIDERS = ['log', 'resend', 'postmark', 'smtp'] as const;
实测
SmtpTransport ✅ 已发布,src/transports/smtp.ts,nodemailer 惰性 import(ADR-0012)
SesTransport 不存在,仓库里没有任何 SES transport 实现文件
OS_EMAIL_SMTP_HOST / _PORT / _SECURE / _USER / _PASSWORD ✅ 全部存在(packages/cli/src/commands/serve.ts)
OS_EMAIL_SES_* ❌ 不存在
OS_EMAIL_PROVIDER=ses 抛错resolveEmailCapabilityArg 用 plugin-email 的 isEmailTransportProvider 把关,serve-email-capability.test.ts 明确断言它 throw
spec 里的 EmailConfigSchema / transport 判别联合 不存在email-config.zod.ts 只有 EmailServiceConfigSchema(平铺 z.object),没有任何判别联合

更关键的是,ses 在 plugin-email 里是被显式退役的 tag,还带着迁移话术:

RETIRED_EMAIL_PROVIDERS = {
  sendgrid: "no SendGrid HTTP-API transport was ever implemented. Use provider='smtp' ...",
  ses:      "no Amazon SES HTTP-API transport was ever implemented. Use provider='smtp' with host
             email-smtp.REGION.amazonaws.com, port 587, and SES SMTP credentials ...",
}

结论: 12:31Z 评论说的「plugin-email 的 SES 支持已在」与 main 相反,它描述的 EmailConfigSchema 判别联合也不存在。按本单的硬规则(spec 不得承诺 main 上没有的 provider),给 spec 加 ses 会制造反方向的 declared != implemented —— 正好是 #5094 被立单的那个缺陷。因此本 PR 按正文原始范围执行:只加 'smtp',并把 ses / sendgrid 的缺席写进 TSDoc 说明理由。本单正文的前提经复核成立

改动

运行时零改动。 plugin-email 与 CLI 未被修改;本 PR 在 plugin-email 下只新增了一个测试文件

  1. EmailProviderSchema 增加 'smtp' —— 纯加值,已有配置不受影响,无需 ADR-0087 conversion / migration。
  2. 重写该段 TSDoc。原文写着「Self-hosted SMTP is intentionally NOT shipped in plugin-email; apps that need SMTP register a custom IEmailTransport themselves」—— plugin-email: 实现 SMTP transport —— 设置页可选 SMTP 但后端无实现 #5087 之后整段为假。新文写明 SMTP 由 plugin-email 内置、nodemailer 惰性加载,并解释 sendgrid / ses 为何不是成员(从未实现 HTTP-API transport,都走 provider: 'smtp')。
  3. provider / options.describe()。原先这两格在生成文档里是空的;现在参考文档正面说明 provider: 'smtp' 的连接参数落在 optionshost(必填)/ port / secure / user / password,与 OS_EMAIL_SMTP_* 一一对应。
  4. 重新生成 content/docs/references/system/email-config.mdx

关于「TSDoc 仍在劝人自带 transport」

正文担心生成文档里还留着这句话 —— 实测它从未出现在文档里:EmailProviderSchema 的 schema 级 TSDoc 不参与 build-docs.ts 渲染,只有文件头块注释和 .describe() 会。所以那句假话的受害面是读源码/IDE 悬浮的作者(含 AI),文档侧的缺陷只是 Allowed Values 少一个值。两处都已修:枚举补值修文档,TSDoc 重写修源码,.describe() 则把 SMTP 的配置方式正面写进文档。

测试:双向锁死 provider 词表

按验收要求「把枚举的接受集钉到实测的运行时集合,让下一个 provider 必须两边都有意识地改」,分两侧:

  • packages/spec/src/system/email-config.test.ts(新增) —— 全部是运行时 safeParse 断言。packages/spec 的 tsconfig.json 排除了 **/*.test.ts,tsc --noEmit 根本不读这个文件(@ts-expect-error 退役 pin 在 packages/spec 里是幽灵检查:tsconfig 把 **/*.test.ts 排除出唯一的 tsc --noEmit #5286),所以在这里写 Assert< Equal< … > > 一类类型级断言会是幻检查。已在文件顶部注明这一点。
  • packages/plugins/plugin-email/src/transports/spec-provider-parity.contract.test.ts(新增,测试文件) —— 跨包契约,把 EmailProviderSchema.optionsEMAIL_TRANSPORT_PROVIDERS 双向比对,并要求 spec 声明的每个值都能被 makeTransport 真正造出来。

这是同一词表的第三条边:mail-manifest-providers.contract.test.ts 已经把设置页下拉框钉到 transports,本 PR 把授权契约也钉上去。放在 plugin-email 而不是 spec,理由与那个既有测试写的一样 —— 两侧各钉一个字面量,永远可以靠改另一个字面量「修好」,而这正是 #5094 里两者漂移的方式。@objectstack/spec 本就是 plugin-email 的正式依赖,无新增运行时边。

注意两侧的不对称:plugin-email 的 tsconfig 不排除测试文件,所以那边的编译期见证是真的 —— 下面已实证。

反向验证:预判方向 = 红,三处全部命中

动手前先预判:把枚举退回三个值,新测试应当变红(标准方向,非 #5046 的计数反转、也非 #5009 的倒置)。实测把 email-config.zod.ts 退回 z.enum(['log', 'resend', 'postmark']) 并重新构建 spec 后:

A. plugin-email 的 typecheck(编译期见证,证明它不是幻检查):

src/transports/spec-provider-parity.contract.test.ts(47,92):
  error TS2322: Type 'true' is not assignable to type 'never'.

B. plugin-email 的契约测试(运行时集合相等):

FAIL src/transports/spec-provider-parity.contract.test.ts
AssertionError: expected Set{ 'log', 'resend', 'postmark' } to deeply equal Set{ 'log', 'resend', 'postmark', …(1) }
 Tests  1 failed | 294 passed (295)

C. spec 侧测试:

× accepts exactly the providers plugin-email can deliver through
× accepts smtp — shipped by plugin-email since #5087 (ADR-0012)
× type-checks a config for every deliverable provider
× carries the SMTP connection on `options`, mirroring OS_EMAIL_SMTP_*
 Tests  4 failed | 3 passed (7)

随后已还原并重新构建。

验证(合入 origin/main 之后重跑)

pnpm --filter @objectstack/spec check:generated
  ✓ 全部 9 项 —— All 9 generated artifacts are up to date.

pnpm --filter @objectstack/spec --filter @objectstack/plugin-email typecheck
  packages/spec typecheck: Done
  packages/plugins/plugin-email typecheck: Done

pnpm --filter @objectstack/spec --filter @objectstack/plugin-email test
  packages/spec          Test Files 309 passed (309)   Tests 7951 passed (7951)
  packages/plugins/plugin-email  Test Files 20 passed (20)  Tests 295 passed (295)

生成物纪律:--fix 只重算了被证明过期的那一项(content/docs/references/**);check:api-surface(无导出增删)、check:authorable-surfacecheck:strictness-ledger 本来就是绿的 —— 本 PR 没有新增可授权键(只是给既有 provider 键的枚举加了一个值),所以 #5220 的分类账纪律不触发,gen:strictness-ledger 未运行、计数未变。

同步:已 git merge origin/main(⛔ 无 rebase / force-push)带入 #5294。它只碰 plugin-auth / verify / qa,与本 PR 零重叠,os-regen-pending 为空;合并后 9 项闸门 + 两包 test/typecheck 已如上重跑。相对 origin/main 的 delta 恰为 5 个文件。

node scripts/check-nul-bytes.mjs 通过;另对本 PR 全部文件做了闸门覆盖不到的自扫(grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]'),无控制字节。

范围外发现

#5307 —— EmailServiceConfigSchema 未声明 CLI 实读的 queueDelivery / appName / defaultTemplateContext,与本单同族、不同键(queueDelivery#5160 落地的耐久队列开关,作者标注类型后写它会拿到类型错误)。不并进本 PR:那需要新增可授权键,要走 #5220 的 strictness-ledger 纪律,体量与风险都不同,合并会让本 PR 的 diff 失焦。已建议与本单串行(同文件 + 同 os-regen 生成物)。

changeset:@objectstack/spec minor(追平既成事实的加值型契约变更)。

⛔ 未改 content/docs/releases/;未新增文档页面。

🤖 Generated with Claude Code

https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB


Generated by Claude Code

claude added 2 commits August 4, 2026 17:04
…#5104)

`EmailProviderSchema` 停在 log/resend/postmark,而它自己的 TSDoc 还在说
「Self-hosted SMTP is intentionally NOT shipped in plugin-email」。#5087 之后
两句都不成立:plugin-email 已内置 SmtpTransport(ADR-0012,nodemailer 惰性
import),CLI 接受 OS_EMAIL_PROVIDER=smtp 与 OS_EMAIL_SMTP_* 一族。

于是形成 spec 落后于运行时的 declared != implemented:用 EmailServiceConfig
标注 objectstack.config.ts 的作者写 provider: 'smtp' 会拿到类型错误,而该
provider 早就能正常投递。

- EmailProviderSchema 增加 'smtp'(纯加值,无需 ADR-0087 conversion)
- 重写 TSDoc:SMTP 内置、nodemailer 惰性加载;写明 sendgrid/ses 不是成员
  (从未实现 HTTP-API transport,走 provider='smtp',见 #5094)
- provider / options 补 .describe(),参考文档正面说明 smtp 连接参数与
  OS_EMAIL_SMTP_* 的一一对应
- 跨包契约测试把 spec 词表与 plugin-email 的 EMAIL_TRANSPORT_PROVIDERS 双向
  锁死(运行时集合相等 + 编译期互相可赋值)
- 重新生成 content/docs/references/system/email-config.mdx

运行时零改动:plugin-email 与 CLI 未被修改,新增的仅为测试文件。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 9:13pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 21:14
@os-zhuang
os-zhuang enabled auto-merge August 4, 2026 21:14
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 37e38d1 Aug 4, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5104-email-provider-types branch August 4, 2026 21:36
os-zhuang pushed a commit that referenced this pull request Aug 4, 2026
Second relay merge: #5300 / #5304 / #5306 / #5308 / #5318 / #5326 / #5327.
Textually clean, but the os-regen driver defers generated artifacts rather than
text-merging them, so `json-schema.manifest.json` again came out holding this
branch's pre-merge side — this time still listing `ui/EmbedConfig` and
`ui/NotificationAction`, both retired by #5300. Reset the deferred artifacts to
`origin/main`, rebuilt from the merged tree, regenerated wholesale.

Post-regen assertions (a silent one-side drop is exactly what this catches):
api-surface delta vs `origin/main` is exactly this PR's four additions and ZERO
removals; manifest delta is one addition (`ui/ViewItemWire`) and zero removals;
every sibling retirement stays removed (`ui/EmbedConfig`, `ui/NotificationAction`,
`system/HttpServerConfig`, `ui/Animation`, `ui/ZIndex`) and every sibling
addition stays present (`FilterArray` ×7, `EmailProvider` ×2).
`check:authorable-surface` (+ its #5304 `.base.json` anchor) is green and the
anchor file is byte-identical to `origin/main` — not hand-edited.

`metadata-form-zod-reconciliation.test.ts` co-edited with #5280/#5318 and merged
SEMANTICALLY, not by taking a side: #5318 rewrote the docblock, imports, helpers
and test bodies, while this PR's only edit is `unwrap`'s `pipe` case, so the two
did not overlap textually — but they do interact, and in the direction that
matters. #5318's `isRetiredAt` / `authorableKeysOf` both route through
`unwrap`/`keysOf`, and `view`'s root is now a `z.preprocess` pipe. Measured both
ways: without this PR's #4488-style fix `unwrap(view root)` resolves to
`transform` and `keysOf` returns NULL, so #5318's brand-new tombstone assertions
would be VACUOUS on `view` (and the pre-existing key-bearing assertion would
fail outright); with it, 89 keys. Both PRs' assertions are live on every type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: EmailProviderSchema 缺 'smtp',TSDoc 仍称 SMTP 不随 plugin-email 发布(#5087 落地后为假)

2 participants