You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Scan root silently changes the graph: src-layout project loses most 'imports' edges, same node count, no warning #2072
The same files produce a different graph depending on which directory you point graphify at. Node count is identical, so nothing looks wrong, and no warning is printed.
For a src-layout project, scanning the repository root instead of the package root silently drops most imports edges. In my real case that was 219 edges out of 2,236, about 10%.
Minimal reproduction
Four files in a package mypkg, using absolute imports (from mypkg.core import Engine), which is what a src-layout project looks like on disk.
mre/
direct/ <- scan THIS: package root
mypkg/
__init__.py from mypkg.core import Engine
core.py class Engine
app.py from mypkg.core import Engine; import mypkg.helpers as helpers
helpers.py from mypkg.core import Engine
nested/
src/ <- identical bytes, one level deeper
mypkg/ ...
nested/src/ is a byte-identical copy of direct/, verified with diff -rq.
Run the same command in each directory, graphify 0.9.22, no API key set:
Same signature at real-project scale: 34 files, 927 nodes in both cases, 2,236 edges vs 2,016, with the gap being 197 imports, 17 imports_from and 5 re_exports. Every other relation matched exactly, and calls differed by one.
Why this is easy to hit and hard to notice
Pointing at a repository root is the natural first thing to do, and it's what the README's graphify . implies.
The node count is unchanged, so the graph looks complete.
No warning is emitted. I grepped the degraded run's output for warn, unresolved, could not and skip and got zero lines.
The ranking changes. In my real project, god-nodes on the correct graph reports A (40), B (39), C (38). On the degraded graph it reports C (37), B (33), A (31). The top three are the same three nodes in a different order, so a reader draws a different conclusion about which component is most central, from a graph that gives no sign of being incomplete.
Point 4 is the reason I'm filing this rather than just changing my command. A silently incomplete graph that still answers questions is worse than one that fails.
Suggestions
Any one of these would have saved me the investigation:
When a module-level import target can't be resolved to a node in the corpus, count them and print a summary line, e.g. 197 import edge(s) unresolved — is the scan root the package root?
Detect a single src/ (or lib/) directory at the scan root containing the package and either descend or warn.
Document in the README that the scan root should be the directory imports resolve against, not necessarily the repository root.
Happy to test a patch against the real 34-file project if useful.
Independent corroboration on a different OS and a different layout, plus two findings that extend this: the exact import form that gets dropped, and a downstream consequence on calls edges that makes affected return silent false negatives.
Environment: graphifyy 0.9.22, Python 3.13.7, Windows 11. Layout is a monorepo (projects/<ticket>/<pkg>/…), not src-layout — so this isn't limited to a src/ directory.
The discriminator: module-object import vs symbol import
Your table shows imports -3 and imports_from +1, which I think is this mechanism. It can be isolated to two adjacent lines in a single file.
pkg/capture.py:
frompkgimportgate# module-object importfrompkg.fetchimportFetcher# symbol importdefrun():
rows=Fetcher().get()
returngate.validate(rows) # call via module attribute
with pkg/gate.py defining validate() and pkg/fetch.py defining Fetcher.
Same four files, extracted twice, differing only in depth below the scan root (graphify extract . --code-only):
The symbol import and its call survive the nesting; the module-object import and its call do not. Node count is 10 in both runs, as you found.
Downstream: the calls edge goes too, and affected then lies
This is the part I'd flag as worse than the ranking distortion. Losing capture --imports--> gate also costs run --calls--> gate.validate, because the call is written gate.validate(...) and gate no longer resolves. The validate node survives, so nothing looks missing — it just has no inbound edge other than contains from its own file.
Consequence on a real repo of mine (95 files, 749 nodes, 1287 edges, scanned from repo root):
$ graphify affected "projects_moneydj_capture_etf_capture_gate_validate" --graph graph.json --depth 2
Affected nodes for validate()
Depth: 2
No affected nodes found.
Ground truth is two call sites — etf_capture/capture.py:81 and a verification script that imports the module and calls m_gate.validate(...). Asking at file level (…_etf_capture_gate) also returns No affected nodes found, despite from etf_capture import gate on line 12.
142 of the 749 nodes (19%) ended up with zero inbound edges in that run.
affected is the reverse-dependency/impact-analysis entry point, so a false negative reads as "nothing depends on this, safe to change." Unlike the god-nodes reordering, there's no plausible-but-wrong ranking to be suspicious of — the answer is empty and definitive.
diagnose multigraph doesn't catch it
Worth noting for the "no warning is emitted" point in your issue — the built-in diagnostic reports the degraded graph as fully clean:
It checks for edge collapse, not edge absence, so running it after a degraded extraction actively increases confidence in the wrong direction.
On your suggested fixes
The unresolved-import counter would have caught this for me too. One addition: it would be worth counting unresolved call targets separately, since in this failure mode each dropped module import silently takes an unknown number of calls edges with it, and that count is what determines whether affected / path can be trusted.
Happy to run a patch against either the 4-file MRE or the 95-file repo.
Fixed in v0.9.23: Python absolute imports now resolve against nested package roots (detected via the __init__.py chain) and import edges are repointed to the real file nodes, so a src-layout project yields the same imports/imports_from edges whether scanned from the repo root or from src/. https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.23
Activity
Kyo14363 commentedon Jul 21, 2026
Independent corroboration on a different OS and a different layout, plus two findings that extend this: the exact import form that gets dropped, and a downstream consequence on
callsedges that makesaffectedreturn silent false negatives.Environment: graphifyy 0.9.22, Python 3.13.7, Windows 11. Layout is a monorepo (
projects/<ticket>/<pkg>/…), not src-layout — so this isn't limited to asrc/directory.The discriminator: module-object import vs symbol import
Your table shows
imports -3andimports_from +1, which I think is this mechanism. It can be isolated to two adjacent lines in a single file.pkg/capture.py:with
pkg/gate.pydefiningvalidate()andpkg/fetch.pydefiningFetcher.Same four files, extracted twice, differing only in depth below the scan root (
graphify extract . --code-only):projects/moneydj-capture/pkg/, scan root = repo top)capture --imports_from--> fetchrun --calls--> Fetchercapture --imports_from--> gaterun --calls--> gate.validateThe symbol import and its call survive the nesting; the module-object import and its call do not. Node count is 10 in both runs, as you found.
Downstream: the
callsedge goes too, andaffectedthen liesThis is the part I'd flag as worse than the ranking distortion. Losing
capture --imports--> gatealso costsrun --calls--> gate.validate, because the call is writtengate.validate(...)andgateno longer resolves. Thevalidatenode survives, so nothing looks missing — it just has no inbound edge other thancontainsfrom its own file.Consequence on a real repo of mine (95 files, 749 nodes, 1287 edges, scanned from repo root):
Ground truth is two call sites —
etf_capture/capture.py:81and a verification script that imports the module and callsm_gate.validate(...). Asking at file level (…_etf_capture_gate) also returnsNo affected nodes found, despitefrom etf_capture import gateon line 12.142 of the 749 nodes (19%) ended up with zero inbound edges in that run.
affectedis the reverse-dependency/impact-analysis entry point, so a false negative reads as "nothing depends on this, safe to change." Unlike thegod-nodesreordering, there's no plausible-but-wrong ranking to be suspicious of — the answer is empty and definitive.diagnose multigraphdoesn't catch itWorth noting for the "no warning is emitted" point in your issue — the built-in diagnostic reports the degraded graph as fully clean:
It checks for edge collapse, not edge absence, so running it after a degraded extraction actively increases confidence in the wrong direction.
On your suggested fixes
The unresolved-import counter would have caught this for me too. One addition: it would be worth counting unresolved call targets separately, since in this failure mode each dropped module import silently takes an unknown number of
callsedges with it, and that count is what determines whetheraffected/pathcan be trusted.Happy to run a patch against either the 4-file MRE or the 95-file repo.
fix(extract): resolve Python imports regardless of scan root (src-lay…
chore: bump to 0.9.23; changelog for #2062/#2074/#2073/#2068/#2072; d…
fix(extract): harden #2072 src-layout resolution (adversarial-review …
safishamsi commentedon Jul 21, 2026
Fixed in v0.9.23: Python absolute imports now resolve against nested package roots (detected via the
__init__.pychain) and import edges are repointed to the real file nodes, so a src-layout project yields the sameimports/imports_fromedges whether scanned from the repo root or fromsrc/. https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.23Merge upstream/v8 into v8-custom (0.9.20 -> 0.9.24)
Squashed commit of the following:
fix(extract): resolve Python imports regardless of scan root (src-lay…
chore: bump to 0.9.23; changelog for Graphify-Labs#2062/Graphify-Labs…
fix(extract): harden Graphify-Labs#2072 src-layout resolution (advers…
merge: backport upstream Python cross-file import fixes (Graphify-Lab…
fix(python): resolve submodule imports inside PEP 420 namespace packa…
fix(python): resolve submodule imports inside PEP 420 namespace packages
fix(python): resolve submodule imports inside PEP 420 namespace packages
fix(python): resolve nested scan-root imports via namespace projection (
fix(python): resolve nested scan-root imports via namespace projection (
fix(python): resolve nested scan-root imports via namespace projection (
fix(python): let package imports pass a same-named type stub
fix(python): park calls from decorated functions and methods; refuse …