Found while implementing #15793 (which repaired the two validate-expressions.ts casts). Filed as a finding only, not claimed.
#15793's ruling was to extend non-record-object-entry.test.ts with a graph-shaped arm rather than ship a local test, precisely because no sweep could express "a flow's inner node list". The arm was added — and on its first run it caught a reader in a different file that the card never named.
Measured
On 7c12e475, driving the whole AUTHORING_RULES table over flows[].nodes with a junk member beside a valid node:
flows[].nodes null threw=[lintFlowPatterns] invented=0
flows[].nodes undefined threw=[lintFlowPatterns] invented=0
flows[].nodes a string threw=[] invented=0
flows[].nodes a number threw=[] invented=0
flows[].nodes an array threw=[] invented=0
TypeError: Cannot read properties of null (reading 'type')
at lint-flow-patterns.ts:1430:39 (Array.find)
Reproduce:
lintFlowPatterns({
objects: [{ name: 'crm_account', fields: [{ name: 'name', type: 'text' }] }],
flows: [{ name: 'crm_flow', nodes: [null, { id: 'start', type: 'start', config: {} }], edges: [] }],
});
The lines
packages/lint/src/lint-flow-patterns.ts holds the same two spellings #15793 removed from validate-expressions.ts, three times over:
:1426 const nodes = Array.isArray(flow.nodes) ? (flow.nodes as AnyRec[]) : []; <- throws at :1430
:456 for (const node of graph.nodes as unknown as AnyRec[]) {
:1522 const graphNodes = graph.nodes as unknown as AnyRec[];
A fourth site is in packages/lint/src/flow-variable-scope.ts:
:225 for (const item of graph.nodes) { const flowNode = item as AnyRec;
if (typeof flowNode.id === 'string' ...) <- throws on a null member
Note collectFlowVariableNames guards flow.variables members three lines above with if (!item || typeof item !== 'object') continue; and does not guard graph.nodes — the guard exists in the same function, one loop over.
Reachability differs between the two, and that matters for the pin
lintFlowPatterns is reachable shallowly — the repro above, an ordinary flow. This is the live half.
collectFlowVariableNames is currently MASKED: every nested route into graph.nodes with a non-record member throws earlier inside packages/spec (see the sibling card on collectFlowGraphs). The one input that reaches it is a region nest exactly MAX_REGION_DEPTH (32) deep, where visit pushes the graph and returns before walking its members:
nesting 32 : OK, 33 graph(s); graphs whose nodes hold a NON-RECORD: 1
nesting 33 : OK, 33 graph(s); graphs whose nodes hold a NON-RECORD: 0
So it is real but only observable at the ceiling today. Fixing the spec-side card will UNMASK it at every depth.
Notes for whoever takes it
Found while implementing #15793 (which repaired the two
validate-expressions.tscasts). Filed as a finding only, not claimed.#15793's ruling was to extend
non-record-object-entry.test.tswith a graph-shaped arm rather than ship a local test, precisely because no sweep could express "a flow's inner node list". The arm was added — and on its first run it caught a reader in a different file that the card never named.Measured
On
7c12e475, driving the wholeAUTHORING_RULEStable overflows[].nodeswith a junk member beside a valid node:Reproduce:
The lines
packages/lint/src/lint-flow-patterns.tsholds the same two spellings #15793 removed fromvalidate-expressions.ts, three times over:A fourth site is in
packages/lint/src/flow-variable-scope.ts:Note
collectFlowVariableNamesguardsflow.variablesmembers three lines above withif (!item || typeof item !== 'object') continue;and does not guardgraph.nodes— the guard exists in the same function, one loop over.Reachability differs between the two, and that matters for the pin
lintFlowPatternsis reachable shallowly — the repro above, an ordinary flow. This is the live half.collectFlowVariableNamesis currently MASKED: every nested route intograph.nodeswith a non-record member throws earlier insidepackages/spec(see the sibling card oncollectFlowGraphs). The one input that reaches it is a region nest exactlyMAX_REGION_DEPTH(32) deep, wherevisitpushes the graph and returns before walking its members:So it is real but only observable at the ceiling today. Fixing the spec-side card will UNMASK it at every depth.
Notes for whoever takes it
recordsOf(object-graph.ts) is the one home of this coercion, as it was for lint: validateStackExpressions throws on a non-record entry of a flow's nodes list — two inline casts no collection sweep can reach #15793; re-pointing adds no copy forcollection-coercion-single-copy.test.tsto count.:1426needs the same treatment lint: validateStackExpressions throws on a non-record entry of a flow's nodes list — two inline casts no collection sweep can reach #15793 gave its twin: the coerced array must also be what is handed tocollectFlowGraphs, or the crash merely relocates intopackages/specrather than going away. That was measured on lint: validateStackExpressions throws on a non-record entry of a flow's nodes list — two inline casts no collection sweep can reach #15793 and is the non-obvious half.non-record-object-entry.test.tshas aflows[].nodesarm as of lint: validateStackExpressions throws on a non-record entry of a flow's nodes list — two inline casts no collection sweep can reach #15793, withlintFlowPatternsrecorded inRESIDUAL_THROWS. Fixing this card means deleting that row, and the test goes red until it does.