java-codebase-rag 0.9.7__py3-none-any.whl → 0.10.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.
- java_codebase_rag/analysis/pr_analysis.py +33 -3
- java_codebase_rag/ast/ast_java.py +2 -1
- java_codebase_rag/cli.py +23 -8
- java_codebase_rag/config.py +68 -1
- java_codebase_rag/graph/build_ast_graph.py +123 -4
- java_codebase_rag/graph/graph_types.py +109 -22
- java_codebase_rag/graph/ladybug_queries.py +45 -2
- java_codebase_rag/index/java_index_flow_lancedb.py +10 -16
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +3 -1
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +3 -1
- java_codebase_rag/jrag.py +627 -661
- java_codebase_rag/jrag_render.py +160 -3
- java_codebase_rag/lance_optimize.py +11 -12
- java_codebase_rag/mcp/mcp_v2.py +2 -1
- java_codebase_rag/pipeline.py +47 -1
- java_codebase_rag/read_payloads.py +781 -0
- java_codebase_rag/search/search_lancedb.py +138 -6
- java_codebase_rag/search/search_lexical.py +140 -30
- java_codebase_rag/search/search_scoring.py +108 -0
- java_codebase_rag/watch/__init__.py +0 -0
- java_codebase_rag/watch/client.py +230 -0
- java_codebase_rag/watch/daemon.py +368 -0
- java_codebase_rag/watch/lock.py +201 -0
- java_codebase_rag/watch/paths.py +76 -0
- java_codebase_rag/watch/protocol.py +122 -0
- java_codebase_rag/watch/server.py +273 -0
- java_codebase_rag/watch/warm.py +105 -0
- java_codebase_rag/watch/watcher.py +352 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/METADATA +30 -31
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/RECORD +34 -24
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/WHEEL +0 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/entry_points.txt +0 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/licenses/LICENSE +0 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/top_level.txt +0 -0
java_codebase_rag/jrag.py
CHANGED
|
@@ -24,7 +24,9 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import argparse
|
|
26
26
|
import os
|
|
27
|
+
import signal
|
|
27
28
|
import sys
|
|
29
|
+
import time
|
|
28
30
|
import traceback
|
|
29
31
|
from pathlib import Path
|
|
30
32
|
|
|
@@ -195,6 +197,46 @@ def _clamped_limit(args: argparse.Namespace) -> int:
|
|
|
195
197
|
return min(raw_limit, 499)
|
|
196
198
|
|
|
197
199
|
|
|
200
|
+
def _emit(env, args: argparse.Namespace, *, noun: str = "",
|
|
201
|
+
shape: str | None = None, next_offset: int | None = None) -> int:
|
|
202
|
+
"""Final render+print funnel honoring ``--count`` / ``--exists`` / ``--fields``;
|
|
203
|
+
returns the exit code.
|
|
204
|
+
|
|
205
|
+
The single output seam every ok / not_found / ambiguous result routes
|
|
206
|
+
through. The shared helpers (:func:`_render_listing`, :func:`_emit_traversal`)
|
|
207
|
+
delegate their tail here; inline success render sites call it directly. True
|
|
208
|
+
usage / setup errors (missing index, kind guard, ``neighbors_v2`` failure,
|
|
209
|
+
argparse errors) bypass it — those render normally via :func:`render`, since a
|
|
210
|
+
count/exists shape would hide the actionable error message.
|
|
211
|
+
|
|
212
|
+
Exit code: ``--exists`` forces 0 when results are present and 2 otherwise
|
|
213
|
+
(resolve miss AND empty ok both count as absent), computed via
|
|
214
|
+
:func:`jrag_render.has_results` so output and exit code agree. Without
|
|
215
|
+
``--exists``, rc follows the envelope (error -> 2, else 0); ``--count`` does
|
|
216
|
+
not gate (a zero count on an ok envelope stays exit 0).
|
|
217
|
+
|
|
218
|
+
``getattr`` defaults keep this safe for parsers that lack the flags
|
|
219
|
+
(``_core_parser`` commands), though only ``_common_parser`` commands are
|
|
220
|
+
routed here today.
|
|
221
|
+
"""
|
|
222
|
+
from java_codebase_rag.jrag_render import has_results, render
|
|
223
|
+
|
|
224
|
+
print(render(
|
|
225
|
+
env,
|
|
226
|
+
fmt=getattr(args, "format", "text"),
|
|
227
|
+
detail=getattr(args, "detail", "normal"),
|
|
228
|
+
noun=noun,
|
|
229
|
+
next_offset=next_offset,
|
|
230
|
+
shape=shape,
|
|
231
|
+
count=getattr(args, "count", False),
|
|
232
|
+
exists=getattr(args, "exists", False),
|
|
233
|
+
fields=getattr(args, "fields", None),
|
|
234
|
+
))
|
|
235
|
+
if getattr(args, "exists", False):
|
|
236
|
+
return 0 if has_results(env, shape) else 2
|
|
237
|
+
return 2 if env.status == "error" else 0
|
|
238
|
+
|
|
239
|
+
|
|
198
240
|
def _render_listing(rows, *, limit: int, args: argparse.Namespace, noun: str,
|
|
199
241
|
extra_hints: list[str] | None = None) -> int:
|
|
200
242
|
"""Apply +1-fetch truncation, build the envelope, render as a listing.
|
|
@@ -209,7 +251,6 @@ def _render_listing(rows, *, limit: int, args: argparse.Namespace, noun: str,
|
|
|
209
251
|
(jobs / listeners / entities).
|
|
210
252
|
"""
|
|
211
253
|
from java_codebase_rag.jrag_envelope import Envelope, mark_truncated, next_actions_hook, to_envelope_rows
|
|
212
|
-
from java_codebase_rag.jrag_render import render
|
|
213
254
|
|
|
214
255
|
node_list = to_envelope_rows(rows) if rows and not isinstance(rows[0], dict) else list(rows)
|
|
215
256
|
display_nodes_list, truncated = mark_truncated(node_list, limit)
|
|
@@ -227,8 +268,7 @@ def _render_listing(rows, *, limit: int, args: argparse.Namespace, noun: str,
|
|
|
227
268
|
seen.add(h)
|
|
228
269
|
env.agent_next_actions.append(h)
|
|
229
270
|
env.agent_next_actions = env.agent_next_actions[:5]
|
|
230
|
-
|
|
231
|
-
return 0
|
|
271
|
+
return _emit(env, args, noun=noun)
|
|
232
272
|
|
|
233
273
|
|
|
234
274
|
def _symbol_hit_to_dict(hit) -> dict:
|
|
@@ -429,6 +469,43 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
429
469
|
"normal = +module/role/file/score; full = +signature/annotations/snippet."
|
|
430
470
|
),
|
|
431
471
|
)
|
|
472
|
+
# Output-shaping flags (issue #376). NOT on _core_parser: status /
|
|
473
|
+
# microservices are aggregate rollups (a count there is meaningless) and
|
|
474
|
+
# vocab-index prints plain text outside the render path, so adding them
|
|
475
|
+
# there would create silently-ignored flags (violates the
|
|
476
|
+
# "inapplicable flags never silently ignored" principle).
|
|
477
|
+
common.add_argument(
|
|
478
|
+
"--count",
|
|
479
|
+
action="store_true",
|
|
480
|
+
default=False,
|
|
481
|
+
help=(
|
|
482
|
+
"Print only the result count (no rows) — bare int in text, "
|
|
483
|
+
"{\"status\",\"count\"} in json. Counts nodes (listing), edges "
|
|
484
|
+
"(traversal), or 1 (inspect)."
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
common.add_argument(
|
|
488
|
+
"--exists",
|
|
489
|
+
action="store_true",
|
|
490
|
+
default=False,
|
|
491
|
+
help=(
|
|
492
|
+
"Print only an exists boolean (true/false, or "
|
|
493
|
+
"{\"status\",\"exists\"} in json). Exit 0 when results exist, "
|
|
494
|
+
"2 otherwise (incl. resolve miss / empty result)."
|
|
495
|
+
),
|
|
496
|
+
)
|
|
497
|
+
common.add_argument(
|
|
498
|
+
"--fields",
|
|
499
|
+
type=str,
|
|
500
|
+
default=None,
|
|
501
|
+
metavar="LIST",
|
|
502
|
+
help=(
|
|
503
|
+
"Comma-separated node-field allowlist that overrides --detail "
|
|
504
|
+
"(e.g. fqn,role,signature). Ignored with --count/--exists; "
|
|
505
|
+
"primarily a --format json lever; text still labels rows from "
|
|
506
|
+
"whatever identity fields survive."
|
|
507
|
+
),
|
|
508
|
+
)
|
|
432
509
|
return common
|
|
433
510
|
|
|
434
511
|
# Core-only parser for AGGREGATE commands (status / microservices) that have
|
|
@@ -484,7 +561,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
484
561
|
parents=[_common_parser()],
|
|
485
562
|
description=(
|
|
486
563
|
"Find nodes by query or filter. Two modes:\n"
|
|
487
|
-
" Query mode (positional <query>): search by
|
|
564
|
+
" Query mode (positional <query>): search by name/FQN (symbols only); --fuzzy\n"
|
|
565
|
+
" falls back exact -> prefix -> substring when the exact match is empty.\n"
|
|
488
566
|
" Filter mode (no positional): apply structured filters (NodeFilter flags).\n"
|
|
489
567
|
"Kind inference: domain flags (--http-method, --client-kind, --producer-kind) imply\n"
|
|
490
568
|
"route/client/producer when --kind is omitted. Contradiction emits an error envelope.\n"
|
|
@@ -507,6 +585,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
507
585
|
find.add_argument("--framework", type=_lower_snake, choices=_FRAMEWORK_CHOICES, default=None, help="Filter by framework.")
|
|
508
586
|
find.add_argument("--source-layer", type=str, default=None, help="Filter by source layer.")
|
|
509
587
|
find.add_argument("--fqn-contains", type=str, default=None, help="Filter by FQN substring.")
|
|
588
|
+
find.add_argument(
|
|
589
|
+
"--fuzzy",
|
|
590
|
+
action="store_true",
|
|
591
|
+
help="Query mode: fall back from exact name/FQN to prefix then substring "
|
|
592
|
+
"(case-sensitive) when the exact match is empty.",
|
|
593
|
+
)
|
|
510
594
|
find.add_argument("--http-method", type=str, default=None, help="Filter by HTTP method (route).")
|
|
511
595
|
find.add_argument("--path-contains", type=str, default=None, help="Filter by path substring (route).")
|
|
512
596
|
find.add_argument("--client-kind", type=str, default=None, help="Filter by client kind (client).")
|
|
@@ -1108,9 +1192,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1108
1192
|
"--table all searches all three. --hybrid enables vector+keyword hybrid. "
|
|
1109
1193
|
"--offset paginates. --path-contains narrows by file path substring. "
|
|
1110
1194
|
"Filters (NodeFilter flags) narrow results.\n\n"
|
|
1111
|
-
"--fuzzy is accepted
|
|
1112
|
-
"
|
|
1113
|
-
"
|
|
1195
|
+
"--fuzzy is accepted as a no-op (search is inherently semantic; "
|
|
1196
|
+
"--fuzzy is implicit). It is kept registered so callers that pass "
|
|
1197
|
+
"it don't hit an argparse error, and is silently ignored."
|
|
1114
1198
|
),
|
|
1115
1199
|
)
|
|
1116
1200
|
search.add_argument("query", help="Natural-language search query.")
|
|
@@ -1132,7 +1216,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1132
1216
|
)
|
|
1133
1217
|
search.add_argument(
|
|
1134
1218
|
"--fuzzy", action="store_true",
|
|
1135
|
-
help="Accepted
|
|
1219
|
+
help="Accepted as a no-op (search is always semantic; --fuzzy is implicit).",
|
|
1136
1220
|
)
|
|
1137
1221
|
search.add_argument(
|
|
1138
1222
|
"--min-score", type=float, default=0.0, dest="min_score",
|
|
@@ -1177,6 +1261,59 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1177
1261
|
)
|
|
1178
1262
|
vocab_index.set_defaults(handler=_cmd_vocab_index, detail="full")
|
|
1179
1263
|
|
|
1264
|
+
# ---- watch subparser (jrag watch foreground/detach/stop/status) ----
|
|
1265
|
+
# Uses _core_parser (no auto-scope): watch is a long-lived daemon over a
|
|
1266
|
+
# whole index, not a per-query command, so --service/--module/--limit would
|
|
1267
|
+
# be dishonest on this surface. Keeps --index-dir/--format/--detail so the
|
|
1268
|
+
# daemon anchors and so --status output respects --format.
|
|
1269
|
+
watch = subparsers.add_parser(
|
|
1270
|
+
"watch",
|
|
1271
|
+
help="keep the index fresh and serve warm queries while running",
|
|
1272
|
+
parents=[_core_parser()],
|
|
1273
|
+
description=(
|
|
1274
|
+
"Long-lived daemon: watches the source tree for changes (reindexing "
|
|
1275
|
+
"vectors/graph on a debounce) and serves the read commands (search/find/"
|
|
1276
|
+
"inspect/callers/callees/flow) over a warm Unix socket so queries skip the "
|
|
1277
|
+
"cold-start model/graph load.\n\n"
|
|
1278
|
+
"Lifecycle:\n"
|
|
1279
|
+
" jrag watch run in the foreground (Ctrl+C / SIGTERM to stop)\n"
|
|
1280
|
+
" jrag watch --detach start as a background daemon and return\n"
|
|
1281
|
+
" jrag watch --status report up/down + pid + socket + last reindex\n"
|
|
1282
|
+
" jrag watch --stop SIGTERM a running daemon (SIGKILL after 5s)\n"
|
|
1283
|
+
"Only one daemon may run per index (project lock). --status/--stop do NOT "
|
|
1284
|
+
"acquire the lock."
|
|
1285
|
+
),
|
|
1286
|
+
)
|
|
1287
|
+
watch.add_argument(
|
|
1288
|
+
"--detach",
|
|
1289
|
+
action="store_true",
|
|
1290
|
+
help="Start the daemon as a detached background process and return.",
|
|
1291
|
+
)
|
|
1292
|
+
watch.add_argument(
|
|
1293
|
+
"--stop",
|
|
1294
|
+
action="store_true",
|
|
1295
|
+
help="Stop a running daemon (SIGTERM; SIGKILL after 5s).",
|
|
1296
|
+
)
|
|
1297
|
+
watch.add_argument(
|
|
1298
|
+
"--status",
|
|
1299
|
+
action="store_true",
|
|
1300
|
+
help="Print whether the daemon is up or down and exit.",
|
|
1301
|
+
)
|
|
1302
|
+
watch.add_argument(
|
|
1303
|
+
"--debounce-ms",
|
|
1304
|
+
type=int,
|
|
1305
|
+
default=None,
|
|
1306
|
+
dest="debounce_ms",
|
|
1307
|
+
help="Reindex debounce window in ms (overrides YAML `watch:debounce_ms`).",
|
|
1308
|
+
)
|
|
1309
|
+
watch.add_argument(
|
|
1310
|
+
"--backend",
|
|
1311
|
+
choices=("auto", "watchdog", "polling"),
|
|
1312
|
+
default=None,
|
|
1313
|
+
help="File-watch backend (overrides YAML `watch:backend`).",
|
|
1314
|
+
)
|
|
1315
|
+
watch.set_defaults(handler=_cmd_watch)
|
|
1316
|
+
|
|
1180
1317
|
return parser
|
|
1181
1318
|
|
|
1182
1319
|
|
|
@@ -1204,6 +1341,11 @@ def _resolve_cfg(args: argparse.Namespace): # type: ignore[no-untyped-def]
|
|
|
1204
1341
|
cfg = resolve_operator_config(
|
|
1205
1342
|
source_root=None,
|
|
1206
1343
|
cli_index_dir=getattr(args, "index_dir", None),
|
|
1344
|
+
# ``jrag watch`` CLI overrides for the watch block (absent / None for
|
|
1345
|
+
# every other subcommand via getattr default; resolve_operator_config
|
|
1346
|
+
# treats ``None`` as "not provided" so non-watch commands are unaffected).
|
|
1347
|
+
cli_watch_debounce_ms=getattr(args, "debounce_ms", None),
|
|
1348
|
+
cli_watch_backend=getattr(args, "backend", None),
|
|
1207
1349
|
)
|
|
1208
1350
|
cfg.apply_to_os_environ()
|
|
1209
1351
|
return cfg
|
|
@@ -1265,6 +1407,269 @@ def _cmd_vocab_index(args: argparse.Namespace) -> int:
|
|
|
1265
1407
|
return 0
|
|
1266
1408
|
|
|
1267
1409
|
|
|
1410
|
+
# ---------------------------------------------------------------------------
|
|
1411
|
+
# jrag watch — long-lived daemon lifecycle (foreground / --detach / --stop / --status)
|
|
1412
|
+
#
|
|
1413
|
+
# ``--status``/``--stop`` are OUT-OF-PROCESS verbs: they read the lock holder
|
|
1414
|
+
# (``ProjectLock.read_holder``) and never acquire the lock themselves. Only the
|
|
1415
|
+
# running daemon (foreground or detached) holds the lock.
|
|
1416
|
+
# ---------------------------------------------------------------------------
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def _watch_child_argv(extra_args: list[str]) -> list[str]:
|
|
1420
|
+
"""Build the argv for the detached ``jrag watch`` child process.
|
|
1421
|
+
|
|
1422
|
+
Invokes the daemon via ``python -m java_codebase_rag.jrag`` (NOT the
|
|
1423
|
+
module's file path): running ``python src/.../jrag.py`` directly would put
|
|
1424
|
+
the package directory on ``sys.path[0]`` and shadow the stdlib ``ast``
|
|
1425
|
+
module with the project's ``java_codebase_rag.ast`` package (breaking
|
|
1426
|
+
``inspect``). ``-m`` runs the module as ``__main__`` within its package
|
|
1427
|
+
context, so stdlib imports resolve correctly. A separate function (rather
|
|
1428
|
+
than inline) so a test can swap in a stub child.
|
|
1429
|
+
"""
|
|
1430
|
+
return [sys.executable, "-m", "java_codebase_rag.jrag", "watch"] + list(extra_args)
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
def _watch_passthrough_args(args: argparse.Namespace) -> list[str]:
|
|
1434
|
+
"""Reconstruct the watch flags to pass through to a detached child.
|
|
1435
|
+
|
|
1436
|
+
Only re-emits the flags that influence the daemon's behavior; --index-dir is
|
|
1437
|
+
included so the child anchors on the same index without re-discovering it.
|
|
1438
|
+
"""
|
|
1439
|
+
out: list[str] = []
|
|
1440
|
+
if getattr(args, "index_dir", None):
|
|
1441
|
+
out += ["--index-dir", str(args.index_dir)]
|
|
1442
|
+
if getattr(args, "debounce_ms", None) is not None:
|
|
1443
|
+
out += ["--debounce-ms", str(args.debounce_ms)]
|
|
1444
|
+
if getattr(args, "backend", None) is not None:
|
|
1445
|
+
out += ["--backend", str(args.backend)]
|
|
1446
|
+
return out
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def _cmd_watch_status(cfg) -> int:
|
|
1450
|
+
"""``jrag watch --status``: print up/down + pid + socket + last reindex.
|
|
1451
|
+
|
|
1452
|
+
Does NOT acquire the lock. Returns 0 if a daemon is alive, 1 otherwise.
|
|
1453
|
+
"""
|
|
1454
|
+
from java_codebase_rag.watch import paths
|
|
1455
|
+
from java_codebase_rag.watch.client import is_daemon_alive
|
|
1456
|
+
from java_codebase_rag.watch.lock import ProjectLock
|
|
1457
|
+
|
|
1458
|
+
sock = paths.socket_path(cfg.index_dir)
|
|
1459
|
+
alive = is_daemon_alive(cfg.index_dir)
|
|
1460
|
+
pid = ProjectLock.read_holder(cfg.index_dir)
|
|
1461
|
+
state = _read_state_file(cfg.index_dir)
|
|
1462
|
+
if alive:
|
|
1463
|
+
print(f"jrag watch: up (pid {pid}, socket {sock})")
|
|
1464
|
+
if state:
|
|
1465
|
+
kind = state.get("last_reindex_kind")
|
|
1466
|
+
at = state.get("last_reindex_at")
|
|
1467
|
+
count = state.get("reindex_count", 0)
|
|
1468
|
+
if kind and at:
|
|
1469
|
+
when = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(at))
|
|
1470
|
+
print(f" last reindex: {kind} at {when} (total {count})")
|
|
1471
|
+
else:
|
|
1472
|
+
print(f" last reindex: none (total {count})")
|
|
1473
|
+
return 0
|
|
1474
|
+
print(f"jrag watch: down (no daemon at {sock})")
|
|
1475
|
+
return 1
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
def _cmd_watch_stop(cfg) -> int:
|
|
1479
|
+
"""``jrag watch --stop``: SIGTERM the daemon, SIGKILL after 5s if needed.
|
|
1480
|
+
|
|
1481
|
+
Polls for socket removal (the daemon's own shutdown unlinks it). Always
|
|
1482
|
+
cleans a leftover socket/state so a fresh start isn't blocked by a corpse.
|
|
1483
|
+
Returns 0 if a daemon was stopped, 1 if none was running.
|
|
1484
|
+
"""
|
|
1485
|
+
from java_codebase_rag.watch import paths
|
|
1486
|
+
from java_codebase_rag.watch.lock import ProjectLock
|
|
1487
|
+
|
|
1488
|
+
sock = paths.socket_path(cfg.index_dir)
|
|
1489
|
+
state_path = paths.state_path(cfg.index_dir)
|
|
1490
|
+
pid = ProjectLock.read_holder(cfg.index_dir)
|
|
1491
|
+
if pid is None:
|
|
1492
|
+
print("jrag watch: not running")
|
|
1493
|
+
_watch_unlink(sock)
|
|
1494
|
+
_watch_unlink(state_path)
|
|
1495
|
+
return 1
|
|
1496
|
+
|
|
1497
|
+
_watch_signal(pid, signal.SIGTERM)
|
|
1498
|
+
# Poll for the socket's removal (the daemon unlinks it on clean shutdown).
|
|
1499
|
+
deadline = time.monotonic() + _WATCH_STOP_TIMEOUT_S
|
|
1500
|
+
while time.monotonic() < deadline:
|
|
1501
|
+
if not sock.exists():
|
|
1502
|
+
break
|
|
1503
|
+
if not _watch_pid_alive(pid):
|
|
1504
|
+
break
|
|
1505
|
+
time.sleep(0.05)
|
|
1506
|
+
# If still alive after the timeout, escalate to SIGKILL.
|
|
1507
|
+
if _watch_pid_alive(pid):
|
|
1508
|
+
_watch_signal(pid, signal.SIGKILL)
|
|
1509
|
+
_watch_unlink(sock)
|
|
1510
|
+
_watch_unlink(state_path)
|
|
1511
|
+
print(f"jrag watch: stopped (pid {pid})")
|
|
1512
|
+
return 0
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def _cmd_watch_detach(args: argparse.Namespace, cfg) -> int:
|
|
1516
|
+
"""``jrag watch --detach``: spawn the daemon detached and wait until it serves.
|
|
1517
|
+
|
|
1518
|
+
``start_new_session=True`` detaches the child from the controlling terminal
|
|
1519
|
+
(setsid); stdio is redirected to a per-index log under ``paths.runtime_dir``
|
|
1520
|
+
so the parent can return. Waits until ``is_daemon_alive`` (socket bound AND a
|
|
1521
|
+
live holder pid) or a timeout, then prints the socket path + pid. Returns 0
|
|
1522
|
+
on success, 2 on timeout / child exit.
|
|
1523
|
+
"""
|
|
1524
|
+
import subprocess
|
|
1525
|
+
|
|
1526
|
+
from java_codebase_rag.watch import paths
|
|
1527
|
+
from java_codebase_rag.watch.client import is_daemon_alive
|
|
1528
|
+
from java_codebase_rag.watch.lock import ProjectLock
|
|
1529
|
+
|
|
1530
|
+
child_argv = _watch_child_argv(_watch_passthrough_args(args))
|
|
1531
|
+
log_path = paths.runtime_dir() / f"jrag-watch-{paths.project_key(cfg.index_dir)}.log"
|
|
1532
|
+
try:
|
|
1533
|
+
log_fh = open(log_path, "ab")
|
|
1534
|
+
except OSError:
|
|
1535
|
+
log_fh = None
|
|
1536
|
+
try:
|
|
1537
|
+
proc = subprocess.Popen(
|
|
1538
|
+
child_argv,
|
|
1539
|
+
stdin=subprocess.DEVNULL,
|
|
1540
|
+
stdout=log_fh,
|
|
1541
|
+
stderr=log_fh,
|
|
1542
|
+
start_new_session=True, # setsid: detach from the controlling terminal
|
|
1543
|
+
close_fds=True,
|
|
1544
|
+
)
|
|
1545
|
+
finally:
|
|
1546
|
+
if log_fh is not None:
|
|
1547
|
+
log_fh.close()
|
|
1548
|
+
|
|
1549
|
+
deadline = time.monotonic() + _WATCH_DETACH_TIMEOUT_S
|
|
1550
|
+
child_exited = False
|
|
1551
|
+
while time.monotonic() < deadline:
|
|
1552
|
+
if is_daemon_alive(cfg.index_dir):
|
|
1553
|
+
break
|
|
1554
|
+
# Fail fast: a child that crashes on startup (model-load/import failure)
|
|
1555
|
+
# should not make the parent wait the whole timeout. proc.poll() is None
|
|
1556
|
+
# while the child lives; a non-None return code means it has exited.
|
|
1557
|
+
if proc.poll() is not None:
|
|
1558
|
+
child_exited = True
|
|
1559
|
+
break
|
|
1560
|
+
time.sleep(0.1)
|
|
1561
|
+
if is_daemon_alive(cfg.index_dir):
|
|
1562
|
+
pid = ProjectLock.read_holder(cfg.index_dir)
|
|
1563
|
+
print(
|
|
1564
|
+
f"jrag watch: detached (pid {pid}, socket "
|
|
1565
|
+
f"{paths.socket_path(cfg.index_dir)}, log {log_path})"
|
|
1566
|
+
)
|
|
1567
|
+
return 0
|
|
1568
|
+
if child_exited:
|
|
1569
|
+
print(
|
|
1570
|
+
f"jrag watch: child exited before serving (see {log_path})",
|
|
1571
|
+
file=sys.stderr,
|
|
1572
|
+
)
|
|
1573
|
+
else:
|
|
1574
|
+
print(
|
|
1575
|
+
f"jrag watch: failed to start within {_WATCH_DETACH_TIMEOUT_S}s "
|
|
1576
|
+
f"(see {log_path})",
|
|
1577
|
+
file=sys.stderr,
|
|
1578
|
+
)
|
|
1579
|
+
return 2
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _cmd_watch(args: argparse.Namespace) -> int:
|
|
1583
|
+
"""Dispatch ``jrag watch`` to its lifecycle verb (or the foreground daemon).
|
|
1584
|
+
|
|
1585
|
+
The lightweight probe verbs (``--status``/``--stop``/``--detach``) must NOT
|
|
1586
|
+
import the daemon module: that import eagerly pulls torch/
|
|
1587
|
+
sentence_transformers/lancedb/pyarrow (~2.5s + ~1GB), defeating their purpose.
|
|
1588
|
+
``WatchDaemon`` is therefore imported inline ONLY on the foreground path below.
|
|
1589
|
+
"""
|
|
1590
|
+
cfg = _resolve_cfg(args)
|
|
1591
|
+
if args.status:
|
|
1592
|
+
return _cmd_watch_status(cfg)
|
|
1593
|
+
if args.stop:
|
|
1594
|
+
return _cmd_watch_stop(cfg)
|
|
1595
|
+
if args.detach:
|
|
1596
|
+
return _cmd_watch_detach(args, cfg)
|
|
1597
|
+
# default: run the daemon in the foreground. Ends with os._exit(0) on the
|
|
1598
|
+
# serving path; only the early-failure returns (lock held / model load) come
|
|
1599
|
+
# back here with a non-zero int.
|
|
1600
|
+
from java_codebase_rag.watch.daemon import WatchDaemon
|
|
1601
|
+
|
|
1602
|
+
return WatchDaemon(cfg).run_foreground()
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
# Small lifecycle helpers (kept here, not in daemon.py, so --stop/--status have
|
|
1606
|
+
# zero coupling to the heavy daemon import path).
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
def _watch_pid_alive(pid: int) -> bool:
|
|
1610
|
+
"""True iff ``pid`` is currently a live process (best-effort signal-0 probe)."""
|
|
1611
|
+
try:
|
|
1612
|
+
os.kill(pid, 0)
|
|
1613
|
+
except ProcessLookupError:
|
|
1614
|
+
return False
|
|
1615
|
+
except PermissionError:
|
|
1616
|
+
return True # exists, just not signalable by us
|
|
1617
|
+
except OSError:
|
|
1618
|
+
return False
|
|
1619
|
+
return True
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
def _watch_signal(pid: int, sig: int) -> None:
|
|
1623
|
+
"""Send ``sig`` to ``pid``, swallowing ProcessLookupError (already gone)."""
|
|
1624
|
+
try:
|
|
1625
|
+
os.kill(pid, sig)
|
|
1626
|
+
except ProcessLookupError:
|
|
1627
|
+
pass
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
def _watch_unlink(path) -> None:
|
|
1631
|
+
"""Idempotent, best-effort unlink."""
|
|
1632
|
+
try:
|
|
1633
|
+
path.unlink()
|
|
1634
|
+
except FileNotFoundError:
|
|
1635
|
+
pass
|
|
1636
|
+
except OSError:
|
|
1637
|
+
pass
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
def _read_state_file(index_dir) -> dict | None:
|
|
1641
|
+
"""Return the parsed daemon state JSON, or ``None`` if missing/unreadable.
|
|
1642
|
+
|
|
1643
|
+
Kept HERE (not in ``watch.daemon``) so ``jrag watch --status`` can read the
|
|
1644
|
+
last reindex WITHOUT importing the daemon module — that import eagerly pulls
|
|
1645
|
+
torch/sentence_transformers/lancedb/pyarrow (~2.5s + ~1GB). A corrupt/partial
|
|
1646
|
+
file yields ``None`` rather than raising.
|
|
1647
|
+
"""
|
|
1648
|
+
import json
|
|
1649
|
+
|
|
1650
|
+
from java_codebase_rag.watch import paths
|
|
1651
|
+
|
|
1652
|
+
path = paths.state_path(index_dir)
|
|
1653
|
+
try:
|
|
1654
|
+
raw = path.read_text()
|
|
1655
|
+
except (FileNotFoundError, OSError):
|
|
1656
|
+
return None
|
|
1657
|
+
try:
|
|
1658
|
+
obj = json.loads(raw)
|
|
1659
|
+
except (ValueError, OSError):
|
|
1660
|
+
return None
|
|
1661
|
+
return obj if isinstance(obj, dict) else None
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
# The daemon's shutdown (watcher.stop joins the debounce thread up to 10s, then
|
|
1665
|
+
# server.shutdown joins the accept thread up to 2s) is well under this on an
|
|
1666
|
+
# idle watcher; 5s is the brief's prescribed SIGTERM->SIGKILL grace window.
|
|
1667
|
+
_WATCH_STOP_TIMEOUT_S = 5.0
|
|
1668
|
+
# Model warm-up dominates the detach readiness window on a cold cache; generous
|
|
1669
|
+
# so a fresh start isn't reported as a failure while the SBERT model loads.
|
|
1670
|
+
_WATCH_DETACH_TIMEOUT_S = 60.0
|
|
1671
|
+
|
|
1672
|
+
|
|
1268
1673
|
def _cmd_status(args: argparse.Namespace) -> int:
|
|
1269
1674
|
from java_codebase_rag.jrag_envelope import Envelope
|
|
1270
1675
|
from java_codebase_rag.jrag_render import render
|
|
@@ -1369,6 +1774,8 @@ def _check_kind_contradiction(args: argparse.Namespace, inferred: str | None) ->
|
|
|
1369
1774
|
def _cmd_find(args: argparse.Namespace) -> int:
|
|
1370
1775
|
from java_codebase_rag.jrag_envelope import Envelope
|
|
1371
1776
|
from java_codebase_rag.jrag_render import render
|
|
1777
|
+
from java_codebase_rag.read_payloads import PayloadError, find_payload
|
|
1778
|
+
from java_codebase_rag.watch.client import get_payload
|
|
1372
1779
|
|
|
1373
1780
|
cfg = _resolve_cfg(args)
|
|
1374
1781
|
try:
|
|
@@ -1382,104 +1789,40 @@ def _cmd_find(args: argparse.Namespace) -> int:
|
|
|
1382
1789
|
# auto-scope default here too (MCP parity).
|
|
1383
1790
|
_apply_auto_scope(args, cfg, graph)
|
|
1384
1791
|
|
|
1385
|
-
#
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1792
|
+
# find_payload does the mode selection (query vs filter), kind-contradiction
|
|
1793
|
+
# check, and the backend call (find_by_name_or_fqn + post-filters, or find_v2).
|
|
1794
|
+
# Rendering (nodes/warnings/empty-result hint/offset) is split into the two
|
|
1795
|
+
# render helpers below, branch by payload["mode"].
|
|
1796
|
+
try:
|
|
1797
|
+
payload = get_payload("find", vars(args), cfg, cold_core=find_payload)
|
|
1798
|
+
except PayloadError as pe:
|
|
1799
|
+
print(render(pe.env, fmt=args.format, detail=args.detail))
|
|
1800
|
+
return pe.rc
|
|
1392
1801
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
limit = min(raw_limit, 499)
|
|
1397
|
-
|
|
1398
|
-
# Query mode: positional <query> present
|
|
1399
|
-
if args.query:
|
|
1400
|
-
# find_by_name_or_fqn is Symbol-only (MATCH (s:Symbol) WHERE s.name=$needle
|
|
1401
|
-
# OR s.fqn=$needle). A positional <query> with a non-symbol kind (explicit
|
|
1402
|
-
# OR inferred from --http-method/--client-kind/--producer-kind/etc.) is a
|
|
1403
|
-
# usage contract violation -> status: error envelope (NOT argparse exit),
|
|
1404
|
-
# telling the user to drop the positional and use filter mode.
|
|
1405
|
-
effective_kind = inferred or "symbol"
|
|
1406
|
-
if effective_kind != "symbol":
|
|
1407
|
-
env = Envelope(
|
|
1408
|
-
status="error",
|
|
1409
|
-
message=(
|
|
1410
|
-
f"query mode (positional <query>) only searches Symbols, but kind "
|
|
1411
|
-
f"'{effective_kind}' was {'inferred from domain flags' if args.kind is None else 'set via --kind'}. "
|
|
1412
|
-
"Drop the positional <query> and use filter mode (the domain flags) "
|
|
1413
|
-
"for route/client/producer searches."
|
|
1414
|
-
),
|
|
1415
|
-
)
|
|
1416
|
-
print(render(env, fmt=args.format, detail=args.detail))
|
|
1417
|
-
return 2
|
|
1418
|
-
return _cmd_find_query_mode(args, cfg, graph, limit)
|
|
1802
|
+
if payload["mode"] == "query":
|
|
1803
|
+
return _render_find_query(args, payload)
|
|
1804
|
+
return _render_find_filter(args, payload)
|
|
1419
1805
|
|
|
1420
|
-
# Filter mode: build NodeFilter and call find_v2
|
|
1421
|
-
return _cmd_find_filter_mode(args, cfg, graph, inferred or "symbol", limit)
|
|
1422
1806
|
|
|
1807
|
+
def _render_find_query(args: argparse.Namespace, payload) -> int:
|
|
1808
|
+
"""Render find query-mode payload (rows from find_by_name_or_fqn + post-filters).
|
|
1423
1809
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
) -> int:
|
|
1430
|
-
"""Find query mode: g.find_by_name_or_fqn (Symbol-only, exact name/FQN match).
|
|
1431
|
-
|
|
1432
|
-
``find_by_name_or_fqn`` runs ``MATCH (s:Symbol) WHERE s.name=$needle OR
|
|
1433
|
-
s.fqn=$needle`` — Symbol-only, exact-only. There is no fuzzy/prefix/contains
|
|
1434
|
-
path; ``--fuzzy`` was deferred (see plans/active/PLAN-JRAG-CLI.md Out of
|
|
1435
|
-
scope). Query mode is gated to ``effective_kind == "symbol"`` upstream in
|
|
1436
|
-
``_cmd_find``, so the only ``kinds`` filter we may pass is symbol sub-kinds
|
|
1437
|
-
derived from ``--java-kind``.
|
|
1810
|
+
The backend call + post-filters live in ``read_payloads.find_payload``; this
|
|
1811
|
+
builds the envelope node dicts, warnings, and empty-result hint, then renders.
|
|
1812
|
+
With ``--fuzzy``, ``find_payload`` widens an empty exact result to prefix then
|
|
1813
|
+
substring (issue #375) and reports the matched tier via ``payload["matched_mode"]``
|
|
1814
|
+
(exact/prefix/contains) plus ``payload["identifier_matched"]`` for the hint.
|
|
1438
1815
|
"""
|
|
1439
|
-
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook
|
|
1816
|
+
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook
|
|
1440
1817
|
from java_codebase_rag.jrag_render import render
|
|
1441
1818
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
kinds = [java_kind_norm.lower()]
|
|
1450
|
-
else:
|
|
1451
|
-
kinds = None
|
|
1452
|
-
|
|
1453
|
-
# Call find_by_name_or_fqn (exact name OR fqn match).
|
|
1454
|
-
rows = graph.find_by_name_or_fqn(
|
|
1455
|
-
query,
|
|
1456
|
-
kinds=kinds,
|
|
1457
|
-
module=args.module,
|
|
1458
|
-
microservice=args.service,
|
|
1459
|
-
limit=limit + 1, # +1 for truncated detection
|
|
1460
|
-
)
|
|
1461
|
-
# Truncation is decided by the RAW name/FQN fetch (limit+1), BEFORE
|
|
1462
|
-
# post-filters reduce the set — otherwise a post-filter that drops rows
|
|
1463
|
-
# would silently clear `truncated` even though more name matches may exist
|
|
1464
|
-
# beyond the fetch (silent wrong-results).
|
|
1465
|
-
raw_truncated = len(rows) > limit
|
|
1466
|
-
|
|
1467
|
-
# Post-filter by role/annotation/capability (SymbolHit carries these).
|
|
1468
|
-
post_filter_active = False
|
|
1469
|
-
if args.role:
|
|
1470
|
-
post_filter_active = True
|
|
1471
|
-
role_norm = normalize_enum(args.role, kind="role")
|
|
1472
|
-
rows = [r for r in rows if (r.role or "").upper().replace("-", "_") == role_norm.upper()]
|
|
1473
|
-
if args.exclude_role:
|
|
1474
|
-
post_filter_active = True
|
|
1475
|
-
exclude_role_norm = normalize_enum(args.exclude_role, kind="role")
|
|
1476
|
-
rows = [r for r in rows if (r.role or "").upper().replace("-", "_") != exclude_role_norm.upper()]
|
|
1477
|
-
if args.annotation:
|
|
1478
|
-
post_filter_active = True
|
|
1479
|
-
rows = [r for r in rows if args.annotation in (r.annotations or [])]
|
|
1480
|
-
if args.capability:
|
|
1481
|
-
post_filter_active = True
|
|
1482
|
-
rows = [r for r in rows if args.capability in (r.capabilities or [])]
|
|
1819
|
+
rows = payload["rows"]
|
|
1820
|
+
raw_truncated = payload["raw_truncated"]
|
|
1821
|
+
post_filter_active = payload["post_filter_active"]
|
|
1822
|
+
limit = payload["limit"]
|
|
1823
|
+
query = payload["query"]
|
|
1824
|
+
matched_mode = payload["matched_mode"]
|
|
1825
|
+
identifier_matched = payload["identifier_matched"]
|
|
1483
1826
|
|
|
1484
1827
|
# Build warnings for filters that cannot apply in query mode. SymbolHit
|
|
1485
1828
|
# carries no framework/source_layer fields; rather than silently dropping
|
|
@@ -1504,6 +1847,12 @@ def _cmd_find_query_mode(
|
|
|
1504
1847
|
|
|
1505
1848
|
# Display at most `limit` of the (post-filtered) rows.
|
|
1506
1849
|
display_rows = rows[:limit]
|
|
1850
|
+
# Map internal mode -> user-facing term (help/empty-hint say "substring").
|
|
1851
|
+
mode_label = "substring" if matched_mode == "contains" else matched_mode
|
|
1852
|
+
if matched_mode != "exact" and display_rows:
|
|
1853
|
+
warnings.append(
|
|
1854
|
+
f"no exact name/FQN match; --fuzzy matched via {mode_label}"
|
|
1855
|
+
)
|
|
1507
1856
|
nodes = {}
|
|
1508
1857
|
for row in display_rows:
|
|
1509
1858
|
node_id = row.id
|
|
@@ -1539,21 +1888,29 @@ def _cmd_find_query_mode(
|
|
|
1539
1888
|
)
|
|
1540
1889
|
next_actions_hook(env)
|
|
1541
1890
|
|
|
1542
|
-
# Empty-result discoverability:
|
|
1543
|
-
#
|
|
1544
|
-
#
|
|
1545
|
-
#
|
|
1546
|
-
#
|
|
1547
|
-
#
|
|
1548
|
-
# hint applies regardless of shape.
|
|
1891
|
+
# Empty-result discoverability: a partial like `find ChatManag` returns 0
|
|
1892
|
+
# under exact match. Three cases: (1) some tier matched the identifier but
|
|
1893
|
+
# --role/--exclude-role/--annotation/--capability removed every hit — blame
|
|
1894
|
+
# the filter, not the query; (2) --fuzzy widened to prefix/substring and
|
|
1895
|
+
# still found nothing; (3) no --fuzzy, so suggest it. Carried as `message`
|
|
1896
|
+
# (inline) + `agent_next_actions` (`next:`/JSON).
|
|
1549
1897
|
if not nodes and query:
|
|
1550
|
-
|
|
1898
|
+
if identifier_matched and post_filter_active:
|
|
1899
|
+
hint = (
|
|
1900
|
+
f"matched {query!r} (via {mode_label}), but "
|
|
1901
|
+
"--role/--exclude-role/--annotation/--capability removed all hits"
|
|
1902
|
+
)
|
|
1903
|
+
elif getattr(args, "fuzzy", False):
|
|
1904
|
+
hint = f"no match for {query!r} (tried exact, prefix, substring)"
|
|
1905
|
+
env.agent_next_actions = [f"jrag find --fqn-contains {query}"]
|
|
1906
|
+
else:
|
|
1907
|
+
hint = f"no exact match for {query!r} — try `jrag find {query} --fuzzy`"
|
|
1908
|
+
env.agent_next_actions = [f"jrag find {query} --fuzzy"]
|
|
1551
1909
|
env.message = hint
|
|
1552
|
-
env.agent_next_actions = [f"jrag find --fqn-contains {query}"]
|
|
1553
1910
|
|
|
1554
1911
|
# Offset is not supported in query mode (find_by_name_or_fqn has no offset).
|
|
1555
|
-
|
|
1556
|
-
|
|
1912
|
+
return _emit(env, args, noun="symbol")
|
|
1913
|
+
|
|
1557
1914
|
|
|
1558
1915
|
|
|
1559
1916
|
def _build_node_filter_or_error(filter_dict: dict):
|
|
@@ -1583,69 +1940,20 @@ def _build_node_filter_or_error(filter_dict: dict):
|
|
|
1583
1940
|
return None, Envelope(status="error", message=f"invalid filter: {message}")
|
|
1584
1941
|
|
|
1585
1942
|
|
|
1586
|
-
def
|
|
1587
|
-
|
|
1588
|
-
cfg,
|
|
1589
|
-
graph,
|
|
1590
|
-
kind: str,
|
|
1591
|
-
limit: int,
|
|
1592
|
-
) -> int:
|
|
1593
|
-
"""Find filter mode: build NodeFilter and call find_v2."""
|
|
1594
|
-
from java_codebase_rag.mcp import mcp_v2
|
|
1943
|
+
def _render_find_filter(args: argparse.Namespace, payload) -> int:
|
|
1944
|
+
"""Render find filter-mode payload (a FindOutput from find_v2).
|
|
1595
1945
|
|
|
1596
|
-
|
|
1946
|
+
The backend call + NodeFilter construction live in
|
|
1947
|
+
``read_payloads.find_payload``; this slices to the limit, builds the envelope
|
|
1948
|
+
node dicts, and renders — verbatim from the original
|
|
1949
|
+
``_cmd_find_filter_mode`` render portion.
|
|
1950
|
+
"""
|
|
1951
|
+
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook, to_envelope_rows
|
|
1597
1952
|
from java_codebase_rag.jrag_render import render
|
|
1598
1953
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
filter_dict["microservice"] = args.service
|
|
1603
|
-
if args.module:
|
|
1604
|
-
filter_dict["module"] = args.module
|
|
1605
|
-
if args.role:
|
|
1606
|
-
filter_dict["role"] = normalize_enum(args.role, kind="role")
|
|
1607
|
-
if args.exclude_role:
|
|
1608
|
-
filter_dict["exclude_roles"] = [normalize_enum(args.exclude_role, kind="role")]
|
|
1609
|
-
if args.annotation:
|
|
1610
|
-
filter_dict["annotation"] = args.annotation
|
|
1611
|
-
if args.capability:
|
|
1612
|
-
filter_dict["capability"] = args.capability
|
|
1613
|
-
if args.fqn_contains:
|
|
1614
|
-
filter_dict["fqn_contains"] = args.fqn_contains
|
|
1615
|
-
if args.java_kind:
|
|
1616
|
-
filter_dict["symbol_kind"] = normalize_enum(args.java_kind, kind="java_kind")
|
|
1617
|
-
if args.framework:
|
|
1618
|
-
filter_dict["framework"] = normalize_enum(args.framework, kind="framework")
|
|
1619
|
-
if args.source_layer:
|
|
1620
|
-
filter_dict["source_layer"] = normalize_enum(args.source_layer, kind="source_layer")
|
|
1621
|
-
if args.http_method:
|
|
1622
|
-
filter_dict["http_method"] = args.http_method.upper()
|
|
1623
|
-
if args.path_contains:
|
|
1624
|
-
filter_dict["path_contains"] = args.path_contains
|
|
1625
|
-
if args.client_kind:
|
|
1626
|
-
filter_dict["client_kind"] = normalize_enum(args.client_kind, kind="client_kind")
|
|
1627
|
-
if args.calls_service:
|
|
1628
|
-
filter_dict["target_service"] = args.calls_service
|
|
1629
|
-
if args.calls_path_contains:
|
|
1630
|
-
filter_dict["target_path_contains"] = args.calls_path_contains
|
|
1631
|
-
if args.producer_kind:
|
|
1632
|
-
filter_dict["producer_kind"] = normalize_enum(args.producer_kind, kind="producer_kind")
|
|
1633
|
-
if args.topic_contains:
|
|
1634
|
-
filter_dict["topic_contains"] = args.topic_contains
|
|
1635
|
-
|
|
1636
|
-
node_filter, err_env = _build_node_filter_or_error(filter_dict)
|
|
1637
|
-
if err_env is not None:
|
|
1638
|
-
print(render(err_env, fmt=args.format, detail=args.detail))
|
|
1639
|
-
return 2
|
|
1640
|
-
|
|
1641
|
-
# Call find_v2
|
|
1642
|
-
out = mcp_v2.find_v2(
|
|
1643
|
-
kind=kind,
|
|
1644
|
-
filter=node_filter,
|
|
1645
|
-
limit=limit + 1, # +1 for has_more_results detection
|
|
1646
|
-
offset=args.offset,
|
|
1647
|
-
graph=graph,
|
|
1648
|
-
)
|
|
1954
|
+
out = payload["out"]
|
|
1955
|
+
kind = payload["kind"]
|
|
1956
|
+
limit = payload["limit"]
|
|
1649
1957
|
|
|
1650
1958
|
if not out.success:
|
|
1651
1959
|
env = Envelope(status="error", message=out.message)
|
|
@@ -1669,14 +1977,12 @@ def _cmd_find_filter_mode(
|
|
|
1669
1977
|
|
|
1670
1978
|
# Render with offset hint if truncated
|
|
1671
1979
|
next_offset = args.offset + limit if truncated else None
|
|
1672
|
-
|
|
1673
|
-
return 0
|
|
1980
|
+
return _emit(env, args, noun=kind, next_offset=next_offset)
|
|
1674
1981
|
|
|
1675
1982
|
|
|
1676
|
-
def _cmd_inspect(args: argparse.Namespace) -> int:
|
|
1677
|
-
from java_codebase_rag.mcp import mcp_v2
|
|
1678
1983
|
|
|
1679
|
-
|
|
1984
|
+
def _cmd_inspect(args: argparse.Namespace) -> int:
|
|
1985
|
+
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook
|
|
1680
1986
|
from java_codebase_rag.jrag_render import render
|
|
1681
1987
|
|
|
1682
1988
|
cfg = _resolve_cfg(args)
|
|
@@ -1687,28 +1993,22 @@ def _cmd_inspect(args: argparse.Namespace) -> int:
|
|
|
1687
1993
|
print(render(env, fmt=args.format, detail=args.detail))
|
|
1688
1994
|
return 2
|
|
1689
1995
|
|
|
1690
|
-
#
|
|
1691
|
-
#
|
|
1692
|
-
#
|
|
1693
|
-
#
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
java_kind=args.java_kind,
|
|
1698
|
-
role=args.role,
|
|
1699
|
-
fqn_contains=args.fqn_contains,
|
|
1700
|
-
cfg=cfg,
|
|
1701
|
-
graph=graph,
|
|
1702
|
-
microservice=args.service or "",
|
|
1703
|
-
module=args.module or "",
|
|
1704
|
-
)
|
|
1705
|
-
|
|
1706
|
-
if env.status != "ok":
|
|
1707
|
-
print(render(env, fmt=args.format, detail=args.detail))
|
|
1708
|
-
return 2 if env.status == "error" else 0
|
|
1996
|
+
# inspect_payload resolves the query (forwarding --service/--module, same as
|
|
1997
|
+
# before) and calls describe_v2. It returns the DescribeOutput plus the
|
|
1998
|
+
# resolve-derived node id/fqn and file_location the flatten+render below
|
|
1999
|
+
# needs (file_location lives on the resolve Envelope, not on DescribeOutput).
|
|
2000
|
+
# On resolve failure it raises PayloadError carrying that Envelope + rc.
|
|
2001
|
+
from java_codebase_rag.read_payloads import PayloadError, inspect_payload
|
|
2002
|
+
from java_codebase_rag.watch.client import get_payload
|
|
1709
2003
|
|
|
1710
|
-
|
|
1711
|
-
|
|
2004
|
+
try:
|
|
2005
|
+
payload = get_payload("inspect", vars(args), cfg, cold_core=inspect_payload)
|
|
2006
|
+
except PayloadError as pe:
|
|
2007
|
+
# Resolve-miss: route through _emit so --exists/--count shape it
|
|
2008
|
+
# (inspect Missing --exists -> false, rc 2), mirroring master's inspect
|
|
2009
|
+
# resolve-miss path. No flag -> rc matches the prior 2-if-error-else-0.
|
|
2010
|
+
return _emit(pe.env, args)
|
|
2011
|
+
desc_out = payload["describe"]
|
|
1712
2012
|
|
|
1713
2013
|
if not desc_out.success or desc_out.record is None:
|
|
1714
2014
|
env = Envelope(status="error", message=desc_out.message or "describe failed")
|
|
@@ -1736,11 +2036,11 @@ def _cmd_inspect(args: argparse.Namespace) -> int:
|
|
|
1736
2036
|
# renamed ``symbol_kind`` to match find/search/listings (which use
|
|
1737
2037
|
# ``symbol_kind`` for the sub-kind and reserve ``kind`` for the category).
|
|
1738
2038
|
record_dict = desc_out.record.model_dump()
|
|
1739
|
-
node_id = record_dict.get("id") or
|
|
2039
|
+
node_id = record_dict.get("id") or payload["node_id"]
|
|
1740
2040
|
data = record_dict.get("data") or {}
|
|
1741
2041
|
flat: dict[str, Any] = {
|
|
1742
2042
|
"kind": record_dict.get("kind") or "symbol",
|
|
1743
|
-
"fqn": record_dict.get("fqn") or data.get("fqn") or
|
|
2043
|
+
"fqn": record_dict.get("fqn") or data.get("fqn") or payload["node_fqn"],
|
|
1744
2044
|
}
|
|
1745
2045
|
# Promote inner data fields. Skip ``kind`` here — renamed to symbol_kind.
|
|
1746
2046
|
for src_key, dest_key in (
|
|
@@ -1772,13 +2072,12 @@ def _cmd_inspect(args: argparse.Namespace) -> int:
|
|
|
1772
2072
|
status="ok",
|
|
1773
2073
|
nodes={node_id: flat},
|
|
1774
2074
|
root=node_id,
|
|
1775
|
-
file_location=
|
|
2075
|
+
file_location=payload["file_location"], # Preserve file_location from resolve
|
|
1776
2076
|
)
|
|
1777
2077
|
next_actions_hook(env, root=node_id, edge_summary=record_dict.get("edge_summary"))
|
|
1778
2078
|
|
|
1779
2079
|
# Render with inspect shape
|
|
1780
|
-
|
|
1781
|
-
return 0
|
|
2080
|
+
return _emit(env, args, shape="inspect")
|
|
1782
2081
|
|
|
1783
2082
|
|
|
1784
2083
|
def _backfill_service_from_filename(row: dict) -> None:
|
|
@@ -1874,9 +2173,45 @@ def _cmd_producers(args: argparse.Namespace) -> int:
|
|
|
1874
2173
|
return _render_listing(rows, limit=limit, args=args, noun="producer")
|
|
1875
2174
|
|
|
1876
2175
|
|
|
2176
|
+
def _producer_summary(producer: dict) -> dict:
|
|
2177
|
+
"""Display-oriented producer dict for the ``topics`` grouping.
|
|
2178
|
+
|
|
2179
|
+
The raw ``list_producers`` row carries 11 fields (incl. empty ``broker``,
|
|
2180
|
+
raw ``filename``/``start_line``/``end_line``, ``direction``, ``source_layer``)
|
|
2181
|
+
which the text renderer used to collapse into one unreadable comma-line.
|
|
2182
|
+
This folds location into a single ``file`` and keeps only the fields an
|
|
2183
|
+
operator needs to identify the producer under a topic header (the topic
|
|
2184
|
+
itself is the group header, so it's dropped here). Empty values omitted.
|
|
2185
|
+
"""
|
|
2186
|
+
member_fqn = str(producer.get("member_fqn") or "")
|
|
2187
|
+
member_simple = member_fqn.rsplit(".", 1)[-1] if member_fqn else ""
|
|
2188
|
+
filename = str(producer.get("filename") or "")
|
|
2189
|
+
start_line = producer.get("start_line")
|
|
2190
|
+
try:
|
|
2191
|
+
sl = int(start_line) if start_line not in (None, "") else None
|
|
2192
|
+
except (TypeError, ValueError):
|
|
2193
|
+
sl = None
|
|
2194
|
+
file_loc = f"{filename}:{sl}" if filename and sl else filename
|
|
2195
|
+
out: dict = {}
|
|
2196
|
+
if member_simple:
|
|
2197
|
+
out["member"] = member_simple
|
|
2198
|
+
if file_loc:
|
|
2199
|
+
out["file"] = file_loc
|
|
2200
|
+
# ``direction`` is intentionally omitted: every Producer is built with
|
|
2201
|
+
# direction="producer" (build_ast_graph), so it's a constant that only
|
|
2202
|
+
# inflates each block with zero information.
|
|
2203
|
+
for k in ("microservice", "module", "producer_kind", "broker"):
|
|
2204
|
+
v = producer.get(k)
|
|
2205
|
+
if v not in (None, "", []):
|
|
2206
|
+
out[k] = v
|
|
2207
|
+
resolved = producer.get("resolved")
|
|
2208
|
+
if resolved is not None:
|
|
2209
|
+
out["resolved"] = bool(resolved)
|
|
2210
|
+
return out
|
|
2211
|
+
|
|
2212
|
+
|
|
1877
2213
|
def _cmd_topics(args: argparse.Namespace) -> int:
|
|
1878
2214
|
from java_codebase_rag.jrag_envelope import Envelope, mark_truncated, next_actions_hook
|
|
1879
|
-
from java_codebase_rag.jrag_render import render
|
|
1880
2215
|
|
|
1881
2216
|
_, graph, rc = _load_graph_or_error(args)
|
|
1882
2217
|
if rc:
|
|
@@ -1908,7 +2243,7 @@ def _cmd_topics(args: argparse.Namespace) -> int:
|
|
|
1908
2243
|
"producers": [],
|
|
1909
2244
|
"broker": producer.get("broker") or "",
|
|
1910
2245
|
}
|
|
1911
|
-
topics_dict[topic]["producers"].append(producer)
|
|
2246
|
+
topics_dict[topic]["producers"].append(_producer_summary(producer))
|
|
1912
2247
|
|
|
1913
2248
|
warnings: list[str] = []
|
|
1914
2249
|
if no_topic_count:
|
|
@@ -1956,8 +2291,7 @@ def _cmd_topics(args: argparse.Namespace) -> int:
|
|
|
1956
2291
|
warnings=warnings + _auto_scope_notice(args),
|
|
1957
2292
|
)
|
|
1958
2293
|
next_actions_hook(env, command=getattr(args, "command", None))
|
|
1959
|
-
|
|
1960
|
-
return 0
|
|
2294
|
+
return _emit(env, args, noun="topic")
|
|
1961
2295
|
|
|
1962
2296
|
|
|
1963
2297
|
def _inspect_hints_for_rows(rows: list[dict], *, limit: int = 2) -> list[str]:
|
|
@@ -2160,7 +2494,6 @@ def _resolve_traversal_node(
|
|
|
2160
2494
|
resolves for the cross-service route-caller flow.
|
|
2161
2495
|
"""
|
|
2162
2496
|
from java_codebase_rag.jrag_envelope import resolve_query
|
|
2163
|
-
from java_codebase_rag.jrag_render import render
|
|
2164
2497
|
|
|
2165
2498
|
node, env = resolve_query(
|
|
2166
2499
|
args.query,
|
|
@@ -2174,8 +2507,10 @@ def _resolve_traversal_node(
|
|
|
2174
2507
|
module=(getattr(args, "module", None) or "") if apply_scope else "",
|
|
2175
2508
|
)
|
|
2176
2509
|
if env.status != "ok":
|
|
2177
|
-
|
|
2178
|
-
|
|
2510
|
+
# Route through _emit so --exists/--count shape a resolve miss (e.g.
|
|
2511
|
+
# `callers Missing --exists` -> false, rc 2) instead of rendering the
|
|
2512
|
+
# not_found body. rc matches the prior 2-if-error-else-0 when no flag.
|
|
2513
|
+
return None, env, _emit(env, args)
|
|
2179
2514
|
return node, env, 0
|
|
2180
2515
|
|
|
2181
2516
|
|
|
@@ -2235,7 +2570,6 @@ def _emit_traversal(
|
|
|
2235
2570
|
``jrag implementations <fqn>``).
|
|
2236
2571
|
"""
|
|
2237
2572
|
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook
|
|
2238
|
-
from java_codebase_rag.jrag_render import render
|
|
2239
2573
|
|
|
2240
2574
|
env = Envelope(
|
|
2241
2575
|
status="ok",
|
|
@@ -2254,8 +2588,7 @@ def _emit_traversal(
|
|
|
2254
2588
|
seen.add(h)
|
|
2255
2589
|
env.agent_next_actions.append(h)
|
|
2256
2590
|
env.agent_next_actions = env.agent_next_actions[:5]
|
|
2257
|
-
|
|
2258
|
-
return 0
|
|
2591
|
+
return _emit(env, args, noun=noun)
|
|
2259
2592
|
|
|
2260
2593
|
|
|
2261
2594
|
def _require_kind(
|
|
@@ -2388,161 +2721,20 @@ def _cmd_callers(args: argparse.Namespace) -> int:
|
|
|
2388
2721
|
cfg, graph, rc = _load_graph_or_error(args)
|
|
2389
2722
|
if rc:
|
|
2390
2723
|
return rc
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
)
|
|
2394
|
-
if rrc or node is None:
|
|
2395
|
-
return rrc
|
|
2396
|
-
limit = _clamped_limit(args)
|
|
2397
|
-
|
|
2398
|
-
root_dict = _noderef_to_node_dict(node)
|
|
2399
|
-
root_id = node.id
|
|
2400
|
-
|
|
2401
|
-
# Route root -> find_route_callers. Route callers are inherently cross-
|
|
2402
|
-
# service (a Client in microservice A calls a server Route in microservice B),
|
|
2403
|
-
# so --service is NOT applied as a caller-microservice post-filter here;
|
|
2404
|
-
# it has already narrowed resolve (which route was selected) via
|
|
2405
|
-
# _resolve_traversal_node -> resolve_query.
|
|
2406
|
-
if node.kind == "route":
|
|
2407
|
-
route_callers = graph.find_route_callers(route_id=root_id)
|
|
2408
|
-
warnings: list[str] = []
|
|
2409
|
-
# No backend limit on find_route_callers; client-side slice for truncation.
|
|
2410
|
-
truncated = len(route_callers) > limit
|
|
2411
|
-
display = route_callers[:limit]
|
|
2412
|
-
nodes: dict[str, dict] = {}
|
|
2413
|
-
edges: list[dict] = []
|
|
2414
|
-
for rc in display:
|
|
2415
|
-
caller_id = rc.caller_node_id
|
|
2416
|
-
if rc.caller_node_kind == "client":
|
|
2417
|
-
edge_type = "HTTP_CALLS"
|
|
2418
|
-
else:
|
|
2419
|
-
edge_type = "ASYNC_CALLS"
|
|
2420
|
-
# The caller's identity is the declaring Symbol (the method that owns
|
|
2421
|
-
# the Client/Producer), not the call-site path — mirrors
|
|
2422
|
-
# trace_request_flow, which surfaces declaring_symbol_fqn. The
|
|
2423
|
-
# path/topic the caller hits is kept as raw_uri/topic so the agent
|
|
2424
|
-
# sees both WHO calls and WHAT they hit.
|
|
2425
|
-
node = {
|
|
2426
|
-
"id": caller_id,
|
|
2427
|
-
"kind": rc.caller_node_kind,
|
|
2428
|
-
"fqn": rc.declaring_symbol_fqn or caller_id,
|
|
2429
|
-
"microservice": rc.caller_microservice,
|
|
2430
|
-
}
|
|
2431
|
-
if rc.target_service:
|
|
2432
|
-
node["target_service"] = rc.target_service
|
|
2433
|
-
if rc.caller_node_kind == "client" and rc.raw_uri:
|
|
2434
|
-
node["raw_uri"] = rc.raw_uri
|
|
2435
|
-
elif rc.caller_node_kind != "client" and rc.topic:
|
|
2436
|
-
node["topic"] = rc.topic
|
|
2437
|
-
nodes[caller_id] = node
|
|
2438
|
-
edges.append(
|
|
2439
|
-
{"other_id": caller_id, "edge_type": edge_type, "confidence": rc.confidence}
|
|
2440
|
-
)
|
|
2441
|
-
# Include the root (Route) node so the zero-callers rendering surfaces
|
|
2442
|
-
# the route path rather than a bare "0 callers" line.
|
|
2443
|
-
nodes[root_id] = root_dict
|
|
2444
|
-
# External-entrypoint detection: a server-exposed HTTP route (kind
|
|
2445
|
-
# http_endpoint with an inbound EXPOSES edge from a controller Symbol)
|
|
2446
|
-
# genuinely has zero in-repo callers — the route IS the entrypoint. Flag
|
|
2447
|
-
# it so the renderer says so instead of emitting a bug-looking bare
|
|
2448
|
-
# "0 callers". NodeRef.kind is the node label ("route"), not the stored
|
|
2449
|
-
# http_endpoint/kafka_topic property, so fetch the property directly.
|
|
2450
|
-
# Kafka topics are excluded: their empty-callers case has different
|
|
2451
|
-
# semantics (a topic with no producers is not an HTTP entrypoint).
|
|
2452
|
-
is_external_entrypoint = False
|
|
2453
|
-
if not display:
|
|
2454
|
-
kind_row = graph._rows( # noqa: SLF001 - same pattern as jrag_envelope._node_file_location
|
|
2455
|
-
"MATCH (r:Route) WHERE r.id = $rid RETURN r.kind AS kind LIMIT 1",
|
|
2456
|
-
{"rid": root_id},
|
|
2457
|
-
)
|
|
2458
|
-
route_kind = str(kind_row[0].get("kind") or "") if kind_row else ""
|
|
2459
|
-
if route_kind == "http_endpoint" and graph.find_route_handlers(route_id=root_id):
|
|
2460
|
-
is_external_entrypoint = True
|
|
2461
|
-
return _emit_traversal(
|
|
2462
|
-
args, root_id=root_id, nodes=nodes, edges=edges,
|
|
2463
|
-
noun="callers", warnings=warnings, truncated=truncated,
|
|
2464
|
-
is_external_entrypoint=is_external_entrypoint,
|
|
2465
|
-
)
|
|
2466
|
-
|
|
2467
|
-
# Symbol root -> find_callers (push down --service/--module/depth/etc.).
|
|
2468
|
-
if node.kind != "symbol":
|
|
2469
|
-
from java_codebase_rag.jrag_envelope import Envelope
|
|
2470
|
-
from java_codebase_rag.jrag_render import render
|
|
2471
|
-
|
|
2472
|
-
env = Envelope(
|
|
2473
|
-
status="error",
|
|
2474
|
-
message=(
|
|
2475
|
-
f"callers expects a Symbol or Route root; resolved node kind is "
|
|
2476
|
-
f"{node.kind!r}. Use --kind to narrow resolve."
|
|
2477
|
-
),
|
|
2478
|
-
)
|
|
2479
|
-
print(render(env, fmt=args.format, detail=args.detail))
|
|
2480
|
-
return 2
|
|
2724
|
+
from java_codebase_rag.read_payloads import PayloadError, callers_payload
|
|
2725
|
+
from java_codebase_rag.watch.client import get_payload
|
|
2481
2726
|
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
min_confidence=min_conf,
|
|
2490
|
-
exclude_external=exclude_external,
|
|
2491
|
-
module=args.module,
|
|
2492
|
-
microservice=args.service,
|
|
2493
|
-
)
|
|
2494
|
-
from java_codebase_rag.jrag_envelope import mark_truncated
|
|
2495
|
-
|
|
2496
|
-
display, truncated = mark_truncated(call_edges, limit)
|
|
2497
|
-
nodes = {}
|
|
2498
|
-
edges = []
|
|
2499
|
-
for ce in display:
|
|
2500
|
-
nodes[ce.src.id] = _symbol_hit_to_dict(ce.src)
|
|
2501
|
-
edges.append(
|
|
2502
|
-
{"other_id": ce.src.id, "edge_type": "CALLS", "confidence": ce.confidence}
|
|
2503
|
-
)
|
|
2504
|
-
# Entry-point awareness. A controller / messaging-listener type is invoked
|
|
2505
|
-
# via the routes its methods EXPOSE (Controller -[:DECLARES]-> method
|
|
2506
|
-
# -[:EXPOSES]-> Route), NOT via in-repo CALLS edges — so find_callers is
|
|
2507
|
-
# typically empty for HTTP handlers. Without this fold, `callers
|
|
2508
|
-
# <Controller>` returns a bug-looking empty list when the controller is the
|
|
2509
|
-
# very thing the agent is investigating. The routes ARE its inbound callers,
|
|
2510
|
-
# so surface them as additional EXPOSES rows alongside any CALLS-in edges.
|
|
2511
|
-
# Gated on having DECLARES.EXPOSES out-edges (covers any entry-point holder,
|
|
2512
|
-
# not just role=CONTROLLER). Routes are additive and usually few, so they do
|
|
2513
|
-
# not count against the CALLS --limit (cf. the callees client/producer path,
|
|
2514
|
-
# which likewise emits its own targets without sharing the CALLS budget).
|
|
2515
|
-
expose_rows = graph._rows( # noqa: SLF001 - one-shot aggregation, cf. _cmd_callees client path
|
|
2516
|
-
"MATCH (t:Symbol {id: $tid})-[:DECLARES]->(m:Symbol)-[e:EXPOSES]->(r:Route) "
|
|
2517
|
-
"RETURN r.id AS rid, r.method AS rmethod, r.path AS rpath, "
|
|
2518
|
-
"r.path_template AS rpt, r.microservice AS rms, "
|
|
2519
|
-
"m.fqn AS via_fqn, e.confidence AS conf",
|
|
2520
|
-
{"tid": root_id},
|
|
2521
|
-
)
|
|
2522
|
-
for row in expose_rows:
|
|
2523
|
-
rid = str(row.get("rid") or "")
|
|
2524
|
-
if not rid or rid in nodes:
|
|
2525
|
-
continue
|
|
2526
|
-
rmethod = str(row.get("rmethod") or "")
|
|
2527
|
-
rpath = str(row.get("rpt") or row.get("rpath") or "")
|
|
2528
|
-
nodes[rid] = {
|
|
2529
|
-
"id": rid,
|
|
2530
|
-
"kind": "route",
|
|
2531
|
-
"fqn": f"{rmethod} {rpath}".strip(),
|
|
2532
|
-
"method": rmethod,
|
|
2533
|
-
"path": rpath,
|
|
2534
|
-
"microservice": str(row.get("rms") or ""),
|
|
2535
|
-
}
|
|
2536
|
-
edge_row: dict = {"other_id": rid, "edge_type": "EXPOSES"}
|
|
2537
|
-
via_fqn = str(row.get("via_fqn") or "")
|
|
2538
|
-
if via_fqn:
|
|
2539
|
-
# Declaring method that exposes the route; rendered at --detail full.
|
|
2540
|
-
edge_row["from_fqn"] = via_fqn
|
|
2541
|
-
edges.append(edge_row)
|
|
2542
|
-
nodes[root_id] = root_dict
|
|
2727
|
+
try:
|
|
2728
|
+
payload = get_payload("callers", vars(args), cfg, cold_core=callers_payload)
|
|
2729
|
+
except PayloadError as pe:
|
|
2730
|
+
# Resolve-miss: route through _emit so --exists/--count shape it
|
|
2731
|
+
# (callers Missing --exists -> false, rc 2), mirroring master's
|
|
2732
|
+
# _resolve_traversal_node resolve-miss path.
|
|
2733
|
+
return _emit(pe.env, args)
|
|
2543
2734
|
return _emit_traversal(
|
|
2544
|
-
args, root_id=root_id, nodes=nodes, edges=edges,
|
|
2545
|
-
noun="
|
|
2735
|
+
args, root_id=payload["root_id"], nodes=payload["nodes"], edges=payload["edges"],
|
|
2736
|
+
noun=payload["noun"], warnings=payload["warnings"], truncated=payload["truncated"],
|
|
2737
|
+
is_external_entrypoint=payload["is_external_entrypoint"],
|
|
2546
2738
|
)
|
|
2547
2739
|
|
|
2548
2740
|
|
|
@@ -2550,165 +2742,24 @@ def _cmd_callees(args: argparse.Namespace) -> int:
|
|
|
2550
2742
|
cfg, graph, rc = _load_graph_or_error(args)
|
|
2551
2743
|
if rc:
|
|
2552
2744
|
return rc
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
return rrc
|
|
2556
|
-
limit = _clamped_limit(args)
|
|
2745
|
+
from java_codebase_rag.read_payloads import PayloadError, callees_payload
|
|
2746
|
+
from java_codebase_rag.watch.client import get_payload
|
|
2557
2747
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
args
|
|
2565
|
-
hint="Use --kind to narrow resolve.",
|
|
2566
|
-
)
|
|
2567
|
-
if guard is not None:
|
|
2568
|
-
return guard
|
|
2569
|
-
|
|
2570
|
-
from java_codebase_rag.jrag_envelope import Envelope, mark_truncated
|
|
2571
|
-
from java_codebase_rag.jrag_render import render
|
|
2572
|
-
|
|
2573
|
-
# Client root -> HTTP_CALLS out (Client -> :Route).
|
|
2574
|
-
# Producer root -> ASYNC_CALLS out (Producer -> :Route, the kafka_topic
|
|
2575
|
-
# Route this producer publishes to — NOT a :Producer node).
|
|
2576
|
-
if node.kind in ("client", "producer"):
|
|
2577
|
-
from java_codebase_rag.mcp import mcp_v2
|
|
2578
|
-
|
|
2579
|
-
edge_types = ["HTTP_CALLS"] if node.kind == "client" else ["ASYNC_CALLS"]
|
|
2580
|
-
out = mcp_v2.neighbors_v2(
|
|
2581
|
-
[node.id], direction="out", edge_types=edge_types,
|
|
2582
|
-
limit=limit + 1, graph=graph,
|
|
2583
|
-
)
|
|
2584
|
-
if not out.success:
|
|
2585
|
-
print(render(Envelope(status="error", message=out.message or "neighbors_v2 failed"), fmt=args.format, detail=args.detail))
|
|
2586
|
-
return 2
|
|
2587
|
-
root_id = node.id
|
|
2588
|
-
nodes: dict[str, dict] = {root_id: _noderef_to_node_dict(node)}
|
|
2589
|
-
edges: list[dict] = []
|
|
2590
|
-
for e in out.results:
|
|
2591
|
-
nodes[e.other.id] = _noderef_to_node_dict(e.other)
|
|
2592
|
-
edges.append(
|
|
2593
|
-
{
|
|
2594
|
-
"other_id": e.other.id,
|
|
2595
|
-
"edge_type": e.edge_type,
|
|
2596
|
-
"confidence": e.attrs.get("confidence"),
|
|
2597
|
-
}
|
|
2598
|
-
)
|
|
2599
|
-
truncated = bool(out.has_more_results) or len(edges) > limit
|
|
2600
|
-
if len(edges) > limit:
|
|
2601
|
-
edges = edges[:limit]
|
|
2602
|
-
# --include-external is accepted but does not apply on Client/Producer
|
|
2603
|
-
# roots (the edges are to :Route, which is always in-graph; there is no
|
|
2604
|
-
# external-exclusion analog). Surface as a warning so the flag is not
|
|
2605
|
-
# silently dropped (plan principle: inapplicable flags never silently ignored).
|
|
2606
|
-
warnings: list[str] = []
|
|
2607
|
-
if getattr(args, "include_external", False):
|
|
2608
|
-
warnings.append(
|
|
2609
|
-
"--include-external does not apply to Client/Producer roots "
|
|
2610
|
-
"(HTTP_CALLS/ASYNC_CALLS reach :Route, which is always in-graph)"
|
|
2611
|
-
)
|
|
2612
|
-
edges = _dedupe_traversal_edges(edges)
|
|
2613
|
-
truncated = truncated or len(edges) > limit
|
|
2614
|
-
edges = edges[:limit]
|
|
2615
|
-
return _emit_traversal(
|
|
2616
|
-
args, root_id=root_id, nodes=nodes, edges=edges,
|
|
2617
|
-
noun="callees", warnings=warnings, truncated=truncated,
|
|
2618
|
-
)
|
|
2619
|
-
|
|
2620
|
-
depth = getattr(args, "depth", 1)
|
|
2621
|
-
min_conf = getattr(args, "min_confidence", 0.0)
|
|
2622
|
-
exclude_external = not getattr(args, "include_external", False)
|
|
2623
|
-
# CLIENT-role type Symbol (e.g. a Feign client interface): its "callees" are
|
|
2624
|
-
# the outbound HTTP endpoints its declared client methods call, NOT CALLS
|
|
2625
|
-
# edges from the interface (a Feign interface declares methods but its
|
|
2626
|
-
# methods' HTTP_CALLS edges carry the real outbound surface). Aggregate the
|
|
2627
|
-
# declared Client nodes and their HTTP_CALLS targets so `jrag callees
|
|
2628
|
-
# 'ChatCoreFeignClient'` shows the routes it hits.
|
|
2629
|
-
if (node.role or "") == "CLIENT":
|
|
2630
|
-
root_id = node.id
|
|
2631
|
-
client_rows = graph._rows( # noqa: SLF001 - one-shot aggregation query
|
|
2632
|
-
"MATCH (iface:Symbol {id: $sid})-[:DECLARES]->(m:Symbol)"
|
|
2633
|
-
"-[:DECLARES_CLIENT]->(c:Client) "
|
|
2634
|
-
"OPTIONAL MATCH (c)-[e:HTTP_CALLS]->(r:Route) "
|
|
2635
|
-
"RETURN c.id AS cid, c.member_fqn AS cfqn, c.path AS cpath, "
|
|
2636
|
-
"c.method AS cmethod, c.microservice AS cms, "
|
|
2637
|
-
"r.id AS rid, r.method AS rmethod, r.path AS rpath, "
|
|
2638
|
-
"r.path_template AS rpt, r.microservice AS rms, "
|
|
2639
|
-
"e.confidence AS conf",
|
|
2640
|
-
{"sid": root_id},
|
|
2641
|
-
)
|
|
2642
|
-
nodes = {root_id: _noderef_to_node_dict(node)}
|
|
2643
|
-
edges: list[dict] = []
|
|
2644
|
-
for row in client_rows:
|
|
2645
|
-
rid = str(row.get("rid") or "")
|
|
2646
|
-
if rid:
|
|
2647
|
-
target_id = rid
|
|
2648
|
-
rmethod = str(row.get("rmethod") or "")
|
|
2649
|
-
rpath = str(row.get("rpt") or row.get("rpath") or "")
|
|
2650
|
-
nodes[target_id] = {
|
|
2651
|
-
"id": target_id,
|
|
2652
|
-
"kind": "route",
|
|
2653
|
-
"fqn": f"{rmethod} {rpath}".strip(),
|
|
2654
|
-
"microservice": str(row.get("rms") or ""),
|
|
2655
|
-
}
|
|
2656
|
-
edge_type = "HTTP_CALLS"
|
|
2657
|
-
else:
|
|
2658
|
-
# Client with no resolved HTTP_CALLS edge: surface the client
|
|
2659
|
-
# node + its declared path so the outbound intent is visible.
|
|
2660
|
-
target_id = str(row.get("cid") or "")
|
|
2661
|
-
if not target_id:
|
|
2662
|
-
continue
|
|
2663
|
-
cmethod = str(row.get("cmethod") or "")
|
|
2664
|
-
cpath = str(row.get("cpath") or "")
|
|
2665
|
-
nodes[target_id] = {
|
|
2666
|
-
"id": target_id,
|
|
2667
|
-
"kind": "client",
|
|
2668
|
-
"fqn": f"{cmethod} {cpath}".strip() or str(row.get("cfqn") or ""),
|
|
2669
|
-
"microservice": str(row.get("cms") or ""),
|
|
2670
|
-
}
|
|
2671
|
-
edge_type = "HTTP_CALLS"
|
|
2672
|
-
edges.append({
|
|
2673
|
-
"other_id": target_id,
|
|
2674
|
-
"edge_type": edge_type,
|
|
2675
|
-
"confidence": float(row.get("conf") or 0.0) or None,
|
|
2676
|
-
})
|
|
2677
|
-
edges = _dedupe_traversal_edges(edges)
|
|
2678
|
-
truncated = len(edges) > limit
|
|
2679
|
-
edges = edges[:limit]
|
|
2680
|
-
return _emit_traversal(
|
|
2681
|
-
args, root_id=root_id, nodes=nodes, edges=edges,
|
|
2682
|
-
noun="callees", truncated=truncated,
|
|
2683
|
-
)
|
|
2684
|
-
|
|
2685
|
-
call_edges = graph.find_callees(
|
|
2686
|
-
node.fqn,
|
|
2687
|
-
depth=depth,
|
|
2688
|
-
limit=limit + 1,
|
|
2689
|
-
min_confidence=min_conf,
|
|
2690
|
-
exclude_external=exclude_external,
|
|
2691
|
-
module=args.module,
|
|
2692
|
-
microservice=args.service,
|
|
2693
|
-
)
|
|
2694
|
-
display, truncated = mark_truncated(call_edges, limit)
|
|
2695
|
-
root_id = node.id
|
|
2696
|
-
nodes = {root_id: _noderef_to_node_dict(node)}
|
|
2697
|
-
edges = []
|
|
2698
|
-
for ce in display:
|
|
2699
|
-
nodes[ce.dst.id] = _symbol_hit_to_dict(ce.dst)
|
|
2700
|
-
edges.append(
|
|
2701
|
-
{"other_id": ce.dst.id, "edge_type": "CALLS", "confidence": ce.confidence}
|
|
2702
|
-
)
|
|
2703
|
-
edges = _dedupe_traversal_edges(edges)
|
|
2704
|
-
truncated = truncated or len(edges) > limit
|
|
2705
|
-
edges = edges[:limit]
|
|
2748
|
+
try:
|
|
2749
|
+
payload = get_payload("callees", vars(args), cfg, cold_core=callees_payload)
|
|
2750
|
+
except PayloadError as pe:
|
|
2751
|
+
# Resolve-miss: route through _emit so --exists/--count shape it
|
|
2752
|
+
# (callees Missing --exists -> false, rc 2), mirroring master's
|
|
2753
|
+
# _resolve_traversal_node resolve-miss path.
|
|
2754
|
+
return _emit(pe.env, args)
|
|
2706
2755
|
return _emit_traversal(
|
|
2707
|
-
args, root_id=root_id, nodes=nodes, edges=edges,
|
|
2708
|
-
noun="
|
|
2756
|
+
args, root_id=payload["root_id"], nodes=payload["nodes"], edges=payload["edges"],
|
|
2757
|
+
noun=payload["noun"], warnings=payload["warnings"], truncated=payload["truncated"],
|
|
2758
|
+
is_external_entrypoint=payload["is_external_entrypoint"],
|
|
2709
2759
|
)
|
|
2710
2760
|
|
|
2711
2761
|
|
|
2762
|
+
|
|
2712
2763
|
def _cmd_hierarchy(args: argparse.Namespace) -> int:
|
|
2713
2764
|
from java_codebase_rag.mcp import mcp_v2
|
|
2714
2765
|
|
|
@@ -3135,93 +3186,23 @@ def _cmd_flow(args: argparse.Namespace) -> int:
|
|
|
3135
3186
|
cfg, graph, rc = _load_graph_or_error(args)
|
|
3136
3187
|
if rc:
|
|
3137
3188
|
return rc
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
if rrc or node is None:
|
|
3141
|
-
return rrc
|
|
3142
|
-
limit = _clamped_limit(args)
|
|
3189
|
+
from java_codebase_rag.read_payloads import PayloadError, flow_payload
|
|
3190
|
+
from java_codebase_rag.watch.client import get_payload
|
|
3143
3191
|
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
warnings = _warn_unapplied_scope(
|
|
3152
|
-
args, reason="trace_request_flow carries no microservice predicate; intra-codebase is an index-time data property"
|
|
3153
|
-
)
|
|
3154
|
-
|
|
3155
|
-
max_hops = max(1, min(8, getattr(args, "depth", 5)))
|
|
3156
|
-
flow_data = graph.trace_request_flow(entry_route_id=node.id, max_hops=max_hops)
|
|
3157
|
-
|
|
3158
|
-
root_id = node.id
|
|
3159
|
-
nodes: dict[str, dict] = {root_id: _noderef_to_node_dict(node)}
|
|
3160
|
-
edges: list[dict] = []
|
|
3161
|
-
# Inbound: cross-service HTTP/async callers (Client/Producer two-hop).
|
|
3162
|
-
for row in flow_data.get("inbound", []):
|
|
3163
|
-
caller_id = str(row.get("caller_node_id") or "")
|
|
3164
|
-
if not caller_id:
|
|
3165
|
-
continue
|
|
3166
|
-
kind = str(row.get("caller_node_kind") or "")
|
|
3167
|
-
nodes[caller_id] = {
|
|
3168
|
-
"id": caller_id,
|
|
3169
|
-
"kind": kind,
|
|
3170
|
-
"fqn": str(row.get("declaring_symbol_fqn") or ""),
|
|
3171
|
-
"microservice": str(row.get("microservice") or ""),
|
|
3172
|
-
}
|
|
3173
|
-
edges.append(
|
|
3174
|
-
{
|
|
3175
|
-
"other_id": caller_id,
|
|
3176
|
-
"edge_type": "HTTP_CALLS" if kind == "client" else "ASYNC_CALLS",
|
|
3177
|
-
"confidence": float(row.get("confidence") or 0.0),
|
|
3178
|
-
}
|
|
3179
|
-
)
|
|
3180
|
-
# Outbound: CALLS hops from the route handler (intra-service by construction).
|
|
3181
|
-
for row in flow_data.get("outbound", []):
|
|
3182
|
-
next_id = str(row.get("next_symbol_id") or "")
|
|
3183
|
-
if not next_id:
|
|
3184
|
-
continue
|
|
3185
|
-
nodes[next_id] = {
|
|
3186
|
-
"id": next_id,
|
|
3187
|
-
"kind": "symbol",
|
|
3188
|
-
"fqn": str(row.get("next_fqn") or ""),
|
|
3189
|
-
"microservice": str(row.get("next_microservice") or ""),
|
|
3190
|
-
}
|
|
3191
|
-
edges.append({"other_id": next_id, "edge_type": "CALLS"})
|
|
3192
|
-
|
|
3193
|
-
# Client-side slice for truncation (trace_request_flow has no limit param).
|
|
3194
|
-
truncated = len(edges) > limit
|
|
3195
|
-
if truncated:
|
|
3196
|
-
edges = edges[:limit]
|
|
3192
|
+
try:
|
|
3193
|
+
payload = get_payload("flow", vars(args), cfg, cold_core=flow_payload)
|
|
3194
|
+
except PayloadError as pe:
|
|
3195
|
+
# Resolve-miss: route through _emit so --exists/--count shape it
|
|
3196
|
+
# (flow Missing --exists -> false, rc 2), mirroring master's
|
|
3197
|
+
# _resolve_traversal_node resolve-miss path.
|
|
3198
|
+
return _emit(pe.env, args)
|
|
3197
3199
|
return _emit_traversal(
|
|
3198
|
-
args, root_id=root_id, nodes=nodes, edges=edges,
|
|
3199
|
-
noun="
|
|
3200
|
+
args, root_id=payload["root_id"], nodes=payload["nodes"], edges=payload["edges"],
|
|
3201
|
+
noun=payload["noun"], warnings=payload["warnings"], truncated=payload["truncated"],
|
|
3202
|
+
is_external_entrypoint=payload["is_external_entrypoint"],
|
|
3200
3203
|
)
|
|
3201
3204
|
|
|
3202
3205
|
|
|
3203
|
-
# ============================================================================
|
|
3204
|
-
# PR-JRAG-3b: compose traversals + connection + outline/imports.
|
|
3205
|
-
#
|
|
3206
|
-
# callees Client/Producer variant (above) re-uses _cmd_callees. The four new
|
|
3207
|
-
# handlers below cover: dependencies (INJECTS out), connection (multi-section
|
|
3208
|
-
# microservice view, resolve-first EXCEPTION), outline (file -> symbols),
|
|
3209
|
-
# imports (file -> tree-sitter parse -> resolve_v2 per FQN).
|
|
3210
|
-
#
|
|
3211
|
-
# Backend signatures verified at PR-JRAG-3b time:
|
|
3212
|
-
# * neighbors_v2(ids, direction, edge_types, limit=25, offset=0, ...) returns
|
|
3213
|
-
# NeighborsOutput.results: list[Edge] where Edge.other: NodeRef,
|
|
3214
|
-
# Edge.edge_type: str, Edge.attrs: dict (mcp_v2.py:1284).
|
|
3215
|
-
# * find_symbols_in_file_range(graph, *, filename, start_line, end_line)
|
|
3216
|
-
# returns list[SymbolHit]; start_line<1 returns [] (ladybug_queries.py:302).
|
|
3217
|
-
# * parse_java(source, *, filename, verbose) -> JavaFileAst with
|
|
3218
|
-
# explicit_imports: dict[str, str] (simple_name -> FQN) (ast_java.py:2612).
|
|
3219
|
-
# * INJECTS is Symbol -> Symbol (java_ontology.py:216); out = types this
|
|
3220
|
-
# symbol injects = direct dependencies.
|
|
3221
|
-
# * HTTP_CALLS is Client -> Route (java_ontology.py:352); ASYNC_CALLS is
|
|
3222
|
-
# Producer -> Route (java_ontology.py:386). Both confirmed.
|
|
3223
|
-
# ============================================================================
|
|
3224
|
-
|
|
3225
3206
|
|
|
3226
3207
|
def _cmd_dependencies(args: argparse.Namespace) -> int:
|
|
3227
3208
|
from java_codebase_rag.mcp import mcp_v2
|
|
@@ -3332,7 +3313,6 @@ def _cmd_connection(args: argparse.Namespace) -> int:
|
|
|
3332
3313
|
limit = _clamped_limit(args)
|
|
3333
3314
|
|
|
3334
3315
|
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook
|
|
3335
|
-
from java_codebase_rag.jrag_render import render
|
|
3336
3316
|
|
|
3337
3317
|
# Validate the microservice against the known set so a bogus name surfaces a
|
|
3338
3318
|
# clear error instead of an empty inbound:/outbound: view (silent wrong
|
|
@@ -3532,8 +3512,7 @@ def _cmd_connection(args: argparse.Namespace) -> int:
|
|
|
3532
3512
|
truncated=truncated,
|
|
3533
3513
|
)
|
|
3534
3514
|
next_actions_hook(env, root=root_id, result_edges=display_edges)
|
|
3535
|
-
|
|
3536
|
-
return 0
|
|
3515
|
+
return _emit(env, args, noun="connection")
|
|
3537
3516
|
|
|
3538
3517
|
|
|
3539
3518
|
def _resolve_source_path(cfg, file_arg: str) -> Path | None:
|
|
@@ -3616,8 +3595,7 @@ def _cmd_outline(args: argparse.Namespace) -> int:
|
|
|
3616
3595
|
# thing to inspect from an outline. Per-row inspect hints for the leading
|
|
3617
3596
|
# entries give the agent a concrete next step.
|
|
3618
3597
|
env.agent_next_actions = _inspect_hints_for_rows(display, limit=2)
|
|
3619
|
-
|
|
3620
|
-
return 0
|
|
3598
|
+
return _emit(env, args, noun="symbol")
|
|
3621
3599
|
|
|
3622
3600
|
|
|
3623
3601
|
def _cmd_imports(args: argparse.Namespace) -> int:
|
|
@@ -3730,8 +3708,7 @@ def _cmd_imports(args: argparse.Namespace) -> int:
|
|
|
3730
3708
|
|
|
3731
3709
|
env = Envelope(status="ok", nodes=nodes, edges=edges, warnings=warnings)
|
|
3732
3710
|
next_actions_hook(env, result_edges=edges)
|
|
3733
|
-
|
|
3734
|
-
return 0
|
|
3711
|
+
return _emit(env, args, noun="import")
|
|
3735
3712
|
|
|
3736
3713
|
|
|
3737
3714
|
# ============================================================================
|
|
@@ -3743,8 +3720,8 @@ def _cmd_imports(args: argparse.Namespace) -> int:
|
|
|
3743
3720
|
# (kv-block + nested dict sections) so the agent sees compact structured data.
|
|
3744
3721
|
#
|
|
3745
3722
|
# Search dispatches to search_v2 (mcp_v2.search_v2) after building a NodeFilter
|
|
3746
|
-
# from flags. --fuzzy is registered on the parser
|
|
3747
|
-
#
|
|
3723
|
+
# from flags. --fuzzy is registered on the parser and accepted as a silent
|
|
3724
|
+
# no-op (search is inherently semantic; --fuzzy is implicit).
|
|
3748
3725
|
# ============================================================================
|
|
3749
3726
|
|
|
3750
3727
|
|
|
@@ -3781,7 +3758,6 @@ def _cmd_map(args: argparse.Namespace) -> int:
|
|
|
3781
3758
|
axis, which made "group by ALL modules" unreachable.
|
|
3782
3759
|
"""
|
|
3783
3760
|
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook
|
|
3784
|
-
from java_codebase_rag.jrag_render import render
|
|
3785
3761
|
|
|
3786
3762
|
_, graph, rc = _load_graph_or_error(args)
|
|
3787
3763
|
if rc:
|
|
@@ -3835,14 +3811,12 @@ def _cmd_map(args: argparse.Namespace) -> int:
|
|
|
3835
3811
|
hints.append(f"jrag overview {drill_scope}")
|
|
3836
3812
|
hints.append("jrag conventions")
|
|
3837
3813
|
env.agent_next_actions = hints[:5]
|
|
3838
|
-
|
|
3839
|
-
return 0
|
|
3814
|
+
return _emit(env, args, noun="map", shape="inspect")
|
|
3840
3815
|
|
|
3841
3816
|
|
|
3842
3817
|
def _cmd_conventions(args: argparse.Namespace) -> int:
|
|
3843
3818
|
"""conventions [--service] — dominant roles + framework tallies."""
|
|
3844
3819
|
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook
|
|
3845
|
-
from java_codebase_rag.jrag_render import render
|
|
3846
3820
|
|
|
3847
3821
|
_, graph, rc = _load_graph_or_error(args)
|
|
3848
3822
|
if rc:
|
|
@@ -3904,8 +3878,7 @@ def _cmd_conventions(args: argparse.Namespace) -> int:
|
|
|
3904
3878
|
hints.append(f"jrag find --role {top_role}{scope_suffix}")
|
|
3905
3879
|
hints.append("jrag map")
|
|
3906
3880
|
env.agent_next_actions = hints[:5]
|
|
3907
|
-
|
|
3908
|
-
return 0
|
|
3881
|
+
return _emit(env, args, noun="conventions", shape="inspect")
|
|
3909
3882
|
|
|
3910
3883
|
|
|
3911
3884
|
def _overview_detect_type(subject: str, graph) -> str:
|
|
@@ -3940,7 +3913,6 @@ def _overview_microservice(args: argparse.Namespace, graph, microservice: str) -
|
|
|
3940
3913
|
bundle to empty (subject node) or keep all samples equally (rollup node).
|
|
3941
3914
|
"""
|
|
3942
3915
|
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook
|
|
3943
|
-
from java_codebase_rag.jrag_render import render
|
|
3944
3916
|
|
|
3945
3917
|
limit = _clamped_limit(args)
|
|
3946
3918
|
routes = graph.list_routes(microservice=microservice, limit=limit + 1)
|
|
@@ -3994,8 +3966,7 @@ def _overview_microservice(args: argparse.Namespace, graph, microservice: str) -
|
|
|
3994
3966
|
nodes={f"microservice:{microservice}": node},
|
|
3995
3967
|
)
|
|
3996
3968
|
next_actions_hook(env)
|
|
3997
|
-
|
|
3998
|
-
return 0
|
|
3969
|
+
return _emit(env, args, noun="overview", shape="inspect")
|
|
3999
3970
|
|
|
4000
3971
|
|
|
4001
3972
|
def _overview_route(args: argparse.Namespace, cfg, graph, route_path: str) -> int:
|
|
@@ -4009,8 +3980,7 @@ def _overview_route(args: argparse.Namespace, cfg, graph, route_path: str) -> in
|
|
|
4009
3980
|
cfg=cfg, graph=graph,
|
|
4010
3981
|
)
|
|
4011
3982
|
if renv.status != "ok" or node is None:
|
|
4012
|
-
|
|
4013
|
-
return 2 if renv.status == "error" else 0
|
|
3983
|
+
return _emit(renv, args)
|
|
4014
3984
|
|
|
4015
3985
|
if node.kind != "route":
|
|
4016
3986
|
env = Envelope(
|
|
@@ -4055,8 +4025,7 @@ def _overview_route(args: argparse.Namespace, cfg, graph, route_path: str) -> in
|
|
|
4055
4025
|
edges = edges[:limit]
|
|
4056
4026
|
env = Envelope(status="ok", nodes=nodes_dict, edges=edges, root=root_id, truncated=truncated)
|
|
4057
4027
|
next_actions_hook(env, root=root_id, result_edges=edges)
|
|
4058
|
-
|
|
4059
|
-
return 0
|
|
4028
|
+
return _emit(env, args, noun="overview")
|
|
4060
4029
|
|
|
4061
4030
|
|
|
4062
4031
|
def _overview_topic(args: argparse.Namespace, graph, topic: str) -> int:
|
|
@@ -4069,7 +4038,6 @@ def _overview_topic(args: argparse.Namespace, graph, topic: str) -> int:
|
|
|
4069
4038
|
samples, full = +limit samples.
|
|
4070
4039
|
"""
|
|
4071
4040
|
from java_codebase_rag.jrag_envelope import Envelope, next_actions_hook
|
|
4072
|
-
from java_codebase_rag.jrag_render import render
|
|
4073
4041
|
|
|
4074
4042
|
limit = _clamped_limit(args)
|
|
4075
4043
|
# Producers: exact topic match first, then substring match as fallback.
|
|
@@ -4123,8 +4091,7 @@ def _overview_topic(args: argparse.Namespace, graph, topic: str) -> int:
|
|
|
4123
4091
|
nodes={f"topic:{topic}": topic_node},
|
|
4124
4092
|
)
|
|
4125
4093
|
next_actions_hook(env)
|
|
4126
|
-
|
|
4127
|
-
return 0
|
|
4094
|
+
return _emit(env, args, noun="overview", shape="inspect")
|
|
4128
4095
|
|
|
4129
4096
|
|
|
4130
4097
|
def _cmd_overview(args: argparse.Namespace) -> int:
|
|
@@ -4247,24 +4214,17 @@ def _cmd_search(args: argparse.Namespace) -> int:
|
|
|
4247
4214
|
"""search <query> — semantic search via search_v2 over Lance tables.
|
|
4248
4215
|
|
|
4249
4216
|
Builds a NodeFilter from flags, calls search_v2 with limit+1 for +1-fetch
|
|
4250
|
-
truncation, and renders. --fuzzy is
|
|
4251
|
-
|
|
4217
|
+
truncation, and renders. --fuzzy is accepted as a silent no-op (search is
|
|
4218
|
+
always semantic; --fuzzy is implicit).
|
|
4252
4219
|
"""
|
|
4253
|
-
from java_codebase_rag.mcp import mcp_v2
|
|
4254
|
-
|
|
4255
4220
|
from java_codebase_rag.jrag_envelope import Envelope, mark_truncated, next_actions_hook, normalize_enum
|
|
4256
4221
|
from java_codebase_rag.jrag_render import render
|
|
4257
4222
|
|
|
4258
|
-
# --fuzzy:
|
|
4259
|
-
#
|
|
4260
|
-
#
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
status="error",
|
|
4264
|
-
message="search is semantic; --fuzzy is implicit",
|
|
4265
|
-
)
|
|
4266
|
-
print(render(env, fmt=args.format, detail=args.detail))
|
|
4267
|
-
return 2
|
|
4223
|
+
# --fuzzy: accepted as a silent no-op. Search is inherently semantic
|
|
4224
|
+
# (vector + lexical), so --fuzzy is implicit; the flag is kept registered
|
|
4225
|
+
# so callers/agents that pass it don't hit an argparse or envelope error,
|
|
4226
|
+
# and is simply ignored here.
|
|
4227
|
+
_ = getattr(args, "fuzzy", False)
|
|
4268
4228
|
|
|
4269
4229
|
cfg, graph, rc = _load_graph_or_error(args)
|
|
4270
4230
|
if rc:
|
|
@@ -4282,8 +4242,7 @@ def _cmd_search(args: argparse.Namespace) -> int:
|
|
|
4282
4242
|
warnings=_auto_scope_notice(args),
|
|
4283
4243
|
)
|
|
4284
4244
|
next_actions_hook(env)
|
|
4285
|
-
|
|
4286
|
-
return 0
|
|
4245
|
+
return _emit(env, args, noun="search")
|
|
4287
4246
|
|
|
4288
4247
|
# Build NodeFilter from flags (same set as `find` filter mode).
|
|
4289
4248
|
filter_dict: dict = {}
|
|
@@ -4325,23 +4284,21 @@ def _cmd_search(args: argparse.Namespace) -> int:
|
|
|
4325
4284
|
)
|
|
4326
4285
|
print(render(env, fmt=args.format, detail=args.detail))
|
|
4327
4286
|
return 2
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
dedup=not getattr(args, "chunks", False),
|
|
4344
|
-
)
|
|
4287
|
+
from java_codebase_rag.read_payloads import PayloadError, search_payload
|
|
4288
|
+
from java_codebase_rag.watch.client import get_payload
|
|
4289
|
+
|
|
4290
|
+
# search_payload builds the NodeFilter from args (same filter_dict set above)
|
|
4291
|
+
# and calls search_v2 with limit+1. On filter-validation failure it raises
|
|
4292
|
+
# PayloadError carrying the error Envelope (rendered identically to before).
|
|
4293
|
+
# get_payload tries the watch daemon first (hot), cold-falling-back to the
|
|
4294
|
+
# identical search_payload core when no daemon is alive (every non-watch jrag
|
|
4295
|
+
# invocation). Reconstructs the SearchOutput object on the hot path so the
|
|
4296
|
+
# downstream render is unchanged.
|
|
4297
|
+
try:
|
|
4298
|
+
out = get_payload("search", vars(args), cfg, cold_core=search_payload)
|
|
4299
|
+
except PayloadError as pe:
|
|
4300
|
+
print(render(pe.env, fmt=args.format, detail=args.detail))
|
|
4301
|
+
return pe.rc
|
|
4345
4302
|
|
|
4346
4303
|
if not out.success:
|
|
4347
4304
|
env = Envelope(status="error", message=out.message or "search failed")
|
|
@@ -4430,8 +4387,7 @@ def _cmd_search(args: argparse.Namespace) -> int:
|
|
|
4430
4387
|
if display:
|
|
4431
4388
|
env.agent_next_actions = _inspect_hints_for_rows(display, limit=2)
|
|
4432
4389
|
next_offset = args.offset + limit if truncated else None
|
|
4433
|
-
|
|
4434
|
-
return 0
|
|
4390
|
+
return _emit(env, args, noun="search", next_offset=next_offset)
|
|
4435
4391
|
|
|
4436
4392
|
|
|
4437
4393
|
def _suppress_runtime_stderr_noise() -> None:
|
|
@@ -4551,9 +4507,19 @@ def _console_script_main() -> None:
|
|
|
4551
4507
|
(SIGABRT, exit -6). Flushing + ``os._exit`` skips that racy teardown - the
|
|
4552
4508
|
command has already done its work and emitted its result. ``main()`` stays
|
|
4553
4509
|
return-based so in-process test callers keep working.
|
|
4510
|
+
|
|
4511
|
+
``KeyboardInterrupt`` (Ctrl+C during a long indexing step) is caught here so
|
|
4512
|
+
it routes through the same flush + ``os._exit`` path — clean, immediate exit
|
|
4513
|
+
(code 130, no traceback) and no finalization-time SIGABRT — instead of
|
|
4514
|
+
propagating past this function.
|
|
4554
4515
|
"""
|
|
4555
4516
|
force_utf8_stdio()
|
|
4556
|
-
|
|
4517
|
+
try:
|
|
4518
|
+
rc = main()
|
|
4519
|
+
except KeyboardInterrupt:
|
|
4520
|
+
sys.stderr.write("\nInterrupted.\n")
|
|
4521
|
+
sys.stderr.flush()
|
|
4522
|
+
rc = 130
|
|
4557
4523
|
sys.stdout.flush()
|
|
4558
4524
|
sys.stderr.flush()
|
|
4559
4525
|
os._exit(rc)
|