Skip to content

Decisions

Architecture decision records live in docs/adr/, and this is their index. An ADR without a row here does not count as a decision; tests/unit/test_status_claims.py fails on an ADR without a row and on a row without an ADR.

Architecture decision records

ADR Title Status Summary
ADR-001 First migration family Accepted; amended by ADR-050 and ADR-051 The first v0 migration is google-generativeai to google-genai, chosen for its end-of-life status, official migration guide, corpus size and lack of an existing codemod.
ADR-003 Model adapter Accepted Model help is one OpenAI-compatible adapter on stdlib urllib, configured only by the user, asked only about needs_review rows, and its untrusted proposals are applied only after the guard and with --accept-model.
ADR-004 Repo visibility and CI Accepted The repository is public and CI runs on Linux, macOS and Windows for every push and pull request; the main ruleset requires fifteen named checks and a tag ruleset restricts v* to me.
ADR-005 Technical stack Accepted; amended by ADR-021, ADR-022, ADR-049 and ADR-050 obelize is built with hatchling, uv, Typer, pydantic v2, libcst, subprocess git, pytest, ruff and strict mypy, runs on Python 3.12-3.15, and has at most seven runtime dependencies.
ADR-006 Pack carries no code Accepted; amended by ADR-050, ADR-053, ADR-055 and ADR-057 A pack is declarative YAML that selects rule kinds from a Python registry and carries no code, and whatever a rule cannot prove becomes needs_review or unsupported, never a guess.
ADR-007 Verify command trust Accepted; amended by ADR-011 and ADR-033 Verification commands come only from the user: --verify, the user allowlist, or .obelize.yml once approved (non-interactively only with --trust-repo-config); they run without a shell, and a pack's suggestions are shown, never run.
ADR-008 Scanner and codemod corrections Accepted; amended by ADR-010, ADR-012, ADR-017, ADR-019, ADR-022 and ADR-050 Resolution branches on the qualified-name set, files are bytes in and bytes out behind a round-trip gate, path containment resolves both sides, output uses a total sort key, and benchmark versions are pinned.
ADR-009 Project named Obelize Accepted The project, command, PyPI distribution, import package and repository are all named Obelize, chosen after register-level trademark clearance.
ADR-010 Fixture oracle and atomicity Accepted; amended by ADR-012, ADR-013, ADR-014, ADR-050 and ADR-053 The hand-built fixture keys are the oracle: a file migrates completely or not at all (F-1), the manifest gains the new pin once anything migrates and loses the old one only when nothing needs it (F-2), and the closed vocabularies live in SCAN_VOCABULARY.md.
ADR-011 CLI contract, open questions Accepted; amended by ADR-015, ADR-033 and ADR-052 Exit 6 means verification reached no verdict (policy_refused stays 5), exit 7 means an invalid pack, a dry run exits 0, and obelize undo takes no --force.
ADR-012 File atomicity Accepted; amended by ADR-050 A file with any bail withholds every otherwise-eligible row, each withheld finding names the most specific rule that fired, and caused_by lists the bails behind it.
ADR-013 Run folder output keys Accepted; amended by ADR-023 and ADR-032 RUN_FOLDER.md specifies the run folder and a test holds the code to it, model_proposed stays a distinct edit status, and a fixture file comes out as its .after.py byte for byte, or unchanged when it has none.
ADR-014 The scan-time grade Accepted; amended by ADR-050 Each closed vocabulary is a Literal in src/obelize/models/vocabulary.py asserted against SCAN_VOCABULARY.md in both directions, a scan publishes a scan-time status and never a verdict, and the models refuse bad data rather than repair it.
ADR-015 Configuration merge and exclusions Accepted; amended by ADR-016 An unusable .obelize.yml exits 2, --verify replaces the file's commands, a boolean flag only adds permission, a published always-excluded set outranks any configuration, and a command that needs a shell is refused on read.
ADR-016 File selection and path guard Accepted One path guard in src/obelize/fsutil.py serves reads and writes, git's file listing is used only when it describes this root, a user's exclude prunes no directory, every refusal is a published reason, and the walker opens no file.
ADR-017 Input gates Accepted A file is opened with O_NOFOLLOW and passes compile(), a depth check, libcst and the byte round-trip in that order, and a failure becomes one of four limitation codes carrying the parser's message (an unexpected error, only its class) and no source line.
ADR-018 Pack schema Accepted; amended by ADR-019, ADR-025, ADR-050, ADR-051 and ADR-053 A pack declares every v0 surface, is refused at load for schema or coherence errors, refuses unsafe prose rather than stripping it, cites one primary source plus a per-claim SOURCES.md, and is checked by obelize pack validate.
ADR-019 Resolution and detection rules Accepted; amended by ADR-020 scan/analysis.py resolves and the planner decides; a mention is anchored to its line, the hyphenated distribution name is never a source finding, and an unresolvable receiver is reported only where the file builds a legacy object.
ADR-020 Impact plan and ladder Accepted; amended by ADR-021, ADR-031 and ADR-050 impact/dataflow.py decides a binding group and impact/planner.py the file, the client rung applies to a module's call rewrites, the bail ladder is one total order, and ImpactPlan refuses a plan F-1 would leave half-applied.
ADR-021 Dependency manifests and verdict Accepted; amended by ADR-050, ADR-053 and ADR-056 A dependency manifest is selected by name and edited as lines (setup.py is parsed), and one repository-wide ManifestPlan adds the new pin once anything migrates and removes the legacy pin only when no file, notebook or transitive import still needs it; where one distribution holds both APIs, any withheld row holds the pin. A notebook is read as code for the legacy API and never written, and a pack may let the pin move by declaring the modules the legacy distribution installed (declare_transitive).
ADR-022 Runner and result order Accepted; amended by ADR-050 and ADR-052 The parent reads, hashes and prefilters while workers parse, resolve and grade; the default pool is min(8, cpu_count - 1) workers from 32 candidates up, a typed --jobs is used as typed, and results are ordered by (path bytes, sha256).
ADR-023 Scan command Accepted; amended by ADR-050, ADR-052 and ADR-056 obelize scan writes its run folder before printing, findings.json and --json carry no clock while run.json carries the run id and timings, a repository whose declared Python floor is unsupported is reported blocked, and untracked files off the plan do not make the tree dirty.
ADR-024 Gate 1 measurement Accepted; amended by ADR-026 Gate 1 is measured on a committed, size-stratified sample of real repositories with keys written before the scan, a row matches on path, line, kind and symbol, precision over zero findings prints --, and a test recomputes the published block offline; the same frame, drawn for any pack, is asked what a fix dry run would write.
ADR-025 Rule protocol and import manager Accepted; amended by ADR-026 and ADR-050 A rule claims findings and a file is written only when every eligible finding in it is claimed, finish() applies a rule's recorded decisions in one pass, and rename_import owns the import statements and the reads of the names a pack maps but no call site.
ADR-026 Client placement and layout Accepted; amended by ADR-050 configure_to_client places the client by a table over where the module's call rewrites are and refuses when no placement reaches them all, carries only the pack's allowed_kwargs and two named keyword roles, and wraps rewritten calls at the pack's line length of 100.
ADR-027 The model object Accepted generative_model_calls deletes GenerativeModel(...) and copies what it held into every call where that copy is portable, maps positional arguments by the legacy order, refuses a group whose uses are not all rewritable, and records nothing until nothing can refuse.
ADR-028 Chat surface and async Accepted; amended by ADR-050 A binding group is the transitive closure of what its calls return, so a chat migrates with its model; a history is reshaped only where every part is certainly a string, and an await is kept, never inserted.
ADR-029 Free functions Accepted; amended by ADR-053 rewrite_call migrates the free functions one call at a time with refusals local to the call, maps arguments only through the legacy signature the pack declares, refuses any other keyword, and writes a call whose result changed shape only where the result is discarded or every read of it is a path the pack lists.
ADR-030 Flagged surfaces and dependency line Accepted; amended by ADR-053 and ADR-054 flag_only writes nothing and records an edit naming the change that refused the surface, and manifest_dependency is a separate ManifestRule that rewrites a declaration as two spans on its line, in place or as a new line below.
ADR-031 The codemod driver Accepted; amended by ADR-050, ADR-052 and ADR-053 The driver runs source rules in pack order, then F-2's survey, then manifest rules; applies F-1 again; withholds a file whose configure another module still uses, and every file of a pack that is its own target while any of its rows is withheld; and returns, unwritten, only output that parses and binds every name it reads.
ADR-032 Writing a file Accepted; amended by ADR-049 An apply refuses when the repository is dirty or git cannot say, checks every path before writing any, writes each file by temporary file, fsync, original mode and rename below an O_NOFOLLOW parent walk, and stops if a file changed since the plan read it.
ADR-033 Running somebody else's command Accepted; amended by ADR-049 One refused command refuses the whole verification phase, interactive means a TTY with no CI and no --non-interactive, a baseline that does not pass stops the after-phase, a deadline kills the whole process group, and a program that never starts is command_not_executable, not fail.
ADR-034 Fix evidence Accepted; amended by ADR-039 plan.json holds what a run would write and run.json's file_edits[] what it wrote, refused writes go in refused[], obelize never applies its own patch, and an apply saves its originals and a journal before the first write so undo can restore them.
ADR-035 Apply order Accepted obelize fix gates the tree and paths before the baseline and again at the write, neither a refused verification nor a failed baseline stops the write, exit 4 means something was withheld, and obelize verify reads its commands from configuration, never the run folder.
ADR-036 Model requests and answers Accepted; amended by ADR-039 A model is asked only about a needs_review row in a writable file whose bail it could repair, is sent the smallest def or class holding the whole binding group (else the group plus 20 lines either side), or nothing past 80 lines, and answers with an untrusted EditProposal.
ADR-037 Proposal validation Accepted; amended by ADR-039 and ADR-052 The guard checks a proposal in one fixed order (the file asked about and unchanged, the sent range and the lines quoted from it, an absolute size limit, encoding, parse and compile, allowlisted names), records the first refusal, and returns the bytes it checked.
ADR-038 One endpoint Accepted; amended by ADR-039 The adapter sends one request, with no retry, to the configured host only (no proxy, redirect, cookies or compression) and no Authorization header without a key, and records a failure as a ProviderFailure word with the reply redacted and capped.
ADR-039 Model command surface Accepted The model pass runs after the tree gate and before the baseline, a file takes at most one accepted proposal, a model edit never clears a review item, --show-context prints every payload string unescaped and stops, and model/ holds one file per consultation.
ADR-040 Candidate corpus Accepted; amended by ADR-041 A benchmark candidate is a repository the scanner reported on at a pinned SHA with an SPDX licence, archived does not disqualify, and a pack surface no candidate exercises is resolved by a dated, published query.
ADR-041 Case definitions and split Accepted A case's key is written from the source before its scan is read and judges the code, not the tool; the split is hashed by owner and stratified on test-file presence; pattern_key comes from the ground truth; and a case with no test command is kept.
ADR-042 Benchmark harness and tiers Accepted; amended by ADR-043 The harness runs both test phases around the SDK swap and compiles written files with the case's interpreter, a tier is computed from the record and an unreviewed case is never verified, --fixtures-only refuses the network, and the ceiling is published before a round.
ADR-043 Round one results Accepted A case runs outside this repository, a pytest exit code is read against pytest's table, a published result carries no absolute path, provenance records the interpreter's sys.version, and a suite that ran no test verifies nothing.
ADR-044 Error analysis Accepted Each unmigrated row gets one of three dispositions (rule change, scan change, recorded limitation) from a committed row-level record (a limitation names its ADR, a change an open COVERAGE gap), and causes are ranked by lever: the round recomputed with that cause alone fixed.
ADR-045 The comparison arm Accepted The comparison arm is one general-purpose coding agent with one committed prompt over the same development-split repositories and keys, scored by scanning the tree it leaves, with its patches reviewed in a separate file and every free protocol choice made in its favour.
ADR-048 Accepted gaps in 0.1.0 Accepted 0.1.0 ships with the open gaps in TM-5, TM-8, TM-9 and TM-10 stated rather than closed, and every format stays unstable through 0.x with each change in the changelog.
ADR-049 The platform seam Accepted The calls behind never following a link and stopping a command with all it started go through src/obelize/native/, one module per mechanism (files, processes, console) that picks its implementation at import; callers hold no platform branch, what obelize writes is the same bytes everywhere, and CI runs the suite on Windows.
ADR-050 Generic pack engine Accepted; amended by ADR-053 and ADR-056 The engine knows no SDK: a pack states only what a rule reads, and a field, code or policy no code reads is deleted rather than documented.
ADR-051 Second migration family Accepted; amended by ADR-056 A family joins the bundled packs after a Gate 0 that measures every fact its rules rest on; PyPDF2 to pypdf is the second, and its target is pypdf 6.19 or later because every earlier release has known advisories, with what the pack cannot see stated as a limitation.
ADR-052 Several packs per run Accepted; amended by ADR-053 A run uses every pack the repository needs, or those --pack names, from the wheel, the user's own directories and files; packs that could feed each other are refused, and the rest run in id order over one read, each as a single-pack run, one gate, write and undo for all, with the evidence naming each pack.
ADR-053 Shared-module migrations Accepted; amended by ADR-055 and ADR-057 A module the new SDK keeps (openai 0 to 1) lists the legacy names, not the kept ones, with the old side held by a frozen snapshot; rewrite_call stays on the module and carries only listed keywords and result paths; where one distribution holds both APIs the pin and every file move together or not at all, and a model is not asked about such a pack.
ADR-054 Pack guidance in the report Accepted scan and fix print the message and suggestion of each flagged change that claimed a finding of its own pack, and REPORT.md lists them with each pack's limitations and verification suggestions, escaped so a viewer runs none of it; no format gains a field, and obelize verify replaces only the last, whole note.
ADR-055 Rename a setting Accepted A rename_setting change moves a shared module's setting to another name by rewriting its plain assignment, with the value ending as the new name demands (a literal gets a /, anything else is formatted and given one), and refuses every other use of the old name; openai.api_base becomes openai.base_url.
ADR-056 The newest legacy release Accepted A pack may name the newest release of the distribution it migrates from (from.newest) and the names that release lacks (from.lacks); a declaration with no version, or one that admits it, is then taken as that release unless the repository shows another (code, a lock, a constraints or index setting, a version written elsewhere, an environment kept in it), and the run says what it took. A pack whose newest release is on the new side names the names only its old range defines (from.since) and takes a declaration that may install a release of that range beside code that uses one, on the same evidence; it can also name what only the new API defines (to.introduces), so a pin that admits only the old releases beside code that uses one is a manifest_code_mismatch.
ADR-057 Rename a symbol Accepted A rename_symbol change moves a class a shared package's submodule defined to the package, in a reference and in the import that names it, only where a local server shows the same responses raise it on both sides (openai.error.RateLimitError, PermissionError and Timeout), and refuses a class that is called, raised or derived from, a handler that reads an attribute of its error, and a handler ahead that may now catch it.
ADR-058 Models by section Accepted obelize.models is a package of six modules (vocabulary, fields, scan, config, verify, run), each importing only the ones before it, and its __init__ re-exports every name, so no import path and no JSON Schema changed.
ADR-059 Verified dependency upgrades Accepted obelize is a tool for verified dependency upgrades, with the three bundled migrations as examples: the local-first, evidence-first and fail-closed rules stay, the things the first boundary ruled out (pack-less discovery, an optional network, more model routes, a registry, branch commits, isolated environments) become planned directions built one release at a time, and anything beyond reading the repository is allowed only by the user, never by the repository.
ADR-060 Rename a name in its own module Accepted A rename_symbol change may carry renamed, a name that stays in its module and takes another spelling (acme.kit.old_greet is acme.kit.greet): references and the name on a from line change, the module's import does not, a collision is alias_collision, and a call is refused unless the change says calls: allowed, the pack author's claim that the arguments are unchanged; invented libraries under tests/packs/generic/ test the other shapes of a kind.
ADR-061 Reach Accepted obelize is offered to CI, code scanning and coding agents as ways to read what the commands already do: scan --sarif is a view of the findings, scan --fail-on exits 4 for a status you name, and verify --json and undo --json print the file the command wrote.
ADR-062 Reading other releases Accepted To migrate a library that has no pack, obelize reads two releases of it: a cached, host-limited network seam, files accepted only by the sha256 the index gave, archives opened for source alone, and the public API read with griffe (the seventh runtime dependency) in a child process, with obelize's own pairing of names and reading of deprecation messages.
ADR-063 Synthesized packs Accepted obelize pack synth makes a pack from the public API of two releases of a library: a rename only where the library's own message names the new name, a move out of a submodule only on an unchanged signature, a flag for everything else with the library's words, and the dependency line; the pack must pass pack test or it is removed, its source type is api_diff and never bundled, and the index is named only in the user's own configuration.
ADR-064 Finding what to upgrade Accepted obelize outdated reads the manifests and asks the index for the gap, the flags (inactive, withdrawn, advisory) and what can migrate each dependency; obelize upgrade plans one move as a dry run with a known pack or one made from the two releases and kept in the cache.
ADR-065 Trying an upgrade Accepted upgrade --apply tries a move in a copy of the repository with two uv environments (the release in use, and the release moved to), compares the repository's own tests test by test, re-runs what broke once, and writes only if no passing test now fails; it asks first and is the one place obelize installs.
ADR-066 A branch on request Accepted upgrade --apply --branch commits a move a trial passed to a new branch with git's plumbing (a temporary index, no checkout, no hook, no signature), never moves a branch that exists and never pushes.
ADR-067 The flagged places Accepted upgrade --apply --branch --agent claude_code puts the places a pack only flags to the person's own claude as a text-only model (every tool, session, MCP server, skill and setting off, a fixed environment), checks each proposal with the guard, writes it to the trial's copy, lets the rules write the rest and the tests judge, and commits only to a branch. A proposal may bring its imports, and a place may be judged to need no edit, on the tests' word.

Decisions not yet promoted to an ADR

Settled and implemented, but too small for an ADR; one that is questioned gets one.

  • No obelize inspect: obelize scan --pack already shows what a pack matches.
  • The console script is obelize.cli:main, not the Typer app, so argv handling, exit codes and lazy imports stay in obelize's hands.
  • The version is single-sourced from src/obelize/__init__.py via [tool.hatch.version]; obelize --version, the wheel and the release tag check all read it.
  • Development tools are PEP 735 dependency groups ([dependency-groups]: dev, and workflows for the workflow audit), never an extra, which PyPI would publish as installable; a unit test enforces it.

How to add a decision

Name the file docs/adr/ADR-NNN-<slug>.md with the next number and a slug of at most five words, and give it the same three headings every ADR here uses: # ADR-NNN: <title> (the title also at most five words), ## Status, ## Decision and ## Consequences. Set the status to Accepted, and add its row here in the same pull request. An ADR states the current decision and git keeps its history: an amendment updates the amended ADR, whose status and row name the amending one, and a superseded ADR is deleted with its row.