java-codebase-rag 0.12.0__py3-none-any.whl → 0.12.2__py3-none-any.whl

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 (77) hide show
  1. java_codebase_rag-0.12.2.dist-info/METADATA +35 -0
  2. java_codebase_rag-0.12.2.dist-info/RECORD +4 -0
  3. {java_codebase_rag-0.12.0.dist-info → java_codebase_rag-0.12.2.dist-info}/WHEEL +1 -1
  4. java_codebase_rag/_deprecation.py +0 -103
  5. java_codebase_rag/_fdlimit.py +0 -56
  6. java_codebase_rag/_stdio.py +0 -32
  7. java_codebase_rag/_version.py +0 -35
  8. java_codebase_rag/absence/__init__.py +0 -0
  9. java_codebase_rag/absence/absence_diagnosis.py +0 -700
  10. java_codebase_rag/absence/absence_types.py +0 -124
  11. java_codebase_rag/absence/absence_vocab.py +0 -460
  12. java_codebase_rag/analysis/__init__.py +0 -0
  13. java_codebase_rag/analysis/pr_analysis.py +0 -563
  14. java_codebase_rag/analysis/resolve_service.py +0 -740
  15. java_codebase_rag/ast/__init__.py +0 -0
  16. java_codebase_rag/ast/ast_java.py +0 -2847
  17. java_codebase_rag/ast/ast_kotlin.py +0 -1794
  18. java_codebase_rag/ast/brownfield_events.py +0 -58
  19. java_codebase_rag/ast/chunk_heuristics.py +0 -83
  20. java_codebase_rag/ast/language.py +0 -117
  21. java_codebase_rag/cli.py +0 -1215
  22. java_codebase_rag/cli_dispatch.py +0 -251
  23. java_codebase_rag/cli_format.py +0 -85
  24. java_codebase_rag/cli_progress.py +0 -94
  25. java_codebase_rag/config.py +0 -833
  26. java_codebase_rag/eval/__init__.py +0 -1
  27. java_codebase_rag/eval/ground_truth.py +0 -100
  28. java_codebase_rag/eval/metrics.py +0 -107
  29. java_codebase_rag/eval/runner.py +0 -556
  30. java_codebase_rag/graph/__init__.py +0 -0
  31. java_codebase_rag/graph/build_ast_graph.py +0 -4593
  32. java_codebase_rag/graph/graph_enrich.py +0 -1940
  33. java_codebase_rag/graph/graph_types.py +0 -224
  34. java_codebase_rag/graph/java_ontology.py +0 -465
  35. java_codebase_rag/graph/ladybug_queries.py +0 -2213
  36. java_codebase_rag/graph/path_filtering.py +0 -509
  37. java_codebase_rag/index/__init__.py +0 -0
  38. java_codebase_rag/index/java_index_flow_lancedb.py +0 -879
  39. java_codebase_rag/index/java_index_v1_common.py +0 -33
  40. java_codebase_rag/install_data/__init__.py +0 -0
  41. java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -110
  42. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
  43. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
  44. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
  45. java_codebase_rag/installer.py +0 -2188
  46. java_codebase_rag/jrag.py +0 -4545
  47. java_codebase_rag/jrag_envelope.py +0 -1107
  48. java_codebase_rag/jrag_hints.py +0 -204
  49. java_codebase_rag/jrag_render.py +0 -926
  50. java_codebase_rag/lance_optimize.py +0 -264
  51. java_codebase_rag/mcp/__init__.py +0 -0
  52. java_codebase_rag/mcp/mcp_hints.py +0 -932
  53. java_codebase_rag/mcp/mcp_v2.py +0 -1916
  54. java_codebase_rag/mcp/server.py +0 -886
  55. java_codebase_rag/pipeline.py +0 -531
  56. java_codebase_rag/progress.py +0 -570
  57. java_codebase_rag/read_payloads.py +0 -781
  58. java_codebase_rag/search/__init__.py +0 -0
  59. java_codebase_rag/search/index_common.py +0 -10
  60. java_codebase_rag/search/search_lancedb.py +0 -1296
  61. java_codebase_rag/search/search_lexical.py +0 -449
  62. java_codebase_rag/search/search_scoring.py +0 -537
  63. java_codebase_rag/watch/__init__.py +0 -0
  64. java_codebase_rag/watch/client.py +0 -230
  65. java_codebase_rag/watch/daemon.py +0 -396
  66. java_codebase_rag/watch/lock.py +0 -201
  67. java_codebase_rag/watch/paths.py +0 -76
  68. java_codebase_rag/watch/protocol.py +0 -122
  69. java_codebase_rag/watch/server.py +0 -273
  70. java_codebase_rag/watch/warm.py +0 -105
  71. java_codebase_rag/watch/watcher.py +0 -394
  72. java_codebase_rag-0.12.0.dist-info/METADATA +0 -340
  73. java_codebase_rag-0.12.0.dist-info/RECORD +0 -75
  74. java_codebase_rag-0.12.0.dist-info/entry_points.txt +0 -5
  75. java_codebase_rag-0.12.0.dist-info/licenses/LICENSE +0 -21
  76. java_codebase_rag-0.12.0.dist-info/top_level.txt +0 -1
  77. /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.2.dist-info/top_level.txt +0 -0
@@ -1,833 +0,0 @@
1
- """Unified operator config: index paths, embedding knobs, YAML (PR-CLI-2).
2
-
3
- Precedence for shared knobs: CLI > env > YAML > built-in default.
4
- Legacy env names and legacy YAML filenames are never read for behaviour;
5
- optional one-line stderr hints may fire when deprecated names are detected.
6
- """
7
- from __future__ import annotations
8
-
9
- import os
10
- import re
11
- import sys
12
- from dataclasses import dataclass
13
- from pathlib import Path
14
- from typing import Any, Literal
15
-
16
- SettingSource = Literal["cli", "env", "yaml", "default"]
17
-
18
- YAML_CONFIG_FILENAMES = (".java-codebase-rag.yml", ".java-codebase-rag.yaml")
19
- LEGACY_YAML_FILENAMES = (".lancedb-mcp.yml", ".lancedb-mcp.yaml")
20
-
21
- # Pointer file written into the index dir at index time so discovery can locate
22
- # a YAML that does not sit beside the index-dir anchor — e.g. a config living in
23
- # a sibling ``project-context/`` dir when the agent's cwd is inside a
24
- # microservice (a descendant of the index anchor, a sibling of the config).
25
- # Contains one line: the absolute path of the YAML used to build the index. A
26
- # direct YAML at the anchor always wins; the pointer only fires when the anchor
27
- # has no YAML beside it (see ``_effective_config_dir``).
28
- CONFIG_SOURCE_FILENAME = "config_source"
29
- # Operator-owned files inside the index dir that ``erase`` removes. Kept separate
30
- # from ``build_ast_graph.BUILDER_OWNED_INDEX_FILES`` (builder-owned artifacts).
31
- OPERATOR_OWNED_INDEX_FILES = (CONFIG_SOURCE_FILENAME,)
32
-
33
- ENV_INDEX_DIR = "JAVA_CODEBASE_RAG_INDEX_DIR"
34
- # Public operator contract is six names: INDEX_DIR, DEBUG_CONTEXT, RUN_HEAVY, SBERT_MODEL, SBERT_DEVICE, HINTS_ENABLED.
35
- # SOURCE_ROOT is still required for MCP / subprocess Java tree resolution (see mcp.json.example); it is not folded into the headline "5".
36
- ENV_SOURCE_ROOT = "JAVA_CODEBASE_RAG_SOURCE_ROOT"
37
- ENV_DEBUG_CONTEXT = "JAVA_CODEBASE_RAG_DEBUG_CONTEXT"
38
- ENV_RUN_HEAVY = "JAVA_CODEBASE_RAG_RUN_HEAVY"
39
-
40
- # CocoIndex inflight-component throttle. CocoIndex's default is 1024 inflight
41
- # components (cocoindex/_internal/app.py: ``_ENV_MAX_INFLIGHT_COMPONENTS``),
42
- # which spawns enough concurrent LanceDB merge-inserts to exhaust OS file
43
- # descriptors under default ulimits -> "Too many open files (os error 24)".
44
- # NOTE: this is the REAL env var. An earlier fix (#293) set the non-existent
45
- # ``COCOINDEX_SOURCE_MAX_INFLIGHT_ROWS`` — CocoIndex never reads it, so it was a
46
- # no-op and the EMFILE error recurred (#306).
47
- COCOINDEX_MAX_INFLIGHT_COMPONENTS_ENV = "COCOINDEX_MAX_INFLIGHT_COMPONENTS"
48
- COCOINDEX_DEFAULT_MAX_INFLIGHT_COMPONENTS = "256"
49
-
50
- # Lance native DataFusion hash-join memory pool ceiling (FairSpillPool). The
51
- # lance default is ~100 MiB, tuned for query workloads — too small for the
52
- # single big ``merge_insert`` cocoindex emits at the end of a flow component.
53
- # On ``--full-reprocess`` (all rows match the existing table → bulk-update
54
- # path) the hash join builds on a large side and exhausts the pool somewhere
55
- # around 75k-100k chunks: "Resources exhausted: Failed to allocate ... for
56
- # HashJoinInput ... N MiB remain available for the total pool". cocoindex is a
57
- # bare pass-through to lancedb (it never sets a Session/memory_limit), so it
58
- # inherits this default — we raise it here. FairSpillPool is a *reservation
59
- # ceiling*, not a pre-allocation: setting 1 GiB does not reserve 1 GiB upfront,
60
- # it just allows the join to grow before spilling/erroring, so it is safe on
61
- # memory-constrained hosts. An operator can still override via their own
62
- # ``LANCE_MEM_POOL_SIZE`` (subprocess_env copies os.environ, and apply is via
63
- # ``setdefault`` so the operator value wins). Increment is unaffected (tiny
64
- # batch → tiny hash table); only the full-reprocess write path is at risk.
65
- LANCE_MEM_POOL_SIZE_ENV = "LANCE_MEM_POOL_SIZE"
66
- LANCE_DEFAULT_MEM_POOL_SIZE = "1073741824" # 1 GiB
67
-
68
-
69
- def cocoindex_subprocess_env_defaults() -> dict[str, str]:
70
- """Env defaults applied to every CocoIndex subprocess.
71
-
72
- Bounds CocoIndex concurrency (``COCOINDEX_MAX_INFLIGHT_COMPONENTS``; see
73
- :issue:`306`) and raises the Lance hash-join memory ceiling
74
- (``LANCE_MEM_POOL_SIZE``) so a large full-reprocess does not exhaust the
75
- default ~100 MiB pool mid-``merge_insert``.
76
-
77
- Apply with ``env.setdefault(...)`` so a caller-provided (operator) value
78
- always wins.
79
- """
80
- return {
81
- COCOINDEX_MAX_INFLIGHT_COMPONENTS_ENV: COCOINDEX_DEFAULT_MAX_INFLIGHT_COMPONENTS,
82
- LANCE_MEM_POOL_SIZE_ENV: LANCE_DEFAULT_MEM_POOL_SIZE,
83
- }
84
-
85
- _DEFAULT_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
86
-
87
- # Matches either $VAR or ${VAR} (POSIX shell variable syntax).
88
- _UNRESOLVED_VAR_RE = re.compile(r"\$(\w+|\{[^}]+\})")
89
-
90
-
91
- def maybe_expand_embedding_model_path(
92
- value: str,
93
- *,
94
- config_dir: Path | None = None,
95
- source_root: Path | None = None,
96
- source: SettingSource | None = None,
97
- ) -> str:
98
- """Expand ``~`` / ``$VAR`` for path-shaped values and resolve relatives to absolute.
99
-
100
- Path-shape: starts with ``/``, ``./``, ``../``, ``~``, or contains ``$``.
101
- Plain ``org/name`` (hub id) does not match and is passed through unchanged.
102
-
103
- Relative resolution mirrors :func:`_resolve_index_dir_path` so a committed
104
- config is portable regardless of process CWD:
105
-
106
- * YAML values (``source == "yaml"``) resolve against ``config_dir`` (the
107
- directory holding ``.java-codebase-rag.yml``).
108
- * CLI / env values resolve against ``source_root``.
109
-
110
- Only a result that still starts with ``./`` or ``../`` *after* ``~`` /
111
- ``$VAR`` expansion is re-based — so hub ids (``org/name``), absolute paths,
112
- ``~/``-expanded paths, and an env var that already yielded an absolute path
113
- are all left untouched.
114
-
115
- When no base is supplied (the runtime ``SBERT_MODEL`` read via
116
- :func:`resolved_sbert_model_for_process_env`), relative resolution is
117
- skipped: the value is returned ``expandvars`` / ``expanduser``-expanded but
118
- not re-based, matching the prior best-effort behavior. The main resolution
119
- path (:func:`resolve_operator_config`) supplies a base, so the absolute path
120
- it stores is what downstream loaders receive.
121
- """
122
- needs_expand = value.startswith(("/", "./", "../", "~")) or "$" in value
123
- if not needs_expand:
124
- return value
125
- expanded = os.path.expandvars(os.path.expanduser(value))
126
- if _UNRESOLVED_VAR_RE.search(expanded):
127
- print(
128
- f"jrag: path-shaped model string contains unresolved variable: {expanded}",
129
- file=sys.stderr,
130
- )
131
- if expanded.startswith(("./", "../")):
132
- base = _embedding_model_base(
133
- source=source, config_dir=config_dir, source_root=source_root
134
- )
135
- if base is not None:
136
- return str((base / expanded).resolve())
137
- return expanded
138
-
139
-
140
- def _embedding_model_base(
141
- *,
142
- source: SettingSource | None,
143
- config_dir: Path | None,
144
- source_root: Path | None,
145
- ) -> Path | None:
146
- """Base directory for a relative ``embedding.model``.
147
-
148
- Mirrors :func:`_resolve_index_dir_path`: YAML values anchor on the config
149
- file's directory; CLI / env values anchor on the resolved ``source_root``.
150
- """
151
- if source == "yaml":
152
- return config_dir
153
- return source_root
154
-
155
-
156
- def resolved_sbert_model_for_process_env(import_time_default: str) -> str:
157
- """``SBERT_MODEL`` from the process environment, with the same expansion as YAML/CLI resolution.
158
-
159
- *import_time_default* is typically ``index_common.SBERT_MODEL`` (expanded at import
160
- when ``SBERT_MODEL`` was unset); when the env var is set or non-empty, that value wins
161
- and is normalized with :func:`maybe_expand_embedding_model_path`.
162
- """
163
- raw = os.environ.get("SBERT_MODEL")
164
- picked = import_time_default if (raw is None or not str(raw).strip()) else str(raw).strip()
165
- return maybe_expand_embedding_model_path(picked)
166
-
167
-
168
- # Legacy env keys: never honored; detection-only hints name the replacement (if any).
169
- _LEGACY_ENV_HINTS: tuple[tuple[str, str], ...] = (
170
- ("LANCEDB_URI", "JAVA_CODEBASE_RAG_INDEX_DIR"),
171
- ("LANCEDB_MCP_PROJECT_ROOT", "cwd or --source-root (no env replacement)"),
172
- ("LANCEDB_MCP_ALLOW_REFRESH", "(removed; use init / increment / reprocess / erase)"),
173
- ("LANCEDB_MCP_GRAPH_ENABLED", "(removed; graph is used when code_graph.lbug exists)"),
174
- ("LANCEDB_MCP_MICROSERVICE_ROOTS", "microservice_roots: in .java-codebase-rag.yml"),
175
- ("LANCEDB_MCP_DEBUG_CONTEXT", ENV_DEBUG_CONTEXT),
176
- ("LANCEDB_MCP_RUN_HEAVY", ENV_RUN_HEAVY),
177
- ("COCOINDEX_DB", "defaults to <JAVA_CODEBASE_RAG_INDEX_DIR>/cocoindex.db"),
178
- )
179
-
180
- _legacy_hint_seen: set[str] = set()
181
- _legacy_yaml_hint_roots: set[str] = set()
182
-
183
-
184
- def emit_legacy_env_hints_if_present() -> None:
185
- """One-line stderr hints when deprecated env vars are set (values are not read)."""
186
- for old, replacement in _LEGACY_ENV_HINTS:
187
- if old not in os.environ:
188
- continue
189
- key = f"env:{old}"
190
- if key in _legacy_hint_seen:
191
- continue
192
- _legacy_hint_seen.add(key)
193
- print(
194
- f"jrag: {old} is set but no longer read; use {replacement}.",
195
- file=sys.stderr,
196
- )
197
-
198
-
199
- def emit_legacy_yaml_hint_if_needed(source_root: Path) -> None:
200
- """If legacy YAML exists without a new config file, print a one-line stderr hint once per root."""
201
- root_s = str(source_root.resolve())
202
- if root_s in _legacy_yaml_hint_roots:
203
- return
204
- has_new = any((source_root / n).is_file() for n in YAML_CONFIG_FILENAMES)
205
- if has_new:
206
- return
207
- for name in LEGACY_YAML_FILENAMES:
208
- if (source_root / name).is_file():
209
- _legacy_yaml_hint_roots.add(root_s)
210
- print(
211
- "jrag: found legacy "
212
- f"{name}; rename to .java-codebase-rag.yml to re-enable config.",
213
- file=sys.stderr,
214
- )
215
- return
216
-
217
-
218
- def find_yaml_config_file(source_root: Path) -> Path | None:
219
- for name in YAML_CONFIG_FILENAMES:
220
- p = source_root / name
221
- if p.is_file():
222
- return p
223
- return None
224
-
225
-
226
- def _has_index_dir(directory: Path) -> bool:
227
- """True if *directory* contains a non-empty ``.java-codebase-rag/`` index directory."""
228
- idx = directory / ".java-codebase-rag"
229
- return idx.is_dir() and any(idx.iterdir())
230
-
231
-
232
- def discover_project_root(start: Path) -> Path | None:
233
- """Walk up from start to find the directory containing a config file or index.
234
-
235
- Looks for ``.java-codebase-rag.yml`` / ``.java-codebase-rag.yaml`` (preferred)
236
- or the ``.java-codebase-rag/`` index directory as a project boundary marker.
237
-
238
- First match wins (closest to start). Config file takes priority over index
239
- directory at the same level. Stops at $HOME inclusive — checks $HOME itself
240
- but does not walk past it. Returns None if no marker found.
241
-
242
- A bare ``.java-codebase-rag/`` index directory at ``$HOME`` is intentionally
243
- NOT treated as an anchor (issue #357): a stray home-level index (e.g. an
244
- accidental ``init`` run from home) would otherwise hijack resolution for any
245
- command run from a ``$HOME`` subdir without its own marker, silently reading
246
- and writing the home-level index. A config file at ``$HOME`` still anchors.
247
- """
248
- start = start.resolve()
249
- home = Path.home().resolve()
250
-
251
- current = start
252
- while True:
253
- # Config file is the primary anchor (valid at every level, including $HOME).
254
- if find_yaml_config_file(current) is not None:
255
- return current
256
- # Index directory is the secondary anchor (supports indexes without config),
257
- # but NOT at $HOME — see the docstring for the cross-project hijack rationale.
258
- if current != home and _has_index_dir(current):
259
- return current
260
-
261
- # Stop if we've reached home (config-file check above already handled home)
262
- if current == home:
263
- return None
264
-
265
- # Stop if we've reached filesystem root
266
- parent = current.parent
267
- if parent == current:
268
- return None
269
-
270
- current = parent
271
-
272
-
273
- _stale_pointer_seen: set[str] = set()
274
-
275
-
276
- def _config_dir_from_pointer(anchor: Path) -> Path | None:
277
- """Return the YAML config dir recorded in the index dir's ``config_source`` pointer.
278
-
279
- Reads ``<anchor>/.java-codebase-rag/config_source`` (one absolute path). If it
280
- names an existing ``.java-codebase-rag.yml`` / ``.yaml``, returns that file's
281
- parent directory; otherwise (missing/blank/stale) returns ``None``. Used only
282
- when the anchor has no direct YAML — see :func:`_effective_config_dir`.
283
- """
284
- pointer = anchor / ".java-codebase-rag" / CONFIG_SOURCE_FILENAME
285
- if not pointer.is_file():
286
- return None
287
- try:
288
- raw = pointer.read_text(encoding="utf-8").strip()
289
- except OSError:
290
- return None
291
- if not raw:
292
- return None
293
- target = Path(raw).expanduser()
294
- if not target.is_absolute():
295
- # Relative to the anchor (the index-dir parent), not the pointer file.
296
- target = (anchor / target).resolve()
297
- if not target.is_file() or target.name not in YAML_CONFIG_FILENAMES:
298
- key = str(pointer.resolve())
299
- if key not in _stale_pointer_seen:
300
- _stale_pointer_seen.add(key)
301
- print(
302
- "jrag: ignoring stale index pointer "
303
- f"{pointer} -> {raw} (target missing or not a config file).",
304
- file=sys.stderr,
305
- )
306
- return None
307
- return target.parent
308
-
309
-
310
- def _effective_config_dir(config_dir: Path) -> Path:
311
- """Resolve the directory YAML config fields are relative to.
312
-
313
- A direct ``.java-codebase-rag.yml`` / ``.yaml`` in ``config_dir`` always wins.
314
- Otherwise, if ``config_dir`` hosts the ``.java-codebase-rag/`` index dir and
315
- that index remembers its config via a ``config_source`` pointer, follow it to
316
- the YAML's directory. This lets a config in a sibling dir (e.g.
317
- ``project-context/`` beside the Java tree) be found when discovery anchors on
318
- the index dir from inside a microservice — without an env var or flag, and
319
- with YAML-relative fields (``index_dir``, ``source_root``, ``embedding.model``)
320
- resolving against the YAML's home rather than the index anchor. Falls back to
321
- ``config_dir`` unchanged when neither applies.
322
- """
323
- if find_yaml_config_file(config_dir) is not None:
324
- return config_dir
325
- return _config_dir_from_pointer(config_dir) or config_dir
326
-
327
-
328
- def load_yaml_mapping(source_root: Path) -> dict[str, Any]:
329
- path = find_yaml_config_file(source_root)
330
- if path is None:
331
- return {}
332
- try:
333
- import yaml
334
- except ImportError:
335
- return {}
336
- try:
337
- data = yaml.safe_load(path.read_text(encoding="utf-8"))
338
- except (yaml.YAMLError, OSError, UnicodeDecodeError) as exc:
339
- # Best-effort loader: a missing/unreadable/malformed config must NOT abort
340
- # startup — return {} and proceed with defaults. Narrowing this to
341
- # ``yaml.YAMLError`` alone let OSError (chmod 000, stat/read TOCTOU) and
342
- # UnicodeDecodeError (non-UTF-8 config) propagate to the caller; the broader
343
- # tuple restores the graceful-degradation contract while still surfacing the
344
- # problem on stderr.
345
- print(
346
- f"jrag: could not load config {path}: {exc}; ignoring config.",
347
- file=sys.stderr,
348
- )
349
- return {}
350
- return data if isinstance(data, dict) else {}
351
-
352
-
353
- @dataclass(frozen=True)
354
- class ResolvedOperatorConfig:
355
- source_root: Path
356
- index_dir: Path
357
- ladybug_path: Path
358
- cocoindex_db: Path
359
- embedding_model: str
360
- embedding_device: str | None
361
- hints_enabled: bool
362
- index_dir_source: SettingSource
363
- embedding_model_source: SettingSource
364
- embedding_device_source: SettingSource
365
- hints_enabled_source: SettingSource
366
- # Absence diagnosis config knobs (PR-ABS-0)
367
- absence_close_threshold: float
368
- absence_absent_floor: float
369
- absence_candidate_count: int
370
- absence_ngram_q: int
371
- absence_diag_enabled: bool
372
- absence_close_threshold_source: SettingSource
373
- absence_absent_floor_source: SettingSource
374
- absence_candidate_count_source: SettingSource
375
- absence_ngram_q_source: SettingSource
376
- absence_diag_enabled_source: SettingSource
377
- # Absolute path of the YAML actually loaded (None when built-in defaults were
378
- # used with no config file). Recorded into the index dir at index time so a
379
- # later discovery run from a sibling/cwd can relocate this config.
380
- yaml_config_path: Path | None = None
381
- # ``watch:`` block knobs (jrag watch / watcher). Defaults make the block
382
- # optional; no env vars are introduced for these (CLI flag > YAML > default).
383
- watch_debounce_ms: int = 1500
384
- watch_backend: str = "auto"
385
- watch_poll_interval_ms: int = 2000
386
- watch_debounce_ms_source: SettingSource = "default"
387
- watch_backend_source: SettingSource = "default"
388
- watch_poll_interval_ms_source: SettingSource = "default"
389
-
390
- def apply_to_os_environ(self) -> None:
391
- """Make downstream modules (server, ladybug_queries, flows) see a consistent environment.
392
-
393
- When ``embedding_device`` is unset, ``SBERT_DEVICE`` is not removed from ``os.environ`` so
394
- a long-lived host process is not mutated for unrelated callers; subprocesses still use
395
- :meth:`subprocess_env`, which omits ``SBERT_DEVICE`` unless explicitly resolved.
396
- """
397
- os.environ[ENV_INDEX_DIR] = str(self.index_dir.resolve())
398
- os.environ[ENV_SOURCE_ROOT] = str(self.source_root.resolve())
399
- os.environ["SBERT_MODEL"] = self.embedding_model
400
- if self.embedding_device is not None:
401
- os.environ["SBERT_DEVICE"] = self.embedding_device
402
- # Publish absence diagnosis knobs for subprocess builds (PR-ABS-1)
403
- os.environ["JAVA_CODEBASE_RAG_ABSENCE_NGRAM_Q"] = str(self.absence_ngram_q)
404
-
405
- def subprocess_env(self, base: dict[str, str] | None = None) -> dict[str, str]:
406
- out = dict(base or os.environ)
407
- out[ENV_INDEX_DIR] = str(self.index_dir.resolve())
408
- out[ENV_SOURCE_ROOT] = str(self.source_root.resolve())
409
- out["SBERT_MODEL"] = self.embedding_model
410
- if self.embedding_device is not None:
411
- out["SBERT_DEVICE"] = self.embedding_device
412
- else:
413
- out.pop("SBERT_DEVICE", None)
414
- # Publish absence diagnosis knobs for subprocess builds (PR-ABS-1)
415
- out["JAVA_CODEBASE_RAG_ABSENCE_NGRAM_Q"] = str(self.absence_ngram_q)
416
- return out
417
-
418
-
419
- def _pick_str(
420
- *,
421
- cli_val: str | None,
422
- env_key: str,
423
- yaml_dict: dict[str, Any],
424
- yaml_path: tuple[str, ...],
425
- default: str,
426
- ) -> tuple[str, SettingSource]:
427
- if cli_val is not None and str(cli_val).strip() != "":
428
- return str(cli_val).strip(), "cli"
429
- env_raw = os.environ.get(env_key, "").strip()
430
- if env_raw:
431
- return env_raw, "env"
432
- cur: Any = yaml_dict
433
- for part in yaml_path:
434
- if not isinstance(cur, dict) or part not in cur:
435
- cur = None
436
- break
437
- cur = cur.get(part)
438
- if isinstance(cur, str) and cur.strip():
439
- return cur.strip(), "yaml"
440
- return default, "default"
441
-
442
-
443
- def _pick_optional_device(
444
- *,
445
- cli_val: str | None,
446
- env_key: str,
447
- yaml_dict: dict[str, Any],
448
- ) -> tuple[str | None, SettingSource]:
449
- if cli_val is not None and str(cli_val).strip() != "":
450
- return str(cli_val).strip(), "cli"
451
- env_raw = os.environ.get(env_key, "").strip()
452
- if env_raw:
453
- return env_raw, "env"
454
- emb = yaml_dict.get("embedding")
455
- if isinstance(emb, dict):
456
- d = emb.get("device")
457
- if isinstance(d, str) and d.strip():
458
- return d.strip(), "yaml"
459
- return None, "default"
460
-
461
-
462
- def _pick_bool(
463
- *,
464
- env_key: str,
465
- yaml_dict: dict[str, Any],
466
- yaml_path: tuple[str, ...],
467
- default: bool,
468
- ) -> tuple[bool, SettingSource]:
469
- env_raw = os.environ.get(env_key, "").strip().lower()
470
- if env_raw in ("1", "true", "yes"):
471
- return True, "env"
472
- if env_raw in ("0", "false", "no"):
473
- return False, "env"
474
- cur: Any = yaml_dict
475
- for part in yaml_path:
476
- if not isinstance(cur, dict) or part not in cur:
477
- cur = None
478
- break
479
- cur = cur.get(part)
480
- if isinstance(cur, bool):
481
- return cur, "yaml"
482
- return default, "default"
483
-
484
-
485
- def _pick_float(
486
- *,
487
- env_key: str,
488
- yaml_dict: dict[str, Any],
489
- yaml_path: tuple[str, ...],
490
- default: float,
491
- ) -> tuple[float, SettingSource]:
492
- """Pick a float setting from env (parsed via float(...)), YAML, or default.
493
-
494
- Precedence: CLI > env > YAML > default. Env values that fail to parse as float
495
- fall back to the default (matching the brief's requirement for graceful degradation).
496
- """
497
- env_raw = os.environ.get(env_key, "").strip()
498
- if env_raw:
499
- try:
500
- return float(env_raw), "env"
501
- except ValueError:
502
- # Invalid env value falls back to default (per brief)
503
- pass
504
- cur: Any = yaml_dict
505
- for part in yaml_path:
506
- if not isinstance(cur, dict) or part not in cur:
507
- cur = None
508
- break
509
- cur = cur.get(part)
510
- if isinstance(cur, (int, float)):
511
- return float(cur), "yaml"
512
- return default, "default"
513
-
514
-
515
- def _pick_int(
516
- *,
517
- cli_val: int | None = None,
518
- env_key: str,
519
- yaml_dict: dict[str, Any],
520
- yaml_path: tuple[str, ...],
521
- default: int,
522
- ) -> tuple[int, SettingSource]:
523
- """Pick an int setting from CLI, env (parsed via int(...)), YAML, or default.
524
-
525
- Precedence: CLI > env > YAML > default. Env values that fail to parse as int
526
- fall back to the default (matching the brief's requirement for graceful degradation).
527
- ``cli_val`` defaults to ``None`` so existing callers are unaffected.
528
- """
529
- if cli_val is not None:
530
- return int(cli_val), "cli"
531
- env_raw = os.environ.get(env_key, "").strip()
532
- if env_raw:
533
- try:
534
- return int(env_raw), "env"
535
- except ValueError:
536
- # Invalid env value falls back to default (per brief)
537
- pass
538
- cur: Any = yaml_dict
539
- for part in yaml_path:
540
- if not isinstance(cur, dict) or part not in cur:
541
- cur = None
542
- break
543
- cur = cur.get(part)
544
- if isinstance(cur, int):
545
- return cur, "yaml"
546
- return default, "default"
547
-
548
-
549
- def _resolve_index_dir_path(
550
- *,
551
- source_root: Path,
552
- config_dir: Path,
553
- cli_index_dir: str | None,
554
- yaml_dict: dict[str, Any],
555
- ) -> tuple[Path, SettingSource]:
556
- # Bases for relative paths:
557
- # - YAML ``index_dir`` -> the config file's directory (``config_dir``),
558
- # the SAME base used for YAML ``source_root``. Paths written in the
559
- # config file are relative to the file, so both keys stay consistent.
560
- # - CLI / env ``index_dir`` -> ``source_root`` (unchanged). These are not
561
- # "in the config file"; preserving the existing base avoids a semantics
562
- # change for operators who pass ``--index-dir`` on the command line.
563
- # - Default ``./.java-codebase-rag`` -> ``source_root`` so the index sits
564
- # beside the Java tree (the layout ``discover_project_root`` anchors on).
565
- raw_cli = cli_index_dir.strip() if isinstance(cli_index_dir, str) else None
566
- if raw_cli:
567
- p = Path(raw_cli).expanduser()
568
- out = p.resolve() if p.is_absolute() else (source_root / p).resolve()
569
- return out, "cli"
570
-
571
- env_raw = os.environ.get(ENV_INDEX_DIR, "").strip()
572
- if env_raw:
573
- p = Path(env_raw).expanduser()
574
- out = p.resolve() if p.is_absolute() else (source_root / p).resolve()
575
- return out, "env"
576
-
577
- idx = yaml_dict.get("index_dir")
578
- if isinstance(idx, str) and idx.strip():
579
- p = Path(idx.strip()).expanduser()
580
- out = p.resolve() if p.is_absolute() else (config_dir / p).resolve()
581
- return out, "yaml"
582
-
583
- return (source_root / ".java-codebase-rag").resolve(), "default"
584
-
585
-
586
- def resolve_operator_config(
587
- *,
588
- source_root: Path | None,
589
- cli_index_dir: str | None = None,
590
- cli_embedding_model: str | None = None,
591
- cli_embedding_device: str | None = None,
592
- cli_watch_debounce_ms: int | None = None,
593
- cli_watch_backend: str | None = None,
594
- cli_watch_poll_interval_ms: int | None = None,
595
- ) -> ResolvedOperatorConfig:
596
- # Phase 1: Find the config file directory
597
- if source_root is not None:
598
- # CLI flag provided: use it as both config_dir and effective source_root
599
- # (skip YAML source_root check - CLI wins). ``_effective_config_dir`` may
600
- # rebase config_dir to a YAML reached via the index-dir pointer; root is
601
- # untouched (explicit source_root wins).
602
- root = source_root.expanduser().resolve()
603
- config_dir = _effective_config_dir(root)
604
- yaml_dict = load_yaml_mapping(config_dir)
605
- else:
606
- # Check env var first
607
- env_raw = os.environ.get(ENV_SOURCE_ROOT, "").strip()
608
- if env_raw:
609
- root = Path(env_raw).expanduser().resolve()
610
- config_dir = _effective_config_dir(root)
611
- yaml_dict = load_yaml_mapping(config_dir)
612
- else:
613
- # Walk up to find config dir
614
- discovered = discover_project_root(Path.cwd())
615
- config_dir = discovered if discovered is not None else Path.cwd().resolve()
616
- # Follow an index-dir pointer to the real config dir when the anchor
617
- # has no YAML beside it (e.g. config in a sibling dir).
618
- config_dir = _effective_config_dir(config_dir)
619
- # Load YAML from config dir
620
- yaml_dict = load_yaml_mapping(config_dir)
621
-
622
- # Phase 2: Resolve effective source root
623
- # Check for YAML source_root field (resolved relative to config dir)
624
- yaml_source_root = yaml_dict.get("source_root")
625
- if isinstance(yaml_source_root, str) and yaml_source_root.strip():
626
- yroot = Path(yaml_source_root.strip()).expanduser()
627
- root = yroot.resolve() if yroot.is_absolute() else (config_dir / yroot).resolve()
628
- else:
629
- root = config_dir
630
-
631
- index_dir, index_src = _resolve_index_dir_path(
632
- source_root=root, config_dir=config_dir, cli_index_dir=cli_index_dir, yaml_dict=yaml_dict
633
- )
634
- model, model_src = _pick_str(
635
- cli_val=cli_embedding_model,
636
- env_key="SBERT_MODEL",
637
- yaml_dict=yaml_dict,
638
- yaml_path=("embedding", "model"),
639
- default=_DEFAULT_EMBEDDING_MODEL,
640
- )
641
- model = maybe_expand_embedding_model_path(
642
- model,
643
- config_dir=config_dir,
644
- source_root=root,
645
- source=model_src,
646
- )
647
- device, device_src = _pick_optional_device(
648
- cli_val=cli_embedding_device,
649
- env_key="SBERT_DEVICE",
650
- yaml_dict=yaml_dict,
651
- )
652
- hints, hints_src = _pick_bool(
653
- env_key="JAVA_CODEBASE_RAG_HINTS_ENABLED",
654
- yaml_dict=yaml_dict,
655
- yaml_path=("hints", "enabled"),
656
- default=True,
657
- )
658
- # Absence diagnosis config (PR-ABS-0)
659
- abs_close, abs_close_src = _pick_float(
660
- env_key="JAVA_CODEBASE_RAG_ABSENCE_CLOSE_THRESHOLD",
661
- yaml_dict=yaml_dict,
662
- yaml_path=("absence", "close_threshold"),
663
- default=0.85,
664
- )
665
- abs_floor, abs_floor_src = _pick_float(
666
- env_key="JAVA_CODEBASE_RAG_ABSENCE_ABSENT_FLOOR",
667
- yaml_dict=yaml_dict,
668
- yaml_path=("absence", "absent_floor"),
669
- default=0.40,
670
- )
671
- abs_cand, abs_cand_src = _pick_int(
672
- env_key="JAVA_CODEBASE_RAG_ABSENCE_CANDIDATE_COUNT",
673
- yaml_dict=yaml_dict,
674
- yaml_path=("absence", "candidate_count"),
675
- default=5,
676
- )
677
- abs_q, abs_q_src = _pick_int(
678
- env_key="JAVA_CODEBASE_RAG_ABSENCE_NGRAM_Q",
679
- yaml_dict=yaml_dict,
680
- yaml_path=("absence", "ngram_q"),
681
- default=3,
682
- )
683
- abs_diag, abs_diag_src = _pick_bool(
684
- env_key="JAVA_CODEBASE_RAG_ABSENCE_DIAG_ENABLED",
685
- yaml_dict=yaml_dict,
686
- yaml_path=("absence", "diag_enabled"),
687
- default=True,
688
- )
689
- # ``watch:`` block knobs. No env vars are introduced for watch (CLI > YAML >
690
- # default), so an empty env_key is passed to the ``_pick_*`` helpers —
691
- # ``os.environ.get("", "")`` never matches, leaving the env tier inert.
692
- w_debounce, w_debounce_src = _pick_int(
693
- cli_val=cli_watch_debounce_ms,
694
- env_key="",
695
- yaml_dict=yaml_dict,
696
- yaml_path=("watch", "debounce_ms"),
697
- default=1500,
698
- )
699
- w_backend, w_backend_src = _pick_str(
700
- cli_val=cli_watch_backend,
701
- env_key="",
702
- yaml_dict=yaml_dict,
703
- yaml_path=("watch", "backend"),
704
- default="auto",
705
- )
706
- w_poll, w_poll_src = _pick_int(
707
- cli_val=cli_watch_poll_interval_ms,
708
- env_key="",
709
- yaml_dict=yaml_dict,
710
- yaml_path=("watch", "poll_interval_ms"),
711
- default=2000,
712
- )
713
- # Inline floors/validation (mirror the existing graceful-degradation style).
714
- if w_debounce < 100:
715
- print(
716
- f"jrag: watch.debounce_ms={w_debounce} is below the 100 ms "
717
- "floor; falling back to 1500.",
718
- file=sys.stderr,
719
- )
720
- w_debounce, w_debounce_src = 1500, "default"
721
- if w_backend not in ("auto", "watchdog", "polling"):
722
- print(
723
- f"jrag: watch.backend={w_backend!r} is not one of "
724
- "auto/watchdog/polling; falling back to 'auto'.",
725
- file=sys.stderr,
726
- )
727
- w_backend, w_backend_src = "auto", "default"
728
- if w_poll < 200:
729
- print(
730
- f"jrag: watch.poll_interval_ms={w_poll} is below the 200 ms "
731
- "floor; falling back to 2000.",
732
- file=sys.stderr,
733
- )
734
- w_poll, w_poll_src = 2000, "default"
735
- ku = index_dir / "code_graph.lbug"
736
- coco = index_dir / "cocoindex.db"
737
- return ResolvedOperatorConfig(
738
- source_root=root,
739
- index_dir=index_dir,
740
- ladybug_path=ku,
741
- cocoindex_db=coco,
742
- embedding_model=model,
743
- embedding_device=device,
744
- hints_enabled=hints,
745
- index_dir_source=index_src,
746
- embedding_model_source=model_src,
747
- embedding_device_source=device_src,
748
- hints_enabled_source=hints_src,
749
- absence_close_threshold=abs_close,
750
- absence_absent_floor=abs_floor,
751
- absence_candidate_count=abs_cand,
752
- absence_ngram_q=abs_q,
753
- absence_diag_enabled=abs_diag,
754
- absence_close_threshold_source=abs_close_src,
755
- absence_absent_floor_source=abs_floor_src,
756
- absence_candidate_count_source=abs_cand_src,
757
- absence_ngram_q_source=abs_q_src,
758
- absence_diag_enabled_source=abs_diag_src,
759
- yaml_config_path=find_yaml_config_file(config_dir),
760
- watch_debounce_ms=w_debounce,
761
- watch_backend=w_backend,
762
- watch_poll_interval_ms=w_poll,
763
- watch_debounce_ms_source=w_debounce_src,
764
- watch_backend_source=w_backend_src,
765
- watch_poll_interval_ms_source=w_poll_src,
766
- )
767
-
768
-
769
- def write_config_source_pointer(
770
- *, index_dir: Path, yaml_config_path: Path | None
771
- ) -> None:
772
- """Record the YAML config path inside the index dir (best-effort).
773
-
774
- Writes ``<index_dir>/config_source`` with the YAML's absolute path so a later
775
- discovery run that anchors on the index dir (but has no YAML beside it) can
776
- relocate the config via :func:`_effective_config_dir`. No-op when
777
- ``yaml_config_path`` is None (pure-default build — nothing to remember). Never
778
- raises: the pointer is an optimization, not a correctness requirement — a
779
- missing/unreadable pointer just falls back to built-in defaults.
780
- """
781
- if yaml_config_path is None:
782
- return
783
- try:
784
- index_dir.mkdir(parents=True, exist_ok=True)
785
- content = str(yaml_config_path.resolve()) + "\n"
786
- target = index_dir / CONFIG_SOURCE_FILENAME
787
- tmp = index_dir / (CONFIG_SOURCE_FILENAME + ".tmp")
788
- tmp.write_text(content, encoding="utf-8")
789
- os.replace(tmp, target)
790
- except OSError:
791
- pass
792
-
793
-
794
- def index_dir_has_existing_artifacts(index_dir: Path) -> tuple[bool, list[str]]:
795
- """True if graph dir or any Lance table already exists under index_dir."""
796
- paths: list[str] = []
797
- ku = index_dir / "code_graph.lbug"
798
- if ku.exists():
799
- paths.append(str(ku.resolve()))
800
- if index_dir.is_dir():
801
- try:
802
- import lancedb
803
-
804
- db = lancedb.connect(str(index_dir.resolve()))
805
- for name in db.list_tables():
806
- paths.append(str((index_dir / name).resolve()) + " (Lance table)")
807
- except Exception:
808
- pass
809
- return bool(paths), paths
810
-
811
-
812
- def describe_path_sizes(paths: list[Path]) -> list[tuple[Path, int]]:
813
- """Return (path, bytes) for files/dirs that exist."""
814
- out: list[tuple[Path, int]] = []
815
-
816
- def _sz(p: Path) -> int:
817
- if p.is_file():
818
- return p.stat().st_size
819
- if p.is_dir():
820
- total = 0
821
- for sub in p.rglob("*"):
822
- if sub.is_file():
823
- try:
824
- total += sub.stat().st_size
825
- except OSError:
826
- pass
827
- return total
828
- return 0
829
-
830
- for p in paths:
831
- if p.exists():
832
- out.append((p, _sz(p)))
833
- return out