Repository navigation
hook-style relative changed_paths corrupt incremental code graph #1348
Description
Summary
graphify.watch._rebuild_code(..., changed_paths=[...]) corrupts the graph when changed_paths contains repo-relative paths, which is the shape used by the installed post-commit hook.
With absolute changed paths, the same small repro preserves the graph. With relative changed paths, the rebuild removes the changed file's nodes and the cross-file import/call edges.
This makes hook-driven incremental updates unsafe for reverse impact / affected-test workflows, because incoming edges to changed code can disappear.
Why this appears public-facing
The installed hook parses GRAPHIFY_CHANGED into relative Path(...) values and calls:
_root = Path('.')
_rebuild_code(_root, changed_paths=changed, force=_force)So even though _rebuild_code is private, this path is reachable through graphify hook install.
Minimal repro
repo=$(mktemp -d /tmp/graphify-changed-path-rel.XXXXXX)
mkdir -p "$repo/src"
cat > "$repo/src/lib.ts" <<'TS'
export function answer() { return 42; }
TS
cat > "$repo/src/app.ts" <<'TS'
import { answer } from "./lib";
export function run() { return answer(); }
TS
graphify extract "$repo" --no-cluster --out "$repo" --max-workers 1
python3 - <<PY
import json
from pathlib import Path
p = Path("$repo") / "graphify-out" / "graph.json"
data = json.loads(p.read_text())
print("before", len(data.get("nodes", [])), len(data.get("links", data.get("edges", []))))
for e in data.get("links", data.get("edges", [])):
print(e.get("source"), "->", e.get("target"), e.get("relation"))
PY
# Simulate the hook-style relative changed path.
cat > "$repo/src/lib.ts" <<'TS'
export function answer() { return 43; }
TS
cd "$repo"
python3 - <<'PY'
from pathlib import Path
from graphify.watch import _rebuild_code
ok = _rebuild_code(Path('.'), changed_paths=[Path('src/lib.ts')], no_cluster=True, block_on_lock=True)
print('ok', ok)
PY
# If graphify is not importable from python3, run the same snippet from a Graphify checkout with `uv run python`.
python3 - <<PY
import json
from pathlib import Path
p = Path("$repo") / "graphify-out" / "graph.json"
data = json.loads(p.read_text())
print("after", len(data.get("nodes", [])), len(data.get("links", data.get("edges", []))))
for e in data.get("links", data.get("edges", [])):
print(e.get("source"), "->", e.get("target"), e.get("relation"))
PYObserved output
Before the incremental rebuild, the graph has both files and cross-file edges:
before 4 5
src_app -> src_lib imports_from
src_app -> src_lib_answer imports
src_app -> src_app_run contains
src_lib -> src_lib_answer contains
src_app_run -> src_lib_answer calls
After _rebuild_code(Path('.'), changed_paths=[Path('src/lib.ts')], ...), it is corrupted:
[graphify watch] Rebuilt (no clustering): 2 nodes, 1 edges
[graphify watch] graph.json updated in /tmp/graphify-changed-path-rel.../graphify-out
ok True
after 2 1
src_app -> src_app_run contains
The changed file (src/lib.ts) and the incoming import/call edges are gone.
Expected behavior
Relative changed paths should be normalized consistently against watch_path / project root before extraction and eviction.
For the repro above, the post-rebuild graph should still contain:
src_app -> src_lib imports_from
src_app -> src_lib_answer imports
src_app_run -> src_lib_answer calls
src_lib -> src_lib_answer contains
Notes
The same repro works when the changed path is passed as an absolute path. That points to path normalization / source-file normalization mismatch rather than TypeScript import resolution itself.
Activity
fix incremental no-cluster graph updates (#1350)
fix incremental no-cluster graph updates (Graphify-Labs#1350)