java-codebase-rag 0.12.0__py3-none-any.whl → 0.12.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- java_codebase_rag-0.12.2.dist-info/METADATA +35 -0
- java_codebase_rag-0.12.2.dist-info/RECORD +4 -0
- {java_codebase_rag-0.12.0.dist-info → java_codebase_rag-0.12.2.dist-info}/WHEEL +1 -1
- java_codebase_rag/_deprecation.py +0 -103
- java_codebase_rag/_fdlimit.py +0 -56
- java_codebase_rag/_stdio.py +0 -32
- java_codebase_rag/_version.py +0 -35
- java_codebase_rag/absence/__init__.py +0 -0
- java_codebase_rag/absence/absence_diagnosis.py +0 -700
- java_codebase_rag/absence/absence_types.py +0 -124
- java_codebase_rag/absence/absence_vocab.py +0 -460
- java_codebase_rag/analysis/__init__.py +0 -0
- java_codebase_rag/analysis/pr_analysis.py +0 -563
- java_codebase_rag/analysis/resolve_service.py +0 -740
- java_codebase_rag/ast/__init__.py +0 -0
- java_codebase_rag/ast/ast_java.py +0 -2847
- java_codebase_rag/ast/ast_kotlin.py +0 -1794
- java_codebase_rag/ast/brownfield_events.py +0 -58
- java_codebase_rag/ast/chunk_heuristics.py +0 -83
- java_codebase_rag/ast/language.py +0 -117
- java_codebase_rag/cli.py +0 -1215
- java_codebase_rag/cli_dispatch.py +0 -251
- java_codebase_rag/cli_format.py +0 -85
- java_codebase_rag/cli_progress.py +0 -94
- java_codebase_rag/config.py +0 -833
- java_codebase_rag/eval/__init__.py +0 -1
- java_codebase_rag/eval/ground_truth.py +0 -100
- java_codebase_rag/eval/metrics.py +0 -107
- java_codebase_rag/eval/runner.py +0 -556
- java_codebase_rag/graph/__init__.py +0 -0
- java_codebase_rag/graph/build_ast_graph.py +0 -4593
- java_codebase_rag/graph/graph_enrich.py +0 -1940
- java_codebase_rag/graph/graph_types.py +0 -224
- java_codebase_rag/graph/java_ontology.py +0 -465
- java_codebase_rag/graph/ladybug_queries.py +0 -2213
- java_codebase_rag/graph/path_filtering.py +0 -509
- java_codebase_rag/index/__init__.py +0 -0
- java_codebase_rag/index/java_index_flow_lancedb.py +0 -879
- java_codebase_rag/index/java_index_v1_common.py +0 -33
- java_codebase_rag/install_data/__init__.py +0 -0
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -110
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
- java_codebase_rag/installer.py +0 -2188
- java_codebase_rag/jrag.py +0 -4545
- java_codebase_rag/jrag_envelope.py +0 -1107
- java_codebase_rag/jrag_hints.py +0 -204
- java_codebase_rag/jrag_render.py +0 -926
- java_codebase_rag/lance_optimize.py +0 -264
- java_codebase_rag/mcp/__init__.py +0 -0
- java_codebase_rag/mcp/mcp_hints.py +0 -932
- java_codebase_rag/mcp/mcp_v2.py +0 -1916
- java_codebase_rag/mcp/server.py +0 -886
- java_codebase_rag/pipeline.py +0 -531
- java_codebase_rag/progress.py +0 -570
- java_codebase_rag/read_payloads.py +0 -781
- java_codebase_rag/search/__init__.py +0 -0
- java_codebase_rag/search/index_common.py +0 -10
- java_codebase_rag/search/search_lancedb.py +0 -1296
- java_codebase_rag/search/search_lexical.py +0 -449
- java_codebase_rag/search/search_scoring.py +0 -537
- java_codebase_rag/watch/__init__.py +0 -0
- java_codebase_rag/watch/client.py +0 -230
- java_codebase_rag/watch/daemon.py +0 -396
- java_codebase_rag/watch/lock.py +0 -201
- java_codebase_rag/watch/paths.py +0 -76
- java_codebase_rag/watch/protocol.py +0 -122
- java_codebase_rag/watch/server.py +0 -273
- java_codebase_rag/watch/warm.py +0 -105
- java_codebase_rag/watch/watcher.py +0 -394
- java_codebase_rag-0.12.0.dist-info/METADATA +0 -340
- java_codebase_rag-0.12.0.dist-info/RECORD +0 -75
- java_codebase_rag-0.12.0.dist-info/entry_points.txt +0 -5
- java_codebase_rag-0.12.0.dist-info/licenses/LICENSE +0 -21
- java_codebase_rag-0.12.0.dist-info/top_level.txt +0 -1
- /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.2.dist-info/top_level.txt +0 -0
java_codebase_rag/watch/warm.py
DELETED
|
@@ -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)
|