Skip to content

Commit dd251cb

Browse files
committed
wip: changeset (minor + BREAKING, Clause-② no (narrowing), adr-0087 registered)
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6445eab commit dd251cb

2 files changed

Lines changed: 58 additions & 2 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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.

‎packages/services/service-automation/src/predicate-slot-blank.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
* and the flow parse refuses these values itself since this card — so the
2020
* refusal this door hands back is the PARSE's: a Zod issue with code `custom`,
2121
* anchored at the slot. That is what these pins assert, because it is what a
22-
* caller receives (the `/automation` write doors map exactly that issue to a
23-
* `VALIDATION_ERROR` field). The engine's own ledger pass refuses the same
22+
* caller receives: the `/automation` write doors (`flowDefinitionRefusal` in
23+
* `runtime/src/domains/automation.ts`) answer a Zod refusal from
24+
* `registerFlow` as `400` `VALIDATION_FAILED`, one `details.fields[]` entry
25+
* per issue, keyed by the issue's path. The engine's own ledger pass refuses the same
2426
* value through the same `predicateSlotRefusal`, one step later — the same
2527
* two-layer shape a blank `edge.condition` has had since #15807.
2628
*/

0 commit comments

Comments
 (0)