refactor(objectql): update 的三分支派发抽成生产者侧唯一判定 + 门禁 update 切片 (#5480) - #5754
Merged
Conversation
…producer-side predicate (#5480) `delete` has had one since #4550 (`resolveEngineDeleteDispatch`), so any test double standing in for the engine can import the producer's own decision and cannot be looser than it. `update`'s identical three-way dispatch was an inline literal throw in `engine.ts` with no exported constant and no reusable function — so #5393 could pin its fake's `delete` to the producer and structurally could not do the same for `update`, on a verb whose predicate branch rewrites every matching row's fields. - new `packages/objectql/src/engine-update-dispatch.ts`: `resolveEngineUpdateDispatch` / `assertEngineUpdateDispatch` / `scalarUpdateId` / `ENGINE_UPDATE_REJECT_MESSAGE` / `ENGINE_UPDATE_DISPATCH_CASES`, all exported from `@objectstack/objectql`. - `ObjectQL.update` now uses it — producer and predicate are one copy. Behaviour-preserving: the three branches, the `$in` predicate test and the reject message text are unchanged. Two places where update is not delete are copied verbatim rather than "improved", and documented as such: `data.id` is taken without a scalar test and outranks both `where` and `multi` (objectstack#5748), and the branch reads truthiness, so `where: { id: 0 }` is not an identifying call. `engine-update-dispatch.test.ts` drives the REAL engine with a recording driver over the case set and asserts observed behaviour equals the predicate's verdict case by case — verified non-vacuous by perturbing the engine's own branch, which turns the `real ObjectQL.update` half red while the predicate half stays green. check-engine-double-contract grows an `update` slice on the same criterion: ledger entries are now keyed on (file, verb) with a required `verb` and a new DECLARED invariant, the driver-only-member veto applies at every arity (a fake driver spelling its key `_i` had 19 of its doubles read as engine doubles), and the self-test covers the update slice on both sides of every decision including cross-slice credit. Measured: delete unchanged at 108 doubles / 91 files / 27 pinned; update 126 doubles / 110 files, 10 pinned (objectql's own fakes), 116 in the shrink-only ledger. Fixes #5480 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ate-dispatch-predicate
… was measured (#5480) Base crossing, not a regression on either side. `packages/services/service-automation/src/plugin-startup-log-cause.test.ts` arrived on main in #5738 (from #5661) after this branch measured the update slice and while main still had no update slice at all — so its own CI could not have flagged its fake engine's `update`, and this ledger could not have recorded it. It becomes a violation only on the merge ref, where the new slice and the new file meet for the first time. Re-measured on the merge with origin/main rather than transcribed: 1 unguarded engine double at line 170. The package already carries @objectstack/objectql in devDependencies, so the pin is one line, but the file is under `packages/services/**` — outside this PR's scope — so it goes to the services lane's batch alongside the `crud-bulk-intent.test.ts` upgrade step 3 leaves it. Like every other update entry, it claims no dormancy probe. Verified on the merged tree: engine-double-contract OK (self-test + run), delete unchanged at 108 doubles / 91 files / 27 pinned with zero RECONCILED, update 127 / 111 with 10 pinned and 117 ledgered; query-options-erasure ratchet holds; check-nul-bytes clean; @objectstack/objectql typecheck clean and 124 files / 2044 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
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 free
to 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 #5480
范围:issue 的第 1、2 步。第 3 步(service-automation 测试升级)不在本单 ——
packages/services/**属 services 车道,本单落地后依赖已就位、零阻塞,该车道按 #5393 的既有线索自行升级即可(crud-bulk-intent.test.ts的文件头那段「update 没有共享判定,所以只断言 options 包」的说明,届时连同一起改写)。前提复核(先证再做)
issue 的两条前提在
origin/main @ 488b66c上都还成立,实测:packages/objectql/src/engine.ts:5682仍是内联字面量throw new Error('Update requires an ID or options.multi=true'),全仓没有对应的导出常量或可复用函数;scripts/check-engine-double-contract.mjs的文件头仍把 "update's twin dispatch" 列在「刻意不覆盖」里,理由写明是「等生产者侧判定先被抽出」。全仓 grep 该消息文本:只有 1 处 throw + 5 处注释/文档散文 +
packages/lint里的一条dispatchNote字符串,没有任何测试断言它。所以把文本搬进导出常量是零风险的,措辞也一字未改。1. 生产者侧判定
新增
packages/objectql/src/engine-update-dispatch.ts,形状照抄engine-delete-dispatch.ts:resolveEngineUpdateDispatch/assertEngineUpdateDispatch/scalarUpdateId/ENGINE_UPDATE_REJECT_MESSAGE/ENGINE_UPDATE_DISPATCH_CASES,全部从@objectstack/objectql导出。ObjectQL.update自身改用它 —— 生产者与判定是同一份,否则只是第二份副本。行为保持:三分支语义、
$in谓词判定、拒绝消息一字未改。有两处 update 与 delete 不同,判定里**照抄而非「改良」**了生产者,并在模块头和测试里写死:data.id不做标量测试,为真即当 id,且优先于where与multi;if (hookContext.input.id))而非!== undefined,所以where: { id: 0 }不走按 id 路径。比生产者更「聪明」的判定就是第二份意见 —— 正是 #4550 要消除的东西。第 1 点本身是缺陷,已单独归档为 #5748;修的时候两个文件一起翻面,现在那是一次编辑。
测试不是对照表,是对照真实引擎
engine-update-dispatch.test.ts用记录型 driver 驱动真实引擎跑完 17 条用例,逐例断言引擎实际行为 == 判定裁决。反向验证(方向先定后跑):抽取之后
observeEngine走的是同一个判定,所以「引擎同意判定」有变成同义反复的风险。真正独立的一半是分支本身(if (hookContext.input.id)与那条options?.multi且driver.updateMany的判断),它不经过判定。所以预定方向是:扰动引擎分支,判定那一半应保持绿、real ObjectQL.update那一半应变红。实测把该分支条件里的options?.multi一项去掉,只留driver.updateMany:8 条 reject 用例全红,且红的都是
real ObjectQL.update断言,predicate断言一条没红 —— 与预测一致。已还原。2. 门禁 update 切片
SLICES化:一个切片 = 「看替身的哪个成员」+「该成员必须够到哪个生产者判定」,其余(引擎/驱动归属、一层 helper 的间接、账本、双向对账)一份实现服务两个动词。基线键从
file变成(file, verb),verb是必填、不默认 —— 继承来的动词等于一条谁也读不出范围的豁免,而两个动词确有实质差别(data.id)。为此加了DECLARED不变量:条目的verb必须是本脚本真的会扫的切片,否则它永远对不上任何东西、变成退不掉的豁免。已负向验证(把某条改成"updat"→ 门禁 exit 1 并指名该文件)。一处判据修正,不是为 update 特设:driver-only 成员的否决改为在任意 arity 生效。原先只在形参少于 2 个时才查,
delete侧侥幸没事(驱动的 delete 第二参数这仓一贯写作id),update侧则不然 ——plugin.integration.test.ts的假驱动把 update 写成三参数箭头函数(_o: string, _i: any, d: any),_i就是主键只是没拼成id,于是一个文件里 19 个驱动替身被当成引擎替身。账本里混进误报比账本窄危害更大。实测:delete 切片一条不变(108 doubles / 91 files / 27 pinned,零 RECONCILED),因为 delete 的替身本就没有 driver-only 成员。自测扩面并已证明能红:切片隔离、假驱动的 update、零参 update 的两半证据、
update不把自己算作 sibling、objectql 自身的相对路径 import。其中最关键的一条是「在 update 体内调用 delete 的判定不算 pin」—— 第一版没有这条,我把pinnedImportsOf扰动成跨切片记账,自测没有红;补上这条 fixture 后同样的扰动立刻红:基线按实测填,没有
--fix式生成:每条的unguarded与行号都是门禁自己打印出来的那一行解析来的;每个包的 devDependency 路线是本分支上真跑turbo run build --filter=... --dry量的(platform-objects 得到WARNING Circular package dependency detected: @objectstack/metadata, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/platform-objects,service-job / plugin-reports / plugin-security / plugin-webhooks / service-datasource 无环警告),量完即还原;delete 侧账本已量过的包直接引用不重量。每条why都写明它不主张什么:与 #5629 的 delete 批次不同,这批没有逐文件的休眠探针,所以没有一条说「这处宽松未被走到」。顺带把 objectql 自己的 10 个假引擎 update 钉上了(相对路径 import,本包内)—— 其中 4 个文件本就钉了 delete,「一个动词绑得上、另一个绑不上」正是本单的由来。全部套件绿,没有暴露出第二个 #4434。
追加一条:合流后的 base 穿越(commit 2)
首轮 CI 在 merge-ref 上红了一条,原因不在两侧任何一方:
packages/services/service-automation/src/plugin-startup-log-cause.test.ts由 #5738(来自 #5661)在本分支测量之后落进 main,而它落地时 main 还没有 update 切片 —— 所以它自己的 CI 不可能标记它,本账本也不可能记录它;新切片与新文件第一次相遇只发生在 merge-ref 上。处理:
git merge origin/main(未 rebase,零冲突),在合流后的树上重新实测并补一条 MEASURED DEBT 条目(line 170,1 个未钉替身)。该包已有@objectstack/objectqldevDependency,所以 pin 只是一行 —— 但文件在packages/services/**,不在本单范围,归 services 车道与第 3 步同批做。条目同样不带休眠探针主张。验证(合流后重跑的实测输出)
delete 侧在合流前后都是 108 / 91 / 27 且零 RECONCILED —— 本 PR 的判据修正没有让 delete 门禁的覆盖面缩水一格。另加自查(门禁扫不到的控制字符):
grep -naP配[\x00-\x08\x0b\x0c\x0e-\x1f]覆盖本 PR 全部改动文件,无命中。范围外发现(PD #10,已归档,本 PR 不修)
where: { id: 0 }判定答 by-id,引擎却 reject #5747 — delete 侧的共享判定与ObjectQL.delete在假值标量 id 上不一致:resolveEngineDeleteDispatch({ where: { id: 0 } })答by-id,引擎却 reject(空字符串 id 同)。真实引擎实测两例。也就是说按assertEngineDeleteDispatch钉死的替身在这一输入上仍比生产者宽松 —— sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434 的形状发生在防线内部,而ENGINE_DELETE_DISPATCH_CASES里没有假值 id 用例,逐例对照够不到它。update 侧本 PR 已补齐两例。data.id不做标量测试 —— 载荷里的算子对象被当成主键绑定,且盖过显式options.multi: true#5748 —ObjectQL.update的data.id不做标量测试:update(o, { id: { $in: [...] } }, { multi: true })走按 id 分支,算子对象被当主键绑进driver.update,显式multi: true被无声吞掉。实测确认(记录到的 driver 调用是update而非updateMany)。本 PR 按「行为保持」原样保留并写死为断言,修法留给该单裁定。🤖 Generated with Claude Code
https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx