Skip to content

automation/etl.zod.ts 的九个类型别名全部导出 parsed 形状,违反 X / XParsed house convention(SYNC_ARCHITECTURE.md 的示例因此不可编译) #4963

Closed

Description

@xuyushun441-sys

发现于 #4001 批 12(automation/etl.zod.ts 的 strict 收紧),不在该 PR 范围内,按 Prime Directive #10 单独记录。与 strictness 正交:.strict() 既不改 z.input 也不改 z.infer,本条在批 12 前后完全一致。

事实

house convention 是 X = 作者写的东西(z.input),XParsed = parse 之后的东西(z.infer)。shared/retry-policy.zod.ts 的 JSDoc 把这条写得最清楚,而且记录了它自己就是从违反态改过来的:

Note for pre-17 @objectstack/spec/system consumers: this used to be z.infer (the post-parse shape, every key present) on that entry only. It is now the input shape on both, matching the house X / XParsed convention used by the sibling control-flow configs.

automation/flow.zod.ts / automation/io-node-config.zod.ts / automation/builtin-node-config.zod.ts 都成对导出(Flow + FlowParsed、NotifyConfig + NotifyConfigParsed……)。

automation/etl.zod.ts 九个别名一个都没跟上,全部是 parsed 形状占用了裸名,且没有任何 *Parsed 对应物:

ETLEndpointType, ETLSource, ETLDestination, ETLTransformationType,
ETLTransformation, ETLSyncMode, ETLPipeline, ETLRunStatus, ETLPipelineRun
    = z.infer< typeof …Schema >

为什么这不是纯风格问题

因为 ETL 上有四个带 .default() 的键 —— ETLDestination.writeMode、ETLTransformation.continueOnError、ETLPipeline.syncMode、ETLPipeline.enabled、ETLPipeline.retry 的两个子键。在 z.infer 下它们全是必填。于是"用 ETLPipeline 标注一份手写的 pipeline 字面量"这个最自然的用法直接编译不过 —— 而这正是本文件唯一的授权门(三仓零 importer、零 parse 点,作者接触这个契约的方式就是 const x: ETLPipeline = { … })。

仓库里现成的证据:packages/spec/docs/SYNC_ARCHITECTURE.md 的 Migration Guide,"After (L2)"一段:

const pipeline: ETLPipeline = {
  name: 'order_analytics_pipeline',
  source: { type: 'api', connector: 'orders' },
  transformations: [
    { type: 'aggregate', config: { groupBy: ['customer_id'] } }
  ],
  destination: { type: 'database', config: { table: 'analytics_order' } }
};

这段不可编译,两个独立原因:source.config 是必填却没写;syncMode / enabled / destination.writeMode / transformations[].continueOnError 在 z.infer 下必填却没写。同一节的"Before (L2)"更短,同样过不了。因为它们在 .md 里,没有任何 gate 读到 —— 这是"declared ≠ enforced"的文档版本:我们向数据工程师展示了一份平台自己拒绝的 L2 pipeline。

ETL.databaseSync / ETL.apiToDatabase 两个 factory 的返回类型标成 ETLPipeline,因此被迫把 syncMode / enabled / writeMode 显式写死 —— 那不是 factory 的设计意图,是被 z.infer 逼出来的。

两个选项

A. 按 house convention 改:裸名 = z.input,新增九个 *Parsed = z.infer。

  • 长期正确性:最好。这正是 spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 对 RetryPolicy 做过的同一件事,理由已经论证过,不需要重新论证;etl 是 automation/ 里最后一个没跟上的文件。
  • 让 AI 写的 metadata 不易出错:最好。作者(人或 LLM)拿到的 ETLPipeline 从此就是"我该写什么",把带默认值的键正确标成可省略;写完即可编译,不再需要靠试错发现 syncMode 其实可以不写。
  • 成本:breaking(裸名语义变化 + 九个新导出),需要 gen:api-surface + changeset 迁移文案(ETLPipeline → ETLPipelineParsed 给读取 parse 结果的消费者)。此处三仓零 importer,所以实际迁移面为空 —— 和 ETLPipeline.retry is a third retry-policy vocabulary that #4661 的收敛没有覆盖到 #4962 一样,这是此刻做最便宜的一类改动。
  • 顺带:SYNC_ARCHITECTURE.md 的两段示例在 A 之后只剩"source.config 必填却没写"一个错误,需要在同一个 PR 里补上。

B. 只修文档示例,类型别名不动。

  • 长期正确性:差。文档被改成"符合 parsed 形状"意味着示例里要写满 syncMode: 'full', enabled: true, writeMode: 'append', continueOnError: false 这些作者本不必写的键 —— 等于把违反 convention 的后果固化成"推荐写法",下一个照抄的作者会以为这些键是必须的。
  • 让 AI 写的 metadata 不易出错:差,且是反向的。LLM 抄文档,抄到的会是一份噪音更多、且暗示"默认值必须显式重复"的模板。
  • 成本:最低,非 breaking。

建议

A,两轴一致。这是 #4661 已经在同一个仓库、同一个 automation/ 目录下走过一遍并写下理由的路径,etl 只是没被那次收敛扫到(它没有导出名冲突,不在 #4535 的雷达上 —— 与 #4962 同一个漏网机制)。B 用最低成本换来一份把错误示范写进文档的结果,在"让 AI 写的 metadata 不易出错"这一轴上是负收益。

如果维护者希望把 breaking 面压到最小,可以把 A 拆成两步:先加九个 *Parsed 导出(纯增量,非 breaking),下一个 major 再翻转裸名 —— 但鉴于此处零 importer,我认为一次做完更省事。

/cc #4001 #4661 #4962

Activity

  1. self-assigned this
    on Aug 5, 2026
  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    认领:spec 车道第 2 批
    会话:session_018fxLGQdatPbBUvCgiVxg6D
    分支:claude/issue-4963-etl-parsed-aliases
    Worktree:objectstack-issue-4963
    域:domain:spec
    文件面:packages/spec/src/automation/etl.zod.ts + packages/spec/docs/SYNC_ARCHITECTURE.md + 生成物(api-surface 系列)+ changeset。

    裁定:取 A(裸名翻转为 z.input,新增九个 *Parsed),一次做完不拆两步 —— 三仓零 importer 使迁移面为空,#4661 同目录先例已论证,两轴同向;v17 pre 窗口未关,是最便宜的落地时刻。维护者可否决(否决窗口)。SYNC_ARCHITECTURE.md 两段示例同 PR 修到可编译。


    Generated by Claude Code

  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    验收(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D):ACCEPT → PR #5514(draft;快速门 10/10 绿,重活在共享 runner 排队,终态由本车道落地扫确认后转 ready 入队)。

    裁定 A 执行完整且有三处超出模板的判断值得记:

    1. enum 别名也成对的理由成立(读者不必先知道九个里哪个带默认值才能选注解,且 enum 日后加 transform 会重开本 issue);factory 只删「复述默认值」的键、保留「helper 自己决定」的对比键 —— 这条区分把设计意图和被迫写法分开了,正确。
    2. compiler-API 测试而非类型级 pin,依据 packages/spec 的「编译期 pin」是失效的:tsconfig 排除了 *.test.ts,vitest 也不做类型检查 #4642 的实证(本包测试层 pin 是 no-op),自带 anti-vacuity 探针 —— 用的是今天上午刚沉淀的车道知识。
    3. 反向验证把「红得不是地方」剥出来不当证据(TS2305 名字不存在 ≠ 方向被钉住),另做真 sabotage 拿到 expected '' to contain 'writeMode' —— parsed 半边 pin 活性的真证据。issue 描述与实测的偏差(Before (L2) 还缺 name/destination)如实纠正并补齐。

    三仓零 importer 复测(git -C + 先 fetch)合规,api-surface +9/−0 无手改。两张新单已核实并分诊:#5507(execution.zod.ts 七对纯同义词,finding 持有 —— 迁移面不为空,不适用零 importer 论证,择期单独裁定)、#5515(SYNC_ARCHITECTURE 的 L3 sapConnector 示例教了三个 schema 拒收的键名,bug 入队;其中 Connector = z.infer 孪生项 20 处裸名在 live parse path,派发时需单独定 appetite)。排空模式下两者暂不派。


    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