Skip to content

Commit 62b4df2

Browse files
Your Nameclaude
andcommitted
fix(resolver): stop dropping call edges for inherited-method calls through a known receiver type
Root-caused via B15's investigation into why calm scored below CodeGraph on file-recall: resolve_sites_to_edges keyed by_name_class lookups on (callee, EXACT declaring class) with no superclass walk. When a receiver's static type was known (target_class populated from a tracked binding -- Java's formal_parameter, Go's parameter_declaration, etc.) but the called method was only inherited from an ancestor class, the exact-key lookup missed and the edge was dropped entirely -- unlike an unknown-type receiver, which correctly falls back to an ambiguous fan-out edge. Knowing more about the receiver's type made calm strictly worse at finding the edge. Fixed by falling back to the unscoped by_name lookup, but only when the receiver's class is itself a symbol this project actually declares (ctx.by_name.contains_key(cls)) -- so a receiver typed as an unmodeled external/stdlib type (Rust's HashMap::new()) keeps the original "no candidates" behavior instead of wrongly fanning out project-wide. Caught test_type_path_call_resolves_scoped_not_fanned_out regressing on the first, unguarded attempt at this fix. Also fixes a benchmark oracle bug found in the same investigation: ground_truth.py's Java method regex (made modifier-optional 2026-08-18 to catch package-private JUnit methods) accidentally became a near-universal matcher for control-flow lines ending in a brace (`if (x.isNew()) {`), causing _looks_like_a_definition to wrongly exclude real call sites from oracle ground truth. New regression test: test_java_formal_parameter_resolves_inherited_superclass_method. Full cargo test -p calm-core --lib (1234) + calm-server --lib (395): all green. Re-ran B15 with both fixes: calm moved from 68/72 (94.4%, behind CodeGraph's 95.8%) to 72/73 (98.6%, ahead of CodeGraph's 94.5%, within 1.4pt of Context+'s recall-maximizing ceiling) -- see benchmarks/b15_cross_lang_competitor_ab/README.md's "Root-cause investigation" section for the full writeup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 0d9aa3a commit 62b4df2

4 files changed

Lines changed: 219 additions & 22 deletions

File tree

benchmarks/b12_tier1_tier2_tool_correctness/ground_truth.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from dataclasses import dataclass
2020
from pathlib import Path
2121

22-
_NOT_A_NAME = {"if", "for", "while", "switch", "catch", "return", "function"}
22+
_NOT_A_NAME = {"if", "for", "while", "switch", "catch", "return", "function", "synchronized", "try"}
2323

2424

2525
@dataclass(frozen=True)
@@ -131,8 +131,29 @@ def _looks_like_a_definition(line_text: str, lang: str) -> bool:
131131
~10 different test functions, not one real call anywhere -- and
132132
`jinja_loader` "call sites" were actually substring hits inside the
133133
unrelated, longer identifier `create_global_jinja_loader(`. Both are
134-
ground-truth bugs this filter (plus the word-boundary fix below) closes."""
135-
return any(re.search(pat, line_text) for pat, _kind in PATTERNS[lang])
134+
ground-truth bugs this filter (plus the word-boundary fix below) closes.
135+
136+
2026-08-18 fix (B15 investigation): the 2026-08-18 fix that made Java's
137+
method-modifier group optional (see PATTERNS["java"]'s own comment)
138+
turned that pattern into an accidental near-universal matcher for ANY
139+
control-flow line ending in a brace -- `if (pet.isNew()) {`, `while
140+
(x) {`, `for (...) {`, `catch (E e) {`, `synchronized (lock) {` all
141+
satisfy "optional modifiers, some word-ish text, NAME(...) {" with
142+
NAME captured as the keyword itself ("if"/"while"/...). Every one of
143+
those was then wrongly treated as "looks like a redefinition" and
144+
excluded from call-site ground truth -- verified live on spring-
145+
petclinic: `isNew()`'s real call sites inside `if (pet.isNew() && ...)
146+
{`-shaped conditions in Owner.java/PetController.java/PetValidator.java
147+
were silently dropped from the oracle, undercounting EVERY tool's
148+
(not just CALM's) B15 recall on that row. Reuses the same `_NOT_A_NAME`
149+
keyword set `extract_definitions` already filters the captured name
150+
against, applied here to the definition-pattern's own captured group
151+
instead of just checking "does any pattern match at all"."""
152+
for pat, _kind in PATTERNS[lang]:
153+
m = re.search(pat, line_text)
154+
if m and m.group(1) not in _NOT_A_NAME:
155+
return True
156+
return False
136157

137158

138159
# 2026-08-02 fix: single-line comment marker per language, used by

benchmarks/b15_cross_lang_competitor_ab/README.md

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ a hit) — but it's a real, disclosable precision problem, worth weighing agains
9494
claim on Context+'s own README, and worth reading the raw `contextplus_files` column for, not just
9595
the recall fraction.
9696

97-
## Results (2026-08-18, `calm` @ `822e238`, N=8 symbols/corpus, single pass)
97+
## Results (2026-08-18, `calm` @ `0d9aa3a` + the inheritance-fallback fix below, N=8 symbols/corpus, single pass)
9898

9999
File-recall on "who calls this symbol", per language (hit/total oracle files):
100100

@@ -105,26 +105,96 @@ File-recall on "who calls this symbol", per language (hit/total oracle files):
105105
| go | 11/11 (100%) | 10/11 (91%) | **1/11 (9%) — see disclosure below, not a capability claim** | 11/11 (100%) |
106106
| javascript | 7/8 (88%) | 8/8 (100%) | **0/8 (0%) — see disclosure below, not a capability claim** | 8/8 (100%) |
107107
| typescript | 11/11 (100%) | 9/11 (82%) | **1/11 (9%) — see disclosure below, not a capability claim** | 11/11 (100%) |
108-
| java | 21/24 (88%) | 24/24 (100%) | 23/24 (96%) | 24/24 (100%) |
109-
| **aggregate (java only for Ctxo — see below)** | **68/72 (94.4%)** | **69/72 (95.8%)** | **23/24 (96%)** | **72/72 (100%)** |
108+
| java | 25/25 (100%) | 24/25 (96%) | 23/25 (92%) | 25/25 (100%) |
109+
| **aggregate (java only for Ctxo — see below)** | **72/73 (98.6%)** | **69/73 (94.5%)** | **23/25 (92%)** | **73/73 (100%)** |
110+
111+
**This table supersedes the first published run** (calm 68/72 = 94.4%, below CodeGraph's 69/72 =
112+
95.8%) — see "Root-cause investigation" below for why, and note the java column's *sample itself*
113+
changed between runs: fixing the oracle bug changed which 8 symbols `sample_symbols` deterministically
114+
picks (the same symbol names — `getName`/`isNew` — that motivated the investigation are no longer in
115+
this particular sample, but the fix they drove is independently verified live in the investigation
116+
section, not just inferred from this table moving).
110117

111118
**Read the "Ctxo go/js/ts: a real, unresolved integration-reliability finding" section below before
112119
citing the go/js/ts Ctxo numbers for anything** — they are published for transparency (raw data in
113120
`results.json`), but this benchmark's own investigation could not confirm they measure Ctxo's real
114121
call-graph quality, so the aggregate row above excludes them and counts only Ctxo's java result
115122
(the one arm verified end-to-end with real, non-empty query results).
116123

117-
**Reading the rest of the table**: CodeGraph and Context+ both land at or near 100% on every
118-
language they run on — CodeGraph misses 3 files total (go/1, typescript/2, both same-file or
119-
private-symbol edge cases, not investigated further here), Context+ misses none in this sample
120-
(see the CSS false-positive section below for why "0 misses" doesn't mean "flawless"). `calm`'s 4
121-
misses were spot-checked, not just counted: the javascript one (`User`, `examples/view-locals/
122-
user.js`) is a real, disclosable gap — the symbol is invoked exclusively via `new User(...)`, and
123-
CALM's JS/TS call extractor doesn't currently treat a `new`-expression as a call site for the
124-
constructed class name. The java ones are inheritance/cross-file dispatch cases (`getName`/`isNew`
125-
defined in a base class, invoked through a subclass instance) — a much harder class of problem for
126-
any syntactic (non-type-checking) resolver, consistent with this suite's own prior findings on
127-
Rust `Self::`/method-name collisions.
124+
**Reading the rest of the table**: `calm`'s only remaining miss across all 6 languages (48 sampled
125+
symbols) is the javascript `User` case — spot-checked, not just counted: the symbol is invoked
126+
exclusively via `new User(...)`, and CALM's JS/TS call extractor doesn't currently treat a
127+
`new`-expression as a call site for the constructed class name (a real, disclosable, separate gap —
128+
not investigated further here). CodeGraph misses 4 files total (go/1, typescript/2, java/1 — not
129+
investigated further here); Context+ misses none in this sample (see the CSS false-positive section
130+
below for why "0 misses" doesn't mean "flawless"). Ctxo's java column moved 23/24→23/25 between runs
131+
purely because the sample changed size (24→25 oracle files) under the fixed oracle, not because
132+
Ctxo's own behavior changed.
133+
134+
## Root-cause investigation (2026-08-18): why did `calm` initially score below CodeGraph?
135+
136+
The first published run above showed `calm` (94.4%) narrowly behind CodeGraph (95.8%) and well
137+
behind Context+ (100%). Investigated end-to-end — not just re-read the numbers — by reproducing
138+
`calm`'s exact misses against a live corpus, inspecting its `call_edges` table directly, and building
139+
a minimal 2-class Java repro. Three distinct things were found, not one:
140+
141+
1. **A benchmark-design property, not a bug**: this task measures *recall only*, never precision.
142+
Context+ and CodeGraph both lean toward "return more, don't worry about false positives" —
143+
Context+'s `get_blast_radius` was caught doing plain substring text matching (see the CSS
144+
false-positive section below), which trivially maximizes recall at zero precision cost under this
145+
scoring. A recall-only metric structurally favors that strategy over a resolver that tries to stay
146+
precise. Not fixed here (would need a precision column, e.g. `len(tool_files - oracle_files)`, to
147+
fairly separate "found the real callers" from "returned everything and hoped").
148+
2. **A real, freshly-introduced oracle bug** (same file this benchmark's own ground truth lives in):
149+
the 2026-08-18 fix that made Java's method-definition regex's modifier group optional (to catch
150+
package-private JUnit test methods, see `ground_truth.py`'s `PATTERNS["java"]` comment) turned that
151+
pattern into an accidental near-universal matcher for control-flow lines ending in a brace —
152+
`if (pet.isNew() && ...) {` matches "modifiers?, some text, NAME(...) {" with NAME captured as the
153+
keyword "if" itself. `_looks_like_a_definition` then wrongly excluded every such line from call-site
154+
ground truth. Verified live: `isNew`'s real oracle should have been 3 files
155+
(`Owner.java`/`PetController.java`/`PetValidator.java` — every OTHER tool in this benchmark found
156+
all 3) but had collapsed to 1. Fixed in `ground_truth.py` by reusing the same `_NOT_A_NAME` keyword
157+
set `extract_definitions` already filters against, applied to the definition pattern's own captured
158+
group instead of just "did any pattern match at all".
159+
3. **A real, previously-undocumented bug in `calm`'s own core resolver** — the one that actually moved
160+
the table above. Root-caused to `crates/calm-core/src/indexer/pipeline.rs::resolve_sites_to_edges`:
161+
when a call site's receiver has a STATICALLY KNOWN type (`target_class` populated from a tracked
162+
binding — Java's `formal_parameter`, Go's `parameter_declaration`, etc.), the code looked up
163+
`ctx.by_name_class.get(&(callee, class))` — keyed on the method's *exact declaring class*, with no
164+
superclass walk — and on a miss returned **zero candidates**, dropping the call edge entirely,
165+
with none of the `ambiguous`-fan-out fallback an UNKNOWN-type receiver already gets. So a call to an
166+
*inherited* method (`getName()`/`isNew()` declared on `NamedEntity`/`BaseEntity`, invoked through a
167+
`Pet`-typed parameter) silently vanished — while the identical call through a same-type LOCAL
168+
variable (untracked, so genuinely "unknown" to the resolver) correctly fell back to `ambiguous`.
169+
**Knowing more about the receiver's type made `calm` strictly worse at finding the edge** — the
170+
opposite of the intended tiered-confidence design. Verified with a minimal, isolated 2-class Java
171+
repro (`Base`/`Sub`/3 receiver shapes) before touching any real code, confirmed as the root cause of
172+
3 of `calm`'s 4 original misses via direct `call_edges` inspection, and fixed by falling back to the
173+
unscoped `by_name` lookup — but ONLY when `cls` is itself a symbol this project actually declares
174+
(`ctx.by_name.contains_key(cls)`), so a receiver typed as an unmodeled external/stdlib type (Rust's
175+
`HashMap::new()` with no `HashMap` anywhere in the project) keeps the original "no candidates"
176+
behavior rather than wrongly fanning out project-wide — caught live by
177+
`test_type_path_call_resolves_scoped_not_fanned_out` regressing when the fallback was first tried
178+
unguarded. New regression test:
179+
`test_java_formal_parameter_resolves_inherited_superclass_method` (`pipeline.rs`). Full
180+
`cargo test -p calm-core --lib` (1234 tests) and `cargo test -p calm-server --lib` (395 tests): all
181+
green, 0 regressions. Re-verified live post-fix against the real spring-petclinic corpus: `getName`
182+
now correctly includes `Owner.java`/`PetController.java`, `isNew` now correctly includes all 3 real
183+
files.
184+
185+
**Net effect**: after both fixes, `calm` moved from 94.4% (behind CodeGraph) to 98.6% (ahead of
186+
CodeGraph, within 1.4 points of Context+'s recall-maximizing ceiling) — with its one remaining miss
187+
being the already-known, separately-tracked `new`-expression gap, not the inheritance bug. This is
188+
also a broader finding than B15 itself: `by_name_class`'s inheritance blindness is generic (any
189+
language populating `target_class` — Go, Rust, TS, C#, PHP, C/C++ — could hit the same shape), not
190+
Java-specific; only Java's was live-confirmed here.
191+
192+
**The benchmark-design finding (point 1) also answers a broader question worth stating plainly**:
193+
this task doesn't exercise `calm`'s actual differentiators at all. "File-recall on who calls X" is
194+
the one axis where a tool that returns more, unfiltered, structurally cannot lose — it says nothing
195+
about the pre-edit safety gate, cross-session memory, or token-efficiency tasks B11 already measures
196+
(and where CodeGraph/Context+ don't compete at all). A benchmark built to showcase `calm`'s strengths
197+
specifically would weight those differently, not just recall on one call-graph query shape.
128198

129199
## Ctxo go/js/ts: a real, unresolved integration-reliability finding
130200

benchmarks/claims.registry.jsonl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
{"id":"B13-CORRECTED-FILE-RECALL","claim":"calm vs CodeGraph file-recall, multi-repo (fd, flask, self-repo); fixes a ground-truth oracle bug (doc/comment lines miscounted as call sites) and a SCIP-overlay race (recall measured before the async formal-confidence upgrade finished), and along the way finds + fixes a real CALM parser bug (Self::method() calls resolved to zero edges)","benchmark":"b13_codegraph_multirepo_ab","calm_commit":"52d1abe682069e018d00d09f2d9ab07b820a557c plus the oracle/scip_refresh-race fixes, uncommitted at benchmark time and later committed as 9c0b0fe; the Self:: parser fix landed separately as c5823a8","competitor_versions":"@colbymchenry/codegraph@1.5.0","corpus_commit":"fd (sharkdp/fd) @ 41532d1, flask, self-repo -- see README for full pins","harness_commit":"9c0b0fe","command":"see benchmarks/b13_codegraph_multirepo_ab/README.md Methodology section","raw_output_path":"benchmarks/b13_codegraph_multirepo_ab/README.md","canonical_result":"combined file-recall, N=24 symbols / 31 oracle files: calm 31/31 (100%), CodeGraph 27/31 (87.1%)","status":"current","supersedes":["B13-PHASE1-2-FILE-RECALL"],"superseded_by":null,"last_reproduced":"2026-08-02","evidence_gap":null}
55
{"id":"DART-ZERO-CALL-EDGES","claim":"Dart call-graph extraction produces zero call edges -- documented as a deliberate scope cut (tree-sitter grammar has no node kind for call), not a bug","benchmark":"resolution","calm_commit":null,"competitor_versions":null,"corpus_commit":"dart-lang/args @ 7a2dfb5","harness_commit":null,"command":"see benchmarks/resolution/README.md Run section","raw_output_path":"benchmarks/resolution/README.md","canonical_result":"178 symbols indexed, 0 call edges","status":"superseded","supersedes":[],"superseded_by":"DART-C3-CALL-EDGE-EXTRACTION","last_reproduced":"2026-07-11","evidence_gap":"calm_commit not recorded in the original write-up; predates this checkout's available git history (starts 2026-07-28) -- exactly the kind of gap this registry exists to stop happening going forward"}
66
{"id":"DART-C3-CALL-EDGE-EXTRACTION","claim":"Dart call-edge extraction (C3) recovers real edges via a dedicated dart_call_from_member_access parser branch; the original 'no node kind for call' assumption was wrong -- Dart's grammar shares member_access/selector/argument_part nodes between calls and plain field access, distinguished only by whether the trailing selector wraps an argument_part","benchmark":"resolution","calm_commit":null,"competitor_versions":null,"corpus_commit":"dart-lang/args @ 7a2dfb5 (same corpus commit as the superseded measurement)","harness_commit":null,"command":"see benchmarks/resolution/README.md Run section","raw_output_path":"benchmarks/resolution/README.md","canonical_result":"178 symbols, 2166 call edges: 7.8% resolved, 11.2% textual, 80.9% ambiguous, 0% formal/inferred (no SCIP provider for Dart)","status":"current","supersedes":["DART-ZERO-CALL-EDGES"],"superseded_by":null,"last_reproduced":"2026-07-28","evidence_gap":"calm_commit not recorded in the original write-up; predates this checkout's available git history (starts 2026-07-28)"}
7-
{"id":"B15-CROSS-LANG-COMPETITOR-AB","claim":"calm vs CodeGraph vs Ctxo vs Context+, file-recall on callers, all 6 Tier-0 languages (python/rust/go/javascript/typescript/java)","benchmark":"b15_cross_lang_competitor_ab","calm_commit":"822e238efc54df32da36505cf25890c2302ee06f","competitor_versions":"@colbymchenry/codegraph@1.5.0, @ctxo/cli@0.11.4, contextplus@1.0.8","corpus_commit":"flask 36e4a824, fd 41532d11, gin 34dac209, express a3714473, zod 912f0f51, spring-petclinic 51045d16 -- see README Version pins section for full pins","harness_commit":null,"command":"see benchmarks/b15_cross_lang_competitor_ab/README.md Run section","raw_output_path":"benchmarks/b15_cross_lang_competitor_ab/README.md","canonical_result":"aggregate file-recall (Ctxo counts java only, see README disclosure): calm 68/72 (94.4%), codegraph 69/72 (95.8%), ctxo 23/24 (96.0%, java-only), contextplus 72/72 (100.0%)","status":"current","supersedes":[],"superseded_by":null,"last_reproduced":"2026-08-18","evidence_gap":"Ctxo go/js/ts excluded from aggregate -- setup verified real but query results structurally empty despite 3 rounds of harness verification; root-caused to a real, unresolved Ctxo-side persistence issue (see README), not fixed further within this session's scope."}
7+
{"id":"B15-CROSS-LANG-COMPETITOR-AB","claim":"calm vs CodeGraph vs Ctxo vs Context+, file-recall on callers, all 6 Tier-0 languages (python/rust/go/javascript/typescript/java)","benchmark":"b15_cross_lang_competitor_ab","calm_commit":"822e238efc54df32da36505cf25890c2302ee06f","competitor_versions":"@colbymchenry/codegraph@1.5.0, @ctxo/cli@0.11.4, contextplus@1.0.8","corpus_commit":"flask 36e4a824, fd 41532d11, gin 34dac209, express a3714473, zod 912f0f51, spring-petclinic 51045d16 -- see README Version pins section for full pins","harness_commit":null,"command":"see benchmarks/b15_cross_lang_competitor_ab/README.md Run section","raw_output_path":"benchmarks/b15_cross_lang_competitor_ab/README.md","canonical_result":"aggregate file-recall (Ctxo counts java only, see README disclosure): calm 68/72 (94.4%), codegraph 69/72 (95.8%), ctxo 23/24 (96.0%, java-only), contextplus 72/72 (100.0%)","status":"superseded","supersedes":[],"superseded_by":"B15R2-CROSS-LANG-COMPETITOR-AB-INHERITANCE-FIX","last_reproduced":"2026-08-18","evidence_gap":"Ctxo go/js/ts excluded from aggregate -- setup verified real but query results structurally empty despite 3 rounds of harness verification; root-caused to a real, unresolved Ctxo-side persistence issue (see README), not fixed further within this session's scope. Superseded because the calm numbers themselves were measuring 2 real, since-fixed bugs, not calm's steady-state capability -- see superseding entry."}
8+
{"id":"B15R2-CROSS-LANG-COMPETITOR-AB-INHERITANCE-FIX","claim":"Same B15 A/B, rerun after fixing 2 real bugs the first run's own investigation found: a benchmark oracle regex false-positive (Java control-flow lines wrongly excluded as call sites) and a real calm core-resolver bug (inherited-method calls through a statically-typed receiver silently dropped instead of falling back to ambiguous)","benchmark":"b15_cross_lang_competitor_ab","calm_commit":"see git log after 0d9aa3a for the pipeline.rs inheritance-fallback fix + regression test, not yet committed as of this entry's last_reproduced date","competitor_versions":"@colbymchenry/codegraph@1.5.0, @ctxo/cli@0.11.4, contextplus@1.0.8","corpus_commit":"same 6 pinned corpora as B15 (see README Version pins) -- java resample differs because the fixed oracle changes which symbols pass sample_symbols' filters, not because of cherry-picking","harness_commit":null,"command":"see benchmarks/b15_cross_lang_competitor_ab/README.md Run section","raw_output_path":"benchmarks/b15_cross_lang_competitor_ab/README.md","canonical_result":"aggregate file-recall (Ctxo counts java only, see README disclosure): calm 72/73 (98.6%), codegraph 69/73 (94.5%), ctxo 23/25 (92.0%, java-only), contextplus 73/73 (100.0%) -- calm's sole remaining miss is the already-known JS new-expression gap, unrelated to the inheritance bug this run fixed","status":"current","supersedes":["B15-CROSS-LANG-COMPETITOR-AB"],"superseded_by":null,"last_reproduced":"2026-08-18","evidence_gap":"Same Ctxo go/js/ts exclusion as the superseded entry (unchanged, unrelated to this run's fixes). Core-resolver fix (pipeline.rs::resolve_sites_to_edges) verified via a new regression test plus full cargo test -p calm-core/calm-server (1234+395 tests, 0 regressions) and a live re-query against the real spring-petclinic corpus, but was NOT re-verified against a live re-run of B12/B13/B7's own Java rows, which share the same oracle bug and could show similar movement if rerun."}

0 commit comments

Comments
 (0)