Skip to content

Commit c10f72d

Browse files
Your Nameclaude
andcommitted
fix(bench): B2 oracle covers feature-gated files (--all-features) + coverage audit
Issue #72 (ask #2): a bare `rust-analyzer scip <repo>` compiles only DEFAULT Cargo features, emitting ZERO occurrences for source files behind non-default features (bundle.rs/index-bundles, lsp/*.rs/lsp-overlay, http.rs/http). CALM's tree-sitter indexer parses those unconditionally, so every CALM edge touching them could never match the oracle — inflating false-positives independent of resolver quality. - run_benchmark.py now runs rust-analyzer with --all-features (--ra-features, default "all") via a --config-path temp config. "all" not a hand-listed set: the gating features live on different workspace members (http on calm-server, not calm-core) and a per-name list would error on the crate that lacks it. - Adds an oracle-coverage audit to the output: oracle_covered_files, uncovered_from_files, and precision_on_covered (precision over only oracle-visible files) — the honest number the floors should eventually track. Verified on this repo: bundle.rs / lsp/overlay.rs / lsp/mod.rs go 0 -> present occurrences under --all-features (http.rs 2 -> 4); all-features .scip is larger, strictly more coverage. Threshold re-baseline (ask #3) is DEFERRED to after the std::/core::/alloc:: qualified-path fix (ask #1): per the issue, the remaining non-formal precision is dominated by BOTH gaps, so resetting floors now would need another reset. Documented in the B2 README limitations. Wave 0.2 of docs/plans/2026-08-19-evidence-architecture-execution-plan.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 042fff3 commit c10f72d

2 files changed

Lines changed: 79 additions & 2 deletions

File tree

benchmarks/b2_call_graph_quality/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ scip-dump` (dùng lại `calm_core::scip::parse`, không viết lại protobuf d
1919
index` (mặc định feature, tức **chỉ Phase A**, SCIP overlay không bật) rồi so `call_edges` (Rust)
2020
với oracle.
2121

22+
Oracle chạy với **`--all-features`** (`--ra-features`, mặc định `all`) — xem issue #72: một lần
23+
`rust-analyzer scip` mặc-định-feature không phát occurrence cho các file sau feature-gate
24+
(`bundle.rs` sau `index-bundles`, `lsp/*.rs` sau `lsp-overlay`, `http.rs` sau `http`, …), trong khi
25+
indexer tree-sitter của CALM parse chúng vô điều kiện — làm mọi edge CALM chạm các file đó không bao
26+
giờ khớp oracle và thổi phồng false-positive. Output giờ báo `oracle_covered_files`,
27+
`uncovered_from_files``precision_on_covered` (precision chỉ tính trên các file oracle thực sự
28+
thấy) để chỉ số coverage-gap luôn quan sát được.
29+
2230
## Kết quả đo lần đầu (self-repo, sau Phase A, trước khi bật SCIP overlay)
2331

2432
| | |
@@ -61,3 +69,9 @@ Theo `edge_confidence`:
6169
dưới thực tế chứ không phải chính xác tuyệt đối.
6270
- Chưa đo lần chạy có bật SCIP overlay (Phase B) — số trên là baseline Phase A thuần, mốc để đối
6371
chiếu khi B cải thiện.
72+
- **Bảng “Kết quả đo lần đầu” và floors trong `scripts/check-b2-thresholds.sh` chưa phản ánh
73+
oracle `--all-features` mới.** Việc re-baseline (đo lại honest baseline rồi reset floors) cố tình
74+
hoãn đến sau khi fix qualified-path `std::`/`core::`/`alloc::` (issue #72 ask #1) — issue tự ghi:
75+
~967 edge non-ambiguous/non-formal còn lại bị chi phối bởi cả coverage-gap (đã fix ở PR này) **
76+
qualified-path gap, nên reset floors bây giờ sẽ phải reset lại lần nữa. Đo + set floors một lần
77+
sau khi cả hai fix đã land.

benchmarks/b2_call_graph_quality/run_benchmark.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ def main() -> None:
150150
default=repo_root_from_here() / "target" / "release" / "calm",
151151
help="Path to a `calm` binary built with --features scip-overlay",
152152
)
153+
parser.add_argument(
154+
"--ra-features",
155+
default="all",
156+
help=(
157+
"Cargo features to activate for the rust-analyzer oracle run. "
158+
"Default 'all' (--all-features) so the oracle covers feature-gated "
159+
"source files CALM's tree-sitter indexer always parses (issue #72); "
160+
"pass a comma-separated list to narrow it."
161+
),
162+
)
153163
args = parser.parse_args()
154164
repo = args.repo.resolve()
155165
calm_bin = args.calm_bin.resolve()
@@ -163,8 +173,24 @@ def main() -> None:
163173

164174
with tempfile.TemporaryDirectory() as tmp:
165175
scip_path = Path(tmp) / "oracle.scip"
166-
print(f"Running rust-analyzer scip on {repo} ...")
167-
run([ra_bin, "scip", str(repo), "--output", str(scip_path)])
176+
# Oracle coverage fix (issue #72): a bare `rust-analyzer scip <repo>`
177+
# compiles only DEFAULT Cargo features, so it emits ZERO occurrences
178+
# for source files gated behind non-default features (bundle.rs behind
179+
# `index-bundles`, lsp/*.rs behind `lsp-overlay`, http.rs behind
180+
# `http`, ...). CALM's tree-sitter indexer parses those files
181+
# UNCONDITIONALLY, so any CALM edge touching them could never match the
182+
# oracle -- inflating the false-positive count independently of
183+
# resolver quality. Activating all features aligns the oracle's file
184+
# coverage with CALM's. `"all"` (not a hand-listed set) because the
185+
# gating features live on different workspace members (`http` on
186+
# calm-server, not calm-core) and a per-name feature list would error
187+
# on the crate that lacks it; --all-features cannot.
188+
feats = "all" if args.ra_features == "all" else args.ra_features.split(",")
189+
ra_config = Path(tmp) / "ra-config.json"
190+
ra_config.write_text(json.dumps({"cargo": {"features": feats}}))
191+
print(f"Running rust-analyzer scip on {repo} (features={args.ra_features!r}) ...")
192+
run([ra_bin, "scip", str(repo), "--output", str(scip_path),
193+
"--config-path", str(ra_config)])
168194

169195
dump = run([str(calm_bin), "scip-dump", str(scip_path)])
170196
occurrences = [json.loads(line) for line in dump.stdout.splitlines() if line.strip()]
@@ -184,6 +210,22 @@ def main() -> None:
184210
oracle_hit = {(e[0], e[1], e[2], e[3]) for e in matched}
185211
recall = len(oracle_hit) / len(oracle) if oracle else 0.0
186212

213+
# Oracle coverage audit (issue #72): a file the oracle never emitted an
214+
# occurrence for (still feature-gated even under --all-features, generated,
215+
# or otherwise excluded) can't corroborate ANY CALM edge originating in it,
216+
# so those edges depress precision for reasons unrelated to resolver
217+
# quality. Report coverage explicitly, plus a `precision_on_covered` that
218+
# scores only edges whose from-file the oracle can actually see -- the
219+
# honest number check-b2-thresholds.sh's floors should eventually track.
220+
oracle_files = {o["file"] for o in occurrences}
221+
calm_from_files = {e[0] for e in calm_edges}
222+
uncovered_from_files = sorted(f for f in calm_from_files if f not in oracle_files)
223+
covered_edges = [e for e in calm_edges if e[0] in oracle_files]
224+
covered_matched = [e for e in covered_edges if (e[0], e[1], e[2], e[3]) in oracle]
225+
precision_on_covered = (
226+
len(covered_matched) / len(covered_edges) if covered_edges else 0.0
227+
)
228+
187229
by_conf: dict[str, list[tuple]] = defaultdict(list)
188230
for e in calm_edges:
189231
by_conf[e[4]].append(e)
@@ -203,13 +245,34 @@ def main() -> None:
203245
for conf, stats in conf_precision.items():
204246
print(f"{conf:<12} {stats['count']:>8} {stats['precision']:>10.3f}")
205247

248+
print()
249+
print(
250+
f"Oracle file coverage: {len(oracle_files)} file(s) with >=1 occurrence; "
251+
f"{len(uncovered_from_files)}/{len(calm_from_files)} CALM from-file(s) "
252+
f"invisible to the oracle"
253+
)
254+
print(
255+
f"Precision on oracle-covered files only: {precision_on_covered:.3f} "
256+
f"({len(covered_matched)}/{len(covered_edges)})"
257+
)
258+
if uncovered_from_files:
259+
preview = ", ".join(uncovered_from_files[:8])
260+
print(
261+
f" uncovered from-files (first 8): {preview}"
262+
f"{', ...' if len(uncovered_from_files) > 8 else ''}"
263+
)
264+
206265
result = {
207266
"repo": str(repo),
208267
"oracle_edges": len(oracle),
209268
"calm_edges": len(calm_edges),
210269
"precision": precision,
211270
"recall": recall,
212271
"by_confidence": conf_precision,
272+
"oracle_covered_files": len(oracle_files),
273+
"calm_from_files": len(calm_from_files),
274+
"uncovered_from_files": len(uncovered_from_files),
275+
"precision_on_covered": precision_on_covered,
213276
}
214277
out_path = Path(__file__).parent / "results.json"
215278
out_path.write_text(json.dumps(result, indent=2))

0 commit comments

Comments
 (0)