B7 — Task-Correctness benchmark (Phase 1: fd/Rust, flask/Python; Phase 2: express/JS, zod/TS, gin/Go; Phase 3: spring-petclinic/Java)
Measures whether the CALM-scripted refactor workflow (edit_context →
rename at each real reference → diff_impact) completes a real rename task
more correctly than a naive grep-and-edit workflow. Deterministic oracle
only — no LLM judge: (1) the corpus's own real build/test gate, and (2) an
independent reference-recall check against ground truth computed once,
outside either arm.
Differs from B11/B12 (tool-surface correctness: given a fixed query, is
the answer right?) — B7 measures task correctness: given a refactor,
does the whole edit loop complete it without breaking the corpus's own tests
or missing a real reference. This is the Serena-style claim ("8–12 manual
steps → 1 call, fewer errors") benchmarks/README.md names as B7's origin.
Full design rationale: docs/superskills/specs/2026-07-30-calm-dfb-levers-design.md
(§1 + its audit-design Risk Assessment).
Reuses B12's pinned registry (benchmarks/b12_tier1_tier2_tool_correctness/corpora.py)
directly:
| lang | corpus | build_cmd | test_cmd |
|---|---|---|---|
| rust | fd (sharkdp/fd) | (none — cargo test builds+tests in one step) |
cargo test --quiet |
| python | flask (pallets/flask) | uv sync --frozen |
uv run pytest -q |
| javascript | express (expressjs/express) | npm install |
npm test |
| typescript | zod (colinhacks/zod) | pnpm install |
pnpm test |
| go | gin (gin-gonic/gin) | go build ./... |
go test ./... |
| java | spring-petclinic (spring-projects) | (none — mvn test compiles+tests in one step) |
./mvnw -q test -Dtest='!*IntegrationTests' -DfailIfNoTests=false |
Go/gin was initially skipped in this session's first Phase-2 pass — go
wasn't installed and passwordless sudo was unavailable (verified live:
apt-cache policy golang-go showed a candidate package, but sudo -n true
failed). The user then provided sudo access specifically to unblock this
(one-time, not persisted anywhere), Go 1.22.2 was installed via
apt-get install golang-go, and gin was added the same day.
Verified live, not assumed — every build/test command below was confirmed correct by actually running it, not by reading a package.json/pyproject.toml/ go.mod and guessing:
- flask needs
uv sync --frozen+uv run pytest, not barepip install pytest— a first attempt with bare pip grabbed the latest pytest, whose internal_pytest.monkeypatch.notsetAPI (removed upstream) broke flask's owntests/test_cli.pyat collection time. flask pins test dependencies viauv.lock([tool.uv] default-groupsincludes"tests") and setsfilterwarnings = ["error"], so a version-mismatched pytest fails outright, not just warns. - express has no committed lockfile — plain
npm installis correct. - zod uses
pnpm(pnpm-lock.yamlpresent, notpackage-lock.json) — a barenpm installwould not respect its lockfile. - gin's
go.modrequires Go 1.25.0, newer than the installed 1.22.2 —GOTOOLCHAIN=auto(Go's default since 1.21) transparently downloaded 1.25.0 on firstgo build, no manual intervention needed. Verified by actually running the build, not assumed from the version mismatch alone. - spring-petclinic's Maven Wrapper +
~/.m2cache were already warm from an earlier session's language-support benchmarking, so no network/toolchain install was needed for Java. The task'stest_cmdexcludes the 4 Testcontainers-backed*IntegrationTestsclasses (they spin up real Docker MySQL/Postgres containers): a timed run including them took 2m04s on a cold image cache alone, real risk againstoracle.py::run_cmd's fixed 300s timeout given B7 runs build/test up to 3× per task (baseline + naive + calm arms). Excluding them costs zero oracle coverage — verified none of the 8 real reference sites fall in those 4 files.-DfailIfNoTests=falseis needed because-Dtestexclusion-only patterns otherwise make surefire treat "0 explicitly-matched classes" as a failure.
Every corpus is a fresh, throwaway clone per (task, arm) — mirrors B12's
prepare_worktree/cleanup_worktree pattern (crash-safe: a crash mid-run
just leaves a clone to garbage-collect), not B11's mutate-then-reset pattern.
Work copies live OUTSIDE the CALM repo tree (../calm-b7-work/, a sibling
directory), unlike B12's own .work/ (nested inside benchmarks/). This
matters specifically for Rust: cargo auto-discovers an ancestor [workspace]
if a crate is nested inside CALM's own cargo workspace — reproduced live this
session ("current package believes it's in a workspace when it's not") when
Phase 1's work copies were first placed inside the repo tree. B12 never hit
this because it only ever runs read-only calm serve queries against its
corpus copies, never a real cargo test inside them; B7 is the first
benchmark that does, so this is a genuinely new failure mode, not something
B12's own docstring warning (about the pinned source location) fully
covered.
Ground truth for "which files need touching for a complete rename" is
oracle.py::real_references — a bare-identifier, word-bounded, extension-
filtered git grep, built on top of (but broader than) B12's
ground_truth.git_grep_call_sites. Two real gaps were found and fixed while
building this, in two separate iterations:
- No file-extension filter (found on flask's
init_db): B12's raw oracle counted 5.rstdocumentation-prose mentions as Python "call sites" alongside 3 real.pyfiles. Fixed by restricting results to the corpus's own source extension. - Call-shaped-only pattern misses non-call references (found on zod's
prettifyError): a first version matched onlyNAME\((calls), which misses a bare re-export statement likeexport { prettifyError }(no trailing paren). A rename needs every bare-identifier reference — calls, re-exports, imports, type positions — not just calls, which is a genuinely different question from "who calls this" (what B12's oracle, and CALM's own call-graph-basedcallers(), are built to answer). Fixed by widening the pattern to match the bare identifier regardless of what follows it.
| task | baseline | naive build_pass | naive recall | naive tool_calls | calm build_pass | calm recall | calm tool_calls |
|---|---|---|---|---|---|---|---|
| rename_fd_pattern_matches_leading_dot | green | True | 1.0 | 3 | True | 1.0 | 4 |
| rename_flask_from_prefixed_env | green | True | 1.0 | 4 | True | 1.0 | 5 |
| rename_gin_clean_path | green | True | 1.0 | 3 | True | 1.0 | 4 |
| rename_express_set_charset | green | True | 1.0 | 4 | False | 0.667 | 4 |
| rename_zod_prettify_error | green | True | 1.0 | 5 | False | 0.5 | 4 |
| rename_petclinic_find_pet_types | green | True | 1.0 | 7 | True | 1.0 | 4 |
Java's row reflects the state after the parser fix below — see Phase 3 for the real bug this run found and fixed live, before this number was reachable.
All three arms hit perfect recall + a passing build, and naive uses fewer or
equal tool calls. Reported as measured, not hidden (project policy — cf.
B6's find_callers=0% precedent): all three symbols are distinctive enough
(unique corpus-wide, picked via B12's sample_distinctive filter) that a
repo-wide git grep already finds every reference. gin's case additionally
confirms why: Go has no equivalent of JS's require('./mod').fn(...)
property-access call shape for same-package calls (Go's intra-package calls
are always bare identifiers), so there's no structural opportunity for the
property-access gap seen in JS to appear here at all. This doesn't mean CALM
has no advantage on these three — it means these symbols don't exercise the
case where an advantage (or the JS/TS gap) would show up.
callers() alone is not sufficient for a complete rename
Naive (bare-identifier grep) hits 100% recall + a passing build on both
Phase-2 tasks. The CALM arm, which relies purely on edit_context's
callers list, fails the build on both — but for two structurally
different reasons, precisely distinguished by checking what was actually
missed:
express/setCharset — a genuine call-graph gap. lib/response.js binds
the import with a destructured bare name
(var setCharset = require('./utils').setCharset;) and calls it as
setCharset(type, 'utf-8') — CALM finds this fine (edge_confidence: "resolved"). test/utils.js instead does var utils = require('../lib/utils')
and calls it as utils.setCharset(...) — a property-access call through the
module namespace object. This is a real invocation (5 real test assertions
depend on it), yet it's completely absent from edit_context's response —
not in callers, not even in blast_radius.files_affected (29 files listed,
this isn't one of them). Verified directly against the raw edit_context
JSON, not inferred from the benchmark's summary. Renaming only what
callers() reports leaves test/utils.js calling a function that no longer
exists → TypeError: utils.setCharset is not a function (5 failing tests).
zod/prettifyError — a task-oracle scope issue, not (clearly) a CALM bug.
CALM's callers() did find the one real call
(z.prettifyError(...) in error-utils.test.ts) — so qualified/property-
access calls can resolve correctly in this codebase (an important
counter-example to reading the express finding as "property access never
resolves"). What it didn't report were packages/zod/src/v4/classic/external.ts
and mini/external.ts's bare re-export statements
(export { prettifyError }) — which aren't calls at all, so a call-graph
tool correctly has no reason to surface them via callers(). A complete
rename needs the import/re-export graph too, which is a different CALM tool
(dependencies()) or a plain text search (search(kind="grep")) — exactly
what AGENTS.md's own documented workflow already treats as a separate
stage from callers(), not something edit_context claims to cover on its
own.
What this means for a real CALM-based rename workflow: don't rely on
edit_context/callers() alone for a rename — it models the call graph,
not the full reference/import graph. A safe rename workflow should follow it
with a search(kind="grep") sweep for the bare identifier (or check
dependencies()) before considering the blast radius complete. This is
existing, documented CALM workflow guidance being validated by a concrete
failure case, not a new gap to file — except for the express finding, which
is a genuine, reproducible call-graph resolution gap worth investigating
in parser.rs's JS/TS call-site extraction (property-access calls on a
required module's bare identifier vs. a destructured bare-name call to the
same export).
live — this.field.method() calls were completely invisible
findPetTypes (PetTypeRepository interface method) was picked for the same
reasons as gin's cleanPath: a single, unambiguous definition, called from 2
production files and 3 test files with no name collision (ruled out
Owner.getPet, 3 overloads, and VetRepository.findAll, which collides with
JpaRepository's own inherited findAll).
First run: naive=1.0 recall/green build, calm=0.333 recall/False build —
CALM's own arm broke its own rename. edit_context("findPetTypes") (raw
JSON) reported only 2 caller edges, both in PetTypeFormatterTests.java,
missing PetController.java and PetTypeFormatter.java (production code)
and PetControllerTests.java/ClinicServiceTests.java. The naive build
failure output pinpointed exactly what didn't get renamed:
cannot find symbol: method findPetTypes().
Root cause, found by reading the actual call sites, not guessing: every
missed site calls this.types.findPetTypes() (this.-qualified field
access); the one call CALM did find is the only site written as a bare
types.findPetTypes() (no this.). That 100%-correlated split led straight
to crates/calm-core/src/indexer/parser.rs::last_ident_segment — the
function that extracts a call's receiver from Java's method_invocation
"object" field text. It split on -> (PHP) and :: (Rust/PHP scope) but
never on a plain . — so for the object text "this.types",
leading_ident walked from byte 0 and stopped at the first ., returning
"this" instead of "types". Tier-2 resolution then looked up a declared
type for the fake pseudo-variable "this", found nothing, and silently
dropped the call edge — no fallback, no low-confidence edge, nothing. This
makes CALM's call graph (and edit_context/callers() built on it) blind to
every this.field.method() call in Java — one of the two idiomatic
field-access styles, used specifically to disambiguate a field from a
same-named constructor parameter (this.types = types;, exactly
PetTypeRepository's own consumers' pattern, and a very common Spring/
enterprise-Java convention).
Fixed by adding . as a third segment separator (alongside ->/::),
taking whichever separator occurs last in the text — the same "rightmost
identifier segment" contract the function already documented, just extended
to the one separator style it was missing. Verified additive, not a behavior
change: PHP's $this->helper (no . in that text) and every existing
this-only receiver test (this.logIt(), no . after this) are
unaffected — confirmed by the full workspace test suite staying green (868
passed) plus a new regression test
(test_java_this_qualified_field_call_produces_receiver_not_this). Re-running
the benchmark after the fix (and a cargo build --release -p calm-cli to
pick it up): calm build_pass=True, recall=1.0, matching naive — see the
Results table above.
slugify (packages/zod/src/v4/core/util.ts:347) was tried first and
discarded: it passed B12's unique_definitions filter (which only scans
free function/class/interface/type regex patterns) but is actually a
3-way name collision — util.ts's free function is called by
api.ts::_slugify, re-exported as _slugify as slugify in checks.ts, and
there's also an unrelated _ZodString.slugify() fluent builder method
(schemas.ts) that calls checks.slugify(), never touching util.ts's
function at all. Plain-text rename can't tell these apart, so neither arm's
result on this symbol would have measured anything real. This is itself a
genuine finding: B12's unique_definitions doesn't see object-literal/
class-method definitions, only top-level declarations — worth knowing before
reusing it as a candidate-picker for an edit task (as opposed to its
original read-only tool-correctness use in B12).
- WORK_ROOT nested-workspace gotcha —
cargo testfailed with "current package believes it's in a workspace when it's not" until work copies moved outside the CALM repo tree (see Isolation above). - Double-escaped regex in the naive arm — an early draft's grep pattern
was built inside an
rf"..."raw string as\\((two literal backslashes(, an unbalanced/invalid ERE).git grepexited non-zero and printed nothing, so the naive arm silently "renamed" 0 files, trivially passed build/test, and scored recall=0.0. Caught by actually running the harness end-to-end, not by reading the code.
- Oracle extension-filter gap (flask's
init_db) and 4. call-shaped- only oracle gap (zod'sprettifyError) — see Oracle above. edit_context'sCallerEntryhas nopathfield — verified against the realedit_context.snapschema before writingrun_calm_arm: the file path is the substring ofsymbolbefore the first::(removed as a duplicate field — see the doc comment incrates/calm-server/src/tools/guardrails.rs). A naivec.get("path")would have silently returnedNonefor every caller.slugifyname-collision task pick — see "A rejected candidate" above.last_ident_segmentnever split on a plain.(Phase 3, spring- petclinic'sfindPetTypes) — a real, previously-undiscovered call-graph gap incrates/calm-core/src/indexer/parser.rs, not a benchmark-harness bug like 1/2 above. See "Phase 3" above for the full root-cause. Fixed inlast_ident_segment+ a new regression test (test_java_this_qualified_field_call_produces_receiver_not_this).
cargo build --release -p calm-cli # calm-cli must already be built
benchmarks/.venv/bin/python benchmarks/b7_task_correctness/run_benchmark.py
# or a single task:
benchmarks/.venv/bin/python benchmarks/b7_task_correctness/run_benchmark.py --task rename_fd_pattern_matches_leading_dotPreconditions: network access (fresh git clone --local of the pinned
sources, plus cargo's crates.io fetch, uv sync's, npm/pnpm install's,
and go build's module resolution on first run); uv on PATH for the flask
task, pnpm on PATH for the zod task, a Go toolchain (GOTOOLCHAIN=auto
handles a version mismatch against go.mod) for the gin task.
benchmarks/b7_task_correctness/.work (B12's own convention) is not used
here — see Isolation above; work copies land in ../calm-b7-work/ instead,
which is not committed and safe to delete between runs.
Per an explicit follow-up ask, the zod gap (§7.1/§6.1's "materially bigger feature") was fixed in
the same session. Two changes: (1) crates/calm-core/src/indexer/imports.rs now walks
export_statement for JS/TS and parses export {...} from '...' / export * from '...' into
import_edges (previously zero export syntax was ever indexed, for any JS/TS project); (2)
reference_impact's import-edge lookup (crates/calm-server/src/tools/trace.rs) is now a bounded
BFS through wildcard re-export chains instead of a single-hop query, closing zod's actual two-hop
barrel-file case. Full detail, including a real fallback-chain bug caught and fixed before it
shipped: docs/plans/2026-08-20-product-uplift-and-b7v2-roadmap.md §10.
cargo test --workspace --release: 0 failed. Re-ran all 6 B7 tasks end-to-end:
| task | calm_v2, before | calm_v2, after |
|---|---|---|
| rename_zod_prettify_error | 0.5/False | 1.0/True |
| the other 5 tasks (incl. express) | 1.0/True | 1.0/True (unchanged) |
B7 now passes 6/6 via the calm_v2 arm. calm v1 stays at 0.5/False for zod by design (v1
never calls reference_impact, the tool this fix lives in). Not covered, documented as a known
scoped-out gap: export * as ns from namespace re-exports, and an as-aliased re-export changing
the name partway through a chain.
The deeper audit above (triggered by a user follow-up: "is there truly no way to fix this?") found
that the actual root cause was neither reference_impact's coverage nor the parser — it was
build_resolution_context's path_lang map being derived only from the symbols table, so a
file with zero top-level named declarations anywhere (any Mocha/Jest/Vitest-style describe/it/
test file, including the real test/utils.js) never got a language entry, causing
resolve_sites_to_edges's same-language safety filter to empty out every outgoing call that file
made — regardless of confidence tier. Full root-cause trace: docs/plans/2026-08-20-product-uplift- and-b7v2-roadmap.md §8.
Fixed in crates/calm-core/src/indexer/pipeline/context.rs (path_lang now seeded from
file_index, which already tracks every indexed file's language regardless of symbol count) —
same doc's §9 has the full verification chain. Re-ran all 6 B7 tasks end-to-end against the fixed
binary:
| task | calm / calm_v2, before | calm / calm_v2, after |
|---|---|---|
| rename_express_set_charset | 0.667/False · 0.667/False | 1.0/True · 1.0/True |
| rename_zod_prettify_error | 0.5/False · 0.5/False | 0.5/False · 0.5/False (unchanged — separate bug, §7.1) |
| the other 4 tasks | 1.0/True | 1.0/True (unchanged, no regression) |
cargo test --workspace --release: 0 failed. New permanent regression test:
test_call_from_a_file_with_no_named_symbols_still_gets_a_call_edge in
crates/calm-core/src/indexer/pipeline.rs. Fix is currently uncommitted, pending the user's
go-ahead to commit/push.
Full rationale and audit trail: docs/plans/2026-08-20-product-uplift-and-b7v2-roadmap.md.
A third arm, calm_v2, was added (run_calm_arm_v2 in run_benchmark.py, alongside naive/
calm — calm v1 kept unchanged for comparison). It supplements edit_context's callers()
with reference_impact's must_change/likely_change hits, on the hypothesis (from
reference_impact's own source comment, crates/calm-server/src/tools/trace.rs:780-784, which
names both tasks below) that this tool — built specifically to close this benchmark's own
documented gap — would fix Express and Zod.
Result: it did not. Both tasks scored identically to v1:
| task | v1 (calm) recall/build |
v2 (calm_v2) recall/build |
|---|---|---|
| rename_express_set_charset | 0.667 / False | 0.667 / False |
| rename_zod_prettify_error | 0.5 / False | 0.5 / False |
The other four tasks (fd, flask, gin, spring-petclinic) stayed at 1.0/True in calm_v2 too — no
regression from the wider reference_impact surface.
Root-caused, not assumed:
- Zod:
reference_impactreturnedmust_change_count: 0forprettifyError. Both missing files re-export it viaexport { …, prettifyError, … } from "../core/index.js";. Traced tocrates/calm-core/src/indexer/imports.rs::import_node_types, which for"javascript" | "typescript"only walks["import_statement", "variable_declarator"]—export_statementis never walked at all, so noimport_edgesrow is ever created for a re-export, for any JS/TS project. This is a real, previously-undocumented gap in the import extraction, not somethingreference_impact's existing import-edge tier can see. (The two realz.prettifyError(...)call sites did get a call_edge, but at"ambiguous"confidence, correctly bucketed asreview—edit_context.callers()already covered those, so this was never where the miss came from.) - Express:
reference_impactproduced 0reviewhits and 7textual_onlyhits forsetCharset—test/utils.js'sutils.setCharset(...)(property access on a barerequire('../lib/utils')) still produces no call_edge at all. Confirms this is exactly the pre-existingparser.rscall-site-extraction gap already named in this section's own "Next steps" above — orthogonal to what an import/export-edge fix could address.
Correction to reference_impact's own source comment: its claim to catch "the exact gap
behind" both tasks holds for a plain-import-side reference, but not for Zod's actual export { X } from 'y' re-export shape, and not for Express's case at all (a different bug class — call-site
extraction, not import/export tracking). Worth narrowing that comment once the export_statement
gap below is fixed.
Follow-up, not done in this pass — revised after a deeper audit (full detail:
docs/plans/2026-08-20-product-uplift-and-b7v2-roadmap.md §6-§7): walking export_statement in
import_node_types/extract_imports_from_tree is real and necessary, but not sufficient on its
own for Zod — its actual re-export is a two-hop barrel chain (external.ts names prettifyError
re-exporting from core/index.ts, which itself re-exports errors.ts via a wildcard
export * from './errors.js' naming no symbols at all). A single-hop import_edges fix would
point external.ts at core/index.ts, not at errors.ts where the symbol is actually defined —
reference_impact's direct to_path lookup still wouldn't find it. Closing this needs transitive
import-edge resolution through wildcard re-export chains, not a single-function patch — re-run
rename_zod_prettify_error after landing whichever shape of fix to confirm empirically, not
assume (this file's own history is a live example of why: the original reference_impact source
comment claimed to close this gap and, empirically re-run here, did not). Express's gap needs a
separate parser.rs call-site-extraction fix (property-access call through a required module's
bare identifier), already scoped above — unaffected by anything import/export-related.
Also independently re-verified this session (adversarial self-audit, not just re-reading the
numbers): both failures reproduce identically outside the benchmark harness (npm test /
pnpm test run directly against the renamed corpus copies — same TypeError/TypeCheckError as
above); both symbols are collision-free and false-positive-free in the pinned corpora (manual grep
matches oracle_callsite_files exactly for both); and naive's "1.0 recall" on every task is close
to tautological by construction (run_naive_arm's file-selection regex and oracle.py's
ground-truth regex are near-identical) — the informative, independent signal is build_pass on
both arms, which is real compiler/test ground truth and was the first thing re-verified. See the
roadmap doc's §7 for the full adversarial audit trail.
- Investigate the express
setCharsetcall-graph gap directly inparser.rs's JS/TS call-site extraction (property-access call through a required module's bare identifier vs. a destructured bare-name call to the same export) — a candidate root-cause worth its own session, not folded into this benchmark's scope. (Phase 3's Java finding was this same shape of bug — receiver misattribution inparser.rs— so it's worth checking whether the.-split fix incidentally helps here too before assuming a separate root cause; not verified either way yet.)