Files
Safi 91f4d120b6 fix worktree hook crash, prune isolated code nodes, restore rationale-node prohibition
- hooks.py: use git rev-parse --git-path hooks so install/status/uninstall
  work in linked worktrees where .git is a file not a directory (fixes #865)
- build.py: prune degree-0 code nodes after graph assembly to remove
  bundled/synthetic symbols with no connections (fixes #728)
- skill.md: restore explicit rule prohibiting file_type:"rationale" nodes
  and remind model to store rationale as an attribute instead (fixes #751)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:03:21 +01:00

52 lines
2.4 KiB
Python

# assemble node+edge dicts into a NetworkX graph, preserving edge direction
from __future__ import annotations
import sys
import networkx as nx
from .validate import validate_extraction
def build_from_json(extraction: dict) -> nx.Graph:
errors = validate_extraction(extraction)
# Dangling edges (stdlib/external imports) are expected - only warn about real schema errors.
real_errors = [e for e in errors if "does not match any node id" not in e]
if real_errors:
print(f"[graphify] Extraction warning ({len(real_errors)} issues): {real_errors[0]}", file=sys.stderr)
G = nx.Graph()
for node in extraction.get("nodes", []):
G.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
node_set = set(G.nodes())
for edge in extraction.get("edges", []):
src, tgt = edge["source"], edge["target"]
if src not in node_set or tgt not in node_set:
continue # skip edges to external/stdlib nodes - expected, not an error
attrs = {k: v for k, v in edge.items() if k not in ("source", "target")}
# Preserve original edge direction - undirected graphs lose it otherwise,
# causing display functions to show edges backwards.
attrs["_src"] = src
attrs["_tgt"] = tgt
G.add_edge(src, tgt, **attrs)
hyperedges = extraction.get("hyperedges", [])
if hyperedges:
G.graph["hyperedges"] = hyperedges
# Strip degree-0 code nodes — they are bundled/synthetic symbols with no
# connections and only inflate god-node centrality and clustering noise.
# Document, paper, and image nodes are kept even when isolated since they
# may be leaf concepts intentionally referenced by the skill.
isolated_code = [
n for n in list(G.nodes())
if G.degree(n) == 0 and G.nodes[n].get("file_type") == "code"
]
G.remove_nodes_from(isolated_code)
return G
def build(extractions: list[dict]) -> nx.Graph:
"""Merge multiple extraction results into one graph."""
combined: dict = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0}
for ext in extractions:
combined["nodes"].extend(ext.get("nodes", []))
combined["edges"].extend(ext.get("edges", []))
combined["input_tokens"] += ext.get("input_tokens", 0)
combined["output_tokens"] += ext.get("output_tokens", 0)
return build_from_json(combined)