From ca480924134f6135adc162e56b3e77ea7c6460f7 Mon Sep 17 00:00:00 2001 From: Safi Date: Sat, 2 May 2026 13:52:09 +0100 Subject: [PATCH] Add --force flag and GRAPHIFY_FORCE env var to graphify update Bypasses the node-count safety check in to_json for refactors that legitimately shrink the graph (renames, package deletions). Also honored by post-commit and post-checkout hooks via GRAPHIFY_FORCE=1. Implements the approach from #639 (targeted at v5) adapted for v6. Co-Authored-By: Claude Sonnet 4.6 --- graphify/__main__.py | 13 ++++++++++--- graphify/hooks.py | 9 ++++++--- graphify/watch.py | 8 ++++++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/graphify/__main__.py b/graphify/__main__.py index e2f6024ec..1277bff05 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -1009,6 +1009,8 @@ def main() -> None: print(" --dir target directory (default: ./raw)") print(" watch watch a folder and rebuild the graph on code changes") print(" update re-extract code files and update the graph (no LLM needed)") + print(" --force overwrite graph.json even if the rebuild has fewer nodes") + print(" (also: GRAPHIFY_FORCE=1 env var; use after refactors that delete code)") print(" cluster-only rerun clustering on an existing graph.json and regenerate report") print(" query \"\" BFS traversal of graph.json for a question") print(" --dfs use depth-first instead of breadth-first") @@ -1423,8 +1425,13 @@ def main() -> None: print(f"Done — {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.") elif cmd == "update": - if len(sys.argv) > 2: - watch_path = Path(sys.argv[2]) + force = os.environ.get("GRAPHIFY_FORCE", "").lower() in ("1", "true", "yes") + argv = list(sys.argv) + if "--force" in argv[2:]: + force = True + argv = [a for a in argv if a != "--force"] + if len(argv) > 2: + watch_path = Path(argv[2]) else: # Try to recover the scan root saved by the last full build saved = Path("graphify-out/.graphify_root") @@ -1437,7 +1444,7 @@ def main() -> None: sys.exit(1) from graphify.watch import _rebuild_code print(f"Re-extracting code files in {watch_path} (no LLM needed)...") - ok = _rebuild_code(watch_path) + ok = _rebuild_code(watch_path, force=force) if ok: print("Code graph updated. For doc/paper/image changes run /graphify --update in your AI assistant.") if not os.environ.get("MOONSHOT_API_KEY") and not os.environ.get("GRAPHIFY_NO_TIPS"): diff --git a/graphify/hooks.py b/graphify/hooks.py index 9341b8541..eebd92ae0 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -80,8 +80,10 @@ if not changed: print(f'[graphify hook] {len(changed)} file(s) changed - rebuilding graph...') try: + import os as _os from graphify.watch import _rebuild_code - _rebuild_code(Path('.')) + _force = _os.environ.get('GRAPHIFY_FORCE', '').lower() in ('1', 'true', 'yes') + _rebuild_code(Path('.'), force=_force) except Exception as exc: print(f'[graphify hook] Rebuild failed: {exc}') sys.exit(1) @@ -124,9 +126,10 @@ echo "[graphify] Branch switched - launching background rebuild (log: $_GRAPHIFY nohup $GRAPHIFY_PYTHON -c " from graphify.watch import _rebuild_code from pathlib import Path -import sys +import os, sys try: - _rebuild_code(Path('.')) + _force = os.environ.get('GRAPHIFY_FORCE', '').lower() in ('1', 'true', 'yes') + _rebuild_code(Path('.'), force=_force) except Exception as exc: print(f'[graphify] Rebuild failed: {exc}') sys.exit(1) diff --git a/graphify/watch.py b/graphify/watch.py index c77572a4c..12496851d 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -33,9 +33,13 @@ def _relativize_source_files(payload: dict, root: Path) -> None: continue -def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False) -> bool: +def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False, force: bool = False) -> bool: """Re-run AST extraction + build + cluster + report for code files. No LLM needed. + When ``force`` is True the node-count safety check in ``to_json`` is bypassed + so the rebuilt graph overwrites graph.json even if it has fewer nodes. + Use this after refactors that legitimately delete code. + Returns True on success, False on error. """ watch_root = watch_path.resolve() @@ -105,7 +109,7 @@ def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False) -> bool: out.mkdir(exist_ok=True) (out / ".graphify_root").write_text(str(watch_root), encoding="utf-8") - json_written = to_json(G, communities, str(out / "graph.json")) + json_written = to_json(G, communities, str(out / "graph.json"), force=force) if not json_written: return False