Skip to content

Commit 8d866f1

Browse files
HumanBean17claude
andcommitted
fix(config): resolve yaml index_dir vs config dir; mcp honors yaml source_root
A config file living in a subdirectory of the Java tree resolved inconsistently between the CLI and the MCP server, so no single index_dir value worked for both. Two compounding bugs: 1. _resolve_index_dir_path resolved a YAML `index_dir` relative to the already-resolved `source_root`, while `source_root` itself resolved relative to the config file's directory. A `../` in index_dir was re-applied on top of source_root and overshot by one level (the "init indexes ~/" symptom). YAML index_dir now resolves against the config file's directory, the same base as source_root. CLI/env index_dir and the default stay source_root-relative (unchanged). 2. server.main() passed source_root=_project_root() (the walk-up- discovered config dir) to resolve_operator_config, routing into the branch that treats it as an explicit override and skips the YAML source_root field. The CLI passes source_root=None, which honors the field -- so the same config produced a different effective root for init vs MCP (the "mcp can't find the index" symptom). main() now passes _source_root_for_operator_config() (env-or-None), so the MCP server honors YAML source_root exactly like the CLI; JAVA_CODEBASE_RAG_SOURCE_ROOT still wins when set. With both fixes a config in my-context/ next to source_root: ../ index_dir: ../.java-codebase-rag resolves identically for init and the MCP server. Docs: CONFIGURATION.md index_dir base comment + tips updated. No ontology/embedding change; existing indexes remain valid. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent da99f74 commit 8d866f1

5 files changed

Lines changed: 147 additions & 7 deletions

File tree

docs/CONFIGURATION.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ A single file at the project root (the directory you pass as `--source-root`, or
8787

8888
# Index directory: where Lance tables, code_graph.kuzu, and cocoindex.db live.
8989
# - Tilde (`~`) is expanded; `$VAR` is NOT (use absolute paths or `~`).
90-
# - Relative paths resolve against source_root, not cwd.
90+
# - Relative paths resolve against the config file's parent directory (same
91+
# base as source_root), not cwd. The bare default ./.java-codebase-rag
92+
# (when this key is omitted) still sits beside the resolved source_root.
9193
# - Env: JAVA_CODEBASE_RAG_INDEX_DIR. CLI: --index-dir. Default: ./.java-codebase-rag/
9294
index_dir: ./.java-codebase-rag
9395

@@ -217,15 +219,15 @@ async_producer_overrides:
217219

218220
| Field | Expanded? | Notes |
219221
|---|---|---|
220-
| `index_dir` | partial | `~` expanded; `$VAR` is NOT expanded. Relative paths resolve against `source_root`. |
222+
| `index_dir` | partial | `~` expanded; `$VAR` is NOT expanded. A YAML relative path resolves against the config file's directory (same base as `source_root`); the default `./.java-codebase-rag` sits beside the resolved `source_root`. |
221223
| `embedding.model` (when path-shaped) | yes | Path-shape = starts with `/`, `./`, `../`, `~`, or contains `$`. Plain `org/name` is treated as a hub id and passed through. Applies to the value after CLI > env > YAML > default precedence. Long-lived MCP hosts also apply the same expansion when reading `SBERT_MODEL` from the process environment (so table metadata and search agree with `index_common` defaults). |
222224
| `embedding.device` | n/a | Device strings (`cpu`, `cuda`, `mps`) aren't paths. |
223225
| `microservice_roots[*]` | no | Each entry is a directory **name** relative to `source_root`, not an arbitrary path. |
224226
| Brownfield `path:` / `topic:` values | no | These are URL paths and Kafka topic names, not filesystem paths. Literal characters preserved. |
225227

226228
**Tips & gotchas:**
227229

228-
- **The file must be at `source_root`**, not in `$HOME`. The MCP server reads `JAVA_CODEBASE_RAG_SOURCE_ROOT` to find it; the CLI uses `--source-root` (else cwd).
230+
- **The config file may live anywhere under your project, including a subdirectory of the Java tree.** Both the CLI (`init` / `increment` / `reprocess`) and the MCP server walk up from cwd to find `.java-codebase-rag.yml`, then resolve `source_root` and `index_dir` relative to the config file's directory. So a config living in `my-context/` next to `source_root: ../` and `index_dir: ../.java-codebase-rag` resolves identically for the CLI and the MCP server. Keep the file under your project (not `$HOME`); set `JAVA_CODEBASE_RAG_SOURCE_ROOT` (MCP) or `--source-root` (CLI) only to override the discovered location.
229231
- **Don't commit secrets** into this YAML — it sits next to your source tree and is read by every operator who clones it.
230232
- **Rebuild after editing brownfield overrides.** Run a full `java-codebase-rag reprocess` (no flags) so Lance and Kuzu stay coherent, or use `--graph-only` / `--vectors-only` when you know only one store needs invalidation. Editing `embedding.model` requires a vector rebuild (`reprocess` or `--vectors-only`).
231233
- **Diagnose what's loaded.** `java-codebase-rag meta` prints the resolved config and each value's `*_source` (`cli` / `env` / `yaml` / `default`) — see `embedding_model_source`, `embedding_device_source`, `index_dir_source`.

java_codebase_rag/config.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,19 @@ def _pick_bool(
306306
def _resolve_index_dir_path(
307307
*,
308308
source_root: Path,
309+
config_dir: Path,
309310
cli_index_dir: str | None,
310311
yaml_dict: dict[str, Any],
311312
) -> tuple[Path, SettingSource]:
313+
# Bases for relative paths:
314+
# - YAML ``index_dir`` -> the config file's directory (``config_dir``),
315+
# the SAME base used for YAML ``source_root``. Paths written in the
316+
# config file are relative to the file, so both keys stay consistent.
317+
# - CLI / env ``index_dir`` -> ``source_root`` (unchanged). These are not
318+
# "in the config file"; preserving the existing base avoids a semantics
319+
# change for operators who pass ``--index-dir`` on the command line.
320+
# - Default ``./.java-codebase-rag`` -> ``source_root`` so the index sits
321+
# beside the Java tree (the layout ``discover_project_root`` anchors on).
312322
raw_cli = cli_index_dir.strip() if isinstance(cli_index_dir, str) else None
313323
if raw_cli:
314324
p = Path(raw_cli).expanduser()
@@ -324,7 +334,7 @@ def _resolve_index_dir_path(
324334
idx = yaml_dict.get("index_dir")
325335
if isinstance(idx, str) and idx.strip():
326336
p = Path(idx.strip()).expanduser()
327-
out = p.resolve() if p.is_absolute() else (source_root / p).resolve()
337+
out = p.resolve() if p.is_absolute() else (config_dir / p).resolve()
328338
return out, "yaml"
329339

330340
return (source_root / ".java-codebase-rag").resolve(), "default"
@@ -368,7 +378,7 @@ def resolve_operator_config(
368378
root = config_dir
369379

370380
index_dir, index_src = _resolve_index_dir_path(
371-
source_root=root, cli_index_dir=cli_index_dir, yaml_dict=yaml_dict
381+
source_root=root, config_dir=config_dir, cli_index_dir=cli_index_dir, yaml_dict=yaml_dict
372382
)
373383
model, model_src = _pick_str(
374384
cli_val=cli_embedding_model,

server.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,27 @@ def _project_root() -> Path:
155155
return discovered if discovered is not None else Path.cwd().resolve()
156156

157157

158+
def _source_root_for_operator_config() -> Path | None:
159+
"""``source_root`` arg to hand ``resolve_operator_config`` from the MCP server.
160+
161+
Returns ``JAVA_CODEBASE_RAG_SOURCE_ROOT`` when set (an explicit operator
162+
override that wins and suppresses the YAML ``source_root`` field, exactly
163+
like CLI ``--source-root``), otherwise ``None`` — so
164+
``resolve_operator_config`` runs its OWN walk-up discovery and HONORS the
165+
YAML ``source_root`` field, matching the CLI (``init`` / ``increment`` /
166+
``reprocess``) path.
167+
168+
Do NOT pass ``_project_root()`` (the walk-up-discovered dir) here: a
169+
non-``None`` value routes into the "explicit source root" branch that
170+
skips the YAML ``source_root`` field, which made the MCP server and the
171+
CLI resolve different ``source_root`` / ``index_dir`` from the same config
172+
file (the init-vs-MCP index_dir divergence). ``_project_root()`` is kept
173+
only for the ``_resolve_lancedb_uri()`` fallback below.
174+
"""
175+
env = os.environ.get("JAVA_CODEBASE_RAG_SOURCE_ROOT", "").strip()
176+
return Path(env).expanduser().resolve() if env else None
177+
178+
158179
def _cocoindex_subprocess_env(project_root: Path) -> dict[str, str]:
159180
sub_env = os.environ.copy()
160181
sub_env["JAVA_CODEBASE_RAG_SOURCE_ROOT"] = str(project_root)
@@ -654,7 +675,7 @@ def main() -> None:
654675
# Load YAML config and apply embedding settings to environment
655676
# This ensures SBERT_MODEL and SBERT_DEVICE from .java-codebase-rag.yml are available
656677
# before any tool handler runs (same behavior as CLI path)
657-
cfg = resolve_operator_config(source_root=_project_root())
678+
cfg = resolve_operator_config(source_root=_source_root_for_operator_config())
658679
cfg.apply_to_os_environ()
659680
mcp_v2.set_hints_enabled(cfg.hints_enabled)
660681

tests/test_config.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,57 @@ def test_source_root_from_yaml_absolute(self, tmp_path, monkeypatch):
175175
assert result.source_root == Path(absolute_path)
176176

177177

178+
class TestIndexDirRelativeToConfigDir:
179+
"""YAML ``index_dir`` must resolve against the config file's directory.
180+
181+
``source_root`` already resolves against the config dir (see
182+
``TestSourceRootFromYaml``). ``index_dir`` must use the SAME base so a
183+
user can express both keys relative to the config file — otherwise a
184+
``../`` in ``index_dir`` gets re-applied on top of the already-resolved
185+
``source_root`` and overshoots by one level (the "init indexes ~/"
186+
symptom when the config lives in a subdirectory of the Java tree).
187+
"""
188+
189+
def test_yaml_index_dir_double_dot_resolves_against_config_dir(self, tmp_path, monkeypatch):
190+
"""``index_dir: ../x`` is relative to the config file's directory, not source_root."""
191+
monkeypatch.delenv("JAVA_CODEBASE_RAG_INDEX_DIR", raising=False)
192+
monkeypatch.delenv("JAVA_CODEBASE_RAG_SOURCE_ROOT", raising=False)
193+
194+
config_dir = tmp_path / "my-project-context"
195+
config_dir.mkdir()
196+
(config_dir / YAML_CONFIG_FILENAMES[0]).write_text(
197+
"source_root: ../\nindex_dir: ../.java-codebase-rag\n"
198+
)
199+
monkeypatch.chdir(config_dir)
200+
201+
result = resolve_operator_config(source_root=None)
202+
# source_root ../ -> tmp_path (one level above the config file)
203+
assert result.source_root == tmp_path
204+
# index_dir ../ -> tmp_path/.java-codebase-rag (one level above the config file),
205+
# NOT tmp_path.parent/.java-codebase-rag (which is what resolving against
206+
# the already-resolved source_root would produce).
207+
assert result.index_dir == (tmp_path / ".java-codebase-rag").resolve()
208+
209+
def test_yaml_index_dir_bare_resolves_against_config_dir(self, tmp_path, monkeypatch):
210+
"""``index_dir: x`` (no ``../``) sits next to the config file."""
211+
monkeypatch.delenv("JAVA_CODEBASE_RAG_INDEX_DIR", raising=False)
212+
monkeypatch.delenv("JAVA_CODEBASE_RAG_SOURCE_ROOT", raising=False)
213+
214+
config_dir = tmp_path / "my-project-context"
215+
config_dir.mkdir()
216+
(config_dir / YAML_CONFIG_FILENAMES[0]).write_text(
217+
"source_root: ../\nindex_dir: .java-codebase-rag\n"
218+
)
219+
monkeypatch.chdir(config_dir)
220+
221+
result = resolve_operator_config(source_root=None)
222+
assert result.source_root == tmp_path
223+
# Bare path resolves against the config dir, so the index sits beside
224+
# the config file — NOT beside source_root.
225+
assert result.index_dir == (config_dir / ".java-codebase-rag").resolve()
226+
assert result.index_dir_source == "yaml"
227+
228+
178229
class TestSourceRootPrecedence:
179230
"""Tests for source_root precedence chain."""
180231

tests/test_mcp_server_project_root.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Tests for server.py _project_root() function in the MCP server context."""
22

3-
from java_codebase_rag.config import YAML_CONFIG_FILENAMES
3+
from java_codebase_rag.config import YAML_CONFIG_FILENAMES, resolve_operator_config
44

55

66
class TestProjectRoot:
@@ -23,3 +23,59 @@ def test_project_root_uses_discover_when_env_unset(self, tmp_path, monkeypatch):
2323

2424
result = _project_root()
2525
assert result == tmp_path
26+
27+
28+
class TestSourceRootForOperatorConfig:
29+
"""The MCP server must honor the YAML ``source_root`` field like the CLI.
30+
31+
``main()`` passes ``_source_root_for_operator_config()`` (not the
32+
walk-up-discovered dir) as the ``source_root`` arg to
33+
``resolve_operator_config``. When the env override is unset that is
34+
``None``, which routes through the walk-up branch that APPLIES the YAML
35+
``source_root`` field. Passing the discovered dir instead would route into
36+
the "explicit source root" branch and silently ignore the YAML field,
37+
diverging the MCP server from ``init``/``increment``/``reprocess``.
38+
"""
39+
40+
def test_returns_none_when_env_unset(self, tmp_path, monkeypatch):
41+
monkeypatch.delenv("JAVA_CODEBASE_RAG_SOURCE_ROOT", raising=False)
42+
from server import _source_root_for_operator_config
43+
44+
assert _source_root_for_operator_config() is None
45+
46+
def test_returns_env_path_when_set(self, tmp_path, monkeypatch):
47+
explicit = tmp_path / "explicit-root"
48+
explicit.mkdir()
49+
monkeypatch.setenv("JAVA_CODEBASE_RAG_SOURCE_ROOT", str(explicit))
50+
from server import _source_root_for_operator_config
51+
52+
assert _source_root_for_operator_config() == explicit.resolve()
53+
54+
def test_mcp_and_init_resolve_identically_for_nested_config(self, tmp_path, monkeypatch):
55+
"""Regression for the init-vs-MCP index_dir divergence.
56+
57+
Config lives in a subdirectory of the Java tree (``my-project-context/``)
58+
and points both ``source_root`` and ``index_dir`` one level up. The MCP
59+
server (env unset) and the CLI must resolve the SAME source_root and
60+
index_dir, landing on the real index at ``tmp_path/.java-codebase-rag``.
61+
"""
62+
monkeypatch.delenv("JAVA_CODEBASE_RAG_SOURCE_ROOT", raising=False)
63+
monkeypatch.delenv("JAVA_CODEBASE_RAG_INDEX_DIR", raising=False)
64+
65+
config_dir = tmp_path / "my-project-context"
66+
config_dir.mkdir()
67+
(config_dir / YAML_CONFIG_FILENAMES[0]).write_text(
68+
"source_root: ../\nindex_dir: ../.java-codebase-rag\n"
69+
)
70+
monkeypatch.chdir(config_dir)
71+
72+
from server import _source_root_for_operator_config
73+
74+
mcp = resolve_operator_config(source_root=_source_root_for_operator_config())
75+
cli = resolve_operator_config(source_root=None)
76+
77+
assert mcp.source_root == tmp_path
78+
assert mcp.index_dir == (tmp_path / ".java-codebase-rag").resolve()
79+
assert mcp.source_root == cli.source_root
80+
assert mcp.index_dir == cli.index_dir
81+

0 commit comments

Comments
 (0)