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 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
- @lru_cache(maxsize=1)
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
- lang = Language(_ts_java.language())
221
- return Parser(lang)
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 _codebase_route_inner_annotation_nodes(container_ann: Node, src: bytes) -> list[Node]:
1569
- found: list[Node] = []
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
- visit(container_ann)
1581
- return found
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 == "CodebaseProducer":
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 _codebase_http_client_inner_annotation_nodes(ann, src):
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 _codebase_producer_inner_annotation_nodes(ann, src):
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 _codebase_route_inner_annotation_nodes(node, src):
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(_codebase_async_route_inner_annotation_nodes(node, src))
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 {"layer_a_meta", "layer_b_ann", "layer_b_fqn", "layer_c_source"}:
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 {"layer_a_meta", "layer_b_ann", "layer_b_fqn", "layer_c_source"}:
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 = frozenset(
2462
- (
2463
- "layer_b_ann",
2464
- "layer_a_meta",
2465
- "layer_c_source",
2466
- "layer_b_fqn",
2467
- "codebase_client",
2468
- "codebase_producer",
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
- def _route_hint_lookup(ann: AnnotationRef, hints: dict[str, RouteHint]) -> RouteHint | None:
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 = _route_hint_lookup(ann, overrides.annotation_to_route_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 = _client_hint_lookup(ann, overrides.annotation_to_http_client_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 = _async_hint_lookup(ann, overrides.annotation_to_async_producer_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 + Kuzu + cocoindex state)")
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/kuzu
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": "Kuzu graph not found"})
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": "Kuzu graph not found"})
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": "Kuzu graph not found"})
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 Kuzu graph or Lance tables. Exit 2 on refusal."
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 Kuzu graph update. Use --vectors-only to skip graph update.",
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 Kuzu (default: both full phases).",
886
+ help="Rebuild vectors and/or LadybugDB (default: both full phases).",
887
887
  description=(
888
- "Default: full Lance reprocess (cocoindex --full-reprocess) then full Kuzu graph rebuild. "
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 Kuzu, and drops Lance tables. Requires --yes or TTY confirmation.",
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`, `kafka`, `rabbitmq`, `jms`, `stream`, `codebase_async_route`,
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
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 **Kuzu** (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**.
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 (Kuzu property graph) are composed in one surface. `search` finds candidate nodes by meaning; `neighbors` walks the exact edge you care about (`CALLS`, `IMPLEMENTS`, `INJECTS`, `DECLARES_ROUTE`, …). The agent picks the right primitive per step instead of being forced into pure-vector or pure-symbol search.
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`, `CLIENT`, `PRODUCER`, `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.
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/Kuzu schemas, config files, and Python APIs may change without a deprecation period. Track `main` and rebuild indexes when ontology or embedding settings change.
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 + Kuzu graph). First run downloads the
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 Kuzu is wired in
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 Kuzu update. `--vectors-only` for Lance only. |
245
- | Lifecycle | `reprocess` | Full Lance + Kuzu rebuild. `--vectors-only` / `--graph-only` for a single phase. |
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 `EMBEDDING_MODEL` env var — see [`docs/CONFIGURATION.md` §1](./docs/CONFIGURATION.md#1-environment-variables).
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=NQgZzstbsMq-PdowoD6r_ixJKxEEFzTP9xUzqDpiXeU,99661
1
+ ast_java.py,sha256=Paee3ZV9G5iy8LqfkVq0Ah_1fV0k632oS5JPkiWzwB8,99206
2
2
  brownfield_events.py,sha256=yxXkKDgMb3VPtaiakGzncHM_EGnda8xIue6w90yYp8s,2055
3
- build_ast_graph.py,sha256=LNU1rIHwgEpbrWnWoywg4JdqbwwT4UKcx1iDay_GemM,156776
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=POT4LwSkTsrjUmP67bsm2UezUam70cunuPDYDh-v1Bs,63332
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=7CS-sdbzhCkUsHfpCZ85uM7vy4aoZI8n-RowLocfiKc,20267
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=912j9VAYDjcU4ReVorWQ6R4DZl0tteKic-Pqu0jyBS0,90837
10
+ ladybug_queries.py,sha256=7vSP7WsUKQYXHFenBMlCdpUFed39h3oul-WRkf55YVc,90330
11
11
  mcp_hints.py,sha256=3swh05LSiWur3tm3-yssndBsLxIxFhy501kBtJI8jJ0,42509
12
- mcp_v2.py,sha256=o94GJI7j6dLJDIA3R_1ZiQhjzQfMAEW3etdeZYnHOUc,80637
13
- path_filtering.py,sha256=-oX16SYLWYwX9pcV1fu3vbVTIhY1GzFflT7J1E2tqPY,17122
14
- pr_analysis.py,sha256=3-5L8_G5XupdJsl9RN73Lq-ejPoK11B3m_VzAx2fGG8,18413
15
- search_lancedb.py,sha256=scG6HBUrsgIeSWFrGcLcGdhWv1qODOx4JOBMAlLDY_E,36793
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=uTCpgsE3gbDPSqPDNJeJ7hZu_MhyKYm4tqHIMkM67LM,38650
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=APl9d-No12qZNZLjU7mwNRwxHIgnT3ZtQZiD4clWlyU,14413
28
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md,sha256=pIM-Xdwq_fXkhhBJCdb-fA2nes5c_mMPcdUXb7Adyxo,12040
29
- java_codebase_rag-0.6.3.dist-info/licenses/LICENSE,sha256=gxvtiHtuviR_q8ZAjWw-QTcF3DyPzg6ZY-lQrr8OPpw,1068
30
- java_codebase_rag-0.6.3.dist-info/METADATA,sha256=zDwRolQN2GTSexo7J2ngIqB-RnqmsnTZLRQ-lWuqBL4,17197
31
- java_codebase_rag-0.6.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
32
- java_codebase_rag-0.6.3.dist-info/entry_points.txt,sha256=wsPZwot0Ui4JI3TIgW8LcbN8bNtKFbwQAlHAAJXfYgQ,117
33
- java_codebase_rag-0.6.3.dist-info/top_level.txt,sha256=syQgi8XPBwY2ws_NZ1uRCxTf_s41NpshwEHNdcdnk3A,245
34
- java_codebase_rag-0.6.3.dist-info/RECORD,,
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,,
@@ -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)[0]:
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)[0]:
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)[0]:
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())[0]:
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
- for ch in chunks:
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())[0]:
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
- for ch in chunks:
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())[0]:
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
- for ch in chunks:
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 find_callers(
1165
- self, needle: str, *,
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("caller", module=module, microservice=microservice, params=params)
1186
- wh_parts = ["callee.id IN $frontier", "c.confidence >= $minc"]
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
- # Filter only discovered callers (src). Needle may be external
1201
- # (e.g. java.util.List#add) while still listing internal callers.
1202
- if exclude_external and _is_external_fqn(ce.src.fqn):
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(ce.src.id)
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
- frontier = self._method_ids_for_call_graph_needle(needle, limit=max(limit, 50))
1227
- if not frontier:
1228
- return []
1229
- caller_proj = ", ".join(f"caller.{c} AS caller_{c}" for c in _SYM_COLS)
1230
- callee_proj = ", ".join(f"callee.{c} AS callee_{c}" for c in _SYM_COLS)
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 Kuzu;
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", hints=[])
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, hints=[])
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}'", hints=[])
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), hints=[], requested_edge_types=[])
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, hints=[], requested_edge_types=[])
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
- # Kuzu 0.11.x can drop `label(e) IN $list` in WHERE; use OR of scalar equalities.
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, hints=[], requested_edge_types=[]
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, hints=[], requested_edge_types=[]
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) -> tuple[bool, IgnoreLayer | None]:
346
- """Return whether ``path`` is ignored and which layer last matched."""
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, None
350
- mega, spec, meta = self._mega(rel)
356
+ return False
357
+ mega, spec, _ = self._mega(rel)
351
358
  if not mega:
352
- return False, None
353
- ignored = spec.match_file(rel)
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
- ign, _ = ignore_ctx.is_ignored(p)
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 Kuzu."""
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: Kuzu rejects `ORDER BY r.id` together with `RETURN DISTINCT r.id` (binder loses `r`).
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=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 Kuzu graph and fuse (RRF) with the original list."""
681
- # Lazy import so the module works without kuzu installed when graph_expand=False.
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: