diff --git a/CHANGELOG.md b/CHANGELOG.md index f81088c2..ea9fc10b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu - Feat: `graphify extract` now runs incrementally - auto-detects prior `manifest.json` and re-extracts only changed/new files; semantic results cached by content hash so unchanged docs cost zero LLM tokens on repeat runs (#698) - Feat: Entity deduplication pipeline runs on every build - entropy gate + MinHash/LSH blocking + Jaro-Winkler verification + same-community boost collapses near-duplicate entities (typos, spacing, plurals) before clustering - Feat: `--dedup-llm` flag for `graphify extract` - optional LLM tiebreaker for ambiguous entity pairs (~$0.01 for 10k-node graphs), off by default +- Fix: `graphify hook install` rebuild now preserves human-readable community labels from `.graphify_labels.json` instead of resetting to generic "Community N" names on every commit (#705) +- Fix: `graphify install --platform gemini` now works correctly (#706) - Deps: `datasketch` and `rapidfuzz` added as base dependencies ## 0.7.4 (2026-05-04) diff --git a/graphify/watch.py b/graphify/watch.py index 88cc8c61..a967b5d8 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -9,6 +9,16 @@ from pathlib import Path _GRAPHIFY_OUT = os.environ.get("GRAPHIFY_OUT", "graphify-out") +def _git_head() -> str | None: + """Return current git HEAD commit hash, or None outside a repo.""" + import subprocess as _sp + try: + r = _sp.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True, timeout=3) + return r.stdout.strip() if r.returncode == 0 else None + except Exception: + return None + + from graphify.detect import CODE_EXTENSIONS, DOC_EXTENSIONS, PAPER_EXTENSIONS, IMAGE_EXTENSIONS _WATCHED_EXTENSIONS = CODE_EXTENSIONS | DOC_EXTENSIONS | PAPER_EXTENSIONS | IMAGE_EXTENSIONS @@ -64,6 +74,7 @@ def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False, force: boo print("[graphify watch] No code files found - nothing to rebuild.") return False + commit = _git_head() result = extract(code_files, cache_root=watch_root) # Preserve semantic nodes/edges from a previous full run. @@ -106,13 +117,22 @@ def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False, force: boo cohesion = score_all(G, communities) gods = god_nodes(G) surprises = surprising_connections(G, communities) - labels = {cid: "Community " + str(cid) for cid in communities} + labels_file = out / ".graphify_labels.json" + try: + raw = json.loads(labels_file.read_text(encoding="utf-8")) if labels_file.exists() else {} + labels = {int(k): v for k, v in raw.items() if int(k) in communities} + except Exception: + raw = {} + labels = {} + for cid in communities: + if cid not in labels: + labels[cid] = "Community " + str(cid) questions = suggest_questions(G, communities, labels) 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"), force=force) + json_written = to_json(G, communities, str(out / "graph.json"), force=force, built_at_commit=commit) if not json_written: return False @@ -123,7 +143,8 @@ def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False, force: boo pass report = generate(G, communities, cohesion, labels, gods, surprises, detection, - {"input": 0, "output": 0}, report_root, suggested_questions=questions) + {"input": 0, "output": 0}, report_root, suggested_questions=questions, + built_at_commit=commit) (out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8") # to_html raises ValueError for graphs > MAX_NODES_FOR_VIZ (5000). @@ -241,10 +262,12 @@ def watch(watch_path: Path, debounce: float = 3.0) -> None: batch = list(changed) changed.clear() print(f"\n[graphify watch] {len(batch)} file(s) changed") - if _has_non_code(batch): - _notify_only(watch_path) - else: + has_non_code = _has_non_code(batch) + has_code = any(p.suffix.lower() in _CODE_EXTENSIONS for p in batch) + if has_code: _rebuild_code(watch_path) + if has_non_code: + _notify_only(watch_path) except KeyboardInterrupt: print("\n[graphify watch] Stopped.") finally: