Skip to content

spec/formula: ExpressionSchema accepts an ast-only envelope that no engine can evaluate — it validates, it registers, it faults at run time #15430

Description

@os-warren

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:

  • AssignmentExpressionValueSchema.safeParse({ dialect: 'cel', ast: {...} }) — succeeds.
  • validateExpression('value', { dialect: 'cel', ast: {...} })ok: true.
  • registerFlow — registers.
  • objectstack validate — silent.
  • 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

  1. 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.
  2. Implement ast evaluation in the CEL engine (blocked on cel-js exposing AST re-execution, per its own comment).
  3. Accept and document — leave the contract, and make both evaluators fail identically and loudly. service-automation: evaluate a value-role CEL envelope in the assignment executor and validate it at registerFlow — the executor half of #14149 (spec half landed in PR #15113) #15137 already does that half for value (it throws with the engine's own prescription attached); the predicate path's silent false would 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 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).

Blocked-by: #15807


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions