java-codebase-rag 0.6.2__py3-none-any.whl → 0.6.3__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.
- build_ast_graph.py +95 -7
- java_codebase_rag/cli.py +87 -14
- java_codebase_rag/cli_format.py +8 -35
- java_codebase_rag/cli_progress.py +17 -21
- java_codebase_rag/installer.py +180 -43
- java_codebase_rag/lance_optimize.py +125 -51
- java_codebase_rag/pipeline.py +71 -23
- java_codebase_rag/progress.py +570 -0
- {java_codebase_rag-0.6.2.dist-info → java_codebase_rag-0.6.3.dist-info}/METADATA +4 -1
- {java_codebase_rag-0.6.2.dist-info → java_codebase_rag-0.6.3.dist-info}/RECORD +16 -15
- java_index_flow_lancedb.py +155 -0
- server.py +75 -9
- {java_codebase_rag-0.6.2.dist-info → java_codebase_rag-0.6.3.dist-info}/WHEEL +0 -0
- {java_codebase_rag-0.6.2.dist-info → java_codebase_rag-0.6.3.dist-info}/entry_points.txt +0 -0
- {java_codebase_rag-0.6.2.dist-info → java_codebase_rag-0.6.3.dist-info}/licenses/LICENSE +0 -0
- {java_codebase_rag-0.6.2.dist-info → java_codebase_rag-0.6.3.dist-info}/top_level.txt +0 -0
java_index_flow_lancedb.py
CHANGED
|
@@ -18,10 +18,13 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import inspect
|
|
20
20
|
import os
|
|
21
|
+
import sys
|
|
22
|
+
import threading
|
|
21
23
|
import uuid
|
|
22
24
|
from collections.abc import AsyncIterator
|
|
23
25
|
from contextlib import asynccontextmanager
|
|
24
26
|
from dataclasses import dataclass
|
|
27
|
+
from fnmatch import fnmatch
|
|
25
28
|
from pathlib import Path
|
|
26
29
|
from typing import Annotated, Any
|
|
27
30
|
|
|
@@ -84,6 +87,138 @@ splitter = RecursiveSplitter()
|
|
|
84
87
|
_NUM_TXN_BEFORE_OPTIMIZE = 10**12
|
|
85
88
|
|
|
86
89
|
|
|
90
|
+
# --- Vectors-phase progress emission (JCIRAG_PROGRESS kind=vectors) -----------
|
|
91
|
+
#
|
|
92
|
+
# The flow runs in a CHILD cocoindex process; it prints structured progress to
|
|
93
|
+
# its stderr and the parent (pipeline._popen_capturing_stderr /
|
|
94
|
+
# cli_progress.accumulate_and_relay_subprocess_streams) parses it via
|
|
95
|
+
# ProgressRelay and feeds the renderer. The flow CANNOT know when all files are
|
|
96
|
+
# done (cocoindex offers no "all files done" hook in the flow), so it emits:
|
|
97
|
+
# - ONE ``total=N status=running`` line from ``app_main`` (approximate
|
|
98
|
+
# pre-walk: matcher includes + LayeredIgnore), and
|
|
99
|
+
# - per-file ``done=k status=running`` ticks (throttled every ~25 files) from
|
|
100
|
+
# ``process_*_file`` (shared atomic counter).
|
|
101
|
+
# The PARENT emits the terminal ``status=done``/``failed`` vectors event on
|
|
102
|
+
# cocoindex exit (drives clamp-on-completion + phase transition to Optimize).
|
|
103
|
+
|
|
104
|
+
# Per-file tick cadence: bound stderr volume on huge trees without making the
|
|
105
|
+
# bar feel stale. Every 25th file (and the modulo boundary is enough — the
|
|
106
|
+
# parent clamps to total on the terminal event anyway).
|
|
107
|
+
_VECTORS_TICK_EVERY = 25
|
|
108
|
+
|
|
109
|
+
# Thread-safe counter: cocoindex may call process_*_file concurrently
|
|
110
|
+
# (mount_each parallelism is implementation-defined). A module-level lock guards
|
|
111
|
+
# both the counter and the emission so two threads never interleave a tick.
|
|
112
|
+
_vectors_done_lock = threading.Lock()
|
|
113
|
+
_vectors_done_count = 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _emit_vectors_progress(
|
|
117
|
+
*,
|
|
118
|
+
done: int | None = None,
|
|
119
|
+
total: int | None = None,
|
|
120
|
+
status: str = "running",
|
|
121
|
+
elapsed_s: float | None = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Emit one ``JCIRAG_PROGRESS kind=vectors …`` line to stderr (flushed).
|
|
124
|
+
|
|
125
|
+
Field order is fixed (kind, done, total, status, elapsed_s) so the parser
|
|
126
|
+
and tests can pin substrings. Omitted fields are simply absent.
|
|
127
|
+
"""
|
|
128
|
+
fields = ["kind=vectors"]
|
|
129
|
+
if done is not None:
|
|
130
|
+
fields.append(f"done={done}")
|
|
131
|
+
if total is not None:
|
|
132
|
+
fields.append(f"total={total}")
|
|
133
|
+
fields.append(f"status={status}")
|
|
134
|
+
if elapsed_s is not None:
|
|
135
|
+
fields.append(f"elapsed_s={elapsed_s:.2f}")
|
|
136
|
+
print("JCIRAG_PROGRESS " + " ".join(fields), file=sys.stderr, flush=True)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _tick_vectors_done() -> None:
|
|
140
|
+
"""Increment the shared per-file counter and emit a throttled ``done=k`` tick.
|
|
141
|
+
|
|
142
|
+
Called once per successfully-processed file (after the ignore / empty
|
|
143
|
+
early-returns). The tick is emitted every ``_VECTORS_TICK_EVERY`` files so
|
|
144
|
+
stderr volume stays bounded on huge trees; the parent clamps to total on
|
|
145
|
+
the terminal event, so the exact tick cadence is not load-bearing.
|
|
146
|
+
"""
|
|
147
|
+
global _vectors_done_count
|
|
148
|
+
with _vectors_done_lock:
|
|
149
|
+
_vectors_done_count += 1
|
|
150
|
+
n = _vectors_done_count
|
|
151
|
+
if n % _VECTORS_TICK_EVERY != 0:
|
|
152
|
+
return
|
|
153
|
+
# Emit under the lock: the docstring above promises the lock guards both
|
|
154
|
+
# the counter AND the emission, so two concurrent ticks can't emit their
|
|
155
|
+
# ``done=N`` lines out of order. Contention is negligible (fires every
|
|
156
|
+
# ~25 files).
|
|
157
|
+
_emit_vectors_progress(done=n, status="running")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _approximate_vectors_total(project_root: Path) -> int:
|
|
161
|
+
"""Reproduce the matchers' include globs + LayeredIgnore for an approximate total.
|
|
162
|
+
|
|
163
|
+
The flow applies two filtering layers: (1) ``PatternFilePathMatcher``
|
|
164
|
+
excludes at walk time via ``LayeredIgnore.cocoindex_excluded_patterns()``,
|
|
165
|
+
then (2) ``LayeredIgnore.is_ignored()`` plus an early-return for empty /
|
|
166
|
+
undecodable files inside each ``process_*_file``. Files that early-return
|
|
167
|
+
never tick, so this pre-walk OVERSTATES the total by the ignored / empty
|
|
168
|
+
count. The parent clamps the bar to 100% on the terminal ``status=done``
|
|
169
|
+
event, so the over-count cannot stall the bar.
|
|
170
|
+
|
|
171
|
+
Mirrors the three ``localfs.walk_dir`` matchers in ``app_main``:
|
|
172
|
+
- ``**/*.java``
|
|
173
|
+
- ``**/src/main/resources/db/migration/*.sql``
|
|
174
|
+
- ``**/src/main/resources/application*.yml`` and ``.yaml``
|
|
175
|
+
"""
|
|
176
|
+
ignore = LayeredIgnore(project_root)
|
|
177
|
+
excluded = ignore.cocoindex_excluded_patterns()
|
|
178
|
+
|
|
179
|
+
def _excluded(rel_posix: str) -> bool:
|
|
180
|
+
return any(fnmatch(rel_posix, pat) for pat in excluded)
|
|
181
|
+
|
|
182
|
+
total = 0
|
|
183
|
+
for dirpath, dirnames, filenames in os.walk(project_root):
|
|
184
|
+
# Prune the same universal nuisance dirs as iter_java_source_files /
|
|
185
|
+
# cocoindex walk. (build-output pruning is matcher-dependent in the
|
|
186
|
+
# real walk; for an APPROXIMATE total this cheap prune is sufficient
|
|
187
|
+
# — the clamp absorbs any residual divergence.)
|
|
188
|
+
dirnames[:] = [
|
|
189
|
+
d for d in dirnames if d not in (".git", ".hg", ".svn", "node_modules", ".venv", "venv")
|
|
190
|
+
]
|
|
191
|
+
for fn in filenames:
|
|
192
|
+
full = Path(dirpath) / fn
|
|
193
|
+
try:
|
|
194
|
+
rel = full.resolve().relative_to(project_root).as_posix()
|
|
195
|
+
except ValueError:
|
|
196
|
+
continue
|
|
197
|
+
if _excluded(rel):
|
|
198
|
+
continue
|
|
199
|
+
# Java: **/*.java
|
|
200
|
+
if fn.endswith(".java"):
|
|
201
|
+
if not ignore.is_ignored(full)[0]:
|
|
202
|
+
total += 1
|
|
203
|
+
continue
|
|
204
|
+
# SQL: **/src/main/resources/db/migration/*.sql
|
|
205
|
+
if fn.endswith(".sql") and "/db/migration/" in rel:
|
|
206
|
+
if not ignore.is_ignored(full)[0]:
|
|
207
|
+
total += 1
|
|
208
|
+
continue
|
|
209
|
+
# YAML: **/src/main/resources/application*.yml / .yaml
|
|
210
|
+
# NOTE: ``fn`` is the bare filename (e.g. ``application-cloud.yml``), so
|
|
211
|
+
# the prefix predicate must be ``fn.startswith("application")`` —
|
|
212
|
+
# ``"/application" in fn`` was always False (no leading slash in a bare
|
|
213
|
+
# name) and under-counted every application YAML, driving the pre-walk
|
|
214
|
+
# total below the actual done count. The ``rel``-based
|
|
215
|
+
# ``"/src/main/resources/"`` gate stays (full path component).
|
|
216
|
+
if fn.endswith((".yml", ".yaml")) and fn.startswith("application") and "/src/main/resources/" in rel:
|
|
217
|
+
if not ignore.is_ignored(full)[0]:
|
|
218
|
+
total += 1
|
|
219
|
+
return total
|
|
220
|
+
|
|
221
|
+
|
|
87
222
|
@dataclass
|
|
88
223
|
class JavaLanceChunk:
|
|
89
224
|
id: str
|
|
@@ -187,6 +322,8 @@ async def process_java_file(
|
|
|
187
322
|
if not content.strip():
|
|
188
323
|
return
|
|
189
324
|
|
|
325
|
+
_tick_vectors_done()
|
|
326
|
+
|
|
190
327
|
language = detect_code_language(filename=file.file_path.path.name) or "text"
|
|
191
328
|
cs, mn, ov = JAVA_CHUNK
|
|
192
329
|
chunks = splitter.split(
|
|
@@ -251,6 +388,8 @@ async def process_sql_file(
|
|
|
251
388
|
if not content.strip():
|
|
252
389
|
return
|
|
253
390
|
|
|
391
|
+
_tick_vectors_done()
|
|
392
|
+
|
|
254
393
|
language = "sql"
|
|
255
394
|
cs, mn, ov = SQL_CHUNK
|
|
256
395
|
chunks = splitter.split(
|
|
@@ -295,6 +434,8 @@ async def process_yaml_file(
|
|
|
295
434
|
if not content.strip():
|
|
296
435
|
return
|
|
297
436
|
|
|
437
|
+
_tick_vectors_done()
|
|
438
|
+
|
|
298
439
|
ext = file.file_path.path.suffix.lower()
|
|
299
440
|
language = "yaml" if ext in (".yml", ".yaml") else "text"
|
|
300
441
|
cs, mn, ov = YAML_CHUNK
|
|
@@ -362,6 +503,20 @@ async def app_main() -> None:
|
|
|
362
503
|
project_root = coco.use_context(PROJECT_ROOT)
|
|
363
504
|
_ignore = LayeredIgnore(project_root)
|
|
364
505
|
_walk_excludes = _ignore.cocoindex_excluded_patterns()
|
|
506
|
+
# Emit ONE approximate total so the parent's renderer can show a determinate
|
|
507
|
+
# bar (clamps to 100% on the terminal vectors event the parent emits on
|
|
508
|
+
# cocoindex exit). Approximate — ignored / empty files over-state it; see
|
|
509
|
+
# ``_approximate_vectors_total``. ``--full-reprocess`` only: on incremental
|
|
510
|
+
# catch-up the @coco.fn(memo=True) cache skips unchanged files, so no total
|
|
511
|
+
# is knowable up front → the parent renders indeterminate from the absence.
|
|
512
|
+
try:
|
|
513
|
+
total = _approximate_vectors_total(project_root)
|
|
514
|
+
if total > 0:
|
|
515
|
+
_emit_vectors_progress(total=total, status="running")
|
|
516
|
+
except Exception:
|
|
517
|
+
# The pre-walk must never break indexing — a failure here just means
|
|
518
|
+
# the parent falls back to indeterminate. Swallow and continue.
|
|
519
|
+
pass
|
|
365
520
|
java_files = localfs.walk_dir(
|
|
366
521
|
PROJECT_ROOT,
|
|
367
522
|
recursive=True,
|
server.py
CHANGED
|
@@ -13,9 +13,8 @@ import mcp_v2
|
|
|
13
13
|
from index_common import SBERT_MODEL
|
|
14
14
|
from java_codebase_rag.cli_progress import (
|
|
15
15
|
accumulate_and_relay_subprocess_streams,
|
|
16
|
-
emit_vectors_finish,
|
|
17
|
-
emit_vectors_start,
|
|
18
16
|
)
|
|
17
|
+
from java_codebase_rag.progress import ProgressEvent
|
|
19
18
|
from java_codebase_rag._fdlimit import raise_fd_limit
|
|
20
19
|
from java_codebase_rag.config import (
|
|
21
20
|
cocoindex_subprocess_env_defaults,
|
|
@@ -113,7 +112,44 @@ class ScopeManager:
|
|
|
113
112
|
|
|
114
113
|
def _detect_scope(self) -> str | None:
|
|
115
114
|
from graph_enrich import detect_microservice_from_path
|
|
116
|
-
|
|
115
|
+
|
|
116
|
+
candidate = detect_microservice_from_path(Path.cwd(), self.source_root)
|
|
117
|
+
if candidate is None:
|
|
118
|
+
return None
|
|
119
|
+
# Only auto-scope to a microservice that actually has indexed code.
|
|
120
|
+
# detect_microservice_from_path can mislabel a non-microservice
|
|
121
|
+
# top-level child of source_root — most importantly the config/context
|
|
122
|
+
# directory the MCP server is launched from (no build marker, no
|
|
123
|
+
# source) — via its "first path segment under root" fallback. Scoping
|
|
124
|
+
# every query to such a name yields zero matches, so all tools return
|
|
125
|
+
# empty. A real microservice the operator is working in is, by
|
|
126
|
+
# definition, present in the index, so validating against the indexed
|
|
127
|
+
# set cannot suppress a legitimate scope. When the index is unreadable
|
|
128
|
+
# (empty known set) we keep the detected candidate rather than silently
|
|
129
|
+
# disabling auto-scope on a transient graph error.
|
|
130
|
+
known = self._indexed_microservices()
|
|
131
|
+
if known and candidate not in known:
|
|
132
|
+
return None
|
|
133
|
+
return candidate
|
|
134
|
+
|
|
135
|
+
def _indexed_microservices(self) -> set[str]:
|
|
136
|
+
"""Microservice names that have indexed type symbols.
|
|
137
|
+
|
|
138
|
+
Graph-only source of truth: the graph is always built alongside Lance,
|
|
139
|
+
and a Lance-only index (no graph) is not a supported state. Any failure
|
|
140
|
+
(graph missing, open error, empty index) returns an empty set, which
|
|
141
|
+
``_detect_scope`` treats as "cannot validate — keep detection".
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
if not LadybugGraph.exists():
|
|
145
|
+
return set()
|
|
146
|
+
# LadybugGraph.get() opens the DB and runs meta(); it can raise
|
|
147
|
+
# (e.g. RuntimeError on ontology-version mismatch). Caught here ->
|
|
148
|
+
# empty set -> _detect_scope keeps the detected scope.
|
|
149
|
+
counts = LadybugGraph.get().microservice_counts()
|
|
150
|
+
return {name for name in counts if name}
|
|
151
|
+
except Exception:
|
|
152
|
+
return set()
|
|
117
153
|
|
|
118
154
|
def _log_detection(self) -> None:
|
|
119
155
|
if self.default_scope:
|
|
@@ -270,10 +306,20 @@ def list_code_index_tables_payload() -> IndexInfoOutput:
|
|
|
270
306
|
)
|
|
271
307
|
|
|
272
308
|
|
|
273
|
-
async def run_refresh_pipeline(
|
|
309
|
+
async def run_refresh_pipeline(
|
|
310
|
+
*,
|
|
311
|
+
quiet: bool = False,
|
|
312
|
+
verbose: bool = True,
|
|
313
|
+
on_progress=None,
|
|
314
|
+
on_progress_console: object | None = None,
|
|
315
|
+
) -> RefreshIndexOutput:
|
|
274
316
|
root = _project_root()
|
|
275
317
|
cocoindex_bin = Path(sys.executable).parent / "cocoindex"
|
|
276
318
|
if not cocoindex_bin.is_file():
|
|
319
|
+
# 127 pre-spawn: emit a terminal failed vectors event so the renderer's
|
|
320
|
+
# task doesn't hang at running (matches the sync pipeline path).
|
|
321
|
+
if on_progress is not None:
|
|
322
|
+
on_progress(ProgressEvent(kind="vectors", phase=None, pass_=None, done=None, total=None, status="failed", elapsed_s=None))
|
|
277
323
|
return RefreshIndexOutput(
|
|
278
324
|
success=False,
|
|
279
325
|
message=f"cocoindex not found next to Python: {cocoindex_bin}",
|
|
@@ -286,6 +332,8 @@ async def run_refresh_pipeline(*, quiet: bool = False, verbose: bool = True) ->
|
|
|
286
332
|
if fallback.is_file():
|
|
287
333
|
flow_path = fallback
|
|
288
334
|
else:
|
|
335
|
+
if on_progress is not None:
|
|
336
|
+
on_progress(ProgressEvent(kind="vectors", phase=None, pass_=None, done=None, total=None, status="failed", elapsed_s=None))
|
|
289
337
|
return RefreshIndexOutput(
|
|
290
338
|
success=False,
|
|
291
339
|
message=f"java_index_flow_lancedb.py not found under {root} nor {bundle_dir}",
|
|
@@ -308,13 +356,14 @@ async def run_refresh_pipeline(*, quiet: bool = False, verbose: bool = True) ->
|
|
|
308
356
|
)
|
|
309
357
|
out_b, err_b = await proc.communicate()
|
|
310
358
|
except Exception as exc:
|
|
359
|
+
if on_progress is not None:
|
|
360
|
+
on_progress(ProgressEvent(kind="vectors", phase=None, pass_=None, done=None, total=None, status="failed", elapsed_s=None))
|
|
311
361
|
return RefreshIndexOutput(
|
|
312
362
|
success=False,
|
|
313
363
|
message=f"spawn failed: {exc!s}",
|
|
314
364
|
phases_run=[],
|
|
315
365
|
)
|
|
316
366
|
else:
|
|
317
|
-
emit_vectors_start()
|
|
318
367
|
t0 = time.perf_counter()
|
|
319
368
|
code_c = -1
|
|
320
369
|
try:
|
|
@@ -329,16 +378,30 @@ async def run_refresh_pipeline(*, quiet: bool = False, verbose: bool = True) ->
|
|
|
329
378
|
stdout=asyncio.subprocess.PIPE,
|
|
330
379
|
stderr=asyncio.subprocess.PIPE,
|
|
331
380
|
)
|
|
332
|
-
|
|
381
|
+
# The vectors task is fed by the child's per-file ticks + the
|
|
382
|
+
# approximate total line, parsed by the ProgressRelay inside the
|
|
383
|
+
# async drain and routed to on_progress.
|
|
384
|
+
out_b, err_b = await accumulate_and_relay_subprocess_streams(
|
|
385
|
+
proc, relay=True, verbose=verbose,
|
|
386
|
+
on_progress=on_progress, on_progress_console=on_progress_console,
|
|
387
|
+
)
|
|
333
388
|
code_c = proc.returncode if proc.returncode is not None else -1
|
|
334
389
|
except Exception as exc:
|
|
390
|
+
if on_progress is not None:
|
|
391
|
+
on_progress(ProgressEvent(kind="vectors", phase=None, pass_=None, done=None, total=None, status="failed", elapsed_s=None))
|
|
335
392
|
return RefreshIndexOutput(
|
|
336
393
|
success=False,
|
|
337
394
|
message=f"spawn failed: {exc!s}",
|
|
338
395
|
phases_run=[],
|
|
339
396
|
)
|
|
340
397
|
finally:
|
|
341
|
-
|
|
398
|
+
# The parent emits the terminal vectors event (the flow can't — no
|
|
399
|
+
# "all files done" hook). Drives clamp-on-completion + phase
|
|
400
|
+
# transition to Optimize.
|
|
401
|
+
if on_progress is not None:
|
|
402
|
+
elapsed = time.perf_counter() - t0
|
|
403
|
+
status = "done" if code_c == 0 else "failed"
|
|
404
|
+
on_progress(ProgressEvent(kind="vectors", phase=None, pass_=None, done=None, total=None, status=status, elapsed_s=elapsed))
|
|
342
405
|
assert proc is not None
|
|
343
406
|
out = out_b.decode(errors="replace")
|
|
344
407
|
err = err_b.decode(errors="replace")
|
|
@@ -366,7 +429,7 @@ async def run_refresh_pipeline(*, quiet: bool = False, verbose: bool = True) ->
|
|
|
366
429
|
idx_dir = Path(idx_raw)
|
|
367
430
|
else:
|
|
368
431
|
idx_dir = (root / ".java-codebase-rag").resolve()
|
|
369
|
-
await optimize_lance_tables(idx_dir, quiet=quiet)
|
|
432
|
+
await optimize_lance_tables(idx_dir, quiet=quiet, on_progress=on_progress)
|
|
370
433
|
except Exception as exc:
|
|
371
434
|
optimize_error = f"lance optimize failed: {exc}"
|
|
372
435
|
print(f"java-codebase-rag: {optimize_error}", file=sys.stderr)
|
|
@@ -394,7 +457,10 @@ async def run_refresh_pipeline(*, quiet: bool = False, verbose: bool = True) ->
|
|
|
394
457
|
if quiet:
|
|
395
458
|
gout_b, gerr_b = await gproc.communicate()
|
|
396
459
|
else:
|
|
397
|
-
gout_b, gerr_b = await accumulate_and_relay_subprocess_streams(
|
|
460
|
+
gout_b, gerr_b = await accumulate_and_relay_subprocess_streams(
|
|
461
|
+
gproc, relay=True, verbose=verbose,
|
|
462
|
+
on_progress=on_progress, on_progress_console=on_progress_console,
|
|
463
|
+
)
|
|
398
464
|
graph_code = gproc.returncode
|
|
399
465
|
graph_out = gout_b.decode(errors="replace")
|
|
400
466
|
graph_err = gerr_b.decode(errors="replace")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|