Engineering Journal
Schema Editor
Schema Editor

The graph bug where nodes exist but nothing connects to them

2026-07-17

TLDR

A connectivity graph can contain perfectly good junction nodes and still be useless if edge registration never references them. We shipped exactly that: junctions were detected and stored, but wire endpoints only resolved to components, so any connection through a junction silently vanished. The fix was endpoint resolution with a fallback chain, plus union-find to make transitive connectivity a first-class artifact.

The bug class

Building a graph in two passes: pass one creates nodes, pass two creates edges. The passes agree on node identity only by convention (a shared key format, a coordinate string, an id scheme). When pass two's lookup logic covers fewer cases than pass one's creation logic, you get orphan nodes: present in nodes, absent from every edge. The graph inspects fine. Traversals are just quietly wrong.

The concrete instance

Our diagram analyzer detects junction dots where three or more wires meet:

epMap.forEach((wireIds, key) => {
  if (wireIds.length >= 3) {
    nodes.set(jid, { kind: 'junction', x, y, degree: wireIds.length });
    adjacency.set(jid, []);   // created... and never touched again
  }
});

Then edge registration resolved each wire endpoint like this:

const fromNode = portMatches.find(nearEndpoint)?.compId || null;

Components only. A wire ending at a junction got from: null, contributed nothing to adjacency, and the junction's empty adjacency list stayed empty forever. Hovering a wire highlighted its two directly-attached components and nothing beyond any T-junction. Users read that as "the analysis is broken," and they were right.

Why this happens easily

The two passes were written at different times for different features. Junction detection was added for rendering (draw the dot). Edge registration predated it and was never revisited. Nothing forces the passes to agree: no type system connects "node kinds that exist" to "node kinds that edges can reference," and no test asserted that a junction-mediated connection traverses.

The fix

Endpoint resolution became a fallback chain that knows about every node kind:

const resolve = (ep) =>
  portMatches.find(p => near(p, ep))?.compId ||
  junctionAt.get(coordKey(ep)) || null;

And rather than trusting pairwise adjacency for transitive questions, we added union-find over canonical endpoints to compute nets (maximal electrically-connected sets) directly:

wires.forEach(w => union(key(w.endpoints[0]), key(w.endpoints[1])));

Wires sharing an endpoint merge; chains and junction fans merge transitively for free. Each wire stores a reference to its net, and highlight queries became "light up everything in wire.net" with no traversal at all.

Preventing the class

Two habits close this hole. First, make node resolution a single shared function used by both passes, so a new node kind added to creation forces a decision at resolution. Second, test the graph with a query, not an inspection: "these two elements connected through an intermediary report as connected" catches orphan nodes; "the nodes map contains a junction" does not.

The lesson

A graph is not its node set. It is its reachability. Any code that creates nodes owes an answer to the question "which edge-building path can produce a reference to this node?" If the answer is none, you have built a cache of dots, and every feature downstream of connectivity will lie with confidence.

Read this post in the full Engineering Journal →