File: src/graph/traversal.ts
Lines: 101 (inner loop visited check), 110–111 (nodes.set/queue.push), 69–77 (dequeue visited check and edge push)
Defect: The inner for loop in traverseBFS (L99–112) guards on visited.has(nextNodeId) (L101) before enqueuing. visited is only updated at L72 — inside the outer while body, after dequeuing. This means a node reachable from the current node via two different edges (e.g., two calls edges or a calls + references edge to the same target) is enqueued twice. When the queue is later drained: the first dequeue marks the node visited and pushes edge1 into edges; the second dequeue hits visited.has → continue, dropping edge2. The resulting Subgraph.edges array is incomplete.
Failure trace:
- Node A has two edges to B:
e1 (A→B, kind:calls) and e2 (A→B, kind:references).
- Processing A: adjacentEdges = [e1, e2].
- e1:
!visited.has("B") = true → nodes.set(B), queue.push({B, e1}).
- e2:
!visited.has("B") = true (B still not in visited) → nodes.set(B) (no-op), queue.push({B, e2}).
- Queue:
[{B,e1}, {B,e2}].
- Dequeue
{B,e1}: !visited.has("B") → visited.add("B"), edges.push(e1).
- Dequeue
{B,e2}: visited.has("B") → continue. e2 never pushed to edges.
- Final
edges is missing e2 — a real A→B relationship is invisible to callers.
Expected fix:
Track which node IDs have already been enqueued (separately from visited) and skip re-enqueueing the same node:
const queued = new Set<string>();
queued.add(startNode.id);
// …
for (const adjEdge of adjacentEdges) {
const nextNodeId = …;
if (visited.has(nextNodeId) || queued.has(nextNodeId)) continue;
…
queued.add(nextNodeId);
nodes.set(nextNode.id, nextNode);
queue.push({ node: nextNode, edge: adjEdge, depth: depth + 1 });
}
This ensures each node is enqueued exactly once (preserving BFS semantics) while still recording the first edge to it. If multi-edge completeness is required, edges should be collected separately from the queue and all edges to a node merged before or after traversal.
File:
src/graph/traversal.tsLines: 101 (inner loop visited check), 110–111 (
nodes.set/queue.push), 69–77 (dequeue visited check and edge push)Defect: The inner
forloop intraverseBFS(L99–112) guards onvisited.has(nextNodeId)(L101) before enqueuing.visitedis only updated at L72 — inside the outerwhilebody, after dequeuing. This means a node reachable from the current node via two different edges (e.g., twocallsedges or acalls+referencesedge to the same target) is enqueued twice. When the queue is later drained: the first dequeue marks the node visited and pushes edge1 intoedges; the second dequeue hitsvisited.has → continue, dropping edge2. The resultingSubgraph.edgesarray is incomplete.Failure trace:
e1(A→B, kind:calls) ande2(A→B, kind:references).!visited.has("B")= true →nodes.set(B),queue.push({B, e1}).!visited.has("B")= true (B still not in visited) →nodes.set(B)(no-op),queue.push({B, e2}).[{B,e1}, {B,e2}].{B,e1}:!visited.has("B")→visited.add("B"),edges.push(e1).{B,e2}:visited.has("B")→continue. e2 never pushed to edges.edgesis missing e2 — a real A→B relationship is invisible to callers.Expected fix:
Track which node IDs have already been enqueued (separately from
visited) and skip re-enqueueing the same node:This ensures each node is enqueued exactly once (preserving BFS semantics) while still recording the first edge to it. If multi-edge completeness is required, edges should be collected separately from the queue and all edges to a node merged before or after traversal.