diff --git a/graphify/hooks.py b/graphify/hooks.py index e921155b..9341b854 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -61,7 +61,13 @@ fi """ + _PYTHON_DETECT + """ export GRAPHIFY_CHANGED="$CHANGED" -$GRAPHIFY_PYTHON -c " + +# Run rebuild detached so git commit returns immediately. +# Full repo rebuilds can take hours; blocking the post-commit hook stalls the shell. +_GRAPHIFY_LOG="${HOME}/.cache/graphify-rebuild.log" +mkdir -p "$(dirname "$_GRAPHIFY_LOG")" +echo "[graphify hook] launching background rebuild (log: $_GRAPHIFY_LOG)" +nohup $GRAPHIFY_PYTHON -c " import os, sys from pathlib import Path @@ -79,7 +85,8 @@ try: except Exception as exc: print(f'[graphify hook] Rebuild failed: {exc}') sys.exit(1) -" +" > "$_GRAPHIFY_LOG" 2>&1 < /dev/null & +disown 2>/dev/null || true # graphify-hook-end """ @@ -111,8 +118,10 @@ GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) [ -f "$GIT_DIR/CHERRY_PICK_HEAD" ] && exit 0 """ + _PYTHON_DETECT + """ -echo "[graphify] Branch switched - rebuilding knowledge graph (code files)..." -$GRAPHIFY_PYTHON -c " +_GRAPHIFY_LOG="${HOME}/.cache/graphify-rebuild.log" +mkdir -p "$(dirname "$_GRAPHIFY_LOG")" +echo "[graphify] Branch switched - launching background rebuild (log: $_GRAPHIFY_LOG)" +nohup $GRAPHIFY_PYTHON -c " from graphify.watch import _rebuild_code from pathlib import Path import sys @@ -121,7 +130,8 @@ try: except Exception as exc: print(f'[graphify] Rebuild failed: {exc}') sys.exit(1) -" +" > "$_GRAPHIFY_LOG" 2>&1 < /dev/null & +disown 2>/dev/null || true # graphify-checkout-hook-end """ diff --git a/graphify/watch.py b/graphify/watch.py index 1e07bd39..c77572a4 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -60,20 +60,25 @@ def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False) -> bool: result = extract(code_files, cache_root=watch_root) # Preserve semantic nodes/edges from a previous full run. - # AST-only rebuild replaces code nodes; doc/paper/image nodes are kept. + # AST-only rebuild replaces nodes for changed files; everything else is kept. + # Filter by node ID membership in the new AST output, not by file_type — + # INFERRED/AMBIGUOUS nodes extracted from code files also carry file_type="code" + # and would be wrongly dropped by a file_type-based filter. out = watch_path / "graphify-out" existing_graph = out / "graph.json" if existing_graph.exists(): try: existing = json.loads(existing_graph.read_text(encoding="utf-8")) - code_ids = {n["id"] for n in existing.get("nodes", []) if n.get("file_type") == "code"} - sem_nodes = [n for n in existing.get("nodes", []) if n.get("file_type") != "code"] - sem_edges = [e for e in existing.get("links", existing.get("edges", [])) - if e.get("confidence") in ("INFERRED", "AMBIGUOUS") - or (e.get("source") not in code_ids and e.get("target") not in code_ids)] + new_ast_ids = {n["id"] for n in result["nodes"]} + preserved_nodes = [n for n in existing.get("nodes", []) if n["id"] not in new_ast_ids] + all_ids = new_ast_ids | {n["id"] for n in preserved_nodes} + preserved_edges = [ + e for e in existing.get("links", existing.get("edges", [])) + if e.get("source") in all_ids and e.get("target") in all_ids + ] result = { - "nodes": result["nodes"] + sem_nodes, - "edges": result["edges"] + sem_edges, + "nodes": result["nodes"] + preserved_nodes, + "edges": result["edges"] + preserved_edges, "hyperedges": existing.get("hyperedges", []), "input_tokens": 0, "output_tokens": 0,