java-codebase-rag 0.6.6__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ast_java.py +8 -3
- build_ast_graph.py +72 -16
- graph_enrich.py +2 -1
- graph_types.py +133 -0
- java_codebase_rag/_fdlimit.py +10 -2
- java_codebase_rag/_stdio.py +32 -0
- java_codebase_rag/cli.py +135 -24
- java_codebase_rag/config.py +128 -9
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +291 -0
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +8 -8
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +8 -8
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +251 -0
- java_codebase_rag/installer.py +438 -103
- java_codebase_rag/jrag.py +4300 -0
- java_codebase_rag/jrag_envelope.py +1085 -0
- java_codebase_rag/jrag_hints.py +204 -0
- java_codebase_rag/jrag_render.py +688 -0
- java_codebase_rag/pipeline.py +20 -0
- {java_codebase_rag-0.6.6.dist-info → java_codebase_rag-0.8.0.dist-info}/METADATA +137 -94
- java_codebase_rag-0.8.0.dist-info/RECORD +43 -0
- {java_codebase_rag-0.6.6.dist-info → java_codebase_rag-0.8.0.dist-info}/WHEEL +1 -1
- {java_codebase_rag-0.6.6.dist-info → java_codebase_rag-0.8.0.dist-info}/entry_points.txt +1 -0
- {java_codebase_rag-0.6.6.dist-info → java_codebase_rag-0.8.0.dist-info}/top_level.txt +2 -0
- java_index_flow_lancedb.py +34 -19
- java_ontology.py +12 -0
- ladybug_queries.py +233 -52
- mcp_hints.py +6 -6
- mcp_v2.py +205 -617
- resolve_service.py +649 -0
- search_lancedb.py +10 -1
- server.py +20 -12
- java_codebase_rag-0.6.6.dist-info/RECORD +0 -34
- {java_codebase_rag-0.6.6.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
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
quoted = re.sub(r'(\w+):',
|
|
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
|
|
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
|
-
|
|
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
|
|
1682
|
-
params["
|
|
1683
|
-
preds.append("r.path
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
1909
|
+
caller_node_id=row["caller_node_id"],
|
|
1780
1910
|
caller_node_kind="client",
|
|
1781
|
-
caller_microservice=
|
|
1782
|
-
declaring_symbol_id=
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
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
|
|
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=
|
|
1923
|
+
caller_node_id=row["caller_node_id"],
|
|
1793
1924
|
caller_node_kind="producer",
|
|
1794
|
-
caller_microservice=
|
|
1795
|
-
declaring_symbol_id=
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1901
|
-
params["
|
|
1902
|
-
preds.append("c.path
|
|
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
|
-
|
|
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
|
|
1958
|
-
params["
|
|
1959
|
-
preds.append("p.topic
|
|
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": ("
|
|
154
|
-
"route": ("
|
|
155
|
-
"client": ("target_service", "
|
|
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={{
|
|
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": {"
|
|
546
|
+
"find", {"kind": "route", "filter": {"path_contains": seed}}, True, PRIORITY_META,
|
|
547
547
|
LABEL_TRY_FIND_ROUTE,
|
|
548
|
-
"no route found for path
|
|
548
|
+
"no route found for path seed",
|
|
549
549
|
))
|
|
550
550
|
elif hint_kind == "client":
|
|
551
551
|
seed = payload.get("target_service_seed")
|