事实(对 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')
为什么是缺陷而不是风格问题
整个语料库都在教这个写法。 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-224、content/docs/api/error-handling-server.mdx:289/382、content/docs/automation/hook-bodies.mdx 的能力表 …… 都是 (ctx: HookContext) + ctx.api.object(…)。这些块都没有 os:check 标记 ,所以从没被编译过 —— 一旦标记就会全红。也就是说:AI 与人照抄的首选写法,在类型层面是不成立的,只是没有任何门看见。
它把「消费者兜底」写成了唯一出路。 [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))在契约里一个字都没有。
对照组说明这不是「Zod 表达不了就只能 unknown」。 同仓 packages/spec/src/contracts/ 已经用普通 TS interface 声明了一堆运行时服务面(IDataEngine、ISharingService、IObjectQLEngine …),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 分诊定级。
事实(对
origin/main核实)packages/spec/src/data/hook.zod.ts里,hook 的主数据通道是这样声明的:于是
HookContext['api']推断为unknown,JSDoc 里那两行示例本身编译不过。实测(用check:skill-examples同一套 tsconfig,对已构建的@objectstack/spec声明编译):为什么是缺陷而不是风格问题
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-224、content/docs/api/error-handling-server.mdx:289/382、content/docs/automation/hook-bodies.mdx的能力表 …… 都是(ctx: HookContext)+ctx.api.object(…)。这些块都没有os:check标记,所以从没被编译过 —— 一旦标记就会全红。也就是说:AI 与人照抄的首选写法,在类型层面是不成立的,只是没有任何门看见。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))在契约里一个字都没有。packages/spec/src/contracts/已经用普通 TS interface 声明了一堆运行时服务面(IDataEngine、ISharingService、IObjectQLEngine…),transaction/ql这些同样"引擎侧对象"的键也是z.unknown(),但它们不是文档教读者天天调的东西 ——api是。待定(不预设结论,交维护者/分诊)
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之类的门看见 —— 这正是收益)。api的真实面继续只存在于 objectql 的实现里。object(name)与transaction(cb)两个成员(文档实际教的全集),其余留待有消费者时再加 —— 与仓内「有证据才声明」的惯例一致(IDataEngine的#4251注释就是这个论证)。我倾向 C:它让文档现在教的写法真的成立(收益立刻兑现),声明面又不超过已被消费的部分;A 的完整 repo 契约可以在有第二个消费者时按同一论证增补。但这是公共契约拍板,故不自行实现。
关联
ctx.services—— hook 上下文从来没有这个键,照抄的 hook 会拒掉每一次写入 #5720 / PR docs(kernel): runtime-services 的 hook 示例改教真实通道 —— 去掉不存在的ctx.services(#5720) #5938(hook 文档改教ctx.api;因该 PR 派发面packages/spec/**零改动,故此项单独记录)check:skill-examples应拒绝 os:check 块内把入参标any—— 标记了却零覆盖的空门 #5943(check:skill-examples禁 os:check 块内入参标any)—— 若本单按 A/C 修好,那些语料块才可能真的进os:check;否则它们进门就红。未加标签、未认领,交 PM 分诊定级。