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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. java_codebase_rag-0.12.1.dist-info/METADATA +35 -0
  2. java_codebase_rag-0.12.1.dist-info/RECORD +4 -0
  3. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.1.dist-info}/WHEEL +1 -1
  4. java_codebase_rag/_fdlimit.py +0 -56
  5. java_codebase_rag/_stdio.py +0 -32
  6. java_codebase_rag/_version.py +0 -35
  7. java_codebase_rag/absence/__init__.py +0 -0
  8. java_codebase_rag/absence/absence_diagnosis.py +0 -700
  9. java_codebase_rag/absence/absence_types.py +0 -124
  10. java_codebase_rag/absence/absence_vocab.py +0 -460
  11. java_codebase_rag/analysis/__init__.py +0 -0
  12. java_codebase_rag/analysis/pr_analysis.py +0 -563
  13. java_codebase_rag/analysis/resolve_service.py +0 -740
  14. java_codebase_rag/ast/__init__.py +0 -0
  15. java_codebase_rag/ast/ast_java.py +0 -2825
  16. java_codebase_rag/ast/brownfield_events.py +0 -58
  17. java_codebase_rag/ast/chunk_heuristics.py +0 -62
  18. java_codebase_rag/cli.py +0 -1215
  19. java_codebase_rag/cli_format.py +0 -85
  20. java_codebase_rag/cli_progress.py +0 -94
  21. java_codebase_rag/config.py +0 -833
  22. java_codebase_rag/eval/__init__.py +0 -1
  23. java_codebase_rag/eval/ground_truth.py +0 -100
  24. java_codebase_rag/eval/metrics.py +0 -107
  25. java_codebase_rag/eval/runner.py +0 -556
  26. java_codebase_rag/graph/__init__.py +0 -0
  27. java_codebase_rag/graph/build_ast_graph.py +0 -4471
  28. java_codebase_rag/graph/graph_enrich.py +0 -1937
  29. java_codebase_rag/graph/graph_types.py +0 -224
  30. java_codebase_rag/graph/java_ontology.py +0 -465
  31. java_codebase_rag/graph/ladybug_queries.py +0 -2213
  32. java_codebase_rag/graph/path_filtering.py +0 -477
  33. java_codebase_rag/index/__init__.py +0 -0
  34. java_codebase_rag/index/java_index_flow_lancedb.py +0 -734
  35. java_codebase_rag/index/java_index_v1_common.py +0 -33
  36. java_codebase_rag/install_data/__init__.py +0 -0
  37. java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -108
  38. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
  39. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
  40. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
  41. java_codebase_rag/installer.py +0 -2188
  42. java_codebase_rag/jrag.py +0 -4531
  43. java_codebase_rag/jrag_envelope.py +0 -1107
  44. java_codebase_rag/jrag_hints.py +0 -204
  45. java_codebase_rag/jrag_render.py +0 -926
  46. java_codebase_rag/lance_optimize.py +0 -264
  47. java_codebase_rag/mcp/__init__.py +0 -0
  48. java_codebase_rag/mcp/mcp_hints.py +0 -932
  49. java_codebase_rag/mcp/mcp_v2.py +0 -1916
  50. java_codebase_rag/mcp/server.py +0 -884
  51. java_codebase_rag/pipeline.py +0 -531
  52. java_codebase_rag/progress.py +0 -570
  53. java_codebase_rag/read_payloads.py +0 -781
  54. java_codebase_rag/search/__init__.py +0 -0
  55. java_codebase_rag/search/index_common.py +0 -10
  56. java_codebase_rag/search/search_lancedb.py +0 -1296
  57. java_codebase_rag/search/search_lexical.py +0 -449
  58. java_codebase_rag/search/search_scoring.py +0 -523
  59. java_codebase_rag/watch/__init__.py +0 -0
  60. java_codebase_rag/watch/client.py +0 -230
  61. java_codebase_rag/watch/daemon.py +0 -396
  62. java_codebase_rag/watch/lock.py +0 -201
  63. java_codebase_rag/watch/paths.py +0 -76
  64. java_codebase_rag/watch/protocol.py +0 -122
  65. java_codebase_rag/watch/server.py +0 -273
  66. java_codebase_rag/watch/warm.py +0 -105
  67. java_codebase_rag/watch/watcher.py +0 -370
  68. java_codebase_rag-0.11.2.dist-info/METADATA +0 -331
  69. java_codebase_rag-0.11.2.dist-info/RECORD +0 -71
  70. java_codebase_rag-0.11.2.dist-info/entry_points.txt +0 -4
  71. java_codebase_rag-0.11.2.dist-info/licenses/LICENSE +0 -21
  72. java_codebase_rag-0.11.2.dist-info/top_level.txt +0 -1
  73. /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.1.dist-info/top_level.txt +0 -0
java_codebase_rag/cli.py DELETED
@@ -1,1215 +0,0 @@
1
- from __future__ import annotations
2
-
3
- # Heavy imports (`server`, `pr_analysis`, `path_filtering.LayeredIgnore`,
4
- # `build_ast_graph`) stay lazy inside handlers so `java-codebase-rag --help` stays fast.
5
-
6
- import argparse
7
- import asyncio
8
- import json
9
- import os
10
- import pprint
11
- import shutil
12
- import sys
13
- import time
14
- from pathlib import Path
15
- from typing import Any, Callable
16
-
17
- from java_codebase_rag.config import (
18
- OPERATOR_OWNED_INDEX_FILES,
19
- ResolvedOperatorConfig,
20
- describe_path_sizes,
21
- emit_legacy_env_hints_if_present,
22
- emit_legacy_yaml_hint_if_needed,
23
- index_dir_has_existing_artifacts,
24
- resolve_operator_config,
25
- write_config_source_pointer,
26
- )
27
- from java_codebase_rag._fdlimit import raise_fd_limit
28
- from java_codebase_rag._version import version_string
29
- from java_codebase_rag.pipeline import (
30
- VECTORS_SKIPPED_GRAPH_ONLY as _VECTORS_SKIPPED_GRAPH_ONLY,
31
- clip,
32
- is_cocoindex_preflight_blocker,
33
- is_graph_preflight_blocker,
34
- run_build_ast_graph,
35
- run_cocoindex_drop,
36
- run_cocoindex_update,
37
- run_incremental_graph,
38
- )
39
- from java_codebase_rag.graph.java_ontology import VALID_UNRESOLVED_CALL_REASONS
40
-
41
- LADYBUG_INCREMENTAL_TRACKING_ISSUE_URL = "https://github.com/HumanBean17/java-codebase-rag/issues/73"
42
-
43
- _INCREMENT_WARNING_LINES = (
44
- "WARNING: AST graph (LadybugDB) incremental rebuild is not yet implemented.",
45
- "The graph reflects the index state from the last `init` or `reprocess`,",
46
- "which means `find`, `neighbors`, and `describe` may return stale results",
47
- "for files changed since then.",
48
- "",
49
- "Lance vector index has been updated incrementally and is current.",
50
- "",
51
- "For an up-to-date graph, run:",
52
- " java-codebase-rag reprocess",
53
- "",
54
- "Track progress on LadybugDB incremental rebuild:",
55
- f" {LADYBUG_INCREMENTAL_TRACKING_ISSUE_URL}",
56
- )
57
-
58
- _REFRESH_DEPRECATION = (
59
- "WARN: 'refresh' is deprecated; use 'reprocess'. "
60
- "This alias will be removed in the next release."
61
- )
62
-
63
- _REPROCESS_DRIFT_VECTORS_ONLY = (
64
- "java-codebase-rag reprocess: rebuilt vectors only; graph (code_graph.lbug) was NOT rebuilt "
65
- "and may now reflect a stale source snapshot."
66
- )
67
-
68
-
69
- def _reprocess_drift_graph_only_line(index_dir: Path) -> str:
70
- return (
71
- "java-codebase-rag reprocess: rebuilt graph only; vectors (Lance tables under "
72
- f"{index_dir}) were NOT rebuilt and may now reflect a stale source snapshot."
73
- )
74
-
75
-
76
- def _reprocess_exit_code(payload: dict[str, Any]) -> int:
77
- if payload.get("success"):
78
- return 0
79
- phases_run = payload.get("phases_run") or []
80
- if not phases_run:
81
- return 2
82
- return 1
83
-
84
-
85
- # Preflight detection delegates to pipeline.is_cocoindex_preflight_blocker /
86
- # is_graph_preflight_blocker, which are co-located with the stub CompletedProcess shapes
87
- # they must match (missing cocoindex / flow / build_ast_graph.py).
88
- def _is_cocoindex_preflight_blocker(coco: Any) -> bool:
89
- """True when ``run_cocoindex_update`` returned without spawning cocoindex."""
90
- return is_cocoindex_preflight_blocker(coco)
91
-
92
-
93
- def _is_graph_preflight_blocker(g: Any) -> bool:
94
- """True when ``run_build_ast_graph`` returned without spawning the builder."""
95
- return is_graph_preflight_blocker(g)
96
-
97
-
98
- def _emit_reprocess_selective_tty(*, mode: str) -> None:
99
- if mode == "vectors":
100
- print("Rebuilt: vectors")
101
- print("Skipped: graph (use `java-codebase-rag reprocess --graph-only` or `reprocess` to refresh)")
102
- else:
103
- print("Rebuilt: graph")
104
- print("Skipped: vectors (use `java-codebase-rag reprocess --vectors-only` or `reprocess` to refresh)")
105
-
106
-
107
- def _reprocess_success_message(mode: str | None, payload: dict[str, Any]) -> str:
108
- """Concise message for a successful reprocess.
109
-
110
- Prefers an explicit ``message`` set by the pipeline (e.g. the graph-only-
111
- install note from ``run_refresh_pipeline``); otherwise derives from the
112
- selective mode. Mirrors ``init``/``increment``, which emit a one-line
113
- success payload rather than re-dumping captured subprocess logs.
114
- """
115
- explicit = payload.get("message")
116
- if isinstance(explicit, str) and explicit:
117
- return explicit
118
- if mode == "vectors":
119
- return "reprocess completed (vectors only; graph not rebuilt)"
120
- if mode == "graph":
121
- return "reprocess completed (graph only; vectors not rebuilt)"
122
- return "reprocess completed"
123
-
124
-
125
- def _emit_reprocess_outcome(payload: dict[str, Any], *, selective_tty_mode: str | None = None) -> None:
126
- if not payload.get("success"):
127
- # Failure: emit the full payload so captured subprocess stdout/stderr
128
- # (cocoindex/graph logs) survive for debugging.
129
- _emit(payload)
130
- return
131
- # Success: the progress renderer / stderr already surfaced the phase logs,
132
- # so emit a concise structured payload (matching init/increment) instead of
133
- # re-dumping them as JSON noise. In a TTY the partial modes additionally
134
- # print a one-line Rebuilt/Skipped summary.
135
- if selective_tty_mode and sys.stdout.isatty():
136
- _emit_reprocess_selective_tty(mode=selective_tty_mode)
137
- return
138
- _emit({"success": True, "message": _reprocess_success_message(selective_tty_mode, payload)})
139
-
140
-
141
- _PIPELINE_SEP = "\u00b7"
142
-
143
-
144
- def _pipeline_header(subcommand: str, cfg: ResolvedOperatorConfig) -> None:
145
- from java_codebase_rag.cli_format import bold
146
-
147
- root = cfg.source_root.resolve()
148
- idx = cfg.index_dir.resolve()
149
- print(
150
- bold(f"java-codebase-rag {subcommand} {_PIPELINE_SEP} source={root} {_PIPELINE_SEP} index={idx}"),
151
- file=sys.stderr,
152
- flush=True,
153
- )
154
-
155
-
156
- def _pipeline_footer(subcommand: str, started: float, exit_code: int) -> None:
157
- from java_codebase_rag.cli_format import bold, styled_check, styled_cross
158
-
159
- elapsed = time.perf_counter() - started
160
- marker = styled_check() if exit_code == 0 else styled_cross()
161
- print(
162
- f"{marker} {bold(f'java-codebase-rag {subcommand} {_PIPELINE_SEP} finished in {elapsed:.2f}s')}"
163
- + (f" (exit={exit_code})" if exit_code != 0 else ""),
164
- file=sys.stderr,
165
- flush=True,
166
- )
167
-
168
-
169
- # Subcommands that build/refresh an index and therefore should record which YAML
170
- # was used (so a later discovery run from a sibling/cwd can relocate it).
171
- _CONFIG_SOURCE_RECORDING_SUBCOMMANDS = frozenset({"init", "increment", "reprocess"})
172
-
173
-
174
- def _maybe_record_config_source(
175
- subcommand: str, cfg: ResolvedOperatorConfig, code: int
176
- ) -> None:
177
- """On a successful index build, remember the YAML path inside the index dir."""
178
- if code == 0 and subcommand in _CONFIG_SOURCE_RECORDING_SUBCOMMANDS:
179
- write_config_source_pointer(
180
- index_dir=cfg.index_dir, yaml_config_path=cfg.yaml_config_path
181
- )
182
-
183
-
184
- def _run_with_pipeline_progress(
185
- subcommand: str,
186
- cfg: ResolvedOperatorConfig,
187
- *,
188
- quiet: bool,
189
- verbose: bool = False,
190
- work: Callable[["PipelineProgress | None"], int],
191
- ) -> int:
192
- """Run ``work`` under the unified progress renderer (default TTY mode only).
193
-
194
- ``work`` receives a :class:`PipelineProgress` whose ``on_progress`` callback
195
- should be forwarded to the graph/vectors pipeline helpers so their
196
- ``JCIRAG_PROGRESS`` events feed the renderer. In ``--quiet`` or ``--verbose``
197
- mode the context is ``None`` (no Live region: quiet is silent, verbose
198
- raw-relays subprocess output).
199
- """
200
- if quiet or verbose:
201
- code = int(work(None))
202
- _maybe_record_config_source(subcommand, cfg, code)
203
- return code
204
- from java_codebase_rag.progress import build_index_progress_context
205
-
206
- # PR-3 owns all three tasks in order: Vectors → Optimize → Graph. The vectors
207
- # task is fed by the cocoindex child's per-file ticks + approximate total
208
- # (subprocess transport, parsed by ProgressRelay); the optimize task is fed
209
- # in-process by lance_optimize; the graph task is fed by the build_ast_graph
210
- # child (subprocess transport). A task only becomes visible/running once its
211
- # first event arrives.
212
- renderer, on_progress, console = build_index_progress_context()
213
- progress = PipelineProgress(renderer=renderer)
214
- progress.on_progress = on_progress
215
- progress.console = console
216
-
217
- _pipeline_header(subcommand, cfg)
218
- t0 = time.perf_counter()
219
- code = 0
220
- # start() always flips _started (the non-TTY fallback is a no-op for Live but
221
- # still needs the flag so apply() routes to the concise-line printer). The
222
- # TTY Live region is entered inside start() only when the console is a TTY.
223
- renderer.start()
224
- try:
225
- code = int(work(progress))
226
- return code
227
- except BaseException as exc:
228
- # Keep footer aligned with process outcome (main maps unhandled Exception -> exit 2).
229
- if isinstance(exc, SystemExit):
230
- c = exc.code
231
- if isinstance(c, int):
232
- code = c
233
- elif c in (None, False):
234
- code = 0
235
- else:
236
- code = 1
237
- elif code == 0:
238
- code = 2
239
- raise
240
- finally:
241
- renderer.stop()
242
- _pipeline_footer(subcommand, t0, code)
243
- _maybe_record_config_source(subcommand, cfg, code)
244
-
245
-
246
- class PipelineProgress:
247
- """Progress context handed to ``work``: the renderer + a ready ``on_progress``.
248
-
249
- ``on_progress``/``console`` are wired by :func:`_run_with_pipeline_progress`
250
- and should be forwarded to the pipeline helpers' ``on_progress`` /
251
- ``on_progress_console`` parameters. ``console`` is the renderer's stderr
252
- ``rich.Console`` so the subprocess drain routes non-progress lines through
253
- ``console.print`` while the Live region is up (single-writer invariant).
254
- """
255
-
256
- def __init__(self, *, renderer: "object | None") -> None:
257
- self.renderer = renderer
258
- self.on_progress: "Callable | None" = None
259
- self.console: "object | None" = None
260
-
261
-
262
- def _jsonable(value: Any) -> Any:
263
- if hasattr(value, "model_dump"):
264
- return value.model_dump()
265
- if isinstance(value, Path):
266
- return str(value)
267
- raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
268
-
269
-
270
- def _to_payload(value: Any) -> Any:
271
- if hasattr(value, "model_dump"):
272
- return value.model_dump()
273
- return value
274
-
275
-
276
- def _emit(value: Any) -> None:
277
- payload = _to_payload(value)
278
- if sys.stdout.isatty():
279
- print(pprint.pformat(payload, sort_dicts=True))
280
- return
281
- print(json.dumps(payload, default=_jsonable, sort_keys=True, indent=None))
282
-
283
-
284
- def _emit_increment_ladybug_warning() -> None:
285
- for line in _INCREMENT_WARNING_LINES:
286
- print(line, file=sys.stderr)
287
-
288
-
289
- def _parse_source_root(ns: argparse.Namespace) -> Path | None:
290
- if ns.source_root:
291
- return Path(ns.source_root).expanduser().resolve()
292
- return None
293
-
294
-
295
- def _resolved_from_ns(ns: argparse.Namespace) -> ResolvedOperatorConfig:
296
- root = _parse_source_root(ns)
297
- return resolve_operator_config(
298
- source_root=root,
299
- cli_index_dir=ns.index_dir,
300
- cli_embedding_model=getattr(ns, "embedding_model", None),
301
- cli_embedding_device=getattr(ns, "embedding_device", None),
302
- )
303
-
304
-
305
- def _startup_hints(cfg: ResolvedOperatorConfig) -> None:
306
- emit_legacy_env_hints_if_present()
307
- emit_legacy_yaml_hint_if_needed(cfg.source_root)
308
-
309
-
310
- def _add_index_embedding_flags(p: argparse.ArgumentParser) -> None:
311
- p.add_argument("--source-root", type=str, default=None, help="Java repository root (default: cwd)")
312
- p.add_argument("--index-dir", type=str, default=None, help="Index directory (Lance + LadybugDB + cocoindex state)")
313
- p.add_argument("--embedding-model", type=str, default=None, help="Override SBERT_MODEL / YAML embedding.model")
314
- p.add_argument("--embedding-device", type=str, default=None, help="Override SBERT_DEVICE / YAML embedding.device")
315
-
316
-
317
- def _add_verbosity_flags(p: argparse.ArgumentParser) -> None:
318
- g = p.add_mutually_exclusive_group()
319
- g.add_argument(
320
- "--quiet", "-q",
321
- action="store_true",
322
- dest="quiet",
323
- help="Suppress stderr progress relay; stdout payload unchanged.",
324
- )
325
- g.add_argument(
326
- "--verbose", "-v",
327
- action="store_true",
328
- dest="verbose",
329
- help="Show full subprocess output (Lance warnings, brownfield events, progress bars).",
330
- )
331
-
332
-
333
- def _cmd_init(args: argparse.Namespace) -> int:
334
- cfg = _resolved_from_ns(args)
335
- # Check for parent config or index
336
- from java_codebase_rag.config import discover_project_root, find_yaml_config_file
337
- parent_config_dir = discover_project_root(cfg.source_root.parent)
338
- if parent_config_dir is not None:
339
- parent_config = find_yaml_config_file(parent_config_dir)
340
- if parent_config is not None:
341
- print(
342
- f"Warning: found existing config at {parent_config}. "
343
- f"Creating a new project here will create a separate index.",
344
- file=sys.stderr,
345
- )
346
- else:
347
- print(
348
- f"Warning: found existing index at {parent_config_dir / '.java-codebase-rag'}. "
349
- f"Creating a new project here will create a separate index.",
350
- file=sys.stderr,
351
- )
352
- _startup_hints(cfg)
353
- cfg.apply_to_os_environ()
354
- occupied, paths = index_dir_has_existing_artifacts(cfg.index_dir)
355
- if occupied:
356
- _emit(
357
- {
358
- "success": False,
359
- "message": (
360
- "init refused: index paths already exist. "
361
- "Use `java-codebase-rag reprocess` to rebuild in place, "
362
- "or `java-codebase-rag erase --yes` then `init` for a clean slate."
363
- ),
364
- "non_empty_paths": paths,
365
- }
366
- )
367
- return 2
368
- cfg.index_dir.mkdir(parents=True, exist_ok=True)
369
-
370
- def work(progress: "PipelineProgress | None") -> int:
371
- env = cfg.subprocess_env()
372
- verbose = bool(args.verbose)
373
- coco = run_cocoindex_update(
374
- env,
375
- full_reprocess=False,
376
- quiet=bool(args.quiet),
377
- verbose=verbose,
378
- lance_project_root=None if args.quiet else cfg.source_root,
379
- on_progress=progress.on_progress if progress is not None else None,
380
- on_progress_console=progress.console if progress is not None else None,
381
- )
382
- # Graph-only install (cocoindex absent, e.g. macOS Intel): skip the vectors phase
383
- # and proceed to the graph build rather than failing — the graph layer is the
384
- # supported surface there. A genuine non-zero cocoindex exit still fails.
385
- vectors_skipped = _is_cocoindex_preflight_blocker(coco)
386
- if coco.returncode != 0 and not vectors_skipped:
387
- _emit(
388
- {
389
- "success": False,
390
- "exit_code": coco.returncode,
391
- "stdout": clip(coco.stdout, 8000),
392
- "stderr": clip(coco.stderr, 8000),
393
- "message": f"cocoindex exit {coco.returncode}",
394
- }
395
- )
396
- return 1
397
- if vectors_skipped:
398
- print(_VECTORS_SKIPPED_GRAPH_ONLY, file=sys.stderr, flush=True)
399
- if not args.quiet:
400
- print(file=sys.stderr, flush=True)
401
- g = run_build_ast_graph(
402
- source_root=cfg.source_root,
403
- ladybug_path=cfg.ladybug_path,
404
- verbose=verbose,
405
- quiet=bool(args.quiet),
406
- env=env,
407
- on_progress=progress.on_progress if progress is not None else None,
408
- on_progress_console=progress.console if progress is not None else None,
409
- )
410
- if g.returncode != 0:
411
- _emit(
412
- {
413
- "success": False,
414
- "exit_code": g.returncode,
415
- "stdout": clip(g.stdout, 4000),
416
- "stderr": clip(g.stderr, 4000),
417
- "message": f"graph builder exit {g.returncode}",
418
- }
419
- )
420
- return 1
421
- _emit(
422
- {
423
- "success": True,
424
- "message": (
425
- "init completed (graph-only; vectors skipped — vector stack not installed)"
426
- if vectors_skipped
427
- else "init completed"
428
- ),
429
- }
430
- )
431
- return 0
432
-
433
- return _run_with_pipeline_progress(
434
- "init", cfg, quiet=bool(args.quiet), verbose=bool(args.verbose), work=work
435
- )
436
-
437
-
438
- def _cmd_increment(args: argparse.Namespace) -> int:
439
- cfg = _resolved_from_ns(args)
440
- _startup_hints(cfg)
441
- cfg.apply_to_os_environ()
442
-
443
- # Check for --vectors-only flag
444
- vectors_only = bool(getattr(args, "vectors_only", False))
445
- if vectors_only:
446
- _emit_increment_ladybug_warning()
447
-
448
- def work(progress: "PipelineProgress | None") -> int:
449
- env = cfg.subprocess_env()
450
- coco = run_cocoindex_update(
451
- env,
452
- full_reprocess=False,
453
- quiet=bool(args.quiet),
454
- verbose=bool(args.verbose),
455
- lance_project_root=None if args.quiet else cfg.source_root,
456
- on_progress=progress.on_progress if progress is not None else None,
457
- on_progress_console=progress.console if progress is not None else None,
458
- )
459
- vectors_skipped = _is_cocoindex_preflight_blocker(coco)
460
- if coco.returncode != 0 and not vectors_skipped:
461
- _emit(
462
- {
463
- "success": False,
464
- "exit_code": coco.returncode,
465
- "stdout": clip(coco.stdout, 8000),
466
- "stderr": clip(coco.stderr, 8000),
467
- "message": f"cocoindex exit {coco.returncode}",
468
- }
469
- )
470
- return 1
471
- if vectors_skipped:
472
- print(_VECTORS_SKIPPED_GRAPH_ONLY, file=sys.stderr, flush=True)
473
-
474
- # If --vectors-only is set, skip graph update
475
- if vectors_only:
476
- if vectors_skipped:
477
- _emit(
478
- {
479
- "success": True,
480
- "message": "increment skipped: vector stack not installed (graph-only mode)",
481
- }
482
- )
483
- return 0
484
- _emit({"success": True, "message": "increment completed (Lance only; graph may be stale — see stderr)"})
485
- return 0
486
-
487
- # Run incremental graph update
488
- g = run_incremental_graph(
489
- source_root=cfg.source_root,
490
- ladybug_path=cfg.ladybug_path,
491
- verbose=bool(args.verbose),
492
- quiet=bool(args.quiet),
493
- env=env,
494
- on_progress=progress.on_progress if progress is not None else None,
495
- on_progress_console=progress.console if progress is not None else None,
496
- )
497
-
498
- # Check if incremental fell back to full rebuild
499
- if g.returncode == 0 and g.stdout:
500
- # Parse stdout to check for full_fallback mode
501
- # The incremental_rebuild function returns a JSON payload with mode field
502
- try:
503
- result = json.loads(g.stdout.strip())
504
- if result.get("mode") == "full_fallback":
505
- print(
506
- "[increment] fell back to full graph rebuild — this is normal after schema changes or first run",
507
- file=sys.stderr,
508
- flush=True,
509
- )
510
- except (json.JSONDecodeError, ValueError):
511
- # If parsing fails, continue silently
512
- pass
513
-
514
- if g.returncode != 0:
515
- _emit(
516
- {
517
- "success": False,
518
- "exit_code": g.returncode,
519
- "stdout": clip(g.stdout, 4000),
520
- "stderr": clip(g.stderr, 4000),
521
- "message": f"graph builder exit {g.returncode}",
522
- }
523
- )
524
- return 1
525
-
526
- _emit(
527
- {
528
- "success": True,
529
- "message": (
530
- "increment completed (graph only; vectors skipped — vector stack not installed)"
531
- if vectors_skipped
532
- else "increment completed (Lance + graph updated)"
533
- ),
534
- }
535
- )
536
- return 0
537
-
538
- return _run_with_pipeline_progress(
539
- "increment", cfg, quiet=bool(args.quiet), verbose=bool(args.verbose), work=work
540
- )
541
-
542
-
543
- def _cmd_reprocess(args: argparse.Namespace) -> int:
544
- cfg = _resolved_from_ns(args)
545
- _startup_hints(cfg)
546
- cfg.apply_to_os_environ()
547
-
548
- def work(progress: "PipelineProgress | None") -> int:
549
- env = cfg.subprocess_env()
550
- verbose = bool(args.verbose)
551
- vectors_only = bool(getattr(args, "vectors_only", False))
552
- graph_only = bool(getattr(args, "graph_only", False))
553
-
554
- if vectors_only:
555
- coco = run_cocoindex_update(
556
- env, full_reprocess=True, quiet=bool(args.quiet), verbose=verbose,
557
- on_progress=progress.on_progress if progress is not None else None,
558
- on_progress_console=progress.console if progress is not None else None,
559
- )
560
- if _is_cocoindex_preflight_blocker(coco):
561
- payload: dict[str, Any] = {
562
- "success": False,
563
- "exit_code": None,
564
- "stdout": clip(coco.stdout, 8000),
565
- "stderr": clip(coco.stderr, 8000),
566
- "message": coco.stderr.strip() or f"cocoindex setup exit {coco.returncode}",
567
- "graph_exit_code": None,
568
- "graph_stdout": "",
569
- "graph_stderr": "",
570
- "phases_run": [],
571
- }
572
- _emit_reprocess_outcome(payload)
573
- return _reprocess_exit_code(payload)
574
- ok = coco.returncode == 0
575
- payload = {
576
- "success": ok,
577
- "exit_code": coco.returncode,
578
- "stdout": clip(coco.stdout, 8000),
579
- "stderr": clip(coco.stderr, 8000),
580
- "message": None if ok else f"cocoindex exit {coco.returncode}",
581
- "graph_exit_code": None,
582
- "graph_stdout": "",
583
- "graph_stderr": "",
584
- "phases_run": ["vectors"],
585
- }
586
- if ok:
587
- print(_REPROCESS_DRIFT_VECTORS_ONLY, file=sys.stderr)
588
- _emit_reprocess_outcome(payload, selective_tty_mode="vectors" if ok else None)
589
- return _reprocess_exit_code(payload)
590
-
591
- if graph_only:
592
- g = run_build_ast_graph(
593
- source_root=cfg.source_root,
594
- ladybug_path=cfg.ladybug_path,
595
- verbose=verbose,
596
- quiet=bool(args.quiet),
597
- env=env,
598
- on_progress=progress.on_progress if progress is not None else None,
599
- on_progress_console=progress.console if progress is not None else None,
600
- )
601
- if _is_graph_preflight_blocker(g):
602
- payload = {
603
- "success": False,
604
- "exit_code": None,
605
- "stdout": "",
606
- "stderr": "",
607
- "message": g.stderr.strip() or f"graph builder setup exit {g.returncode}",
608
- "graph_exit_code": None,
609
- "graph_stdout": clip(g.stdout, 4000),
610
- "graph_stderr": clip(g.stderr, 4000),
611
- "phases_run": [],
612
- }
613
- _emit_reprocess_outcome(payload)
614
- return _reprocess_exit_code(payload)
615
- ok = g.returncode == 0
616
- payload = {
617
- "success": ok,
618
- "exit_code": None,
619
- "stdout": "",
620
- "stderr": "",
621
- "message": None if ok else f"graph builder exit {g.returncode}",
622
- "graph_exit_code": g.returncode,
623
- "graph_stdout": clip(g.stdout, 4000),
624
- "graph_stderr": clip(g.stderr, 4000),
625
- "phases_run": ["graph"],
626
- }
627
- if ok:
628
- print(_reprocess_drift_graph_only_line(cfg.index_dir), file=sys.stderr)
629
- _emit_reprocess_outcome(payload, selective_tty_mode="graph" if ok else None)
630
- return _reprocess_exit_code(payload)
631
-
632
- from java_codebase_rag.mcp import server # lazy: pulls sentence_transformers/torch/lancedb/ladybug
633
-
634
- result = asyncio.run(
635
- server.run_refresh_pipeline(
636
- quiet=bool(args.quiet),
637
- verbose=verbose,
638
- on_progress=progress.on_progress if progress is not None else None,
639
- on_progress_console=progress.console if progress is not None else None,
640
- )
641
- )
642
- payload = result.model_dump()
643
- _emit_reprocess_outcome(payload)
644
- return _reprocess_exit_code(payload)
645
-
646
- return _run_with_pipeline_progress(
647
- "reprocess", cfg, quiet=bool(args.quiet), verbose=bool(args.verbose), work=work
648
- )
649
-
650
-
651
- def _cmd_install(args: argparse.Namespace) -> int:
652
- from java_codebase_rag.installer import run_install
653
-
654
- return run_install(
655
- non_interactive=bool(args.non_interactive),
656
- agents=args.agent, # list of str (may be empty)
657
- scope=args.scope,
658
- model=args.model,
659
- surface=args.surface,
660
- source_root=None, # None means cwd; installer confirms interactively
661
- quiet=bool(args.quiet),
662
- verbose=bool(args.verbose),
663
- )
664
-
665
-
666
- def _cmd_update(args: argparse.Namespace) -> int:
667
- from java_codebase_rag.installer import run_update
668
-
669
- return run_update(
670
- force=bool(args.force),
671
- dry_run=bool(args.dry_run),
672
- quiet=bool(args.quiet),
673
- verbose=bool(args.verbose),
674
- surface=args.surface,
675
- )
676
-
677
-
678
- def _rm_any(path: Path) -> None:
679
- """Remove ``path`` whether it is a regular file, directory, or symlink.
680
-
681
- ``code_graph.lbug`` is a single regular file in this repo, but kuzu may lay
682
- the graph out as a directory; ``cocoindex.db`` is always a directory.
683
- ``shutil.rmtree`` is a silent no-op on a regular file and ``Path.unlink``
684
- raises ``IsADirectoryError`` on a directory, so a type-blind delete left
685
- index artifacts on disk (issue #346). A symlinked directory is unlinked, not
686
- recursed into, so the link target is never followed. Failures are warned to
687
- stderr rather than swallowed, so erase does not report success while leaving
688
- an artifact behind (the exact failure mode issue #346 reported).
689
- """
690
- try:
691
- if path.is_dir() and not path.is_symlink():
692
- shutil.rmtree(path)
693
- elif path.exists() or path.is_symlink():
694
- path.unlink(missing_ok=True)
695
- except OSError as exc:
696
- print(f"warning: failed to remove {path}: {exc}", file=sys.stderr)
697
-
698
-
699
- def _cmd_erase(args: argparse.Namespace) -> int:
700
- cfg = _resolved_from_ns(args)
701
- _startup_hints(cfg)
702
- cfg.apply_to_os_environ()
703
- # Lazy import: build_ast_graph transitively pulls numpy/ladybug/pyarrow/
704
- # tree_sitter (~54ms), and these filenames are only needed on the erase path.
705
- # Keeping it out of the top-level import lets `java-codebase-rag --help` (and
706
- # every other command) stay fast -- see the lazy-import invariant atop this file.
707
- from java_codebase_rag.graph.build_ast_graph import BUILDER_OWNED_INDEX_FILES
708
- builder_paths = [cfg.ladybug_path.parent / name for name in BUILDER_OWNED_INDEX_FILES]
709
- operator_paths = [cfg.index_dir / name for name in OPERATOR_OWNED_INDEX_FILES]
710
- to_describe: list[Path] = [
711
- cfg.ladybug_path,
712
- cfg.cocoindex_db,
713
- *builder_paths,
714
- *operator_paths,
715
- ]
716
- if cfg.index_dir.is_dir():
717
- try:
718
- import lancedb
719
-
720
- db = lancedb.connect(str(cfg.index_dir.resolve()))
721
- for name in db.list_tables():
722
- to_describe.append(cfg.index_dir / name)
723
- except Exception:
724
- pass
725
- rows = describe_path_sizes(to_describe)
726
- summary_lines = [f" {p}: {sz} bytes" for p, sz in rows] or [" (nothing to delete under resolved index dir)"]
727
- print("Will delete:", file=sys.stderr)
728
- print("\n".join(summary_lines), file=sys.stderr)
729
- if not args.yes:
730
- if not sys.stdin.isatty():
731
- print(
732
- "java-codebase-rag erase: non-interactive stdin; pass --yes to confirm.",
733
- file=sys.stderr,
734
- )
735
- return 2
736
- try:
737
- ans = input("Delete these paths? [y/N]: ").strip().lower()
738
- except EOFError:
739
- # Non-interactive stdin that nonetheless reported isatty() == True
740
- # (the Windows NUL device is a character device, so isatty() lies).
741
- # Treat it as a refusal instead of crashing with an EOF traceback.
742
- print(
743
- "java-codebase-rag erase: non-interactive stdin; pass --yes to confirm.",
744
- file=sys.stderr,
745
- )
746
- return 2
747
- if ans not in ("y", "yes"):
748
- print("Aborted.", file=sys.stderr)
749
- return 2
750
-
751
- def work(progress: "PipelineProgress | None") -> int:
752
- env = cfg.subprocess_env()
753
- drop = run_cocoindex_drop(env, quiet=bool(args.quiet))
754
- if drop.returncode == 127:
755
- print(
756
- "java-codebase-rag erase: cocoindex CLI not found next to this Python; "
757
- "skipped `cocoindex drop` — cocoindex.db (if any) was not removed by CocoIndex.",
758
- file=sys.stderr,
759
- )
760
- elif drop.returncode != 0:
761
- print(clip(drop.stderr, 4000), file=sys.stderr)
762
- # Remove the LadybugDB graph, the cocoindex state store, and every
763
- # builder-owned bookkeeping file next to code_graph.lbug (the content-hash
764
- # store, its atomic-write temp, and the incremental crash marker). Each is
765
- # removed by type (see _rm_any): code_graph.lbug is a file here but may be
766
- # a dir under kuzu, while cocoindex.db is a directory — a type-blind delete
767
- # silently no-oped on one or the other, and the builder files were never
768
- # targeted at all (issues #346 / #349 / #350). The list comes from
769
- # build_ast_graph.BUILDER_OWNED_INDEX_FILES so erase and the builder cannot drift.
770
- _rm_any(cfg.ladybug_path)
771
- _rm_any(cfg.cocoindex_db)
772
- for builder_path in builder_paths:
773
- _rm_any(builder_path)
774
- # Operator-owned index files (the config_source pointer recording which
775
- # YAML built this index) — owned by the CLI/installer, not the graph
776
- # builder, so removed here rather than via BUILDER_OWNED_INDEX_FILES.
777
- for operator_path in operator_paths:
778
- _rm_any(operator_path)
779
- if cfg.index_dir.is_dir():
780
- try:
781
- import lancedb
782
-
783
- db = lancedb.connect(str(cfg.index_dir.resolve()))
784
- for name in list(db.list_tables()):
785
- try:
786
- db.drop_table(name)
787
- except Exception as exc:
788
- print(f"warning: failed to drop Lance table {name!r}: {exc}", file=sys.stderr)
789
- except Exception:
790
- pass
791
- _emit({"success": True, "message": "erase completed"})
792
- return 0
793
-
794
- return _run_with_pipeline_progress("erase", cfg, quiet=bool(args.quiet), verbose=bool(getattr(args, "verbose", False)), work=work)
795
-
796
-
797
- def _cmd_meta(args: argparse.Namespace) -> int:
798
- from java_codebase_rag.mcp import server # lazy
799
-
800
- cfg = _resolved_from_ns(args)
801
- _startup_hints(cfg)
802
- cfg.apply_to_os_environ()
803
- from java_codebase_rag.graph.ladybug_queries import LadybugGraph # lazy
804
-
805
- LadybugGraph._instance = None
806
- LadybugGraph._instance_path = None
807
- payload = server._graph_meta_output().model_dump()
808
- payload["embedding_model"] = cfg.embedding_model
809
- payload["embedding_device"] = cfg.embedding_device
810
- payload["embedding_model_source"] = cfg.embedding_model_source
811
- payload["embedding_device_source"] = cfg.embedding_device_source
812
- payload["index_dir"] = str(cfg.index_dir.resolve())
813
- payload["ladybug_path"] = str(cfg.ladybug_path.resolve())
814
- payload["index_dir_source"] = cfg.index_dir_source
815
- payload["hints_enabled"] = cfg.hints_enabled
816
- payload["hints_enabled_source"] = cfg.hints_enabled_source
817
- _emit(payload)
818
- return 0 if payload.get("success") else 2
819
-
820
-
821
- def _cmd_tables(args: argparse.Namespace) -> int:
822
- from java_codebase_rag.mcp import server # lazy
823
-
824
- cfg = _resolved_from_ns(args)
825
- _startup_hints(cfg)
826
- cfg.apply_to_os_environ()
827
- payload = server.list_code_index_tables_payload().model_dump()
828
- _emit(payload)
829
- return 0
830
-
831
-
832
- def _cmd_diagnose_ignore(args: argparse.Namespace) -> int:
833
- from java_codebase_rag.mcp import server # lazy
834
- from java_codebase_rag.graph.path_filtering import LayeredIgnore # lazy
835
-
836
- cfg = _resolved_from_ns(args)
837
- _startup_hints(cfg)
838
- cfg.apply_to_os_environ()
839
- root = server._project_root()
840
- raw = Path(args.path)
841
- try:
842
- abs_path = raw.resolve() if raw.is_absolute() else (root / raw).resolve()
843
- except OSError as exc:
844
- _emit({"success": False, "message": f"Invalid path: {exc}"})
845
- return 1
846
- li = LayeredIgnore(root)
847
- _emit(li.diagnose_dict(abs_path))
848
- return 0
849
-
850
-
851
- def _read_diff_text(args: argparse.Namespace) -> str:
852
- if args.diff_file:
853
- return Path(args.diff_file).read_text(encoding="utf-8")
854
- if args.diff_stdin:
855
- return sys.stdin.read()
856
- raise ValueError("Provide exactly one of --diff-file or --diff-stdin")
857
-
858
-
859
- def _cmd_unresolved_calls_list(args: argparse.Namespace) -> int:
860
- cfg = _resolved_from_ns(args)
861
- _startup_hints(cfg)
862
- cfg.apply_to_os_environ()
863
- from java_codebase_rag.graph.ladybug_queries import LadybugGraph # lazy
864
-
865
- if not LadybugGraph.exists():
866
- _emit({"success": False, "message": "LadybugDB graph not found"})
867
- return 1
868
- graph = LadybugGraph.get()
869
- rows = graph.list_unresolved_call_sites(
870
- method_id=args.method_id,
871
- reason=args.reason,
872
- microservice=args.microservice,
873
- callee_simple=args.callee_simple,
874
- limit=int(args.limit),
875
- )
876
- # Drop the raw caller symbol id: the row already carries the agent-facing
877
- # ``caller_fqn``, so ``caller_id`` is redundant noise in an operator-facing
878
- # report. The call-site ``id`` (``ucs:``) is kept — it's each site's
879
- # primary key, not a caller reference.
880
- sites = [
881
- {k: v for k, v in row.items() if k != "caller_id"}
882
- for row in rows
883
- ]
884
- _emit({"success": True, "count": len(sites), "sites": sites})
885
- return 0
886
-
887
-
888
- def _cmd_unresolved_calls_stats(args: argparse.Namespace) -> int:
889
- cfg = _resolved_from_ns(args)
890
- _startup_hints(cfg)
891
- cfg.apply_to_os_environ()
892
- from java_codebase_rag.graph.ladybug_queries import LadybugGraph # lazy
893
-
894
- if not LadybugGraph.exists():
895
- _emit({"success": False, "message": "LadybugDB graph not found"})
896
- return 1
897
- graph = LadybugGraph.get()
898
- buckets = graph.stats_unresolved_call_sites(by=args.by)
899
- total = sum(int(r.get("n") or 0) for r in buckets)
900
- _emit({"success": True, "total": total, "by": args.by, "buckets": buckets})
901
- return 0
902
-
903
-
904
- def _cmd_analyze_pr(args: argparse.Namespace) -> int:
905
- cfg = _resolved_from_ns(args)
906
- _startup_hints(cfg)
907
- cfg.apply_to_os_environ()
908
- try:
909
- diff_text = _read_diff_text(args)
910
- except Exception as exc:
911
- _emit({"success": False, "message": str(exc)})
912
- return 1
913
- if not diff_text.strip():
914
- _emit({"success": False, "message": "Diff is empty"})
915
- return 1
916
- from java_codebase_rag.analysis import pr_analysis # lazy
917
- from java_codebase_rag.graph.ladybug_queries import LadybugGraph # lazy
918
-
919
- if not LadybugGraph.exists():
920
- _emit({"success": False, "message": "LadybugDB graph not found"})
921
- return 1
922
- graph = LadybugGraph.get()
923
- report = pr_analysis.analyze_pr_pipeline(graph, diff_text)
924
- _emit(pr_analysis.pr_report_to_dict(report))
925
- return 0
926
-
927
-
928
- def build_parser() -> argparse.ArgumentParser:
929
- description = (
930
- "java-codebase-rag — graph-native code intelligence for Java microservices.\n\n"
931
- "Lifecycle commands stream subprocess progress to stderr (including relayed child stdout); "
932
- "--quiet suppresses that stream; stdout remains the machine-readable payload.\n\n"
933
- "Lifecycle (manage the index):\n"
934
- " init Create a fresh index from a Java repository.\n"
935
- " increment Pick up changes since the last index update (Lance + graph).\n"
936
- " reprocess Full vector + graph rebuild (default); optional --vectors-only / --graph-only.\n"
937
- " erase Delete the index from disk.\n\n"
938
- "Introspection (inspect the index):\n"
939
- " meta Print ontology version, edge counts, and table summary.\n"
940
- " tables List Lance tables and row counts.\n"
941
- " diagnose-ignore Show which ignore-pattern layer decided a path's fate.\n"
942
- " unresolved-calls List or aggregate receiver-failure call sites (not in CALLS).\n\n"
943
- "Analysis (work with code changes):\n"
944
- " analyze-pr Compute blast-radius + risk score for a unified diff.\n\n"
945
- "Run `java-codebase-rag <command> --help` for command-specific options."
946
- )
947
- parser = argparse.ArgumentParser(
948
- prog="java-codebase-rag",
949
- description=description,
950
- formatter_class=argparse.RawDescriptionHelpFormatter,
951
- exit_on_error=False,
952
- )
953
- parser.add_argument(
954
- "--version",
955
- action="version",
956
- version=version_string(parser.prog),
957
- )
958
- subparsers = parser.add_subparsers(dest="subcommand")
959
-
960
- init = subparsers.add_parser(
961
- "init",
962
- help="Create a fresh index from a Java repository.",
963
- description=(
964
- "First-time index creation. Refuses if the resolved index directory "
965
- "already contains a LadybugDB graph or Lance tables. Exit 2 on refusal."
966
- ),
967
- )
968
- _add_index_embedding_flags(init)
969
- _add_verbosity_flags(init)
970
- init.set_defaults(handler=_cmd_init)
971
-
972
- install = subparsers.add_parser(
973
- "install",
974
- help="Interactive setup wizard: config, MCP registration, skill/agent deployment, indexing.",
975
- description=(
976
- "Interactive setup wizard that guides users through: Java source detection, "
977
- "embedding model selection, agent host configuration, artifact deployment, "
978
- "and YAML config generation. Use --non-interactive for CI/automation."
979
- ),
980
- )
981
- install.add_argument(
982
- "--non-interactive",
983
- action="store_true",
984
- help="Run without prompts (requires --agent).",
985
- )
986
- install.add_argument(
987
- "--agent",
988
- choices=["claude-code", "qwen-code", "gigacode"],
989
- default=[],
990
- action="append",
991
- help="Agent host to configure (can be passed multiple times).",
992
- )
993
- install.add_argument(
994
- "--scope",
995
- choices=["project", "user"],
996
- default=None,
997
- help="Installation scope (default: project).",
998
- )
999
- install.add_argument(
1000
- "--model",
1001
- type=str,
1002
- default=None,
1003
- help="Embedding model path or 'auto' (default: auto).",
1004
- )
1005
- install.add_argument(
1006
- "--surface",
1007
- choices=["mcp", "cli"],
1008
- default=None,
1009
- help=(
1010
- "Agent surface to install: 'mcp' (stdio MCP server + explore-codebase "
1011
- "skill + explorer-rag-enhanced subagent) or 'cli' (jrag console-script "
1012
- "skill + explorer-rag-cli subagent, no MCP entry). Omit to choose "
1013
- "interactively; non-interactive mode defaults to 'mcp'."
1014
- ),
1015
- )
1016
- _add_verbosity_flags(install)
1017
- install.set_defaults(handler=_cmd_install)
1018
-
1019
- update = subparsers.add_parser(
1020
- "update",
1021
- help="Refresh shipped artifacts (skill, agent, MCP entry) after pip upgrade.",
1022
- description=(
1023
- "Post-upgrade refresh: overwrites skill and agent files with the latest "
1024
- "shipped versions and updates the MCP command path. If an index exists, "
1025
- "also runs an incremental Lance + graph catch-up (same as `increment`). "
1026
- "Use --dry-run to preview changes without writing. Pass --surface to "
1027
- "switch between the mcp and cli surfaces (migrates artifacts + marker). "
1028
- "Requires a prior `install` run."
1029
- ),
1030
- )
1031
- update.add_argument(
1032
- "--force",
1033
- action="store_true",
1034
- help="Overwrite all artifacts even if content matches.",
1035
- )
1036
- update.add_argument(
1037
- "--dry-run",
1038
- action="store_true",
1039
- help="Print changes without writing files.",
1040
- )
1041
- update.add_argument(
1042
- "--surface",
1043
- choices=["mcp", "cli"],
1044
- default=None,
1045
- help=(
1046
- "Switch agent surface: 'mcp' or 'cli'. Tears down the old surface's "
1047
- "artifacts and deploys the new surface's (also rewrites the install "
1048
- "marker). Omit to keep the current surface; on a TTY you'll be prompted."
1049
- ),
1050
- )
1051
- _add_verbosity_flags(update)
1052
- update.set_defaults(handler=_cmd_update)
1053
-
1054
- increment = subparsers.add_parser(
1055
- "increment",
1056
- help="Pick up changes since the last index update.",
1057
- description="Runs cocoindex catch-up and incremental LadybugDB graph update. Use --vectors-only to skip graph update.",
1058
- )
1059
- _add_index_embedding_flags(increment)
1060
- _add_verbosity_flags(increment)
1061
- increment.add_argument(
1062
- "--vectors-only",
1063
- action="store_true",
1064
- help="Run only cocoindex catch-up (Lance); skip graph update.",
1065
- )
1066
- increment.set_defaults(handler=_cmd_increment)
1067
-
1068
- reprocess = subparsers.add_parser(
1069
- "reprocess",
1070
- help="Rebuild vectors and/or LadybugDB (default: both full phases).",
1071
- description=(
1072
- "Default: full Lance reprocess (cocoindex --full-reprocess) then full LadybugDB graph rebuild. "
1073
- "Use --vectors-only or --graph-only to run a single phase (mutually exclusive)."
1074
- ),
1075
- )
1076
- _add_index_embedding_flags(reprocess)
1077
- _add_verbosity_flags(reprocess)
1078
- _rex = reprocess.add_mutually_exclusive_group()
1079
- _rex.add_argument(
1080
- "--vectors-only",
1081
- action="store_true",
1082
- help="Run only the Lance/cocoindex full reprocess phase (no graph builder).",
1083
- )
1084
- _rex.add_argument(
1085
- "--graph-only",
1086
- action="store_true",
1087
- help="Run only build_ast_graph.py (no cocoindex / Lance reprocess).",
1088
- )
1089
- reprocess.set_defaults(handler=_cmd_reprocess)
1090
-
1091
- erase = subparsers.add_parser(
1092
- "erase",
1093
- help="Delete the index from disk.",
1094
- description="Runs cocoindex drop, removes LadybugDB, and drops Lance tables. Requires --yes or TTY confirmation.",
1095
- )
1096
- _add_index_embedding_flags(erase)
1097
- erase.add_argument("--yes", action="store_true", help="Confirm destructive deletion (required in CI)")
1098
- _add_verbosity_flags(erase)
1099
- erase.set_defaults(handler=_cmd_erase)
1100
-
1101
- meta = subparsers.add_parser("meta", help="Print graph meta and embedding resolution.")
1102
- _add_index_embedding_flags(meta)
1103
- meta.set_defaults(handler=_cmd_meta)
1104
-
1105
- tables = subparsers.add_parser("tables", help="List Lance tables and row counts.")
1106
- _add_index_embedding_flags(tables)
1107
- tables.set_defaults(handler=_cmd_tables)
1108
-
1109
- diagnose = subparsers.add_parser(
1110
- "diagnose-ignore",
1111
- help="Show which ignore-pattern layer decided the fate of a path.",
1112
- )
1113
- _add_index_embedding_flags(diagnose)
1114
- diagnose.add_argument("path", type=str)
1115
- diagnose.set_defaults(handler=_cmd_diagnose_ignore)
1116
-
1117
- analyze = subparsers.add_parser("analyze-pr", help="Blast-radius + risk score for a unified diff.")
1118
- _add_index_embedding_flags(analyze)
1119
- group = analyze.add_mutually_exclusive_group(required=True)
1120
- group.add_argument("--diff-file", type=str)
1121
- group.add_argument("--diff-stdin", action="store_true")
1122
- analyze.set_defaults(handler=_cmd_analyze_pr)
1123
-
1124
- unresolved = subparsers.add_parser(
1125
- "unresolved-calls",
1126
- help="List or aggregate UnresolvedCallSite rows (receiver-failure call sites).",
1127
- )
1128
- _add_index_embedding_flags(unresolved)
1129
- unresolved_sub = unresolved.add_subparsers(dest="unresolved_command", required=True)
1130
-
1131
- uc_list = unresolved_sub.add_parser("list", help="List unresolved call sites.")
1132
- _add_index_embedding_flags(uc_list)
1133
- uc_list.add_argument("--method-id", type=str, default=None, help="Caller Symbol id")
1134
- uc_list.add_argument(
1135
- "--reason",
1136
- type=str,
1137
- default=None,
1138
- choices=sorted(VALID_UNRESOLVED_CALL_REASONS),
1139
- help="Filter by UnresolvedCallSite.reason",
1140
- )
1141
- uc_list.add_argument("--microservice", type=str, default=None)
1142
- uc_list.add_argument("--callee-simple", type=str, default=None, dest="callee_simple")
1143
- uc_list.add_argument("--limit", type=int, default=100)
1144
- uc_list.set_defaults(handler=_cmd_unresolved_calls_list)
1145
-
1146
- uc_stats = unresolved_sub.add_parser("stats", help="Aggregate unresolved call site counts.")
1147
- _add_index_embedding_flags(uc_stats)
1148
- uc_stats.add_argument(
1149
- "--by",
1150
- type=str,
1151
- choices=("reason", "microservice", "caller_role"),
1152
- default="reason",
1153
- )
1154
- uc_stats.set_defaults(handler=_cmd_unresolved_calls_stats)
1155
-
1156
- return parser
1157
-
1158
-
1159
- def main(argv: list[str] | None = None) -> int:
1160
- raise_fd_limit()
1161
- raw = list(argv if argv is not None else sys.argv[1:])
1162
- if raw and raw[0] == "refresh":
1163
- print(_REFRESH_DEPRECATION, file=sys.stderr)
1164
- raw[0] = "reprocess"
1165
- parser = build_parser()
1166
- try:
1167
- args = parser.parse_args(raw)
1168
- except SystemExit as e:
1169
- if e.code in (0, None):
1170
- return 0
1171
- return int(e.code) if isinstance(e.code, int) else 2
1172
- except argparse.ArgumentError as exc:
1173
- print(f"java-codebase-rag: {exc}", file=sys.stderr)
1174
- return 2
1175
- handler = getattr(args, "handler", None)
1176
- if handler is None:
1177
- parser.print_help()
1178
- return 2
1179
- try:
1180
- return int(handler(args))
1181
- except Exception as exc: # pragma: no cover - defensive top-level guard
1182
- _emit({"success": False, "exit_code": 2, "message": f"internal error: {exc}"})
1183
- return 2
1184
-
1185
-
1186
- def _console_script_main() -> None:
1187
- """Real CLI entry: terminate without interpreter finalization.
1188
-
1189
- A pyarrow/lance worker thread (loaded via lancedb in lifecycle commands) can
1190
- outlive CPython finalization in a one-shot CLI subprocess and trip
1191
- ``PyGILState_Release`` (SIGABRT, exit -6). Flushing + ``os._exit`` skips that
1192
- racy teardown — the command has already done its work and emitted its result.
1193
- ``main()`` stays return-based so in-process test callers (``cli.main(...)``)
1194
- keep working.
1195
-
1196
- ``KeyboardInterrupt`` (Ctrl+C during a long indexing step) is caught here
1197
- rather than left to propagate: an uncaught interrupt bypasses this function
1198
- and runs full interpreter finalization (traceback + thread teardown),
1199
- whereas routing it through the same flush + ``os._exit`` path gives a clean,
1200
- immediate exit (code 130) and avoids the finalization-time SIGABRT noted
1201
- above for commands that loaded lancedb.
1202
- """
1203
- try:
1204
- rc = main()
1205
- except KeyboardInterrupt:
1206
- sys.stderr.write("\nInterrupted.\n")
1207
- sys.stderr.flush()
1208
- rc = 130
1209
- sys.stdout.flush()
1210
- sys.stderr.flush()
1211
- os._exit(rc)
1212
-
1213
-
1214
- if __name__ == "__main__":
1215
- _console_script_main()