Summary
graphify.extract derives every node ID from path.stem (the extension-less basename). Two source files that share a basename therefore collapse into a single graph node — regardless of language or directory. This silently cross-wires unrelated code: edges from one file attach to another file's symbols.
Environment
- graphify (PyPI
graphifyy), current release as of 2026-07
- Python 3.14, Windows; the bug is language-agnostic, not OS-specific
Reproduce
Two files sharing the basename layout:
a/layout.py — a Python class Layout
b/layout.ts — an unrelated TypeScript module
from graphify.extract import extract
from pathlib import Path
r = extract([Path("a/layout.py"), Path("b/layout.ts")])
ids = [n["id"] for n in r["nodes"]]
print("collisions:", len(ids) - len(set(ids))) # > 0
Observed: both files map to the file node ID layout. The TS file's contains edges point at the Python Layout class/methods, and imports_from edges make the TS file appear to import Python modules.
Expected: each file gets a distinct node ID; edges stay within their own file.
Root cause
extract.py:
def _make_id(*parts: str) -> str:
combined = "_".join(p.strip("_.") for p in parts if p)
cleaned = re.sub(r"[^a-zA-Z0-9]+", "_", combined)
return cleaned.strip("_").lower()
# ...in each per-file extractor:
stem = path.stem # no extension, no path -> collides
file_nid = _make_id(stem)
_make_id has no path/extension awareness, so stem is the only disambiguator and it isn't unique across files.
Suggested fix
Add a path-aware stem and use it wherever a file-scoped ID is built:
def _stem_id(path: Path) -> str:
import hashlib
rel = str(path).replace("\\", "/")
h = hashlib.sha1(rel.encode("utf-8")).hexdigest()[:6]
suffix = path.suffix.lstrip(".") or "noext"
return f"{path.stem}_{suffix}_{h}" # layout.py -> layout_py_acdcd1
Replace stem = path.stem -> stem = _stem_id(path) in every per-file extractor (and the Go extractor), and update the Python import-resolver's file-node exclusion check to _make_id(_stem_id(path)).
Do NOT change the import-resolver's module-name keys (Path(src).stem, {p.stem: p for p in paths}) — those must stay extensionless because they match Python import module names (from layout import X), which have no extension.
Impact / migration note
The fix changes every code node's ID, so existing persisted graphs won't merge on the next incremental --update; a full rebuild is needed once. Worth calling out in the release notes.
Summary
graphify.extractderives every node ID frompath.stem(the extension-less basename). Two source files that share a basename therefore collapse into a single graph node — regardless of language or directory. This silently cross-wires unrelated code: edges from one file attach to another file's symbols.Environment
graphifyy), current release as of 2026-07Reproduce
Two files sharing the basename
layout:a/layout.py— a Python classLayoutb/layout.ts— an unrelated TypeScript moduleObserved: both files map to the file node ID
layout. The TS file'scontainsedges point at the PythonLayoutclass/methods, andimports_fromedges make the TS file appear to import Python modules.Expected: each file gets a distinct node ID; edges stay within their own file.
Root cause
extract.py:_make_idhas no path/extension awareness, sostemis the only disambiguator and it isn't unique across files.Suggested fix
Add a path-aware stem and use it wherever a file-scoped ID is built:
Replace
stem = path.stem->stem = _stem_id(path)in every per-file extractor (and the Go extractor), and update the Python import-resolver's file-node exclusion check to_make_id(_stem_id(path)).Do NOT change the import-resolver's module-name keys (
Path(src).stem,{p.stem: p for p in paths}) — those must stay extensionless because they match Python import module names (from layout import X), which have no extension.Impact / migration note
The fix changes every code node's ID, so existing persisted graphs won't merge on the next incremental
--update; a full rebuild is needed once. Worth calling out in the release notes.