verify-diff takes a change (your AI agent’s, a codemod’s, or your own) and answers one question: did it preserve behavior, and can you prove it?
Refactron applies the diff in an isolated shadow tree, runs the syntax, imports, and test gates against it, checks whether your tests actually exercise the changed statements, and returns one of three verdicts. Your working tree is never touched.
A diff enters, the gates run in an isolated shadow tree, and one of three verdicts comes out.
The verdict, in one line
UNPROVEN is the honest verdict. “Tests pass” is not the same as “this change is proven safe”: if nothing runs the lines you changed, a green suite proves nothing about them. Refactron says so out loud and tells you which test to add. See Verdicts for the full model.
If your project is installed into the environment (an editable pip install -e . counts), your tests may import that copy instead of the one being verified, in which case the run proves nothing about your change. Put the verified tree first on sys.path by prefixing the test command: PYTHONPATH=. python3 -m pytest -q, or PYTHONPATH=src for a src layout. Use module form rather than a bare pytest, which cannot always be measured. See Make sure the tests run the code being verified.
Install
Requires Node.js ≥ 18, plus Python 3.8+ withcoverage.py for the coverage half.
refactron (and refactron-mcp) on your PATH. To skip the install, run npx refactron verify-diff ... instead.
pip install refactron==0.3.0 installs a thin refactron shim that shells out to the npm CLI. It
is not a Node-free path: you still need Node.js ≥ 18 and npm install -g refactron.
When the npm CLI is missing, the shim prints the exact matching install command and exits
non-zero.Build from source (contributors)
node dist/cli/index.js <command>. Substitute that for refactron in every example below.
Authenticate
verify-diff is auth-gated. Log in once on this machine:
7.
Run it
repoRoot defaults to .. The --diff file is a standard unified (git) diff.
UNPROVEN by lack of coverage) the uncovered lines:
Flags
Exit codes
Unsupported operations
v1 verifies content edits. A diff that deletes, renames, or copies a file, or changes a binary file, is refused with exit2 rather than partially verified. This is deliberate: a diff that deleted a module while also making one innocuous edit once verified SAFE, yet applying it broke every import in the package. A partial verdict on the verifiable half must never read as a verdict on the whole diff.
Detection is belt and braces: both the parsed diff and a raw scan of the diff text are checked, so a pure rename or deletion that the diff parser drops entirely is still caught. Full deletion and rename support is planned; until then, verify those changes manually.
Test files touched
When a diff changes files that look like tests (atests/ or test/ path segment, test_*.py, *_test.py, conftest.py, *.test.ts, or *.spec.ts), the human output prints one advisory line and the JSON report carries the list under testFilesChanged:
The JSON report
--json emits a reproducible record of the verdict: the same structure the MCP tool returns:
reportVersion: schema version of this report. The shape below is a public contract; the version tells a consumer that stores reports which shape it is holding.gates: each gate’spassedflag and wall-clock. A failed gate also carries ablockingReason.testFilesChanged: changed files matching test conventions. A note that the diff touched tests, never a verdict input.coverage.tool:coverage.pywhen coverage was assessed,nonewhen it couldn’t be (non-Python diff, or nocoverage.pyinstalled).coverage.uncovered: one entry per unexercised statement, at the statement’s first line, mapped from the changed line by AST containment. A multi-line statement reports once rather than once per wrapped line. Present onSAFEtoo: the per-file rule clearsSAFEon one exercised statement per file, so aSAFEchange can still hold statements no test ran, and hiding them would make the verdict read stronger than it is. An entry marked"excluded": truesits in a# pragma: no coverorif TYPE_CHECKING:block that no test can reach.coverage.changedStatements:{ total, covered }across the whole diff, so you can read the ratio (“12 of 40 changed statements exercised”) rather than only the boolean.coverage.filesWithUncovered: distinct files with at least one uncovered statement, counted before the cap.coverage.inertOnlyFiles/coverage.removalOnlyFiles: changed files with nothing for coverage to attest, because their added lines are all blank/comment lines, or because the file only had lines removed.missingTests: present onUNPROVEN-by-coverage, a concrete hint per uncovered statement, capped at 50.coverage.uncoveredTruncatedandmissingTestsTruncated({ shown, total }) appear whenever a list was capped, so a truncated report never reads as a complete one.
Verify in CI
Runverify-diff on a pull request’s diff and let the exit code gate the merge. UNSAFE returns 1 and fails the job; SAFE and UNPROVEN return 0.
verify-diff as a merge gate: UNSAFE fails the job, SAFE and UNPROVEN pass.
verify-diff has no --fail-on-unproven flag. To fail CI on UNPROVEN, read the verdict field from --json output and set the exit code yourself; the process exits 0 for UNPROVEN by design, so a green suite on untested lines never silently blocks a merge unless you choose to.
Next
Verdicts
The three verdicts, coverage fusion, and the Python-only limitation in depth.
MCP server
Give your AI agent the same gate as a
verify_change tool call.Safety model
How the shadow tree and the three gates work.