java-codebase-rag 0.6.3__py3-none-any.whl → 0.6.4__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 +28 -57
- build_ast_graph.py +23 -19
- graph_enrich.py +13 -35
- java_codebase_rag/cli.py +10 -10
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +3 -3
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +5 -5
- {java_codebase_rag-0.6.3.dist-info → java_codebase_rag-0.6.4.dist-info}/METADATA +10 -10
- {java_codebase_rag-0.6.3.dist-info → java_codebase_rag-0.6.4.dist-info}/RECORD +18 -18
- java_index_flow_lancedb.py +86 -21
- ladybug_queries.py +45 -51
- mcp_v2.py +9 -17
- path_filtering.py +14 -17
- pr_analysis.py +3 -4
- search_lancedb.py +2 -2
- {java_codebase_rag-0.6.3.dist-info → java_codebase_rag-0.6.4.dist-info}/WHEEL +0 -0
- {java_codebase_rag-0.6.3.dist-info → java_codebase_rag-0.6.4.dist-info}/entry_points.txt +0 -0
- {java_codebase_rag-0.6.3.dist-info → java_codebase_rag-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {java_codebase_rag-0.6.3.dist-info → java_codebase_rag-0.6.4.dist-info}/top_level.txt +0 -0
ast_java.py
CHANGED
|
@@ -14,8 +14,8 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import posixpath
|
|
16
16
|
import sys
|
|
17
|
+
import threading
|
|
17
18
|
from dataclasses import dataclass, field
|
|
18
|
-
from functools import lru_cache
|
|
19
19
|
from typing import Iterable
|
|
20
20
|
|
|
21
21
|
import tree_sitter_java as _ts_java
|
|
@@ -215,10 +215,23 @@ _ANON_SUPER_AS_INTERFACE: frozenset[str] = frozenset({
|
|
|
215
215
|
})
|
|
216
216
|
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
# tree-sitter's ``Parser`` mutates internal state during ``parse()`` and is NOT
|
|
219
|
+
# thread-safe, so each OS thread gets its own instance. ``parse_java`` is called
|
|
220
|
+
# concurrently from worker threads when indexing runs with cocoindex's inflight
|
|
221
|
+
# parallelism — both directly (java_index_flow_lancedb.py: process_java_file
|
|
222
|
+
# offloads parse+enrich to asyncio.to_thread) and transitively
|
|
223
|
+
# (graph_enrich.collect_annotation_meta_chain -> _collect_annotation_decl_index
|
|
224
|
+
# -> parse_java, reached from enrich_chunk). The ``Language`` is immutable and
|
|
225
|
+
# shared; per-thread ``Parser`` construction is lazy and cheap (once per thread),
|
|
226
|
+
# which also preserves parse parallelism instead of serializing it.
|
|
227
|
+
_parser_tls = threading.local()
|
|
228
|
+
|
|
229
|
+
|
|
219
230
|
def _parser() -> Parser:
|
|
220
|
-
|
|
221
|
-
|
|
231
|
+
p = getattr(_parser_tls, "parser", None)
|
|
232
|
+
if p is None:
|
|
233
|
+
_parser_tls.parser = p = Parser(Language(_ts_java.language()))
|
|
234
|
+
return p
|
|
222
235
|
|
|
223
236
|
|
|
224
237
|
# ---------- dataclasses ----------
|
|
@@ -1565,62 +1578,20 @@ def _parse_codebase_http_route_inner_annotation(
|
|
|
1565
1578
|
return out
|
|
1566
1579
|
|
|
1567
1580
|
|
|
1568
|
-
def
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
def visit(n: Node) -> None:
|
|
1572
|
-
if n.type == "annotation":
|
|
1573
|
-
name_node = n.child_by_field_name("name")
|
|
1574
|
-
n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
|
|
1575
|
-
if n_simple == "CodebaseHttpRoute":
|
|
1576
|
-
found.append(n)
|
|
1577
|
-
for c in n.children:
|
|
1578
|
-
visit(c)
|
|
1581
|
+
def _inner_annotation_nodes(container_ann: Node, src: bytes, target_simple: str) -> list[Node]:
|
|
1582
|
+
"""Collect nested ``@<target_simple>`` annotations anywhere under ``container_ann``.
|
|
1579
1583
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
def _codebase_async_route_inner_annotation_nodes(container_ann: Node, src: bytes) -> list[Node]:
|
|
1585
|
-
found: list[Node] = []
|
|
1586
|
-
|
|
1587
|
-
def visit(n: Node) -> None:
|
|
1588
|
-
if n.type == "annotation":
|
|
1589
|
-
name_node = n.child_by_field_name("name")
|
|
1590
|
-
n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
|
|
1591
|
-
if n_simple == "CodebaseAsyncRoute":
|
|
1592
|
-
found.append(n)
|
|
1593
|
-
for c in n.children:
|
|
1594
|
-
visit(c)
|
|
1595
|
-
|
|
1596
|
-
visit(container_ann)
|
|
1597
|
-
return found
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
def _codebase_http_client_inner_annotation_nodes(container_ann: Node, src: bytes) -> list[Node]:
|
|
1601
|
-
found: list[Node] = []
|
|
1602
|
-
|
|
1603
|
-
def visit(n: Node) -> None:
|
|
1604
|
-
if n.type == "annotation":
|
|
1605
|
-
name_node = n.child_by_field_name("name")
|
|
1606
|
-
n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
|
|
1607
|
-
if n_simple == "CodebaseHttpClient":
|
|
1608
|
-
found.append(n)
|
|
1609
|
-
for c in n.children:
|
|
1610
|
-
visit(c)
|
|
1611
|
-
|
|
1612
|
-
visit(container_ann)
|
|
1613
|
-
return found
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
def _codebase_producer_inner_annotation_nodes(container_ann: Node, src: bytes) -> list[Node]:
|
|
1584
|
+
Shared by the four brownfield container walkers — ``CodebaseHttpRoute``,
|
|
1585
|
+
``CodebaseAsyncRoute``, ``CodebaseHttpClient``, ``CodebaseProducer`` — which
|
|
1586
|
+
differ only by the target annotation simple name.
|
|
1587
|
+
"""
|
|
1617
1588
|
found: list[Node] = []
|
|
1618
1589
|
|
|
1619
1590
|
def visit(n: Node) -> None:
|
|
1620
1591
|
if n.type == "annotation":
|
|
1621
1592
|
name_node = n.child_by_field_name("name")
|
|
1622
1593
|
n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
|
|
1623
|
-
if n_simple ==
|
|
1594
|
+
if n_simple == target_simple:
|
|
1624
1595
|
found.append(n)
|
|
1625
1596
|
for c in n.children:
|
|
1626
1597
|
visit(c)
|
|
@@ -1842,7 +1813,7 @@ def _outgoing_calls_from_codebase_http_client_producer_annotations(
|
|
|
1842
1813
|
),
|
|
1843
1814
|
)
|
|
1844
1815
|
elif simple == "CodebaseHttpClients":
|
|
1845
|
-
for inner in
|
|
1816
|
+
for inner in _inner_annotation_nodes(ann, src, "CodebaseHttpClient"):
|
|
1846
1817
|
out.append(
|
|
1847
1818
|
_parse_codebase_http_client_annotation(
|
|
1848
1819
|
inner,
|
|
@@ -1869,7 +1840,7 @@ def _outgoing_calls_from_codebase_http_client_producer_annotations(
|
|
|
1869
1840
|
),
|
|
1870
1841
|
)
|
|
1871
1842
|
elif simple == "CodebaseProducers":
|
|
1872
|
-
for inner in
|
|
1843
|
+
for inner in _inner_annotation_nodes(ann, src, "CodebaseProducer"):
|
|
1873
1844
|
out.append(
|
|
1874
1845
|
_parse_codebase_producer_annotation(
|
|
1875
1846
|
inner,
|
|
@@ -2343,7 +2314,7 @@ def _collect_routes(
|
|
|
2343
2314
|
),
|
|
2344
2315
|
)
|
|
2345
2316
|
elif simple == "CodebaseHttpRoutes":
|
|
2346
|
-
for inner in
|
|
2317
|
+
for inner in _inner_annotation_nodes(node, src, "CodebaseHttpRoute"):
|
|
2347
2318
|
routes.extend(
|
|
2348
2319
|
_parse_codebase_http_route_inner_annotation(
|
|
2349
2320
|
inner,
|
|
@@ -2359,7 +2330,7 @@ def _collect_routes(
|
|
|
2359
2330
|
elif simple in ("CodebaseAsyncRoute", "CodebaseAsyncRoutes"):
|
|
2360
2331
|
nodes = [node]
|
|
2361
2332
|
if simple == "CodebaseAsyncRoutes":
|
|
2362
|
-
nodes = list(
|
|
2333
|
+
nodes = list(_inner_annotation_nodes(node, src, "CodebaseAsyncRoute"))
|
|
2363
2334
|
for ann in nodes:
|
|
2364
2335
|
pairs, _ = _annotation_kv_nodes(ann, src)
|
|
2365
2336
|
topic_node = pairs.get("topic")
|
build_ast_graph.py
CHANGED
|
@@ -2010,8 +2010,21 @@ def _producer_id(
|
|
|
2010
2010
|
return f"p:{hashlib.sha1(key.encode()).hexdigest()[:16]}"
|
|
2011
2011
|
|
|
2012
2012
|
|
|
2013
|
+
# The four brownfield source layers — single source of truth. Consumed by the
|
|
2014
|
+
# client/producer source-layer classifiers, the *_from_brownfield_pct stats
|
|
2015
|
+
# (via brownfield_strategies), and the brownfield_only authoritativeness gate in
|
|
2016
|
+
# _is_brownfield_sourced. codebase_client/codebase_producer are caller-side
|
|
2017
|
+
# declaration strategies, not layers — they extend brownfield_strategies only.
|
|
2018
|
+
_BROWNFIELD_LAYERS = frozenset({
|
|
2019
|
+
"layer_a_meta",
|
|
2020
|
+
"layer_b_ann",
|
|
2021
|
+
"layer_b_fqn",
|
|
2022
|
+
"layer_c_source",
|
|
2023
|
+
})
|
|
2024
|
+
|
|
2025
|
+
|
|
2013
2026
|
def _client_source_layer(strategy: str) -> str:
|
|
2014
|
-
if strategy in
|
|
2027
|
+
if strategy in _BROWNFIELD_LAYERS:
|
|
2015
2028
|
return strategy
|
|
2016
2029
|
# Some caller extraction paths emit client kind as strategy; treat those
|
|
2017
2030
|
# as builtin-source declarations instead of warning on every row.
|
|
@@ -2023,7 +2036,7 @@ def _client_source_layer(strategy: str) -> str:
|
|
|
2023
2036
|
|
|
2024
2037
|
|
|
2025
2038
|
def _producer_source_layer(strategy: str) -> str:
|
|
2026
|
-
if strategy in
|
|
2039
|
+
if strategy in _BROWNFIELD_LAYERS:
|
|
2027
2040
|
return strategy
|
|
2028
2041
|
if strategy in VALID_PRODUCER_KINDS:
|
|
2029
2042
|
return "builtin"
|
|
@@ -2458,15 +2471,14 @@ def pass5_imperative_edges(
|
|
|
2458
2471
|
tables.producer_stats.producers_by_kind = defaultdict(int)
|
|
2459
2472
|
for row in tables.producer_rows:
|
|
2460
2473
|
tables.producer_stats.producers_by_kind[row.producer_kind] += 1
|
|
2461
|
-
brownfield_strategies =
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
),
|
|
2474
|
+
# brownfield_strategies = the four brownfield layers plus the two
|
|
2475
|
+
# caller-side declaration strategies (@CodebaseHttpClient /
|
|
2476
|
+
# @CodebaseProducer). These extend _BROWNFIELD_LAYERS deliberately:
|
|
2477
|
+
# the *_from_brownfield_pct stats count annotation-declared callers as
|
|
2478
|
+
# brownfield-sourced even though they are not "layers" and so do not
|
|
2479
|
+
# gate brownfield_only authoritativeness in _is_brownfield_sourced.
|
|
2480
|
+
brownfield_strategies = _BROWNFIELD_LAYERS | frozenset(
|
|
2481
|
+
{"codebase_client", "codebase_producer"},
|
|
2470
2482
|
)
|
|
2471
2483
|
if tables.call_edge_stats.http_calls_total:
|
|
2472
2484
|
n_http = sum(
|
|
@@ -2568,14 +2580,6 @@ def _match_call_edge(
|
|
|
2568
2580
|
return "cross_service", candidates
|
|
2569
2581
|
|
|
2570
2582
|
|
|
2571
|
-
_BROWNFIELD_LAYERS = frozenset({
|
|
2572
|
-
"layer_c_source",
|
|
2573
|
-
"layer_b_ann",
|
|
2574
|
-
"layer_b_fqn",
|
|
2575
|
-
"layer_a_meta",
|
|
2576
|
-
})
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
2583
|
def _is_brownfield_sourced(
|
|
2580
2584
|
call_strategy: str,
|
|
2581
2585
|
candidates: list[RouteRow],
|
graph_enrich.py
CHANGED
|
@@ -23,7 +23,7 @@ import sys
|
|
|
23
23
|
from dataclasses import dataclass, field, replace
|
|
24
24
|
from functools import lru_cache
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import Any
|
|
26
|
+
from typing import Any, TypeVar
|
|
27
27
|
from ast_java import (
|
|
28
28
|
AnnotationRef,
|
|
29
29
|
JavaFileAst,
|
|
@@ -820,7 +820,15 @@ def _route_path_atom(raw_value: str, value_kind: str | None) -> tuple[str, str,
|
|
|
820
820
|
return "", "constant_ref", 0.7, False
|
|
821
821
|
|
|
822
822
|
|
|
823
|
-
|
|
823
|
+
_HINT = TypeVar("_HINT")
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _hint_lookup(ann: AnnotationRef, hints: dict[str, _HINT]) -> _HINT | None:
|
|
827
|
+
"""Resolve a brownfield hint by qualified name, then simple name, then suffix.
|
|
828
|
+
|
|
829
|
+
Shared by route / http-client / async-producer hint resolution; the three
|
|
830
|
+
former copies differed only in the hint value type.
|
|
831
|
+
"""
|
|
824
832
|
q = ann.qualified.strip()
|
|
825
833
|
if q in hints:
|
|
826
834
|
return hints[q]
|
|
@@ -1118,7 +1126,7 @@ def resolve_routes_for_method(
|
|
|
1118
1126
|
|
|
1119
1127
|
# ----- Step 2: Layer B — annotation route hints -----
|
|
1120
1128
|
for _is_m, ann in combined_anns:
|
|
1121
|
-
hint =
|
|
1129
|
+
hint = _hint_lookup(ann, overrides.annotation_to_route_hint)
|
|
1122
1130
|
if hint is None:
|
|
1123
1131
|
continue
|
|
1124
1132
|
working.append(
|
|
@@ -1172,36 +1180,6 @@ def resolve_routes_for_method(
|
|
|
1172
1180
|
return working
|
|
1173
1181
|
|
|
1174
1182
|
|
|
1175
|
-
def _client_hint_lookup(
|
|
1176
|
-
ann: AnnotationRef,
|
|
1177
|
-
hints: dict[str, HttpClientHint],
|
|
1178
|
-
) -> HttpClientHint | None:
|
|
1179
|
-
q = ann.qualified.strip()
|
|
1180
|
-
if q in hints:
|
|
1181
|
-
return hints[q]
|
|
1182
|
-
if ann.name in hints:
|
|
1183
|
-
return hints[ann.name]
|
|
1184
|
-
for k, h in sorted(hints.items(), key=lambda kv: kv[0]):
|
|
1185
|
-
if k.endswith("." + ann.name):
|
|
1186
|
-
return h
|
|
1187
|
-
return None
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
def _async_hint_lookup(
|
|
1191
|
-
ann: AnnotationRef,
|
|
1192
|
-
hints: dict[str, AsyncProducerHint],
|
|
1193
|
-
) -> AsyncProducerHint | None:
|
|
1194
|
-
q = ann.qualified.strip()
|
|
1195
|
-
if q in hints:
|
|
1196
|
-
return hints[q]
|
|
1197
|
-
if ann.name in hints:
|
|
1198
|
-
return hints[ann.name]
|
|
1199
|
-
for k, h in sorted(hints.items(), key=lambda kv: kv[0]):
|
|
1200
|
-
if k.endswith("." + ann.name):
|
|
1201
|
-
return h
|
|
1202
|
-
return None
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
1183
|
def _call_from_http_hint(
|
|
1206
1184
|
*,
|
|
1207
1185
|
hint: HttpClientHint,
|
|
@@ -1296,7 +1274,7 @@ def resolve_http_client_for_method(
|
|
|
1296
1274
|
anchor = builtin_http[0] if builtin_http else (layer_c_src[0] if layer_c_src else None)
|
|
1297
1275
|
|
|
1298
1276
|
for _is_m, ann in combined_anns:
|
|
1299
|
-
hint =
|
|
1277
|
+
hint = _hint_lookup(ann, overrides.annotation_to_http_client_hint)
|
|
1300
1278
|
if hint is None:
|
|
1301
1279
|
continue
|
|
1302
1280
|
brownfield_calls.append(
|
|
@@ -1388,7 +1366,7 @@ def resolve_async_producer_for_method(
|
|
|
1388
1366
|
anchor = builtin_async[0] if builtin_async else (layer_c_src[0] if layer_c_src else None)
|
|
1389
1367
|
|
|
1390
1368
|
for _is_m, ann in combined_anns:
|
|
1391
|
-
hint =
|
|
1369
|
+
hint = _hint_lookup(ann, overrides.annotation_to_async_producer_hint)
|
|
1392
1370
|
if hint is None:
|
|
1393
1371
|
continue
|
|
1394
1372
|
brownfield_calls.append(
|
java_codebase_rag/cli.py
CHANGED
|
@@ -251,7 +251,7 @@ def _startup_hints(cfg: ResolvedOperatorConfig) -> None:
|
|
|
251
251
|
|
|
252
252
|
def _add_index_embedding_flags(p: argparse.ArgumentParser) -> None:
|
|
253
253
|
p.add_argument("--source-root", type=str, default=None, help="Java repository root (default: cwd)")
|
|
254
|
-
p.add_argument("--index-dir", type=str, default=None, help="Index directory (Lance +
|
|
254
|
+
p.add_argument("--index-dir", type=str, default=None, help="Index directory (Lance + LadybugDB + cocoindex state)")
|
|
255
255
|
p.add_argument("--embedding-model", type=str, default=None, help="Override SBERT_MODEL / YAML embedding.model")
|
|
256
256
|
p.add_argument("--embedding-device", type=str, default=None, help="Override SBERT_DEVICE / YAML embedding.device")
|
|
257
257
|
|
|
@@ -536,7 +536,7 @@ def _cmd_reprocess(args: argparse.Namespace) -> int:
|
|
|
536
536
|
_emit_reprocess_outcome(payload, selective_tty_mode="graph" if ok else None)
|
|
537
537
|
return _reprocess_exit_code(payload)
|
|
538
538
|
|
|
539
|
-
import server # lazy: pulls sentence_transformers/torch/lancedb/
|
|
539
|
+
import server # lazy: pulls sentence_transformers/torch/lancedb/ladybug
|
|
540
540
|
|
|
541
541
|
result = asyncio.run(
|
|
542
542
|
server.run_refresh_pipeline(
|
|
@@ -715,7 +715,7 @@ def _cmd_unresolved_calls_list(args: argparse.Namespace) -> int:
|
|
|
715
715
|
from ladybug_queries import LadybugGraph # lazy
|
|
716
716
|
|
|
717
717
|
if not LadybugGraph.exists():
|
|
718
|
-
_emit({"success": False, "message": "
|
|
718
|
+
_emit({"success": False, "message": "LadybugDB graph not found"})
|
|
719
719
|
return 1
|
|
720
720
|
graph = LadybugGraph.get()
|
|
721
721
|
rows = graph.list_unresolved_call_sites(
|
|
@@ -736,7 +736,7 @@ def _cmd_unresolved_calls_stats(args: argparse.Namespace) -> int:
|
|
|
736
736
|
from ladybug_queries import LadybugGraph # lazy
|
|
737
737
|
|
|
738
738
|
if not LadybugGraph.exists():
|
|
739
|
-
_emit({"success": False, "message": "
|
|
739
|
+
_emit({"success": False, "message": "LadybugDB graph not found"})
|
|
740
740
|
return 1
|
|
741
741
|
graph = LadybugGraph.get()
|
|
742
742
|
buckets = graph.stats_unresolved_call_sites(by=args.by)
|
|
@@ -761,7 +761,7 @@ def _cmd_analyze_pr(args: argparse.Namespace) -> int:
|
|
|
761
761
|
from ladybug_queries import LadybugGraph # lazy
|
|
762
762
|
|
|
763
763
|
if not LadybugGraph.exists():
|
|
764
|
-
_emit({"success": False, "message": "
|
|
764
|
+
_emit({"success": False, "message": "LadybugDB graph not found"})
|
|
765
765
|
return 1
|
|
766
766
|
graph = LadybugGraph.get()
|
|
767
767
|
report = pr_analysis.analyze_pr_pipeline(graph, diff_text)
|
|
@@ -801,7 +801,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
801
801
|
help="Create a fresh index from a Java repository.",
|
|
802
802
|
description=(
|
|
803
803
|
"First-time index creation. Refuses if the resolved index directory "
|
|
804
|
-
"already contains a
|
|
804
|
+
"already contains a LadybugDB graph or Lance tables. Exit 2 on refusal."
|
|
805
805
|
),
|
|
806
806
|
)
|
|
807
807
|
_add_index_embedding_flags(init)
|
|
@@ -870,7 +870,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
870
870
|
increment = subparsers.add_parser(
|
|
871
871
|
"increment",
|
|
872
872
|
help="Pick up changes since the last index update.",
|
|
873
|
-
description="Runs cocoindex catch-up and incremental
|
|
873
|
+
description="Runs cocoindex catch-up and incremental LadybugDB graph update. Use --vectors-only to skip graph update.",
|
|
874
874
|
)
|
|
875
875
|
_add_index_embedding_flags(increment)
|
|
876
876
|
_add_verbosity_flags(increment)
|
|
@@ -883,9 +883,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
883
883
|
|
|
884
884
|
reprocess = subparsers.add_parser(
|
|
885
885
|
"reprocess",
|
|
886
|
-
help="Rebuild vectors and/or
|
|
886
|
+
help="Rebuild vectors and/or LadybugDB (default: both full phases).",
|
|
887
887
|
description=(
|
|
888
|
-
"Default: full Lance reprocess (cocoindex --full-reprocess) then full
|
|
888
|
+
"Default: full Lance reprocess (cocoindex --full-reprocess) then full LadybugDB graph rebuild. "
|
|
889
889
|
"Use --vectors-only or --graph-only to run a single phase (mutually exclusive)."
|
|
890
890
|
),
|
|
891
891
|
)
|
|
@@ -907,7 +907,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
907
907
|
erase = subparsers.add_parser(
|
|
908
908
|
"erase",
|
|
909
909
|
help="Delete the index from disk.",
|
|
910
|
-
description="Runs cocoindex drop, removes
|
|
910
|
+
description="Runs cocoindex drop, removes LadybugDB, and drops Lance tables. Requires --yes or TTY confirmation.",
|
|
911
911
|
)
|
|
912
912
|
_add_index_embedding_flags(erase)
|
|
913
913
|
erase.add_argument("--yes", action="store_true", help="Confirm destructive deletion (required in CI)")
|
|
@@ -151,15 +151,15 @@ Simple types in parentheses; generics erased. No spaces after commas. No-arg: `(
|
|
|
151
151
|
|
|
152
152
|
### Shared NodeFilter
|
|
153
153
|
|
|
154
|
-
For `find`, `filter` is required — `{}` means no predicates. **Strict frame:** unknown keys or inapplicable populated fields → `success=false
|
|
154
|
+
For `find`, `filter` is required — `{}` means no predicates. **Strict frame:** unknown keys or inapplicable populated fields → `success=false`; invalid enum values (e.g. wrong case) are rejected earlier at the schema layer with the valid set listed.
|
|
155
155
|
|
|
156
156
|
| Keys | Applies to |
|
|
157
157
|
| ---- | ---------- |
|
|
158
158
|
| `microservice`, `module` | All kinds |
|
|
159
159
|
| `role`, `exclude_roles`, `annotation`, `capability`, `fqn_prefix`, `symbol_kind`, `symbol_kinds` | **symbol** |
|
|
160
160
|
| `http_method`, `path_prefix`, `framework` | **route** |
|
|
161
|
-
| `client_kind`, `target_service`, `target_path_prefix`, `http_method` | **client** |
|
|
162
|
-
| `producer_kind`, `topic_prefix` | **producer** |
|
|
161
|
+
| `source_layer`, `client_kind`, `target_service`, `target_path_prefix`, `http_method` | **client** |
|
|
162
|
+
| `source_layer`, `producer_kind`, `topic_prefix` | **producer** |
|
|
163
163
|
|
|
164
164
|
No wildcards in prefix fields — use `search(query=…)` for fuzzy text.
|
|
165
165
|
|
|
@@ -125,15 +125,15 @@ Use these strings **verbatim** in `neighbors(..., edge_types=[...])`.
|
|
|
125
125
|
|
|
126
126
|
### NodeFilter (`find`, `search.filter`, `neighbors.filter`)
|
|
127
127
|
|
|
128
|
-
For `find`, `filter` is required — `{}` means no predicates. **Strict frame:** unknown keys or inapplicable populated fields → `success=false
|
|
128
|
+
For `find`, `filter` is required — `{}` means no predicates. **Strict frame:** unknown keys or inapplicable populated fields → `success=false`; invalid enum values (e.g. wrong case) are rejected earlier at the schema layer with the valid set listed.
|
|
129
129
|
|
|
130
130
|
| Applicable to | Keys |
|
|
131
131
|
| ------------- | ---- |
|
|
132
132
|
| All kinds | `microservice`, `module` |
|
|
133
133
|
| **symbol** only | `role`, `exclude_roles`, `annotation`, `capability`, `fqn_prefix`, `symbol_kind`, `symbol_kinds` |
|
|
134
134
|
| **route** only | `http_method`, `path_prefix`, `framework` |
|
|
135
|
-
| **client** only | `client_kind`, `target_service`, `target_path_prefix`, `http_method` |
|
|
136
|
-
| **producer** only | `producer_kind`, `topic_prefix` |
|
|
135
|
+
| **client** only | `source_layer`, `client_kind`, `target_service`, `target_path_prefix`, `http_method` |
|
|
136
|
+
| **producer** only | `source_layer`, `producer_kind`, `topic_prefix` |
|
|
137
137
|
|
|
138
138
|
No wildcards in prefix fields — use `search(query=…)` for ranked text.
|
|
139
139
|
|
|
@@ -166,8 +166,8 @@ Exclude `DTO`, `OTHER`, `MAPPER` with `exclude_roles` when tracing business logi
|
|
|
166
166
|
|
|
167
167
|
**Symbol kinds:** `class`, `interface`, `enum`, `record`, `annotation`, `method`, `constructor`.
|
|
168
168
|
|
|
169
|
-
**Route frameworks:** `spring_mvc`, `webflux
|
|
170
|
-
**Client kinds:** `feign_method`, `rest_template`, `web_client`. **Producer kinds:** `kafka_send`, `stream_bridge_send`.
|
|
169
|
+
**Route frameworks:** `spring_mvc`, `webflux`. (Route *kinds* are `http_endpoint`, `http_consumer`, `kafka_topic`, `rabbit_queue`, `jms_destination`, `stream_binding`.)
|
|
170
|
+
**Client kinds:** `feign_method`, `rest_template`, `web_client`. **Producer kinds:** `kafka_send`, `stream_bridge_send`. **Source layers (client/producer):** `builtin`, `layer_a_meta`, `layer_b_ann`, `layer_b_fqn`, `layer_c_source`.
|
|
171
171
|
**Match types:** `cross_service`, `intra_service`, `ambiguous`, `phantom`, `unresolved`.
|
|
172
172
|
|
|
173
173
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-codebase-rag
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: MCP server for semantic + structural search over Java codebases
|
|
5
5
|
Author: HumanBean17
|
|
6
6
|
License-Expression: MIT
|
|
@@ -44,7 +44,7 @@ Dynamic: license-file
|
|
|
44
44
|
|
|
45
45
|
A graph-native code intelligence layer for Java microservice estates, exposed to LLM agents via the **Model Context Protocol (MCP)**.
|
|
46
46
|
|
|
47
|
-
The system extracts a deterministic property graph from Java source (tree-sitter), stores it in **
|
|
47
|
+
The system extracts a deterministic property graph from Java source (tree-sitter), stores it in **LadybugDB** (graph) alongside a **LanceDB** vector index (chunks), and exposes a deliberately small MCP surface — **five tools**: `search`, `find`, `describe`, `neighbors`, `resolve` — that collapse onto three primitive agent operations: **locate**, **inspect**, **walk**.
|
|
48
48
|
|
|
49
49
|
> **What this MCP is:** a **GPS for code navigation**, not a reasoning engine.
|
|
50
50
|
> Agents use a simple loop:
|
|
@@ -63,9 +63,9 @@ For the design rationale, the GPS metaphor, and the full ontology, see [`docs/pa
|
|
|
63
63
|
|
|
64
64
|
Generic code-search tools (grep, ctags, vector-only RAG) hit a ceiling on real Java microservice estates: they find files but lose the structure that makes a Spring/JAX-RS system navigable. This project is built around five choices that target that gap.
|
|
65
65
|
|
|
66
|
-
- **Hybrid RAG + GraphRAG, not either-or.** Semantic recall (LanceDB chunk vectors) and structural navigation (
|
|
66
|
+
- **Hybrid RAG + GraphRAG, not either-or.** Semantic recall (LanceDB chunk vectors) and structural navigation (LadybugDB property graph) are composed in one surface. `search` finds candidate nodes by meaning; `neighbors` walks the exact edge you care about (`CALLS`, `IMPLEMENTS`, `INJECTS`, `EXPOSES`, …). The agent picks the right primitive per step instead of being forced into pure-vector or pure-symbol search.
|
|
67
67
|
|
|
68
|
-
- **A Java-tuned role model.** Symbols are labelled with stereotypes inferred from Spring and JAX-RS conventions — `CONTROLLER`, `SERVICE`, `REPOSITORY`, `
|
|
68
|
+
- **A Java-tuned role model.** Symbols are labelled with stereotypes inferred from Spring and JAX-RS conventions — `CONTROLLER`, `SERVICE`, `REPOSITORY`, `COMPONENT`, `CONFIG`, `ENTITY`, `CLIENT`, `MAPPER`, `DTO`. Agents can ask "list controllers" or "who injects this repository" directly, instead of grep-ing for `@RestController` and hoping for the best. Roles drive both filtering (`find` with a `NodeFilter`) and ranking.
|
|
69
69
|
|
|
70
70
|
- **Ranking specialized for Java codebases.** The composite ranker is aware of role, microservice, and FQN structure — not a generic BM25. A search for `"chat ingress"` surfaces controllers before utility classes; a search scoped to one microservice doesn't drown in matches from the other 19. Defaults are tuned on the bank-chat fixture and exposed in `docs/CONFIGURATION.md` for per-repo overrides.
|
|
71
71
|
|
|
@@ -113,7 +113,7 @@ All indexing lifecycle commands (`init`, `increment`, `reprocess`, `install`, `u
|
|
|
113
113
|
|
|
114
114
|
If you prefer manual configuration, see [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) for the full CLI reference.
|
|
115
115
|
|
|
116
|
-
> **Stability disclaimer.** This package does **not** promise backward compatibility. MCP tool contracts, env vars, Lance/
|
|
116
|
+
> **Stability disclaimer.** This package does **not** promise backward compatibility. MCP tool contracts, env vars, Lance/LadybugDB schemas, config files, and Python APIs may change without a deprecation period. Track `main` and rebuild indexes when ontology or embedding settings change.
|
|
117
117
|
|
|
118
118
|
---
|
|
119
119
|
|
|
@@ -126,7 +126,7 @@ This repo ships a small multi-module Spring fixture under [`tests/bank-chat-syst
|
|
|
126
126
|
git clone https://github.com/HumanBean17/java-codebase-rag
|
|
127
127
|
cd java-codebase-rag
|
|
128
128
|
|
|
129
|
-
# 2. Build the index (Lance vectors +
|
|
129
|
+
# 2. Build the index (Lance vectors + LadybugDB graph). First run downloads the
|
|
130
130
|
# embedding model (~90 MB) and takes ~30-60s on the fixture.
|
|
131
131
|
java-codebase-rag init --source-root tests/bank-chat-system --index-dir /tmp/bank-chat-index
|
|
132
132
|
|
|
@@ -141,7 +141,7 @@ Smoke-test the index with two checks (`search_lancedb` ships with the package):
|
|
|
141
141
|
JAVA_CODEBASE_RAG_INDEX_DIR=/tmp/bank-chat-index \
|
|
142
142
|
python -m search_lancedb "chat ingress controller" --table java --limit 3
|
|
143
143
|
|
|
144
|
-
# Vector + graph expansion — proves
|
|
144
|
+
# Vector + graph expansion — proves LadybugDB is wired in
|
|
145
145
|
JAVA_CODEBASE_RAG_INDEX_DIR=/tmp/bank-chat-index \
|
|
146
146
|
python -m search_lancedb "chat ingress controller" --table java --limit 3 \
|
|
147
147
|
--graph-expand --expand-depth 2
|
|
@@ -241,8 +241,8 @@ Run `java-codebase-rag --help` to list grouped subcommands. Operator playbook wi
|
|
|
241
241
|
| Setup | `install` | Interactive setup wizard: config, MCP registration, skill/agent deployment, indexing. |
|
|
242
242
|
| Setup | `update` | Refresh shipped artifacts (skill, agent, MCP entry) + incremental Lance/graph catch-up after pip upgrade. |
|
|
243
243
|
| Lifecycle | `init` | First-time index. Refuses if artifacts already exist. |
|
|
244
|
-
| Lifecycle | `increment` | CocoIndex catch-up + incremental
|
|
245
|
-
| Lifecycle | `reprocess` | Full Lance +
|
|
244
|
+
| Lifecycle | `increment` | CocoIndex catch-up + incremental LadybugDB update. `--vectors-only` for Lance only. |
|
|
245
|
+
| Lifecycle | `reprocess` | Full Lance + LadybugDB rebuild. `--vectors-only` / `--graph-only` for a single phase. |
|
|
246
246
|
| Lifecycle | `erase` | Delete index artifacts. Requires `--yes` or TTY confirm. |
|
|
247
247
|
| Introspection | `meta`, `tables`, `diagnose-ignore`, `unresolved-calls` | Health, table listing, ignore-layer diagnostics, receiver-failure call sites. |
|
|
248
248
|
| Analysis | `analyze-pr` | Blast-radius / risk from a unified diff. |
|
|
@@ -277,7 +277,7 @@ python3 -m venv .venv
|
|
|
277
277
|
|
|
278
278
|
The `cocoindex` package powers lifecycle commands that run the indexer (`init`, `increment`, `reprocess`, `erase`). Search and MCP navigation do not invoke it directly.
|
|
279
279
|
|
|
280
|
-
The default embedding model is `sentence-transformers/all-MiniLM-L6-v2` (downloaded on first `init`). Override via the `
|
|
280
|
+
The default embedding model is `sentence-transformers/all-MiniLM-L6-v2` (downloaded on first `init`). Override via the `SBERT_MODEL` env var — see [`docs/CONFIGURATION.md` §1](./docs/CONFIGURATION.md#1-environment-variables).
|
|
281
281
|
|
|
282
282
|
---
|
|
283
283
|
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
ast_java.py,sha256=
|
|
1
|
+
ast_java.py,sha256=Paee3ZV9G5iy8LqfkVq0Ah_1fV0k632oS5JPkiWzwB8,99206
|
|
2
2
|
brownfield_events.py,sha256=yxXkKDgMb3VPtaiakGzncHM_EGnda8xIue6w90yYp8s,2055
|
|
3
|
-
build_ast_graph.py,sha256=
|
|
3
|
+
build_ast_graph.py,sha256=vXWUuNC_k6-yKv6XawlHCXiC_dTNs1RoSKdZTXYBxgA,157374
|
|
4
4
|
chunk_heuristics.py,sha256=aQk2NOKxzUdqoUAJUO3G3LE0MN_bYZWNLQ0tkmj5uts,1813
|
|
5
|
-
graph_enrich.py,sha256=
|
|
5
|
+
graph_enrich.py,sha256=W_OQK7YRU1K8wi-vfrxZWUd-EErTqWx840UBrhVpNsM,62788
|
|
6
6
|
index_common.py,sha256=HT6FKHFJ084eFvd3fR1j8z8gf4eWoPHVW8GXLpw464I,285
|
|
7
|
-
java_index_flow_lancedb.py,sha256=
|
|
7
|
+
java_index_flow_lancedb.py,sha256=HClxupO2uHpGw9GNVIDs24bjoCQzg3VLbOZoA0hE_Gc,23761
|
|
8
8
|
java_index_v1_common.py,sha256=nF1KrSqboF_RRvWerG9knRRFmWwsrG_CvhgnsoZ8KqA,1154
|
|
9
9
|
java_ontology.py,sha256=71bCLDNvMy0SpZPzSR5apJ0qJXNd6y5ggkLdBEw_PFo,16682
|
|
10
|
-
ladybug_queries.py,sha256=
|
|
10
|
+
ladybug_queries.py,sha256=7vSP7WsUKQYXHFenBMlCdpUFed39h3oul-WRkf55YVc,90330
|
|
11
11
|
mcp_hints.py,sha256=3swh05LSiWur3tm3-yssndBsLxIxFhy501kBtJI8jJ0,42509
|
|
12
|
-
mcp_v2.py,sha256=
|
|
13
|
-
path_filtering.py,sha256
|
|
14
|
-
pr_analysis.py,sha256=
|
|
15
|
-
search_lancedb.py,sha256=
|
|
12
|
+
mcp_v2.py,sha256=S8PiVGDlyI7cvTmeMdQobhKUxWqKoY2YxXXr4pu9lQI,80348
|
|
13
|
+
path_filtering.py,sha256=cN_znZRXIY1CfTIWlfiOrnNqafb2BZs00oDaqjTxmFE,17169
|
|
14
|
+
pr_analysis.py,sha256=zrmZZD5yotJtM02Kif6_jgI_oeformOao793akp0N6Y,18394
|
|
15
|
+
search_lancedb.py,sha256=ga_qySeCzA6hCibxUPXpCbTxW9mCjTdwirMhMhfNCz4,36801
|
|
16
16
|
server.py,sha256=DcJwocSknBy0vwsUvYiC5hr-hrD_rRT-u9_DmuRTC9k,35035
|
|
17
17
|
java_codebase_rag/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
18
18
|
java_codebase_rag/_fdlimit.py,sha256=WroFdfSNbcriKok6q8znTf74dqlznxea_1Fd5bHl_3o,1930
|
|
19
|
-
java_codebase_rag/cli.py,sha256=
|
|
19
|
+
java_codebase_rag/cli.py,sha256=TIrKNQMxu3_dSF2LXKsRKWY5RqRnSLQMrbKAoN4uQBQ,38698
|
|
20
20
|
java_codebase_rag/cli_format.py,sha256=CT7-xdwZ0bMCdP68_UOwkvm-mnLluU3LutlM-mDNk60,1839
|
|
21
21
|
java_codebase_rag/cli_progress.py,sha256=q6Wh97yzLGs1B8UFk_WAKivfQu7Y5RnUUE-T2YHWkIs,3237
|
|
22
22
|
java_codebase_rag/config.py,sha256=bfwYI4R8PU9YV_M4r8-03iaUZ_0TW-qN_NuhIsDXy2M,18769
|
|
@@ -24,11 +24,11 @@ java_codebase_rag/installer.py,sha256=sXsHPo24aoDFoTr0D_vYLg0MFdGAV2wdL05FqRaul6
|
|
|
24
24
|
java_codebase_rag/lance_optimize.py,sha256=25Rwj7HNO8F-35MxhFK6naqgbjd3H-T0zKb3pXB4H0s,9268
|
|
25
25
|
java_codebase_rag/pipeline.py,sha256=ydNktEGL1YniAjJsr37yKBo_bGV4cN_LTGVTmmrsrZw,14688
|
|
26
26
|
java_codebase_rag/progress.py,sha256=2IxdMALDM0wAQCyJrrfZ975zM_85C-4BfHxf4AtYifE,23212
|
|
27
|
-
java_codebase_rag/install_data/agents/explorer-rag-enhanced.md,sha256=
|
|
28
|
-
java_codebase_rag/install_data/skills/explore-codebase/SKILL.md,sha256=
|
|
29
|
-
java_codebase_rag-0.6.
|
|
30
|
-
java_codebase_rag-0.6.
|
|
31
|
-
java_codebase_rag-0.6.
|
|
32
|
-
java_codebase_rag-0.6.
|
|
33
|
-
java_codebase_rag-0.6.
|
|
34
|
-
java_codebase_rag-0.6.
|
|
27
|
+
java_codebase_rag/install_data/agents/explorer-rag-enhanced.md,sha256=BkdQpBEWqSdvGHgbqMdRb5CWfEiFRJK4Dgqbyal3l6s,14551
|
|
28
|
+
java_codebase_rag/install_data/skills/explore-codebase/SKILL.md,sha256=YkRnrM7Wh5E8raFjAW3RrN2V9-ov8upaGC3UdpSx6U8,12346
|
|
29
|
+
java_codebase_rag-0.6.4.dist-info/licenses/LICENSE,sha256=gxvtiHtuviR_q8ZAjWw-QTcF3DyPzg6ZY-lQrr8OPpw,1068
|
|
30
|
+
java_codebase_rag-0.6.4.dist-info/METADATA,sha256=FR0VQ3dS_OS2v-n_zxOqcE0pZsHRvhDAiOGLse0pAoI,17242
|
|
31
|
+
java_codebase_rag-0.6.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
32
|
+
java_codebase_rag-0.6.4.dist-info/entry_points.txt,sha256=wsPZwot0Ui4JI3TIgW8LcbN8bNtKFbwQAlHAAJXfYgQ,117
|
|
33
|
+
java_codebase_rag-0.6.4.dist-info/top_level.txt,sha256=syQgi8XPBwY2ws_NZ1uRCxTf_s41NpshwEHNdcdnk3A,245
|
|
34
|
+
java_codebase_rag-0.6.4.dist-info/RECORD,,
|
java_index_flow_lancedb.py
CHANGED
|
@@ -16,6 +16,7 @@ Usage:
|
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import asyncio
|
|
19
20
|
import inspect
|
|
20
21
|
import os
|
|
21
22
|
import sys
|
|
@@ -50,7 +51,7 @@ from java_index_v1_common import (
|
|
|
50
51
|
)
|
|
51
52
|
from path_filtering import LayeredIgnore
|
|
52
53
|
from ast_java import ONTOLOGY_VERSION, parse_java
|
|
53
|
-
from graph_enrich import enrich_chunk
|
|
54
|
+
from graph_enrich import collect_annotation_meta_chain, enrich_chunk, load_brownfield_overrides
|
|
54
55
|
|
|
55
56
|
# Older cocoindex (e.g. 1.0.0a43) uses ``tracked=False``; newer releases renamed
|
|
56
57
|
# the flag to ``detect_change`` (default False) and reject ``tracked``.
|
|
@@ -198,12 +199,12 @@ def _approximate_vectors_total(project_root: Path) -> int:
|
|
|
198
199
|
continue
|
|
199
200
|
# Java: **/*.java
|
|
200
201
|
if fn.endswith(".java"):
|
|
201
|
-
if not ignore.is_ignored(full)
|
|
202
|
+
if not ignore.is_ignored(full):
|
|
202
203
|
total += 1
|
|
203
204
|
continue
|
|
204
205
|
# SQL: **/src/main/resources/db/migration/*.sql
|
|
205
206
|
if fn.endswith(".sql") and "/db/migration/" in rel:
|
|
206
|
-
if not ignore.is_ignored(full)
|
|
207
|
+
if not ignore.is_ignored(full):
|
|
207
208
|
total += 1
|
|
208
209
|
continue
|
|
209
210
|
# YAML: **/src/main/resources/application*.yml / .yaml
|
|
@@ -214,7 +215,7 @@ def _approximate_vectors_total(project_root: Path) -> int:
|
|
|
214
215
|
# total below the actual done count. The ``rel``-based
|
|
215
216
|
# ``"/src/main/resources/"`` gate stays (full path component).
|
|
216
217
|
if fn.endswith((".yml", ".yaml")) and fn.startswith("application") and "/src/main/resources/" in rel:
|
|
217
|
-
if not ignore.is_ignored(full)
|
|
218
|
+
if not ignore.is_ignored(full):
|
|
218
219
|
total += 1
|
|
219
220
|
return total
|
|
220
221
|
|
|
@@ -306,6 +307,40 @@ async def coco_lifespan(builder: coco.EnvironmentBuilder) -> AsyncIterator[None]
|
|
|
306
307
|
yield
|
|
307
308
|
|
|
308
309
|
|
|
310
|
+
def _parse_and_enrich_java(
|
|
311
|
+
content_bytes: bytes,
|
|
312
|
+
chunks: list[Any],
|
|
313
|
+
rel: str,
|
|
314
|
+
project_root: Path,
|
|
315
|
+
) -> list[Any]:
|
|
316
|
+
"""Parse one Java file and enrich every chunk, off the event loop.
|
|
317
|
+
|
|
318
|
+
Returns a list of :class:`graph_enrich.ChunkEnrichment` aligned 1:1 with
|
|
319
|
+
``chunks``. Intended to run via ``asyncio.to_thread`` from
|
|
320
|
+
``process_java_file`` (vectors perf lever #2): while the worker thread
|
|
321
|
+
parses + enriches, the event loop is free to drive other files and keep the
|
|
322
|
+
embedder's batching queue fed.
|
|
323
|
+
|
|
324
|
+
Thread-safety: ``parse_java`` uses a per-thread tree-sitter ``Parser``
|
|
325
|
+
(see ``ast_java._parser``), so it is safe to call concurrently from these
|
|
326
|
+
worker threads — including the transitive ``parse_java`` that ``enrich_chunk``
|
|
327
|
+
triggers via ``collect_annotation_meta_chain`` → ``_collect_annotation_decl_index``.
|
|
328
|
+
``enrich_chunk`` is otherwise pure-Python over the now-immutable AST; its
|
|
329
|
+
``lru_cache`` reads are thread-safe under the GIL.
|
|
330
|
+
"""
|
|
331
|
+
ast = parse_java(content_bytes)
|
|
332
|
+
return [
|
|
333
|
+
enrich_chunk(
|
|
334
|
+
ast,
|
|
335
|
+
chunk_start_byte=ch.start.byte_offset,
|
|
336
|
+
chunk_end_byte=ch.end.byte_offset,
|
|
337
|
+
file_path=rel,
|
|
338
|
+
project_root=project_root,
|
|
339
|
+
)
|
|
340
|
+
for ch in chunks
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
|
|
309
344
|
@coco.fn(memo=True)
|
|
310
345
|
async def process_java_file(
|
|
311
346
|
file: localfs.File,
|
|
@@ -313,7 +348,7 @@ async def process_java_file(
|
|
|
313
348
|
) -> None:
|
|
314
349
|
embedder = coco.use_context(EMBEDDER)
|
|
315
350
|
project_root = coco.use_context(PROJECT_ROOT)
|
|
316
|
-
if LayeredIgnore(project_root).is_ignored((project_root / file.file_path.path).resolve())
|
|
351
|
+
if LayeredIgnore(project_root).is_ignored((project_root / file.file_path.path).resolve()):
|
|
317
352
|
return
|
|
318
353
|
try:
|
|
319
354
|
content = await file.read_text()
|
|
@@ -326,6 +361,9 @@ async def process_java_file(
|
|
|
326
361
|
|
|
327
362
|
language = detect_code_language(filename=file.file_path.path.name) or "text"
|
|
328
363
|
cs, mn, ov = JAVA_CHUNK
|
|
364
|
+
# ``splitter.split`` stays inline: the module-level ``RecursiveSplitter``
|
|
365
|
+
# shares one Rust object, so keeping split on the event loop preserves its
|
|
366
|
+
# existing single-threaded access (no new cross-file concurrency hazard).
|
|
329
367
|
chunks = splitter.split(
|
|
330
368
|
content,
|
|
331
369
|
cs,
|
|
@@ -335,18 +373,21 @@ async def process_java_file(
|
|
|
335
373
|
)
|
|
336
374
|
rel = file.file_path.path.as_posix()
|
|
337
375
|
content_bytes = content.encode("utf-8", errors="replace")
|
|
338
|
-
ast = parse_java(content_bytes)
|
|
339
376
|
|
|
340
|
-
|
|
377
|
+
# (vectors perf lever #2) parse + enrich off the event loop so the loop can
|
|
378
|
+
# keep the embedder's batching queue fed while this file is being parsed.
|
|
379
|
+
# parse_java is thread-safe (per-thread tree-sitter Parser in ast_java).
|
|
380
|
+
enrichments = await asyncio.to_thread(
|
|
381
|
+
_parse_and_enrich_java, content_bytes, chunks, rel, project_root
|
|
382
|
+
)
|
|
383
|
+
# (vectors perf lever #1) embed all chunks concurrently so the batched
|
|
384
|
+
# embedder groups them into one ``model.encode(...)`` (max_batch_size=64)
|
|
385
|
+
# instead of N serial batch-of-1 calls. Dominant win for ``increment``
|
|
386
|
+
# (few changed files → little cross-file concurrency → otherwise no batching).
|
|
387
|
+
embeddings = await asyncio.gather(*(embedder.embed(ch.text) for ch in chunks))
|
|
388
|
+
|
|
389
|
+
for ch, enrich, emb in zip(chunks, enrichments, embeddings):
|
|
341
390
|
rs, re = chunk_key_range(ch)
|
|
342
|
-
enrich = enrich_chunk(
|
|
343
|
-
ast,
|
|
344
|
-
chunk_start_byte=ch.start.byte_offset,
|
|
345
|
-
chunk_end_byte=ch.end.byte_offset,
|
|
346
|
-
file_path=rel,
|
|
347
|
-
project_root=project_root,
|
|
348
|
-
)
|
|
349
|
-
emb = await embedder.embed(ch.text)
|
|
350
391
|
table.declare_row(
|
|
351
392
|
row=JavaLanceChunk(
|
|
352
393
|
id=str(uuid.uuid4()),
|
|
@@ -379,7 +420,7 @@ async def process_sql_file(
|
|
|
379
420
|
) -> None:
|
|
380
421
|
embedder = coco.use_context(EMBEDDER)
|
|
381
422
|
project_root = coco.use_context(PROJECT_ROOT)
|
|
382
|
-
if LayeredIgnore(project_root).is_ignored((project_root / file.file_path.path).resolve())
|
|
423
|
+
if LayeredIgnore(project_root).is_ignored((project_root / file.file_path.path).resolve()):
|
|
383
424
|
return
|
|
384
425
|
try:
|
|
385
426
|
content = await file.read_text()
|
|
@@ -401,9 +442,11 @@ async def process_sql_file(
|
|
|
401
442
|
)
|
|
402
443
|
rel = file.file_path.path.as_posix()
|
|
403
444
|
|
|
404
|
-
|
|
445
|
+
# (vectors perf lever #1) embed chunks concurrently → batched encode.
|
|
446
|
+
embeddings = await asyncio.gather(*(embedder.embed(ch.text) for ch in chunks))
|
|
447
|
+
|
|
448
|
+
for ch, emb in zip(chunks, embeddings):
|
|
405
449
|
rs, re = chunk_key_range(ch)
|
|
406
|
-
emb = await embedder.embed(ch.text)
|
|
407
450
|
table.declare_row(
|
|
408
451
|
row=SqlLanceChunk(
|
|
409
452
|
id=str(uuid.uuid4()),
|
|
@@ -425,7 +468,7 @@ async def process_yaml_file(
|
|
|
425
468
|
) -> None:
|
|
426
469
|
embedder = coco.use_context(EMBEDDER)
|
|
427
470
|
project_root = coco.use_context(PROJECT_ROOT)
|
|
428
|
-
if LayeredIgnore(project_root).is_ignored((project_root / file.file_path.path).resolve())
|
|
471
|
+
if LayeredIgnore(project_root).is_ignored((project_root / file.file_path.path).resolve()):
|
|
429
472
|
return
|
|
430
473
|
try:
|
|
431
474
|
content = await file.read_text()
|
|
@@ -448,9 +491,11 @@ async def process_yaml_file(
|
|
|
448
491
|
)
|
|
449
492
|
rel = file.file_path.path.as_posix()
|
|
450
493
|
|
|
451
|
-
|
|
494
|
+
# (vectors perf lever #1) embed chunks concurrently → batched encode.
|
|
495
|
+
embeddings = await asyncio.gather(*(embedder.embed(ch.text) for ch in chunks))
|
|
496
|
+
|
|
497
|
+
for ch, emb in zip(chunks, embeddings):
|
|
452
498
|
rs, re = chunk_key_range(ch)
|
|
453
|
-
emb = await embedder.embed(ch.text)
|
|
454
499
|
table.declare_row(
|
|
455
500
|
row=YamlLanceChunk(
|
|
456
501
|
id=str(uuid.uuid4()),
|
|
@@ -501,6 +546,26 @@ async def app_main() -> None:
|
|
|
501
546
|
)
|
|
502
547
|
|
|
503
548
|
project_root = coco.use_context(PROJECT_ROOT)
|
|
549
|
+
# Warm per-project enrichment caches ONCE on the event-loop thread, BEFORE
|
|
550
|
+
# coco.mount_each fans files into worker threads. collect_annotation_meta_chain
|
|
551
|
+
# and load_brownfield_overrides are lru_cached per (resolved) project root;
|
|
552
|
+
# without warming, the first wave of concurrent process_java_file worker
|
|
553
|
+
# threads each cold-miss and redundantly walk+parse the ENTIRE project (a
|
|
554
|
+
# thundering herd that would offset the embedding-batching win on large
|
|
555
|
+
# repos — perf lever #2 made enrich concurrent). With warming, every worker
|
|
556
|
+
# hits a populated cache (lru_cache reads are thread-safe). Key derivation
|
|
557
|
+
# mirrors enrich_chunk exactly so the warmed entries are the ones workers hit.
|
|
558
|
+
try:
|
|
559
|
+
load_brownfield_overrides(project_root)
|
|
560
|
+
try:
|
|
561
|
+
prs = str(Path(project_root).resolve())
|
|
562
|
+
except OSError:
|
|
563
|
+
prs = str(project_root)
|
|
564
|
+
collect_annotation_meta_chain(prs)
|
|
565
|
+
except Exception:
|
|
566
|
+
# Warm-up must never break indexing — a failure just means workers
|
|
567
|
+
# cold-miss lazily (the pre-warming behavior). Swallow and continue.
|
|
568
|
+
pass
|
|
504
569
|
_ignore = LayeredIgnore(project_root)
|
|
505
570
|
_walk_excludes = _ignore.cocoindex_excluded_patterns()
|
|
506
571
|
# Emit ONE approximate total so the parent's renderer can show a determinate
|
ladybug_queries.py
CHANGED
|
@@ -1161,8 +1161,11 @@ class LadybugGraph:
|
|
|
1161
1161
|
)
|
|
1162
1162
|
return [str(r["id"]) for r in rows2 if r.get("id")]
|
|
1163
1163
|
|
|
1164
|
-
def
|
|
1165
|
-
self,
|
|
1164
|
+
def _walk_calls(
|
|
1165
|
+
self,
|
|
1166
|
+
needle: str,
|
|
1167
|
+
*,
|
|
1168
|
+
side: str,
|
|
1166
1169
|
depth: int = 1,
|
|
1167
1170
|
limit: int = 100,
|
|
1168
1171
|
min_confidence: float = 0.0,
|
|
@@ -1170,6 +1173,17 @@ class LadybugGraph:
|
|
|
1170
1173
|
module: str | None = None,
|
|
1171
1174
|
microservice: str | None = None,
|
|
1172
1175
|
) -> list[CallEdge]:
|
|
1176
|
+
"""BFS the CALLS graph outward from ``needle`` along one relationship end.
|
|
1177
|
+
|
|
1178
|
+
``side="callers"`` treats the needle as the callee: the frontier matches
|
|
1179
|
+
the ``callee`` end and discovered/expanded/external-filtered nodes are the
|
|
1180
|
+
``caller`` (src) end. ``side="callees"`` is the mirror. The two public
|
|
1181
|
+
methods differ only in that orientation, so the BFS body is shared here.
|
|
1182
|
+
"""
|
|
1183
|
+
if side == "callers":
|
|
1184
|
+
scope_alias, frontier_end, discovered = "caller", "callee", "src"
|
|
1185
|
+
else:
|
|
1186
|
+
scope_alias, frontier_end, discovered = "callee", "caller", "dst"
|
|
1173
1187
|
frontier = self._method_ids_for_call_graph_needle(needle, limit=max(limit, 50))
|
|
1174
1188
|
if not frontier:
|
|
1175
1189
|
return []
|
|
@@ -1182,8 +1196,8 @@ class LadybugGraph:
|
|
|
1182
1196
|
"frontier": list(frontier),
|
|
1183
1197
|
"minc": float(min_confidence),
|
|
1184
1198
|
}
|
|
1185
|
-
sc = _scope_filters(
|
|
1186
|
-
wh_parts = ["
|
|
1199
|
+
sc = _scope_filters(scope_alias, module=module, microservice=microservice, params=params)
|
|
1200
|
+
wh_parts = [f"{frontier_end}.id IN $frontier", "c.confidence >= $minc"]
|
|
1187
1201
|
wh_parts.extend(sc)
|
|
1188
1202
|
wh = " AND ".join(wh_parts)
|
|
1189
1203
|
q = (
|
|
@@ -1197,16 +1211,19 @@ class LadybugGraph:
|
|
|
1197
1211
|
next_frontier: list[str] = []
|
|
1198
1212
|
for row in self._rows(q, params):
|
|
1199
1213
|
ce = _row_to_call_edge(row)
|
|
1200
|
-
#
|
|
1201
|
-
#
|
|
1202
|
-
|
|
1214
|
+
# The needle itself may be external (e.g. java.util.List#add);
|
|
1215
|
+
# filter only the discovered end so internal callers/callees
|
|
1216
|
+
# that touch it are still surfaced.
|
|
1217
|
+
disc_fqn = ce.src.fqn if discovered == "src" else ce.dst.fqn
|
|
1218
|
+
disc_id = ce.src.id if discovered == "src" else ce.dst.id
|
|
1219
|
+
if exclude_external and _is_external_fqn(disc_fqn):
|
|
1203
1220
|
continue
|
|
1204
1221
|
key = (ce.src.id, ce.dst.id, ce.call_site_line, ce.call_site_byte)
|
|
1205
1222
|
if key in seen:
|
|
1206
1223
|
continue
|
|
1207
1224
|
seen.add(key)
|
|
1208
1225
|
out.append(ce)
|
|
1209
|
-
next_frontier.append(
|
|
1226
|
+
next_frontier.append(disc_id)
|
|
1210
1227
|
if len(out) >= limit:
|
|
1211
1228
|
return out
|
|
1212
1229
|
frontier = list(dict.fromkeys(next_frontier))
|
|
@@ -1214,6 +1231,21 @@ class LadybugGraph:
|
|
|
1214
1231
|
break
|
|
1215
1232
|
return out
|
|
1216
1233
|
|
|
1234
|
+
def find_callers(
|
|
1235
|
+
self, needle: str, *,
|
|
1236
|
+
depth: int = 1,
|
|
1237
|
+
limit: int = 100,
|
|
1238
|
+
min_confidence: float = 0.0,
|
|
1239
|
+
exclude_external: bool = True,
|
|
1240
|
+
module: str | None = None,
|
|
1241
|
+
microservice: str | None = None,
|
|
1242
|
+
) -> list[CallEdge]:
|
|
1243
|
+
return self._walk_calls(
|
|
1244
|
+
needle, side="callers", depth=depth, limit=limit,
|
|
1245
|
+
min_confidence=min_confidence, exclude_external=exclude_external,
|
|
1246
|
+
module=module, microservice=microservice,
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1217
1249
|
def find_callees(
|
|
1218
1250
|
self, needle: str, *,
|
|
1219
1251
|
depth: int = 1,
|
|
@@ -1223,49 +1255,11 @@ class LadybugGraph:
|
|
|
1223
1255
|
module: str | None = None,
|
|
1224
1256
|
microservice: str | None = None,
|
|
1225
1257
|
) -> list[CallEdge]:
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
out: list[CallEdge] = []
|
|
1232
|
-
seen: set[tuple[str, str, int, int]] = set()
|
|
1233
|
-
for _ in range(max(1, int(depth))):
|
|
1234
|
-
params: dict[str, Any] = {
|
|
1235
|
-
"frontier": list(frontier),
|
|
1236
|
-
"minc": float(min_confidence),
|
|
1237
|
-
}
|
|
1238
|
-
sc = _scope_filters("callee", module=module, microservice=microservice, params=params)
|
|
1239
|
-
wh_parts = ["caller.id IN $frontier", "c.confidence >= $minc"]
|
|
1240
|
-
wh_parts.extend(sc)
|
|
1241
|
-
wh = " AND ".join(wh_parts)
|
|
1242
|
-
q = (
|
|
1243
|
-
f"MATCH (caller:Symbol)-[c:CALLS]->(callee:Symbol) WHERE {wh} "
|
|
1244
|
-
f"RETURN {caller_proj}, {callee_proj}, "
|
|
1245
|
-
f"c.call_site_line AS call_site_line, c.call_site_byte AS call_site_byte, "
|
|
1246
|
-
f"c.arg_count AS arg_count, c.confidence AS confidence, c.strategy AS strategy, "
|
|
1247
|
-
f"c.source AS source, c.resolved AS resolved "
|
|
1248
|
-
f"LIMIT {int(limit) * 8}"
|
|
1249
|
-
)
|
|
1250
|
-
next_frontier: list[str] = []
|
|
1251
|
-
for row in self._rows(q, params):
|
|
1252
|
-
ce = _row_to_call_edge(row)
|
|
1253
|
-
# Filter only discovered callees (dst). Needle may be external while
|
|
1254
|
-
# still listing non-external outbound calls when any exist.
|
|
1255
|
-
if exclude_external and _is_external_fqn(ce.dst.fqn):
|
|
1256
|
-
continue
|
|
1257
|
-
key = (ce.src.id, ce.dst.id, ce.call_site_line, ce.call_site_byte)
|
|
1258
|
-
if key in seen:
|
|
1259
|
-
continue
|
|
1260
|
-
seen.add(key)
|
|
1261
|
-
out.append(ce)
|
|
1262
|
-
next_frontier.append(ce.dst.id)
|
|
1263
|
-
if len(out) >= limit:
|
|
1264
|
-
return out
|
|
1265
|
-
frontier = list(dict.fromkeys(next_frontier))
|
|
1266
|
-
if not frontier:
|
|
1267
|
-
break
|
|
1268
|
-
return out
|
|
1258
|
+
return self._walk_calls(
|
|
1259
|
+
needle, side="callees", depth=depth, limit=limit,
|
|
1260
|
+
min_confidence=min_confidence, exclude_external=exclude_external,
|
|
1261
|
+
module=module, microservice=microservice,
|
|
1262
|
+
)
|
|
1269
1263
|
|
|
1270
1264
|
def expand_methods(
|
|
1271
1265
|
self, fqns: list[str], *, depth: int = 1,
|
mcp_v2.py
CHANGED
|
@@ -817,7 +817,7 @@ def _merge_overrides_edge_summary(
|
|
|
817
817
|
"""Reconcile `OVERRIDES` with `override_axis_rollup_for` without clobbering stored `in`.
|
|
818
818
|
|
|
819
819
|
Rollup rows reuse the ``OVERRIDES`` key for dispatch-up counts only (``in`` is always
|
|
820
|
-
zero there). Stored ``[:OVERRIDES]`` edges contribute real ``in``/``out`` from
|
|
820
|
+
zero there). Stored ``[:OVERRIDES]`` edges contribute real ``in``/``out`` from LadybugDB;
|
|
821
821
|
merge per direction with ``max`` so inbound override edges stay visible.
|
|
822
822
|
"""
|
|
823
823
|
roll = _incident_counts(summary_after_rollups.get("OVERRIDES"))
|
|
@@ -1095,9 +1095,9 @@ def describe_v2(
|
|
|
1095
1095
|
has_id = bool(id and str(id).strip())
|
|
1096
1096
|
has_fqn = bool(fqn and str(fqn).strip())
|
|
1097
1097
|
if not has_id and not has_fqn:
|
|
1098
|
-
return DescribeOutput(success=False, message="id or fqn required"
|
|
1098
|
+
return DescribeOutput(success=False, message="id or fqn required")
|
|
1099
1099
|
if has_id and str(id).strip().startswith("ucs:"):
|
|
1100
|
-
return DescribeOutput(success=False, message=_DESCRIBE_UCS_ID_MESSAGE
|
|
1100
|
+
return DescribeOutput(success=False, message=_DESCRIBE_UCS_ID_MESSAGE)
|
|
1101
1101
|
hint_message: str | None = None
|
|
1102
1102
|
node_id: str
|
|
1103
1103
|
if has_id:
|
|
@@ -1109,7 +1109,7 @@ def describe_v2(
|
|
|
1109
1109
|
{"fqn": fqn_val},
|
|
1110
1110
|
)
|
|
1111
1111
|
if not rows:
|
|
1112
|
-
return DescribeOutput(success=False, message=f"No Symbol found for fqn='{fqn_val}'"
|
|
1112
|
+
return DescribeOutput(success=False, message=f"No Symbol found for fqn='{fqn_val}'")
|
|
1113
1113
|
node_id = str(rows[0]["id"] or "")
|
|
1114
1114
|
if len(rows) > 1:
|
|
1115
1115
|
hint_message = (
|
|
@@ -1784,7 +1784,7 @@ def neighbors_v2(
|
|
|
1784
1784
|
)
|
|
1785
1785
|
except ValueError as exc:
|
|
1786
1786
|
_log_fail_loud("edge_filter")
|
|
1787
|
-
return NeighborsOutput(success=False, message=str(exc),
|
|
1787
|
+
return NeighborsOutput(success=False, message=str(exc), requested_edge_types=[])
|
|
1788
1788
|
if include_unresolved and ef is not None:
|
|
1789
1789
|
return NeighborsOutput(
|
|
1790
1790
|
success=False,
|
|
@@ -1792,21 +1792,18 @@ def neighbors_v2(
|
|
|
1792
1792
|
"include_unresolved=True is incompatible with edge_filter; "
|
|
1793
1793
|
"UnresolvedCallSite rows have no edge attributes to filter on"
|
|
1794
1794
|
),
|
|
1795
|
-
hints=[],
|
|
1796
1795
|
requested_edge_types=requested_edge_types,
|
|
1797
1796
|
)
|
|
1798
1797
|
if include_unresolved and requested_edge_types != ["CALLS"]:
|
|
1799
1798
|
return NeighborsOutput(
|
|
1800
1799
|
success=False,
|
|
1801
1800
|
message="include_unresolved requires edge_types=['CALLS']",
|
|
1802
|
-
hints=[],
|
|
1803
1801
|
requested_edge_types=requested_edge_types,
|
|
1804
1802
|
)
|
|
1805
1803
|
if include_unresolved and direction != "out":
|
|
1806
1804
|
return NeighborsOutput(
|
|
1807
1805
|
success=False,
|
|
1808
1806
|
message='include_unresolved requires direction="out"',
|
|
1809
|
-
hints=[],
|
|
1810
1807
|
requested_edge_types=requested_edge_types,
|
|
1811
1808
|
)
|
|
1812
1809
|
if ef and (err := _edgefilter_applicability_error(requested_edge_types, ef)):
|
|
@@ -1814,17 +1811,15 @@ def neighbors_v2(
|
|
|
1814
1811
|
return NeighborsOutput(
|
|
1815
1812
|
success=False,
|
|
1816
1813
|
message=err,
|
|
1817
|
-
hints=[],
|
|
1818
1814
|
requested_edge_types=requested_edge_types,
|
|
1819
1815
|
)
|
|
1820
1816
|
if nf and (err := _validate_no_wildcards(nf)):
|
|
1821
1817
|
_log_fail_loud("wildcard")
|
|
1822
|
-
return NeighborsOutput(success=False, message=err,
|
|
1818
|
+
return NeighborsOutput(success=False, message=err, requested_edge_types=[])
|
|
1823
1819
|
if composed_keys and direction != "out":
|
|
1824
1820
|
return NeighborsOutput(
|
|
1825
1821
|
success=False,
|
|
1826
1822
|
message='Composed edge types require direction="out"',
|
|
1827
|
-
hints=[],
|
|
1828
1823
|
requested_edge_types=requested_edge_types,
|
|
1829
1824
|
)
|
|
1830
1825
|
use_calls_path = flat_labels == ["CALLS"] and not composed_keys
|
|
@@ -1849,7 +1844,6 @@ def neighbors_v2(
|
|
|
1849
1844
|
return NeighborsOutput(
|
|
1850
1845
|
success=False,
|
|
1851
1846
|
message=axis_msg,
|
|
1852
|
-
hints=[],
|
|
1853
1847
|
requested_edge_types=requested_edge_types,
|
|
1854
1848
|
)
|
|
1855
1849
|
origin_row = _load_node_record(g, origin_id, "symbol")
|
|
@@ -1865,7 +1859,6 @@ def neighbors_v2(
|
|
|
1865
1859
|
return NeighborsOutput(
|
|
1866
1860
|
success=False,
|
|
1867
1861
|
message=err,
|
|
1868
|
-
hints=[],
|
|
1869
1862
|
requested_edge_types=requested_edge_types,
|
|
1870
1863
|
)
|
|
1871
1864
|
if use_calls_path:
|
|
@@ -1891,7 +1884,6 @@ def neighbors_v2(
|
|
|
1891
1884
|
return NeighborsOutput(
|
|
1892
1885
|
success=False,
|
|
1893
1886
|
message=str(exc),
|
|
1894
|
-
hints=[],
|
|
1895
1887
|
requested_edge_types=requested_edge_types,
|
|
1896
1888
|
)
|
|
1897
1889
|
if (
|
|
@@ -1904,7 +1896,7 @@ def neighbors_v2(
|
|
|
1904
1896
|
results.extend(origin_edges)
|
|
1905
1897
|
continue
|
|
1906
1898
|
if flat_labels:
|
|
1907
|
-
#
|
|
1899
|
+
# Some Cypher binders can drop `label(e) IN $list` in WHERE; use OR of scalar equalities.
|
|
1908
1900
|
label_params = [f"l{i}" for i in range(len(flat_labels))]
|
|
1909
1901
|
label_predicate = "(" + " OR ".join(f"label(e) = ${name}" for name in label_params) + ")"
|
|
1910
1902
|
q_params = {"id": origin_id, **dict(zip(label_params, flat_labels, strict=True))}
|
|
@@ -1941,7 +1933,7 @@ def neighbors_v2(
|
|
|
1941
1933
|
if nf and (err := _nodefilter_applicability_error(other_kind, nf)):
|
|
1942
1934
|
_log_fail_loud("applicability")
|
|
1943
1935
|
return NeighborsOutput(
|
|
1944
|
-
success=False, message=err,
|
|
1936
|
+
success=False, message=err, requested_edge_types=[]
|
|
1945
1937
|
)
|
|
1946
1938
|
if not _node_matches_filter(other_kind, other_rec, nf):
|
|
1947
1939
|
continue
|
|
@@ -1968,7 +1960,7 @@ def neighbors_v2(
|
|
|
1968
1960
|
if nf and (err := _nodefilter_applicability_error(other_kind, nf)):
|
|
1969
1961
|
_log_fail_loud("applicability")
|
|
1970
1962
|
return NeighborsOutput(
|
|
1971
|
-
success=False, message=err,
|
|
1963
|
+
success=False, message=err, requested_edge_types=[]
|
|
1972
1964
|
)
|
|
1973
1965
|
if not _node_matches_filter(other_kind, other_rec, nf):
|
|
1974
1966
|
continue
|
path_filtering.py
CHANGED
|
@@ -342,24 +342,22 @@ class LayeredIgnore:
|
|
|
342
342
|
)
|
|
343
343
|
return mega, GitIgnoreSpec.from_lines(mega), meta
|
|
344
344
|
|
|
345
|
-
def is_ignored(self, path: Path) ->
|
|
346
|
-
"""Return whether ``path`` is ignored
|
|
345
|
+
def is_ignored(self, path: Path) -> bool:
|
|
346
|
+
"""Return whether ``path`` is ignored by any configured layer.
|
|
347
|
+
|
|
348
|
+
Boolean-only fast path for the per-file index walk. It deliberately does
|
|
349
|
+
not compute *which* layer/source last matched: that attribution is
|
|
350
|
+
O(rules²) via :func:`_winning_row` (one ``GitIgnoreSpec`` rebuild per
|
|
351
|
+
rule prefix) and is only needed for ``diagnose-ignore``, so it lives in
|
|
352
|
+
:meth:`diagnose_dict` and is never paid on the hot path.
|
|
353
|
+
"""
|
|
347
354
|
rel = self._rel_project(path)
|
|
348
355
|
if rel is None:
|
|
349
|
-
return False
|
|
350
|
-
mega, spec,
|
|
356
|
+
return False
|
|
357
|
+
mega, spec, _ = self._mega(rel)
|
|
351
358
|
if not mega:
|
|
352
|
-
return False
|
|
353
|
-
|
|
354
|
-
if not ignored:
|
|
355
|
-
return False, None
|
|
356
|
-
src, fp, ln, _pat = _winning_row(rel, mega, meta)
|
|
357
|
-
return True, IgnoreLayer(
|
|
358
|
-
root=self.project_root,
|
|
359
|
-
spec=spec,
|
|
360
|
-
source=src,
|
|
361
|
-
ignore_file=fp,
|
|
362
|
-
)
|
|
359
|
+
return False
|
|
360
|
+
return spec.match_file(rel)
|
|
363
361
|
|
|
364
362
|
def diagnose(self, path: Path) -> str:
|
|
365
363
|
"""Human-readable, multi-line explanation of the ignore decision."""
|
|
@@ -466,7 +464,6 @@ def iter_java_source_files(
|
|
|
466
464
|
if not fn.endswith(".java"):
|
|
467
465
|
continue
|
|
468
466
|
p = Path(dirpath) / fn
|
|
469
|
-
|
|
470
|
-
if ign:
|
|
467
|
+
if ignore_ctx.is_ignored(p):
|
|
471
468
|
continue
|
|
472
469
|
yield p
|
pr_analysis.py
CHANGED
|
@@ -152,7 +152,7 @@ def _resolve_graph_filename(
|
|
|
152
152
|
*,
|
|
153
153
|
ambiguity_notes: list[str] | None = None,
|
|
154
154
|
) -> str | None:
|
|
155
|
-
"""Map a diff path to `Symbol.filename` values stored in
|
|
155
|
+
"""Map a diff path to `Symbol.filename` values stored in LadybugDB."""
|
|
156
156
|
variants = {_strip_ab_prefix(path)}
|
|
157
157
|
for v in list(variants):
|
|
158
158
|
if v.startswith("./"):
|
|
@@ -362,7 +362,7 @@ def _is_public_interface_method(graph: Any, sym: SymbolHit) -> bool:
|
|
|
362
362
|
|
|
363
363
|
|
|
364
364
|
def _route_ids_for_symbol(graph: Any, symbol_id: str) -> list[str]:
|
|
365
|
-
# Note:
|
|
365
|
+
# Note: LadybugDB rejects `ORDER BY r.id` together with `RETURN DISTINCT r.id` (binder loses `r`).
|
|
366
366
|
q = (
|
|
367
367
|
"MATCH (s:Symbol)-[e:EXPOSES]->(r:Route) WHERE s.id = $sid "
|
|
368
368
|
"RETURN r.id AS id ORDER BY id"
|
|
@@ -384,7 +384,6 @@ def compute_risk(graph: Any, changed: list[ChangedSymbol]) -> PrRiskReport:
|
|
|
384
384
|
bump (up to +1.0) after normalization so they influence rank while
|
|
385
385
|
preserving the public scalar contract.
|
|
386
386
|
"""
|
|
387
|
-
notes: list[str] = []
|
|
388
387
|
blast_by: dict[str, int] = {}
|
|
389
388
|
blast_total = 0
|
|
390
389
|
routes: list[str] = []
|
|
@@ -495,7 +494,7 @@ def compute_risk(graph: Any, changed: list[ChangedSymbol]) -> PrRiskReport:
|
|
|
495
494
|
routes_touched=routes,
|
|
496
495
|
risk_score=score,
|
|
497
496
|
risk_band=band,
|
|
498
|
-
notes=
|
|
497
|
+
notes=[],
|
|
499
498
|
)
|
|
500
499
|
|
|
501
500
|
|
search_lancedb.py
CHANGED
|
@@ -677,8 +677,8 @@ def _graph_expand_merge(
|
|
|
677
677
|
expand_depth: int,
|
|
678
678
|
ladybug_path: str | None,
|
|
679
679
|
) -> list[dict]:
|
|
680
|
-
"""Expand vector top-k through the
|
|
681
|
-
# Lazy import so the module works without
|
|
680
|
+
"""Expand vector top-k through the LadybugDB graph and fuse (RRF) with the original list."""
|
|
681
|
+
# Lazy import so the module works without ladybug installed when graph_expand=False.
|
|
682
682
|
try:
|
|
683
683
|
from ladybug_queries import LadybugGraph
|
|
684
684
|
except Exception:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|