Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions graphify/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,49 @@ def graph_has_legacy_ids(nodes: list, root: str | Path | None = None, sample: in
return False


def legacy_id_collisions(nodes: list, root: str | Path | None = None) -> int:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionlegacy_id_collisions()

fans out to 7 callees (efferent coupling); 8 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionlegacy_id_collisions()

fans out to 7 callees (efferent coupling); 8 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

"""How many pre-#1504 node IDs are AMBIGUOUS — one id claimed by more than one
source file. This is the harm the new scheme exists to prevent; a legacy id
that no other file claims resolves to exactly the node it always did.

`graph_has_legacy_ids` answers "was this graph built by the old extractor",
which is true of any graph predating the change and stays true no matter how
harmless the old ids turn out to be. Read-only consumers were printing a
rebuild nudge on that answer alone, on every single query. Measured on a
51k-node graph: 591 file nodes carried legacy ids and **none** of them
collided, so every query paid a warning for a hazard that had not occurred and
recommended a full re-extraction that would have changed nothing (#RANK1).

Callers should nudge on THIS being non-zero and stay quiet otherwise.
"""
from graphify.extractors.base import _file_stem
_r = str(root) if root is not None else None
claims: dict[str, set[str]] = {}
for node in nodes:
if not isinstance(node, dict):
continue
if str(node.get("source_location") or "") != "L1":
continue
if _has_global_id(node):
continue
nid = node.get("id")
sf = node.get("source_file")
if not nid or not isinstance(nid, str) or not sf:
continue
sf_norm = _norm_source_file(str(sf), _r) or str(sf)
rel = Path(sf_norm)
if _is_abs(sf_norm) or not rel.name:
continue
new_stem = make_id(_file_stem(rel))
if not new_stem:
continue
norm = _normalize_id(nid)
if norm == new_stem or norm.startswith(new_stem + "_"):
continue # already path-qualified
claims.setdefault(norm, set()).add(str(sf))
return sum(1 for files in claims.values() if len(files) > 1)


def _doc_twin_remap(nodes: list) -> dict[str, str]:
"""Map a markdown quick-scan's bare doc node ``<slug>`` to the semantic
``<slug>_doc`` node for the SAME file (#1799).
Expand Down
32 changes: 23 additions & 9 deletions graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1283,12 +1283,19 @@ def dispatch_command(cmd: str) -> None:
except TypeError:
G = json_graph.node_link_graph(_raw)
try:
from graphify.build import graph_has_legacy_ids as _legacy
if _legacy(_raw.get("nodes", [])):
# Nudge only when a legacy id is genuinely AMBIGUOUS (claimed by
# more than one file). Warning on "this graph predates #1504"
# alone fired on every query of every older graph, including the
# measured 51k-node case where 591 legacy ids collided zero times
# (#RANK1).
from graphify.build import legacy_id_collisions as _legacy_collisions
_collisions = _legacy_collisions(_raw.get("nodes", []))
if _collisions:
print(
"[graphify] note: this graph uses the pre-#1504 node-ID scheme; "
"rebuild with `graphify extract --force` to get path-qualified IDs "
"(fixes same-name-file collisions).",
f"[graphify] note: {_collisions} node ID(s) in this graph are "
"claimed by more than one file (pre-#1504 scheme); those nodes "
"may answer for the wrong file. Rebuild with "
"`graphify extract --force` for path-qualified IDs.",
file=sys.stderr,
)
except Exception:
Expand Down Expand Up @@ -2220,11 +2227,18 @@ def dispatch_command(cmd: str) -> None:
if have_label:
changed += 1
if changed:
# Clustering re-runs on every rebuild, so a corpus that changed at all
# produces a different community set — this is the normal case, not a
# fault. The affected names have ALREADY been refreshed deterministically
# from each community's hub by the lines above, so the old wording
# ("Run `graphify label`") named a required repair for work that was
# already done, on every single run (#RANK1). Report what happened and
# say plainly that the LLM pass is optional.
print(
f"[graphify] community set changed since labeling "
f"({len(existing_labels)} saved labels, {len(communities)} communities now; "
f"renamed {changed} community(ies) by their hub). "
f"Run `graphify label` to refresh names with the LLM.",
f"[graphify] renamed {changed} of {len(communities)} communities "
f"after re-clustering ({len(existing_labels)} saved labels); new "
f"names come from each community's hub. Optional: `graphify label` "
f"regenerates them with the LLM.",
file=sys.stderr,
)
elif no_label and not force_relabel:
Expand Down
Loading