Skip to content

补上 defineHook():hook 的作者侧工厂(defineDatasource 范式),并修正 object.zod.ts 两处引用不存在函数的错误处方 #4269

Description

@os-zhuang

#4001 的 hook/datasource 步骤(#4207)延伸出来。#4207HookSchema / HookBodySchema 收成 .strict() 并配了可修错误(别名 timeoutms → timeoutbackoff → backoffMs,处方 enabled/active → 「hook 没有开关键,用 condition」等)——但这些错误信息只在 parse 真正跑起来时才会出现,而 hook 恰好有一条作者路径完全不跑 parse。

问题一:约定扫描路径在作者时零运行时校验

hook 的元数据作者路径有两条,校验时机不对称:

路径 parse 时机 #4207 的错误信息何时可见
defineStack({hooks}) / 模板 seeder / metadata service bindHooksToEngine(packages/objectql/src/hook-binder.ts)绑定时 部署/绑定期
约定扫描 src/objects/<name>.hook.ts 裸字面量 作者时不 parse,字面量直接进产物 最早也要等到部署

裸注解 const H: Hook = {…}(Hook = z.input<typeof HookSchema>)在新鲜对象字面量上已有 TS excess-property checking,未知键编译期能抓到——这部分不是缺口。真实缺口是:

  1. 约束级校验类型层拿不到:name 的 snake_case 正则、URL 格式、min 边界等;
  2. 非字面量构造路径(spread、计算键、JSON.parse 来的对象)绕过 excess-property checking;
  3. 错误信息质量:tsc 只报 "Object literal may only specify known properties",feat(spec)!: hook 与 datasource 拒绝未知键(#4001 data 步,收尾最后两个待验证分类) #4207 精心写的别名/处方要等运行时才见到;
  4. 默认值不物化:扫描路径产物是 z.input 形状,defineStack 路径绑定后是 z.output,两条路径产物不一致;
  5. AI 自验证闭环缺失:没有「import 即校验」的廉价反馈,AI 作者拿到 tsc 绿就报告完成。

问题二:错误处方引用了一个不存在的函数

packages/spec/src/data/object.zod.tsUNKNOWN_KEY_GUIDANCE 有两处(workflowshooks 两个墓碑,约 1247 / 1255 行)prescribe:

registered via `defineHook()`

全仓 grep 确认 defineHook 在任何包里都不存在。作者在 object 上写 hooks: […] 被 strict 拒绝后,照着错误信息去 import defineHook 会再失败一次——错误处方本身在制造第二个错误。这对 AI 作者尤其糟糕:处方是我们自己写的,AI 会无条件照做。

方案

defineDatasource 模板(packages/spec/src/data/datasource.zod.ts:538):

export function defineHook(config: Hook): ResolvedHook {
  return HookSchema.parse(config);
}

Hook / ResolvedHook(z.input / z.output)已存在于 hook.zod.ts:387-388,不需要新类型。范围:

  • 工厂函数 + drift-guard 测试(工厂产物 = schema parse 产物,防止两者分叉)
  • 文档一处「工厂优先于裸字面量」注记(datasource 已有先例)
  • UNKNOWN_KEY_GUIDANCE 两处引用随实现落地即成真,顺手复核措辞
  • changeset(minor,新公共 API)
  • v1 保持纯 parse:不在工厂里塞 body/L1 的劝导逻辑——handler 弃用告警已由 binder 的 warnDeprecatedHandler 覆盖,不要两处维护同一套劝导

已验证的非阻塞点

  • handler 为函数值时经 z.custom 原样通过 .parse(),不会被序列化/剥离;
  • 双重校验幂等:绑定期对已 parse 的输出再 parse 是安全的;
  • 迁移纯增量:存量裸字面量不受影响,工厂是可选入口。

有意的行为变化

parse-at-import 把坏 hook 从「绑定期 skip + warning」变成作者期硬失败。这是 #4001 的姿态:静默失效比硬报错更坏,因为它制造虚假的完成。

参考

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions