java-codebase-rag 0.6.7__py3-none-any.whl → 0.8.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.
- ast_java.py +8 -3
- build_ast_graph.py +72 -16
- graph_enrich.py +2 -1
- graph_types.py +133 -0
- java_codebase_rag/_fdlimit.py +10 -2
- java_codebase_rag/_stdio.py +32 -0
- java_codebase_rag/cli.py +135 -24
- java_codebase_rag/config.py +128 -9
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +291 -0
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +8 -8
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +8 -8
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +251 -0
- java_codebase_rag/installer.py +438 -103
- java_codebase_rag/jrag.py +4300 -0
- java_codebase_rag/jrag_envelope.py +1085 -0
- java_codebase_rag/jrag_hints.py +204 -0
- java_codebase_rag/jrag_render.py +688 -0
- java_codebase_rag/pipeline.py +20 -0
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/METADATA +137 -94
- java_codebase_rag-0.8.0.dist-info/RECORD +43 -0
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/WHEEL +1 -1
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/entry_points.txt +1 -0
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/top_level.txt +2 -0
- java_index_flow_lancedb.py +34 -19
- java_ontology.py +12 -0
- ladybug_queries.py +233 -52
- mcp_hints.py +6 -6
- mcp_v2.py +205 -617
- resolve_service.py +649 -0
- search_lancedb.py +10 -1
- server.py +20 -12
- java_codebase_rag-0.6.7.dist-info/RECORD +0 -34
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/licenses/LICENSE +0 -0
ast_java.py
CHANGED
|
@@ -1868,6 +1868,11 @@ def _collect_outgoing_calls(
|
|
|
1868
1868
|
file_rel: str,
|
|
1869
1869
|
) -> list[OutgoingCallDecl]:
|
|
1870
1870
|
del project_root
|
|
1871
|
+
from java_ontology import ( # deferred: java_ontology imports ast_java
|
|
1872
|
+
CLIENT_KIND_FEIGN_METHOD,
|
|
1873
|
+
CLIENT_KIND_REST_TEMPLATE,
|
|
1874
|
+
CLIENT_KIND_WEB_CLIENT,
|
|
1875
|
+
)
|
|
1871
1876
|
out: list[OutgoingCallDecl] = []
|
|
1872
1877
|
method_fqn = f"{type_fqn}#{method_decl.signature}"
|
|
1873
1878
|
type_mods = _find_modifiers_child(type_node) if type_node is not None else None
|
|
@@ -1899,7 +1904,7 @@ def _collect_outgoing_calls(
|
|
|
1899
1904
|
OutgoingCallDecl(
|
|
1900
1905
|
method_fqn=method_fqn,
|
|
1901
1906
|
method_sig=method_decl.signature,
|
|
1902
|
-
client_kind=
|
|
1907
|
+
client_kind=CLIENT_KIND_FEIGN_METHOD,
|
|
1903
1908
|
channel="http",
|
|
1904
1909
|
feign_target_name=feign_target_name,
|
|
1905
1910
|
feign_target_url=feign_target_url,
|
|
@@ -1998,7 +2003,7 @@ def _collect_outgoing_calls(
|
|
|
1998
2003
|
OutgoingCallDecl(
|
|
1999
2004
|
method_fqn=method_fqn,
|
|
2000
2005
|
method_sig=method_decl.signature,
|
|
2001
|
-
client_kind=
|
|
2006
|
+
client_kind=CLIENT_KIND_REST_TEMPLATE,
|
|
2002
2007
|
channel="http",
|
|
2003
2008
|
feign_target_name="",
|
|
2004
2009
|
feign_target_url="",
|
|
@@ -2051,7 +2056,7 @@ def _collect_outgoing_calls(
|
|
|
2051
2056
|
OutgoingCallDecl(
|
|
2052
2057
|
method_fqn=method_fqn,
|
|
2053
2058
|
method_sig=method_decl.signature,
|
|
2054
|
-
client_kind=
|
|
2059
|
+
client_kind=CLIENT_KIND_WEB_CLIENT,
|
|
2055
2060
|
channel="http",
|
|
2056
2061
|
feign_target_name="",
|
|
2057
2062
|
feign_target_url="",
|
build_ast_graph.py
CHANGED
|
@@ -66,7 +66,13 @@ from graph_enrich import (
|
|
|
66
66
|
symbol_id,
|
|
67
67
|
)
|
|
68
68
|
from path_filtering import LayeredIgnore, iter_java_source_files
|
|
69
|
-
from java_ontology import
|
|
69
|
+
from java_ontology import (
|
|
70
|
+
CLIENT_KIND_FEIGN_METHOD,
|
|
71
|
+
CLIENT_KIND_REST_TEMPLATE,
|
|
72
|
+
VALID_CLIENT_KINDS,
|
|
73
|
+
VALID_HTTP_CALL_MATCHES,
|
|
74
|
+
VALID_PRODUCER_KINDS,
|
|
75
|
+
)
|
|
70
76
|
|
|
71
77
|
log = logging.getLogger(__name__)
|
|
72
78
|
|
|
@@ -472,10 +478,26 @@ class IncrementalResult:
|
|
|
472
478
|
elapsed_sec: float
|
|
473
479
|
|
|
474
480
|
|
|
481
|
+
# --- Builder-owned files in the index dir (single source of truth) ---------------
|
|
482
|
+
# Every artifact the graph builder writes next to code_graph.lbug. The lifecycle
|
|
483
|
+
# CLI's `erase` clears all of these from one list so the builder and erase cannot
|
|
484
|
+
# drift (issues #349 / #350): previously erase hardcoded ".graph_hashes.json" only
|
|
485
|
+
# and left the crash marker (.graph_increment_in_progress) and the atomic-write
|
|
486
|
+
# temp (.graph_hashes.json.tmp) behind on disk.
|
|
487
|
+
GRAPH_HASHES_FILENAME = ".graph_hashes.json"
|
|
488
|
+
GRAPH_HASHES_TMP_FILENAME = ".graph_hashes.json.tmp"
|
|
489
|
+
GRAPH_INCREMENT_MARKER_FILENAME = ".graph_increment_in_progress"
|
|
490
|
+
BUILDER_OWNED_INDEX_FILES: tuple[str, ...] = (
|
|
491
|
+
GRAPH_HASHES_FILENAME,
|
|
492
|
+
GRAPH_HASHES_TMP_FILENAME,
|
|
493
|
+
GRAPH_INCREMENT_MARKER_FILENAME,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
475
497
|
class FileHashTracker:
|
|
476
498
|
"""Track content hashes for incremental graph rebuild."""
|
|
477
499
|
def __init__(self, index_dir: Path):
|
|
478
|
-
self._path = index_dir /
|
|
500
|
+
self._path = index_dir / GRAPH_HASHES_FILENAME
|
|
479
501
|
self._hashes: dict[str, str] = {} # rel_path -> sha256_hex
|
|
480
502
|
|
|
481
503
|
def load(self) -> None:
|
|
@@ -491,7 +513,7 @@ class FileHashTracker:
|
|
|
491
513
|
|
|
492
514
|
def save(self) -> None:
|
|
493
515
|
"""Persist hashes to disk atomically (write .tmp, rename)."""
|
|
494
|
-
tmp_path = self._path.
|
|
516
|
+
tmp_path = self._path.parent / GRAPH_HASHES_TMP_FILENAME
|
|
495
517
|
try:
|
|
496
518
|
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
497
519
|
json.dump(self._hashes, f, sort_keys=True)
|
|
@@ -576,7 +598,7 @@ def _load_existing_types(conn: ladybug.Connection, tables: GraphTables, exclude_
|
|
|
576
598
|
query = f"""
|
|
577
599
|
MATCH (s:Symbol)
|
|
578
600
|
{where}
|
|
579
|
-
RETURN s.kind, s.fqn, s.name, s.filename, s.module, s.microservice, s.id
|
|
601
|
+
RETURN s.kind, s.fqn, s.name, s.filename, s.module, s.microservice, s.id, s.role
|
|
580
602
|
"""
|
|
581
603
|
result = conn.execute(query, params)
|
|
582
604
|
while result.has_next():
|
|
@@ -585,6 +607,7 @@ def _load_existing_types(conn: ladybug.Connection, tables: GraphTables, exclude_
|
|
|
585
607
|
module = row[4] if len(row) > 4 else ""
|
|
586
608
|
microservice = row[5] if len(row) > 5 else ""
|
|
587
609
|
node_id = row[6] if len(row) > 6 else ""
|
|
610
|
+
role = row[7] if len(row) > 7 else ""
|
|
588
611
|
|
|
589
612
|
decl = TypeDecl(name, kind, fqn)
|
|
590
613
|
package = fqn[: -(len(name) + 1)] if fqn.endswith("." + name) else ""
|
|
@@ -602,6 +625,10 @@ def _load_existing_types(conn: ladybug.Connection, tables: GraphTables, exclude_
|
|
|
602
625
|
tables.types[fqn] = entry
|
|
603
626
|
tables.by_simple_name.setdefault(name, []).append(entry)
|
|
604
627
|
tables.by_package.setdefault(package, []).append(entry)
|
|
628
|
+
# Seed the persisted role so the annotation-less stub is not recomputed to
|
|
629
|
+
# the default during node staging (issue #352 divergence #2).
|
|
630
|
+
if role:
|
|
631
|
+
tables.type_role_by_node_id[node_id] = role
|
|
605
632
|
|
|
606
633
|
|
|
607
634
|
def _load_existing_members(conn: ladybug.Connection, tables: GraphTables, exclude_files: set[str] | None = None) -> None:
|
|
@@ -2382,7 +2409,7 @@ def pass5_imperative_edges(
|
|
|
2382
2409
|
)
|
|
2383
2410
|
rid = ""
|
|
2384
2411
|
strategy = call.resolution_strategy
|
|
2385
|
-
if call.client_kind ==
|
|
2412
|
+
if call.client_kind == CLIENT_KIND_FEIGN_METHOD:
|
|
2386
2413
|
exposing = next((e for e in tables.exposes_rows if e.symbol_id == member.node_id), None)
|
|
2387
2414
|
if exposing is not None:
|
|
2388
2415
|
rid = exposing.route_id
|
|
@@ -2585,7 +2612,7 @@ def _match_call_edge(
|
|
|
2585
2612
|
return "unresolved", []
|
|
2586
2613
|
|
|
2587
2614
|
candidates: list[RouteRow] = []
|
|
2588
|
-
if call.client_kind ==
|
|
2615
|
+
if call.client_kind == CLIENT_KIND_FEIGN_METHOD:
|
|
2589
2616
|
# Prefer endpoint matching by target service + path/method for Feign declarations.
|
|
2590
2617
|
path_value = call.path_template_call
|
|
2591
2618
|
method_value = call.method_call
|
|
@@ -2714,7 +2741,7 @@ def pass6_match_edges(
|
|
|
2714
2741
|
if src_route is None and member is not None:
|
|
2715
2742
|
# Recover feign caller hints from persisted caller-side Client declarations.
|
|
2716
2743
|
for client in client_hints_by_member.get(member.node_id, ()):
|
|
2717
|
-
if client.client_kind !=
|
|
2744
|
+
if client.client_kind != CLIENT_KIND_FEIGN_METHOD:
|
|
2718
2745
|
continue
|
|
2719
2746
|
path_template, path_regex = _normalize_path(client.path)
|
|
2720
2747
|
src_route = RouteRow(
|
|
@@ -2750,7 +2777,7 @@ def pass6_match_edges(
|
|
|
2750
2777
|
call = OutgoingCallDecl(
|
|
2751
2778
|
method_fqn=f"{member.parent_fqn}#{member.decl.signature}" if member else "",
|
|
2752
2779
|
method_sig=member.decl.signature if member else "",
|
|
2753
|
-
client_kind=
|
|
2780
|
+
client_kind=CLIENT_KIND_FEIGN_METHOD if _feign_like else CLIENT_KIND_REST_TEMPLATE,
|
|
2754
2781
|
channel="http",
|
|
2755
2782
|
feign_target_name=src_route.feign_name if src_route else "",
|
|
2756
2783
|
feign_target_url=src_route.feign_url if src_route else "",
|
|
@@ -3207,15 +3234,24 @@ def _write_nodes_impl(
|
|
|
3207
3234
|
))
|
|
3208
3235
|
# types
|
|
3209
3236
|
for entry in tables.types.values():
|
|
3210
|
-
if entry.loaded_from_db:
|
|
3211
|
-
stub_ids.add(entry.node_id)
|
|
3212
3237
|
d = entry.decl
|
|
3213
3238
|
role, capabilities = resolve_role_and_capabilities(
|
|
3214
3239
|
d,
|
|
3215
3240
|
overrides=overrides,
|
|
3216
3241
|
meta_chain=mch,
|
|
3217
3242
|
)
|
|
3218
|
-
|
|
3243
|
+
if entry.loaded_from_db:
|
|
3244
|
+
stub_ids.add(entry.node_id)
|
|
3245
|
+
# Out-of-scope stub: its annotation-less decl collapses role to the
|
|
3246
|
+
# default. The real node's role was persisted at index time and seeded
|
|
3247
|
+
# into type_role_by_node_id by _load_existing_types; trust it so CALLS
|
|
3248
|
+
# edges into this type keep the correct callee_declaring_role (#352).
|
|
3249
|
+
# The staged row is filtered out of the write via stub_ids, so its
|
|
3250
|
+
# capabilities placeholder never reaches the graph.
|
|
3251
|
+
role = tables.type_role_by_node_id.get(entry.node_id, role)
|
|
3252
|
+
capabilities = []
|
|
3253
|
+
else:
|
|
3254
|
+
tables.type_role_by_node_id[entry.node_id] = role
|
|
3219
3255
|
rows.append(_node_row(
|
|
3220
3256
|
id=entry.node_id, kind=d.kind, name=d.name, fqn=d.fqn,
|
|
3221
3257
|
package=entry.package,
|
|
@@ -3424,13 +3460,15 @@ def _write_edges(conn: ladybug.Connection, tables: GraphTables, _file_by_node_id
|
|
|
3424
3460
|
_bulk_copy(conn, "OVERRIDES", _REL_OVERRIDES_COLUMNS, overrides_rows)
|
|
3425
3461
|
|
|
3426
3462
|
# Stage CALLS rows with dedup and callee_declaring_role materialization
|
|
3427
|
-
seen_calls: set[tuple[str, str, int, int]] = set()
|
|
3463
|
+
seen_calls: set[tuple[str, str, int, int, int]] = set()
|
|
3428
3464
|
calls_rows: list[dict] = []
|
|
3429
3465
|
member_by_id = {m.node_id: m for m in tables.members}
|
|
3430
3466
|
for row in tables.calls_rows:
|
|
3431
3467
|
if row.src_id not in valid_ids or row.dst_id not in valid_ids:
|
|
3432
3468
|
continue
|
|
3433
|
-
|
|
3469
|
+
# Include call_site_byte so two call sites of the same method on the same
|
|
3470
|
+
# source line (same arg_count) are kept as distinct edges (issue #359).
|
|
3471
|
+
key = (row.src_id, row.dst_id, row.arg_count, row.call_site_line, row.call_site_byte)
|
|
3434
3472
|
if key in seen_calls:
|
|
3435
3473
|
continue
|
|
3436
3474
|
seen_calls.add(key)
|
|
@@ -3606,10 +3644,15 @@ def _write_routes_and_exposes(conn: ladybug.Connection, tables: GraphTables, _fi
|
|
|
3606
3644
|
|
|
3607
3645
|
|
|
3608
3646
|
def _write_meta(conn: ladybug.Connection, tables: GraphTables, source_root: Path) -> None:
|
|
3609
|
-
|
|
3647
|
+
# Dedup key MUST match _write_edges (build_ast_graph.py, _REL_CALLS writer): the
|
|
3648
|
+
# 5-tuple includes call_site_byte so two call sites of the same method on the
|
|
3649
|
+
# same source line are counted separately. A previous version used the 4-tuple
|
|
3650
|
+
# here, which made counts['calls'] (678) diverge from the real CALLS edge count
|
|
3651
|
+
# (684) that _write_edges actually persisted — describe/stats then undercounted.
|
|
3652
|
+
seen_calls: set[tuple[str, str, int, int, int]] = set()
|
|
3610
3653
|
calls_unique = 0
|
|
3611
3654
|
for row in tables.calls_rows:
|
|
3612
|
-
key = (row.src_id, row.dst_id, row.arg_count, row.call_site_line)
|
|
3655
|
+
key = (row.src_id, row.dst_id, row.arg_count, row.call_site_line, row.call_site_byte)
|
|
3613
3656
|
if key not in seen_calls:
|
|
3614
3657
|
seen_calls.add(key)
|
|
3615
3658
|
calls_unique += 1
|
|
@@ -3773,6 +3816,7 @@ def incremental_rebuild(
|
|
|
3773
3816
|
if verbose:
|
|
3774
3817
|
_verbose_stderr_line(f"[increment] ontology version {version} < 17; falling back to full rebuild")
|
|
3775
3818
|
conn.close()
|
|
3819
|
+
db.close()
|
|
3776
3820
|
del conn, db
|
|
3777
3821
|
return _fallback_to_full(source_root, ladybug_path, verbose, t_start)
|
|
3778
3822
|
except Exception as e:
|
|
@@ -3780,6 +3824,7 @@ def incremental_rebuild(
|
|
|
3780
3824
|
_verbose_stderr_line(f"[increment] failed to read ontology version: {e}; falling back to full rebuild")
|
|
3781
3825
|
try:
|
|
3782
3826
|
conn.close()
|
|
3827
|
+
db.close()
|
|
3783
3828
|
except Exception:
|
|
3784
3829
|
pass
|
|
3785
3830
|
del conn, db
|
|
@@ -3798,6 +3843,7 @@ def incremental_rebuild(
|
|
|
3798
3843
|
if verbose:
|
|
3799
3844
|
_verbose_stderr_line("[increment] no changes detected; no-op")
|
|
3800
3845
|
conn.close()
|
|
3846
|
+
db.close()
|
|
3801
3847
|
return IncrementalResult(
|
|
3802
3848
|
mode="incremental",
|
|
3803
3849
|
files_changed=0,
|
|
@@ -3811,11 +3857,12 @@ def incremental_rebuild(
|
|
|
3811
3857
|
_verbose_stderr_line(f"[increment] detected {len(added)} added, {len(changed)} changed, {len(removed)} removed files")
|
|
3812
3858
|
|
|
3813
3859
|
# Step 2: Crash marker check
|
|
3814
|
-
crash_marker_path = index_dir /
|
|
3860
|
+
crash_marker_path = index_dir / GRAPH_INCREMENT_MARKER_FILENAME
|
|
3815
3861
|
if crash_marker_path.exists():
|
|
3816
3862
|
if verbose:
|
|
3817
3863
|
_verbose_stderr_line("[increment] crash marker exists; falling back to full rebuild")
|
|
3818
3864
|
conn.close()
|
|
3865
|
+
db.close()
|
|
3819
3866
|
crash_marker_path.unlink(missing_ok=True)
|
|
3820
3867
|
return _fallback_to_full(source_root, ladybug_path, verbose, t_start)
|
|
3821
3868
|
|
|
@@ -3850,6 +3897,7 @@ def incremental_rebuild(
|
|
|
3850
3897
|
if verbose:
|
|
3851
3898
|
_verbose_stderr_line(f"[increment] dependent expansion cap ({expansion_cap}) exceeded ({len(scope_files)} files); falling back to full rebuild")
|
|
3852
3899
|
conn.close()
|
|
3900
|
+
db.close()
|
|
3853
3901
|
crash_marker_path.unlink(missing_ok=True)
|
|
3854
3902
|
return _fallback_to_full(source_root, ladybug_path, verbose, t_start)
|
|
3855
3903
|
|
|
@@ -3896,6 +3944,11 @@ def incremental_rebuild(
|
|
|
3896
3944
|
# Rebuild full tables for global pass 5-6 (pass1 populates members from scratch)
|
|
3897
3945
|
tables_for_global = GraphTables()
|
|
3898
3946
|
global_asts = pass1_parse(source_root, tables_for_global, verbose=verbose)
|
|
3947
|
+
# pass4 (routes/EXPOSES) must run on the global pass5/6 tables too (issue
|
|
3948
|
+
# #352): pass5 links Feign HTTP_CALLS to routes via exposes_rows, and pass6
|
|
3949
|
+
# matches against routes_rows. Without pass4 both stay empty and the
|
|
3950
|
+
# HTTP_CALLS match outcome drifts from a full rebuild. Mirrors main().
|
|
3951
|
+
pass4_routes(tables_for_global, global_asts, source_root=source_root, verbose=verbose)
|
|
3899
3952
|
|
|
3900
3953
|
pass5_imperative_edges(tables_for_global, global_asts, source_root=source_root, verbose=verbose)
|
|
3901
3954
|
|
|
@@ -3929,6 +3982,7 @@ def incremental_rebuild(
|
|
|
3929
3982
|
crash_marker_path.unlink(missing_ok=True)
|
|
3930
3983
|
|
|
3931
3984
|
conn.close()
|
|
3985
|
+
db.close()
|
|
3932
3986
|
|
|
3933
3987
|
elapsed = time.time() - t_start
|
|
3934
3988
|
if verbose:
|
|
@@ -3948,6 +4002,7 @@ def incremental_rebuild(
|
|
|
3948
4002
|
if verbose:
|
|
3949
4003
|
_verbose_stderr_line(f"[increment] error during incremental rebuild: {e}; falling back to full rebuild")
|
|
3950
4004
|
conn.close()
|
|
4005
|
+
db.close()
|
|
3951
4006
|
crash_marker_path.unlink(missing_ok=True)
|
|
3952
4007
|
return _fallback_to_full(source_root, ladybug_path, verbose, t_start)
|
|
3953
4008
|
|
|
@@ -4152,6 +4207,7 @@ def write_ladybug(
|
|
|
4152
4207
|
_verbose_stderr_line(f"[graph] writing · routes/exposes written in {time.time() - t2:.2f}s")
|
|
4153
4208
|
_write_meta(conn, tables, source_root)
|
|
4154
4209
|
conn.close()
|
|
4210
|
+
db.close()
|
|
4155
4211
|
_init_hash_tracker(source_root, db_path)
|
|
4156
4212
|
|
|
4157
4213
|
|
graph_enrich.py
CHANGED
|
@@ -43,6 +43,7 @@ from ast_java import (
|
|
|
43
43
|
_TYPE_ANN_TO_CAPABILITY,
|
|
44
44
|
)
|
|
45
45
|
from java_ontology import (
|
|
46
|
+
CLIENT_KIND_REST_TEMPLATE,
|
|
46
47
|
VALID_CAPABILITIES,
|
|
47
48
|
VALID_CLIENT_KINDS,
|
|
48
49
|
VALID_PRODUCER_KINDS,
|
|
@@ -1301,7 +1302,7 @@ def resolve_http_client_for_method(
|
|
|
1301
1302
|
hint = overrides.annotation_to_http_client_hint.get("CodebaseHttpClient")
|
|
1302
1303
|
if hint is None:
|
|
1303
1304
|
hint = HttpClientHint(
|
|
1304
|
-
client_kind=anchor.client_kind if anchor else
|
|
1305
|
+
client_kind=anchor.client_kind if anchor else CLIENT_KIND_REST_TEMPLATE,
|
|
1305
1306
|
target_service=anchor.feign_target_name if anchor else "",
|
|
1306
1307
|
path=anchor.path_template_call if anchor else "",
|
|
1307
1308
|
method=anchor.method_call if anchor else "",
|
graph_types.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Shared graph types and helpers used by mcp_v2 and resolve_service.
|
|
2
|
+
|
|
3
|
+
This is the neutral, acyclic shared module. It must NOT import from
|
|
4
|
+
``mcp_v2`` or ``resolve_service`` — both of those import FROM here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from ladybug_queries import LadybugGraph
|
|
14
|
+
from mcp_hints import generate_hints
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"NodeRef",
|
|
18
|
+
"StructuredHint",
|
|
19
|
+
"set_hints_enabled",
|
|
20
|
+
"_hints_or_skip",
|
|
21
|
+
"_node_kind_from_id",
|
|
22
|
+
"_resolve_node_kind",
|
|
23
|
+
"_node_ref_from_row",
|
|
24
|
+
"_to_structured_hints",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NodeRef(BaseModel):
|
|
29
|
+
id: str
|
|
30
|
+
kind: Literal["symbol", "route", "client", "producer", "unresolved_call_site"]
|
|
31
|
+
fqn: str
|
|
32
|
+
name: str | None = None
|
|
33
|
+
symbol_kind: str | None = None
|
|
34
|
+
microservice: str | None = None
|
|
35
|
+
module: str | None = None
|
|
36
|
+
role: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StructuredHint(BaseModel):
|
|
40
|
+
label: str = ""
|
|
41
|
+
tool: Literal["search", "find", "describe", "neighbors", "resolve"]
|
|
42
|
+
args: dict[str, Any]
|
|
43
|
+
actionable: bool = True
|
|
44
|
+
reason: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Module-level flag set by server.py at startup from resolved config.
|
|
48
|
+
# Single source of truth — both mcp_v2 and resolve_service read this via
|
|
49
|
+
# _hints_or_skip, and server.py sets it via set_hints_enabled (re-exported
|
|
50
|
+
# by mcp_v2 for back-comat).
|
|
51
|
+
_hints_enabled: bool = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def set_hints_enabled(enabled: bool) -> None:
|
|
55
|
+
global _hints_enabled
|
|
56
|
+
_hints_enabled = enabled
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _hints_or_skip(tool: str, payload: dict) -> tuple[list, list]:
|
|
60
|
+
return generate_hints(tool, payload) if _hints_enabled else ([], [])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _node_kind_from_id(
|
|
64
|
+
id_str: str,
|
|
65
|
+
) -> Literal["symbol", "route", "client", "producer", "unresolved_call_site"]:
|
|
66
|
+
if id_str.startswith("ucs:"):
|
|
67
|
+
return "unresolved_call_site"
|
|
68
|
+
if id_str.startswith("sym:"):
|
|
69
|
+
return "symbol"
|
|
70
|
+
if id_str.startswith("route:") or id_str.startswith("r:"):
|
|
71
|
+
return "route"
|
|
72
|
+
if id_str.startswith("client:") or id_str.startswith("c:"):
|
|
73
|
+
return "client"
|
|
74
|
+
if id_str.startswith("producer:") or id_str.startswith("p:"):
|
|
75
|
+
return "producer"
|
|
76
|
+
raise ValueError(f"Unknown id prefix for `{id_str}`")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _resolve_node_kind(
|
|
80
|
+
graph: LadybugGraph,
|
|
81
|
+
node_id: str,
|
|
82
|
+
) -> Literal["symbol", "route", "client", "producer", "unresolved_call_site"]:
|
|
83
|
+
try:
|
|
84
|
+
return _node_kind_from_id(node_id)
|
|
85
|
+
except ValueError:
|
|
86
|
+
pass
|
|
87
|
+
if graph._rows("MATCH (n:Symbol) WHERE n.id = $id RETURN n.id AS id LIMIT 1", {"id": node_id}): # noqa: SLF001
|
|
88
|
+
return "symbol"
|
|
89
|
+
if graph._rows("MATCH (n:Route) WHERE n.id = $id RETURN n.id AS id LIMIT 1", {"id": node_id}): # noqa: SLF001
|
|
90
|
+
return "route"
|
|
91
|
+
if graph._rows("MATCH (n:Client) WHERE n.id = $id RETURN n.id AS id LIMIT 1", {"id": node_id}): # noqa: SLF001
|
|
92
|
+
return "client"
|
|
93
|
+
if graph._rows("MATCH (n:Producer) WHERE n.id = $id RETURN n.id AS id LIMIT 1", {"id": node_id}): # noqa: SLF001
|
|
94
|
+
return "producer"
|
|
95
|
+
raise ValueError(f"Unknown id prefix for `{node_id}`")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _node_ref_from_row(kind: Literal["symbol", "route", "client", "producer"], row: dict[str, Any]) -> NodeRef:
|
|
99
|
+
symbol_kind: str | None = None
|
|
100
|
+
if kind == "symbol":
|
|
101
|
+
fqn = str(row.get("fqn") or "")
|
|
102
|
+
role = str(row.get("role") or "") or None
|
|
103
|
+
symbol_kind_val = str(row.get("symbol_kind") or row.get("kind") or "").strip()
|
|
104
|
+
symbol_kind = symbol_kind_val or None
|
|
105
|
+
elif kind == "route":
|
|
106
|
+
method = str(row.get("method") or "")
|
|
107
|
+
path = str(row.get("path_template") or row.get("path") or "")
|
|
108
|
+
fqn = f"{method} {path}".strip()
|
|
109
|
+
role = None
|
|
110
|
+
elif kind == "client":
|
|
111
|
+
method = str(row.get("method") or "")
|
|
112
|
+
target = str(row.get("target_service") or "")
|
|
113
|
+
path = str(row.get("path_template") or row.get("path") or "")
|
|
114
|
+
fqn = f"{target} {method} {path}".strip()
|
|
115
|
+
role = None
|
|
116
|
+
else:
|
|
117
|
+
topic = str(row.get("topic") or "")
|
|
118
|
+
broker = str(row.get("broker") or "")
|
|
119
|
+
fqn = f"{topic} {broker}".strip()
|
|
120
|
+
role = None
|
|
121
|
+
return NodeRef(
|
|
122
|
+
id=str(row.get("id") or ""),
|
|
123
|
+
kind=kind,
|
|
124
|
+
fqn=fqn,
|
|
125
|
+
symbol_kind=symbol_kind,
|
|
126
|
+
microservice=str(row.get("microservice") or "") or None,
|
|
127
|
+
module=str(row.get("module") or "") or None,
|
|
128
|
+
role=role,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _to_structured_hints(raw: list[Any]) -> list[StructuredHint]:
|
|
133
|
+
return [StructuredHint(label=h.label, tool=h.tool, args=h.args, actionable=h.actionable, reason=h.reason) for h in raw]
|
java_codebase_rag/_fdlimit.py
CHANGED
|
@@ -22,7 +22,15 @@ See https://github.com/HumanBean17/java-codebase-rag/issues/306
|
|
|
22
22
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
try:
|
|
26
|
+
# Unix-only: the ``resource`` module does not exist on Windows. Importing it
|
|
27
|
+
# unconditionally at module scope crashes on Windows, which (because
|
|
28
|
+
# ``cli.py`` and ``server.py`` both import ``raise_fd_limit``) made the
|
|
29
|
+
# entire CLI and MCP server fail to start there. Guard the import so the
|
|
30
|
+
# module loads everywhere; the function below no-ops when it's absent.
|
|
31
|
+
import resource
|
|
32
|
+
except ImportError: # pragma: no cover - Windows lacks the resource module
|
|
33
|
+
resource = None # type: ignore[assignment]
|
|
26
34
|
|
|
27
35
|
# Safe ceiling well above LanceDB's appetite, comfortably below macOS libc
|
|
28
36
|
# quirks. The hard limit caps it further if lower (locked-down servers).
|
|
@@ -35,7 +43,7 @@ def raise_fd_limit(cap: int = _DEFAULT_CAP) -> None:
|
|
|
35
43
|
Best-effort and silent: never raises. No-op where ``RLIMIT_NOFILE`` is
|
|
36
44
|
unsupported (Windows) or where the soft limit already meets ``min(hard, cap)``.
|
|
37
45
|
"""
|
|
38
|
-
if not hasattr(resource, "RLIMIT_NOFILE"):
|
|
46
|
+
if resource is None or not hasattr(resource, "RLIMIT_NOFILE"):
|
|
39
47
|
return
|
|
40
48
|
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
41
49
|
target = min(hard, cap)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Force stdout/stderr to UTF-8 so non-ASCII glyphs never crash the CLI.
|
|
2
|
+
|
|
3
|
+
The text renderers emit Unicode glyphs — ``↑``/``↓`` (hierarchy tree headers
|
|
4
|
+
in ``jrag_render``), ``✓`` (success markers in ``cli_format``), ``→``/``…``
|
|
5
|
+
(listing/role lines). On Windows, ``sys.stdout``/``sys.stderr`` default to the
|
|
6
|
+
system ANSI codepage (cp1252 on en-US Windows), which can't encode those
|
|
7
|
+
characters, so ``print()`` raises ``UnicodeEncodeError`` and the process exits
|
|
8
|
+
non-zero. Unix platforms already default to UTF-8, so this is a no-op there.
|
|
9
|
+
|
|
10
|
+
Called from the console-script entry points (``_console_script_main``), not
|
|
11
|
+
from in-process ``main()`` callers, so a test that drives ``main()`` directly
|
|
12
|
+
keeps whatever stdout the host wired up.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def force_utf8_stdio() -> None:
|
|
21
|
+
"""Reconfigure ``sys.stdout``/``sys.stderr`` to UTF-8.
|
|
22
|
+
|
|
23
|
+
Best-effort and silent: never raises. No-op where a stream lacks
|
|
24
|
+
``reconfigure`` (streams replaced by capture frameworks that don't expose
|
|
25
|
+
it). ``errors="replace"`` is a last-resort safety net so a hostile console
|
|
26
|
+
can never crash a run; under UTF-8 every codepoint encodes cleanly, so
|
|
27
|
+
replacement never actually fires for the glyphs we emit.
|
|
28
|
+
"""
|
|
29
|
+
for stream in (sys.stdout, sys.stderr):
|
|
30
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
31
|
+
if reconfigure is not None:
|
|
32
|
+
reconfigure(encoding="utf-8", errors="replace")
|