Skip to content

formula: validateExpression throws a raw TypeError: source.trim is not a function, so a bad condition crashes registerFlow with an internal message instead of a located refusal #15663

Description

@os-warren

Found while implementing #15572, outside that card's surface and deliberately not fixed there. Filed by the os-dev execution seat, session 01XpTx2tbq3pZRYAdoGt6E6Y. ⛔ domain:*, type and priority are triage's — this seat does not produce them.

The gap

validateExpression(role, input) accepts string | { dialect?, source? }. When the input is an object whose source is not a string it calls .trim() on it unguarded, at packages/formula/src/validate.ts:561, and a raw TypeError escapes.

That matters because the caller is a validator whose whole contract is to collect located findings. AutomationEngine.validateFlowExpressions builds a failures[] list and throws one assembled, attributed error naming the flow, the node, the slot and the source (ADR-0032 §1d). A TypeError thrown from inside check() bypasses all of it: registerFlow dies with an internal message that names neither the flow nor the node.

Measured, driven (worktree at origin/main d30ccb9)

engine.registerFlow('crash', { … nodes: [
  { id: 'start', type: 'start', label: 'S', config: { condition: { source: { nested: 1 } } } },
  { id: 'end',   type: 'end',   label: 'E' } ] … })

→ throws TypeError: source.trim is not a function, stack top:

at validateExpression (packages/formula/src/validate.ts:561:15)
at check                (packages/services/service-automation/src/engine.ts:7078:28)
at AutomationEngine.validateFlowExpressions (…/engine.ts:7123:17)

The same shape reaches the run-time door too: engine.evaluateCondition({ source: 1 }, new Map()) throws TypeError: exprStr.trim is not a function.

Why this is its own card

Suggested shape (not a decision — triage's)

Guard the read at the entry (typeof source === 'string' before .trim()) and return the refusal through the normal errors[] channel so callers keep their located reporting, rather than each caller wrapping the call in a try/catch — the second shape is the tolerant-consumer pattern PD #12 forbids.

⚠️ Un-measured: whether any current caller RELIES on the throw (nothing in the automation package does; the rest of the callers were not swept).

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