graphnav 1.2.0__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.0/graphnav.egg-info → graphnav-1.2.2}/PKG-INFO +1 -1
- {graphnav-1.2.0 → graphnav-1.2.2}/README.md +42 -21
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/cli.py +23 -17
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/doctor.py +3 -3
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/multirepo.py +82 -25
- {graphnav-1.2.0 → graphnav-1.2.2/graphnav.egg-info}/PKG-INFO +1 -1
- {graphnav-1.2.0 → graphnav-1.2.2}/pyproject.toml +1 -1
- {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_cli.py +23 -4
- {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_doctor.py +2 -2
- {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_multirepo.py +23 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/LICENSE +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/__init__.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/config.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/graph_cache.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/graph_nav.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/graph_query.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/mcp_server.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/runner.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/SOURCES.txt +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/dependency_links.txt +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/entry_points.txt +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/requires.txt +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/top_level.txt +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/setup.cfg +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_config.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_graph_cache.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_graph_nav.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_graph_query.py +0 -0
- {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_mcp_server.py +0 -0
- {graphnav-1.2.0 → 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
|
|
|
@@ -360,6 +375,12 @@ The generated `CLAUDE.md`, `AGENTS.md`, and `.github/copilot-instructions.md` ca
|
|
|
360
375
|
|
|
361
376
|
---
|
|
362
377
|
|
|
378
|
+
## Changelog
|
|
379
|
+
|
|
380
|
+
See [CHANGELOG.md](CHANGELOG.md) for a full version history.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
363
384
|
## License
|
|
364
385
|
|
|
365
386
|
[MIT](LICENSE) © 2026 Amogh Rao
|
|
@@ -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()
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
-
import shutil
|
|
6
5
|
import subprocess
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
|
|
@@ -13,6 +12,7 @@ from codex_graph.multirepo import (
|
|
|
13
12
|
_load_env_file,
|
|
14
13
|
_overarching_graph_path,
|
|
15
14
|
detect_services,
|
|
15
|
+
find_graphify,
|
|
16
16
|
staleness_note,
|
|
17
17
|
)
|
|
18
18
|
|
|
@@ -35,9 +35,9 @@ class CheckResult:
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _check_graphify_binary() -> CheckResult:
|
|
38
|
-
path =
|
|
38
|
+
path = find_graphify()
|
|
39
39
|
if path is None:
|
|
40
|
-
return CheckResult("fail", "graphify binary", "not found
|
|
40
|
+
return CheckResult("fail", "graphify binary", "not found — install with: pip install graphifyy")
|
|
41
41
|
detail = path
|
|
42
42
|
try:
|
|
43
43
|
out = subprocess.run([path, "--version"], capture_output=True, text=True, timeout=5)
|
|
@@ -13,6 +13,52 @@ from dataclasses import dataclass, field
|
|
|
13
13
|
from codex_graph.config import MonoConfig, QueryConfig
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def find_graphify() -> str | None:
|
|
17
|
+
path = shutil.which("graphify")
|
|
18
|
+
if path:
|
|
19
|
+
return path
|
|
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
|
|
39
|
+
return None
|
|
40
|
+
|
|
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
|
+
|
|
16
62
|
def _warn(msg: str) -> None:
|
|
17
63
|
print(f"[graphnav] warning: {msg}", file=sys.stderr)
|
|
18
64
|
|
|
@@ -806,9 +852,13 @@ def _refresh(
|
|
|
806
852
|
root: str,
|
|
807
853
|
services: list[ServiceInfo],
|
|
808
854
|
overarching_graph_path: str,
|
|
855
|
+
single: bool = False,
|
|
809
856
|
) -> dict[str, list[BridgeRow]]:
|
|
810
|
-
|
|
811
|
-
|
|
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)
|
|
812
862
|
for svc in services:
|
|
813
863
|
write_bridges_md(svc, bridges[svc.name])
|
|
814
864
|
write_symbols_md(svc)
|
|
@@ -825,18 +875,19 @@ def run_map(
|
|
|
825
875
|
dry_run: bool = False,
|
|
826
876
|
) -> int:
|
|
827
877
|
root = os.path.abspath(root)
|
|
828
|
-
graphify_path =
|
|
878
|
+
graphify_path = find_graphify()
|
|
829
879
|
if graphify_path is None:
|
|
830
|
-
print("Error: 'graphify' not found
|
|
880
|
+
print("Error: 'graphify' not found. Install with: pip install graphifyy", file=sys.stderr)
|
|
831
881
|
return 1
|
|
832
882
|
|
|
833
|
-
services =
|
|
883
|
+
services, single = resolve_services(root, mono_cfg.marker_files, mono_cfg.extra_skip_dirs)
|
|
834
884
|
if not services:
|
|
835
|
-
print(f"No
|
|
885
|
+
print(f"No source code found in {root}. Run graphnav from a directory that contains code.", file=sys.stderr)
|
|
836
886
|
return 1
|
|
837
887
|
|
|
888
|
+
shape = "whole repo (single project)" if single else f"{len(services)} service(s): {', '.join(s.name for s in services)}"
|
|
838
889
|
if dry_run:
|
|
839
|
-
print(f"Detected {
|
|
890
|
+
print(f"Detected {shape}:")
|
|
840
891
|
for svc in services:
|
|
841
892
|
print(f" {svc.name} {svc.abs_path}")
|
|
842
893
|
print("[dry-run] No graphify calls made.")
|
|
@@ -846,23 +897,29 @@ def run_map(
|
|
|
846
897
|
env = _build_subprocess_env(root)
|
|
847
898
|
overarching_path = _overarching_graph_path(root)
|
|
848
899
|
|
|
849
|
-
print(f"[graphnav] Building
|
|
900
|
+
print(f"[graphnav] Building knowledge graph for {shape} ...", file=sys.stderr)
|
|
850
901
|
rc = build_overarching_graph(root, graphify_path, backend, env=env)
|
|
851
902
|
if rc != 0 or not os.path.exists(overarching_path):
|
|
852
|
-
print(f"Error:
|
|
903
|
+
print(f"Error: graphify extraction failed (exit {rc}).", file=sys.stderr)
|
|
853
904
|
print(" Ensure an API key is available (e.g. ANTHROPIC_API_KEY or ANTHROPIC_KEY in a .env file).", file=sys.stderr)
|
|
854
905
|
return 1
|
|
855
906
|
|
|
856
|
-
bridges = _refresh(root, services, overarching_path)
|
|
907
|
+
bridges = _refresh(root, services, overarching_path, single=single)
|
|
857
908
|
total_bridges = sum(len(rows) for rows in bridges.values())
|
|
858
909
|
|
|
859
|
-
print(f"\
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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.)")
|
|
866
923
|
return 0
|
|
867
924
|
|
|
868
925
|
|
|
@@ -872,14 +929,14 @@ def run_watch(
|
|
|
872
929
|
backend_override: str | None = None,
|
|
873
930
|
) -> int:
|
|
874
931
|
root = os.path.abspath(root)
|
|
875
|
-
graphify_path =
|
|
932
|
+
graphify_path = find_graphify()
|
|
876
933
|
if graphify_path is None:
|
|
877
|
-
print("Error: 'graphify' not found
|
|
934
|
+
print("Error: 'graphify' not found. Install with: pip install graphifyy", file=sys.stderr)
|
|
878
935
|
return 1
|
|
879
936
|
|
|
880
|
-
services =
|
|
937
|
+
services, single = resolve_services(root, mono_cfg.marker_files, mono_cfg.extra_skip_dirs)
|
|
881
938
|
if not services:
|
|
882
|
-
print(f"No
|
|
939
|
+
print(f"No source code found in {root}.", file=sys.stderr)
|
|
883
940
|
return 1
|
|
884
941
|
|
|
885
942
|
backend = backend_override or mono_cfg.graphify_backend
|
|
@@ -887,13 +944,13 @@ def run_watch(
|
|
|
887
944
|
overarching_path = _overarching_graph_path(root)
|
|
888
945
|
|
|
889
946
|
if not os.path.exists(overarching_path):
|
|
890
|
-
print(f"[graphnav] Bootstrapping
|
|
947
|
+
print(f"[graphnav] Bootstrapping knowledge graph ...", file=sys.stderr)
|
|
891
948
|
rc = build_overarching_graph(root, graphify_path, backend, env=env)
|
|
892
949
|
if rc != 0 or not os.path.exists(overarching_path):
|
|
893
950
|
print(f"Error: bootstrap extraction failed (exit {rc}).", file=sys.stderr)
|
|
894
951
|
return 1
|
|
895
952
|
|
|
896
|
-
_refresh(root, services, overarching_path)
|
|
953
|
+
_refresh(root, services, overarching_path, single=single)
|
|
897
954
|
|
|
898
955
|
def _start_watch() -> subprocess.Popen:
|
|
899
956
|
return subprocess.Popen(
|
|
@@ -927,8 +984,8 @@ def run_watch(
|
|
|
927
984
|
last_mtime = mtime
|
|
928
985
|
pending_mtime = None
|
|
929
986
|
ts = time.strftime("%H:%M:%S")
|
|
930
|
-
print(f"[graphnav] {ts} graph updated —
|
|
931
|
-
_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)
|
|
932
989
|
else:
|
|
933
990
|
pending_mtime = mtime
|
|
934
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
|