Measured on the pinned @objectstack/lint@17.3.0 artifact as installed by objectstack-ai/hotcrm, ⛔ not on the platform source tree. Filed unassigned by the hotcrm step-3 rule survey (hotcrm#1613); the local assertion that currently covers this stays in place there.
The gap
lintFlowPatterns reports flow-decision-unconditional-branch only when a decision has BOTH gated and ungated out-edges. The scan is guarded by:
const gated = outs.filter((e) => e.condition || e.isDefault === true);
if (gated.length === 0) continue;
So the strictly worse shape — a decision whose out-edges carry no condition and no isDefault, and whose node declares no config.conditions[] — falls out of the loop before any finding is pushed. That decision branches on nothing: every successor runs on every pass, and the gateway is decoration.
The mixed case (some edges guarded, some not) is at least partially routed. The fully-inert case is not routed at all, and it is the one a reader is least likely to notice, because the node still says type: 'decision'.
Measurement, with a working control
One committed tree (hotcrm at a0362a37), one injection at a time, restored by blob hash after each.
injected defect at opportunity_stagnation.check_not_nudged |
pnpm lint result |
add config.condition to the decision node |
flow-inert-node-condition warning, exit 0 |
| add an unconditional out-edge beside the guarded one |
flow-decision-unconditional-branch warning, exit 0 |
add two isDefault: true out-edges |
flow-multiple-default-edges warning, exit 0 |
mark the guarded edge isDefault: true as well |
flow-default-edge-with-condition error, exit 1 |
| remove the condition from the only out-edge (decision now gates on nothing) |
nothing fires — 0 errors, 17 warnings, exit 0, byte-identical to the clean baseline |
The first four rows are the control: the same command, the same flow, the same run design, all four naming their rule id. The fifth is therefore a reading about the rule, not about the harness.
The clean-tree baseline for that repo is 0 error(s), 17 warning(s), 12 suggestion(s), exit 0.
Site
objectstack-ai/hotcrm — src/flows/opportunity-stagnation.flow.ts, decision check_not_nudged inside loop_opps. Region descent is NOT the problem here: flow-inert-node-condition fired on that same nested node, so the walk reaches it (the #5383 class is fixed on this pin).
Suggested direction, not a prescription
Either drop the gated.length === 0 short-circuit and report an all-unconditional decision under the same rule id, or give it its own id. A decision with exactly one unconditional out-edge and no conditions[] is indistinguishable from a noop node in behaviour, so a distinct message may read better than folding it into the mixed-branch wording.
Related, and why this is not a duplicate
Refs: hotcrm#1613 · hotcrm#1582 (the family card whose deletion half depends on this rule)
Measured on the pinned
@objectstack/lint@17.3.0artifact as installed byobjectstack-ai/hotcrm, ⛔ not on the platform source tree. Filed unassigned by the hotcrm step-3 rule survey (hotcrm#1613); the local assertion that currently covers this stays in place there.The gap
lintFlowPatternsreportsflow-decision-unconditional-branchonly when a decision has BOTH gated and ungated out-edges. The scan is guarded by:So the strictly worse shape — a
decisionwhose out-edges carry noconditionand noisDefault, and whose node declares noconfig.conditions[]— falls out of the loop before any finding is pushed. That decision branches on nothing: every successor runs on every pass, and the gateway is decoration.The mixed case (some edges guarded, some not) is at least partially routed. The fully-inert case is not routed at all, and it is the one a reader is least likely to notice, because the node still says
type: 'decision'.Measurement, with a working control
One committed tree (hotcrm at
a0362a37), one injection at a time, restored by blob hash after each.opportunity_stagnation.check_not_nudgedpnpm lintresultconfig.conditionto the decision nodeflow-inert-node-conditionwarning, exit 0flow-decision-unconditional-branchwarning, exit 0isDefault: trueout-edgesflow-multiple-default-edgeswarning, exit 0isDefault: trueas wellflow-default-edge-with-conditionerror, exit 1The first four rows are the control: the same command, the same flow, the same run design, all four naming their rule id. The fifth is therefore a reading about the rule, not about the harness.
The clean-tree baseline for that repo is
0 error(s), 17 warning(s), 12 suggestion(s), exit 0.Site
objectstack-ai/hotcrm—src/flows/opportunity-stagnation.flow.ts, decisioncheck_not_nudgedinsideloop_opps. Region descent is NOT the problem here:flow-inert-node-conditionfired on that same nested node, so the walk reaches it (the #5383 class is fixed on this pin).Suggested direction, not a prescription
Either drop the
gated.length === 0short-circuit and report an all-unconditional decision under the same rule id, or give it its own id. A decision with exactly one unconditional out-edge and noconditions[]is indistinguishable from anoopnode in behaviour, so a distinct message may read better than folding it into the mixed-branch wording.Related, and why this is not a duplicate
config.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429 (open) — a decision with noconfig.conditionstakes every out-edge whose condition holds, in parallel; nothing enforces that intended-exclusive conditional edges partition. That is the multi-guarded-edge case. This card is the zero-guarded-edge case, which A decision node with no declaredconfig.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429's subject does not reach.loopbody — the whole family is blind to nested nodes (8 real inert conditions shipped pastflow-inert-node-condition) #5383 — loop-body descent; fixed on this pin, confirmed above.Refs: hotcrm#1613 · hotcrm#1582 (the family card whose deletion half depends on this rule)