java-codebase-rag 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ast_java.py +2813 -0
- brownfield_events.py +58 -0
- build_ast_graph.py +3081 -0
- chunk_heuristics.py +62 -0
- graph_enrich.py +1681 -0
- index_common.py +10 -0
- java_codebase_rag/__init__.py +1 -0
- java_codebase_rag/cli.py +761 -0
- java_codebase_rag/cli_progress.py +52 -0
- java_codebase_rag/config.py +327 -0
- java_codebase_rag/pipeline.py +189 -0
- java_codebase_rag-0.1.0.dist-info/METADATA +818 -0
- java_codebase_rag-0.1.0.dist-info/RECORD +27 -0
- java_codebase_rag-0.1.0.dist-info/WHEEL +5 -0
- java_codebase_rag-0.1.0.dist-info/entry_points.txt +3 -0
- java_codebase_rag-0.1.0.dist-info/licenses/LICENSE +21 -0
- java_codebase_rag-0.1.0.dist-info/top_level.txt +17 -0
- java_index_flow_lancedb.py +398 -0
- java_index_v1_common.py +33 -0
- java_ontology.py +446 -0
- kuzu_queries.py +1989 -0
- mcp_hints.py +748 -0
- mcp_v2.py +1957 -0
- path_filtering.py +472 -0
- pr_analysis.py +534 -0
- search_lancedb.py +1075 -0
- server.py +578 -0
mcp_hints.py
ADDED
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
"""Pure MCP v2 road-sign hint generation (no graph I/O, no search, no LLM).
|
|
2
|
+
|
|
3
|
+
Locked v1 catalog: ``propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`` Appendix A
|
|
4
|
+
(issue #161 producer/override-route amendments in that appendix).
|
|
5
|
+
v2 resolve + neighbors fuzzy-strategy catalog: ``propose/completed/HINTS-V2-PROPOSE.md`` Appendix A.
|
|
6
|
+
v3 empty-neighbors structural catalog: ``propose/completed/HINTS-V3-PROPOSE.md`` §3.1–3.3.
|
|
7
|
+
v4 success-path catalog: ``propose/completed/HINTS-V4-SUCCESS-PATH-PROPOSE.md``.
|
|
8
|
+
Priority cap: same propose §7.12 / ``plans/completed/PLAN-HINTS.md`` principles.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from java_ontology import EDGE_SCHEMA, FUZZY_STRATEGY_SET
|
|
16
|
+
|
|
17
|
+
# Normative schema description (propose §3.1) — imported by ``mcp_v2`` for Field(description=...).
|
|
18
|
+
MCP_HINTS_FIELD_DESCRIPTION = (
|
|
19
|
+
"Road-sign hints pointing to likely next calls. Each hint is a short string "
|
|
20
|
+
"referencing one MCP V2 tool call. Hints are advisory and may be safely ignored. "
|
|
21
|
+
"Maximum 5 hints per output. Describe-time type rollup hints may recommend "
|
|
22
|
+
"DECLARES.* and OVERRIDDEN_BY.* dot-keys for neighbors() on matching Symbol origins; "
|
|
23
|
+
"empty neighbors structural hints never use "
|
|
24
|
+
"dot-key edge labels. For neighbors with multiple origin ids, empty-result "
|
|
25
|
+
"structural hints describe the first origin only. On neighbors with "
|
|
26
|
+
"edge_types=['CALLS'] only, optional edge_filter projects the ordered CALLS stream "
|
|
27
|
+
"(min_confidence, strategies, callee_declaring_role axes); fail-loud with composed "
|
|
28
|
+
"dot-keys or additional stored labels. include_unresolved interleaves "
|
|
29
|
+
"UnresolvedCallSite rows (mutually exclusive with edge_filter). dedup_calls collapses "
|
|
30
|
+
"identical (origin, callee) CALLS rows."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# --- Appendix A verbatim templates (substitute {id}, {kind}, {limit}) ---
|
|
34
|
+
|
|
35
|
+
TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS = (
|
|
36
|
+
"clients via members: neighbors(['{id}'],'out',['DECLARES.DECLARES_CLIENT'])"
|
|
37
|
+
)
|
|
38
|
+
TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS = (
|
|
39
|
+
"routes via members: neighbors(['{id}'],'out',['DECLARES.EXPOSES'])"
|
|
40
|
+
)
|
|
41
|
+
TPL_DESCRIBE_TYPE_PRODUCERS_VIA_MEMBERS = (
|
|
42
|
+
"producers via members: neighbors(['{id}'],'out',['DECLARES.DECLARES_PRODUCER'])"
|
|
43
|
+
)
|
|
44
|
+
TPL_DESCRIBE_METHOD_OVERRIDERS = "overriders: neighbors(['{id}'],'out',['OVERRIDDEN_BY'])"
|
|
45
|
+
TPL_DESCRIBE_METHOD_CLIENTS_IN_OVERRIDERS = (
|
|
46
|
+
"clients in overriders: neighbors(['{id}'],'out',['OVERRIDDEN_BY.DECLARES_CLIENT'])"
|
|
47
|
+
)
|
|
48
|
+
TPL_DESCRIBE_METHOD_PRODUCERS_IN_OVERRIDERS = (
|
|
49
|
+
"producers in overriders: neighbors(['{id}'],'out',['OVERRIDDEN_BY.DECLARES_PRODUCER'])"
|
|
50
|
+
)
|
|
51
|
+
TPL_DESCRIBE_METHOD_ROUTES_IN_OVERRIDERS = (
|
|
52
|
+
"routes in overriders: neighbors(['{id}'],'out',['OVERRIDDEN_BY.EXPOSES'])"
|
|
53
|
+
)
|
|
54
|
+
TPL_DESCRIBE_METHOD_OUTBOUND_CLIENT = "outbound client: neighbors(['{id}'],'out',['DECLARES_CLIENT'])"
|
|
55
|
+
TPL_DESCRIBE_METHOD_OUTBOUND_PRODUCER = "outbound producer: neighbors(['{id}'],'out',['DECLARES_PRODUCER'])"
|
|
56
|
+
TPL_DESCRIBE_METHOD_INBOUND_ROUTE = "inbound route: neighbors(['{id}'],'out',['EXPOSES'])"
|
|
57
|
+
TPL_DESCRIBE_METHOD_MANY_CALLS = "many CALLS — consider filtering by target microservice"
|
|
58
|
+
TPL_DESCRIBE_ROUTE_DECLARING = "declaring method: neighbors(['{id}'],'in',['EXPOSES'])"
|
|
59
|
+
TPL_DESCRIBE_CLIENT_DECLARING = "declaring method: neighbors(['{id}'],'in',['DECLARES_CLIENT'])"
|
|
60
|
+
TPL_DESCRIBE_PRODUCER_DECLARING = "declaring method: neighbors(['{id}'],'in',['DECLARES_PRODUCER'])"
|
|
61
|
+
|
|
62
|
+
TPL_FIND_EMPTY_RESOLVE = "no matches — try resolve(identifier, hint_kind='{kind}') for canonical lookup"
|
|
63
|
+
TPL_FIND_PAGE_FULL = "result page full at {limit} — narrow filter or paginate"
|
|
64
|
+
TPL_FIND_SUCCESS_HANDLER = "handler: neighbors(['{id}'],'in',['EXPOSES'])"
|
|
65
|
+
TPL_FIND_SUCCESS_HTTP_TARGETS = "HTTP targets: neighbors(['{id}'],'out',['HTTP_CALLS'])"
|
|
66
|
+
TPL_FIND_SUCCESS_ASYNC_TARGETS = "async targets: neighbors(['{id}'],'out',['ASYNC_CALLS'])"
|
|
67
|
+
|
|
68
|
+
_FIND_SUCCESS_MAX_CHARS = 120
|
|
69
|
+
|
|
70
|
+
TPL_NEIGHBORS_WRONG_SUBJECT_KIND = (
|
|
71
|
+
"0 results — '{edge}' connects {src_kind} → {dst_kind}; "
|
|
72
|
+
"this is a {subject_kind}. Try: {canonical_traversal}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
TPL_NEIGHBORS_WRONG_DIRECTION = (
|
|
76
|
+
"0 results — '{edge}' is {src_kind} → {dst_kind}; "
|
|
77
|
+
"you requested direction='{requested_dir}'. Try direction='{correct_dir}'."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
TPL_NEIGHBORS_TYPE_LEVEL_REQUERY = (
|
|
81
|
+
"0 results — '{edge}' lives on methods, not on {subject_kind}. "
|
|
82
|
+
"Try: {canonical_traversal}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
TPL_NEIGHBORS_BROWNFIELD_RESOLVED_MAYBE_UNRESOLVED = (
|
|
86
|
+
"edges on '{edge}' are emitted by the brownfield resolver — "
|
|
87
|
+
"absence here may mean unresolved (no matching annotation/target), "
|
|
88
|
+
"not absent from the codebase"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
TPL_SEARCH_WEAK = "results look weak — narrow the query or try find(role=…)"
|
|
92
|
+
|
|
93
|
+
# --- v2: resolve templates (propose/HINTS-V2-PROPOSE.md Appendix A) ---
|
|
94
|
+
|
|
95
|
+
TPL_RESOLVE_NONE_TRY_SEARCH = (
|
|
96
|
+
"no match — try search(query='{identifier}') for ranked fuzzy lookup"
|
|
97
|
+
)
|
|
98
|
+
TPL_RESOLVE_NONE_TRY_FIND_ROUTE = (
|
|
99
|
+
"no match — try find(kind='route', filter={{path_prefix: '{seed}'}})"
|
|
100
|
+
)
|
|
101
|
+
TPL_RESOLVE_NONE_TRY_FIND_CLIENT = (
|
|
102
|
+
"no match — try find(kind='client', filter={{target_service: '{seed}'}})"
|
|
103
|
+
)
|
|
104
|
+
TPL_RESOLVE_MANY_TIGHTEN = (
|
|
105
|
+
"{n} candidates — tighten identifier or pick a candidate by id"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
_RESOLVE_HINT_MAX_CHARS = 120
|
|
109
|
+
_RESOLVE_WILDCARDS = ("*", "?")
|
|
110
|
+
|
|
111
|
+
TPL_NEIGHBORS_FUZZY_STRATEGY = (
|
|
112
|
+
"some edges resolved via brownfield/fallback strategy — check attrs.strategy on each row"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
TPL_NEIGHBORS_CALLS_ROLE_FILTER_OTHER_FALLBACK = (
|
|
116
|
+
"0 CALLS matched callee_declaring_role filter but method has many callees — "
|
|
117
|
+
"targets may be OTHER (interface/JDK); try "
|
|
118
|
+
"edge_filter={{exclude_callee_declaring_roles: ['ENTITY','DTO']}} instead of role exact match"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
TPL_NEIGHBORS_CALLS_NODEFILTER_ROLE_COLLISION = (
|
|
122
|
+
"NodeFilter.role filters the neighbor method's role (usually OTHER), not the callee's "
|
|
123
|
+
"declaring type — use edge_filter={{callee_declaring_role: 'SERVICE'}} (or REPOSITORY) "
|
|
124
|
+
"for CALLS stereotype projection"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
_CALLS_HIGH_FANOUT_THRESHOLD = 10
|
|
128
|
+
|
|
129
|
+
TPL_NEIGHBORS_CALLS_HIGH_FANOUT = (
|
|
130
|
+
"{n} CALLS on this method; the noisy axes are callee_declaring_role "
|
|
131
|
+
"and per-call-site multiplicity. Try edge_filter={{callee_declaring_role: 'SERVICE'}} "
|
|
132
|
+
"for delegation hops, edge_filter={{exclude_callee_declaring_roles: ['ENTITY','DTO']}} "
|
|
133
|
+
"to drop accessor noise, edge_filter={{min_confidence: 0.5}} to trim low-confidence rows "
|
|
134
|
+
"(exclude_external is find_callers-only, not neighbors), or dedup_calls=True to collapse "
|
|
135
|
+
"identical callees."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
TPL_NEIGHBORS_CALLS_HAS_UNRESOLVED = (
|
|
139
|
+
"{n} CALLS shown; this method also has {k} unresolved call sites "
|
|
140
|
+
"(see describe(method_id).unresolved_call_sites, or call neighbors with "
|
|
141
|
+
"include_unresolved=True for a source-ordered interleaved view — note "
|
|
142
|
+
"include_unresolved is mutually exclusive with edge_filter)."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# v4 neighbors success-path (propose/HINTS-V4-SUCCESS-PATH-PROPOSE.md); N1a/N1b alias describe templates.
|
|
146
|
+
TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS = "HTTP targets: neighbors(client_ids,'out',['HTTP_CALLS'])"
|
|
147
|
+
TPL_NEIGHBORS_SUCCESS_ASYNC_TARGETS = "async targets: neighbors(producer_ids,'out',['ASYNC_CALLS'])"
|
|
148
|
+
TPL_NEIGHBORS_SUCCESS_CALLERS = "callers: neighbors(handler_ids,'in',['CALLS'])"
|
|
149
|
+
TPL_NEIGHBORS_SUCCESS_DECLARING_CLIENT = (
|
|
150
|
+
"declaring method: neighbors(client_ids,'in',['DECLARES_CLIENT'])"
|
|
151
|
+
)
|
|
152
|
+
TPL_NEIGHBORS_SUCCESS_DECLARING_PRODUCER = (
|
|
153
|
+
"declaring method: neighbors(producer_ids,'in',['DECLARES_PRODUCER'])"
|
|
154
|
+
)
|
|
155
|
+
TPL_NEIGHBORS_SUCCESS_HANDLER = "handler: neighbors(route_ids,'in',['EXPOSES'])"
|
|
156
|
+
|
|
157
|
+
_NEIGHBORS_SUCCESS_MAX_CHARS = 120
|
|
158
|
+
_EDGE_DECLARES_CLIENT = frozenset({"DECLARES_CLIENT", "DECLARES.DECLARES_CLIENT"})
|
|
159
|
+
_EDGE_DECLARES_PRODUCER = frozenset({"DECLARES_PRODUCER", "DECLARES.DECLARES_PRODUCER"})
|
|
160
|
+
|
|
161
|
+
# §7.12 priority: DECLARES.* type rollups > OVERRIDDEN_BY.* > leaf follow-ups > meta.
|
|
162
|
+
PRIORITY_DECLARES_TYPE_ROLLUP = 4
|
|
163
|
+
PRIORITY_OVERRIDDEN_AXIS = 3
|
|
164
|
+
PRIORITY_LEAF_FOLLOWUP = 2
|
|
165
|
+
PRIORITY_META = 1
|
|
166
|
+
|
|
167
|
+
_TYPE_SYMBOL_KINDS = frozenset({"class", "interface", "enum", "record", "annotation"})
|
|
168
|
+
_METHOD_SYMBOL_KINDS = frozenset({"method", "constructor"})
|
|
169
|
+
|
|
170
|
+
_COMPOSED_DOT_KEY_PREFIXES = ("DECLARES.", "OVERRIDDEN_BY.")
|
|
171
|
+
# Row 4 (brownfield absence): only when the subject is a resolver endpoint node, not a
|
|
172
|
+
# structurally valid Symbol query that happens to be empty (DECLARES_CLIENT, EXPOSES, …).
|
|
173
|
+
_BROWNFIELD_ABSENCE_SUBJECT_LABELS = frozenset({"Client", "Producer", "Route"})
|
|
174
|
+
_REQUIRED_TRAVERSAL_ROLE_KEYS = frozenset({"type_subject", "member_subject", "alien_subject"})
|
|
175
|
+
|
|
176
|
+
_IDENTIFIER_FILTER_FIELDS: dict[str, tuple[str, ...]] = {
|
|
177
|
+
"symbol": ("fqn_prefix",),
|
|
178
|
+
"route": ("path_prefix",),
|
|
179
|
+
"client": ("target_service", "target_path_prefix"),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _out_count(edge_summary: dict[str, Any] | None, key: str) -> int:
|
|
184
|
+
if not edge_summary or key not in edge_summary:
|
|
185
|
+
return 0
|
|
186
|
+
cell = edge_summary[key]
|
|
187
|
+
if not isinstance(cell, dict):
|
|
188
|
+
return 0
|
|
189
|
+
return int(cell.get("out", 0) or 0)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _symbol_declaration_kind(record: dict[str, Any]) -> str | None:
|
|
193
|
+
data = record.get("data")
|
|
194
|
+
if isinstance(data, dict):
|
|
195
|
+
k = data.get("kind")
|
|
196
|
+
if k is not None:
|
|
197
|
+
return str(k).strip() or None
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _subject_node_label(subject_record: dict[str, Any]) -> str:
|
|
202
|
+
if "producer_kind" in subject_record:
|
|
203
|
+
return "Producer"
|
|
204
|
+
if "client_kind" in subject_record:
|
|
205
|
+
return "Client"
|
|
206
|
+
if "framework" in subject_record:
|
|
207
|
+
return "Route"
|
|
208
|
+
return "Symbol"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _traversal_role_for_wrong_kind(subject_label: str, subject_record: dict[str, Any]) -> str:
|
|
212
|
+
if subject_label == "Symbol":
|
|
213
|
+
sk = str(subject_record.get("kind") or "")
|
|
214
|
+
if sk in _METHOD_SYMBOL_KINDS:
|
|
215
|
+
return "member_subject"
|
|
216
|
+
if sk in _TYPE_SYMBOL_KINDS:
|
|
217
|
+
return "alien_subject"
|
|
218
|
+
return "alien_subject"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def typical_traversal_for(
|
|
222
|
+
edge: str,
|
|
223
|
+
role_key: str,
|
|
224
|
+
*,
|
|
225
|
+
subject_id: str,
|
|
226
|
+
direction: str,
|
|
227
|
+
) -> str:
|
|
228
|
+
template = EDGE_SCHEMA[edge].typical_traversals[role_key]
|
|
229
|
+
return template.format(id=subject_id, direction=direction, edge=edge)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def neighbors_empty_hints(
|
|
233
|
+
*,
|
|
234
|
+
subject_record: dict[str, Any],
|
|
235
|
+
requested_edge_types: list[str],
|
|
236
|
+
requested_direction: Literal["in", "out"],
|
|
237
|
+
) -> list[tuple[int, str]]:
|
|
238
|
+
"""Structural empty-neighbors hints from ``EDGE_SCHEMA`` (at most one row 1–3 per edge)."""
|
|
239
|
+
pairs: list[tuple[int, str]] = []
|
|
240
|
+
subject_label = _subject_node_label(subject_record)
|
|
241
|
+
subject_id = str(subject_record.get("id") or "")
|
|
242
|
+
|
|
243
|
+
for edge in requested_edge_types:
|
|
244
|
+
spec = EDGE_SCHEMA.get(edge)
|
|
245
|
+
if spec is None:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
if subject_label != spec.src and subject_label != spec.dst:
|
|
249
|
+
role = _traversal_role_for_wrong_kind(subject_label, subject_record)
|
|
250
|
+
trav = typical_traversal_for(
|
|
251
|
+
edge, role, subject_id=subject_id, direction=requested_direction,
|
|
252
|
+
)
|
|
253
|
+
pairs.append(
|
|
254
|
+
(
|
|
255
|
+
PRIORITY_META,
|
|
256
|
+
TPL_NEIGHBORS_WRONG_SUBJECT_KIND.format(
|
|
257
|
+
edge=edge,
|
|
258
|
+
src_kind=spec.src,
|
|
259
|
+
dst_kind=spec.dst,
|
|
260
|
+
subject_kind=subject_label,
|
|
261
|
+
canonical_traversal=trav,
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
wrong_direction = spec.src != spec.dst and (
|
|
268
|
+
(requested_direction == "out" and subject_label == spec.dst)
|
|
269
|
+
or (requested_direction == "in" and subject_label == spec.src)
|
|
270
|
+
)
|
|
271
|
+
if wrong_direction:
|
|
272
|
+
correct_dir = "in" if requested_direction == "out" else "out"
|
|
273
|
+
pairs.append(
|
|
274
|
+
(
|
|
275
|
+
PRIORITY_META,
|
|
276
|
+
TPL_NEIGHBORS_WRONG_DIRECTION.format(
|
|
277
|
+
edge=edge,
|
|
278
|
+
src_kind=spec.src,
|
|
279
|
+
dst_kind=spec.dst,
|
|
280
|
+
requested_dir=requested_direction,
|
|
281
|
+
correct_dir=correct_dir,
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
subject_label == "Symbol"
|
|
289
|
+
and str(subject_record.get("kind") or "") in _TYPE_SYMBOL_KINDS
|
|
290
|
+
and spec.member_only
|
|
291
|
+
):
|
|
292
|
+
trav = typical_traversal_for(
|
|
293
|
+
edge, "type_subject", subject_id=subject_id, direction=requested_direction,
|
|
294
|
+
)
|
|
295
|
+
pairs.append(
|
|
296
|
+
(
|
|
297
|
+
PRIORITY_META,
|
|
298
|
+
TPL_NEIGHBORS_TYPE_LEVEL_REQUERY.format(
|
|
299
|
+
edge=edge,
|
|
300
|
+
subject_kind=subject_label,
|
|
301
|
+
canonical_traversal=trav,
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if subject_label in _BROWNFIELD_ABSENCE_SUBJECT_LABELS:
|
|
307
|
+
for edge in requested_edge_types:
|
|
308
|
+
spec = EDGE_SCHEMA.get(edge)
|
|
309
|
+
if spec is not None and spec.brownfield_resolver_sourced:
|
|
310
|
+
pairs.append(
|
|
311
|
+
(
|
|
312
|
+
PRIORITY_META,
|
|
313
|
+
TPL_NEIGHBORS_BROWNFIELD_RESOLVED_MAYBE_UNRESOLVED.format(edge=edge),
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
break
|
|
317
|
+
|
|
318
|
+
return pairs
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _hint_contains_composed_dotkey(hint: str) -> bool:
|
|
322
|
+
return any(prefix in hint for prefix in _COMPOSED_DOT_KEY_PREFIXES)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _filter_neighbors_dotkey_hints(pairs: list[tuple[int, str]]) -> list[tuple[int, str]]:
|
|
326
|
+
return [(pri, text) for pri, text in pairs if not _hint_contains_composed_dotkey(text)]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _neighbors_success_subject_is_type(subject_record: dict[str, Any]) -> bool:
|
|
330
|
+
return (
|
|
331
|
+
_subject_node_label(subject_record) == "Symbol"
|
|
332
|
+
and str(subject_record.get("kind") or "") in _TYPE_SYMBOL_KINDS
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _neighbors_results_homogeneous(
|
|
337
|
+
results: list[dict[str, Any]],
|
|
338
|
+
*,
|
|
339
|
+
endpoint_kind: str | None = None,
|
|
340
|
+
symbol_kinds: frozenset[str] | None = None,
|
|
341
|
+
) -> bool:
|
|
342
|
+
if not results:
|
|
343
|
+
return False
|
|
344
|
+
for row in results:
|
|
345
|
+
other = row.get("other")
|
|
346
|
+
if not isinstance(other, dict):
|
|
347
|
+
return False
|
|
348
|
+
ok = str(other.get("kind") or "")
|
|
349
|
+
if endpoint_kind is not None and ok != endpoint_kind:
|
|
350
|
+
return False
|
|
351
|
+
if symbol_kinds is not None:
|
|
352
|
+
if ok != "symbol":
|
|
353
|
+
return False
|
|
354
|
+
if str(other.get("symbol_kind") or "") not in symbol_kinds:
|
|
355
|
+
return False
|
|
356
|
+
return True
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _append_neighbors_success_hint(pairs: list[tuple[int, str]], text: str) -> None:
|
|
360
|
+
# v4 neighbors cap only (describe uses the same N1a/N1b templates without this gate).
|
|
361
|
+
if text and len(text) <= _NEIGHBORS_SUCCESS_MAX_CHARS:
|
|
362
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, text))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def neighbors_calls_fanout_hints(payload: dict[str, Any]) -> list[tuple[int, str]]:
|
|
366
|
+
"""High-fanout and unresolved-site nudges for CALLS-on-method neighbors (PR-3)."""
|
|
367
|
+
pairs: list[tuple[int, str]] = []
|
|
368
|
+
req_types = payload.get("requested_edge_types")
|
|
369
|
+
if not isinstance(req_types, list) or req_types != ["CALLS"]:
|
|
370
|
+
return pairs
|
|
371
|
+
if payload.get("include_unresolved"):
|
|
372
|
+
return pairs
|
|
373
|
+
page_n = len(list(payload.get("results") or []))
|
|
374
|
+
calls_n = int(payload.get("calls_row_count") or 0) or page_n
|
|
375
|
+
unresolved = int(payload.get("unresolved_count") or 0)
|
|
376
|
+
if not payload.get("edge_filter_provided") and calls_n >= _CALLS_HIGH_FANOUT_THRESHOLD:
|
|
377
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_NEIGHBORS_CALLS_HIGH_FANOUT.format(n=calls_n)))
|
|
378
|
+
if unresolved > 0:
|
|
379
|
+
pairs.append(
|
|
380
|
+
(PRIORITY_LEAF_FOLLOWUP, TPL_NEIGHBORS_CALLS_HAS_UNRESOLVED.format(n=page_n, k=unresolved))
|
|
381
|
+
)
|
|
382
|
+
return pairs
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def neighbors_calls_meta_hints(payload: dict[str, Any]) -> list[tuple[int, str]]:
|
|
386
|
+
"""CALLS-specific hints: role-filter OTHER fallback (Decision 20) and NodeFilter.role trap (30)."""
|
|
387
|
+
pairs: list[tuple[int, str]] = []
|
|
388
|
+
req_types = payload.get("requested_edge_types")
|
|
389
|
+
if not isinstance(req_types, list) or req_types != ["CALLS"]:
|
|
390
|
+
return pairs
|
|
391
|
+
results = list(payload.get("results") or [])
|
|
392
|
+
edge_flt = payload.get("edge_filter") if isinstance(payload.get("edge_filter"), dict) else {}
|
|
393
|
+
node_flt = payload.get("node_filter") if isinstance(payload.get("node_filter"), dict) else {}
|
|
394
|
+
role_exact = edge_flt.get("callee_declaring_role")
|
|
395
|
+
if (
|
|
396
|
+
role_exact in ("SERVICE", "REPOSITORY")
|
|
397
|
+
and not results
|
|
398
|
+
and int(payload.get("unfiltered_calls_count") or 0) >= 5
|
|
399
|
+
):
|
|
400
|
+
pairs.append((PRIORITY_META, TPL_NEIGHBORS_CALLS_ROLE_FILTER_OTHER_FALLBACK))
|
|
401
|
+
node_role = node_flt.get("role")
|
|
402
|
+
if node_role and results:
|
|
403
|
+
method_rows = [
|
|
404
|
+
r
|
|
405
|
+
for r in results
|
|
406
|
+
if str(((r.get("other") or {}) if isinstance(r.get("other"), dict) else {}).get("symbol_kind") or "")
|
|
407
|
+
== "method"
|
|
408
|
+
]
|
|
409
|
+
if method_rows:
|
|
410
|
+
other_roles = [
|
|
411
|
+
str(
|
|
412
|
+
((r.get("other") or {}) if isinstance(r.get("other"), dict) else {}).get("role")
|
|
413
|
+
or ""
|
|
414
|
+
)
|
|
415
|
+
for r in method_rows
|
|
416
|
+
]
|
|
417
|
+
if other_roles and sum(1 for role in other_roles if role == "OTHER") >= max(
|
|
418
|
+
1, (len(other_roles) * 3) // 4
|
|
419
|
+
):
|
|
420
|
+
pairs.append((PRIORITY_META, TPL_NEIGHBORS_CALLS_NODEFILTER_ROLE_COLLISION))
|
|
421
|
+
return pairs
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def neighbors_success_hints(payload: dict[str, Any]) -> list[tuple[int, str]]:
|
|
425
|
+
"""v4 non-empty neighbors follow-ups (N1a–N7); no graph I/O."""
|
|
426
|
+
if not payload.get("success"):
|
|
427
|
+
return []
|
|
428
|
+
results = list(payload.get("results") or [])
|
|
429
|
+
if not results or int(payload.get("offset") or 0) != 0:
|
|
430
|
+
return []
|
|
431
|
+
req_types = payload.get("requested_edge_types")
|
|
432
|
+
if not isinstance(req_types, list) or len(req_types) != 1:
|
|
433
|
+
return []
|
|
434
|
+
edge = str(req_types[0]).strip()
|
|
435
|
+
if not edge:
|
|
436
|
+
return []
|
|
437
|
+
direction = payload.get("requested_direction")
|
|
438
|
+
if direction not in ("in", "out"):
|
|
439
|
+
return []
|
|
440
|
+
|
|
441
|
+
pairs: list[tuple[int, str]] = []
|
|
442
|
+
origin_id = str(payload.get("origin_id") or "")
|
|
443
|
+
if not origin_id:
|
|
444
|
+
origin_id = str(results[0].get("origin_id") or "")
|
|
445
|
+
subject_record = payload.get("subject_record")
|
|
446
|
+
is_type_subject = (
|
|
447
|
+
isinstance(subject_record, dict) and _neighbors_success_subject_is_type(subject_record)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
edge == "DECLARES"
|
|
452
|
+
and direction == "out"
|
|
453
|
+
and is_type_subject
|
|
454
|
+
and _neighbors_results_homogeneous(results, symbol_kinds=_METHOD_SYMBOL_KINDS)
|
|
455
|
+
):
|
|
456
|
+
if origin_id:
|
|
457
|
+
_append_neighbors_success_hint(
|
|
458
|
+
pairs, TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id=origin_id),
|
|
459
|
+
)
|
|
460
|
+
_append_neighbors_success_hint(
|
|
461
|
+
pairs, TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS.format(id=origin_id),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if edge in _EDGE_DECLARES_CLIENT and direction == "out":
|
|
465
|
+
if _neighbors_results_homogeneous(results, endpoint_kind="client"):
|
|
466
|
+
_append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS)
|
|
467
|
+
|
|
468
|
+
if edge in _EDGE_DECLARES_PRODUCER and direction == "out":
|
|
469
|
+
if _neighbors_results_homogeneous(results, endpoint_kind="producer"):
|
|
470
|
+
_append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_ASYNC_TARGETS)
|
|
471
|
+
|
|
472
|
+
if (
|
|
473
|
+
edge == "EXPOSES"
|
|
474
|
+
and direction == "in"
|
|
475
|
+
and _neighbors_results_homogeneous(results, symbol_kinds=_METHOD_SYMBOL_KINDS)
|
|
476
|
+
):
|
|
477
|
+
_append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_CALLERS)
|
|
478
|
+
|
|
479
|
+
if edge == "HTTP_CALLS" and direction == "in":
|
|
480
|
+
if _neighbors_results_homogeneous(results, endpoint_kind="client"):
|
|
481
|
+
_append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_DECLARING_CLIENT)
|
|
482
|
+
|
|
483
|
+
if edge == "ASYNC_CALLS" and direction == "in":
|
|
484
|
+
if _neighbors_results_homogeneous(results, endpoint_kind="producer"):
|
|
485
|
+
_append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_DECLARING_PRODUCER)
|
|
486
|
+
|
|
487
|
+
if edge == "DECLARES.EXPOSES" and direction == "out":
|
|
488
|
+
if _neighbors_results_homogeneous(results, endpoint_kind="route"):
|
|
489
|
+
_append_neighbors_success_hint(pairs, TPL_NEIGHBORS_SUCCESS_HANDLER)
|
|
490
|
+
|
|
491
|
+
return pairs
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _find_is_page_full(payload: dict[str, Any], results: list[dict[str, Any]]) -> bool:
|
|
495
|
+
lim = payload.get("limit")
|
|
496
|
+
return (
|
|
497
|
+
lim is not None
|
|
498
|
+
and len(results) >= int(lim)
|
|
499
|
+
and payload.get("has_more_results") is True
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _append_find_success_hint(pairs: list[tuple[int, str]], text: str) -> None:
|
|
504
|
+
if text and len(text) <= _FIND_SUCCESS_MAX_CHARS:
|
|
505
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, text))
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def find_success_hints(payload: dict[str, Any]) -> list[tuple[int, str]]:
|
|
509
|
+
"""v4 non-empty find follow-ups (F1–F3); no graph I/O."""
|
|
510
|
+
if not payload.get("success"):
|
|
511
|
+
return []
|
|
512
|
+
results = list(payload.get("results") or [])
|
|
513
|
+
if not results or _find_is_page_full(payload, results):
|
|
514
|
+
return []
|
|
515
|
+
node_id = str(results[0].get("id") or "")
|
|
516
|
+
if not node_id:
|
|
517
|
+
return []
|
|
518
|
+
kind = str(payload.get("kind") or "")
|
|
519
|
+
pairs: list[tuple[int, str]] = []
|
|
520
|
+
if kind == "route":
|
|
521
|
+
_append_find_success_hint(pairs, TPL_FIND_SUCCESS_HANDLER.format(id=node_id))
|
|
522
|
+
elif kind == "client":
|
|
523
|
+
_append_find_success_hint(pairs, TPL_FIND_SUCCESS_HTTP_TARGETS.format(id=node_id))
|
|
524
|
+
elif kind == "producer":
|
|
525
|
+
_append_find_success_hint(pairs, TPL_FIND_SUCCESS_ASYNC_TARGETS.format(id=node_id))
|
|
526
|
+
return pairs
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _any_fuzzy_strategy(edges: list[dict[str, Any]]) -> bool:
|
|
530
|
+
for e in edges:
|
|
531
|
+
attrs = e.get("attrs") if isinstance(e.get("attrs"), dict) else {}
|
|
532
|
+
s = attrs.get("strategy") if isinstance(attrs, dict) else None
|
|
533
|
+
if isinstance(s, str) and s in FUZZY_STRATEGY_SET:
|
|
534
|
+
return True
|
|
535
|
+
return False
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _find_has_identifier_shaped_filter(kind: str, flt: dict[str, Any]) -> bool:
|
|
539
|
+
for name in _IDENTIFIER_FILTER_FIELDS.get(kind, ()):
|
|
540
|
+
val = flt.get(name)
|
|
541
|
+
if val is None:
|
|
542
|
+
continue
|
|
543
|
+
if isinstance(val, str) and val.strip():
|
|
544
|
+
return True
|
|
545
|
+
if not isinstance(val, str):
|
|
546
|
+
return True
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def finalize_hint_list(scored: list[tuple[int, str]]) -> list[str]:
|
|
551
|
+
"""Dedupe identical rendered strings keeping the highest priority; cap to 5 (drop lowest).
|
|
552
|
+
|
|
553
|
+
Within the same priority tier, keep hints in emission order (first scored wins the cap).
|
|
554
|
+
"""
|
|
555
|
+
best: dict[str, tuple[int, int]] = {}
|
|
556
|
+
for idx, (pri, text) in enumerate(scored):
|
|
557
|
+
if not text:
|
|
558
|
+
continue
|
|
559
|
+
prev = best.get(text)
|
|
560
|
+
if prev is None or pri > prev[0]:
|
|
561
|
+
best[text] = (pri, idx)
|
|
562
|
+
elif pri == prev[0]:
|
|
563
|
+
best[text] = (pri, min(prev[1], idx))
|
|
564
|
+
ordered = sorted(best.items(), key=lambda kv: (-kv[1][0], kv[1][1]))
|
|
565
|
+
return [text for text, _pri in ordered[:5]]
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def generate_hints(
|
|
569
|
+
output_kind: Literal["search", "find", "describe", "neighbors", "resolve"],
|
|
570
|
+
payload: dict[str, Any],
|
|
571
|
+
) -> list[str]:
|
|
572
|
+
"""Return up to 5 road-sign hint strings for a success-only MCP v2 payload dict.
|
|
573
|
+
|
|
574
|
+
For ``search`` / ``find`` / ``describe`` / ``neighbors``, callers must pass
|
|
575
|
+
``success: True``; this function returns ``[]`` when ``success`` is false or
|
|
576
|
+
missing. The ``resolve`` branch is **status-driven** (``status``,
|
|
577
|
+
``resolved_identifier``, ``candidates``, optional seeds) and does not require
|
|
578
|
+
``success`` in the payload; an explicit ``success: False`` still suppresses
|
|
579
|
+
hints (defense in depth).
|
|
580
|
+
"""
|
|
581
|
+
pairs: list[tuple[int, str]] = []
|
|
582
|
+
|
|
583
|
+
if output_kind == "resolve":
|
|
584
|
+
if payload.get("success") is False:
|
|
585
|
+
return []
|
|
586
|
+
status = str(payload.get("status") or "")
|
|
587
|
+
if status == "one":
|
|
588
|
+
return []
|
|
589
|
+
if status == "many":
|
|
590
|
+
n = len(payload.get("candidates") or [])
|
|
591
|
+
if n > 1:
|
|
592
|
+
pairs.append((PRIORITY_META, TPL_RESOLVE_MANY_TIGHTEN.format(n=n)))
|
|
593
|
+
return finalize_hint_list(pairs)
|
|
594
|
+
if status == "none":
|
|
595
|
+
identifier = payload.get("resolved_identifier")
|
|
596
|
+
hint_kind = payload.get("hint_kind")
|
|
597
|
+
if not isinstance(identifier, str) or not identifier.strip():
|
|
598
|
+
return finalize_hint_list(pairs)
|
|
599
|
+
if any(w in identifier for w in _RESOLVE_WILDCARDS):
|
|
600
|
+
return finalize_hint_list(pairs)
|
|
601
|
+
rendered: str | None = None
|
|
602
|
+
if hint_kind == "route":
|
|
603
|
+
seed = payload.get("path_prefix_seed")
|
|
604
|
+
if isinstance(seed, str) and seed.strip():
|
|
605
|
+
rendered = TPL_RESOLVE_NONE_TRY_FIND_ROUTE.format(seed=seed)
|
|
606
|
+
elif hint_kind == "client":
|
|
607
|
+
seed = payload.get("target_service_seed")
|
|
608
|
+
if isinstance(seed, str) and seed.strip():
|
|
609
|
+
rendered = TPL_RESOLVE_NONE_TRY_FIND_CLIENT.format(seed=seed)
|
|
610
|
+
else:
|
|
611
|
+
rendered = TPL_RESOLVE_NONE_TRY_SEARCH.format(identifier=identifier)
|
|
612
|
+
if rendered is not None and len(rendered) <= _RESOLVE_HINT_MAX_CHARS:
|
|
613
|
+
pairs.append((PRIORITY_META, rendered))
|
|
614
|
+
return finalize_hint_list(pairs)
|
|
615
|
+
return []
|
|
616
|
+
|
|
617
|
+
if not payload.get("success"):
|
|
618
|
+
return []
|
|
619
|
+
|
|
620
|
+
if output_kind == "search":
|
|
621
|
+
results: list[dict[str, Any]] = list(payload.get("results") or [])
|
|
622
|
+
lim = payload.get("limit")
|
|
623
|
+
if lim is not None and len(results) == int(lim) and results:
|
|
624
|
+
scores = [float(r.get("score", 0.0) or 0.0) for r in results]
|
|
625
|
+
mx = max(scores)
|
|
626
|
+
mn = min(scores)
|
|
627
|
+
if mx > 0.0 and (mx - mn) < 0.1 * mx:
|
|
628
|
+
pairs.append((PRIORITY_META, TPL_SEARCH_WEAK))
|
|
629
|
+
return finalize_hint_list(pairs)
|
|
630
|
+
|
|
631
|
+
if output_kind == "find":
|
|
632
|
+
kind = str(payload.get("kind") or "")
|
|
633
|
+
results = list(payload.get("results") or [])
|
|
634
|
+
flt = payload.get("filter") if isinstance(payload.get("filter"), dict) else {}
|
|
635
|
+
lim = payload.get("limit")
|
|
636
|
+
if not results and _find_has_identifier_shaped_filter(kind, flt):
|
|
637
|
+
pairs.append((PRIORITY_META, TPL_FIND_EMPTY_RESOLVE.format(kind=kind)))
|
|
638
|
+
if _find_is_page_full(payload, results) and lim is not None:
|
|
639
|
+
pairs.append((PRIORITY_META, TPL_FIND_PAGE_FULL.format(limit=int(lim))))
|
|
640
|
+
pairs.extend(find_success_hints(payload))
|
|
641
|
+
return finalize_hint_list(pairs)
|
|
642
|
+
|
|
643
|
+
if output_kind == "neighbors":
|
|
644
|
+
results = list(payload.get("results") or [])
|
|
645
|
+
req_types = payload.get("requested_edge_types")
|
|
646
|
+
if not isinstance(req_types, list):
|
|
647
|
+
req_types = []
|
|
648
|
+
edge_labels = [str(x).strip() for x in req_types if str(x).strip()]
|
|
649
|
+
offset = int(payload.get("offset") or 0)
|
|
650
|
+
empty_pairs: list[tuple[int, str]] = []
|
|
651
|
+
success_pairs: list[tuple[int, str]] = []
|
|
652
|
+
meta_pairs: list[tuple[int, str]] = []
|
|
653
|
+
if not results and edge_labels and offset == 0:
|
|
654
|
+
subject_record = payload.get("subject_record")
|
|
655
|
+
requested_direction = payload.get("requested_direction")
|
|
656
|
+
if (
|
|
657
|
+
isinstance(subject_record, dict)
|
|
658
|
+
and subject_record
|
|
659
|
+
and requested_direction in ("in", "out")
|
|
660
|
+
):
|
|
661
|
+
empty_pairs.extend(
|
|
662
|
+
neighbors_empty_hints(
|
|
663
|
+
subject_record=subject_record,
|
|
664
|
+
requested_edge_types=edge_labels,
|
|
665
|
+
requested_direction=requested_direction,
|
|
666
|
+
)
|
|
667
|
+
)
|
|
668
|
+
elif results and offset == 0:
|
|
669
|
+
success_pairs = neighbors_success_hints(payload)
|
|
670
|
+
meta_pairs.extend(neighbors_calls_meta_hints(payload))
|
|
671
|
+
meta_pairs.extend(neighbors_calls_fanout_hints(payload))
|
|
672
|
+
if results and _any_fuzzy_strategy(results):
|
|
673
|
+
meta_pairs.append((PRIORITY_META, TPL_NEIGHBORS_FUZZY_STRATEGY))
|
|
674
|
+
return finalize_hint_list(
|
|
675
|
+
_filter_neighbors_dotkey_hints(empty_pairs) + success_pairs + meta_pairs,
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
if output_kind == "describe":
|
|
679
|
+
rec = payload.get("record")
|
|
680
|
+
if not isinstance(rec, dict):
|
|
681
|
+
return []
|
|
682
|
+
node_id = str(rec.get("id") or "")
|
|
683
|
+
if not node_id:
|
|
684
|
+
return []
|
|
685
|
+
kind = str(rec.get("kind") or "")
|
|
686
|
+
es = rec.get("edge_summary")
|
|
687
|
+
edge_summary = es if isinstance(es, dict) else None
|
|
688
|
+
|
|
689
|
+
if kind == "route":
|
|
690
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_ROUTE_DECLARING.format(id=node_id)))
|
|
691
|
+
return finalize_hint_list(pairs)
|
|
692
|
+
if kind == "client":
|
|
693
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_CLIENT_DECLARING.format(id=node_id)))
|
|
694
|
+
return finalize_hint_list(pairs)
|
|
695
|
+
if kind == "producer":
|
|
696
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_PRODUCER_DECLARING.format(id=node_id)))
|
|
697
|
+
return finalize_hint_list(pairs)
|
|
698
|
+
|
|
699
|
+
if kind != "symbol":
|
|
700
|
+
return finalize_hint_list(pairs)
|
|
701
|
+
|
|
702
|
+
decl_kind = _symbol_declaration_kind(rec)
|
|
703
|
+
is_type = decl_kind in _TYPE_SYMBOL_KINDS
|
|
704
|
+
is_method = decl_kind in _METHOD_SYMBOL_KINDS
|
|
705
|
+
|
|
706
|
+
if is_type:
|
|
707
|
+
if _out_count(edge_summary, "DECLARES.DECLARES_CLIENT") > 0:
|
|
708
|
+
pairs.append(
|
|
709
|
+
(PRIORITY_DECLARES_TYPE_ROLLUP, TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS.format(id=node_id))
|
|
710
|
+
)
|
|
711
|
+
if _out_count(edge_summary, "DECLARES.EXPOSES") > 0:
|
|
712
|
+
pairs.append(
|
|
713
|
+
(PRIORITY_DECLARES_TYPE_ROLLUP, TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS.format(id=node_id))
|
|
714
|
+
)
|
|
715
|
+
if _out_count(edge_summary, "DECLARES.DECLARES_PRODUCER") > 0:
|
|
716
|
+
pairs.append(
|
|
717
|
+
(PRIORITY_DECLARES_TYPE_ROLLUP, TPL_DESCRIBE_TYPE_PRODUCERS_VIA_MEMBERS.format(id=node_id))
|
|
718
|
+
)
|
|
719
|
+
return finalize_hint_list(pairs)
|
|
720
|
+
|
|
721
|
+
if is_method:
|
|
722
|
+
if _out_count(edge_summary, "OVERRIDDEN_BY") > 0:
|
|
723
|
+
pairs.append((PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_OVERRIDERS.format(id=node_id)))
|
|
724
|
+
if _out_count(edge_summary, "OVERRIDDEN_BY.DECLARES_CLIENT") > 0:
|
|
725
|
+
pairs.append(
|
|
726
|
+
(PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_CLIENTS_IN_OVERRIDERS.format(id=node_id))
|
|
727
|
+
)
|
|
728
|
+
if _out_count(edge_summary, "OVERRIDDEN_BY.DECLARES_PRODUCER") > 0:
|
|
729
|
+
pairs.append(
|
|
730
|
+
(PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_PRODUCERS_IN_OVERRIDERS.format(id=node_id))
|
|
731
|
+
)
|
|
732
|
+
if _out_count(edge_summary, "OVERRIDDEN_BY.EXPOSES") > 0:
|
|
733
|
+
pairs.append(
|
|
734
|
+
(PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_ROUTES_IN_OVERRIDERS.format(id=node_id))
|
|
735
|
+
)
|
|
736
|
+
if _out_count(edge_summary, "DECLARES_CLIENT") > 0:
|
|
737
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_OUTBOUND_CLIENT.format(id=node_id)))
|
|
738
|
+
if _out_count(edge_summary, "DECLARES_PRODUCER") > 0:
|
|
739
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_OUTBOUND_PRODUCER.format(id=node_id)))
|
|
740
|
+
if _out_count(edge_summary, "EXPOSES") > 0:
|
|
741
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_INBOUND_ROUTE.format(id=node_id)))
|
|
742
|
+
if _out_count(edge_summary, "CALLS") >= 10:
|
|
743
|
+
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_MANY_CALLS))
|
|
744
|
+
return finalize_hint_list(pairs)
|
|
745
|
+
|
|
746
|
+
return finalize_hint_list(pairs)
|
|
747
|
+
|
|
748
|
+
return []
|