Skip to content

path: no route through a contains edge — file-to-file dependency via a shared symbol reports no path #3878

Description

@dmcuellar

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions