ADR-024: Gate 1 measurement¶
Status¶
Accepted; amended by ADR-026 (D9).
Decision¶
D1. The gate measurement is not a benchmark round, and it says so in the file it shares¶
The scan measurement has its own frame and keys (bench/gate1/), collector
(bench/gate1_collect.py), scorer (bench/gate1_score.py) and records
(bench/results/gate1/scan.json), and needs no SDK install or test run. Its
numbers live in the generated gate1 block of
BENCHMARK_RESULTS.md,
under its own heading, because "how good is it" belongs in one file.
D2. The answer keys are written before the scan, and committed¶
Every key under bench/gate1/labels/ is written from the repository's source
before any obelize scan output for it exists, because a key written after the
output transcribes it. Only what
BENCHMARK.md allows is
committed (URL, SHA, license, line references, notes), and the collector drops
Finding.evidence, so no source is vendored.
D3. A row is its path, its line, its kind and its symbol¶
A finding on the right line naming the wrong symbol is one false positive and
one miss, because obelize scan prints the symbol. Rows compare as multisets,
so two findings sharing all four fields are two rows.
D4. The rate is computed over four kinds, and the other five are published beside it¶
Precision and recall count import, call, method_call and attribute.
text_mention, manifest, dynamic, star_import and parse_error are
not call sites, so they are tabled apart against the count each key predicted.
Each key also lists must_not_report positions, where nothing may be reported.
D5. Precision over zero findings is undefined, and is printed as --¶
0 / 0 is not 1: a scanner that reported nothing would otherwise post perfect
precision.
D6. A zero-bail file needs an actionable eligible finding and no withheld finding¶
Both halves are ADR-010 F-1 read back: one bail leaves the whole file
untouched, and an eligible text_mention qualifies nothing because no rule
acts on it. The count of repositories with at least one zero-bail file is what
decided Phase 2's scope.
D7. Gate 1's three clauses are replaced by value sets, in the document¶
docs/BENCHMARK.md defines the rate's denominator, the two confidence_reason
sets that mean "cannot be resolved statically", and says the prefilter's
file-level precision (42.9%) is not a false-positive rate. A test asserts the
sets are disjoint vocabulary members that leave out exactly the four reasons
that are not a usage. PRODUCT.md keeps the gate's wording, because a change
to a gate is written where its readers are.
D8. The frame is stratified on file size, and the drawn list is committed rather than the query¶
GitHub code search is relevance-ordered and favours short, simpler files, so
the draw is split into four file-size bands, allocated by population and
sampled systematically within each. The index changes daily, so
bench/gate1/sample.yaml commits the drawn list, the band populations and
every rejection; the development-corpus owners are excluded.
D9. The scorer is offline, and a test recomputes what the document says¶
bench/gate1_score.py reads only committed files, so tests/unit/test_gate1.py
recomputes the published block on every CI run. It binds the records to the
shipped pack's spec_sha256, the digest of what a scan reads, and not to the
pack file's sha256, which a rewrite-only parameter moves. The scorer is under
the 100% branch gate; the collector clones repositories and is omitted from
coverage in pyproject.toml.
D10. A dry run asks any pack's frame what the rules alone would write¶
python -m bench.gate1_collect dryrun --pack <id> runs fix --model none over a pack's frame, which
sample --pack <id> --term <t> draws as D8 does; the Gemini frame keeps its place and every other
pack's sits in bench/gate1/<provider>__<change>/. A record keeps the plan's file count, the run
record's blocked and its withheld rows with their symbols, and from findings.json the number of
rows the scanner calls a usage (used: not a manifest row, not a not_a_usage) and of manifest rows.
The plan alone is not enough: with no manifest declaring openai it is empty, and so it is for a
repository the pack never ran on. The scorer bench/dryrun_score.py renders the dryrun block of
BENCHMARK_RESULTS.md, and tests/unit/test_dryrun.py recomputes it and
binds each record to the shipped pack's sha256, since a pack edit moves a dry run. The command
runs a whole frame and never a subset, so a retry cannot replace the other records. It has no
answer key, so it counts what is written and not whether it is right.
Consequences¶
- Gate 1 is met, neither its
scanfallback nor Phase 2's pre-registered narrowing fires, and the numbers are only in the generated block. bench/is undermypy --strict, and its pure half under the branch gate.- A scanner change leaves the records describing an older scanner and no test
notices; re-running
python -m bench.gate1_collect scanis manual. A rule change that leaves a pack file as it was does the same to the dry-run records, whoseobelize_versionsays which tool wrote them. - The one miss is open as
tests/fixtures/scan/COVERAGE.mdgap 20.