feat(extract): add --code-only to index code without an LLM key on a mixed repo (#1734)

`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) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-07-08 17:07:31 +01:00
co-authored by Claude Opus 4.8
parent 0d206cbe11
commit 20bfdf60ac
2 changed files with 78 additions and 1 deletions
+23 -1
View File
@@ -2472,6 +2472,7 @@ def main() -> None:
print(" --out DIR output dir (default: <path>); writes <DIR>/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)
+55
View File
@@ -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"