graphnav 1.2.1__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {graphnav-1.2.1/graphnav.egg-info → graphnav-1.2.2}/PKG-INFO +1 -1
- {graphnav-1.2.1 → graphnav-1.2.2}/README.md +36 -21
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/cli.py +23 -17
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/multirepo.py +71 -24
- {graphnav-1.2.1 → graphnav-1.2.2/graphnav.egg-info}/PKG-INFO +1 -1
- {graphnav-1.2.1 → graphnav-1.2.2}/pyproject.toml +1 -1
- {graphnav-1.2.1 → graphnav-1.2.2}/tests/test_cli.py +23 -4
- {graphnav-1.2.1 → graphnav-1.2.2}/tests/test_doctor.py +2 -2
- {graphnav-1.2.1 → graphnav-1.2.2}/tests/test_multirepo.py +23 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/LICENSE +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/__init__.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/config.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/doctor.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/graph_cache.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/graph_nav.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/graph_query.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/mcp_server.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/codex_graph/runner.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/graphnav.egg-info/SOURCES.txt +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/graphnav.egg-info/dependency_links.txt +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/graphnav.egg-info/entry_points.txt +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/graphnav.egg-info/requires.txt +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/graphnav.egg-info/top_level.txt +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/setup.cfg +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/tests/test_config.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/tests/test_graph_cache.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/tests/test_graph_nav.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/tests/test_graph_query.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/tests/test_mcp_server.py +0 -0
- {graphnav-1.2.1 → graphnav-1.2.2}/tests/test_runner.py +0 -0
|
@@ -27,13 +27,22 @@ GraphNav solves this by:
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Setup is one command
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
pip install graphnav
|
|
34
|
+
graphnav
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
That's the whole setup. Run `graphnav` from your project root and it:
|
|
38
|
+
|
|
39
|
+
1. Auto-detects your project — a **single folder** or a **monorepo** of services
|
|
40
|
+
2. Builds the knowledge graph
|
|
41
|
+
3. Writes the agent instruction files
|
|
42
|
+
|
|
43
|
+
Then open the repo in your AI coding tool and start working. **There is nothing else to run.**
|
|
44
|
+
|
|
45
|
+
Requires Python ≥ 3.11. Pulls `graphifyy` (the `graphify` binary) automatically — including under `pipx`, `--user`, and virtualenv installs.
|
|
37
46
|
|
|
38
47
|
**API key:** Place a `.env` file anywhere up the directory tree from your project (or inside any service subfolder). graphnav walks up and down to find it:
|
|
39
48
|
|
|
@@ -42,22 +51,7 @@ OPENAI_API_KEY=sk-...
|
|
|
42
51
|
ANTHROPIC_KEY=sk-ant-...
|
|
43
52
|
```
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
## Quickstart
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
# In your monorepo root — detects services, builds graphs, writes agent instructions
|
|
51
|
-
graphnav map
|
|
52
|
-
|
|
53
|
-
# Get a context pack for a task (free, no LLM, ~instant)
|
|
54
|
-
graphnav context "add a critique scoring function to the coach"
|
|
55
|
-
|
|
56
|
-
# Keep graphs live as you edit
|
|
57
|
-
graphnav watch
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
After `map`, every AI agent in the repo has access to:
|
|
54
|
+
After running `graphnav`, every AI agent in the repo has access to:
|
|
61
55
|
|
|
62
56
|
- **`CLAUDE.md`** — picked up by Claude Code
|
|
63
57
|
- **`AGENTS.md`** — picked up by OpenAI Codex CLI
|
|
@@ -66,13 +60,34 @@ After `map`, every AI agent in the repo has access to:
|
|
|
66
60
|
- **`<service>/graphify-out/BRIDGES.md`** — exact cross-service call sites with line numbers
|
|
67
61
|
- **`graphify-out/MONOREPO_MAP.md`** — overview of all services and their connections
|
|
68
62
|
|
|
63
|
+
### Optional
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
graphnav watch # keep the graph live as you edit
|
|
67
|
+
graphnav doctor # diagnose the setup if something looks wrong
|
|
68
|
+
```
|
|
69
|
+
|
|
69
70
|
---
|
|
70
71
|
|
|
71
72
|
## Commands
|
|
72
73
|
|
|
74
|
+
> **You only need `graphnav`.** Running it bare does the full setup (it runs `map` for you). `watch` and `doctor` are the only other commands you'd type by hand. Everything below `graphnav watch` — `context`, `serve`, `find`, `neighbors`, `impact` — is meant for your **AI agent** to call automatically (via the generated instruction files or the MCP server), not for you to run manually. They're documented here for completeness.
|
|
75
|
+
|
|
76
|
+
### `graphnav` (just this)
|
|
77
|
+
|
|
78
|
+
From your project root, run `graphnav` with no arguments. It auto-detects whether you have a single-folder project or a monorepo, builds the knowledge graph, writes all agent instruction files, and stops. This is the one command you run.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
graphnav
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For a single-folder project it maps the whole repo as one graph; for a monorepo it builds per-service graphs and cross-service bridges. Equivalent to running `graphnav map` explicitly.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
73
88
|
### `graphnav map`
|
|
74
89
|
|
|
75
|
-
Builds the knowledge graph and generates all agent instruction files.
|
|
90
|
+
The build step that bare `graphnav` runs for you. Builds the knowledge graph and generates all agent instruction files.
|
|
76
91
|
|
|
77
92
|
```
|
|
78
93
|
graphnav map [--root PATH] [--backend BACKEND] [--dry-run]
|
|
@@ -212,7 +227,7 @@ It checks the `graphify` binary, the config file (and reports any validation war
|
|
|
212
227
|
|
|
213
228
|
### `graphnav` (no subcommand)
|
|
214
229
|
|
|
215
|
-
|
|
230
|
+
Run with no arguments from any project root, it auto-detects the project shape (single folder or monorepo) and runs the full setup automatically. If a prompt is given, it falls through to the context-injection path for the Codex CLI.
|
|
216
231
|
|
|
217
232
|
---
|
|
218
233
|
|
|
@@ -344,7 +359,7 @@ ANTHROPIC_KEY=sk-ant-...
|
|
|
344
359
|
Then:
|
|
345
360
|
|
|
346
361
|
```bash
|
|
347
|
-
graphnav
|
|
362
|
+
graphnav # one-time setup, or re-run after large refactors
|
|
348
363
|
graphnav watch # optional: keep graphs live during active development
|
|
349
364
|
```
|
|
350
365
|
|
|
@@ -51,13 +51,16 @@ def _auto_map_if_needed(cfg_path: str | None) -> None:
|
|
|
51
51
|
|
|
52
52
|
cfg = load_config(cfg_path)
|
|
53
53
|
root = os.path.abspath(".")
|
|
54
|
-
services = multirepo.
|
|
54
|
+
services, single = multirepo.resolve_services(root, cfg.mono.marker_files, cfg.mono.extra_skip_dirs)
|
|
55
55
|
if not services:
|
|
56
|
-
|
|
56
|
+
print("[graphnav] No source code found here. Run graphnav from your project's root directory.", file=sys.stderr)
|
|
57
|
+
sys.exit(1)
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
if single:
|
|
60
|
+
print(f"[graphnav] Single project detected. Building knowledge graph ...", file=sys.stderr)
|
|
61
|
+
else:
|
|
62
|
+
names = ", ".join(s.name for s in services)
|
|
63
|
+
print(f"[graphnav] Detected {len(services)} service(s): {names}. Building knowledge graphs ...", file=sys.stderr)
|
|
61
64
|
rc = multirepo.run_map(root=root, mono_cfg=cfg.mono)
|
|
62
65
|
sys.exit(rc)
|
|
63
66
|
|
|
@@ -216,16 +219,20 @@ def main() -> None:
|
|
|
216
219
|
parser = argparse.ArgumentParser(
|
|
217
220
|
prog="graphnav",
|
|
218
221
|
description=(
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
222
|
+
"GraphNav — knowledge-graph context for AI coding agents.\n\n"
|
|
223
|
+
"Setup is ONE command. From your project root, just run:\n"
|
|
224
|
+
" graphnav\n"
|
|
225
|
+
"It auto-detects your project (single folder or monorepo), builds the\n"
|
|
226
|
+
"knowledge graph, and writes the agent instruction files. Nothing else\n"
|
|
227
|
+
"to run — open the repo in your AI coding tool and start working.\n\n"
|
|
228
|
+
"Optional commands:\n"
|
|
229
|
+
" watch Keep the graph live as you edit\n"
|
|
230
|
+
" doctor Diagnose the setup if something looks wrong\n\n"
|
|
231
|
+
"Commands your AI agent calls for you (you rarely run these by hand):\n"
|
|
232
|
+
" context Token-budgeted context pack for a task (free, no LLM)\n"
|
|
233
|
+
" serve MCP server exposing the graph tools natively\n"
|
|
234
|
+
" find / neighbors / impact Symbol lookup and blast-radius queries\n"
|
|
235
|
+
" map The build step that `graphnav` runs for you"
|
|
229
236
|
),
|
|
230
237
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
231
238
|
)
|
|
@@ -243,8 +250,7 @@ def main() -> None:
|
|
|
243
250
|
if not prompt:
|
|
244
251
|
if sys.stdin.isatty():
|
|
245
252
|
_auto_map_if_needed(args.config)
|
|
246
|
-
|
|
247
|
-
sys.exit(1)
|
|
253
|
+
return
|
|
248
254
|
prompt = sys.stdin.read().strip()
|
|
249
255
|
if not prompt:
|
|
250
256
|
parser.print_help()
|
|
@@ -17,12 +17,48 @@ def find_graphify() -> str | None:
|
|
|
17
17
|
path = shutil.which("graphify")
|
|
18
18
|
if path:
|
|
19
19
|
return path
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
exe = "graphify.exe" if os.name == "nt" else "graphify"
|
|
21
|
+
search_dirs = [os.path.dirname(sys.executable)]
|
|
22
|
+
import sysconfig
|
|
23
|
+
|
|
24
|
+
for scheme in sysconfig.get_scheme_names():
|
|
25
|
+
try:
|
|
26
|
+
d = sysconfig.get_path("scripts", scheme)
|
|
27
|
+
except Exception:
|
|
28
|
+
d = None
|
|
29
|
+
if d:
|
|
30
|
+
search_dirs.append(d)
|
|
31
|
+
seen: set[str] = set()
|
|
32
|
+
for d in search_dirs:
|
|
33
|
+
if not d or d in seen:
|
|
34
|
+
continue
|
|
35
|
+
seen.add(d)
|
|
36
|
+
candidate = os.path.join(d, exe)
|
|
37
|
+
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
|
38
|
+
return candidate
|
|
23
39
|
return None
|
|
24
40
|
|
|
25
41
|
|
|
42
|
+
def resolve_services(
|
|
43
|
+
root: str,
|
|
44
|
+
marker_files: list[str],
|
|
45
|
+
extra_skip_dirs: list[str] | None = None,
|
|
46
|
+
) -> tuple[list[ServiceInfo], bool]:
|
|
47
|
+
services = detect_services(root, marker_files, extra_skip_dirs)
|
|
48
|
+
if services:
|
|
49
|
+
return services, False
|
|
50
|
+
skip_dirs = SKIP_DIRS | frozenset(extra_skip_dirs or ())
|
|
51
|
+
if _has_source_files(root, skip_dirs=skip_dirs):
|
|
52
|
+
name = os.path.basename(os.path.abspath(root).rstrip(os.sep)) or "repo"
|
|
53
|
+
root_service = ServiceInfo(
|
|
54
|
+
name=name,
|
|
55
|
+
abs_path=root,
|
|
56
|
+
graph_path=_overarching_graph_path(root),
|
|
57
|
+
)
|
|
58
|
+
return [root_service], True
|
|
59
|
+
return [], False
|
|
60
|
+
|
|
61
|
+
|
|
26
62
|
def _warn(msg: str) -> None:
|
|
27
63
|
print(f"[graphnav] warning: {msg}", file=sys.stderr)
|
|
28
64
|
|
|
@@ -816,9 +852,13 @@ def _refresh(
|
|
|
816
852
|
root: str,
|
|
817
853
|
services: list[ServiceInfo],
|
|
818
854
|
overarching_graph_path: str,
|
|
855
|
+
single: bool = False,
|
|
819
856
|
) -> dict[str, list[BridgeRow]]:
|
|
820
|
-
|
|
821
|
-
|
|
857
|
+
if single:
|
|
858
|
+
bridges = {s.name: [] for s in services}
|
|
859
|
+
else:
|
|
860
|
+
partition_graph(overarching_graph_path, services)
|
|
861
|
+
bridges = analyze_bridges(overarching_graph_path, services)
|
|
822
862
|
for svc in services:
|
|
823
863
|
write_bridges_md(svc, bridges[svc.name])
|
|
824
864
|
write_symbols_md(svc)
|
|
@@ -840,13 +880,14 @@ def run_map(
|
|
|
840
880
|
print("Error: 'graphify' not found. Install with: pip install graphifyy", file=sys.stderr)
|
|
841
881
|
return 1
|
|
842
882
|
|
|
843
|
-
services =
|
|
883
|
+
services, single = resolve_services(root, mono_cfg.marker_files, mono_cfg.extra_skip_dirs)
|
|
844
884
|
if not services:
|
|
845
|
-
print(f"No
|
|
885
|
+
print(f"No source code found in {root}. Run graphnav from a directory that contains code.", file=sys.stderr)
|
|
846
886
|
return 1
|
|
847
887
|
|
|
888
|
+
shape = "whole repo (single project)" if single else f"{len(services)} service(s): {', '.join(s.name for s in services)}"
|
|
848
889
|
if dry_run:
|
|
849
|
-
print(f"Detected {
|
|
890
|
+
print(f"Detected {shape}:")
|
|
850
891
|
for svc in services:
|
|
851
892
|
print(f" {svc.name} {svc.abs_path}")
|
|
852
893
|
print("[dry-run] No graphify calls made.")
|
|
@@ -856,23 +897,29 @@ def run_map(
|
|
|
856
897
|
env = _build_subprocess_env(root)
|
|
857
898
|
overarching_path = _overarching_graph_path(root)
|
|
858
899
|
|
|
859
|
-
print(f"[graphnav] Building
|
|
900
|
+
print(f"[graphnav] Building knowledge graph for {shape} ...", file=sys.stderr)
|
|
860
901
|
rc = build_overarching_graph(root, graphify_path, backend, env=env)
|
|
861
902
|
if rc != 0 or not os.path.exists(overarching_path):
|
|
862
|
-
print(f"Error:
|
|
903
|
+
print(f"Error: graphify extraction failed (exit {rc}).", file=sys.stderr)
|
|
863
904
|
print(" Ensure an API key is available (e.g. ANTHROPIC_API_KEY or ANTHROPIC_KEY in a .env file).", file=sys.stderr)
|
|
864
905
|
return 1
|
|
865
906
|
|
|
866
|
-
bridges = _refresh(root, services, overarching_path)
|
|
907
|
+
bridges = _refresh(root, services, overarching_path, single=single)
|
|
867
908
|
total_bridges = sum(len(rows) for rows in bridges.values())
|
|
868
909
|
|
|
869
|
-
print(f"\
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
910
|
+
print(f"\nSetup complete. Your AI coding agents are now configured for this repo.")
|
|
911
|
+
if single:
|
|
912
|
+
print(f" Knowledge graph : {overarching_path}")
|
|
913
|
+
print(f" Symbol index : {os.path.join(root, 'graphify-out', 'SYMBOLS.md')}")
|
|
914
|
+
else:
|
|
915
|
+
print(f" {len(services)} service(s) mapped, {total_bridges} cross-service connection(s) found.")
|
|
916
|
+
print(f" Overarching graph : {overarching_path}")
|
|
917
|
+
for svc in services:
|
|
918
|
+
to = ", ".join(svc.bridges_to) if svc.bridges_to else "none"
|
|
919
|
+
print(f" {svc.name}/graphify-out/ (bridges -> {to})")
|
|
920
|
+
print(f" Agent instructions : CLAUDE.md, AGENTS.md, .github/copilot-instructions.md")
|
|
921
|
+
print(f"\nNothing else to run. Open the repo in your AI coding tool and start working.")
|
|
922
|
+
print(f"(Optional: `graphnav watch` keeps the graph live as you edit.)")
|
|
876
923
|
return 0
|
|
877
924
|
|
|
878
925
|
|
|
@@ -887,9 +934,9 @@ def run_watch(
|
|
|
887
934
|
print("Error: 'graphify' not found. Install with: pip install graphifyy", file=sys.stderr)
|
|
888
935
|
return 1
|
|
889
936
|
|
|
890
|
-
services =
|
|
937
|
+
services, single = resolve_services(root, mono_cfg.marker_files, mono_cfg.extra_skip_dirs)
|
|
891
938
|
if not services:
|
|
892
|
-
print(f"No
|
|
939
|
+
print(f"No source code found in {root}.", file=sys.stderr)
|
|
893
940
|
return 1
|
|
894
941
|
|
|
895
942
|
backend = backend_override or mono_cfg.graphify_backend
|
|
@@ -897,13 +944,13 @@ def run_watch(
|
|
|
897
944
|
overarching_path = _overarching_graph_path(root)
|
|
898
945
|
|
|
899
946
|
if not os.path.exists(overarching_path):
|
|
900
|
-
print(f"[graphnav] Bootstrapping
|
|
947
|
+
print(f"[graphnav] Bootstrapping knowledge graph ...", file=sys.stderr)
|
|
901
948
|
rc = build_overarching_graph(root, graphify_path, backend, env=env)
|
|
902
949
|
if rc != 0 or not os.path.exists(overarching_path):
|
|
903
950
|
print(f"Error: bootstrap extraction failed (exit {rc}).", file=sys.stderr)
|
|
904
951
|
return 1
|
|
905
952
|
|
|
906
|
-
_refresh(root, services, overarching_path)
|
|
953
|
+
_refresh(root, services, overarching_path, single=single)
|
|
907
954
|
|
|
908
955
|
def _start_watch() -> subprocess.Popen:
|
|
909
956
|
return subprocess.Popen(
|
|
@@ -937,8 +984,8 @@ def run_watch(
|
|
|
937
984
|
last_mtime = mtime
|
|
938
985
|
pending_mtime = None
|
|
939
986
|
ts = time.strftime("%H:%M:%S")
|
|
940
|
-
print(f"[graphnav] {ts} graph updated —
|
|
941
|
-
_refresh(root, services, overarching_path)
|
|
987
|
+
print(f"[graphnav] {ts} graph updated — refreshing symbols and bridges ...", file=sys.stderr)
|
|
988
|
+
_refresh(root, services, overarching_path, single=single)
|
|
942
989
|
else:
|
|
943
990
|
pending_mtime = mtime
|
|
944
991
|
elif mtime != last_mtime:
|
|
@@ -170,7 +170,7 @@ class TestAutoMap:
|
|
|
170
170
|
assert exc.value.code == 0
|
|
171
171
|
assert str(tmp_path) == called_with["root"]
|
|
172
172
|
|
|
173
|
-
def
|
|
173
|
+
def test_no_args_without_source_exits_with_guidance(self, tmp_path, monkeypatch, capsys):
|
|
174
174
|
monkeypatch.setattr(sys, "argv", ["codex-graph"])
|
|
175
175
|
monkeypatch.chdir(tmp_path)
|
|
176
176
|
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
|
@@ -178,8 +178,27 @@ class TestAutoMap:
|
|
|
178
178
|
from codex_graph.cli import main
|
|
179
179
|
main()
|
|
180
180
|
assert exc.value.code == 1
|
|
181
|
-
|
|
182
|
-
assert "
|
|
181
|
+
err = capsys.readouterr().err
|
|
182
|
+
assert "No source code" in err
|
|
183
|
+
|
|
184
|
+
def test_no_args_flat_repo_runs_map(self, tmp_path, monkeypatch, capsys):
|
|
185
|
+
(tmp_path / "app.py").write_text("def main():\n return 1\n")
|
|
186
|
+
monkeypatch.setattr(sys, "argv", ["codex-graph"])
|
|
187
|
+
monkeypatch.chdir(tmp_path)
|
|
188
|
+
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
|
189
|
+
|
|
190
|
+
called = {}
|
|
191
|
+
|
|
192
|
+
def fake_run_map(root, mono_cfg, backend_override=None, dry_run=False):
|
|
193
|
+
called["root"] = root
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
monkeypatch.setattr("codex_graph.multirepo.run_map", fake_run_map)
|
|
197
|
+
with pytest.raises(SystemExit) as exc:
|
|
198
|
+
from codex_graph.cli import main
|
|
199
|
+
main()
|
|
200
|
+
assert exc.value.code == 0
|
|
201
|
+
assert called["root"] == str(tmp_path)
|
|
183
202
|
|
|
184
203
|
|
|
185
204
|
class TestExistingPromptPathUnaffected:
|
|
@@ -275,7 +294,7 @@ class TestDoctorDispatch:
|
|
|
275
294
|
def test_doctor_empty_root_fails(self, tmp_path, monkeypatch, capsys):
|
|
276
295
|
from codex_graph import doctor
|
|
277
296
|
|
|
278
|
-
monkeypatch.setattr(doctor
|
|
297
|
+
monkeypatch.setattr(doctor, "find_graphify", lambda: None)
|
|
279
298
|
monkeypatch.setattr(sys, "argv", ["graphnav", "doctor", "--root", str(tmp_path)])
|
|
280
299
|
with pytest.raises(SystemExit) as exc:
|
|
281
300
|
from codex_graph.cli import main
|
|
@@ -27,7 +27,7 @@ def fresh_memo():
|
|
|
27
27
|
|
|
28
28
|
@pytest.fixture
|
|
29
29
|
def fake_graphify(monkeypatch):
|
|
30
|
-
monkeypatch.setattr(doctor
|
|
30
|
+
monkeypatch.setattr(doctor, "find_graphify", lambda: "/usr/bin/graphify")
|
|
31
31
|
monkeypatch.setattr(
|
|
32
32
|
doctor.subprocess, "run",
|
|
33
33
|
lambda *a, **k: subprocess.CompletedProcess(a, 0, stdout="graphify 0.9", stderr=""),
|
|
@@ -61,7 +61,7 @@ class TestDoctorAllPass:
|
|
|
61
61
|
|
|
62
62
|
class TestDoctorGraphifyMissing:
|
|
63
63
|
def test_missing_binary_fails(self, healthy_repo, monkeypatch, capsys):
|
|
64
|
-
monkeypatch.setattr(doctor
|
|
64
|
+
monkeypatch.setattr(doctor, "find_graphify", lambda: None)
|
|
65
65
|
rc = run_doctor(str(healthy_repo))
|
|
66
66
|
out = capsys.readouterr().out
|
|
67
67
|
assert rc == 1
|
|
@@ -28,6 +28,7 @@ from codex_graph.multirepo import (
|
|
|
28
28
|
build_playbook_text,
|
|
29
29
|
detect_services,
|
|
30
30
|
partition_graph,
|
|
31
|
+
resolve_services,
|
|
31
32
|
run_extract,
|
|
32
33
|
run_map,
|
|
33
34
|
run_watch,
|
|
@@ -188,6 +189,27 @@ class TestDetectServices:
|
|
|
188
189
|
assert result[0].graph_path == str(d / "graphify-out" / "graph.json")
|
|
189
190
|
assert result[0].bridges_to == []
|
|
190
191
|
|
|
192
|
+
def test_resolve_falls_back_to_whole_repo_when_no_subdir_services(self, tmp_path):
|
|
193
|
+
(tmp_path / "app.py").write_text("def main():\n return 1\n")
|
|
194
|
+
services, single = resolve_services(str(tmp_path), ["pyproject.toml"])
|
|
195
|
+
assert single is True
|
|
196
|
+
assert len(services) == 1
|
|
197
|
+
assert services[0].abs_path == str(tmp_path)
|
|
198
|
+
assert services[0].graph_path == str(tmp_path / "graphify-out" / "graph.json")
|
|
199
|
+
|
|
200
|
+
def test_resolve_prefers_subdir_services(self, tmp_path):
|
|
201
|
+
d = tmp_path / "svc-a"
|
|
202
|
+
d.mkdir()
|
|
203
|
+
(d / "pyproject.toml").touch()
|
|
204
|
+
services, single = resolve_services(str(tmp_path), ["pyproject.toml"])
|
|
205
|
+
assert single is False
|
|
206
|
+
assert [s.name for s in services] == ["svc-a"]
|
|
207
|
+
|
|
208
|
+
def test_resolve_empty_dir_returns_nothing(self, tmp_path):
|
|
209
|
+
services, single = resolve_services(str(tmp_path), ["pyproject.toml"])
|
|
210
|
+
assert services == []
|
|
211
|
+
assert single is False
|
|
212
|
+
|
|
191
213
|
def test_services_returned_in_sorted_order(self, tmp_path):
|
|
192
214
|
for name in ("zebra", "alpha", "middle"):
|
|
193
215
|
d = tmp_path / name
|
|
@@ -1182,6 +1204,7 @@ class TestRunMap:
|
|
|
1182
1204
|
return []
|
|
1183
1205
|
|
|
1184
1206
|
monkeypatch.setattr("codex_graph.multirepo.detect_services", fake_detect)
|
|
1207
|
+
monkeypatch.setattr("codex_graph.multirepo._has_source_files", lambda *a, **k: False)
|
|
1185
1208
|
run_map(".", MonoConfig())
|
|
1186
1209
|
assert os.path.isabs(roots_seen[0])
|
|
1187
1210
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|