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,531 +0,0 @@
1
- """Subprocess helpers for cocoindex + graph builder (no heavy ML imports at import time)."""
2
- from __future__ import annotations
3
-
4
- import asyncio
5
- import importlib.util
6
- import os
7
- import shutil
8
- import subprocess
9
- import sys
10
- import threading
11
- import time
12
- from pathlib import Path
13
- from typing import Callable
14
-
15
- from java_codebase_rag.cli_format import is_noise_line
16
- from java_codebase_rag.config import cocoindex_subprocess_env_defaults
17
- from java_codebase_rag.progress import ProgressEvent, ProgressRelay, make_relay
18
-
19
- COCOINDEX_TARGET = "java_index_flow_lancedb.py:JavaCodeIndexLance"
20
-
21
- # Operator-facing line printed when an indexing command skips the vectors phase
22
- # because the vector stack is absent (graph-only install, e.g. macOS Intel where
23
- # cocoindex[lancedb]/lancedb/sentence-transformers are gated off by PEP 508 markers).
24
- # Single source of truth — imported by cli.py (init/increment) and server.py
25
- # (reprocess) so the wording can't drift between the two paths.
26
- VECTORS_SKIPPED_GRAPH_ONLY = (
27
- "jrag: vectors skipped — vector stack not installed on this platform "
28
- "(graph-only mode). The graph is built/refreshed; semantic search is unavailable."
29
- )
30
-
31
-
32
- # Package-internal locations of the cocoindex flow definition and the graph
33
- # builder. Both are executed *by file path* — cocoindex loads the flow via a
34
- # ``file:Class`` target (COCOINDEX_TARGET below) and the builder runs as
35
- # ``python <path>`` — so we must resolve their paths without importing them
36
- # (this module deliberately stays free of heavy ML imports at import time).
37
- # Derived from this file's location so they resolve under both editable and
38
- # wheel installs.
39
- _PKG_DIR = Path(__file__).resolve().parent
40
- _FLOW_DIR = _PKG_DIR / "index"
41
- _FLOW_FILE = _FLOW_DIR / "java_index_flow_lancedb.py"
42
- _BUILDER_FILE = _PKG_DIR / "graph" / "build_ast_graph.py"
43
-
44
-
45
- def cocoindex_bin() -> Path:
46
- candidate = Path(sys.executable).parent / "cocoindex"
47
- if candidate.is_file():
48
- return candidate
49
- found = shutil.which("cocoindex")
50
- if found:
51
- return Path(found)
52
- return candidate
53
-
54
-
55
- class _LineFilter:
56
- """Buffer byte chunks and relay only non-noise lines to stderr."""
57
-
58
- def __init__(self) -> None:
59
- self._buf = bytearray()
60
- self._suppress_next = False
61
-
62
- def feed(self, chunk: bytes) -> None:
63
- self._buf.extend(chunk)
64
- while b"\n" in self._buf:
65
- line, self._buf = self._buf.split(b"\n", 1)
66
- line += b"\n"
67
- noise = is_noise_line(line)
68
- if noise:
69
- self._suppress_next = True
70
- continue
71
- if self._suppress_next and line[:1] in (b" ", b"\t"):
72
- continue
73
- self._suppress_next = False
74
- sys.stderr.buffer.write(line)
75
- sys.stderr.buffer.flush()
76
-
77
- def flush(self) -> None:
78
- if self._buf:
79
- if not is_noise_line(self._buf):
80
- sys.stderr.buffer.write(bytes(self._buf))
81
- sys.stderr.buffer.flush()
82
- self._buf.clear()
83
- self._suppress_next = False
84
-
85
-
86
- def _popen_capturing_stderr(
87
- proc: subprocess.Popen[bytes],
88
- *,
89
- verbose: bool = True,
90
- on_progress: Callable[[ProgressEvent], None] | None = None,
91
- on_progress_console: object | None = None,
92
- ) -> tuple[str, str, int]:
93
- """Capture stdout/stderr; relay stderr through noise filter (or verbatim in verbose mode).
94
-
95
- When ``on_progress`` is set, stderr is drained through a :class:`ProgressRelay`
96
- instead of the bare ``_LineFilter``: progress lines are parsed first, routed to
97
- ``on_progress``, and suppressed from the relay; non-progress lines follow the
98
- relay's routing (``console.print`` while a Live region is up via
99
- ``on_progress_console``, raw ``buffer.write`` in verbose mode).
100
- """
101
- out_buf = bytearray()
102
- err_buf = bytearray()
103
- if on_progress is not None:
104
- relay = make_relay(
105
- on_progress, console=on_progress_console, verbose=verbose
106
- )
107
- filt: _LineFilter | ProgressRelay | None = relay
108
- else:
109
- filt = _LineFilter() if not verbose else None
110
-
111
- def drain_out() -> None:
112
- assert proc.stdout is not None
113
- while True:
114
- chunk = proc.stdout.read(65536)
115
- if not chunk:
116
- break
117
- out_buf.extend(chunk)
118
-
119
- def drain_err() -> None:
120
- assert proc.stderr is not None
121
- while True:
122
- chunk = proc.stderr.read(65536)
123
- if not chunk:
124
- break
125
- err_buf.extend(chunk)
126
- if filt is not None:
127
- filt.feed(chunk)
128
- else:
129
- sys.stderr.buffer.write(chunk)
130
- sys.stderr.buffer.flush()
131
-
132
- t_out = threading.Thread(target=drain_out, name="stream-stdout", daemon=True)
133
- t_err = threading.Thread(target=drain_err, name="stream-stderr", daemon=True)
134
- t_out.start()
135
- t_err.start()
136
- # Wait on the CHILD before joining the drain threads. ``Popen.wait()`` is
137
- # interruptible by Ctrl+C — the underlying ``os.waitpid`` returns EINTR and
138
- # CPython raises ``KeyboardInterrupt`` — whereas ``Thread.join()`` blocks on
139
- # an internal lock whose infinite-timeout acquire CPython never polls for
140
- # signals. Joining *first* therefore made the whole indexing step ignore
141
- # Ctrl+C until the child happened to close its pipes; cocoindex's teardown
142
- # (and any flow-server grandchild it spawned) can hold them open for a long
143
- # time, so an install/reprocess could not be aborted. The daemon drain
144
- # threads keep the child's stdout/stderr pipes empty while we wait, so the
145
- # child never blocks on a full pipe.
146
- try:
147
- code = proc.wait()
148
- except BaseException:
149
- # Ctrl+C or any other abort: best-effort, NON-BLOCKING child teardown
150
- # so a process we spawned does not outlive us, then re-raise WITHOUT
151
- # joining the drain threads. They may be blocked on a pipe the child
152
- # still owns, and a join here would re-introduce the very hang this
153
- # reordering fixes. The threads are daemons, so they vanish at exit.
154
- _abort_child(proc)
155
- raise
156
- # Normal exit: the child is gone, its pipes hit EOF, and the drain threads
157
- # return promptly — safe to join here. (If a pipe-inheriting grandchild
158
- # lingered past the child's exit, these joins would block until it too
159
- # closed its write ends — on Ctrl+C the shared-process-group SIGINT reaches
160
- # it and closes them. This window is the short teardown-after-indexing phase,
161
- # not the long indexing phase the wait-first reorder already made
162
- # interruptible.)
163
- t_out.join()
164
- t_err.join()
165
- if filt is not None:
166
- filt.flush()
167
- return out_buf.decode(errors="replace"), err_buf.decode(errors="replace"), code
168
-
169
-
170
- def _abort_child(proc: subprocess.Popen[bytes]) -> None:
171
- """Best-effort, NON-BLOCKING teardown of a spawned child on an abort path.
172
-
173
- Runs when ``proc.wait()`` raised (Ctrl+C, or any other exception). Sends
174
- SIGTERM and returns immediately — this path exists to let the operator exit
175
- *promptly*, and waiting for the child would defeat that. On Ctrl+C the child
176
- already received SIGINT (same process group); this just guarantees teardown
177
- for non-signal aborts, after which the child finishes shutting down on its
178
- own. ``OSError`` (incl. ``ProcessLookupError`` — already-dead / zombie /
179
- reaped) is swallowed — the only caller re-raises regardless.
180
- """
181
- fn = getattr(proc, "terminate", None)
182
- if fn is None:
183
- return
184
- try:
185
- fn()
186
- except OSError:
187
- pass
188
-
189
-
190
- def run_cocoindex_update(
191
- env: dict[str, str],
192
- *,
193
- full_reprocess: bool,
194
- quiet: bool,
195
- verbose: bool = True,
196
- lance_project_root: Path | None = None,
197
- on_progress: Callable[[ProgressEvent], None] | None = None,
198
- on_progress_console: object | None = None,
199
- ) -> subprocess.CompletedProcess[str]:
200
- if full_reprocess:
201
- # A full reprocess rebuilds every row, so DROP the Lance target tables
202
- # first and let cocoindex recreate them via the fast INSERT path. The
203
- # in-place alternative (cocoindex's bulk-update merge_insert) emits
204
- # ~one deletion-vector + version commit PER matched row — O(rows) of
205
- # tiny file IO that scales to multi-minute hangs on large repos
206
- # (measured ~83s sys time / 3474 deletion files for 3475 chunks on
207
- # Shopizer; drop+recreate is ~3.6s sys / 0 deletions, ~3.7x faster and
208
- # hang-free). Output is identical either way (full recompute); only the
209
- # write path differs. Drop failure is non-fatal — if it somehow fails,
210
- # the update falls back to the slow in-place path. The same fix is
211
- # applied on the async server path (``server.run_refresh_pipeline``).
212
- drop = run_cocoindex_drop(env, quiet=quiet)
213
- if drop.returncode != 0 and not is_cocoindex_preflight_blocker(drop):
214
- print(
215
- "jrag: drop-before-reprocess failed "
216
- f"(exit {drop.returncode}); falling back to in-place update: "
217
- f"{(drop.stderr or '').strip()[:200]}",
218
- file=sys.stderr,
219
- )
220
- result = _run_cocoindex_update_impl(
221
- env,
222
- full_reprocess=full_reprocess,
223
- quiet=quiet,
224
- verbose=verbose,
225
- lance_project_root=lance_project_root,
226
- on_progress=on_progress,
227
- on_progress_console=on_progress_console,
228
- )
229
- # After cocoindex returns exit 0 there are no concurrent writers, so this
230
- # is the safe window to compact the Lance tables. The flow disabled its
231
- # in-flight background optimize (see java_index_flow_lancedb.py), making
232
- # this serialized pass the sole optimizer. Optimize failure does not flip
233
- # the cocoindex CompletedProcess (a successful index is still usable, just
234
- # not compacted); the outcome is logged to stderr only. Thread the
235
- # in-process on_progress so the optimize phase renders via the same
236
- # renderer (the flow cannot emit it — it runs in the child).
237
- if result.returncode == 0:
238
- _maybe_run_serialized_optimize(env, quiet=quiet, on_progress=on_progress)
239
- return result
240
-
241
-
242
- def _maybe_run_serialized_optimize(
243
- env: dict[str, str], *, quiet: bool, on_progress: Callable | None = None
244
- ) -> None:
245
- """Resolve the index dir from *env* and run the serialized Lance optimize.
246
-
247
- The flow's lifespan reads ``JAVA_CODEBASE_RAG_INDEX_DIR`` (set by the CLI /
248
- config.subprocess_env), so it is guaranteed present when cocoindex ran.
249
- If it is somehow absent we skip optimize with a stderr warning rather than
250
- crash — a successful index is still searchable un-compacted.
251
- """
252
- idx_raw = env.get("JAVA_CODEBASE_RAG_INDEX_DIR", "").strip()
253
- if not idx_raw:
254
- print(
255
- "jrag: optimize skipped — JAVA_CODEBASE_RAG_INDEX_DIR "
256
- "not set in subprocess env",
257
- file=sys.stderr,
258
- )
259
- return
260
- try:
261
- from java_codebase_rag.lance_optimize import optimize_lance_tables
262
-
263
- asyncio.run(optimize_lance_tables(Path(idx_raw), quiet=quiet, on_progress=on_progress))
264
- except Exception as exc:
265
- # Never crash the CLI on an optimize failure — surface on stderr only.
266
- print(f"jrag: optimize failed: {exc}", file=sys.stderr)
267
-
268
-
269
- def is_cocoindex_preflight_blocker(proc: subprocess.CompletedProcess[str]) -> bool:
270
- """True when ``run_cocoindex_update`` returned a pre-spawn stub, not a real cocoindex run.
271
-
272
- The stubs are emitted by ``_run_cocoindex_update_impl`` just below:
273
- * returncode 127, args=[exe] -> cocoindex binary not installed (graph-only install,
274
- e.g. macOS Intel where the vector extra is gated off).
275
- * returncode 126, args=[] -> ``java_index_flow_lancedb.py`` missing from the bundle.
276
- A real cocoindex run has ``args`` = the full command list (length > 1), so the
277
- ``len(args) <= 1`` guard distinguishes a stub from a genuine non-zero exit. This is the
278
- single authoritative detector — ``cli.py`` and ``installer.py`` both call it so the
279
- "treat as skip, not failure" decision stays aligned with the stub shapes here.
280
- """
281
- return bool(proc.returncode in (126, 127) and len(getattr(proc, "args", ()) or ()) <= 1)
282
-
283
-
284
- def is_graph_preflight_blocker(proc: subprocess.CompletedProcess[str]) -> bool:
285
- """True when ``run_build_ast_graph`` returned a pre-spawn stub (builder missing)."""
286
- return bool(proc.returncode in (126, 127) and len(getattr(proc, "args", ()) or ()) <= 1)
287
-
288
-
289
- def vector_stack_installed() -> bool:
290
- """True when the optional vector stack (cocoindex/lancedb/sentence-transformers) is importable.
291
-
292
- False on graph-only installs (macOS Intel), where PEP 508 markers exclude the trio.
293
- Used to skip vector-only wizard steps (e.g. embedding-model selection) and to preflight
294
- branching without spawning cocoindex. Probes all three since they are gated together.
295
- """
296
- return all(
297
- importlib.util.find_spec(m) is not None
298
- for m in ("cocoindex", "lancedb", "sentence_transformers")
299
- )
300
-
301
-
302
- def _run_cocoindex_update_impl(
303
- env: dict[str, str],
304
- *,
305
- full_reprocess: bool,
306
- quiet: bool,
307
- verbose: bool = True,
308
- lance_project_root: Path | None = None,
309
- on_progress: Callable[[ProgressEvent], None] | None = None,
310
- on_progress_console: object | None = None,
311
- ) -> subprocess.CompletedProcess[str]:
312
- exe = cocoindex_bin()
313
- if not exe.is_file():
314
- # 127 pre-spawn stub: never mark the vectors task running — emit a
315
- # terminal failed event so the renderer doesn't leave it hung.
316
- if on_progress is not None:
317
- on_progress(ProgressEvent(kind="vectors", phase=None, pass_=None, done=None, total=None, status="failed", elapsed_s=None))
318
- return subprocess.CompletedProcess(
319
- args=[str(exe)],
320
- returncode=127,
321
- stdout="",
322
- stderr=f"cocoindex not found: {exe}",
323
- )
324
- bd = _FLOW_DIR
325
- flow = bd / "java_index_flow_lancedb.py"
326
- if not flow.is_file():
327
- if on_progress is not None:
328
- on_progress(ProgressEvent(kind="vectors", phase=None, pass_=None, done=None, total=None, status="failed", elapsed_s=None))
329
- return subprocess.CompletedProcess(
330
- args=[],
331
- returncode=126,
332
- stdout="",
333
- stderr=f"java_index_flow_lancedb.py not found under {bd}",
334
- )
335
- # Cap CocoIndex concurrency to avoid EMFILE ("too many open files") under
336
- # default OS fd limits. See: https://github.com/HumanBean17/java-codebase-rag/issues/306
337
- env = env.copy()
338
- for _k, _v in cocoindex_subprocess_env_defaults().items():
339
- env.setdefault(_k, _v)
340
- cmd: list[str] = [str(exe), "update", COCOINDEX_TARGET]
341
- if full_reprocess:
342
- cmd.extend(["--full-reprocess", "-f"])
343
- else:
344
- cmd.append("-f")
345
- if quiet:
346
- cmd.append("-q")
347
- return subprocess.run(
348
- cmd,
349
- cwd=str(bd),
350
- env=env,
351
- capture_output=True,
352
- text=True,
353
- )
354
-
355
- t0 = time.perf_counter()
356
- code = -1
357
- out_s, err_s = "", ""
358
- proc: subprocess.Popen[bytes] | None = None
359
- try:
360
- proc = subprocess.Popen(
361
- cmd,
362
- cwd=str(bd),
363
- env=env,
364
- stdout=subprocess.PIPE,
365
- stderr=subprocess.PIPE,
366
- bufsize=0,
367
- )
368
- # Vectors task is marked running only AFTER Popen succeeds — the flow's
369
- # per-file ticks + approximate total stream in from the child via the
370
- # relay (parsed by ProgressRelay, routed to on_progress).
371
- out_s, err_s, code = _popen_capturing_stderr(
372
- proc, verbose=verbose, on_progress=on_progress, on_progress_console=on_progress_console
373
- )
374
- finally:
375
- # The flow cannot emit the terminal vectors event (no "all files done"
376
- # hook in cocoindex flows), so the PARENT emits it here based on the
377
- # cocoindex exit code. This drives clamp-on-completion + the phase
378
- # transition to Optimize. Emitted even on a spawn failure (code stays
379
- # -1 → failed) so the renderer's task never hangs at running.
380
- if on_progress is not None:
381
- elapsed = time.perf_counter() - t0
382
- status = "done" if code == 0 else "failed"
383
- on_progress(
384
- ProgressEvent(
385
- kind="vectors", phase=None, pass_=None, done=None, total=None,
386
- status=status, elapsed_s=elapsed,
387
- )
388
- )
389
- return subprocess.CompletedProcess(args=cmd, returncode=code, stdout=out_s, stderr=err_s)
390
-
391
-
392
- def run_cocoindex_drop(env: dict[str, str], *, quiet: bool) -> subprocess.CompletedProcess[str]:
393
- exe = cocoindex_bin()
394
- if not exe.is_file():
395
- return subprocess.CompletedProcess(
396
- args=[str(exe)],
397
- returncode=127,
398
- stdout="",
399
- stderr=f"cocoindex not found: {exe}",
400
- )
401
- bd = _FLOW_DIR
402
- cmd = [str(exe), "drop", COCOINDEX_TARGET, "-f"]
403
- if quiet:
404
- cmd.append("-q")
405
- return subprocess.run(
406
- cmd,
407
- cwd=str(bd),
408
- env=env,
409
- capture_output=True,
410
- text=True,
411
- )
412
-
413
-
414
- def run_build_ast_graph(
415
- *,
416
- source_root: Path,
417
- ladybug_path: Path,
418
- verbose: bool,
419
- quiet: bool = False,
420
- env: dict[str, str] | None = None,
421
- on_progress: Callable[[ProgressEvent], None] | None = None,
422
- on_progress_console: object | None = None,
423
- ) -> subprocess.CompletedProcess[str]:
424
- builder = _BUILDER_FILE
425
- if not builder.is_file():
426
- return subprocess.CompletedProcess(
427
- args=[],
428
- returncode=126,
429
- stdout="",
430
- stderr=f"build_ast_graph.py not found under {builder.parent}",
431
- )
432
- cmd: list[str] = [
433
- sys.executable,
434
- str(builder),
435
- "--source-root",
436
- str(source_root),
437
- "--ladybug-path",
438
- str(ladybug_path),
439
- ]
440
- # Three-tier: --quiet (silent) / default (filtered progress) / --verbose (raw).
441
- # Default passes --verbose so the builder emits per-pass progress lines,
442
- # which the parent filters via _LineFilter. --verbose bypasses the filter.
443
- if verbose or not quiet:
444
- cmd.append("--verbose")
445
- if quiet:
446
- return subprocess.run(
447
- cmd,
448
- cwd=str(source_root),
449
- env=env or os.environ.copy(),
450
- capture_output=True,
451
- text=True,
452
- )
453
- proc = subprocess.Popen(
454
- cmd,
455
- cwd=str(source_root),
456
- env=env or os.environ.copy(),
457
- stdout=subprocess.PIPE,
458
- stderr=subprocess.PIPE,
459
- bufsize=0,
460
- )
461
- out_s, err_s, code = _popen_capturing_stderr(
462
- proc, verbose=verbose, on_progress=on_progress, on_progress_console=on_progress_console
463
- )
464
- if not verbose:
465
- from java_codebase_rag.cli_format import bold_cyan, styled_check, styled_cross
466
- marker = styled_check() if code == 0 else styled_cross()
467
- print(f"{marker} {bold_cyan('[graph]')} done", file=sys.stderr, flush=True)
468
- return subprocess.CompletedProcess(args=cmd, returncode=code, stdout=out_s, stderr=err_s)
469
-
470
-
471
- def run_incremental_graph(
472
- *,
473
- source_root: Path,
474
- ladybug_path: Path,
475
- verbose: bool,
476
- quiet: bool = False,
477
- env: dict[str, str] | None = None,
478
- on_progress: Callable[[ProgressEvent], None] | None = None,
479
- on_progress_console: object | None = None,
480
- ) -> subprocess.CompletedProcess[str]:
481
- """Run incremental graph rebuild by passing --incremental flag to build_ast_graph.py."""
482
- builder = _BUILDER_FILE
483
- if not builder.is_file():
484
- return subprocess.CompletedProcess(
485
- args=[],
486
- returncode=126,
487
- stdout="",
488
- stderr=f"build_ast_graph.py not found under {builder.parent}",
489
- )
490
- cmd: list[str] = [
491
- sys.executable,
492
- str(builder),
493
- "--source-root",
494
- str(source_root),
495
- "--ladybug-path",
496
- str(ladybug_path),
497
- "--incremental",
498
- ]
499
- # Three-tier: --quiet (silent) / default (filtered progress) / --verbose (raw).
500
- # Default passes --verbose so the builder emits per-pass progress lines,
501
- # which the parent filters via _LineFilter. --verbose bypasses the filter.
502
- if verbose or not quiet:
503
- cmd.append("--verbose")
504
- if quiet:
505
- return subprocess.run(
506
- cmd,
507
- cwd=str(source_root),
508
- env=env or os.environ.copy(),
509
- capture_output=True,
510
- text=True,
511
- )
512
- proc = subprocess.Popen(
513
- cmd,
514
- cwd=str(source_root),
515
- env=env or os.environ.copy(),
516
- stdout=subprocess.PIPE,
517
- stderr=subprocess.PIPE,
518
- bufsize=0,
519
- )
520
- out_s, err_s, code = _popen_capturing_stderr(
521
- proc, verbose=verbose, on_progress=on_progress, on_progress_console=on_progress_console
522
- )
523
- if not verbose:
524
- from java_codebase_rag.cli_format import bold_cyan, styled_check, styled_cross
525
- marker = styled_check() if code == 0 else styled_cross()
526
- print(f"{marker} {bold_cyan('[increment]')} done", file=sys.stderr, flush=True)
527
- return subprocess.CompletedProcess(args=cmd, returncode=code, stdout=out_s, stderr=err_s)
528
-
529
-
530
- def clip(s: str, n: int) -> str:
531
- return s[-n:] if len(s) > n else s