feat(spec,automation): flow 的 update_record / delete_record 可以声明批量意图 multi - #5485
Merged
Conversation
…record (#5393) `UpdateRecordConfigSchema` / `DeleteRecordConfigSchema` are strictObjects and neither declared any spelling of bulk intent, while the CRUD executors never passed `options.multi`. The data engine accepts a write only when `where.id` is a scalar or `options.multi` is truthy and throws otherwise, so a predicate update/delete was unreachable from any flow in any app — while the node descriptors advertised "Delete records matching a filter." Declared != enforced (PD #10); #5225's showcase sweep flow was the site it surfaced at. Adds `multi` (boolean, default false) to both config contracts and wires the executors to forward it as `options.multi`. One name for one concept (PD #12): `multi` is the engine's own word for it, so the concept is greppable from node config to driver call. The engine's rejection path is untouched — refusing an undeclared predicate write IS the contract. - spec: `multi` on both schemas, with `bulk`/`all`/`multiple` prescriptions and a wrong-layer answer for `options: { multi: true }` (edit distance reaches `multi` from none of them); the same curation is copied to the registration door in service-automation's engine, per #4001's finding that detection generalizes for free while prose does not. - descriptors: `multi` declared on both designer forms, so the form<->Zod ledger reconciles and the key is authorable in Studio, not only by hand. - tests: `crud-bulk-intent.test.ts`, whose delete double opens with `assertEngineDeleteDispatch` from @objectstack/objectql — the producer's own predicate — so it cannot accept a call the real engine refuses. That is the #5197 blind spot closed for this file: `run-summary.test.ts`'s zero-parameter `async delete() { return false }` accepts predicate deletes AND is invisible to check-engine-double-contract's arity test. Needed a devDependency on @objectstack/objectql (acyclic — objectql's closure does not contain service-automation); the eight service-automation baseline entries citing that missing dep as their blocker are updated to say so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 5, 2026
Closed
os-zhuang
marked this pull request as ready for review
August 5, 2026 14:28
Contributor
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31015413774 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Contributor
Author
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…te node (objectstack-ai#5225) (objectstack-ai#5534) `InquiryPurgeFlow`'s `purge` node deletes by the predicate `{ status: 'closed' }` but declared no bulk intent. The data engine accepts a write without `options.multi` only when `filter` names ONE row by a scalar `id`, so every run of this flow failed on that node: Node 'purge' failed: delete_record(showcase_inquiry) failed: Delete requires an ID or options.multi=true with `acted: 0` — identically on both paths, the declarative endpoint `POST /api/v1/apps/showcase/inquiries/purge` and the built-in trigger route `POST /api/v1/automation/showcase_inquiry_purge/trigger`. The delete half of the CRUD quartet `src/coverage.ts` claims this flow demonstrates had therefore never executed once (declared != enforced, PD objectstack-ai#10); objectstack-ai#5112's boot probes are what finally reached it. The fix is a DECLARATION, not a rewrite. Until objectstack-ai#5393 (PR objectstack-ai#5485) no spelling of bulk intent existed on the node config at all, which is why the third triage round correctly refused a get-then-loop-then-delete-by-id rewrite as a PD objectstack-ai#5 workaround and escalated instead. `filter` stays: `multi: true` with an absent or empty filter is a declared whole-object delete, and this node is meant to be objectstack-ai#5482's zero-warning sample for exactly that distinction. Verified on a real `--fresh` boot, both probes, with row counts: endpoint selected 2 -> purge acted 2, success true, 4 rows -> 2 (both `closed` rows gone, both non-closed survived) trigger selected 3 -> purge acted 3, success true, 5 rows -> 2 Reverse-verified by stripping the declaration and rebooting: both probes returned to the byte-identical original failure above with `acted: 0` and the row count unchanged. The new example test states the rule as a two-sided invariant over EVERY `delete_record` / `update_record` node rather than asserting one node, and walks ADR-0031 structured containers — `showcase_task_crm_sync`'s `catch` region holds an `update_record` a flat scan of `flow.nodes` misses. Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh Co-authored-by: Claude <noreply@anthropic.com>
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 #5393
前提复核(rule 6,对 origin/main @ ed0d2aa)
issue 的三条事实全部成立,逐条实测:
DeleteRecordConfigSchema/UpdateRecordConfigSchema(packages/spec/src/automation/builtin-node-config.zod.ts)是strictObject,前者只声明objectName/filter,后者只多一个fields—— 任何批量意图拼法都被unrecognized_keys拒。packages/services/service-automation/src/builtin/crud-nodes.ts的两个执行器调用data.update/data.delete时不传options.multi。resolveEngineDeleteDispatch(标量where.id→ 按 id;否则options.multi→deleteMany;否则抛Delete requires an ID or options.multi=true),update 侧在engine.ts有同款内联 throw。所以谓词批量写从任何 app 的任何 flow 都不可达,而节点描述符写着
Delete Records/Delete records matching a filter.—— declared ≠ enforced(PD #10)。按在册裁定 A 实现。改了什么
spec —— 两个 config 各声明一个同名键
multi(z.boolean().optional(),默认 false):false:执行器传multi: false,写入必须以标量id点名一行,谓词(含id: { $in: [...] })由引擎拒绝。拒绝即契约,不是要绕过的缺陷。true:执行器传options.multi: true,落到driver.updateMany/deleteMany,步骤的acted指标报出命中行数。EngineUpdateOptions.multi/EngineDeleteOptions.multi)—— 一概念一名(PD Add comprehensive test suite for Zod schema validation #12),从节点 config 到 driver 调用全程可 grep,不发明第二个词。guidance(#4001 纪律,两道门都写) ——
bulk/all/multiple拿到点名处方,options: { multi: true }被当作写错层回答(那是引擎的 options 包,不是节点 config)。这四个拼法是 #5225 诊断期真喂过safeParse的,编辑距离从它们中的任何一个都够不到multi,没有这些条目拒绝就只会报个键名。同一份处方复制到 service-automation 引擎的注册门(FLOW_NODE_UNKNOWN_KEY_GUIDANCE),因为注册是作者在 boot 时撞到的第一道门,而 #4001 的发现正是:检测面会随默认值翻转自动泛化,处方不会。描述符表单 —— 两个节点的
configSchema各加multi,所以 form ↔ Zod 台账对得上,键在 Studio 里可授权,而不是只能手写元数据。执行器 ——
multi: cfg.multi === true在每次调用上都写出来,而不是 true 时才 spread:multi: false是让引擎拒绝谓词写的那一半契约,调用点的读者应该直接看见问的是哪一半,而不是从一个缺席的键推断。packages/objectql零改动。#3810 的抹除守卫零改动,并新增用例钉住它:声明multi不会解除它。测试
新增
packages/services/service-automation/src/builtin/crud-bulk-intent.test.ts(10 例)。它的 delete 替身以assertEngineDeleteDispatch(从@objectstack/objectqlimport 的生产者自己的判定)开头,所以在任何输入上都不可能比真引擎松 —— 包括手抄守卫必漏的id: { $in: [...] }(看着像 id,其实是谓词)。check-engine-double-contract已把该文件认作pinned。这正是 issue 点名的 #5197 盲点:
run-summary.test.ts的内联async delete() { return false; }既接受真引擎拒绝的谓词删除,又因为零参而躲过门禁的 arity 判定(isEngineDeleteShape要求引擎的(object, options)形状),连被发现都做不到。为此给
@objectstack/service-automation加了@objectstack/objectqldevDependency —— 无环:objectql 的传递依赖闭包(12 包)不含 service-automation,turbo run build --filter=@objectstack/service-automation --dry正常解析。scripts/engine-double-contract.baseline.json里 8 条 service-automation 条目的why原本写着「不依赖 objectql,加依赖是另一次可评审动作」—— 那句话被本 PR 变成假的,所以按 plugin-auth 的 #3585 先例把它们改成 MEASURED 记述,并给crud-filter-guard.test.ts多留一句:它有几条 #3810 fixture 断言谓词删除成功,真引擎会拒;#5393 之后那是可表达的,所以将来 pin 它时那些 fixture 应该补multi: true,而不是删掉。update 侧刻意不 pin,并在文件头写明原因:objectql 没有导出 update 的判定函数(
engine.ts里是内联 throw),门禁自己的文件头也把 "update's twin dispatch" 列为待抽取后再覆盖的切片。所以 update 用例只断言执行器交给引擎的 options 包——执行器的全部义务——并明确不对引擎会不会接受它发表第二份意见;在 fake 里手抄一份 update 判定,恰恰是那道门禁存在的理由。已另开 issue 记录。反向验证(方向是先判定后跑的:红)
预判:把执行器的
multi接线撤掉,multi: true的用例应当变红,因为替身钉在生产者判定上,谓词删除会拿到引擎真实的拒绝。实测撤线后10 例中 7 例红:三例仍绿,各有诚实的理由,其中一条据此改强了:「
multi: false写出来与不写是同一个拒绝」原本只断言拒绝本身,而拒绝在接线前后都成立 —— 它会穿过本文件存在的意义所在的那次 revert 保持绿。已补上对 options 包的断言。另两条(#3810 守卫、描述符表单)本就与执行器接线无关,绿在两边是设计使然,不是空断言。命令与真实输出
消费半径按规则的调用方扫过,不是按被改的包:CRUD node config 被 cli(validate/lint/migrate)、lint、metadata-protocol(canonicalization)、studio flow-builder 一并消费,全部跑过。cli 首轮 30 红,原因是新 worktree 里
@objectstack/setup等依赖没构建(Failed to resolve entry for package),pnpm --filter '@objectstack/cli^...' build后 79/79 全绿 —— 与本改动无关。门禁:
生成物随行:
authorable-surface.json(+2 键)、content/docs/references/automation/builtin-node-config.mdx(+2 行),两者都由gen:schema/gen:docs整体重生成,未手改。strictness 台账重生成后无变化(往已 strict 的 shape 上加键不动计数)。两处刻意留下的行为,写在这里而不是留给下一个读者
multi: true且没有filter= 按声明清空整个对象。 引擎的派发表本就把「multi 且完全没有谓词」列为合法,Flow node filters silently blank date macros: the template engine consumes{…}before the query engine sees it #3810 的守卫按「作者写过的条件被抹掉」判定而不是按「filter 为空」判定(那是它注释里的原话,也有专门用例钉住)。flow 的delete_record/update_record无法表达批量意图 —— 节点 schema 无键、执行器不传options.multi,谓词批量写对所有 flow 平台级不可达,而节点描述符宣称支持 #5393 之前这条路径不可达,现在可达了,但它是显式、可 grep 的声明。是否要在 authoring 期加告警,已另开 issue 交 PM 定级,不在本 PR 内发明。update_record的破坏性并不比delete_record低多少(谓词 update 覆盖整表字段),但它的测试替身在结构上不能被绑到生产者契约。同上,另开 issue。范围外发现(PD #10,均已建单、未指派)
resolveEngineDeleteDispatch,update 的同款三分支只是 engine.ts 里的一个内联 throw #5480 —— objectql 的 UPDATE dispatch 没有共享判定函数;delete 有engine-delete-dispatch.ts,update 只是engine.ts里的内联 throw,门禁的 update 切片因此无从建立。multi: true且filter为空的 delete_record / update_record 是「按声明清空整个对象」,authoring 期零诊断 —— #3810 的守卫按「条件被抹掉」判定,不按「条件为空」判定 #5482 ——multi: true+ 空filter在 authoring 期零诊断(上面第 1 点)。后续
#5225(showcase 清扫流)挂 Blocked-by 本单,本单落地后由它以声明批量意图收尾。示例目录本 PR 未动。
Generated by Claude Code