You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec)!: refuse a blank string in a flow node's predicate slot — decision branch expression, screen field visibleWhen (#17493) (#19960)
Fixes#17493
Clause-②: no (narrowing)
Executes ruling A (`5651023407`). A string that is blank after trimming
is refused in two flow-node predicate slots: `decision`
`config.conditions[].expression` and `screen`
`config.fields[].visibleWhen`. The refusal fires at `FlowSchema.parse`,
`AutomationEngine.registerFlow` and `objectstack validate`, with a
message led by `PREDICATE_SLOT_STRING_REFUSAL`.
- **Census first** (ruling item 1): at base `3b5607019f`, the dev found
no flow in the tree or in the example stacks carrying either blank. The
method is in report `5811268231`.
- **Code:**
- `flow.zod.ts`: a `FlowSchema` refinement over the ledger predicate
slots.
- `flow-node-expression-paths.ts`: the resolver emits a blank predicate
string, and `predicateSlotRefusal` refuses it.
- `engine.ts` and `validate-expressions.ts`: comments only.
- **Card item 1:** the `structuralConditionRefusal` docblock now records
that ruling A answered its open question, and its doors line is
corrected for the node slot.
- **Card item 2:** a new ADR-0087 entry,
`flow-predicate-slot-blank-string-refused`; `registry.ts` is
regenerated.
- **Pins:** one file per door, plus re-judged existing pins. The dev
ablated each refusal red (report `5811268231`).
`predicate-slot-blank.test.ts` also pins, on the real decision executor,
that `'false'` runs what the blank ran, and that dropping the only
branch runs the out-edge it labelled. Both were ablated red (report
`5812924875`).
- **Seat rulings** (`5811310916`, corrected by `5811904464` and
`5812959979`): the parse door stays although `config.condition` has
none. On a decision branch, the prescription that keeps the run is
`expression: 'false'`, the value the blank evaluated to. Dropping a
decision's only branch is named as the thing not to do.
Changeset: `@objectstack/spec`, `@objectstack/service-automation` and
`@objectstack/lint` at `minor`, plus **BREAKING** (the launch window
refuses `major`), with an ADR-0087 `registered` marker.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
---------
Co-authored-by: Claude <noreply@anthropic.com>
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)
**BREAKING** — an accept-set narrowing on two authored flow-node slots, shipped as
14
+
`minor` under the launch-window convention (`check-changeset-no-major` refuses
15
+
`major` until GA; breaking-ness is carried by this banner and the ADR-0087
16
+
disposition above, not by the level).
17
+
18
+
**What changed.** A `decision` node's `config.conditions[].expression` and a
19
+
`screen` node's `config.fields[].visibleWhen` are declared bare CEL text. A string
20
+
that is blank after trimming (`''`, `' '`, a tab or a newline) used to be
21
+
accepted there by `FlowSchema.parse`, `AutomationEngine.registerFlow` and
22
+
`objectstack validate`, and was then read as "no predicate": the evaluator answers
23
+
a blank decision predicate `false`, so that branch was not taken, and nothing said
24
+
so. It is now refused at those doors — by `FlowSchema.parse` with a `custom` issue
25
+
anchored at the slot (for example `nodes.1.config.conditions.0.expression`), and
26
+
by `registerFlow` and `objectstack validate` through that same parse — with a
27
+
message that leads with the published `PREDICATE_SLOT_STRING_REFUSAL` sentence,
28
+
the one these slots already answered with for a non-string value. Where such a
29
+
value already sits, the whole flow is refused: registered from the metadata
30
+
registry or `sys_metadata` at boot, it is skipped with a
31
+
`failed to register flow` warn naming it while the flows beside it register; a
32
+
`defineStack({ flows })` source throws `StackSchemaInvalidError` for the whole
33
+
stack; an artifact file is refused whole at load.
34
+
35
+
## FROM → TO
36
+
37
+
| you wrote | write instead |
38
+
|:--|:--|
39
+
|`conditions: [{ label: 'high', expression: ' ' }]` on a `decision` node | the predicate you meant — `{ label: 'high', expression: 'record.amount > 10000' }` — or, to keep what the blank did, `expression: 'false'`|
40
+
|`fields: [{ name: 'reason', visibleWhen: '' }]` on a `screen` node | the predicate you meant — `visibleWhen: "status == 'rejected'"` — or, to keep what the blank did, drop the `visibleWhen` key |
41
+
42
+
**One-line fix:** write the predicate, or keep what the blank did — `'false'` on
43
+
a decision branch (the value the blank evaluated to), no `visibleWhen` on a
44
+
screen field (a blank one was read as absent). ⚠️ Do not drop a decision's only
45
+
branch: the node then routes by its out-edges alone, and the out-edge that branch
46
+
labelled is no longer held back. A blank structural `condition` is another case —
47
+
see the `flow-edge-condition-evaluated-slot-source-required` migration entry.
48
+
49
+
**Unchanged.** A non-blank predicate parses, registers and validates as before;
50
+
a non-string in these slots keeps its existing refusal at `registerFlow` and
51
+
`objectstack validate`; `edges[].condition` and a node's `config.condition` keep
52
+
their own rule and sentence (`EVALUATED_EXPRESSION_SOURCE_REQUIRED`); and
53
+
`AutomationEngine.evaluateCondition` still answers a blank predicate `false` for
54
+
a caller that reaches it directly. The `PREDICATE_SLOT_STRING_REFUSAL` constant
55
+
keeps its name and now also names the blank string, so code matching the
56
+
constant rather than a copy of its text is unaffected.
0 commit comments