Skip to content

service-automation: evaluateCondition answers a silent false for a non-string predicate, and a non-string config.condition registers clean #15662

Description

@os-warren

Found while implementing #15572 (the envelope in a z.string() predicate slot), 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

#15572 is about the ledger-declared predicate slots (decision.conditions[].expression, screen.fields[].visibleWhen). This is the structural predicate surface next to it — config.condition on any node and edge.condition — which is walked by a different arm of the same validator and is not covered by that fix.

evaluateCondition derives its source as typeof expression === 'string' ? expression : (expression?.source ?? ''). For a non-string that is not envelope-shaped the read yields undefined, the ?? supplies '', and the empty-source arm returns false — the documented "an unauthored branch must not open" rule, applied to a value that was very much authored.

Measured, driven (worktree at origin/main d30ccb9 + the #15572 branch; AutomationEngine constructed directly and through LiteKernel)

engine.evaluateCondition(<value>, new Map()):

value result
42 false — silent
true false — silent
['a'] false — silent
{ source: 1 } throws TypeError: exprStr.trim is not a function

And through registerFlow, a decision node carrying config: { condition: 42 }:

  • registerFlowREGISTERED
  • executesuccess: true, no error, nothing said anywhere

edge.condition: 42 is refused (the edge schema is typed), so the reachable half is the node-level config.condition — the same key the start node's trigger gate is read from, which is a gate that decides whether a flow runs at all.

Why this is its own card

⛔ Not a defect, stated so nobody "fixes" it: a whitespace-only string condition returning false is consistent on both sides and is ruled correct.

Suggested shape (not a decision — triage's)

The refusal already exists and is exported: predicateSlotRefusal / PREDICATE_SLOT_STRING_REFUSAL in @objectstack/spec/automation, landed for #15572. The structural arm (check(...) in AutomationEngine.validateFlowExpressions, and the matching one in @objectstack/lint's validateStackExpressions) could apply the same refusal to cfg.condition / edge.condition, which would make one notion cover both arms. ⚠️ Blast radius unmeasured: config.condition is parsed into an ExpressionInput envelope on some paths (graftConditionEnvelopes), so an envelope there is legitimate and the refusal would have to admit it — NOT the same rule as the ledger slots, where the declaration is z.string().

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