Repository navigation
lead_assignment's check_hot decision node carries a config.condition the engine never reads #642
Description
Found while measuring #633. Not fixed there — #633's scope was CEL totality, and this is a wiring/liveness question with its own answer.
What
src/flows/lead-assignment.flow.ts declares:
{
id: 'check_hot', type: 'decision', label: 'Hot Lead (rating ≥ 4)?',
config: { condition: P`has(record.rating) && record.rating != null && record.rating >= 4` },
}The builtin decision node executor in @objectstack/service-automation reads config.conditions (plural, an array of { label, expression }) and never looks at config.condition:
async execute(node, variables, _context) {
const config = node.config;
const conditions = config?.conditions ?? [];
for (const cond of conditions) {
if (engine.evaluateCondition(cond.expression, variables)) {
return { success: true, branchLabel: cond.label };
}
}
return { success: true, branchLabel: 'default' };
}So check_hot always returns branchLabel: 'default'. traverseNext then filters out-edges by that label, finds none labelled default, falls back to the full edge set, and evaluates edges e2 (Hot) and e3 (Standard) — which carry their own conditions and are what actually branches.
The node's config.condition is therefore inert metadata. It happens to agree with edge e2 today (#633 kept them in sync deliberately, and left a comment saying so), but nothing enforces that: an author who edits the node and not the edge changes nothing at runtime while believing they changed the branch.
Why it matters beyond this one node
This is the declared-≠-enforced shape ADR-0049 exists for, and it is the kind of thing an AI author reproduces by pattern-matching. os lint and os validate both pass on it — validation can see that a condition string exists, not that anything reads it.
Suggested work
- Confirm the reading above against the installed
service-automation(the executor is indist/index.js,registerLogicNodes). - Decide the route:
- A. Remove
check_hot.config.condition, leaving the edges as the single source of truth. Cheapest, and matches how the flow already behaves. Costs the node its self-documenting label→predicate pairing. - B. Convert it to the shape the executor actually reads (
config.conditions: [{ label: 'Hot', expression: … }]) and let the branch label drive traversal, making the edges' duplicate conditions redundant. - C. Add a metadata test asserting that a
decisionnode's declared condition matches its outgoing conditional edges, so the two cannot drift whichever route is chosen.
- A. Remove
- Sweep the other
decisionnodes for the same shape —campaign_enrollment.check_campaign_open,lead_conversion.decision_*,demo_bootstrap.has_user,quote_generation.check_stage,opportunity_approval.check_high_valueall declareconfig.conditionalongside edges that duplicate it.
Filed unassigned.
Activity
os-zhuang commentedon Aug 2, 2026
收敛到 #650,本件作为重复关闭。
两件是同一个机制的窄版与全版:本件是在 #633 期间发现的,只覆盖
lead_assignment.check_hot;#650 是在 #643 期间把src/flows/全部 24 个流程扫了一遍的结果——恰好就是本件待办清单第 3 条("sweep the otherdecisionnodes")。#650 还多两样本件没有的东西:三个求值点的完整清单(start 节点的config.condition、decision 节点的config.conditions[]复数、边上的condition),以及 A/B 两条路线在架构上的差别(让节点成为权威 vs 让边成为唯一真相)。本件的
prio:p1已带到 #650。路线选择留在那边裁决;无论选哪条,"节点与出边的条件不得漂移"这条断言都要补——那才是今天真正缺的东西。Generated by Claude Code
decisionnode's singularconfig.conditionis never evaluated — 24 flows author inert metadata #650