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"})atintel_repo/neo4j/import_code_graph.py:945-947, gated inside_resolve_target_pyby the_gate_callsclosure at979-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)); docstring969-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_KINDSallow-set +_gate_callsCALLS gate are already inimport_code_graph.py(945-947+979-983, inside_resolve_target_pyat950-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 viaimport_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 thelen(matches) == 1uniqueness 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/
selfoffset 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 calleefoo(&self, a)declares 2 params. The Rust tree-sitter capture (queries/rust/decls.scm) tags everyfunction_item— impl-block methods included — as kindfunction; the corpus carries NOmethodkind, so the receiver offset CANNOT be keyed on symbol kind.extract_symbols.pyMUST emit ahas_self_receiverflag (first declared param isself/&self/&mut self); the arity comparison normalizeseffective_params = declared_params - (1 if has_self_receiver else 0)and matches the call-site arg count againsteffective_paramsso 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).
- (a) Candidate narrowing: extract declared parameter counts in
- 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::expectcross-crate count drops); arity-mismatched candidate rejected; a method-call homonym whose declaration carries aselfreceiver resolves under thehas_self_receivernormalization (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 stdOption/Resultwhose method is ungraphed, so the lone user-method match is wrong. cursor::expectis 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.pyemitscall_shape∈ {method, path, free, macro} per CALLS record, from thecall_expressionfunction-field node type (field_expression→method,scoped_identifier→path,identifier→free).calls.scmalready 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 (unwrap15-way) correctly returnNone.- 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 ahas_self_receivercandidate is suppressed (None). Domain methods (get_expr/alloc_expr/resolve_fully) are NOT in the denylist; same-crate binds (plausible localOutcomereceiver) 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.mdfailing-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-1209f1e9needss-9ba53a79— this section consumes the kind-filter foundation the walking-skeleton landed. - Consumer edge (declared via
hint_needs):s-9fb83bc7(query-primitives) needss-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 throughresolve_file_relationships(live sync) + bulk Phase 2. - intel_repo/neo4j/extract_symbols.py:
_derive_call_shape+call_shapeon 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::unwrap637,cursor::expect513, the std Option/Result/AsRef method family) by collapsing receiver-blind candidate sets to their lone arity-match. Corrected mechanism:call_shapeextraction + method-call arity bypass + cross-crate_STD_METHOD_DENYLISTsuppression; 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.)