diff --git a/.changeset/decision-default-path-is-the-edge.md b/.changeset/decision-default-path-is-the-edge.md new file mode 100644 index 000000000..f2910ecb7 --- /dev/null +++ b/.changeset/decision-default-path-is-the-edge.md @@ -0,0 +1,29 @@ +--- +"@object-ui/app-shell": patch +--- + +Say what the Decision inspector actually does: the default path is the edge marker, not the branch. + +Two help strings described mechanisms the engine does not have. + +The **Branches** editor said a branch whose expression is `"true"` *is* the +default/else path. It is how you **ask** for one — `FlowEdgeInspector.applyBranch()` +turns such a branch into `isDefault: true` on the out-edge it wires, and the marker +on that edge is what routes. Conflating the two is the reading that let +objectstack-ai/objectstack#4414 ship a decision whose guard did not guard, and it is +worth being exact about now that `isDefault` is finally enforced: the key had **zero +readers** in the engine until then, so this designer had been writing a marker +nothing honoured, and every Studio "default/else" edge ran unconditionally alongside +whichever branch matched. The help also now states that branches are tried in order +and that the expression is bare CEL — a braced predicate there is a build failure +since objectstack-ai/objectstack#4439. + +The legacy single **Condition** field said *"Prefer Branches above"*, which reads as +"this works, but the other is better". It does not work at all: the decision executor +never reads `config.condition`. The engine honours that key only on a Start node, as +the trigger gate, and `os validate` now reports it as `flow-inert-node-condition`. +The field stays render-only (its `__legacy__` controller never matches, so it is not +offered for new authoring) so a stored value is not invisible — but the help says it +is inert and where the predicate belongs instead. + +Text only; no behaviour change on this side. diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts b/packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts index 2a649796f..8363c5698 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts +++ b/packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts @@ -407,7 +407,12 @@ const FLOW_NODE_CONFIG: Record = { ], decision: [ cfg('conditions', 'Branches', 'objectList', { - help: 'Each branch has a label, a CEL expression, and a target node. A branch whose expression is "true" is the default/else path. Picking a target wires the branch’s outgoing edge (creating or updating it); clearing it detaches that edge.', + // framework#4414: the default/else path is the out-edge marked + // `isDefault`, not the branch itself — a `true` branch is how you ask for + // one, and FlowEdgeInspector.applyBranch() writes the marker. Saying + // "a `true` branch IS the default path" conflated the two, which is the + // reading that let a decision ship with a guard that did not guard. + help: 'Each branch has a label, a CEL expression (no {braces}), and a target node. Branches are tried in order and the first match wins. A branch whose expression is "true" is the catch-all: picking its target marks that out-edge as the default path, taken only when no other branch matched. Picking a target wires the branch’s outgoing edge (creating or updating it); clearing it detaches that edge.', columns: [ { key: 'label', label: 'Label', kind: 'text', placeholder: 'Has deals' }, { key: 'expression', label: 'Expression', kind: 'expression', placeholder: 'expiring_deals.length > 0' }, @@ -416,9 +421,16 @@ const FLOW_NODE_CONFIG: Record = { { key: 'target', label: 'Target', kind: 'reference', placeholder: 'next node', ref: { kind: 'node' } }, ], }), + // Render-only for a stored legacy value (`__legacy__` never matches, so it + // is not offered for new authoring). The old help said "Prefer Branches + // above", which reads as "this works, but the other is better" — it does + // not work at all: framework#4414 confirmed the decision executor never + // reads `config.condition`. The key is the trigger gate on a `start` node + // and inert on every other node type, and `os validate` now reports it as + // `flow-inert-node-condition`. cfg('condition', 'Condition (single)', 'expression', { placeholder: 'amount > 10000', - help: 'Legacy single-branch condition (CEL). Prefer Branches above; per-edge conditions also live on the outgoing edges.', + help: 'Inert — nothing reads this. The engine only honours `condition` on a Start node (the trigger gate); on a Decision it gates nothing. Shown so a stored value is not invisible. Move the predicate to a branch above, or to the outgoing edge’s own condition, then clear this.', showWhen: { field: '__legacy__', equals: [] }, }), ],