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/lock.py
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
"""Project-level pidfile + ``fcntl.flock`` mutual exclusion for ``jrag watch``.
|
|
2
|
-
|
|
3
|
-
``ProjectLock`` is the single-writer primitive the watch daemon (Task 11)
|
|
4
|
-
acquires so that at most one watcher — and no concurrent manual
|
|
5
|
-
``jrag increment`` — runs per project.
|
|
6
|
-
|
|
7
|
-
The lock is keyed on the project's index dir: the pidfile path comes from
|
|
8
|
-
``paths.pid_path(index_dir)`` (Task 3). We hold an advisory exclusive flock on
|
|
9
|
-
that file for the lifetime of the ``ProjectLock`` object; the file also carries
|
|
10
|
-
the holder's pid so other tools can report who holds it (``read_holder``).
|
|
11
|
-
|
|
12
|
-
Unix-only by design. The ``fcntl`` import is guarded so that, on a platform
|
|
13
|
-
without it, constructing a ``ProjectLock`` fails cleanly with
|
|
14
|
-
``WatchUnsupportedPlatform`` instead of crashing at import time.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import os
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
try: # Unix-only; absent on Windows.
|
|
21
|
-
import fcntl
|
|
22
|
-
except ImportError: # pragma: no cover - exercised via monkeypatch in tests.
|
|
23
|
-
fcntl = None
|
|
24
|
-
|
|
25
|
-
from .paths import pid_path
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _read_pid_file(path: Path):
|
|
29
|
-
"""Return the integer pid in ``path``, or ``None``.
|
|
30
|
-
|
|
31
|
-
``None`` covers: missing file, empty contents, or non-integer contents.
|
|
32
|
-
"""
|
|
33
|
-
try:
|
|
34
|
-
raw = path.read_text().strip()
|
|
35
|
-
except (FileNotFoundError, OSError):
|
|
36
|
-
return None
|
|
37
|
-
if not raw:
|
|
38
|
-
return None
|
|
39
|
-
try:
|
|
40
|
-
return int(raw)
|
|
41
|
-
except ValueError:
|
|
42
|
-
return None
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class LockHeldError(Exception):
|
|
46
|
-
"""Raised by ``ProjectLock.acquire`` when the project lock is held elsewhere.
|
|
47
|
-
|
|
48
|
-
Attributes:
|
|
49
|
-
pid: the pid currently recorded in the pid file, or ``None`` if it could
|
|
50
|
-
not be read/parsed (e.g. the file was empty).
|
|
51
|
-
path: the pid file ``Path``.
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
def __init__(self, pid, path: Path):
|
|
55
|
-
self.pid = pid
|
|
56
|
-
self.path = path
|
|
57
|
-
super().__init__(f"project lock held by pid={pid!r} at {path}")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class WatchUnsupportedPlatform(Exception):
|
|
61
|
-
"""Raised when the host platform lacks ``fcntl`` (the daemon is Unix-only)."""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class ProjectLock:
|
|
65
|
-
"""Exclusive per-project lock backed by a pidfile + ``fcntl.flock``.
|
|
66
|
-
|
|
67
|
-
The flock is held for the lifetime of the object: ``acquire`` opens the pid
|
|
68
|
-
file and takes ``LOCK_EX | LOCK_NB`` on it, retaining the file handle;
|
|
69
|
-
``release`` closes that handle (which releases the flock) and unlinks the
|
|
70
|
-
pid file if it still records our pid.
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
def __init__(self, index_dir: Path):
|
|
74
|
-
if fcntl is None:
|
|
75
|
-
raise WatchUnsupportedPlatform(
|
|
76
|
-
"jrag watch requires fcntl, which is unavailable on this platform"
|
|
77
|
-
)
|
|
78
|
-
self.index_dir = index_dir
|
|
79
|
-
self.pid_path: Path = pid_path(index_dir)
|
|
80
|
-
self._fh = None # retained file handle while we hold the flock
|
|
81
|
-
|
|
82
|
-
# ------------------------------------------------------------------
|
|
83
|
-
# pid file helpers
|
|
84
|
-
# ------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
def _read_pid(self):
|
|
87
|
-
"""Return the integer pid recorded in our pid file, or ``None``."""
|
|
88
|
-
return _read_pid_file(self.pid_path)
|
|
89
|
-
|
|
90
|
-
# ------------------------------------------------------------------
|
|
91
|
-
# acquire / release
|
|
92
|
-
# ------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
def acquire(self) -> None:
|
|
95
|
-
"""Take the exclusive lock, writing our pid into the pid file.
|
|
96
|
-
|
|
97
|
-
Opens the pid file read/write (creating it if absent), then takes
|
|
98
|
-
``LOCK_EX | LOCK_NB``. If another holder blocks us, raise
|
|
99
|
-
``LockHeldError`` carrying the pid recorded in the file (or ``None`` if
|
|
100
|
-
unreadable). On success, (over)write our pid and keep the handle open.
|
|
101
|
-
"""
|
|
102
|
-
# "r+" preserves an existing holder's pid so we can report it on
|
|
103
|
-
# contention; fall back to "w+" only when the file does not yet exist
|
|
104
|
-
# (so we never truncate a real pid file before reading it).
|
|
105
|
-
try:
|
|
106
|
-
fh = open(self.pid_path, "r+")
|
|
107
|
-
except FileNotFoundError:
|
|
108
|
-
fh = open(self.pid_path, "w+")
|
|
109
|
-
|
|
110
|
-
try:
|
|
111
|
-
fcntl.flock(fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
112
|
-
except BlockingIOError:
|
|
113
|
-
fh.close()
|
|
114
|
-
raise LockHeldError(self._read_pid(), self.pid_path)
|
|
115
|
-
except OSError:
|
|
116
|
-
# Unexpected flock failure — don't leak the handle.
|
|
117
|
-
fh.close()
|
|
118
|
-
raise
|
|
119
|
-
|
|
120
|
-
# We hold the lock: record our pid (overwriting any stale value).
|
|
121
|
-
fh.seek(0)
|
|
122
|
-
fh.truncate()
|
|
123
|
-
fh.write(f"{os.getpid()}\n")
|
|
124
|
-
fh.flush()
|
|
125
|
-
self._fh = fh
|
|
126
|
-
|
|
127
|
-
def release(self) -> None:
|
|
128
|
-
"""Release the flock and, if the pid file still names us, unlink it.
|
|
129
|
-
|
|
130
|
-
Closing the handle releases the flock. We only unlink the pid file when
|
|
131
|
-
its contents still equal our pid, so we never clobber a successor's file
|
|
132
|
-
if they acquired between our close and our unlink.
|
|
133
|
-
"""
|
|
134
|
-
if self._fh is not None:
|
|
135
|
-
try:
|
|
136
|
-
self._fh.close()
|
|
137
|
-
finally:
|
|
138
|
-
self._fh = None
|
|
139
|
-
|
|
140
|
-
if self._read_pid() == os.getpid():
|
|
141
|
-
try:
|
|
142
|
-
self.pid_path.unlink()
|
|
143
|
-
except FileNotFoundError:
|
|
144
|
-
pass
|
|
145
|
-
|
|
146
|
-
# ------------------------------------------------------------------
|
|
147
|
-
# introspection
|
|
148
|
-
# ------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
def is_holder(self) -> bool:
|
|
151
|
-
"""True iff this lock currently holds the project lock.
|
|
152
|
-
|
|
153
|
-
Implementation note on flock semantics: ``flock`` treats separate file
|
|
154
|
-
descriptors even within a single process as independent holders, so a
|
|
155
|
-
probe handle CANNOT acquire ``LOCK_EX | LOCK_NB`` while our retained
|
|
156
|
-
handle holds it. The probe therefore FAILS exactly when the lock is
|
|
157
|
-
held (by us or another). Combined with the pid file recording our own
|
|
158
|
-
pid, that means: probe blocked AND our pid on disk => we are the holder.
|
|
159
|
-
|
|
160
|
-
(The prose in the task brief said the probe would "succeed"; on Unix
|
|
161
|
-
flock the polarity is inverted. See the task-2 report.)
|
|
162
|
-
"""
|
|
163
|
-
try:
|
|
164
|
-
probe = open(self.pid_path, "r+")
|
|
165
|
-
except FileNotFoundError:
|
|
166
|
-
return False
|
|
167
|
-
try:
|
|
168
|
-
try:
|
|
169
|
-
fcntl.flock(probe.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
170
|
-
except BlockingIOError:
|
|
171
|
-
# Held by someone; it is us iff our pid is on disk.
|
|
172
|
-
return self._read_pid() == os.getpid()
|
|
173
|
-
# Probe acquired the lock => it was free => we do not hold it.
|
|
174
|
-
fcntl.flock(probe.fileno(), fcntl.LOCK_UN)
|
|
175
|
-
return False
|
|
176
|
-
finally:
|
|
177
|
-
probe.close()
|
|
178
|
-
|
|
179
|
-
@classmethod
|
|
180
|
-
def read_holder(cls, index_dir: Path):
|
|
181
|
-
"""Return the live holder's pid, or ``None`` if the lock is free/stale.
|
|
182
|
-
|
|
183
|
-
Reads the pid file and returns the recorded pid iff a process with that
|
|
184
|
-
pid is currently alive (``os.kill(pid, 0)``); otherwise ``None`` (file
|
|
185
|
-
missing, unreadable, or naming a dead/stale pid). Does NOT take or test
|
|
186
|
-
the flock, and does not require ``fcntl``.
|
|
187
|
-
"""
|
|
188
|
-
path = pid_path(index_dir)
|
|
189
|
-
pid = _read_pid_file(path)
|
|
190
|
-
if pid is None:
|
|
191
|
-
return None
|
|
192
|
-
try:
|
|
193
|
-
os.kill(pid, 0)
|
|
194
|
-
except ProcessLookupError:
|
|
195
|
-
# No such process => stale.
|
|
196
|
-
return None
|
|
197
|
-
except PermissionError:
|
|
198
|
-
# The pid exists but is not ours to signal — it is alive, just owned
|
|
199
|
-
# by another user, so it is a legitimate holder.
|
|
200
|
-
return pid
|
|
201
|
-
return pid
|
java_codebase_rag/watch/paths.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""Runtime socket/pid/state path derivation for the jrag watch daemon.
|
|
2
|
-
|
|
3
|
-
Pure-path logic only — no sockets, no I/O except mkdir for the runtime dir.
|
|
4
|
-
No dependencies on other watch modules.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import getpass
|
|
8
|
-
import hashlib
|
|
9
|
-
import os
|
|
10
|
-
import sys
|
|
11
|
-
import tempfile
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def runtime_dir() -> Path:
|
|
16
|
-
"""Return the per-user runtime directory, created if missing.
|
|
17
|
-
|
|
18
|
-
Resolution order:
|
|
19
|
-
1. env XDG_RUNTIME_DIR
|
|
20
|
-
2. env TMPDIR
|
|
21
|
-
3. ~/Library/Caches/JragWatch on macOS (sys.platform == "darwin")
|
|
22
|
-
4. tempfile.gettempdir() / f"jrag-watch-{getpass.getuser()}"
|
|
23
|
-
|
|
24
|
-
The directory is created with parents=True, exist_ok=True before returning.
|
|
25
|
-
"""
|
|
26
|
-
# 1. XDG_RUNTIME_DIR
|
|
27
|
-
if "XDG_RUNTIME_DIR" in os.environ:
|
|
28
|
-
rt_dir = Path(os.environ["XDG_RUNTIME_DIR"])
|
|
29
|
-
# 2. TMPDIR
|
|
30
|
-
elif "TMPDIR" in os.environ:
|
|
31
|
-
rt_dir = Path(os.environ["TMPDIR"])
|
|
32
|
-
# 3. macOS-specific path
|
|
33
|
-
elif sys.platform == "darwin":
|
|
34
|
-
home = Path(os.environ.get("HOME") or os.path.expanduser("~"))
|
|
35
|
-
rt_dir = home / "Library" / "Caches" / "JragWatch"
|
|
36
|
-
# 4. Fallback
|
|
37
|
-
else:
|
|
38
|
-
rt_dir = Path(tempfile.gettempdir()) / f"jrag-watch-{getpass.getuser()}"
|
|
39
|
-
|
|
40
|
-
# Create if missing
|
|
41
|
-
rt_dir.mkdir(parents=True, exist_ok=True)
|
|
42
|
-
return rt_dir
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def project_key(index_dir: Path) -> str:
|
|
46
|
-
"""Return the first 12 hex characters of SHA256 of the resolved index_dir path.
|
|
47
|
-
|
|
48
|
-
The path is resolved before hashing so symlinks/relative paths are stable.
|
|
49
|
-
"""
|
|
50
|
-
resolved = str(index_dir.resolve())
|
|
51
|
-
hash_hex = hashlib.sha256(resolved.encode()).hexdigest()
|
|
52
|
-
return hash_hex[:12]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def socket_path(index_dir: Path) -> Path:
|
|
56
|
-
"""Return the Unix socket path for a given index_dir.
|
|
57
|
-
|
|
58
|
-
Format: runtime_dir() / "jrag-watch-{project_key(index_dir)}.sock"
|
|
59
|
-
"""
|
|
60
|
-
return runtime_dir() / f"jrag-watch-{project_key(index_dir)}.sock"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def pid_path(index_dir: Path) -> Path:
|
|
64
|
-
"""Return the pidfile path for a given index_dir.
|
|
65
|
-
|
|
66
|
-
Format: runtime_dir() / "jrag-watch-{project_key(index_dir)}.pid"
|
|
67
|
-
"""
|
|
68
|
-
return runtime_dir() / f"jrag-watch-{project_key(index_dir)}.pid"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def state_path(index_dir: Path) -> Path:
|
|
72
|
-
"""Return the state file path for a given index_dir.
|
|
73
|
-
|
|
74
|
-
Format: runtime_dir() / "jrag-watch-{project_key(index_dir)}.state"
|
|
75
|
-
"""
|
|
76
|
-
return runtime_dir() / f"jrag-watch-{project_key(index_dir)}.state"
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"""IPC request/response contract + NDJSON codec for jrag watch daemon.
|
|
2
|
-
|
|
3
|
-
Pure data + codec logic: dataclasses, protocol version, valid commands,
|
|
4
|
-
encode/decode to newline-delimited JSON, and protocol mismatch exception.
|
|
5
|
-
No sockets, no I/O, no dependency on other watch modules.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
from dataclasses import asdict, dataclass
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
PROTOCOL_VERSION: int = 1
|
|
13
|
-
|
|
14
|
-
VALID_CMDS: frozenset[str] = frozenset({
|
|
15
|
-
"search",
|
|
16
|
-
"find",
|
|
17
|
-
"inspect",
|
|
18
|
-
"callers",
|
|
19
|
-
"callees",
|
|
20
|
-
"flow",
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
# Error kind constants — used as Response.ErrorShape.kind values.
|
|
24
|
-
ERR_UNKNOWN_COMMAND = "unknown_command"
|
|
25
|
-
ERR_BAD_ARGS = "bad_args"
|
|
26
|
-
ERR_BACKEND_ERROR = "backend_error"
|
|
27
|
-
ERR_STALE_INDEX = "stale_index"
|
|
28
|
-
ERR_BUSY = "busy"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class ProtocolMismatch(Exception):
|
|
32
|
-
"""Raised when the protocol version in a request/response doesn't match PROTOCOL_VERSION."""
|
|
33
|
-
|
|
34
|
-
def __init__(self, got: int):
|
|
35
|
-
self.got = got
|
|
36
|
-
super().__init__(f"Protocol version mismatch: expected {PROTOCOL_VERSION}, got {got}")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@dataclass
|
|
40
|
-
class Request:
|
|
41
|
-
"""IPC request from client to daemon."""
|
|
42
|
-
|
|
43
|
-
v: int
|
|
44
|
-
cmd: str
|
|
45
|
-
args: dict[str, Any]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass
|
|
49
|
-
class ErrorShape:
|
|
50
|
-
"""Error detail in a failed Response."""
|
|
51
|
-
|
|
52
|
-
kind: str
|
|
53
|
-
message: str
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@dataclass
|
|
57
|
-
class Response:
|
|
58
|
-
"""IPC response from daemon to client."""
|
|
59
|
-
|
|
60
|
-
v: int
|
|
61
|
-
ok: bool
|
|
62
|
-
result: Any | None = None
|
|
63
|
-
error: ErrorShape | None = None
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def encode_request(r: Request) -> bytes:
|
|
67
|
-
"""Encode a Request to newline-delimited JSON."""
|
|
68
|
-
return (json.dumps(asdict(r), default=str) + "\n").encode("utf-8")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def decode_request(line: bytes) -> Request:
|
|
72
|
-
"""Decode a Request from newline-delimited JSON.
|
|
73
|
-
|
|
74
|
-
Raises:
|
|
75
|
-
ProtocolMismatch: if the protocol version doesn't match
|
|
76
|
-
ValueError: if the command is invalid or line is blank
|
|
77
|
-
"""
|
|
78
|
-
if not line or line.strip() == b"":
|
|
79
|
-
raise ValueError("Blank line")
|
|
80
|
-
|
|
81
|
-
parsed = json.loads(line.decode("utf-8"))
|
|
82
|
-
|
|
83
|
-
if parsed["v"] != PROTOCOL_VERSION:
|
|
84
|
-
raise ProtocolMismatch(got=parsed["v"])
|
|
85
|
-
|
|
86
|
-
if parsed["cmd"] not in VALID_CMDS:
|
|
87
|
-
raise ValueError(f"Unknown command: {parsed['cmd']}")
|
|
88
|
-
|
|
89
|
-
return Request(**parsed)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def encode_response(r: Response) -> bytes:
|
|
93
|
-
"""Encode a Response to newline-delimited JSON."""
|
|
94
|
-
return (json.dumps(asdict(r), default=str) + "\n").encode("utf-8")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def decode_response(line: bytes) -> Response:
|
|
98
|
-
"""Decode a Response from newline-delimited JSON.
|
|
99
|
-
|
|
100
|
-
Raises:
|
|
101
|
-
ProtocolMismatch: if the protocol version doesn't match
|
|
102
|
-
ValueError: if the line is blank
|
|
103
|
-
"""
|
|
104
|
-
if not line or line.strip() == b"":
|
|
105
|
-
raise ValueError("Blank line")
|
|
106
|
-
|
|
107
|
-
parsed = json.loads(line.decode("utf-8"))
|
|
108
|
-
|
|
109
|
-
if parsed["v"] != PROTOCOL_VERSION:
|
|
110
|
-
raise ProtocolMismatch(got=parsed["v"])
|
|
111
|
-
|
|
112
|
-
# Reconstruct ErrorShape if present
|
|
113
|
-
error = None
|
|
114
|
-
if parsed.get("error"):
|
|
115
|
-
error = ErrorShape(**parsed["error"])
|
|
116
|
-
|
|
117
|
-
return Response(
|
|
118
|
-
v=parsed["v"],
|
|
119
|
-
ok=parsed["ok"],
|
|
120
|
-
result=parsed.get("result"),
|
|
121
|
-
error=error,
|
|
122
|
-
)
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
"""Unix-socket server for the ``jrag watch`` daemon.
|
|
2
|
-
|
|
3
|
-
Accepts newline-delimited JSON requests (Task 4's protocol), dispatches each to
|
|
4
|
-
the matching read-command payload core (Task 5), and writes the encoded
|
|
5
|
-
response (Task 4's codec). Transport + dispatch only — the server stores nothing
|
|
6
|
-
between requests except the warm resources and the operator config.
|
|
7
|
-
|
|
8
|
-
Lifecycle (Task 11 wires this into the daemon process):
|
|
9
|
-
|
|
10
|
-
* ``start()`` binds an ``AF_UNIX`` / ``SOCK_STREAM`` socket to
|
|
11
|
-
:func:`paths.socket_path`, ``chmod 0o600``, ``listen(8)``, and spawns an
|
|
12
|
-
accept thread running :meth:`serve`.
|
|
13
|
-
* ``serve()`` is the accept loop. Each connection is handled INLINE (the read
|
|
14
|
-
commands are fast): read newline-delimited bytes, decode, dispatch, encode,
|
|
15
|
-
flush. Multiple pipelined requests on one connection are each answered; a
|
|
16
|
-
connection that closes mid-line is dropped silently.
|
|
17
|
-
* ``shutdown()`` closes the listening socket and joins the accept thread.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import argparse
|
|
23
|
-
import dataclasses
|
|
24
|
-
import logging
|
|
25
|
-
import socket
|
|
26
|
-
import threading
|
|
27
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
28
|
-
|
|
29
|
-
from java_codebase_rag.jrag import _IndexStale
|
|
30
|
-
from java_codebase_rag.read_payloads import (
|
|
31
|
-
callers_payload,
|
|
32
|
-
callees_payload,
|
|
33
|
-
find_payload,
|
|
34
|
-
flow_payload,
|
|
35
|
-
inspect_payload,
|
|
36
|
-
search_payload,
|
|
37
|
-
)
|
|
38
|
-
from java_codebase_rag.watch import paths
|
|
39
|
-
from java_codebase_rag.watch.lock import ProjectLock
|
|
40
|
-
from java_codebase_rag.watch.protocol import (
|
|
41
|
-
ERR_BAD_ARGS,
|
|
42
|
-
ERR_BACKEND_ERROR,
|
|
43
|
-
ERR_STALE_INDEX,
|
|
44
|
-
ERR_UNKNOWN_COMMAND,
|
|
45
|
-
PROTOCOL_VERSION,
|
|
46
|
-
ErrorShape,
|
|
47
|
-
ProtocolMismatch,
|
|
48
|
-
Request,
|
|
49
|
-
Response,
|
|
50
|
-
decode_request,
|
|
51
|
-
encode_response,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
if TYPE_CHECKING:
|
|
55
|
-
# Type-only: the server never constructs these (the daemon passes them in),
|
|
56
|
-
# so they are not needed at import time. Avoids coupling this transport
|
|
57
|
-
# module to the heavy warm-resources/config import chain on minimal installs.
|
|
58
|
-
from java_codebase_rag.config import ResolvedOperatorConfig
|
|
59
|
-
from java_codebase_rag.warm import WarmResources
|
|
60
|
-
|
|
61
|
-
log = logging.getLogger(__name__)
|
|
62
|
-
|
|
63
|
-
# cmd -> payload core (Task 5). ``dispatch`` looks cmds up here; it ALSO defends
|
|
64
|
-
# in depth against an unknown cmd (returns ERR_UNKNOWN_COMMAND) even though
|
|
65
|
-
# ``decode_request`` already rejects anything outside VALID_CMDS — so a direct
|
|
66
|
-
# caller of ``dispatch`` (or a future divergence between the two sets) still
|
|
67
|
-
# gets the right error kind instead of a KeyError.
|
|
68
|
-
PAYLOAD_FNS: dict[str, Callable[[argparse.Namespace, Any, Any], Any]] = {
|
|
69
|
-
"search": search_payload,
|
|
70
|
-
"find": find_payload,
|
|
71
|
-
"inspect": inspect_payload,
|
|
72
|
-
"callers": callers_payload,
|
|
73
|
-
"callees": callees_payload,
|
|
74
|
-
"flow": flow_payload,
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def serialize(payload: Any) -> Any:
|
|
79
|
-
"""Return a JSON-safe representation of a payload core's return value.
|
|
80
|
-
|
|
81
|
-
The cold read path's ``--format json`` emits a rendered *Envelope* via
|
|
82
|
-
``Envelope.to_json()`` (after ``project_envelope``). That requires the
|
|
83
|
-
handler-specific payload->Envelope projection, which lives in the ``jrag``
|
|
84
|
-
read handlers (``_cmd_search`` etc.) and is NOT a separable shared function
|
|
85
|
-
— it is tangled with the terminal-rendering code path. Per the task brief,
|
|
86
|
-
when that path is tangled we serialize the *payload* directly and let the
|
|
87
|
-
client (Task 8) reconstruct + render it locally with the same handler code:
|
|
88
|
-
|
|
89
|
-
* pydantic models -> ``model_dump(mode="json")`` (search/find/inspect
|
|
90
|
-
``*Output`` models, nested ``SymbolHit`` rows, etc.);
|
|
91
|
-
* dict / list -> recursed element-wise;
|
|
92
|
-
* dataclass -> ``dataclasses.asdict``;
|
|
93
|
-
* everything else -> passed through (str/int/float/bool/None already
|
|
94
|
-
JSON-safe).
|
|
95
|
-
|
|
96
|
-
NOTE for Task 8: the daemon ships the serialized *payload*, not the rendered
|
|
97
|
-
envelope. The client must reconstruct the payload object and run the same
|
|
98
|
-
render path the cold ``jrag`` handler runs on it. ``encode_response`` then
|
|
99
|
-
``json.dumps`` this structure, so the values here must already be JSON-safe.
|
|
100
|
-
"""
|
|
101
|
-
if hasattr(payload, "model_dump"):
|
|
102
|
-
return payload.model_dump(mode="json")
|
|
103
|
-
if isinstance(payload, dict):
|
|
104
|
-
return {key: serialize(val) for key, val in payload.items()}
|
|
105
|
-
if isinstance(payload, (list, tuple)):
|
|
106
|
-
return [serialize(item) for item in payload]
|
|
107
|
-
if dataclasses.is_dataclass(payload) and not isinstance(payload, type):
|
|
108
|
-
return dataclasses.asdict(payload)
|
|
109
|
-
return payload
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class WatchServer:
|
|
113
|
-
"""AF_UNIX socket server dispatching NDJSON requests to payload cores."""
|
|
114
|
-
|
|
115
|
-
def __init__(self, warm: "WarmResources", cfg: "ResolvedOperatorConfig") -> None:
|
|
116
|
-
self.warm = warm
|
|
117
|
-
self.cfg = cfg
|
|
118
|
-
self._sock: socket.socket | None = None
|
|
119
|
-
self._thread: threading.Thread | None = None
|
|
120
|
-
self._stopping = threading.Event()
|
|
121
|
-
|
|
122
|
-
# -- lifecycle ----------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
def start(self) -> None:
|
|
125
|
-
"""Bind the Unix socket (chmod 0o600), listen, spawn the accept thread."""
|
|
126
|
-
sock_path = paths.socket_path(self.cfg.index_dir)
|
|
127
|
-
# Unlink a stale socket ONLY when no live daemon holds the project lock
|
|
128
|
-
# (a live holder means another daemon owns this path — leave it alone).
|
|
129
|
-
if sock_path.exists() and ProjectLock.read_holder(self.cfg.index_dir) is None:
|
|
130
|
-
try:
|
|
131
|
-
sock_path.unlink()
|
|
132
|
-
except OSError:
|
|
133
|
-
log.warning("Could not unlink stale socket %s", sock_path, exc_info=True)
|
|
134
|
-
|
|
135
|
-
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
136
|
-
sock.bind(str(sock_path))
|
|
137
|
-
try:
|
|
138
|
-
sock_path.chmod(0o600)
|
|
139
|
-
except OSError:
|
|
140
|
-
log.warning("Could not chmod socket %s to 0o600", sock_path, exc_info=True)
|
|
141
|
-
sock.listen(8)
|
|
142
|
-
self._sock = sock
|
|
143
|
-
|
|
144
|
-
self._stopping.clear()
|
|
145
|
-
self._thread = threading.Thread(
|
|
146
|
-
target=self.serve, name="jrag-watch-accept", daemon=True
|
|
147
|
-
)
|
|
148
|
-
self._thread.start()
|
|
149
|
-
|
|
150
|
-
def serve(self) -> None:
|
|
151
|
-
"""Accept loop: one inline connection at a time (commands are fast)."""
|
|
152
|
-
sock = self._sock
|
|
153
|
-
if sock is None:
|
|
154
|
-
return
|
|
155
|
-
while not self._stopping.is_set():
|
|
156
|
-
try:
|
|
157
|
-
conn, _ = sock.accept()
|
|
158
|
-
except OSError:
|
|
159
|
-
# Listening socket closed (shutdown) -> exit cleanly. Any other
|
|
160
|
-
# OSError here is transient; the loop retries while not stopping.
|
|
161
|
-
if self._stopping.is_set() or self._sock is None:
|
|
162
|
-
break
|
|
163
|
-
log.debug("accept() failed; retrying", exc_info=True)
|
|
164
|
-
continue
|
|
165
|
-
try:
|
|
166
|
-
self._handle(conn)
|
|
167
|
-
except Exception: # noqa: BLE001 — a handler crash must not kill the loop
|
|
168
|
-
log.exception("Unhandled error servicing watch connection")
|
|
169
|
-
finally:
|
|
170
|
-
try:
|
|
171
|
-
conn.close()
|
|
172
|
-
except OSError:
|
|
173
|
-
pass
|
|
174
|
-
|
|
175
|
-
def shutdown(self) -> None:
|
|
176
|
-
"""Close the listening socket and join the accept thread."""
|
|
177
|
-
self._stopping.set()
|
|
178
|
-
if self._sock is not None:
|
|
179
|
-
try:
|
|
180
|
-
self._sock.close()
|
|
181
|
-
except OSError:
|
|
182
|
-
pass
|
|
183
|
-
self._sock = None
|
|
184
|
-
thread = self._thread
|
|
185
|
-
if thread is not None and thread is not threading.current_thread():
|
|
186
|
-
thread.join(timeout=2.0)
|
|
187
|
-
self._thread = None
|
|
188
|
-
|
|
189
|
-
# -- per-connection handling -------------------------------------------
|
|
190
|
-
|
|
191
|
-
def _handle(self, conn: socket.socket) -> None:
|
|
192
|
-
"""Read newline-delimited requests, dispatch, write responses.
|
|
193
|
-
|
|
194
|
-
Loops so multiple pipelined requests on one connection are each
|
|
195
|
-
answered. A peer that closes mid-line (a partial buffer with no
|
|
196
|
-
trailing newline when recv returns empty) is dropped silently.
|
|
197
|
-
"""
|
|
198
|
-
buffer = b""
|
|
199
|
-
while True:
|
|
200
|
-
# Answer every complete line currently buffered.
|
|
201
|
-
while b"\n" in buffer:
|
|
202
|
-
line, _, buffer = buffer.partition(b"\n")
|
|
203
|
-
response = self._respond_to_line(line)
|
|
204
|
-
try:
|
|
205
|
-
conn.sendall(encode_response(response))
|
|
206
|
-
except OSError:
|
|
207
|
-
return # peer gone — stop servicing this connection
|
|
208
|
-
# Need more bytes.
|
|
209
|
-
try:
|
|
210
|
-
chunk = conn.recv(4096)
|
|
211
|
-
except OSError:
|
|
212
|
-
return
|
|
213
|
-
if not chunk:
|
|
214
|
-
return # connection closed; any trailing partial line is dropped
|
|
215
|
-
buffer += chunk
|
|
216
|
-
|
|
217
|
-
def _respond_to_line(self, line: bytes) -> Response:
|
|
218
|
-
"""Decode one request line and dispatch it, mapping decode errors."""
|
|
219
|
-
try:
|
|
220
|
-
req = decode_request(line)
|
|
221
|
-
except (ProtocolMismatch, ValueError) as exc:
|
|
222
|
-
# ProtocolMismatch (wrong v) and ValueError (blank line / unknown
|
|
223
|
-
# cmd as decode sees it) both surface to the client as bad_args.
|
|
224
|
-
return Response(
|
|
225
|
-
v=PROTOCOL_VERSION,
|
|
226
|
-
ok=False,
|
|
227
|
-
error=ErrorShape(ERR_BAD_ARGS, str(exc)),
|
|
228
|
-
)
|
|
229
|
-
return self.dispatch(req, self.warm, self.cfg)
|
|
230
|
-
|
|
231
|
-
# -- dispatch -----------------------------------------------------------
|
|
232
|
-
|
|
233
|
-
def dispatch(self, req: Request, warm: "WarmResources", cfg: Any) -> Response:
|
|
234
|
-
"""Map a decoded :class:`Request` to a :class:`Response` via the cmd core.
|
|
235
|
-
|
|
236
|
-
Rebuilds ``args = argparse.Namespace(**req.args)`` (the client sends the
|
|
237
|
-
full parsed-namespace dict; keys are argparse ``dest`` names) and calls
|
|
238
|
-
``<cmd>_payload(args, cfg, warm.graph())``. Error mapping (pinned
|
|
239
|
-
contract): ``_IndexStale`` -> ``stale_index``;
|
|
240
|
-
``(ProtocolMismatch, ValueError)`` -> ``bad_args``; any other
|
|
241
|
-
``Exception`` (incl. :class:`PayloadError`) -> ``backend_error``;
|
|
242
|
-
unknown cmd -> ``unknown_command`` (defense in depth).
|
|
243
|
-
"""
|
|
244
|
-
payload_fn = PAYLOAD_FNS.get(req.cmd)
|
|
245
|
-
if payload_fn is None:
|
|
246
|
-
return Response(
|
|
247
|
-
v=PROTOCOL_VERSION,
|
|
248
|
-
ok=False,
|
|
249
|
-
error=ErrorShape(ERR_UNKNOWN_COMMAND, f"Unknown command: {req.cmd}"),
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
args = argparse.Namespace(**req.args)
|
|
253
|
-
try:
|
|
254
|
-
payload = payload_fn(args, cfg, warm.graph())
|
|
255
|
-
except _IndexStale as exc:
|
|
256
|
-
return Response(
|
|
257
|
-
v=PROTOCOL_VERSION,
|
|
258
|
-
ok=False,
|
|
259
|
-
error=ErrorShape(ERR_STALE_INDEX, str(exc)),
|
|
260
|
-
)
|
|
261
|
-
except (ProtocolMismatch, ValueError) as exc:
|
|
262
|
-
return Response(
|
|
263
|
-
v=PROTOCOL_VERSION,
|
|
264
|
-
ok=False,
|
|
265
|
-
error=ErrorShape(ERR_BAD_ARGS, str(exc)),
|
|
266
|
-
)
|
|
267
|
-
except Exception as exc: # noqa: BLE001 — includes PayloadError; -> backend_error
|
|
268
|
-
return Response(
|
|
269
|
-
v=PROTOCOL_VERSION,
|
|
270
|
-
ok=False,
|
|
271
|
-
error=ErrorShape(ERR_BACKEND_ERROR, str(exc)),
|
|
272
|
-
)
|
|
273
|
-
return Response(v=PROTOCOL_VERSION, ok=True, result=serialize(payload))
|