fix(lint): flow-runas-unscoped 跨区域搜索它的数据节点证据 (#5633) - #5695
Merged
Conversation
…e evidence (#5633) #5383 gave the flow anti-pattern family a per-region walk and deliberately left this one rule reading the flow's top-level `nodes` only, because it is the family's only build-GATING member and widening it has its own blast radius. Its data-node search is the rule's evidence that the flow performs a data operation at all, and a data node inside a `loop` body is exactly as unscoped as one at the top level. So a scheduled flow that queried a set, looped it and wrote per item — the standard shape, where the write is almost always nested — passed the build clean and was then refused at run time (#3760). Passing the build and then being unable to run is precisely what promoting this rule to `error` was for. The search now runs across `collectFlowGraphs(flow)` at any depth while the finding stays flow-level: one per flow, `where` = `flow 'x' · runAs`, because `runAs` is a flow property and the region only supplies the evidence. The region is named in the message instead, so the author can find the node. Top-level behaviour is unchanged, bit for bit: the message for a top-level evidence node is byte-identical (no region clause), and a flow with data nodes at both altitudes still cites the top-level one, since `collectFlowGraphs` yields the flow's own graph before it descends. Both pinned by tests. Verified: `os validate` over app-showcase / app-crm / app-todo is line-for-line identical before and after; the docs/skills flow fixtures A/B is identical too.
|
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:
|
…pin it (#5633) The new fixtures bound the loop item with `itemVar`, which `LoopConfigSchema` — a `strictObject` — reports as an `unrecognized_key` (the declared key is `iteratorVariable`; `itemVariable` exists only as a diagnostic alias). Nothing here would have gone red for it: region collection reads `config.body`, which is unaffected, so the rule was being proven against a `loop` an author cannot actually write — the #4966 trap one container down. Re-spelled to `iteratorVariable`, added the `label` that `FlowNodeSchema` requires on the body nodes, and pinned the container against the schema in both directions: the fixture parses green, and the near-miss spelling really is rejected so the pin has teeth. Full `safeParse` green rather than merely "no unrecognized keys", because this rule judges a VALUE verdict (`runAs` against the trigger kind) whose evidence must be a node reachable inside an authorable container. Pre-existing fixtures spelling the same key (the #5383 `loopBodyFlow` helper and the #5482 nested cases) are left alone and filed separately — they are not exposed by this change.
os-zhuang
marked this pull request as ready for review
August 6, 2026 01:02
os-zhuang
enabled auto-merge
August 6, 2026 01:02
This was referenced Aug 6, 2026
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 #5633
这是什么
flow-runas-unscoped是 flow lint 规则族里唯一的 build 门禁成员(severity: 'error')。#5383/PR #5635 给整个规则族接上了 ADR-0031 区域下钻,唯独把这条规则留在只读顶层nodes——因为收紧一条门禁规则有它自己的爆炸半径,值得单独一单。本 PR 就是那一单。规则的
dataNode搜索是「这个 flow 到底有没有做数据操作」的证据;而loopbody 里的数据节点,和顶层的一样 unscoped——运行时resolveRunAsIdentity拒绝嵌套写的理由和拒绝顶层写的理由完全相同(#3760),嵌套深度不是它会去查的属性。所以原来漏掉的形状是「build 过、运行必拒」,而这恰恰是这条规则当初升到error要防的形状。更糟的是,漏掉的不是边角,而是定时 flow 的默认形状:查一批、循环、逐条写——写节点几乎总在循环体里。
实测(同一个 flow,只挪动节点位置):
怎么修的
dataNode搜索改走collectFlowGraphs(flow),覆盖任意深度的每个区域;finding 本身保持 flow 级,和以前完全一样:一条/flow,where=flow 'x' · runAs。这一点是设计核心,两个高度分得很清楚:runAs是 flow 属性、trigger 是 start 节点的属性,所以「这个 flow 是否 unscoped」每个 flow 只有一个答案——不按区域报,也不按数据节点报。loopbody 和顶层一样属于这个 flow。区域名字放在 message 里而不是
where里:where说哪条声明写错了,message 说去哪儿找证明它的那个节点。顶层行为逐字节不变
两条都有测试钉住:
collectFlowGraphs先产出 flow 自己的图再下钻),即连「引用哪个节点」都没变。另外
userLessTriggerKind与flow.runAs的 flow 级判定完全没动。反向验证:先红后绿(方向如预期)
先写测试、后写实现,红的方向和预期一致——嵌套用例产出 0 findings、断言 1 条:
4 条红的正好是 4 条嵌套用例。同一次运行里,顶层孪生用例(逐字节断言 message)、
runAs:'system'反例、非 user-less trigger 反例、无数据节点反例全部已绿——即非回归面在改动前就绿,改动后仍绿。实现后:1382 passed。保险丝:examples 三 app + docs fixtures 的 A/B 均零新增
按 PM 裁定的硬性保险丝逐项实测,真 before(revert 规则源文件 → 重建
packages/lintdist → 重跑,已grep确认 dist 里没有新函数):os validate三个 app,before/after 逐行 diff,只差 load-time 计时:三个都
✓ Validation passed、exit 0、error 计数 0——零新增 error,零新增 warning。content/docs/**+skills/**的 flow fixtures 同样 A/B:唯一命中是flows.mdx的contract_expiration_check,它的证据节点是顶层get_record,改动前后各 1 条、完全一致(是既存的语料缺陷,已另立 #5692,不在本 PR 范围)。正向对照:error 确实穿透 CLI 面
把
examples/app-todo的overdue_escalation临时改成「去掉runAs、唯一数据节点挪进 loop body」,os validate真的拒绝(exit 1),文案点名区域:之后
git checkout --还原,重跑确认该 app 回到✓ Validation passed;git status干净。验证清单
pnpm --filter @objectstack/lint test59 passed,1382 passedpnpm --filter @objectstack/lint typecheckpnpm --filter @objectstack/lint buildpnpm --filter @objectstack/cli test83 passed,825 passedpnpm --filter @objectstack/metadata-protocol test43 passed,402 passedos validateA/Bnode scripts/check-nul-bytes.mjsOK (scanned 5609 tracked text file(s))npx eslint改动文件origin/main后check:generated✓ All 10 generated artifacts are up to date规则的消费半径已按调用者枚举扫过:
cli的 validate/lint/compile、metadata-protocol的 runtime authoring gate、以及三处 parity 测试的 fixtures——没有 fixture 拼写了本 PR 收紧掉的形状(用「user-less trigger + 区域」双条件全仓 grep 过,命中的只有 spec/engine 的 schema 与执行测试,它们不跑这条规则)。changeset
真 changeset(
"@objectstack/lint": patch),正文明确写了这是一条 build-gating 规则的覆盖面收紧、爆炸半径、以及「如果你的 build 新红了怎么办」(加runAs: 'system')——release 汇编从它取材。未碰content/docs/releases/。顺手记录的范围外发现(未在本 PR 修)
flows.mdxcontract_expiration_checkis a schedule flow with a top-levelget_recordand norunAs: 'system'#5692:content/docs/automation/flows.mdx的contract_expiration_check教了一个门禁会拒的 flow(schedule + 顶层get_record+ 无runAs)。先于本 PR 存在、与本 PR 无关(证据节点在顶层),同文件 35 行之下的邻居示例反而写对了。flow-runas-unscoped's "the defaultrunAs:'user'" wording is unreachable on the CLI surface — the parse materializes the default, so an author who declared nothing is told they declared it #5693(finding,观察级):规则里the default \runAs:'user'`这一支在 CLI 面**不可达**——规则是input: 'parsed',schema 会把默认值实体化,所以什么都没声明的作者会被告知「runs asrunAs:'user'`」。同样先于本 PR 存在。🤖 Generated with Claude Code
https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE
Generated by Claude Code