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.
Files changed (30) hide show
  1. {graphnav-1.2.0/graphnav.egg-info → graphnav-1.2.2}/PKG-INFO +1 -1
  2. {graphnav-1.2.0 → graphnav-1.2.2}/README.md +42 -21
  3. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/cli.py +23 -17
  4. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/doctor.py +3 -3
  5. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/multirepo.py +82 -25
  6. {graphnav-1.2.0 → graphnav-1.2.2/graphnav.egg-info}/PKG-INFO +1 -1
  7. {graphnav-1.2.0 → graphnav-1.2.2}/pyproject.toml +1 -1
  8. {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_cli.py +23 -4
  9. {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_doctor.py +2 -2
  10. {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_multirepo.py +23 -0
  11. {graphnav-1.2.0 → graphnav-1.2.2}/LICENSE +0 -0
  12. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/__init__.py +0 -0
  13. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/config.py +0 -0
  14. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/graph_cache.py +0 -0
  15. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/graph_nav.py +0 -0
  16. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/graph_query.py +0 -0
  17. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/mcp_server.py +0 -0
  18. {graphnav-1.2.0 → graphnav-1.2.2}/codex_graph/runner.py +0 -0
  19. {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/SOURCES.txt +0 -0
  20. {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/dependency_links.txt +0 -0
  21. {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/entry_points.txt +0 -0
  22. {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/requires.txt +0 -0
  23. {graphnav-1.2.0 → graphnav-1.2.2}/graphnav.egg-info/top_level.txt +0 -0
  24. {graphnav-1.2.0 → graphnav-1.2.2}/setup.cfg +0 -0
  25. {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_config.py +0 -0
  26. {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_graph_cache.py +0 -0
  27. {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_graph_nav.py +0 -0
  28. {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_graph_query.py +0 -0
  29. {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_mcp_server.py +0 -0
  30. {graphnav-1.2.0 → graphnav-1.2.2}/tests/test_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphnav
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Knowledge-graph context injection for AI coding agents in monorepos
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -27,13 +27,22 @@ GraphNav solves this by:
27
27
 
28
28
  ---
29
29
 
30
- ## Install
30
+ ## Setup is one command
31
31
 
32
32
  ```bash
33
33
  pip install graphnav
34
+ graphnav
34
35
  ```
35
36
 
36
- Requires Python 3.11. Pulls `graphifyy` (the `graphify` binary) automatically.
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
- If run with no arguments in a monorepo root, auto-detects services and runs `map` automatically. If a prompt is given, falls through to the context-injection path for the Codex CLI.
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 map # one-time setup, or re-run after large refactors
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.detect_services(root, cfg.mono.marker_files, cfg.mono.extra_skip_dirs)
54
+ services, single = multirepo.resolve_services(root, cfg.mono.marker_files, cfg.mono.extra_skip_dirs)
55
55
  if not services:
56
- return
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
- names = ", ".join(s.name for s in services)
59
- print(f"[graphnav] Detected {len(services)} service(s): {names}")
60
- print(f"[graphnav] Running 'graphnav map' to build knowledge graphs ...", file=sys.stderr)
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
- "Codex CLI with knowledge-graph context injection for monorepos.\n\n"
220
- "First-run (after pip install): just run 'graphnav' or 'graphnav map'\n"
221
- "in your monorepo root — services are auto-detected and graphs are built.\n\n"
222
- "Subcommands:\n"
223
- " map Build per-service graphs and cross-service bridge notes\n"
224
- " watch Keep graphs and bridge notes up-to-date as files change\n"
225
- " context Print a token-budgeted context pack for a task (free, no LLM)\n"
226
- " serve Run the MCP server so AI agents call the graph tools natively\n"
227
- " find Find symbols by query; neighbors/impact show a symbol's blast radius\n"
228
- " doctor Diagnose the setup (graphify binary, config, graph, API key, cache)"
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
- parser.print_help()
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 = shutil.which("graphify")
38
+ path = find_graphify()
39
39
  if path is None:
40
- return CheckResult("fail", "graphify binary", "not found on PATH — install with: pip install graphifyy")
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
- partition_graph(overarching_graph_path, services)
811
- bridges = analyze_bridges(overarching_graph_path, services)
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 = shutil.which("graphify")
878
+ graphify_path = find_graphify()
829
879
  if graphify_path is None:
830
- print("Error: 'graphify' not found on PATH. Install with: pip install graphifyy", file=sys.stderr)
880
+ print("Error: 'graphify' not found. Install with: pip install graphifyy", file=sys.stderr)
831
881
  return 1
832
882
 
833
- services = detect_services(root, mono_cfg.marker_files, mono_cfg.extra_skip_dirs)
883
+ services, single = resolve_services(root, mono_cfg.marker_files, mono_cfg.extra_skip_dirs)
834
884
  if not services:
835
- print(f"No services detected in {root}. Add code to subdirectories (or marker files like package.json/pyproject.toml).", file=sys.stderr)
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 {len(services)} service(s):")
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 overarching graph across {len(services)} service(s): {', '.join(s.name for s in services)}", file=sys.stderr)
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: overarching graphify extraction failed (exit {rc}).", file=sys.stderr)
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"\nDone. {len(services)} service(s) mapped, {total_bridges} cross-service connection(s) found.")
860
- print(f" Overarching graph : {overarching_path}")
861
- for svc in services:
862
- to = ", ".join(svc.bridges_to) if svc.bridges_to else "none"
863
- print(f" {svc.name}/graphify-out/ (bridges -> {to})")
864
- print(f" Monorepo map : {os.path.join(root, 'graphify-out', 'MONOREPO_MAP.md')}")
865
- print(f" Copilot instructions : {os.path.join(root, '.github', 'copilot-instructions.md')}")
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 = shutil.which("graphify")
932
+ graphify_path = find_graphify()
876
933
  if graphify_path is None:
877
- print("Error: 'graphify' not found on PATH. Install with: pip install graphifyy", file=sys.stderr)
934
+ print("Error: 'graphify' not found. Install with: pip install graphifyy", file=sys.stderr)
878
935
  return 1
879
936
 
880
- services = detect_services(root, mono_cfg.marker_files, mono_cfg.extra_skip_dirs)
937
+ services, single = resolve_services(root, mono_cfg.marker_files, mono_cfg.extra_skip_dirs)
881
938
  if not services:
882
- print(f"No services detected in {root}.", file=sys.stderr)
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 overarching graph for {len(services)} service(s) ...", file=sys.stderr)
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 — re-partitioning and re-analyzing bridges ...", file=sys.stderr)
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphnav
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Knowledge-graph context injection for AI coding agents in monorepos
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphnav"
7
- version = "1.2.0"
7
+ version = "1.2.2"
8
8
  description = "Knowledge-graph context injection for AI coding agents in monorepos"
9
9
  license = "MIT"
10
10
  requires-python = ">=3.11"
@@ -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 test_no_args_without_services_shows_help(self, tmp_path, monkeypatch, capsys):
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
- out = capsys.readouterr().out
182
- assert "graphnav" in out
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.shutil, "which", lambda _: None)
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.shutil, "which", lambda _: "/usr/bin/graphify")
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.shutil, "which", lambda _: None)
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