Filed by the os-dev seat working #14964 (session session_01M59rPZZFzqhfMUPFqqZTkf), as the out-of-scope measurement that card's dispatch asked for. Observation-level, unassigned; the dispatch named finding as the label. ⛔ Not addressed by the #14964 PR, which is scoped to edges[] by ruling.
The gap
FlowSchema.safeParse() accepts a flow whose top-level nodes[] contains two nodes carrying the same id. Measured on origin/main a55efc6c1, FlowSchema imported from source, a four-node flow with nodes[1].id === nodes[2].id === 'n':
duplicate node ids → success: true parsed node ids: ["start","n","n","end"]
⭐ Two lit controls on the SAME schema instance, so the acceptance is a missing rule rather than a disabled validator:
control A (node missing its required `label`) → success: false path nodes.1.label
control B (unknown key on a node) → success: false code unrecognized_keys
(A first attempt used an invalid node type as the control and it did NOT fire — the node-type vocabulary is open by contract under ADR-0018, so that lever is not a control here. Recorded so nobody reuses it.)
Where uniqueness IS enforced, and where it is not
packages/spec/src/automation/control-flow.zod.ts analyzeRegion() (line 383 on that tree) pushes duplicate node id 'X' for the nodes of a region (loop / try_catch bodies) and is consulted at registerFlow(), so a region with a duplicate node id never runs.
- Nothing does the same for the flow's top-level
nodes[]: not FlowSchema (measured above), not @objectstack/lint's flow-pattern rules (lint-flow-patterns.ts builds a nodeIds set only to check edge endpoints), and a grep over packages/services/service-automation/src and packages/core/src finds no duplicate-node-id check outside analyzeRegion.
Why it matters
Every edge's source / target names a node by id, and the engine's traversal filters out-edges by source — with two nodes sharing an id, an edge from n is ambiguous and whichever node wins is decided by array order, silently. A designer, a BPMN export and a flow diff key on node ids the same way edge ids are keyed (#14964). The id space is hand-authored, so the next author picking a "free" id cannot tell it is taken.
Suggested shape (not proposed as an implementation)
The sibling of the #14964 ruling: a parse-time refinement on nodes[], error level, message naming the id and both positions, anchored on the later node. Whether region nodes and top-level nodes should share one id space (an edge from a top-level node into a region node is already a documented shape) is a decision the card owner should make explicitly; analyzeRegion today judges each region alone.
Census lead
Not measured for nodes. The #14964 edge-id census scanner (AST over packages/** and examples/**) can be pointed at nodes: [ … ] arrays with a one-line change; a zero without a lit control is not a reading.
Refs: #14964 (edge ids — ruled A, refinement on edges[]), hotcrm#1558 (the incident class).
Generated by Claude Code
Filed by the
os-devseat working #14964 (sessionsession_01M59rPZZFzqhfMUPFqqZTkf), as the out-of-scope measurement that card's dispatch asked for. Observation-level, unassigned; the dispatch namedfindingas the label. ⛔ Not addressed by the #14964 PR, which is scoped toedges[]by ruling.The gap
FlowSchema.safeParse()accepts a flow whose top-levelnodes[]contains two nodes carrying the sameid. Measured onorigin/maina55efc6c1,FlowSchemaimported from source, a four-node flow withnodes[1].id === nodes[2].id === 'n':⭐ Two lit controls on the SAME schema instance, so the acceptance is a missing rule rather than a disabled validator:
(A first attempt used an invalid node
typeas the control and it did NOT fire — the node-type vocabulary is open by contract under ADR-0018, so that lever is not a control here. Recorded so nobody reuses it.)Where uniqueness IS enforced, and where it is not
packages/spec/src/automation/control-flow.zod.tsanalyzeRegion()(line 383 on that tree) pushesduplicate node id 'X'for the nodes of a region (loop / try_catch bodies) and is consulted atregisterFlow(), so a region with a duplicate node id never runs.nodes[]: notFlowSchema(measured above), not@objectstack/lint's flow-pattern rules (lint-flow-patterns.tsbuilds anodeIdsset only to check edge endpoints), and a grep overpackages/services/service-automation/srcandpackages/core/srcfinds no duplicate-node-id check outsideanalyzeRegion.Why it matters
Every edge's
source/targetnames a node by id, and the engine's traversal filters out-edges bysource— with two nodes sharing an id, an edge fromnis ambiguous and whichever node wins is decided by array order, silently. A designer, a BPMN export and a flow diff key on node ids the same way edge ids are keyed (#14964). The id space is hand-authored, so the next author picking a "free" id cannot tell it is taken.Suggested shape (not proposed as an implementation)
The sibling of the #14964 ruling: a parse-time refinement on
nodes[],errorlevel, message naming the id and both positions, anchored on the later node. Whether region nodes and top-level nodes should share one id space (an edge from a top-level node into a region node is already a documented shape) is a decision the card owner should make explicitly;analyzeRegiontoday judges each region alone.Census lead
Not measured for nodes. The #14964 edge-id census scanner (AST over
packages/**andexamples/**) can be pointed atnodes: [ … ]arrays with a one-line change; a zero without a lit control is not a reading.Refs: #14964 (edge ids — ruled A, refinement on
edges[]), hotcrm#1558 (the incident class).Generated by Claude Code