From e1eab420231ace2f70684d3dcdf8582e93daf4fb Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Sun, 24 May 2026 02:35:27 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(hints):=20describe=20structural=20hint?= =?UTF-8?q?s=20=E2=80=94=20IMPLEMENTS/INJECTS=20wiring=20+=20method=20road?= =?UTF-8?q?=20signs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- mcp_hints.py | 93 +++++ .../DESCRIBE-HINTS-STRUCTURAL-PROPOSE.md | 0 tests/test_mcp_hints.py | 343 +++++++++++++++++- 3 files changed, 434 insertions(+), 2 deletions(-) rename propose/{ => completed}/DESCRIBE-HINTS-STRUCTURAL-PROPOSE.md (100%) diff --git a/mcp_hints.py b/mcp_hints.py index a209dc31..71c75c5e 100644 --- a/mcp_hints.py +++ b/mcp_hints.py @@ -123,6 +123,13 @@ def _extract_other_ids(results: list[dict[str, Any]]) -> list[str]: TPL_DESCRIBE_ROUTE_DECLARING = "declaring method: neighbors(['{id}'],'in',['EXPOSES'])" TPL_DESCRIBE_CLIENT_DECLARING = "declaring method: neighbors(['{id}'],'in',['DECLARES_CLIENT'])" TPL_DESCRIBE_PRODUCER_DECLARING = "declaring method: neighbors(['{id}'],'in',['DECLARES_PRODUCER'])" +TPL_DESCRIBE_TYPE_IMPLEMENTORS = "implementors: neighbors(['{id}'],'in',['IMPLEMENTS'])" +TPL_DESCRIBE_TYPE_IMPLEMENTS = "implements: neighbors(['{id}'],'out',['IMPLEMENTS'])" +TPL_DESCRIBE_TYPE_DEPENDENCIES = "dependencies: neighbors(['{id}'],'out',['INJECTS'])" +TPL_DESCRIBE_TYPE_INJECTORS = "injectors: neighbors(['{id}'],'in',['INJECTS'])" +TPL_DESCRIBE_METHOD_OUTBOUND_CALLS = "outbound calls: neighbors(['{id}'],'out',['CALLS'])" +TPL_DESCRIBE_METHOD_SUPER_DECL = "super declaration: neighbors(['{id}'],'out',['OVERRIDES'])" +TPL_DESCRIBE_METHOD_UNRESOLVED = "unresolved: neighbors(['{id}'],'out',['CALLS'],include_unresolved=True)" TPL_FIND_EMPTY_RESOLVE = "no matches — try resolve(identifier, hint_kind='{kind}') for canonical lookup" TPL_FIND_PAGE_FULL = "result page full at {limit} — narrow filter or paginate" @@ -254,6 +261,23 @@ def _out_count(edge_summary: dict[str, Any] | None, key: str) -> int: return int(cell.get("out", 0) or 0) +def _in_count(edge_summary: dict[str, Any] | None, key: str) -> int: + if not edge_summary or key not in edge_summary: + return 0 + cell = edge_summary[key] + if not isinstance(cell, dict): + return 0 + return int(cell.get("in", 0) or 0) + + +def _type_rollup_would_emit(edge_summary: dict[str, Any] | None) -> bool: + return ( + _out_count(edge_summary, "DECLARES.DECLARES_CLIENT") > 0 + or _out_count(edge_summary, "DECLARES.EXPOSES") > 0 + or _out_count(edge_summary, "DECLARES.DECLARES_PRODUCER") > 0 + ) + + def _symbol_declaration_kind(record: dict[str, Any]) -> str | None: data = record.get("data") if isinstance(data, dict): @@ -1037,6 +1061,12 @@ def generate_hints( "neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["DECLARES_CLIENT"]}, True, PRIORITY_LEAF_FOLLOWUP, )) + if _out_count(edge_summary, "HTTP_CALLS") > 0: + pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_FIND_SUCCESS_HTTP_TARGETS.format(id=node_id))) + struct_pairs.append(_StructuredHint( + "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["HTTP_CALLS"]}, + True, PRIORITY_LEAF_FOLLOWUP, + )) return (finalize_hint_list(pairs), finalize_structured_hints(struct_pairs)) if kind == "producer": pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_PRODUCER_DECLARING.format(id=node_id))) @@ -1044,6 +1074,12 @@ def generate_hints( "neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["DECLARES_PRODUCER"]}, True, PRIORITY_LEAF_FOLLOWUP, )) + if _out_count(edge_summary, "ASYNC_CALLS") > 0: + pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_FIND_SUCCESS_ASYNC_TARGETS.format(id=node_id))) + struct_pairs.append(_StructuredHint( + "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["ASYNC_CALLS"]}, + True, PRIORITY_LEAF_FOLLOWUP, + )) return (finalize_hint_list(pairs), finalize_structured_hints(struct_pairs)) if kind != "symbol": @@ -1078,6 +1114,33 @@ def generate_hints( "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["DECLARES.DECLARES_PRODUCER"]}, True, PRIORITY_DECLARES_TYPE_ROLLUP, )) + + if not _type_rollup_would_emit(edge_summary): + if decl_kind == "interface" and _in_count(edge_summary, "IMPLEMENTS") > 0: + pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_TYPE_IMPLEMENTORS.format(id=node_id))) + struct_pairs.append(_StructuredHint( + "neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["IMPLEMENTS"]}, + True, PRIORITY_LEAF_FOLLOWUP, + )) + if decl_kind == "class" and _out_count(edge_summary, "IMPLEMENTS") > 0: + pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_TYPE_IMPLEMENTS.format(id=node_id))) + struct_pairs.append(_StructuredHint( + "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["IMPLEMENTS"]}, + True, PRIORITY_LEAF_FOLLOWUP, + )) + if decl_kind == "class" and str(rec.get("data", {}).get("role") or rec.get("role") or "") == "SERVICE" and _out_count(edge_summary, "INJECTS") > 0: + pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_TYPE_DEPENDENCIES.format(id=node_id))) + struct_pairs.append(_StructuredHint( + "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["INJECTS"]}, + True, PRIORITY_LEAF_FOLLOWUP, + )) + if decl_kind in {"interface", "class"} and _in_count(edge_summary, "INJECTS") > 0: + pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_TYPE_INJECTORS.format(id=node_id))) + struct_pairs.append(_StructuredHint( + "neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["INJECTS"]}, + True, PRIORITY_LEAF_FOLLOWUP, + )) + return (finalize_hint_list(pairs), finalize_structured_hints(struct_pairs)) if is_method: @@ -1129,6 +1192,36 @@ def generate_hints( "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["EXPOSES"]}, True, PRIORITY_LEAF_FOLLOWUP, )) + calls_out = _out_count(edge_summary, "CALLS") + if 1 <= calls_out <= 9: + method_role = str((rec.get("data") or {}).get("role") or rec.get("role") or "") + if method_role != "OTHER" or calls_out >= 3: + pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_OUTBOUND_CALLS.format(id=node_id))) + struct_pairs.append(_StructuredHint( + "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["CALLS"]}, + True, PRIORITY_LEAF_FOLLOWUP, + )) + if _out_count(edge_summary, "OVERRIDES") > 0: + override_axis_emits = any( + _out_count(edge_summary, k) > 0 + for k in ["OVERRIDDEN_BY"] + [k for k in (edge_summary or {}) if k == "OVERRIDDEN_BY" or k.startswith("OVERRIDDEN_BY.")] + ) + if not override_axis_emits: + pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_SUPER_DECL.format(id=node_id))) + struct_pairs.append(_StructuredHint( + "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["OVERRIDES"]}, + True, PRIORITY_LEAF_FOLLOWUP, + )) + data = rec.get("data") + unresolved = 0 + if isinstance(data, dict): + unresolved = int(data.get("unresolved_call_sites_total") or 0) + if unresolved > 0: + pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_UNRESOLVED.format(id=node_id))) + struct_pairs.append(_StructuredHint( + "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["CALLS"], "include_unresolved": True}, + True, PRIORITY_LEAF_FOLLOWUP, + )) if _out_count(edge_summary, "CALLS") >= 10: pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_MANY_CALLS)) struct_pairs.append(_StructuredHint( diff --git a/propose/DESCRIBE-HINTS-STRUCTURAL-PROPOSE.md b/propose/completed/DESCRIBE-HINTS-STRUCTURAL-PROPOSE.md similarity index 100% rename from propose/DESCRIBE-HINTS-STRUCTURAL-PROPOSE.md rename to propose/completed/DESCRIBE-HINTS-STRUCTURAL-PROPOSE.md diff --git a/tests/test_mcp_hints.py b/tests/test_mcp_hints.py index accca72f..f034f744 100644 --- a/tests/test_mcp_hints.py +++ b/tests/test_mcp_hints.py @@ -308,7 +308,7 @@ def test_hints_describe_client_always_declaring_method(kuzu_graph) -> None: out = describe_v2(cid, graph=kuzu_graph) assert out.success and out.record want = mcp_hints.TPL_DESCRIBE_CLIENT_DECLARING.format(id=cid) - assert out.hints == [want] + assert want in out.hints def test_hints_describe_producer_always_declaring_method(kuzu_graph) -> None: @@ -316,7 +316,7 @@ def test_hints_describe_producer_always_declaring_method(kuzu_graph) -> None: out = describe_v2(pid, graph=kuzu_graph) assert out.success and out.record want = mcp_hints.TPL_DESCRIBE_PRODUCER_DECLARING.format(id=pid) - assert out.hints == [want] + assert want in out.hints def test_hints_find_empty_identifier_filter_suggests_resolve(kuzu_graph) -> None: @@ -1090,6 +1090,13 @@ def test_hints_neighbors_v2_declares_success_emits_dot_key_clients(kuzu_graph) - (mcp_hints.TPL_FIND_SUCCESS_HANDLER, {"id": "route:svc:GET:/api/v1/chat"}), (mcp_hints.TPL_FIND_SUCCESS_HTTP_TARGETS, {"id": "client:svc:feign:target:GET:/p"}), (mcp_hints.TPL_FIND_SUCCESS_ASYNC_TARGETS, {"id": "producer:svc:kafka:topic:t"}), + (mcp_hints.TPL_DESCRIBE_TYPE_IMPLEMENTORS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_TYPE_IMPLEMENTS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_TYPE_DEPENDENCIES, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_TYPE_INJECTORS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_METHOD_OUTBOUND_CALLS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_METHOD_SUPER_DECL, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_METHOD_UNRESOLVED, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), ], ) def test_hints_all_v4_templates_under_120_chars(template: str, substitutions: dict[str, str]) -> None: @@ -1771,6 +1778,227 @@ def test_neighbors_calls_nodefilter_role_collision_hint(kuzu_graph) -> None: assert mcp_hints.TPL_NEIGHBORS_CALLS_NODEFILTER_ROLE_COLLISION in out.hints +# --------------------------------------------------------------------------- +# Describe structural hints — helpers + tests (PR-DESCRIBE-STRUCTURAL-1) +# --------------------------------------------------------------------------- + + +def _interface_with_implements_in(kuzu_graph) -> str: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (iface:Symbol)<-[:IMPLEMENTS]-(impl:Symbol) " + "WHERE iface.kind = 'interface' " + "WITH iface, count(impl) AS nin WHERE nin > 0 " + "RETURN iface.id AS id LIMIT 1", + ) + if not rows: + pytest.skip("no interface with IMPLEMENTS.in > 0 in fixture") + return str(rows[0]["id"]) + + +def _class_with_implements_out(kuzu_graph) -> str: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (cls:Symbol)-[:IMPLEMENTS]->(iface:Symbol) " + "WHERE cls.kind = 'class' " + "WITH cls, count(iface) AS nout WHERE nout > 0 " + "RETURN cls.id AS id LIMIT 1", + ) + if not rows: + pytest.skip("no class with IMPLEMENTS.out > 0 in fixture") + return str(rows[0]["id"]) + + +def _service_with_injects_out(kuzu_graph) -> str: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (cls:Symbol)-[:INJECTS]->(dep:Symbol) " + "WHERE cls.kind = 'class' AND cls.role = 'SERVICE' " + "RETURN cls.id AS id LIMIT 1", + ) + if not rows: + pytest.skip("no SERVICE class with INJECTS.out > 0 in fixture") + return str(rows[0]["id"]) + + +def _type_with_injects_in(kuzu_graph) -> str: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (dep:Symbol)<-[:INJECTS]-(cls:Symbol) " + "WHERE dep.kind IN ['interface', 'class'] " + "RETURN DISTINCT dep.id AS id LIMIT 1", + ) + if not rows: + pytest.skip("no type with INJECTS.in > 0 in fixture") + return str(rows[0]["id"]) + + +def _method_with_mid_calls_out(kuzu_graph) -> str: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (m:Symbol)-[c:CALLS]->() WHERE m.kind = 'method' " + "WITH m, count(c) AS nout WHERE nout >= 3 AND nout <= 9 " + "RETURN m.id AS id LIMIT 1", + ) + if not rows: + pytest.skip("no method with 3 <= CALLS.out <= 9 in fixture") + return str(rows[0]["id"]) + + +def _method_with_overrides_out(kuzu_graph) -> str: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (m:Symbol)-[:OVERRIDES]->() WHERE m.kind = 'method' " + "RETURN m.id AS id LIMIT 1", + ) + if not rows: + pytest.skip("no method with OVERRIDES.out > 0 in fixture") + return str(rows[0]["id"]) + + +def _method_with_unresolved(kuzu_graph) -> str: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (m:Symbol)-[c:CALLS]->() WHERE m.kind = 'method' " + "WITH m, count(c) AS nout WHERE nout >= 1 " + "RETURN m.id AS id, m.fqn AS fqn LIMIT 200", + ) + for r in rows: + mid = str(r["id"]) + out = describe_v2(mid, graph=kuzu_graph) + if out.record and isinstance(out.record.data, dict): + unc = int(out.record.data.get("unresolved_call_sites_total") or 0) + if unc > 0: + return mid + pytest.skip("no method with unresolved_call_sites_total > 0 in fixture") + + +def _client_with_http_calls_out(kuzu_graph) -> str: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (c:Client)-[:HTTP_CALLS]->() RETURN DISTINCT c.id AS id LIMIT 1", + ) + if not rows: + pytest.skip("no client with HTTP_CALLS.out > 0 in fixture") + return str(rows[0]["id"]) + + +def _producer_with_async_calls_out(kuzu_graph) -> str: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (p:Producer)-[:ASYNC_CALLS]->() RETURN DISTINCT p.id AS id LIMIT 1", + ) + if not rows: + pytest.skip("no producer with ASYNC_CALLS.out > 0 in fixture") + return str(rows[0]["id"]) + + +# --- String hint tests --- + + +def test_hints_describe_interface_implementors_emits(kuzu_graph) -> None: + tid = _interface_with_implements_in(kuzu_graph) + out = describe_v2(tid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_DESCRIBE_TYPE_IMPLEMENTORS.format(id=tid) + assert want in out.hints + + +def test_hints_describe_class_implements_emits(kuzu_graph) -> None: + tid = _class_with_implements_out(kuzu_graph) + out = describe_v2(tid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_DESCRIBE_TYPE_IMPLEMENTS.format(id=tid) + assert want in out.hints + + +def test_hints_describe_service_dependencies_emits(kuzu_graph) -> None: + tid = _service_with_injects_out(kuzu_graph) + out = describe_v2(tid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_DESCRIBE_TYPE_DEPENDENCIES.format(id=tid) + assert want in out.hints + + +def test_hints_describe_type_injectors_emits(kuzu_graph) -> None: + tid = _type_with_injects_in(kuzu_graph) + out = describe_v2(tid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_DESCRIBE_TYPE_INJECTORS.format(id=tid) + assert want in out.hints + + +def test_hints_describe_type_skips_tier1_when_rollups(kuzu_graph) -> None: + tid = _controller_class_id_with_exposes(kuzu_graph) + out = describe_v2(tid, graph=kuzu_graph) + assert out.success and out.record + assert out.hints + tier1_markers = ("implementors:", "implements:", "dependencies:", "injectors:") + for h in out.hints: + assert not any(h.startswith(m) for m in tier1_markers) + + +def test_hints_describe_method_outbound_calls_mid_fanout_emits(kuzu_graph) -> None: + mid = _method_with_mid_calls_out(kuzu_graph) + out = describe_v2(mid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_DESCRIBE_METHOD_OUTBOUND_CALLS.format(id=mid) + assert want in out.hints + + +def test_hints_describe_method_outbound_calls_low_fanout_non_other_emits(kuzu_graph) -> None: + rows = kuzu_graph._rows( # noqa: SLF001 + "MATCH (m:Symbol)-[c:CALLS]->() WHERE m.kind = 'method' AND m.role <> 'OTHER' " + "WITH m, count(c) AS nout WHERE nout >= 1 AND nout <= 2 " + "RETURN m.id AS id LIMIT 1", + ) + if not rows: + pytest.skip("no non-OTHER method with 1-2 CALLS.out in fixture") + mid = str(rows[0]["id"]) + out = describe_v2(mid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_DESCRIBE_METHOD_OUTBOUND_CALLS.format(id=mid) + assert want in out.hints + + +def test_hints_describe_method_super_declaration_emits(kuzu_graph) -> None: + mid = _method_with_overrides_out(kuzu_graph) + out = describe_v2(mid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_DESCRIBE_METHOD_SUPER_DECL.format(id=mid) + assert want in out.hints + + +def test_hints_describe_method_unresolved_emits(kuzu_graph) -> None: + mid = _method_with_unresolved(kuzu_graph) + out = describe_v2(mid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_DESCRIBE_METHOD_UNRESOLVED.format(id=mid) + assert want in out.hints + + +def test_hints_describe_client_http_targets_emits(kuzu_graph) -> None: + cid = _client_with_http_calls_out(kuzu_graph) + out = describe_v2(cid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_FIND_SUCCESS_HTTP_TARGETS.format(id=cid) + assert want in out.hints + + +def test_hints_describe_producer_async_targets_emits(kuzu_graph) -> None: + pid = _producer_with_async_calls_out(kuzu_graph) + out = describe_v2(pid, graph=kuzu_graph) + assert out.success and out.record + want = mcp_hints.TPL_FIND_SUCCESS_ASYNC_TARGETS.format(id=pid) + assert want in out.hints + + +def test_hints_describe_structural_templates_char_cap() -> None: + templates = [ + (mcp_hints.TPL_DESCRIBE_TYPE_IMPLEMENTORS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_TYPE_IMPLEMENTS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_TYPE_DEPENDENCIES, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_TYPE_INJECTORS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_METHOD_OUTBOUND_CALLS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_METHOD_SUPER_DECL, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + (mcp_hints.TPL_DESCRIBE_METHOD_UNRESOLVED, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), + ] + for template, kwargs in templates: + rendered = template.format(**kwargs) + assert len(rendered) <= 120, f"{template!r} rendered to {len(rendered)} chars: {rendered!r}" + + # --------------------------------------------------------------------------- # Structured hint tests (PR-1) # --------------------------------------------------------------------------- @@ -1983,6 +2211,117 @@ def test_structured_hint_describe_producer_declaring(kuzu_graph) -> None: ) +# --- Describe structural structured hints --- + + +def test_structured_hints_describe_interface_implementors(kuzu_graph) -> None: + tid = _interface_with_implements_in(kuzu_graph) + out = describe_v2(tid, graph=kuzu_graph) + assert out.success and out.record + _assert_structured_hint( + out.hints_structured, + tool="neighbors", + args_subset={"ids": [tid], "direction": "in", "edge_types": ["IMPLEMENTS"]}, + actionable=True, + ) + + +def test_structured_hints_describe_class_implements(kuzu_graph) -> None: + tid = _class_with_implements_out(kuzu_graph) + out = describe_v2(tid, graph=kuzu_graph) + assert out.success and out.record + _assert_structured_hint( + out.hints_structured, + tool="neighbors", + args_subset={"ids": [tid], "direction": "out", "edge_types": ["IMPLEMENTS"]}, + actionable=True, + ) + + +def test_structured_hints_describe_service_dependencies(kuzu_graph) -> None: + tid = _service_with_injects_out(kuzu_graph) + out = describe_v2(tid, graph=kuzu_graph) + assert out.success and out.record + _assert_structured_hint( + out.hints_structured, + tool="neighbors", + args_subset={"ids": [tid], "direction": "out", "edge_types": ["INJECTS"]}, + actionable=True, + ) + + +def test_structured_hints_describe_type_injectors(kuzu_graph) -> None: + tid = _type_with_injects_in(kuzu_graph) + out = describe_v2(tid, graph=kuzu_graph) + assert out.success and out.record + _assert_structured_hint( + out.hints_structured, + tool="neighbors", + args_subset={"ids": [tid], "direction": "in", "edge_types": ["INJECTS"]}, + actionable=True, + ) + + +def test_structured_hints_describe_method_outbound_calls(kuzu_graph) -> None: + mid = _method_with_mid_calls_out(kuzu_graph) + out = describe_v2(mid, graph=kuzu_graph) + assert out.success and out.record + _assert_structured_hint( + out.hints_structured, + tool="neighbors", + args_subset={"ids": [mid], "direction": "out", "edge_types": ["CALLS"]}, + actionable=True, + ) + + +def test_structured_hints_describe_method_super_declaration(kuzu_graph) -> None: + mid = _method_with_overrides_out(kuzu_graph) + out = describe_v2(mid, graph=kuzu_graph) + assert out.success and out.record + _assert_structured_hint( + out.hints_structured, + tool="neighbors", + args_subset={"ids": [mid], "direction": "out", "edge_types": ["OVERRIDES"]}, + actionable=True, + ) + + +def test_structured_hints_describe_method_unresolved(kuzu_graph) -> None: + mid = _method_with_unresolved(kuzu_graph) + out = describe_v2(mid, graph=kuzu_graph) + assert out.success and out.record + _assert_structured_hint( + out.hints_structured, + tool="neighbors", + args_subset={"ids": [mid], "direction": "out", "edge_types": ["CALLS"], "include_unresolved": True}, + actionable=True, + ) + + +def test_structured_hints_describe_client_http_targets(kuzu_graph) -> None: + cid = _client_with_http_calls_out(kuzu_graph) + out = describe_v2(cid, graph=kuzu_graph) + assert out.success and out.record + _assert_structured_hint( + out.hints_structured, + tool="neighbors", + args_subset={"ids": [cid], "direction": "out", "edge_types": ["HTTP_CALLS"]}, + actionable=True, + ) + + +def test_structured_hints_describe_producer_async_targets(kuzu_graph) -> None: + pid = _producer_with_async_calls_out(kuzu_graph) + out = describe_v2(pid, graph=kuzu_graph) + assert out.success and out.record + _assert_structured_hint( + out.hints_structured, + tool="neighbors", + args_subset={"ids": [pid], "direction": "out", "edge_types": ["ASYNC_CALLS"]}, + actionable=True, + ) + + # --- Find structured hints --- From d7352017e873d13b50362ded2e0da17d236fb3a5 Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Sun, 24 May 2026 10:37:20 +0300 Subject: [PATCH 2/2] review: simplify override-axis gate, extract _record_role, remove duplicate char-cap test Co-Authored-By: Claude Opus 4.7 --- mcp_hints.py | 14 +++++++------- tests/test_mcp_hints.py | 15 --------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/mcp_hints.py b/mcp_hints.py index 71c75c5e..bd64d9b7 100644 --- a/mcp_hints.py +++ b/mcp_hints.py @@ -270,6 +270,10 @@ def _in_count(edge_summary: dict[str, Any] | None, key: str) -> int: return int(cell.get("in", 0) or 0) +def _record_role(rec: dict[str, Any]) -> str: + return str((rec.get("data") or {}).get("role") or rec.get("role") or "") + + def _type_rollup_would_emit(edge_summary: dict[str, Any] | None) -> bool: return ( _out_count(edge_summary, "DECLARES.DECLARES_CLIENT") > 0 @@ -1128,7 +1132,7 @@ def generate_hints( "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["IMPLEMENTS"]}, True, PRIORITY_LEAF_FOLLOWUP, )) - if decl_kind == "class" and str(rec.get("data", {}).get("role") or rec.get("role") or "") == "SERVICE" and _out_count(edge_summary, "INJECTS") > 0: + if decl_kind == "class" and _record_role(rec) == "SERVICE" and _out_count(edge_summary, "INJECTS") > 0: pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_TYPE_DEPENDENCIES.format(id=node_id))) struct_pairs.append(_StructuredHint( "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["INJECTS"]}, @@ -1194,7 +1198,7 @@ def generate_hints( )) calls_out = _out_count(edge_summary, "CALLS") if 1 <= calls_out <= 9: - method_role = str((rec.get("data") or {}).get("role") or rec.get("role") or "") + method_role = _record_role(rec) if method_role != "OTHER" or calls_out >= 3: pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_OUTBOUND_CALLS.format(id=node_id))) struct_pairs.append(_StructuredHint( @@ -1202,11 +1206,7 @@ def generate_hints( True, PRIORITY_LEAF_FOLLOWUP, )) if _out_count(edge_summary, "OVERRIDES") > 0: - override_axis_emits = any( - _out_count(edge_summary, k) > 0 - for k in ["OVERRIDDEN_BY"] + [k for k in (edge_summary or {}) if k == "OVERRIDDEN_BY" or k.startswith("OVERRIDDEN_BY.")] - ) - if not override_axis_emits: + if _out_count(edge_summary, "OVERRIDDEN_BY") == 0: pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_SUPER_DECL.format(id=node_id))) struct_pairs.append(_StructuredHint( "neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["OVERRIDES"]}, diff --git a/tests/test_mcp_hints.py b/tests/test_mcp_hints.py index f034f744..494bde43 100644 --- a/tests/test_mcp_hints.py +++ b/tests/test_mcp_hints.py @@ -1984,21 +1984,6 @@ def test_hints_describe_producer_async_targets_emits(kuzu_graph) -> None: assert want in out.hints -def test_hints_describe_structural_templates_char_cap() -> None: - templates = [ - (mcp_hints.TPL_DESCRIBE_TYPE_IMPLEMENTORS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), - (mcp_hints.TPL_DESCRIBE_TYPE_IMPLEMENTS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), - (mcp_hints.TPL_DESCRIBE_TYPE_DEPENDENCIES, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), - (mcp_hints.TPL_DESCRIBE_TYPE_INJECTORS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), - (mcp_hints.TPL_DESCRIBE_METHOD_OUTBOUND_CALLS, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), - (mcp_hints.TPL_DESCRIBE_METHOD_SUPER_DECL, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), - (mcp_hints.TPL_DESCRIBE_METHOD_UNRESOLVED, {"id": "sym:com.example.RegexComplianceScanner#scan(String)"}), - ] - for template, kwargs in templates: - rendered = template.format(**kwargs) - assert len(rendered) <= 120, f"{template!r} rendered to {len(rendered)} chars: {rendered!r}" - - # --------------------------------------------------------------------------- # Structured hint tests (PR-1) # ---------------------------------------------------------------------------