Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): give check-engine-double-contract a scoped-repository arm - #6496
Merged
Conversation
engine/driver 二分法少了一支。#5945 引入的 `IScopedObjectRepository` 绑定在 单个对象上,写动词里根本没有对象名(`update(data, options?)`),但它的成员集 含四个 ENGINE_SIBLINGS、且 `insert` 在 ENGINE_ONLY_MEMBERS 里 —— 于是每一个 该接口的类型符合性见证都被归到 engine 侧。对扫描能看见的信息而言准确,对那个 对象究竟是什么而言错误。 而 spec 原则上无法路由 `assertEngineUpdateDispatch`(谓词的两个家都依赖 spec,import 会反转依赖),所以此后每个见证只能手写一条 EXEMPT —— ledger 会 越长越像噪音,而 ledger 的可读性正是这道门的价值。 判据用与 DRIVER_ONLY_MEMBERS 完全同构的证据结构:REPOSITORY_ONLY_MEMBERS (`updateById` / `deleteById` —— 逐个核对过,IDataEngine、IDataDriver、 ObjectQL 类都没有这两个名字),并与「写动词第一个参数不是对象名」合取。两半 各有职责:成员是「这是 repository」的证据,参数测试防止成员本身变成豁免令牌 (`update(objectName, …)` 的 fake 无论挂多少 by-id 便利方法都留在范围内)。 `takesObjectNameFirst` 只给正面证据,读不出来时返回 false —— 该方向只会多留 一条 ledger,反方向才会把真 engine double 挪出扫描范围。 先测量后落笔:250 个已发现 double(82 个 pinned)里判据只移动 2 个,恰好是 #5945 的两个见证,0 个 pinned 受影响。仅按声明类型(方向 2)只覆盖 1/2 —— hook.test.ts 的 repository 没有标注。仅按参数(无成员要求)会扫到 3 个,但要 靠参数名白名单,而本仓 `o` 本身歧义(12 个 double 里 `o: string` 是对象名, action-body-identity 的 `o?: any` 是 options)——那一个无标记的 scoped facade 因此留在 ledger 里,脚本头已写明为「deliberately not covered」。 两条 scoped 见证的 EXEMPT 随之删除(它们自己的 `closes` 就预告了这一点: "permanent, unless this gate grows a scoped-repository arm")。data-engine.test.ts 的两条 EXEMPT 原样保留 —— 它们是 IDataEngine 见证,对象名在第一位、无 repository 成员。update slice 135→133,delete slice 115 不变,pinned 82 不变。 自测新增 A/B 对照(同一个字面量加减 `updateById`)与一个混合 fixture 的精确 集合断言,因为「不再被报告」在扫描彻底失灵时会空过。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
hotlong
marked this pull request as ready for review
August 8, 2026 02:26
hotlong
enabled auto-merge
August 8, 2026 02:26
hotlong
marked this pull request as draft
August 8, 2026 02:37
hotlong
marked this pull request as ready for review
August 8, 2026 02:42
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6327
scripts/check-engine-double-contract.mjs的归属判据一直是二分的:一个字面量要么是 engine double,要么是 driver double。#5945 引入的IScopedObjectRepository是第三种数据访问形状,这道门没有它的那一支 —— 于是该接口的每一个类型符合性见证都被判成 engine double,只能靠手写 EXEMPT 出场。本 PR 加上第三支。一、立单前提的复核(在
origin/main上重测,未引用卡片)三者的写动词并排,逐条对着源文件核过:
IDataEngineupdate(objectName, data, options?)IDataDriverupdate(object, id, data, options?)IScopedObjectRepositoryupdate(data, options?)packages/spec/src/contracts/data-engine.ts:138/data-driver.ts:179/contracts/scoped-context.ts:160。find/findOne/count/insert/update/updateById—— 四个ENGINE_SIBLINGS,且insert在ENGINE_ONLY_MEMBERS里。所以isEngineVerbShape在params.length不足 2 的分支上拿insert当 engine 证据,把见证归到 engine 侧。对扫描能看见的信息而言准确,对那个对象究竟是什么而言错误 —— 立单正文的这句话成立。assertEngineUpdateDispatch」也成立:谓词在@objectstack/objectql/@objectstack/metadata-core,两者都依赖@objectstack/spec,import 会反转依赖。所以此后每个实现或见证都只能再手写一条 EXEMPT。改门前的门是绿的:
OK — 82 pinned, 133 in the DEBT ledger, 4 exempt。这不是一道报红的门,是一道把正确代码判成债务的门 —— 代价落在 ledger 的可读性上,而它的$comment明写 shrink-only、手工评审。二、走了哪条方向,以及数据为什么这么说
取方向 1(形状归属支),但两半合取,并且成员那一半是必需的。 判据与
DRIVER_ONLY_MEMBERS完全同构 —— 新增REPOSITORY_ONLY_MEMBERS,成员为updateById与deleteById:放在 DRIVER veto 之后、
ENGINE_ONLY_MEMBERS读insert之前,任何 arity 都判 —— 和 driver veto 同一个优先级理由。命中即出扫描范围(像 driver double 那样),不进 ledger。选名字不是拍脑袋:
updateById/deleteById逐个核对过 ——IDataEngine没有任何 by-id 形式,IDataDriver用参数里的id加bulkUpdate/updateMany/bulkDelete/deleteMany,而packages/objectql/src/engine.ts里这两个名字的唯一声明处是class ObjectRepository implements IScopedObjectRepository(7831 / 7846 行)。repository 的create/upsert/delete/aggregate/execute故意不收:每一个都同时在 driver 或 engine 上,分不开任何东西。为什么不取方向 2(按声明类型):测下来它只覆盖一半。250 个已发现 double 里带
: IScopedObjectRepository标注的只有 1 个;packages/spec/src/data/hook.test.ts:1176的 repository 是object(name)返回的字面量,没有任何标注,方向 2 看不见它 —— 那条 EXEMPT 会永远留在 ledger 里。判据的覆盖面取决于作者当时有没有顺手写标注,这不是判据。为什么不取方向 3(维持现状):见上面的成本论证,且方向 1 的测量结果足够干净(下一节),没有理由把它记进「deliberately not covered」。
为什么两半必须合取:
updateById就能把它悄悄移出这道门。参数测试挡住这个:update(objectName, data, opts)拿了 engine 拿对象名的位置,就留在范围内。o本身歧义:12 个已发现 double 里o: string是对象名,而packages/runtime/src/action-body-identity.test.ts:71的o?: any是 options 包。猜错的方向是把真 engine double 挪出扫描范围,那是这道门唯一无法自报的错误。takesObjectNameFirst因此只给正面证据:读不出来或没有第一个参数一律返回false,绝不返回true。这个不对称是安全性质本身 —— 它的true是「留在范围内」,宽松只会多留一条 ledger;宽松地给false才会造成假阴性。三、判据的分辨力测量(先测量,后落笔)
对全语料 250 个已发现 double(115 delete + 135 update,其中 82 个 pinned) 逐个打分:
updateById或deleteById)移动的两个恰好是 #5945 的两个见证,没有一个 pinned double 被影响,没有一个真 engine double 受影响。语料库范围内把
updateById/deleteById声明为成员的测试文件也只有这两个。故意没有覆盖到的一个,写进了脚本头:
packages/runtime/src/action-body-identity.test.ts:71是一个真的 scoped facade(createContext().object(name)返回的对象),但它只写了find/count/insert/update/delete,没有 repository 专属成员,所以留在 ledger 里,条目数不变(该文件两个 verb 各unguarded: 2,未动)。要看见它就得读参数名,而o恰恰在本仓两头都用 —— 用一条 ledger 换假阴性风险不划算,这个取舍已写在脚本头的「deliberately NOT covered」里,而不是留给下一个读者去踩。四、两条现有 EXEMPT 的去向(前后实测)
packages/spec/src/contracts/scoped-context.test.ts、packages/spec/src/data/hook.test.ts。删除是 RECONCILED 逼出来的 —— 改完门后主跑直接报「baseline entry ... which declares no engine double with a update any more」。两条条目自己的closes早就预告了这一步:「permanent, unless this gate grows a scoped-repository arm (then this witness becomes out of scope rather than exempt)」。packages/spec/src/contracts/data-engine.test.ts的两条(delete / update,各unguarded: 5)。它们是IDataEngine见证 —— 对象名在第一位、不含任何 repository 专属成员,新判据碰不到它们,实测计数 5 未变。packages/spec/src/contracts/scoped-context.ts也未动(那是 [spec]HookContext.api声明为z.unknown()—— 按(ctx: HookContext)标类型的 hook 无法调用ctx.api.object(…),而文档/技能全在这么教 #5945 的面)。五、反向验证(先声明方向,再跑)
只回退 veto 本身(保留新自测 fixture、保留已删的两条 baseline),声明在前:
预期变红:4 条新断言 + 主跑 2 个 PINNED 错误。预期保持绿:A/B 对照「同一字面量去掉
updateById仍被发现」与「带updateById的 engine double 仍在范围内」,以及全部既有断言。实测完全一致:
两条对照断言均未出现在失败列表里 —— 这正是要点。「不再被报告」在扫描彻底失灵时会空过,所以新增断言全部配了正面对照:
updateById(带标记 ⇒ 出范围;去掉 ⇒ 仍被发现 1 个)。不是「另找一个对照」,而是同一个对象拿掉证据,这是唯一能区分「veto 生效」和「发现能力死了」的写法。六、门
pnpm lint(ESLint)pnpm check:engine-double-contract(CI 的原命令,含--self-test)OK — 82 pinned, 133 in the DEBT ledger, 2 exemptpnpm check:nul-bytespnpm exec turbo run typecheck125 successful, 125 totalgrep -naP,越过门的盲区)skip-changeset在其重读之前落地)scripts/不在任何 tsconfig 的编译范围内,所以 typecheck 对本改动没有判别力 —— 仍然全量跑了一遍,只为证明没有连带回归,不当作本改动的证据。七、changeset
无。仓库内部门禁工具,无对外交付面,不发布任何包 ⇒ 已打
skip-changeset(读回确认:size/m+skip-changeset)。