Repository navigation
v0.4.0
Benchmark
I did not run the benchmark again for this release. Its numbers are still 0.1.0's and are for the
Gemini pack alone: scan precision 100.0% and recall 98.4% over 64 usages in five repositories, and
25 of 277 usages (9.0%) migrated in 20 repositories with no wrong edit, on the
results page. The
scanner and the rewriter have changed since, so read those numbers as dated. The rule for a held
generation config is measured without the tests: a fix dry run over the 23 round-1 repositories at
their pinned commits gives the rows 0.3.0 gave, except 26 that moved from needs_review to auto
(20 in kvcops__Virtual-Herbal-Garden, 6 in
Momen2410__Question-Answering-ChatBot-Using-Gemini1.5). That says what is written, not whether it
passes a repository's tests, which a new round measures, with those two repositories in dev.
openai/openai-0-to-1 and py-pdf/pypdf2-to-pypdf have no benchmark. They rest on fixtures, on
each change measured against both library versions installed side by side, and on a weekly job that
runs their facts on the oldest and newest releases.
Unsupported patterns
docs/KNOWN_ISSUES.md lists
what I know about and have chosen not to fix yet. The gaps listed under 0.3.0 stand, except that
Python 3.15 is supported. The Gemini pack still withholds a model and the chat it starts in one
chain (GenerativeModel(...).start_chat(...)), and a generation config or safety settings held by a
name that is assigned twice, changed after its assignment, passed on, read by anything but the call
or taken as a parameter.
Added
fix --applyandverifyprint one line on stderr before each phase of tests: how many
commands are about to run and after how many seconds each is stopped, so a long suite no longer
looks like a hang. It names no command, since one can carry a credential, and stdout is unchanged.- The Gemini pack rewrites a
generation_configorsafety_settingsheld by a name, where it
refused asgeneration_config_not_staticorsafety_settings_not_static(COVERAGE gap 29).
The name needs one plain assignment, a mapping, a list of mappings or aGenerationConfig(...),
and no reader but that argument of a call the rule rewrites, so nothing else sees the value or
changes it between the assignment and the call. The keys are written into each call. A module's name stays where it
is, and aGenerationConfig(...)it holds becomes the new SDK'sGenerateContentConfig(...);
a function's own goes with the model, and so does theGenerationConfig(...)it holds. A name
assigned twice, changed by a stored key orupdate, passed on, read elsewhere or a parameter is
still refused. A module's name that another module changes after importing it is not seen.
Changed
scanandfixdo less work on a large repository and write the same findings. The pass that
finds a module another one reads (model_object_read_elsewhere) parses only the modules that
spell both the module's name and a name read, and walks each once, where it walked every parsed
module again for each model (9.5 s to 3.5 s on 800 synthetic modules).fixno longer parses
a file that only mentions the library in prose or a comment: 28 s to 4 s on 200 files, 180 of
them such. A pack the repository does not use, with no code to migrate and no pin to judge, no
longer parses the files that name a module only its legacy distribution installed, and one that
does reads imports only where a statement can hold one (11 s to 6 s over 5,000 files).
End to end,fixtakes 4.6 s on a repository of 1,000 files where 200 use the legacy SDK, which
took 40 s, and 4.4 s on one file of 800 calls, which took 15 s.patch.difffor a file of more than four million line pairs (about 2,000 lines each side) is
made without the matcher's repeated-line pass, which cost the square of a repeated line's
count. Below that size the hunks are the ones 0.3.0 wrote; above it a hunk can be larger or
split differently, andgit applyreads the patch the same.
Fixed
- A second
obelize undoof a run exits0when every file already holds its original, where it
exited4as if something had gone wrong, and it leaves the firstundo.json, which says what
was put back, instead of replacing it with a row ofhash_mismatchskips.undo.json's
exit_codeis now derived from files that were reverted or already original. - The note
obelize verifyappends toREPORT.mdnames the commands it ran, on one line, so a
reader of the report is not left with the first run's. A command with a backtick or a newline in
it stays inside its code span: the fence is longer than any run of backticks, and the newline
is spelled out. - A run folder that an older obelize wrote and this one cannot read now names the release that
wrote it and the command that undoes the run (uvx obelize@0.2.0 undo --run <id>), whereundo
andverifyonly said the record was unreadable. - The terminal output of
scanandfixhas less in it that nobody acts on. A planned file shows
path (N hunk(s))and not the list of rules that produced the hunks, whichREPORT.mdand
plan.jsonkeep. An eligible row has a blank reason, where it printed the code that resolved
its name. A dry run prints noVerification:line and itsREPORT.mdno empty phase, since no
test was attempted, and two packs that suggest the same tests say them once. fix --applyno longer refuses a dirty tree when it has nothing to write. A repository where
every finding is held back used to exit5with "uncommitted changes", which hid the real
reason; it now exits4with the findings. A configured model keeps the gate, since a
proposal could write.fix --apply --verify "<cmd>"looks for the command's program before it reads a file. A typo, or
a.venv/bin/pythonthat is not there, is a usage error (exit2) and nothing is written; it
used to write the patch and endinconclusive(exit6) withcommand_not_executable. A
command from.obelize.ymlor the allowlist is still started, and still ends that way.- When the commands you pass to
--verifychange a tracked file, the refusal offix --apply
says they did, where it asked you to commit or stash changes you had not made. The tree was
clean when the run began, whichgit_dirty: falsealready recorded. - A verification command that timed out gets a hint naming
--timeout <s>and the default of 600
seconds, which the flag's help anddocs/CLI.mdnow state.
Security
fix --applytries to make a file in each directory it will write into before it writes any,
and refuses the whole apply asunreadablefor a directory that takes none. A read-only
directory used to stop the write partway and leave a half-migrated tree; a partial write that a
race still causes now ends withobelize undoin itsNext:line.- A file name that holds a control or format character, a backtick or a pipe is skipped as
unusable_name. Printed as it was, an escape sequence in a name moved a terminal's cursor or
colours, and a backtick or a pipe ended a code span or a table cell inREPORT.md. Its name is
spelled out (\x1b) in the skip, and the detail of a limitations row is escaped as a pack's
text is. .obelize/latestis replaced by a rename below a.obelizeopened without following a link,
where a check for a link and then a write could race a swap; a link there is now replaced and
what it named is not written..obelize.ymlis read without following a link, so a repository
cannot make a parse error quote another file. A relativeXDG_CONFIG_HOMEis ignored, as the
specification says, where it could have put the user's model block, allowlist and pack
directories under the repository being scanned. Approving a command appends a row and keeps the
rows already there, with their command text, where it rebuilt the file from the keys.
Python 3.15
- obelize supports Python 3.15, which CI runs on Linux, macOS and Windows and the classifier names.
uv.lockmoves to pydantic 2.14.1 and pydantic-core 2.50.1, which have 3.15 wheels, where the
older pair made pip compile pydantic-core with Rust. PyYAML has no 3.15 wheel yet, so pip builds
it in pure Python, with no compiler. An Intel Mac installs libcst 1.8, which has no 3.15 wheel,
so it compiles that with Rust there; the other systems install libcst 1.9's wheel.
tomllibreads TOML 1.1 from 3.15 on, so apyproject.tomlthat uses it has its Python floor
read there, and is still reported as unreadable on 3.12 to 3.14.
Packs
gemini/google-generativeai-to-google-genai0.3.0 writesgoogle-genai>=1.55where it wrote
>=1. Releases 1.0 to 1.54 do not hold what the pack claims: 1.54 has no
enable_enhanced_civic_answers, one of the fifteen generation-config keys the pack moves into a
config, so code it wrote could fail on an install>=1admitted. Agoogle-genailine you
already declare is left as you wrote it. The weekly job now runs the pack's facts on 1.55.0 and
on the newestgoogle-genai, and thepy-pdf/pypdf2-to-pypdffacts on the newestpypdf; the
Gemini facts also check that every keyword a rewritten call carries is one its method takes.