java-codebase-rag 0.1.0__py3-none-any.whl → 0.2.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.
- java_codebase_rag-0.2.0.dist-info/METADATA +228 -0
- {java_codebase_rag-0.1.0.dist-info → java_codebase_rag-0.2.0.dist-info}/RECORD +9 -9
- mcp_hints.py +647 -463
- mcp_v2.py +57 -28
- server.py +5 -6
- java_codebase_rag-0.1.0.dist-info/METADATA +0 -818
- {java_codebase_rag-0.1.0.dist-info → java_codebase_rag-0.2.0.dist-info}/WHEEL +0 -0
- {java_codebase_rag-0.1.0.dist-info → java_codebase_rag-0.2.0.dist-info}/entry_points.txt +0 -0
- {java_codebase_rag-0.1.0.dist-info → java_codebase_rag-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {java_codebase_rag-0.1.0.dist-info → java_codebase_rag-0.2.0.dist-info}/top_level.txt +0 -0
mcp_hints.py
CHANGED
|
@@ -10,153 +10,129 @@ Priority cap: same propose §7.12 / ``plans/completed/PLAN-HINTS.md`` principles
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any, Literal, NamedTuple
|
|
14
15
|
|
|
15
16
|
from java_ontology import EDGE_SCHEMA, FUZZY_STRATEGY_SET
|
|
16
17
|
|
|
17
18
|
# Normative schema description (propose §3.1) — imported by ``mcp_v2`` for Field(description=...).
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
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."
|
|
19
|
+
MCP_HINTS_STRUCTURED_FIELD_DESCRIPTION = (
|
|
20
|
+
"Machine-parseable next-action objects. Each element has "
|
|
21
|
+
"label (short semantic name, e.g. 'routes via members', 'implementors'), "
|
|
22
|
+
"tool (MCP tool name), args (ready-to-use parameters), and actionable "
|
|
23
|
+
"(True = direct call with complete args; False = advisory/partial — agent "
|
|
24
|
+
"fills missing values or uses as guidance). reason explains why the hint was emitted. "
|
|
25
|
+
"Advisories (separate field) carry pure informational text with no tool call suggestion."
|
|
31
26
|
)
|
|
32
27
|
|
|
33
|
-
# ---
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
)
|
|
28
|
+
# --- Internal structured hint representation (no mcp_v2 import) ---
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _StructuredHint(NamedTuple):
|
|
32
|
+
tool: str
|
|
33
|
+
args: dict[str, Any]
|
|
34
|
+
actionable: bool
|
|
35
|
+
priority: int
|
|
36
|
+
label: str = ""
|
|
37
|
+
reason: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def finalize_structured_hints(scored: list[_StructuredHint]) -> list[_StructuredHint]:
|
|
41
|
+
"""Dedupe by ``(tool, json.dumps(args, sort_keys=True))``, keep highest priority, cap to 5."""
|
|
42
|
+
best: dict[tuple[str, str], tuple[int, int]] = {}
|
|
43
|
+
hints: dict[tuple[str, str], _StructuredHint] = {}
|
|
44
|
+
for idx, h in enumerate(scored):
|
|
45
|
+
key = (h.tool, json.dumps(h.args, sort_keys=True))
|
|
46
|
+
prev = best.get(key)
|
|
47
|
+
if prev is None or h.priority > prev[0]:
|
|
48
|
+
best[key] = (h.priority, idx)
|
|
49
|
+
hints[key] = h
|
|
50
|
+
elif h.priority == prev[0]:
|
|
51
|
+
best[key] = (h.priority, min(prev[1], idx))
|
|
52
|
+
ordered = sorted(best.items(), key=lambda kv: (-kv[1][0], kv[1][1]))
|
|
53
|
+
return [hints[k] for k, _ in ordered[:5]]
|
|
90
54
|
|
|
91
|
-
TPL_SEARCH_WEAK = "results look weak — narrow the query or try find(role=…)"
|
|
92
55
|
|
|
93
|
-
|
|
56
|
+
def finalize_advisories(scored: list[tuple[int, str]]) -> list[str]:
|
|
57
|
+
"""Dedupe identical strings keeping the highest priority; cap to 5 (drop lowest).
|
|
94
58
|
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
59
|
+
Within the same priority tier, keep advisories in emission order (first scored wins the cap).
|
|
60
|
+
"""
|
|
61
|
+
best: dict[str, tuple[int, int]] = {}
|
|
62
|
+
for idx, (pri, text) in enumerate(scored):
|
|
63
|
+
if not text:
|
|
64
|
+
continue
|
|
65
|
+
prev = best.get(text)
|
|
66
|
+
if prev is None or pri > prev[0]:
|
|
67
|
+
best[text] = (pri, idx)
|
|
68
|
+
elif pri == prev[0]:
|
|
69
|
+
best[text] = (pri, min(prev[1], idx))
|
|
70
|
+
ordered = sorted(best.items(), key=lambda kv: (-kv[1][0], kv[1][1]))
|
|
71
|
+
return [text for text, _pri in ordered[:5]]
|
|
107
72
|
|
|
108
|
-
_RESOLVE_HINT_MAX_CHARS = 120
|
|
109
|
-
_RESOLVE_WILDCARDS = ("*", "?")
|
|
110
73
|
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
|
|
74
|
+
_MEMBER_ONLY_DOT_KEY: dict[str, str] = {
|
|
75
|
+
"DECLARES_CLIENT": "DECLARES.DECLARES_CLIENT",
|
|
76
|
+
"DECLARES_PRODUCER": "DECLARES.DECLARES_PRODUCER",
|
|
77
|
+
"EXPOSES": "DECLARES.EXPOSES",
|
|
78
|
+
}
|
|
114
79
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"edge_filter={{exclude_callee_declaring_roles: ['ENTITY','DTO']}} instead of role exact match"
|
|
119
|
-
)
|
|
80
|
+
_CALLS_HIGH_FANOUT_THRESHOLD = 10
|
|
81
|
+
_EDGE_DECLARES_CLIENT = frozenset({"DECLARES_CLIENT", "DECLARES.DECLARES_CLIENT"})
|
|
82
|
+
_EDGE_DECLARES_PRODUCER = frozenset({"DECLARES_PRODUCER", "DECLARES.DECLARES_PRODUCER"})
|
|
120
83
|
|
|
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
84
|
|
|
127
|
-
_CALLS_HIGH_FANOUT_THRESHOLD = 10
|
|
128
85
|
|
|
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
86
|
|
|
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
87
|
|
|
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
88
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
89
|
+
def _extract_other_ids(results: list[dict[str, Any]]) -> list[str]:
|
|
90
|
+
ids: list[str] = []
|
|
91
|
+
for r in results:
|
|
92
|
+
other = r.get("other")
|
|
93
|
+
if isinstance(other, dict):
|
|
94
|
+
oid = other.get("id")
|
|
95
|
+
if isinstance(oid, str) and oid:
|
|
96
|
+
ids.append(oid)
|
|
97
|
+
return ids
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- Labels for structured hints (semantic name of each hint) ---
|
|
101
|
+
|
|
102
|
+
LABEL_CLIENTS_VIA_MEMBERS = "clients via members"
|
|
103
|
+
LABEL_ROUTES_VIA_MEMBERS = "routes via members"
|
|
104
|
+
LABEL_PRODUCERS_VIA_MEMBERS = "producers via members"
|
|
105
|
+
LABEL_OVERRIDERS = "overriders"
|
|
106
|
+
LABEL_CLIENTS_IN_OVERRIDERS = "clients in overriders"
|
|
107
|
+
LABEL_PRODUCERS_IN_OVERRIDERS = "producers in overriders"
|
|
108
|
+
LABEL_ROUTES_IN_OVERRIDERS = "routes in overriders"
|
|
109
|
+
LABEL_OUTBOUND_CLIENT = "outbound client"
|
|
110
|
+
LABEL_OUTBOUND_PRODUCER = "outbound producer"
|
|
111
|
+
LABEL_INBOUND_ROUTE = "inbound route"
|
|
112
|
+
LABEL_HIGH_FANOUT = "high fanout"
|
|
113
|
+
LABEL_DECLARING_METHOD = "declaring method"
|
|
114
|
+
LABEL_IMPLEMENTORS = "implementors"
|
|
115
|
+
LABEL_IMPLEMENTS = "implements"
|
|
116
|
+
LABEL_DEPENDENCIES = "dependencies"
|
|
117
|
+
LABEL_INJECTORS = "injectors"
|
|
118
|
+
LABEL_OUTBOUND_CALLS = "outbound calls"
|
|
119
|
+
LABEL_SUPER_DECLARATION = "super declaration"
|
|
120
|
+
LABEL_UNRESOLVED = "unresolved"
|
|
121
|
+
LABEL_TRY_RESOLVE = "try resolve"
|
|
122
|
+
LABEL_PAGE_FULL = "page full"
|
|
123
|
+
LABEL_HANDLER = "handler"
|
|
124
|
+
LABEL_HTTP_TARGETS = "HTTP targets"
|
|
125
|
+
LABEL_ASYNC_TARGETS = "async targets"
|
|
126
|
+
LABEL_WEAK_RESULTS = "weak results"
|
|
127
|
+
LABEL_TRY_SEARCH = "try search"
|
|
128
|
+
LABEL_TRY_FIND_ROUTE = "try find route"
|
|
129
|
+
LABEL_TRY_FIND_CLIENT = "try find client"
|
|
130
|
+
LABEL_TIGHTEN_IDENTIFIER = "tighten identifier"
|
|
131
|
+
LABEL_FUZZY_STRATEGY = "fuzzy strategy"
|
|
132
|
+
LABEL_CALLERS = "callers"
|
|
133
|
+
LABEL_WRONG_SUBJECT_KIND = "wrong subject kind"
|
|
134
|
+
LABEL_WRONG_DIRECTION = "wrong direction"
|
|
135
|
+
LABEL_TYPE_LEVEL_REQUERY = "type-level requery"
|
|
160
136
|
|
|
161
137
|
# §7.12 priority: DECLARES.* type rollups > OVERRIDDEN_BY.* > leaf follow-ups > meta.
|
|
162
138
|
PRIORITY_DECLARES_TYPE_ROLLUP = 4
|
|
@@ -189,6 +165,27 @@ def _out_count(edge_summary: dict[str, Any] | None, key: str) -> int:
|
|
|
189
165
|
return int(cell.get("out", 0) or 0)
|
|
190
166
|
|
|
191
167
|
|
|
168
|
+
def _in_count(edge_summary: dict[str, Any] | None, key: str) -> int:
|
|
169
|
+
if not edge_summary or key not in edge_summary:
|
|
170
|
+
return 0
|
|
171
|
+
cell = edge_summary[key]
|
|
172
|
+
if not isinstance(cell, dict):
|
|
173
|
+
return 0
|
|
174
|
+
return int(cell.get("in", 0) or 0)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _record_role(rec: dict[str, Any]) -> str:
|
|
178
|
+
return str((rec.get("data") or {}).get("role") or rec.get("role") or "")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _type_rollup_would_emit(edge_summary: dict[str, Any] | None) -> bool:
|
|
182
|
+
return (
|
|
183
|
+
_out_count(edge_summary, "DECLARES.DECLARES_CLIENT") > 0
|
|
184
|
+
or _out_count(edge_summary, "DECLARES.EXPOSES") > 0
|
|
185
|
+
or _out_count(edge_summary, "DECLARES.DECLARES_PRODUCER") > 0
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
192
189
|
def _symbol_declaration_kind(record: dict[str, Any]) -> str | None:
|
|
193
190
|
data = record.get("data")
|
|
194
191
|
if isinstance(data, dict):
|
|
@@ -229,102 +226,9 @@ def typical_traversal_for(
|
|
|
229
226
|
return template.format(id=subject_id, direction=direction, edge=edge)
|
|
230
227
|
|
|
231
228
|
|
|
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
229
|
|
|
318
|
-
return pairs
|
|
319
230
|
|
|
320
231
|
|
|
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
232
|
|
|
329
233
|
def _neighbors_success_subject_is_type(subject_record: dict[str, Any]) -> bool:
|
|
330
234
|
return (
|
|
@@ -356,73 +260,114 @@ def _neighbors_results_homogeneous(
|
|
|
356
260
|
return True
|
|
357
261
|
|
|
358
262
|
|
|
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
263
|
|
|
364
264
|
|
|
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
265
|
|
|
384
266
|
|
|
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
267
|
|
|
423
268
|
|
|
424
|
-
|
|
425
|
-
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _any_fuzzy_strategy(edges: list[dict[str, Any]]) -> bool:
|
|
272
|
+
for e in edges:
|
|
273
|
+
attrs = e.get("attrs") if isinstance(e.get("attrs"), dict) else {}
|
|
274
|
+
s = attrs.get("strategy") if isinstance(attrs, dict) else None
|
|
275
|
+
if isinstance(s, str) and s in FUZZY_STRATEGY_SET:
|
|
276
|
+
return True
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _find_has_identifier_shaped_filter(kind: str, flt: dict[str, Any]) -> bool:
|
|
281
|
+
for name in _IDENTIFIER_FILTER_FIELDS.get(kind, ()):
|
|
282
|
+
val = flt.get(name)
|
|
283
|
+
if val is None:
|
|
284
|
+
continue
|
|
285
|
+
if isinstance(val, str) and val.strip():
|
|
286
|
+
return True
|
|
287
|
+
if not isinstance(val, str):
|
|
288
|
+
return True
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _neighbors_empty_structured_hints(
|
|
295
|
+
*,
|
|
296
|
+
subject_record: dict[str, Any],
|
|
297
|
+
requested_edge_types: list[str],
|
|
298
|
+
requested_direction: Literal["in", "out"],
|
|
299
|
+
) -> list[_StructuredHint]:
|
|
300
|
+
"""Structured counterparts to neighbors empty results (Rows 1–3)."""
|
|
301
|
+
out: list[_StructuredHint] = []
|
|
302
|
+
subject_label = _subject_node_label(subject_record)
|
|
303
|
+
subject_id = str(subject_record.get("id") or "")
|
|
304
|
+
|
|
305
|
+
for edge in requested_edge_types:
|
|
306
|
+
spec = EDGE_SCHEMA.get(edge)
|
|
307
|
+
if spec is None:
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
# Row 1: wrong subject kind
|
|
311
|
+
if subject_label != spec.src and subject_label != spec.dst:
|
|
312
|
+
role = _traversal_role_for_wrong_kind(subject_label, subject_record)
|
|
313
|
+
if role != "alien_subject":
|
|
314
|
+
template = spec.typical_traversals.get(role, "")
|
|
315
|
+
# Parse template like "neighbors(['{id}'],'out',['DECLARES_CLIENT'])"
|
|
316
|
+
# Intentionally removed regex fallback — the schema-controlled format
|
|
317
|
+
# is stable; silence on format mismatch is acceptable for this low-risk
|
|
318
|
+
# requery hint (main users are search→neighbors divergences, not edge types).
|
|
319
|
+
parts = template.split("'")
|
|
320
|
+
if len(parts) >= 6:
|
|
321
|
+
direction = parts[3]
|
|
322
|
+
edge_type = parts[5].replace("']", "").replace("['", "")
|
|
323
|
+
out.append(_StructuredHint(
|
|
324
|
+
"neighbors",
|
|
325
|
+
{"ids": [subject_id], "direction": direction, "edge_types": [edge_type]},
|
|
326
|
+
False,
|
|
327
|
+
PRIORITY_META,
|
|
328
|
+
LABEL_WRONG_SUBJECT_KIND,
|
|
329
|
+
f"'{edge}' connects {spec.src} → {spec.dst}; this is a {subject_label}.",
|
|
330
|
+
))
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
# Row 2: wrong direction
|
|
334
|
+
wrong_direction = spec.src != spec.dst and (
|
|
335
|
+
(requested_direction == "out" and subject_label == spec.dst)
|
|
336
|
+
or (requested_direction == "in" and subject_label == spec.src)
|
|
337
|
+
)
|
|
338
|
+
if wrong_direction:
|
|
339
|
+
correct_dir = "in" if requested_direction == "out" else "out"
|
|
340
|
+
out.append(_StructuredHint(
|
|
341
|
+
"neighbors",
|
|
342
|
+
{"ids": [subject_id], "direction": correct_dir, "edge_types": [edge]},
|
|
343
|
+
False,
|
|
344
|
+
PRIORITY_META,
|
|
345
|
+
LABEL_WRONG_DIRECTION,
|
|
346
|
+
f"'{edge}' is {spec.src} → {spec.dst}; you requested direction='{requested_direction}'.",
|
|
347
|
+
))
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
# Row 3: type-level requery
|
|
351
|
+
if (
|
|
352
|
+
subject_label == "Symbol"
|
|
353
|
+
and str(subject_record.get("kind") or "") in _TYPE_SYMBOL_KINDS
|
|
354
|
+
and spec.member_only
|
|
355
|
+
):
|
|
356
|
+
dot_key = _MEMBER_ONLY_DOT_KEY.get(edge)
|
|
357
|
+
if dot_key:
|
|
358
|
+
out.append(_StructuredHint(
|
|
359
|
+
"neighbors",
|
|
360
|
+
{"ids": [subject_id], "direction": requested_direction, "edge_types": [dot_key]},
|
|
361
|
+
False,
|
|
362
|
+
PRIORITY_META,
|
|
363
|
+
LABEL_TYPE_LEVEL_REQUERY,
|
|
364
|
+
f"'{edge}' lives on methods, not on {subject_label}.",
|
|
365
|
+
))
|
|
366
|
+
return out
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _neighbors_success_structured_hints(payload: dict[str, Any]) -> list[_StructuredHint]:
|
|
370
|
+
"""Structured counterparts to ``neighbors_success_hints`` (N1a–N7)."""
|
|
426
371
|
if not payload.get("success"):
|
|
427
372
|
return []
|
|
428
373
|
results = list(payload.get("results") or [])
|
|
@@ -438,7 +383,7 @@ def neighbors_success_hints(payload: dict[str, Any]) -> list[tuple[int, str]]:
|
|
|
438
383
|
if direction not in ("in", "out"):
|
|
439
384
|
return []
|
|
440
385
|
|
|
441
|
-
|
|
386
|
+
out: list[_StructuredHint] = []
|
|
442
387
|
origin_id = str(payload.get("origin_id") or "")
|
|
443
388
|
if not origin_id:
|
|
444
389
|
origin_id = str(results[0].get("origin_id") or "")
|
|
@@ -447,6 +392,7 @@ def neighbors_success_hints(payload: dict[str, Any]) -> list[tuple[int, str]]:
|
|
|
447
392
|
isinstance(subject_record, dict) and _neighbors_success_subject_is_type(subject_record)
|
|
448
393
|
)
|
|
449
394
|
|
|
395
|
+
# N1a/N1b: DECLARES out from type → dot-key clients/routes
|
|
450
396
|
if (
|
|
451
397
|
edge == "DECLARES"
|
|
452
398
|
and direction == "out"
|
|
@@ -454,168 +400,174 @@ def neighbors_success_hints(payload: dict[str, Any]) -> list[tuple[int, str]]:
|
|
|
454
400
|
and _neighbors_results_homogeneous(results, symbol_kinds=_METHOD_SYMBOL_KINDS)
|
|
455
401
|
):
|
|
456
402
|
if origin_id:
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
403
|
+
out.append(_StructuredHint(
|
|
404
|
+
"neighbors",
|
|
405
|
+
{"ids": [origin_id], "direction": "out", "edge_types": ["DECLARES.DECLARES_CLIENT"]},
|
|
406
|
+
True,
|
|
407
|
+
PRIORITY_LEAF_FOLLOWUP,
|
|
408
|
+
LABEL_CLIENTS_VIA_MEMBERS,
|
|
409
|
+
"type has members with DECLARES_CLIENT edges",
|
|
410
|
+
))
|
|
411
|
+
out.append(_StructuredHint(
|
|
412
|
+
"neighbors",
|
|
413
|
+
{"ids": [origin_id], "direction": "out", "edge_types": ["DECLARES.EXPOSES"]},
|
|
414
|
+
True,
|
|
415
|
+
PRIORITY_LEAF_FOLLOWUP,
|
|
416
|
+
LABEL_ROUTES_VIA_MEMBERS,
|
|
417
|
+
"type has members with EXPOSES edges",
|
|
418
|
+
))
|
|
419
|
+
|
|
420
|
+
# N2: DECLARES_CLIENT / DECLARES.DECLARES_CLIENT out → HTTP_CALLS
|
|
464
421
|
if edge in _EDGE_DECLARES_CLIENT and direction == "out":
|
|
465
422
|
if _neighbors_results_homogeneous(results, endpoint_kind="client"):
|
|
466
|
-
|
|
467
|
-
|
|
423
|
+
other_ids = _extract_other_ids(results)
|
|
424
|
+
out.append(_StructuredHint(
|
|
425
|
+
"neighbors",
|
|
426
|
+
{"ids": other_ids, "direction": "out", "edge_types": ["HTTP_CALLS"]},
|
|
427
|
+
bool(other_ids),
|
|
428
|
+
PRIORITY_LEAF_FOLLOWUP,
|
|
429
|
+
LABEL_HTTP_TARGETS,
|
|
430
|
+
"clients have outbound HTTP_CALLS edges",
|
|
431
|
+
))
|
|
432
|
+
|
|
433
|
+
# N3: DECLARES_PRODUCER / DECLARES.DECLARES_PRODUCER out → ASYNC_CALLS
|
|
468
434
|
if edge in _EDGE_DECLARES_PRODUCER and direction == "out":
|
|
469
435
|
if _neighbors_results_homogeneous(results, endpoint_kind="producer"):
|
|
470
|
-
|
|
471
|
-
|
|
436
|
+
other_ids = _extract_other_ids(results)
|
|
437
|
+
out.append(_StructuredHint(
|
|
438
|
+
"neighbors",
|
|
439
|
+
{"ids": other_ids, "direction": "out", "edge_types": ["ASYNC_CALLS"]},
|
|
440
|
+
bool(other_ids),
|
|
441
|
+
PRIORITY_LEAF_FOLLOWUP,
|
|
442
|
+
LABEL_ASYNC_TARGETS,
|
|
443
|
+
"producers have outbound ASYNC_CALLS edges",
|
|
444
|
+
))
|
|
445
|
+
|
|
446
|
+
# N4: EXPOSES in → CALLS (callers)
|
|
472
447
|
if (
|
|
473
448
|
edge == "EXPOSES"
|
|
474
449
|
and direction == "in"
|
|
475
450
|
and _neighbors_results_homogeneous(results, symbol_kinds=_METHOD_SYMBOL_KINDS)
|
|
476
451
|
):
|
|
477
|
-
|
|
478
|
-
|
|
452
|
+
other_ids = _extract_other_ids(results)
|
|
453
|
+
out.append(_StructuredHint(
|
|
454
|
+
"neighbors",
|
|
455
|
+
{"ids": other_ids, "direction": "in", "edge_types": ["CALLS"]},
|
|
456
|
+
bool(other_ids),
|
|
457
|
+
PRIORITY_LEAF_FOLLOWUP,
|
|
458
|
+
LABEL_CALLERS,
|
|
459
|
+
"handler methods may have inbound CALLS edges",
|
|
460
|
+
))
|
|
461
|
+
|
|
462
|
+
# N5: HTTP_CALLS in → DECLARES_CLIENT
|
|
479
463
|
if edge == "HTTP_CALLS" and direction == "in":
|
|
480
464
|
if _neighbors_results_homogeneous(results, endpoint_kind="client"):
|
|
481
|
-
|
|
482
|
-
|
|
465
|
+
other_ids = _extract_other_ids(results)
|
|
466
|
+
out.append(_StructuredHint(
|
|
467
|
+
"neighbors",
|
|
468
|
+
{"ids": other_ids, "direction": "in", "edge_types": ["DECLARES_CLIENT"]},
|
|
469
|
+
bool(other_ids),
|
|
470
|
+
PRIORITY_LEAF_FOLLOWUP,
|
|
471
|
+
LABEL_DECLARING_METHOD,
|
|
472
|
+
"clients are declared on methods",
|
|
473
|
+
))
|
|
474
|
+
|
|
475
|
+
# N6: ASYNC_CALLS in → DECLARES_PRODUCER
|
|
483
476
|
if edge == "ASYNC_CALLS" and direction == "in":
|
|
484
477
|
if _neighbors_results_homogeneous(results, endpoint_kind="producer"):
|
|
485
|
-
|
|
486
|
-
|
|
478
|
+
other_ids = _extract_other_ids(results)
|
|
479
|
+
out.append(_StructuredHint(
|
|
480
|
+
"neighbors",
|
|
481
|
+
{"ids": other_ids, "direction": "in", "edge_types": ["DECLARES_PRODUCER"]},
|
|
482
|
+
bool(other_ids),
|
|
483
|
+
PRIORITY_LEAF_FOLLOWUP,
|
|
484
|
+
LABEL_DECLARING_METHOD,
|
|
485
|
+
"producers are declared on methods",
|
|
486
|
+
))
|
|
487
|
+
|
|
488
|
+
# N7: DECLARES.EXPOSES out → EXPOSES (handler)
|
|
487
489
|
if edge == "DECLARES.EXPOSES" and direction == "out":
|
|
488
490
|
if _neighbors_results_homogeneous(results, endpoint_kind="route"):
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
and len(results) >= int(lim)
|
|
499
|
-
and payload.get("has_more_results") is True
|
|
500
|
-
)
|
|
491
|
+
other_ids = _extract_other_ids(results)
|
|
492
|
+
out.append(_StructuredHint(
|
|
493
|
+
"neighbors",
|
|
494
|
+
{"ids": other_ids, "direction": "in", "edge_types": ["EXPOSES"]},
|
|
495
|
+
bool(other_ids),
|
|
496
|
+
PRIORITY_LEAF_FOLLOWUP,
|
|
497
|
+
LABEL_HANDLER,
|
|
498
|
+
"routes expose handler methods",
|
|
499
|
+
))
|
|
501
500
|
|
|
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]]
|
|
501
|
+
return out
|
|
566
502
|
|
|
567
503
|
|
|
568
504
|
def generate_hints(
|
|
569
505
|
output_kind: Literal["search", "find", "describe", "neighbors", "resolve"],
|
|
570
506
|
payload: dict[str, Any],
|
|
571
|
-
) -> list[str]:
|
|
572
|
-
"""Return
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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).
|
|
507
|
+
) -> tuple[list[_StructuredHint], list[str]]:
|
|
508
|
+
"""Return structured hints and advisories for a success-only MCP v2 payload dict.
|
|
509
|
+
|
|
510
|
+
Returns (structured_hints, advisories) where structured_hints are machine-parseable
|
|
511
|
+
next-action objects and advisories are pure informational strings.
|
|
580
512
|
"""
|
|
581
|
-
|
|
513
|
+
struct_pairs: list[_StructuredHint] = []
|
|
514
|
+
advisories: list[tuple[int, str]] = []
|
|
582
515
|
|
|
583
516
|
if output_kind == "resolve":
|
|
584
517
|
if payload.get("success") is False:
|
|
585
|
-
return []
|
|
518
|
+
return ([], [])
|
|
586
519
|
status = str(payload.get("status") or "")
|
|
587
520
|
if status == "one":
|
|
588
|
-
return []
|
|
521
|
+
return ([], [])
|
|
589
522
|
if status == "many":
|
|
590
523
|
n = len(payload.get("candidates") or [])
|
|
591
524
|
if n > 1:
|
|
592
|
-
|
|
593
|
-
|
|
525
|
+
advisories.append((PRIORITY_META, f"{n} candidates — tighten identifier or pick a candidate by id"))
|
|
526
|
+
struct_pairs.append(_StructuredHint(
|
|
527
|
+
"resolve",
|
|
528
|
+
{"identifier": str(payload.get("resolved_identifier") or ""), "hint_kind": str(payload.get("hint_kind") or "")},
|
|
529
|
+
False, PRIORITY_META,
|
|
530
|
+
LABEL_TIGHTEN_IDENTIFIER,
|
|
531
|
+
"multiple matches found for identifier",
|
|
532
|
+
))
|
|
533
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
594
534
|
if status == "none":
|
|
595
535
|
identifier = payload.get("resolved_identifier")
|
|
596
536
|
hint_kind = payload.get("hint_kind")
|
|
597
537
|
if not isinstance(identifier, str) or not identifier.strip():
|
|
598
|
-
return
|
|
599
|
-
if any(w in identifier for w in
|
|
600
|
-
return
|
|
601
|
-
rendered: str | None = None
|
|
538
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
539
|
+
if any(w in identifier for w in ("*", "?")):
|
|
540
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
602
541
|
if hint_kind == "route":
|
|
603
542
|
seed = payload.get("path_prefix_seed")
|
|
604
543
|
if isinstance(seed, str) and seed.strip():
|
|
605
|
-
|
|
544
|
+
advisories.append((PRIORITY_META, f"no match — try find(kind='route', filter={{path_prefix: '{seed}'}})"))
|
|
545
|
+
struct_pairs.append(_StructuredHint(
|
|
546
|
+
"find", {"kind": "route", "filter": {"path_prefix": seed}}, True, PRIORITY_META,
|
|
547
|
+
LABEL_TRY_FIND_ROUTE,
|
|
548
|
+
"no route found for path prefix seed",
|
|
549
|
+
))
|
|
606
550
|
elif hint_kind == "client":
|
|
607
551
|
seed = payload.get("target_service_seed")
|
|
608
552
|
if isinstance(seed, str) and seed.strip():
|
|
609
|
-
|
|
553
|
+
advisories.append((PRIORITY_META, f"no match — try find(kind='client', filter={{target_service: '{seed}'}})"))
|
|
554
|
+
struct_pairs.append(_StructuredHint(
|
|
555
|
+
"find", {"kind": "client", "filter": {"target_service": seed}}, True, PRIORITY_META,
|
|
556
|
+
LABEL_TRY_FIND_CLIENT,
|
|
557
|
+
"no client found for target service seed",
|
|
558
|
+
))
|
|
610
559
|
else:
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
560
|
+
advisories.append((PRIORITY_META, f"no match — try search(query='{identifier}') for ranked fuzzy lookup"))
|
|
561
|
+
struct_pairs.append(_StructuredHint(
|
|
562
|
+
"search", {"query": identifier}, True, PRIORITY_META,
|
|
563
|
+
LABEL_TRY_SEARCH,
|
|
564
|
+
"no exact match found for identifier",
|
|
565
|
+
))
|
|
566
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
567
|
+
return ([], [])
|
|
616
568
|
|
|
617
569
|
if not payload.get("success"):
|
|
618
|
-
return []
|
|
570
|
+
return ([], [])
|
|
619
571
|
|
|
620
572
|
if output_kind == "search":
|
|
621
573
|
results: list[dict[str, Any]] = list(payload.get("results") or [])
|
|
@@ -625,8 +577,13 @@ def generate_hints(
|
|
|
625
577
|
mx = max(scores)
|
|
626
578
|
mn = min(scores)
|
|
627
579
|
if mx > 0.0 and (mx - mn) < 0.1 * mx:
|
|
628
|
-
|
|
629
|
-
|
|
580
|
+
advisories.append((PRIORITY_META, "results look weak — narrow the query or try find(role=…)"))
|
|
581
|
+
struct_pairs.append(_StructuredHint(
|
|
582
|
+
"find", {"kind": "symbol", "filter": {"role": "SERVICE"}}, False, PRIORITY_META,
|
|
583
|
+
LABEL_WEAK_RESULTS,
|
|
584
|
+
"search results have low score variance",
|
|
585
|
+
))
|
|
586
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
630
587
|
|
|
631
588
|
if output_kind == "find":
|
|
632
589
|
kind = str(payload.get("kind") or "")
|
|
@@ -634,11 +591,50 @@ def generate_hints(
|
|
|
634
591
|
flt = payload.get("filter") if isinstance(payload.get("filter"), dict) else {}
|
|
635
592
|
lim = payload.get("limit")
|
|
636
593
|
if not results and _find_has_identifier_shaped_filter(kind, flt):
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
594
|
+
advisories.append((PRIORITY_META, f"no matches — try resolve(identifier, hint_kind='{kind}') for canonical lookup"))
|
|
595
|
+
identifier = ""
|
|
596
|
+
for fname in _IDENTIFIER_FILTER_FIELDS.get(kind, ()):
|
|
597
|
+
val = flt.get(fname)
|
|
598
|
+
if isinstance(val, str) and val.strip():
|
|
599
|
+
identifier = val.strip()
|
|
600
|
+
break
|
|
601
|
+
struct_pairs.append(_StructuredHint(
|
|
602
|
+
"resolve", {"identifier": identifier, "hint_kind": kind}, True, PRIORITY_META,
|
|
603
|
+
LABEL_TRY_RESOLVE,
|
|
604
|
+
f"no {kind} found with filter",
|
|
605
|
+
))
|
|
606
|
+
if lim is not None and len(results) >= int(lim) and payload.get("has_more_results") is True:
|
|
607
|
+
advisories.append((PRIORITY_META, f"result page full at {lim} — narrow filter or paginate"))
|
|
608
|
+
struct_pairs.append(_StructuredHint(
|
|
609
|
+
"find", {"kind": kind, "filter": flt, "limit": int(lim)}, False, PRIORITY_META,
|
|
610
|
+
LABEL_PAGE_FULL,
|
|
611
|
+
f"result page full at {lim}",
|
|
612
|
+
))
|
|
613
|
+
if results and (lim is None or len(results) < int(lim)):
|
|
614
|
+
node_id = str(results[0].get("id") or "")
|
|
615
|
+
if node_id:
|
|
616
|
+
if kind == "route":
|
|
617
|
+
struct_pairs.append(_StructuredHint(
|
|
618
|
+
"neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["EXPOSES"]},
|
|
619
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
620
|
+
LABEL_HANDLER,
|
|
621
|
+
"route exposes handler method",
|
|
622
|
+
))
|
|
623
|
+
elif kind == "client":
|
|
624
|
+
struct_pairs.append(_StructuredHint(
|
|
625
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["HTTP_CALLS"]},
|
|
626
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
627
|
+
LABEL_HTTP_TARGETS,
|
|
628
|
+
"client has outbound HTTP_CALLS edges",
|
|
629
|
+
))
|
|
630
|
+
elif kind == "producer":
|
|
631
|
+
struct_pairs.append(_StructuredHint(
|
|
632
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["ASYNC_CALLS"]},
|
|
633
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
634
|
+
LABEL_ASYNC_TARGETS,
|
|
635
|
+
"producer has outbound ASYNC_CALLS edges",
|
|
636
|
+
))
|
|
637
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
642
638
|
|
|
643
639
|
if output_kind == "neighbors":
|
|
644
640
|
results = list(payload.get("results") or [])
|
|
@@ -647,57 +643,142 @@ def generate_hints(
|
|
|
647
643
|
req_types = []
|
|
648
644
|
edge_labels = [str(x).strip() for x in req_types if str(x).strip()]
|
|
649
645
|
offset = int(payload.get("offset") or 0)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
646
|
+
subject_record = payload.get("subject_record")
|
|
647
|
+
requested_direction = payload.get("requested_direction")
|
|
648
|
+
struct_empty: list[_StructuredHint] = []
|
|
649
|
+
struct_success: list[_StructuredHint] = []
|
|
650
|
+
struct_meta: list[_StructuredHint] = []
|
|
653
651
|
if not results and edge_labels and offset == 0:
|
|
654
|
-
subject_record = payload.get("subject_record")
|
|
655
|
-
requested_direction = payload.get("requested_direction")
|
|
656
652
|
if (
|
|
657
653
|
isinstance(subject_record, dict)
|
|
658
654
|
and subject_record
|
|
659
655
|
and requested_direction in ("in", "out")
|
|
660
656
|
):
|
|
661
|
-
|
|
662
|
-
|
|
657
|
+
struct_empty.extend(
|
|
658
|
+
_neighbors_empty_structured_hints(
|
|
663
659
|
subject_record=subject_record,
|
|
664
660
|
requested_edge_types=edge_labels,
|
|
665
661
|
requested_direction=requested_direction,
|
|
666
662
|
)
|
|
667
663
|
)
|
|
664
|
+
# Brownfield absence advisory (Row 4)
|
|
665
|
+
subject_label = _subject_node_label(subject_record)
|
|
666
|
+
if subject_label in _BROWNFIELD_ABSENCE_SUBJECT_LABELS:
|
|
667
|
+
for edge in edge_labels:
|
|
668
|
+
spec = EDGE_SCHEMA.get(edge)
|
|
669
|
+
if spec is not None and spec.brownfield_resolver_sourced:
|
|
670
|
+
advisories.append((PRIORITY_META, f"edges on '{edge}' are emitted by the brownfield resolver — absence here may mean unresolved (no matching annotation/target), not absent from the codebase"))
|
|
671
|
+
break
|
|
668
672
|
elif results and offset == 0:
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
673
|
+
struct_success.extend(_neighbors_success_structured_hints(payload))
|
|
674
|
+
# CALLS-specific meta hints
|
|
675
|
+
if isinstance(req_types, list) and req_types == ["CALLS"]:
|
|
676
|
+
if not payload.get("edge_filter_provided") and not payload.get("include_unresolved"):
|
|
677
|
+
calls_n = int(payload.get("calls_row_count") or 0) or len(results)
|
|
678
|
+
if calls_n >= _CALLS_HIGH_FANOUT_THRESHOLD:
|
|
679
|
+
origin_id = str(payload.get("origin_id") or "")
|
|
680
|
+
if origin_id:
|
|
681
|
+
struct_meta.append(_StructuredHint(
|
|
682
|
+
"neighbors",
|
|
683
|
+
{"ids": [origin_id], "direction": "out", "edge_types": ["CALLS"], "edge_filter": {}},
|
|
684
|
+
False, PRIORITY_LEAF_FOLLOWUP,
|
|
685
|
+
LABEL_HIGH_FANOUT,
|
|
686
|
+
f"{calls_n} CALLS — noisy axes are callee_declaring_role and per-call-site multiplicity",
|
|
687
|
+
))
|
|
688
|
+
# Unresolved sites advisory
|
|
689
|
+
unresolved = int(payload.get("unresolved_count") or 0)
|
|
690
|
+
if unresolved > 0:
|
|
691
|
+
page_n = len(results)
|
|
692
|
+
# Advisory shortened to stay under 200 char cap (test_advisories_char_cap)
|
|
693
|
+
advisories.append((PRIORITY_LEAF_FOLLOWUP, f"{page_n} CALLS shown; {unresolved} unresolved — see describe().unresolved_call_sites or neighbors(include_unresolved=True)"))
|
|
694
|
+
# Fuzzy strategy advisory
|
|
695
|
+
if results and _any_fuzzy_strategy(results):
|
|
696
|
+
advisories.append((PRIORITY_META, "some edges resolved via brownfield/fallback strategy — check attrs.strategy on each row"))
|
|
697
|
+
# Role-filter OTHER fallback advisory
|
|
698
|
+
edge_flt = payload.get("edge_filter") if isinstance(payload.get("edge_filter"), dict) else {}
|
|
699
|
+
role_exact = edge_flt.get("callee_declaring_role")
|
|
700
|
+
if (
|
|
701
|
+
role_exact in ("SERVICE", "REPOSITORY")
|
|
702
|
+
and not results
|
|
703
|
+
and int(payload.get("unfiltered_calls_count") or 0) >= 5
|
|
704
|
+
):
|
|
705
|
+
advisories.append((PRIORITY_META, "0 CALLS matched callee_declaring_role filter but method has many callees — targets may be OTHER (interface/JDK); try edge_filter={{exclude_callee_declaring_roles: ['ENTITY','DTO']}} instead of role exact match"))
|
|
706
|
+
# NodeFilter.role collision advisory
|
|
707
|
+
node_flt = payload.get("node_filter") if isinstance(payload.get("node_filter"), dict) else {}
|
|
708
|
+
node_role = node_flt.get("role")
|
|
709
|
+
if node_role and results:
|
|
710
|
+
method_rows = [
|
|
711
|
+
r
|
|
712
|
+
for r in results
|
|
713
|
+
if str(((r.get("other") or {}) if isinstance(r.get("other"), dict) else {}).get("symbol_kind") or "")
|
|
714
|
+
== "method"
|
|
715
|
+
]
|
|
716
|
+
if method_rows:
|
|
717
|
+
other_roles = [
|
|
718
|
+
str(
|
|
719
|
+
((r.get("other") or {}) if isinstance(r.get("other"), dict) else {}).get("role")
|
|
720
|
+
or ""
|
|
721
|
+
)
|
|
722
|
+
for r in method_rows
|
|
723
|
+
]
|
|
724
|
+
if other_roles and sum(1 for role in other_roles if role == "OTHER") >= max(
|
|
725
|
+
1, (len(other_roles) * 3) // 4
|
|
726
|
+
):
|
|
727
|
+
advisories.append((PRIORITY_META, "NodeFilter.role filters the neighbor method's role (usually OTHER), not the callee's declaring type — use edge_filter={{callee_declaring_role: 'SERVICE'}} (or REPOSITORY) for CALLS stereotype projection"))
|
|
728
|
+
return (finalize_structured_hints(struct_empty + struct_success + struct_meta), finalize_advisories(advisories))
|
|
677
729
|
|
|
678
730
|
if output_kind == "describe":
|
|
679
731
|
rec = payload.get("record")
|
|
680
732
|
if not isinstance(rec, dict):
|
|
681
|
-
return []
|
|
733
|
+
return ([], [])
|
|
682
734
|
node_id = str(rec.get("id") or "")
|
|
683
735
|
if not node_id:
|
|
684
|
-
return []
|
|
736
|
+
return ([], [])
|
|
685
737
|
kind = str(rec.get("kind") or "")
|
|
686
738
|
es = rec.get("edge_summary")
|
|
687
739
|
edge_summary = es if isinstance(es, dict) else None
|
|
688
740
|
|
|
689
741
|
if kind == "route":
|
|
690
|
-
|
|
691
|
-
|
|
742
|
+
struct_pairs.append(_StructuredHint(
|
|
743
|
+
"neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["EXPOSES"]},
|
|
744
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
745
|
+
LABEL_DECLARING_METHOD,
|
|
746
|
+
"route exposes handler method",
|
|
747
|
+
))
|
|
748
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
692
749
|
if kind == "client":
|
|
693
|
-
|
|
694
|
-
|
|
750
|
+
struct_pairs.append(_StructuredHint(
|
|
751
|
+
"neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["DECLARES_CLIENT"]},
|
|
752
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
753
|
+
LABEL_DECLARING_METHOD,
|
|
754
|
+
"client is declared on a method",
|
|
755
|
+
))
|
|
756
|
+
if _out_count(edge_summary, "HTTP_CALLS") > 0:
|
|
757
|
+
struct_pairs.append(_StructuredHint(
|
|
758
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["HTTP_CALLS"]},
|
|
759
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
760
|
+
LABEL_HTTP_TARGETS,
|
|
761
|
+
"client has outbound HTTP_CALLS edges",
|
|
762
|
+
))
|
|
763
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
695
764
|
if kind == "producer":
|
|
696
|
-
|
|
697
|
-
|
|
765
|
+
struct_pairs.append(_StructuredHint(
|
|
766
|
+
"neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["DECLARES_PRODUCER"]},
|
|
767
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
768
|
+
LABEL_DECLARING_METHOD,
|
|
769
|
+
"producer is declared on a method",
|
|
770
|
+
))
|
|
771
|
+
if _out_count(edge_summary, "ASYNC_CALLS") > 0:
|
|
772
|
+
struct_pairs.append(_StructuredHint(
|
|
773
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["ASYNC_CALLS"]},
|
|
774
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
775
|
+
LABEL_ASYNC_TARGETS,
|
|
776
|
+
"producer has outbound ASYNC_CALLS edges",
|
|
777
|
+
))
|
|
778
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
698
779
|
|
|
699
780
|
if kind != "symbol":
|
|
700
|
-
return
|
|
781
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
701
782
|
|
|
702
783
|
decl_kind = _symbol_declaration_kind(rec)
|
|
703
784
|
is_type = decl_kind in _TYPE_SYMBOL_KINDS
|
|
@@ -705,44 +786,147 @@ def generate_hints(
|
|
|
705
786
|
|
|
706
787
|
if is_type:
|
|
707
788
|
if _out_count(edge_summary, "DECLARES.DECLARES_CLIENT") > 0:
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
789
|
+
struct_pairs.append(_StructuredHint(
|
|
790
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["DECLARES.DECLARES_CLIENT"]},
|
|
791
|
+
True, PRIORITY_DECLARES_TYPE_ROLLUP,
|
|
792
|
+
LABEL_CLIENTS_VIA_MEMBERS,
|
|
793
|
+
"type has members with DECLARES_CLIENT edges",
|
|
794
|
+
))
|
|
711
795
|
if _out_count(edge_summary, "DECLARES.EXPOSES") > 0:
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
796
|
+
struct_pairs.append(_StructuredHint(
|
|
797
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["DECLARES.EXPOSES"]},
|
|
798
|
+
True, PRIORITY_DECLARES_TYPE_ROLLUP,
|
|
799
|
+
LABEL_ROUTES_VIA_MEMBERS,
|
|
800
|
+
"type has members with EXPOSES edges",
|
|
801
|
+
))
|
|
715
802
|
if _out_count(edge_summary, "DECLARES.DECLARES_PRODUCER") > 0:
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
803
|
+
struct_pairs.append(_StructuredHint(
|
|
804
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["DECLARES.DECLARES_PRODUCER"]},
|
|
805
|
+
True, PRIORITY_DECLARES_TYPE_ROLLUP,
|
|
806
|
+
LABEL_PRODUCERS_VIA_MEMBERS,
|
|
807
|
+
"type has members with DECLARES_PRODUCER edges",
|
|
808
|
+
))
|
|
809
|
+
|
|
810
|
+
if not _type_rollup_would_emit(edge_summary):
|
|
811
|
+
if decl_kind == "interface" and _in_count(edge_summary, "IMPLEMENTS") > 0:
|
|
812
|
+
struct_pairs.append(_StructuredHint(
|
|
813
|
+
"neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["IMPLEMENTS"]},
|
|
814
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
815
|
+
LABEL_IMPLEMENTORS,
|
|
816
|
+
"interface is implemented by other types",
|
|
817
|
+
))
|
|
818
|
+
if decl_kind == "class" and _out_count(edge_summary, "IMPLEMENTS") > 0:
|
|
819
|
+
struct_pairs.append(_StructuredHint(
|
|
820
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["IMPLEMENTS"]},
|
|
821
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
822
|
+
LABEL_IMPLEMENTS,
|
|
823
|
+
"class implements other types",
|
|
824
|
+
))
|
|
825
|
+
if decl_kind == "class" and _record_role(rec) == "SERVICE" and _out_count(edge_summary, "INJECTS") > 0:
|
|
826
|
+
struct_pairs.append(_StructuredHint(
|
|
827
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["INJECTS"]},
|
|
828
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
829
|
+
LABEL_DEPENDENCIES,
|
|
830
|
+
"SERVICE injects other types",
|
|
831
|
+
))
|
|
832
|
+
if decl_kind in {"interface", "class"} and _in_count(edge_summary, "INJECTS") > 0:
|
|
833
|
+
struct_pairs.append(_StructuredHint(
|
|
834
|
+
"neighbors", {"ids": [node_id], "direction": "in", "edge_types": ["INJECTS"]},
|
|
835
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
836
|
+
LABEL_INJECTORS,
|
|
837
|
+
"type is injected by other types",
|
|
838
|
+
))
|
|
839
|
+
|
|
840
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
720
841
|
|
|
721
842
|
if is_method:
|
|
722
843
|
if _out_count(edge_summary, "OVERRIDDEN_BY") > 0:
|
|
723
|
-
|
|
844
|
+
struct_pairs.append(_StructuredHint(
|
|
845
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["OVERRIDDEN_BY"]},
|
|
846
|
+
True, PRIORITY_OVERRIDDEN_AXIS,
|
|
847
|
+
LABEL_OVERRIDERS,
|
|
848
|
+
"method is overridden by other methods",
|
|
849
|
+
))
|
|
724
850
|
if _out_count(edge_summary, "OVERRIDDEN_BY.DECLARES_CLIENT") > 0:
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
851
|
+
struct_pairs.append(_StructuredHint(
|
|
852
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["OVERRIDDEN_BY.DECLARES_CLIENT"]},
|
|
853
|
+
True, PRIORITY_OVERRIDDEN_AXIS,
|
|
854
|
+
LABEL_CLIENTS_IN_OVERRIDERS,
|
|
855
|
+
"overriding methods have DECLARES_CLIENT edges",
|
|
856
|
+
))
|
|
728
857
|
if _out_count(edge_summary, "OVERRIDDEN_BY.DECLARES_PRODUCER") > 0:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
858
|
+
struct_pairs.append(_StructuredHint(
|
|
859
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["OVERRIDDEN_BY.DECLARES_PRODUCER"]},
|
|
860
|
+
True, PRIORITY_OVERRIDDEN_AXIS,
|
|
861
|
+
LABEL_PRODUCERS_IN_OVERRIDERS,
|
|
862
|
+
"overriding methods have DECLARES_PRODUCER edges",
|
|
863
|
+
))
|
|
732
864
|
if _out_count(edge_summary, "OVERRIDDEN_BY.EXPOSES") > 0:
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
865
|
+
struct_pairs.append(_StructuredHint(
|
|
866
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["OVERRIDDEN_BY.EXPOSES"]},
|
|
867
|
+
True, PRIORITY_OVERRIDDEN_AXIS,
|
|
868
|
+
LABEL_ROUTES_IN_OVERRIDERS,
|
|
869
|
+
"overriding methods have EXPOSES edges",
|
|
870
|
+
))
|
|
736
871
|
if _out_count(edge_summary, "DECLARES_CLIENT") > 0:
|
|
737
|
-
|
|
872
|
+
struct_pairs.append(_StructuredHint(
|
|
873
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["DECLARES_CLIENT"]},
|
|
874
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
875
|
+
LABEL_OUTBOUND_CLIENT,
|
|
876
|
+
"method declares outbound HTTP client",
|
|
877
|
+
))
|
|
738
878
|
if _out_count(edge_summary, "DECLARES_PRODUCER") > 0:
|
|
739
|
-
|
|
879
|
+
struct_pairs.append(_StructuredHint(
|
|
880
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["DECLARES_PRODUCER"]},
|
|
881
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
882
|
+
LABEL_OUTBOUND_PRODUCER,
|
|
883
|
+
"method declares outbound async producer",
|
|
884
|
+
))
|
|
740
885
|
if _out_count(edge_summary, "EXPOSES") > 0:
|
|
741
|
-
|
|
886
|
+
struct_pairs.append(_StructuredHint(
|
|
887
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["EXPOSES"]},
|
|
888
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
889
|
+
LABEL_INBOUND_ROUTE,
|
|
890
|
+
"method is exposed as a route handler",
|
|
891
|
+
))
|
|
892
|
+
calls_out = _out_count(edge_summary, "CALLS")
|
|
893
|
+
if 1 <= calls_out <= 9:
|
|
894
|
+
method_role = _record_role(rec)
|
|
895
|
+
if method_role != "OTHER" or calls_out >= 3:
|
|
896
|
+
struct_pairs.append(_StructuredHint(
|
|
897
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["CALLS"]},
|
|
898
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
899
|
+
LABEL_OUTBOUND_CALLS,
|
|
900
|
+
"method has outbound CALLS edges",
|
|
901
|
+
))
|
|
902
|
+
if _out_count(edge_summary, "OVERRIDES") > 0:
|
|
903
|
+
if _out_count(edge_summary, "OVERRIDDEN_BY") == 0:
|
|
904
|
+
struct_pairs.append(_StructuredHint(
|
|
905
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["OVERRIDES"]},
|
|
906
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
907
|
+
LABEL_SUPER_DECLARATION,
|
|
908
|
+
"method overrides a super declaration",
|
|
909
|
+
))
|
|
910
|
+
data = rec.get("data")
|
|
911
|
+
unresolved = 0
|
|
912
|
+
if isinstance(data, dict):
|
|
913
|
+
unresolved = int(data.get("unresolved_call_sites_total") or 0)
|
|
914
|
+
if unresolved > 0:
|
|
915
|
+
struct_pairs.append(_StructuredHint(
|
|
916
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["CALLS"], "include_unresolved": True},
|
|
917
|
+
True, PRIORITY_LEAF_FOLLOWUP,
|
|
918
|
+
LABEL_UNRESOLVED,
|
|
919
|
+
f"method has {unresolved} unresolved call sites",
|
|
920
|
+
))
|
|
742
921
|
if _out_count(edge_summary, "CALLS") >= 10:
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
922
|
+
struct_pairs.append(_StructuredHint(
|
|
923
|
+
"neighbors", {"ids": [node_id], "direction": "out", "edge_types": ["CALLS"]},
|
|
924
|
+
False, PRIORITY_LEAF_FOLLOWUP,
|
|
925
|
+
LABEL_HIGH_FANOUT,
|
|
926
|
+
f"method has {_out_count(edge_summary, 'CALLS')} CALLS — consider filtering",
|
|
927
|
+
))
|
|
928
|
+
# Advisory for many CALLS
|
|
929
|
+
advisories.append((PRIORITY_LEAF_FOLLOWUP, "many CALLS — consider filtering by target microservice"))
|
|
930
|
+
return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
|
|
931
|
+
|
|
932
|
+
return ([], [])
|