java-codebase-rag 0.11.2__py3-none-any.whl → 0.12.0__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 (37) hide show
  1. java_codebase_rag/_deprecation.py +103 -0
  2. java_codebase_rag/_version.py +2 -2
  3. java_codebase_rag/ast/ast_java.py +22 -0
  4. java_codebase_rag/ast/ast_kotlin.py +1794 -0
  5. java_codebase_rag/ast/chunk_heuristics.py +26 -5
  6. java_codebase_rag/ast/language.py +117 -0
  7. java_codebase_rag/cli.py +17 -17
  8. java_codebase_rag/cli_dispatch.py +251 -0
  9. java_codebase_rag/config.py +8 -8
  10. java_codebase_rag/eval/runner.py +3 -3
  11. java_codebase_rag/graph/build_ast_graph.py +130 -8
  12. java_codebase_rag/graph/graph_enrich.py +8 -5
  13. java_codebase_rag/graph/ladybug_queries.py +1 -1
  14. java_codebase_rag/graph/path_filtering.py +39 -7
  15. java_codebase_rag/index/java_index_flow_lancedb.py +160 -15
  16. java_codebase_rag/install_data/agents/explorer-rag-cli.md +6 -4
  17. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +4 -4
  18. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +4 -4
  19. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +5 -5
  20. java_codebase_rag/installer.py +15 -15
  21. java_codebase_rag/jrag.py +25 -11
  22. java_codebase_rag/lance_optimize.py +7 -7
  23. java_codebase_rag/mcp/mcp_v2.py +2 -2
  24. java_codebase_rag/mcp/server.py +6 -4
  25. java_codebase_rag/pipeline.py +4 -4
  26. java_codebase_rag/progress.py +1 -1
  27. java_codebase_rag/search/search_lexical.py +1 -1
  28. java_codebase_rag/search/search_scoring.py +19 -5
  29. java_codebase_rag/watch/lock.py +1 -1
  30. java_codebase_rag/watch/watcher.py +45 -21
  31. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/METADATA +31 -22
  32. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/RECORD +36 -32
  33. java_codebase_rag-0.12.0.dist-info/entry_points.txt +5 -0
  34. java_codebase_rag-0.11.2.dist-info/entry_points.txt +0 -4
  35. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/WHEEL +0 -0
  36. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/licenses/LICENSE +0 -0
  37. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/top_level.txt +0 -0
@@ -22,6 +22,16 @@ _JAVA_TYPE = re.compile(
22
22
  r"(?:class|interface|enum|record)\s+([A-Za-z_][A-Za-z0-9_]*)"
23
23
  )
24
24
 
25
+ # Kotlin type declarations. Covers ``class`` / ``interface`` / ``object`` /
26
+ # ``enum class`` plus the modifiers Kotlin uses (``internal`` / ``open`` / etc.).
27
+ # ``object`` is the Kotlin-specific kind the Java regex misses; top-level
28
+ # ``fun`` is intentionally NOT a type declaration (no name to pin a type to).
29
+ _KOTLIN_TYPE = re.compile(
30
+ r"\b(?:public\s+|private\s+|protected\s+|internal\s+|"
31
+ r"final\s+|open\s+|abstract\s+|sealed\s+|data\s+)*"
32
+ r"(?:class|interface|object|enum\s+class)\s+([A-Za-z_][A-Za-z0-9_]*)"
33
+ )
34
+
25
35
 
26
36
  def analyze_chunk(text: str | None, *, language: str, kind: str) -> ChunkHints:
27
37
  if not text or not text.strip():
@@ -30,16 +40,27 @@ def analyze_chunk(text: str | None, *, language: str, kind: str) -> ChunkHints:
30
40
  lines = text.strip().split("\n")
31
41
  n = len(lines)
32
42
  lang = (language or "").lower()
33
- is_java = kind == "java" or lang == "java"
34
-
43
+ # Kotlin chunks live in the java LanceDB table (``kind == "java"``), so the
44
+ # ``language`` field — not ``kind`` — is what distinguishes them. Detect
45
+ # Kotlin first so the Kotlin regex wins for ``object`` / Kotlin modifiers.
46
+ is_kotlin = lang == "kotlin"
47
+ is_java = not is_kotlin and (kind == "java" or lang == "java")
48
+
49
+ # Both Java and Kotlin use ``import <pkg.Type>`` lines (Java ends the line
50
+ # with ``;``, Kotlin does not), so the import-density heuristic matches on
51
+ # the ``import `` prefix only — the trailing-``;`` difference is irrelevant.
35
52
  import_heavy = False
36
- if is_java and n >= 3:
53
+ if (is_java or is_kotlin) and n >= 3:
37
54
  imp = sum(1 for L in lines if L.lstrip().startswith("import "))
38
55
  import_heavy = imp / n >= 0.55
39
56
 
40
57
  primary: str | None = None
41
- if is_java:
42
- head = "\n".join(lines[: min(80, n)])
58
+ head = "\n".join(lines[: min(80, n)])
59
+ if is_kotlin:
60
+ m = _KOTLIN_TYPE.search(head)
61
+ if m:
62
+ primary = m.group(1)
63
+ elif is_java:
43
64
  m = _JAVA_TYPE.search(head)
44
65
  if m:
45
66
  primary = m.group(1)
@@ -0,0 +1,117 @@
1
+ """Language-dispatch seam for AST extraction.
2
+
3
+ A registry of ``LanguageBackend`` objects keyed by ``language_id``; each backend
4
+ owns the source suffixes it claims (``.java`` always; ``.kt`` when the
5
+ ``tree-sitter-kotlin`` grammar imports) and a ``parse`` entry point returning a
6
+ ``JavaFileAst`` — the single AST shape shared by both languages (Kotlin reuses
7
+ this surface; it does not introduce a separate type).
8
+
9
+ Both backends parse into ``JavaFileAst``, so ``FileAst`` remains a direct alias
10
+ for ``JavaFileAst``. The Kotlin backend is registered conditionally on the
11
+ grammar wheel importing (see the ``try``/``except ImportError`` below); a
12
+ minimal or graph-only install with no ``tree-sitter-kotlin`` simply has Java in
13
+ the registry, and ``backend_for`` returns ``None`` for ``.kt``.
14
+
15
+ Import cycle note: ``ast_java`` defines ``JavaFileAst`` whose ``__post_init__``
16
+ validates against ``KNOWN_LANGUAGE_IDS`` (defined here). To keep the cycle
17
+ one-directional, ``ast_java`` does NOT import this module at top level — it
18
+ imports ``KNOWN_LANGUAGE_IDS`` lazily inside ``__post_init__``. This module
19
+ freely imports from ``ast_java``.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from pathlib import Path
24
+ from typing import Protocol, runtime_checkable
25
+
26
+ from java_codebase_rag.ast.ast_java import JavaFileAst, parse_java
27
+
28
+ __all__ = [
29
+ "LanguageBackend",
30
+ "JavaBackend",
31
+ "LANG_BACKENDS",
32
+ "KNOWN_LANGUAGE_IDS",
33
+ "backend_for",
34
+ "FileAst",
35
+ ]
36
+
37
+
38
+ @runtime_checkable
39
+ class LanguageBackend(Protocol):
40
+ """A pluggable parser backend for one source language."""
41
+
42
+ language_id: str
43
+ suffixes: tuple[str, ...]
44
+
45
+ def parse(
46
+ self,
47
+ source: bytes | str,
48
+ *,
49
+ filename: str,
50
+ verbose: bool = False,
51
+ ) -> JavaFileAst: ...
52
+
53
+
54
+ class JavaBackend:
55
+ """The Java backend — delegates to the existing tree-sitter ``parse_java``."""
56
+
57
+ language_id: str = "java"
58
+ suffixes: tuple[str, ...] = (".java",)
59
+
60
+ def parse(
61
+ self, source: bytes | str, *, filename: str = "", verbose: bool = False
62
+ ) -> JavaFileAst:
63
+ return parse_java(source, filename=filename, verbose=verbose)
64
+
65
+
66
+ # Registry: language_id -> backend. Kotlin is appended conditionally on the
67
+ # grammar wheel importing (try/except below), so minimal/graph-only installs
68
+ # simply skip `.kt` files instead of crashing at import time.
69
+ LANG_BACKENDS: dict[str, LanguageBackend] = {
70
+ "java": JavaBackend(),
71
+ }
72
+
73
+ # Conditional Kotlin registration. The grammar wheel (tree-sitter-kotlin) is an
74
+ # optional dependency on some platforms (Intel-Mac graph-only installs); when it
75
+ # is absent, KotlinBackend stays out of the registry and ``backend_for`` returns
76
+ # ``None`` for ``.kt`` — the file is then skipped by every parse site.
77
+ try: # pragma: no cover - branch depends on whether the wheel is installed
78
+ import tree_sitter_kotlin as _ts_kotlin # noqa: F401
79
+
80
+ from java_codebase_rag.ast.ast_kotlin import parse_kotlin as _parse_kotlin
81
+
82
+ class KotlinBackend:
83
+ """The Kotlin backend — delegates to ``parse_kotlin``."""
84
+
85
+ language_id: str = "kotlin"
86
+ suffixes: tuple[str, ...] = (".kt",)
87
+
88
+ def parse(
89
+ self, source: bytes | str, *, filename: str = "", verbose: bool = False
90
+ ) -> JavaFileAst:
91
+ return _parse_kotlin(source, filename=filename, verbose=verbose)
92
+
93
+ LANG_BACKENDS["kotlin"] = KotlinBackend()
94
+ except ImportError:
95
+ pass
96
+
97
+ # Derived from the registry so the two never drift apart.
98
+ KNOWN_LANGUAGE_IDS: frozenset[str] = frozenset(LANG_BACKENDS.keys())
99
+
100
+
101
+ def backend_for(path: Path | str) -> LanguageBackend | None:
102
+ """Return the first backend whose ``suffixes`` contain ``path``'s suffix.
103
+
104
+ Suffix matching is case-sensitive against ``Path(path).suffix``. Returns
105
+ ``None`` when no backend claims the file (e.g. ``.md``), or for a ``.kt``
106
+ file on an install where the ``tree-sitter-kotlin`` grammar is absent.
107
+ """
108
+ suffix = Path(path).suffix
109
+ for backend in LANG_BACKENDS.values():
110
+ if suffix in backend.suffixes:
111
+ return backend
112
+ return None
113
+
114
+
115
+ # Single-language alias. Re-exported so downstream call sites can target the
116
+ # generic name and stay source-stable when a Kotlin AST shape is introduced.
117
+ FileAst = JavaFileAst
java_codebase_rag/cli.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  # Heavy imports (`server`, `pr_analysis`, `path_filtering.LayeredIgnore`,
4
- # `build_ast_graph`) stay lazy inside handlers so `java-codebase-rag --help` stays fast.
4
+ # `build_ast_graph`) stay lazy inside handlers so `jrag --help` stays fast.
5
5
 
6
6
  import argparse
7
7
  import asyncio
@@ -49,7 +49,7 @@ _INCREMENT_WARNING_LINES = (
49
49
  "Lance vector index has been updated incrementally and is current.",
50
50
  "",
51
51
  "For an up-to-date graph, run:",
52
- " java-codebase-rag reprocess",
52
+ " jrag reprocess",
53
53
  "",
54
54
  "Track progress on LadybugDB incremental rebuild:",
55
55
  f" {LADYBUG_INCREMENTAL_TRACKING_ISSUE_URL}",
@@ -61,14 +61,14 @@ _REFRESH_DEPRECATION = (
61
61
  )
62
62
 
63
63
  _REPROCESS_DRIFT_VECTORS_ONLY = (
64
- "java-codebase-rag reprocess: rebuilt vectors only; graph (code_graph.lbug) was NOT rebuilt "
64
+ "jrag reprocess: rebuilt vectors only; graph (code_graph.lbug) was NOT rebuilt "
65
65
  "and may now reflect a stale source snapshot."
66
66
  )
67
67
 
68
68
 
69
69
  def _reprocess_drift_graph_only_line(index_dir: Path) -> str:
70
70
  return (
71
- "java-codebase-rag reprocess: rebuilt graph only; vectors (Lance tables under "
71
+ "jrag reprocess: rebuilt graph only; vectors (Lance tables under "
72
72
  f"{index_dir}) were NOT rebuilt and may now reflect a stale source snapshot."
73
73
  )
74
74
 
@@ -98,10 +98,10 @@ def _is_graph_preflight_blocker(g: Any) -> bool:
98
98
  def _emit_reprocess_selective_tty(*, mode: str) -> None:
99
99
  if mode == "vectors":
100
100
  print("Rebuilt: vectors")
101
- print("Skipped: graph (use `java-codebase-rag reprocess --graph-only` or `reprocess` to refresh)")
101
+ print("Skipped: graph (use `jrag reprocess --graph-only` or `reprocess` to refresh)")
102
102
  else:
103
103
  print("Rebuilt: graph")
104
- print("Skipped: vectors (use `java-codebase-rag reprocess --vectors-only` or `reprocess` to refresh)")
104
+ print("Skipped: vectors (use `jrag reprocess --vectors-only` or `reprocess` to refresh)")
105
105
 
106
106
 
107
107
  def _reprocess_success_message(mode: str | None, payload: dict[str, Any]) -> str:
@@ -147,7 +147,7 @@ def _pipeline_header(subcommand: str, cfg: ResolvedOperatorConfig) -> None:
147
147
  root = cfg.source_root.resolve()
148
148
  idx = cfg.index_dir.resolve()
149
149
  print(
150
- bold(f"java-codebase-rag {subcommand} {_PIPELINE_SEP} source={root} {_PIPELINE_SEP} index={idx}"),
150
+ bold(f"jrag {subcommand} {_PIPELINE_SEP} source={root} {_PIPELINE_SEP} index={idx}"),
151
151
  file=sys.stderr,
152
152
  flush=True,
153
153
  )
@@ -159,7 +159,7 @@ def _pipeline_footer(subcommand: str, started: float, exit_code: int) -> None:
159
159
  elapsed = time.perf_counter() - started
160
160
  marker = styled_check() if exit_code == 0 else styled_cross()
161
161
  print(
162
- f"{marker} {bold(f'java-codebase-rag {subcommand} {_PIPELINE_SEP} finished in {elapsed:.2f}s')}"
162
+ f"{marker} {bold(f'jrag {subcommand} {_PIPELINE_SEP} finished in {elapsed:.2f}s')}"
163
163
  + (f" (exit={exit_code})" if exit_code != 0 else ""),
164
164
  file=sys.stderr,
165
165
  flush=True,
@@ -358,8 +358,8 @@ def _cmd_init(args: argparse.Namespace) -> int:
358
358
  "success": False,
359
359
  "message": (
360
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."
361
+ "Use `jrag reprocess` to rebuild in place, "
362
+ "or `jrag erase --yes` then `init` for a clean slate."
363
363
  ),
364
364
  "non_empty_paths": paths,
365
365
  }
@@ -702,7 +702,7 @@ def _cmd_erase(args: argparse.Namespace) -> int:
702
702
  cfg.apply_to_os_environ()
703
703
  # Lazy import: build_ast_graph transitively pulls numpy/ladybug/pyarrow/
704
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
705
+ # Keeping it out of the top-level import lets `jrag --help` (and
706
706
  # every other command) stay fast -- see the lazy-import invariant atop this file.
707
707
  from java_codebase_rag.graph.build_ast_graph import BUILDER_OWNED_INDEX_FILES
708
708
  builder_paths = [cfg.ladybug_path.parent / name for name in BUILDER_OWNED_INDEX_FILES]
@@ -729,7 +729,7 @@ def _cmd_erase(args: argparse.Namespace) -> int:
729
729
  if not args.yes:
730
730
  if not sys.stdin.isatty():
731
731
  print(
732
- "java-codebase-rag erase: non-interactive stdin; pass --yes to confirm.",
732
+ "jrag erase: non-interactive stdin; pass --yes to confirm.",
733
733
  file=sys.stderr,
734
734
  )
735
735
  return 2
@@ -740,7 +740,7 @@ def _cmd_erase(args: argparse.Namespace) -> int:
740
740
  # (the Windows NUL device is a character device, so isatty() lies).
741
741
  # Treat it as a refusal instead of crashing with an EOF traceback.
742
742
  print(
743
- "java-codebase-rag erase: non-interactive stdin; pass --yes to confirm.",
743
+ "jrag erase: non-interactive stdin; pass --yes to confirm.",
744
744
  file=sys.stderr,
745
745
  )
746
746
  return 2
@@ -753,7 +753,7 @@ def _cmd_erase(args: argparse.Namespace) -> int:
753
753
  drop = run_cocoindex_drop(env, quiet=bool(args.quiet))
754
754
  if drop.returncode == 127:
755
755
  print(
756
- "java-codebase-rag erase: cocoindex CLI not found next to this Python; "
756
+ "jrag erase: cocoindex CLI not found next to this Python; "
757
757
  "skipped `cocoindex drop` — cocoindex.db (if any) was not removed by CocoIndex.",
758
758
  file=sys.stderr,
759
759
  )
@@ -927,7 +927,7 @@ def _cmd_analyze_pr(args: argparse.Namespace) -> int:
927
927
 
928
928
  def build_parser() -> argparse.ArgumentParser:
929
929
  description = (
930
- "java-codebase-rag — graph-native code intelligence for Java microservices.\n\n"
930
+ "jrag — graph-native code intelligence for Java microservices.\n\n"
931
931
  "Lifecycle commands stream subprocess progress to stderr (including relayed child stdout); "
932
932
  "--quiet suppresses that stream; stdout remains the machine-readable payload.\n\n"
933
933
  "Lifecycle (manage the index):\n"
@@ -942,7 +942,7 @@ def build_parser() -> argparse.ArgumentParser:
942
942
  " unresolved-calls List or aggregate receiver-failure call sites (not in CALLS).\n\n"
943
943
  "Analysis (work with code changes):\n"
944
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."
945
+ "Run `jrag <command> --help` for command-specific options."
946
946
  )
947
947
  parser = argparse.ArgumentParser(
948
948
  prog="java-codebase-rag",
@@ -1170,7 +1170,7 @@ def main(argv: list[str] | None = None) -> int:
1170
1170
  return 0
1171
1171
  return int(e.code) if isinstance(e.code, int) else 2
1172
1172
  except argparse.ArgumentError as exc:
1173
- print(f"java-codebase-rag: {exc}", file=sys.stderr)
1173
+ print(f"jrag: {exc}", file=sys.stderr)
1174
1174
  return 2
1175
1175
  handler = getattr(args, "handler", None)
1176
1176
  if handler is None:
@@ -0,0 +1,251 @@
1
+ """Unified ``jrag`` CLI dispatcher (Task 2 of the rename).
2
+
3
+ The tool exposes two disjoint CLI surfaces that this module unifies behind a
4
+ single ``jrag`` console-script entry point:
5
+
6
+ * :mod:`java_codebase_rag.cli` — operator / lifecycle verbs (``init``,
7
+ ``install``, ``update``, ``increment``, ``reprocess``, ``erase``, ``meta``,
8
+ ``tables``, ``diagnose-ignore``, ``analyze-pr``, ``unresolved-calls``).
9
+ * :mod:`java_codebase_rag.jrag` — agent verbs (``find``, ``search``,
10
+ ``inspect``, ``callers``, ``callees``, ``hierarchy``, ``watch``, ``status``,
11
+ … — 32 verbs total).
12
+
13
+ Both modules already expose a zero-argument ``_console_script_main`` that
14
+ reads ``sys.argv`` itself, performs its own startup (``raise_fd_limit``, UTF-8
15
+ stdio, error handling), and calls ``sys.exit`` (via ``os._exit``). This
16
+ dispatcher does NOT reimplement argparse, fd-limit, or error handling: it picks
17
+ the target module and forwards. Letting the chosen ``_console_script_main`` run
18
+ unwinds as normal, including its ``SystemExit`` / ``os._exit``.
19
+
20
+ Routing contract
21
+ ----------------
22
+ For an invocation ``[argv0, *args]``:
23
+
24
+ * If any token in ``args`` is a member of :data:`OPERATOR_VERBS`, route to
25
+ ``cli._console_script_main``.
26
+ * Else if any token is a member of :data:`AGENT_VERBS`, route to
27
+ ``jrag._console_script_main``.
28
+ * Otherwise fall back to the identity default: the basename of ``argv0``.
29
+ ``jrag`` → ``jrag._console_script_main``; anything else (including the
30
+ legacy ``java-codebase-rag`` alias) → ``cli._console_script_main``.
31
+
32
+ The "first matching verb" rule scans left-to-right and routes by *that* verb's
33
+ set. Operator and agent verb sets are disjoint, so the scan is unambiguous.
34
+
35
+ Known non-goal: if a global flag's value token happens to equal a verb name
36
+ (e.g. a hypothetical ``--config find``), routing may follow the verb heuristic.
37
+ Acceptable edge case — the alternative (a flag-aware parser) would duplicate
38
+ argparse here, which the design explicitly forbids.
39
+
40
+ Unified help contract
41
+ ---------------------
42
+ For the canonical ``jrag`` identity, a top-level help request (``jrag``,
43
+ ``jrag --help``, ``jrag -h`` — i.e. ``-h``/``--help`` appears before any
44
+ recognized verb, or no tokens at all) is served by :func:`_print_unified_help`:
45
+ it prints the agent parser's full help (agent verbs + global flags) and
46
+ appends a clearly labeled "Operator commands" section listing the operator
47
+ verbs with one-line descriptions sourced from :func:`cli.build_parser`'s
48
+ subparser choices. This makes ``jrag --help`` the single discovery surface for
49
+ all 11+32 verbs, satisfying the unification design contract.
50
+
51
+ The legacy ``java-codebase-rag`` alias does NOT get unified help: it keeps its
52
+ pre-rename behavior (operator-only parser) for backward compatibility, since
53
+ operators with shell scripts that parse ``java-codebase-rag --help`` output
54
+ must not see new verbs appear under that alias.
55
+
56
+ Before routing, :func:`maybe_warn_legacy_alias` runs once. It is a no-op
57
+ unless the tool was invoked through a legacy alias in an interactive context
58
+ (see :mod:`java_codebase_rag._deprecation`); in tests ``sys.stderr`` is not a
59
+ TTY so it stays silent.
60
+ """
61
+ from __future__ import annotations
62
+
63
+ import argparse
64
+ import sys
65
+ from typing import Callable
66
+
67
+ from java_codebase_rag import cli as _cli_mod
68
+ from java_codebase_rag import jrag as _jrag_mod
69
+ from java_codebase_rag._deprecation import _invoked_program_name
70
+ from java_codebase_rag._deprecation import maybe_warn_legacy_alias
71
+
72
+ __all__ = [
73
+ "OPERATOR_VERBS",
74
+ "AGENT_VERBS",
75
+ "_console_script_main",
76
+ ]
77
+
78
+ #: Tokens that request top-level help. argparse's default help action accepts
79
+ #: both ``-h`` and ``--help`` (neither parser disables ``add_help``).
80
+ _HELP_TOKENS: frozenset[str] = frozenset({"-h", "--help"})
81
+
82
+
83
+ #: Operator / lifecycle verbs. Must match ``cli.build_parser()``'s registered
84
+ #: top-level subcommand choice names (drift-guarded by the test suite).
85
+ #:
86
+ #: Note: the rename design spec lists ``diagnose`` and ``unresolved`` here, but
87
+ #: the parser actually registers ``diagnose-ignore`` and ``unresolved-calls``
88
+ #: (the spec abbreviated the names). The drift-guard test pins the dispatcher
89
+ #: to the parser's truth, so the hyphenated names are what we ship.
90
+ OPERATOR_VERBS: frozenset[str] = frozenset(
91
+ {
92
+ "init",
93
+ "install",
94
+ "update",
95
+ "increment",
96
+ "reprocess",
97
+ "erase",
98
+ "meta",
99
+ "tables",
100
+ "diagnose-ignore",
101
+ "analyze-pr",
102
+ "unresolved-calls",
103
+ }
104
+ )
105
+
106
+ #: Agent verbs. Must match ``jrag.build_parser()``'s registered top-level
107
+ #: subcommand choice names (drift-guarded by the test suite).
108
+ AGENT_VERBS: frozenset[str] = frozenset(
109
+ {
110
+ "find",
111
+ "search",
112
+ "inspect",
113
+ "callers",
114
+ "callees",
115
+ "hierarchy",
116
+ "implementations",
117
+ "subclasses",
118
+ "overrides",
119
+ "overridden-by",
120
+ "dependents",
121
+ "impact",
122
+ "decompose",
123
+ "flow",
124
+ "dependencies",
125
+ "connection",
126
+ "outline",
127
+ "imports",
128
+ "microservices",
129
+ "map",
130
+ "conventions",
131
+ "overview",
132
+ "vocab-index",
133
+ "watch",
134
+ "status",
135
+ "http-routes",
136
+ "http-clients",
137
+ "producers",
138
+ "topics",
139
+ "jobs",
140
+ "listeners",
141
+ "entities",
142
+ }
143
+ )
144
+
145
+
146
+ def _is_unified_help_request() -> bool:
147
+ """True iff argv is a top-level help/no-args request.
148
+
149
+ Returns True when ``sys.argv[1:]`` is empty (no-args) OR contains
150
+ ``-h``/``--help`` as the first recognized token (i.e. before any verb).
151
+ Returns False as soon as a verb token is seen, so verb-specific help
152
+ (``jrag install --help``, ``jrag find --help``) still routes to the
153
+ verb's own parser. Also returns False for non-help requests such as
154
+ ``jrag --version`` (no help token, no verb — falls through to the
155
+ identity default).
156
+ """
157
+ rest = sys.argv[1:] if len(sys.argv) > 1 else []
158
+ if not rest:
159
+ return True
160
+ for token in rest:
161
+ if token in OPERATOR_VERBS or token in AGENT_VERBS:
162
+ return False
163
+ if token in _HELP_TOKENS:
164
+ return True
165
+ return False
166
+
167
+
168
+ def _operator_subcommand_helps() -> list[tuple[str, str]]:
169
+ """Return ``[(verb, one_line_help), ...]`` for operator verbs, parser order.
170
+
171
+ Pulls the ``(metavar, help)`` of each ``_ChoicesPseudoAction`` registered
172
+ on ``cli.build_parser()``'s top-level subparsers action. Parser order
173
+ (lifecycle flow: ``init``, ``install``, ``update``, ...) is preserved
174
+ rather than sorting alphabetically.
175
+ """
176
+ parser = _cli_mod.build_parser()
177
+ for action in parser._actions:
178
+ if isinstance(action, argparse._SubParsersAction):
179
+ return [(ca.metavar, ca.help or "") for ca in action._choices_actions]
180
+ return []
181
+
182
+
183
+ def _print_unified_help(stream=None) -> None:
184
+ """Print the canonical ``jrag`` unified help: agent verbs + operator verbs.
185
+
186
+ Prints the agent parser's full help (which already lists the 32 agent
187
+ verbs with one-line descriptions plus the global ``-h``/``--version``
188
+ options and the descriptive epilog), then appends a clearly labeled
189
+ "Operator commands (indexing & maintenance)" section listing the 11
190
+ operator verbs with their one-line descriptions sourced from
191
+ :func:`cli.build_parser`'s subparser choices.
192
+
193
+ Writes to ``stream`` (default ``sys.stdout``) and returns without exiting
194
+ — the caller (``_console_script_main``) returns to the pip wrapper, which
195
+ exits 0.
196
+ """
197
+ target = stream if stream is not None else sys.stdout
198
+ agent_parser = _jrag_mod.build_parser()
199
+ agent_parser.print_help(target)
200
+ target.write("\n")
201
+ target.write(
202
+ "Operator commands (indexing & maintenance; run `jrag <command> --help` "
203
+ "for details):\n"
204
+ )
205
+ for name, help_text in _operator_subcommand_helps():
206
+ target.write(f" {name:<20} {help_text}\n")
207
+
208
+
209
+ def _choose_target() -> Callable[[], None]:
210
+ """Pick the ``_console_script_main`` to delegate to.
211
+
212
+ Scans ``sys.argv[1:]`` left-to-right for the first token that is a member
213
+ of ``OPERATOR_VERBS`` or ``AGENT_VERBS``; routes by that set. If no token
214
+ matches, routes by identity default (``jrag`` basename → jrag; anything
215
+ else → cli, including the legacy ``java-codebase-rag`` alias).
216
+ """
217
+ rest = sys.argv[1:] if len(sys.argv) > 1 else []
218
+ for token in rest:
219
+ if token in OPERATOR_VERBS:
220
+ return _cli_mod._console_script_main
221
+ if token in AGENT_VERBS:
222
+ return _jrag_mod._console_script_main
223
+
224
+ # No verb token: identity default by argv[0] basename.
225
+ if _invoked_program_name() == "jrag":
226
+ return _jrag_mod._console_script_main
227
+ return _cli_mod._console_script_main
228
+
229
+
230
+ def _console_script_main() -> None:
231
+ """Unified ``jrag`` entry point: warn, maybe serve unified help, delegate.
232
+
233
+ For the canonical ``jrag`` identity with a top-level help/no-args request,
234
+ print the unified help (agent verbs + operator verbs) and return — the pip
235
+ wrapper then exits 0. This is the discovery surface for all verbs.
236
+
237
+ Otherwise: pick the target module and forward. The chosen target's
238
+ ``_console_script_main`` does its own startup and terminates the process
239
+ (via ``os._exit``); we let ``SystemExit`` propagate rather than
240
+ reimplementing startup here.
241
+ """
242
+ maybe_warn_legacy_alias()
243
+ if _invoked_program_name() == "jrag" and _is_unified_help_request():
244
+ _print_unified_help()
245
+ return
246
+ target = _choose_target()
247
+ target()
248
+
249
+
250
+ if __name__ == "__main__": # pragma: no cover - direct invocation
251
+ _console_script_main()
@@ -125,7 +125,7 @@ def maybe_expand_embedding_model_path(
125
125
  expanded = os.path.expandvars(os.path.expanduser(value))
126
126
  if _UNRESOLVED_VAR_RE.search(expanded):
127
127
  print(
128
- f"java-codebase-rag: path-shaped model string contains unresolved variable: {expanded}",
128
+ f"jrag: path-shaped model string contains unresolved variable: {expanded}",
129
129
  file=sys.stderr,
130
130
  )
131
131
  if expanded.startswith(("./", "../")):
@@ -191,7 +191,7 @@ def emit_legacy_env_hints_if_present() -> None:
191
191
  continue
192
192
  _legacy_hint_seen.add(key)
193
193
  print(
194
- f"java-codebase-rag: {old} is set but no longer read; use {replacement}.",
194
+ f"jrag: {old} is set but no longer read; use {replacement}.",
195
195
  file=sys.stderr,
196
196
  )
197
197
 
@@ -208,7 +208,7 @@ def emit_legacy_yaml_hint_if_needed(source_root: Path) -> None:
208
208
  if (source_root / name).is_file():
209
209
  _legacy_yaml_hint_roots.add(root_s)
210
210
  print(
211
- "java-codebase-rag: found legacy "
211
+ "jrag: found legacy "
212
212
  f"{name}; rename to .java-codebase-rag.yml to re-enable config.",
213
213
  file=sys.stderr,
214
214
  )
@@ -299,7 +299,7 @@ def _config_dir_from_pointer(anchor: Path) -> Path | None:
299
299
  if key not in _stale_pointer_seen:
300
300
  _stale_pointer_seen.add(key)
301
301
  print(
302
- "java-codebase-rag: ignoring stale index pointer "
302
+ "jrag: ignoring stale index pointer "
303
303
  f"{pointer} -> {raw} (target missing or not a config file).",
304
304
  file=sys.stderr,
305
305
  )
@@ -343,7 +343,7 @@ def load_yaml_mapping(source_root: Path) -> dict[str, Any]:
343
343
  # tuple restores the graceful-degradation contract while still surfacing the
344
344
  # problem on stderr.
345
345
  print(
346
- f"java-codebase-rag: could not load config {path}: {exc}; ignoring config.",
346
+ f"jrag: could not load config {path}: {exc}; ignoring config.",
347
347
  file=sys.stderr,
348
348
  )
349
349
  return {}
@@ -713,21 +713,21 @@ def resolve_operator_config(
713
713
  # Inline floors/validation (mirror the existing graceful-degradation style).
714
714
  if w_debounce < 100:
715
715
  print(
716
- f"java-codebase-rag: watch.debounce_ms={w_debounce} is below the 100 ms "
716
+ f"jrag: watch.debounce_ms={w_debounce} is below the 100 ms "
717
717
  "floor; falling back to 1500.",
718
718
  file=sys.stderr,
719
719
  )
720
720
  w_debounce, w_debounce_src = 1500, "default"
721
721
  if w_backend not in ("auto", "watchdog", "polling"):
722
722
  print(
723
- f"java-codebase-rag: watch.backend={w_backend!r} is not one of "
723
+ f"jrag: watch.backend={w_backend!r} is not one of "
724
724
  "auto/watchdog/polling; falling back to 'auto'.",
725
725
  file=sys.stderr,
726
726
  )
727
727
  w_backend, w_backend_src = "auto", "default"
728
728
  if w_poll < 200:
729
729
  print(
730
- f"java-codebase-rag: watch.poll_interval_ms={w_poll} is below the 200 ms "
730
+ f"jrag: watch.poll_interval_ms={w_poll} is below the 200 ms "
731
731
  "floor; falling back to 2000.",
732
732
  file=sys.stderr,
733
733
  )
@@ -8,7 +8,7 @@ sentence_transformers) and invokes the operator CLI to build a real index.
8
8
  Pipeline (``run_eval``):
9
9
 
10
10
  1. Build a fresh index into ``cfg.index_dir`` via the operator CLI
11
- (``java-codebase-rag init``) as a **subprocess** — the stable operator
11
+ (``jrag init``) as a **subprocess** — the stable operator
12
12
  surface. Reaching into cocoindex/pipeline internals is fragile (process env,
13
13
  progress renderers), so the subprocess wins on robustness. A non-zero exit
14
14
  surfaces stdout/stderr in the raised ``RuntimeError``.
@@ -159,7 +159,7 @@ class EvalReport:
159
159
 
160
160
 
161
161
  def _build_index_subprocess(*, corpus_dir: str, index_dir: str) -> None:
162
- """Build a fresh index via ``java-codebase-rag init``.
162
+ """Build a fresh index via ``jrag init``.
163
163
 
164
164
  Raises FileNotFoundError if the corpus is missing, RuntimeError on a
165
165
  non-zero CLI exit (surfacing clipped stdout/stderr).
@@ -196,7 +196,7 @@ def _build_index_subprocess(*, corpus_dir: str, index_dir: str) -> None:
196
196
  )
197
197
  if proc.returncode != 0:
198
198
  raise RuntimeError(
199
- f"java-codebase-rag init exited {proc.returncode} for corpus {corpus_dir}.\n"
199
+ f"jrag init exited {proc.returncode} for corpus {corpus_dir}.\n"
200
200
  f"--- stdout (clipped 8000) ---\n{proc.stdout[-8000:]}\n"
201
201
  f"--- stderr (clipped 8000) ---\n{proc.stderr[-8000:]}"
202
202
  )