feat(lint): multi: true 且无 filter 的 delete/update 在 authoring 期告警(#5482) - #5663
Merged
Conversation
…5482) `config: { objectName: 'lead', multi: true }` with no `filter` is a WHOLE-OBJECT write: the executor forwards `where: {}` plus the bulk intent, the engine classifies it as a legal `multi` call, and it lands on `deleteMany`/`updateMany` with no predicate. Reachable only since #5393 gave these nodes a bulk declaration, and silent ever since — the author's only feedback was the step's `acted` count, after the rows were gone. `flow-multi-write-unfiltered` says it at authoring time. A warning, not a gate: the engine's dispatch case-set lists "bulk intent, no predicate" as a valid call, so an explicit purge is an intent the platform grants — which is also why this is not a spec refine. Not a second copy of the #3810 run-time guard: that one refuses a node when a condition the author WROTE interpolated to nothing, and is deliberately keyed on "a written condition is gone" rather than "the filter is empty". The two judge different facts, and the diagnostic names the other one so they are not mistaken for one check. Reported at every nesting depth (#5383/#5635), because a loop-body sweep is the standard janitor shape. Empty combinator arrays are deliberately out of range: #5322/#5134 already ruled their identities, and deciding them here would be a fourth hand-written copy of a producer-side reduction.
`update_record` has no extracted dispatch module — only delete's is case-set-pinned (`engine-delete-dispatch.ts`). The message used delete's wording for both, which credited update with a case-set that does not exist. Each node type now carries its own authority: delete cites the case-set, update cites the `options.multi` branch (whose refusal fires only when the declaration is absent), and both tests assert their own.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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 #5482
按 2026-08-05 分诊裁决执行方案 1(authoring 期 warning);⛔ 未加 spec refine、未动 #3810 运行期守卫 /
crud-nodes.ts/ 引擎派发表 / #5651 的 time-relative 规则。前提核实(PD #6)
先在
origin/main(5b60b36)上实测,四种形状全部零诊断,与 issue 描述一致:filter与multi在两个 config schema 里各自 optional、彼此无约束(packages/spec/src/automation/builtin-node-config.zod.ts),lintFlowPatterns也没有任何针对破坏性节点 config 的检查。前提成立。改了什么
packages/lint/src/lint-flow-patterns.ts新增flow-multi-write-unfiltered(warning):delete_record/update_record且config.multi === true且 filter 无有效条件时告警,说明这是「按声明清空整个对象」,给出两条出路(补 filter 约束 / 确认整表意图属实——运行期照常允许)。规则 id 沿用 #5496 留的flow-{descriptor}-{verdict}家族前缀;barrel 补packages/lint/src/index.ts导出。lintFlowPatterns本来就挂在AUTHORING_RULES上,严重度缺省即 warning,所以os validate/os build/ runtime publish gate 三个面自动生效,无需新增注册项。诊断落在每个 graph 上(不只顶层),所以loop body 内的清扫节点天然命中——这正是定时 janitor 流的标准形状,也是 #5383/#5635 刚打开的那片区域。
「无有效条件」判定表
只判可证的两格,窄是刻意的:一条说「这是整表写」的告警必须说对。
filter键缺失{}(resolveNodeFilter(cfg.filter ?? {}, …))filter: {}driver-memory的 matcher 开篇即Object.keys(filter).length === 0 → return truefilter: { owner: '{record.ownr}' }filter: { status: 'closed' }multi/multi: false… requires an ID or options.multi=true)multi: 'true'(字符串)z.boolean(),parse 阶段就refuse,节点跑不起来filter非对象(字符串/数组)z.record(z.string(), z.unknown()),parseNodeConfig按名拒绝filter: { $and: [] }engine-delete-dispatch.ts被抽出来防的事故。已单独立 #5659filter: { $or: [] }/{ $not: {} }与 #3810 不重复:两者判不同的事
crud-nodes.ts的注释与crud-filter-guard.test.ts都明写那条守卫「deliberately keyed on 'a condition the author wrote is gone', not on 'the filter is empty'」,并专门钉住「an intentionally empty filter is still allowed」。所以两者互不覆盖:filter: { owner: '{record.ownr}' }在本规则处沉默、在那条守卫处被拒;完全没有filter的节点在本规则处告警、在那条守卫处——正确地——被放行。诊断文案点名对方,并说明各自判什么。crud-nodes.ts的 refusal 文案,而本单派发明确 ⛔ 不动该文件,故未做。若要补,是一行文案改动,请 PM 定夺(未另立 issue,以免为自家一行遗漏散单)。验证
先红后绿(方向预先声明:6 条正向用例应因「0 findings」变红,9 条 false-positive guard 断言的是「不报」,规则关掉时会空洞地绿——所以证明力全在正向那 6 条):
用例覆盖:delete/update 各一条、
filter: {}一条(并断言文案区分「no filter key」与「an EMPTY filter」)、#3810 互相点名断言、loop body 嵌套(scope 前缀flow 'campaign_enrollment' · loop 'loop_leads' body · node 'sweep' (delete_record))、两层嵌套 update、以及 8 组 false-positive guard。showcase 清扫流 A/B(真 before:revert 源码 → 重建 dist → 重跑),三个 example app 逐行对照:
67 条告警两侧完全一致,零新增。
并做了正向对照,免得「零新增」是空的:临时删掉 showcase 清扫流的
filter后重跑os validate,规则确实穿透到 CLI 面,且校验仍然 PASS(warning 不 gate):filter 放回后该告警消失(0 命中)。
范围外发现
flow-multi-write-unfiltered不判空组合子:filter: { $and: [] }+multi: true是整表删除,却零告警 —— 身份归约在 producer 侧有三份,lint 侧不该再抄第四份 #5659:flow-multi-write-unfiltered不判空组合子,filter: { $and: [] }+multi: true按 空组合子在同仓有两个对立答案:五个后端归约成布尔单位元,service-analytics 的两个编译器 fail-closed 抛错 —— #5239 的一致性表四条因此进不了表 #5322/SqlDriver.applyFilterCondition 丢弃编译成空的 $and/$or 子过滤器,而不是套用布尔单位元 —— 与同仓 matchesFilterCondition / driver-memory 相反 #5134 恒等元是整表删除却零告警;建议把布尔身份归约提到 spec 侧共享谓词,四个消费者共用一个答案,而不是第四份手抄。changeset:
.changeset/flow-unbounded-bulk-write-lint.md(@objectstack/lint": patch)。Generated by Claude Code