diff --git a/mcp_hints.py b/mcp_hints.py index 265492c..e89fdc9 100644 --- a/mcp_hints.py +++ b/mcp_hints.py @@ -4,6 +4,7 @@ (issue #161 producer/override-route amendments in that appendix). v2 resolve + neighbors fuzzy-strategy catalog: ``propose/completed/HINTS-V2-PROPOSE.md`` Appendix A. v3 empty-neighbors structural catalog: ``propose/completed/HINTS-V3-PROPOSE.md`` §3.1–3.3. +v4 non-empty neighbors success-path catalog: ``propose/HINTS-V4-SUCCESS-PATH-PROPOSE.md``. Priority cap: same propose §7.12 / ``plans/completed/PLAN-HINTS.md`` principles. """ @@ -103,6 +104,22 @@ "some edges resolved via brownfield/fallback strategy — check attrs.strategy on each row" ) +# v4 neighbors success-path (propose/HINTS-V4-SUCCESS-PATH-PROPOSE.md); N1a/N1b alias describe templates. +TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS = "HTTP targets: neighbors(client_ids,'out',['HTTP_CALLS'])" +TPL_NEIGHBORS_SUCCESS_ASYNC_TARGETS = "async targets: neighbors(producer_ids,'out',['ASYNC_CALLS'])" +TPL_NEIGHBORS_SUCCESS_CALLERS = "callers: neighbors(handler_ids,'in',['CALLS'])" +TPL_NEIGHBORS_SUCCESS_DECLARING_CLIENT = ( + "declaring method: neighbors(client_ids,'in',['DECLARES_CLIENT'])" +) +TPL_NEIGHBORS_SUCCESS_DECLARING_PRODUCER = ( + "declaring method: neighbors(producer_ids,'in',['DECLARES_PRODUCER'])" +) +TPL_NEIGHBORS_SUCCESS_HANDLER = "handler: neighbors(route_ids,'in',['EXPOSES'])" + +_NEIGHBORS_SUCCESS_MAX_CHARS = 120 +_EDGE_DECLARES_CLIENT = frozenset({"DECLARES_CLIENT", "DECLARES.DECLARES_CLIENT"}) +_EDGE_DECLARES_PRODUCER = frozenset({"DECLARES_PRODUCER", "DECLARES.DECLARES_PRODUCER"}) + # §7.12 priority: DECLARES.* type rollups > OVERRIDDEN_BY.* > leaf follow-ups > meta. PRIORITY_DECLARES_TYPE_ROLLUP = 4 PRIORITY_OVERRIDDEN_AXIS = 3 @@ -271,6 +288,112 @@ def _filter_neighbors_dotkey_hints(pairs: list[tuple[int, str]]) -> list[tuple[i return [(pri, text) for pri, text in pairs if not _hint_contains_composed_dotkey(text)] +def _neighbors_success_subject_is_type(subject_record: dict[str, Any]) -> bool: + return ( + _subject_node_label(subject_record) == "Symbol" + and str(subject_record.get("kind") or "") in _TYPE_SYMBOL_KINDS + ) + + +def _neighbors_results_homogeneous( + results: list[dict[str, Any]], + *, + endpoint_kind: str | None = None, + symbol_kinds: frozenset[str] | None = None, +) -> bool: + if not results: + return False + for row in results: + other = row.get("other") + if not isinstance(other, dict): + return False + ok = str(other.get("kind") or "") + if endpoint_kind is not None and ok != endpoint_kind: + return False + if symbol_kinds is not None: + if ok != "symbol": + return False + if str(other.get("symbol_kind") or "") not in symbol_kinds: + return False + return True + + +def _append_neighbors_success_hint(pairs: list[tuple[int, str]], text: str) -> None: + # v4 neighbors cap only (describe uses the same N1a/N1b templates without this gate). + if text and len(text) <= _NEIGHBORS_SUCCESS_MAX_CHARS: + pairs.append((PRIORITY_LEAF_FOLLOWUP, text)) + + +def neighbors_success_hints(payload: dict[str, Any]) -> list[tuple[int, str]]: + """v4 non-empty neighbors follow-ups (N1a–N7); no graph I/O.""" + if not payload.get("success"): + return [] + results = list(payload.get("results") or []) + if not results or int(payload.get("offset") or 0) != 0: + return [] + req_types = payload.get("requested_edge_types") + if not isinstance(req_types, list) or len(req_types) != 1: + return [] + edge = str(req_types[0]).strip() + if not edge: + return [] + direction = payload.get("requested_direction") + if direction not in ("in", "out"): + return [] + + pairs: list[tuple[int, str]] = [] + origin_id = str(payload.get("origin_id") or "") + if not origin_id: + origin_id = str(results[0].get("origin_id") or "") + subject_record = payload.get("subject_record") + is_type_subject = ( + isinstance(subject_record, dict) and _neighbors_success_subject_is_type(subject_record) + ) + + if ( + edge == "DECLARES" + and direction == "out" + and is_type_subject + and _neighbors_results_homogeneous(results, symbol_kinds=_METHOD_SYMBOL_KINDS) + ): + if origin_id: + _append_neighbors_success_hint( + pairs, TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id=origin_id), + ) + _append_neighbors_success_hint( + pairs, TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS.format(id=origin_id), + ) + + if edge in _EDGE_DECLARES_CLIENT and direction == "out": + if _neighbors_results_homogeneous(results, endpoint_kind="client"): + _append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS) + + if edge in _EDGE_DECLARES_PRODUCER and direction == "out": + if _neighbors_results_homogeneous(results, endpoint_kind="producer"): + _append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_ASYNC_TARGETS) + + if ( + edge == "EXPOSES" + and direction == "in" + and _neighbors_results_homogeneous(results, symbol_kinds=_METHOD_SYMBOL_KINDS) + ): + _append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_CALLERS) + + if edge == "HTTP_CALLS" and direction == "in": + if _neighbors_results_homogeneous(results, endpoint_kind="client"): + _append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_DECLARING_CLIENT) + + if edge == "ASYNC_CALLS" and direction == "in": + if _neighbors_results_homogeneous(results, endpoint_kind="producer"): + _append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_DECLARING_PRODUCER) + + if edge == "DECLARES.EXPOSES" and direction == "out": + if _neighbors_results_homogeneous(results, endpoint_kind="route"): + _append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_HANDLER) + + return pairs + + def _any_fuzzy_strategy(edges: list[dict[str, Any]]) -> bool: for e in edges: attrs = e.get("attrs") if isinstance(e.get("attrs"), dict) else {} @@ -395,6 +518,9 @@ def generate_hints( req_types = [] edge_labels = [str(x).strip() for x in req_types if str(x).strip()] offset = int(payload.get("offset") or 0) + empty_pairs: list[tuple[int, str]] = [] + success_pairs: list[tuple[int, str]] = [] + meta_pairs: list[tuple[int, str]] = [] if not results and edge_labels and offset == 0: subject_record = payload.get("subject_record") requested_direction = payload.get("requested_direction") @@ -403,16 +529,21 @@ def generate_hints( and subject_record and requested_direction in ("in", "out") ): - pairs.extend( + empty_pairs.extend( neighbors_empty_hints( subject_record=subject_record, requested_edge_types=edge_labels, requested_direction=requested_direction, ) ) - elif _any_fuzzy_strategy(results): - pairs.append((PRIORITY_META, TPL_NEIGHBORS_FUZZY_STRATEGY)) - return finalize_hint_list(_filter_neighbors_dotkey_hints(pairs)) + else: + if results and offset == 0: + success_pairs = neighbors_success_hints(payload) + if _any_fuzzy_strategy(results): + meta_pairs.append((PRIORITY_META, TPL_NEIGHBORS_FUZZY_STRATEGY)) + return finalize_hint_list( + _filter_neighbors_dotkey_hints(empty_pairs) + success_pairs + meta_pairs, + ) if output_kind == "describe": rec = payload.get("record") diff --git a/plans/PLAN-HINTS-V4.md b/plans/PLAN-HINTS-V4.md index 84f4dc2..d0cd989 100644 --- a/plans/PLAN-HINTS-V4.md +++ b/plans/PLAN-HINTS-V4.md @@ -1,6 +1,6 @@ # Plan: HINTS-V4 (success-path road signs) -Status: **active (planning)**. This plan implements +Status: **active (PR-A in progress)**. This plan implements [`propose/HINTS-V4-SUCCESS-PATH-PROPOSE.md`](../propose/HINTS-V4-SUCCESS-PATH-PROPOSE.md) (issue [#163](https://github.com/HumanBean17/java-codebase-rag/issues/163)). @@ -131,12 +131,12 @@ Implement **verbatim** names from the propose: ## Definition of done (PR-A) -- [ ] N1a–N7 wired; empty vs success dot-key filter split correct. -- [ ] All named PR-A tests pass. -- [ ] `.venv/bin/ruff check .` and `.venv/bin/python -m pytest tests -v` green. -- [ ] No `ONTOLOGY_VERSION` / graph / `mcp_v2.py` changes. -- [ ] Propose remains **locked** (done in #174). -- [ ] `test_hints_neighbors_v2_declares_success_emits_dot_key_clients` passes. +- [x] N1a–N7 wired; empty vs success dot-key filter split correct. +- [x] All named PR-A tests pass. +- [x] `.venv/bin/ruff check .` and `.venv/bin/python -m pytest tests -v` green. +- [x] No `ONTOLOGY_VERSION` / graph / `mcp_v2.py` changes. +- [x] Propose remains **locked** (done in #174). +- [x] `test_hints_neighbors_v2_declares_success_emits_dot_key_clients` passes. ## Implementation step list @@ -227,7 +227,7 @@ Implement **verbatim** names from the propose: # Tracking -- `PR-A`: _pending_ +- `PR-A`: _landed (PR open)_ - `PR-B`: _pending_ ## Cursor handoff diff --git a/tests/test_mcp_hints.py b/tests/test_mcp_hints.py index ecdf8fa..1660770 100644 --- a/tests/test_mcp_hints.py +++ b/tests/test_mcp_hints.py @@ -336,6 +336,8 @@ def _neighbors_hint_payload( requested_edge_types: list[str] | None = None, subject_record: dict[str, Any] | None = None, requested_direction: str = "out", + origin_id: str = "sym:com.example.T", + offset: int = 0, ) -> dict[str, Any]: return { "success": True, @@ -344,7 +346,41 @@ def _neighbors_hint_payload( "requested_direction": requested_direction, "subject_record": subject_record if subject_record is not None - else {"id": "sym:com.example.T", "kind": "class"}, + else {"id": origin_id, "kind": "class"}, + "origin_id": origin_id, + "offset": offset, + } + + +def _type_subject_record(node_id: str, decl_kind: str = "class") -> dict[str, Any]: + return {"id": node_id, "kind": decl_kind} + + +def _symbol_other( + node_id: str, + *, + symbol_kind: str = "method", +) -> dict[str, Any]: + return {"id": node_id, "kind": "symbol", "symbol_kind": symbol_kind} + + +def _terminal_other(node_id: str, kind: str) -> dict[str, Any]: + return {"id": node_id, "kind": kind} + + +def _success_edge( + other: dict[str, Any], + *, + edge_type: str = "DECLARES", + direction: str = "out", + origin_id: str = "sym:com.example.T", +) -> dict[str, Any]: + return { + "origin_id": origin_id, + "edge_type": edge_type, + "direction": direction, + "other": other, + "attrs": {}, } @@ -407,14 +443,20 @@ def _method_id_with_fuzzy_calls(kuzu_graph) -> str: def test_hints_neighbors_fuzzy_strategy_layer_c_source_emits() -> None: - payload = _neighbors_hint_payload([_edge_result(strategy="layer_c_source")]) + payload = _neighbors_hint_payload( + [_edge_result(strategy="layer_c_source", edge_type="CALLS")], + requested_edge_types=["CALLS"], + ) hints = generate_hints("neighbors", payload) assert mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY in hints assert "attrs.strategy" in hints[0] def test_hints_neighbors_fuzzy_strategy_annotation_absent() -> None: - payload = _neighbors_hint_payload([_edge_result(strategy="annotation")]) + payload = _neighbors_hint_payload( + [_edge_result(strategy="annotation", edge_type="CALLS")], + requested_edge_types=["CALLS"], + ) assert generate_hints("neighbors", payload) == [] @@ -448,7 +490,10 @@ def test_hints_neighbors_multi_origin_fuzzy_emits_once() -> None: def test_hints_neighbors_layer_a_meta_no_fuzzy_hint() -> None: - payload = _neighbors_hint_payload([_edge_result(strategy="layer_a_meta")]) + payload = _neighbors_hint_payload( + [_edge_result(strategy="layer_a_meta", edge_type="CALLS")], + requested_edge_types=["CALLS"], + ) assert generate_hints("neighbors", payload) == [] @@ -727,6 +772,7 @@ def test_hints_neighbors_offset_suppresses_empty_structural_hints() -> None: def test_hints_hv20_no_dotkey_edge_labels_in_rendered_neighbors_hints() -> None: + """Empty structural neighbors hints only — success-path N1a/N1b may use DECLARES.* dot-keys.""" payloads = [ _neighbors_empty_payload({"id": "sym:com.example.T", "kind": "class"}, ["DECLARES_CLIENT"]), _neighbors_empty_payload({"id": "sym:com.example.T#m()", "kind": "method"}, ["HTTP_CALLS"]), @@ -741,6 +787,252 @@ def test_hints_hv20_no_dotkey_edge_labels_in_rendered_neighbors_hints() -> None: assert "OVERRIDDEN_BY." not in hint +def test_hints_neighbors_success_may_emit_declares_dot_keys() -> None: + origin = "sym:com.example.T" + payload = _neighbors_hint_payload( + [_success_edge(_symbol_other("sym:com.example.T#m()"), edge_type="DECLARES")], + requested_edge_types=["DECLARES"], + subject_record=_type_subject_record(origin), + origin_id=origin, + ) + hints = generate_hints("neighbors", payload) + assert any("DECLARES.DECLARES_CLIENT" in h for h in hints) + assert any("DECLARES.EXPOSES" in h for h in hints) + + +def test_hints_neighbors_declares_methods_emits_dot_key_clients() -> None: + origin = "sym:com.example.T" + payload = _neighbors_hint_payload( + [ + _success_edge(_symbol_other("sym:com.example.T#m()"), edge_type="DECLARES"), + _success_edge( + _symbol_other("sym:com.example.T#c()", symbol_kind="constructor"), + edge_type="DECLARES", + ), + ], + requested_edge_types=["DECLARES"], + subject_record=_type_subject_record(origin), + origin_id=origin, + ) + want = mcp_hints.TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id=origin) + assert want in generate_hints("neighbors", payload) + + +def test_hints_neighbors_declares_methods_emits_dot_key_routes() -> None: + origin = "sym:com.example.T" + payload = _neighbors_hint_payload( + [_success_edge(_symbol_other("sym:com.example.T#m()"), edge_type="DECLARES")], + requested_edge_types=["DECLARES"], + subject_record=_type_subject_record(origin), + origin_id=origin, + ) + want = mcp_hints.TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS.format(id=origin) + assert want in generate_hints("neighbors", payload) + + +def test_hints_neighbors_declares_client_homogeneous_emits_http_calls() -> None: + payload = _neighbors_hint_payload( + [_success_edge(_terminal_other("client:a", "client"), edge_type="DECLARES_CLIENT")], + requested_edge_types=["DECLARES_CLIENT"], + ) + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS in generate_hints("neighbors", payload) + + +def test_hints_neighbors_declares_dot_key_client_homogeneous_emits_http_calls() -> None: + payload = _neighbors_hint_payload( + [_success_edge(_terminal_other("client:a", "client"), edge_type="DECLARES.DECLARES_CLIENT")], + requested_edge_types=["DECLARES.DECLARES_CLIENT"], + ) + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS in generate_hints("neighbors", payload) + + +def test_hints_neighbors_declares_producer_homogeneous_emits_async_calls() -> None: + payload = _neighbors_hint_payload( + [_success_edge(_terminal_other("producer:a", "producer"), edge_type="DECLARES_PRODUCER")], + requested_edge_types=["DECLARES_PRODUCER"], + ) + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_ASYNC_TARGETS in generate_hints("neighbors", payload) + + +def test_hints_neighbors_declares_dot_key_producer_homogeneous_emits_async_calls() -> None: + payload = _neighbors_hint_payload( + [ + _success_edge( + _terminal_other("producer:a", "producer"), + edge_type="DECLARES.DECLARES_PRODUCER", + ), + ], + requested_edge_types=["DECLARES.DECLARES_PRODUCER"], + ) + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_ASYNC_TARGETS in generate_hints("neighbors", payload) + + +def test_hints_neighbors_declares_dot_key_exposes_homogeneous_emits_handler() -> None: + payload = _neighbors_hint_payload( + [_success_edge(_terminal_other("route:a", "route"), edge_type="DECLARES.EXPOSES")], + requested_edge_types=["DECLARES.EXPOSES"], + ) + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_HANDLER in generate_hints("neighbors", payload) + + +def test_hints_neighbors_exposes_in_methods_emits_calls() -> None: + payload = _neighbors_hint_payload( + [_success_edge(_symbol_other("sym:pkg.Handler#run()"), edge_type="EXPOSES", direction="in")], + requested_edge_types=["EXPOSES"], + requested_direction="in", + subject_record={"id": "route:svc:GET:/p", "framework": "spring"}, + ) + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_CALLERS in generate_hints("neighbors", payload) + + +def test_hints_neighbors_http_calls_in_clients_emits_declares_client() -> None: + payload = _neighbors_hint_payload( + [_success_edge(_terminal_other("client:a", "client"), edge_type="HTTP_CALLS", direction="in")], + requested_edge_types=["HTTP_CALLS"], + requested_direction="in", + subject_record={"id": "route:svc:GET:/p", "framework": "spring"}, + ) + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_DECLARING_CLIENT in generate_hints("neighbors", payload) + + +def test_hints_neighbors_async_calls_in_producers_emits_declares_producer() -> None: + payload = _neighbors_hint_payload( + [ + _success_edge( + _terminal_other("producer:a", "producer"), + edge_type="ASYNC_CALLS", + direction="in", + ), + ], + requested_edge_types=["ASYNC_CALLS"], + requested_direction="in", + subject_record={"id": "route:svc:GET:/p", "framework": "spring"}, + ) + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_DECLARING_PRODUCER in generate_hints("neighbors", payload) + + +def test_hints_neighbors_multi_edge_types_suppresses_success_hints() -> None: + origin = "sym:com.example.T" + payload = _neighbors_hint_payload( + [_success_edge(_symbol_other("sym:com.example.T#m()"), edge_type="DECLARES")], + requested_edge_types=["DECLARES", "DECLARES_CLIENT"], + subject_record=_type_subject_record(origin), + origin_id=origin, + ) + hints = generate_hints("neighbors", payload) + assert mcp_hints.TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id=origin) not in hints + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS not in hints + + +def test_hints_neighbors_declares_from_method_origin_no_n1_rollups() -> None: + origin = "sym:com.example.T#m()" + payload = _neighbors_hint_payload( + [_success_edge(_symbol_other("sym:com.example.T#other()"), edge_type="DECLARES")], + requested_edge_types=["DECLARES"], + subject_record={"id": origin, "kind": "method"}, + origin_id=origin, + ) + hints = generate_hints("neighbors", payload) + assert mcp_hints.TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id=origin) not in hints + assert mcp_hints.TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS.format(id=origin) not in hints + + +def test_hints_neighbors_n1a_n1b_dropped_when_rendered_exceeds_char_cap() -> None: + long_origin = "sym:com." + ("x" * 100) + ".Type" + payload = _neighbors_hint_payload( + [_success_edge(_symbol_other("sym:pkg.T#m()"), edge_type="DECLARES")], + requested_edge_types=["DECLARES"], + subject_record=_type_subject_record(long_origin), + origin_id=long_origin, + ) + rendered_n1a = mcp_hints.TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id=long_origin) + assert len(rendered_n1a) > 120 + hints = generate_hints("neighbors", payload) + assert rendered_n1a not in hints + + +def test_hints_neighbors_mixed_endpoint_kinds_silent() -> None: + payload = _neighbors_hint_payload( + [ + _success_edge(_terminal_other("client:a", "client"), edge_type="DECLARES_CLIENT"), + _success_edge(_terminal_other("route:a", "route"), edge_type="DECLARES_CLIENT"), + ], + requested_edge_types=["DECLARES_CLIENT"], + ) + v4_markers = ( + mcp_hints.TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS, + mcp_hints.TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id="sym:com.example.T"), + ) + hints = generate_hints("neighbors", payload) + assert not any(m in h for h in hints for m in v4_markers) + + +def test_hints_neighbors_offset_suppresses_success_hints() -> None: + origin = "sym:com.example.T" + payload = _neighbors_hint_payload( + [_success_edge(_symbol_other("sym:com.example.T#m()"), edge_type="DECLARES")], + requested_edge_types=["DECLARES"], + subject_record=_type_subject_record(origin), + origin_id=origin, + offset=3, + ) + hints = generate_hints("neighbors", payload) + assert mcp_hints.TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id=origin) not in hints + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS not in hints + + +def test_hints_neighbors_success_beats_fuzzy_in_cap() -> None: + payload = _neighbors_hint_payload( + [ + _success_edge( + _terminal_other("client:a", "client"), + edge_type="DECLARES_CLIENT", + ), + ], + requested_edge_types=["DECLARES_CLIENT"], + ) + payload["results"][0]["attrs"] = {"strategy": "layer_c_source"} + hints = generate_hints("neighbors", payload) + assert mcp_hints.TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS in hints + assert mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY in hints + assert hints.index(mcp_hints.TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS) < hints.index( + mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY, + ) + + +def test_hints_neighbors_v2_declares_success_emits_dot_key_clients(kuzu_graph) -> None: + class_id = _class_symbol_id(kuzu_graph) + out = neighbors_v2(class_id, direction="out", edge_types=["DECLARES"], graph=kuzu_graph, limit=50) + assert out.success is True + assert out.results + want = mcp_hints.TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id=class_id) + assert want in out.hints + + +@pytest.mark.parametrize( + ("template", "substitutions"), + [ + (mcp_hints.TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS, {}), + (mcp_hints.TPL_NEIGHBORS_SUCCESS_ASYNC_TARGETS, {}), + (mcp_hints.TPL_NEIGHBORS_SUCCESS_CALLERS, {}), + (mcp_hints.TPL_NEIGHBORS_SUCCESS_DECLARING_CLIENT, {}), + (mcp_hints.TPL_NEIGHBORS_SUCCESS_DECLARING_PRODUCER, {}), + (mcp_hints.TPL_NEIGHBORS_SUCCESS_HANDLER, {}), + ( + mcp_hints.TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS, + {"id": "sym:com.example.bank.chat.Controller"}, + ), + ( + mcp_hints.TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS, + {"id": "sym:com.example.bank.chat.Controller"}, + ), + ], +) +def test_hints_all_v4_templates_under_120_chars(template: str, substitutions: dict[str, str]) -> None: + rendered = template.format(**substitutions) + assert len(rendered) <= 120 + + def test_hints_neighbors_empty_kind_check_template_removed() -> None: assert not hasattr(mcp_hints, "TPL_NEIGHBORS_EMPTY_KIND_CHECK")