fix(analyze): make the cross-repo/directory surprise bonus match its reason

_top_level_dir() returned the whole path when it had no "/", so two
files at the scan root (a flat folder of notes, or README.md and
setup.py) got different "top-level dirs". Every such cross-file edge
(other than suppressed INFERRED calls/uses) received +2 and the reason
"connects across different repos/directories" in surprising_connections,
which is false.

- A path without "/" now maps to the scan root ("."). "." rather than ""
  keeps root files distinct from an absolute path outside the scan root,
  whose first component is "".
- Backslashes and a leading "./" are normalized first: build_from_json
  converts backslashes but keeps "./", and serve.py's _load_graph() and
  API callers can skip build_from_json altogether.
- The comparison key is (node "repo", top-level dir). merge-graphs and
  global add keep source_file repo-relative and tag nodes with "repo",
  so two repos can share directory names; single-repo graphs have no
  "repo" and are unaffected. On merged graphs a cross-repo edge now gets
  the bonus whenever the repos differ; before, it depended on whether
  the top-level directory names happened to differ.

Tests: the flat case (fails on d6eaa8a), merged graphs, backslash and
"./" paths, plus pins for what must not change (root vs subdirectory,
root vs out-of-root absolute path).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 05d6410535)
This commit is contained in:
neo1777 authored and safishamsi committed 2026-09-30 20:41:10 +01:00
1 parent 4dc58d17a7
commit a20c272d46
2 files changed
+87 -4

No files matched your search

+19 -4
View File
@@ -214,8 +214,19 @@ def _file_category(path: str) -> str:
def _top_level_dir(path: str) -> str:
"""Return the first path component - used to detect cross-repo edges."""
return path.split("/")[0] if "/" in path else path
"""Return the first path component - used to detect cross-repo edges.
A path with no "/" is a file at the scan root, so its top-level directory
is the root itself ("."), not the filename: otherwise every pair of
root-level files would look like it crosses repos/directories. "." keeps
root files distinct from an absolute path outside the root, whose first
component is "". Backslashes and a leading "./" are normalised first,
since a graph.json loaded without build_from_json can still carry them.
"""
path = path.replace("\\", "/")
while path.startswith("./"):
path = path[2:]
return path.split("/")[0] if "/" in path else "."
def _surprise_score(
@@ -263,8 +274,12 @@ def _surprise_score(
score += 2
reasons.append(f"crosses file types ({cat_u} ↔ {cat_v})")
# 3. Cross-repo bonus - different top-level directory
if _top_level_dir(u_source) != _top_level_dir(v_source) and not _suppress_structural:
# 3. Cross-repo bonus - different top-level directory. merge-graphs /
# global add keep source_file repo-relative and tag nodes with `repo`, so
# the repo is part of the key (absent on a single-repo graph).
top_u = (G.nodes[u].get("repo"), _top_level_dir(u_source))
top_v = (G.nodes[v].get("repo"), _top_level_dir(v_source))
if top_u != top_v and not _suppress_structural:
score += 2
reasons.append("connects across different repos/directories")
+68
View File
@@ -734,3 +734,71 @@ def test_find_import_cycles_no_cycles():
G.add_node(y_id, **y)
G.add_edge(x_id, y_id, relation="imports_from", source_file="x.ts", confidence="EXTRACTED")
assert find_import_cycles(G) == []
def test_surprise_score_flat_directory_is_not_cross_directory():
"""Two files in the same (flat) scan root share a directory.
_top_level_dir() used to return the whole filename when the path had no
"/", so every pair of root-level files looked like it crossed repos and
got +2 with the reason "connects across different repos/directories".
"""
G = nx.Graph()
G.add_node("a", label="handle", source_file="service.py", file_type="code")
G.add_node("b", label="Store", source_file="store.py", file_type="code")
G.add_edge("a", "b", relation="calls", confidence="EXTRACTED", weight=1.0, source_file="service.py")
nc = {"a": 0, "b": 0}
score_flat, reasons_flat = _surprise_score(G, "a", "b", G.edges["a", "b"], nc, "service.py", "store.py")
score_nested, reasons_nested = _surprise_score(G, "a", "b", G.edges["a", "b"], nc, "pkg/service.py", "pkg/store.py")
assert "connects across different repos/directories" not in reasons_flat
assert score_flat == score_nested
def test_surprise_score_root_file_vs_subdirectory_still_crosses():
"""A root-level file and a file under a subdirectory are in different top-level dirs."""
G = nx.Graph()
G.add_node("a", label="main", source_file="main.py", file_type="code")
G.add_node("b", label="Store", source_file="pkg/store.py", file_type="code")
G.add_edge("a", "b", relation="calls", confidence="EXTRACTED", weight=1.0, source_file="main.py")
nc = {"a": 0, "b": 0}
_, reasons = _surprise_score(G, "a", "b", G.edges["a", "b"], nc, "main.py", "pkg/store.py")
assert "connects across different repos/directories" in reasons
def test_surprise_score_root_file_vs_out_of_root_absolute_path_still_crosses():
"""_norm_source_file leaves a path outside the scan root absolute; its
first component is "". A root-level file must not collapse onto it."""
G = nx.Graph()
G.add_node("a", label="main", source_file="main.py", file_type="code")
G.add_node("b", label="Ext", source_file="/opt/vendor/ext.py", file_type="code")
G.add_edge("a", "b", relation="calls", confidence="EXTRACTED", weight=1.0, source_file="main.py")
nc = {"a": 0, "b": 0}
_, reasons = _surprise_score(G, "a", "b", G.edges["a", "b"], nc, "main.py", "/opt/vendor/ext.py")
assert "connects across different repos/directories" in reasons
def _cross_reasons(src_u, src_v, repo_u=None, repo_v=None):
G = nx.Graph()
G.add_node("a", label="A", source_file=src_u, file_type="code", **({"repo": repo_u} if repo_u else {}))
G.add_node("b", label="B", source_file=src_v, file_type="code", **({"repo": repo_v} if repo_v else {}))
G.add_edge("a", "b", relation="calls", confidence="EXTRACTED", weight=1.0, source_file=src_u)
_, reasons = _surprise_score(G, "a", "b", G.edges["a", "b"], {"a": 0, "b": 0}, src_u, src_v)
return "connects across different repos/directories" in reasons
def test_surprise_score_merged_graph_counts_different_repos_as_crossing():
"""merge-graphs / global add keep source_file repo-relative and tag each
node with `repo`, so two repos can share top-level directory names."""
assert _cross_reasons("server.py", "client.py", "svcA", "svcB")
assert _cross_reasons("src/x.py", "src/y.py", "svcA", "svcB")
assert not _cross_reasons("src/x.py", "src/y.py", "svcA", "svcA")
def test_surprise_score_normalizes_backslash_paths():
assert _cross_reasons("pkg\\a.py", "lib\\b.py")
assert not _cross_reasons("pkg\\a.py", "pkg\\b.py")
def test_surprise_score_ignores_leading_dot_slash():
assert _cross_reasons("./pkg/a.py", "b.py")
assert not _cross_reasons("./a.py", "b.py")