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
feat(service-automation): evaluate a value-role CEL envelope in the assignment executor and validate it at registerFlow (#15137) (#15432)
* feat(service-automation): evaluate a value-role CEL envelope in the assignment executor and validate it at registerFlow (#15137)
The executor half of the maintainer's 2026-09-02 ruling on #14149, whose spec
half landed in PR #15113. An `assignment` value that is an `{ dialect: 'cel',
source }` envelope is now evaluated by the expression engine and the result
assigned; before this it went to `interpolate()`, which recursed into it as a
plain object and wrote it into the variable verbatim, so `notify` rendered
`{"dialect":"cel","source":"…"}` as JSON and the declared CEL stdlib was
unreachable from metadata.
Three parts:
- `AutomationEngine.evaluateValueEnvelope` evaluates a declared envelope in the
CEL scope `evaluateCondition` already builds — extracted to one shared
`celScope`, so a predicate and a value expression cannot disagree about what a
variable means. A refusal throws with the source attached (ADR-0032 §1c/§1d):
a value that failed to compute has no falsy default to hide behind.
- `registerFlow` refuses a malformed envelope (throws) and `objectstack validate`
reports it as a located finding — the severity split the `predicate` role
already uses. Both compose the SAME two published primitives, in the same
order: `AssignmentValueSchema` for shape (it alone catches the source-less
`{ dialect: 'cel' }` that `validateExpression` reads as "not authored") and
`validateExpression('value', …)` for CEL. One notion of malformed, derived
once, so registration and evaluation cannot refuse different sets.
- The behaviour is scoped to the slot the ledger declares, `assignments.*`. The
legacy `assignments: [{ variable, value }]` array and the bare
`{ <variable>: <value> }` config keep every meaning they had, envelope-shaped
values included: `AssignmentConfigSchema` is deliberately NOT wired into
`parseNodeConfig` for the array form, because refusing it would break flows
that register today and that refusal is a ruling, not a lane's call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
* fix(service-automation): clear the ADR-0087 gate, correct the docs this landing falsified, and pin the bound of the reject-set property (#15137)
The four items from the Clause-② contract review on PR #15432. The mechanism is
untouched — no change to engine.ts, logic-nodes.ts or validate-expressions.ts.
- **ADR-0087 gate (red, required).** The changeset declares BREAKING and carried
no disposition marker, so `check-adr-0087-registration.mjs` failed. It now
carries `not-required (no-migration-prescription)`: no authorable key is
renamed, retired or re-typed, so `objectstack migrate meta` has nothing to
rewrite. The gate was missed locally because `dispatch-gates.mjs` presents
`--self-test` as the runnable member and files the real `--base` invocation as
not-runnable, although the script's own usage line defaults `--base` to
`origin/main`.
- **Docs this PR made false.** `content/docs/automation/flows.mdx` still told
authors the executor writes the envelope verbatim and prescribed a `script`
node "for a digest body today". Replaced with what landed: where a malformed
envelope is refused (three doors, one composition) and which two shapes fault
at run time instead. The same stale sentence in
`packages/spec/src/automation/flow-node-expression-paths.ts` is corrected as a
COMMENT — no contract, schema, export or generated artifact moves.
- **The property's bound, pinned.** `{ dialect: 'cel', source: ' ' }`
registers and then faults: `source` is `z.string().min(1)` so whitespace
passes the shape rule, `validateExpression` trims to empty and answers "not
authored", and the CEL engine parses it untrimmed. Same seam class as the
`ast`-only case. Pinned loud on both sides (engine faults, lint is silent) and
added to #15430 — deliberately NOT closed here with a trim rule of the
engine's own, which would be the third locally-invented notion of "malformed"
this design exists to prevent.
- **Accuracy.** "Exactly four things" was an under-count (`source: 5` and
`meta: 'bad'` are refused too); the reject set is stated as the composition.
The runtime publish gate is named as the third door —
`validateStackExpressions` is registered `CLI_AND_RUNTIME` / `['flow']`
(`authoring-rules.ts:431-441`), so Studio / REST / MCP flow writes get the
same refusal at `severity: 'error'`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(service-automation): an `assignment` value may be a CEL envelope — evaluated at run time, validated at `registerFlow`, `objectstack validate` and the runtime publish gate (#15137, the executor half of #14149)
7
+
8
+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key is renamed, retired or re-typed: the `assignments` map and every value form it accepted still parse. The only newly refused shape is a malformed CEL value envelope, a spelling declared one day earlier in #15113 and offered by no authoring surface before it, so `objectstack migrate meta` has nothing to rewrite and this changeset carries no rewrite instructions for a consumer to follow. -->
9
+
10
+
**BREAKING** in the accept-set sense, landing in the launch window as `minor`
11
+
(the lockstep convention; the level also follows the 2026-09-04 bump ruling —
12
+
this adds `AutomationEngine.evaluateValueEnvelope` to a published surface, and an
13
+
additive widening is at least `minor`). No ADR-0087 conversion: no authorable key
14
+
is renamed or retired, and the shape this refuses was never a shape any surface
15
+
offered.
16
+
17
+
The maintainer's 2026-09-02 ruling on #14149 made an assignment value able to be
18
+
a CEL **value** expression, so the declared stdlib (`joinNonEmpty`, `map`, `size`
19
+
…) is finally reachable from metadata — until now CEL was only ever asked for a
20
+
boolean. The spec half landed the contract (PR #15113); this is the half that
21
+
makes it do something.
22
+
23
+
```yaml
24
+
# before: written into the variable verbatim, and rendered by `notify` as
25
+
# {"dialect":"cel","source":"joinNonEmpty(...)"}
26
+
# now: evaluated — digest is "Renewal due\nInvoice overdue"
0 commit comments