从 #4001 的 hook/datasource 步骤(#4207)延伸出来。#4207 把 `HookSchema` / `HookBodySchema` 收成 `.strict()` 并配了可修错误(别名 `timeoutms → timeout`、`backoff → 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",#4207 精心写的别名/处方要等运行时才见到; 4. **默认值不物化**:扫描路径产物是 `z.input` 形状,defineStack 路径绑定后是 `z.output`,两条路径产物不一致; 5. **AI 自验证闭环缺失**:没有「import 即校验」的廉价反馈,AI 作者拿到 tsc 绿就报告完成。 ## 问题二:错误处方引用了一个不存在的函数 `packages/spec/src/data/object.zod.ts` 的 `UNKNOWN_KEY_GUIDANCE` 有两处(`workflows`、`hooks` 两个墓碑,约 1247 / 1255 行)prescribe: > registered via \`defineHook()\` 全仓 grep 确认 **`defineHook` 在任何包里都不存在**。作者在 object 上写 `hooks: […]` 被 strict 拒绝后,照着错误信息去 import `defineHook` 会再失败一次——错误处方本身在制造第二个错误。这对 AI 作者尤其糟糕:处方是我们自己写的,AI 会无条件照做。 ## 方案 照 `defineDatasource` 模板(`packages/spec/src/data/datasource.zod.ts:538`): ```ts 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 的姿态:静默失效比硬报错更坏,因为它制造虚假的完成。 ## 参考 - #4001(母 issue)、#4207(hook/datasource strict + 别名/处方错误) - `packages/spec/src/data/datasource.zod.ts:538`(`defineDatasource` 先例) - `packages/objectql/src/hook-binder.ts`(元数据 hook 的唯一绑定入口) - `docs/audits/2026-07-unknown-key-strictness-ledger.md`(分类账)
Activity
feat(spec): defineHook() — authoring-time factory for lifecycle hooks (…
test(spec): the fourth data/ file group's test titles state each cite…