From 20bfdf60ac7187edc2f8594252222dc8e9b96399 Mon Sep 17 00:00:00 2001 From: safishamsi Date: Wed, 8 Jul 2026 17:07:31 +0100 Subject: [PATCH] feat(extract): add --code-only to index code without an LLM key on a mixed repo (#1734) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `graphify extract` on a repo containing docs/papers/images hard-failed when no LLM backend was configured — even for a user who only wants the code graph. The only workaround was hand-building a .graphifyignore of everything non-code, which is onerous (the "not code" set is far larger than the code set). `--code-only` skips the semantic (doc/paper/image) pass entirely: it indexes the code via pure local AST (no key required) and reports what it skipped ("skipping N non-code file(s) ...") rather than silently dropping it. The no-key error on a mixed repo now also points users at the flag. Code-only was always keyless; this just makes a *mixed* repo usable without a key instead of failing outright. Co-Authored-By: Claude Opus 4.8 (1M context) --- graphify/__main__.py | 24 ++++++++++++- tests/test_extract_code_only_cli.py | 55 +++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 tests/test_extract_code_only_cli.py diff --git a/graphify/__main__.py b/graphify/__main__.py index 9aa2cf8..2e0a71c 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -2472,6 +2472,7 @@ def main() -> None: print(" --out DIR output dir (default: ); writes /graphify-out/") print(" --google-workspace export .gdoc/.gsheet/.gslides shortcuts via gws before extraction") print(" --no-cluster skip clustering, write raw extraction only") + print(" --code-only index code (local AST, no API key) and skip doc/paper/image files") print(" --postgres DSN extract schema from a live PostgreSQL database") print(" maps tables, views, functions + FK relationships;") print(" column-level detail is not represented in the graph") @@ -4526,6 +4527,7 @@ def main() -> None: dedup_llm = False google_workspace = False global_merge = False + code_only = False global_repo_tag: str | None = None # Performance/tuning knobs (issue #792). None means "use library default". cli_max_workers: int | None = None @@ -4584,6 +4586,8 @@ def main() -> None: no_cluster = True; i += 1 elif a == "--dedup-llm": dedup_llm = True; i += 1 + elif a == "--code-only": + code_only = True; i += 1 elif a == "--google-workspace": google_workspace = True; i += 1 elif a == "--global": @@ -4707,6 +4711,20 @@ def main() -> None: unchanged_total = 0 semantic_files = doc_files + paper_files + image_files + # --code-only: index code (pure local AST, no key) and skip the semantic + # (doc/paper/image) pass entirely, so a mixed repo doesn't hard-fail when no + # LLM backend is configured (#1734). Report what was skipped rather than + # silently dropping it. + if code_only and semantic_files: + print( + f"[graphify extract] --code-only: skipping {len(semantic_files)} " + f"non-code file(s) ({len(doc_files)} docs, {len(paper_files)} papers, " + f"{len(image_files)} images) — no LLM extraction" + ) + semantic_files = [] + doc_files = [] + paper_files = [] + image_files = [] if incremental_mode: print( f"[graphify extract] {len(code_files)} code, {len(doc_files)} docs, " @@ -4762,12 +4780,16 @@ def main() -> None: ) if dedup_llm: reasons.append("--dedup-llm was passed") + hint = "" + if semantic_files: + hint = (" Or pass --code-only to index just the code " + "(local AST, no key) and skip the non-code files.") print( "error: no LLM API key found (" + "; ".join(reasons) + "). " "Set GEMINI_API_KEY or GOOGLE_API_KEY (gemini), MOONSHOT_API_KEY " "(kimi), ANTHROPIC_API_KEY (claude), OPENAI_API_KEY (openai), " "DEEPSEEK_API_KEY (deepseek), or pass --backend. A code-only " - "corpus needs no key.", + "corpus needs no key." + hint, file=sys.stderr, ) sys.exit(1) diff --git a/tests/test_extract_code_only_cli.py b/tests/test_extract_code_only_cli.py new file mode 100644 index 0000000..2fcc8a7 --- /dev/null +++ b/tests/test_extract_code_only_cli.py @@ -0,0 +1,55 @@ +"""`graphify extract --code-only` indexes code without an LLM key (#1734). + +A mixed repo (code + docs) with no API key configured used to hard-fail on the +doc/paper/image files. `--code-only` skips the semantic pass so the code graph +still builds, and the no-key error now points users at the flag. +""" +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +PYTHON = sys.executable +_KEY_VARS = ("GEMINI_API_KEY", "GOOGLE_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL", + "ANTHROPIC_API_KEY", "MOONSHOT_API_KEY", "DEEPSEEK_API_KEY") + + +def _mixed_repo(tmp_path: Path) -> Path: + repo = tmp_path / "repo" + repo.mkdir() + (repo / "app.py").write_text("def hello():\n return 1\n") + (repo / "README.md").write_text("# Design\n\nHow it works.\n") + (repo / "NOTES.txt").write_text("Architecture notes and rationale.\n") + return repo + + +def _run(repo: Path, *extra: str): + env = {k: v for k, v in os.environ.items() if k not in _KEY_VARS} + env["GRAPHIFY_OUT"] = str(repo / "graphify-out") + return subprocess.run( + [PYTHON, "-m", "graphify", "extract", ".", *extra], + cwd=repo, capture_output=True, text=True, env=env, + ) + + +def test_code_only_succeeds_without_key(tmp_path): + repo = _mixed_repo(tmp_path) + r = _run(repo, "--code-only") + assert r.returncode == 0, f"--code-only should succeed with no key: {r.stderr}" + out = r.stdout + r.stderr + assert "--code-only: skipping" in out + graph = repo / "graphify-out" / "graph.json" + assert graph.exists(), "code graph must still be written" + import json + g = json.loads(graph.read_text()) + labels = [n.get("label") for n in g["nodes"]] + assert any(str(l).startswith("hello") for l in labels), "code was indexed" + + +def test_mixed_repo_without_key_errors_and_points_at_code_only(tmp_path): + repo = _mixed_repo(tmp_path) + r = _run(repo) # no --code-only, no key + assert r.returncode != 0, "mixed repo with no key should still error without the flag" + assert "--code-only" in r.stderr, "the no-key error must point users at --code-only"