java-codebase-rag 0.9.7__py3-none-any.whl → 0.10.0__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/analysis/pr_analysis.py +33 -3
- java_codebase_rag/ast/ast_java.py +2 -1
- java_codebase_rag/cli.py +23 -8
- java_codebase_rag/config.py +68 -1
- java_codebase_rag/graph/build_ast_graph.py +123 -4
- java_codebase_rag/graph/graph_types.py +109 -22
- java_codebase_rag/graph/ladybug_queries.py +45 -2
- java_codebase_rag/index/java_index_flow_lancedb.py +10 -16
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +3 -1
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +3 -1
- java_codebase_rag/jrag.py +627 -661
- java_codebase_rag/jrag_render.py +160 -3
- java_codebase_rag/lance_optimize.py +11 -12
- java_codebase_rag/mcp/mcp_v2.py +2 -1
- java_codebase_rag/pipeline.py +47 -1
- java_codebase_rag/read_payloads.py +781 -0
- java_codebase_rag/search/search_lancedb.py +138 -6
- java_codebase_rag/search/search_lexical.py +128 -30
- java_codebase_rag/search/search_scoring.py +82 -0
- java_codebase_rag/watch/__init__.py +0 -0
- java_codebase_rag/watch/client.py +230 -0
- java_codebase_rag/watch/daemon.py +368 -0
- java_codebase_rag/watch/lock.py +201 -0
- java_codebase_rag/watch/paths.py +76 -0
- java_codebase_rag/watch/protocol.py +122 -0
- java_codebase_rag/watch/server.py +273 -0
- java_codebase_rag/watch/warm.py +105 -0
- java_codebase_rag/watch/watcher.py +352 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.0.dist-info}/METADATA +30 -31
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.0.dist-info}/RECORD +34 -24
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.0.dist-info}/WHEEL +0 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.0.dist-info}/entry_points.txt +0 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Warm-resources holder for the ``jrag watch`` daemon.
|
|
2
|
+
|
|
3
|
+
``WarmResources`` keeps two expensive objects resident for the daemon's lifetime:
|
|
4
|
+
|
|
5
|
+
* the ``SentenceTransformer`` embedding model (loaded once, reused for every
|
|
6
|
+
query), and
|
|
7
|
+
* a read-only ``LadybugGraph`` over ``cfg.ladybug_path``.
|
|
8
|
+
|
|
9
|
+
It also owns the graph copy-on-write snapshot lifecycle (design §4.7): while a
|
|
10
|
+
graph-reindex subprocess writes the ORIGINAL ``code_graph.lbug``, the daemon serves
|
|
11
|
+
graph reads from a file COPY (sidecar) so readers and the single-writer subprocess
|
|
12
|
+
never collide. The ``ladybug`` engine has no transaction API and is single-writer,
|
|
13
|
+
which is why graph builds run as subprocesses and reads are served from a copy.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import shutil
|
|
19
|
+
import threading
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from java_codebase_rag.config import ResolvedOperatorConfig
|
|
23
|
+
from java_codebase_rag.graph.ladybug_queries import LadybugGraph
|
|
24
|
+
from java_codebase_rag.mcp.mcp_v2 import _get_sentence_transformer
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WarmResources:
|
|
30
|
+
"""Holder for the warm embedding model + the read-only graph reader.
|
|
31
|
+
|
|
32
|
+
The model is cached automatically by ``mcp_v2``'s module global, so ``model()``
|
|
33
|
+
returning the same instance across calls is the warmth win. ``graph()`` returns
|
|
34
|
+
the snapshot reader when a snapshot is active, else the original reader. Both are
|
|
35
|
+
safe to call from server threads (the underlying singletons are lock-guarded).
|
|
36
|
+
|
|
37
|
+
The snapshot flip (``begin_graph_snapshot`` / ``commit_graph_snapshot``) is
|
|
38
|
+
serialized against ``graph()`` reads by ``self._lock`` so a reader can never
|
|
39
|
+
observe a torn state (e.g. read ``_snapshot_path=None`` after ``begin`` has
|
|
40
|
+
already reset the original singleton but before it set the sidecar, then
|
|
41
|
+
re-cache the original a subprocess is concurrently overwriting). Lock ordering:
|
|
42
|
+
``WarmResources._lock`` is always the OUTER lock; ``LadybugGraph._lock`` (taken
|
|
43
|
+
inside ``get``/``reset_for_path``) is INNER — never reversed. The lock is held
|
|
44
|
+
only across the in-process flip + read, NEVER across the long reindex
|
|
45
|
+
subprocess (the watcher calls ``begin`` before it and ``commit`` after).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, cfg: ResolvedOperatorConfig) -> None:
|
|
49
|
+
self.cfg = cfg
|
|
50
|
+
self._snapshot_path: Path | None = None
|
|
51
|
+
# Serializes the snapshot flip against graph() reads (see class docstring).
|
|
52
|
+
self._lock = threading.Lock()
|
|
53
|
+
|
|
54
|
+
def model(self):
|
|
55
|
+
"""Return the warm ``SentenceTransformer`` (cached by the module global)."""
|
|
56
|
+
return _get_sentence_transformer(self.cfg.embedding_model, self.cfg.embedding_device)
|
|
57
|
+
|
|
58
|
+
def graph(self) -> LadybugGraph:
|
|
59
|
+
"""Return the current graph reader: the sidecar if a snapshot is active, else the original.
|
|
60
|
+
|
|
61
|
+
The read of ``_snapshot_path`` and the matching ``LadybugGraph.get(...)`` are
|
|
62
|
+
atomic w.r.t. the snapshot flip (held under ``self._lock``) so a reader always
|
|
63
|
+
pairs the right path with the right cached singleton.
|
|
64
|
+
"""
|
|
65
|
+
with self._lock:
|
|
66
|
+
if self._snapshot_path is not None:
|
|
67
|
+
return LadybugGraph.get(str(self._snapshot_path))
|
|
68
|
+
return LadybugGraph.get(str(self.cfg.ladybug_path))
|
|
69
|
+
|
|
70
|
+
def begin_graph_snapshot(self) -> None:
|
|
71
|
+
"""Copy the graph to a sidecar and serve subsequent ``graph()`` reads from it.
|
|
72
|
+
|
|
73
|
+
Drops the cached original reader (so the subprocess is free to overwrite the
|
|
74
|
+
original file) and switches ``graph()`` to a fresh reader on the sidecar copy.
|
|
75
|
+
The whole flip runs under ``self._lock`` so no ``graph()`` read can observe an
|
|
76
|
+
intermediate state (original reset but sidecar not yet set).
|
|
77
|
+
"""
|
|
78
|
+
with self._lock:
|
|
79
|
+
sidecar = self.cfg.ladybug_path.with_suffix(".lbug.snapshot")
|
|
80
|
+
shutil.copy2(self.cfg.ladybug_path, sidecar)
|
|
81
|
+
LadybugGraph.reset_for_path(str(self.cfg.ladybug_path))
|
|
82
|
+
self._snapshot_path = sidecar
|
|
83
|
+
|
|
84
|
+
def commit_graph_snapshot(self) -> None:
|
|
85
|
+
"""Drop the sidecar reader, remove the sidecar, and reopen the updated original.
|
|
86
|
+
|
|
87
|
+
After this call ``graph()`` reads the original again (which the subprocess has
|
|
88
|
+
just rewritten), and the sidecar file is gone. Idempotent no-op when no
|
|
89
|
+
snapshot is active. The whole flip runs under ``self._lock`` so no ``graph()``
|
|
90
|
+
read can observe the sidecar mid-teardown.
|
|
91
|
+
"""
|
|
92
|
+
with self._lock:
|
|
93
|
+
if self._snapshot_path is None:
|
|
94
|
+
return
|
|
95
|
+
sidecar = self._snapshot_path
|
|
96
|
+
LadybugGraph.reset_for_path(str(sidecar))
|
|
97
|
+
try:
|
|
98
|
+
sidecar.unlink(missing_ok=True)
|
|
99
|
+
except OSError:
|
|
100
|
+
log.warning("Failed to remove graph snapshot sidecar %s", sidecar, exc_info=True)
|
|
101
|
+
finally:
|
|
102
|
+
# Clear state and reopen the original even if unlink failed, so a
|
|
103
|
+
# possibly-deleted sidecar isn't kept serving reads.
|
|
104
|
+
self._snapshot_path = None
|
|
105
|
+
LadybugGraph.reset_for_path(str(self.cfg.ladybug_path))
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""File watcher + debounced per-type reindex dispatcher for ``jrag watch``.
|
|
2
|
+
|
|
3
|
+
``SourceWatcher`` observes the project source tree and, after a debounce window,
|
|
4
|
+
re-runs the minimal reindex for the changed file types:
|
|
5
|
+
|
|
6
|
+
* a ``.java`` change -> vectors THEN graph. The graph reindex runs as a
|
|
7
|
+
subprocess under a copy-on-write snapshot (design §4.7) so concurrent graph
|
|
8
|
+
reads keep being served from a sidecar copy while the single-writer
|
|
9
|
+
subprocess overwrites the original. ``ladybug`` has no transactions and is
|
|
10
|
+
single-writer, which is why graph builds are subprocesses and reads are
|
|
11
|
+
served from a copy.
|
|
12
|
+
* a matching ``.sql`` / ``.yml`` / ``.yaml`` resource change -> vectors only
|
|
13
|
+
(the graph does not index SQL/YAML, so no snapshot is needed).
|
|
14
|
+
|
|
15
|
+
Change bursts are coalesced: the leading event arms a ``debounce_ms`` timer that
|
|
16
|
+
keeps getting re-armed while events keep arriving, and a single ``reindex`` fires
|
|
17
|
+
once the window goes quiet. ``reindex`` runs on a dedicated background thread so
|
|
18
|
+
the watchdog observer thread is never blocked.
|
|
19
|
+
|
|
20
|
+
The cocoindex flow indexes three sets (``**/*.java``,
|
|
21
|
+
``**/src/main/resources/db/migration/*.sql``,
|
|
22
|
+
``**/src/main/resources/application*.yml``/``.yaml``). There is NO shared
|
|
23
|
+
iterator (``iter_java_source_files`` yields ``.java`` only), so the watcher
|
|
24
|
+
defines this UNION and classifies each event path into a reindex kind.
|
|
25
|
+
|
|
26
|
+
All status is reported via ``on_event(kind, detail)`` callbacks (kinds:
|
|
27
|
+
``indexing_started`` / ``vectors`` / ``graph`` / ``indexing_done`` / ``error``);
|
|
28
|
+
the watcher never prints directly.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
import threading
|
|
34
|
+
import time
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
37
|
+
|
|
38
|
+
from watchdog.events import FileSystemEventHandler
|
|
39
|
+
from watchdog.observers import Observer
|
|
40
|
+
from watchdog.observers.polling import PollingObserver
|
|
41
|
+
|
|
42
|
+
from java_codebase_rag.graph.path_filtering import LayeredIgnore
|
|
43
|
+
from java_codebase_rag.pipeline import run_cocoindex_update, run_incremental_graph
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from java_codebase_rag.config import ResolvedOperatorConfig
|
|
47
|
+
from java_codebase_rag.watch.warm import WarmResources
|
|
48
|
+
|
|
49
|
+
log = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
# Suffixes the watcher treats as java sources. The non-java resource types are
|
|
52
|
+
# matched by the glob helpers below (the cocoindex set has no shared iterator).
|
|
53
|
+
INDEXED_SUFFIXES: tuple[str, ...] = (".java",)
|
|
54
|
+
_YAML_SUFFIXES: tuple[str, ...] = (".yml", ".yaml")
|
|
55
|
+
|
|
56
|
+
# Project-relative anchored prefixes for the two non-java resource globs:
|
|
57
|
+
# **/src/main/resources/db/migration/*.sql
|
|
58
|
+
# **/src/main/resources/application*.yml / .yaml
|
|
59
|
+
_SQL_MIGRATION_PREFIX = "src/main/resources/db/migration/"
|
|
60
|
+
_RESOURCES_PREFIX = "src/main/resources/"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_migration_sql(rel_posix: str) -> bool:
|
|
64
|
+
"""True iff ``rel_posix`` is a ``.sql`` directly under a ``.../db/migration/`` dir."""
|
|
65
|
+
if not rel_posix.endswith(".sql"):
|
|
66
|
+
return False
|
|
67
|
+
idx = rel_posix.rfind(_SQL_MIGRATION_PREFIX)
|
|
68
|
+
if idx == -1:
|
|
69
|
+
return False
|
|
70
|
+
# The file must sit directly under migration/ (no further path segment).
|
|
71
|
+
return "/" not in rel_posix[idx + len(_SQL_MIGRATION_PREFIX):]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_application_yaml(rel_posix: str) -> bool:
|
|
75
|
+
"""True iff ``rel_posix`` is ``application*.yml``/``.yaml`` directly under
|
|
76
|
+
a ``.../src/main/resources/`` dir."""
|
|
77
|
+
name = rel_posix.rsplit("/", 1)[-1]
|
|
78
|
+
if not name.startswith("application"):
|
|
79
|
+
return False
|
|
80
|
+
if not name.endswith((".yml", ".yaml")):
|
|
81
|
+
return False
|
|
82
|
+
idx = rel_posix.rfind(_RESOURCES_PREFIX)
|
|
83
|
+
if idx == -1:
|
|
84
|
+
return False
|
|
85
|
+
return "/" not in rel_posix[idx + len(_RESOURCES_PREFIX):]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _ChangeHandler(FileSystemEventHandler):
|
|
89
|
+
"""Bridge watchdog events to the watcher's classify+schedule path.
|
|
90
|
+
|
|
91
|
+
Directory events are ignored. Each file event's path (and, for moves, the
|
|
92
|
+
destination) is classified; if it matches an indexed type and survives
|
|
93
|
+
``LayeredIgnore`` its kind is scheduled.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, watcher: "SourceWatcher") -> None:
|
|
97
|
+
super().__init__()
|
|
98
|
+
self._watcher = watcher
|
|
99
|
+
|
|
100
|
+
def on_any_event(self, event) -> None: # type: ignore[override]
|
|
101
|
+
if event.is_directory:
|
|
102
|
+
return
|
|
103
|
+
self._watcher._handle_path(event.src_path)
|
|
104
|
+
dest = getattr(event, "dest_path", None)
|
|
105
|
+
if dest:
|
|
106
|
+
self._watcher._handle_path(dest)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SourceWatcher:
|
|
110
|
+
"""Watch ``cfg.source_root`` and dispatch debounced per-type reindexes.
|
|
111
|
+
|
|
112
|
+
Thread model: watchdog's observer thread calls the handler (which only does a
|
|
113
|
+
quick classify + schedule -- never blocks). A single dedicated debounce
|
|
114
|
+
worker thread runs ``reindex`` so the observer is never held up by a build.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
cfg: "ResolvedOperatorConfig",
|
|
120
|
+
warm: "WarmResources",
|
|
121
|
+
*,
|
|
122
|
+
debounce_ms: int,
|
|
123
|
+
backend: str,
|
|
124
|
+
poll_interval_ms: int,
|
|
125
|
+
on_event: Callable[[str, dict[str, Any]] | None] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
self.cfg = cfg
|
|
128
|
+
self.warm = warm
|
|
129
|
+
self._debounce_s = max(int(debounce_ms), 1) / 1000.0
|
|
130
|
+
self._backend = backend
|
|
131
|
+
# watchdog's PollingObserver takes a float timeout in SECONDS (it flows
|
|
132
|
+
# straight into ``threading.Event.wait``); a timedelta crashes the emitter.
|
|
133
|
+
self._poll_interval_s = max(int(poll_interval_ms), 1) / 1000.0
|
|
134
|
+
self._on_event = on_event
|
|
135
|
+
|
|
136
|
+
self._ignore = LayeredIgnore(cfg.source_root)
|
|
137
|
+
self._source_root_resolved = Path(cfg.source_root).resolve()
|
|
138
|
+
|
|
139
|
+
self._observer = self._make_observer()
|
|
140
|
+
self._handler = _ChangeHandler(self)
|
|
141
|
+
|
|
142
|
+
# Debounce state -- touched by the observer thread and the debounce worker.
|
|
143
|
+
self._lock = threading.Lock()
|
|
144
|
+
self._pending: set[str] = set()
|
|
145
|
+
self._activity = threading.Event()
|
|
146
|
+
self._stop = threading.Event()
|
|
147
|
+
self._debounce_thread = threading.Thread(
|
|
148
|
+
target=self._debounce_loop, name="jrag-watch-debounce", daemon=True
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# In-memory reindex state, refreshed after each successful reindex.
|
|
152
|
+
self.last_reindex: dict[str, Any] | None = None
|
|
153
|
+
|
|
154
|
+
# -- observer backend ----------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def _make_observer(self):
|
|
157
|
+
if self._backend == "polling":
|
|
158
|
+
return PollingObserver(timeout=self._poll_interval_s)
|
|
159
|
+
# "watchdog" and "auto" both prefer the native observer; "auto" falls
|
|
160
|
+
# back to polling in :meth:`start` if the native backend cannot start.
|
|
161
|
+
return Observer()
|
|
162
|
+
|
|
163
|
+
# -- lifecycle -----------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def start(self) -> None:
|
|
166
|
+
"""Schedule the observer on ``cfg.source_root`` (recursive) and start the
|
|
167
|
+
debounce loop. Under ``backend="auto"`` a native observer that fails to
|
|
168
|
+
start (e.g. on some network filesystems) falls back to polling."""
|
|
169
|
+
self._observer.schedule(self._handler, str(self.cfg.source_root), recursive=True)
|
|
170
|
+
try:
|
|
171
|
+
self._observer.start()
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
if self._backend != "auto":
|
|
174
|
+
raise
|
|
175
|
+
self._emit(
|
|
176
|
+
"error",
|
|
177
|
+
{"phase": "observer", "fallback": "polling", "error": repr(exc)},
|
|
178
|
+
)
|
|
179
|
+
self._observer = PollingObserver(timeout=self._poll_interval_s)
|
|
180
|
+
self._observer.schedule(self._handler, str(self.cfg.source_root), recursive=True)
|
|
181
|
+
self._observer.start()
|
|
182
|
+
self._debounce_thread.start()
|
|
183
|
+
|
|
184
|
+
def stop(self) -> None:
|
|
185
|
+
"""Stop the observer and the debounce loop; join both."""
|
|
186
|
+
self._stop.set()
|
|
187
|
+
self._activity.set() # unblock the debounce worker's wait
|
|
188
|
+
try:
|
|
189
|
+
if self._observer.is_alive():
|
|
190
|
+
self._observer.stop()
|
|
191
|
+
self._observer.join(timeout=5.0)
|
|
192
|
+
except Exception:
|
|
193
|
+
log.warning("watchdog observer stop failed", exc_info=True)
|
|
194
|
+
if (
|
|
195
|
+
self._debounce_thread.is_alive()
|
|
196
|
+
and threading.current_thread() is not self._debounce_thread
|
|
197
|
+
):
|
|
198
|
+
self._debounce_thread.join(timeout=10.0)
|
|
199
|
+
|
|
200
|
+
# -- event classification (observer thread) ------------------------------
|
|
201
|
+
#
|
|
202
|
+
# The watcher fires on exactly the cocoindex-indexed set (``.java`` plus the
|
|
203
|
+
# SQL/YAML resource patterns below) filtered through ``LayeredIgnore``.
|
|
204
|
+
# ``target/generated-sources/**/*.java`` therefore correctly fires: generated
|
|
205
|
+
# sources are first-class, cocoindex does NOT exclude ``target/``, so the
|
|
206
|
+
# watcher must fire to keep them fresh. ``LayeredIgnore.is_ignored`` does NOT
|
|
207
|
+
# prune build-output dirs (``target/``/``build``/``out``) -- that pruning lives
|
|
208
|
+
# in ``iter_java_source_files``'s ``os.walk`` (``_is_build_output_dir``), used
|
|
209
|
+
# by the graph builder, not by cocoindex. (Compiled ``.class`` output under
|
|
210
|
+
# ``target/`` doesn't match the ``.java`` suffix anyway.)
|
|
211
|
+
|
|
212
|
+
def _handle_path(self, src_path: object) -> None:
|
|
213
|
+
"""Classify one observed path and schedule its kind if indexed & not ignored."""
|
|
214
|
+
kinds = self._classify(Path(str(src_path)))
|
|
215
|
+
if kinds:
|
|
216
|
+
self._schedule(kinds)
|
|
217
|
+
|
|
218
|
+
def _classify(self, path: Path) -> set[str]:
|
|
219
|
+
"""Return the set of reindex kinds for ``path`` (empty if ignored/unknown).
|
|
220
|
+
|
|
221
|
+
``LayeredIgnore`` wins: an ignored path yields no kind even when its
|
|
222
|
+
suffix is ``.java``. Outside-source-root paths also yield nothing.
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
if self._ignore.is_ignored(path):
|
|
226
|
+
return set()
|
|
227
|
+
except Exception:
|
|
228
|
+
return set()
|
|
229
|
+
try:
|
|
230
|
+
rel = path.resolve().relative_to(self._source_root_resolved).as_posix()
|
|
231
|
+
except ValueError:
|
|
232
|
+
return set()
|
|
233
|
+
suffix = path.suffix.lower()
|
|
234
|
+
if suffix == ".java":
|
|
235
|
+
return {"java"}
|
|
236
|
+
if suffix == ".sql":
|
|
237
|
+
return {"sql"} if _is_migration_sql(rel) else set()
|
|
238
|
+
if suffix in _YAML_SUFFIXES:
|
|
239
|
+
return {"yaml"} if _is_application_yaml(rel) else set()
|
|
240
|
+
return set()
|
|
241
|
+
|
|
242
|
+
def _schedule(self, kinds: set[str]) -> None:
|
|
243
|
+
"""Union ``kinds`` into the debounce collector and (re)arm the debounce window."""
|
|
244
|
+
if not kinds:
|
|
245
|
+
return
|
|
246
|
+
with self._lock:
|
|
247
|
+
self._pending |= kinds
|
|
248
|
+
self._activity.set()
|
|
249
|
+
|
|
250
|
+
# -- debounce worker thread ----------------------------------------------
|
|
251
|
+
|
|
252
|
+
def _debounce_loop(self) -> None:
|
|
253
|
+
"""Fire ``reindex`` once per quiet period, coalescing bursts.
|
|
254
|
+
|
|
255
|
+
Leading edge: the first event arms a ``debounce_ms`` window. Each further
|
|
256
|
+
event during the window re-arms it. When the window expires with no new
|
|
257
|
+
events, the accumulated kinds are flushed to ``reindex`` in ONE call.
|
|
258
|
+
"""
|
|
259
|
+
while not self._stop.is_set():
|
|
260
|
+
# Wait for the leading edge of a burst.
|
|
261
|
+
self._activity.wait()
|
|
262
|
+
if self._stop.is_set():
|
|
263
|
+
return
|
|
264
|
+
# Re-arm while events keep arriving (debounce).
|
|
265
|
+
while not self._stop.is_set():
|
|
266
|
+
self._activity.clear()
|
|
267
|
+
if self._activity.wait(timeout=self._debounce_s):
|
|
268
|
+
continue # new activity -> reset the window
|
|
269
|
+
break # quiet period elapsed -> fire
|
|
270
|
+
if self._stop.is_set():
|
|
271
|
+
return
|
|
272
|
+
with self._lock:
|
|
273
|
+
kinds = self._pending
|
|
274
|
+
self._pending = set()
|
|
275
|
+
if kinds:
|
|
276
|
+
try:
|
|
277
|
+
self.reindex(kinds)
|
|
278
|
+
except Exception: # noqa: BLE001 -- reindex emits its own error; never kill the worker
|
|
279
|
+
log.warning("reindex raised unexpectedly", exc_info=True)
|
|
280
|
+
|
|
281
|
+
# -- reindex (runs on the debounce worker thread) ------------------------
|
|
282
|
+
|
|
283
|
+
def reindex(self, kinds: set[str]) -> None:
|
|
284
|
+
"""Run one debounced reindex: vectors always; graph only when java changed.
|
|
285
|
+
|
|
286
|
+
COW lifecycle (design §4.7): the graph subprocess writes the ORIGINAL
|
|
287
|
+
graph while reads are served from a sidecar copy. ``begin_graph_snapshot``
|
|
288
|
+
is called BEFORE the subprocess and ``commit_graph_snapshot`` ALWAYS
|
|
289
|
+
(success OR failure) so a snapshot reader is never left dangling -- on
|
|
290
|
+
graph failure the existing ``.graph_increment_in_progress`` crash marker
|
|
291
|
+
drives the next full rebuild. Lance needs no snapshot (commits are atomic
|
|
292
|
+
per version; fresh per-query reads are fine).
|
|
293
|
+
"""
|
|
294
|
+
if not kinds:
|
|
295
|
+
return
|
|
296
|
+
kind_list = sorted(kinds)
|
|
297
|
+
self._emit("indexing_started", {"kinds": kind_list})
|
|
298
|
+
try:
|
|
299
|
+
# Vectors run for every indexed type (java/sql/yaml all flow through cocoindex).
|
|
300
|
+
self._emit("vectors", {"kinds": kind_list})
|
|
301
|
+
vres = run_cocoindex_update(
|
|
302
|
+
self.cfg.subprocess_env(),
|
|
303
|
+
full_reprocess=False,
|
|
304
|
+
quiet=True,
|
|
305
|
+
verbose=False,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
graph_rc = 0
|
|
309
|
+
if "java" in kinds:
|
|
310
|
+
# Graph indexes java only; reindex under a COW snapshot so graph
|
|
311
|
+
# reads continue (from the sidecar) during the subprocess write.
|
|
312
|
+
self._emit("graph", {"kinds": kind_list})
|
|
313
|
+
try:
|
|
314
|
+
# begin/commit paired in this try/finally. begin_graph_snapshot
|
|
315
|
+
# sets ``_snapshot_path`` last, so if it raises commit no-ops;
|
|
316
|
+
# moving begin inside the try keeps the pairing locally evident.
|
|
317
|
+
self.warm.begin_graph_snapshot()
|
|
318
|
+
gres = run_incremental_graph(
|
|
319
|
+
source_root=self.cfg.source_root,
|
|
320
|
+
ladybug_path=self.cfg.ladybug_path,
|
|
321
|
+
verbose=False,
|
|
322
|
+
quiet=True,
|
|
323
|
+
env=self.cfg.subprocess_env(),
|
|
324
|
+
)
|
|
325
|
+
graph_rc = gres.returncode
|
|
326
|
+
finally:
|
|
327
|
+
# ALWAYS drop the snapshot reader -- even on failure -- so it
|
|
328
|
+
# is never left dangling. No-ops when no snapshot is active.
|
|
329
|
+
self.warm.commit_graph_snapshot()
|
|
330
|
+
|
|
331
|
+
if vres.returncode != 0:
|
|
332
|
+
self._emit("error", {"phase": "vectors", "returncode": vres.returncode})
|
|
333
|
+
return
|
|
334
|
+
if graph_rc != 0:
|
|
335
|
+
self._emit("error", {"phase": "graph", "returncode": graph_rc})
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
self.last_reindex = {"time": time.time(), "kinds": kind_list}
|
|
339
|
+
self._emit("indexing_done", {"kinds": kind_list})
|
|
340
|
+
except Exception as exc: # noqa: BLE001 -- the daemon must survive any reindex error
|
|
341
|
+
self._emit("error", {"phase": "reindex", "error": repr(exc)})
|
|
342
|
+
|
|
343
|
+
# -- status --------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
def _emit(self, kind: str, detail: dict[str, Any]) -> None:
|
|
346
|
+
"""Forward a status callback to the UI; never let it crash the watcher."""
|
|
347
|
+
if self._on_event is None:
|
|
348
|
+
return
|
|
349
|
+
try:
|
|
350
|
+
self._on_event(kind, detail)
|
|
351
|
+
except Exception: # noqa: BLE001 -- a UI callback must not crash the watcher
|
|
352
|
+
log.warning("on_event callback raised", exc_info=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-codebase-rag
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: MCP server for semantic + structural search over Java codebases
|
|
5
5
|
Author: HumanBean17
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,21 +21,22 @@ Classifier: Operating System :: Microsoft :: Windows
|
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: cocoindex[lancedb]<2,>=1.0.
|
|
24
|
+
Requires-Dist: cocoindex[lancedb]<2,>=1.0.15; sys_platform != "darwin" or platform_machine != "x86_64"
|
|
25
25
|
Requires-Dist: ladybug<0.18,>=0.17.1
|
|
26
|
-
Requires-Dist: lancedb<0.
|
|
26
|
+
Requires-Dist: lancedb<0.36,>=0.34; sys_platform != "darwin" or platform_machine != "x86_64"
|
|
27
27
|
Requires-Dist: mcp<2,>=1.27.0
|
|
28
28
|
Requires-Dist: numpy<2.5,>=1.26.4
|
|
29
29
|
Requires-Dist: pathspec<2,>=1.0.4
|
|
30
|
-
Requires-Dist: pyarrow<
|
|
30
|
+
Requires-Dist: pyarrow<26,>=23.0.1
|
|
31
31
|
Requires-Dist: pydantic<3,>=2.0
|
|
32
32
|
Requires-Dist: PyYAML<7,>=6.0.3
|
|
33
33
|
Requires-Dist: questionary<3,>=2.0
|
|
34
|
-
Requires-Dist: rich<
|
|
34
|
+
Requires-Dist: rich<16,>=14
|
|
35
35
|
Requires-Dist: sentence-transformers<6,>=5.4.0; sys_platform != "darwin" or platform_machine != "x86_64"
|
|
36
36
|
Requires-Dist: tree-sitter<0.26,>=0.25.2
|
|
37
37
|
Requires-Dist: tree-sitter-java<0.24,>=0.23.5
|
|
38
38
|
Requires-Dist: unidiff<1,>=0.7.3
|
|
39
|
+
Requires-Dist: watchdog<7,>=6
|
|
39
40
|
Provides-Extra: dev
|
|
40
41
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
41
42
|
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
@@ -45,18 +46,16 @@ Dynamic: license-file
|
|
|
45
46
|
|
|
46
47
|
# java-codebase-rag
|
|
47
48
|
|
|
48
|
-
A graph-native code intelligence layer for Java microservice estates —
|
|
49
|
+
A graph-native code intelligence layer for Java microservice estates, surfaced through the **`jrag` CLI** — one command per engineering intent. A **legacy MCP server** (`search` / `find` / `describe` / `neighbors` / `resolve`) is also available for existing setups. Both are thin surfaces over the same **AST Graph**: a deterministic property graph extracted from Java source with tree-sitter, stored **locally** in **LadybugDB** (graph) alongside a **LanceDB** vector index (chunks). There is no server to host and no cloud round-trip — the index lives on your disk and your source never leaves the machine. Both surfaces collapse onto three primitive operations: **locate**, **inspect**, **walk**.
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
> **What this MCP is:** a **GPS for code navigation**, not a reasoning engine.
|
|
51
|
+
> **What this is: a GPS for code navigation**, not a reasoning engine.
|
|
53
52
|
> Agents use a simple loop:
|
|
54
53
|
>
|
|
55
|
-
> 1. **Locate** entry nodes (`
|
|
56
|
-
> 2. **Inspect** what a node is (`
|
|
57
|
-
> 3. **Walk** one hop at a time (`
|
|
54
|
+
> 1. **Locate** entry nodes (`jrag find`, `jrag search`, or identifier-shaped lookup)
|
|
55
|
+
> 2. **Inspect** what a node is (`jrag inspect`)
|
|
56
|
+
> 3. **Walk** one hop at a time (`jrag callers` / `callees` / `hierarchy` / …) until enough evidence is gathered
|
|
58
57
|
>
|
|
59
|
-
> The
|
|
58
|
+
> The tool exposes structure and adjacency; the agent owns multi-hop reasoning and stop conditions.
|
|
60
59
|
|
|
61
60
|
For the design rationale, the GPS metaphor, and the full ontology, see [`docs/paper/paper.pdf`](./docs/paper/paper.pdf) (architecture report).
|
|
62
61
|
|
|
@@ -66,7 +65,7 @@ For the design rationale, the GPS metaphor, and the full ontology, see [`docs/pa
|
|
|
66
65
|
|
|
67
66
|
Generic code-search tools (grep, ctags, vector-only RAG) hit a ceiling on real Java microservice estates: they find files but lose the structure that makes a Spring/JAX-RS system navigable. This project is built around five choices that target that gap.
|
|
68
67
|
|
|
69
|
-
- **Hybrid RAG +
|
|
68
|
+
- **Hybrid RAG + AST Graph, not either-or.** Semantic recall (LanceDB chunk vectors) and structural navigation (the LadybugDB AST Graph) are composed in one surface. `search` finds candidate nodes by meaning; `neighbors` walks the exact edge you care about (`CALLS`, `IMPLEMENTS`, `INJECTS`, `EXPOSES`, …). The agent picks the right primitive per step instead of being forced into pure-vector or pure-symbol search.
|
|
70
69
|
|
|
71
70
|
- **A Java-tuned role model.** Symbols are labelled with stereotypes inferred from Spring and JAX-RS conventions — `CONTROLLER`, `SERVICE`, `REPOSITORY`, `COMPONENT`, `CONFIG`, `ENTITY`, `CLIENT`, `MAPPER`, `DTO`. Agents can ask "list controllers" or "who injects this repository" directly, instead of grep-ing for `@RestController` and hoping for the best. Roles drive both filtering (`find` with a `NodeFilter`) and ranking.
|
|
72
71
|
|
|
@@ -86,7 +85,7 @@ The rest of this README is the install, the tool/command orientation, and the re
|
|
|
86
85
|
pip install java-codebase-rag
|
|
87
86
|
```
|
|
88
87
|
|
|
89
|
-
Python **3.11+** required, on **Linux, macOS, and Windows**. On Linux, Windows, and **Apple Silicon** Macs every native dependency (LanceDB, LadybugDB
|
|
88
|
+
Python **3.11+** required, on **Linux, macOS, and Windows**. On Linux, Windows, and **Apple Silicon** Macs every native dependency (LanceDB, LadybugDB, CocoIndex) ships a wheel and you get the full semantic + graph search. **Intel Macs (x86_64) install graph-only**: PyTorch ≥2.3 and LanceDB ≥0.26 dropped macOS Intel wheels, so the vector stack is auto-excluded via PEP 508 markers — `pip install java-codebase-rag` works out of the box, the graph layer (`find` / `describe` / `neighbors` / `resolve`) is fully usable, and the `search` tool falls back to **lexical search** over the symbol graph — BM25-ranked over a LadybugDB full-text index (same tool contract, keyword-ranked instead of semantic; an advisory notes the mode). Semantic/vector search needs Apple Silicon, Linux, or Windows. After install, `java-codebase-rag --help` should print the CLI groups.
|
|
90
89
|
The package includes the CocoIndex lifecycle dependency used by `init`, `increment`, `reprocess`, and `erase` on platforms that have it (it is absent on Intel Mac).
|
|
91
90
|
|
|
92
91
|
### Interactive setup (recommended)
|
|
@@ -122,19 +121,7 @@ If you prefer manual configuration, see [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs
|
|
|
122
121
|
|
|
123
122
|
## Tools & commands at a glance
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
**MCP surface — five tools over stdio**
|
|
128
|
-
|
|
129
|
-
| Tool | Purpose | Required args |
|
|
130
|
-
|---|---|---|
|
|
131
|
-
| `search` | Locate nodes by NL / code text. | `query` |
|
|
132
|
-
| `find` | Locate nodes by structured filter. | `kind`, `filter` |
|
|
133
|
-
| `describe` | Full record + edge counts for one node. | `id` |
|
|
134
|
-
| `resolve` | Identifier-shaped lookup (FQN-collision-safe). Returns `one` / `many` / `none`. | `identifier` |
|
|
135
|
-
| `neighbors` | Graph walk, one hop. | `ids`, `direction`, `edge_types` |
|
|
136
|
-
|
|
137
|
-
Full schemas, `NodeFilter` / `EdgeFilter` semantics, and the hints contract live in [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md). Edge types and traversal directions are listed in [`docs/EDGE-NAVIGATION.md`](./docs/EDGE-NAVIGATION.md).
|
|
124
|
+
`jrag` is the default and recommended surface (`java-codebase-rag install --surface cli`). The **MCP server** (`--surface mcp`) is kept as a **legacy** option for existing setups. Both surfaces walk the same LanceDB vectors + LadybugDB **AST Graph**. Switch an existing install later with `java-codebase-rag update --surface mcp|cli`.
|
|
138
125
|
|
|
139
126
|
**CLI surface — `jrag`, one command per engineering intent**
|
|
140
127
|
|
|
@@ -143,7 +130,7 @@ Full schemas, `NodeFilter` / `EdgeFilter` semantics, and the hints contract live
|
|
|
143
130
|
jrag status # index health (ontology version, freshness, counts)
|
|
144
131
|
jrag microservices # microservices with resolved type counts
|
|
145
132
|
jrag map # counts per kind per service/module
|
|
146
|
-
jrag map --module
|
|
133
|
+
jrag map --by module # group by module instead (--module filters)
|
|
147
134
|
jrag conventions # dominant roles + framework tallies
|
|
148
135
|
jrag overview chat-core # bundle for a microservice
|
|
149
136
|
jrag overview /chat/assign # route flow (inbound callers + outbound CALLS)
|
|
@@ -191,9 +178,21 @@ jrag search "audit" --offset 5 # paginated
|
|
|
191
178
|
|
|
192
179
|
Every `<query>` command takes human-readable identifiers (FQN / simple name / route path / topic) — never raw node IDs. Output contract, flags, and the resolve-first rule are in [`jrag` — agent CLI](#jrag--agent-cli) below.
|
|
193
180
|
|
|
181
|
+
**MCP surface — five tools over stdio (legacy)**
|
|
182
|
+
|
|
183
|
+
| Tool | Purpose | Required args |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `search` | Locate nodes by NL / code text. | `query` |
|
|
186
|
+
| `find` | Locate nodes by structured filter. | `kind`, `filter` |
|
|
187
|
+
| `describe` | Full record + edge counts for one node. | `id` |
|
|
188
|
+
| `resolve` | Identifier-shaped lookup (FQN-collision-safe). Returns `one` / `many` / `none`. | `identifier` |
|
|
189
|
+
| `neighbors` | Graph walk, one hop. | `ids`, `direction`, `edge_types` |
|
|
190
|
+
|
|
191
|
+
Full schemas, `NodeFilter` / `EdgeFilter` semantics, and the hints contract live in [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md). Edge types and traversal directions are listed in [`docs/EDGE-NAVIGATION.md`](./docs/EDGE-NAVIGATION.md).
|
|
192
|
+
|
|
194
193
|
### Three-layer architecture
|
|
195
194
|
|
|
196
|
-
Layer 1 (storage) → Layer 2 (
|
|
195
|
+
Layer 1 (storage) → Layer 2 (the `jrag` CLI, **or** the legacy 5-tool MCP) → Layer 3 (skill). The CLI-surface skill **[`/explore-codebase-cli`](./skills/explore-codebase-cli/SKILL.md)** documents the `jrag` CLI; the MCP-surface skill **[`/explore-codebase`](./skills/explore-codebase/SKILL.md)** documents the legacy 5-tool MCP (PR-JRAG-5). See the [architecture diagram in `skills/README.md`](./skills/README.md#three-layer-architecture).
|
|
197
196
|
|
|
198
197
|
---
|
|
199
198
|
|
|
@@ -303,7 +302,7 @@ full design and per-PR breakdown.
|
|
|
303
302
|
| [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md) | Environment variables, project YAML, graph ontology, brownfield overrides, ignore patterns. |
|
|
304
303
|
| [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) | CLI operator playbook: workflows, exit codes, env alignment. |
|
|
305
304
|
| [`docs/EDGE-NAVIGATION.md`](./docs/EDGE-NAVIGATION.md) | MCP-traversable edges, directions, dot-key composition. |
|
|
306
|
-
| [`skills/`](./skills/) | `/explore-codebase` (
|
|
305
|
+
| [`skills/`](./skills/) | `/explore-codebase-cli` (CLI surface) + `/explore-codebase` (legacy MCP surface) skills — operating manuals for hosts with skill discovery (alternative to copy-pasting AGENT-GUIDE). See [`skills/README.md`](./skills/README.md). |
|
|
307
306
|
| [`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md) | 7-phase agent-driven verification after indexing your project. |
|
|
308
307
|
| [`docs/CODEBASE_REQUIREMENTS.md`](./docs/CODEBASE_REQUIREMENTS.md) | Assumptions about your Java repo + per-file edit map for non-conforming codebases. |
|
|
309
308
|
| [`docs/PRODUCT-VISION.md`](./docs/PRODUCT-VISION.md) | Long-term product direction. |
|