Skip to content

[spec] HookContext.api 声明为 z.unknown() —— 按 (ctx: HookContext) 标类型的 hook 无法调用 ctx.api.object(…),而文档/技能全在这么教 #5945

Description

@os-zhuang

事实(对 origin/main 核实)

packages/spec/src/data/hook.zod.ts 里,hook 的主数据通道是这样声明的:

  /**
   * Cross-Object API
   * …
   * Usage in hooks:
   *   const users = ctx.api.object('user');
   *   const admin = await users.findOne({ where: { role: 'admin' } });
   */
  api: z.unknown().optional().describe('Cross-object data access (ScopedContext)'),

于是 HookContext['api'] 推断为 unknown,JSDoc 里那两行示例本身编译不过。实测(用 check:skill-examples 同一套 tsconfig,对已构建的 @objectstack/spec 声明编译):

error TS18046: 'ctx.api' is of type 'unknown'.      // ctx.api.object('x')

为什么是缺陷而不是风格问题

  1. 整个语料库都在教这个写法。 skills/objectstack-data/references/data-hooks.md:732-745(handler: async (ctx: HookContext) => { const users = ctx.api?.object('user'); … })、content/docs/automation/hooks.mdx:173-224content/docs/api/error-handling-server.mdx:289/382content/docs/automation/hook-bodies.mdx 的能力表 …… 都是 (ctx: HookContext) + ctx.api.object(…)。这些块都没有 os:check 标记,所以从没被编译过 —— 一旦标记就会全红。也就是说:AI 与人照抄的首选写法,在类型层面是不成立的,只是没有任何门看见。
  2. 它把「消费者兜底」写成了唯一出路。 [docs] runtime-services 的 hook 示例在教 ctx.services —— hook 上下文从来没有这个键,照抄的 hook 会拒掉每一次写入 #5720 的 PR docs(kernel): runtime-services 的 hook 示例改教真实通道 —— 去掉不存在的 ctx.services (#5720) #5938 需要一个真编译的 hook 示例,只能在示例里写 const api = ctx.api as CrossObjectApi(自建局部结构类型)。这正是 contract-first 反对的方向:每个消费者各自 cast 一遍,cast 的形状彼此不一致,而真值(ObjectQL 的 ScopedContext:object(name) → repo,transaction(cb))在契约里一个字都没有。
  3. 对照组说明这不是「Zod 表达不了就只能 unknown」。 同仓 packages/spec/src/contracts/ 已经用普通 TS interface 声明了一堆运行时服务面(IDataEngineISharingServiceIObjectQLEngine …),transaction / ql 这些同样"引擎侧对象"的键也是 z.unknown(),但它们不是文档教读者天天调的东西 —— api 是。

待定(不预设结论,交维护者/分诊)

  • A. 在 packages/spec/src/contracts/ 声明作用域跨对象 API 的接口(如 IScopedContext:object(name) 返回一个 repo 接口,transaction(cb)),HookContext.api 的 TS 类型指向它(Zod 侧可继续 z.custom/z.unknown,运行时不校验活对象),让 ctx.api.object('x').findOne(…) 直接编译过。成本:要把 repo 的方法面(find/findOne/count/aggregate/insert/update/upsert/delete)定成公共契约,并与 ScopedContext 实现对齐(此后二者漂移会被 check:api-surface 之类的门看见 —— 这正是收益)。
  • B. 什么都不改,把语料库改成显式窄化(每处示例自带 cast)。成本:教 AI 写 cast,且 cast 形状无人校验;api 的真实面继续只存在于 objectql 的实现里。
  • C. 折中:只声明 object(name)transaction(cb) 两个成员(文档实际教的全集),其余留待有消费者时再加 —— 与仓内「有证据才声明」的惯例一致(IDataEngine#4251 注释就是这个论证)。

我倾向 C:它让文档现在教的写法真的成立(收益立刻兑现),声明面又不超过已被消费的部分;A 的完整 repo 契约可以在有第二个消费者时按同一论证增补。但这是公共契约拍板,故不自行实现。

关联

未加标签、未认领,交 PM 分诊定级。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions