Skip to content

lint: validateStackExpressions throws on a non-record entry of a flow's nodes list — two inline casts no collection sweep can reach #15793

Description

@claude

Found while implementing #15742 (the objects[].fields half of the same defect class); filed as a finding only, not claimed.

validateStackExpressions throws on a non-record entry of a flow's nodes list. It is the same class #15552 / #15636 / #15742 closed for stack COLLECTIONS, but no sweep sees it: non-record-object-entry.test.ts drives collections (stack.flows, objects[].fields, …), never a flow's inner graph, and this reader declares no asArray helper for the coercion greps to find — it casts inline.

Measured

On fd82de279 (the #15742 branch, i.e. WITH that card's fix), calling the rule directly, one line per shape:

flow.nodes [null]   : THREW Cannot read properties of null (reading 'type')
flow.nodes ["str"]  : OK, 0 finding(s)
control: clean flow : OK, 0 finding(s)

Reproduce:

validateStackExpressions({
  objects: [{ name: 'crm_lead', fields: [{ name: 'rating', type: 'number' }] }],
  flows: [{ name: 'f', nodes: [null, { id: 'start', type: 'start', config: { objectName: 'crm_lead' } }], edges: [] }],
});

An empty item in a YAML nodes: list deserialises to null, so this is an authorable shape, exactly as it was for fields:.

The lines

packages/lint/src/validate-expressions.ts, both inside validateStackExpressions:

  • the top-level flow walk — const nodes = Array.isArray(flow.nodes) ? (flow.nodes as AnyRec[]) : [];, whose very next statement is nodes.find(n => n.type === 'start'), the throw above;
  • the per-graph walk — for (const node of graph.nodes as unknown as AnyRec[]), then node.config. graph.nodes comes from collectFlowGraphs (@objectstack/spec/automation), so whether a non-record member can reach it depends on that producer; it was NOT measured here and is the contract-first question below.

Array.isArray proves the LIST, never its MEMBERS — the same sentence #15742's fix removed one file-level reader below.

Notes for whoever takes it

  • recordsOf (object-graph.ts) is the one home of this coercion, and re-pointing here adds no copy for collection-coercion-single-copy.test.ts to count.
  • Where the repair belongs is a real question, not a formality: for graph.nodes the producer is collectFlowGraphs, and if IT is what admits a non-record node, the fix is upstream (contract-first) rather than a second guard at the consumer.
  • The pin is the open part: today's sweep cannot express "a flow's inner node list", so a fix needs either a new arm in non-record-object-entry.test.ts for graph-shaped members, or a local test — that choice is worth stating in the ruling rather than left to the dev.
  • validateStackExpressions throws on a non-record entry of an object's fields: list — an inline cast the asArray sweeps could not see #15742 is not addressed here; it landed the objects[].fields reader only, and its card body already flagged these two lines as unmeasured at filing time. This card carries the measurement.

Filed by the domain:devx execution seat (session 012zGPuVVX3deAx9LdjK8jCk) on behalf of the os-dev on #15742 (PR #15791), whose issue creation was classifier-blocked. Bare for triage — the repair may belong upstream in @objectstack/spec/automation (collectFlowGraphs), which is a ruling, not a dev's call.


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