|
| 1 | +# Plan: Walk-up config discovery and configurable source root |
| 2 | + |
| 3 | +Status: **active (planning)**. This plan implements |
| 4 | +[`propose/active/DIRS-HIERARCHY-PROPOSE.md`](../../propose/active/DIRS-HIERARCHY-PROPOSE.md) |
| 5 | +as a single PR. |
| 6 | + |
| 7 | +Depends on: none. |
| 8 | + |
| 9 | +## Goal |
| 10 | + |
| 11 | +- Users can run CLI and MCP commands from any subdirectory within their project — the tool walks up to find `.java-codebase-rag.yml`, like git finds `.git`. |
| 12 | +- The YAML config gains an optional `source_root` field so the config can live separately from the Java source code, and the index dir auto-derives from the resolved source root. |
| 13 | +- Existing workflows where cwd = config dir produce identical behavior. No breaking changes. |
| 14 | + |
| 15 | +## Principles (do not relitigate in review) |
| 16 | + |
| 17 | +- **First match wins.** Closest config to cwd, not "most specific" or "deepest". Matches git behavior. |
| 18 | +- **`$HOME` is the inclusive boundary.** Check `$HOME` itself, do not walk past it. |
| 19 | +- **Walk-up is always-on** when no explicit source root is given (CLI flag or env var). No `--walk-up` opt-out flag. |
| 20 | +- **YAML `source_root` resolves relative to the config file directory.** CLI `--source-root` resolves relative to cwd. Different resolution bases are intentional — the precedence table handles priority. |
| 21 | +- **Index dir follows source root.** Default index dir = `<source_root>/.java-codebase-rag/`. This does not change; walk-up just changes how source root itself is found. |
| 22 | +- **No changes to `init` behavior** beyond a soft warning when a parent config exists. |
| 23 | +- **No changes to indexing, query, or graph-building logic.** This is config discovery only. |
| 24 | + |
| 25 | +## PR breakdown - overview |
| 26 | + |
| 27 | +| PR | Scope | Ontology bump | Areas of concern | Test buckets | Independent of | |
| 28 | +| --- | --- | --- | --- | --- | --- | |
| 29 | +| PR-1 | Walk-up config discovery + `source_root` YAML field | none | Precedence chain correctness; server/CLI parity; boundary conditions ($HOME, root) | unit tests for discovery + precedence + integration | — | |
| 30 | + |
| 31 | +Landing order: **PR-1** (single PR). |
| 32 | + |
| 33 | +## Resolved design decisions |
| 34 | + |
| 35 | +| Topic | Decision | |
| 36 | +| --- | --- | |
| 37 | +| Config file name checked | `.java-codebase-rag.yml` and `.java-codebase-rag.yaml` (existing `YAML_CONFIG_FILENAMES` tuple) | |
| 38 | +| Boundary for walk-up | `$HOME` inclusive — check `$HOME` itself but do not walk past it | |
| 39 | +| YAML field name | `source_root` — same name as CLI flag for conceptual consistency | |
| 40 | +| Resolution base for YAML `source_root` | Config file's parent directory (not cwd) | |
| 41 | +| `init` behavior | Unchanged — creates config + index in the specified directory. Only adds a soft warning if a parent config is detected | |
| 42 | +| Multiple nested configs | First match wins (closest to cwd). Mirrors git behavior | |
| 43 | +| New function location | `config.py` — all config resolution logic lives there already | |
| 44 | +| `discover_project_root` return | `Path | None` — returns the directory containing the config file, not the config file path itself | |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +# PR-1 — Walk-up config discovery and configurable source root |
| 49 | + |
| 50 | +## File-by-file changes |
| 51 | + |
| 52 | +### 1. `java_codebase_rag/config.py` |
| 53 | + |
| 54 | +**New function: `discover_project_root(start: Path) -> Path | None`** |
| 55 | + |
| 56 | +- Canonicalize `start` via `Path.resolve()` |
| 57 | +- Walk from `start` upward, checking each directory for files matching `YAML_CONFIG_FILENAMES` |
| 58 | +- First match returns that directory (the parent of the found config file, not the file path itself) |
| 59 | +- Stop at `$HOME` (inclusive — check `$HOME` itself) or filesystem root. Do not walk past `$HOME`. |
| 60 | +- Return `None` if no config found |
| 61 | + |
| 62 | +**Modify: `resolve_operator_config()` — two-phase resolution** |
| 63 | + |
| 64 | +The core change is separating *config file discovery* from *effective source root resolution*. The exact sequence: |
| 65 | + |
| 66 | +1. **Phase 1 — find the config file directory.** If `source_root` is provided (CLI flag or env var), the config dir = that value (no walk-up). Otherwise, call `discover_project_root(Path.cwd())`. If walk-up found a config dir, use it. Otherwise fall back to `Path.cwd().resolve()` (unchanged behavior). |
| 67 | +2. **Load YAML** from the config dir via `load_yaml_mapping(config_dir)`. |
| 68 | +3. **Phase 2 — resolve effective source root.** Check for a `source_root` key in the YAML. If present, resolve it relative to the config dir (not cwd). The effective source root is then: |
| 69 | + - CLI `--source-root` (already handled — `source_root` is not `None` in phase 1, so phase 2 is skipped) |
| 70 | + - env `JAVA_CODEBASE_RAG_SOURCE_ROOT` (checked before walk-up in both `server.py` and `resolve_operator_config`) |
| 71 | + - YAML `source_root` (resolved relative to config dir) |
| 72 | + - Walk-up discovery result (= config dir itself, which is the default when no YAML override) |
| 73 | + - `Path.cwd()` (no config found, no YAML override) |
| 74 | +4. **Derive index dir** from the effective source root via `_resolve_index_dir_path()`. No edits to `_resolve_index_dir_path` itself — the caller ensures the effective source root (after YAML resolution) is what gets passed through. |
| 75 | + |
| 76 | +**Note:** Do NOT introduce a `find_config_dir` wrapper. The two-phase logic lives directly in `resolve_operator_config()` for clarity. The only new public function is `discover_project_root()`. |
| 77 | + |
| 78 | +### 2. `server.py` |
| 79 | + |
| 80 | +**Modify: `_project_root()`** |
| 81 | + |
| 82 | +- Current logic: env var → cwd fallback |
| 83 | +- New logic: env var → `discover_project_root(Path.cwd())` → cwd fallback |
| 84 | +- Import `discover_project_root` from `java_codebase_rag.config` |
| 85 | + |
| 86 | +**Modify: `_resolve_lancedb_uri()`** |
| 87 | + |
| 88 | +- Currently falls back to `Path.cwd() / ".java-codebase-rag"` when `JAVA_CODEBASE_RAG_INDEX_DIR` is unset. |
| 89 | +- After walk-up, this should use the discovered source root (via `_project_root()`) for consistency. |
| 90 | +- The server's `list_code_index_tables_payload()` calls `resolve_operator_config(source_root=_project_root())`, so index dir is derived from the effective source root. But `_resolve_lancedb_uri()` is called independently in some paths. Ensure it uses `_project_root()` instead of raw `Path.cwd()` when the env var is unset. |
| 91 | + |
| 92 | +### 3. `java_codebase_rag/cli.py` |
| 93 | + |
| 94 | +**Modify: `_parse_source_root()` / `_resolved_from_ns()`** |
| 95 | + |
| 96 | +- `_parse_source_root()` stays the same (returns `None` when `--source-root` is not given) |
| 97 | +- `_resolved_from_ns()` already passes `source_root=root` to `resolve_operator_config()` — walk-up logic in `resolve_operator_config()` handles the `None` case |
| 98 | + |
| 99 | +**Modify: `init` command handler** |
| 100 | + |
| 101 | +- After resolving `cfg = _resolved_from_ns(args)`, check for a parent config by calling `discover_project_root(cfg.source_root.parent)` — this checks whether a config exists in any ancestor of the *resolved source root* (not the config dir, since `init` creates the config at the source root) |
| 102 | +- If found, emit a soft warning to stderr: |
| 103 | + > Warning: found existing config at `[parent]/.java-codebase-rag.yml`. Creating a new project here will create a separate index. |
| 104 | +
|
| 105 | +### 4. `mcp.json.example` |
| 106 | + |
| 107 | +- Add a comment block showing the minimal zero-env-var config |
| 108 | +- Keep the existing full example as an alternative |
| 109 | +- Show both Claude Desktop and Claude Code variants |
| 110 | + |
| 111 | +### 5. `README.md` |
| 112 | + |
| 113 | +- Update the MCP host wiring section to mention walk-up discovery |
| 114 | +- Document the `source_root` YAML field |
| 115 | +- Update the minimal `.mcp.json` example to show that env vars are now optional |
| 116 | + |
| 117 | +### 6. `docs/CONFIGURATION.md` |
| 118 | + |
| 119 | +- Add `source_root` to the YAML config reference table |
| 120 | +- Document the walk-up discovery behavior |
| 121 | +- Update the precedence chain table to include the YAML `source_root` field |
| 122 | + |
| 123 | +## Tests for PR-1 |
| 124 | + |
| 125 | +All new tests go in **`tests/test_config.py`** (new file). Tests that exercise `_project_root()` in `server.py` go in **`tests/test_mcp_server_project_root.py`** (new file) to keep MCP test concerns separate. |
| 126 | + |
| 127 | +### Config discovery tests (`tests/test_config.py`) |
| 128 | + |
| 129 | +1. `test_discover_project_root_finds_config_in_cwd` — config in cwd, returns cwd |
| 130 | +2. `test_discover_project_root_walks_up` — config in parent, returns parent |
| 131 | +3. `test_discover_project_root_stops_at_home_boundary` — config in `$HOME` itself, walk-up from subdirectory of `$HOME` finds it (inclusive boundary) |
| 132 | +4. `test_discover_project_root_not_found_above_home` — no config anywhere under `$HOME`, returns `None` |
| 133 | +5. `test_discover_project_root_not_found` — no config anywhere, returns `None` |
| 134 | +6. `test_discover_project_root_first_match_wins` — configs at two levels (cwd subdirectory has one, parent has another), closest to cwd wins |
| 135 | + |
| 136 | +### Source root resolution tests (`tests/test_config.py`) |
| 137 | + |
| 138 | +7. `test_source_root_from_yaml_relative` — `source_root: ../` resolves to parent of config dir |
| 139 | +8. `test_source_root_from_yaml_absolute` — `source_root: /abs/path` resolves to absolute path |
| 140 | +9. `test_source_root_precedence_cli_over_yaml` — CLI flag wins over YAML `source_root` |
| 141 | +10. `test_source_root_precedence_yaml_over_discovery` — YAML `source_root` wins over config dir default |
| 142 | +11. `test_source_root_precedence_env_over_yaml` — env var wins over YAML `source_root` |
| 143 | +12. `test_existing_behavior_unchanged` — no walk-up, cwd = config dir → identical behavior to today |
| 144 | + |
| 145 | +### Server integration test (`tests/test_mcp_server_project_root.py`) |
| 146 | + |
| 147 | +13. `test_project_root_uses_discover_when_env_unset` — `_project_root()` returns discovered config dir when `JAVA_CODEBASE_RAG_SOURCE_ROOT` is unset |
| 148 | + |
| 149 | +## Definition of done (PR-1) |
| 150 | + |
| 151 | +- [ ] `discover_project_root()` works with first-match-wins semantics, stops at `$HOME` (inclusive) |
| 152 | +- [ ] `source_root` YAML field is parsed and resolved relative to config dir |
| 153 | +- [ ] Precedence chain: CLI > env > YAML > discovery > cwd |
| 154 | +- [ ] `_project_root()` in `server.py` uses walk-up when env var is unset |
| 155 | +- [ ] `_resolve_lancedb_uri()` in `server.py` uses `_project_root()` instead of raw `Path.cwd()` for fallback |
| 156 | +- [ ] CLI commands work from subdirectories (walk-up finds config) |
| 157 | +- [ ] `init` emits soft warning when parent config detected |
| 158 | +- [ ] All 13 named tests pass |
| 159 | +- [ ] Existing test suite passes (no regressions) |
| 160 | +- [ ] `mcp.json.example` shows minimal zero-env-var config |
| 161 | +- [ ] README and CONFIGURATION docs updated |
| 162 | + |
| 163 | +## Implementation step list |
| 164 | + |
| 165 | +| # | Step | File(s) | Done when | |
| 166 | +| - | - | - | - | |
| 167 | +| 1 | Add `discover_project_root(start)` | `config.py`, `tests/test_config.py` | Tests 1–6 pass | |
| 168 | +| 2 | Add `source_root` YAML field parsing and resolution | `config.py`, `tests/test_config.py` | Tests 7–8 pass | |
| 169 | +| 3 | Wire precedence chain in `resolve_operator_config()` | `config.py`, `tests/test_config.py` | Tests 9–12 pass | |
| 170 | +| 4 | Update `_project_root()` to use walk-up | `server.py`, `tests/test_mcp_server_project_root.py` | Test 13 passes; server resolves source root via walk-up when env var unset | |
| 171 | +| 5 | Update `_resolve_lancedb_uri()` to use `_project_root()` fallback | `server.py` | Lance URI and source root derive from same discovered root | |
| 172 | +| 6 | Add `init` parent-config warning | `cli.py` | `init` prints warning when parent config exists | |
| 173 | +| 7 | Update `mcp.json.example` | `mcp.json.example` | Shows minimal zero-env-var config | |
| 174 | +| 8 | Update README and CONFIGURATION docs | `README.md`, `docs/CONFIGURATION.md` | Walk-up and `source_root` documented | |
| 175 | +| 9 | Run full validation | all | `ruff check` + `pytest tests -v` green | |
| 176 | + |
| 177 | +--- |
| 178 | + |
| 179 | +# Cross-PR risks and mitigations |
| 180 | + |
| 181 | +N/A — single PR. |
| 182 | + |
| 183 | +# Out of scope |
| 184 | + |
| 185 | +- Auto-detecting multiple systems and splitting indexes |
| 186 | +- Changing index directory structure |
| 187 | +- Global config or project registry |
| 188 | +- Changes to indexing, query, or graph-building logic |
| 189 | +- `init` command behavior changes beyond the parent-config warning |
| 190 | +- Changes to `build_ast_graph.py` or `search_lancedb.py` |
| 191 | + |
| 192 | +# Whole-plan done definition |
| 193 | + |
| 194 | +1. All 13 named tests pass. |
| 195 | +2. Existing test suite passes without `JAVA_CODEBASE_RAG_RUN_HEAVY`. |
| 196 | +3. `ruff check .` is clean. |
| 197 | +4. CLI and MCP server both resolve source root via walk-up from subdirectories. |
| 198 | +5. `mcp.json.example` shows a zero-env-var configuration. |
| 199 | +6. README and CONFIGURATION docs reflect the new behavior. |
| 200 | + |
| 201 | +# Tracking |
| 202 | + |
| 203 | +- `PR-1`: _pending_ |
0 commit comments