File: src/graph/traversal.ts
Lines: 506–509 (depth guard), 522 (container-children gate), 543 (incoming-edges gate)
Defect: getImpactRecursive uses two different sets to gate whether a node is re-processed:
- Container-children loop (L522):
if (childNode && !visited.has(childNode.id))
- Incoming-edges loop (L543):
if (sourceNode && !nodes.has(sourceNode.id))
The depth guard at L506–508 (if (currentDepth >= maxDepth || visited.has(nodeId)) return;) returns before visited.add(nodeId) at L509 when the depth limit fires. This means a node can be added to nodes (by its parent's loop) but absent from visited. When that node later appears as an incoming-edge source for a different node, the !nodes.has() gate at L543 skips it — and the corresponding edge is silently dropped from the result.
Failure trace:
- Graph: container
P has child method M. Both M and P have an incoming calls edge from Q.
getImpactRadius("Z", 2) where Z → P → M in the dependency chain; currentDepth=1 when reaching P.
getImpactRecursive(P, 2, 1, …): visited.add(P). Container loop: !visited.has(M) = true → nodes.set(M); recurse (M, 2, 1, …).
- Inside M's recursion:
visited.add(M). Incoming from Q: !nodes.has(Q) = true → nodes.set(Q); recurse (Q, 2, 2, …).
currentDepth=2 ≥ maxDepth=2 → return without visited.add(Q). Q is in nodes, not in visited.
- Back in P's incoming-edges loop (L541): P has incoming from Q (
Q→P).
!nodes.has(Q) = false (Q was added via M's processing). Gate fails → Q→P edge silently dropped.
- Impact result is missing edge Q→P; Q's path to P via its direct edge is invisible.
Expected fix:
Unify the two gates to use the same set. Since visited tracks "fully processed" nodes while nodes tracks "collected" nodes, the correct guard for both loops should be !nodes.has() (skip if already in result) OR !visited.has() (re-process if not yet traversed). Given the depth-guard asymmetry, the simplest fix is to ensure visited.add(nodeId) always runs before any early-return from the depth arm:
if (visited.has(nodeId)) return;
visited.add(nodeId); // mark before depth check
if (currentDepth >= maxDepth) return;
Then both loops can safely use !visited.has() for consistency, or keep the nodes check (since a visited node will also be in nodes).
File:
src/graph/traversal.tsLines: 506–509 (depth guard), 522 (container-children gate), 543 (incoming-edges gate)
Defect:
getImpactRecursiveuses two different sets to gate whether a node is re-processed:if (childNode && !visited.has(childNode.id))if (sourceNode && !nodes.has(sourceNode.id))The depth guard at L506–508 (
if (currentDepth >= maxDepth || visited.has(nodeId)) return;) returns beforevisited.add(nodeId)at L509 when the depth limit fires. This means a node can be added tonodes(by its parent's loop) but absent fromvisited. When that node later appears as an incoming-edge source for a different node, the!nodes.has()gate at L543 skips it — and the corresponding edge is silently dropped from the result.Failure trace:
Phas child methodM. BothMandPhave an incomingcallsedge fromQ.getImpactRadius("Z", 2)whereZ → P → Min the dependency chain;currentDepth=1when reaching P.getImpactRecursive(P, 2, 1, …):visited.add(P). Container loop:!visited.has(M)= true →nodes.set(M); recurse(M, 2, 1, …).visited.add(M). Incoming from Q:!nodes.has(Q)= true →nodes.set(Q); recurse(Q, 2, 2, …).currentDepth=2 ≥ maxDepth=2→ return withoutvisited.add(Q). Q is innodes, not invisited.Q→P).!nodes.has(Q)= false (Q was added via M's processing). Gate fails → Q→P edge silently dropped.Expected fix:
Unify the two gates to use the same set. Since
visitedtracks "fully processed" nodes whilenodestracks "collected" nodes, the correct guard for both loops should be!nodes.has()(skip if already in result) OR!visited.has()(re-process if not yet traversed). Given the depth-guard asymmetry, the simplest fix is to ensurevisited.add(nodeId)always runs before any early-return from the depth arm:Then both loops can safely use
!visited.has()for consistency, or keep thenodescheck (since a visited node will also be in nodes).