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.
- java_codebase_rag/_deprecation.py +103 -0
- java_codebase_rag/_version.py +2 -2
- java_codebase_rag/ast/ast_java.py +22 -0
- java_codebase_rag/ast/ast_kotlin.py +1794 -0
- java_codebase_rag/ast/chunk_heuristics.py +26 -5
- java_codebase_rag/ast/language.py +117 -0
- java_codebase_rag/cli.py +17 -17
- java_codebase_rag/cli_dispatch.py +251 -0
- java_codebase_rag/config.py +8 -8
- java_codebase_rag/eval/runner.py +3 -3
- java_codebase_rag/graph/build_ast_graph.py +130 -8
- java_codebase_rag/graph/graph_enrich.py +8 -5
- java_codebase_rag/graph/ladybug_queries.py +1 -1
- java_codebase_rag/graph/path_filtering.py +39 -7
- java_codebase_rag/index/java_index_flow_lancedb.py +160 -15
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +6 -4
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +4 -4
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +4 -4
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +5 -5
- java_codebase_rag/installer.py +15 -15
- java_codebase_rag/jrag.py +25 -11
- java_codebase_rag/lance_optimize.py +7 -7
- java_codebase_rag/mcp/mcp_v2.py +2 -2
- java_codebase_rag/mcp/server.py +6 -4
- java_codebase_rag/pipeline.py +4 -4
- java_codebase_rag/progress.py +1 -1
- java_codebase_rag/search/search_lexical.py +1 -1
- java_codebase_rag/search/search_scoring.py +19 -5
- java_codebase_rag/watch/lock.py +1 -1
- java_codebase_rag/watch/watcher.py +45 -21
- {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/METADATA +31 -22
- {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/RECORD +36 -32
- java_codebase_rag-0.12.0.dist-info/entry_points.txt +5 -0
- java_codebase_rag-0.11.2.dist-info/entry_points.txt +0 -4
- {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/WHEEL +0 -0
- {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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 `
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 `
|
|
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 `
|
|
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"
|
|
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'
|
|
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 `
|
|
362
|
-
"or `
|
|
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 `
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 `
|
|
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"
|
|
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()
|
java_codebase_rag/config.py
CHANGED
|
@@ -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"
|
|
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"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
)
|
java_codebase_rag/eval/runner.py
CHANGED
|
@@ -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
|
-
(``
|
|
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 ``
|
|
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"
|
|
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
|
)
|