|
| 1 | +--- |
| 2 | +'@objectstack/spec': minor |
| 3 | +--- |
| 4 | + |
| 5 | +fix(spec)!: a blank string in a flow node's predicate slot — a `decision` branch `expression`, a screen field `visibleWhen` — is refused at authoring (#17493) |
| 6 | + |
| 7 | +Clause-②: no (narrowing) |
| 8 | + |
| 9 | +<!-- adr-0087: registered flow-predicate-slot-blank-string-refused --> |
| 10 | + |
| 11 | +**BREAKING** — an accept-set narrowing on two authored flow-node slots, shipped as |
| 12 | +`minor` under the launch-window convention (`check-changeset-no-major` refuses |
| 13 | +`major` until GA; breaking-ness is carried by this banner and the ADR-0087 |
| 14 | +disposition above, not by the level). |
| 15 | + |
| 16 | +**What changed.** A `decision` node's `config.conditions[].expression` and a |
| 17 | +`screen` node's `config.fields[].visibleWhen` are declared bare CEL text. A string |
| 18 | +that is blank after trimming (`''`, `' '`, a tab or a newline) used to be |
| 19 | +accepted there by `FlowSchema.parse`, `AutomationEngine.registerFlow` and |
| 20 | +`objectstack validate`, and was then read as "no predicate": the evaluator answers |
| 21 | +a blank decision predicate `false`, so that branch was not taken, and nothing said |
| 22 | +so. It is now refused at those doors — by `FlowSchema.parse` with a `custom` issue |
| 23 | +anchored at the slot (for example `nodes.1.config.conditions.0.expression`), and |
| 24 | +by `registerFlow` and `objectstack validate` through that same parse — with a |
| 25 | +message that leads with the published `PREDICATE_SLOT_STRING_REFUSAL` sentence, |
| 26 | +the one these slots already answered with for a non-string value. A flow stored |
| 27 | +with such a value no longer registers: the boot log carries a |
| 28 | +`failed to register flow` warn naming it, and its trigger is not armed. |
| 29 | + |
| 30 | +## FROM → TO |
| 31 | + |
| 32 | +| you wrote | write instead | |
| 33 | +|:--|:--| |
| 34 | +| `conditions: [{ label: 'high', expression: ' ' }]` on a `decision` node | the predicate you meant — `{ label: 'high', expression: 'record.amount > 10000' }` — or drop that branch | |
| 35 | +| `fields: [{ name: 'reason', visibleWhen: '' }]` on a `screen` node | the predicate you meant — `visibleWhen: "status == 'rejected'"` — or drop the `visibleWhen` key | |
| 36 | + |
| 37 | +**One-line fix:** write the predicate, or remove it — drop `visibleWhen` to show |
| 38 | +the field unconditionally, or drop the whole decision branch (a branch requires |
| 39 | +its `expression`). |
| 40 | + |
| 41 | +Removing is behaviour-preserving on these two slots: a blank `visibleWhen` was |
| 42 | +already read as absent at run time, and a branch with a blank predicate was not |
| 43 | +taken. That is not true of a blank structural `condition` on an edge or a node, |
| 44 | +where removing the key makes the step unconditional — see the separate |
| 45 | +`flow-edge-condition-evaluated-slot-source-required` migration entry. |
| 46 | + |
| 47 | +**Unchanged.** A non-blank predicate parses, registers and validates as before; |
| 48 | +a non-string in these slots keeps its existing refusal at `registerFlow` and |
| 49 | +`objectstack validate`; `edges[].condition` and a node's `config.condition` keep |
| 50 | +their own rule and sentence (`EVALUATED_EXPRESSION_SOURCE_REQUIRED`); and |
| 51 | +`AutomationEngine.evaluateCondition` still answers a blank predicate `false` for |
| 52 | +a caller that reaches it directly. The `PREDICATE_SLOT_STRING_REFUSAL` constant |
| 53 | +keeps its name and now also names the blank string, so code matching the |
| 54 | +constant rather than a copy of its text is unaffected. |
0 commit comments