Uh oh!
There was an error while loading. Please reload this page.
fix(automation,objectql): a filter that loses a condition must not run (#3810) - #3831
Merged
os-zhuang merged 3 commits intoJul 28, 2026
Merged
Conversation
#3810) Three related holes, all ending in "the query matched rows the author excluded". 1. A FLOW FILTER COULD SILENTLY WIDEN TO MATCH EVERYTHING. The flow template interpolator expresses "this token did not resolve" as `undefined`. In a message that renders as empty text — harmless. In a FILTER it removes the condition, and a removed condition matches MORE rows. When it was the only condition, `{ owner: '{record.ownr}' }` became `{}`, and `{}` handed to `deleteMany` is every row in the table. So one mistyped field name in a `delete_record` node silently emptied the object. Reproduced with all four causes: a typo (`{record.ownr}`), an input the run never received, a lookup hop (`{record.account.name}` — the trigger record carries a scalar id), and a filter placeholder. `get_record` / `update_record` / `delete_record` now refuse to execute when interpolation erased any authored condition, naming the offending template. The guard keys on LOSS, not emptiness: an author who deliberately wrote no filter is unaffected, and losing one of two conditions still fails, because widening from "my open records" to "all open records" is the same class of bug. 2. FILTER PLACEHOLDERS NEVER REACHED THE ENGINE THAT RESOLVES THEM. `config.filter` is where two `{…}` dialects meet — the flow template dialect (`{record.owner}`) and the filter placeholder dialect (`{current_year_start}`, `{current_user_id}`, resolved by `resolveFilterTokens()`). Evaluation order picked the winner by accident: the flow interpolator ran first, found no flow variable by that name, and erased it. `interpolateFilter()` hands that position back to the dialect that owns it — a whole-string token that no flow variable resolves and that IS a recognised placeholder passes through verbatim for the engine to expand. Flow variables keep precedence, so a template that works today cannot change meaning. 3. THE ENGINE RESOLVED PLACEHOLDERS ON READS BUT NOT ON WRITES. `resolveFilterTokens()` reached find/findOne/count/aggregate only. So the SAME filter selected different rows depending on the verb: `find({owner: '{current_user_id}'})` matched the signed-in user's rows while `update`/`delete` compared the literal token text and matched none — a flow that previewed with one and acted with the other operated on two different row sets. The #3106 shape one layer down: the evaluator existed, only some call sites reached it. `update` and `delete` now resolve too, BEFORE the by-id fast path claims a scalar `where.id` (otherwise an unresolved `{current_user_id}` would be bound as the primary key itself). Caller options are never mutated. Docs corrected: the caveat added in #3582 saying flow filters cannot use tokens is now wrong, and the build lint's "resolves to an empty string silently" no longer describes filter positions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EdCvGtER9SzJopS4Yh3Pa1
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
`content/docs/references/` is generated from the spec TSDoc; the resolver description in `date-macros.zod.ts` / `context-tokens.zod.ts` changed in the previous commit without the regenerate step, so `check:docs` failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EdCvGtER9SzJopS4Yh3Pa1
…s-filters-notifications-nicmhd
os-zhuang
marked this pull request as ready for review
July 28, 2026 09:52
Uh oh!
There was an error while loading. Please reload this page.
os-zhuang
deleted the
claude/template-tokens-filters-notifications-nicmhd
branch
July 28, 2026 09:52
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.
Closes#3810.
开 #3810 时我描述的是"flow 筛选静默吞掉 date macro"。动手前先实测,发现真实情况严重得多,而且大部分与 filter token 无关。下面三条是同一个失败模式的三个出口:查询匹配了作者本想排除的行。
1. flow 筛选会静默塌缩成"匹配全部"
flow 模板插值器把"这个 token 没解析出来"表达为
undefined。在 message 里那渲染成空文本——无害。在 filter 里它移除了一个条件,而少一个条件匹配的行更多。 当它是唯一条件时:{}交给deleteMany就是整张表。实测四种成因全部命中(探针跑完即删):
{record.ownr}字段名拼错{}{someInputVar}该次运行没传{}{record.owner.manager}lookup 穿透{}{current_user_id}filter token{}{status:'open', owner:'{record.ownr}'}{"status":"open"}一个字段名拼写错误 + 一个
delete_record节点 = 静默清空整张表。 这是既有行为,不是 #3582 引入的。get_record/update_record/delete_record现在在插值抹掉任何作者写下的条件时拒绝执行,并指名是哪个模板。护栏的判据是丢失而不是为空——刻意不写 filter 的作者不受影响("删除全部"仍然可表达),而三缺一同样失败,因为从"我的 open"放宽到"所有 open"是同一类 bug。2. filter 占位符从未到达能解析它的引擎(#3810 本体)
config.filter是两种{…}方言唯一相遇的位置:flow 模板方言({record.owner})和 filter 占位符方言({current_year_start},由resolveFilterTokens()解析)。谁赢完全由求值顺序偶然决定:flow 插值器先跑,找不到叫current_year_start的 flow 变量,就把它抹了。新增
interpolateFilter()把这个位置归还给拥有它的方言:整串 token 若无 flow 变量可解析、且是已知 filter 占位符,则原样透传交给引擎展开。这是所有权归还,不是宽容 fallback(PD #12):
config.filter本来就是 filter 方言的地盘,flow 模板只是路过。flow 变量保留优先级——同名 flow 变量仍然遮蔽占位符,所以今天能跑的模板不会改变含义(有测试锁住)。3. 引擎解析读路径但不解析写路径(#3582 的遗漏,我的)
resolveFilterTokens()上个 PR 只接到了find/findOne/count/aggregate。于是同一个 filter 因动词不同选中不同的行集:一个 flow 用
find_records预览"哪些会被改",再用update_records用同一个 filter 去改——两者作用于不同的行集。这正是 AGENTS.md PD #10 点名的 #3106 形状,只是下沉了一层:evaluator 存在,只是没接到全部调用点。"A
caselabel is not enforcement; check the call site."update/delete现在也解析,且在 by-id 快路径认领标量where.id之前——否则未解析的{current_user_id}会被当成主键本身绑定。调用方的 options 不被改写(flow 节点 config 会跨次运行复用)。三者的关系
不是三个独立补丁,是一套:②放行之后必须有人解析,所以③是前提;①兜住剩下的 fail-open——它覆盖拼写错误/缺变量/lookup 穿透,这些是②③都碰不到的。
方向也值得记一笔:③是 fail-closed(token 当字面量 → 匹配零行,少改少删),①②修的是 fail-open(条件消失 → 匹配全部)。后者才是数据破坏。
验证
crud-filter-guard.test.ts(13,含"刻意空 filter 仍可执行"和"flow 变量遮蔽占位符"两个反向锁),engine-filter-tokens.test.ts补 6 个写路径用例(含 by-id 顺序回归、读写一致性、不改写调用方 options)。pnpm build通过;pnpm test132/132 全绿。写测试时踩到一个值得说的点:三个"应当成功"的用例最初失败,原因是 ADR-0049 的
runAs门(#3760)先拒绝了数据操作。这说明护栏位置足够靠前(在任何数据操作之前),但也意味着若不声明runAs: 'system',那些"应当失败"的用例会因为错误的原因通过。已在测试 flow 上显式声明并注释。文档
修正两处因本次改动而失效的说法:
skills/objectstack-query/rules/filters.md里我在 Template tokens in filter values and notification templates are not evaluated at runtime #3582 加的"flow 筛选用不了 token"——现在能用了;validate-flow-template-paths.ts的 "resolves to an empty string at runtime (silently)"——在 filter 位置现在是拒绝执行,不再是静默空串。该 lint 仍然值得保留(构建期抓到胜过运行期失败,且其他 config 块行为不变),注释里写清了。🤖 Generated with Claude Code
https://claude.ai/code/session_01EdCvGtER9SzJopS4Yh3Pa1
Generated by Claude Code