100%

CALLS resolution precision (Defect 1)

Goal

Eliminate the CALLS over-resolution class; unblock dead-code + layering queries.

  • The callable+constructable-kind filter on the by_name fallback ALREADY LANDED via the walking-skeleton slice (s-9ba53a79, w-c7fc07e3).
  • This section’s remaining deliverable is the re-resolve/verify pass plus the arity follow-on that recovers recall from callable homonyms.

Implementation Sketch

As-built kind-filter (ALREADY LANDED via the walking-skeleton slice s-9ba53a79, w-c7fc07e3):

  • _CALLS_TARGET_KINDS = frozenset({"function", "method", "macro", "type", "sum_type", "type_alias"}) at intel_repo/neo4j/import_code_graph.py:945-947, gated inside _resolve_target_py by the _gate_calls closure at 979-983.
  • The as-built allow-set is 6 kinds, NOT 3: callables (function/method/macro) AND constructables (type/sum_type/type_alias).
  • Constructables are deliberately KEPT — a newtype / tuple-struct / enum-variant / Err(...) constructor surfaces as a CALLS edge onto the type symbol (e.g. ExprId(42), Err(e)); docstring 969-977.
  • Only non-callable, non-constructable kinds (module/field/variable/const/trait/impl) are rejected → the by_name match returns None → the edge becomes an UnresolvedSymbol rather than a false CALLS-to-non-callable edge.
  • Constructor-keep is a load-bearing precision invariant: dropping constructable kinds would lose real call/construct edges.

Remaining deliverable — re-resolve/verify + arity follow-on. Arity disambiguation RECOVERS recall (it does NOT merely reject homonyms after resolution):

  • Carry the call-site arg count into target metadata; filter the by_name candidate set by declared param count BEFORE the uniqueness check.
  • Today every callable homonym (e.g. the ~547 cross-crate binds onto ori_parse::cursor::expect) goes unresolved because by_name returns multiple matches.
  • Arity filtering narrows that multi-set to the single arity-compatible callable, so an otherwise-unresolvable call becomes correctly resolved.
  • Macros + constructable kinds (type/sum_type/type_alias) carry no declared parameter count → arity filtering MUST bypass them (kind-only path retained; never rejected for arity mismatch).

Work Items

  • Re-resolve + verify the landed kind-filter: the 6-kind _CALLS_TARGET_KINDS allow-set + _gate_calls CALLS gate are already in import_code_graph.py (945-947 + 979-983, inside _resolve_target_py at 950-1013); run the resolver pass against the live graph and confirm the gate drops every CALLS edge whose target kind is outside the 6-kind set while keeping callable + constructable targets. No re-implementation of the filter — it landed via w-c7fc07e3.
  • Run a re-resolve pass; verify the 1,378 CALLS-edges-to-non-callable-kinds (incl. the 133-edge extend collision) drop to 0.
  • Follow-on arity filter (recall recovery, not post-hoc rejection). Two distinct sub-problems, both required:
    • (a) Candidate narrowing: extract declared parameter counts in extract_symbols.py (absent today) and load them into the symbol index via import_code_graph.py _build_symbol_index; carry the call-site arg count into target metadata; in _resolve_target_py, filter the by_name candidate set to arity-compatible callables BEFORE the len(matches) == 1 uniqueness check so a callable homonym (e.g. ori_parse::cursor::expect, ~547 cross-crate false binds) resolves to its single arity match instead of going unresolved.
    • (b) Receiver/self offset accounting: a method call carries a receiver the free-function arity convention does not — x.foo(a) has call-site arg count 1 but the callee foo(&self, a) declares 2 params. The Rust tree-sitter capture (queries/rust/decls.scm) tags every function_item — impl-block methods included — as kind function; the corpus carries NO method kind, so the receiver offset CANNOT be keyed on symbol kind. extract_symbols.py MUST emit a has_self_receiver flag (first declared param is self / &self / &mut self); the arity comparison normalizes effective_params = declared_params - (1 if has_self_receiver else 0) and matches the call-site arg count against effective_params so a method homonym is not spuriously rejected by an off-by-one.
    • Macros + constructable kinds (type/sum_type/type_alias) carry no declared param count → bypass them in the arity filter (kind-only path retained; never rejected for arity mismatch).
  • pytest in intel_repo covering: module/field/const target rejected (the landed kind-filter); a constructable target (type/sum_type/type_alias constructor) KEPT (no false-rejection of Err(...)-style construct edges); callable homonym still unresolved under the kind-filter alone (no recall regression); under the arity filter, an arity-compatible callable homonym now RESOLVES (recall recovery — ori_parse::cursor::expect cross-crate count drops); arity-mismatched candidate rejected; a method-call homonym whose declaration carries a self receiver resolves under the has_self_receiver normalization (effective_params = declared_params - 1), distinct from a free function of the same name; macro + constructable kinds bypass the arity filter. The builder module is graph-dark (file-symbols returns 0 symbols), so pytest is the sole regression surface.

Deepening — call-shape-aware resolution (the real cross-crate de-noiser)

As-built finding (direct resolver instrumentation against the live index): the arity follow-on (w-edab2636), applied to METHOD-shaped calls, is net-HARMFUL — it manufactures false binds, not recall.

  • resolve("unwrap", arg_count=0)ori_parse::outcome::unwrap (false bind); resolve("unwrap", arg_count=None)None (correct, unresolved).
  • Arity narrowing collapses a receiver-BLIND candidate set to its lone arity-match; for .unwrap()/.expect() the real receiver is a std Option/Result whose method is ungraphed, so the lone user-method match is wrong.
  • cursor::expect is a SINGLE by_name candidate (effective arity 1) arity-compatible with .expect("msg") (arg 1) — arity could never touch it.

Corrected mechanism (call shape, not arity, disambiguates method calls):

  • extract_symbols.py emits call_shape ∈ {method, path, free, macro} per CALLS record, from the call_expression function-field node type (field_expression→method, scoped_identifier→path, identifier→free). calls.scm already distinguishes the shapes.
  • _resolve_target_py(call_shape, source_file): a METHOD-shaped call BYPASSES arity narrowing (receiver type, unavailable, is the true disambiguator — arity only narrows free/path function references). Multi-candidate std-methods (unwrap 15-way) correctly return None.
  • Single-candidate residue: a method-shaped call whose bare name is in _STD_METHOD_DENYLIST (Option/Result/AsRef/conversion std-trait methods) resolving CROSS-CRATE (_crate_of_file(source) != _crate_of_file(target)) to a has_self_receiver candidate is suppressed (None). Domain methods (get_expr/alloc_expr/resolve_fully) are NOT in the denylist; same-crate binds (plausible local Outcome receiver) are kept.
  • Free/path arity behavior is UNCHANGED — the arity follow-on stays correct for direct function references.

As-built result (full re-sync, 35,271 symbols 0-failed): ori_parse::cursor::expect cross-crate CALLS 513→0; ori_parse::outcome::unwrap 637→0; total Rust cross-crate self-method false binds 5,795→2,337 (−60%, the false binds become correctly-unresolved); genuine cross-crate domain methods preserved (get_expr 148→149, alloc_expr 115→124, resolve_fully 166→200); non-callable CALLS still 0 (kind filter intact). pytest: test_import_code_graph.py 32 pass (+6 method-shape cells), test_extract_symbols.py 25 pass (+1 call_shape cell).

Ordering and dependencies

  • Work-item ordering (TDD: tests precede the code they pin, per tests.md failing-cells-first matrix-squeeze): w-2c024690 (re-resolve + verify the landed kind-filter) → w-7c436d31 (re-resolve pass, non-callable CALLS drop to 0) → w-a48c19b9 (author the pytest matrix TEST-FIRST — the arity + receiver-offset cells fail RED against the pre-arity resolver; the already-landed kind-filter cells are green from the start) → w-edab2636 (implement the arity follow-on, turning the RED arity/receiver cells GREEN). The pytest is authored BEFORE the arity implementation and fails on the cells the implementation must satisfy; it is not a trailing after-the-fact pin.
  • Predecessor edge (declared via plan.json hint_needs, NOT prose-only): s-1209f1e9 needs s-9ba53a79 — this section consumes the kind-filter foundation the walking-skeleton landed.
  • Consumer edge (declared via hint_needs): s-9fb83bc7 (query-primitives) needs s-1209f1e9 — its dead-code + layering primitives are unblocked only by this section’s de-noised CALLS graph (the homonym false-binds are removed here, not in the walking-skeleton slice).

References

  • intel_repo/neo4j/import_code_graph.py: _CALLS_TARGET_KINDS + _STD_METHOD_DENYLIST + _crate_of_file + _is_cross_crate_std_method_bind + _resolve_target_py(call_shape, source_file) (method-shape arity-bypass + cross-crate std-method suppression); threaded through resolve_file_relationships (live sync) + bulk Phase 2.
  • intel_repo/neo4j/extract_symbols.py: _derive_call_shape + call_shape on CALLS records.
  • queries/rust/calls.scm (free/path/method shapes).

HISTORY

  • 2026-06-15 — Deepened past the arity premise (cross-crate-aware resolution): verify-first grounding proved sc-e1b408b3 (cursor::expect→0) unachievable by the arity follow-on (single arity-compatible candidate). Direct resolver instrumentation then showed the arity narrowing was net-HARMFUL for method calls — manufacturing ~3,458 cross-crate false binds (outcome::unwrap 637, cursor::expect 513, the std Option/Result/AsRef method family) by collapsing receiver-blind candidate sets to their lone arity-match. Corrected mechanism: call_shape extraction + method-call arity bypass + cross-crate _STD_METHOD_DENYLIST suppression; free/path arity unchanged. Result: cursor::expect 513→0, total cross-crate self-method false binds 5,795→2,337 (−60%), genuine domain methods preserved, non-callable still 0. Scope: tooling (intel_repo). pytest 32+25 green. (User-directed deepen of s-1209f1e9.)