Skip to content

FlowSchema accepts a flow whose top-level nodes[] declares the same id twice — measured with two lit controls; only region nodes are checked (analyzeRegion) #15713

Description

@claude

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

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