diff --git a/graphify/__main__.py b/graphify/__main__.py index d23059d..fb28cf4 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -1546,7 +1546,15 @@ def main() -> None: cohesion = score_all(G, communities) gods = god_nodes(G) surprises = surprising_connections(G, communities) - labels = {cid: f"Community {cid}" for cid in communities} + out = watch_path / "graphify-out" + labels_path = out / ".graphify_labels.json" + if labels_path.exists(): + try: + labels = {int(k): v for k, v in json.loads(labels_path.read_text(encoding="utf-8")).items()} + except Exception: + labels = {cid: f"Community {cid}" for cid in communities} + else: + labels = {cid: f"Community {cid}" for cid in communities} questions = suggest_questions(G, communities, labels) tokens = {"input": 0, "output": 0} from graphify.export import _git_head as _gh @@ -1555,9 +1563,9 @@ def main() -> None: {"warning": "cluster-only mode — file stats not available"}, tokens, str(watch_path), suggested_questions=questions, min_community_size=min_community_size, built_at_commit=_commit) - out = watch_path / "graphify-out" (out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8") to_json(G, communities, str(out / "graph.json")) + labels_path.write_text(json.dumps({str(k): v for k, v in labels.items()}, ensure_ascii=False), encoding="utf-8") # Mirror watch.py pattern: gate to_html so core outputs (graph.json + # GRAPH_REPORT.md) always land. Honor --no-viz explicitly; otherwise @@ -1872,6 +1880,13 @@ def main() -> None: elif subcmd == "wiki": from graphify.wiki import to_wiki as _to_wiki from graphify.analyze import god_nodes as _god_nodes + if not communities: + print( + "error: .graphify_analysis.json is missing or empty — refusing to export wiki to prevent data loss.\n" + "Run `graphify extract .` (or `graphify cluster-only .`) to regenerate community data first.", + file=sys.stderr, + ) + sys.exit(1) if not gods_data: gods_data = _god_nodes(G) n = _to_wiki(G, communities, str(out_dir / "wiki"), diff --git a/graphify/llm.py b/graphify/llm.py index 07c8455..77fcbb7 100644 --- a/graphify/llm.py +++ b/graphify/llm.py @@ -144,9 +144,10 @@ def _call_openai_compat( try: from openai import OpenAI except ImportError as exc: + pkg_hint = "graphifyy[kimi]" if backend == "kimi" else "openai" raise ImportError( - "Kimi/OpenAI-compatible extraction requires the openai package. " - "Run: pip install openai" + f"{'Ollama' if backend == 'ollama' else 'Kimi'}/OpenAI-compatible extraction requires the openai package. " + f"Run: pip install {pkg_hint}" ) from exc client = OpenAI(api_key=api_key, base_url=base_url) diff --git a/graphify/wiki.py b/graphify/wiki.py index 8cc845f..973ed89 100644 --- a/graphify/wiki.py +++ b/graphify/wiki.py @@ -196,6 +196,12 @@ def to_wiki( out = Path(output_dir) out.mkdir(parents=True, exist_ok=True) + if not communities: + raise ValueError( + "communities dict is empty — refusing to clear wiki/. " + "Run `graphify extract .` or `graphify cluster-only .` first." + ) + # Clear stale .md files from previous runs to prevent orphan accumulation. # Community labels are LLM-generated (per skill.md Step 5) and non-deterministic # across runs — the same conceptual community may be named differently each time diff --git a/pyproject.toml b/pyproject.toml index 378a3c9..837c9b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ leiden = ["graspologic; python_version < '3.13'"] office = ["python-docx", "openpyxl"] video = ["faster-whisper", "yt-dlp"] kimi = ["openai", "tiktoken"] +ollama = ["openai"] sql = ["tree-sitter-sql"] all = ["mcp", "neo4j", "pypdf", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "faster-whisper", "yt-dlp", "matplotlib", "openai", "tiktoken", "tree-sitter-sql"]