From 7cb5e86fc1b0882102ab154ee2f61791a9d9fc87 Mon Sep 17 00:00:00 2001 From: Vlladie Condeno Date: Thu, 11 Jun 2026 14:02:32 +0800 Subject: [PATCH] fix: anchor extraction cache at --out root so external output leaves the scanned project clean graphify extract --out R is documented to send all output to R/graphify-out/, but the AST and semantic extraction caches were still anchored at the scanned project (cache_root=target / root=target). That re-created a graphify-out/ directory inside the project the user explicitly asked to keep clean. Anchor both caches at out_root instead. With --out unset, out_root equals target, so existing in-project behavior is unchanged. Adds test_extract_out_keeps_project_root_clean: runs extract from a project root with --out pointing elsewhere and asserts the artifacts land under --out while the project directory stays byte-identical. --- graphify/__main__.py | 9 ++++++--- tests/test_extract_cli.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/graphify/__main__.py b/graphify/__main__.py index 192af325b..e954698f9 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -4206,7 +4206,10 @@ def main() -> None: ast_result: dict = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0} if code_files: from graphify.extract import extract as _ast_extract - ast_kwargs: dict = {"cache_root": target} + # Anchor the cache at the output root, not the scanned project: + # with --out, a /graphify-out/cache/ would leak a + # graphify-out/ dir into a project that asked for external output. + ast_kwargs: dict = {"cache_root": out_root} if cli_max_workers is not None: ast_kwargs["max_workers"] = cli_max_workers print(f"[graphify extract] AST extraction on {len(code_files)} code files...") @@ -4230,7 +4233,7 @@ def main() -> None: if semantic_files: sem_paths_str = [str(p) for p in semantic_files] cached_nodes, cached_edges, cached_hyperedges, uncached_paths = ( - _check_semantic_cache(sem_paths_str, root=target) + _check_semantic_cache(sem_paths_str, root=out_root) ) sem_cache_hits = len(semantic_files) - len(uncached_paths) sem_cache_misses = len(uncached_paths) @@ -4300,7 +4303,7 @@ def main() -> None: fresh.get("nodes", []), fresh.get("edges", []), fresh.get("hyperedges", []), - root=target, + root=out_root, ) except Exception as exc: print(f"[graphify extract] warning: could not write semantic cache: {exc}", file=sys.stderr) diff --git a/tests/test_extract_cli.py b/tests/test_extract_cli.py index 13d6fe91d..5b752dbd7 100644 --- a/tests/test_extract_cli.py +++ b/tests/test_extract_cli.py @@ -172,6 +172,43 @@ def test_extract_codeonly_succeeds_without_api_key(monkeypatch, tmp_path): assert len(json.loads(graph.read_text()).get("nodes", [])) > 0 +def test_extract_out_keeps_project_root_clean(monkeypatch, tmp_path): + """`extract --out DIR` routes every artifact to DIR/graphify-out/ and the + scanned project must not grow a graphify-out/ (or anything else) beside + its sources. + + Guards the centralized-output workflow: run from the project root with + --out pointing outside the repo, and the repo stays byte-identical. + """ + project = tmp_path / "project" + project.mkdir() + corpus = _code_only_corpus(project) + external = tmp_path / "external-graphs" + + _clear_backend_keys(monkeypatch) + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.chdir(corpus) # run from the project root, like a real user + monkeypatch.setattr( + mainmod.sys, "argv", + ["graphify", "extract", ".", "--out", str(external)], + ) + + try: + mainmod.main() + except SystemExit as exc: + assert exc.code in (None, 0), f"unexpected exit code {exc.code}" + + out = external / "graphify-out" + assert (out / "graph.json").exists(), "graph.json must land under --out" + assert (out / "manifest.json").exists(), "manifest.json must land under --out" + assert not (corpus / "graphify-out").exists(), ( + "scanned project must not grow a graphify-out/ when --out is set" + ) + assert sorted(p.name for p in corpus.iterdir()) == ["auth.py"], ( + "no stray files may appear in the project root" + ) + + def test_extract_without_key_still_errors_when_docs_present( monkeypatch, tmp_path, capsys ):