|
21 | 21 | "Maximum 5 hints per output. Describe-time type rollup hints may recommend " |
22 | 22 | "DECLARES.* dot-keys for neighbors(); empty neighbors structural hints never use " |
23 | 23 | "dot-key edge labels. For neighbors with multiple origin ids, empty-result " |
24 | | - "structural hints describe the first origin only." |
| 24 | + "structural hints describe the first origin only. On neighbors with " |
| 25 | + "edge_types=['CALLS'], optional edge_filter projects the ordered CALLS stream " |
| 26 | + "(min_confidence, strategies, callee_declaring_role axes); include_unresolved and " |
| 27 | + "dedup_calls are separate knobs (mutually exclusive with edge_filter where noted)." |
25 | 28 | ) |
26 | 29 |
|
27 | 30 | # --- Appendix A verbatim templates (substitute {id}, {kind}, {limit}) --- |
|
109 | 112 | "some edges resolved via brownfield/fallback strategy — check attrs.strategy on each row" |
110 | 113 | ) |
111 | 114 |
|
| 115 | +TPL_NEIGHBORS_CALLS_ROLE_FILTER_OTHER_FALLBACK = ( |
| 116 | + "0 CALLS matched callee_declaring_role filter but method has many callees — " |
| 117 | + "targets may be OTHER (interface/JDK); try " |
| 118 | + "edge_filter={{exclude_callee_declaring_roles: ['ENTITY','DTO']}} instead of role exact match" |
| 119 | +) |
| 120 | + |
| 121 | +TPL_NEIGHBORS_CALLS_NODEFILTER_ROLE_COLLISION = ( |
| 122 | + "NodeFilter.role filters the neighbor method's role (usually OTHER), not the callee's " |
| 123 | + "declaring type — use edge_filter={{callee_declaring_role: 'SERVICE'}} (or REPOSITORY) " |
| 124 | + "for CALLS stereotype projection" |
| 125 | +) |
| 126 | + |
112 | 127 | # v4 neighbors success-path (propose/HINTS-V4-SUCCESS-PATH-PROPOSE.md); N1a/N1b alias describe templates. |
113 | 128 | TPL_NEIGHBORS_SUCCESS_HTTP_TARGETS = "HTTP targets: neighbors(client_ids,'out',['HTTP_CALLS'])" |
114 | 129 | TPL_NEIGHBORS_SUCCESS_ASYNC_TARGETS = "async targets: neighbors(producer_ids,'out',['ASYNC_CALLS'])" |
@@ -329,6 +344,45 @@ def _append_neighbors_success_hint(pairs: list[tuple[int, str]], text: str) -> N |
329 | 344 | pairs.append((PRIORITY_LEAF_FOLLOWUP, text)) |
330 | 345 |
|
331 | 346 |
|
| 347 | +def neighbors_calls_meta_hints(payload: dict[str, Any]) -> list[tuple[int, str]]: |
| 348 | + """CALLS-specific hints: role-filter OTHER fallback (Decision 20) and NodeFilter.role trap (30).""" |
| 349 | + pairs: list[tuple[int, str]] = [] |
| 350 | + req_types = payload.get("requested_edge_types") |
| 351 | + if not isinstance(req_types, list) or req_types != ["CALLS"]: |
| 352 | + return pairs |
| 353 | + results = list(payload.get("results") or []) |
| 354 | + edge_flt = payload.get("edge_filter") if isinstance(payload.get("edge_filter"), dict) else {} |
| 355 | + node_flt = payload.get("node_filter") if isinstance(payload.get("node_filter"), dict) else {} |
| 356 | + role_exact = edge_flt.get("callee_declaring_role") |
| 357 | + if ( |
| 358 | + role_exact in ("SERVICE", "REPOSITORY") |
| 359 | + and not results |
| 360 | + and int(payload.get("unfiltered_calls_count") or 0) >= 5 |
| 361 | + ): |
| 362 | + pairs.append((PRIORITY_META, TPL_NEIGHBORS_CALLS_ROLE_FILTER_OTHER_FALLBACK)) |
| 363 | + node_role = node_flt.get("role") |
| 364 | + if node_role and results: |
| 365 | + method_rows = [ |
| 366 | + r |
| 367 | + for r in results |
| 368 | + if str(((r.get("other") or {}) if isinstance(r.get("other"), dict) else {}).get("symbol_kind") or "") |
| 369 | + == "method" |
| 370 | + ] |
| 371 | + if method_rows: |
| 372 | + other_roles = [ |
| 373 | + str( |
| 374 | + ((r.get("other") or {}) if isinstance(r.get("other"), dict) else {}).get("role") |
| 375 | + or "" |
| 376 | + ) |
| 377 | + for r in method_rows |
| 378 | + ] |
| 379 | + if other_roles and sum(1 for role in other_roles if role == "OTHER") >= max( |
| 380 | + 1, (len(other_roles) * 3) // 4 |
| 381 | + ): |
| 382 | + pairs.append((PRIORITY_META, TPL_NEIGHBORS_CALLS_NODEFILTER_ROLE_COLLISION)) |
| 383 | + return pairs |
| 384 | + |
| 385 | + |
332 | 386 | def neighbors_success_hints(payload: dict[str, Any]) -> list[tuple[int, str]]: |
333 | 387 | """v4 non-empty neighbors follow-ups (N1a–N7); no graph I/O.""" |
334 | 388 | if not payload.get("success"): |
@@ -573,11 +627,11 @@ def generate_hints( |
573 | 627 | requested_direction=requested_direction, |
574 | 628 | ) |
575 | 629 | ) |
576 | | - else: |
577 | | - if results and offset == 0: |
578 | | - success_pairs = neighbors_success_hints(payload) |
579 | | - if _any_fuzzy_strategy(results): |
580 | | - meta_pairs.append((PRIORITY_META, TPL_NEIGHBORS_FUZZY_STRATEGY)) |
| 630 | + elif results and offset == 0: |
| 631 | + success_pairs = neighbors_success_hints(payload) |
| 632 | + meta_pairs.extend(neighbors_calls_meta_hints(payload)) |
| 633 | + if results and _any_fuzzy_strategy(results): |
| 634 | + meta_pairs.append((PRIORITY_META, TPL_NEIGHBORS_FUZZY_STRATEGY)) |
581 | 635 | return finalize_hint_list( |
582 | 636 | _filter_neighbors_dotkey_hints(empty_pairs) + success_pairs + meta_pairs, |
583 | 637 | ) |
|
0 commit comments