Benchmark results¶
What was measured under the protocol in BENCHMARK.md. Each
fenced block is generated by the script its marker names, whose --check fails
when the block is stale, and recomputed by a test on every CI run. Prose
outside a fence is written by hand.
| Section | Generated by | Recomputed by |
|---|---|---|
| Gate 1 | bench/gate1_score.py |
tests/unit/test_gate1.py |
| Fix dry run | bench/dryrun_score.py |
tests/unit/test_dryrun.py |
| Round 1 | bench/summarize.py |
tests/unit/test_bench_summarize.py |
| Error analysis | bench/errors.py |
tests/unit/test_bench_errors.py |
| Comparison arm | bench/comparison.py |
tests/unit/test_bench_comparison.py |
Scan precision and recall (gate 1)¶
Computed from the frame in bench/gate1/sample.yaml, the five answer keys in
bench/gate1/labels/ and the scan records in bench/results/gate1/scan.json.
The keys were written before the scanner ran on those repositories. Definitions:
BENCHMARK.md; the reasoning behind
them is written up separately.
Frame drawn 2026-09-18T14:05:55Z over the term generativeai; 80 repositories in the frame, 80 scanned, 665 file(s) selected and 144 parsed -- the byte prefilter eliminated the rest -- for 701 findings over 210 file(s), 90 of which nothing bails in.
Measured with obelize 0.7.0 on CPython 3.12.9 / arm64, against pack gemini/google-generativeai-to-google-genai sha256 5017c6856ceb122c.... Nothing re-fetches eighty checkouts on CI, so these records are a dated artefact: a change to the scanner does not invalidate them visibly, and re-running python -m bench.gate1_collect scan is what makes the number current again. A change to what the pack tells a scan to look for does fail a test -- the projection fa59b7a16c9d722b..., which is the whole of what scan/ and impact/ read, and not the pack's own bytes, which move whenever a rewrite rule gains a parameter.
Precision and recall, over the hand-labelled repositories. Actionable kinds only (attribute, call, import, method_call); a row matches on path, line, kind and symbol together.
| Repository | Files read | Labelled | Found | TP | FP | Missed | Precision | Recall |
|---|---|---|---|---|---|---|---|---|
AyaanKhaan5/Story_Create_AI_Agent |
3 | 8 | 8 | 8 | 0 | 0 | 100.0% | 100.0% |
LucasHJin/vit |
3 | 13 | 13 | 13 | 0 | 0 | 100.0% | 100.0% |
Vaibhav8299/Conversational_AI_App |
7 | 20 | 20 | 20 | 0 | 0 | 100.0% | 100.0% |
xNabil/discord-ai-bot |
3 | 14 | 14 | 14 | 0 | 0 | 100.0% | 100.0% |
yihong0618/tg_bing_dalle |
4 | 9 | 8 | 8 | 0 | 1 | 100.0% | 88.9% |
| Total | 64 | 63 | 63 | 0 | 1 | 100.0% | 98.4% |
Everything else those repositories reported. Excluded from the rate above; the keys predict these rows too, and the positions they forbid outright are counted under the table.
| Kind | Reported | Expected by the keys | Unexpected |
|---|---|---|---|
manifest |
7 | 6 | 1 |
13 position(s) the keys forbid outright, 0 of them reported.
Repositories with at least one file where nothing bails. The number Phase 2's scope turns on: a file with an actionable eligible finding and no withheld finding.
| File-size band | Population | Scanned | Errored | With a zero-bail file | Share |
|---|---|---|---|---|---|
size:<1000 |
41,152 | 8 | 0 | 6 | 75.0% |
size:1000..4000 |
122,112 | 24 | 0 | 18 | 75.0% |
size:4000..16000 |
169,984 | 33 | 0 | 24 | 72.7% |
size:>16000 |
79,488 | 15 | 0 | 9 | 60.0% |
| Total | 412,736 | 80 | 0 | 57 | 71.2% |
Of the 80 repositories scanned, 47 had nothing withheld anywhere, 10 had a zero-bail file beside a withheld one, and 23 had no migratable file at all.
What withheld the rest. The ranked reason a real file is not migratable, which is the table Phase 2's rule order is read from.
| Bail | Rows | |
|---|---|---|
file_not_fully_migrated |
123 | derived from another row's bail |
multiple_assignments |
41 | |
repo_not_fully_migrated |
27 | derived from another row's bail |
receiver_unresolved |
24 | |
multiple_configure_calls |
17 | |
conditional_binding |
14 | |
model_object_escapes |
11 | |
local_import |
9 | |
client_source_unresolved |
9 | |
attribute_removed |
5 | |
module_alias_rebound |
3 | |
| Primary causes | 133 |
What repositories declare about their Python. blocked: runtime_unsupported is read from pyproject.toml, setup.cfg, a Pipfile or a string python_requires= in setup.py; this is how often any of that fires outside a fixture.
| Declares | Repositories | Reads as |
|---|---|---|
(none declared) |
75 | not blocked |
==3.10.* |
1 | not blocked |
==3.11.5 |
1 | not blocked |
>=3.11 |
1 | not blocked |
>=3.8 |
1 | blocked |
^3.10 |
1 | not blocked |
| Blocked | 1 |
Static resolution, over every usage in the frame. Gate 1 narrows scan when more than 30% of usages cannot be resolved statically.
| Usages | Statically resolved | Unresolved | Unresolved share | Gate 1 trigger |
|---|---|---|---|---|
| 615 | 577 | 38 | 6.2% | does not fire |
The one miss is client.last, an attribute read on an untyped function
parameter, in a module where client.send_message(...) on the same parameter
is reported: a defect, tests/fixtures/scan/COVERAGE.md gap 20, left unfixed
so the number is what the shipped scanner produces.
The one unexpected row is the key being incomplete: one requirements.txt
line yields two manifest findings, the added google-genai pin beside the
withheld legacy pin, in a repository that migrates one of its two modules.
The scan records were taken again for 0.7.0, against the scanner and the Gemini pack of this tree
(obelize scan --pack, since a scan without it runs every pack a repository uses), over the same 80
repositories at the same commits, so they read obelize 0.7.0. Every record is the one 0.6.0 gave,
finding for finding. The shares are the ones the record taken with 0.1.0 gave, 57 repositories (71.2%) with a zero-bail file
and 47 with nothing withheld, apart from one repository that now stops on the Python floor it
declares. Precision and recall are unchanged.
The zero-bail share decided Phase 2's scope. It clears the roughly 40% threshold pre-registered before the round, although the three development-corpus repositories the rules were written against produce no automatic edit. It describes small public applications, not codebases in general (the sample's biases).
Fix dry run¶
What the rules alone would write for each bundled pack over a stratified frame of eighty public
repositories that use its library. Each record is one obelize fix --pack <id> --model none dry run
at the repository's pinned commit, kept in bench/results/dryrun/; the frames are in
bench/gate1/. Definitions and the frames' biases:
BENCHMARK.md; the reasoning is
ADR-024 D10. The records were taken again for 0.7.0,
against the packs and the engine of this tree, over the same frames at the same commits, so they
read obelize 0.7.0. Every record is the one 0.6.0 gave, withheld row for withheld row. Each record names the sha of the pack it was
taken with, and tests/unit/test_dryrun.py fails when a pack has changed since.
gemini/google-generativeai-to-google-genai¶
obelize 0.7.0, pack sha256 5017c6856ceb, frame drawn 2026-09-18T14:05:55Z for generativeai (412,736 files in the index): 80 repositories planned, 0 errored.
| Outcome | Repositories | Share of planned | Share of those the pack ran on |
|---|---|---|---|
blocked: the pack stops before it plans, for the reason below |
1 | 1.2% | -- |
none: no usage found |
0 | 0.0% | 0.0% |
held: usages found, no file written |
40 | 50.0% | 50.6% |
partial: a file written, a row withheld |
12 | 15.0% | 15.2% |
complete: a file written, nothing withheld |
27 | 33.8% | 34.2% |
| Why the pack stopped | A manifest declares it | Repositories |
|---|---|---|
runtime_unsupported |
yes | 1 |
| Why a row was withheld | Rows | Repositories | |
|---|---|---|---|
file_not_fully_migrated |
185 | 48 | a consequence of another hold |
client_placement_ambiguous |
52 | 7 | |
repo_not_fully_migrated |
51 | 41 | a consequence of another hold |
multiple_assignments |
41 | 11 | |
response_shape_changed |
39 | 11 | |
receiver_unresolved |
24 | 11 | |
multiple_configure_calls |
17 | 2 | |
conditional_binding |
14 | 2 | |
model_object_escapes |
11 | 7 | |
client_source_unresolved |
9 | 3 | |
local_import |
9 | 5 | |
attribute_removed |
6 | 3 | |
history_parts_shape_incompatible |
3 | 1 | |
module_alias_rebound |
3 | 1 | |
alias_collision |
1 | 1 | |
configure_consumed_elsewhere |
1 | 1 | |
configure_kwargs_unsupported |
1 | 1 | |
from_import_unmigrated_symbol |
1 | 1 | |
manifest_pin_shape_unsupported |
1 | 1 | |
usage_unmapped |
1 | 1 |
| The 10 symbols that hold back most rows | Bail | Rows | Repositories |
|---|---|---|---|
google.generativeai.GenerativeModel |
multiple_assignments |
21 | 11 |
google.generativeai.GenerativeModel.generate_content |
response_shape_changed |
19 | 9 |
google.generativeai.GenerativeModel.generate_content |
multiple_assignments |
18 | 9 |
google.generativeai.configure |
client_placement_ambiguous |
15 | 7 |
google.generativeai.GenerativeModel |
client_placement_ambiguous |
12 | 4 |
google.generativeai.GenerativeModel |
response_shape_changed |
12 | 11 |
google.generativeai.ChatSession.send_message |
client_placement_ambiguous |
11 | 2 |
google.generativeai.GenerativeModel.generate_content |
receiver_unresolved |
11 | 8 |
google.generativeai |
local_import |
9 | 5 |
google.generativeai.GenerativeModel.generate_content |
multiple_configure_calls |
9 | 2 |
openai/openai-0-to-1¶
obelize 0.7.0, pack sha256 62b392e7f30d, frame drawn 2026-10-13T13:11:56Z for "openai.ChatCompletion" (89,152 files in the index): 80 repositories planned, 0 errored.
| Outcome | Repositories | Share of planned | Share of those the pack ran on |
|---|---|---|---|
blocked: the pack stops before it plans, for the reason below |
27 | 33.8% | -- |
none: no usage found |
0 | 0.0% | 0.0% |
held: usages found, no file written |
26 | 32.5% | 49.1% |
partial: a file written, a row withheld |
0 | 0.0% | 0.0% |
complete: a file written, nothing withheld |
27 | 33.8% | 50.9% |
| Why the pack stopped | A manifest declares it | Repositories |
|---|---|---|
legacy_version_unsupported |
no | 22 |
legacy_version_unsupported |
yes | 4 |
runtime_unsupported |
yes | 1 |
| Why a row was withheld | Rows | Repositories | |
|---|---|---|---|
repo_not_fully_migrated |
103 | 34 | a consequence of another hold |
file_not_fully_migrated |
79 | 24 | a consequence of another hold |
flag_only_surface |
72 | 17 | |
response_shape_changed |
34 | 19 | |
unsupported_kwarg |
25 | 8 | |
conditional_binding |
20 | 1 | |
legacy_version_unsupported |
11 | 8 | |
module_alias_rebound |
3 | 2 | |
from_import_unmigrated_symbol |
2 | 1 | |
attribute_removed |
1 | 1 | |
manifest_pin_shape_unsupported |
1 | 1 |
| The 10 symbols that hold back most rows | Bail | Rows | Repositories |
|---|---|---|---|
openai.ChatCompletion.create |
response_shape_changed |
23 | 17 |
openai.error.OpenAIError |
flag_only_surface |
18 | 4 |
openai.api_type |
flag_only_surface |
15 | 8 |
openai.ChatCompletion.create |
unsupported_kwarg |
14 | 7 |
openai.api_version |
flag_only_surface |
13 | 8 |
openai.ChatCompletion.create |
legacy_version_unsupported |
11 | 8 |
openai.Completion.create |
response_shape_changed |
8 | 3 |
openai.api_base |
conditional_binding |
8 | 1 |
openai.Completion.create |
unsupported_kwarg |
7 | 1 |
openai.error.InvalidRequestError |
flag_only_surface |
7 | 4 |
py-pdf/pypdf2-to-pypdf¶
obelize 0.7.0, pack sha256 34db4a1315d5, frame drawn 2026-10-13T13:17:31Z for PyPDF2 (157,888 files in the index): 80 repositories planned, 0 errored.
| Outcome | Repositories | Share of planned | Share of those the pack ran on |
|---|---|---|---|
blocked: the pack stops before it plans, for the reason below |
36 | 45.0% | -- |
none: no usage found |
2 | 2.5% | 4.5% |
held: usages found, no file written |
5 | 6.2% | 11.4% |
partial: a file written, a row withheld |
5 | 6.2% | 11.4% |
complete: a file written, nothing withheld |
32 | 40.0% | 72.7% |
| Why the pack stopped | A manifest declares it | Repositories |
|---|---|---|
legacy_version_unsupported |
no | 20 |
legacy_version_unsupported |
yes | 11 |
runtime_unsupported |
yes | 3 |
new_version_unsupported |
yes | 2 |
| Why a row was withheld | Rows | Repositories | |
|---|---|---|---|
flag_only_surface |
73 | 17 | |
legacy_version_unsupported |
49 | 16 | |
file_not_fully_migrated |
38 | 21 | a consequence of another hold |
repo_not_fully_migrated |
24 | 23 | a consequence of another hold |
conditional_binding |
11 | 3 | |
local_import |
8 | 6 | |
module_alias_rebound |
8 | 2 | |
alias_collision |
4 | 1 | |
runtime_unsupported |
4 | 1 | |
usage_unmapped |
4 | 1 | |
from_import_unmigrated_symbol |
3 | 1 | |
manifest_code_mismatch |
2 | 2 | |
new_version_unsupported |
2 | 1 | |
manifest_pin_shape_unsupported |
1 | 1 | |
roundtrip_mismatch |
1 | 1 |
| The 10 symbols that hold back most rows | Bail | Rows | Repositories |
|---|---|---|---|
PyPDF2.PdfFileReader |
flag_only_surface |
44 | 16 |
PyPDF2.PdfFileWriter |
flag_only_surface |
25 | 10 |
PyPDF2 |
legacy_version_unsupported |
22 | 16 |
PyPDF2.PdfReader |
legacy_version_unsupported |
19 | 15 |
PyPDF2 |
local_import |
8 | 6 |
PyPDF2 |
conditional_binding |
7 | 3 |
PyPDF2 |
module_alias_rebound |
4 | 2 |
PyPDF2.PdfReader |
conditional_binding |
4 | 3 |
PyPDF2._page.PageObject.create_blank_page |
usage_unmapped |
4 | 1 |
PyPDF2 |
runtime_unsupported |
3 | 1 |
The version gate comes before any rule. The openai pack plans nothing for a repository that uses
the library and declares it nowhere, or declares it with a pin under the pack's floor (>=0.27.0)
or in a form obelize cannot take. A declaration with no lower bound is taken as a release in
>=0.27.0,<1 where the code calls openai.ChatCompletion or openai.Audio, and stops otherwise.
That stops 27 of the 80 openai repositories, a third where it stopped about half under 0.5.0: 22
declare the library nowhere obelize reads, 4 declare it at a version or in a form the pack cannot
take, and 1 stops on the Python it declares. The PyPDF2 pack's floor is >=3, but a declaration
that names no version, or a range below 3 that admits 3.0.1, its newest release, is taken as that
release unless the repository shows another one
(ADR-056). It still stops 36 of its 80 repositories: 20
declare the library nowhere obelize reads, 11 declare it with a version under 3 or beside
evidence of another release, 3 stop on the Python they declare and 2 on a pypdf pin that admits
nothing the pack writes. The run says what to declare, so the stop is a message and not a silent
miss. Many of the stopped repositories carry a rule's own bail as well, so the stop is an upper
bound on what lifting it would write. The Gemini pack has no legacy-version gate and stops on a
Python floor only, which it did for 1 of its 80.
Since the 0.5.0 records, the same 80 repositories per pack moved as follows. For openai, 9 went from
blocked to complete, 6 from held to complete and 6 from blocked to held, and 1 went from held to
blocked on a Python floor that is now read; complete rose from 12 to 27. For PyPDF2, 2 went from
blocked to complete because a requirement.txt is now read, and 3 went to blocked (a pypdf pin
that admits nothing the pack writes, twice, and a Python floor read from setup.py, once); complete
rose from 31 to 32. For Gemini, 8 went from complete to held and 1 from partial to held, because a
read of response.text inside a bare, Exception, BaseException or ValueError handler is now
refused as response_shape_changed, which is the price of failing closed, and 1 went from held to
blocked on a Python floor read from a Pipfile or setup.py; complete fell from 35 to 27. Every
one of these is a plan that wrote something or did not, not a measure of whether it was right.
Beyond the gate, the symbols hold back differently by pack. For openai they are the exception
classes and settings the pack flags (openai.error.*, openai.api_type, openai.api_version),
keyword arguments it does not carry across, and result reads whose shape changed; for PyPDF2 the
camelCase classes (PdfFileReader, PdfFileWriter) the pack flags rather than renames; for Gemini,
client placement, a model name assigned more than once, configure called more than once and a
read of response.text that a handler could catch. In the openai pack one withheld row
holds the whole repository, because openai is one distribution
(ADR-053); in the other two it holds its file. A
repository held only by a notebook that calls the legacy API, which the pack reads and never
writes, has no cause row, so it adds nothing to the symbol table.
This is not an accuracy figure. There is no answer key, so complete means a plan wrote something,
not that what it wrote is right; the Gemini rounds below measure that. The openai frame is files
that call openai.ChatCompletion, so a repository that only embeds is missing from it. The Gemini
frame is the Gate 1 sample, drawn on 2026-09-18, and it holds some of the round-1 cases the Gemini
rules were written against, so its row is a comparator and not a holdout.
Round 1¶
The harness design runs in tiers.
Running the round found four defects in it, all in bench/ and none in
src/obelize/; each, its fix, the discarded first execution and what a
published result may carry are
written up separately.
Round 1, from bench/cases.yaml and bench/results/round-1/.
| Round | Cases (n) | dev / holdout | Unique patterns | Controls | Results on disk |
|---|---|---|---|---|---|
| 1 | 20 | 13 / 7 | 16 | 3 | 23, obelize 0.1.0, pack fbe82d40afea4894... |
What can reach the headline, computed from the keys before the round runs. verified_success needs a baseline pass, an after pass, tests that cover the change and no hunk a human had to write, so every row the key names has to be one a rule can make. A case with a manual row is capped below the headline whatever the scanner does, and so is a case whose key names a line the pack flags rather than edits.
| Case | Split | Can reach verified_success |
What caps it |
|---|---|---|---|
AmineChr54__CleanVision-Futury_AI-Hackathon |
holdout | no | no test command |
Arnav3241__Jarvis-v13 |
dev | no | no test command |
LucasHJin__vit |
dev | no | a row the key marks manual |
RuntimeAI__silly-merchants-for-safe-hackathon |
dev | no | no test command |
dongxnb__gupiao |
dev | no | no test command |
grapeot__devin.cursorrules |
holdout | no | a row the key marks manual |
humanbound__humanbound-firewall |
dev | no | a row the key marks manual |
navamai__navamai |
holdout | no | a row the pack flags rather than edits |
ImprintLab__Medical-Graph-RAG |
holdout | no | no test command |
LotusSerene__Gemini-Novel-Maker |
dev | no | no test command |
Omadithya07__Phidata-Multimodal-Video-Summarizer-Using-AI-Agents |
dev | no | no test command |
Sharukesh3__LLM-for-hydrogen-storage |
dev | no | no test command |
LJX2017__A-Song-of-Ice-and-Fire |
holdout | no | no test command |
Momen2410__Question-Answering-ChatBot-Using-Gemini1.5 |
dev | no | no test command |
Satheesh2908__AI-Assistant-for-Satheesh |
dev | no | no test command |
Zierax__HackerHelper |
dev | no | no test command |
kvcops__Virtual-Herbal-Garden |
holdout | no | no test command |
taitran501__dashboard_app |
holdout | no | no test command |
000wan__ara-ai-bot |
dev | no | no test command |
ashishkumar5de__PDF-QL |
dev | no | no test command |
| Total | 0 of 20 | 3 a row the key marks manual; 1 a row the pack flags rather than edits; 16 no test command |
The rate. Only verified_success counts toward it, and it is reported over cases and again over unique pattern_key values, each with its own n.
| Scope | n | verified_success | Rate |
|---|---|---|---|
| Cases | 20 | 0 | 0.0% |
| Cases (dev) | 13 | 0 | 0.0% |
| Cases (holdout) | 7 | 0 | 0.0% |
| Unique patterns | 16 | 0 | 0.0% |
| Unique patterns (dev) | 10 | 0 | 0.0% |
| Unique patterns (holdout) | 7 | 0 | 0.0% |
Outcome tiers. Computed from the result files, never assigned by hand.
| Tier | dev | holdout | Total |
|---|---|---|---|
verified_success |
0 | 0 | 0 |
patched_unverified |
1 | 0 | 1 |
partial |
3 | 2 | 5 |
unsupported |
9 | 5 | 14 |
wrong |
0 | 0 | 0 |
error |
0 | 0 | 0 |
Every case in the round, including the wrong, the errored and the unsupported.
| Case id | Split | Tier | Detected | FP | Missed | Unmigrated | Human edits | Runtime (s) |
|---|---|---|---|---|---|---|---|---|
AmineChr54__CleanVision-Futury_AI-Hackathon |
holdout | unsupported |
11 | 0 | 3 | 14 | -- | 1.795 |
Arnav3241__Jarvis-v13 |
dev | partial |
32 | 0 | 3 | 26 | -- | 2.514 |
LucasHJin__vit |
dev | unsupported |
13 | 0 | 0 | 14 | -- | 1.63 |
RuntimeAI__silly-merchants-for-safe-hackathon |
dev | unsupported |
5 | 0 | 0 | 6 | -- | 0.996 |
dongxnb__gupiao |
dev | unsupported |
27 | 0 | 0 | 27 | -- | 2.181 |
grapeot__devin.cursorrules |
holdout | partial |
5 | 0 | 4 | 9 | -- | 1.833 |
humanbound__humanbound-firewall |
dev | unsupported |
2 | 0 | 8 | 18 | -- | 1.271 |
navamai__navamai |
holdout | unsupported |
5 | 0 | 0 | 9 | -- | 1.242 |
ImprintLab__Medical-Graph-RAG |
holdout | unsupported |
23 | 1 | 0 | 22 | -- | 1.586 |
LotusSerene__Gemini-Novel-Maker |
dev | partial |
19 | 0 | 0 | 17 | -- | 1.863 |
Omadithya07__Phidata-Multimodal-Video-Summarizer-Using-AI-Agents |
dev | unsupported |
5 | 0 | 0 | 6 | -- | 0.86 |
Sharukesh3__LLM-for-hydrogen-storage |
dev | unsupported |
14 | 0 | 0 | 15 | -- | 0.981 |
LJX2017__A-Song-of-Ice-and-Fire |
holdout | partial |
19 | 2 | 4 | 14 | -- | 1.468 |
Momen2410__Question-Answering-ChatBot-Using-Gemini1.5 |
dev | unsupported |
5 | 0 | 0 | 6 | -- | 0.823 |
Satheesh2908__AI-Assistant-for-Satheesh |
dev | partial |
7 | 0 | 0 | 7 | -- | 0.94 |
Zierax__HackerHelper |
dev | unsupported |
3 | 0 | 2 | 6 | -- | 1.154 |
kvcops__Virtual-Herbal-Garden |
holdout | unsupported |
20 | 0 | 0 | 20 | -- | 2.571 |
taitran501__dashboard_app |
holdout | unsupported |
6 | 0 | 0 | 7 | -- | 2.196 |
000wan__ara-ai-bot |
dev | unsupported |
8 | 0 | 0 | 9 | -- | 1.441 |
ashishkumar5de__PDF-QL |
dev | patched_unverified |
2 | 0 | 0 | 0 | -- | 1.562 |
| Total | 231 | 3 | 24 | 252 | 30.9 |
What the suites did. A pass is not a suite that ran: the skip column is the difference, and tests_cover_change is still read from the key rather than from the run.
| Case id | Baseline | pass / fail / skip | After | pass / fail / skip |
|---|---|---|---|---|
LucasHJin__vit |
pass |
100 / 0 / 0 | pass |
100 / 0 / 0 |
grapeot__devin.cursorrules |
pass |
2 / 0 / 19 | pass |
2 / 0 / 19 |
humanbound__humanbound-firewall |
pass |
14 / 0 / 0 | pass |
14 / 0 / 0 |
navamai__navamai |
pass |
7 / 0 / 0 | pass |
7 / 0 / 0 |
Negative controls. They feed the false-positive table and are excluded from every rate above; an applied edit on one is a false positive by construction.
| Case id | Must produce | Tier | Findings | False-positive edits |
|---|---|---|---|---|
ParamveerSinghS__alex-ai-assistant |
no call site and no pin | unsupported |
0 | 0 |
GoogleCloudPlatform__Open_Data_QnA |
no call site and no pin | unsupported |
0 | 0 |
pfnet-research__japanese-lm-fin-harness |
no call site and no pin | unsupported |
0 | 0 |
Round 1's after-runs ran with google-generativeai still installed beside google-genai
(protocol step 7), so a file the migration missed still imported
and the after-run could pass over it. The after column of the suite table above stands as measured,
and round 1 has not been run again. It also recorded neither the installed packages nor the version
of uv, which later rounds do, and its records name the initial public commit.
Every case is capped for the reason the ceiling gave before the round ran.
navamai/navamai's cap is three mock.patch targets naming the legacy module
as a string, which the pack reports as flag_only_surface and never edits.
Nothing was wrong and nothing errored, controls included, and every
version assertion held. unsupported is fail-closed working: obelize reported
what it found and wrote nothing.
The scan agreement reproduced exactly: the round's fresh fetch and scan
gave the counts of the separately collected table in
bench/CASES.md.
The three false positives are two shapes. Two are
genai.GenerativeModel('gemini-pro').start_chat(history=[]) in
LJX2017/A-Song-of-Ice-and-Fire: the key names a call and a method_call,
the scanner reports one call named ...GenerativeModel.start_chat, so each
line is one false positive and two misses (COVERAGE.md gap 25). The third is
self.client.count_tokens(...) in a vendored camel/ module that imports the
legacy SDK elsewhere: an unresolved receiver whose method name the pack names,
reported by design.
One suite passes without running the covering test.
grapeot/devin.cursorrules skips the whole TestLLMAPI class,
test_create_gemini_client included, because it needs a local LLM server; its
key still says tests_cover_change: true. Checking that against the run needs
the key to name the covering tests: COVERAGE.md gap 27.
Gate 4 (PRODUCT.md) reads this rate, and it lands in the lowest branch. The written analysis that branch asks for is the error analysis; the decision it asks for is not made here. The one complete migration has no test command, and each case with a suite is capped by something its key named in advance.
Error analysis¶
Protocol: BENCHMARK.md; the reasoning behind it is written up separately.
Round 1, from bench/cases.yaml, bench/results/errors/round-1.json and bench/errors.yaml.
Where the rows went. Every row every answer key names, once. derived is one of the two atomicity codes and is a consequence of a row in the same file or repository, never a fact about itself; missed is a row the scan delivered nothing at, so it carries no bail code and its cause is hand-written.
| What happened to the row | Rows | Share |
|---|---|---|
migrated |
25 | 9.0% |
withheld |
129 | 46.6% |
derived |
98 | 35.4% |
missed |
25 | 9.0% |
| Total | 277 |
Every cause, and what happens about it. The lever is the number this table exists for: how many rows would migrate if this cause alone were fixed and nothing else were, under the same two atomicity rules the round ran with. Levers do not add up -- two causes in one file each buy nothing on their own -- so the column has no total.
| Cause | Class | Rows | Lever | Disposition | Recorded |
|---|---|---|---|---|---|
file_not_fully_migrated |
derived | 85 | +0 | limitation |
ADR-010 |
multiple_configure_calls |
withheld | 19 | +0 | limitation |
ADR-020 |
local_import |
withheld | 18 | +26 | rule_change |
COVERAGE 28 |
module_alias_rebound |
withheld | 17 | +18 | limitation |
ADR-019 |
repo_not_fully_migrated |
derived | 13 | +0 | limitation |
ADR-010 |
module_returned_from_function |
missed | 12 | +0 | limitation |
ADR-019 |
flag_only_surface |
withheld | 11 | +11 | limitation |
ADR-030 |
generation_config_not_static |
withheld | 11 | +26 | rule_change |
COVERAGE 29 |
model_object_read_elsewhere |
withheld | 9 | +13 | limitation |
ADR-006 |
client_placement_ambiguous |
withheld | 8 | +0 | limitation |
ADR-026 |
receiver_unresolved |
withheld | 8 | +12 | limitation |
ADR-019 |
history_parts_shape_incompatible |
withheld | 6 | +10 | limitation |
ADR-028 |
model_object_escapes |
withheld | 5 | +0 | limitation |
ADR-027 |
attribute_removed |
withheld | 4 | +0 | limitation |
ADR-030 |
multiple_assignments |
withheld | 4 | +9 | rule_change |
COVERAGE 30 |
client_name_collision |
withheld | 3 | +5 | limitation |
ADR-026 |
star_import_binding |
missed | 3 | +0 | limitation |
ADR-019 |
chained_constructor_one_row |
missed | 2 | +0 | scan_change |
COVERAGE 25 |
client_source_unresolved |
withheld | 2 | +0 | limitation |
ADR-020 |
module_from_subscript |
missed | 2 | +0 | limitation |
ADR-019 |
prefilter_token_absent |
missed | 2 | +0 | limitation |
ADR-022 |
alias_collision |
withheld | 1 | +0 | rule_change |
COVERAGE 31 |
attribute_read_via_getattr |
missed | 1 | +0 | scan_change |
COVERAGE 22 |
configure_consumed_elsewhere |
withheld | 1 | +4 | limitation |
ADR-031 |
from_import_unmigrated_symbol |
withheld | 1 | +0 | limitation |
ADR-025 |
manifest_filename_case |
missed | 1 | +0 | scan_change |
COVERAGE 33 |
manifest_filename_unknown |
missed | 1 | +0 | limitation |
ADR-021 |
text_mention_not_flagged |
missed | 1 | +0 | scan_change |
COVERAGE 32 |
usage_unmapped |
withheld | 1 | +0 | limitation |
ADR-031 |
| Total | 252 | -- |
The three dispositions, over the causes above.
| Disposition | Causes | Rows |
|---|---|---|
rule_change |
4 | 34 |
scan_change |
4 | 5 |
limitation |
21 | 213 |
Per case. Reachable is what would migrate with every bail this round raised lifted at once, which is an upper bound and not a plan; out of reach is the rest, and no rule change reaches it because no pass reported the row.
| Case id | Split | Key rows | Migrated | Reachable | Out of reach |
|---|---|---|---|---|---|
AmineChr54__CleanVision-Futury_AI-Hackathon |
holdout | 14 | 0 | 11 | 3 |
Arnav3241__Jarvis-v13 |
dev | 36 | 10 | 33 | 3 |
LucasHJin__vit |
dev | 14 | 0 | 14 | 0 |
RuntimeAI__silly-merchants-for-safe-hackathon |
dev | 6 | 0 | 6 | 0 |
dongxnb__gupiao |
dev | 27 | 0 | 27 | 0 |
grapeot__devin.cursorrules |
holdout | 10 | 1 | 6 | 4 |
humanbound__humanbound-firewall |
dev | 18 | 0 | 9 | 9 |
navamai__navamai |
holdout | 9 | 0 | 9 | 0 |
ImprintLab__Medical-Graph-RAG |
holdout | 22 | 0 | 22 | 0 |
LotusSerene__Gemini-Novel-Maker |
dev | 20 | 3 | 20 | 0 |
Omadithya07__Phidata-Multimodal-Video-Summarizer-Using-AI-Agents |
dev | 6 | 0 | 6 | 0 |
Sharukesh3__LLM-for-hydrogen-storage |
dev | 15 | 0 | 14 | 1 |
LJX2017__A-Song-of-Ice-and-Fire |
holdout | 21 | 7 | 19 | 2 |
Momen2410__Question-Answering-ChatBot-Using-Gemini1.5 |
dev | 6 | 0 | 6 | 0 |
Satheesh2908__AI-Assistant-for-Satheesh |
dev | 8 | 1 | 7 | 1 |
Zierax__HackerHelper |
dev | 6 | 0 | 4 | 2 |
kvcops__Virtual-Herbal-Garden |
holdout | 20 | 0 | 20 | 0 |
taitran501__dashboard_app |
holdout | 7 | 0 | 7 | 0 |
000wan__ara-ai-bot |
dev | 9 | 0 | 9 | 0 |
ashishkumar5de__PDF-QL |
dev | 3 | 3 | 3 | 0 |
| Total | 277 | 25 | 252 | 25 |
What the scan reported that no key claims. A false positive that is withheld is not a false-positive edit, and none of these was applied.
| Case id | Where | Symbol | Cause | Disposition | Recorded |
|---|---|---|---|---|---|
ImprintLab__Medical-Graph-RAG |
camel/utils/token_counting.py:422 |
google.generativeai.GenerativeModel.count_tokens |
method_name_collision |
limitation |
ADR-019 |
LJX2017__A-Song-of-Ice-and-Fire |
basic_game.py:27 |
google.generativeai.GenerativeModel.start_chat |
chained_constructor_one_row |
scan_change |
COVERAGE 25 |
LJX2017__A-Song-of-Ice-and-Fire |
reconstruct_timeline.py:10 |
google.generativeai.GenerativeModel.start_chat |
chained_constructor_one_row |
scan_change |
COVERAGE 25 |
The two derived codes are consequences, not causes.
file_not_fully_migrated is a ready row in a file that was not;
repo_not_fully_migrated is a legacy pin kept until the code importing it is
gone. Both are atomicity working as
the fixture rules say it should.
generation_config_not_static ties for the largest lever, against a written
prediction. Every instance is a name assigned once and readable: a
module-level genai.GenerationConfig(...), a module-level dict literal, a local
built two statements above the call. Tracing this bail
through the three-repository development corpus found it bought no edit;
that corpus under-represented the commonest way to write a generation config.
multiple_configure_calls is worth nothing on its own, because every file
it touches also carries local_import. Ranked by count it would come second.
One alias_collision has a lever of zero, because the file it sits in also
carries three client_placement_ambiguous rows.
Omadithya07/Phidata-Multimodal-Video-Summarizer-Using-AI-Agents imports
from google.generativeai import upload_file,get_file on line 5 and
import google.generativeai as genai on line 6: rewriting the first asks for
the alias the second already binds.
One lever must not be taken. history_parts_shape_incompatible is two
lines, in two near-identical copies of one file, reading
model.start_chat(history=str(time.time())): a float rendered as a string,
which is not a history in either SDK. A rule that rewrote it would migrate a
defect, which is why the lever column carries a disposition, not a ranking.
Three causes are one wall. module_returned_from_function,
star_import_binding and module_from_subscript are the legacy module bound
to a name through a function return, a star import in another file or a
container subscript, and the resolver follows none of the three. That, not any
rewrite rule, is where the round's recall went.
Lifting every bail would not move the headline. It would complete twelve of
the twenty repositories, but ten of those have no test command, one has a
manual row and one a line the pack flags, so all twelve stay capped below
verified_success.
One key row was wrong, and correcting it moved no published number; see
bench/CASES.md.
Comparison arm¶
Protocol: BENCHMARK.md; the reasoning behind
it is written up separately.
Obelize's cleared rows under the shared scan equal what the round published
from its plan and withheld list, for every case;
tests/unit/test_bench_comparison.py asserts it, and that agreement is what
licenses scoring the agent, which publishes no plan, the same way.
The agent's side is a snapshot from 2026-09-22 and was not rerun for 0.1.0;
its guide and the model behind it may have moved since. Obelize's side is
current: Satheesh2908/AI-Assistant-for-Satheesh's count was corrected by
hand from five cleared rows to one, because a fix landed on 2026-09-24
teaching the rule not to remove a configure() another file still runs on,
and four of the five rows this arm once credited relied on exactly that
removal.
Round 1, development split, from bench/cases.yaml, bench/results/comparison/round-1.json, bench/results/round-1/ and bench/comparison.yaml. The other arm is a general-purpose coding agent given the migration guide -- snapshotted 2026-09-22T07:20:07Z, sha256 223b9cd7aabf -- and one fixed prompt, and nothing else.
Both arms, the same cases. Rows migrated is a key row whose legacy usage a scan of the tree afterwards no longer reports; corrections left is every key row still legacy plus every edit the review rejected.
| Arm | Cases | verified_success |
Key rows | Rows migrated | Corrections left | Runtime |
|---|---|---|---|---|---|---|
| Obelize | 13 | 0 | 174 | 17 (9.8%) | 157 | 18.2s |
| General agent | 13 | 1 | 174 | 154 (88.5%) | 22 | 14.3m |
The tiers. The same ladder for both arms, which is the round's own minus the two rungs that read a findings document -- an agent publishes none. Over this split the ladder returns the tier the round published for every obelize case.
| Tier | Obelize | General agent |
|---|---|---|
verified_success |
0 | 1 |
patched_unverified |
1 | 7 |
partial |
3 | 3 |
unsupported |
9 | 0 |
wrong |
0 | 2 |
error |
0 | 0 |
Per case. Seen is the key rows the scanner reports on the untouched tree; the rows it does not see are credited to neither arm and are in left for both. Off key is files an arm changed that the key names no row in, which is a pointer for the review and not a verdict.
| Case id | Key rows | Seen | Arm | Cleared | Left | Corrections | Files | Off key | Runtime | Tier |
|---|---|---|---|---|---|---|---|---|---|---|
Arnav3241__Jarvis-v13 |
36 | 33 | Obelize | 10 | 26 | 26 | 3 | 0 | 2.5s | partial |
| General agent | 28 | 8 | 9 | 9 | 0 | 1.9m | wrong |
|||
LucasHJin__vit |
14 | 14 | Obelize | 0 | 14 | 14 | 0 | 0 | 1.6s | unsupported |
| General agent | 14 | 0 | 0 | 4 | 2 | 1.4m | verified_success |
|||
RuntimeAI__silly-merchants-for-safe-hackathon |
6 | 6 | Obelize | 0 | 6 | 6 | 0 | 0 | 1.0s | unsupported |
| General agent | 6 | 0 | 0 | 2 | 0 | 51.0s | patched_unverified |
|||
dongxnb__gupiao |
27 | 27 | Obelize | 0 | 27 | 27 | 0 | 0 | 2.2s | unsupported |
| General agent | 27 | 0 | 0 | 5 | 0 | 2.4m | patched_unverified |
|||
humanbound__humanbound-firewall |
18 | 10 | Obelize | 0 | 18 | 18 | 0 | 0 | 1.3s | unsupported |
| General agent | 10 | 8 | 9 | 5 | 1 | 2.0m | wrong |
|||
LotusSerene__Gemini-Novel-Maker |
20 | 20 | Obelize | 3 | 17 | 17 | 3 | 0 | 1.9s | partial |
| General agent | 20 | 0 | 0 | 5 | 0 | 1.1m | patched_unverified |
|||
Omadithya07__Phidata-Multimodal-Video-Summarizer-Using-AI-Agents |
6 | 6 | Obelize | 0 | 6 | 6 | 0 | 0 | 0.9s | unsupported |
| General agent | 6 | 0 | 0 | 3 | 1 | 31.0s | patched_unverified |
|||
Sharukesh3__LLM-for-hydrogen-storage |
15 | 14 | Obelize | 0 | 15 | 15 | 0 | 0 | 1.0s | unsupported |
| General agent | 14 | 1 | 1 | 3 | 0 | 1.2m | partial |
|||
Momen2410__Question-Answering-ChatBot-Using-Gemini1.5 |
6 | 6 | Obelize | 0 | 6 | 6 | 0 | 0 | 0.8s | unsupported |
| General agent | 6 | 0 | 0 | 2 | 0 | 33.0s | patched_unverified |
|||
Satheesh2908__AI-Assistant-for-Satheesh |
8 | 7 | Obelize | 1 | 7 | 7 | 1 | 0 | 0.9s | partial |
| General agent | 7 | 1 | 1 | 5 | 1 | 28.0s | partial |
|||
Zierax__HackerHelper |
6 | 4 | Obelize | 0 | 6 | 6 | 0 | 0 | 1.2s | unsupported |
| General agent | 4 | 2 | 2 | 2 | 0 | 21.0s | partial |
|||
000wan__ara-ai-bot |
9 | 9 | Obelize | 0 | 9 | 9 | 0 | 0 | 1.4s | unsupported |
| General agent | 9 | 0 | 0 | 2 | 0 | 30.0s | patched_unverified |
|||
ashishkumar5de__PDF-QL |
3 | 3 | Obelize | 3 | 0 | 0 | 2 | 0 | 1.6s | patched_unverified |
| General agent | 3 | 0 | 0 | 3 | 1 | 1.2m | patched_unverified |
|||
| Total | 174 | 159 | Obelize | 17 | 157 | 157 | 0 | 18.2s | ||
| Total | General agent | 154 | 20 | 22 | 6 | 14.3m |
Where an arm went outside the key, and what the review made of it.
| Case id | Arm | Files off the key | New legacy usage | Edits the review rejected |
|---|---|---|---|---|
Arnav3241__Jarvis-v13 |
General agent | -- | 0 | 1 |
LucasHJin__vit |
General agent | CLAUDE.md, docs/AI_MERGE_DETAILS.md |
0 | 0 |
humanbound__humanbound-firewall |
General agent | CHANGELOG.md |
0 | 1 |
Omadithya07__Phidata-Multimodal-Video-Summarizer-Using-AI-Agents |
General agent | README.md |
0 | 0 |
Satheesh2908__AI-Assistant-for-Satheesh |
General agent | README.md |
0 | 0 |
ashishkumar5de__PDF-QL |
General agent | README.md |
0 | 0 |
Run it twice. Obelize's answer is byte-identical output, asserted since Phase 1. This is the other arm's, measured over the same starting bytes.
| Case id | Files touched | Same files | Same line counts | Cleared, run 1 / run 2 | Runtime, run 1 / run 2 |
|---|---|---|---|---|---|
Arnav3241__Jarvis-v13 |
9 | yes | no | 28 / 28 | 1.9m / 2.9m |
LotusSerene__Gemini-Novel-Maker |
5 | yes | no | 20 / 20 | 1.1m / 57.0s |
ashishkumar5de__PDF-QL |
3 | yes | no | 3 / 3 | 1.2m / 1.0m |
The agent cleared about nine times as many rows, about forty-five times slower.
Both of its wrong cases are real defects, and a suite caught one. In
humanbound/humanbound-firewall, whose suite covers the change,
_import_genai() exists to turn a missing optional dependency into
an ImportError naming the [gemini] extra. from google import genai does
not raise there, because google is a namespace package, so the user gets
AttributeError: 'NoneType' object has no attribute 'Client' and a test fails,
although the agent also rewrote both test modules, including the lazy-import
guard's FORBIDDEN_MODULES. In Arnav3241/Jarvis-v13 it took the lever the
error analysis above says must not be taken, turning
start_chat(history=str(time.time())) into
client.chats.create(history=[types.Content(role='user',
parts=[types.Part(text=str(time.time()))])]): a user turn invented from a
Unix timestamp.
The agent's account of its work was wrong on the largest case. Its report
for Arnav3241/Jarvis-v13 says a repository-wide grep finds no legacy
reference. Tests/r1.py has five, which neither run touched, and they are the
whole gap between the rows the scanner sees there and the rows the agent
cleared. Only the answer key shows it.
Comparing the arms exposed a scanner defect. Round 1 matches rows on
(path, line, symbol), so key and scanner never had to agree on kind. In
humanbound/humanbound-firewall the key grades six sys.modules stubs
dynamic and the scanner reports them as text_mention, which is
not_a_usage: a repository whose tests mock the legacy module has those lines
left behind silently, and tests_touched_by_migration never fires. The agent
had to rewrite all six. COVERAGE.md gap 34.
The agent is not repeatable. Its re-runs touched the same files and
cleared the same rows with different line counts, and on
Arnav3241/Jarvis-v13 the first run turned a line into a fabricated turn and
the second deleted it, which keeps behaviour: same input, same score, opposite
judgement about a defect.
The ceiling caps a rule, not a tool. On LucasHJin/vit the agent migrated
all eight manual rows and the project's hundred tests pass, so it reached
verified_success on a case the ceiling closes to a rule. It did so with an
option the key did not list: an adapter class that is a model object,
leaving the six callers and the seven tests that patch that seam untouched. The
review accepted it; whether the abstraction is worth carrying is the
maintainer's call. So the two cases where a suite could speak produced one
verified success and one regression.
Obelize is ahead on what a reviewer must read, not on accuracy: no rejected
edit, no file off the key, no new legacy usage, byte-identical output, and a
run.json naming every refused row and why. The agent's off-key edits to
READMEs, a changelog and project documentation were not asked for and are
recorded nowhere a reviewer would see.
Limits. One agent, one day, one guide snapshot, and only the split obelize's rules were tuned against. The agent ran up to eight cases concurrently on one machine, so its runtime is an order of magnitude, not a benchmark. Every free choice favoured the agent.
Retired cases¶
None. A case that leaves the active set is listed here with its reason.
| Case id | Retired in round | Reason |
|---|---|---|