java-codebase-rag 0.12.0__py3-none-any.whl → 0.12.2__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.
Files changed (77) hide show
  1. java_codebase_rag-0.12.2.dist-info/METADATA +35 -0
  2. java_codebase_rag-0.12.2.dist-info/RECORD +4 -0
  3. {java_codebase_rag-0.12.0.dist-info → java_codebase_rag-0.12.2.dist-info}/WHEEL +1 -1
  4. java_codebase_rag/_deprecation.py +0 -103
  5. java_codebase_rag/_fdlimit.py +0 -56
  6. java_codebase_rag/_stdio.py +0 -32
  7. java_codebase_rag/_version.py +0 -35
  8. java_codebase_rag/absence/__init__.py +0 -0
  9. java_codebase_rag/absence/absence_diagnosis.py +0 -700
  10. java_codebase_rag/absence/absence_types.py +0 -124
  11. java_codebase_rag/absence/absence_vocab.py +0 -460
  12. java_codebase_rag/analysis/__init__.py +0 -0
  13. java_codebase_rag/analysis/pr_analysis.py +0 -563
  14. java_codebase_rag/analysis/resolve_service.py +0 -740
  15. java_codebase_rag/ast/__init__.py +0 -0
  16. java_codebase_rag/ast/ast_java.py +0 -2847
  17. java_codebase_rag/ast/ast_kotlin.py +0 -1794
  18. java_codebase_rag/ast/brownfield_events.py +0 -58
  19. java_codebase_rag/ast/chunk_heuristics.py +0 -83
  20. java_codebase_rag/ast/language.py +0 -117
  21. java_codebase_rag/cli.py +0 -1215
  22. java_codebase_rag/cli_dispatch.py +0 -251
  23. java_codebase_rag/cli_format.py +0 -85
  24. java_codebase_rag/cli_progress.py +0 -94
  25. java_codebase_rag/config.py +0 -833
  26. java_codebase_rag/eval/__init__.py +0 -1
  27. java_codebase_rag/eval/ground_truth.py +0 -100
  28. java_codebase_rag/eval/metrics.py +0 -107
  29. java_codebase_rag/eval/runner.py +0 -556
  30. java_codebase_rag/graph/__init__.py +0 -0
  31. java_codebase_rag/graph/build_ast_graph.py +0 -4593
  32. java_codebase_rag/graph/graph_enrich.py +0 -1940
  33. java_codebase_rag/graph/graph_types.py +0 -224
  34. java_codebase_rag/graph/java_ontology.py +0 -465
  35. java_codebase_rag/graph/ladybug_queries.py +0 -2213
  36. java_codebase_rag/graph/path_filtering.py +0 -509
  37. java_codebase_rag/index/__init__.py +0 -0
  38. java_codebase_rag/index/java_index_flow_lancedb.py +0 -879
  39. java_codebase_rag/index/java_index_v1_common.py +0 -33
  40. java_codebase_rag/install_data/__init__.py +0 -0
  41. java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -110
  42. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
  43. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
  44. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
  45. java_codebase_rag/installer.py +0 -2188
  46. java_codebase_rag/jrag.py +0 -4545
  47. java_codebase_rag/jrag_envelope.py +0 -1107
  48. java_codebase_rag/jrag_hints.py +0 -204
  49. java_codebase_rag/jrag_render.py +0 -926
  50. java_codebase_rag/lance_optimize.py +0 -264
  51. java_codebase_rag/mcp/__init__.py +0 -0
  52. java_codebase_rag/mcp/mcp_hints.py +0 -932
  53. java_codebase_rag/mcp/mcp_v2.py +0 -1916
  54. java_codebase_rag/mcp/server.py +0 -886
  55. java_codebase_rag/pipeline.py +0 -531
  56. java_codebase_rag/progress.py +0 -570
  57. java_codebase_rag/read_payloads.py +0 -781
  58. java_codebase_rag/search/__init__.py +0 -0
  59. java_codebase_rag/search/index_common.py +0 -10
  60. java_codebase_rag/search/search_lancedb.py +0 -1296
  61. java_codebase_rag/search/search_lexical.py +0 -449
  62. java_codebase_rag/search/search_scoring.py +0 -537
  63. java_codebase_rag/watch/__init__.py +0 -0
  64. java_codebase_rag/watch/client.py +0 -230
  65. java_codebase_rag/watch/daemon.py +0 -396
  66. java_codebase_rag/watch/lock.py +0 -201
  67. java_codebase_rag/watch/paths.py +0 -76
  68. java_codebase_rag/watch/protocol.py +0 -122
  69. java_codebase_rag/watch/server.py +0 -273
  70. java_codebase_rag/watch/warm.py +0 -105
  71. java_codebase_rag/watch/watcher.py +0 -394
  72. java_codebase_rag-0.12.0.dist-info/METADATA +0 -340
  73. java_codebase_rag-0.12.0.dist-info/RECORD +0 -75
  74. java_codebase_rag-0.12.0.dist-info/entry_points.txt +0 -5
  75. java_codebase_rag-0.12.0.dist-info/licenses/LICENSE +0 -21
  76. java_codebase_rag-0.12.0.dist-info/top_level.txt +0 -1
  77. /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.2.dist-info/top_level.txt +0 -0
@@ -1,932 +0,0 @@
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
- import json
14
- from typing import Any, Literal, NamedTuple
15
-
16
- from java_codebase_rag.graph.java_ontology import EDGE_SCHEMA, FUZZY_STRATEGY_SET
17
-
18
- # Normative schema description (propose §3.1) — imported by ``mcp_v2`` for Field(description=...).
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."
26
- )
27
-
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]]
54
-
55
-
56
- def finalize_advisories(scored: list[tuple[int, str]]) -> list[str]:
57
- """Dedupe identical strings keeping the highest priority; cap to 5 (drop lowest).
58
-
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]]
72
-
73
-
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
- }
79
-
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"})
83
-
84
-
85
-
86
-
87
-
88
-
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"
136
-
137
- # §7.12 priority: DECLARES.* type rollups > OVERRIDDEN_BY.* > leaf follow-ups > meta.
138
- PRIORITY_DECLARES_TYPE_ROLLUP = 4
139
- PRIORITY_OVERRIDDEN_AXIS = 3
140
- PRIORITY_LEAF_FOLLOWUP = 2
141
- PRIORITY_META = 1
142
-
143
- _TYPE_SYMBOL_KINDS = frozenset({"class", "interface", "enum", "record", "annotation"})
144
- _METHOD_SYMBOL_KINDS = frozenset({"method", "constructor"})
145
-
146
- _COMPOSED_DOT_KEY_PREFIXES = ("DECLARES.", "OVERRIDDEN_BY.")
147
- # Row 4 (brownfield absence): only when the subject is a resolver endpoint node, not a
148
- # structurally valid Symbol query that happens to be empty (DECLARES_CLIENT, EXPOSES, …).
149
- _BROWNFIELD_ABSENCE_SUBJECT_LABELS = frozenset({"Client", "Producer", "Route"})
150
- _REQUIRED_TRAVERSAL_ROLE_KEYS = frozenset({"type_subject", "member_subject", "alien_subject"})
151
-
152
- _IDENTIFIER_FILTER_FIELDS: dict[str, tuple[str, ...]] = {
153
- "symbol": ("fqn_contains",),
154
- "route": ("path_contains",),
155
- "client": ("target_service", "target_path_contains"),
156
- }
157
-
158
-
159
- def _out_count(edge_summary: dict[str, Any] | None, key: str) -> int:
160
- if not edge_summary or key not in edge_summary:
161
- return 0
162
- cell = edge_summary[key]
163
- if not isinstance(cell, dict):
164
- return 0
165
- return int(cell.get("out", 0) or 0)
166
-
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
-
189
- def _symbol_declaration_kind(record: dict[str, Any]) -> str | None:
190
- data = record.get("data")
191
- if isinstance(data, dict):
192
- k = data.get("kind")
193
- if k is not None:
194
- return str(k).strip() or None
195
- return None
196
-
197
-
198
- def _subject_node_label(subject_record: dict[str, Any]) -> str:
199
- if "producer_kind" in subject_record:
200
- return "Producer"
201
- if "client_kind" in subject_record:
202
- return "Client"
203
- if "framework" in subject_record:
204
- return "Route"
205
- return "Symbol"
206
-
207
-
208
- def _traversal_role_for_wrong_kind(subject_label: str, subject_record: dict[str, Any]) -> str:
209
- if subject_label == "Symbol":
210
- sk = str(subject_record.get("kind") or "")
211
- if sk in _METHOD_SYMBOL_KINDS:
212
- return "member_subject"
213
- if sk in _TYPE_SYMBOL_KINDS:
214
- return "alien_subject"
215
- return "alien_subject"
216
-
217
-
218
- def typical_traversal_for(
219
- edge: str,
220
- role_key: str,
221
- *,
222
- subject_id: str,
223
- direction: str,
224
- ) -> str:
225
- template = EDGE_SCHEMA[edge].typical_traversals[role_key]
226
- return template.format(id=subject_id, direction=direction, edge=edge)
227
-
228
-
229
-
230
-
231
-
232
-
233
- def _neighbors_success_subject_is_type(subject_record: dict[str, Any]) -> bool:
234
- return (
235
- _subject_node_label(subject_record) == "Symbol"
236
- and str(subject_record.get("kind") or "") in _TYPE_SYMBOL_KINDS
237
- )
238
-
239
-
240
- def _neighbors_results_homogeneous(
241
- results: list[dict[str, Any]],
242
- *,
243
- endpoint_kind: str | None = None,
244
- symbol_kinds: frozenset[str] | None = None,
245
- ) -> bool:
246
- if not results:
247
- return False
248
- for row in results:
249
- other = row.get("other")
250
- if not isinstance(other, dict):
251
- return False
252
- ok = str(other.get("kind") or "")
253
- if endpoint_kind is not None and ok != endpoint_kind:
254
- return False
255
- if symbol_kinds is not None:
256
- if ok != "symbol":
257
- return False
258
- if str(other.get("symbol_kind") or "") not in symbol_kinds:
259
- return False
260
- return True
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
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)."""
371
- if not payload.get("success"):
372
- return []
373
- results = list(payload.get("results") or [])
374
- if not results or int(payload.get("offset") or 0) != 0:
375
- return []
376
- req_types = payload.get("requested_edge_types")
377
- if not isinstance(req_types, list) or len(req_types) != 1:
378
- return []
379
- edge = str(req_types[0]).strip()
380
- if not edge:
381
- return []
382
- direction = payload.get("requested_direction")
383
- if direction not in ("in", "out"):
384
- return []
385
-
386
- out: list[_StructuredHint] = []
387
- origin_id = str(payload.get("origin_id") or "")
388
- if not origin_id:
389
- origin_id = str(results[0].get("origin_id") or "")
390
- subject_record = payload.get("subject_record")
391
- is_type_subject = (
392
- isinstance(subject_record, dict) and _neighbors_success_subject_is_type(subject_record)
393
- )
394
-
395
- # N1a/N1b: DECLARES out from type → dot-key clients/routes
396
- if (
397
- edge == "DECLARES"
398
- and direction == "out"
399
- and is_type_subject
400
- and _neighbors_results_homogeneous(results, symbol_kinds=_METHOD_SYMBOL_KINDS)
401
- ):
402
- if origin_id:
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
421
- if edge in _EDGE_DECLARES_CLIENT and direction == "out":
422
- if _neighbors_results_homogeneous(results, endpoint_kind="client"):
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
434
- if edge in _EDGE_DECLARES_PRODUCER and direction == "out":
435
- if _neighbors_results_homogeneous(results, endpoint_kind="producer"):
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)
447
- if (
448
- edge == "EXPOSES"
449
- and direction == "in"
450
- and _neighbors_results_homogeneous(results, symbol_kinds=_METHOD_SYMBOL_KINDS)
451
- ):
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
463
- if edge == "HTTP_CALLS" and direction == "in":
464
- if _neighbors_results_homogeneous(results, endpoint_kind="client"):
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
476
- if edge == "ASYNC_CALLS" and direction == "in":
477
- if _neighbors_results_homogeneous(results, endpoint_kind="producer"):
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)
489
- if edge == "DECLARES.EXPOSES" and direction == "out":
490
- if _neighbors_results_homogeneous(results, endpoint_kind="route"):
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
- ))
500
-
501
- return out
502
-
503
-
504
- def generate_hints(
505
- output_kind: Literal["search", "find", "describe", "neighbors", "resolve"],
506
- payload: dict[str, Any],
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.
512
- """
513
- struct_pairs: list[_StructuredHint] = []
514
- advisories: list[tuple[int, str]] = []
515
-
516
- if output_kind == "resolve":
517
- if payload.get("success") is False:
518
- return ([], [])
519
- status = str(payload.get("status") or "")
520
- if status == "one":
521
- return ([], [])
522
- if status == "many":
523
- n = len(payload.get("candidates") or [])
524
- if n > 1:
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))
534
- if status == "none":
535
- identifier = payload.get("resolved_identifier")
536
- hint_kind = payload.get("hint_kind")
537
- if not isinstance(identifier, str) or not identifier.strip():
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))
541
- if hint_kind == "route":
542
- seed = payload.get("path_prefix_seed")
543
- if isinstance(seed, str) and seed.strip():
544
- advisories.append((PRIORITY_META, f"no match — try find(kind='route', filter={{path_contains: '{seed}'}})"))
545
- struct_pairs.append(_StructuredHint(
546
- "find", {"kind": "route", "filter": {"path_contains": seed}}, True, PRIORITY_META,
547
- LABEL_TRY_FIND_ROUTE,
548
- "no route found for path seed",
549
- ))
550
- elif hint_kind == "client":
551
- seed = payload.get("target_service_seed")
552
- if isinstance(seed, str) and seed.strip():
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
- ))
559
- else:
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 ([], [])
568
-
569
- if not payload.get("success"):
570
- return ([], [])
571
-
572
- if output_kind == "search":
573
- results: list[dict[str, Any]] = list(payload.get("results") or [])
574
- lim = payload.get("limit")
575
- if lim is not None and len(results) == int(lim) and results:
576
- scores = [float(r.get("score", 0.0) or 0.0) for r in results]
577
- mx = max(scores)
578
- mn = min(scores)
579
- if mx > 0.0 and (mx - mn) < 0.1 * mx:
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))
587
-
588
- if output_kind == "find":
589
- kind = str(payload.get("kind") or "")
590
- results = list(payload.get("results") or [])
591
- flt = payload.get("filter") if isinstance(payload.get("filter"), dict) else {}
592
- lim = payload.get("limit")
593
- if not results and _find_has_identifier_shaped_filter(kind, flt):
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))
638
-
639
- if output_kind == "neighbors":
640
- results = list(payload.get("results") or [])
641
- req_types = payload.get("requested_edge_types")
642
- if not isinstance(req_types, list):
643
- req_types = []
644
- edge_labels = [str(x).strip() for x in req_types if str(x).strip()]
645
- offset = int(payload.get("offset") or 0)
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] = []
651
- if not results and edge_labels and offset == 0:
652
- if (
653
- isinstance(subject_record, dict)
654
- and subject_record
655
- and requested_direction in ("in", "out")
656
- ):
657
- struct_empty.extend(
658
- _neighbors_empty_structured_hints(
659
- subject_record=subject_record,
660
- requested_edge_types=edge_labels,
661
- requested_direction=requested_direction,
662
- )
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
672
- elif results and offset == 0:
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))
729
-
730
- if output_kind == "describe":
731
- rec = payload.get("record")
732
- if not isinstance(rec, dict):
733
- return ([], [])
734
- node_id = str(rec.get("id") or "")
735
- if not node_id:
736
- return ([], [])
737
- kind = str(rec.get("kind") or "")
738
- es = rec.get("edge_summary")
739
- edge_summary = es if isinstance(es, dict) else None
740
-
741
- if kind == "route":
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))
749
- if kind == "client":
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))
764
- if kind == "producer":
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))
779
-
780
- if kind != "symbol":
781
- return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))
782
-
783
- decl_kind = _symbol_declaration_kind(rec)
784
- is_type = decl_kind in _TYPE_SYMBOL_KINDS
785
- is_method = decl_kind in _METHOD_SYMBOL_KINDS
786
-
787
- if is_type:
788
- if _out_count(edge_summary, "DECLARES.DECLARES_CLIENT") > 0:
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
- ))
795
- if _out_count(edge_summary, "DECLARES.EXPOSES") > 0:
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
- ))
802
- if _out_count(edge_summary, "DECLARES.DECLARES_PRODUCER") > 0:
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))
841
-
842
- if is_method:
843
- if _out_count(edge_summary, "OVERRIDDEN_BY") > 0:
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
- ))
850
- if _out_count(edge_summary, "OVERRIDDEN_BY.DECLARES_CLIENT") > 0:
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
- ))
857
- if _out_count(edge_summary, "OVERRIDDEN_BY.DECLARES_PRODUCER") > 0:
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
- ))
864
- if _out_count(edge_summary, "OVERRIDDEN_BY.EXPOSES") > 0:
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
- ))
871
- if _out_count(edge_summary, "DECLARES_CLIENT") > 0:
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
- ))
878
- if _out_count(edge_summary, "DECLARES_PRODUCER") > 0:
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
- ))
885
- if _out_count(edge_summary, "EXPOSES") > 0:
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
- ))
921
- if _out_count(edge_summary, "CALLS") >= 10:
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 ([], [])