Skip to content

Commit dd1dc4e

Browse files
fix(pr-3 review r2): fail-loud describe(ucs:), EDGE-NAVIGATION appendix, tests
describe rejects ucs: ids with a teaching message; generator documents UnresolvedCallSite storage; tighten has-unresolved hint and combined dedup+interleave tests. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d0d6ae4 commit dd1dc4e

8 files changed

Lines changed: 98 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ A deterministic property graph derived from tree-sitter Java parsing lives next
372372
| `Client` | Outbound HTTP / messaging call site |
373373
| `UnresolvedCallSite` | Receiver-failure call site (`chained_receiver`, `phantom_unresolved_receiver`) — not a `Symbol`; ids use the `ucs:` prefix |
374374

375-
Known-receiver-external JDK / Spring / Lombok callees stay on **`CALLS`** as phantom **method** symbols (`resolved=false`). Receiver-failure sites (unresolved receiver or chained receiver) are **`UnresolvedCallSite`** nodes linked by **`UNRESOLVED_AT`** (not in `EDGE_SCHEMA`; use `describe`, `neighbors(..., include_unresolved=True)`, or `java-codebase-rag unresolved-calls`).
375+
Known-receiver-external JDK / Spring / Lombok callees stay on **`CALLS`** as phantom **method** symbols (`resolved=false`). Receiver-failure sites (unresolved receiver or chained receiver) are **`UnresolvedCallSite`** nodes linked by **`UNRESOLVED_AT`** (not in `EDGE_SCHEMA`; use `describe(method_id).unresolved_call_sites`, `neighbors(..., include_unresolved=True)`, or `java-codebase-rag unresolved-calls`).
376376

377377
### Edge types (MCP-traversable)
378378

docs/AGENT-GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ Identifier lookup; three statuses above. Args: `identifier`, optional `hint_kind
213213

214214
One hop. Args: `ids` (string or array), **`direction`**, **`edge_types`**, `limit` (default 25), `offset`, optional `filter` on the other node, optional **`edge_filter`** (`edge_types` must be exactly `['CALLS']` — no composed dot-keys or second stored label; fail-loud otherwise).
215215

216-
**Multiple origin ids:** each id loads the full CALLS stream (or generic hop) in list order; `offset`/`limit` apply to the **concatenated** edge list (`ids[0]` edges first, then `ids[1]`, …), not global source order across origins — a large first origin can leave no rows for later ids within the same page. High fan-out methods are slow; prefer one id per call or a smaller `limit`.
216+
**Multiple origin ids:** each id loads the full CALLS stream (or generic hop) in list order; `offset`/`limit` apply to the **concatenated** edge list (`ids[0]` edges first, then `ids[1]`, …), not global source order across origins — a large first origin can leave no rows for later ids within the same page. High fan-out methods are slow; prefer one id per call or a smaller `limit`. **Hints:** `TPL_NEIGHBORS_CALLS_HIGH_FANOUT` / `TPL_NEIGHBORS_CALLS_HAS_UNRESOLVED` fire only for a **single** origin id (multi-origin CALLS skips those nudges).
217217

218218
Returns **edges** with `attrs` (`confidence`, `strategy`, `match`, … on cross-service edges) and **`other`** node.
219219

docs/EDGE-NAVIGATION.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,20 @@
252252
- `member_subject`: neighbors(['{id}'],'out',['DECLARES_PRODUCER']) then neighbors(producer_ids,'out',['ASYNC_CALLS'])
253253
- `route_subject`: neighbors(['{id}'],'in',['ASYNC_CALLS']) then neighbors(producer_ids,'in',['DECLARES_PRODUCER']) for declaring method
254254
- `alien_subject`: ASYNC_CALLS connects Producer→Route; use DECLARES_PRODUCER from a method Symbol, or neighbors(producer_id,'out',['ASYNC_CALLS']) from a Producer id
255+
256+
257+
## Graph storage (not MCP `neighbors` edge_types)
258+
259+
### `UnresolvedCallSite` + `UNRESOLVED_AT` (ontology 15 / CALLS-NOISE PR-3)
260+
261+
Receiver-failure call sites (`chained_receiver`, `phantom_unresolved_receiver`) are **not** `CALLS` rows. They are `UnresolvedCallSite` nodes (`id` prefix `ucs:`) linked from the caller method Symbol via `UNRESOLVED_AT`.
262+
263+
| Surface | How to read them |
264+
| --- | --- |
265+
| `describe(method_id)` | `record.data.unresolved_call_sites` (capped at 5) + footer when more exist |
266+
| `neighbors(..., ['CALLS'], include_unresolved=True)` | Interleaved transcript; `row_kind='unresolved_call_site'`; `other.kind=unresolved_call_site` |
267+
| CLI | `java-codebase-rag unresolved-calls list|stats` |
268+
269+
- **Not** in `EDGE_SCHEMA` — do not pass `UNRESOLVED_AT` to `neighbors(edge_types=…)`.
270+
- **`describe(ucs:…)`** is invalid (fail-loud); describe the **caller method** instead.
271+
- Fresh graphs: `CALLS.strategy` no longer includes `phantom` or `chained_receiver` for receiver failure (those literals remain on HTTP/ASYNC `match` and brownfield resolver sets).

mcp_v2.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,13 @@ def find_v2(
10121012
return FindOutput(success=False, message=str(exc), hints=[], limit=None, offset=None)
10131013

10141014

1015+
_DESCRIBE_UCS_ID_MESSAGE = (
1016+
"UnresolvedCallSite ids (ucs:…) are not describable — use describe(caller_method_id) "
1017+
"for record.data.unresolved_call_sites, neighbors(..., include_unresolved=True), "
1018+
"or java-codebase-rag unresolved-calls list --method-id <caller_id>"
1019+
)
1020+
1021+
10151022
def describe_v2(
10161023
id: str | None = None,
10171024
fqn: str | None = None,
@@ -1023,6 +1030,8 @@ def describe_v2(
10231030
has_fqn = bool(fqn and str(fqn).strip())
10241031
if not has_id and not has_fqn:
10251032
return DescribeOutput(success=False, message="id or fqn required", hints=[])
1033+
if has_id and str(id).strip().startswith("ucs:"):
1034+
return DescribeOutput(success=False, message=_DESCRIBE_UCS_ID_MESSAGE, hints=[])
10261035
hint_message: str | None = None
10271036
node_id: str
10281037
if has_id:
@@ -1043,6 +1052,8 @@ def describe_v2(
10431052
"then describe(id=...) on the chosen node"
10441053
)
10451054
kind = _resolve_node_kind(g, node_id)
1055+
if kind == "unresolved_call_site":
1056+
return DescribeOutput(success=False, message=_DESCRIBE_UCS_ID_MESSAGE, hints=[])
10461057
row = _load_node_record(g, node_id, kind)
10471058
if row is None:
10481059
return DescribeOutput(success=False, message=f"No node found for `{node_id}`", hints=[])

propose/completed/CALLS-NOISE-AND-RESOLUTION-PROPOSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# CALLS-NOISE-AND-RESOLUTION — clean the CALLS edge by removing one bucket and projecting the other
22

3-
**Status**: under review
3+
**Status**: landed (PR-3)
44
**Author**: Dmitriy Teriaev + Perplexity Computer
55
**Date**: 2026-05-18
66
**Tracks**: [#177](https://github.com/HumanBean17/java-codebase-rag/issues/177)

scripts/generate_edge_navigation.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@
1818

1919
_COMPOSED_MEMBER_EDGE_NAMES = frozenset({"EXPOSES", "DECLARES_CLIENT", "DECLARES_PRODUCER"})
2020

21+
_GRAPH_STORAGE_APPENDIX = """
22+
## Graph storage (not MCP `neighbors` edge_types)
23+
24+
### `UnresolvedCallSite` + `UNRESOLVED_AT` (ontology 15 / CALLS-NOISE PR-3)
25+
26+
Receiver-failure call sites (`chained_receiver`, `phantom_unresolved_receiver`) are **not** `CALLS` rows. They are `UnresolvedCallSite` nodes (`id` prefix `ucs:`) linked from the caller method Symbol via `UNRESOLVED_AT`.
27+
28+
| Surface | How to read them |
29+
| --- | --- |
30+
| `describe(method_id)` | `record.data.unresolved_call_sites` (capped at 5) + footer when more exist |
31+
| `neighbors(..., ['CALLS'], include_unresolved=True)` | Interleaved transcript; `row_kind='unresolved_call_site'`; `other.kind=unresolved_call_site` |
32+
| CLI | `java-codebase-rag unresolved-calls list|stats` |
33+
34+
- **Not** in `EDGE_SCHEMA` — do not pass `UNRESOLVED_AT` to `neighbors(edge_types=…)`.
35+
- **`describe(ucs:…)`** is invalid (fail-loud); describe the **caller method** instead.
36+
- Fresh graphs: `CALLS.strategy` no longer includes `phantom` or `chained_receiver` for receiver failure (those literals remain on HTTP/ASYNC `match` and brownfield resolver sets).
37+
"""
38+
2139
_DEFAULT_OUT = _REPO_ROOT / "docs" / "EDGE-NAVIGATION.md"
2240
_BANNER = (
2341
"# Edge Navigation Schema\n\n"
@@ -75,6 +93,7 @@ def generate_markdown() -> str:
7593
parts.append("")
7694
for spec in EDGE_SCHEMA.values():
7795
parts.extend(_render_edge(spec))
96+
parts.append(_GRAPH_STORAGE_APPENDIX.rstrip())
7897
return "\n".join(parts).rstrip() + "\n"
7998

8099

tests/test_mcp_hints.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,11 +512,12 @@ def test_hints_neighbors_calls_high_fanout(kuzu_graph) -> None:
512512

513513
def test_hints_neighbors_calls_has_unresolved(kuzu_graph) -> None:
514514
mid = client_message_processor_process_id(kuzu_graph)
515+
unresolved = kuzu_graph.count_unresolved_for_caller(mid)
516+
assert unresolved >= 1
515517
out = neighbors_v2(mid, direction="out", edge_types=["CALLS"], limit=5, graph=kuzu_graph)
516518
assert out.success is True
517-
assert any(
518-
mcp_hints.TPL_NEIGHBORS_CALLS_HAS_UNRESOLVED.split("{")[0] in h for h in out.hints
519-
)
519+
want = mcp_hints.TPL_NEIGHBORS_CALLS_HAS_UNRESOLVED.format(n=len(out.results), k=unresolved)
520+
assert want in out.hints
520521

521522

522523
def test_hints_neighbors_calls_high_fanout_suppressed_with_edge_filter(kuzu_graph) -> None:

tests/test_mcp_v2.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from java_ontology import VALID_RESOLVE_REASONS
1818

1919
from mcp_v2 import (
20+
Edge,
2021
NodeFilter,
2122
_NODEFILTER_APPLICABLE_FIELDS,
2223
describe_v2,
@@ -1559,6 +1560,49 @@ def test_neighbors_dedup_calls_collapses_identical_dst(kuzu_graph) -> None:
15591560
assert multi, "dedup_calls should emit call_site_count on collapsed rows"
15601561

15611562

1563+
def test_describe_ucs_id_not_describable(kuzu_graph) -> None:
1564+
rows = kuzu_graph._rows( # noqa: SLF001
1565+
"MATCH (u:UnresolvedCallSite) RETURN u.id AS id LIMIT 1",
1566+
)
1567+
assert rows
1568+
ucs_id = str(rows[0]["id"])
1569+
assert ucs_id.startswith("ucs:")
1570+
out = describe_v2(ucs_id, graph=kuzu_graph)
1571+
assert out.success is False
1572+
assert out.record is None
1573+
assert "not describable" in (out.message or "").lower()
1574+
assert "unresolved-calls" in (out.message or "").lower()
1575+
1576+
1577+
def test_neighbors_dedup_calls_include_unresolved(kuzu_graph) -> None:
1578+
mid = client_message_processor_process_id(kuzu_graph)
1579+
out = neighbors_v2(
1580+
mid,
1581+
direction="out",
1582+
edge_types=["CALLS"],
1583+
include_unresolved=True,
1584+
dedup_calls=True,
1585+
limit=500,
1586+
graph=kuzu_graph,
1587+
)
1588+
assert out.success is True
1589+
kinds = {str((e.attrs or {}).get("row_kind") or "resolved") for e in out.results}
1590+
assert "resolved" in kinds
1591+
assert "unresolved_call_site" in kinds
1592+
keys = [_calls_transcript_sort_key_from_edge(e) for e in out.results]
1593+
assert keys == sorted(keys)
1594+
resolved = [e for e in out.results if (e.attrs or {}).get("row_kind") == "resolved"]
1595+
assert any(int((e.attrs or {}).get("call_site_count") or 0) > 1 for e in resolved)
1596+
1597+
1598+
def _calls_transcript_sort_key_from_edge(edge: Edge) -> tuple[int, int, int]:
1599+
attrs = edge.attrs or {}
1600+
line = int(attrs.get("call_site_line") or 0)
1601+
byte = int(attrs.get("call_site_byte") or 0)
1602+
kind_rank = 0 if str(attrs.get("row_kind") or "resolved") == "resolved" else 1
1603+
return (line, byte, kind_rank)
1604+
1605+
15621606
def test_describe_unresolved_call_sites_rollup_cap_footer_and_total(kuzu_graph) -> None:
15631607
mid = client_message_processor_process_id(kuzu_graph)
15641608
out = describe_v2(mid, graph=kuzu_graph)

0 commit comments

Comments
 (0)