fix: deterministic GRAPH_REPORT on large graphs, stable edge node IDs, correct common-root inference

- analyze.py: add seed=42 to betweenness_centrality() — eliminates non-deterministic GRAPH_REPORT.md diffs on graphs >1000 nodes (#499)
- extract.py: fix common-root inference to stop at first diverging segment not sum of all matches (#502)
- extract.py: resolve root to absolute path; post-process file node IDs to project-relative after extraction so graph.json edge endpoints are stable across machines (#502)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-04-22 09:04:38 +01:00
co-authored by Claude Sonnet 4.6
parent f8fd8f8479
commit d9b2928da1
3 changed files with 32 additions and 7 deletions
+1 -1
View File
@@ -363,7 +363,7 @@ def suggest_questions(
# 2. Bridge nodes (high betweenness) → cross-cutting concern questions
if G.number_of_edges() > 0:
k = min(100, G.number_of_nodes()) if G.number_of_nodes() > 1000 else None
betweenness = nx.betweenness_centrality(G, k=k)
betweenness = nx.betweenness_centrality(G, k=k, seed=42)
# Top bridge nodes that are NOT file-level hubs
bridges = sorted(
[(n, s) for n, s in betweenness.items()
+30 -5
View File
@@ -3085,20 +3085,24 @@ def extract(paths: list[Path], cache_root: Path | None = None) -> dict:
_check_tree_sitter_version()
per_file: list[dict] = []
# Infer a common root for cache keys
# Infer a common root for cache keys (use first diverging segment, not sum of all matches)
try:
if not paths:
root = Path(".")
elif len(paths) == 1:
root = paths[0].parent
else:
common_len = sum(
1 for i in range(min(len(p.parts) for p in paths))
if len({p.parts[i] for p in paths}) == 1
)
min_parts = min(len(p.parts) for p in paths)
common_len = 0
for i in range(min_parts):
if len({p.parts[i] for p in paths}) == 1:
common_len += 1
else:
break
root = Path(*paths[0].parts[:common_len]) if common_len else Path(".")
except Exception:
root = Path(".")
root = root.resolve()
_DISPATCH: dict[str, Any] = {
".py": extract_python,
@@ -3168,6 +3172,27 @@ def extract(paths: list[Path], cache_root: Path | None = None) -> dict:
all_nodes.extend(result.get("nodes", []))
all_edges.extend(result.get("edges", []))
# Remap file node IDs from absolute-path-derived to project-relative so
# graph.json edge endpoints are stable across machines (#502)
id_remap: dict[str, str] = {}
for path in paths:
old_id = _make_id(str(path))
try:
new_id = _make_id(str(path.relative_to(root)))
except ValueError:
continue
if old_id != new_id:
id_remap[old_id] = new_id
if id_remap:
for n in all_nodes:
if n.get("id") in id_remap:
n["id"] = id_remap[n["id"]]
for e in all_edges:
if e.get("source") in id_remap:
e["source"] = id_remap[e["source"]]
if e.get("target") in id_remap:
e["target"] = id_remap[e["target"]]
# Add cross-file class-level edges (Python only - uses Python parser internally)
py_paths = [p for p in paths if p.suffix == ".py"]
if py_paths:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "graphifyy"
version = "0.4.26"
version = "0.4.27"
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
readme = "README.md"
license = { file = "LICENSE" }