diff --git a/graphify/__main__.py b/graphify/__main__.py index 736d7bea2..77a87a581 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -2329,6 +2329,7 @@ def main() -> None: print(" --half-life-days N signal weight halves every N days (default 30)") print(" --min-corroboration N distinct useful results to prefer a node (default 2)") print(" check-update check needs_update flag and notify if semantic re-extraction is pending (cron-safe)") + print(" inspect list corpus files by category (no LLM or graph build)") print(" tree emit a D3 v7 collapsible-tree HTML for graph.json") print(" --graph PATH path to graph.json (default graphify-out/graph.json)") print(" --output HTML output path (default graphify-out/GRAPH_TREE.html)") @@ -3758,6 +3759,18 @@ def main() -> None: check_update(Path(sys.argv[2]).resolve()) sys.exit(0) + elif cmd == "inspect": + if len(sys.argv) < 3: + print("Usage: graphify inspect ", file=sys.stderr) + sys.exit(1) + target = Path(sys.argv[2]).resolve() + if not target.exists(): + print(f"error: path not found: {target}", file=sys.stderr) + sys.exit(1) + from graphify.detect import detect as _detect, format_detect_summary + + detection = _detect(target) + print(format_detect_summary(detection, root=target)) elif cmd == "tree": # Emit a D3 v7 collapsible-tree HTML view of graph.json: # expand-all / collapse-all / reset-view buttons, multi-line diff --git a/graphify/detect.py b/graphify/detect.py index 92b773f7b..844273a9e 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -1167,6 +1167,57 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: } +def format_detect_summary(detection: dict, root: Path | None = None) -> str: + """Human-readable corpus summary for ``graphify inspect``.""" + files = detection.get("files", {}) + if not any(files.get(key, []) for key in ("code", "document", "paper", "image", "video")): + return "No supported files found." + + scan_root = Path(detection.get("scan_root") or root or ".").resolve() + + lines: list[str] = [] + for key, label in ( + ("code", "code"), + ("document", "docs"), + ("paper", "papers"), + ("image", "images"), + ("video", "video"), + ): + count = len(files.get(key, [])) + if count: + lines.append(f" {label}:{' ' * max(1, 9 - len(label))}{count} files") + + semantic_groups = ( + ("document", "Documents"), + ("paper", "Papers"), + ("image", "Images"), + ) + has_semantic = False + for key, heading in semantic_groups: + paths = files.get(key, []) + if not paths: + continue + has_semantic = True + lines.append("") + lines.append(f"{heading}:") + for path_str in sorted(paths): + p = Path(path_str) + try: + rel = str(p.relative_to(scan_root)) + except ValueError: + rel = path_str + lines.append(f" {rel}") + + if has_semantic: + lines.append("") + lines.append( + "Note: documents, papers, and images require an LLM API key during " + "`graphify extract` (code-only corpora do not)." + ) + + return "\n".join(lines) + + def _md5_file(path: Path) -> str: """MD5 of file contents streamed in 64KB chunks — for change detection only.""" import hashlib as _hl diff --git a/tests/test_inspect_cli.py b/tests/test_inspect_cli.py new file mode 100644 index 000000000..4c18be509 --- /dev/null +++ b/tests/test_inspect_cli.py @@ -0,0 +1,115 @@ +"""Tests for `graphify inspect` CLI.""" +from __future__ import annotations + +import pytest + +import graphify.__main__ as mainmod + + +def _mixed_corpus(tmp_path): + (tmp_path / "main.go").write_text("package main\nfunc main() {}\n") + (tmp_path / "README.md").write_text("# Notes\nEntry point.\n") + (tmp_path / "diagram.png").write_bytes(b"\x89PNG\r\n\x1a\n") + return tmp_path + + +def _code_only_corpus(tmp_path): + (tmp_path / "auth.py").write_text("def login():\n return True\n") + return tmp_path + + +def test_inspect_lists_counts_and_semantic_paths(monkeypatch, tmp_path, capsys): + corpus = _mixed_corpus(tmp_path) + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, "argv", ["graphify", "inspect", str(corpus)], + ) + + mainmod.main() + + out = capsys.readouterr().out + assert "code:" in out + assert "docs:" in out + assert "images:" in out + assert "Documents:" in out + assert "README.md" in out + assert "Images:" in out + assert "diagram.png" in out + + +def test_inspect_code_only_exits_zero_without_llm_note(monkeypatch, tmp_path, capsys): + corpus = _code_only_corpus(tmp_path) + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, "argv", ["graphify", "inspect", str(corpus)], + ) + + mainmod.main() + + out = capsys.readouterr().out + assert "code:" in out + assert "LLM" not in out + assert "semantic" not in out.lower() + + +def test_inspect_semantic_corpus_mentions_llm_note(monkeypatch, tmp_path, capsys): + corpus = _mixed_corpus(tmp_path) + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, "argv", ["graphify", "inspect", str(corpus)], + ) + + mainmod.main() + + out = capsys.readouterr().out + assert "LLM API key" in out + assert "graphify extract" in out + + +def test_inspect_missing_path_exits_nonzero(monkeypatch, tmp_path, capsys): + missing = tmp_path / "nope" + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, "argv", ["graphify", "inspect", str(missing)], + ) + + with pytest.raises(SystemExit) as exc_info: + mainmod.main() + assert exc_info.value.code == 1 + assert "path not found" in capsys.readouterr().err + + +def test_inspect_empty_dir_prints_message_and_exits_zero(monkeypatch, tmp_path, capsys): + empty = tmp_path / "empty" + empty.mkdir() + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, "argv", ["graphify", "inspect", str(empty)], + ) + + mainmod.main() + + out = capsys.readouterr().out + assert out.strip() == "No supported files found." + + +def test_inspect_without_path_prints_usage_and_exits_nonzero(monkeypatch, capsys): + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr(mainmod.sys, "argv", ["graphify", "inspect"]) + + with pytest.raises(SystemExit) as exc_info: + mainmod.main() + assert exc_info.value.code == 1 + assert "Usage: graphify inspect " in capsys.readouterr().err + + +def test_inspect_does_not_create_graphify_out(monkeypatch, tmp_path): + corpus = _mixed_corpus(tmp_path) + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, "argv", ["graphify", "inspect", str(corpus)], + ) + + mainmod.main() + + assert not (corpus / "graphify-out").exists()