Skip to content

fix(cli): gate the two decision-routing shapes that can never work, flag the inert config.condition (#4414) - #4493

Merged
os-zhuang merged 2 commits into
mainfrom
claude/app-crm-decision-routing-bug-aufqaj
Aug 1, 2026
Merged

fix(cli): gate the two decision-routing shapes that can never work, flag the inert config.condition (#4414)#4493
os-zhuang merged 2 commits into
mainfrom
claude/app-crm-decision-routing-bug-aufqaj

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#4414 的两项收尾,都是「读起来像守卫、实际不是守卫」的元数据。承接 #4440(运行时)与 #4474(账本)。

配套 objectui:objectstack-ai/objectui#3148(Decision 检查器的两处文案)。

① 两条规则升 error

flow-branch-label-unmatchedflow-default-edge-with-condition 现在打断构建而不是警告。

真正需要改的不是接线,而是那句判据。文件头原来写的是 error 只留给「保证运行时失败」的形状 —— 按字面这两条都不该升,因为它们不失败,它们走错路。所以判据如实重述为:

没有任何读法能让作者写的东西生效,确定性地,每次运行都错。

两条都符合:一个没有任何出边携带的分支 label 不可能路由;一条同时是 isDefault 又带 condition 的边,永远是 condition 赢、标记什么也不做。它们不失败 —— 它们每次都错,而且是静默的,这更糟。

另外两条故意留 warning,政策里现在写明了理由:flow-decision-unconditional-branch 通常是守卫不守,但「一条守卫边 + 一条无条件边」也是合法的「可能通知、总是继续」扇出;flow-multiple-default-edges 确实可以表示「都不匹配时两件事都做」。判据是可证伪性,不是后果严重程度 —— 拿一个我们证不了错的形状去打断客户构建,是更差的交易。

接线无需改动:lintFlowPatterns 已经是 tier: 'gating' 且跑在全部三个命令上(#4409),而这正是 authoring-rule-wiring.test.ts 存在要守的那道缝。

② 新规则 flow-inert-node-condition

config.condition 只在 start 节点上是活的(触发门),其它任何节点类型都不读 —— 引擎在注册时对每个节点都 parse-validate 它(所以畸形的会被抓),然后就忽略。而在 decision 上,这个名字让它读起来就是分支谓词,这也正是它被写出来的原因。

三个 bundled app 里有两个中招:

  • app-todocheck_recurring;
  • app-showcaseneeds_exec —— 而且它旁边的注释已经写明「node config.condition alone is not evaluated by the engine」,知道是死的还是留着了。这比前一个更能说明规则的价值:人知道了也会留下残留,只有 gate 会清掉。

两者都是各自出边已经在执行的谓词的第三份拷贝。现在都改成纯 exclusive gateway。

Advisory 而非 gating:周围的边通常仍然路由正确,所以它是死重量,不是可证明的误路由。

节点类型用的是我实际读过其执行器的内置类型闭集,不是「所有非 start」—— ADR-0018 让 node.type 保持开放,插件执行器完全可以合法地声明并读取自己的 config.condition,我们只能对自己出的那些类型下这个断言。新增内置类型必须被检查,而不是默默继承。

验证

  • 5 个新测试覆盖新规则(含插件类型不误报、start 不误报、空/缺省不误报),4 处断言补上 severity —— 两条 gating 断言 'error',两条 advisory 断言 undefined,免得将来有人顺手全升。
  • 一处既有测试收窄:它用「全部规则为空」来断言 [P2] flow: create_record node value semantics ambiguous (literal vs CEL vs macro vs ref) #1315 的插值规则跳过 CEL condition,而新规则在那个形状上正确地触发了;改成只断言它真正针对的两条规则。
  • 三个 example app 用重建后的 CLI 全扫:五条规则全部 0 findings;stash 掉修复后重跑,规则确实亮起。
  • pnpm build + pnpm test 整仓 132/132 全绿,pnpm typecheck 122/122,pnpm lint 干净。

相关

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q8as8yR67v41xEdomiTba9


Generated by Claude Code

claude added 2 commits August 1, 2026 11:01
…nert condition (#4414)

Two follow-ups to #4440, both about metadata that reads like a guard and is
not one.

`flow-branch-label-unmatched` and `flow-default-edge-with-condition` now FAIL
the build. The bar is restated at the top of the file, because the old one —
"a guaranteed runtime failure" — no longer described the set: it is now **no
reading of the author's metadata does what it says, deterministically, on
every run**. A branch label no out-edge carries cannot route; an edge that is
both `isDefault` and conditional always lets the condition win, so the marker
routes nothing. Neither FAILS; both are wrong every time and silently.

The other two stay advisory, and the policy now says why. A decision with one
guarded and one unconditional out-edge is usually a guard that does not guard,
but it is also a legal "maybe notify, always continue" fan-out, and two
default edges can genuinely mean "when nothing matched, do both". The bar is
provability, not severity of consequence — failing a customer's build on a
shape we cannot prove wrong is the worse trade. No wiring change: the rule is
already `tier: 'gating'` across all three commands (#4409).

`flow-inert-node-condition` is new. `config.condition` is the trigger gate on
a `start` node and is read by no other node type — the engine parse-validates
it everywhere and then ignores it, so on a `decision`, where the name makes it
read as the branch predicate, it is a guard that gates nothing. Two of the
three bundled apps had one: app-todo's `check_recurring` and app-showcase's
`needs_exec`, each a third copy of a predicate its out-edges were already
enforcing. The showcase even carried a comment saying the node condition "is
not evaluated by the engine" and kept it anyway — the residue this rule exists
to stop accumulating. Both are now plain exclusive gateways.

Advisory: the surrounding edges usually still route correctly, so it is dead
weight rather than a provable misroute. The node-type list is a closed set of
builtins whose executors were actually read, not "everything but `start`" —
ADR-0018 keeps `node.type` open and a plugin executor may legitimately declare
and read its own `config.condition`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8as8yR67v41xEdomiTba9
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 1, 2026 12:04pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

20 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added the size/m label Aug 1, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit d449b0c Aug 1, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/app-crm-decision-routing-bug-aufqaj branch August 1, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants