Skip to content

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 12 Oct 11:49
· 301 commits to main since this release

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 the openai.error classes are reported and never rewritten. A write to a module setting
    through exec, globals(), sys.modules or setattr, OPENAI_API_BASE set in code, and a base
    URL formed once at assignment are not seen. A handler for top-level openai.APIError that reads
    http_status or json_body fails 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
    Decimal into float, or that extract_text puts different whitespace between the pieces of a
    line. A repository that does not declare PyPDF2 at 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_error row, not a rewrite. On
    every Python compile() accepts a chain of about thirteen thousand and or or operands, 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/config and, on
    Windows, a DLL placed in the repository; obelize verify or obelize undo run 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, SIGTERM or
    SIGHUP sent to obelize itself, which leaves a verification command running where Ctrl-C does
    not; and, on Windows, a process that WMI, Task Scheduler or docker starts 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 as manifest_pin_shape_unsupported and 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 of nots and killed the process; the file is one
    input_does_not_parse row. A tree that parses but overruns the analysis or the rewrite is that
    row, or is kept as it was with output_does_not_parse. A file compile() and libcst both refuse
    is now blamed on the line compile() names. A setup.py that does not compile declares nothing.

  • A PEP 810 lazy import of 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 one needs_review row, lazy_import, a new value of
    the closed bail vocabulary in findings, plan, run and proposal schemas; the file and
    the pin stay.

  • A legacy pin that nothing imports is no longer not_a_usage. Code already on google-genai or
    pypdf with google-generativeai or PyPDF2 still pinned is now a needs_review row,
    manifest_code_mismatch, so the CI check in docs/CLI.md exits 1 on it and a pack that only a leftover pin
    names is chosen. fix still writes nothing for it. An excluded file that names the library, a
    module only it installs, and openai (one distribution for both APIs) keep not_a_usage.
    legacy_version_unsupported is no longer raised for a pin below the floor when no code uses the
    library.

  • A notebook, stub or windowed script that the default include leaves out and that names the
    library kept the pin whatever exclude said. exclude now releases it: the file is named as
    excluded and blocks nothing, so the rest migrates. A module a narrower include leaves 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 a pyproject.toml list
    or a setup.py install_requires) was copied as it stood, which left a pyproject.toml that
    tomllib refuses and a setup.py requirement of PyPDF2==3.0.1pypdf>=6.19. The row is now
    withheld as manifest_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 reported auto
    with the manifest unchanged, so the run said Verification: pass and exited 0 or 6. It is now
    in withheld[], is not counted in counts.auto, and the exit is 4.

  • A setup.py with 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 (a SyntaxError on 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 ended scan and fix with a RecursionError.

  • A SyntaxWarning in the code obelize reads (an invalid escape, is with a literal) is no longer
    printed to stderr, where compile() and libcst printed one for each use, with the line that
    caused it.

Changed

  • The fix summary 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, and Next: obelize fix ... shows what it would change. after a scan with an eligible
    row and no blocked pack. Both repeat --pack and --repo. After exit 6 the step is now one path
    for every pack: undo, fix --apply, install what it wrote, then verify --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.
  • scan names the first three files that block a pin and, in REPORT.md, the files that import a
    module only the old distribution installed. A blocked pack's rows are introduced as what fix
    would try once the block is lifted. The limitations of a pack name its guide.
  • --verify says 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; a Check line after --apply says 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 is needs_review or unsupported; a
    verification command with a | and the words of a model endpoint no longer break the tables of
    REPORT.md.
  • A pack's source.url may hold no whitespace, control character or Markdown or HTML character,
    since the report prints it.
  • REPORT.md no 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 for 3), gives openai a four-step way to check a migration whose two versions are one
    install, and has a "With a coding agent" section. skills/obelize/SKILL.md is the same workflow as
    a skill file, and a test parses every command in it.
  • docs/CLI.md's CI check prints one ::error annotation per finding and pins X.Y.Z, and has a
    paragraph on monorepos. The phase order says the compile check belongs to the verification, and
    that pytest's exit 5 is a failure here. PACK_SPEC.md no longer promises the rewrite matches
    your formatter. KNOWN_ISSUES.md names the half-migrated file, the import order, lockfiles and
    an openai notebook on a pin that has already moved. PRODUCT.md fixes Gate 5 at 2026-11-03 and
    updates the neighbouring tools. The issue templates name no single pack. BENCHMARK_RESULTS.md,
    bench/errors.yaml and ADR-044 no longer give counts and levers that disagree with the table
    bench/errors.py generates: an alias_collision is worth nothing alone, and ADR-044 says 25
    migrated rows where it said 34.

Packs

  • openai/openai-0-to-1 (0.2.0) migrates from openai 0.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 types google-genai returns. 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_text spaces text differently. The Gemini pack's pack_version rises from 0.1.0, which
    0.2.0 should have done.

Security

  • Every git command obelize runs sets core.fsmonitor to empty, so a hook the repository's own
    .git/config names 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, a Bearer token, the password of a URL and a quoted password.
  • Every git command also takes --no-optional-locks, so status does not rewrite a stale index
    and run a post-index-change hook that the repository's config names through core.hooksPath.
  • The JWT and PEM patterns of the redactor took minutes on a megabyte of repeated starts
    (eyJeyJ..., a BEGIN line with no END); 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 [REDACTED placeholder it was shown, where the lines it replaces
    hold none, is refused as replacement_carries_redaction, a new value of GuardRefusal in the
    proposal schema (the refusal of model/proposals-<n>.json); run.json is unchanged.