Update the mechanical-refactor-verify skill to the latest sglang version (#1890)

This commit is contained in:
fzyzcjy
2026-08-26 07:21:13 +08:00
committed by GitHub
parent 2c78e2abd5
commit 3eb2fd2bff
42 changed files with 8978 additions and 220 deletions
@@ -1,152 +1,82 @@
---
name: mechanical-refactor-verify
description: Verify mechanical refactoring commits by requiring a reproducible transform script (gist) in the PR description. Use when doing or reviewing file splits, function moves, or module extractions.
description: Make mechanical refactoring (file splits, function moves, module extractions, renames) machine-checkable instead of eyeballed. Reproduce a relocation commit byte-for-byte from faithful primitives, and split an extraction into a verifiable prepare + move + postpare. Use when doing or reviewing such changes.
user_invocable: true
argument: "[verify <pr_url_or_commit>] — verify an existing PR, or omit to see the workflow guide"
argument: "split <base>..<tip> | construct <base>..<tip> [--match REGEX] [--out DIR] | verify --base <base> --branch <branch> --proof <folder> [--jobs N] [--skip-passed]"
---
# Mechanical Refactor — Reproducible Verification
# Mechanical Refactor — Machine-Checkable Verification
## Core Principle
## 1. Overview
The deliverable of a mechanical move (file split, function move, module extraction) is NOT the diff — it is **the script that produces the diff**.
A script is auditable; a diff is not.
- The correctness of a mechanical change (file split, function move, module extraction,
rename) must be **machine-checkable, not eyeballed** — the proof is something anyone can
re-run, whoever made the change and whenever.
- **One property**: *a commit is a pure relocation*. **One proof**: **reproduce** —
regenerate the move from the base commit with faithful primitives, run the formatter,
byte-diff against the target.
- Empty diff = the proof. Any residual = a bundled non-move change, surfaced for review.
- A reshape must not ride along: split into optional **prepare** + certified **move** +
optional **postpare** (`guide-split.md`).
## Workflow
## 2. Commands — what do you want to do?
Regardless of who did the move (human or agent) and when (before or after committing), the workflow is the same:
The skill takes an argument naming one of three commands; invoked without one, pick the
row matching your task.
### Step 1: Write the transform script to /tmp/
- **`split <base>..<tip>`** — author a compliant refactor branch: split it into commits,
satisfy the contract (extract, move, file split) → `guide-split.md`: §1 splits the PR
into classified pieces (the chain contract: classification format, correct labeling,
proofs PASS, non-mechanical commits correctness-reviewed); §2 splits one piece into
prepare + move + postpare (the case recipes and the anti-patterns). The argument is the
chain to author (or a single commit / a description of the change to split).
- **`construct <base>..<tip> [--match REGEX] [--out DIR]`** — construct the proof, for
the chain or one commit → `guide-construct-proof.md`: §1 generates + publishes the
whole chain's proof folder (the flags are the generator's:
`scripts/mechanical_refactor_proof_generator.py <base>..<tip> --match REGEX --out DIR`);
§2 proves a single commit — pass just `<commit>` (the generator, or a hand-written
`Repro` when it reports `UNSUPPORTED`).
- **`verify --base <base> --branch <branch> --proof <folder> [--jobs N] [--skip-passed]`**
— verify someone's proof: a whole chain / PR branch → `guide-verify-proof.md`: run the
chain verifier with exactly these flags
(`scripts/mechanical_refactor_reproduction_cli.py`) — it checks every commit declares
`mechanical_provable` or `non_mechanical_provable`, runs **every** provable commit's
proof (never a sample), and writes one full report; then audit the authored surfaces
and the `HUMAN_REVIEW` rows. Re-running one commit's script is for diagnosis only.
- **Decide whether a change counts as a clean move** → `spec-reproduction-utils.md`: the
property, the whole whitelist / not-allowed list, and each primitive's contract. The
source of truth for the reproduction module; if any other file disagrees, it wins.
- **Change this skill itself** (edit the engine, the generator, or the spec) →
`guide-modify-skill.md`: the spec-leads rule, the faithfulness invariant, and the testing
bar a change must clear before it is trusted.
Write the script to `/tmp/transform_<short_description>.py`. **Never write it inside the repo.**
## 3. Files
The scaffold (worktree creation, diff check, ruff format, result reporting) lives in `mechanical_refactor_verify_utils.py` next to this skill.
**MANDATORY**: The transform script MUST use `verify_mechanical_refactor()` from the utils module. Do NOT reimplement the verification scaffold — no hand-written worktree management, no hand-written diff checking. The script only defines `transform()` and calls `verify_mechanical_refactor`.
#### Key principle: move lines, don't paste content
The transform script must **read lines from the source file and write them to the target**, not hardcode large blocks of code as string literals. This is what makes the script auditable — a reviewer can verify line ranges against the original file.
**Rules:**
1. **Move code by line ranges**: read the source, extract line slices, write to target. Never paste >3 lines of code as string literals in the script.
2. **Minimal replacements**: when adapting moved code (e.g. `self.foo` → `foo`), use small targeted `str.replace()` or `re.sub()` calls. Each replacement should be a short pattern, not a full block of code.
3. **New content is OK only for glue**: import statements, function signatures (the `def` line + parameters), and other 1-2 line connective tissue can be written as literals. The *body* of moved functions must come from line extraction.
#### Script template (follow this structure exactly)
```python
#!/usr/bin/env python3
"""Reproducible transform for: <describe the mechanical move>
Run from the repo root: python3 /tmp/transform_<short_description>.py
"""
import sys
from pathlib import Path
sys.path.append(".claude/skills/mechanical-refactor-verify")
from mechanical_refactor_verify_utils import verify_mechanical_refactor, exec_command, git_add_and_commit, dedent
BASE_COMMIT = "<base_sha>"
TARGET_COMMIT = "<pr_mechanical_move_final_sha>"
def transform(dir_root: Path) -> None:
"""Perform the mechanical transformation and commit each step.
Args:
dir_root: Path to the worktree (checked out at BASE_COMMIT).
"""
# --- Step 1: Extract functions by line range ---
source = dir_root / "path/to/source.py"
content = source.read_text()
lines = content.splitlines(keepends=True)
# Move lines 100-150 to new file, dedenting from method to top-level
extracted = "".join(lines[99:150])
extracted = dedent(extracted, 4) # remove class indentation
# Minimal adaptations: self.x -> x (parameter)
extracted = extracted.replace("self.args", "args")
target = dir_root / "path/to/pkg/target.py"
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("import logging\n\n\n" + extracted)
# --- Step 2: Remove extracted lines from source, add import ---
# Delete lines 100-150 from source
new_lines = lines[:99] + lines[150:]
source.write_text("".join(new_lines))
# Small targeted replacement: add import, replace call site
content = source.read_text()
content = content.replace(
"from path.to.old import foo",
"from path.to.old import foo\nfrom path.to.pkg.target import extracted_func",
)
# Replace inline code with function call (use a short unique anchor)
content = content.replace("old_call_pattern", "new_call_pattern")
source.write_text(content)
git_add_and_commit("mechanical: extract functions", cwd=str(dir_root))
# Note: pre-commit run --all-files is run automatically after transform() returns
if __name__ == "__main__":
verify_mechanical_refactor(
base_commit=BASE_COMMIT,
target_commit=TARGET_COMMIT,
transform=transform,
)
```
### Step 2: Run the script from the repo root
```bash
cd <repo_root>
python3 /tmp/transform_<short_description>.py
# Expected: "PASS: transform reproduces the commit exactly."
```
If FAIL, fix the script and re-run until PASS.
### Step 3: Upload gist, delete local file, update PR description
One gist per PR. Do all three:
```bash
# 1. Create gist (or update existing)
gh gist create --public -d "Mechanical refactor transform: <description>" /tmp/transform_<short_description>.py
# Or update: gh gist edit <gist_id> -a /tmp/transform_<short_description>.py
# 2. Delete local file
rm /tmp/transform_<short_description>.py
# 3. Update PR description (paste the block below)
```
PR description must include:
````markdown
## Mechanical Move
Transform script: <gist_url>
### One-click verification
```bash
python3 <(curl -sL <gist_raw_url>)
```
````
### Step 4: PR scope
A mechanical refactor PR must contain **only** mechanical changes (moves, splits, renames, import fixes, formatting). All of these must be reproducible by the transform script.
Semantic changes (new logic, API restructuring, behavior changes) belong in a **separate PR**.
## Verifying an existing PR (`/mechanical-refactor-verify verify`)
1. Find the gist URL and one-click command in the PR description
2. Run the one-click command from the repo root
3. Report: PASS or show the diff
- [`guide-split.md`](guide-split.md) — split the PR into classified pieces (§1, the
chain contract) and each piece into prepare + move + postpare (§2: case recipes, what
stays mechanical, anti-patterns).
- [`guide-construct-proof.md`](guide-construct-proof.md) — produce the proof: the whole
chain's proof folder + publishing (§1), and a single commit's proof — generator or
hand-written `Repro` (§2).
- [`guide-verify-proof.md`](guide-verify-proof.md) — consume the proof: the whole-chain
verifier, single-commit re-runs, verdicts, and the audit checklist for authored
surfaces.
- [`spec-reproduction-utils.md`](spec-reproduction-utils.md) — the normative spec of the
clean-move property and the reproduction primitives.
- [`spec-reproduction-cli.md`](spec-reproduction-cli.md) — the normative spec of the
verified-chain property: the classification word rule, the proof obligation, the report,
and the exit codes.
- [`guide-modify-skill.md`](guide-modify-skill.md) — change the engine, the generator, or
the spec: the spec-leads rule, the byte-faithfulness invariant, and the testing bar.
- [`scripts/mechanical_refactor_proof_generator.py`](scripts/mechanical_refactor_proof_generator.py) —
the **generator**: infers a reproduce recipe from a commit's diff and emits/runs a
standalone, auditable script per commit, with a `PASS` / `RESIDUAL` / `UNSUPPORTED` verdict.
- [`scripts/mechanical_refactor_reproduction_utils.py`](scripts/mechanical_refactor_reproduction_utils.py) — the
**proof engine**: the `Repro` builder's faithful relocation primitives plus the worktree +
pre-commit + byte-diff scaffold. Self-contained — only git and the standard library.
- [`scripts/mechanical_refactor_reproduction_cli.py`](scripts/mechanical_refactor_reproduction_cli.py) — the
**chain verifier**: classifies every commit in `base..branch`, runs every provable
commit's proof from the proof folder, and emits the full chain report.
- [`scripts/tests/`](scripts/tests/) — pytest suites, one folder per module:
`reproduction_utils/` for the proof engine, `proof_generator/` for the generator,
`reproduction_cli/` for the chain verifier.
@@ -0,0 +1,181 @@
# Construct a proof
- Two levels, one chapter each: §1 constructs the **proof folder for a whole chain** (and
publishes it with the PR); §2 constructs the proof for a **single commit** (the
generator, the hand-written `Repro`, the hand-written transform).
- The property and primitive contracts: `spec-reproduction-utils.md`. Splitting the change
so the move is provable: `guide-split.md`. Consuming the proof: `guide-verify-proof.md`.
## 1. Construct proofs for a whole chain
### 1.1 Generate the proof folder
```bash
# a range: write a self-contained folder for every mechanical_provable commit
python3 .claude/skills/mechanical-refactor-verify/scripts/mechanical_refactor_proof_generator.py \
<base>..<tip> --match '(?<!_)mechanical_provable' --out repro_out
```
- `mechanical_refactor_proof_generator.py` infers each recipe from a commit's diff and
before-state AST.
- It emits and runs a standalone, auditable script per commit — no one hand-writes it.
### 1.2 The folder product
Self-contained, auditable without the skill installed:
- `repro_scripts/<sha>.py` — one script per commit;
- `output.log` + `output.html` — the verdicts;
- a copy of `mechanical_refactor_reproduction_utils.py` — the scripts' only dependency.
The folder is also the `--proof` input to the chain verifier
(`mechanical_refactor_reproduction_cli.py`, contract in `spec-reproduction-cli.md`).
### 1.3 Publish the proof with the PR
#### 1.3.1 What to share
- Share the scripts **plus** the copied `mechanical_refactor_reproduction_utils.py` — the
scripts import it, so a lone raw file is not runnable.
- Never a `python3 <(curl ...)` one-liner: process substitution gives the script no real
directory, so the import breaks.
- Flat layouts work: Python puts the script's own directory on `sys.path`, so the utils
module can sit either next to the script or one level up (the `--out` layout).
#### 1.3.2 Author: create a gist
```bash
cd repro_out
gh gist create --desc "mechanical-move proof for PR #NNNN" \
repro_scripts/*.py mechanical_refactor_reproduction_utils.py output.log
# prints https://gist.github.com/<user>/<gist_id> -- put it in the PR description
```
- `gh gist create` flattens paths — fine per §1.3.1.
- Alternatives: a PR attachment (zip the `--out` folder) or a branch holding it.
- Put the reviewer's download-and-re-run commands (`guide-verify-proof.md` §2.1) in the
PR description under a "Mechanical move — reproducible" heading.
#### 1.3.3 Keep the classification honest — in both directions
- A `mechanical_provable` commit (and a PR made only of such commits) contains **only**
mechanical changes (moves, splits, renames, import fixes, formatting). Semantic changes
go in their own `non_mechanical_provable` commits — a chain, and therefore a PR, need
not be purely mechanical, but a single commit never mixes the two.
- The dual holds too: a semantic (`non_mechanical_provable`) commit must not swallow a
provable relocation to skip the proof — split it out and prove it
(`guide-split.md` §2.2; property: `spec-reproduction-cli.md` §2.1).
## 2. Construct the proof for a single commit
### 2.1 What a proof is
- A runnable script that regenerates the commit from its base with the faithful relocation
primitives and byte-diffs the result against it.
### 2.2 Auto-generate the script (primary path)
```bash
# one commit: print the inferred script and run it (non-zero exit unless PASS)
python3 .claude/skills/mechanical-refactor-verify/scripts/mechanical_refactor_proof_generator.py <commit>
```
#### 2.2.1 What the inference covers
- **Method → existing class**: call sites lowered (`Owner.m(recv, …)` → `recv.m(…)`), the
orphaned local import removed.
- **Method → module-level free function**: call sites requalified (`Owner.m(…)` → `m(…)`).
- **Free function → existing module**: the call stays bare; callers repath their import
(`repath_import` when function-scoped; module-level repoints realised as remove-old +
add-new).
- **New-module extract of scattered defs**: `extract_symbols_to_new_module` under the
audited header; a constant that relocated into the header is dropped from the source.
A contiguous-tail source still uses `extract_to_new_module`.
- **Inline-block extract-function** (intra-file): a new helper whose verbatim body is a
block cut from a sibling function, that function's block replaced by a call — inferred as
`extract_function`, authoring only the signature, the call, and (when the block ends in
`lhs = expr` returned by the helper) a `return lhs`. A body edited on the way out (a
de-self / restructure) does not infer — the residual surfaces it.
- **A move landing just above an `if TYPE_CHECKING:` guard**: anchored with
`move_symbol(after=<preceding symbol>)` rather than a `before=` that would overshoot past
the guard.
- **A source file the commit deletes** once its defs relocated: `delete_file`.
- **The module-level import diff**, realised directly from the target: gained names added
(a wholly new module's statement verbatim, wrapping kept, or one name folded into an
existing `from module import …` with `add_imported_name`), lost names removed with
`remove_imported_name`.
- Non-Python files in the commit do not block inference; their diff is noted and left to
the residual.
#### 2.2.2 What it reports `UNSUPPORTED`
- Single-commit mode prints the verdict with notes and exits non-zero; range mode
records it in the outputs.
- Review such a commit as prepare, or hand-write the `Repro` (§2.3).
- The cases:
- **no definition relocated** — a rename (even a privacy flip `_foo` → `foo`) or a
statement-level reorder; reshapes belong in prepare;
- **a new-module extract whose symbols are not all top-level in the source** — a
method still inside a class; prepare must de-self it out first;
- **an extract drawing from more than one source file** — compose `extract_function` by
hand. An inline-block extract-function within one file is inferred (§2.2.1), but only
when the body is a verbatim cut; a de-self / restructure on the way out is a separate
semantic commit and does not infer.
### 2.3 Hand-write the `Repro` when inference falls short
- Compose the transform from the same primitives (`spec-reproduction-utils.md` §3).
- The same byte-diff then certifies it.
- **`UNSUPPORTED`, or a primitive that cannot express the exact edit, is never a reason to
relabel a relocation as `non_mechanical_provable`.** If the change is a relocation, it
gets a proof: hand-write the `Repro` here, or (when a primitive genuinely lacks the needed
form, e.g. an insertion anchor) enhance the primitive first, then prove it. See
guide-split.md §2.7.6.
```python
import sys
from pathlib import Path
sys.path.append(".claude/skills/mechanical-refactor-verify/scripts")
from mechanical_refactor_reproduction_utils import Repro
r = Repro(base="<base_sha>", target="<commit>")
# Adapt call sites / repath imports BEFORE moving, so a call to a moved method from inside
# another moved method is lowered while still in the source and travels with the body.
r.lower_call_sites("update_weights_from_ipc", "ModelRunner", paths=["a.py", "b.py"])
r.remove_import("a.py", "from x import ModelRunner", in_function="update_weights_from_ipc")
r.move_symbol("update_weights_from_ipc", src="a.py", dst="dst.py", into_class="WeightUpdater", dedent=0)
r.add_import("dst.py", "import gc")
r.run() # PASS = byte-identical; otherwise prints the residual
```
### 2.4 A hand-written transform for a non-relocation mechanical change
- For a whole-file split or rename — no single symbol relocates — write a `transform()`
and call `verify_mechanical_refactor`.
- The scaffold (worktree, pre-commit, diff, reporting) lives in the skill's utils.
```python
import sys
from pathlib import Path
sys.path.append(".claude/skills/mechanical-refactor-verify/scripts")
from mechanical_refactor_reproduction_utils import verify_mechanical_refactor, git_add_and_commit
BASE_COMMIT = "<base_sha>"
TARGET_COMMIT = "<final_sha>"
def transform(dir_root: Path) -> None:
source = dir_root / "path/to/source.py"
lines = source.read_text().splitlines(keepends=True)
for target_path, start, end in [("path/to/a.py", 1, 50), ("path/to/b.py", 51, 120)]:
target = dir_root / target_path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("".join(lines[start - 1 : end]))
source.unlink()
git_add_and_commit("split source.py", cwd=str(dir_root))
# A rename is just: for each file, write content.replace(OLD, NEW); commit.
if __name__ == "__main__":
verify_mechanical_refactor(BASE_COMMIT, TARGET_COMMIT, transform)
```
@@ -0,0 +1,100 @@
# Modify this skill: the engine, the generator, or the spec
- How to change `scripts/mechanical_refactor_reproduction_utils.py` (the proof engine),
`scripts/mechanical_refactor_proof_generator.py` (the generator),
`scripts/mechanical_refactor_reproduction_cli.py` (the chain verifier), or the specs —
without silently weakening the proof.
- Read this **before** editing any file under this skill. The engine is trusted: a wrong
primitive certifies a non-mechanical commit as clean, and every downstream reviewer
believes it. Changes here carry a higher bar than ordinary code.
## 1. What has which bar
| File | Role | Bar |
|---|---|---|
| `spec-reproduction-utils.md` | **normative** source of truth (SKILL.md §2): the clean-move property, the whitelist / not-allowed lists, each primitive's contract, the arbiter | any behavior change lands here first |
| `spec-reproduction-cli.md` | **normative** source of truth for the chain verifier: the word rule, the proof obligation, the report, the exit codes | any behavior change lands here first |
| `mechanical_refactor_reproduction_utils.py` | **trusted engine**: the relocation primitives + the arbiter | highest — byte-faithfulness proven by tests |
| `mechanical_refactor_proof_generator.py` | **convenience**: infers a recipe from a diff | lower — may report `RESIDUAL`/`UNSUPPORTED` without compromising trust, but still tested |
| `mechanical_refactor_reproduction_cli.py` | **gatekeeper**: walks a chain, runs the proofs, reports | high — a false chain PASS certifies an unproven commit; classification, resolution, and PASS-criterion behavior proven by tests |
| `guide-*.md`, `SKILL.md` | workflow + file map | kept in sync, never describe behavior the code lacks |
## 2. Cardinal rule — the spec leads, code follows
- `spec-reproduction-utils.md` wins over every other file (SKILL.md §2). Code serves the
spec, not the reverse.
- A behavior change to a primitive, to §2.1/§2.2 (what counts as a clean move), or to the
§4 arbiter **must edit the spec in the same commit as the code**. Code and spec never
diverge across commits.
- New primitive → add its contract to §3.
- Changed clean-move boundary → edit §2.1 (allowed) / §2.2 (not allowed).
- Changed reproduce/diff behavior → edit §4.
- If you discover code and spec already disagree, the spec is authoritative: fix the code
to match — or, if the spec itself is wrong, change it **deliberately** in one commit with
the reasoning, not as a silent side effect.
## 3. The faithfulness invariant — never break this
- Every primitive relocates **original source bytes**: AST-located, spliced as the source
text that was there, **never regenerated**. A byte match after the formatter is the
*entire* proof — the moment a primitive regenerates instead of splicing, the proof is
worthless (it can no longer distinguish "moved" from "rewritten to look moved").
- A new or changed primitive **must**:
- locate its target through the AST, not by string search over source;
- splice the original bytes (interiors of multi-line strings, comments, a magic
trailing comma, semicolon-joined statements all survive verbatim);
- preserve the file's newline style (CRLF round-trips) and UTF-8-byte-accurate columns.
- Do **not** add a primitive that normalizes, reflows, or reformats. Formatting belongs to
the pre-commit pass in the §4 arbiter, applied to **both** sides; primitives do
relocation only.
- When the generator cannot infer a move, the answer is a hand-written `Repro`
(guide-construct-proof §2.3) — **not** loosening a primitive to make it fit.
## 4. Testing rules — the hard bar
- The engine is trusted, so an untested change to it is not acceptable. "It ran once" is
not a test.
- Run the **full** suite and keep it green:
```bash
cd scripts && uv run --with pytest --python 3.12 python -m pytest tests/ -q
```
Baseline at the time of writing: **188 passed**. Your change must leave the count at or
above baseline — never delete a case to make the suite pass.
- Layout mirrors the modules; put your test where it belongs:
- `tests/reproduction_utils/` — one `test_<primitive>.py` per engine primitive.
- `tests/proof_generator/` — the inference layer (`test_infer_*`, `test_script_and_diff`).
- `tests/reproduction_cli/` — the chain verifier (classification, proof discovery,
chain walking, the report).
- A new or changed **primitive** requires, in its `test_<primitive>.py`:
- a **byte-exact** assertion on the resulting file (compare full bytes, not "contains");
- at least one **adversarial** case where a regenerating implementation would differ
from splicing — a comment mid-body, a magic trailing comma, odd indentation, a
semicolon-joined import, non-ASCII text, or a CRLF file — asserting the original
bytes survive;
- the **raise paths** the spec promises: ambiguous anchor raises, missing anchor
raises, wrong/absent `from_class` raises.
- A change to the **generator** requires a `tests/proof_generator/` case that runs it on a
synthetic commit and asserts `PASS`, plus one non-move / bundled-change case that asserts
`RESIDUAL` or `UNSUPPORTED` — so a future regression that makes it "pass" a dirty commit
is caught.
- A change to the **chain verifier** requires a `tests/reproduction_cli/` case asserting a
verified chain passes, plus one asserting the broken shape it guards (an unclassified
commit, a missing proof, a failing proof) still fails — so a regression cannot silently
green a dirty chain.
- The engine stays **self-contained**: `mechanical_refactor_reproduction_utils.py` imports
only `git` (via subprocess) and the standard library. Do not add a third-party dependency
to it.
## 5. Before you commit — checklist
- [ ] `spec-reproduction-utils.md` / `spec-reproduction-cli.md` edited in this same
commit (if any behavior changed).
- [ ] Full pytest suite green; case count ≥ prior baseline.
- [ ] New/changed primitive has: byte-exact test + ≥1 adversarial (regeneration-would-differ)
case + the raise-path tests.
- [ ] Generator change has a `PASS` test **and** a `RESIDUAL`/`UNSUPPORTED` test.
- [ ] `SKILL.md` §3 file map and the relevant `guide-*.md` updated for any new file or
workflow change.
- [ ] `mechanical_refactor_reproduction_utils.py` still imports only git + stdlib.
@@ -0,0 +1,357 @@
# Split a mechanical refactor
- Two levels of splitting, one chapter each: §1 splits the **PR/branch** into small
classified pieces (the chain contract); §2 splits **one piece** into prepare + move +
postpare so its move is provable.
## 1. Split the PR into small verifiable pieces
### 1.1 The chain contract — what a compliant branch satisfies
When asked to make (or fix) a refactor branch so it "satisfies this skill", ALL of the
following must hold over `base..branch`; run the chain verifier
(`guide-verify-proof.md` §1) to check the machine-checkable part in one command.
1. **Every commit is classified, in the required subject format:**
```text
<group-id>(<commit-id>,<kind>): <message>
```
with `<kind>` exactly `mechanical_provable` or `non_mechanical_provable`, and
`<group-id>` / `<commit-id>` kebab-case (contiguous same-`<group-id>` commits form one
future PR). The verifier machine-checks the standalone-word rule
(`spec-reproduction-cli.md` §2.1); the full format is required on top of it so the
chain can be grouped into PRs.
2. **Classification is correct — mechanical work is labeled mechanical.** Every operation
expressible as the whitelisted relocations (an extract-function, a bulk move, a file
split, an import repoint, …) is its own `mechanical_provable` commit. Hiding provable
content inside a `non_mechanical_provable` commit — dodging the verifier — is
forbidden (§2.2 maximality); catching it is the reviewer's duty
(`guide-verify-proof.md` §1). How to split so this holds: §2.
3. **Every `mechanical_provable` commit has a proof that PASSes.** Produce the proofs
with the generator (`guide-construct-proof.md` §1); the chain verifier re-runs every
one of them against the proof folder.
4. **Every `non_mechanical_provable` commit is correctness-reviewed by eyes.** Its diff
must be confirmed to do exactly what its message claims: no lost logic, no hidden bug,
no unintended behavior change riding along (`guide-verify-proof.md` §1). When the
commit claims to be behavior-preserving that means checking equivalence — but a chain
need not be a pure refactor, and a commit that intentionally changes behavior is
reviewed for the correctness of that change instead. The machine never certifies
these — that is exactly why they must stay minimal (item 2).
### 1.2 Commit naming and classification
- The subject format is exactly §1.1 item 1 — no reserved phase suffixes are required.
The `<commit-id>` is free (naming it after the phase, e.g. `foo-prepare` / `foo-move`,
is fine but optional).
- The phases map onto the classification word directly: **move** commits declare
`mechanical_provable`; **prepare**, **postpare**, and standalone semantic commits
declare `non_mechanical_provable`.
- The generator's range command selects the provable commits by the word itself:
`--match '(?<!_)mechanical_provable'` (the lookbehind keeps `non_mechanical_provable`
from matching).
## 2. Split one piece into prepare + move + postpare
### 2.1 Why split
- A "move a method/function" change is really **two operations with different
correctness criteria**:
| Operation | What it does | How you check it |
|---|---|---|
| **Semantic reshape** | method → free function or method; `self.X` → a parameter, or `self` retyped to the target class; signature / typing change | behavior unchanged: lint + tests pass |
| **Physical move** | cut from the source, paste into the target, fix imports | the moved body is byte-identical, line for line; the only other changes are move artifacts |
- Put both in one commit and the criteria contaminate each other:
- one hunk then holds the reshape **and** an indentation shift **and** a cross-file
relocation;
- neither a human nor a tool can mechanically confirm "the body that landed is the
body that left" — you must re-read the logic.
### 2.2 The rule — up to three commits, in this order
- **prepare (optional)** — a **minimal** in-place reshape the relocation needs (de-self a
method, retype `self`). Human-reviewed, so: small, **no cross-file def relocation, no
body relocation** — the code stays where it is.
- **move** — the pure relocation; carries the **bulk**; certified by the reproduce proof
(`guide-construct-proof.md`; property: `spec-reproduction-utils.md`).
- **postpare (optional)** — a **minimal** tail fixup the move cannot do mechanically (a
module path inside a string literal, a doc reference). Human-reviewed.
Hard lines ("prep" below = the prepare phase):
- Both ends are optional, minimal, and covered by tests; neither ever relocates a def
across files or moves a body.
- The move-artifact whitelist is what a relocation *forces* — **not** a licence to fold
reshape work into the move. Anything outside the artifacts in the move's diff = the
reshape leaked; push it back into prep.
- **A large semantic refactor is not a phase.** Consolidating bookkeeping, deduplicating
logic, restructuring control flow, redesigning an API → its **own commit**, reviewed for
**equivalence** (tests or a written argument). Never smuggled into prep as a "small
reshape".
- **Provable content never hides in a non-provable commit.** The dual of the previous
two rules: a commit declared `non_mechanical_provable` must be the **minimal residue**
the relocation primitives cannot express. Any part reproducible as whitelisted
relocations (`spec-reproduction-utils.md` §2.1) — a def moved across files, a scattered
extract, an import repoint riding along — is split into its own `mechanical_provable`
commit with a proof, never folded into a semantic commit where the verifier cannot see
it. Declaring provable work non-provable to dodge the verifier violates the chain
property (`spec-reproduction-cli.md` §2.1); the reviewer is instructed to hunt for
exactly this (`guide-verify-proof.md` §1).
- **"Semantic" is not banned from prepare — oversized or hidden semantics are.** prepare's
own edits *are* meaning-carrying (de-self, retype-`self`, co-locating bookkeeping);
"minimal" caps their **size**, it does not forbid semantics. The two bans are narrower:
(1) no semantic change inside the **move** commit — the move is a pure relocation; and
(2) don't pass a **large** reshape off as a trivial "small reshape" to dodge the
equivalence review. A large but honestly-labeled, equivalence-reviewed reshape placed
*before* the move is legitimate — that is exactly what "its own commit" means, and it
may serve as the prepare.
- The prep's shape depends on the destination: a module-level function (§2.3) or a class
(§2.4).
- The move is the same idea in both: a pure relocation, body byte-identical.
### 2.3 Case 1: method → free function
#### 2.3.1 Commit 1 — prep: de-self in place (no relocation)
Reshape the method **in its original file and position** so it no longer needs `self`.
The body stays put:
- `self.X` (read) → pass `X` in as a parameter.
- `self.X = v` (write) → `return v`; the caller assigns. (Or pass an explicit mutable
object.)
- `self.other_method(...)` → prep that method in the same commit, or inject it as a
`Callable` argument.
- Once `self` is gone → mark `@staticmethod`; the body **does not move**.
- Call site: `self.foo(args)` → `TheClass.foo(args)`.
- **Seed the destination's module-level scaffolding here too**, if the target module
lacks what the moved body needs — a `logger = logging.getLogger(__name__)`, a
module-level constant the body reads (`_is_hip = is_hip()`), and the `import` each
requires. This is destination groundwork (like a class skeleton, §2.7.4), **not** the
body: adding it in prep keeps the move a pure cut+paste. Folding it into the move
instead bundles a non-relocation edit and breaks the byte proof (the move would both
paste the body **and** author a new `logger`, which the whitelist does not forgive).
A move into a **new** module is the exception — there the whole header, logger
included, is authored in the move itself (§2.5).
- The decorator and the qualifier are the only artifacts the move will carry — exactly
what the whitelist (`spec-reproduction-utils.md` §2.1) forgives.
**Check:** lint + tests pass; the diff is the body reshape, the call-site qualifier, and
any destination scaffolding seeded above; nothing moved.
#### 2.3.2 Commit 2 — move: relocate to the module
- Cut the `@staticmethod` block; paste into the target module.
- Drop `@staticmethod`, dedent to module level — body **unchanged, line for line**.
- Source file: import the moved symbol; drop now-unused imports.
- Call site: `TheClass.foo(args)` → `foo(args)` (args untouched).
**Check:** `mechanical_refactor_proof_generator.py <commit>` reports `PASS`. Cross-check:
`git show <commit> --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change`
marks the whole block as moved.
### 2.4 Case 2: method → method on a class
- For pulling **several methods and the fields they touch** into a new (or existing)
class.
- Prep does **not** de-self — it builds the class and retypes `self`, body untouched.
#### 2.4.1 Commit 1 — prep: build the class, retype `self`
1. Create the target class with the fields the moved methods touch (a frozen dataclass is
simplest; drop `frozen` only if they mutate).
2. Wire an instance into the call path — composition (`self.component = Target(...)` in
the source ctor), construction at the call site, or temporarily both.
3. Retype each moved method as a `@staticmethod` whose parameter is still **named** `self`
but **typed** as the target class — body unchanged:
```python
class Source:
component: Target
@staticmethod
def foo(self: Target) -> None:
... # body still reads self.field_a / self.field_b
```
4. Caller: `self.foo(...)` → `Source.foo(self.component, ...)`.
Why keep the name `self`:
- it is an ordinary parameter name, so every `self.X` resolves against the target class
statically and at runtime (the argument *is* a target-class instance);
- renaming it would rewrite every `self.X` and destroy the "body unchanged across both
commits" invariant.
Boundaries:
- **Prep stays minimal.** Signature redesign, helper extraction, parameter objects,
mutate→return, renames, method splits, dead-branch removal → later non-mechanical
commits, never prep.
- **Runtime-mutable state → inject a `Callable` getter (still prep).** State that changes
every step (counters, the current batch, running stats): inject `Callable[[], T]` into
the target ctor; rewrite `self.X` → `self.get_X()`. Do **not** thread it per call and do
**not** reach back into the source object — per-call kwargs make every call site noisy,
the API non-self-contained, and the threading a caller chore.
```python
class Target:
def __init__(self, *, static_field, get_running_state: "Callable[[], State]"):
self.static_field = static_field
self.get_running_state = get_running_state
@staticmethod
def check(self: "Target") -> None:
running = self.get_running_state() # was self.running_state
...
```
```python
# source ctor
self.component = Target(
static_field=...,
get_running_state=lambda: self.running_state,
)
```
**Check:** lint + tests pass; body unchanged; types check (`self: Target` matches the
instance the caller passes).
#### 2.4.2 Commit 2 — move: relocate into the class
- Cut `foo` into the target class; drop `@staticmethod` — body **unchanged, line for
line**.
- Header: `def foo(self: Target)` → `def foo(self)` (type redundant inside the class).
- Caller: `Source.foo(self.component, ...)` → `self.component.foo(...)` — the receiver
moves out of the argument list (replayed by `lower_call_sites`).
**Check:** `mechanical_refactor_proof_generator.py <commit>` reports `PASS`. The split
paid off: prep left the body untouched, so the move is a clean cut/paste.
### 2.5 Case 3: extract to a new module — one move commit, no prep
- The move gathers the defs **from wherever they sit** — no prep staging at the source
tail. Replayed by `extract_symbols_to_new_module`.
- Each def/class is cut **verbatim** (the byte diff certifies the bodies); the new file's
small header (imports, a logger, constants, a `TYPE_CHECKING` block) is authored from
the target and audited (`spec-reproduction-utils.md` §2.1).
- A module-level constant that moved into the header (e.g. `_is_hip = is_hip()`) is
dropped from the source too.
- The only work outside the move: a non-mechanical reference the move cannot derive (a
string-literal module path) — a one-line **postpare**.
- A symbol **not top-level** in the source (a method still in a class): prepare de-selfs
it out first (§2.3); the proof reports `UNSUPPORTED` until then.
### 2.6 Case 4: extract-function — the bulk goes in the move
- The relocated body belongs in a certified move, not buried in a prep: the
`extract_function` primitive cuts the inline block **verbatim** and authors only the
interface (signature, optional `return`, the replacing `call`).
- Faithful **only when the body moves unchanged.** De-self, control-flow restructure, or a
bookkeeping change folded in → do that as a separate semantic commit (reviewed for
equivalence) **first**, then move the now-unchanged body.
- An extraction that rewrites the body *as* it extracts is a semantic commit, not a
certifiable move — do not dress it up as one.
### 2.7 Remarks
#### 2.7.1 A move never renames
- The moved symbol keeps the **same name on both sides**.
- A rename — even a privacy flip `_foo` → `foo` — is its own single-purpose commit
*before* the move (rename in place, update call sites).
- A move that also renames cannot be machine-certified: split it — rename first, then
move.
#### 2.7.2 Anti-pattern: prep adds the body, move deletes it
- Symptom: prep **adds** a large block to the target; the move **deletes** the same block
from the source. The order is reversed.
- Correct order: prep leaves the body in the source (target skeleton, header retype,
caller qualification only); the move does the cut/paste.
- The body appears and disappears exactly once — on the move side. Fix by pushing the
"add the body" work out of prep into the move.
#### 2.7.3 Anti-pattern: the giant prep (relocating inside the source to stage the move)
- Symptom: the prep's diff is **hundreds of lines** for a single function — because it
also moved the function to the source file's tail, rewrote it as a free function next
to a staged import/constant block, or reordered its neighbors so the move can cut one
contiguous block.
- All of that staging is unnecessary: `extract_symbols_to_new_module` gathers symbols
**from wherever they sit** (§2.5) — the move needs no contiguity and no tail parking.
- A prep's legitimate diff is the handful of lines the primitives cannot derive: the
`@staticmethod` decorator, the kwargs signature, `self.x` → parameter reads, an added
`return`, the class-qualified call site. For one function that is **tens of lines, not
hundreds** — a prep in the hundreds is the signal the relocation leaked into it.
- Why it matters: every relocated-but-not-certified line in a prep is a line the machine
never checks and a reviewer must eyeball; parking blocks mid-file also leaves broken or
duplicated intermediate states (a staged import for a module that does not exist yet).
- Fix: strip the prep back to the interface edits above, leave the body **in place**,
and let the certified move do all relocation.
#### 2.7.4 When NOT to split (single commit)
- Moving an **already** module-level free function.
- Pure file rename / whole-file move.
- Trivial field deletion, or `getattr(obj, "x", ...)` → direct attribute access.
- A class-internal helper relocated next to another helper in the same module.
#### 2.7.5 Which actions are mechanical vs not
- Boundary: building the component correctly the first time is mechanical; reshaping it
*after* it exists is not.
| Action | Bucket |
|---|---|
| target class skeleton + ctor + fields | mechanical (prep) |
| destination module scaffolding (a `logger`, a module-level constant) the moved symbol needs, when moving into an **existing** module | mechanical (prep) |
| `@dataclass(frozen=True, slots=True, kw_only=True)` decoration | mechanical (prep) |
| composition wiring (`self.component = Target(...)`) | mechanical (prep) |
| `Callable` getter injection for runtime-mutable state | mechanical (prep) |
| platform conditionals carried along with the body | mechanical (prep / move) |
| cross-file import path rewrites | mechanical (move) |
| field-ownership migration into the component ctor | mechanical (a single pre-step) |
| inlining an `init_*` method body into a ctor | mechanical (a single pre-step) |
| privacy flip (`_x` ↔ `x`) | mechanical (a single rename) |
| signature redesign (new kwargs, changed defaults, positional → kw-only) | **not** mechanical |
| body simplification / dead-branch removal / logic rewrite | **not** mechanical |
| semantic method rename | **not** mechanical |
- The smaller the prep, the easier "behavior unchanged" is to confirm.
- Many small, independently reviewable commits beat one big prep mixing ten flavors of
change.
- Review order = commit order: prep → move → non-mechanical follow-ups.
#### 2.7.6 Anti-pattern: the non-mechanical label as an escape hatch
- Symptom: a commit whose body is a **pure relocation** (a cut+paste move, a module-level
constant move, a verbatim inline-block extract) is labelled `non_mechanical_provable` and
ships with no proof — because the generator reported `UNSUPPORTED` or a primitive could
not express the exact insertion point, so the author reached for the softer label instead
of a proof.
- Real example from this repo's history: `kvc-move-lazy-compaction-gate` relocated the
module-level `_should_enable_lazy_compaction` unchanged into `kv_cache_configurator.py` but
was labelled `non_mechanical_provable`, because it had to land *above* an
`if TYPE_CHECKING:` guard and `move_symbol` only anchored with `before=`, which overshot
past the guard. The relocation was fully mechanical; only the tool's insertion-anchor was
missing — so the fix was to add a `move_symbol(after=)` anchor and prove it, not to keep
the softer label. (A sibling commit that moves a *contiguous block* of constants plus their
leading comment needs a block-move primitive the toolkit does not yet have — that one is
still awaiting an enhancement, which is the correct disposition, not a relabel.)
- The rule, in order:
1. A pure relocation **must** be `mechanical_provable` and carry a proof. The label is
a claim about the change, not about how easy the tooling made it.
2. Generator says `UNSUPPORTED` but the change *is* a relocation → **hand-write the
`Repro`** from the same primitives (guide-construct-proof.md §2.3). Inference falling
short is not a licence to drop the proof.
3. A primitive genuinely cannot express the faithful edit (the missing `after=` anchor
above) → **enhance the primitive first**, then prove it. The fix for a tooling gap is
to close the gap, not to relabel the commit as unprovable.
- Only a change that is genuinely *not* a relocation (a signature redesign, a logic rewrite,
a de-self restructure) earns `non_mechanical_provable`. If you cannot say which
non-relocation edit justifies the label, the label is wrong.
@@ -0,0 +1,148 @@
# Verify a proof
- How the reviewer of a claimed-mechanical chain (or a single commit) consumes its proof.
- The certified property and primitive contracts: `spec-reproduction-utils.md`; the
chain-level contract: `spec-reproduction-cli.md`.
- How the proof was produced and the folder it arrives in: `guide-construct-proof.md`.
## 0. Do not trust the PR — verify yourself
- Everything the PR shows you is a **claim**, not evidence: a pasted `PASS` verdict, a
pasted chain report, a green checkmark, the classification words themselves. All of it
is text the author (or the author's tooling) produced and could be wrong or fabricated.
- The proof is only ever the run **you** perform locally: run the chain verifier (§1)
against the PR's actual base and head, with the proof folder you downloaded — never
approve from the author's pasted output.
- This is cheap by design: the whole point of the machinery is that re-verification is
one command, so there is no excuse to trust instead of re-run.
- **Sampling is not verification.** Re-running a subset of the proofs ("spot-check 8 of
43") proves nothing about the rest and must never be the basis for approval — the only
acceptable run is the §1 chain verifier, which executes **every** provable commit's
proof. The same holds for the manual duties: audit every `HUMAN_REVIEW` row and every
PASS's authored surfaces (§2.3), not a sample of them.
## 1. Verify the whole chain
- The default — and the only sufficient — entry point: do not re-run proofs one by one,
and never a sample; run the chain verifier over the whole chain:
```bash
python3 .claude/skills/mechanical-refactor-verify/scripts/mechanical_refactor_reproduction_cli.py \
--base <base-commit> --branch <pr-branch-name> --proof <folder>
```
- It checks every commit declares `mechanical_provable` or `non_mechanical_provable`,
runs every provable commit's proof, and prints + writes a full report
(`<folder>/chain_report.md`); exit 0 iff the chain verifies.
- Proofs run up to `--jobs` at a time (default 3; each proof works in its own throwaway
worktree, so this is safe) — raise it to shorten a long chain's wall clock.
- Re-running a long chain: add `--skip-passed` to reuse **this machine's own** earlier
PASS verdicts for unchanged proofs (keyed by sha + script hash + utils hash, stored
under the repo's `.git/`, never shipped with the proof folder — so §0 still holds;
contract: `spec-reproduction-cli.md` §3.5).
- The contract (word rule, proof resolution, PASS criterion, exit codes):
`spec-reproduction-cli.md`.
- The `HUMAN_REVIEW` rows in the report are your remaining manual surface — the declared
non-mechanical commits, plus the §2.3 authored-surface audit of each PASS.
- Each `HUMAN_REVIEW` row carries **two** review duties, and the commit is not approved
until both hold.
- Duty 1 — **correctness-review the diff itself**: a `non_mechanical_provable` commit is
exactly the part the machine never certifies, so read its diff and confirm it does
exactly what its message claims — no lost logic (a branch, a write, an early return
dropped on the floor), no hidden bug, no unintended behavior change riding along. When
the commit claims to be behavior-preserving, that means checking equivalence; a commit
that intentionally changes behavior (a chain need not be a pure refactor) is reviewed
for the correctness of that change instead. Tests passing is supporting evidence, not
the review.
- Duty 2 — **verify the declaration itself**: the commit asserts **nothing in it is a
provable relocation** (`spec-reproduction-cli.md` §2.1), and hiding provable content
there to dodge the verifier is exactly the escape this chain check exists to close.
- Read the commit's diff for relocated code. Concretely, run
`git show <sha> --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change`
and look for moved blocks, and run
`python3 .claude/skills/mechanical-refactor-verify/scripts/mechanical_refactor_proof_generator.py <sha>`
to see what a relocation recipe would cover.
- A hidden provable part is not a judgement call: demand the split
(`guide-split.md` §2.2) — do not approve the commit as-is.
- **A `non_mechanical_provable` commit whose body is a large verbatim block relocation
the primitives can express** — a cut+paste move (including one landing above an
`if TYPE_CHECKING:` guard, now anchorable with `move_symbol(after=)`), a module-level
constant move, or a verbatim inline-block extract (the generator now infers it as
`extract_function`) — is a **FINDING**, not an acceptable label. The generator being
unable to infer it, or a past tooling gap, does not license the softer label: demand
it be relabelled `mechanical_provable` with a hand-written `Repro`, or the primitive
enhanced (guide-split.md §2.7.6). Only a genuine non-relocation edit (signature
redesign, logic rewrite, de-self restructure) justifies the label.
## 2. Verify a single commit
- For diagnosing one commit (a failing proof, a suspicious script) — never a substitute
for §1: approving a chain requires the full §1 run, not single-commit re-runs of a
chosen subset.
### 2.1 Re-run it
- From the repo root:
```bash
python3 <folder>/repro_scripts/<sha>.py
```
- When the proof arrived as a gist (`guide-construct-proof.md` §1.3), download it first:
```bash
gh gist clone <gist_id> /tmp/proof # or: git clone https://gist.github.com/<gist_id>.git /tmp/proof
cd <repo-root> # the run resolves the repo from the cwd
python3 /tmp/proof/<sha>.py # PASS = byte-identical to this commit
```
- The run *is* the proof — it replays the primitives from the base commit and byte-diffs
against the target in a throwaway worktree.
- The script prints the verdict and exits 0 only on PASS (a residual exits non-zero), so
a harness can consume the exit code.
- Do not trust a pasted verdict you did not re-run.
### 2.2 Read the verdict
- **PASS** — byte-identical: the commit is exactly the relocations listed in the script,
nothing else.
- **RESIDUAL** — a non-empty diff: precisely the bundled non-move change. Review it as
semantic content; a legitimate tail fixup (string-literal module path, doc reference)
belongs in a postpare commit, not the move.
- **UNSUPPORTED** — no recipe inferred (cases: `guide-construct-proof.md` §2.2.2). Not
thereby wrong, but not machine-certified: review by hand as a prepare-style reshape, or
ask the author for a hand-written `Repro`.
### 2.3 Audit the authored surfaces
- A PASS certifies the relocated bytes; the small **authored** surfaces are reproduced
from the target and need human eyes.
- In the script, check:
- the `header=` of `extract_symbols_to_new_module` — the module audits its content
(imports / docstring / TYPE_CHECKING imports / logger / relocated `drop_assigns`
copies only); what remains for you: should those assignments move at all?
- a `leave_delegate=` on `move_symbol` — the forwarding stub is authored code in the
source file;
- the `signature=` / `return_text=` / `call=` of `extract_function` — the new
function's interface is authored; only its body is certified;
- the `drop_assigns=` list — each named constant leaves the source file.
### 2.4 Know what a PASS does and does not assert
- Requalification / lowering / repath in a script is tied to symbols the same script
relocates; a consumer-only call or import rewrite (no relocated definition) cannot
reproduce as a move — it surfaces as a residual.
- Whatever the repo's pre-commit hooks auto-fix is absorbed on both sides
(`spec-reproduction-utils.md` §4) — the hook set is part of what you trust.
- A PASS judges the **shape of a relocation**, not **intent**: "this commit is exactly
these relocations", not "this relocation was a good idea". Confirm the commit's subject
matches what the script actually moves before approving.
### 2.5 Why the mechanism is trustworthy
- It runs the real formatter and compares bytes — no diff-shape heuristic to fool
(`spec-reproduction-utils.md` §4).
- The proof is the few primitive calls in the script; auditing them (plus §2.3) is the
whole human surface.
- The folder is self-contained and re-runnable by anyone — a CI step or a reviewer —
without the skill installed.
@@ -1,80 +0,0 @@
"""Utilities for mechanical refactor verification scripts.
See SKILL.md for usage and transform script template.
"""
import shlex
import subprocess
import sys
import tempfile
from collections.abc import Callable
from pathlib import Path
def exec_command(cmd: str, cwd: str | None = None, check: bool = True) -> str:
print(f" $ {cmd}", flush=True)
result = subprocess.run(
cmd,
shell=True,
cwd=cwd,
capture_output=True,
text=True,
)
if check and result.returncode != 0:
print(f"FAILED: {result.stderr}", file=sys.stderr)
sys.exit(1)
return result.stdout.strip()
def git_add_and_commit(message: str, cwd: str) -> None:
exec_command(f"git add -A && git commit -m {shlex.quote(message)}", cwd=cwd)
def dedent(text: str, n: int) -> str:
"""Remove exactly n leading spaces from each line."""
lines = text.splitlines(keepends=True)
return "".join(line[n:] if line[:n] == " " * n else line for line in lines)
def verify_mechanical_refactor(
base_commit: str,
target_commit: str,
transform: "Callable[[Path], None]",
) -> None:
repo_root = exec_command("git rev-parse --show-toplevel")
worktree_dir = tempfile.mkdtemp(prefix="verify-mechanical-")
branch_name = f"verify-mechanical-{base_commit[:8]}"
try:
print(f"[1/4] Creating worktree at {base_commit[:8]}...")
exec_command(
f"git worktree add -b {branch_name} {worktree_dir} {base_commit}",
cwd=repo_root,
)
print("[2/4] Running transformation...")
transform(Path(worktree_dir))
print("[3/4] Running pre-commit...")
exec_command("pre-commit run --all-files", cwd=worktree_dir, check=False)
if exec_command("git status --porcelain", cwd=worktree_dir):
git_add_and_commit("pre-commit fixes", cwd=worktree_dir)
print(f"[4/4] Diffing against {target_commit[:8]}...")
diff = exec_command(
f"git diff {target_commit} -- .",
cwd=worktree_dir,
check=False,
)
if diff:
print(f"\nFAIL: diff is non-empty:\n{diff}")
sys.exit(1)
else:
print("\nPASS: transform reproduces the commit exactly.")
finally:
print(f"\nWorktree left at: {worktree_dir}")
print(f"Branch: {branch_name}")
print("To clean up manually:")
print(f" git worktree remove {worktree_dir} && git branch -D {branch_name}")
@@ -0,0 +1,484 @@
"""Verify a whole mechanical-refactor chain: classification, proofs, and a full report.
Every commit in ``base..branch`` must classify itself by carrying exactly one of the two
words ``mechanical_provable`` or ``non_mechanical_provable`` anywhere in its message (the
rest of the message format is free). Every ``mechanical_provable`` commit must ship a
proof script in the proof folder (``<proof>/repro_scripts/<sha-prefix>.py`` or a flat
``<proof>/<sha-prefix>.py``), and running the proof must PASS -- reproduce the commit
byte-for-byte. A ``non_mechanical_provable`` commit carries no machine proof and is left
to human review.
The run prints a markdown report, writes it into the proof folder (``chain_report.md``),
and exits 0 iff the whole chain verifies. Normative contract: spec-reproduction-cli.md.
python3 mechanical_refactor_reproduction_cli.py \
--base <base-commit> --branch <pr-branch-name> --proof path/to/proof/folder
"""
import argparse
import hashlib
import json
import re
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
from pathlib import Path
_DEFAULT_JOBS = 3
_PASSED_CACHE_FILENAME = "mechanical_refactor_passed_proofs.json"
_CACHED_PASS_DETAIL = "reused this machine's earlier PASS (--skip-passed)"
KIND_MECHANICAL = "mechanical_provable"
KIND_NON_MECHANICAL = "non_mechanical_provable"
VERDICT_PASS = "PASS"
VERDICT_FAIL = "FAIL"
VERDICT_MISSING_PROOF = "MISSING_PROOF"
VERDICT_AMBIGUOUS_PROOF = "AMBIGUOUS_PROOF"
VERDICT_HUMAN_REVIEW = "HUMAN_REVIEW"
VERDICT_UNCLASSIFIED = "UNCLASSIFIED"
VERDICT_AMBIGUOUS_KIND = "AMBIGUOUS_KIND"
_OK_VERDICTS = (VERDICT_PASS, VERDICT_HUMAN_REVIEW)
# The words are matched standalone: delimited by any non-[0-9A-Za-z_] character or the
# string boundary, so `non_mechanical_provable` never also counts as the bare word.
_KIND_WORD_RE = re.compile(r"(?<![0-9A-Za-z_])(non_)?mechanical_provable(?![0-9A-Za-z_])")
# The arbiter's verdict line (Repro.run / verify_mechanical_refactor both print `PASS:`).
_PASS_LINE_RE = re.compile(r"^PASS:", re.MULTILINE)
_MIN_PROOF_STEM_LEN = 7
_REPORT_FILENAME = "chain_report.md"
_FAIL_OUTPUT_TAIL_LINES = 60
class ChainVerificationError(Exception):
"""A setup problem (bad refs, non-linear range, missing proof folder): exit code 2."""
@dataclass(frozen=True)
class CommitVerdict:
sha: str
subject: str
kind: "str | None"
verdict: str
detail: str = ""
cached: bool = False
@property
def ok(self) -> bool:
return self.verdict in _OK_VERDICTS
@dataclass(frozen=True)
class _PendingProof:
sha: str
subject: str
kind: str
script: Path
@dataclass(frozen=True)
class ChainResult:
base: str
branch: str
base_sha: str
branch_sha: str
proof_dir: Path
verdicts: "list[CommitVerdict]" = field(default_factory=list)
@property
def passed(self) -> bool:
return bool(self.verdicts) and all(v.ok for v in self.verdicts)
def main(argv: "list[str]") -> int:
parser = argparse.ArgumentParser(description="Verify a whole mechanical-refactor chain against its proof folder.")
parser.add_argument("--base", required=True, help="base commit of the chain")
parser.add_argument("--branch", required=True, help="PR branch name (chain tip)")
parser.add_argument("--proof", required=True, help="proof folder path")
parser.add_argument("--repo-root", default=None, help="repo root (default: cwd's)")
parser.add_argument(
"--report",
default=None,
help=f"report file path (default: <proof>/{_REPORT_FILENAME})",
)
parser.add_argument(
"--jobs",
type=int,
default=_DEFAULT_JOBS,
help=f"max concurrent proof runs (default {_DEFAULT_JOBS})",
)
parser.add_argument(
"--skip-passed",
action="store_true",
help="reuse this machine's earlier PASS verdicts for unchanged proofs",
)
args = parser.parse_args(argv)
try:
result = verify_chain(
base=args.base,
branch=args.branch,
proof=Path(args.proof),
repo_root=args.repo_root,
jobs=args.jobs,
skip_passed=args.skip_passed,
)
except ChainVerificationError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
report = render_report(result)
report_path = Path(args.report) if args.report else result.proof_dir / _REPORT_FILENAME
report_path.write_text(report)
print(report)
print(f"report written to: {report_path}")
return 0 if result.passed else 1
def verify_chain(
*,
base: str,
branch: str,
proof: Path,
repo_root: "str | None" = None,
jobs: int = _DEFAULT_JOBS,
skip_passed: bool = False,
) -> ChainResult:
"""Classify every commit in ``base..branch`` and run every provable commit's proof.
Classification and proof resolution are sequential (cheap); the proof runs execute
concurrently, up to ``jobs`` at a time — safe because each proof works in its own
throwaway worktree. The verdict list keeps chain order. With ``skip_passed``, a
pending proof whose (sha, script hash, utils hash) triple this machine already ran to
a PASS is reused instead of re-executed; every fresh PASS is recorded either way."""
root = repo_root or _repo_root()
if not proof.is_dir():
raise ChainVerificationError(f"proof folder does not exist: {proof}")
base_sha = _rev_parse(base, root)
branch_sha = _rev_parse(branch, root)
commits = _linear_commits(base_sha=base_sha, branch_sha=branch_sha, root=root)
resolved: list[CommitVerdict | _PendingProof] = []
for sha in commits:
subject = _git_output(["log", "-1", "--format=%s", sha], root).strip()
message = _git_output(["log", "-1", "--format=%B", sha], root)
resolved.append(_resolve_commit(sha=sha, subject=subject, message=message, proof=proof))
cache_path = _passed_cache_path(root)
cache = _load_passed_cache(cache_path)
if skip_passed:
resolved = [_reuse_cached_pass(item, cache=cache) for item in resolved]
pending_by_sha = {item.sha: item for item in resolved if isinstance(item, _PendingProof)}
verdicts: list[CommitVerdict] = _run_pending_proofs(resolved=resolved, root=root, jobs=jobs)
_record_passes(
cache=cache,
cache_path=cache_path,
verdicts=verdicts,
pending_by_sha=pending_by_sha,
)
return ChainResult(
base=base,
branch=branch,
base_sha=base_sha,
branch_sha=branch_sha,
proof_dir=proof,
verdicts=verdicts,
)
def render_report(result: ChainResult) -> str:
"""The full chain report as markdown: header, per-commit table, failure details."""
n_mech = sum(1 for v in result.verdicts if v.kind == KIND_MECHANICAL)
n_non_mech = sum(1 for v in result.verdicts if v.kind == KIND_NON_MECHANICAL)
n_unclassified = sum(1 for v in result.verdicts if v.kind is None)
n_pass = sum(1 for v in result.verdicts if v.verdict == VERDICT_PASS)
n_cached = sum(1 for v in result.verdicts if v.cached)
lines = [
"# Mechanical refactor chain report",
"",
f"- base: `{result.base}` (`{result.base_sha[:12]}`)",
f"- branch: `{result.branch}` (`{result.branch_sha[:12]}`)",
f"- proof folder: `{result.proof_dir}`",
f"- chain verdict: **{'PASS' if result.passed else 'FAIL'}**",
f"- commits: {len(result.verdicts)} total — {n_mech} {KIND_MECHANICAL}, "
f"{n_non_mech} {KIND_NON_MECHANICAL}, {n_unclassified} classification error(s)",
f"- proofs: {n_pass}/{n_mech} PASS",
*([f"- reused from the passed-proof cache (--skip-passed): {n_cached}"] if n_cached else []),
"",
"| # | commit | kind | verdict | subject |",
"|---|--------|------|---------|---------|",
]
for i, v in enumerate(result.verdicts, start=1):
kind = v.kind or "?"
subject = v.subject.replace("|", "\\|")
lines.append(f"| {i} | `{v.sha[:9]}` | {kind} | {v.verdict} | {subject} |")
failures = [v for v in result.verdicts if not v.ok]
if failures:
lines += ["", "## Failure details"]
for v in failures:
lines += [
"",
f"### `{v.sha[:9]}` — {v.verdict}",
"",
v.detail or "(no detail)",
]
return "\n".join(lines) + "\n"
def _reuse_cached_pass(item: "CommitVerdict | _PendingProof", *, cache: dict) -> "CommitVerdict | _PendingProof":
"""Turn a pending proof into a cached PASS verdict on an exact cache-key match."""
if not isinstance(item, _PendingProof):
return item
if cache.get("passed", {}).get(item.sha) != _proof_cache_key(item.script):
return item
print(f"proof {item.sha[:9]} {VERDICT_PASS} (cached)", flush=True)
return CommitVerdict(
sha=item.sha,
subject=item.subject,
kind=item.kind,
verdict=VERDICT_PASS,
detail=_CACHED_PASS_DETAIL,
cached=True,
)
def _record_passes(
*,
cache: dict,
cache_path: Path,
verdicts: "list[CommitVerdict]",
pending_by_sha: "dict[str, _PendingProof]",
) -> None:
"""Record every freshly-run PASS into the cache (a FAIL is never recorded)."""
fresh = [v for v in verdicts if v.verdict == VERDICT_PASS and not v.cached and v.sha in pending_by_sha]
if not fresh:
return
for v in fresh:
cache.setdefault("passed", {})[v.sha] = _proof_cache_key(pending_by_sha[v.sha].script)
try:
cache_path.write_text(json.dumps(cache, indent=2, sort_keys=True) + "\n")
except OSError as exc:
print(f"note: could not write passed-proof cache {cache_path}: {exc}")
def _proof_cache_key(script: Path) -> "dict[str, str]":
"""The cache key parts beyond the sha: hashes of the script and its utils module."""
utils_sha256 = ""
for directory in (script.parent, script.parent.parent):
utils = directory / "mechanical_refactor_reproduction_utils.py"
if utils.is_file():
utils_sha256 = hashlib.sha256(utils.read_bytes()).hexdigest()
break
return {
"script_sha256": hashlib.sha256(script.read_bytes()).hexdigest(),
"utils_sha256": utils_sha256,
}
def _passed_cache_path(root: str) -> Path:
common_dir = _git_output(["rev-parse", "--git-common-dir"], root).strip()
common = Path(common_dir)
if not common.is_absolute():
common = Path(root) / common
return common / _PASSED_CACHE_FILENAME
def _load_passed_cache(path: Path) -> dict:
"""The cache is best-effort: missing, corrupt, or unreadable means empty."""
try:
data = json.loads(path.read_text())
except (OSError, ValueError):
return {"passed": {}}
if not isinstance(data, dict) or not isinstance(data.get("passed"), dict):
return {"passed": {}}
return data
def _run_pending_proofs(
*, resolved: "list[CommitVerdict | _PendingProof]", root: str, jobs: int
) -> "list[CommitVerdict]":
"""Execute the pending proofs on a bounded thread pool; keep chain order."""
pending = [(i, item) for i, item in enumerate(resolved) if isinstance(item, _PendingProof)]
finished: dict[int, CommitVerdict] = {}
if pending:
with ThreadPoolExecutor(max_workers=max(1, jobs)) as pool:
futures = {i: pool.submit(_proof_verdict, item, root=root) for i, item in pending}
for i, future in futures.items():
finished[i] = future.result()
return [finished[i] if isinstance(item, _PendingProof) else item for i, item in enumerate(resolved)]
def _proof_verdict(pending: _PendingProof, *, root: str) -> CommitVerdict:
passed, output = _run_proof(script=pending.script, root=root)
if passed:
verdict = CommitVerdict(
sha=pending.sha,
subject=pending.subject,
kind=pending.kind,
verdict=VERDICT_PASS,
detail="",
)
else:
tail = "\n".join(output.splitlines()[-_FAIL_OUTPUT_TAIL_LINES:])
verdict = CommitVerdict(
sha=pending.sha,
subject=pending.subject,
kind=pending.kind,
verdict=VERDICT_FAIL,
detail=(f"proof `{pending.script}` did not PASS; output tail:\n\n" f"```\n{tail}\n```"),
)
print(f"proof {pending.sha[:9]} {verdict.verdict}", flush=True)
return verdict
def _resolve_commit(*, sha: str, subject: str, message: str, proof: Path) -> "CommitVerdict | _PendingProof":
kind, classification_error = _classify(message)
if kind is None:
return CommitVerdict(
sha=sha,
subject=subject,
kind=None,
verdict=classification_error,
detail=(
f"the commit message must contain exactly one of the words "
f"`{KIND_MECHANICAL}` or `{KIND_NON_MECHANICAL}`"
),
)
if kind == KIND_NON_MECHANICAL:
return CommitVerdict(
sha=sha,
subject=subject,
kind=kind,
verdict=VERDICT_HUMAN_REVIEW,
detail="declared non_mechanical_provable: no machine proof, review by hand",
)
scripts = _find_proof_scripts(proof=proof, sha=sha)
if not scripts:
return CommitVerdict(
sha=sha,
subject=subject,
kind=kind,
verdict=VERDICT_MISSING_PROOF,
detail=(
f"no proof script found; searched `{proof / 'repro_scripts'}` and "
f"`{proof}` for `<sha-prefix>.py` (>= {_MIN_PROOF_STEM_LEN} hex chars)"
),
)
if len(scripts) > 1:
listing = ", ".join(f"`{p}`" for p in scripts)
return CommitVerdict(
sha=sha,
subject=subject,
kind=kind,
verdict=VERDICT_AMBIGUOUS_PROOF,
detail=f"multiple proof scripts match this commit: {listing}",
)
return _PendingProof(sha=sha, subject=subject, kind=kind, script=scripts[0])
def _classify(message: str) -> "tuple[str | None, str]":
"""The commit's declared kind, or (None, error-verdict) when the word rule is broken.
Exactly one of the two words must appear (any number of times, but only one of the
two): zero occurrences is UNCLASSIFIED, both words present is AMBIGUOUS_KIND."""
kinds = {KIND_NON_MECHANICAL if match.group(1) else KIND_MECHANICAL for match in _KIND_WORD_RE.finditer(message)}
if not kinds:
return None, VERDICT_UNCLASSIFIED
if len(kinds) > 1:
return None, VERDICT_AMBIGUOUS_KIND
return kinds.pop(), ""
def _find_proof_scripts(*, proof: Path, sha: str) -> "list[Path]":
"""Proof scripts naming this commit: a ``<sha-prefix>.py`` (lowercase hex, >= 7 chars)
under ``<proof>/repro_scripts/`` or flat in ``<proof>/``."""
found: list[Path] = []
for directory in (proof / "repro_scripts", proof):
if not directory.is_dir():
continue
for path in sorted(directory.glob("*.py")):
stem = path.stem
is_sha_prefix = (
len(stem) >= _MIN_PROOF_STEM_LEN
and all(c in "0123456789abcdef" for c in stem)
and sha.startswith(stem)
)
if is_sha_prefix:
found.append(path)
return found
def _run_proof(*, script: Path, root: str) -> "tuple[bool, str]":
"""Run one proof script from the repo root. A PASS is exit code 0 AND the arbiter's
``PASS:`` verdict line on stdout (an old-style script that exits 0 with a residual is
therefore still a FAIL)."""
result = subprocess.run(
[sys.executable, str(script.resolve())],
cwd=root,
capture_output=True,
text=True,
)
output = result.stdout + result.stderr
passed = result.returncode == 0 and bool(_PASS_LINE_RE.search(result.stdout))
return passed, output
def _linear_commits(*, base_sha: str, branch_sha: str, root: str) -> "list[str]":
if not _is_ancestor(base_sha=base_sha, branch_sha=branch_sha, root=root):
raise ChainVerificationError(f"base {base_sha[:12]} is not an ancestor of branch {branch_sha[:12]}")
commits = _git_output(["rev-list", "--reverse", f"{base_sha}..{branch_sha}"], root).split()
if not commits:
raise ChainVerificationError(f"no commits in {base_sha[:12]}..{branch_sha[:12]}")
merges = [sha for sha in commits if len(_git_output(["rev-list", "--parents", "-n", "1", sha], root).split()) > 2]
if merges:
listing = ", ".join(sha[:9] for sha in merges)
raise ChainVerificationError(f"the chain must be linear, but it contains merge commit(s): {listing}")
return commits
def _is_ancestor(*, base_sha: str, branch_sha: str, root: str) -> bool:
result = subprocess.run(
["git", "merge-base", "--is-ancestor", base_sha, branch_sha],
cwd=root,
capture_output=True,
)
return result.returncode == 0
def _rev_parse(ref: str, root: str) -> str:
result = subprocess.run(
["git", "rev-parse", "--verify", f"{ref}^{{commit}}"],
cwd=root,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise ChainVerificationError(f"cannot resolve {ref!r}: {result.stderr.strip()}")
return result.stdout.strip()
def _git_output(args: "list[str]", root: str) -> str:
result = subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, check=True)
return result.stdout
def _repo_root() -> str:
return subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,19 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from generator_testlib import _git
@pytest.fixture
def repo(tmp_path: Path) -> Path:
root = tmp_path / "repo"
root.mkdir()
_git(root, "init", "-q")
_git(root, "config", "user.email", "test@example.com")
_git(root, "config", "user.name", "test")
_git(root, "config", "commit.gpgsign", "false")
return root
@@ -0,0 +1,92 @@
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
def _git(repo: Path, *args: str) -> str:
return subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True, text=True).stdout.strip()
def _write(repo: Path, **files: str | None) -> None:
for name, content in files.items():
path = repo / name.replace("__", "/")
if content is None:
path.unlink()
else:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
def _commit(repo: Path, message: str) -> str:
_git(repo, "add", "-A")
_git(repo, "commit", "-q", "-m", message)
return _git(repo, "rev-parse", "HEAD")
def _method_onto_class(repo: Path) -> None:
"""Stage a base + a 'move foo from M onto C, lower the caller' commit."""
_write(
repo,
**{
"model.py": (
"class M:\n"
" @staticmethod\n"
" def foo(self, x):\n"
" return x + 1\n"
"\n"
" def other(self):\n"
" return 0\n"
),
"comp.py": "class C:\n def keep(self):\n return 1\n",
"caller.py": (
"class K:\n"
" def run(self):\n"
" from model import M\n"
"\n"
" return M.foo(self.c, 9)\n"
),
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "class M:\n def other(self):\n return 0\n",
"comp.py": (
"class C:\n"
" def keep(self):\n"
" return 1\n"
"\n"
" def foo(self, x):\n"
" return x + 1\n"
),
"caller.py": ("class K:\n def run(self):\n return self.c.foo(9)\n"),
},
)
_commit(repo, "move foo onto C")
def _free_function_move_with_module_level_caller(repo: Path) -> None:
"""Stage a free function moved model.py -> util.py whose caller imports it at module
level (so the repoint shows up in the symmetric module-level import diff)."""
_write(
repo,
**{
"model.py": "def keep():\n return 0\n\n\ndef resolve(m):\n return m\n",
"util.py": "import os\n",
"caller.py": ("from model import resolve\n\n\ndef run(m):\n return resolve(m)\n"),
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "def keep():\n return 0\n",
"util.py": "import os\n\n\ndef resolve(m):\n return m\n",
"caller.py": ("from util import resolve\n\n\ndef run(m):\n return resolve(m)\n"),
},
)
_commit(repo, "move resolve to util")
@@ -0,0 +1,315 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from generator_testlib import _commit, _git, _write # noqa: F401
from mechanical_refactor_proof_generator import build_repro, infer_recipe, recipe_to_script
def test_infer_extract_function_with_returned_local(repo: Path) -> None:
"""A block ending in ``pool = make(...)`` carved into a helper that returns ``pool`` infers
an extract_function whose body is the verbatim block and whose return_text is authored.
"""
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" base = self.setup()\n"
" if self.flag:\n"
" x = self.a\n"
" y = x + n\n"
" pool = make(\n"
" a=x,\n"
" b=y,\n"
" )\n"
" return pool\n"
"\n"
" def keep(self):\n"
" return 0\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" base = self.setup()\n"
" if self.flag:\n"
" pool = self._build_pool(n=n)\n"
" return pool\n"
"\n"
" def _build_pool(self, *, n):\n"
" x = self.a\n"
" y = x + n\n"
" pool = make(\n"
" a=x,\n"
" b=y,\n"
" )\n"
" return pool\n"
"\n"
" def keep(self):\n"
" return 0\n"
)
},
)
_commit(repo, "extract _build_pool from dispatch")
recipe = infer_recipe("HEAD", str(repo))
assert recipe.supported
assert recipe.moves == []
assert len(recipe.extract_functions) == 1
ex = recipe.extract_functions[0]
assert ex["name"] == "_build_pool"
assert ex["src"] == "kv.py" and ex["dst"] == "kv.py"
assert ex["into_class"] == "C"
assert ex["before"] == "keep"
assert ex["body_indent"] == 12
assert ex["body"] == (
" x = self.a\n"
" y = x + n\n"
" pool = make(\n"
" a=x,\n"
" b=y,\n"
" )\n"
)
assert ex["call"] == " pool = self._build_pool(n=n)\n"
assert ex["return_text"] == " return pool"
assert ex["signature"] == " def _build_pool(self, *, n):\n"
def test_infer_extract_function_keeps_leading_comment_in_body(repo: Path) -> None:
"""A block whose first line is a comment extracts with that comment in the body, not
absorbed into the authored signature (which is the def header through its colon only).
"""
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" if self.flag:\n"
" # pick the pool class for this backend\n"
" cls = PoolA\n"
" pool = cls(n)\n"
" return pool\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" if self.flag:\n"
" pool = self._build_pool(n=n)\n"
" return pool\n"
"\n"
" def _build_pool(self, *, n):\n"
" # pick the pool class for this backend\n"
" cls = PoolA\n"
" pool = cls(n)\n"
" return pool\n"
)
},
)
commit = _commit(repo, "extract _build_pool with a leading comment")
recipe = infer_recipe(commit, str(repo))
assert len(recipe.extract_functions) == 1
ex = recipe.extract_functions[0]
assert ex["signature"] == " def _build_pool(self, *, n):\n"
assert ex["body"].lstrip().startswith("# pick the pool class")
assert build_repro(recipe, repo_root=str(repo)).run() == ""
def test_infer_extract_function_no_return_text_when_body_is_whole_helper(
repo: Path,
) -> None:
"""When the helper body reproduces the source block with no trailing return, return_text
is None (the block is a side-effecting statement sequence, not a value producer)."""
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def run(self):\n"
" self.pre()\n"
" self.log(1)\n"
" self.log(2)\n"
" self.post()\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def run(self):\n"
" self.pre()\n"
" self._emit()\n"
" self.post()\n"
"\n"
" def _emit(self):\n"
" self.log(1)\n"
" self.log(2)\n"
)
},
)
_commit(repo, "extract _emit from run")
recipe = infer_recipe("HEAD", str(repo))
assert recipe.supported
assert len(recipe.extract_functions) == 1
ex = recipe.extract_functions[0]
assert ex["name"] == "_emit"
assert ex["return_text"] is None
assert ex["call"] == " self._emit()\n"
def test_infer_extract_function_edited_body_does_not_pass(repo: Path) -> None:
"""A helper whose body was edited (not a verbatim cut) never yields a false pass: the
reproduction's byte-diff surfaces the bundled change as a non-empty residual."""
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def run(self):\n"
" self.pre()\n"
" self.log(1)\n"
" self.post()\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def run(self):\n"
" self.pre()\n"
" self._emit()\n"
" self.post()\n"
"\n"
" def _emit(self):\n"
" self.log(2)\n"
)
},
)
commit = _commit(repo, "extract _emit but change the arg")
recipe = infer_recipe(commit, str(repo))
residual = build_repro(recipe, repo_root=str(repo)).run()
assert residual != ""
def test_infer_extract_function_when_block_and_call_share_closing_paren(
repo: Path,
) -> None:
"""The removed block and its replacement call both end in a lone ``)``; the prefix/suffix
split must not absorb that shared line, or the extracted body loses its final line.
"""
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" if self.flag:\n"
" pool = make_pool(\n"
" a=n,\n"
" b=self.b,\n"
" )\n"
" return pool\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" if self.flag:\n"
" pool = self._build_pool(\n"
" n=n,\n"
" )\n"
" return pool\n"
"\n"
" def _build_pool(self, *, n):\n"
" pool = make_pool(\n"
" a=n,\n"
" b=self.b,\n"
" )\n"
" return pool\n"
)
},
)
commit = _commit(repo, "extract _build_pool")
recipe = infer_recipe(commit, str(repo))
assert len(recipe.extract_functions) == 1
ex = recipe.extract_functions[0]
assert ex["body"].rstrip().endswith(")")
assert ex["return_text"] == " return pool"
assert build_repro(recipe, repo_root=str(repo)).run() == ""
def test_emitted_script_passes_on_extract_function(repo: Path, tmp_path: Path) -> None:
"""The recipe for an extract_function reproduces the commit byte-for-byte (bare repo, no
formatter) so build_repro returns an empty residual."""
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" base = self.setup()\n"
" if self.flag:\n"
" x = self.a\n"
" pool = make(\n"
" a=x,\n"
" )\n"
" return pool\n"
"\n"
" def keep(self):\n"
" return 0\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" base = self.setup()\n"
" if self.flag:\n"
" pool = self._build_pool(n=n)\n"
" return pool\n"
"\n"
" def _build_pool(self, *, n):\n"
" x = self.a\n"
" pool = make(\n"
" a=x,\n"
" )\n"
" return pool\n"
"\n"
" def keep(self):\n"
" return 0\n"
)
},
)
commit = _commit(repo, "extract _build_pool from dispatch")
recipe = infer_recipe(commit, str(repo))
residual = build_repro(recipe, repo_root=str(repo)).run()
assert residual == "", residual
assert "extract_function" in recipe_to_script(recipe, "extract")
@@ -0,0 +1,216 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from generator_testlib import ( # noqa: F401
_commit,
_free_function_move_with_module_level_caller,
_git,
_method_onto_class,
_write,
)
from mechanical_refactor_proof_generator import infer_recipe, recipe_to_script
def test_infer_recipe_new_file_extract_from_class_method_unsupported(
repo: Path,
) -> None:
"""A method still inside the class cut straight into a new module cannot be cut as a
top-level symbol, so the extract is reported unsupported (prep must lift it out first).
"""
_write(
repo,
**{
"model.py": (
"class M:\n"
" @staticmethod\n"
" def foo(self):\n"
" return 1\n"
"\n"
" def other(self):\n"
" return 0\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "class M:\n def other(self):\n return 0\n",
"newmod.py": "def foo():\n return 1\n",
},
)
_commit(repo, "extract foo to a new module")
recipe = infer_recipe("HEAD", str(repo))
assert recipe.supported is False
assert any("not all top-level" in note for note in recipe.notes)
def test_infer_recipe_new_file_extract_from_staged_tail(repo: Path) -> None:
"""A staged trailing block (scaffolding + def at the source tail) cut into a new file
infers an extract_to_new_module, prepending the future import."""
_write(
repo,
**{
"model.py": (
"class M:\n"
" def keep(self):\n"
" return 1\n"
"\n"
"\n"
"import logging\n"
"\n"
"logger = logging.getLogger(__name__)\n"
"\n"
"\n"
"def foo(x):\n"
" return x + 1\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "class M:\n def keep(self):\n return 1\n",
"newmod.py": (
"from __future__ import annotations\n"
"\n"
"import logging\n"
"\n"
"logger = logging.getLogger(__name__)\n"
"\n"
"\n"
"def foo(x):\n"
" return x + 1\n"
),
},
)
_commit(repo, "extract foo to a new module")
recipe = infer_recipe("HEAD", str(repo))
assert recipe.supported
assert recipe.moves == []
assert recipe.extracts == [
{
"src": "model.py",
"dst": "newmod.py",
"symbols": ["foo"],
"future_import": True,
}
]
def test_infer_recipe_scattered_new_module_extract(repo: Path) -> None:
"""Scattered top-level defs cut into a new module (no staged trailing block) infer a scatter
extract with the authored header and target order, not UNSUPPORTED."""
_write(
repo,
**{
"common.py": (
"import os\n"
"\n"
"\n"
"def keep():\n"
" return 0\n"
"\n"
"\n"
"def beta():\n"
" return 2\n"
"\n"
"\n"
"def stay():\n"
" return 9\n"
"\n"
"\n"
"def alpha():\n"
" return 1\n"
),
},
)
_commit(repo, "base")
_write(
repo,
**{
"common.py": (
"import os\n" "\n" "\n" "def keep():\n" " return 0\n" "\n" "\n" "def stay():\n" " return 9\n"
),
"alloc.py": (
"from __future__ import annotations\n"
"\n"
"import logging\n"
"\n"
"logger = logging.getLogger(__name__)\n"
"\n"
"\n"
"def alpha():\n"
" return 1\n"
"\n"
"\n"
"def beta():\n"
" return 2\n"
),
},
)
_commit(repo, "extract alpha, beta to alloc.py")
recipe = infer_recipe("HEAD", str(repo))
assert recipe.supported
assert recipe.extracts == []
assert recipe.moves == []
assert len(recipe.scatter_extracts) == 1
sx = recipe.scatter_extracts[0]
assert sx["src"] == "common.py" and sx["dst"] == "alloc.py"
assert sorted(sx["symbols"]) == ["alpha", "beta"]
assert sx["order"] == ["alpha", "beta"]
assert sx["header"].startswith("from __future__ import annotations\n")
assert "logger = logging.getLogger(__name__)" in sx["header"]
assert sx["drop_assigns"] == []
script = recipe_to_script(recipe, "extract alpha, beta to alloc.py")
assert "extract_symbols_to_new_module" in script
def test_infer_recipe_scatter_extract_drops_relocated_constant(repo: Path) -> None:
"""A module-level constant relocated into the new module is inferred as a drop_assign so the
scatter extract removes it from the source too; a constant the source keeps is not.
"""
_write(
repo,
**{
"common.py": (
"from u import is_hip\n"
"\n"
"_IS_HIP = is_hip()\n"
"logger = 1\n"
"\n"
"\n"
"def moved():\n"
" return _IS_HIP\n"
"\n"
"\n"
"def keep():\n"
" return logger\n"
),
},
)
_commit(repo, "base")
_write(
repo,
**{
"common.py": ("logger = 1\n\n\ndef keep():\n return logger\n"),
"alloc.py": (
"from __future__ import annotations\n"
"\n"
"from u import is_hip\n"
"\n"
"_IS_HIP = is_hip()\n"
"\n"
"\n"
"def moved():\n"
" return _IS_HIP\n"
),
},
)
_commit(repo, "extract moved to alloc.py")
recipe = infer_recipe("HEAD", str(repo))
assert len(recipe.scatter_extracts) == 1
assert recipe.scatter_extracts[0]["drop_assigns"] == ["_IS_HIP"]
@@ -0,0 +1,154 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from generator_testlib import ( # noqa: F401
_commit,
_free_function_move_with_module_level_caller,
_git,
_method_onto_class,
_write,
)
from mechanical_refactor_proof_generator import infer_recipe
def test_infer_recipe_infers_added_module_imports(repo: Path) -> None:
"""An import the destination module gains (the moved code needs it) is inferred."""
_write(
repo,
**{
"model.py": (
"import gc\n"
"\n"
"class M:\n"
" @staticmethod\n"
" def foo(self):\n"
" gc.collect()\n"
" return 1\n"
"\n"
" def other(self):\n"
" return 0\n"
),
"comp.py": "class C:\n def keep(self):\n return 1\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "class M:\n def other(self):\n return 0\n",
"comp.py": (
"import gc\n"
"\n"
"class C:\n"
" def keep(self):\n"
" return 1\n"
"\n"
" def foo(self):\n"
" gc.collect()\n"
" return 1\n"
),
},
)
_commit(repo, "move foo onto C")
recipe = infer_recipe("HEAD", str(repo))
assert {"path": "comp.py", "text": "import gc"} in recipe.import_additions
def test_infer_recipe_module_level_import_repoint_realised_by_diff(repo: Path) -> None:
"""A module-level consumer whose import is repointed old -> new yields a remove of the old
name and an add of the new -- not a reliance on the formatter pruning a duplicate.
"""
_free_function_move_with_module_level_caller(repo)
recipe = infer_recipe("HEAD", str(repo))
assert recipe.repaths == []
assert {
"path": "caller.py",
"module": "model",
"name": "resolve",
"asname": None,
} in recipe.module_import_removals
assert {"path": "caller.py", "text": "from util import resolve"} in (recipe.import_additions)
def test_infer_recipe_removes_an_import_the_source_no_longer_uses(repo: Path) -> None:
"""When the moved body took the source's only use of an import, the source's lost name is
realised as a removal (deterministic, not left to the formatter)."""
_write(
repo,
**{
"model.py": (
"import gc\n"
"\n"
"class M:\n"
" @staticmethod\n"
" def foo(self):\n"
" gc.collect()\n"
" return 1\n"
"\n"
" def other(self):\n"
" return 0\n"
),
"comp.py": "class C:\n def keep(self):\n return 1\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "class M:\n def other(self):\n return 0\n",
"comp.py": (
"import gc\n"
"\n"
"class C:\n"
" def keep(self):\n"
" return 1\n"
"\n"
" def foo(self):\n"
" gc.collect()\n"
" return 1\n"
),
},
)
_commit(repo, "move foo onto C")
recipe = infer_recipe("HEAD", str(repo))
assert {
"path": "model.py",
"module": None,
"name": "gc",
"asname": None,
} in recipe.module_import_removals
def test_infer_recipe_adds_wholly_new_module_import_verbatim(repo: Path) -> None:
"""An import gained from a module not present in base is captured as the target's verbatim
statement (so an exploded/magic-comma wrapping is reproduced, not collapsed per-name).
"""
_write(
repo,
**{
"model.py": "def keep():\n return 0\n\n\ndef solve(x):\n return x\n",
"util.py": "import os\n",
"caller.py": ("from model import solve\n\n\ndef run():\n return solve(1)\n"),
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "def keep():\n return 0\n",
"util.py": "import os\n\n\ndef solve(x):\n return x\n",
"caller.py": ("from util import (\n solve,\n)\n\n\ndef run():\n return solve(1)\n"),
},
)
_commit(repo, "move solve to util")
recipe = infer_recipe("HEAD", str(repo))
caller_adds = [a["text"] for a in recipe.import_additions if a["path"] == "caller.py"]
assert "from util import (\n solve,\n)" in caller_adds
assert {
"path": "caller.py",
"module": "model",
"name": "solve",
"asname": None,
} in recipe.module_import_removals
@@ -0,0 +1,525 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from generator_testlib import ( # noqa: F401
_commit,
_free_function_move_with_module_level_caller,
_git,
_method_onto_class,
_write,
)
from mechanical_refactor_proof_generator import infer_recipe, recipe_to_script
def test_infer_recipe_method_onto_class(repo: Path) -> None:
"""A method move onto a class infers the move, the call-site lowering, and the orphaned
local import removal."""
_method_onto_class(repo)
recipe = infer_recipe("HEAD", str(repo))
assert recipe.supported
assert [(m["name"], m["src"], m["dst"], m["into_class"], m["dedent"]) for m in recipe.moves] == [
("foo", "model.py", "comp.py", "C", 0)
]
assert recipe.lowerings == [{"name": "foo", "owner": "M", "path": "caller.py", "kind": "lower"}]
assert recipe.import_removals == [{"path": "caller.py", "text": "from model import M", "in_function": "run"}]
assert recipe.import_additions == []
def test_infer_recipe_move_before_typechecking_uses_after_anchor(repo: Path) -> None:
"""A module-level def relocated to land just above an ``if TYPE_CHECKING:`` guard cannot
be anchored with before= (the next def sits past the guard), so the recipe anchors it with
after=<the preceding assignment>."""
_write(
repo,
**{
"model.py": ("def keep():\n return 0\n\n\ndef helper(x):\n return x + 1\n"),
"util.py": (
"from u import is_hip\n"
"\n"
"_is_hip = is_hip()\n"
"\n"
"if TYPE_CHECKING:\n"
" from m import Thing\n"
),
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "def keep():\n return 0\n",
"util.py": (
"from u import is_hip\n"
"\n"
"_is_hip = is_hip()\n"
"\n"
"\n"
"def helper(x):\n"
" return x + 1\n"
"\n"
"\n"
"if TYPE_CHECKING:\n"
" from m import Thing\n"
),
},
)
_commit(repo, "move helper above the TYPE_CHECKING guard")
recipe = infer_recipe("HEAD", str(repo))
assert recipe.supported
assert len(recipe.moves) == 1
move = recipe.moves[0]
assert move["name"] == "helper" and move["dst"] == "util.py"
assert move["before"] is None
assert move["after"] == "_is_hip"
def test_infer_recipe_free_function_move_uses_requalify(repo: Path) -> None:
"""A move to a module-level free function dedents and requalifies the call site
(drops the qualifier), rather than lowering a receiver."""
_write(
repo,
**{
"model.py": (
"class M:\n"
" @staticmethod\n"
" def foo(x):\n"
" return x + 1\n"
"\n"
" def other(self):\n"
" return 0\n"
),
"util.py": "import os\n",
"caller.py": (
"class K:\n" " def run(self):\n" " from model import M\n" "\n" " return M.foo(9)\n"
),
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "class M:\n def other(self):\n return 0\n",
"util.py": "import os\n\n\ndef foo(x):\n return x + 1\n",
"caller.py": ("class K:\n def run(self):\n return foo(9)\n"),
},
)
_commit(repo, "move foo to util as a free function")
recipe = infer_recipe("HEAD", str(repo))
assert [(m["name"], m["into_class"], m["dedent"]) for m in recipe.moves] == [("foo", None, 4)]
assert recipe.lowerings == [{"name": "foo", "owner": "M", "path": "caller.py", "kind": "requalify"}]
def test_infer_recipe_excludes_the_moved_bodys_own_call(repo: Path) -> None:
"""A same-named call on a different receiver inside the moved body is not a caller
lowering (only `M.foo(...)` is, not `worker.foo(...)`)."""
_write(
repo,
**{
"model.py": (
"class M:\n"
" @staticmethod\n"
" def foo(self, x):\n"
" worker.foo(x)\n"
" return x\n"
"\n"
" def other(self):\n"
" return 0\n"
),
"comp.py": "class C:\n def keep(self):\n return 1\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "class M:\n def other(self):\n return 0\n",
"comp.py": (
"class C:\n"
" def keep(self):\n"
" return 1\n"
"\n"
" def foo(self, x):\n"
" worker.foo(x)\n"
" return x\n"
),
},
)
_commit(repo, "move foo onto C")
recipe = infer_recipe("HEAD", str(repo))
assert recipe.lowerings == []
def test_infer_recipe_skips_nested_functions(repo: Path) -> None:
"""A def nested inside a moved method is not inferred as its own move."""
_write(
repo,
**{
"model.py": (
"class M:\n"
" def wrap(self):\n"
" def inner(z):\n"
" return z\n"
" return inner\n"
"\n"
" def other(self):\n"
" return 0\n"
),
"comp.py": "class C:\n def keep(self):\n return 1\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "class M:\n def other(self):\n return 0\n",
"comp.py": (
"class C:\n"
" def keep(self):\n"
" return 1\n"
"\n"
" def wrap(self):\n"
" def inner(z):\n"
" return z\n"
" return inner\n"
),
},
)
_commit(repo, "move wrap onto C")
recipe = infer_recipe("HEAD", str(repo))
names = [m["name"] for m in recipe.moves]
assert names == ["wrap"]
assert any("inner" in n for n in recipe.notes)
def test_infer_recipe_free_function_source_move_repaths_caller(repo: Path) -> None:
"""A free function moved to an existing module becomes a move_symbol with the call left
bare; a caller's function-scoped import is repathed."""
_write(
repo,
**{
"model.py": "def keep():\n return 0\n\n\ndef resolve(m):\n return m\n",
"util.py": "import os\n",
"caller.py": (
"class K:\n"
" def run(self):\n"
" from model import resolve\n"
"\n"
" return resolve(self.m)\n"
),
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "def keep():\n return 0\n",
"util.py": "import os\n\n\ndef resolve(m):\n return m\n",
"caller.py": (
"class K:\n"
" def run(self):\n"
" from util import resolve\n"
"\n"
" return resolve(self.m)\n"
),
},
)
_commit(repo, "move resolve to util")
recipe = infer_recipe("HEAD", str(repo))
assert recipe.supported
assert [(m["name"], m["src"], m["dst"], m["into_class"]) for m in recipe.moves] == [
("resolve", "model.py", "util.py", None)
]
assert recipe.lowerings == []
assert recipe.repaths == [
{
"path": "caller.py",
"old_module": "model",
"new_module": "util",
"name": "resolve",
}
]
def test_infer_recipe_survives_a_non_python_file_in_the_commit(repo: Path) -> None:
"""A commit also touching a .md file infers the move and notes the non-Python path."""
_write(
repo,
**{
"model.py": "def foo():\n return 1\n\n\ndef keep():\n return 0\n",
"util.py": "x = 1\n",
"README.md": "hello\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "def keep():\n return 0\n",
"util.py": "x = 1\n\n\ndef foo():\n return 1\n",
"README.md": "hello world, this is plain markdown text\n",
},
)
commit = _commit(repo, "move foo and touch docs")
recipe = infer_recipe(commit, str(repo))
assert [mv["name"] for mv in recipe.moves] == ["foo"]
assert any("README.md" in note for note in recipe.notes)
def test_infer_recipe_records_the_source_class_for_disambiguation(repo: Path) -> None:
"""A method move carries from_class so the cut cannot hit a same-named other method."""
_write(
repo,
**{
"model.py": (
"class M:\n"
" def foo(self, x):\n"
" return x + 1\n"
"\n"
"\n"
"class Other:\n"
" def foo(self, x):\n"
" return x + 2\n"
),
"comp.py": "class C:\n def keep(self):\n return 1\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": (
"class M:\n" " pass\n" "\n" "\n" "class Other:\n" " def foo(self, x):\n" " return x + 2\n"
),
"comp.py": (
"class C:\n"
" def keep(self):\n"
" return 1\n"
"\n"
" def foo(self, x):\n"
" return x + 1\n"
),
},
)
commit = _commit(repo, "move M.foo onto C")
recipe = infer_recipe(commit, str(repo))
assert [mv["from_class"] for mv in recipe.moves] == ["M"]
script = recipe_to_script(recipe, "move M.foo onto C")
assert "from_class='M'" in script
def test_infer_recipe_module_level_def_shadowed_by_method_name(repo: Path) -> None:
"""A column-0 cut resolves to the module-level def even when a method shares its name."""
_write(
repo,
**{
"model.py": (
"def foo(*, x):\n"
" return x + 1\n"
"\n"
"\n"
"class M:\n"
" def foo(self):\n"
" return foo(x=self.x)\n"
),
"util.py": "def keep():\n return 1\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": (
"from util import foo\n" "\n" "\n" "class M:\n" " def foo(self):\n" " return foo(x=self.x)\n"
),
"util.py": ("def keep():\n" " return 1\n" "\n" "\n" "def foo(*, x):\n" " return x + 1\n"),
},
)
commit = _commit(repo, "move module-level foo to util")
recipe = infer_recipe(commit, str(repo))
assert recipe.supported
assert [mv["name"] for mv in recipe.moves] == ["foo"]
assert recipe.moves[0]["from_class"] is None
assert recipe.moves[0]["into_class"] is None
def test_infer_recipe_class_move_between_existing_files(repo: Path) -> None:
"""A top-level class relocated to an existing module moves whole; its methods do not."""
_write(
repo,
**{
"model.py": (
"class Payload:\n"
" def get(self):\n"
" return 1\n"
"\n"
"\n"
"def stay():\n"
" return 2\n"
),
"comp.py": "def keep():\n return 3\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "def stay():\n return 2\n",
"comp.py": (
"def keep():\n"
" return 3\n"
"\n"
"\n"
"class Payload:\n"
" def get(self):\n"
" return 1\n"
),
},
)
commit = _commit(repo, "move Payload to comp")
recipe = infer_recipe(commit, str(repo))
assert recipe.supported
assert [mv["name"] for mv in recipe.moves] == ["Payload"]
assert recipe.moves[0]["from_class"] is None
assert recipe.moves[0]["into_class"] is None
def test_infer_recipe_move_leaving_a_forwarding_delegate(repo: Path) -> None:
"""A same-named stub re-added to the source infers leave_delegate on the move."""
_write(
repo,
**{
"model.py": ("class M:\n" " def work(self, x):\n" " return x + 1\n"),
"comp.py": "class C:\n def keep(self):\n return 1\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": ("class M:\n" " def work(self, x):\n" " return self.comp.work(x)\n"),
"comp.py": (
"class C:\n"
" def keep(self):\n"
" return 1\n"
"\n"
" def work(self, x):\n"
" return x + 1\n"
),
},
)
commit = _commit(repo, "move M.work onto C, leaving a delegate")
recipe = infer_recipe(commit, str(repo))
assert recipe.supported
assert [mv["name"] for mv in recipe.moves] == ["work"]
assert recipe.moves[0]["dst"] == "comp.py"
assert recipe.moves[0]["leave_delegate"] == "comp"
assert recipe.moves[0]["delegate_name"] is None
script = recipe_to_script(recipe, "move with delegate")
assert "leave_delegate='comp'" in script
def test_infer_recipe_constant_relocated_with_the_move(repo: Path) -> None:
"""A module constant that vanished from the source and appeared in the existing
destination becomes a move_assign."""
_write(
repo,
**{
"model.py": (
"RATIO = 3\n"
"\n"
"\n"
"def work(x):\n"
" return x * RATIO\n"
"\n"
"\n"
"def stay():\n"
" return 1\n"
),
"comp.py": "import os\n\n\ndef keep():\n return 2\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "def stay():\n return 1\n",
"comp.py": (
"import os\n"
"\n"
"RATIO = 3\n"
"\n"
"\n"
"def keep():\n"
" return 2\n"
"\n"
"\n"
"def work(x):\n"
" return x * RATIO\n"
),
},
)
commit = _commit(repo, "move work + RATIO to comp")
recipe = infer_recipe(commit, str(repo))
assert recipe.supported
assert [am["name"] for am in recipe.assign_moves] == ["RATIO"]
script = recipe_to_script(recipe, "move with constant")
assert "move_assign" in script
def test_infer_recipe_in_file_method_reorder(repo: Path) -> None:
"""A method cut and re-inserted elsewhere in the same class (no other file gains it) infers
an in-file move_symbol (src == dst) anchored above its new next sibling."""
_write(
repo,
**{
"m.py": (
"class C:\n"
" def a(self):\n"
" return 1\n"
"\n"
" def b(self):\n"
" return 2\n"
"\n"
" def c(self):\n"
" return 3\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"m.py": (
"class C:\n"
" def c(self):\n"
" return 3\n"
"\n"
" def a(self):\n"
" return 1\n"
"\n"
" def b(self):\n"
" return 2\n"
)
},
)
commit = _commit(repo, "move c above a")
recipe = infer_recipe(commit, str(repo))
assert recipe.supported
assert len(recipe.moves) == 1
mv = recipe.moves[0]
assert mv["name"] == "c" and mv["src"] == "m.py" and mv["dst"] == "m.py"
assert mv["into_class"] == "C" and mv["before"] == "a"
@@ -0,0 +1,69 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from generator_testlib import _commit, _write # noqa: F401
from mechanical_refactor_proof_generator import _main
def _extract_function_commit(repo: Path) -> str:
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" x = self.a\n"
" y = x + n\n"
" return y\n"
"\n"
" def keep(self):\n"
" return 0\n"
)
},
)
_commit(repo, "base")
_write(
repo,
**{
"kv.py": (
"class C:\n"
" def dispatch(self, n):\n"
" y = self._combine(n=n)\n"
" return y\n"
"\n"
" def _combine(self, *, n):\n"
" x = self.a\n"
" y = x + n\n"
" return y\n"
"\n"
" def keep(self):\n"
" return 0\n"
)
},
)
return _commit(repo, "extract _combine from dispatch")
def test_single_commit_extract_function_reproduces_instead_of_unsupported(
repo: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A pure intra-file extract_function commit run in single-commit mode reproduces (exit 0),
not UNSUPPORTED -- the relocates check must count extract_functions like the range path.
"""
sha = _extract_function_commit(repo)
monkeypatch.chdir(repo)
assert _main([sha]) == 0
def test_single_commit_pure_rename_is_unsupported(repo: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""A commit that relocates no definition (a bare rename) stays UNSUPPORTED with exit 1."""
_write(repo, **{"m.py": "def foo():\n return 1\n"})
_commit(repo, "base")
_write(repo, **{"m.py": "def bar():\n return 1\n"})
sha = _commit(repo, "rename foo to bar")
monkeypatch.chdir(repo)
assert _main([sha]) == 1
@@ -0,0 +1,164 @@
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from generator_testlib import ( # noqa: F401
_commit,
_free_function_move_with_module_level_caller,
_git,
_method_onto_class,
_write,
)
from mechanical_refactor_proof_generator import infer_recipe, recipe_to_script
def test_recipe_to_script_is_self_contained_and_ordered(repo: Path) -> None:
"""The emitted script imports only the reproduce util and lowers before moving."""
_method_onto_class(repo)
script = recipe_to_script(infer_recipe("HEAD", str(repo)), "move foo onto C")
assert "from mechanical_refactor_reproduction_utils import Repro" in script
assert script.index("lower_call_sites") < script.index("move_symbol")
assert "residual = r.run()" in script
assert "sys.exit(1 if residual else 0)" in script
# importing nothing else from the skill keeps the script auditable in isolation
assert "mechanical_refactor_verify_utils" not in script
assert "mechanical_refactor_proof_generator" not in script
def test_recipe_to_script_orders_import_ops_after_moves(repo: Path) -> None:
"""The emitted script applies module-level import add/remove AFTER the move, matching
build_repro's run order so the script and the in-process verdict cannot diverge."""
_free_function_move_with_module_level_caller(repo)
script = recipe_to_script(infer_recipe("HEAD", str(repo)), "move resolve to util")
assert script.index("move_symbol") < script.index("remove_imported_name")
assert script.index("move_symbol") < script.index("add_import")
def _emit_runnable_script(repo: Path, out: Path, commit: str, subject: str) -> Path:
"""Write the emitted script plus its util dependency into a proof-folder layout."""
scripts_dir = out / "repro_scripts"
scripts_dir.mkdir(parents=True, exist_ok=True)
utils_src = Path(__file__).resolve().parents[2] / ("mechanical_refactor_reproduction_utils.py")
(out / "mechanical_refactor_reproduction_utils.py").write_text(utils_src.read_text())
script = recipe_to_script(infer_recipe(commit, str(repo)), subject)
script_path = scripts_dir / f"{commit[:9]}.py"
script_path.write_text(script)
return script_path
def test_emitted_script_exits_zero_on_faithful_commit(repo: Path, tmp_path: Path) -> None:
"""Running the emitted script on a clean move exits 0 and prints the PASS verdict."""
_write(
repo,
**{
"model.py": "def keep():\n return 0\n\n\ndef resolve(m):\n return m\n",
"util.py": "import os\n",
"caller.py": ("from model import resolve\n\n\ndef run(m):\n return resolve(m)\n"),
},
)
_commit(repo, "base")
# The after-state is the primitives' exact output (this bare repo has no formatter
# to absorb the cut's leftover blank lines, unlike a pre-commit-clean real repo).
_write(
repo,
**{
"model.py": "def keep():\n return 0\n\n\n",
"util.py": "import os\n\ndef resolve(m):\n return m\n",
"caller.py": ("from util import resolve\n\n\ndef run(m):\n return resolve(m)\n"),
},
)
commit = _commit(repo, "move resolve to util")
script_path = _emit_runnable_script(repo, tmp_path / "out", commit, "move")
result = subprocess.run([sys.executable, str(script_path)], cwd=repo, capture_output=True, text=True)
assert result.returncode == 0, result.stdout + result.stderr
assert "PASS" in result.stdout
def test_emitted_script_exits_nonzero_on_bundled_change(repo: Path, tmp_path: Path) -> None:
"""A commit bundling a non-move change makes the emitted script exit non-zero."""
_write(
repo,
**{
"model.py": "def keep():\n return 0\n\n\ndef resolve(m):\n return m\n",
"util.py": "import os\n",
},
)
_commit(repo, "base")
_write(
repo,
**{
"model.py": "def keep():\n return 99\n",
"util.py": "import os\n\n\ndef resolve(m):\n return m\n",
},
)
commit = _commit(repo, "move resolve AND change keep")
script_path = _emit_runnable_script(repo, tmp_path / "out", commit, "dirty move")
result = subprocess.run([sys.executable, str(script_path)], cwd=repo, capture_output=True, text=True)
assert result.returncode == 1, result.stdout + result.stderr
assert "RESIDUAL" in result.stdout
def test_emitted_script_passes_on_move_above_typechecking_guard(repo: Path, tmp_path: Path) -> None:
"""A module-level def relocated to just above an ``if TYPE_CHECKING:`` guard reproduces
via an inferred after= anchor and the emitted script exits 0."""
_write(
repo,
**{
"model.py": ("def keep():\n return 0\n\n\ndef helper(x):\n return x + 1\n"),
"util.py": (
"from u import is_hip\n"
"\n"
"_is_hip = is_hip()\n"
"\n"
"if TYPE_CHECKING:\n"
" from m import Thing\n"
),
},
)
_commit(repo, "base")
# After-state = the primitive's exact output (bare repo, no formatter to absorb blanks).
_write(
repo,
**{
"model.py": "def keep():\n return 0\n\n\n",
"util.py": (
"from u import is_hip\n"
"\n"
"_is_hip = is_hip()\n"
"\n"
"def helper(x):\n"
" return x + 1\n"
"\n"
"if TYPE_CHECKING:\n"
" from m import Thing\n"
),
},
)
commit = _commit(repo, "move helper above the TYPE_CHECKING guard")
script_path = _emit_runnable_script(repo, tmp_path / "out", commit, "after-anchor move")
result = subprocess.run([sys.executable, str(script_path)], cwd=repo, capture_output=True, text=True)
assert result.returncode == 0, result.stdout + result.stderr
assert "PASS" in result.stdout
assert "after='_is_hip'" in script_path.read_text()
def test_per_file_diff_keeps_content_lines_starting_with_plus_signs(repo: Path) -> None:
"""An added content line beginning with '++' is collected, not mistaken for a header."""
from mechanical_refactor_proof_generator import _per_file_diff
_write(repo, **{"notes.py": "a = 1\n"})
_commit(repo, "base")
_write(repo, **{"notes.py": 'a = 1\nb = "++x"\n'})
commit = _commit(repo, "add plus-plus line")
files = _per_file_diff(commit, str(repo))
assert files["notes.py"]["added"] == ['b = "++x"']
@@ -0,0 +1,54 @@
import subprocess
from pathlib import Path
_PASSING_PROOF = "import sys\n" 'print("PASS: reproduces the commit byte-for-byte.")\n' "sys.exit(0)\n"
_FAILING_PROOF = "import sys\n" 'print("RESIDUAL (2 lines):\\n+x\\n-y")\n' "sys.exit(1)\n"
def _git(repo: Path, *args: str) -> str:
return subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True, text=True).stdout.strip()
def _write(repo: Path, **files: "str | None") -> None:
for name, content in files.items():
path = repo / name.replace("__", "/")
if content is None:
path.unlink()
else:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
def _commit(repo: Path, message: str) -> str:
_git(repo, "add", "-A")
_git(repo, "commit", "-q", "-m", message)
return _git(repo, "rev-parse", "HEAD")
def _chain(repo: Path, messages: "list[str]") -> "tuple[str, list[str]]":
"""A base commit plus one single-file commit per message, on a `chain` branch.
Returns (base_sha, commit_shas)."""
_write(repo, **{"seed.py": "SEED = 0\n"})
base = _commit(repo, "base")
_git(repo, "switch", "-q", "-c", "chain")
shas: list[str] = []
for i, message in enumerate(messages):
_write(repo, **{f"file_{i}.py": f"VALUE = {i}\n"})
shas.append(_commit(repo, message))
return base, shas
def _write_stub_proof(
proof_dir: Path,
sha: str,
*,
passing: bool = True,
flat: bool = False,
stem_len: int = 9,
) -> Path:
"""A stand-in proof script printing the arbiter's verdict line and exiting to match."""
directory = proof_dir if flat else proof_dir / "repro_scripts"
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{sha[:stem_len]}.py"
path.write_text(_PASSING_PROOF if passing else _FAILING_PROOF)
return path
@@ -0,0 +1,20 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
sys.path.insert(0, str(Path(__file__).resolve().parent))
from cli_testlib import _git
@pytest.fixture
def repo(tmp_path: Path) -> Path:
root = tmp_path / "repo"
root.mkdir()
_git(root, "init", "-q", "-b", "main")
_git(root, "config", "user.email", "test@example.com")
_git(root, "config", "user.name", "test")
_git(root, "config", "commit.gpgsign", "false")
return root
@@ -0,0 +1,108 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cli_testlib import _chain, _write_stub_proof
from mechanical_refactor_reproduction_cli import (
KIND_MECHANICAL,
KIND_NON_MECHANICAL,
VERDICT_AMBIGUOUS_KIND,
VERDICT_HUMAN_REVIEW,
VERDICT_PASS,
VERDICT_UNCLASSIFIED,
verify_chain,
)
def _single_verdict(repo: Path, tmp_path: Path, message: str, *, with_proof: bool):
proof = tmp_path / "proof"
proof.mkdir(exist_ok=True)
base, shas = _chain(repo, [message])
if with_proof:
_write_stub_proof(proof, shas[0])
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
return result.verdicts[0]
def test_mechanical_provable_word_classifies_the_commit_as_mechanical(repo: Path, tmp_path: Path) -> None:
"""A message carrying mechanical_provable is classified mechanical and needs a proof."""
verdict = _single_verdict(repo, tmp_path, "grp(step,mechanical_provable): move foo", with_proof=True)
assert verdict.kind == KIND_MECHANICAL
assert verdict.verdict == VERDICT_PASS
def test_non_mechanical_provable_word_is_not_double_counted_as_the_bare_word(repo: Path, tmp_path: Path) -> None:
"""non_mechanical_provable classifies as non-mechanical, not as both words at once."""
verdict = _single_verdict(
repo,
tmp_path,
"grp(step,non_mechanical_provable): rework foo",
with_proof=False,
)
assert verdict.kind == KIND_NON_MECHANICAL
assert verdict.verdict == VERDICT_HUMAN_REVIEW
def test_message_without_either_word_is_unclassified_and_fails_the_chain(repo: Path, tmp_path: Path) -> None:
"""A commit missing both words gets UNCLASSIFIED and the chain does not pass."""
proof = tmp_path / "proof"
proof.mkdir()
base, _ = _chain(repo, ["plain subject with no kind word"])
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
assert result.verdicts[0].verdict == VERDICT_UNCLASSIFIED
assert result.verdicts[0].kind is None
assert not result.passed
def test_message_with_both_words_is_ambiguous(repo: Path, tmp_path: Path) -> None:
"""A commit declaring both kinds gets AMBIGUOUS_KIND and fails the chain."""
verdict = _single_verdict(
repo,
tmp_path,
"subject mechanical_provable\n\nbody also says non_mechanical_provable",
with_proof=True,
)
assert verdict.verdict == VERDICT_AMBIGUOUS_KIND
assert verdict.kind is None
def test_kind_word_must_stand_alone_not_as_a_substring(repo: Path, tmp_path: Path) -> None:
"""xmechanical_provable / mechanical_provable_x do not count as the standalone word."""
verdict = _single_verdict(
repo,
tmp_path,
"xmechanical_provable and mechanical_provable_x only",
with_proof=False,
)
assert verdict.verdict == VERDICT_UNCLASSIFIED
def test_kind_word_delimited_by_punctuation_counts(repo: Path, tmp_path: Path) -> None:
"""The word inside punctuation, e.g. (step,mechanical_provable), is a valid match."""
verdict = _single_verdict(repo, tmp_path, "grp(step,mechanical_provable): move", with_proof=True)
assert verdict.kind == KIND_MECHANICAL
def test_repeating_the_same_kind_word_is_accepted(repo: Path, tmp_path: Path) -> None:
"""Multiple occurrences of one kind word still classify unambiguously."""
verdict = _single_verdict(
repo,
tmp_path,
"mechanical_provable move\n\nthis commit is mechanical_provable",
with_proof=True,
)
assert verdict.kind == KIND_MECHANICAL
assert verdict.verdict == VERDICT_PASS
def test_kind_word_in_the_body_counts_when_subject_is_free_form(repo: Path, tmp_path: Path) -> None:
"""Classification scans the whole message, so a body-only word is enough."""
verdict = _single_verdict(
repo,
tmp_path,
"Move resolve to util\n\nKind: non_mechanical_provable",
with_proof=False,
)
assert verdict.kind == KIND_NON_MECHANICAL
@@ -0,0 +1,77 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cli_testlib import _chain, _write_stub_proof
from mechanical_refactor_reproduction_cli import (
VERDICT_AMBIGUOUS_PROOF,
VERDICT_MISSING_PROOF,
VERDICT_PASS,
verify_chain,
)
_MSG = "mechanical_provable: move foo"
def _run_single(repo: Path, proof: Path):
base, shas = _chain(repo, [_MSG])
return shas[0], verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
def test_proof_is_found_under_repro_scripts_by_sha_prefix(repo: Path, tmp_path: Path) -> None:
"""The generator layout repro_scripts/<sha9>.py resolves to the commit's proof."""
proof = tmp_path / "proof"
base, shas = _chain(repo, [_MSG])
_write_stub_proof(proof, shas[0], stem_len=9)
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
assert result.verdicts[0].verdict == VERDICT_PASS
def test_proof_is_found_flat_in_the_proof_folder_by_full_sha(repo: Path, tmp_path: Path) -> None:
"""A flat <proof>/<full-sha>.py layout is also accepted."""
proof = tmp_path / "proof"
base, shas = _chain(repo, [_MSG])
_write_stub_proof(proof, shas[0], flat=True, stem_len=40)
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
assert result.verdicts[0].verdict == VERDICT_PASS
def test_provable_commit_without_a_proof_script_is_missing_proof(repo: Path, tmp_path: Path) -> None:
"""A mechanical_provable commit with no matching script fails as MISSING_PROOF."""
proof = tmp_path / "proof"
proof.mkdir()
sha, result = _run_single(repo, proof)
assert result.verdicts[0].verdict == VERDICT_MISSING_PROOF
assert not result.passed
def test_unrelated_and_non_hex_scripts_do_not_match(repo: Path, tmp_path: Path) -> None:
"""Scripts named for another sha or with a non-hex stem are not this commit's proof."""
proof = tmp_path / "proof"
scripts = proof / "repro_scripts"
scripts.mkdir(parents=True)
(scripts / "0123456789abcdef.py").write_text("raise SystemExit(1)\n")
(scripts / "not_a_sha.py").write_text("raise SystemExit(1)\n")
sha, result = _run_single(repo, proof)
assert result.verdicts[0].verdict == VERDICT_MISSING_PROOF
def test_two_scripts_matching_one_commit_are_ambiguous(repo: Path, tmp_path: Path) -> None:
"""A commit matched by both a nested and a flat script fails as AMBIGUOUS_PROOF."""
proof = tmp_path / "proof"
base, shas = _chain(repo, [_MSG])
_write_stub_proof(proof, shas[0], stem_len=9)
_write_stub_proof(proof, shas[0], flat=True, stem_len=12)
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
assert result.verdicts[0].verdict == VERDICT_AMBIGUOUS_PROOF
assert not result.passed
def test_short_hex_stem_below_minimum_length_is_ignored(repo: Path, tmp_path: Path) -> None:
"""A 6-char hex stem is too short to name a commit and is not treated as a proof."""
proof = tmp_path / "proof"
base, shas = _chain(repo, [_MSG])
_write_stub_proof(proof, shas[0], stem_len=6)
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
assert result.verdicts[0].verdict == VERDICT_MISSING_PROOF
@@ -0,0 +1,87 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cli_testlib import _chain, _write_stub_proof
from mechanical_refactor_reproduction_cli import main, render_report, verify_chain
def _mixed_chain_result(repo: Path, tmp_path: Path):
proof = tmp_path / "proof"
base, shas = _chain(
repo,
[
"mechanical_provable: move foo",
"non_mechanical_provable: rework bar",
"mechanical_provable: move baz",
],
)
_write_stub_proof(proof, shas[0])
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
return proof, base, shas, result
def test_report_has_header_table_row_per_commit_and_chain_verdict(repo: Path, tmp_path: Path) -> None:
"""The report carries base/branch/proof, one table row per commit, and the verdict."""
proof, base, shas, result = _mixed_chain_result(repo, tmp_path)
report = render_report(result)
assert "# Mechanical refactor chain report" in report
assert f"`{base[:12]}`" in report
assert "chain verdict: **FAIL**" in report
assert "3 total — 2 mechanical_provable, 1 non_mechanical_provable" in report
for sha in shas:
assert f"`{sha[:9]}`" in report
assert "| mechanical_provable | PASS |" in report
assert "| non_mechanical_provable | HUMAN_REVIEW |" in report
assert "| mechanical_provable | MISSING_PROOF |" in report
def test_report_lists_failure_details_for_each_non_ok_commit(repo: Path, tmp_path: Path) -> None:
"""Every non-ok commit gets a failure-details section with its explanation."""
proof, base, shas, result = _mixed_chain_result(repo, tmp_path)
report = render_report(result)
assert "## Failure details" in report
assert f"### `{shas[2][:9]}` — MISSING_PROOF" in report
assert "no proof script found" in report
def test_passing_report_has_no_failure_details_section(repo: Path, tmp_path: Path) -> None:
"""A fully verified chain renders a PASS report without a failure section."""
proof = tmp_path / "proof"
base, shas = _chain(repo, ["mechanical_provable: move foo"])
_write_stub_proof(proof, shas[0])
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
report = render_report(result)
assert "chain verdict: **PASS**" in report
assert "proofs: 1/1 PASS" in report
assert "## Failure details" not in report
def test_main_writes_the_report_into_the_proof_folder_by_default(repo: Path, tmp_path: Path, capsys) -> None:
"""main prints the report and writes <proof>/chain_report.md (or --report PATH)."""
proof = tmp_path / "proof"
base, shas = _chain(repo, ["mechanical_provable: move foo"])
_write_stub_proof(proof, shas[0])
args = [
"--base",
base,
"--branch",
"chain",
"--proof",
str(proof),
"--repo-root",
str(repo),
]
assert main(args) == 0
default_report = proof / "chain_report.md"
assert "chain verdict: **PASS**" in default_report.read_text()
assert "chain verdict: **PASS**" in capsys.readouterr().out
custom = tmp_path / "custom_report.md"
assert main([*args, "--report", str(custom)]) == 0
assert "chain verdict: **PASS**" in custom.read_text()
@@ -0,0 +1,157 @@
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cli_testlib import _chain, _write_stub_proof
from mechanical_refactor_reproduction_cli import VERDICT_FAIL, VERDICT_PASS, main, render_report, verify_chain
_MSG = "mechanical_provable: move foo"
def _counting_proof(script: Path, counter: Path, *, passing: bool = True) -> None:
"""Make the stub proof bump a run counter so re-execution is observable."""
verdict = (
'print("PASS: reproduces the commit byte-for-byte.")\nsys.exit(0)\n'
if passing
else 'print("RESIDUAL (1 lines):\\n+x")\nsys.exit(1)\n'
)
script.write_text(
"import sys\n"
f"counter = __import__('pathlib').Path({str(counter)!r})\n"
"runs = int(counter.read_text()) if counter.exists() else 0\n"
"counter.write_text(str(runs + 1))\n" + verdict
)
def _cache_file(repo: Path) -> Path:
return repo / ".git" / "mechanical_refactor_passed_proofs.json"
def test_skip_passed_reuses_an_unchanged_pass_without_rerunning(repo: Path, tmp_path: Path) -> None:
"""A PASS recorded on the first run is reused: the proof does not execute again."""
proof = tmp_path / "proof"
counter = tmp_path / "runs"
base, shas = _chain(repo, [_MSG])
_counting_proof(_write_stub_proof(proof, shas[0]), counter)
args = dict(base=base, branch="chain", proof=proof, repo_root=str(repo))
first = verify_chain(**args)
assert first.verdicts[0].verdict == VERDICT_PASS
assert counter.read_text() == "1"
second = verify_chain(**args, skip_passed=True)
assert second.verdicts[0].verdict == VERDICT_PASS
assert second.verdicts[0].cached
assert counter.read_text() == "1"
assert second.passed
def test_without_the_flag_the_proof_always_reruns(repo: Path, tmp_path: Path) -> None:
"""The cache is recorded on every run but consulted only under skip_passed."""
proof = tmp_path / "proof"
counter = tmp_path / "runs"
base, shas = _chain(repo, [_MSG])
_counting_proof(_write_stub_proof(proof, shas[0]), counter)
args = dict(base=base, branch="chain", proof=proof, repo_root=str(repo))
verify_chain(**args)
result = verify_chain(**args)
assert counter.read_text() == "2"
assert not result.verdicts[0].cached
def test_editing_the_proof_script_invalidates_the_cache(repo: Path, tmp_path: Path) -> None:
"""A changed script hash misses the cache, so the edited (failing) proof reruns."""
proof = tmp_path / "proof"
base, shas = _chain(repo, [_MSG])
script = _write_stub_proof(proof, shas[0])
verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
script.write_text('print("RESIDUAL (1 lines):\\n+x")\nraise SystemExit(1)\n')
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo), skip_passed=True)
assert result.verdicts[0].verdict == VERDICT_FAIL
def test_editing_the_utils_copy_invalidates_the_cache(repo: Path, tmp_path: Path) -> None:
"""The utils module next to the scripts is part of the key: editing it forces a rerun."""
proof = tmp_path / "proof"
counter = tmp_path / "runs"
utils = proof / "mechanical_refactor_reproduction_utils.py"
base, shas = _chain(repo, [_MSG])
_counting_proof(_write_stub_proof(proof, shas[0]), counter)
utils.parent.mkdir(parents=True, exist_ok=True)
utils.write_text("ENGINE = 1\n")
args = dict(base=base, branch="chain", proof=proof, repo_root=str(repo))
verify_chain(**args)
utils.write_text("ENGINE = 2\n")
result = verify_chain(**args, skip_passed=True)
assert counter.read_text() == "2"
assert not result.verdicts[0].cached
def test_a_fail_is_never_recorded_in_the_cache(repo: Path, tmp_path: Path) -> None:
"""Only PASS verdicts enter the cache; a failing proof leaves no entry for its sha."""
proof = tmp_path / "proof"
base, shas = _chain(repo, [_MSG])
_write_stub_proof(proof, shas[0], passing=False)
verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
cache = _cache_file(repo)
assert not cache.exists() or shas[0] not in json.loads(cache.read_text())["passed"]
def test_corrupt_cache_file_is_treated_as_empty(repo: Path, tmp_path: Path) -> None:
"""A garbage cache file never crashes the walk; the proof simply runs."""
proof = tmp_path / "proof"
base, shas = _chain(repo, [_MSG])
_write_stub_proof(proof, shas[0])
_cache_file(repo).write_text("{not json")
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo), skip_passed=True)
assert result.verdicts[0].verdict == VERDICT_PASS
assert not result.verdicts[0].cached
def test_cache_lives_in_the_git_common_dir_and_records_the_pass(repo: Path, tmp_path: Path) -> None:
"""A PASS writes the (sha, script hash, utils hash) entry under .git/."""
proof = tmp_path / "proof"
base, shas = _chain(repo, [_MSG])
_write_stub_proof(proof, shas[0])
verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
entry = json.loads(_cache_file(repo).read_text())["passed"][shas[0]]
assert set(entry) == {"script_sha256", "utils_sha256"}
assert len(entry["script_sha256"]) == 64
def test_report_counts_reused_proofs_and_main_accepts_the_flag(repo: Path, tmp_path: Path) -> None:
"""The report carries the reused count and --skip-passed works through main."""
proof = tmp_path / "proof"
base, shas = _chain(repo, [_MSG])
_write_stub_proof(proof, shas[0])
cli_args = [
"--base",
base,
"--branch",
"chain",
"--proof",
str(proof),
"--repo-root",
str(repo),
]
assert main(cli_args) == 0
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo), skip_passed=True)
report = render_report(result)
assert "reused from the passed-proof cache (--skip-passed): 1" in report
assert main([*cli_args, "--skip-passed"]) == 0
@@ -0,0 +1,203 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cli_testlib import _chain, _commit, _git, _write, _write_stub_proof
from mechanical_refactor_proof_generator import generate_range
from mechanical_refactor_reproduction_cli import (
VERDICT_FAIL,
VERDICT_HUMAN_REVIEW,
VERDICT_PASS,
ChainVerificationError,
main,
verify_chain,
)
def test_chain_of_proved_and_declared_commits_passes(repo: Path, tmp_path: Path) -> None:
"""A proved mechanical commit plus a declared non-mechanical one verifies as PASS."""
proof = tmp_path / "proof"
base, shas = _chain(
repo,
["mechanical_provable: move foo", "non_mechanical_provable: rework bar"],
)
_write_stub_proof(proof, shas[0])
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
assert [v.verdict for v in result.verdicts] == [VERDICT_PASS, VERDICT_HUMAN_REVIEW]
assert result.passed
def test_failing_proof_fails_the_commit_and_the_chain(repo: Path, tmp_path: Path) -> None:
"""A proof that exits non-zero yields FAIL with the output tail in the detail."""
proof = tmp_path / "proof"
base, shas = _chain(repo, ["mechanical_provable: move foo"])
_write_stub_proof(proof, shas[0], passing=False)
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
assert result.verdicts[0].verdict == VERDICT_FAIL
assert "RESIDUAL" in result.verdicts[0].detail
assert not result.passed
def test_proof_exiting_zero_without_a_pass_line_is_a_fail(repo: Path, tmp_path: Path) -> None:
"""PASS needs exit 0 AND the PASS: verdict line, so a residual under exit 0 fails."""
proof = tmp_path / "proof"
base, shas = _chain(repo, ["mechanical_provable: move foo"])
script = _write_stub_proof(proof, shas[0])
script.write_text('print("RESIDUAL (1 lines):\\n+x")\n')
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
assert result.verdicts[0].verdict == VERDICT_FAIL
def test_main_exit_codes_reflect_the_chain_verdict(repo: Path, tmp_path: Path) -> None:
"""main returns 0 for a verified chain and 1 once an unverifiable commit appears."""
proof = tmp_path / "proof"
base, shas = _chain(repo, ["mechanical_provable: move"])
_write_stub_proof(proof, shas[0])
args = [
"--base",
base,
"--branch",
"chain",
"--proof",
str(proof),
"--repo-root",
str(repo),
]
assert main(args) == 0
_git(repo, "commit", "-q", "--allow-empty", "-m", "plain subject with no kind word")
assert main(args) == 1
def test_unresolvable_refs_and_missing_proof_folder_are_setup_errors(repo: Path, tmp_path: Path) -> None:
"""Bad --base/--branch/--proof inputs raise ChainVerificationError (exit code 2)."""
proof = tmp_path / "proof"
proof.mkdir()
base, _ = _chain(repo, ["mechanical_provable: move"])
with pytest.raises(ChainVerificationError):
verify_chain(base=base, branch="no-such-branch", proof=proof, repo_root=str(repo))
with pytest.raises(ChainVerificationError):
verify_chain(
base=base,
branch="chain",
proof=tmp_path / "missing",
repo_root=str(repo),
)
assert (
main(
[
"--base",
base,
"--branch",
"no-such-branch",
"--proof",
str(proof),
"--repo-root",
str(repo),
]
)
== 2
)
def test_non_ancestor_base_and_empty_range_are_setup_errors(repo: Path, tmp_path: Path) -> None:
"""A base off the branch or an empty base..branch range refuses to verify."""
proof = tmp_path / "proof"
proof.mkdir()
base, shas = _chain(repo, ["mechanical_provable: move"])
_git(repo, "switch", "-q", "main")
_write(repo, **{"other.py": "OTHER = 1\n"})
off_branch = _commit(repo, "unrelated main-side commit")
with pytest.raises(ChainVerificationError):
verify_chain(base=off_branch, branch="chain", proof=proof, repo_root=str(repo))
with pytest.raises(ChainVerificationError):
verify_chain(base=shas[0], branch=shas[0], proof=proof, repo_root=str(repo))
def test_merge_commit_in_the_chain_is_a_setup_error(repo: Path, tmp_path: Path) -> None:
"""A non-linear chain (contains a merge commit) refuses to verify."""
proof = tmp_path / "proof"
proof.mkdir()
base, _ = _chain(repo, ["mechanical_provable: move"])
_git(repo, "switch", "-q", "main")
_write(repo, **{"other.py": "OTHER = 1\n"})
_commit(repo, "mechanical_provable: main-side")
_git(repo, "switch", "-q", "chain")
_git(repo, "merge", "-q", "--no-ff", "-m", "non_mechanical_provable: merge", "main")
with pytest.raises(ChainVerificationError):
verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
def test_proofs_run_concurrently_up_to_jobs(repo: Path, tmp_path: Path) -> None:
"""With jobs>=2 a proof that waits on a sibling proof's sentinel still completes."""
proof = tmp_path / "proof"
sentinel = tmp_path / "sentinel"
base, shas = _chain(repo, ["mechanical_provable: move a", "mechanical_provable: move b"])
waiter = _write_stub_proof(proof, shas[0])
waiter.write_text(
"import sys, time\n"
f"deadline = time.monotonic() + 30\n"
f"while not __import__('pathlib').Path({str(sentinel)!r}).exists():\n"
" if time.monotonic() > deadline:\n"
" sys.exit(1)\n"
" time.sleep(0.05)\n"
'print("PASS: reproduces the commit byte-for-byte.")\n'
"sys.exit(0)\n"
)
creator = _write_stub_proof(proof, shas[1])
creator.write_text(
"import sys\n"
f"__import__('pathlib').Path({str(sentinel)!r}).write_text('go')\n"
'print("PASS: reproduces the commit byte-for-byte.")\n'
"sys.exit(0)\n"
)
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo), jobs=2)
assert [v.verdict for v in result.verdicts] == [VERDICT_PASS, VERDICT_PASS]
assert [v.sha for v in result.verdicts] == shas
assert result.passed
def test_end_to_end_with_a_generated_proof_folder(repo: Path, tmp_path: Path) -> None:
"""A real move commit proved by generate_range verifies through the CLI end-to-end."""
_write(
repo,
**{
"model.py": "def keep():\n return 0\n\n\ndef resolve(m):\n return m\n",
"util.py": "import os\n",
},
)
base = _commit(repo, "base")
_git(repo, "switch", "-q", "-c", "chain")
# The after-state is the primitives' exact output (this bare repo has no formatter
# to absorb the cut's leftover blank lines, unlike a pre-commit-clean real repo).
_write(
repo,
**{
"model.py": "def keep():\n return 0\n\n\n",
"util.py": "import os\n\ndef resolve(m):\n return m\n",
},
)
move_sha = _commit(repo, "mechanical_provable: move resolve to util")
proof = tmp_path / "proof"
generate_range(f"{base}..chain", out_dir=str(proof), repo_root=str(repo))
result = verify_chain(base=base, branch="chain", proof=proof, repo_root=str(repo))
assert result.verdicts[0].sha == move_sha
assert result.verdicts[0].verdict == VERDICT_PASS
assert result.passed
@@ -0,0 +1,22 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from reproduction_testlib import _git
@pytest.fixture
def repo(tmp_path: Path) -> Path:
root = tmp_path / "repo"
root.mkdir()
_git(root, "init", "-q")
_git(root, "config", "user.email", "test@example.com")
_git(root, "config", "user.name", "test")
_git(root, "config", "commit.gpgsign", "false")
return root
# --- exec_command --------------------------------------------------------------
@@ -0,0 +1,33 @@
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
def _apply(repro: Repro, root: Path) -> None:
"""Run a built Repro's recorded operations against a plain directory (no git)."""
for op in repro.ops:
op(root)
def _git(repo: Path, *args: str) -> str:
return subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True, text=True).stdout.strip()
def _write(repo: Path, **files: str | None) -> None:
for name, content in files.items():
path = repo / name.replace("__", "/")
if content is None:
path.unlink()
else:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
def _commit(repo: Path, message: str) -> str:
_git(repo, "add", "-A")
_git(repo, "commit", "-q", "-m", message)
return _git(repo, "rev-parse", "HEAD")
@@ -0,0 +1,244 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
# --- add_import ----------------------------------------------------------------
def test_add_import_appends_after_last_top_level_import(tmp_path: Path) -> None:
"""A new import is inserted right after the last module-level import."""
(tmp_path / "m.py").write_text("import os\nimport sys\n\nx = 1\n")
r = Repro("b", "t").add_import("m.py", "from pkg import Thing")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "import os\nimport sys\nfrom pkg import Thing\n\nx = 1\n"
# --- add_imported_name ---------------------------------------------------------
def test_add_imported_name_extends_a_single_line_import(tmp_path: Path) -> None:
"""A new name is appended to an existing from-import on the same statement."""
(tmp_path / "m.py").write_text("from pkg import a\n\nx = 1\n")
r = Repro("b", "t").add_imported_name("m.py", module="pkg", name="b")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "from pkg import a, b\n\nx = 1\n"
def test_add_imported_name_carries_an_asname(tmp_path: Path) -> None:
"""The added name keeps its `as` alias."""
(tmp_path / "m.py").write_text("from pkg import a\n")
r = Repro("b", "t").add_imported_name("m.py", module="pkg", name="b", asname="c")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "from pkg import a, b as c\n"
def test_add_imported_name_refuses_a_commented_import(tmp_path: Path) -> None:
"""An import carrying comments is refused, since a rebuild would drop them."""
(tmp_path / "m.py").write_text("from pkg import (\n a, # keep\n)\n")
r = Repro("b", "t").add_imported_name("m.py", module="pkg", name="b")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_add_imported_name_rejects_a_name_already_present(tmp_path: Path) -> None:
"""Adding a name the import already has fails loudly."""
(tmp_path / "m.py").write_text("from pkg import a, b\n")
r = Repro("b", "t").add_imported_name("m.py", module="pkg", name="b")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_add_imported_name_raises_without_a_matching_import(tmp_path: Path) -> None:
"""A file lacking a `from module import` for the module fails loudly."""
(tmp_path / "m.py").write_text("from other import a\n")
r = Repro("b", "t").add_imported_name("m.py", module="pkg", name="b")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
# --- repath_import / add_typechecking_import -----------------------------------
def test_add_typechecking_import_inserts_in_block(tmp_path: Path) -> None:
"""The import is appended inside the existing TYPE_CHECKING block."""
(tmp_path / "m.py").write_text(
"from typing import TYPE_CHECKING\n"
"\n"
"if TYPE_CHECKING:\n"
" from a import X\n"
"\n"
"\n"
"def f():\n"
" pass\n"
)
r = Repro("b", "t").add_typechecking_import("m.py", "from b import Y")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == (
"from typing import TYPE_CHECKING\n"
"\n"
"if TYPE_CHECKING:\n"
" from a import X\n"
" from b import Y\n"
"\n"
"\n"
"def f():\n"
" pass\n"
)
def test_add_typechecking_import_creates_missing_block(tmp_path: Path) -> None:
"""With no TYPE_CHECKING block, one is created after the trailing module import."""
(tmp_path / "m.py").write_text(
"from typing import TYPE_CHECKING\n" "\n" "from a import X\n" "\n" "\n" "def f():\n" " pass\n"
)
r = Repro("b", "t").add_typechecking_import("m.py", "from b import Y")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == (
"from typing import TYPE_CHECKING\n"
"\n"
"from a import X\n"
"\n"
"if TYPE_CHECKING:\n"
" from b import Y\n"
"\n"
"\n"
"def f():\n"
" pass\n"
)
def test_add_import_into_an_empty_file(tmp_path: Path) -> None:
"""Adding an import to an empty file writes just the statement."""
(tmp_path / "m.py").write_text("")
r = Repro("b", "t").add_import("m.py", "import os")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "import os\n"
def test_add_import_lands_below_a_module_docstring(tmp_path: Path) -> None:
"""In a file with only a docstring, the new import must land below the docstring."""
(tmp_path / "m.py").write_text('"""Module doc."""\n\nx = 1\n')
r = Repro("b", "t").add_import("m.py", "import os")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text().startswith('"""Module doc."""')
def test_add_typechecking_import_matches_qualified_typing_form(tmp_path: Path) -> None:
"""A `if typing.TYPE_CHECKING:` block is recognized and receives the import."""
(tmp_path / "m.py").write_text(
"import typing\n" "\n" "if typing.TYPE_CHECKING:\n" " from a import X\n" "\n" "\n" "def f():\n" " pass\n"
)
r = Repro("b", "t").add_typechecking_import("m.py", "from b import Y")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == (
"import typing\n"
"\n"
"if typing.TYPE_CHECKING:\n"
" from a import X\n"
" from b import Y\n"
"\n"
"\n"
"def f():\n"
" pass\n"
)
def test_add_typechecking_import_after_a_multiline_final_import(tmp_path: Path) -> None:
"""The insert lands after the closing paren of a multi-line final guarded import."""
(tmp_path / "m.py").write_text(
"from typing import TYPE_CHECKING\n"
"\n"
"if TYPE_CHECKING:\n"
" from a import (\n"
" X,\n"
" )\n"
"\n"
"x = 1\n"
)
r = Repro("b", "t").add_typechecking_import("m.py", "from b import Y")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == (
"from typing import TYPE_CHECKING\n"
"\n"
"if TYPE_CHECKING:\n"
" from a import (\n"
" X,\n"
" )\n"
" from b import Y\n"
"\n"
"x = 1\n"
)
def test_add_typechecking_import_raises_without_imports(tmp_path: Path) -> None:
"""A file with no imports cannot anchor a new TYPE_CHECKING block and fails loudly."""
(tmp_path / "m.py").write_text("x = 1\n")
r = Repro("b", "t").add_typechecking_import("m.py", "from b import Y")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_add_typechecking_import_drops_a_lone_pass_placeholder(tmp_path: Path) -> None:
"""Populating a `pass`-only TYPE_CHECKING block replaces the placeholder."""
(tmp_path / "m.py").write_text(
"from typing import TYPE_CHECKING\n" "\n" "if TYPE_CHECKING:\n" " pass\n" "\n" "x = 1\n"
)
r = Repro("b", "t").add_typechecking_import("m.py", "from b import Y")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == (
"from typing import TYPE_CHECKING\n" "\n" "if TYPE_CHECKING:\n" " from b import Y\n" "\n" "x = 1\n"
)
def test_add_typechecking_import_keeps_a_pass_that_is_not_alone(tmp_path: Path) -> None:
"""A `pass` beside a real import is left untouched; only the new import is appended."""
(tmp_path / "m.py").write_text(
"from typing import TYPE_CHECKING\n"
"\n"
"if TYPE_CHECKING:\n"
" from a import X\n"
" pass\n"
"\n"
"x = 1\n"
)
r = Repro("b", "t").add_typechecking_import("m.py", "from b import Y")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == (
"from typing import TYPE_CHECKING\n"
"\n"
"if TYPE_CHECKING:\n"
" from a import X\n"
" pass\n"
" from b import Y\n"
"\n"
"x = 1\n"
)
# --- add_import(after=...) -----------------------------------------------------
def test_add_import_after_anchors_into_a_split_import_block(tmp_path: Path) -> None:
"""With `after`, the import lands right after the named import -- needed when a
statement splits the imports into separate blocks and the default (after the last
import) would land in the wrong block."""
(tmp_path / "m.py").write_text("import os\n\n_flag = os.getpid()\n\nfrom pkg import a\n\nx = 1\n")
r = Repro("b", "t").add_import("m.py", "from new import Thing", after="import os")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == (
"import os\nfrom new import Thing\n\n_flag = os.getpid()\n\nfrom pkg import a\n\nx = 1\n"
)
def test_add_import_after_raises_when_anchor_absent(tmp_path: Path) -> None:
"""An `after` substring that matches no top-level import raises."""
(tmp_path / "m.py").write_text("import os\n\nx = 1\n")
r = Repro("b", "t").add_import("m.py", "from new import Thing", after="import nope")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
@@ -0,0 +1,167 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
def test_lowered_call_text_preserves_magic_trailing_comma(tmp_path: Path) -> None:
"""A magic trailing comma in the original call survives the textual lowering."""
(tmp_path / "m.py").write_text("x = Old.foo(\n self.r,\n a,\n b,\n)\n")
r = Repro("b", "t").lower_call_sites("foo", "Old", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "x = self.r.foo(\n a,\n b,\n)\n"
# --- lower_call_sites ----------------------------------------------------------
def test_lower_call_sites_moves_receiver_out_of_args(tmp_path: Path) -> None:
"""Owner.foo(receiver, rest) becomes receiver.foo(rest)."""
(tmp_path / "m.py").write_text("x = ModelRunner.foo(self.r, a, b)\n")
r = Repro("b", "t").lower_call_sites("foo", "ModelRunner", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "x = self.r.foo(a, b)\n"
def test_lower_call_sites_handles_only_receiver_arg(tmp_path: Path) -> None:
"""Owner.foo(receiver) becomes receiver.foo() without re-lowering the result."""
(tmp_path / "m.py").write_text("ModelRunner.foo(self.r)\n")
r = Repro("b", "t").lower_call_sites("foo", "ModelRunner", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "self.r.foo()\n"
def test_lower_call_sites_ignores_a_different_owner(tmp_path: Path) -> None:
"""A same-named call on another receiver (e.g. the moved body's own call) is untouched."""
(tmp_path / "m.py").write_text("worker.foo(zmq)\n")
r = Repro("b", "t").lower_call_sites("foo", "ModelRunner", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "worker.foo(zmq)\n"
def test_lower_call_sites_preserves_magic_trailing_comma(tmp_path: Path) -> None:
"""A magic trailing comma is kept so the formatter re-explodes the lowered call."""
(tmp_path / "m.py").write_text("ModelRunner.foo(\n self.r,\n a,\n)\n")
r = Repro("b", "t").lower_call_sites("foo", "ModelRunner", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "self.r.foo(\n a,\n)\n"
# --- requalify_call_sites ------------------------------------------------------
# --- requalify_call_sites ------------------------------------------------------
def test_requalify_call_sites_drops_the_qualifier(tmp_path: Path) -> None:
"""Owner.bar(args) becomes bar(args) when bar moves to a free function."""
(tmp_path / "m.py").write_text("y = ModelRunner.bar(a, b)\n")
r = Repro("b", "t").requalify_call_sites("bar", "ModelRunner", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "y = bar(a, b)\n"
def test_route_call_sites_through_field_inserts_the_field(tmp_path: Path) -> None:
"""recv.bar(a) becomes recv.updater.bar(a) when bar moves onto a collaborator field."""
(tmp_path / "m.py").write_text("y = self.worker.runner.bar(a)\n")
r = Repro("b", "t").route_call_sites_through_field("bar", field="updater", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "y = self.worker.runner.updater.bar(a)\n"
def test_route_call_sites_through_field_skips_an_already_routed_call(
tmp_path: Path,
) -> None:
"""A call already going through the field is left alone, so the pass converges."""
(tmp_path / "m.py").write_text("y = self.runner.updater.bar(a)\n")
r = Repro("b", "t").route_call_sites_through_field("bar", field="updater", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "y = self.runner.updater.bar(a)\n"
def test_route_call_sites_through_field_honors_owner_filter(tmp_path: Path) -> None:
"""With owner set, only calls on that exact receiver are routed through the field."""
(tmp_path / "m.py").write_text("a = x.bar(1)\nb = y.bar(2)\n")
r = Repro("b", "t").route_call_sites_through_field("bar", field="updater", paths=["m.py"], owner="x")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "a = x.updater.bar(1)\nb = y.bar(2)\n"
# --- adversarial audit: call-site rewrites ---------------------------------------
# --- adversarial audit: call-site rewrites ---------------------------------------
def test_requalify_call_sites_matches_a_zero_argument_call(tmp_path: Path) -> None:
"""Owner.bar() with no arguments is requalified to bar()."""
(tmp_path / "m.py").write_text("y = Owner.bar()\n")
r = Repro("b", "t").requalify_call_sites("bar", "Owner", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "y = bar()\n"
def test_lower_call_sites_preserves_comments_inside_a_multiline_call(
tmp_path: Path,
) -> None:
"""A comment between arguments of the rewritten call must survive."""
(tmp_path / "m.py").write_text("x = Old.foo(\n self.r,\n a, # keep me\n b,\n)\n")
r = Repro("b", "t").lower_call_sites("foo", "Old", paths=["m.py"])
_apply(r, tmp_path)
assert "# keep me" in (tmp_path / "m.py").read_text()
def test_lower_call_sites_preserves_arg_literal_spelling(tmp_path: Path) -> None:
"""Hex literals and quote styles inside the rewritten call must not be normalized."""
(tmp_path / "m.py").write_text('x = Old.foo(self.r, 0x10, "s")\n')
r = Repro("b", "t").lower_call_sites("foo", "Old", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == 'x = self.r.foo(0x10, "s")\n'
def test_lower_call_sites_lowers_a_nested_matching_call_too(tmp_path: Path) -> None:
"""A matching call nested inside another matching call is lowered as well."""
(tmp_path / "m.py").write_text("x = Old.foo(self.r, Old.foo(self.q, 1))\n")
r = Repro("b", "t").lower_call_sites("foo", "Old", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "x = self.r.foo(self.q.foo(1))\n"
def test_lower_call_sites_magic_comma_with_sole_receiver_arg_stays_valid(
tmp_path: Path,
) -> None:
"""Lowering a magic-comma call whose only argument is the receiver stays valid Python."""
(tmp_path / "m.py").write_text("Owner.foo(\n self.r,\n)\n")
r = Repro("b", "t").lower_call_sites("foo", "Owner", paths=["m.py"])
_apply(r, tmp_path)
out = (tmp_path / "m.py").read_text()
compile(out, "m.py", "exec")
def test_call_rewrite_is_column_accurate_on_non_ascii_lines(tmp_path: Path) -> None:
"""A call after a non-ASCII string on the same line is rewritten at the right columns."""
(tmp_path / "m.py").write_text('x = "中文"; y = Owner.foo(self.r, 1)\n')
r = Repro("b", "t").lower_call_sites("foo", "Owner", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == 'x = "中文"; y = self.r.foo(1)\n'
def test_call_rewrite_survives_a_form_feed_line_start(tmp_path: Path) -> None:
"""A form feed at a line start must not shift the rewrite onto the wrong line."""
(tmp_path / "m.py").write_text("a = 1\n\x0cb = 2\ny = Owner.foo(self.r, 1)\n")
r = Repro("b", "t").lower_call_sites("foo", "Owner", paths=["m.py"])
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "a = 1\n\x0cb = 2\ny = self.r.foo(1)\n"
def test_requalify_call_sites_preserves_redundant_parens_in_kwargs(
tmp_path: Path,
) -> None:
"""Redundant parentheses around a keyword value survive the requalification."""
(tmp_path / "m.py").write_text("y = Old.bar(\n a=1,\n b=(2),\n)\n")
r = Repro("b", "t").requalify_call_sites("bar", "Old", paths=["m.py"])
_apply(r, tmp_path)
assert "b=(2)" in (tmp_path / "m.py").read_text()
@@ -0,0 +1,53 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
def test_delete_file_removes_emptied_source(tmp_path: Path) -> None:
"""delete_file removes a source module left empty after its defs relocated."""
(tmp_path / "gone.py").write_text("import os\n")
r = Repro("b", "t").delete_file("gone.py")
_apply(r, tmp_path)
assert not (tmp_path / "gone.py").exists()
def test_delete_file_refuses_a_file_with_remaining_definitions(tmp_path: Path) -> None:
"""Deleting a module that still contains defs must fail loudly."""
(tmp_path / "live.py").write_text("def still_used():\n return 42\n")
r = Repro("b", "t").delete_file("live.py")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
assert (tmp_path / "live.py").exists()
def test_delete_file_on_a_missing_path_is_a_no_op(tmp_path: Path) -> None:
"""Deleting an already-absent file does nothing and raises nothing."""
r = Repro("b", "t").delete_file("nope.py")
_apply(r, tmp_path)
assert not (tmp_path / "nope.py").exists()
def test_delete_file_allows_a_bare_module_logger(tmp_path: Path) -> None:
"""A leftover module holding only imports and a `logger` is deletable scaffolding."""
(tmp_path / "gone.py").write_text("import logging\n\nlogger = logging.getLogger(__name__)\n")
r = Repro("b", "t").delete_file("gone.py")
_apply(r, tmp_path)
assert not (tmp_path / "gone.py").exists()
def test_delete_file_still_refuses_a_non_logger_assignment(tmp_path: Path) -> None:
"""A leftover module-level assignment other than a logger blocks deletion."""
(tmp_path / "live.py").write_text("CONFIG = {'a': 1}\n")
r = Repro("b", "t").delete_file("live.py")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
assert (tmp_path / "live.py").exists()
# --- adversarial audit: extract_function -----------------------------------------
@@ -0,0 +1,169 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
# --- extract_function ----------------------------------------------------------
def test_extract_function_relocates_body_and_replaces_with_call(tmp_path: Path) -> None:
"""An inline block is cut verbatim, re-indented under the new signature, and the call site
replaced; the body lands at function-body indent."""
(tmp_path / "src.py").write_text(
"class Q:\n"
" def run(self, n):\n"
" total = 0\n"
" for i in range(n):\n"
" total += i * i\n"
" return total\n"
)
(tmp_path / "dst.py").write_text("def existing():\n return 0\n")
body = " total = 0\n for i in range(n):\n total += i * i\n"
r = Repro("b", "t").extract_function(
"src.py",
"dst.py",
name="sum_squares",
signature="def sum_squares(n):",
body=body,
body_indent=8,
call=" total = sum_squares(n)\n",
return_text=" return total\n",
)
_apply(r, tmp_path)
src_out = (tmp_path / "src.py").read_text()
assert " total = sum_squares(n)\n" in src_out
assert "for i in range(n)" not in src_out
assert (
"def sum_squares(n):\n"
" total = 0\n"
" for i in range(n):\n"
" total += i * i\n"
" return total\n"
) in (tmp_path / "dst.py").read_text()
def test_extract_function_inserts_before_named_sibling(tmp_path: Path) -> None:
"""With before=, the new function lands immediately above that sibling at module level."""
(tmp_path / "src.py").write_text("x = compute()\n")
(tmp_path / "dst.py").write_text("def a():\n return 1\n\n\ndef c():\n return 3\n")
r = Repro("b", "t").extract_function(
"src.py",
"dst.py",
name="b",
signature="def b():",
body="x = compute()\n",
body_indent=0,
call="x = b()\n",
return_text=" return x\n",
before="c",
)
_apply(r, tmp_path)
dst_out = (tmp_path / "dst.py").read_text()
assert dst_out.index("def a") < dst_out.index("def b") < dst_out.index("def c")
assert "x = b()\n" == (tmp_path / "src.py").read_text()
def test_extract_function_asserts_block_not_unique(tmp_path: Path) -> None:
"""A block that occurs more than once in the source raises, so the cut is unambiguous."""
(tmp_path / "src.py").write_text("p = f()\np = f()\n")
(tmp_path / "dst.py").write_text("def z():\n return 0\n")
r = Repro("b", "t").extract_function(
"src.py",
"dst.py",
name="g",
signature="def g():",
body="p = f()\n",
body_indent=0,
call="p = g()\n",
)
with pytest.raises(AssertionError):
_apply(r, tmp_path)
# --- adversarial audit: module extraction ----------------------------------------
# --- adversarial audit: extract_function -----------------------------------------
def test_extract_function_does_not_pad_blank_lines_in_the_body(tmp_path: Path) -> None:
"""Interior blank lines of the extracted body stay bare newlines, unpadded."""
(tmp_path / "src.py").write_text(" a = 1\n\n b = 2\n")
(tmp_path / "dst.py").write_text("def z():\n return 0\n")
r = Repro("b", "t").extract_function(
"src.py",
"dst.py",
name="g",
signature="def g():",
body=" a = 1\n\n b = 2\n",
body_indent=8,
call=" g()\n",
)
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == " g()\n"
assert (tmp_path / "dst.py").read_text() == ("def z():\n return 0\n\ndef g():\n a = 1\n\n b = 2\n")
def test_extract_function_does_not_reindent_string_literal_interiors(
tmp_path: Path,
) -> None:
"""Triple-quoted string interior lines keep their exact bytes through the extraction."""
(tmp_path / "src.py").write_text("TEMPLATE = '''\nliteral line\n'''\nx = TEMPLATE\n")
(tmp_path / "dst.py").write_text("def existing():\n return 0\n")
r = Repro("b", "t").extract_function(
"src.py",
"dst.py",
name="make",
signature="def make():",
body="TEMPLATE = '''\nliteral line\n'''\nx = TEMPLATE\n",
body_indent=0,
call="x = make()\n",
return_text=" return x\n",
)
_apply(r, tmp_path)
assert "\nliteral line\n" in (tmp_path / "dst.py").read_text()
def test_extract_function_rejects_a_mid_line_substring_match(tmp_path: Path) -> None:
"""A body that only matches mid-line must fail loudly instead of splicing the call."""
(tmp_path / "src.py").write_text("value = prefix_total = 0\n")
(tmp_path / "dst.py").write_text("def z():\n return 0\n")
r = Repro("b", "t").extract_function(
"src.py",
"dst.py",
name="g",
signature="def g():",
body="total = 0\n",
body_indent=0,
call="total = g()\n",
)
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_extract_function_into_class_indents_body_to_method_depth(
tmp_path: Path,
) -> None:
"""Extracting into a class must indent the relocated body to method depth."""
(tmp_path / "src.py").write_text("val = compute_thing()\n")
(tmp_path / "dst.py").write_text("class H:\n def last(self):\n return 0\n")
r = Repro("b", "t").extract_function(
"src.py",
"dst.py",
name="helper",
signature=" def helper(self):",
body="val = compute_thing()\n",
body_indent=0,
call="val = h.helper()\n",
return_text=" return val\n",
into_class="H",
)
_apply(r, tmp_path)
out = (tmp_path / "dst.py").read_text()
compile(out, "dst.py", "exec")
assert " def helper(self):\n val = compute_thing()\n" in out
@@ -0,0 +1,218 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
# --- extract_symbols_to_new_module ---------------------------------------------
def test_extract_symbols_to_new_module_gathers_scattered_defs(tmp_path: Path) -> None:
"""Scattered top-level defs are cut from the source and assembled under the authored header
in the given order; the source keeps everything else."""
(tmp_path / "src.py").write_text(
"import os\n"
"\n"
"\n"
"def keep_a():\n"
" return 1\n"
"\n"
"\n"
"def moved_b():\n"
" return 2\n"
"\n"
"\n"
"def keep_c():\n"
" return 3\n"
"\n"
"\n"
"def moved_a():\n"
" return 4\n"
)
header = (
"from __future__ import annotations\n" "\n" "import logging\n" "\n" "logger = logging.getLogger(__name__)\n"
)
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py",
"new.py",
symbols=["moved_b", "moved_a"],
header=header,
order=["moved_a", "moved_b"],
)
_apply(r, tmp_path)
src_out = (tmp_path / "src.py").read_text()
assert "def moved_a" not in src_out and "def moved_b" not in src_out
assert "def keep_a" in src_out and "def keep_c" in src_out
new_out = (tmp_path / "new.py").read_text()
assert new_out.startswith("from __future__ import annotations\n")
assert "logger = logging.getLogger(__name__)" in new_out
assert new_out.index("def moved_a") < new_out.index("def moved_b")
assert " return 4\n" in new_out and " return 2\n" in new_out
def test_extract_symbols_to_new_module_asserts_order_permutes_symbols(
tmp_path: Path,
) -> None:
"""An order that is not a permutation of the symbols raises, so a wrong recipe fails."""
(tmp_path / "src.py").write_text("def a():\n return 1\n\n\ndef b():\n return 2\n")
r = Repro("b", "t").extract_symbols_to_new_module("src.py", "n.py", symbols=["a", "b"], header="", order=["a"])
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_extract_symbols_to_new_module_asserts_when_symbol_absent(
tmp_path: Path,
) -> None:
"""A symbol that is not a top-level def/class in the source raises."""
(tmp_path / "src.py").write_text("def a():\n return 1\n")
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py", "n.py", symbols=["a", "missing"], header="", order=["a", "missing"]
)
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_extract_symbols_to_new_module_drops_relocated_assigns(tmp_path: Path) -> None:
"""A module-level constant that moved into the new module's header is deleted from the
source (its copy lives in the authored header); a kept assignment stays."""
(tmp_path / "src.py").write_text(
"import os\n" "\n" "_FLAG = os.cpu_count()\n" "stay = 1\n" "\n" "\n" "def moved():\n" " return _FLAG\n"
)
header = "from __future__ import annotations\n" "\n" "import os\n" "\n" "_FLAG = os.cpu_count()\n"
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py",
"new.py",
symbols=["moved"],
header=header,
order=["moved"],
drop_assigns=["_FLAG"],
)
_apply(r, tmp_path)
src_out = (tmp_path / "src.py").read_text()
assert "_FLAG = os.cpu_count()" not in src_out
assert "stay = 1" in src_out
assert "_FLAG = os.cpu_count()" in (tmp_path / "new.py").read_text()
def test_extract_symbols_to_new_module_asserts_unknown_drop_assign(
tmp_path: Path,
) -> None:
"""A drop_assigns name that is not assigned at module level in the source raises."""
(tmp_path / "src.py").write_text("X = 1\n\n\ndef m():\n return X\n")
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py", "n.py", symbols=["m"], header="", order=["m"], drop_assigns=["Y"]
)
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_extract_symbols_to_new_module_header_accepts_a_typechecking_block(
tmp_path: Path,
) -> None:
"""An authored header may carry an `if TYPE_CHECKING:` import block; the audit accepts it
and the block is reproduced verbatim in the new module."""
(tmp_path / "src.py").write_text("def moved(x):\n return x\n")
header = (
"from __future__ import annotations\n"
"\n"
"from typing import TYPE_CHECKING\n"
"\n"
"if TYPE_CHECKING:\n"
" from other import Thing\n"
"\n"
)
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py", "new.py", symbols=["moved"], header=header, order=["moved"]
)
_apply(r, tmp_path)
new_out = (tmp_path / "new.py").read_text()
assert "if TYPE_CHECKING:\n from other import Thing\n" in new_out
assert "def moved(x):\n return x\n" in new_out
# --- extract_function ----------------------------------------------------------
def test_extract_symbols_to_new_module_joins_blocks_with_two_blank_lines(
tmp_path: Path,
) -> None:
"""Relocated blocks are joined with exactly two blank lines (the formatter's spacing)."""
(tmp_path / "src.py").write_text("def moved_a():\n return 1\n\n\n\n\ndef moved_b():\n return 2\n")
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py",
"new.py",
symbols=["moved_a", "moved_b"],
header="",
order=["moved_a", "moved_b"],
)
_apply(r, tmp_path)
assert (tmp_path / "new.py").read_text() == ("def moved_a():\n return 1\n\n\ndef moved_b():\n return 2\n")
def test_extract_symbols_to_new_module_leaves_a_comment_above_a_moved_def(
tmp_path: Path,
) -> None:
"""A section comment directly above a moved def stays behind in the source."""
(tmp_path / "src.py").write_text("x = 1\n\n\n# --- movers ---\ndef moved():\n return 2\n")
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py", "new.py", symbols=["moved"], header="", order=["moved"]
)
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == "x = 1\n\n\n# --- movers ---\n"
assert (tmp_path / "new.py").read_text() == "def moved():\n return 2\n"
def test_extract_symbols_drop_assigns_preserves_other_targets_of_chained_assign(
tmp_path: Path,
) -> None:
"""Dropping A from `A = B = 1` must not delete B's binding from the source."""
(tmp_path / "src.py").write_text("A = B = 1\n\n\ndef moved():\n return A\n")
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py",
"new.py",
symbols=["moved"],
header="A = 1\n",
order=["moved"],
drop_assigns=["A"],
)
_apply(r, tmp_path)
assert "B" in (tmp_path / "src.py").read_text()
def test_extract_symbols_to_new_module_allows_a_rederived_surviving_constant(
tmp_path: Path,
) -> None:
"""A header constant that also survives verbatim in the source (re-derived boilerplate,
e.g. `_is_hip = is_hip()`) is allowed: it is provably not fiction because the same
statement remains in the source."""
(tmp_path / "src.py").write_text(
"from pkg import is_hip\n" "\n" "_is_hip = is_hip()\n" "\n" "\n" "def moved():\n" " return _is_hip\n"
)
header = "from pkg import is_hip\n\n_is_hip = is_hip()\n"
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py", "new.py", symbols=["moved"], header=header, order=["moved"]
)
_apply(r, tmp_path)
assert "_is_hip = is_hip()" in (tmp_path / "src.py").read_text()
assert "_is_hip = is_hip()" in (tmp_path / "new.py").read_text()
def test_extract_symbols_to_new_module_rejects_a_fictional_header_constant(
tmp_path: Path,
) -> None:
"""A header constant that is neither dropped from nor surviving in the source is fiction
and raises: the audit refuses code the extraction cannot vouch for."""
(tmp_path / "src.py").write_text("def moved():\n return 1\n")
r = Repro("b", "t").extract_symbols_to_new_module(
"src.py",
"new.py",
symbols=["moved"],
header="_fake = evil()\n",
order=["moved"],
)
with pytest.raises(AssertionError):
_apply(r, tmp_path)
@@ -0,0 +1,103 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
# --- extract_to_new_module -----------------------------------------------------
def test_extract_to_new_module_cuts_trailing_block(tmp_path: Path) -> None:
"""Cuts the trailing scaffolding+def block into a new file, prepending the future import."""
(tmp_path / "src.py").write_text(
"class M:\n"
" def keep(self):\n"
" return 1\n"
"\n"
"\n"
"import logging\n"
"\n"
"logger = logging.getLogger(__name__)\n"
"\n"
"\n"
"def foo(x):\n"
" return x + 1\n"
)
r = Repro("b", "t").extract_to_new_module("src.py", "new.py", symbols=["foo"], future_import=True)
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == ("class M:\n def keep(self):\n return 1\n\n\n")
assert (tmp_path / "new.py").read_text() == (
"from __future__ import annotations\n"
"import logging\n"
"\n"
"logger = logging.getLogger(__name__)\n"
"\n"
"\n"
"def foo(x):\n"
" return x + 1\n"
)
def test_extract_to_new_module_carries_a_trailing_class(tmp_path: Path) -> None:
"""A class in the staged tail (not just a def) travels with the cut block."""
(tmp_path / "src.py").write_text(
"class M:\n"
" pass\n"
"\n"
"\n"
"from dataclasses import dataclass\n"
"\n"
"\n"
"@dataclass\n"
"class Cfg:\n"
" x: int\n"
"\n"
"\n"
"def foo():\n"
" return Cfg(1)\n"
)
r = Repro("b", "t").extract_to_new_module("src.py", "new.py", symbols=["Cfg", "foo"], future_import=False)
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == "class M:\n pass\n\n\n"
assert "class Cfg:" in (tmp_path / "new.py").read_text()
assert "def foo():" in (tmp_path / "new.py").read_text()
# --- extract_symbols_to_new_module ---------------------------------------------
# --- adversarial audit: module extraction ----------------------------------------
def test_extract_to_new_module_asserts_when_symbol_not_in_the_tail(
tmp_path: Path,
) -> None:
"""A wanted symbol above a non-scaffolding statement is not in the tail and raises."""
(tmp_path / "src.py").write_text("def wanted():\n return 1\n\n\nprint('side effect')\n")
r = Repro("b", "t").extract_to_new_module("src.py", "n.py", symbols=["wanted"])
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_extract_to_new_module_refuses_a_trailing_main_guard(tmp_path: Path) -> None:
"""A trailing __main__ guard is executable code, not scaffolding: the tail cut raises."""
(tmp_path / "src.py").write_text(
"class Keep:\n"
" pass\n"
"\n"
"\n"
"def foo():\n"
" return 1\n"
"\n"
"\n"
'if __name__ == "__main__":\n'
" foo()\n"
)
r = Repro("b", "t").extract_to_new_module("src.py", "new.py", symbols=["foo"], future_import=False)
with pytest.raises(AssertionError):
_apply(r, tmp_path)
assert "__main__" in (tmp_path / "src.py").read_text()
@@ -0,0 +1,148 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import mechanical_refactor_reproduction_utils as rr
from mechanical_refactor_reproduction_utils import (
_def_span,
_find_class,
_find_def,
_replace_span,
_slice_span,
dedent,
exec_command,
git_add_and_commit,
)
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
# --- exec_command --------------------------------------------------------------
def test_exec_command_returns_stripped_stdout_on_success() -> None:
"""A successful command returns its stdout with surrounding whitespace stripped."""
assert exec_command("echo hello") == "hello"
def test_exec_command_respects_cwd(tmp_path: Path) -> None:
"""The command runs in the supplied working directory."""
sub = tmp_path / "workdir"
sub.mkdir()
assert exec_command("pwd", cwd=str(sub)) == str(sub.resolve())
def test_exec_command_check_true_raises_on_failure() -> None:
"""With check=True a non-zero exit status raises RuntimeError with the command."""
with pytest.raises(RuntimeError, match="exit 7"):
exec_command("exit 7", check=True)
def test_exec_command_check_false_returns_stdout_without_exiting() -> None:
"""With check=False a failing command returns its stdout and does not exit."""
assert exec_command("echo partial; exit 3", check=False) == "partial"
# --- git_add_and_commit --------------------------------------------------------
# --- git_add_and_commit --------------------------------------------------------
def test_git_add_and_commit_stages_and_commits(repo: Path) -> None:
"""It stages every change in the cwd and records a commit with the message."""
_write(repo, **{"file.txt": "content\n"})
git_add_and_commit("add file", cwd=str(repo))
assert _git(repo, "log", "-1", "--format=%s") == "add file"
assert _git(repo, "status", "--porcelain") == ""
@pytest.mark.parametrize(
"message",
[
"subject with spaces",
"has 'single' and \"double\" quotes",
"shell $HOME && rm -rf / ; metacharacters",
"trailing parens (a, b) and pipe | semicolon ;",
],
)
def test_git_add_and_commit_message_round_trips_with_metacharacters(repo: Path, message: str) -> None:
"""Messages with shell metacharacters are quoted safely and survive verbatim."""
_write(repo, **{"file.txt": "content\n"})
git_add_and_commit(message, cwd=str(repo))
assert _git(repo, "log", "-1", "--format=%B") == message
# --- dedent --------------------------------------------------------------------
# --- dedent --------------------------------------------------------------------
def test_dedent_with_zero_leaves_text_unchanged() -> None:
"""Dedenting by zero spaces returns the text untouched."""
text = " indented\nplain\n"
assert dedent(text, 0) == text
def test_dedent_removes_exactly_n_leading_spaces() -> None:
"""Exactly n leading spaces are removed from each qualifying line."""
assert dedent(" four\n eight\n", 4) == "four\n eight\n"
def test_dedent_leaves_lines_with_fewer_than_n_spaces_unchanged() -> None:
"""A line with fewer than n leading spaces is not modified at all."""
assert dedent(" four\n two\nzero\n", 4) == "four\n two\nzero\n"
def test_dedent_does_not_strip_tabs() -> None:
"""Tab characters are never treated as the spaces dedent removes."""
assert dedent("\t\ttabbed\n", 2) == "\t\ttabbed\n"
def test_dedent_preserves_blank_lines_and_trailing_newline() -> None:
"""Blank lines and a final newline are preserved across line boundaries."""
assert dedent(" a\n\n b\n", 4) == "a\n\nb\n"
def test_dedent_preserves_absence_of_trailing_newline() -> None:
"""A text without a trailing newline keeps it absent after dedenting."""
assert dedent(" a\n b", 4) == "a\nb"
# --- span / call helpers -------------------------------------------------------
# --- span / call helpers -------------------------------------------------------
def test_replace_span_single_line() -> None:
"""A span within one line is replaced in place."""
assert _replace_span("ab cd ef\n", 1, 3, 1, 5, "XY") == "ab XY ef\n"
def test_replace_span_across_lines() -> None:
"""A span crossing lines collapses to the replacement between the kept prefix/suffix."""
text = "a = foo(\n x,\n) + 1\n"
assert _replace_span(text, 1, 4, 3, 1, "bar()") == "a = bar() + 1\n"
def test_slice_span_returns_the_overwritten_text() -> None:
"""_slice_span returns exactly the region _replace_span would overwrite."""
text = "a = foo(\n x,\n) + 1\n"
assert _slice_span(text, 1, 4, 3, 1) == "foo(\n x,\n)"
def test_find_def_span_includes_decorators() -> None:
"""A def's span starts at its first decorator and ends at its last body line."""
src = "class C:\n @staticmethod\n def foo(self):\n return 1\n"
node = _find_def(rr.ast.parse(src), "foo")
assert node is not None and _def_span(node) == (2, 4)
def test_find_class_returns_named_class_or_none() -> None:
"""_find_class locates a class by name and returns None when absent."""
tree = rr.ast.parse("class A:\n pass\nclass B:\n pass\n")
assert _find_class(tree, "B").name == "B"
assert _find_class(tree, "Z") is None
@@ -0,0 +1,51 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply # noqa: F401
def test_move_assign_relocates_a_module_constant(tmp_path: Path) -> None:
"""The assignment is cut verbatim from the source and lands after the destination's imports."""
(tmp_path / "src.py").write_text("import os\n\nLIMIT = 480 # seconds\n\n\ndef stay():\n return LIMIT\n")
(tmp_path / "dst.py").write_text("import sys\n\n\ndef keep():\n return 1\n")
r = Repro("b", "t").move_assign("LIMIT", src="src.py", dst="dst.py")
_apply(r, tmp_path)
assert "LIMIT" not in (tmp_path / "src.py").read_text().split("def stay")[0]
assert (tmp_path / "dst.py").read_text() == (
"import sys\n" "\n" "LIMIT = 480 # seconds\n" "\n" "\n" "def keep():\n" " return 1\n"
)
def test_move_assign_pastes_above_the_named_sibling(tmp_path: Path) -> None:
"""With before=, the constant lands immediately above the named top-level statement."""
(tmp_path / "src.py").write_text("RATIO = 3\n")
(tmp_path / "dst.py").write_text("def first():\n return 1\n")
r = Repro("b", "t").move_assign("RATIO", src="src.py", dst="dst.py", before="first")
_apply(r, tmp_path)
assert (tmp_path / "dst.py").read_text() == ("RATIO = 3\n\ndef first():\n return 1\n")
def test_move_assign_relocates_an_annotated_constant(tmp_path: Path) -> None:
"""An annotated module constant (AnnAssign) is cut verbatim with its annotation intact."""
(tmp_path / "src.py").write_text("import os\n\nLIMIT: int = 480\n\n\ndef stay():\n return LIMIT\n")
(tmp_path / "dst.py").write_text("import sys\n\n\ndef keep():\n return 1\n")
r = Repro("b", "t").move_assign("LIMIT", src="src.py", dst="dst.py")
_apply(r, tmp_path)
assert "LIMIT" not in (tmp_path / "src.py").read_text().split("def stay")[0]
assert (tmp_path / "dst.py").read_text() == (
"import sys\n" "\n" "LIMIT: int = 480\n" "\n" "\n" "def keep():\n" " return 1\n"
)
def test_move_assign_missing_source_raises(tmp_path: Path) -> None:
"""A name with no module-level assignment in the source fails loudly."""
(tmp_path / "src.py").write_text("x = 1\n")
(tmp_path / "dst.py").write_text("import os\n")
r = Repro("b", "t").move_assign("MISSING", src="src.py", dst="dst.py")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
@@ -0,0 +1,337 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
def test_move_symbol_drops_self_annotation_into_class(tmp_path: Path) -> None:
"""Moving a `def foo(self: Target)` into Target drops the now-redundant annotation."""
(tmp_path / "src.py").write_text(
"class M:\n" " @staticmethod\n" " def foo(self: Target, x):\n" " return self.y + x\n"
)
(tmp_path / "dst.py").write_text("class Target:\n def keep(self):\n return 1\n")
r = Repro("b", "t").move_symbol(
"foo",
src="src.py",
dst="dst.py",
into_class="Target",
dedent=0,
drop_self_annotation=True,
)
_apply(r, tmp_path)
text = (tmp_path / "dst.py").read_text()
assert "def foo(self, x):" in text
assert "self: Target" not in text
# --- adversarial audit: import primitives ----------------------------------------
# --- move_symbol ---------------------------------------------------------------
def test_move_symbol_into_class_drops_decorator_and_appends(tmp_path: Path) -> None:
"""The def leaves the source, its @staticmethod is dropped, and it lands at the end of
the destination class with its body verbatim."""
(tmp_path / "src.py").write_text(
"class Old:\n"
" @staticmethod\n"
" def foo(x):\n"
" return x + 1\n"
"\n"
" def keep(self):\n"
" return 0\n"
)
(tmp_path / "dst.py").write_text("class New:\n def existing(self):\n return 1\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")
_apply(r, tmp_path)
src_out = (tmp_path / "src.py").read_text()
dst_out = (tmp_path / "dst.py").read_text()
assert "def foo" not in src_out and "def keep" in src_out
assert "@staticmethod" not in dst_out
assert dst_out.index("def existing") < dst_out.index("def foo")
assert " return x + 1\n" in dst_out
def test_move_symbol_to_module_level_with_dedent(tmp_path: Path) -> None:
"""With into_class=None and a dedent, the def lands at module level, dedented."""
(tmp_path / "src.py").write_text("class Old:\n @staticmethod\n def helper(x):\n return x * 2\n")
(tmp_path / "dst.py").write_text("import os\n")
r = Repro("b", "t").move_symbol("helper", src="src.py", dst="dst.py", into_class=None, dedent=4)
_apply(r, tmp_path)
assert "def helper(x):\n return x * 2\n" in (tmp_path / "dst.py").read_text()
assert "def helper" not in (tmp_path / "src.py").read_text()
def test_move_symbol_before_inserts_above_named_sibling(tmp_path: Path) -> None:
"""With before=, the relocated def lands immediately above that sibling, not at the end."""
(tmp_path / "src.py").write_text("class Old:\n @staticmethod\n def moved(self):\n return 1\n")
(tmp_path / "dst.py").write_text(
"class New:\n" " def first(self):\n return 0\n" "\n" " def last(self):\n return 2\n"
)
r = Repro("b", "t").move_symbol("moved", src="src.py", dst="dst.py", into_class="New", before="last")
_apply(r, tmp_path)
dst_out = (tmp_path / "dst.py").read_text()
assert dst_out.index("def first") < dst_out.index("def moved") < dst_out.index("def last")
# --- adversarial audit: move_symbol edge cases -----------------------------------
def test_move_symbol_moves_an_async_def_verbatim(tmp_path: Path) -> None:
"""An async def relocates with its `async` keyword and body byte-identical."""
(tmp_path / "src.py").write_text(
"class Old:\n"
" async def foo(self):\n"
" return 1\n"
"\n"
" def keep(self):\n"
" return 0\n"
)
(tmp_path / "dst.py").write_text("class New:\n def e(self):\n return 1\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == ("class Old:\n\n def keep(self):\n return 0\n")
assert (tmp_path / "dst.py").read_text() == (
"class New:\n" " def e(self):\n" " return 1\n" "\n" " async def foo(self):\n" " return 1\n"
)
def test_move_symbol_moves_a_def_within_the_same_file(tmp_path: Path) -> None:
"""With src == dst the def is cut and re-inserted above its sibling in one file."""
(tmp_path / "m.py").write_text("def a():\n return 1\n\n\ndef b():\n return 2\n")
r = Repro("b", "t").move_symbol("b", src="m.py", dst="m.py", into_class=None, before="a")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == ("def b():\n return 2\n\ndef a():\n return 1\n\n\n")
def test_move_symbol_prefers_a_module_level_def_over_an_earlier_class_method(
tmp_path: Path,
) -> None:
"""When a class method and a module-level def share a name, the module-level def moves."""
(tmp_path / "src.py").write_text(
"class C:\n"
" def foo(self):\n"
" return 'method'\n"
"\n"
"\n"
"def foo():\n"
" return 'module'\n"
)
(tmp_path / "dst.py").write_text("x = 1\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class=None)
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == ("class C:\n def foo(self):\n return 'method'\n\n\n")
assert (tmp_path / "dst.py").read_text() == ("x = 1\n\ndef foo():\n return 'module'\n")
def test_move_symbol_keeps_a_real_decorator_while_dropping_classmethod(
tmp_path: Path,
) -> None:
"""@classmethod is shed on the move but any other decorator travels verbatim."""
(tmp_path / "src.py").write_text(
"import functools\n"
"\n"
"\n"
"class Old:\n"
" @classmethod\n"
" @functools.lru_cache(maxsize=None)\n"
" def foo(cls, x):\n"
" return x + 1\n"
)
(tmp_path / "dst.py").write_text("def z():\n return 0\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class=None, dedent=4)
_apply(r, tmp_path)
assert (tmp_path / "dst.py").read_text() == (
"def z():\n"
" return 0\n"
"\n"
"@functools.lru_cache(maxsize=None)\n"
"def foo(cls, x):\n"
" return x + 1\n"
)
def test_move_symbol_leaves_a_comment_above_the_def_in_the_source(
tmp_path: Path,
) -> None:
"""A comment above the def is not part of its span, so it stays behind in the source."""
(tmp_path / "src.py").write_text("# explains foo\ndef foo():\n return 1\n\n\ndef keep():\n return 2\n")
(tmp_path / "dst.py").write_text("x = 1\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class=None)
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == ("# explains foo\n\n\ndef keep():\n return 2\n")
assert (tmp_path / "dst.py").read_text() == "x = 1\n\ndef foo():\n return 1\n"
def test_move_symbol_without_trailing_newlines_keeps_moved_bytes(
tmp_path: Path,
) -> None:
"""Files lacking a final newline lose no bytes of the moved def or the remainder."""
(tmp_path / "src.py").write_text("def keep():\n return 0\n\n\ndef foo():\n return 1")
(tmp_path / "dst.py").write_text("x = 1")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class=None)
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == "def keep():\n return 0\n\n\n"
assert (tmp_path / "dst.py").read_text() == "x = 1\ndef foo():\n return 1"
def test_move_symbol_dedent_leaves_string_literal_interior_lines(
tmp_path: Path,
) -> None:
"""Dedent only strips lines with exactly n leading spaces, so string interiors survive."""
(tmp_path / "src.py").write_text(
"class Old:\n"
" class Deep:\n"
" def foo(self):\n"
" s = '''raw\n"
" partial\n"
"'''\n"
" return s\n"
)
(tmp_path / "dst.py").write_text("import os\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class=None, dedent=8)
_apply(r, tmp_path)
assert (tmp_path / "dst.py").read_text() == (
"import os\n" "\n" "def foo(self):\n" " s = '''raw\n" " partial\n" "'''\n" " return s\n"
)
def test_move_symbol_asserts_when_destination_class_missing(tmp_path: Path) -> None:
"""Naming an into_class absent from the destination fails loudly."""
(tmp_path / "src.py").write_text("def foo():\n return 1\n")
(tmp_path / "dst.py").write_text("x = 1\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="Nope")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_move_symbol_preserves_staticmethod_inside_moved_body(tmp_path: Path) -> None:
"""A @staticmethod on a nested def inside the moved body must survive the move."""
(tmp_path / "src.py").write_text(
"class Old:\n"
" @staticmethod\n"
" def foo(x):\n"
" class Inner:\n"
" @staticmethod\n"
" def helper(y):\n"
" return y\n"
" return Inner.helper(x)\n"
)
(tmp_path / "dst.py").write_text("class New:\n def keep(self):\n return 0\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")
_apply(r, tmp_path)
dst_out = (tmp_path / "dst.py").read_text()
assert " @staticmethod\n def helper(y):\n" in dst_out
def test_move_symbol_rejects_ambiguous_duplicate_names(tmp_path: Path) -> None:
"""Two same-named defs at equal depth must raise instead of silently picking one."""
(tmp_path / "src.py").write_text(
"class A:\n"
" def foo(self):\n"
" return 'A'\n"
"\n"
"class B:\n"
" def foo(self):\n"
" return 'B'\n"
)
(tmp_path / "dst.py").write_text("class New:\n def keep(self):\n return 0\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_move_symbol_after_inserts_below_named_function(tmp_path: Path) -> None:
"""With after=, the relocated def lands immediately below that sibling def."""
(tmp_path / "src.py").write_text("def moved():\n return 1\n")
(tmp_path / "dst.py").write_text("def first():\n return 0\n\n\ndef last():\n return 2\n")
r = Repro("b", "t").move_symbol("moved", src="src.py", dst="dst.py", into_class=None, after="first")
_apply(r, tmp_path)
dst_out = (tmp_path / "dst.py").read_text()
assert dst_out.index("def first") < dst_out.index("def moved") < dst_out.index("def last")
def test_move_symbol_after_assign_lands_before_typechecking_guard(
tmp_path: Path,
) -> None:
"""after= anchors on a module-level assignment target, landing the def just below it and
above a following ``if TYPE_CHECKING:`` guard (which is not a nameable anchor)."""
(tmp_path / "src.py").write_text("def helper(x):\n return x + 1\n")
(tmp_path / "dst.py").write_text(
"from u import is_hip\n" "\n" "_is_hip = is_hip()\n" "\n" "if TYPE_CHECKING:\n" " from m import Thing\n"
)
r = Repro("b", "t").move_symbol("helper", src="src.py", dst="dst.py", into_class=None, after="_is_hip")
_apply(r, tmp_path)
dst_out = (tmp_path / "dst.py").read_text()
assert dst_out.index("_is_hip = is_hip()") < dst_out.index("def helper") < dst_out.index("if TYPE_CHECKING:")
def test_move_symbol_before_and_after_are_mutually_exclusive(tmp_path: Path) -> None:
"""Passing both before= and after= is rejected up front."""
(tmp_path / "src.py").write_text("def moved():\n return 1\n")
(tmp_path / "dst.py").write_text("def z():\n return 0\n")
with pytest.raises(AssertionError):
Repro("b", "t").move_symbol(
"moved",
src="src.py",
dst="dst.py",
into_class=None,
before="z",
after="z",
)
def test_move_symbol_asserts_when_after_symbol_missing(tmp_path: Path) -> None:
"""An after= anchor absent from the destination must raise, not fall back to append."""
(tmp_path / "src.py").write_text("def moved():\n return 1\n")
(tmp_path / "dst.py").write_text("def z():\n return 0\n")
r = Repro("b", "t").move_symbol("moved", src="src.py", dst="dst.py", into_class=None, after="NO_SUCH_SYMBOL")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_move_symbol_asserts_when_before_sibling_missing(tmp_path: Path) -> None:
"""A before= anchor absent from the destination must raise, not fall back to append."""
(tmp_path / "src.py").write_text("def moved():\n return 1\n")
(tmp_path / "dst.py").write_text("def z():\n return 0\n")
r = Repro("b", "t").move_symbol("moved", src="src.py", dst="dst.py", into_class=None, before="NO_SUCH_DEF")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_move_symbol_preserves_crlf_line_endings(tmp_path: Path) -> None:
"""Moving a def in a CRLF file must keep every line ending CRLF."""
(tmp_path / "src.py").write_bytes(b"class Old:\r\n def foo(self):\r\n return 1\r\n")
(tmp_path / "dst.py").write_bytes(b"class New:\r\n def keep(self):\r\n return 0\r\n")
r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")
_apply(r, tmp_path)
dst_bytes = (tmp_path / "dst.py").read_bytes()
assert dst_bytes.count(b"\n") == dst_bytes.count(b"\r\n")
def test_move_symbol_negative_dedent_indents_into_the_class(tmp_path: Path) -> None:
"""Moving a module-level def into a class with dedent=-4 must indent it as a method."""
(tmp_path / "src.py").write_text("def helper(x):\n return x\n")
(tmp_path / "dst.py").write_text("class New:\n def e(self):\n return 0\n")
r = Repro("b", "t").move_symbol("helper", src="src.py", dst="dst.py", into_class="New", dedent=-4)
_apply(r, tmp_path)
assert " def helper(x):\n return x\n" in (tmp_path / "dst.py").read_text()
def test_move_symbol_relocates_a_top_level_class(tmp_path: Path) -> None:
"""move_symbol relocates a whole top-level class (with its methods) verbatim."""
(tmp_path / "src.py").write_text("x = 1\n\n\nclass Widget:\n def get(self, rank):\n return rank\n")
(tmp_path / "dst.py").write_text("y = 2\n")
r = Repro("b", "t").move_symbol("Widget", src="src.py", dst="dst.py", into_class=None)
_apply(r, tmp_path)
assert "class Widget:" not in (tmp_path / "src.py").read_text()
assert "class Widget:\n def get(self, rank):\n return rank\n" in (tmp_path / "dst.py").read_text()
# --- adversarial audit: leave_delegate stubs -------------------------------------
@@ -0,0 +1,253 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
def test_move_symbol_leave_delegate_keeps_forwarding_stub(tmp_path: Path) -> None:
"""With leave_delegate, the source keeps a forwarding stub through the named field and the
destination gets the full method body."""
(tmp_path / "src.py").write_text(
"class Mixin:\n" " def compute(self, n: int) -> int:\n" " return n + self.cfg.base\n"
)
(tmp_path / "dst.py").write_text("class Cfg:\n def existing(self):\n return 0\n")
r = Repro("b", "t").move_symbol(
"compute",
src="src.py",
dst="dst.py",
into_class="Cfg",
leave_delegate="cfg",
)
_apply(r, tmp_path)
src_out = (tmp_path / "src.py").read_text()
dst_out = (tmp_path / "dst.py").read_text()
assert "def compute(self, n: int) -> int:" in src_out
assert "return self.cfg.compute(n)" in src_out
assert "return n + self.cfg.base" not in src_out
assert "return n + self.cfg.base" in dst_out
def test_move_symbol_leave_delegate_does_not_absorb_leading_comments(
tmp_path: Path,
) -> None:
"""A leading comment before the first statement is not an AST node, so it must not be
pulled into the forwarding stub -- the delegate is just the header plus the return.
"""
(tmp_path / "src.py").write_text(
"class Mixin:\n"
" def compute(self, n: int) -> int:\n"
" # explain the maths\n"
" # second comment line\n"
" return n + self.cfg.base\n"
)
(tmp_path / "dst.py").write_text("class Cfg:\n def existing(self):\n return 0\n")
r = Repro("b", "t").move_symbol("compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg")
_apply(r, tmp_path)
src_out = (tmp_path / "src.py").read_text()
dst_out = (tmp_path / "dst.py").read_text()
assert "# explain the maths" not in src_out
assert src_out == "class Mixin:\n" " def compute(self, n: int) -> int:\n" " return self.cfg.compute(n)\n"
assert "# explain the maths" in dst_out
# --- adversarial audit: move_symbol edge cases -----------------------------------
# --- adversarial audit: leave_delegate stubs -------------------------------------
def test_move_symbol_leave_delegate_keeps_a_multiline_signature_verbatim(
tmp_path: Path,
) -> None:
"""A multi-line header is carried into the stub byte-for-byte via the bracket scan."""
(tmp_path / "src.py").write_text(
"class Mixin:\n"
" def compute(\n"
" self,\n"
" n: int,\n"
" *,\n"
" scale: float = 1.0,\n"
" ) -> int:\n"
" return int(n * scale) + self.cfg.base\n"
)
(tmp_path / "dst.py").write_text("class Cfg:\n def e(self):\n return 0\n")
r = Repro("b", "t").move_symbol("compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg")
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == (
"class Mixin:\n"
" def compute(\n"
" self,\n"
" n: int,\n"
" *,\n"
" scale: float = 1.0,\n"
" ) -> int:\n"
" return self.cfg.compute(n, scale=scale)\n"
)
assert (tmp_path / "dst.py").read_text() == (
"class Cfg:\n"
" def e(self):\n"
" return 0\n"
"\n"
" def compute(\n"
" self,\n"
" n: int,\n"
" *,\n"
" scale: float = 1.0,\n"
" ) -> int:\n"
" return int(n * scale) + self.cfg.base\n"
)
def test_move_symbol_leave_delegate_forwards_posonly_vararg_kwonly_kwargs(
tmp_path: Path,
) -> None:
"""Every parameter kind is forwarded correctly in the delegate's return call."""
(tmp_path / "src.py").write_text(
"class Mixin:\n" " def compute(self, a, /, b, *args, c, d=3, **kw):\n" " return a\n"
)
(tmp_path / "dst.py").write_text("class Cfg:\n def keep(self):\n return 0\n")
r = Repro("b", "t").move_symbol("compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg")
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == (
"class Mixin:\n"
" def compute(self, a, /, b, *args, c, d=3, **kw):\n"
" return self.cfg.compute(a, b, *args, c=c, d=d, **kw)\n"
)
def test_move_symbol_leave_delegate_survives_paren_in_string_default(
tmp_path: Path,
) -> None:
"""A string default containing '(' must not break the delegate's header scan."""
(tmp_path / "src.py").write_text(
"class Mixin:\n"
" def compute(\n"
" self,\n"
' sep: str = "(",\n'
" n: int = 0,\n"
" ) -> int:\n"
" return n + self.cfg.base\n"
)
(tmp_path / "dst.py").write_text("class Cfg:\n def keep(self):\n return 0\n")
r = Repro("b", "t").move_symbol("compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg")
_apply(r, tmp_path)
src_out = (tmp_path / "src.py").read_text()
compile(src_out, "src.py", "exec")
assert "return self.cfg.compute(sep, n)" in src_out
def test_move_symbol_async_leave_delegate_awaits_the_forwarded_call(
tmp_path: Path,
) -> None:
"""An async method's delegate stub must await the forwarded coroutine."""
(tmp_path / "src.py").write_text(
"class Mixin:\n" " async def compute(self, n):\n" " return n + self.cfg.base\n"
)
(tmp_path / "dst.py").write_text("class Cfg:\n def e(self):\n return 0\n")
r = Repro("b", "t").move_symbol("compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg")
_apply(r, tmp_path)
assert "return await self.cfg.compute(n)" in (tmp_path / "src.py").read_text()
def test_move_symbol_leave_delegate_on_self_annotated_staticmethod(
tmp_path: Path,
) -> None:
"""A de-self'd staticmethod (self: Target) moves into Target; the stub drops the
decorator and the self annotation."""
(tmp_path / "src.py").write_text(
"class Runner:\n"
" @staticmethod\n"
" def work(self: Comp, n: int) -> int:\n"
" return n + self.base\n"
)
(tmp_path / "dst.py").write_text("class Comp:\n def existing(self):\n return 0\n")
r = Repro("b", "t").move_symbol(
"work",
src="src.py",
dst="dst.py",
into_class="Comp",
from_class="Runner",
drop_self_annotation=True,
leave_delegate="comp",
)
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == (
"class Runner:\n" " def work(self, n: int) -> int:\n" " return self.comp.work(n)\n"
)
assert (tmp_path / "dst.py").read_text() == (
"class Comp:\n"
" def existing(self):\n"
" return 0\n"
"\n"
" def work(self, n: int) -> int:\n"
" return n + self.base\n"
)
def test_move_symbol_leave_delegate_keeps_unrelated_self_annotation(
tmp_path: Path,
) -> None:
"""A self annotation naming a class other than the destination survives in the stub."""
(tmp_path / "src.py").write_text(
"class Mixin:\n" " def work(self: Runner, n: int) -> int:\n" " return n + self.base\n"
)
(tmp_path / "dst.py").write_text("class Comp:\n def existing(self):\n return 0\n")
r = Repro("b", "t").move_symbol(
"work",
src="src.py",
dst="dst.py",
into_class="Comp",
from_class="Mixin",
drop_self_annotation=True,
leave_delegate="comp",
)
_apply(r, tmp_path)
assert (tmp_path / "src.py").read_text() == (
"class Mixin:\n" " def work(self: Runner, n: int) -> int:\n" " return self.comp.work(n)\n"
)
def test_move_symbol_delegate_name_forwards_to_the_renamed_collaborator_method(
tmp_path: Path,
) -> None:
"""delegate_name makes the stub call a differently-named method on the collaborator."""
(tmp_path / "src.py").write_text(
"class Mixin:\n" " def compute(self, n: int) -> int:\n" " return n + self.cfg.base\n"
)
(tmp_path / "dst.py").write_text("class Cfg:\n def existing(self):\n return 0\n")
r = Repro("b", "t").move_symbol(
"compute",
src="src.py",
dst="dst.py",
into_class="Cfg",
leave_delegate="cfg",
delegate_name="compute_impl",
)
_apply(r, tmp_path)
assert "return self.cfg.compute_impl(n)" in (tmp_path / "src.py").read_text()
assert "def compute(self, n: int) -> int:" in (tmp_path / "dst.py").read_text()
def test_move_symbol_leave_delegate_on_unannotated_staticmethod_raises(
tmp_path: Path,
) -> None:
"""A staticmethod with no self: Target annotation has no receiver to forward through, so
leave_delegate refuses rather than author a bogus self.<field>.<name>(...) stub."""
(tmp_path / "src.py").write_text(
"class Runner:\n" " @staticmethod\n" " def work(x: int) -> int:\n" " return x + 1\n"
)
(tmp_path / "dst.py").write_text("class Comp:\n def existing(self):\n return 0\n")
r = Repro("b", "t").move_symbol(
"work",
src="src.py",
dst="dst.py",
into_class="Comp",
leave_delegate="comp",
)
with pytest.raises(AssertionError):
_apply(r, tmp_path)
@@ -0,0 +1,126 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
# --- remove_import -------------------------------------------------------------
def test_remove_import_scoped_leaves_module_level_same_text(tmp_path: Path) -> None:
"""Scoped to a function, it removes the local import but not a same-text module-level
one (e.g. a TYPE_CHECKING guard), and drops the import's trailing blank line."""
(tmp_path / "m.py").write_text(
"from typing import TYPE_CHECKING\n"
"\n"
"if TYPE_CHECKING:\n"
" from pkg.mod import Thing\n"
"\n"
"def caller(self):\n"
" from pkg.mod import Thing\n"
"\n"
" return Thing.go(self.x)\n"
)
r = Repro("b", "t").remove_import("m.py", "from pkg.mod import Thing", in_function="caller")
_apply(r, tmp_path)
out = (tmp_path / "m.py").read_text()
assert out.count("from pkg.mod import Thing") == 1
assert "if TYPE_CHECKING:\n from pkg.mod import Thing" in out
assert "def caller(self):\n return Thing.go(self.x)\n" in out
def test_remove_import_removes_every_occurrence_in_scope(tmp_path: Path) -> None:
"""All matching local imports in the function are removed, not just the first."""
(tmp_path / "m.py").write_text(
"def caller(self):\n"
" from pkg import M\n"
"\n"
" M.a(self.x)\n"
" if cond:\n"
" from pkg import M\n"
"\n"
" M.b(self.y)\n"
)
r = Repro("b", "t").remove_import("m.py", "from pkg import M", in_function="caller")
_apply(r, tmp_path)
assert "from pkg import M" not in (tmp_path / "m.py").read_text()
# --- remove_imported_name ------------------------------------------------------
# --- adversarial audit: import primitives ----------------------------------------
def test_remove_import_unscoped_removes_module_level_import_and_blank(
tmp_path: Path,
) -> None:
"""Without in_function the matching module-level import and its trailing blank go."""
(tmp_path / "m.py").write_text("import os\nfrom pkg import Thing\n\nx = Thing\n")
r = Repro("b", "t").remove_import("m.py", "from pkg import Thing")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "import os\nx = Thing\n"
def test_remove_import_keeps_a_code_line_directly_after_the_import(
tmp_path: Path,
) -> None:
"""Only a blank line after the import is absorbed; a code line stays untouched."""
(tmp_path / "m.py").write_text("import os\nx = 1\n")
r = Repro("b", "t").remove_import("m.py", "import os")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "x = 1\n"
def test_remove_import_asserts_when_text_absent(tmp_path: Path) -> None:
"""Removing an import text that matches nothing fails loudly."""
(tmp_path / "m.py").write_text("import os\n")
r = Repro("b", "t").remove_import("m.py", "from pkg import Q")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_remove_import_asserts_when_scope_function_missing(tmp_path: Path) -> None:
"""Scoping to a function that does not exist fails loudly."""
(tmp_path / "m.py").write_text("def f():\n import os\n")
r = Repro("b", "t").remove_import("m.py", "import os", in_function="nope")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_remove_import_leaves_other_statements_on_a_semicolon_line(
tmp_path: Path,
) -> None:
"""Removing 'import os' from a semicolon-joined line must keep 'import sys'."""
(tmp_path / "m.py").write_text("import os; import sys\nprint(sys.path)\n")
r = Repro("b", "t").remove_import("m.py", "import os")
_apply(r, tmp_path)
out = (tmp_path / "m.py").read_text()
assert "import sys" in out and "print(sys.path)" in out
def test_remove_import_trailing_on_a_semicolon_line_leaves_no_dangling_separator(
tmp_path: Path,
) -> None:
"""Removing the trailing import on a semicolon-joined line drops the dangling ';' too
(a trailing space may remain for the formatter to strip, but the separator is gone).
"""
(tmp_path / "m.py").write_text("import sys; import os\nprint(sys.path)\n")
r = Repro("b", "t").remove_import("m.py", "import os")
_apply(r, tmp_path)
out = (tmp_path / "m.py").read_text()
assert ";" not in out
assert "import os" not in out
assert "import sys" in out and "print(sys.path)" in out
def test_remove_import_does_not_overmatch_a_submodule_import(tmp_path: Path) -> None:
"""Removing 'import os' must not also remove 'import os.path'."""
(tmp_path / "m.py").write_text("import os\nimport os.path\nprint(os.path.sep)\n")
r = Repro("b", "t").remove_import("m.py", "import os")
_apply(r, tmp_path)
assert "import os.path\n" in (tmp_path / "m.py").read_text()
@@ -0,0 +1,128 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
# --- remove_imported_name ------------------------------------------------------
def test_remove_imported_name_drops_one_name_from_a_multi_name_import(
tmp_path: Path,
) -> None:
"""One name is dropped from a `from m import a, b, c`; the others stay on the line."""
(tmp_path / "m.py").write_text("from pkg import a, moved, b\n\nx = a + b\n")
r = Repro("b", "t").remove_imported_name("m.py", module="pkg", name="moved")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "from pkg import a, b\n\nx = a + b\n"
def test_remove_imported_name_drops_whole_statement_when_sole_name(
tmp_path: Path,
) -> None:
"""Dropping the only name removes the whole `from` statement."""
(tmp_path / "m.py").write_text("from pkg import moved\nimport os\n\nx = 1\n")
r = Repro("b", "t").remove_imported_name("m.py", module="pkg", name="moved")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "import os\n\nx = 1\n"
def test_remove_imported_name_drops_a_plain_import_with_module_none(
tmp_path: Path,
) -> None:
"""With module=None a plain `import name` statement is removed."""
(tmp_path / "m.py").write_text("import gc\nimport os\n\nx = 1\n")
r = Repro("b", "t").remove_imported_name("m.py", module=None, name="gc")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "import os\n\nx = 1\n"
def test_remove_imported_name_matches_an_asname(tmp_path: Path) -> None:
"""The alias is matched on both the name and the asname, so `import numpy as np` is found."""
(tmp_path / "m.py").write_text("import numpy as np\nimport os\n\nx = 1\n")
r = Repro("b", "t").remove_imported_name("m.py", module=None, name="numpy", asname="np")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "import os\n\nx = 1\n"
def test_remove_imported_name_asserts_when_absent(tmp_path: Path) -> None:
"""Removing a name that is not imported raises, so a wrong recipe fails loudly."""
(tmp_path / "m.py").write_text("from pkg import a, b\n")
r = Repro("b", "t").remove_imported_name("m.py", module="pkg", name="missing")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
# --- add_import ----------------------------------------------------------------
def test_remove_imported_name_preserves_the_multiline_form(
tmp_path: Path,
) -> None:
"""Pruning a name from an exploded import deletes only that line, so the parens and the
magic trailing comma survive and the formatter keeps it multi-line (a flat rebuild would
collapse an import the target left multi-line)."""
(tmp_path / "m.py").write_text("from pkg import (\n a,\n moved,\n b,\n)\n\nx = a + b\n")
r = Repro("b", "t").remove_imported_name("m.py", module="pkg", name="moved")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "from pkg import (\n a,\n b,\n)\n\nx = a + b\n"
def test_remove_imported_name_multiline_down_to_one_collapses(
tmp_path: Path,
) -> None:
"""Pruning an exploded import down to a single surviving name collapses it to one line:
the formatter does not keep a lone name exploded, so a preserved-multiline form would
not match the target."""
(tmp_path / "m.py").write_text("from pkg import (\n moved,\n a,\n)\n\nx = a\n")
r = Repro("b", "t").remove_imported_name("m.py", module="pkg", name="moved")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "from pkg import a\n\nx = a\n"
def test_remove_imported_name_down_to_one_with_a_comment_stays_exploded(
tmp_path: Path,
) -> None:
"""A lone survivor that carries a comment stays exploded (a rebuild would drop the
comment); only its own line is deleted."""
(tmp_path / "m.py").write_text("from pkg import (\n moved,\n a, # keep me\n)\n\nx = a\n")
r = Repro("b", "t").remove_imported_name("m.py", module="pkg", name="moved")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "from pkg import (\n a, # keep me\n)\n\nx = a\n"
def test_remove_imported_name_matches_a_relative_module(tmp_path: Path) -> None:
"""A relative `from .pkg import` is matched via its level dots."""
(tmp_path / "m.py").write_text("from .pkg import a, moved\n\nx = a\n")
r = Repro("b", "t").remove_imported_name("m.py", module=".pkg", name="moved")
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "from .pkg import a\n\nx = a\n"
def test_remove_imported_name_preserves_comments_in_a_multiline_import(
tmp_path: Path,
) -> None:
"""Comments on surviving lines of a pruned parenthesized import must not vanish."""
(tmp_path / "m.py").write_text(
"from pkg import (\n" " a, # used by frobnicator\n" " moved,\n" " b,\n" ")\n" "\n" "x = a + b\n"
)
r = Repro("b", "t").remove_imported_name("m.py", module="pkg", name="moved")
_apply(r, tmp_path)
assert "# used by frobnicator" in (tmp_path / "m.py").read_text()
def test_remove_imported_name_keep_exploded_holds_a_lone_survivor_multiline(
tmp_path: Path,
) -> None:
"""With keep_exploded, pruning down to a single survivor deletes only the removed line,
so the survivor keeps its magic trailing comma and the import stays multi-line (the
author's choice, which the source cannot reveal). A regenerating impl would collapse.
"""
(tmp_path / "m.py").write_text("from pkg import (\n moved,\n a,\n)\n\nx = a\n")
r = Repro("b", "t").remove_imported_name("m.py", module="pkg", name="moved", keep_exploded=True)
_apply(r, tmp_path)
assert (tmp_path / "m.py").read_text() == "from pkg import (\n a,\n)\n\nx = a\n"
@@ -0,0 +1,53 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from mechanical_refactor_reproduction_utils import Repro
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
# --- repath_import / add_typechecking_import -----------------------------------
def test_repath_import_rewrites_nested_import(tmp_path: Path) -> None:
"""A function-scoped import is repathed in place; the bare call is untouched."""
(tmp_path / "c.py").write_text(
"class K:\n" " def run(self):\n" " from old.mod import foo\n" "\n" " return foo(1)\n"
)
r = Repro("b", "t").repath_import("c.py", old_module="old.mod", new_module="new.mod", name="foo")
_apply(r, tmp_path)
assert (tmp_path / "c.py").read_text() == (
"class K:\n" " def run(self):\n" " from new.mod import foo\n" "\n" " return foo(1)\n"
)
def test_repath_import_leaves_a_module_level_import(tmp_path: Path) -> None:
"""Only nested imports are repathed; a module-level import is left to the sorter."""
(tmp_path / "c.py").write_text("from old.mod import foo\n\n\nx = foo(1)\n")
r = Repro("b", "t").repath_import("c.py", old_module="old.mod", new_module="new.mod", name="foo")
with pytest.raises(AssertionError):
_apply(r, tmp_path)
def test_repath_import_repaths_a_multiline_aliased_nested_import(
tmp_path: Path,
) -> None:
"""A nested multi-line from-import with an alias is repathed on its first line."""
(tmp_path / "c.py").write_text(
"def run():\n" " from old.mod import (\n" " foo as f,\n" " )\n" "\n" " return f(1)\n"
)
r = Repro("b", "t").repath_import("c.py", old_module="old.mod", new_module="new.mod", name="foo")
_apply(r, tmp_path)
assert (tmp_path / "c.py").read_text() == (
"def run():\n" " from new.mod import (\n" " foo as f,\n" " )\n" "\n" " return f(1)\n"
)
def test_repath_import_rewrites_a_relative_nested_import(tmp_path: Path) -> None:
"""A nested `from .mod import` matched by module name must actually be repathed."""
(tmp_path / "c.py").write_text("def run():\n from .mod import foo\n\n return foo(1)\n")
r = Repro("b", "t").repath_import("c.py", old_module="mod", new_module="pkg.mod", name="foo")
_apply(r, tmp_path)
assert "from pkg.mod import foo" in (tmp_path / "c.py").read_text()
@@ -0,0 +1,158 @@
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import mechanical_refactor_reproduction_utils as rr
from mechanical_refactor_reproduction_utils import Repro, verify_mechanical_refactor
from reproduction_testlib import _apply, _commit, _git, _write # noqa: F401
# --- verify_mechanical_refactor ------------------------------------------------
def _silence_precommit(monkeypatch) -> None:
real = rr.exec_command
def fake(cmd: str, cwd=None, check=True):
if cmd.startswith("pre-commit"):
return ""
return real(cmd, cwd=cwd, check=check)
monkeypatch.setattr(rr, "exec_command", fake)
def test_reproduce_passes_when_transform_matches_target(repo: Path, tmp_path: Path, monkeypatch, capsys) -> None:
"""A transform that recreates the target tree reports PASS and does not exit."""
_write(repo, **{"src.py": "line1\nline2\nline3\n"})
base = _commit(repo, "base")
_write(repo, **{"src.py": None, "a.py": "line1\nline2\n", "b.py": "line3\n"})
target = _commit(repo, "split")
def transform(root: Path) -> None:
lines = (root / "src.py").read_text().splitlines(keepends=True)
(root / "a.py").write_text("".join(lines[0:2]))
(root / "b.py").write_text("".join(lines[2:3]))
(root / "src.py").unlink()
rr.git_add_and_commit("split", cwd=str(root))
monkeypatch.chdir(repo)
monkeypatch.setattr(rr.tempfile, "mkdtemp", lambda prefix="": str(tmp_path / "wt"))
_silence_precommit(monkeypatch)
verify_mechanical_refactor(base, target, transform)
assert "PASS" in capsys.readouterr().out
def test_reproduce_exits_when_transform_diverges(repo: Path, tmp_path: Path, monkeypatch) -> None:
"""A transform that produces a different tree fails with a non-zero exit."""
_write(repo, **{"src.py": "line1\nline2\nline3\n"})
base = _commit(repo, "base")
_write(repo, **{"src.py": None, "a.py": "line1\nline2\n", "b.py": "line3\n"})
target = _commit(repo, "split")
def wrong_transform(root: Path) -> None:
(root / "a.py").write_text("WRONG\n")
(root / "b.py").write_text("line3\n")
(root / "src.py").unlink()
rr.git_add_and_commit("split", cwd=str(root))
monkeypatch.chdir(repo)
monkeypatch.setattr(rr.tempfile, "mkdtemp", lambda prefix="": str(tmp_path / "wt"))
_silence_precommit(monkeypatch)
with pytest.raises(SystemExit):
verify_mechanical_refactor(base, target, wrong_transform)
def test_reproduce_creates_verify_branch_on_pass(repo: Path, tmp_path: Path, monkeypatch, capsys) -> None:
"""A PASS run leaves a verify-mechanical-<base[:8]> branch in the repo."""
_write(repo, **{"src.py": "line1\nline2\nline3\n"})
base = _commit(repo, "base")
_write(repo, **{"src.py": None, "a.py": "line1\nline2\n", "b.py": "line3\n"})
target = _commit(repo, "split")
def transform(root: Path) -> None:
lines = (root / "src.py").read_text().splitlines(keepends=True)
(root / "a.py").write_text("".join(lines[0:2]))
(root / "b.py").write_text("".join(lines[2:3]))
(root / "src.py").unlink()
rr.git_add_and_commit("split", cwd=str(root))
monkeypatch.chdir(repo)
monkeypatch.setattr(rr.tempfile, "mkdtemp", lambda prefix="": str(tmp_path / "wt"))
_silence_precommit(monkeypatch)
verify_mechanical_refactor(base, target, transform)
assert "PASS" in capsys.readouterr().out
branch = f"verify-mechanical-{base[:8]}"
assert _git(repo, "branch", "--list", branch).endswith(branch)
def _precommit_writes_file(monkeypatch, filename: str, contents: str) -> None:
real = rr.exec_command
def fake(cmd: str, cwd=None, check=True):
if cmd.startswith("pre-commit"):
(Path(cwd) / filename).write_text(contents)
return ""
return real(cmd, cwd=cwd, check=check)
monkeypatch.setattr(rr, "exec_command", fake)
def test_reproduce_commits_pre_commit_fixes_when_tree_left_dirty(
repo: Path, tmp_path: Path, monkeypatch, capsys
) -> None:
"""When pre-commit reformats and leaves the tree dirty, a 'pre-commit fixes' commit
is created on top of the transform commit."""
_write(repo, **{"src.py": "hello\n"})
base = _commit(repo, "base")
_write(repo, **{"src.py": "hello world\n", "formatted.py": "auto\n"})
target = _commit(repo, "edit")
def transform(root: Path) -> None:
(root / "src.py").write_text("hello world\n")
rr.git_add_and_commit("transform", cwd=str(root))
monkeypatch.chdir(repo)
monkeypatch.setattr(rr.tempfile, "mkdtemp", lambda prefix="": str(tmp_path / "wt"))
_precommit_writes_file(monkeypatch, "formatted.py", "auto\n")
verify_mechanical_refactor(base, target, transform)
assert "PASS" in capsys.readouterr().out
branch = f"verify-mechanical-{base[:8]}"
subjects = _git(repo, "log", "--format=%s", "-2", branch).splitlines()
assert subjects == ["pre-commit fixes", "transform"]
# --- Repro.run end-to-end ------------------------------------------------------
def test_repro_run_passes_on_a_faithful_call_site_lowering(repo: Path, monkeypatch, capsys) -> None:
"""End-to-end: a lowering reproduces the commit byte-for-byte (pre-commit stubbed)."""
_write(repo, **{"c.py": "r = Old.foo(self.n, 5)\n"})
base = _commit(repo, "base")
_write(repo, **{"c.py": "r = self.n.foo(5)\n"})
target = _commit(repo, "lower the call site")
monkeypatch.chdir(repo)
_silence_precommit(monkeypatch)
diff = Repro(base, target).lower_call_sites("foo", "Old", paths=["c.py"]).run()
assert diff == ""
assert "PASS" in capsys.readouterr().out
def test_repro_run_reports_residual_when_a_change_is_bundled(repo: Path, monkeypatch, capsys) -> None:
"""A bundled non-relocation change surfaces as a non-empty residual diff."""
_write(repo, **{"c.py": "r = Old.foo(self.n, 5)\nUNRELATED = 1\n"})
base = _commit(repo, "base")
_write(repo, **{"c.py": "r = self.n.foo(5)\nUNRELATED = 2\n"})
target = _commit(repo, "lower the call AND change a constant")
monkeypatch.chdir(repo)
_silence_precommit(monkeypatch)
diff = Repro(base, target).lower_call_sites("foo", "Old", paths=["c.py"]).run()
assert "UNRELATED" in diff
assert "RESIDUAL" in capsys.readouterr().out
@@ -0,0 +1,153 @@
# Reproduction CLI — chain verification specification (source of truth)
## 1. Scope
- Source of truth for `scripts/mechanical_refactor_reproduction_cli.py`: the
**verified-chain property** (§2), the CLI contract (§3), the report (§4), and the exit
codes (§5).
- The single-commit clean-move property and the proof scripts themselves are specified in
`spec-reproduction-utils.md`; this file only says how a whole chain of commits is
checked against a folder of such proofs.
- The CLI, its tests, and the guides defer to this file; on any disagreement, this file
wins.
## 2. The property — a "verified chain"
> A branch is a **verified chain** over a base iff every commit in `base..branch` is
> **classified** (§2.1) and every `mechanical_provable` commit has exactly one **proof**
> in the proof folder whose run **PASSes** (§2.2).
### 2.1 Classification — the word rule
- Every commit message must contain **exactly one** of the two words:
- `mechanical_provable` — the commit claims to be a machine-provable relocation;
- `non_mechanical_provable` — the commit declares that **nothing in it** is
expressible as the whitelisted relocations of `spec-reproduction-utils.md` §2 — it
is the minimal unprovable residue, left to human review.
- The declaration is an assertion, not an opt-out: labeling provable content
`non_mechanical_provable` to dodge the verifier **violates the chain property**, even
where no machine check catches it. A provable part hiding inside a semantic commit
belongs in its own `mechanical_provable` commit with a proof
(`guide-split.md` §2.2).
- The rest of the message format is unconstrained **by the machine rule**: the word may
appear anywhere in the subject or body, in any surrounding syntax. The authoring
contract additionally fixes the subject format
(`<group-id>(<commit-id>,<kind>): <message>`, `guide-split.md` §1.1), which satisfies
this rule by construction; the verifier deliberately checks only the word, so a chain
from a different convention still verifies.
- A word counts only standalone: delimited by a non-`[0-9A-Za-z_]` character or the
message boundary, lowercase, so `non_mechanical_provable` never also counts as the bare
word, and `xmechanical_provable` counts as neither.
- Repeating the same word is fine; the rule is about **which** of the two is declared:
- neither word present → `UNCLASSIFIED`;
- both words present → `AMBIGUOUS_KIND`.
### 2.2 The proof obligation
- Each `mechanical_provable` commit must resolve to exactly one proof script (§3.3);
none is `MISSING_PROOF`, several is `AMBIGUOUS_PROOF`.
- The proof must run to a PASS (§3.4): the commit reproduces byte-for-byte from its
parent (`spec-reproduction-utils.md` §4). Anything else is `FAIL`.
- A `non_mechanical_provable` commit has no machine obligation; its verdict is
`HUMAN_REVIEW` — the report marks it for eyes, never certifies it. Whether its
declaration is honest is the reviewer's duty to check (`guide-verify-proof.md` §1).
- The chain verdict is PASS iff every commit's verdict is `PASS` or `HUMAN_REVIEW`.
## 3. The CLI contract
### 3.1 Invocation
```bash
python3 .claude/skills/mechanical-refactor-verify/scripts/mechanical_refactor_reproduction_cli.py \
--base <base-commit> --branch <pr-branch-name> --proof path/to/proof/folder
```
- `--base` / `--branch`: any commit-ish; both must resolve, `base` must be an ancestor of
`branch`.
- `--proof`: the proof folder (must exist) — typically the generator's `--out` product
(`guide-construct-proof.md` §1.2).
- `--repo-root DIR`: run against that repo instead of the cwd's.
- `--report PATH`: write the report there instead of `<proof>/chain_report.md`.
- `--jobs N`: run up to N proofs concurrently (default 3).
- `--skip-passed`: reuse this machine's own earlier PASS verdicts (§3.5).
### 3.2 The chain
- The commits are `git rev-list --reverse base..branch`, i.e. the whole chain in order.
- The chain must be **linear**: a merge commit anywhere in it is a setup error — per-commit
proofs are meaningless across a merge.
- An empty range is a setup error, not a trivially-green chain.
### 3.3 Proof resolution
- A commit's proof is a `<sha-prefix>.py` whose stem is lowercase hex, at least 7
characters, and a prefix of the commit's full sha.
- Searched locations, in order, both always considered: `<proof>/repro_scripts/` (the
generator layout) and `<proof>/` flat (the gist layout,
`guide-construct-proof.md` §1.3.1).
- Proofs are keyed by current shas: after a rebase the shas change, so the proofs must be
regenerated for the rebased chain.
### 3.4 Proof execution and the PASS criterion
- Each proof runs as `python3 <script>` with the repo root as cwd (the run resolves the
repo from the cwd, `guide-verify-proof.md` §2.1).
- A proof PASSes iff **both**: exit code 0, **and** the arbiter's `PASS:` verdict line on
stdout. Requiring the line keeps an old-style script that exits 0 while printing a
residual from false-passing; requiring the exit code keeps a crash before any verdict
from passing.
- Proofs run **concurrently**, up to `--jobs` at a time (default 3). This is safe because
each proof works in its own throwaway worktree with a unique branch name and never
touches the checked-out tree; per-proof verdicts are independent. Classification and
proof resolution stay sequential (they are cheap), and the report keeps chain order
regardless of completion order. A completion line (`sha PASS/FAIL`) is printed as each
proof finishes, so a long chain shows progress.
### 3.5 The passed-proof cache (`--skip-passed`)
- Purpose: incremental re-verification. Re-running a long chain repeats work for proofs
whose commit and proof did not change; those earlier PASSes can be reused.
- **What is recorded.** Every run (flag or not) records each proof that PASSed into the
cache; a FAIL is **never** recorded. An entry's key is the triple:
- the commit's **full sha** (a rebase changes the sha, so a rebased commit never
hits);
- the **sha256 of the proof script's bytes**;
- the **sha256 of the `mechanical_refactor_reproduction_utils.py` bytes** sitting
next to the script or one level up (the script's only dependency; `""` when
absent) — an edited engine invalidates the cache.
- **What is skipped.** Only with `--skip-passed`, and only on an exact triple match, is a
pending proof skipped: its verdict is `PASS`, marked as reused (a
`proof <sha> PASS (cached)` progress line, and a reused count in the report). Any
mismatch — different sha, edited script, edited utils, no entry — runs the proof
normally.
- **Where the cache lives — and why that is trust-safe.** The cache file
(`mechanical_refactor_passed_proofs.json`) sits in the repo's **git common dir**
(`git rev-parse --git-common-dir`), shared across that repo's worktrees. It is
machine-local state: it never travels with the proof folder, a gist, or the PR, so
`--skip-passed` can only ever reuse verdicts **this machine's own runs** produced —
the do-not-trust-the-PR rule (`guide-verify-proof.md` §0) is not weakened.
- A missing, corrupt, or unreadable cache file is treated as empty; the cache is
best-effort infrastructure and must never fail the chain walk.
## 4. The report
- The full report is markdown, printed to stdout **and** written to the report path
(§3.1), so the folder stays self-describing.
- It contains:
- the resolved base / branch / proof folder and the **chain verdict**;
- the commit counts per kind and the proof PASS count (plus, when any proof was
skipped via `--skip-passed`, the reused count);
- one table row per commit, in chain order: sha, kind, verdict, subject;
- a **Failure details** section with one entry per non-ok commit — the missing-proof
search locations, the classification rule broken, or the failing proof's output
tail.
- Verdict vocabulary: `PASS`, `HUMAN_REVIEW`, `FAIL`, `MISSING_PROOF`,
`AMBIGUOUS_PROOF`, `UNCLASSIFIED`, `AMBIGUOUS_KIND` — the first two are the only ok
verdicts.
## 5. Exit codes
- `0` — the chain verifies (§2).
- `1` — the chain was walked but at least one commit does not verify.
- `2` — setup error: unresolvable ref, base not an ancestor, empty range, merge commit in
the chain, or a missing proof folder. Nothing was certified either way.
@@ -0,0 +1,202 @@
# Reproduction utils — specification (source of truth)
## 1. Scope
- Source of truth for `scripts/mechanical_refactor_reproduction_utils.py`: the
**clean-move property** its primitives implement (§2), each primitive's contract (§3),
and the byte-diff arbiter's semantics (§4).
- The module, its tests, and the guides defer to this file; on any disagreement, this
file wins.
- Elsewhere: commit splitting → `guide-split.md`; producing a proof →
`guide-construct-proof.md`; reading one → `guide-verify-proof.md`.
## 2. The property — a "clean move"
> A commit is a **clean move** iff every change it makes is code **relocated in the same
> order** — allowing one **uniform indentation shift** of the whole block — plus a small
> fixed set of **move artifacts**, and nothing else.
- Equivalently: the commit is reproducible by composing only the primitives of §3.
- The whitelist (§2.1) is exactly what they do; the not-allowed list (§2.2) is what they
refuse, so it surfaces as a residual diff.
### 2.1 Allowed — the whole whitelist
- A line **relocated in order**, modulo one **uniform** leading-indentation shift of the
whole block.
- **Defs/classes gathered from scattered positions** into a **new module**, each cut
verbatim, assembled under an **audited authored header**:
- the byte diff certifies the bodies; the header is reproduced from the target;
- the header audit accepts only: imports, a docstring, a TYPE_CHECKING import block,
a `logging.getLogger(__name__)` logger, an unparse-equivalent copy of an assignment
actually deleted from the source (`drop_assigns`), or an unparse-equivalent copy of a
module constant that **survives verbatim in the source** — re-derived boilerplate such
as `_is_hip = is_hip()`, provably not fiction because the same statement remains in the
source;
- every dropped assignment must reappear in the header — anything else raises instead
of certifying.
- The **body of an extracted function** — an inline block relocated verbatim into a new
def; the `def` signature, an optional `return`, and the replacing `call` are authored.
Faithful **only** when the body moves unchanged; a de-self, control-flow restructure, or
bookkeeping consolidation is semantic and goes in its own commit first.
- **Import statements** — added, removed, or repathed; single-line or parenthesised.
Realised directly from the target (a wholly new module's statement verbatim, wrapping
preserved); a new-module move may add `from __future__ import annotations`.
- A one-sided **`@staticmethod` / `@classmethod`** — method ↔ free function.
- A **`self` type annotation dropped** from the moved definition — relocating
`@staticmethod def foo(self: Target)` into `Target` as `def foo(self)`.
- A **call-site requalification** — `Owner.foo(x)` → `foo(x)`: same symbol, same argument
bytes, only the qualifier dropped. (An `Old.foo(x)` → `New.foo(x)` owner swap is not a
primitive; it surfaces as a residual.)
- A **call-site lowering** — `Owner.method(receiver, rest)` → `receiver.method(rest)`:
the receiver moves out of the argument list.
- **Deleting a source file the relocation emptied** — nothing left beyond a docstring,
imports, or a `TYPE_CHECKING` block (`delete_file` refuses anything else).
- **Blank-line changes** — ignored (§2.3).
### 2.2 Not allowed — the commit is **not** a clean move
- A **reorder** of lines within the moved block.
- A **statement-level reorder** that relocates no definition — it changes evaluation
order: a reshape a human must confirm, not a certifiable relocation.
- A **non-uniform** indentation change — it can change Python semantics.
- A **trailing-whitespace** change, an internal-whitespace change, or a **line
merge/split**.
- A **changed argument** in an otherwise-requalified call.
- A **call rewrite for a symbol that did not move** in this commit.
- A **signature change** other than dropping the `self` annotation.
- A **rename** of the moved symbol (even a privacy flip `_foo` → `foo`).
- **Scaffolding or a constant authored into an existing module** — a logger, a module
constant, a `TYPE_CHECKING` guard, a re-derived `_flag = compute_flag()`. (A *new*
module's header is authored from the target, §2.1; an existing module's body is not a
place to author fresh code. A constant *relocated* from the source is not authored —
`move_assign` certifies it.)
- A **changed body in an extracted function** — de-self, control-flow restructure, or a
folded-in bookkeeping change: a semantic rewrite, not a relocation.
- Reshape work (rename, fresh scaffolding, statement reorder, changed extraction body)
belongs in the prepare/postpare phases of `guide-split.md`.
- The proof reports it as a residual — never certifies it.
### 2.3 Blank lines are ignored
- A blank line never changes Python behavior; PEP 8 separator blanks legitimately collapse
on relocation.
- The formatter normalises both the reproduced and target sides, so a blank-line-only
difference cannot reach the byte diff.
- Assumption: the **target commit is itself pre-commit-clean** (true for any commit that
passed this repo's hooks); a target that skipped the formatter can show blank-line
residuals.
## 3. The faithful relocation primitives
- Each primitive does only a relocation-faithful edit — AST-located, spliced as original
source text, never regenerated.
- Therefore a byte match after the formatter certifies the commit is *exactly* that
relocation.
- `move_symbol(name, *, src, dst, into_class, from_class, dedent, drop_self_annotation,
before, after, leave_delegate, delegate_name)`:
- cuts a `def` or a whole `class` (with its methods) with its decorators; drops its own
`@staticmethod`/`@classmethod`;
- shifts indentation uniformly (negative `dedent` indents into a class);
- pastes at a class end, at module level, above the named sibling `before`, or
immediately below the top-level symbol `after` (a sibling def/class or a module-level
assignment target — the anchor for landing a def just above a following
`if TYPE_CHECKING:` guard, which has no nameable anchor of its own); `before` and
`after` are mutually exclusive;
- same-named defs need `from_class`; an ambiguous name or missing anchor raises;
- `leave_delegate` **authors** a forwarding stub in the source (original header + one
`return self.<attr>.<name>(...)`, `await`ed for async) — audit it like any header.
- `extract_to_new_module(src, dst, *, symbols, future_import)`:
- cuts the contiguous source tail: the moved defs/classes plus leading scaffolding
(imports, TYPE_CHECKING guards, name-target assignments only);
- an executable trailing statement stops the cut;
- prepends `from __future__ import annotations` when the move adds it.
- `extract_symbols_to_new_module(src, dst, *, symbols, header, order, drop_assigns)`:
- cuts the named defs/classes from **scattered** positions; assembles the new module
under the audited `header` (§2.1);
- `drop_assigns` deletes a relocated module-level constant from the source; a chained
`A = B = 1` keeps the surviving bindings;
- the header audit also accepts an unparse-equivalent copy of a module constant that
**survives** in the source (re-derived boilerplate, e.g. `_is_hip = is_hip()` kept in
both modules) — provable because the same statement remains in the source.
- `extract_function(src, dst, *, name, signature, body, body_indent, call, return_text,
before, into_class)`:
- cuts an inline `body` verbatim (must match at a line boundary);
- re-indents under the authored `signature` — multi-line string interiors keep their
exact bytes;
- replaces the block with the authored `call`.
- `move_assign(name, *, src, dst, before)` — cuts the module-level assignment binding
`name` from `src` verbatim and pastes it at module level in `dst` (above the named
sibling `before`, else after the trailing import) — a module constant relocated
together with the code that reads it.
- `lower_call_sites(name, owner, *, paths)` — `Owner.m(receiver, rest)` →
`receiver.m(rest)` by splicing the original argument bytes (literal spelling, comments,
magic trailing comma survive); nested matching calls are all rewritten.
- `requalify_call_sites(name, owner, *, paths)` — `Owner.m(args)` → `m(args)`; only the
qualifier span changes.
- `route_call_sites_through_field(name, *, field, paths, owner)` — `recv.m(args)` →
`recv.field.m(args)` when `m` moved onto a collaborator reached via `self.field`; the
call-side dual of `move_symbol(leave_delegate=...)`. A call already routed through `field`
is skipped so the pass converges; `owner` restricts to one exact receiver.
- `remove_import(rel, import_text, *, in_function)` — function-scoped or module-level;
whole-statement match with token boundaries (`import os` cannot hit `import os.path`);
removes exactly the matched import even on a semicolon-joined line.
- `remove_imported_name(rel, *, module, name, asname)` — drops one name from a
`from m import a, b` (or a plain `import x`), realising a lost import directly (this
repo's ruff has no F811). A name on its own line in an exploded, parenthesized import is
deleted in place when **2+ names survive** (or the import carries comments): the parens,
the magic trailing comma, and the comments are preserved and the formatter leaves it
multi-line — a flat rebuild would drop the magic comma and collapse an import the target
left multi-line. A **lone** surviving name with no comments collapses to a single line
(the formatter does not keep one name exploded) **by default**; pass `keep_exploded=True`
when the target left the sole survivor exploded (its magic comma preserved) — the choice
is the commit author's and cannot be inferred from the source. A name sharing a line (a
flat single-line import) is always rebuilt. Dropping the sole name removes the whole
statement.
- `add_imported_name(rel, *, module, name, asname)` — the dual of `remove_imported_name`:
adds one name to an existing `from module import a, b`. Use it (over `add_import`) when the
target extends an existing line rather than adding a fresh statement — the sorter will not
merge a new statement across an intervening non-import (e.g. a module-level assignment
between two import blocks). An import carrying comments is refused (a rebuild would drop
them); a name already present fails loudly.
- `add_import(rel, import_stmt, *, after)` — the import sorter places it; with no existing
imports it lands below the module docstring. `after=<substr>` inserts it immediately below
the top-level import statement whose text contains the substring — needed when a statement
splits the imports into separate isort sections (e.g. `_is_hip = is_hip()` between two
blocks) and the default (after the last import) would land in the wrong block; a substring
matching no top-level import raises.
- `add_typechecking_import(rel, import_stmt)` — appends inside the destination's
`if TYPE_CHECKING:` block (creating the block after the trailing module import when
absent); the sorter orders it. A lone `pass` placeholder (the block's
only statement) is dropped, since populating an empty block makes its placeholder redundant.
- `repath_import(rel, *, old_module, new_module, name)` — repaths a function-scoped
`from old import … name …` (relative imports included) in place; module-level repaths
fall out of add/remove + the sorter.
- `delete_file(path)` — deletes a source module the relocation emptied; refuses anything
beyond a docstring, imports, a `TYPE_CHECKING` block, or a bare module `logger`.
Cross-cutting guarantees:
- CRLF sources round-trip byte-for-byte; synthesized lines follow the file's newline
style.
- Column arithmetic is UTF-8-byte-accurate; non-ASCII text does not shift a rewrite.
## 4. The arbiter — reproduce and byte-diff
`Repro.run()` (and the lower-level `verify_mechanical_refactor`):
- checks out the base commit in a throwaway worktree;
- replays the recorded primitives;
- runs the repo's pre-commit hooks on the changed files;
- byte-diffs against the target commit — an empty diff is the proof; a non-empty diff is
returned as the residual, exactly what the relocation does not account for.
Properties:
- It runs the **real formatter**: a call split across an `= (` line, or a reflow leaving a
closing bracket as context, reproduces exactly — no diff-shape heuristic to fool.
- Explicit tradeoff: whatever the **pre-commit hooks auto-fix is absorbed** on both sides
(e.g. ruff's F401 removing a now-unused import). A hook-introduced change rides under a
byte match, so the hook set is part of the trusted base.