java-codebase-rag 0.6.3__py3-none-any.whl → 0.6.5__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.
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
@@ -300,6 +300,7 @@ class LayeredIgnore:
300
300
  _scan_negation_any_bundle_ignore(self.project_root)
301
301
  or (use_gitignore and _scan_negation_any_gitignore(self.project_root))
302
302
  )
303
+ self._mega_cache: dict[str, tuple[list[str], GitIgnoreSpec, list[tuple[str, Path | None, int, str]]]] = {}
303
304
 
304
305
  def cocoindex_excluded_patterns(self) -> list[str]:
305
306
  """Patterns for CocoIndex ``PatternFilePathMatcher.excluded_patterns``.
@@ -332,6 +333,11 @@ class LayeredIgnore:
332
333
  return path.as_posix()
333
334
 
334
335
  def _mega(self, rel_project: str) -> tuple[list[str], GitIgnoreSpec, list[tuple[str, Path | None, int, str]]]:
336
+ # Cache by directory (parent of rel_project). _mega_build_for_rel reads only dir_parts,
337
+ # so files in the same directory share the same mega/spec/meta tuple.
338
+ cache_key = Path(rel_project).parent.as_posix()
339
+ if cache_key in self._mega_cache:
340
+ return self._mega_cache[cache_key]
335
341
  mega, meta = _mega_build_for_rel(
336
342
  self.project_root,
337
343
  rel_project,
@@ -340,26 +346,26 @@ class LayeredIgnore:
340
346
  project_ignore_path=self._project_ignore_path,
341
347
  project_lines=self._project_lines,
342
348
  )
343
- return mega, GitIgnoreSpec.from_lines(mega), meta
344
-
345
- def is_ignored(self, path: Path) -> tuple[bool, IgnoreLayer | None]:
346
- """Return whether ``path`` is ignored and which layer last matched."""
349
+ result = (mega, GitIgnoreSpec.from_lines(mega), meta)
350
+ self._mega_cache[cache_key] = result
351
+ return result
352
+
353
+ def is_ignored(self, path: Path) -> bool:
354
+ """Return whether ``path`` is ignored by any configured layer.
355
+
356
+ Boolean-only fast path for the per-file index walk. It deliberately does
357
+ not compute *which* layer/source last matched: that attribution is
358
+ O(rules²) via :func:`_winning_row` (one ``GitIgnoreSpec`` rebuild per
359
+ rule prefix) and is only needed for ``diagnose-ignore``, so it lives in
360
+ :meth:`diagnose_dict` and is never paid on the hot path.
361
+ """
347
362
  rel = self._rel_project(path)
348
363
  if rel is None:
349
- return False, None
350
- mega, spec, meta = self._mega(rel)
364
+ return False
365
+ mega, spec, _ = self._mega(rel)
351
366
  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
- )
367
+ return False
368
+ return spec.match_file(rel)
363
369
 
364
370
  def diagnose(self, path: Path) -> str:
365
371
  """Human-readable, multi-line explanation of the ignore decision."""
@@ -466,7 +472,6 @@ def iter_java_source_files(
466
472
  if not fn.endswith(".java"):
467
473
  continue
468
474
  p = Path(dirpath) / fn
469
- ign, _ = ignore_ctx.is_ignored(p)
470
- if ign:
475
+ if ignore_ctx.is_ignored(p):
471
476
  continue
472
477
  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: