java-codebase-rag 0.11.2__py3-none-any.whl → 0.12.1__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 (73) hide show
  1. java_codebase_rag-0.12.1.dist-info/METADATA +35 -0
  2. java_codebase_rag-0.12.1.dist-info/RECORD +4 -0
  3. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.1.dist-info}/WHEEL +1 -1
  4. java_codebase_rag/_fdlimit.py +0 -56
  5. java_codebase_rag/_stdio.py +0 -32
  6. java_codebase_rag/_version.py +0 -35
  7. java_codebase_rag/absence/__init__.py +0 -0
  8. java_codebase_rag/absence/absence_diagnosis.py +0 -700
  9. java_codebase_rag/absence/absence_types.py +0 -124
  10. java_codebase_rag/absence/absence_vocab.py +0 -460
  11. java_codebase_rag/analysis/__init__.py +0 -0
  12. java_codebase_rag/analysis/pr_analysis.py +0 -563
  13. java_codebase_rag/analysis/resolve_service.py +0 -740
  14. java_codebase_rag/ast/__init__.py +0 -0
  15. java_codebase_rag/ast/ast_java.py +0 -2825
  16. java_codebase_rag/ast/brownfield_events.py +0 -58
  17. java_codebase_rag/ast/chunk_heuristics.py +0 -62
  18. java_codebase_rag/cli.py +0 -1215
  19. java_codebase_rag/cli_format.py +0 -85
  20. java_codebase_rag/cli_progress.py +0 -94
  21. java_codebase_rag/config.py +0 -833
  22. java_codebase_rag/eval/__init__.py +0 -1
  23. java_codebase_rag/eval/ground_truth.py +0 -100
  24. java_codebase_rag/eval/metrics.py +0 -107
  25. java_codebase_rag/eval/runner.py +0 -556
  26. java_codebase_rag/graph/__init__.py +0 -0
  27. java_codebase_rag/graph/build_ast_graph.py +0 -4471
  28. java_codebase_rag/graph/graph_enrich.py +0 -1937
  29. java_codebase_rag/graph/graph_types.py +0 -224
  30. java_codebase_rag/graph/java_ontology.py +0 -465
  31. java_codebase_rag/graph/ladybug_queries.py +0 -2213
  32. java_codebase_rag/graph/path_filtering.py +0 -477
  33. java_codebase_rag/index/__init__.py +0 -0
  34. java_codebase_rag/index/java_index_flow_lancedb.py +0 -734
  35. java_codebase_rag/index/java_index_v1_common.py +0 -33
  36. java_codebase_rag/install_data/__init__.py +0 -0
  37. java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -108
  38. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
  39. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
  40. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
  41. java_codebase_rag/installer.py +0 -2188
  42. java_codebase_rag/jrag.py +0 -4531
  43. java_codebase_rag/jrag_envelope.py +0 -1107
  44. java_codebase_rag/jrag_hints.py +0 -204
  45. java_codebase_rag/jrag_render.py +0 -926
  46. java_codebase_rag/lance_optimize.py +0 -264
  47. java_codebase_rag/mcp/__init__.py +0 -0
  48. java_codebase_rag/mcp/mcp_hints.py +0 -932
  49. java_codebase_rag/mcp/mcp_v2.py +0 -1916
  50. java_codebase_rag/mcp/server.py +0 -884
  51. java_codebase_rag/pipeline.py +0 -531
  52. java_codebase_rag/progress.py +0 -570
  53. java_codebase_rag/read_payloads.py +0 -781
  54. java_codebase_rag/search/__init__.py +0 -0
  55. java_codebase_rag/search/index_common.py +0 -10
  56. java_codebase_rag/search/search_lancedb.py +0 -1296
  57. java_codebase_rag/search/search_lexical.py +0 -449
  58. java_codebase_rag/search/search_scoring.py +0 -523
  59. java_codebase_rag/watch/__init__.py +0 -0
  60. java_codebase_rag/watch/client.py +0 -230
  61. java_codebase_rag/watch/daemon.py +0 -396
  62. java_codebase_rag/watch/lock.py +0 -201
  63. java_codebase_rag/watch/paths.py +0 -76
  64. java_codebase_rag/watch/protocol.py +0 -122
  65. java_codebase_rag/watch/server.py +0 -273
  66. java_codebase_rag/watch/warm.py +0 -105
  67. java_codebase_rag/watch/watcher.py +0 -370
  68. java_codebase_rag-0.11.2.dist-info/METADATA +0 -331
  69. java_codebase_rag-0.11.2.dist-info/RECORD +0 -71
  70. java_codebase_rag-0.11.2.dist-info/entry_points.txt +0 -4
  71. java_codebase_rag-0.11.2.dist-info/licenses/LICENSE +0 -21
  72. java_codebase_rag-0.11.2.dist-info/top_level.txt +0 -1
  73. /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.1.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 ([], [])