From 1201149cb6c661285e9bf0d62bd280eb77552771 Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Fri, 2 Oct 2026 22:29:36 +0530 Subject: [PATCH 1/2] Let path route through a contains edge back out to its file A contains edge only runs file to symbol. Reaching a symbol via an imports/calls/references hop had no edge back out to the file that defines it, so a file to file dependency that must route through a shared symbol (the common case for cross file dependencies) reported no path at all even though both halves of the route existed. Add the implied reverse hop to path's own directed traversal graph only, for routing purposes. The printed segment still recovers the real stored contains edge and its true direction from the graph, same as any other hop, so the output stays honest about what is actually stored. Fixes #3878. --- graphify/cli.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/graphify/cli.py b/graphify/cli.py index 717e517d10..06d9243b94 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -1660,6 +1660,20 @@ def dispatch_command(cmd: str) -> None: _dg.add_edges_from(sorted( (d.get("_src", u), d.get("_tgt", v)) for u, v, d in G.edges(data=True) )) + # A `contains` edge only runs file -> symbol; there is no + # stored edge back out to the containing file, so a route that + # reaches a symbol (via an `imports`/`calls`/`references` hop) + # can never continue on to the file that defines it, and a + # file-to-file dependency routed through a shared symbol finds + # no path at all even though both halves of the route exist + # (#3878). Add the implied reverse hop for traversal only — the + # printed segment still recovers the real stored `contains` + # edge and its true direction from G, same as any other hop. + _dg.add_edges_from(sorted( + (d.get("_tgt", v), d.get("_src", u)) + for u, v, d in G.edges(data=True) + if d.get("relation") == "contains" + )) path_nodes = _nx.shortest_path(_dg, src_nid, tgt_nid) except (_nx.NetworkXNoPath, _nx.NodeNotFound): if undirected: From 6a7a720894f934fce199286817b40cf633ec1c92 Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Fri, 2 Oct 2026 22:29:36 +0530 Subject: [PATCH 2/2] Add a regression test for routing through a contains edge Covers the exact shape from the issue: a file imports a symbol that a second file contains, with no direct file to file edge between them. Confirmed to fail against pre fix code (reports no path found despite both halves of the route existing). --- tests/test_path_cli.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_path_cli.py b/tests/test_path_cli.py index e7c969804f..d29ccc8def 100644 --- a/tests/test_path_cli.py +++ b/tests/test_path_cli.py @@ -269,6 +269,38 @@ def test_path_canonical_marker_graph_still_forward(monkeypatch, tmp_path, capsys assert "Beta <--calls [EXTRACTED]-- Alpha" in out +# ── #3878: a `contains` edge has no reverse hop back out to its file ──────── + +def _file_to_symbol_only_graph(tmp_path): + """`a.py` imports `helper()`, which `b.py` contains — but no edge runs + file-to-file directly, and `contains` only runs b.py -> helper().""" + data = { + "directed": False, "multigraph": False, "graph": {}, + "nodes": [ + {"id": "a", "label": "a.py", "source_file": "a.py"}, + {"id": "b", "label": "b.py", "source_file": "b.py"}, + {"id": "helper", "label": "helper()", "source_file": "b.py"}, + ], + "links": [ + {"source": "a", "target": "helper", "relation": "imports", "confidence": "EXTRACTED"}, + {"source": "b", "target": "helper", "relation": "contains", "confidence": "EXTRACTED"}, + ], + } + p = tmp_path / "graph.json" + p.write_text(json.dumps(data)) + return p + + +def test_path_routes_through_a_contains_edge_to_reach_the_file(monkeypatch, tmp_path, capsys): + """A file-to-file dependency that only closes through a contained symbol + must still resolve, not report no path despite both halves existing.""" + p = _file_to_symbol_only_graph(tmp_path) + out = _run(monkeypatch, p, "a.py", "b.py", capsys) + assert "Shortest path (2 hops):" in out + assert "a.py --imports [EXTRACTED]--> helper() <--contains [EXTRACTED]-- b.py" in out + assert "No directed path found" not in out + + def test_explain_direction_recovered_from_src_tgt_markers(monkeypatch, tmp_path, capsys): """#2309: explain's in/out classification must honor _src markers — an edge persisted as hub->spoke but truly spoke->hub is an IN edge of hub."""