Skip to content

Commit eb61ee6

Browse files
address PR-B review: omit async from RouteCaller until PR-C
Stop labeling Symbol-backed async callers as client in find_route_callers and trace_request_flow; surface caller_client_id on trace_flow HTTP hops; strengthen UC9 test via compute_risk; fix neighbors mock for Client callers. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ad274a0 commit eb61ee6

3 files changed

Lines changed: 68 additions & 45 deletions

File tree

kuzu_queries.py

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,10 @@ class ViaEdge:
116116
in the same chain (e.g. `INJECTS` vs `IMPLEMENTS` vs `CALLS`) and at what hop
117117
from the frontier they were reached.
118118
"""
119-
edge_type: str # INJECTS | EXTENDS | IMPLEMENTS | CALLS
119+
edge_type: str # INJECTS | EXTENDS | IMPLEMENTS | CALLS | HTTP_CALLS | ASYNC_CALLS
120120
from_fqn: str
121121
hop: int # 1 = direct neighbour of previous-stage frontier
122+
caller_node_id: str = "" # Client id when edge_type is HTTP_CALLS (SCHEMA v2)
122123

123124

124125
@dataclass
@@ -1281,6 +1282,7 @@ def _ingest_flow_row(
12811282
edge_type=str(row.get("edge_type") or ""),
12821283
from_fqn=str(row.get("from_fqn") or ""),
12831284
hop=hop,
1285+
caller_node_id=str(row.get("caller_client_id") or ""),
12841286
)
12851287
existing = stage_results.get(sym.fqn)
12861288
if existing is None:
@@ -1342,7 +1344,8 @@ def _ingest_flow_row(
13421344
"AND n.resolved AND n.kind IN ['class','interface','enum','record','annotation'] "
13431345
"AND e.confidence >= $mc AND root.microservice <> n.microservice "
13441346
f"{scrf} "
1345-
f"RETURN {_symbol_return_for('n')}, 'HTTP_CALLS' AS edge_type, root.fqn AS from_fqn "
1347+
f"RETURN {_symbol_return_for('n')}, 'HTTP_CALLS' AS edge_type, "
1348+
f"root.fqn AS from_fqn, c.id AS caller_client_id "
13461349
f"LIMIT {max(1, remaining * 4)}"
13471350
)
13481351
for row in self._rows(qrf, params_rf):
@@ -1352,14 +1355,14 @@ def _ingest_flow_row(
13521355
if len(stage_results) < stage_limit:
13531356
remaining = stage_limit - len(stage_results)
13541357
qrf_async = (
1355-
"MATCH (root:Symbol)-[:DECLARES]->(m1:Symbol)-[e:ASYNC_CALLS]->(rt:Route)"
1356-
"<-[:EXPOSES]-(handler:Symbol)<-[:DECLARES]-(n:Symbol) "
1357-
"WHERE root.fqn IN $fqns AND n.role IN $roles "
1358-
"AND n.resolved AND n.kind IN ['class','interface','enum','record','annotation'] "
1359-
"AND e.confidence >= $mc AND root.microservice <> n.microservice "
1360-
f"{scrf} "
1361-
f"RETURN {_symbol_return_for('n')}, 'ASYNC_CALLS' AS edge_type, root.fqn AS from_fqn "
1362-
f"LIMIT {max(1, remaining * 4)}"
1358+
"MATCH (root:Symbol)-[:DECLARES]->(m1:Symbol)-[e:ASYNC_CALLS]->(rt:Route)"
1359+
"<-[:EXPOSES]-(handler:Symbol)<-[:DECLARES]-(n:Symbol) "
1360+
"WHERE root.fqn IN $fqns AND n.role IN $roles "
1361+
"AND n.resolved AND n.kind IN ['class','interface','enum','record','annotation'] "
1362+
"AND e.confidence >= $mc AND root.microservice <> n.microservice "
1363+
f"{scrf} "
1364+
f"RETURN {_symbol_return_for('n')}, 'ASYNC_CALLS' AS edge_type, root.fqn AS from_fqn "
1365+
f"LIMIT {max(1, remaining * 4)}"
13631366
)
13641367
for row in self._rows(qrf_async, params_rf):
13651368
_ingest_flow_row(row, filter_external_fqn=True)
@@ -1488,6 +1491,7 @@ def find_route_callers(
14881491
path_template: str = "",
14891492
method: str = "",
14901493
) -> list[RouteCaller]:
1494+
"""HTTP callers via Client (two-hop). Async callers omitted until PR-C (Producer)."""
14911495
rid = route_id or ""
14921496
if not rid:
14931497
params: dict[str, Any] = {
@@ -1514,12 +1518,6 @@ def find_route_callers(
15141518
"ORDER BY e.confidence DESC, c.id",
15151519
{"rid": rid},
15161520
)
1517-
async_rows = self._rows(
1518-
"MATCH (s:Symbol)-[e:ASYNC_CALLS]->(r:Route {id: $rid}) "
1519-
"RETURN s.id AS caller_node_id, s.microservice AS caller_microservice, "
1520-
"s.id AS declaring_symbol_id, e.confidence AS confidence, e.match AS match",
1521-
{"rid": rid},
1522-
)
15231521
out: list[RouteCaller] = []
15241522
for row in http_rows:
15251523
out.append(
@@ -1534,22 +1532,10 @@ def find_route_callers(
15341532
raw_uri=str(row.get("raw_uri") or ""),
15351533
),
15361534
)
1537-
for row in async_rows:
1538-
sym_id = str(row.get("caller_node_id") or "")
1539-
out.append(
1540-
RouteCaller(
1541-
caller_node_id=sym_id,
1542-
caller_node_kind="client",
1543-
caller_microservice=str(row.get("caller_microservice") or ""),
1544-
declaring_symbol_id=str(row.get("declaring_symbol_id") or ""),
1545-
confidence=float(row.get("confidence") or 0.0),
1546-
match=str(row.get("match") or ""),
1547-
),
1548-
)
1549-
out.sort(key=lambda c: (-c.confidence, c.caller_node_id))
15501535
return out
15511536

15521537
def trace_request_flow(self, entry_route_id: str, max_hops: int = 5) -> dict[str, Any]:
1538+
"""Inbound HTTP via Client two-hop. Async inbound omitted until PR-C (Producer)."""
15531539
hops = max(1, min(int(max_hops), 8))
15541540
inbound_http = self._rows(
15551541
f"MATCH (entry:Route {{id: $rid}})<-[e:HTTP_CALLS]-(caller:Client)"
@@ -1562,17 +1548,7 @@ def trace_request_flow(self, entry_route_id: str, max_hops: int = 5) -> dict[str
15621548
"ORDER BY confidence DESC, caller_node_id",
15631549
{"rid": entry_route_id},
15641550
)
1565-
inbound_async = self._rows(
1566-
f"MATCH (entry:Route {{id: $rid}})<-[e:ASYNC_CALLS]-(caller:Symbol) "
1567-
f"OPTIONAL MATCH (origin:Symbol)-[:CALLS*0..{hops}]->(caller) "
1568-
"RETURN DISTINCT caller.id AS caller_node_id, 'client' AS caller_node_kind, "
1569-
"caller.id AS declaring_symbol_id, caller.fqn AS declaring_symbol_fqn, "
1570-
"caller.microservice AS microservice, e.confidence AS confidence, "
1571-
"e.match AS match, origin.id AS origin_symbol_id, origin.fqn AS origin_fqn "
1572-
"ORDER BY confidence DESC, caller_node_id",
1573-
{"rid": entry_route_id},
1574-
)
1575-
inbound = inbound_http + inbound_async
1551+
inbound = inbound_http
15761552
outbound = self._rows(
15771553
f"MATCH (handler:Symbol)-[:EXPOSES]->(entry:Route {{id: $rid}}) "
15781554
f"OPTIONAL MATCH (handler)-[:CALLS*0..{hops}]->(next:Symbol) "

tests/test_mcp_v2.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,28 @@ def _rows(self, query: str, params: dict[str, Any] | None = None) -> list[dict[s
420420
and "WHERE a.id" in query
421421
and "RETURN b.id AS other_id" in query
422422
):
423-
return [{"other_id": "sym:caller", "edge_type": "HTTP_CALLS", "confidence": 0.8, "match": "cross_service"}]
423+
return [{"other_id": "client:caller", "edge_type": "HTTP_CALLS", "confidence": 0.8, "match": "cross_service"}]
424+
if "MATCH (n:Client)" in query:
425+
return [
426+
{
427+
"id": "client:caller",
428+
"client_kind": "feign_method",
429+
"target_service": "chat-core",
430+
"method": "POST",
431+
"path": "/chat/joinOperator",
432+
"path_template": "/chat/joinOperator",
433+
"path_regex": "",
434+
"member_fqn": "com.example.Caller#call()",
435+
"member_id": "sym:caller",
436+
"microservice": "chat-core",
437+
"module": "chat-app",
438+
"filename": "Caller.java",
439+
"start_line": 1,
440+
"end_line": 2,
441+
"resolved": True,
442+
"source_layer": "builtin",
443+
}
444+
]
424445
if "MATCH (n:Symbol)" in query:
425446
return [
426447
{
@@ -455,6 +476,8 @@ def _rows(self, query: str, params: dict[str, Any] | None = None) -> list[dict[s
455476
)
456477
assert out.success is True
457478
assert len(out.results) == 1
479+
assert out.results[0].other.id == "client:caller"
480+
assert out.results[0].other.kind == "client"
458481

459482

460483
def test_neighbors_batch_ids_carries_origin_id(kuzu_graph) -> None:

tests/test_pr_analysis.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,37 @@ def test_pr_analysis_changed_methods_finds_routes_via_declares_client(
308308
) -> None:
309309
from kuzu_queries import KuzuGraph
310310

311+
from pr_analysis import ChangedSymbol, compute_risk
312+
311313
g = KuzuGraph(str(kuzu_db_path_cross_service_smoke))
312-
rows = g._rows( # noqa: SLF001
313-
"MATCH (s:Symbol)-[:DECLARES_CLIENT]->(c:Client)-[e:HTTP_CALLS]->(r:Route) "
314-
"WHERE e.match = 'cross_service' RETURN count(*) AS n",
314+
route_rows = g._rows( # noqa: SLF001
315+
"MATCH (r:Route) "
316+
"WHERE r.microservice = 'svc-b' AND r.path_template = '/chat/joinOperator' "
317+
"AND r.method = 'POST' RETURN r.id AS id LIMIT 1",
315318
{},
316319
)
317-
assert int(rows[0].get("n") or 0) >= 1
320+
assert route_rows
321+
rid = str(route_rows[0]["id"])
322+
handler_rows = g._rows( # noqa: SLF001
323+
"MATCH (s:Symbol)-[:EXPOSES]->(r:Route {id: $rid}) RETURN s.id AS id, s.fqn AS fqn LIMIT 1",
324+
{"rid": rid},
325+
)
326+
assert handler_rows
327+
rep = compute_risk(
328+
g,
329+
[
330+
ChangedSymbol(
331+
symbol_id=str(handler_rows[0]["id"]),
332+
fqn=str(handler_rows[0]["fqn"]),
333+
kind="method",
334+
change_type="modified",
335+
file="",
336+
hunk_lines=[1],
337+
),
338+
],
339+
)
340+
assert rep.changed_symbols
341+
assert rep.changed_symbols[0].cross_service_callers_count >= 1
318342

319343

320344
def test_36_removed_symbol_from_minus_only_hunk(kuzu_graph) -> None:

0 commit comments

Comments
 (0)