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.
- java_codebase_rag-0.12.2.dist-info/METADATA +35 -0
- java_codebase_rag-0.12.2.dist-info/RECORD +4 -0
- {java_codebase_rag-0.12.0.dist-info → java_codebase_rag-0.12.2.dist-info}/WHEEL +1 -1
- java_codebase_rag/_deprecation.py +0 -103
- java_codebase_rag/_fdlimit.py +0 -56
- java_codebase_rag/_stdio.py +0 -32
- java_codebase_rag/_version.py +0 -35
- java_codebase_rag/absence/__init__.py +0 -0
- java_codebase_rag/absence/absence_diagnosis.py +0 -700
- java_codebase_rag/absence/absence_types.py +0 -124
- java_codebase_rag/absence/absence_vocab.py +0 -460
- java_codebase_rag/analysis/__init__.py +0 -0
- java_codebase_rag/analysis/pr_analysis.py +0 -563
- java_codebase_rag/analysis/resolve_service.py +0 -740
- java_codebase_rag/ast/__init__.py +0 -0
- java_codebase_rag/ast/ast_java.py +0 -2847
- java_codebase_rag/ast/ast_kotlin.py +0 -1794
- java_codebase_rag/ast/brownfield_events.py +0 -58
- java_codebase_rag/ast/chunk_heuristics.py +0 -83
- java_codebase_rag/ast/language.py +0 -117
- java_codebase_rag/cli.py +0 -1215
- java_codebase_rag/cli_dispatch.py +0 -251
- java_codebase_rag/cli_format.py +0 -85
- java_codebase_rag/cli_progress.py +0 -94
- java_codebase_rag/config.py +0 -833
- java_codebase_rag/eval/__init__.py +0 -1
- java_codebase_rag/eval/ground_truth.py +0 -100
- java_codebase_rag/eval/metrics.py +0 -107
- java_codebase_rag/eval/runner.py +0 -556
- java_codebase_rag/graph/__init__.py +0 -0
- java_codebase_rag/graph/build_ast_graph.py +0 -4593
- java_codebase_rag/graph/graph_enrich.py +0 -1940
- java_codebase_rag/graph/graph_types.py +0 -224
- java_codebase_rag/graph/java_ontology.py +0 -465
- java_codebase_rag/graph/ladybug_queries.py +0 -2213
- java_codebase_rag/graph/path_filtering.py +0 -509
- java_codebase_rag/index/__init__.py +0 -0
- java_codebase_rag/index/java_index_flow_lancedb.py +0 -879
- java_codebase_rag/index/java_index_v1_common.py +0 -33
- java_codebase_rag/install_data/__init__.py +0 -0
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -110
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
- java_codebase_rag/installer.py +0 -2188
- java_codebase_rag/jrag.py +0 -4545
- java_codebase_rag/jrag_envelope.py +0 -1107
- java_codebase_rag/jrag_hints.py +0 -204
- java_codebase_rag/jrag_render.py +0 -926
- java_codebase_rag/lance_optimize.py +0 -264
- java_codebase_rag/mcp/__init__.py +0 -0
- java_codebase_rag/mcp/mcp_hints.py +0 -932
- java_codebase_rag/mcp/mcp_v2.py +0 -1916
- java_codebase_rag/mcp/server.py +0 -886
- java_codebase_rag/pipeline.py +0 -531
- java_codebase_rag/progress.py +0 -570
- java_codebase_rag/read_payloads.py +0 -781
- java_codebase_rag/search/__init__.py +0 -0
- java_codebase_rag/search/index_common.py +0 -10
- java_codebase_rag/search/search_lancedb.py +0 -1296
- java_codebase_rag/search/search_lexical.py +0 -449
- java_codebase_rag/search/search_scoring.py +0 -537
- java_codebase_rag/watch/__init__.py +0 -0
- java_codebase_rag/watch/client.py +0 -230
- java_codebase_rag/watch/daemon.py +0 -396
- java_codebase_rag/watch/lock.py +0 -201
- java_codebase_rag/watch/paths.py +0 -76
- java_codebase_rag/watch/protocol.py +0 -122
- java_codebase_rag/watch/server.py +0 -273
- java_codebase_rag/watch/warm.py +0 -105
- java_codebase_rag/watch/watcher.py +0 -394
- java_codebase_rag-0.12.0.dist-info/METADATA +0 -340
- java_codebase_rag-0.12.0.dist-info/RECORD +0 -75
- java_codebase_rag-0.12.0.dist-info/entry_points.txt +0 -5
- java_codebase_rag-0.12.0.dist-info/licenses/LICENSE +0 -21
- java_codebase_rag-0.12.0.dist-info/top_level.txt +0 -1
- /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.2.dist-info/top_level.txt +0 -0
java_codebase_rag/config.py
DELETED
|
@@ -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
|