Uh oh!
There was an error while loading. Please reload this page.
feat(spec)!: HookContext.api 从 z.unknown() 收窄为最小 IScopedContext (#5945) - #6311
Conversation
) 维护者裁决 C:`HookContext.api` 不再是 `z.unknown()`,改为指向 `packages/spec/src/contracts/scoped-context.ts` 里新增的 `IScopedContext` / `IScopedObjectRepository`(与 IDataEngine / IObjectQLEngine 同层同风格,含 evidence-bar 模块头)。 声明面 = 语料库实测的调用点,不多不少: IScopedContext object(name) + transaction(cb, opts?) IScopedObjectRepository find / findOne / count / insert / update / updateById upsert / delete / aggregate / create 只出现在文档的方法表与能力表里、没有任何 调用点(表格不过编译器);sudo() 的三个调用方全部把值持成 any 且它是提权动作。 一律不声明,等到有调用点再按同一条规则加 —— 与 IDataEngine (#4251) 同款纪律。 运行时零变化:Zod 侧仍是 z.unknown(),收窄是静态 cast(与 object.zod.ts 的 ObjectCapabilities.apiMethods 同一惯用法)。z.custom 试过,它让 HookContext 在 JSON Schema 里不可表达 —— gen:schema 直接不再产出 json-schema/data/HookContext.json, 会在下次 gen:docs 抹掉参考页(#2978),故不用。接受的值、JSON Schema、生成的参考页 行全部不变,只有 .describe() 文案改了。 漂移由编译器盯着:objectql 的 ScopedContext / ObjectRepository 声明 implements。 实测把 updateById 改名,objectql 的 tsc 在 implements 处 + 五个 hook 派发点同时报错。 content/docs/kernel/runtime-services/examples.mdx 里那段 os:check 块删掉了自建的 `type CrossObjectApi` + `ctx.api as CrossObjectApi`,改为直接读契约。 scripts/engine-double-contract.baseline.json 新增两条 EXEMPT:两个新 fake 是 IScopedObjectRepository 的类型符合性见证,不是 engine double(scoped repository 是 该门二分法没有的第三种);spec 也无法 import objectql/metadata-core(依赖反转)。 dormancy 已用 stderr 探针实测(对照组会打印,被测的 update 全程静默)。 Fixes#5945 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 115 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
qq9340100
commented
Aug 7, 2026
ACCEPT(spec 车道 PM 验收, 逐 job 结论亲读:24 checks 全 completed,23 success + 1 预期 skipped(Console Pin Gate),ESLint / TypeScript / Check Changeset 均
已翻 ready + auto-merge。 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#5945
落地 2026-08-07 维护者裁决 C:
HookContext.api停止z.unknown(),声明语料库实际教的最小IScopedContext。问题
HookContext.api是文档教的主数据通道,类型却是unknown。于是所有文档、技能、示例里那个标准写法一行都编译不过 —— 包括hook.zod.ts里api这个键自己 JSDoc 上的示例:唯一进了
os:check的那块(content/docs/kernel/runtime-services/examples.mdx)只能靠在示例里自建type CrossObjectApi = …再ctx.api as CrossObjectApi才编得过 —— 每个消费方各 cast 一遍、形状无人校验,正是 contract-first 要终结的方向。声明面 —— 证据表(逐条 file:line)
判据(写进了模块头,可复现):一个成员在语料库里存在对 hook
ctx.api的调用时才声明 —— 调用才是要过编译器的东西。表格是散文,它不过编译器。声明(8)
object(name)transaction(cb)api/error-handling-server.mdx:382;data-hooks.md:447(api.transaction能力行)findOnedata-hooks.md:402,421,422,423,480,524;rules/hooks.md:72;data-service.mdx:26;runtime-services/examples.mdx;hook.zod.tsJSDocfinddata-hooks.md:403,405,425countdata-hooks.md:404,832;hook-bodies.mdx:84insertdata-hooks.md:756,920;automation/hooks.mdx:187;error-handling-server.mdx:391(tx 回调内)updatedata-hooks.md:491,527,977;rules/hooks.md:74;hook-bodies.mdx:163,210,223,298;error-handling-server.mdx:395updateByIderror-handling-server.mdx:289;hook-bodies.mdx:197update两种形态都要:update({ id, ...fields })与update(data, { where, multi: true })。不声明(测量过,不是漏了)
upsertdata-hooks.md:395,446deletedata-hooks.md:446、hook-bodies.mdx:101,315。另有packages/lint测试里的 body-source 字符串 fixture,那是 lint 规则的输入,同样不过编译器aggregatehook-bodies.mdx:100,314createdeleteById/executesudo()persistAuditTrailRow/captureBefore都调它,但三处全部把值持成any(api: any、(ctx as any).api)—— 没有一个是本次声明能解锁的类型化消费方;且sudo()是提权,把它放进第一个 hook 的文档面等于把「绕过记录级权限」写进入门词汇,没有任何文档这么教,裁决也没授权。要不要声明是维护者的判断,不是一次测量beginTransaction/commit/rollback三件套userId/tenantId/spaceId/roles/isSystem/transactionHandlegetterctx.session(已声明、已类型化、带 ADR-0090 D3 词汇);在这里再开一套拼写就是同一事实的第二种方言,而roles这个拼法 ADR-0090 D3 直接禁与
IDataEngine(#4251)确立的「有证据才声明」同款纪律。裁决里的选项 A(整个 repo 方法面)按同一条规则以后再长。三个刻意的偏离,附论证
1.
transaction的签名给全,不只cb一参。证据门槛管的是哪些成员存在,不是允许对一个已声明成员的签名说假话。回调的参数表描述的是生产方递进去什么,而生产方无条件递两个(
ScopedContext.transaction,#5696);只声明一参既是假陈述,又会让async (tx, info) => …这段能跑的代码编译报错。逆变保证语料库真正写的零参/一参回调照样满足两参声明 —— 所以真实的签名同时也是更宽松的那个。opts同理:ADR-0119 D1 + #5696 明确裁定这个面是IObjectQLEngine.transaction的第二个实现、绝不是第二种方言(opts.require的 fail-closed 在这里一模一样地兑现),契约里省掉它等于在声明层把实现刚关掉的方言重新打开。两个类型都复用 #6165 刚落的EngineTransactionInfo/EngineTransactionOptions,没有新词汇。代价已测量:
scoped-context.ts因此 import./objectql-engine.js,check:skill-refs的objectstack-data传递依赖索引多了 4 条(flow-function.zod/driver.zod/query.zod/enums.zod)。这是生成器如实测出来的,不是噪声;若维护者更认可「最小 = 一参」,回退是 3 行。2. 查询参数不用
EngineQueryOptions,是开放对象。repository 把 query 原样 spread 给
IDataEngine,所以顺手会想用 spec 自己的EngineQueryOptions。那是假收窄:引擎读 query 前先折叠文档化的别名拼写(filter→where、top→limit,RPC_QUERY_ALIAS_SLOTS,在find/findOne/update三处生效),data-hooks.md明确教filter是被容忍的对象值别名并且有活的示例(:832)。EngineQueryOptions没有filter,对象字面量会被 excess-property check 拒掉 —— 对一个运行时接受、文档教的调用报编译错。选项词汇归EngineQueryOptions所有、归引擎强制(#4371 起拒绝未声明选项键);本接口的职责是方法面。legal-filter-alias探针把这条钉住了。3. Zod 侧仍是
z.unknown()+ 静态 cast,不是z.custom< IScopedContext >()。z.custom是显然的拼法,而且是错的:custom 在 JSON Schema 里不可表达,gen:schema直接不再产出json-schema/data/HookContext.json—— 构建原地报 "1 previously published schema disappeared",而这会在下次gen:docs抹掉references/data/hook.mdx那一页(#2978)。实测出来的,不是推测。改用object.zod.ts里ObjectCapabilities.apiMethods的同一惯用法:于是接受的值、JSON Schema、生成的参考页行逐字节不变,只有
.describe()文案动了(那是那一页说明这个值到底是什么的唯一通道)。漂移由编译器盯着
packages/objectql的ScopedContext/ObjectRepository声明了implements。实测:把ObjectRepository.updateById改个名,objectql 的tsc在implements处和五个 hook 派发点(api: this.buildHookApi(...))同时报错 —— 契约与引擎真正绑定的那个对象再也不能各说各话。反向验证 —— 方向先声明,然后实测,而实测推翻了一半预测
预测:还原
api: z.unknown(),legal-*全红报 TS18046,rejects-*会绿得没意义。跑了。实际:no-chainctx.api.object('x')TS18046: 'ctx.api' is of type 'unknown'.chain-legalctx.api?.object('x')TS2339: Property 'object' does not exist on type '{}'.rejects-upsertctx.api?.object(…).upsert(…)TS2339: … on type '{}'.rejects-sudoctx.api?.sudo()TS2339: … on type '{}'.TS18046 是 issue 报的症状,逐字复现 —— 但只在不带可选链时。
?.把unknown去掉null | undefined后剩{},所以语料库真正的写法(api是可选的)报的是 TS2339。于是只断言错误码不够:rejects-*在还原前后都报 TS2339,toContain('TS2339')在被还原的契约上照样通过 —— 一个 phantom check,只有真去跑反向验证才抓得到。改成断言诊断里点名归属类型(IScopedContext/IScopedObjectRepository),'{}'再也满足不了。再跑一次还原:失败从 2 个测试涨到 3 个,rejects-*如期变红。测试
packages/spec/src/contracts/scoped-context.test.ts:编译探针(ts.createProgram+ 真实诊断码),11 条legal-*全绿、6 条rejects-*报 TS2339 且点名类型、每组都带 harness 自检对照(解析失败会零诊断,看起来跟成功一样);hook.zod.ts的 JSDoc 示例从源文件读出来逐字编译(headline 抱怨的就是它编译不过),带反空洞断言。hook.test.ts新增#5945块:parse 仍接受活引擎对象且返回同一个实例(不 clone —— clone 出来的 repository 闭包指向错的执行上下文,形状检查看不见)、api仍可选、z.unknown()接受的值一个不少、文档教的写法 tsc 过、.describe()措辞。该块诚实地标注为回归护栏而非落地证据:还原后它全绿(z.custom/z.unknown接受的值相同,这正是选它的原因)。check:generated十门全绿(重生成skill-refs/api-surface/docs三件);六项源审计(liveness/empty-state/skill-examples/variant-docs/exported-any/dual-source-exports)全绿 ——check:skill-examples208 例通过,含改过的examples.mdx:124。@objectstack/spec332 文件 / 8439 测试通过;typecheck通过。@objectstack/objectql135 文件 / 2214 测试通过,typecheck+build通过。消费半径:runtime/plugin-audittypecheck 通过(两者对ctx.api的读取都持any,不受影响)。check:engine-double-contract:两个新 fake 被判为 engine double —— 它们其实是IScopedObjectRepository的类型符合性见证,scoped repository 是该门 engine/driver 二分法没有的第三种(它的写动词是update(data, options?),对象名根本不是参数)。按data-engine.test.ts已有的两条同类先例记 EXEMPT:spec 无法 import objectql/metadata-core(依赖反转)。dormancy 用 stderr 探针实测(check:engine-double-contract 看不见「delete 少于两个形参」的假引擎 —— 实测 150 个 double 里 91 个(49 个文件)因此不在扫描面内 #5629 的方法,而上面两条 update 条目自己说它们没做):被测的update全程静默,同一次运行里注入到会被调用的updateById的对照标记打印了 —— 静默因此是证据而不是坏探针。Generated by Claude Code