Skip to content

spec/automation: FlowEdgeSchema.condition still accepts an envelope the engine cannot evaluate (ast-only, whitespace-only source) — the evaluated-slot rule of #15430 has not reached the edge condition #15807

Description

@claude

Split out of #15430 by its os-dev seat (PM session session_01M59rPZZFzqhfMUPFqqZTkf): the half of that card this seat could not execute inside its file surface. Filed as a sub-issue of #15430; no assignee; domain:*, type and priority are triage's.

Blocked-by: #15792 (in flight — structuralConditionRefusal admits an ast-carrying envelope on purpose with a rec.ast !== undefined clause naming #15430; that admission is the line to revisit together with this narrowing, see #15430's comment 5550509137).

The remaining evaluated slot

The #15430 PR narrows the ledger's value slot (assignment.assignments.*, AssignmentExpressionValueSchema) onto the new EvaluatedExpressionSchema (packages/spec/src/shared/expression.zod.ts): source required and non-blank after trimming, one rule, one message (EVALUATED_EXPRESSION_SOURCE_REQUIRED). The ledger's predicate slots (screen.fields[].visibleWhen, decision.conditions[].expression) are typed z.string() and refuse every envelope already (#15572's predicateSlotRefusal), so nothing to narrow there.

One evaluated predicate surface IS typed by an expression envelope and is not in the ledger (it is a structural surface, "deliberately not listed"): FlowEdgeSchema.condition: ExpressionInputSchema.optional() (packages/spec/src/automation/flow.zod.ts, the condition key of the edge schema). evaluateCondition reads it at run time (engine.ts, the edge.condition walk) as expression.source ?? '', so an ast-only envelope authored there lands in the empty-source arm and answers a silent false — the quiet half of #15430's class, measured by #15662's seat (#15430 comment 5550509137: { dialect: 'cel', ast: { kind: 'const', value: true } } through evaluateCondition answers false; registration says nothing). A whitespace-only source there is the same seam through the other key.

start.config.condition (the trigger gate, read by the same arm) has no Zod schema at all — the start node's config is an open record — so there is no schema to narrow for it; that surface belongs with #15662 / #15792's structural refusal.

What this sub-issue does

  • packages/spec/src/automation/flow.zod.ts: type FlowEdgeSchema.condition with an evaluated input form — the bare-string shorthand still normalises to { dialect: 'cel', source }, and the envelope arm composes EvaluatedExpressionSchema instead of ExpressionSchema (an EvaluatedExpressionInputSchema sibling beside ExpressionInputSchema in shared/expression.zod.ts is the one-spelling way to say it). ExpressionSchema / ExpressionInputSchema themselves stay the persistence contract.
  • Reproduction pins on FlowEdgeSchema for both spellings (ast-only, whitespace-only source) asserting issue code / path / message substance, the accepted envelope unchanged, and the control that ExpressionInputSchema still accepts both.
  • Consumer readings: service-automation (registerFlow on such an edge), lint (objectstack validate), cli.
  • Revisit service-automation: refuse a structural flow condition that is neither CEL text nor an expression #15792's rec.ast !== undefined admission in the same PR (it exists only because the spec still admitted the shape).
  • Changeset: @objectstack/spec minor, BREAKING banner, ADR-0087 disposition (the spec/formula: ExpressionSchema accepts an ast-only envelope that no engine can evaluate — it validates, it registers, it faults at run time #15430 census found zero in-repo instances of either spelling on any evaluated slot; re-run it for edges).

Why not in the #15430 PR: flow.zod.ts was held by #15716 when the card was dispatched (landed since as 52804cd), and the structural-condition surface is being changed by #15792 in the same window; landing two refusal rules on one surface from two lanes at once is how a consumer ends up with two notions of "malformed".


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