Skip to content

lead_assignment's check_hot decision node carries a config.condition the engine never reads #642

Closed as duplicate

Description

@os-zhuang

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

  1. Confirm the reading above against the installed service-automation (the executor is in dist/index.js, registerLogicNodes).
  2. 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 decision node's declared condition matches its outgoing conditional edges, so the two cannot drift whichever route is chosen.
  3. Sweep the other decision nodes for the same shape — campaign_enrollment.check_campaign_open, lead_conversion.decision_*, demo_bootstrap.has_user, quote_generation.check_stage, opportunity_approval.check_high_value all declare config.condition alongside edges that duplicate it.

Filed unassigned.

Activity

  1. added
    bugSomething isn't working
    metadataDeclarative metadata — schema, security posture, UI surfaces
    on Aug 2, 2026
  2. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    收敛到 #650,本件作为重复关闭。

    两件是同一个机制的窄版与全版:本件是在 #633 期间发现的,只覆盖 lead_assignment.check_hot;#650 是在 #643 期间把 src/flows/ 全部 24 个流程扫了一遍的结果——恰好就是本件待办清单第 3 条("sweep the other decision nodes")。#650 还多两样本件没有的东西:三个求值点的完整清单(start 节点的 config.condition、decision 节点的 config.conditions[] 复数、边上的 condition),以及 A/B 两条路线在架构上的差别(让节点成为权威 vs 让边成为唯一真相)。

    本件的 prio:p1 已带到 #650。路线选择留在那边裁决;无论选哪条,"节点与出边的条件不得漂移"这条断言都要补——那才是今天真正缺的东西。


    Generated by Claude Code

  3. added
    priority:p1High: required for production / M2
    and removed on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmetadataDeclarative metadata — schema, security posture, UI surfacespriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions