发现于 #5393 (flow update_record / delete_record 补 multi 批量意图键)的实现过程,PR 见该单。属 PD #10 的范围外发现,不在 #5393 内修(#5393 的 scope guard 明写 packages/objectql 零改动)。
事实(origin/main)
delete 的派发决策已经被抽成生产者侧的唯一判定,任何测试替身都能 import 它:
packages/objectql/src/engine-delete-dispatch.ts —— resolveEngineDeleteDispatch / assertEngineDeleteDispatch / scalarDeleteId / ENGINE_DELETE_REJECT_MESSAGE / ENGINE_DELETE_DISPATCH_CASES,全部从 @objectstack/objectql 导出;
scripts/check-engine-double-contract.mjs 以「fake 的 delete 是否路由该判定」为门禁,当前 23 pinned / 32 DEBT / 1 exempt。
update 的同款三分支没有对应物:
packages/objectql/src/engine.ts 的 update 路径里,标量 where.id → 按 id;否则 options.multi → driver.updateMany;否则 throw new Error('Update requires an ID or options.multi=true') —— 这条 throw 是内联字面量 ,既没有导出的常量,也没有可复用的判定函数。
check-engine-double-contract.mjs 的文件头把这一条列为刻意不覆盖 :"every other method a fake engine offers (find filter semantics, update's twin dispatch , unknown-option rejection). Same family, but each needs its own producer-side predicate extracted first. delete had one available because sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434 already paid for it."
为什么现在值得记
#5393 在 service-automation 里补真实契约测试时,delete 侧可以用 assertEngineDeleteDispatch 把假引擎钉死在生产者契约上(该文件已被门禁认作 pinned);update 侧只能退而断言执行器交给引擎的 options 包 ,并在文件头写明「不对引擎会不会接受它发表第二份意见」—— 因为唯一的替代做法是在 fake 里手抄一遍 update 的判定,而那正是 #4434 / #4550 记录的、门禁存在的理由(手抄必然漏掉 where: { id: { $in: [...] } } 看着像 id 其实是谓词这一半)。
也就是说:同一个执行器的两个写入动词,一个能被绑定到生产者契约,另一个结构上不能 ,而 update_record 的破坏性并不比 delete_record 低多少(谓词 update 覆盖整表字段)。
建议动作
按 engine-delete-dispatch.ts 的形状抽出 engine-update-dispatch.ts:resolveEngineUpdateDispatch / assertEngineUpdateDispatch / ENGINE_UPDATE_REJECT_MESSAGE,让 ObjectQL.update 自身改用它(生产者与判定必须是同一份,否则又是第二份副本);
check-engine-double-contract.mjs 增加 update 切片,基线按实测填(不要 --fix 式生成);
顺带可关掉 flow 的 delete_record / update_record 无法表达批量意图 —— 节点 schema 无键、执行器不传 options.multi,谓词批量写对所有 flow 平台级不可达,而节点描述符宣称支持 #5393 留下的这条不对称:packages/services/service-automation/src/builtin/crud-bulk-intent.test.ts 的 update 用例可以升级为同样 pinned。
注:@objectstack/objectql 已在 #5393 中加入 @objectstack/service-automation 的 devDependencies(无环:objectql 的传递依赖闭包 12 个包不含 service-automation),所以第 3 步不再需要额外的依赖变更。这一点也已回填到 scripts/engine-double-contract.baseline.json 里那 8 条 service-automation 条目的 why / closes。
关联:#5393 (本发现来源)、#5197 (检测器盲点:零参 async delete() 连发现都发现不了)、#4550 (门禁本体)、#4434 (家族起源)、#4987 (metadata-protocol 侧的成环阻塞,与本单无关但同族)。
发现于 #5393(flow
update_record/delete_record补multi批量意图键)的实现过程,PR 见该单。属 PD #10 的范围外发现,不在 #5393 内修(#5393 的 scope guard 明写packages/objectql零改动)。事实(origin/main)
delete的派发决策已经被抽成生产者侧的唯一判定,任何测试替身都能 import 它:packages/objectql/src/engine-delete-dispatch.ts——resolveEngineDeleteDispatch/assertEngineDeleteDispatch/scalarDeleteId/ENGINE_DELETE_REJECT_MESSAGE/ENGINE_DELETE_DISPATCH_CASES,全部从@objectstack/objectql导出;scripts/check-engine-double-contract.mjs以「fake 的delete是否路由该判定」为门禁,当前 23 pinned / 32 DEBT / 1 exempt。update的同款三分支没有对应物:packages/objectql/src/engine.ts的 update 路径里,标量where.id→ 按 id;否则options.multi→driver.updateMany;否则throw new Error('Update requires an ID or options.multi=true')—— 这条 throw 是内联字面量,既没有导出的常量,也没有可复用的判定函数。check-engine-double-contract.mjs的文件头把这一条列为刻意不覆盖:"every other method a fake engine offers (findfilter semantics, update's twin dispatch, unknown-option rejection). Same family, but each needs its own producer-side predicate extracted first.deletehad one available because sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434 already paid for it."为什么现在值得记
#5393 在 service-automation 里补真实契约测试时,delete 侧可以用
assertEngineDeleteDispatch把假引擎钉死在生产者契约上(该文件已被门禁认作 pinned);update 侧只能退而断言执行器交给引擎的 options 包,并在文件头写明「不对引擎会不会接受它发表第二份意见」—— 因为唯一的替代做法是在 fake 里手抄一遍 update 的判定,而那正是 #4434 / #4550 记录的、门禁存在的理由(手抄必然漏掉where: { id: { $in: [...] } }看着像 id 其实是谓词这一半)。也就是说:同一个执行器的两个写入动词,一个能被绑定到生产者契约,另一个结构上不能,而
update_record的破坏性并不比delete_record低多少(谓词 update 覆盖整表字段)。建议动作
engine-delete-dispatch.ts的形状抽出engine-update-dispatch.ts:resolveEngineUpdateDispatch/assertEngineUpdateDispatch/ENGINE_UPDATE_REJECT_MESSAGE,让ObjectQL.update自身改用它(生产者与判定必须是同一份,否则又是第二份副本);check-engine-double-contract.mjs增加 update 切片,基线按实测填(不要--fix式生成);delete_record/update_record无法表达批量意图 —— 节点 schema 无键、执行器不传options.multi,谓词批量写对所有 flow 平台级不可达,而节点描述符宣称支持 #5393 留下的这条不对称:packages/services/service-automation/src/builtin/crud-bulk-intent.test.ts的 update 用例可以升级为同样 pinned。注:
@objectstack/objectql已在 #5393 中加入@objectstack/service-automation的 devDependencies(无环:objectql 的传递依赖闭包 12 个包不含 service-automation),所以第 3 步不再需要额外的依赖变更。这一点也已回填到scripts/engine-double-contract.baseline.json里那 8 条 service-automation 条目的why/closes。关联:#5393(本发现来源)、#5197(检测器盲点:零参
async delete()连发现都发现不了)、#4550(门禁本体)、#4434(家族起源)、#4987(metadata-protocol 侧的成环阻塞,与本单无关但同族)。