diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c3e8767dc..bc2ae8386f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu - Fix: the incremental rebuild no longer purges AST nodes it just reported as fail-closed "kept" — the eviction pass re-checks the kept set, so a moved-file/symlink layout can't deadlock the shrink guard into refusing every update (#3697, #3695, thanks @hopstreax). - Fix: `graph.html` no longer crashes vis-network with a stack overflow on large graphs — nodes are seeded on a spiral before physics runs so overlap-avoidance can't blow the layout recursion (#3699, thanks @sanjaiyan-dev). - Fix: node and edge tooltips now show special characters literally (C++ templates like `vector`, generics, `&`, quotes) instead of raw HTML entities, while the HTML sinks that need escaping keep it (#3686, #3664, thanks @hopstreax). +- Fix: `graphify extract --cargo` no longer aborts the whole extraction, discarding the AST pass that already completed, when no `Cargo.toml` exists at the scan root — an ordinary condition for any repo whose manifest lives in a subdirectory. It now prints a note and continues with an empty cargo result instead, matching the merge step's existing handling of that shape (#3677, thanks @ExhibitJ). - Docs: repository links now point at `Graphify-Labs/graphify` instead of the old account (including in generated wiki output), translated READMEs use the current logo, GitHub issue/PR templates were added, and the Enterprise link was corrected (#3692, #3694, #3693, thanks @Abdul535). ## 0.9.64 (2026-09-18) diff --git a/graphify/cli.py b/graphify/cli.py index 969d7d3c87..e2bbb03ed7 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -4182,6 +4182,14 @@ def _progress(idx: int, total: int, _result: dict) -> None: print("[graphify extract] introspecting Cargo workspace...") try: cargo_result = introspect_cargo(target) + except FileNotFoundError: + # No Cargo.toml at the scan root is an ordinary condition + # (e.g. Tauri keeps its manifest under src-tauri/), not a + # failure — the AST pass already completed and cargo_result + # is already the empty, handled shape the merge below + # expects, so degrade instead of discarding that work (#3677). + print("[graphify extract] --cargo: no Cargo.toml at scan root, " + "skipping crate edges") except (ConnectionError, ImportError, OSError) as exc: print(f"error: {exc}", file=sys.stderr) sys.exit(1) diff --git a/tests/test_cargo_missing_manifest.py b/tests/test_cargo_missing_manifest.py new file mode 100644 index 0000000000..af900b6afb --- /dev/null +++ b/tests/test_cargo_missing_manifest.py @@ -0,0 +1,71 @@ +"""`graphify extract --cargo` must degrade, not abort, when no Cargo.toml +exists at the scan root (#3677). + +A missing root manifest is an ordinary condition (e.g. a Tauri app keeps its +manifest under src-tauri/), not a failure. Before the fix, FileNotFoundError +(a subclass of OSError) was caught by a handler meant for ImportError/ +ConnectionError and the whole process exited, discarding the AST pass that +had already completed and never writing graph.json. +""" +from __future__ import annotations + +import json +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 _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", ".", "--code-only", "--cargo", *extra], + cwd=repo, capture_output=True, text=True, env=env, + ) + + +def test_cargo_flag_without_a_manifest_still_writes_the_graph(tmp_path): + repo = tmp_path / "repo" + repo.mkdir() + (repo / "app.py").write_text("def hello():\n return 1\n", encoding="utf-8") + + r = _run(repo) + + assert r.returncode == 0, ( + f"a missing Cargo.toml must not abort the whole extraction: {r.stderr}" + ) + out = r.stdout + r.stderr + assert "no Cargo.toml at scan root" in out, ( + f"the missing manifest should be reported as a skip, not silently dropped: {out}" + ) + graph = repo / "graphify-out" / "graph.json" + assert graph.exists(), "the AST work already done must still be written to graph.json" + g = json.loads(graph.read_text(encoding="utf-8")) + labels = [n.get("label") for n in g["nodes"]] + assert any(str(l).startswith("hello") for l in labels), "code was indexed" + + +def test_cargo_flag_with_a_manifest_still_adds_crate_nodes(tmp_path): + repo = tmp_path / "repo" + repo.mkdir() + (repo / "app.py").write_text("def hello():\n return 1\n", encoding="utf-8") + (repo / "Cargo.toml").write_text( + '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n', + encoding="utf-8", + ) + + r = _run(repo) + + assert r.returncode == 0, f"a valid manifest must extract cleanly: {r.stderr}" + graph = repo / "graphify-out" / "graph.json" + assert graph.exists() + g = json.loads(graph.read_text(encoding="utf-8")) + node_ids = {n.get("id") for n in g["nodes"]} + assert any("app" in str(i) for i in node_ids), ( + f"a real Cargo.toml should still contribute a crate node; got {sorted(node_ids)}" + )