Repository navigation
v0.3.0
Benchmark
I did not run the benchmark again for this release. Its numbers are 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 scan
specification the Gemini pack hands the scanner is the one measured, which tests/unit/test_gate1.py
checks, and the verdict of every hand-written answer key under tests/fixtures/scan/ is unchanged;
one row of the encoding key moved from line 8 to line 6, the line compile() names. The scanner and
the rewriter are not unchanged, so read those numbers as dated. 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, for openai, a weekly job that runs the same
fixtures on 0.27.0, on 0.28.1 and on the new releases.
Unsupported patterns
docs/KNOWN_ISSUES.md lists
what I know about and have chosen not to fix yet. The 0.1.0 gaps for the Gemini pack stand. The main
gaps of the two other packs:
openai: async and streamed calls, Azure, every resource but the seven calls under 0.2.0's Added,
and theopenai.errorclasses are reported and never rewritten. A write to a module setting
throughexec,globals(),sys.modulesorsetattr,OPENAI_API_BASEset in code, and a base
URL formed once at assignment are not seen. A handler for top-levelopenai.APIErrorthat reads
http_statusorjson_bodyfails after the pin moves, with no warning. The pack writes nothing
while any row of it is withheld.PyPDF2: a method or a parameter is never rewritten or reported, so only your own tests find that
PdfWriter(path)starts empty on PyPDF2 and clones the file on pypdf, that page boxes turn from
Decimalintofloat, or thatextract_textputs different whitespace between the pieces of a
line. A repository that does not declarePyPDF2at 3 or above, or a Python below 3.9, is
blocked.- Model names are kept as they are, in every pack.
- A source file with a chain of about four hundred terms is a
parse_errorrow, not a rewrite. On
every Pythoncompile()accepts a chain of about thirteen thousandandororoperands, and
on 3.14 and later a chain of tens of thousands of terms, and libcst can still segfault on them. - Python 3.15 is not in the supported range yet.
- The four safety gaps in
docs/THREAT_MODEL.md are
narrower and still open: a filter driver named in a repository's own.git/configand, on
Windows, a DLL placed in the repository;obelize verifyorobelize undorun twice overwriting
the record of the first; a credential shape the redactor does not list, a secret a Windows program
writes as UTF-16 and a key in the model adapter's traceback; on Linux and macOS,SIGTERMor
SIGHUPsent to obelize itself, which leaves a verification command running where Ctrl-C does
not; and, on Windows, a process that WMI, Task Scheduler ordockerstarts for a verification
command.
Fixed
-
A manifest that already declares the new distribution is no longer given a second line for it:
the legacy row is withheld asmanifest_pin_shape_unsupportedand the file stays as written. -
One deeply nested expression no longer ends the whole scan.
compile()now runs before libcst's
parser, which overruns the C stack on thousands of nested parentheses, a chain of tens of
thousands of operands or a stack ofnots and killed the process; the file is one
input_does_not_parserow. A tree that parses but overruns the analysis or the rewrite is that
row, or is kept as it was withoutput_does_not_parse. A filecompile()and libcst both refuse
is now blamed on the linecompile()names. Asetup.pythat does not compile declares nothing. -
A PEP 810
lazy importof the legacy module (Python 3.15) is no longer invisible. libcst binds
no name for it, so the uses after it were never seen and the pin could be rewritten under code
that still used it. The statement is now oneneeds_reviewrow,lazy_import, a new value of
the closedbailvocabulary infindings,plan,runandproposalschemas; the file and
the pin stay. -
A legacy pin that nothing imports is no longer
not_a_usage. Code already ongoogle-genaior
pypdfwithgoogle-generativeaiorPyPDF2still pinned is now aneeds_reviewrow,
manifest_code_mismatch, so the CI check indocs/CLI.mdexits1on it and a pack that only a leftover pin
names is chosen.fixstill writes nothing for it. An excluded file that names the library, a
module only it installs, andopenai(one distribution for both APIs) keepnot_a_usage.
legacy_version_unsupportedis no longer raised for a pin below the floor when no code uses the
library. -
A notebook, stub or windowed script that the default
includeleaves out and that names the
library kept the pin whateverexcludesaid.excludenow releases it: the file is named as
excluded and blocks nothing, so the rest migrates. A module a narrowerincludeleaves out is
named the same way instead of dropped without a word. -
A pin copied below a legacy one that something still imports is no longer joined to its
neighbour. A quoted array element that no comma follows (the last one of apyproject.tomllist
or asetup.pyinstall_requires) was copied as it stood, which left apyproject.tomlthat
tomllibrefuses and asetup.pyrequirement ofPyPDF2==3.0.1pypdf>=6.19. The row is now
withheld asmanifest_pin_shape_unsupported. -
A manifest edit that its rule would not write (extras, a URL, the case above, the new
distribution already declared) is withheld like a source rule's refusal. It was reportedauto
with the manifest unchanged, so the run saidVerification: passand exited0or6. It is now
inwithheld[], is not counted incounts.auto, and the exit is4. -
A
setup.pywith a lone carriage return before the requirement, or a byte order mark on its
line, no longer has its pin written at the wrong place (aSyntaxErroron a line ending
)>=6.19pypdf): libcst counts those lines differently from the edit, so the row is named
without a pin. One that parses but overruns the stack in libcst's metadata pass (about four
hundred terms) declares nothing, where it endedscanandfixwith aRecursionError. -
A
SyntaxWarningin the code obelize reads (an invalid escape,iswith a literal) is no longer
printed to stderr, wherecompile()and libcst printed one for each use, with the line that
caused it.
Changed
- The
fixsummary says why findings were left for you, with a file written or not: how many, the
most common reasons, up to five of the rows that cause them (path:line symbol code), the files
that hold the pin without a row (a notebook, a file importing a module only the old distribution
installed), and a link to what each code means at this release's tag. A pack whose one
distribution holds both APIs says that the repository moves whole or not at all. - A warning on an edit that is written, or would be, is printed as
Check <file>:<line>: <code>: <what to look at>. - A command that follows:
Next: obelize fix --apply ... writes it.after a dry run that planned a
file, andNext: obelize fix ... shows what it would change.after ascanwith aneligible
row and no blocked pack. Both repeat--packand--repo. After exit6the step is now one path
for every pack: undo,fix --apply, install what it wrote, thenverify --run <new id>, since
a baseline that failed leaves nothing to compare with; when the tests merely gave no verdict, the
line says to install what the run wrote into the manifest first. scannames the first three files that block a pin and, inREPORT.md, the files that import a
module only the old distribution installed. A blocked pack's rows are introduced as whatfix
would try once the block is lifted. The limitations of a pack name its guide.--verifysays in its help that it runs only with--apply.- A report that was misleading now is not. An apply that wrote nothing prints no
Next:line,
since there is nothing to undo; aCheckline after--applysays its line number is the one
before the change; a blocked migration says that migration plans no change, not the run; the
importers of a module only the old distribution installed are not listed as unanalysed; the
count of dependency declarations counts declarations, not the rows one is graded into; the
legend says a file is left as it was when any row in it isneeds_revieworunsupported; a
verification command with a|and the words of a model endpoint no longer break the tables of
REPORT.md. - A pack's
source.urlmay hold no whitespace, control character or Markdown or HTML character,
since the report prints it. REPORT.mdno longer lists a manifest as "still importing the legacy distribution" when the
pin waits on a module only that distribution installed; it lists the files that import it.
Documentation
- The README leads with the example, says what to do after each exit code (three guides for
4,
two causes for3), givesopenaia four-step way to check a migration whose two versions are one
install, and has a "With a coding agent" section.skills/obelize/SKILL.mdis the same workflow as
a skill file, and a test parses every command in it. docs/CLI.md's CI check prints one::errorannotation per finding and pinsX.Y.Z, and has a
paragraph on monorepos. The phase order says the compile check belongs to the verification, and
that pytest's exit5is a failure here.PACK_SPEC.mdno longer promises the rewrite matches
your formatter.KNOWN_ISSUES.mdnames the half-migrated file, the import order, lockfiles and
anopenainotebook on a pin that has already moved.PRODUCT.mdfixes Gate 5 at 2026-11-03 and
updates the neighbouring tools. The issue templates name no single pack.BENCHMARK_RESULTS.md,
bench/errors.yamland ADR-044 no longer give counts and levers that disagree with the table
bench/errors.pygenerates: analias_collisionis worth nothing alone, and ADR-044 says 25
migrated rows where it said 34.
Packs
openai/openai-0-to-1(0.2.0) migrates fromopenai0.27.0, not only 0.28.1. A second Gate 0
found one surface across 0.27.0 to 0.28.1 and identical results from the ten legacy fixtures;
a pin below 0.27.0 blocks the pack. The weekly job runs the legacy fixtures on 0.27.0 too.- The openai and Gemini packs name model deprecations. All three suggest
<project python> -m pytest -q
and say a patch target "stops standing in for the code once it has moved". The Gemini pack
(0.2.0) also names the class-wrapper refusal and the optional typesgoogle-genaireturns. The
openai pack names the Azure environment variables,httpx2, optional types, libraries that call
0.x themselves, and what a notebook does to the pin. The pypdf pack (0.2.0) names that
extract_textspaces text differently. The Gemini pack'spack_versionrises from 0.1.0, which
0.2.0 should have done.
Security
- Every git command obelize runs sets
core.fsmonitorto empty, so a hook the repository's own
.git/confignames no longer runs while obelize reads the tree. A filter driver in that file,
which a clone does not carry, and on Windows a DLL placed in the repository, remain. - Ctrl-C no longer leaves a verification command running on Linux and macOS: closing the command
kills what is left of its process group. - The command and the command line a run records are redacted, and the redactor knows more shapes:
AWS session keys, GitHub OAuth, server and fine-grained tokens, Hugging Face, Slack, Stripe, GitLab
and PyPI tokens, JWTs, aBearertoken, the password of a URL and a quotedpassword. - Every git command also takes
--no-optional-locks, sostatusdoes not rewrite a stale index
and run apost-index-changehook that the repository's config names throughcore.hooksPath. - The JWT and PEM patterns of the redactor took minutes on a megabyte of repeated starts
(eyJeyJ..., aBEGINline with noEND); both are read once now. A URL password with no user
(redis://:secret@host) is redacted. A redaction that crossed a quote no longer leaves a
record that cannot be split, which raised after the command had run: the record keeps the program
and[REDACTED]. - A model proposal that repeats a
[REDACTEDplaceholder it was shown, where the lines it replaces
hold none, is refused asreplacement_carries_redaction, a new value ofGuardRefusalin the
proposalschema (therefusalofmodel/proposals-<n>.json);run.jsonis unchanged.