java-codebase-rag 0.11.2__py3-none-any.whl → 0.12.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. java_codebase_rag-0.12.1.dist-info/METADATA +35 -0
  2. java_codebase_rag-0.12.1.dist-info/RECORD +4 -0
  3. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.1.dist-info}/WHEEL +1 -1
  4. java_codebase_rag/_fdlimit.py +0 -56
  5. java_codebase_rag/_stdio.py +0 -32
  6. java_codebase_rag/_version.py +0 -35
  7. java_codebase_rag/absence/__init__.py +0 -0
  8. java_codebase_rag/absence/absence_diagnosis.py +0 -700
  9. java_codebase_rag/absence/absence_types.py +0 -124
  10. java_codebase_rag/absence/absence_vocab.py +0 -460
  11. java_codebase_rag/analysis/__init__.py +0 -0
  12. java_codebase_rag/analysis/pr_analysis.py +0 -563
  13. java_codebase_rag/analysis/resolve_service.py +0 -740
  14. java_codebase_rag/ast/__init__.py +0 -0
  15. java_codebase_rag/ast/ast_java.py +0 -2825
  16. java_codebase_rag/ast/brownfield_events.py +0 -58
  17. java_codebase_rag/ast/chunk_heuristics.py +0 -62
  18. java_codebase_rag/cli.py +0 -1215
  19. java_codebase_rag/cli_format.py +0 -85
  20. java_codebase_rag/cli_progress.py +0 -94
  21. java_codebase_rag/config.py +0 -833
  22. java_codebase_rag/eval/__init__.py +0 -1
  23. java_codebase_rag/eval/ground_truth.py +0 -100
  24. java_codebase_rag/eval/metrics.py +0 -107
  25. java_codebase_rag/eval/runner.py +0 -556
  26. java_codebase_rag/graph/__init__.py +0 -0
  27. java_codebase_rag/graph/build_ast_graph.py +0 -4471
  28. java_codebase_rag/graph/graph_enrich.py +0 -1937
  29. java_codebase_rag/graph/graph_types.py +0 -224
  30. java_codebase_rag/graph/java_ontology.py +0 -465
  31. java_codebase_rag/graph/ladybug_queries.py +0 -2213
  32. java_codebase_rag/graph/path_filtering.py +0 -477
  33. java_codebase_rag/index/__init__.py +0 -0
  34. java_codebase_rag/index/java_index_flow_lancedb.py +0 -734
  35. java_codebase_rag/index/java_index_v1_common.py +0 -33
  36. java_codebase_rag/install_data/__init__.py +0 -0
  37. java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -108
  38. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
  39. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
  40. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
  41. java_codebase_rag/installer.py +0 -2188
  42. java_codebase_rag/jrag.py +0 -4531
  43. java_codebase_rag/jrag_envelope.py +0 -1107
  44. java_codebase_rag/jrag_hints.py +0 -204
  45. java_codebase_rag/jrag_render.py +0 -926
  46. java_codebase_rag/lance_optimize.py +0 -264
  47. java_codebase_rag/mcp/__init__.py +0 -0
  48. java_codebase_rag/mcp/mcp_hints.py +0 -932
  49. java_codebase_rag/mcp/mcp_v2.py +0 -1916
  50. java_codebase_rag/mcp/server.py +0 -884
  51. java_codebase_rag/pipeline.py +0 -531
  52. java_codebase_rag/progress.py +0 -570
  53. java_codebase_rag/read_payloads.py +0 -781
  54. java_codebase_rag/search/__init__.py +0 -0
  55. java_codebase_rag/search/index_common.py +0 -10
  56. java_codebase_rag/search/search_lancedb.py +0 -1296
  57. java_codebase_rag/search/search_lexical.py +0 -449
  58. java_codebase_rag/search/search_scoring.py +0 -523
  59. java_codebase_rag/watch/__init__.py +0 -0
  60. java_codebase_rag/watch/client.py +0 -230
  61. java_codebase_rag/watch/daemon.py +0 -396
  62. java_codebase_rag/watch/lock.py +0 -201
  63. java_codebase_rag/watch/paths.py +0 -76
  64. java_codebase_rag/watch/protocol.py +0 -122
  65. java_codebase_rag/watch/server.py +0 -273
  66. java_codebase_rag/watch/warm.py +0 -105
  67. java_codebase_rag/watch/watcher.py +0 -370
  68. java_codebase_rag-0.11.2.dist-info/METADATA +0 -331
  69. java_codebase_rag-0.11.2.dist-info/RECORD +0 -71
  70. java_codebase_rag-0.11.2.dist-info/entry_points.txt +0 -4
  71. java_codebase_rag-0.11.2.dist-info/licenses/LICENSE +0 -21
  72. java_codebase_rag-0.11.2.dist-info/top_level.txt +0 -1
  73. /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.1.dist-info/top_level.txt +0 -0
@@ -1,105 +0,0 @@
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))
@@ -1,370 +0,0 @@
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 (
44
- run_cocoindex_update,
45
- run_incremental_graph,
46
- vector_stack_installed,
47
- )
48
-
49
- if TYPE_CHECKING:
50
- from java_codebase_rag.config import ResolvedOperatorConfig
51
- from java_codebase_rag.watch.warm import WarmResources
52
-
53
- log = logging.getLogger(__name__)
54
-
55
- # Suffixes the watcher treats as java sources. The non-java resource types are
56
- # matched by the glob helpers below (the cocoindex set has no shared iterator).
57
- INDEXED_SUFFIXES: tuple[str, ...] = (".java",)
58
- _YAML_SUFFIXES: tuple[str, ...] = (".yml", ".yaml")
59
-
60
- # Project-relative anchored prefixes for the two non-java resource globs:
61
- # **/src/main/resources/db/migration/*.sql
62
- # **/src/main/resources/application*.yml / .yaml
63
- _SQL_MIGRATION_PREFIX = "src/main/resources/db/migration/"
64
- _RESOURCES_PREFIX = "src/main/resources/"
65
-
66
-
67
- def _is_migration_sql(rel_posix: str) -> bool:
68
- """True iff ``rel_posix`` is a ``.sql`` directly under a ``.../db/migration/`` dir."""
69
- if not rel_posix.endswith(".sql"):
70
- return False
71
- idx = rel_posix.rfind(_SQL_MIGRATION_PREFIX)
72
- if idx == -1:
73
- return False
74
- # The file must sit directly under migration/ (no further path segment).
75
- return "/" not in rel_posix[idx + len(_SQL_MIGRATION_PREFIX):]
76
-
77
-
78
- def _is_application_yaml(rel_posix: str) -> bool:
79
- """True iff ``rel_posix`` is ``application*.yml``/``.yaml`` directly under
80
- a ``.../src/main/resources/`` dir."""
81
- name = rel_posix.rsplit("/", 1)[-1]
82
- if not name.startswith("application"):
83
- return False
84
- if not name.endswith((".yml", ".yaml")):
85
- return False
86
- idx = rel_posix.rfind(_RESOURCES_PREFIX)
87
- if idx == -1:
88
- return False
89
- return "/" not in rel_posix[idx + len(_RESOURCES_PREFIX):]
90
-
91
-
92
- class _ChangeHandler(FileSystemEventHandler):
93
- """Bridge watchdog events to the watcher's classify+schedule path.
94
-
95
- Directory events are ignored. Each file event's path (and, for moves, the
96
- destination) is classified; if it matches an indexed type and survives
97
- ``LayeredIgnore`` its kind is scheduled.
98
- """
99
-
100
- def __init__(self, watcher: "SourceWatcher") -> None:
101
- super().__init__()
102
- self._watcher = watcher
103
-
104
- def on_any_event(self, event) -> None: # type: ignore[override]
105
- if event.is_directory:
106
- return
107
- self._watcher._handle_path(event.src_path)
108
- dest = getattr(event, "dest_path", None)
109
- if dest:
110
- self._watcher._handle_path(dest)
111
-
112
-
113
- class SourceWatcher:
114
- """Watch ``cfg.source_root`` and dispatch debounced per-type reindexes.
115
-
116
- Thread model: watchdog's observer thread calls the handler (which only does a
117
- quick classify + schedule -- never blocks). A single dedicated debounce
118
- worker thread runs ``reindex`` so the observer is never held up by a build.
119
- """
120
-
121
- def __init__(
122
- self,
123
- cfg: "ResolvedOperatorConfig",
124
- warm: "WarmResources",
125
- *,
126
- debounce_ms: int,
127
- backend: str,
128
- poll_interval_ms: int,
129
- on_event: Callable[[str, dict[str, Any]] | None] = None,
130
- ) -> None:
131
- self.cfg = cfg
132
- self.warm = warm
133
- # Probed once: when False (graph-only install — macOS Intel) the vectors
134
- # (cocoindex) reindex step is skipped entirely, so the graph reindex still
135
- # completes and fires ``indexing_done`` instead of bailing on a 127 stub.
136
- self._vector_enabled = vector_stack_installed()
137
- self._debounce_s = max(int(debounce_ms), 1) / 1000.0
138
- self._backend = backend
139
- # watchdog's PollingObserver takes a float timeout in SECONDS (it flows
140
- # straight into ``threading.Event.wait``); a timedelta crashes the emitter.
141
- self._poll_interval_s = max(int(poll_interval_ms), 1) / 1000.0
142
- self._on_event = on_event
143
-
144
- self._ignore = LayeredIgnore(cfg.source_root)
145
- self._source_root_resolved = Path(cfg.source_root).resolve()
146
-
147
- self._observer = self._make_observer()
148
- self._handler = _ChangeHandler(self)
149
-
150
- # Debounce state -- touched by the observer thread and the debounce worker.
151
- self._lock = threading.Lock()
152
- self._pending: set[str] = set()
153
- self._activity = threading.Event()
154
- self._stop = threading.Event()
155
- self._debounce_thread = threading.Thread(
156
- target=self._debounce_loop, name="jrag-watch-debounce", daemon=True
157
- )
158
-
159
- # In-memory reindex state, refreshed after each successful reindex.
160
- self.last_reindex: dict[str, Any] | None = None
161
-
162
- # -- observer backend ----------------------------------------------------
163
-
164
- def _make_observer(self):
165
- if self._backend == "polling":
166
- return PollingObserver(timeout=self._poll_interval_s)
167
- # "watchdog" and "auto" both prefer the native observer; "auto" falls
168
- # back to polling in :meth:`start` if the native backend cannot start.
169
- return Observer()
170
-
171
- # -- lifecycle -----------------------------------------------------------
172
-
173
- def start(self) -> None:
174
- """Schedule the observer on ``cfg.source_root`` (recursive) and start the
175
- debounce loop. Under ``backend="auto"`` a native observer that fails to
176
- start (e.g. on some network filesystems) falls back to polling."""
177
- self._observer.schedule(self._handler, str(self.cfg.source_root), recursive=True)
178
- try:
179
- self._observer.start()
180
- except Exception as exc:
181
- if self._backend != "auto":
182
- raise
183
- self._emit(
184
- "error",
185
- {"phase": "observer", "fallback": "polling", "error": repr(exc)},
186
- )
187
- self._observer = PollingObserver(timeout=self._poll_interval_s)
188
- self._observer.schedule(self._handler, str(self.cfg.source_root), recursive=True)
189
- self._observer.start()
190
- self._debounce_thread.start()
191
-
192
- def stop(self) -> None:
193
- """Stop the observer and the debounce loop; join both."""
194
- self._stop.set()
195
- self._activity.set() # unblock the debounce worker's wait
196
- try:
197
- if self._observer.is_alive():
198
- self._observer.stop()
199
- self._observer.join(timeout=5.0)
200
- except Exception:
201
- log.warning("watchdog observer stop failed", exc_info=True)
202
- if (
203
- self._debounce_thread.is_alive()
204
- and threading.current_thread() is not self._debounce_thread
205
- ):
206
- self._debounce_thread.join(timeout=10.0)
207
-
208
- # -- event classification (observer thread) ------------------------------
209
- #
210
- # The watcher fires on exactly the cocoindex-indexed set (``.java`` plus the
211
- # SQL/YAML resource patterns below) filtered through ``LayeredIgnore``.
212
- # ``target/generated-sources/**/*.java`` therefore correctly fires: generated
213
- # sources are first-class, cocoindex does NOT exclude ``target/``, so the
214
- # watcher must fire to keep them fresh. ``LayeredIgnore.is_ignored`` does NOT
215
- # prune build-output dirs (``target/``/``build``/``out``) -- that pruning lives
216
- # in ``iter_java_source_files``'s ``os.walk`` (``_is_build_output_dir``), used
217
- # by the graph builder, not by cocoindex. (Compiled ``.class`` output under
218
- # ``target/`` doesn't match the ``.java`` suffix anyway.)
219
-
220
- def _handle_path(self, src_path: object) -> None:
221
- """Classify one observed path and schedule its kind if indexed & not ignored."""
222
- kinds = self._classify(Path(str(src_path)))
223
- if kinds:
224
- self._schedule(kinds)
225
-
226
- def _classify(self, path: Path) -> set[str]:
227
- """Return the set of reindex kinds for ``path`` (empty if ignored/unknown).
228
-
229
- ``LayeredIgnore`` wins: an ignored path yields no kind even when its
230
- suffix is ``.java``. Outside-source-root paths also yield nothing.
231
- """
232
- try:
233
- if self._ignore.is_ignored(path):
234
- return set()
235
- except Exception:
236
- return set()
237
- try:
238
- rel = path.resolve().relative_to(self._source_root_resolved).as_posix()
239
- except ValueError:
240
- return set()
241
- suffix = path.suffix.lower()
242
- if suffix == ".java":
243
- return {"java"}
244
- if suffix == ".sql":
245
- return {"sql"} if _is_migration_sql(rel) else set()
246
- if suffix in _YAML_SUFFIXES:
247
- return {"yaml"} if _is_application_yaml(rel) else set()
248
- return set()
249
-
250
- def _schedule(self, kinds: set[str]) -> None:
251
- """Union ``kinds`` into the debounce collector and (re)arm the debounce window."""
252
- if not kinds:
253
- return
254
- with self._lock:
255
- self._pending |= kinds
256
- self._activity.set()
257
-
258
- # -- debounce worker thread ----------------------------------------------
259
-
260
- def _debounce_loop(self) -> None:
261
- """Fire ``reindex`` once per quiet period, coalescing bursts.
262
-
263
- Leading edge: the first event arms a ``debounce_ms`` window. Each further
264
- event during the window re-arms it. When the window expires with no new
265
- events, the accumulated kinds are flushed to ``reindex`` in ONE call.
266
- """
267
- while not self._stop.is_set():
268
- # Wait for the leading edge of a burst.
269
- self._activity.wait()
270
- if self._stop.is_set():
271
- return
272
- # Re-arm while events keep arriving (debounce).
273
- while not self._stop.is_set():
274
- self._activity.clear()
275
- if self._activity.wait(timeout=self._debounce_s):
276
- continue # new activity -> reset the window
277
- break # quiet period elapsed -> fire
278
- if self._stop.is_set():
279
- return
280
- with self._lock:
281
- kinds = self._pending
282
- self._pending = set()
283
- if kinds:
284
- try:
285
- self.reindex(kinds)
286
- except Exception: # noqa: BLE001 -- reindex emits its own error; never kill the worker
287
- log.warning("reindex raised unexpectedly", exc_info=True)
288
-
289
- # -- reindex (runs on the debounce worker thread) ------------------------
290
-
291
- def reindex(self, kinds: set[str]) -> None:
292
- """Run one debounced reindex: vectors always (when installed); graph only when java changed.
293
-
294
- COW lifecycle (design §4.7): the graph subprocess writes the ORIGINAL
295
- graph while reads are served from a sidecar copy. ``begin_graph_snapshot``
296
- is called BEFORE the subprocess and ``commit_graph_snapshot`` ALWAYS
297
- (success OR failure) so a snapshot reader is never left dangling -- on
298
- graph failure the existing ``.graph_increment_in_progress`` crash marker
299
- drives the next full rebuild. Lance needs no snapshot (commits are atomic
300
- per version; fresh per-query reads are fine).
301
-
302
- Graph-only installs (macOS Intel) have no vector stack: the cocoindex
303
- step is skipped (``vres=None``) so the graph reindex completes and fires
304
- ``indexing_done`` instead of bailing on a 127 cocoindex-not-found stub.
305
- """
306
- if not kinds:
307
- return
308
- kind_list = sorted(kinds)
309
- self._emit("indexing_started", {"kinds": kind_list})
310
- try:
311
- # Vectors run for every indexed type (java/sql/yaml all flow through
312
- # cocoindex) — but only when the vector stack is installed. On a
313
- # graph-only install cocoindex is absent and the call would return a
314
- # 127 stub; skipping it keeps ``indexing_done`` reachable.
315
- if self._vector_enabled:
316
- self._emit("vectors", {"kinds": kind_list})
317
- vres = run_cocoindex_update(
318
- self.cfg.subprocess_env(),
319
- full_reprocess=False,
320
- quiet=True,
321
- verbose=False,
322
- )
323
- else:
324
- vres = None
325
-
326
- graph_rc = 0
327
- if "java" in kinds:
328
- # Graph indexes java only; reindex under a COW snapshot so graph
329
- # reads continue (from the sidecar) during the subprocess write.
330
- self._emit("graph", {"kinds": kind_list})
331
- try:
332
- # begin/commit paired in this try/finally. begin_graph_snapshot
333
- # sets ``_snapshot_path`` last, so if it raises commit no-ops;
334
- # moving begin inside the try keeps the pairing locally evident.
335
- self.warm.begin_graph_snapshot()
336
- gres = run_incremental_graph(
337
- source_root=self.cfg.source_root,
338
- ladybug_path=self.cfg.ladybug_path,
339
- verbose=False,
340
- quiet=True,
341
- env=self.cfg.subprocess_env(),
342
- )
343
- graph_rc = gres.returncode
344
- finally:
345
- # ALWAYS drop the snapshot reader -- even on failure -- so it
346
- # is never left dangling. No-ops when no snapshot is active.
347
- self.warm.commit_graph_snapshot()
348
-
349
- if vres is not None and vres.returncode != 0:
350
- self._emit("error", {"phase": "vectors", "returncode": vres.returncode})
351
- return
352
- if graph_rc != 0:
353
- self._emit("error", {"phase": "graph", "returncode": graph_rc})
354
- return
355
-
356
- self.last_reindex = {"time": time.time(), "kinds": kind_list}
357
- self._emit("indexing_done", {"kinds": kind_list})
358
- except Exception as exc: # noqa: BLE001 -- the daemon must survive any reindex error
359
- self._emit("error", {"phase": "reindex", "error": repr(exc)})
360
-
361
- # -- status --------------------------------------------------------------
362
-
363
- def _emit(self, kind: str, detail: dict[str, Any]) -> None:
364
- """Forward a status callback to the UI; never let it crash the watcher."""
365
- if self._on_event is None:
366
- return
367
- try:
368
- self._on_event(kind, detail)
369
- except Exception: # noqa: BLE001 -- a UI callback must not crash the watcher
370
- log.warning("on_event callback raised", exc_info=True)