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

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