Skip to content

propose: CALLS noise + resolution — resolved-only edges + EdgeFilter#178

Merged
HumanBean17 merged 4 commits into
masterfrom
propose/calls-noise
May 19, 2026
Merged

propose: CALLS noise + resolution — resolved-only edges + EdgeFilter#178
HumanBean17 merged 4 commits into
masterfrom
propose/calls-noise

Conversation

@HumanBean17
Copy link
Copy Markdown
Owner

Status: draft
Tracks: #177

Adds propose/CALLS-NOISE-AND-RESOLUTION-PROPOSE.md.

Frame

CALLS is a sequence (ordered by call_site_line, call_site_byte), not a set. Source-order traversal is the dominant agent use case and must be preserved. Today's noise (~80% in the #177 sample) comes from two unrelated burdens being carried on one edge:

  1. Phantom + chained-receiver edges encoding "the resolver gave up here" — they point at synthetic phantom Symbols the agent has no traversal use for.
  2. Resolved edges to accessor / repository / delegation targets — these have legitimate sequential meaning but cannot be projected by callee role without a filter knob.

Two locked moves

  1. CALLS carries only resolved invocations. Phantom + chained + name-only-zero-candidates sites move to a sibling node table UnresolvedCallSite + UNRESOLVED_AT edge from the caller Symbol. Phantom Symbol rows + _phantom_method_id + tables.phantoms deleted. Re-index required; ontology v14 → v15.
  2. callee_declaring_role becomes a CALLS edge attribute, populated at pass3_calls emission time from the callee parent's role. neighbors_v2 gains a typed EdgeFilter Pydantic model (min_confidence, exclude_strategies, include_strategies, callee_declaring_role, callee_declaring_roles, exclude_callee_declaring_roles). Default ordering by (call_site_line, call_site_byte) is unchanged when filter is omitted.

Why not a semantic split

A CALLSDELEGATES_TO/PERSISTS_VIA/ACCESSES_STATE/UNRESOLVED_CALL split was the first reframe I tried — it fails the test "edge type should encode semantic intent only when the agent never needs to interleave edges of different types in a single traversal." Reading a method body is exactly the interleaved case; a split would force 4-way fan-merge on (line, byte). Recorded as locked Decision 6.

Three sub-PRs (sequential)

  • PR-1 — resolved-only CALLS + UnresolvedCallSite + UNRESOLVED_AT + describe.unresolved_call_sites rollup. Ontology bump. Re-index.
  • PR-2EdgeFilter on neighbors_v2 + pc unresolved-calls CLI subcommand + explicit supersession of the MCP-V2 "no per-edge filter" design rule.
  • PR-3dedup_calls on neighbors_v2 (default off) + TPL_NEIGHBORS_CALLS_HIGH_FANOUT HINTS-V4 success-path template (threshold = 10).

19 locked decisions

Including: no semantic split; resolved BOOLEAN removed from CALLS DDL; UnresolvedCallSite is a sibling node table (not a JSON column); callee_declaring_role is CALLS-specific (no symmetry on HTTP_CALLS/ASYNC_CALLS); find_callers does not gain callee_declaring_role filtering; dedup_calls=False by default; high-fanout template threshold = 10; no back-compat alias for the removed resolved=False rows.

20-row use-case re-walk

Covers ordered transcript (HV1, HV2), filter projections (HV3, HV4, HV5), find_callers semantics (HV6, HV17), trace_request_flow step (HV7), graph-quality CLI / describe rollup (HV8, HV9), HINTS-V3 interaction (HV10), edge-attribute filter semantics (HV11, HV12, HV13, HV14), dedup (HV15), success-path hint (HV16), 100%-unresolved edge case (HV18), CI invariants (HV19, HV20).

Files

  • propose/CALLS-NOISE-AND-RESOLUTION-PROPOSE.md

Drafted per the propose-doc-author skill structure. Will move Status to under review after first review pass.

Draft propose addressing #177.

Two locked moves, no edge-type explosion:

1. CALLS carries only resolved invocations. Phantom + chained-receiver
   sites move to caller-side UnresolvedCallSite node + UNRESOLVED_AT
   edge. Phantom Symbol rows deleted from the graph.

2. callee_declaring_role becomes a CALLS edge attribute. neighbors_v2
   gains a typed EdgeFilter surface (min_confidence, exclude_strategies,
   callee_declaring_role, ...) that projects the ordered stream by
   attribute without breaking (call_site_line, call_site_byte) order.

3 sequential code PRs (PR-1 resolved-only CALLS + ontology v15; PR-2
EdgeFilter + CLI; PR-3 dedup + HINTS-V4 high-fanout template).

19 locked decisions. 20-row use-case re-walk. Explicitly supersedes the
MCP-V2 'no per-edge filter on neighbors' design rule.
…, Decisions 20-33

Round-1 author grill amendments:
- Add §4.5 'Pre-#177 use cases (regression-style)' with HV21-HV37
- Add Surface B: neighbors_v2(include_unresolved=True) interleaves
  UnresolvedCallSite projections with CALLS rows in (line,byte) order
- Add row_kind discriminator ('call_edge' | 'unresolved_call_site')
- Add second HINTS-V4 template: TPL_NEIGHBORS_CALLS_HAS_UNRESOLVED
- Lock Decisions 20-33:
  20 callee_declaring_role source; 21 depth>1 filter at every hop;
  22 NodeFilter+EdgeFilter AND-compose; 23 unresolved telemetry counters;
  24 cursor-pr-review consumer; 25 cross-MS skips not in UnresolvedCallSite;
  26 include_unresolved mutually exclusive with edge_filter;
  27 no MCP find-unresolved-callers; 28 find_callers keeps min_confidence;
  29 package-relativity OOS; 30 brownfield role transparent;
  31 NodeFilter.role vs EdgeFilter.callee_declaring_role NOT renamed;
  32 Kuzu predicate-pushdown perf named-scenario; 33 only role exposed
- Expand out-of-scope table with 6 new rows
- Update Appendix B traceability
Six fixes from author-grill round 2:

G1+G2: Drop Decision 26 mutual-exclusivity (now Decision 25, reversed).
  include_unresolved composes with edge_filter — filter applies to
  resolved rows; unresolved rows pass through unfiltered. Add HV38
  (delegation skeleton + unresolved sites looking like delegations).

G3: row_kind is global on EdgeRowBase (default 'resolved'), not
  CALLS-only. Future-proofs the discriminator for similar splits.

G6: New Decision 33 — pass3_calls dedups multi-candidate resolution
  by (src_id, call_site_line, call_site_byte) BEFORE emit. Preference:
  concrete > interface, same-MS > cross-MS, first candidate as tiebreak.
  One source-line, one CALLS edge, one callee_declaring_role.
  Add HV37 covering the fixture test.

G8: Delete HV28 (cursor-pr-review use case) and Decision 24
  (cursor-pr-review as downstream consumer). .cursor skills are
  dev-time tooling, not end-agent workflows on the indexed codebase.
  Renumber HV29-HV37 → HV28-HV36, Decisions 25-33 → 24-32.

G10: Decision 23 replaces JSON-map column with three discrete INT64
  counters (pass3_unresolved_phantom_receiver / _chained / _name_only).
  Kuzu can WHERE/SUM these directly.

Plus: §5 out-of-scope rows for callee_microservice and
  unresolved_filter axis to pre-empt obvious bolt-ons.
  HV35 (perf) names the OrderService.process scenario and
  pre-PR-2 baseline. HV34/Decision 30 collision-hint trigger
  refined to 'dominantly OTHER rows' (zero-rows under-fires).

Decisions: 33 (1-33 contiguous, no gaps).
Use cases: HV1-HV20 + HV21-HV38 = 38 rows.
Principles: 8 (unchanged).
Copy link
Copy Markdown
Owner Author

@HumanBean17 HumanBean17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical review pass on the propose. I think the issue is real and the core direction is good (keep CALLS ordered; add role-aware projection instead of splitting semantic edge types), but I would not merge this propose as-is.

Main blockers:

  1. The plan drops useful known-receiver external-call data. Current CALLS distinguishes true zero-confidence receiver failures from the case where the receiver is resolved but the callee method is not indexed (JDK/Spring/external/library calls). That path preserves confidence, strategy, arg_count, and a deterministic phantom FQN; README also documents JDK/Spring/Lombok callees as phantom method symbols. The proposed UnresolvedCallSite(reason='name_only_zero_candidates') shape only carries callee_simple, receiver_expr, and reason, so it loses graph-quality and transcript data. Either preserve those fields in UnresolvedCallSite, or keep known-receiver external calls as non-traversable CALLS/separate external-call rows.

  2. Build-time dedup by (src_id, call_site_line, call_site_byte) erases resolver truth. Current multi-candidate emission is how overload_ambiguous represents ambiguity. If the concrete bug is interface+concrete duplication for one source call, solve that specific duplicate class. A blanket pre-emit winner selection makes find_callers / explain flows see one arbitrary callee and hides ambiguity that agents may need to know about.

  3. Silent no-op edge_filter semantics conflict with the existing strict filter contract. mcp_v2.py currently frames filters as typed predicates where inapplicable fields fail loud with a teaching message. Returning filtered CALLS plus unfiltered OVERRIDES for edge_types=['CALLS','OVERRIDES'], edge_filter={callee_declaring_role:'SERVICE'} is surprising and likely to reintroduce noise. I would prefer fail-loud or an explicit per-edge-type filter shape.

  4. include_unresolved=True + edge_filter reintroduces unfiltered noise. With edge_filter={callee_declaring_role:'SERVICE'}, the proposed behavior returns SERVICE resolved calls plus all unresolved sites, not “service-like unresolved sites.” That undermines the filter. Make these mutually exclusive again, or add a real unresolved filter axis once a concrete workflow justifies it.

  5. The propose invents neighbors_v2(depth=2) semantics. Current neighbors_v2 is one-hop only and the README frames MCP walking as one hop at a time. If depth is intended, it needs to be a first-class MCP surface change with docs/tests/tool-description updates; otherwise remove HV28 / Decision 21.

  6. CLI naming is inconsistent. The proposal says pc unresolved-calls, but the operator CLI in this repo is java-codebase-rag with top-level subcommands. Unless pc is a planned alias, the propose should use java-codebase-rag unresolved-calls ....

Suggested safer sequence: PR-1 add callee_declaring_role and edge_filter without deleting rows; PR-2 move only true zero-confidence receiver/chained failures out of CALLS, preserving known-receiver external-call metadata; PR-3 revisit dedup as response-shape only, not build-time candidate loss.

@HumanBean17
Copy link
Copy Markdown
Owner Author

Thanks — this is the right grilling. All six blockers stand; I agree with the substance of every one. Itemized:

#1 — Known-receiver external calls data loss → AGREE. I missed that build_ast_graph.py:1257-1271 already distinguishes "receiver resolved, callee method not indexed (JDK/Spring/external)" from true receiver failures, and preserves confidence/strategy/arg_count/deterministic phantom FQN with resolved=False. README §"Phantom nodes" documents this explicitly. The propose collapsing these into UnresolvedCallSite(reason='name_only_zero_candidates') strips real data — graph-quality counters, transcript-quality metadata, and the receiver-tier strategy that downstream consumers may project on.

The fix I'll lock: only true receiver-failure sites move out of CALLS. Concretely:

  • strategy='phantom' with resolved=False (unresolved receiver) → UnresolvedCallSite(reason='phantom_unresolved_receiver')
  • strategy='chained_receiver'UnresolvedCallSite(reason='chained_receiver')
  • Everything else stays as a CALLS row, including known-receiver-external (resolved=False with preserved receiver-tier strategy), overload_ambiguous (resolved=True), name-only-fb single-candidate (resolved=True), implicit_super, constructor, etc.

UnresolvedCallSite carries receiver_expr, callee_simple, arg_count, reason, call_site_line, call_site_byte. Known-external calls keep their existing row shape with resolved=False; agents who want to exclude them keep using exclude_external=True (FQN prefix), exactly as today.

That means PR-1's CALLS is not "resolved-only" — it's "no-true-receiver-failure rows." I'll rewrite TL;DR + Principle 5 + §3.1/§3.3 + every UC row that said "resolved-only" to reflect this.

#2 — Build-time dedup erases overload_ambiguous → AGREE, scope down. The concrete bug I was solving in HV37 / Decision 33 was interface declaration + concrete impl emitted as two CALLS rows for the same source line. overload_ambiguous (name-only-fb with >1 candidate at the same level) is the resolver's own ambiguity signal and must stay as N rows with strategy='overload_ambiguous' so find_callers / explain flows can see it.

Fix: rewrite Decision 33 as supertype-walk dedup only. When _lookup_method_candidates returns multiple candidates because one candidate is the declared concrete method on the receiver type and others are inherited supertype declarations, collapse to the concrete-class candidate. overload_ambiguous is left alone. PR-1 fixture test asserts: (a) myRepository.save(x) where MyRepository extends JpaRepository → one CALLS row pointing at MyRepository.save (post-dedup); (b) overloaded save(Foo) / save(Bar) with arg_count=-1 → N rows with strategy='overload_ambiguous' (unchanged).

#3 — Silent no-op edge_filter conflicts with fail-loud filter contract → AGREE. I missed the fail-loud-on-inapplicable-fields invariant at mcp_v2.py:6,82-91,191-206. Principle 7 / Decision 10 ("silently no-op on edges that don't carry the attribute") was a direct contradiction.

Fix: lock that edge_filter requires edge_types to be a single edge type whose schema declares the filtered attribute. edge_types=['CALLS','OVERRIDES'] + edge_filter={callee_declaring_role:'SERVICE'} raises a fail-loud ValueError with a teaching message ("callee_declaring_role not on OVERRIDES; split into two calls"). This matches the existing _nodefilter_inapplicable_fields pattern exactly. Per-edge-type-keyed filter map is a richer future direction but adds surface; deferring.

This kills Principle 7 in its current form. Rewriting it as "edge_filter is single-edge-type-scoped and fail-loud on inapplicable attributes."

#4include_unresolved=True + edge_filter reintroduces noise → AGREE, reverting my round-2 reversal. You're right that "SERVICE-resolved CALLS + all unresolved" is not "service-like unresolved sites." HV38's framing was wrong — the receiver-expression heuristic I waved at would require a real classifier on the unresolved row, which is a new primitive.

Fix: re-lock the original mutual exclusivity (include_unresolved=True is incompatible with edge_filter, fail-loud). HV38 deleted; HV23 (full transcript including unresolved) stays. If a real "service-like unresolved" workflow emerges, it gets its own propose with an unresolved_filter axis backed by a concrete classifier (e.g. receiver type-name suffix matching).

#5neighbors_v2(depth=2) semantics invented → AGREE, drop entirely. README:12 and mcp_v2.py:39 are both explicit: neighbors_v2 is one-hop. I invented depth>1 semantics out of thin air for HV28/Decision 21. Multi-hop is a separate design problem (visited-set, cycle handling, fanout cap, hint behavior at depth boundaries) and shouldn't ride along.

Fix: delete HV28 + Decision 21. Add row to §5 out-of-scope: "Multi-hop neighbors_v2 — out of scope; separate propose if/when needed."

#6 — CLI naming → AGREE. Mechanical fix. pc unresolved-callsjava-codebase-rag unresolved-calls everywhere in §3.6, HV26, decisions, out-of-scope rows.


On the suggested safer sequence: I think it's strictly better than my 3-PR ordering and I'll adopt it:

  • PR-1: Add callee_declaring_role to CALLS DDL + populate in pass3_calls + supertype-walk dedup. No rows deleted, no schema removals. Ontology bump 14→15 for the new column. Adds the GraphMeta counters (Decision 23 — three discrete INT64 columns). HV1, HV31, HV32, HV33, HV37 land here.
  • PR-2: Add EdgeFilter + edge_filter parameter to neighbors_v2 (single-edge-type, fail-loud). Adds CLI surface for graph quality. Still no CALLS row deletions. HV2, HV21, HV22, HV28 (renumbered, the depth=2 row goes away — was HV28 in revision 2), HV33, HV34, HV35, HV36 land here.
  • PR-3: Move only strategy='phantom' (true receiver-failure) and strategy='chained_receiver' rows out of CALLS into UnresolvedCallSite + UNRESOLVED_AT. Delete _phantom_method_id / tables.phantoms for just those two reasons. Known-receiver-external resolved=False rows stay as CALLS. Add include_unresolved (mutually exclusive with edge_filter). Add the two HINTS-V4 templates. HV23, HV24, HV30 land here.

This makes PR-1 and PR-2 strictly additive (zero behavior change for existing readers), and PR-3 the only breaking change — with a much narrower blast radius than before.


Amendment plan (single force-push to propose/calls-noise):

  1. Rewrite TL;DR — drop "resolved-only CALLS" framing; replace with "true receiver-failure sites move to a caller-side facet; known-external calls stay in CALLS."
  2. Rewrite Principle 5 (was "CALLS carries only successfully-resolved invocations") → "CALLS sheds true receiver-failure rows; all other resolved=False semantics preserved."
  3. Rewrite Principle 7 → "edge_filter is single-edge-type-scoped and fail-loud on inapplicable attributes" (delete "silently no-op").
  4. Rewrite §3.1 emission-table to reflect the three-bucket mapping (receiver-failure → UCS; supertype-walk dedup; everything else unchanged).
  5. Rewrite Decision 25 → mutual exclusivity restored.
  6. Rewrite Decision 33 → supertype-walk dedup only; overload_ambiguous unchanged.
  7. Delete Decision 21, HV28 (depth=2). Renumber.
  8. Delete HV38 (compose-with-filter). Renumber.
  9. Add §5 row: multi-hop neighbors_v2 out of scope.
  10. Mechanical: pcjava-codebase-rag everywhere.
  11. PR ordering rewritten per your suggested safer sequence.
  12. Update Appendix B with revision 3 traceability.

Will land as a single revision-3 force-push. Final counts will likely be ~30 decisions and ~36 use cases. Going to apply now.

Applies the 12-item amendment plan posted at #178 issuecomment-4487386547.
Reverses three round-1/round-2 decisions that the external review correctly
flagged as data-loss or contract violations.

Six blockers addressed:

1. Known-external CALLS preservation (build_ast_graph.py:1257-1271).
   - Decision 1 / Principle 5 / §3.1 / §3.3 / §3.10 / HV37 / Decision 34:
     CALLS sheds *only* strategy='phantom' (unresolved receiver) and
     strategy='chained_receiver'. Receiver-resolved-but-callee-not-indexed
     rows stay in CALLS with resolved=False and preserved receiver-tier
     strategy/confidence/arg_count metadata.
   - Decision 2 reversed: 'resolved BOOLEAN' STAYS in CALLS DDL.

2. Multi-candidate dedup scope-narrowed (Decision 33).
   - Supertype-walk dedup only (one concrete + N inherited supertype
     declarations -> collapse to concrete).
   - overload_ambiguous rows preserved as N rows. The 'one row per
     ambiguous site' phrasing from round-2 deleted; resolver ambiguity
     stays visible to find_callers/explain.

3. edge_filter fail-loud (Principle 7 + Decision 10 reversed; HV13).
   - Mixed edge_types + edge_filter raises ValueError with a teaching
     message matching the existing _nodefilter_inapplicable_fields
     pattern at mcp_v2.py:191-206. Silent no-op was the wrong call.

4. include_unresolved + edge_filter mutex restored (Decision 25 + HV23).
   - The round-2 'compose them' reversal would have reintroduced
     unfiltered phantom/chained noise into the interleaved transcript
     view. Mutually exclusive (fail-loud), as in revision 1.

5. neighbors_v2 stays one-hop.
   - HV28 (depth=2 walk) deleted entirely. Decision 21 deleted.
   - README:12 + mcp_v2.py:39 ('Stored graph edge labels for one-hop
     neighbors.') are the locked contract; multi-hop needs its own
     propose (visited-set, cycles, fanout cap, hint behavior).

6. CLI binary rename: 'pc' -> 'java-codebase-rag'.
   - docs/JAVA-CODEBASE-RAG-CLI.md:1-17 is the source of truth.
   - All §3.6 / HV8 / HV9 / HV26 CLI references updated.

Counters reduced 3 -> 2:
- pass3_unresolved_phantom_receiver, pass3_unresolved_chained.
- No more pass3_unresolved_name_only (those rows stay in CALLS).
- UnresolvedCallSite.reason enum reduced to two values; name_only branch
  removed. UnresolvedCallSite gains arg_count INT64.

New decisions:
- Decision 34: known-receiver-external rows preserved in CALLS.
- Decision 35: fail-loud validator on _nodefilter_inapplicable_fields.

Sub-PR sequence reordered for safer adoption (per reviewer's suggestion):
- PR-1: callee_declaring_role + supertype-walk dedup + GraphMeta counters
  (strictly additive).
- PR-2: EdgeFilter + CLI stub (strictly additive).
- PR-3: UnresolvedCallSite + UNRESOLVED_AT + phantom/chained branch
  removal + include_unresolved + dedup_calls (only breaking PR).

Counts: 35 decisions (was 33), 37 use cases (HV1-HV37; deleted HV28 and
the old HV38, added HV37), 8 principles (Principle 7 reversed), 3 PRs.

Tracks: #177
PR: #178
@HumanBean17 HumanBean17 marked this pull request as ready for review May 19, 2026 12:56
@HumanBean17 HumanBean17 merged commit 33b299c into master May 19, 2026
1 check passed
@HumanBean17 HumanBean17 deleted the propose/calls-noise branch May 23, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant