Summary
graphify path <fileA> <fileB> reports "No directed path found" for a real, direct, single-hop dependency between two files — not because of a direction-flip in a stored edge (that's #2309, fixed in 0.9.31), but because the edge needed to complete the route does not exist in either direction. contains edges only go file → symbol; there is no symbol → file edge, so a route like a.py --imports--> helper() [defined in b.py] can never reach b.py.
Repro
Given:
a.py does from b import helper and calls it
helper() is defined in b.py
The graph then has:
a.py --imports--> helper() (EXTRACTED)
b.py --contains--> helper() (EXTRACTED)
Both edges point into helper(). There is no edge from helper() back out to b.py (or to a.py), so nx.shortest_path over the digraph path builds (from raw contains/imports edges) can never find a.py -> helper() -> b.py, even though graphify explain a.py and graphify explain b.py both correctly show the real edges on either side.
Reproduced on a real repo (graphifyy 0.9.69, code-only extraction, ~22k nodes / 53k edges): a module doing from engine import get_bot and calling it. explain on both files is accurate. path fileA.py fileB.py → "No directed path found between...". --undirected doesn't help either — it returns a longer, wrong detour through an unrelated file instead of the trivial 2-hop route, because undirected search has no reason to prefer the real containment edge over any other 2-hop U-turn through a shared node.
Root cause
path's traversal graph is built directly from the graph's raw symbol-level contains/imports edges. contains is directionally asymmetric for reachability purposes (file → symbol only), so any file-to-file question that must route through a symbol node (the common case — most cross-file dependencies are "file A imports a symbol defined in file B") has no directed edge to complete the hop back out to the target file.
This is a different failure mode from #2309 (which was a rendering bug — the true direction existed in _src/_tgt but was displayed backwards, fixed in 0.9.31 by reading those markers). Here the edge to complete the route is structurally absent, so correct direction-reading doesn't help.
Existing correct pattern already in the codebase
analyze.find_import_cycles() solves the equivalent file-level problem correctly: it collapses symbol-level nodes to their parent file via the source_file attribute, and builds the file-level digraph from imports_from/re_exports edges — which carry a source_file attribute that resolves the true file→file direction — instead of from raw contains edges. path/query don't reuse this; they operate on the raw symbol graph.
Suggested fix
When both path endpoints resolve to file nodes (the common CLI usage — graphify path some/file.py other/file.py), build the traversal graph the same way find_import_cycles does (collapse to source_file, route over imports_from/re_exports), instead of over the raw symbol digraph. That would make the trivial "does A depend on B" case — probably the single most common use of path — actually work.
Environment
- graphifyy 0.9.69 (PyPI), Python 3.12
graphify extract . --code-only (no LLM backend configured)
- Graph: ~22k nodes / 53k edges
Happy to share a minimal synthetic repo reproducing this in isolation if useful — the example above is genericized from a real internal codebase.
Summary
graphify path <fileA> <fileB>reports "No directed path found" for a real, direct, single-hop dependency between two files — not because of a direction-flip in a stored edge (that's #2309, fixed in 0.9.31), but because the edge needed to complete the route does not exist in either direction.containsedges only go file → symbol; there is no symbol → file edge, so a route likea.py --imports--> helper() [defined in b.py]can never reachb.py.Repro
Given:
a.pydoesfrom b import helperand calls ithelper()is defined inb.pyThe graph then has:
Both edges point into
helper(). There is no edge fromhelper()back out tob.py(or toa.py), sonx.shortest_pathover the digraphpathbuilds (from rawcontains/importsedges) can never finda.py -> helper() -> b.py, even thoughgraphify explain a.pyandgraphify explain b.pyboth correctly show the real edges on either side.Reproduced on a real repo (graphifyy 0.9.69, code-only extraction, ~22k nodes / 53k edges): a module doing
from engine import get_botand calling it.explainon both files is accurate.path fileA.py fileB.py→ "No directed path found between...".--undirecteddoesn't help either — it returns a longer, wrong detour through an unrelated file instead of the trivial 2-hop route, because undirected search has no reason to prefer the real containment edge over any other 2-hop U-turn through a shared node.Root cause
path's traversal graph is built directly from the graph's raw symbol-levelcontains/importsedges.containsis directionally asymmetric for reachability purposes (file → symbol only), so any file-to-file question that must route through a symbol node (the common case — most cross-file dependencies are "file A imports a symbol defined in file B") has no directed edge to complete the hop back out to the target file.This is a different failure mode from #2309 (which was a rendering bug — the true direction existed in
_src/_tgtbut was displayed backwards, fixed in 0.9.31 by reading those markers). Here the edge to complete the route is structurally absent, so correct direction-reading doesn't help.Existing correct pattern already in the codebase
analyze.find_import_cycles()solves the equivalent file-level problem correctly: it collapses symbol-level nodes to their parent file via thesource_fileattribute, and builds the file-level digraph fromimports_from/re_exportsedges — which carry asource_fileattribute that resolves the true file→file direction — instead of from rawcontainsedges.path/querydon't reuse this; they operate on the raw symbol graph.Suggested fix
When both
pathendpoints resolve to file nodes (the common CLI usage —graphify path some/file.py other/file.py), build the traversal graph the same wayfind_import_cyclesdoes (collapse tosource_file, route overimports_from/re_exports), instead of over the raw symbol digraph. That would make the trivial "does A depend on B" case — probably the single most common use ofpath— actually work.Environment
graphify extract . --code-only(no LLM backend configured)Happy to share a minimal synthetic repo reproducing this in isolation if useful — the example above is genericized from a real internal codebase.