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
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
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.
→ 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
It is in @objectstack/formula, not in automation: validateExpression is the shared parse every predicate/value slot in the platform goes through, so the guard belongs at that entry, once.
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).
Found while implementing #15572, outside that card's surface and deliberately not fixed there. Filed by the
os-devexecution seat, session01XpTx2tbq3pZRYAdoGt6E6Y. ⛔domain:*, type and priority are triage's — this seat does not produce them.The gap
validateExpression(role, input)acceptsstring | { dialect?, source? }. When the input is an object whosesourceis not a string it calls.trim()on it unguarded, atpackages/formula/src/validate.ts:561, and a rawTypeErrorescapes.That matters because the caller is a validator whose whole contract is to collect located findings.
AutomationEngine.validateFlowExpressionsbuilds afailures[]list and throws one assembled, attributed error naming the flow, the node, the slot and the source (ADR-0032 §1d). ATypeErrorthrown from insidecheck()bypasses all of it:registerFlowdies with an internal message that names neither the flow nor the node.Measured, driven (worktree at
origin/maind30ccb9)→ throws
TypeError: source.trim is not a function, stack top:The same shape reaches the run-time door too:
engine.evaluateCondition({ source: 1 }, new Map())throwsTypeError: exprStr.trim is not a function.Why this is its own card
@objectstack/formula, not in automation:validateExpressionis the shared parse every predicate/value slot in the platform goes through, so the guard belongs at that entry, once.decisioncondition accepts a CEL envelope that neither validator can see — a malformed one evaluates tofalseSILENTLY at run time and takes the wrong branch #15572 (this seat's, in flight) refuses a non-string in the ledger-declared predicate slots beforevalidateExpressionis ever called, so on those slots this crash is now unreachable — but every other caller ofvalidateExpression(validation rules, sharing rules, hooks, page visibility, the structuralconfig.conditionarm) still reaches it with whatever the metadata holds.evaluateConditionanswers a silentfalsefor a non-string predicate, and a non-stringconfig.conditionregisters clean #15662 is the silentfalsehalf of the same non-string family.Suggested shape (not a decision — triage's)
Guard the read at the entry (
typeof source === 'string'before.trim()) and return the refusal through the normalerrors[]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.