java-codebase-rag 0.6.7__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. ast_java.py +8 -3
  2. build_ast_graph.py +72 -16
  3. graph_enrich.py +2 -1
  4. graph_types.py +133 -0
  5. java_codebase_rag/_fdlimit.py +10 -2
  6. java_codebase_rag/_stdio.py +32 -0
  7. java_codebase_rag/cli.py +135 -24
  8. java_codebase_rag/config.py +128 -9
  9. java_codebase_rag/install_data/agents/explorer-rag-cli.md +291 -0
  10. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +8 -8
  11. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +8 -8
  12. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +251 -0
  13. java_codebase_rag/installer.py +438 -103
  14. java_codebase_rag/jrag.py +4300 -0
  15. java_codebase_rag/jrag_envelope.py +1085 -0
  16. java_codebase_rag/jrag_hints.py +204 -0
  17. java_codebase_rag/jrag_render.py +688 -0
  18. java_codebase_rag/pipeline.py +20 -0
  19. {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/METADATA +137 -94
  20. java_codebase_rag-0.8.0.dist-info/RECORD +43 -0
  21. {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/WHEEL +1 -1
  22. {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/entry_points.txt +1 -0
  23. {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/top_level.txt +2 -0
  24. java_index_flow_lancedb.py +34 -19
  25. java_ontology.py +12 -0
  26. ladybug_queries.py +233 -52
  27. mcp_hints.py +6 -6
  28. mcp_v2.py +205 -617
  29. resolve_service.py +649 -0
  30. search_lancedb.py +10 -1
  31. server.py +20 -12
  32. java_codebase_rag-0.6.7.dist-info/RECORD +0 -34
  33. {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/licenses/LICENSE +0 -0
ladybug_queries.py CHANGED
@@ -30,21 +30,27 @@ log = logging.getLogger(__name__)
30
30
 
31
31
 
32
32
  def _parse_ladybug_json(raw: str | None) -> dict[str, Any]:
33
- """Parse JSON from LadybugDB which returns unquoted keys like {key: value}."""
33
+ """Parse JSON from LadybugDB which returns unquoted keys like {key: value}.
34
+
35
+ Only quote keys at key positions (after ``{``, ``,`` or ``[``) so values
36
+ containing word-colon patterns (e.g. a URL ``https://...`` inside a quoted
37
+ string) are not corrupted. The previous ``(\\w+):`` regex matched ``word:``
38
+ anywhere, including inside values (issue #359).
39
+ """
34
40
  if not raw:
35
41
  return {}
36
- # LadybugDB returns JSON without quotes around keys: {packages: 1, files: 2}
37
- # Convert to standard JSON: {"packages": 1, "files": 2}
38
- # This regex matches word characters followed by ':' at the start of a key
39
- quoted = re.sub(r'(\w+):', r'"\1":', raw)
42
+ # Quote unquoted keys only where a key is expected: preceded by '{', ',' or
43
+ # '[' (with optional whitespace). This leaves word-colon runs inside values
44
+ # untouched.
45
+ quoted = re.sub(r'([,{\[]\s*)(\w+):', lambda m: f'{m.group(1)}"{m.group(2)}":', raw)
40
46
  try:
41
47
  return json.loads(quoted)
42
48
  except Exception:
43
49
  try:
44
- # Fallback: try parsing as-is (for standard JSON)
50
+ # Fallback: try parsing as-is (for standard JSON).
45
51
  return json.loads(raw)
46
52
  except Exception:
47
- log.warning("Failed to parse counts_json: %s", raw[:100])
53
+ log.warning("Failed to parse graph_meta JSON blob: %s", raw[:100])
48
54
  return {}
49
55
 
50
56
  # Composed describe / neighbors dot-keys (not stored graph edge labels).
@@ -178,6 +184,7 @@ class RouteCaller:
178
184
  declaring_symbol_id: str
179
185
  confidence: float
180
186
  match: str
187
+ declaring_symbol_fqn: str = ""
181
188
  target_service: str = ""
182
189
  raw_uri: str = ""
183
190
  topic: str = ""
@@ -1665,9 +1672,11 @@ class LadybugGraph:
1665
1672
  *,
1666
1673
  microservice: str | None = None,
1667
1674
  framework: str | None = None,
1668
- path_prefix: str | None = None,
1675
+ path_contains: str | None = None,
1669
1676
  method: str | None = None,
1670
1677
  limit: int = 100,
1678
+ server_exposed: bool = False,
1679
+ include_kafka: bool = True,
1671
1680
  ) -> list[dict[str, Any]]:
1672
1681
  lim = max(1, min(int(limit), 500))
1673
1682
  params: dict[str, Any] = {"lim": lim}
@@ -1678,12 +1687,28 @@ class LadybugGraph:
1678
1687
  if framework:
1679
1688
  params["framework"] = framework
1680
1689
  preds.append("r.framework = $framework")
1681
- if path_prefix:
1682
- params["path_prefix"] = path_prefix
1683
- preds.append("r.path STARTS WITH $path_prefix")
1690
+ if path_contains:
1691
+ params["path_contains"] = path_contains
1692
+ preds.append("r.path CONTAINS $path_contains")
1684
1693
  if method is not None and method != "":
1685
1694
  params["method"] = method
1686
1695
  preds.append("r.method = $method")
1696
+ if server_exposed:
1697
+ # `routes` CLI surface: only server-exposed entrypoints. The inbound
1698
+ # EXPOSES clause is what makes a route "exposed" by a Symbol — a
1699
+ # client http_endpoint mirror is a call-site reached via HTTP_CALLS,
1700
+ # never EXPOSES'd, so it drops out here regardless of include_kafka.
1701
+ # Kafka topics (kind=kafka_topic) live under the `topics` command;
1702
+ # include_kafka=False (the CLI default) excludes them, while the
1703
+ # OR-branch lets `--include-kafka` opt them back in (still excluding
1704
+ # client mirrors, which never carry an inbound EXPOSES).
1705
+ if include_kafka:
1706
+ preds.append(
1707
+ "(r.kind = 'kafka_topic' OR EXISTS { MATCH (s:Symbol)-[:EXPOSES]->(r) })"
1708
+ )
1709
+ else:
1710
+ preds.append("r.kind = 'http_endpoint'")
1711
+ preds.append("EXISTS { MATCH (s:Symbol)-[:EXPOSES]->(r) }")
1687
1712
  where = (" WHERE " + " AND ".join(preds)) if preds else ""
1688
1713
  q = (
1689
1714
  f"MATCH (r:Route){where} RETURN {self._ROUTE_RETURN} "
@@ -1737,7 +1762,20 @@ class LadybugGraph:
1737
1762
  path_template: str = "",
1738
1763
  method: str = "",
1739
1764
  ) -> list[RouteCaller]:
1740
- """HTTP callers via Client; async callers via Producer (two-hop each)."""
1765
+ """HTTP callers via Client; async callers via Producer (two-hop each).
1766
+
1767
+ Mirror-agnostic: cross-service ``HTTP_CALLS``/``ASYNC_CALLS`` edges
1768
+ sometimes terminate at a client-side mirror Route rather than the
1769
+ server Route by id (the enrichment links a Client to whichever Route
1770
+ node it could resolve — a mirror when the server route wasn't found),
1771
+ so the strict edge query can return 0 rows for genuine cross-service
1772
+ callers. We therefore UNION edge-based results (which carry
1773
+ confidence/match/raw_uri metadata) with path/topic-based results that
1774
+ match ``Client``/``Producer`` nodes directly against the entry route's
1775
+ ``microservice`` + ``path_template``/``topic``, deduping by
1776
+ ``caller_node_id`` so edge-based rows win and path-matched rows only
1777
+ fill the gaps (default ``confidence=1.0``, ``match='path_template'``).
1778
+ """
1741
1779
  rid = route_id or ""
1742
1780
  if not rid:
1743
1781
  params: dict[str, Any] = {
@@ -1756,77 +1794,220 @@ class LadybugGraph:
1756
1794
  rid = str(rows[0].get("id") or "")
1757
1795
  if not rid:
1758
1796
  return []
1759
- http_rows = self._rows(
1797
+ # Edge-based HTTP callers — carry confidence/match/raw_uri metadata.
1798
+ http_edge_rows = self._rows(
1760
1799
  "MATCH (s:Symbol)-[:DECLARES_CLIENT]->(c:Client)-[e:HTTP_CALLS]->(r:Route {id: $rid}) "
1761
1800
  "RETURN c.id AS caller_node_id, c.microservice AS caller_microservice, "
1762
- "s.id AS declaring_symbol_id, e.confidence AS confidence, e.match AS match, "
1763
- "c.target_service AS target_service, e.raw_uri AS raw_uri "
1764
- "ORDER BY e.confidence DESC, c.id",
1801
+ "s.id AS declaring_symbol_id, s.fqn AS declaring_symbol_fqn, e.confidence AS confidence, e.match AS match, "
1802
+ "c.target_service AS target_service, e.raw_uri AS raw_uri",
1765
1803
  {"rid": rid},
1766
1804
  )
1767
- async_rows = self._rows(
1805
+ # Path-based HTTP callers (mirror-agnostic): match Client nodes by the
1806
+ # entry route's microservice + path_template + method regardless of
1807
+ # whether an HTTP_CALLS edge reaches this Route node.
1808
+ http_path_rows = self._rows(
1809
+ "MATCH (entry:Route {id: $rid}) "
1810
+ "MATCH (s:Symbol)-[:DECLARES_CLIENT]->(c:Client) "
1811
+ "WHERE (entry.microservice = '' OR c.target_service = entry.microservice) "
1812
+ "AND ("
1813
+ "(entry.path_template <> '' AND c.path_template = entry.path_template) OR "
1814
+ "(entry.path <> '' AND c.path = entry.path) OR "
1815
+ "(entry.path_template <> '' AND c.path = entry.path_template) OR "
1816
+ "(entry.path <> '' AND c.path_template = entry.path)"
1817
+ ") "
1818
+ "AND (entry.method = '' OR c.method = entry.method OR c.method = '') "
1819
+ "RETURN c.id AS caller_node_id, c.microservice AS caller_microservice, "
1820
+ "s.id AS declaring_symbol_id, s.fqn AS declaring_symbol_fqn, c.target_service AS target_service, "
1821
+ "c.path AS raw_uri",
1822
+ {"rid": rid},
1823
+ )
1824
+ # Edge-based async callers — carry confidence/match metadata.
1825
+ async_edge_rows = self._rows(
1768
1826
  "MATCH (s:Symbol)-[:DECLARES_PRODUCER]->(p:Producer)-[e:ASYNC_CALLS]->(r:Route {id: $rid}) "
1769
1827
  "RETURN p.id AS caller_node_id, p.microservice AS caller_microservice, "
1770
- "s.id AS declaring_symbol_id, e.confidence AS confidence, e.match AS match, "
1771
- "p.topic AS topic, p.broker AS broker "
1772
- "ORDER BY e.confidence DESC, p.id",
1828
+ "s.id AS declaring_symbol_id, s.fqn AS declaring_symbol_fqn, e.confidence AS confidence, e.match AS match, "
1829
+ "p.topic AS topic, p.broker AS broker",
1773
1830
  {"rid": rid},
1774
1831
  )
1832
+ # Topic-based async callers (mirror-agnostic): match Producer nodes by
1833
+ # the entry route's topic (kafka_topic routes).
1834
+ async_path_rows = self._rows(
1835
+ "MATCH (entry:Route {id: $rid}) "
1836
+ "MATCH (s:Symbol)-[:DECLARES_PRODUCER]->(p:Producer) "
1837
+ "WHERE entry.topic <> '' AND p.topic = entry.topic "
1838
+ "RETURN p.id AS caller_node_id, p.microservice AS caller_microservice, "
1839
+ "s.id AS declaring_symbol_id, s.fqn AS declaring_symbol_fqn, p.topic AS topic, p.broker AS broker",
1840
+ {"rid": rid},
1841
+ )
1842
+
1843
+ # Dedup by caller_node_id: edge-based rows (with metadata) win; the
1844
+ # path/topic-based rows only fill gaps with default metadata so the
1845
+ # same caller is never reported twice.
1846
+ http_by_id: dict[str, dict[str, Any]] = {}
1847
+ for row in http_edge_rows:
1848
+ cid = str(row.get("caller_node_id") or "")
1849
+ if not cid:
1850
+ continue
1851
+ http_by_id[cid] = {
1852
+ "caller_node_id": cid,
1853
+ "caller_microservice": str(row.get("caller_microservice") or ""),
1854
+ "declaring_symbol_id": str(row.get("declaring_symbol_id") or ""),
1855
+ "declaring_symbol_fqn": str(row.get("declaring_symbol_fqn") or ""),
1856
+ "confidence": float(row.get("confidence") or 0.0),
1857
+ "match": str(row.get("match") or ""),
1858
+ "target_service": str(row.get("target_service") or ""),
1859
+ "raw_uri": str(row.get("raw_uri") or ""),
1860
+ }
1861
+ for row in http_path_rows:
1862
+ cid = str(row.get("caller_node_id") or "")
1863
+ if not cid or cid in http_by_id:
1864
+ continue
1865
+ http_by_id[cid] = {
1866
+ "caller_node_id": cid,
1867
+ "caller_microservice": str(row.get("caller_microservice") or ""),
1868
+ "declaring_symbol_id": str(row.get("declaring_symbol_id") or ""),
1869
+ "declaring_symbol_fqn": str(row.get("declaring_symbol_fqn") or ""),
1870
+ "confidence": 1.0,
1871
+ "match": "path_template",
1872
+ "target_service": str(row.get("target_service") or ""),
1873
+ "raw_uri": str(row.get("raw_uri") or ""),
1874
+ }
1875
+ async_by_id: dict[str, dict[str, Any]] = {}
1876
+ for row in async_edge_rows:
1877
+ pid = str(row.get("caller_node_id") or "")
1878
+ if not pid:
1879
+ continue
1880
+ async_by_id[pid] = {
1881
+ "caller_node_id": pid,
1882
+ "caller_microservice": str(row.get("caller_microservice") or ""),
1883
+ "declaring_symbol_id": str(row.get("declaring_symbol_id") or ""),
1884
+ "declaring_symbol_fqn": str(row.get("declaring_symbol_fqn") or ""),
1885
+ "confidence": float(row.get("confidence") or 0.0),
1886
+ "match": str(row.get("match") or ""),
1887
+ "topic": str(row.get("topic") or ""),
1888
+ "broker": str(row.get("broker") or ""),
1889
+ }
1890
+ for row in async_path_rows:
1891
+ pid = str(row.get("caller_node_id") or "")
1892
+ if not pid or pid in async_by_id:
1893
+ continue
1894
+ async_by_id[pid] = {
1895
+ "caller_node_id": pid,
1896
+ "caller_microservice": str(row.get("caller_microservice") or ""),
1897
+ "declaring_symbol_id": str(row.get("declaring_symbol_id") or ""),
1898
+ "declaring_symbol_fqn": str(row.get("declaring_symbol_fqn") or ""),
1899
+ "confidence": 1.0,
1900
+ "match": "topic",
1901
+ "topic": str(row.get("topic") or ""),
1902
+ "broker": str(row.get("broker") or ""),
1903
+ }
1904
+
1775
1905
  out: list[RouteCaller] = []
1776
- for row in http_rows:
1906
+ for row in sorted(http_by_id.values(), key=lambda r: (-r["confidence"], r["caller_node_id"])):
1777
1907
  out.append(
1778
1908
  RouteCaller(
1779
- caller_node_id=str(row.get("caller_node_id") or ""),
1909
+ caller_node_id=row["caller_node_id"],
1780
1910
  caller_node_kind="client",
1781
- caller_microservice=str(row.get("caller_microservice") or ""),
1782
- declaring_symbol_id=str(row.get("declaring_symbol_id") or ""),
1783
- confidence=float(row.get("confidence") or 0.0),
1784
- match=str(row.get("match") or ""),
1785
- target_service=str(row.get("target_service") or ""),
1786
- raw_uri=str(row.get("raw_uri") or ""),
1911
+ caller_microservice=row["caller_microservice"],
1912
+ declaring_symbol_id=row["declaring_symbol_id"],
1913
+ declaring_symbol_fqn=row["declaring_symbol_fqn"],
1914
+ confidence=row["confidence"],
1915
+ match=row["match"],
1916
+ target_service=row["target_service"],
1917
+ raw_uri=row["raw_uri"],
1787
1918
  ),
1788
1919
  )
1789
- for row in async_rows:
1920
+ for row in sorted(async_by_id.values(), key=lambda r: (-r["confidence"], r["caller_node_id"])):
1790
1921
  out.append(
1791
1922
  RouteCaller(
1792
- caller_node_id=str(row.get("caller_node_id") or ""),
1923
+ caller_node_id=row["caller_node_id"],
1793
1924
  caller_node_kind="producer",
1794
- caller_microservice=str(row.get("caller_microservice") or ""),
1795
- declaring_symbol_id=str(row.get("declaring_symbol_id") or ""),
1796
- confidence=float(row.get("confidence") or 0.0),
1797
- match=str(row.get("match") or ""),
1798
- topic=str(row.get("topic") or ""),
1799
- broker=str(row.get("broker") or ""),
1925
+ caller_microservice=row["caller_microservice"],
1926
+ declaring_symbol_id=row["declaring_symbol_id"],
1927
+ declaring_symbol_fqn=row["declaring_symbol_fqn"],
1928
+ confidence=row["confidence"],
1929
+ match=row["match"],
1930
+ topic=row["topic"],
1931
+ broker=row["broker"],
1800
1932
  ),
1801
1933
  )
1802
1934
  return out
1803
1935
 
1804
1936
  def trace_request_flow(self, entry_route_id: str, max_hops: int = 5) -> dict[str, Any]:
1805
- """Inbound HTTP via Client; async inbound via Producer (two-hop each)."""
1937
+ """Inbound HTTP via Client; async inbound via Producer (two-hop each).
1938
+
1939
+ Mirror-agnostic (see ``find_route_callers``): cross-service
1940
+ ``HTTP_CALLS``/``ASYNC_CALLS`` edges may land on a client-side mirror
1941
+ Route rather than the server Route by id, so the edge-based inbound
1942
+ query is UNIONED with a path/topic-based query that matches
1943
+ ``Client``/``Producer`` nodes against the entry route's
1944
+ ``microservice`` + ``path_template``/``topic``. Rows are deduped by
1945
+ ``caller_node_id`` (edge-based win) so each inbound caller appears once.
1946
+ """
1806
1947
  hops = max(1, min(int(max_hops), 8))
1807
- inbound_http = self._rows(
1948
+ inbound_http_edge = self._rows(
1808
1949
  f"MATCH (entry:Route {{id: $rid}})<-[e:HTTP_CALLS]-(caller:Client)"
1809
1950
  "<-[:DECLARES_CLIENT]-(decl:Symbol) "
1810
1951
  f"OPTIONAL MATCH (origin:Symbol)-[:CALLS*0..{hops}]->(decl) "
1811
1952
  "RETURN DISTINCT caller.id AS caller_node_id, 'client' AS caller_node_kind, "
1812
1953
  "decl.id AS declaring_symbol_id, decl.fqn AS declaring_symbol_fqn, "
1813
1954
  "caller.microservice AS microservice, e.confidence AS confidence, "
1814
- "e.match AS match, origin.id AS origin_symbol_id, origin.fqn AS origin_fqn "
1815
- "ORDER BY confidence DESC, caller_node_id",
1955
+ "e.match AS match, origin.id AS origin_symbol_id, origin.fqn AS origin_fqn",
1956
+ {"rid": entry_route_id},
1957
+ )
1958
+ inbound_http_path = self._rows(
1959
+ "MATCH (entry:Route {id: $rid}) "
1960
+ "MATCH (caller:Client)<-[:DECLARES_CLIENT]-(decl:Symbol) "
1961
+ "WHERE (entry.microservice = '' OR caller.target_service = entry.microservice) "
1962
+ "AND ("
1963
+ "(entry.path_template <> '' AND caller.path_template = entry.path_template) OR "
1964
+ "(entry.path <> '' AND caller.path = entry.path) OR "
1965
+ "(entry.path_template <> '' AND caller.path = entry.path_template) OR "
1966
+ "(entry.path <> '' AND caller.path_template = entry.path)"
1967
+ ") "
1968
+ "AND (entry.method = '' OR caller.method = entry.method OR caller.method = '') "
1969
+ f"OPTIONAL MATCH (origin:Symbol)-[:CALLS*0..{hops}]->(decl) "
1970
+ "RETURN DISTINCT caller.id AS caller_node_id, 'client' AS caller_node_kind, "
1971
+ "decl.id AS declaring_symbol_id, decl.fqn AS declaring_symbol_fqn, "
1972
+ "caller.microservice AS microservice, 1.0 AS confidence, "
1973
+ "'path_template' AS match, origin.id AS origin_symbol_id, origin.fqn AS origin_fqn",
1816
1974
  {"rid": entry_route_id},
1817
1975
  )
1818
- inbound_async = self._rows(
1976
+ inbound_async_edge = self._rows(
1819
1977
  f"MATCH (entry:Route {{id: $rid}})<-[e:ASYNC_CALLS]-(caller:Producer)"
1820
1978
  "<-[:DECLARES_PRODUCER]-(decl:Symbol) "
1821
1979
  f"OPTIONAL MATCH (origin:Symbol)-[:CALLS*0..{hops}]->(decl) "
1822
1980
  "RETURN DISTINCT caller.id AS caller_node_id, 'producer' AS caller_node_kind, "
1823
1981
  "decl.id AS declaring_symbol_id, decl.fqn AS declaring_symbol_fqn, "
1824
1982
  "caller.microservice AS microservice, e.confidence AS confidence, "
1825
- "e.match AS match, origin.id AS origin_symbol_id, origin.fqn AS origin_fqn "
1826
- "ORDER BY confidence DESC, caller_node_id",
1983
+ "e.match AS match, origin.id AS origin_symbol_id, origin.fqn AS origin_fqn",
1984
+ {"rid": entry_route_id},
1985
+ )
1986
+ inbound_async_path = self._rows(
1987
+ "MATCH (entry:Route {id: $rid}) "
1988
+ "MATCH (caller:Producer)<-[:DECLARES_PRODUCER]-(decl:Symbol) "
1989
+ "WHERE entry.topic <> '' AND caller.topic = entry.topic "
1990
+ f"OPTIONAL MATCH (origin:Symbol)-[:CALLS*0..{hops}]->(decl) "
1991
+ "RETURN DISTINCT caller.id AS caller_node_id, 'producer' AS caller_node_kind, "
1992
+ "decl.id AS declaring_symbol_id, decl.fqn AS declaring_symbol_fqn, "
1993
+ "caller.microservice AS microservice, 1.0 AS confidence, "
1994
+ "'topic' AS match, origin.id AS origin_symbol_id, origin.fqn AS origin_fqn",
1827
1995
  {"rid": entry_route_id},
1828
1996
  )
1829
- inbound = inbound_http + inbound_async
1997
+ # Dedup by caller_node_id (edge-based rows carry real confidence/match;
1998
+ # path-based rows fill gaps). Preserve the origin columns from whichever
1999
+ # row wins (the OPTIONAL MATCH over CALLS is the same for both).
2000
+ inbound: list[dict[str, Any]] = []
2001
+ seen: set[str] = set()
2002
+ for row in inbound_http_edge + inbound_http_path + inbound_async_edge + inbound_async_path:
2003
+ cid = str(row.get("caller_node_id") or "")
2004
+ if not cid or cid in seen:
2005
+ continue
2006
+ seen.add(cid)
2007
+ inbound.append(row)
2008
+ inbound.sort(
2009
+ key=lambda r: (-float(r.get("confidence") or 0.0), str(r.get("caller_node_id") or ""))
2010
+ )
1830
2011
  outbound = self._rows(
1831
2012
  f"MATCH (handler:Symbol)-[:EXPOSES]->(entry:Route {{id: $rid}}) "
1832
2013
  f"OPTIONAL MATCH (handler)-[:CALLS*0..{hops}]->(next:Symbol) "
@@ -1881,7 +2062,7 @@ class LadybugGraph:
1881
2062
  microservice: str | None = None,
1882
2063
  client_kind: str | None = None,
1883
2064
  target_service: str | None = None,
1884
- path_prefix: str | None = None,
2065
+ path_contains: str | None = None,
1885
2066
  method: str | None = None,
1886
2067
  limit: int = 100,
1887
2068
  ) -> list[dict[str, Any]]:
@@ -1897,9 +2078,9 @@ class LadybugGraph:
1897
2078
  if target_service:
1898
2079
  params["target_service"] = target_service
1899
2080
  preds.append("c.target_service = $target_service")
1900
- if path_prefix:
1901
- params["path_prefix"] = path_prefix
1902
- preds.append("c.path STARTS WITH $path_prefix")
2081
+ if path_contains:
2082
+ params["path_contains"] = path_contains
2083
+ preds.append("c.path CONTAINS $path_contains")
1903
2084
  if method is not None and method != "":
1904
2085
  params["method"] = method
1905
2086
  preds.append("c.method = $method")
@@ -1942,7 +2123,7 @@ class LadybugGraph:
1942
2123
  *,
1943
2124
  microservice: str | None = None,
1944
2125
  producer_kind: str | None = None,
1945
- topic_prefix: str | None = None,
2126
+ topic_contains: str | None = None,
1946
2127
  limit: int = 100,
1947
2128
  ) -> list[dict[str, Any]]:
1948
2129
  lim = max(1, min(int(limit), 500))
@@ -1954,9 +2135,9 @@ class LadybugGraph:
1954
2135
  if producer_kind:
1955
2136
  params["producer_kind"] = producer_kind
1956
2137
  preds.append("p.producer_kind = $producer_kind")
1957
- if topic_prefix:
1958
- params["topic_prefix"] = topic_prefix
1959
- preds.append("p.topic STARTS WITH $topic_prefix")
2138
+ if topic_contains:
2139
+ params["topic_contains"] = topic_contains
2140
+ preds.append("p.topic CONTAINS $topic_contains")
1960
2141
  where = (" WHERE " + " AND ".join(preds)) if preds else ""
1961
2142
  q = (
1962
2143
  f"MATCH (p:Producer){where} RETURN {self._PRODUCER_RETURN} "
mcp_hints.py CHANGED
@@ -150,9 +150,9 @@ _BROWNFIELD_ABSENCE_SUBJECT_LABELS = frozenset({"Client", "Producer", "Route"})
150
150
  _REQUIRED_TRAVERSAL_ROLE_KEYS = frozenset({"type_subject", "member_subject", "alien_subject"})
151
151
 
152
152
  _IDENTIFIER_FILTER_FIELDS: dict[str, tuple[str, ...]] = {
153
- "symbol": ("fqn_prefix",),
154
- "route": ("path_prefix",),
155
- "client": ("target_service", "target_path_prefix"),
153
+ "symbol": ("fqn_contains",),
154
+ "route": ("path_contains",),
155
+ "client": ("target_service", "target_path_contains"),
156
156
  }
157
157
 
158
158
 
@@ -541,11 +541,11 @@ def generate_hints(
541
541
  if hint_kind == "route":
542
542
  seed = payload.get("path_prefix_seed")
543
543
  if isinstance(seed, str) and seed.strip():
544
- advisories.append((PRIORITY_META, f"no match — try find(kind='route', filter={{path_prefix: '{seed}'}})"))
544
+ advisories.append((PRIORITY_META, f"no match — try find(kind='route', filter={{path_contains: '{seed}'}})"))
545
545
  struct_pairs.append(_StructuredHint(
546
- "find", {"kind": "route", "filter": {"path_prefix": seed}}, True, PRIORITY_META,
546
+ "find", {"kind": "route", "filter": {"path_contains": seed}}, True, PRIORITY_META,
547
547
  LABEL_TRY_FIND_ROUTE,
548
- "no route found for path prefix seed",
548
+ "no route found for path seed",
549
549
  ))
550
550
  elif hint_kind == "client":
551
551
  seed = payload.get("target_service_seed")