Skip to content

Commit a801446

Browse files
emit neighbors fuzzy-strategy hint for brownfield edge attrs
Add FUZZY_STRATEGY_SET to the ontology and extend generate_hints so neighbors responses with fallback resolution strategies surface a single meta-tier advisory hint; document behavior and cover UC6–UC17 plus round-trip. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9280d22 commit a801446

5 files changed

Lines changed: 128 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ Example:
261261
{"kind":"symbol","filter":{"microservice":"chat-core","symbol_kind":"interface"}}
262262
```
263263

264-
**MCP v2 response extras (`hints`, pagination echo):** On success, `search`, `find`, `describe`, `neighbors`, and `resolve` return a `hints` field (`list[str]`, capped at five unique strings) with short, templated suggestions for likely next tool calls; hints are advisory. `hints` is always empty when `success` is false. `resolve` additionally echoes `resolved_identifier` (post-validation trimmed identifier) on every `success=true` response; it is `null` when `success` is false. Resolve hints fire only on `status: none` or `status: many` (not on `status: one`). `search` and `find` additionally echo the request’s `limit` and `offset` on success; on failure those echoed fields are omitted (`null` in JSON). The find page-full hint fires only when another page may exist (handler over-fetches by one row; not exposed on the output model). `neighbors` echoes `requested_edge_types` (deduped edge labels from the request) on success for empty-result hints and diagnostics. See [`propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`](./propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md) Appendix A for the locked v1 template catalog; see [`propose/HINTS-V2-PROPOSE.md`](./propose/HINTS-V2-PROPOSE.md) for v2 additions (`resolve` rules; neighbors fuzzy-strategy in a follow-up PR).
264+
**MCP v2 response extras (`hints`, pagination echo):** On success, `search`, `find`, `describe`, `neighbors`, and `resolve` return a `hints` field (`list[str]`, capped at five unique strings) with short, templated suggestions for likely next tool calls; hints are advisory. `hints` is always empty when `success` is false. `resolve` additionally echoes `resolved_identifier` (post-validation trimmed identifier) on every `success=true` response; it is `null` when `success` is false. Resolve hints fire only on `status: none` or `status: many` (not on `status: one`). `search` and `find` additionally echo the request’s `limit` and `offset` on success; on failure those echoed fields are omitted (`null` in JSON). The find page-full hint fires only when another page may exist (handler over-fetches by one row; not exposed on the output model). `neighbors` echoes `requested_edge_types` (deduped edge labels from the request) on success for empty-result hints and diagnostics; when any result edge carries a brownfield/fallback `attrs.strategy` (see `FUZZY_STRATEGY_SET` in `java_ontology.py`), a single meta-tier fuzzy-strategy hint may also appear. See [`propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`](./propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md) Appendix A for the locked v1 template catalog; see [`propose/HINTS-V2-PROPOSE.md`](./propose/HINTS-V2-PROPOSE.md) for v2 additions (`resolve` rules and neighbors fuzzy-strategy hint).
265265

266266
---
267267

java_ontology.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@
8585
"client_target_path",
8686
))
8787

88+
# Brownfield / fallback edge resolution strategies (hints v2 neighbors fuzzy signal).
89+
FUZZY_STRATEGY_SET: frozenset[str] = frozenset({
90+
"layer_c_source",
91+
"layer_b_fqn",
92+
"phantom",
93+
"chained_receiver",
94+
"overload_ambiguous",
95+
"implicit_super",
96+
})
97+
8898
ResolveReason = Literal[
8999
"exact_id",
90100
"exact_fqn",
@@ -107,5 +117,6 @@
107117
"VALID_ASYNC_CALL_STRATEGIES",
108118
"VALID_HTTP_CALL_MATCHES",
109119
"VALID_RESOLVE_REASONS",
120+
"FUZZY_STRATEGY_SET",
110121
"ResolveReason",
111122
]

mcp_hints.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
"""Pure MCP v2 road-sign hint generation (no graph I/O, no search, no LLM).
22
33
Locked v1 catalog: ``propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`` Appendix A.
4-
v2 resolve catalog: ``propose/HINTS-V2-PROPOSE.md`` Appendix A.
4+
v2 resolve + neighbors fuzzy-strategy catalog: ``propose/HINTS-V2-PROPOSE.md`` Appendix A.
55
Priority cap: same propose §7.12 / ``plans/completed/PLAN-HINTS.md`` principles.
66
"""
77

88
from __future__ import annotations
99

1010
from typing import Any, Literal
1111

12+
from java_ontology import FUZZY_STRATEGY_SET
13+
1214
# Normative schema description (propose §3.1) — imported by ``mcp_v2`` for Field(description=...).
1315
MCP_HINTS_FIELD_DESCRIPTION = (
1416
"Road-sign hints pointing to likely next calls. Each hint is a short string "
@@ -65,6 +67,10 @@
6567
_RESOLVE_HINT_MAX_CHARS = 120
6668
_RESOLVE_WILDCARDS = ("*", "?")
6769

70+
TPL_NEIGHBORS_FUZZY_STRATEGY = (
71+
"some edges resolved via brownfield/fallback strategy — check attrs.strategy on each row"
72+
)
73+
6874
# §7.12 priority: DECLARES.* type rollups > OVERRIDDEN_BY.* > leaf follow-ups > meta.
6975
PRIORITY_DECLARES_TYPE_ROLLUP = 4
7076
PRIORITY_OVERRIDDEN_AXIS = 3
@@ -99,6 +105,15 @@ def _symbol_declaration_kind(record: dict[str, Any]) -> str | None:
99105
return None
100106

101107

108+
def _any_fuzzy_strategy(edges: list[dict[str, Any]]) -> bool:
109+
for e in edges:
110+
attrs = e.get("attrs") if isinstance(e.get("attrs"), dict) else {}
111+
s = attrs.get("strategy") if isinstance(attrs, dict) else None
112+
if isinstance(s, str) and s in FUZZY_STRATEGY_SET:
113+
return True
114+
return False
115+
116+
102117
def _find_has_identifier_shaped_filter(kind: str, flt: dict[str, Any]) -> bool:
103118
for name in _IDENTIFIER_FILTER_FIELDS.get(kind, ()):
104119
val = flt.get(name)
@@ -215,6 +230,8 @@ def generate_hints(
215230
n_types = len([x for x in req_types if str(x).strip()])
216231
if not results and n_types > 0:
217232
pairs.append((PRIORITY_META, TPL_NEIGHBORS_EMPTY_KIND_CHECK))
233+
elif _any_fuzzy_strategy(results):
234+
pairs.append((PRIORITY_META, TPL_NEIGHBORS_FUZZY_STRATEGY))
218235
return finalize_hint_list(pairs)
219236

220237
if output_kind == "describe":

server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,8 @@ async def describe(
448448
"Optional `filter` applies to each neighbor endpoint row; populated fields must be applicable to that "
449449
"neighbor's kind—mixed-kind result sets fail on the first inapplicable neighbor (strict frame). "
450450
"Wildcards in prefix fields are rejected. Unknown NodeFilter keys return success=false. "
451-
"Successful responses echo `requested_edge_types` and may include `hints` (advisory next-step strings)."
451+
"Successful responses echo `requested_edge_types` and may include `hints` (advisory next-step strings). "
452+
"Each edge's `attrs.strategy` indicates resolution quality (brownfield/fallback vs primary paths)."
452453
),
453454
)
454455
async def neighbors(

tests/test_mcp_hints.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
finalize_hint_list,
1616
generate_hints,
1717
)
18+
from java_ontology import FUZZY_STRATEGY_SET
1819
from mcp_v2 import FindOutput, SearchOutput, describe_v2, find_v2, neighbors_v2, resolve_v2, search_v2
1920

2021
_TYPE_KINDS = frozenset({"class", "interface", "enum", "record", "annotation"})
@@ -253,6 +254,100 @@ def test_hints_neighbors_empty_with_edge_types_emits_kind_check(kuzu_graph) -> N
253254
assert mcp_hints.TPL_NEIGHBORS_EMPTY_KIND_CHECK in out.hints
254255

255256

257+
def _neighbors_hint_payload(
258+
results: list[dict[str, Any]],
259+
*,
260+
requested_edge_types: list[str] | None = None,
261+
) -> dict[str, Any]:
262+
return {
263+
"success": True,
264+
"results": results,
265+
"requested_edge_types": requested_edge_types or ["DECLARES_CLIENT"],
266+
}
267+
268+
269+
def _edge_result(*, strategy: str | None = None, edge_type: str = "DECLARES_CLIENT") -> dict[str, Any]:
270+
attrs: dict[str, Any] = {}
271+
if strategy is not None:
272+
attrs["strategy"] = strategy
273+
return {
274+
"origin_id": "sym:pkg.Type#m()",
275+
"edge_type": edge_type,
276+
"direction": "out",
277+
"other": {"id": "client:svc:feign:t:GET:/p", "kind": "client"},
278+
"attrs": attrs,
279+
}
280+
281+
282+
def _method_id_with_fuzzy_calls(kuzu_graph) -> str:
283+
rows = kuzu_graph._rows( # noqa: SLF001
284+
"MATCH (m:Symbol)-[e:CALLS]->() "
285+
"WHERE e.strategy IN $strategies "
286+
"RETURN m.id AS id LIMIT 1",
287+
{"strategies": sorted(FUZZY_STRATEGY_SET)},
288+
)
289+
if not rows:
290+
pytest.fail("no CALLS edge with fuzzy strategy in bank fixture")
291+
return str(rows[0]["id"])
292+
293+
294+
def test_hints_neighbors_fuzzy_strategy_layer_c_source_emits() -> None:
295+
payload = _neighbors_hint_payload([_edge_result(strategy="layer_c_source")])
296+
hints = generate_hints("neighbors", payload)
297+
assert mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY in hints
298+
assert "attrs.strategy" in hints[0]
299+
300+
301+
def test_hints_neighbors_fuzzy_strategy_annotation_absent() -> None:
302+
payload = _neighbors_hint_payload([_edge_result(strategy="annotation")])
303+
assert generate_hints("neighbors", payload) == []
304+
305+
306+
def test_hints_neighbors_fuzzy_strategy_calls_phantom_emits() -> None:
307+
payload = _neighbors_hint_payload(
308+
[_edge_result(strategy="phantom", edge_type="CALLS")],
309+
requested_edge_types=["CALLS"],
310+
)
311+
hints = generate_hints("neighbors", payload)
312+
assert mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY in hints
313+
314+
315+
def test_hints_neighbors_declares_no_strategy_attrs_empty() -> None:
316+
payload = _neighbors_hint_payload(
317+
[_edge_result(edge_type="DECLARES")],
318+
requested_edge_types=["DECLARES"],
319+
)
320+
assert generate_hints("neighbors", payload) == []
321+
322+
323+
def test_hints_neighbors_multi_origin_fuzzy_emits_once() -> None:
324+
payload = _neighbors_hint_payload(
325+
[
326+
_edge_result(strategy="phantom", edge_type="CALLS"),
327+
_edge_result(strategy="annotation", edge_type="CALLS"),
328+
],
329+
requested_edge_types=["CALLS"],
330+
)
331+
hints = generate_hints("neighbors", payload)
332+
assert hints.count(mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY) == 1
333+
334+
335+
def test_hints_neighbors_layer_a_meta_no_fuzzy_hint() -> None:
336+
payload = _neighbors_hint_payload([_edge_result(strategy="layer_a_meta")])
337+
assert generate_hints("neighbors", payload) == []
338+
339+
340+
def test_hints_neighbors_fuzzy_strategy_neighbors_v2_round_trip(kuzu_graph) -> None:
341+
mid = _method_id_with_fuzzy_calls(kuzu_graph)
342+
out = neighbors_v2(mid, direction="out", edge_types=["CALLS"], graph=kuzu_graph, limit=50)
343+
assert out.success is True
344+
assert out.results
345+
strategies = [e.attrs.get("strategy") for e in out.results]
346+
assert any(s in FUZZY_STRATEGY_SET for s in strategies if isinstance(s, str))
347+
assert mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY in out.hints
348+
assert "brownfield/fallback strategy" in out.hints[0]
349+
350+
256351
def test_hints_search_weak_structural_signal_emits(monkeypatch, kuzu_graph) -> None:
257352
rows = [
258353
{
@@ -735,6 +830,7 @@ def test_hints_pagination_none_skips_page_derived_hints() -> None:
735830
),
736831
(mcp_hints.TPL_RESOLVE_NONE_TRY_FIND_CLIENT, {"seed": "smartcare-assign-chat"}),
737832
(mcp_hints.TPL_RESOLVE_MANY_TIGHTEN, {"n": 10}),
833+
(mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY, {}),
738834
],
739835
)
740836
def test_hints_template_rendered_length_leq_120(template: str, fmt: dict[str, Any]) -> None:

0 commit comments

Comments
 (0)