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
Found while implementing #15137 (the value-role CEL envelope in the assignment executor). Not fixed there: packages/spec is not that card's surface, and the executor half handles it correctly by refusing loudly. Filed for triage.
The seam
ExpressionSchema (packages/spec/src/shared/expression.zod.ts:75-89) declares source and ast both optional and refines "at least one of source or ast". So { dialect: 'cel', ast: ... } with no source is a valid expression by the contract, and every schema built on it inherits that — including AssignmentExpressionValueSchema (builtin-node-config.zod.ts), which narrows only the dialect.
The CEL engine cannot evaluate it. packages/formula/src/cel-engine.ts:1616-1626:
const source = expr.source;
if (typeof source !== 'string' || source.length === 0) {
// AST-only inputs: cel-js does not currently expose a public API to
// re-execute a parsed AST without re-serializing. ...
return { ok: false, error: { kind: 'parse', message: 'AST-only evaluation not yet supported; persist `source`' } };
validateExpression cannot report it either: toSource maps a missing source to '', and an empty source returns { ok: true } — "not authored".
Why it is worth a card rather than a comment
Every gate says yes and the run says no, which is the shape that costs an author a debugging session:
run time — throws AST-only evaluation not yet supported; persist source.
It is not confined to the new value role. The predicate path has the same property with a worse answer: evaluateCondition reduces an ast-only envelope to exprStr === '' and returns false — a gate that never opens, with nothing said (engine.ts, the "an absent / empty condition is not a predicate to evaluate" arm).
Reachability today
Low, and that is why this is a finding and not a stop-and-report. The compile step that would populate ast persists source as the canonical form (cel-engine's own comment, M9.1), so nothing in the repo emits an ast-only envelope today. The exposure is authored metadata and any future artifact normalization that starts trusting the refine.
Dispositions, for triage
Narrow the contract where the engine is the reader — require source on the schemas whose values are evaluated (a value/predicate slot), leaving ExpressionSchema itself alone for the persistence story it was written for. Refuses the shape at authoring, where the author can fix it.
Implement ast evaluation in the CEL engine (blocked on cel-js exposing AST re-execution, per its own comment).
Recommendation: 1, scoped to the evaluated slots — it is the only option that moves the refusal to where the author is, and it does not touch the persistence contract ExpressionSchema exists for.
Back-link: #15137, packages/services/service-automation/src/builtin/assignment-value-envelope.test.ts pins the run-time refusal (an ast-only envelope faults with the engine's own prescription, not a wrong value).
Found while implementing #15137 (the
value-role CEL envelope in theassignmentexecutor). Not fixed there:packages/specis not that card's surface, and the executor half handles it correctly by refusing loudly. Filed for triage.The seam
ExpressionSchema(packages/spec/src/shared/expression.zod.ts:75-89) declaressourceandastboth optional and refines "at least one ofsourceorast". So{ dialect: 'cel', ast: ... }with nosourceis a valid expression by the contract, and every schema built on it inherits that — includingAssignmentExpressionValueSchema(builtin-node-config.zod.ts), which narrows only the dialect.The CEL engine cannot evaluate it.
packages/formula/src/cel-engine.ts:1616-1626:validateExpressioncannot report it either:toSourcemaps a missingsourceto'', and an empty source returns{ ok: true }— "not authored".Why it is worth a card rather than a comment
Every gate says yes and the run says no, which is the shape that costs an author a debugging session:
AssignmentExpressionValueSchema.safeParse({ dialect: 'cel', ast: {...} })— succeeds.validateExpression('value', { dialect: 'cel', ast: {...} })—ok: true.registerFlow— registers.objectstack validate— silent.AST-only evaluation not yet supported; persist source.It is not confined to the new
valuerole. Thepredicatepath has the same property with a worse answer:evaluateConditionreduces anast-only envelope toexprStr === ''and returnsfalse— a gate that never opens, with nothing said (engine.ts, the "an absent / empty condition is not a predicate to evaluate" arm).Reachability today
Low, and that is why this is a finding and not a stop-and-report. The compile step that would populate
astpersistssourceas the canonical form (cel-engine's own comment, M9.1), so nothing in the repo emits anast-only envelope today. The exposure is authored metadata and any future artifact normalization that starts trusting the refine.Dispositions, for triage
sourceon the schemas whose values are evaluated (avalue/predicateslot), leavingExpressionSchemaitself alone for the persistence story it was written for. Refuses the shape at authoring, where the author can fix it.astevaluation in the CEL engine (blocked on cel-js exposing AST re-execution, per its own comment).value(it throws with the engine's own prescription attached); thepredicatepath's silentfalsewould still need fixing, and that one is a behaviour change.Recommendation: 1, scoped to the evaluated slots — it is the only option that moves the refusal to where the author is, and it does not touch the persistence contract
ExpressionSchemaexists for.Back-link: #15137,
packages/services/service-automation/src/builtin/assignment-value-envelope.test.tspins the run-time refusal (an ast-only envelope faults with the engine's own prescription, not a wrong value).Blocked-by: #15807
Generated by Claude Code