CHANGELOG.md.
August 20, 2026
Two fixes to what the verified test suite can reach.Neither is exploitable without already being able to hand Refactron a diff or a
test command, which is the normal mode of use. That is the point: the CI gate
this tool is built for verifies untrusted pull requests, so “the attacker
controls the input” is the design assumption.The verified suite no longer inherits your credentials. Refactron runs your
repository’s own test suite, and the diff under verification defines that
suite. It was handed the full parent environment. Reproduced: a test read
REFACTRON_TOKEN, GITHUB_TOKEN, NPM_TOKEN and AWS_SECRET_ACCESS_KEY in
plaintext. This matters most in the deployment Refactron is built for, a CI
gate holding the credentials of the repository being protected.Credentials are now removed from the environment of every spawn that executes
your suite — the common names, plus anything ending in _TOKEN, _SECRET,
_API_KEY, _PASSWORD or _CREDENTIALS. PATH, HOME, VIRTUAL_ENV and
the rest of your toolchain are untouched.This is redaction, not a sandbox. Running verify-diff still runs your tests,
exactly as running them yourself does. SECURITY.md now says so plainly.A diff can no longer name a file outside the repository. The path came from
the diff’s own +++ header and was read with no containment check, so a diff
naming ../../../.ssh/id_rsa caused Refactron to open it. The shadow tree
blocked the resulting write, but the read had already happened, and whether the
patch applied was an oracle for the file’s contents. Containment now runs at
intake, before the first read, and resolves symlinks — a link planted inside
the repository defeated a purely lexical check.Both fixes shipped with a bypass of themselves, found in review of the release
and fixed in it. The coverage probe still ran unredacted, and it executes a
coverage.py planted at your repository root. Recording that because the
pattern is the lesson: a redaction covering two of three spawns is not a
redaction.SECURITY.md was also rewritten. The previous version described the
refactoring product removed in 0.4.0, and it now documents what actually ships,
including a section on what Refactron explicitly does not defend.August 19, 2026
Security: Refactron could write to your working tree. Update immediately.The shadow tree was populated with hardlinks, so every file your diff did
not change shared an inode with your real file. The tests gate runs the suite
as the diff defines it — and a diff may edit
conftest.py, a fixture, or any
test file — so any in-place write from that suite went straight into your
repository. The verdict said SAFE while it happened.No attacker required. Any suite with a snapshot updater, a golden-file
regenerator, or a test that writes a fixture could silently modify the
repository being verified.Affected: every published version, 0.1.0-beta.2 through 0.4.1, on npm and
PyPI. Reachable from the CLI and from the MCP verify_change tool, which
applies no authentication. It contradicts the guarantee the README, the docs and
SECURITY.md all state — “your working tree is never touched” — which was false
for the entire life of the product.Fixed by copying instead of hardlinking, with copy-on-write where the
filesystem supports it. Two related fixes ship with it: shadow-tree containment
could be escaped by a repository symlink pointing outside itself, and a rejected
change left a full copy of your source in the temp directory. Both now have
tests; neither did before, which is how this survived four minor releases.Also fixed: four more false SAFE verdicts in the 0.4.1 narrowing check,
found by an adversarial review of that release. The serious one is that the
command scanner stopped at the first flag it did not recognise and discarded
any filter after it — so pytest -q --durations-min=0.5 tests/test_a.py, using
a stock pytest flag, returned SAFE. Also unittest discover -s tests/unit,
pytest --cov --collect-only, and a bare runtests.py being granted the
classifier’s strongest claim about a file it never opened.The docs no longer state the narrowing check as an absolute: it is a strong
check on the runners and flags Refactron knows, not a guarantee. Run the bare
command if you need certainty.August 19, 2026
Five false
SAFE verdicts fixed, and SAFE now means something narrower.
The report’s shape did not change, so the version number alone will not tell
you that — read this entry.Each of these was reproduced before being fixed. In every case the suite
passed and coverage was measured, and the verdict was still wrong:Two rules changed to close them. A test command that names a subset of
your suite now caps the verdict at
UNPROVEN, and SAFE requires every
changed statement a test could reach to have run, rather than one per file.
Statements coverage.py excludes (# pragma: no cover, if TYPE_CHECKING:)
are subtracted rather than held against you.Every verdict that moves, moves from SAFE toward UNPROVEN. Nothing that was
UNSAFE or UNPROVEN can become SAFE. Exit codes are unchanged, so no CI
pipeline breaks — pipelines that read the verdict string will see more
UNPROVEN.If you acted on a SAFE from 0.4.0 or earlier for a change verified with a
narrowed test command, or one whose coverage.changedStatements showed
covered < total, that verdict claimed more than it proved. Re-verify.Two new report fields: testScope (was the command full, narrowed or
unknown) and engineVersion (which rules produced this verdict —
reportVersion only tells you which shape you hold). unittest joins
pytest, vitest and jest as a recognised runner. Also clears two high
security advisories, lockfile-only.August 6, 2026
Breaking. Refactron is now only a verification layer.
analyze, run,
document, rollback, preflight, init, the interactive TUI and the 20 AST
transforms have been removed from this package, along with blast-radius scoring
and the tier taxonomy. They were the demo of the verification engine, not the
product.If you use any of them, pin refactron@0.3.1. The code is archived with its
full history and is not currently published under any name.verify-diff and the MCP verify_change tool are unchanged. Bare refactron
now prints help and exits 2 instead of opening the TUI, refactron login is a
real command for the first time, and import { verifyDiff } from 'refactron'
resolves for the first time. Full detail in
CHANGELOG.md.August 6, 2026
Two false
SAFE verdicts, found and fixed. Both had the same shape: coverage
measured a different program than the tests gate ran, then reported the
changed lines as covered. A false SAFE is the one defect this product
cannot have, so upgrade rather than pin: npm install -g refactron@0.3.1.A testCmd carrying a leading PYTHONPATH= no longer silently disables
coverage measurement, and a console entry point is now resolved the way the
shell resolves it, or declined. A testCmd naming an entry point that cannot
be resolved reports UNPROVEN rather than SAFE, which is a verdict change
in the safe direction. Full detail in
CHANGELOG.md.August 2, 2026
The verification layer ships. Verify any diff (an AI agent’s, a codemod’s, or
your own) for a
SAFE / UNSAFE / UNPROVEN verdict, and expose the same
gate to agents over MCP. Purely additive: nothing was renamed or removed, and
the transform CLI behaves exactly as it did in 0.2.4. Install with
npm install -g refactron@0.3.0.Addedverify-diffcommand: verify an arbitrary unified diff end to end and print[SAFE|UNSAFE|UNPROVEN] <reason>. Applies the change in an isolated shadow tree, runs the syntax / imports / tests gates, and fuses changed-line coverage into the verdict. Read-only: your working tree is never mutated. (PR #75)refactron-mcp, an MCP server exposingverify_change: a stdio server an AI agent calls before it lands a change; accepts full-fileeditsor aunifiedDiffand returns the same JSON verdict report. Ships as a second bin in the same package. (PR #75)- Three-way verdict fusion:
SAFE(gates pass and the changed lines are exercised),UNSAFE(a gate failed),UNPROVEN(tests pass but the change isn’t proven, or coverage couldn’t be measured). Coverage is Python-only viacoverage.py; a non-Python or mixed diff returnsUNPROVEN, never a falseSAFE. (PR #75) preflightcommand: a coverage-aware SQLAlchemy 1.x to 2.0 migration safety report that classifies eachModel.querysite as safe-to-automate, unproven, or needs-review;--fail-on-unprovengates CI. (PR #74)- A versioned report shape:
reportVersion, pluschangedStatements,filesWithUncovered,testFilesChanged, andflakyTests, so the evidence behind a verdict is legible in--jsonand over MCP rather than implied. (PRs #79, #83, #86)
verify-diff is new here; they are listed
because what a verification tool refuses to claim is the product.- A diff that deleted a file could read
SAFE. Unmodelable operations were silently skipped, so a diff removing a module plus one benign edit passed the gates. Deletions, renames, copies, and binary changes are now refused with exit2. So are anchorless hunks that misrepresent a live file as new, submodule pointer bumps, and non-UTF-8 bases. (PRs #79, #82) - A changed blank line could vouch for a function that never ran. Coverage
attribution walked back to the nearest preceding statement start, so an
executed
defheader covered a body that was never called. Attribution is now exact line-to-statement containment from the Python AST, and a line carrying no code token never marks a file exercised. (PR #86) - A flaky suite became a false
UNSAFErate. The tests gate now computes new failures against the baseline set and retries once on a fresh shadow tree, so a timing flake heals but an idempotency break does not. Flakiness floors the verdict atUNPROVEN: a green that needed a retry is not proof. (PR #83) - The imports gate failed modern Python.
if TYPE_CHECKING:imports, platform-conditional imports, and the repo’s pre-existing breakage were all blamed on the change. The gate is now delta-aware andTYPE_CHECKING-safe, failing only on imports the change introduced or newly broke. (PR #78) - Coverage reported fake zeros. Script-form test commands, quoted
arguments, a directory named
coverage/onsys.path, dynamically compiled code, and pip-installed projects each produced an empty covered set that read as “not exercised”. Each now reports honestly, and an unmeasurable run says coverage could not be determined instead of guessing. (PRs #78, #80, #84) - Removal-only diffs read like a coverage miss. A diff that only deletes
lines now says so: there are no added lines for coverage to attest. The
verdict stays a conservative
UNPROVEN. (PR #81) - CRLF diffs could produce a false
SAFE. Changed-line derivation now normalizes CRLF before the diff, so a change that only differs in line endings no longer mismatches coverage and slips through as covered. (PR #75) - Coverage was unavailable in CI.
coverage.pyis now installed into the samepython3the test suite spawns, so the coverage-based verdict is exercised in the CI runner instead of degrading toUNPROVEN. (PR #74)
- Dependency advisories cleared:
npm auditreports 0, down from 6 (4 high).brace-expansionandfast-urimoved to patched versions in the lockfile, with no change to any declared range. - The PyPI wrapper no longer runs an unpinned global npm install.
pip install refactronused to shell out tonpm install -g refactronon first use, fetching whatever waslatestregardless of the version you pinned. It now detects the CLI and, if missing, prints the exact matching command and exits non-zero. The wrapper is also relicensed to Apache-2.0 to match the rest of the project.
June 17, 2026
Reliability and observability release. Five real fixes, one feature (tier
taxonomy), one license change. No new transforms, no API breakage; every
existing call site keeps working.Added
- Tier taxonomy on every transform (debt / modernization / style).
analyzeoutput groups findings and remediation minutes by tier: the headline “N findings” splits into “57 debt, 102 modernization, 2,569 style” instead of one undifferentiated count. byTierandminutesByTierfields inanalyze --jsonoutput, with the invariantdebt + modernization + style === totalMinutes.- BY TIER section in the boxed TUI analyze output.
- License: MIT → Apache 2.0. Same permissive freedoms; adds an explicit
patent grant from contributors. See
LICENSE,NOTICE, and the FAQ.
run --transforms=allsilently dropped 8 transforms. The CLI’s local list had drifted out of sync with the engine’s canonical order when the v0.2.3 catalog expansion landed. CLI now importsTRANSFORM_ORDERdirectly; drift is pinned by a test. (#48, PR #49)--files=<glob>was ignored on--apply. The glob only narrowed the dry-run preview; the apply path silently rewrote every matching finding. Filter is now applied toplan.changesbefore the split, so both paths honour it. (#50, PR #52)- Documenter broke files with multi-line return-type signatures. On
def f() -> type[Union[…]]:shapes that span multiple lines, the inserter latched onto the first inner line of the type subscript as if it were the body. The walker now tracks bracket depth and skips inline Protocol stubs. (#51, PR #52) applyandrollbackdropped POSIX file modes. Both paths now round-trip mode bits.class_to_dataclassinjected imports beforefrom __future__. New imports now land after the__future__block, preserving PEP 236 ordering.- Silent refusals in four transform sidecars.
pep585_generics,pep604_optional_union,datetime_utc_alias,callback_to_async_awaitnow emit apreconditionrecord on every refusal: no more “detected, but nothing changed” with no explanation. manual_typecheck_to_hintswas the silent sidecar Bug #3 missed. Every refusal path now records why; gating prevents noise from unrelated siblings; the nested-def scan stops at function boundaries. On Ansible: 16 silent files → 0; 4 records → 87 covering all 20 affected files. (#57, PR #58)
manual_typecheck_to_hintsnow records refusals but on Ansible still rewrites 0 of 20 files. Expanding the rewriter to handle docstring + body and dispatcher + fallthrough is tracked as #59.- Eight new transform candidates derived from a deeper Ansible scan are filed as #62 to #69 for v0.3 / v0.4 prioritisation.
May 27, 2026
Ten new deterministic transforms (six for Python, four for TypeScript) roughly
doubling Refactron’s transform coverage. Adds the
pythonVersion config key
so version-gated rewrites can be opted in safely.Added: Pythonsuper_no_args:super(ClassName, self).method(...)→super().method(...). Refuses sibling/parent class names and nested-class shadows to preserve MRO.lru_cache_to_cache:@functools.lru_cache(maxsize=None)→@functools.cache(≥ 3.9); also rewrites thefrom functools import …line.pep585_generics:typing.List/Dict/Tuple/Iterable/ … →list/dict/tuple/collections.abc.Iterable/ … (≥ 3.9, orfrom __future__ import annotations). Refuses files with Pydantic v1 orget_type_hintsto avoid runtime-eval crashes.pep604_optional_union:Optional[X]→X | None;Union[A, B]→A | B(≥ 3.10, orfrom __future__ import annotations).datetime_utc_alias:datetime.timezone.utc→datetime.UTC(≥ 3.11). No__future__override:UTCis a runtime attribute.yield_from_for_loop:for x in y: yield x→yield from y. Refuses insideasync def(a CPython compile-stage SyntaxError LibCST’s parser does not catch).
indexof_to_includes:arr.indexOf(x) !== -1and friends →arr.includes(x). Type-aware via ts-morph (String / Array / ReadonlyArray receivers). Gated on tsconfig target ≥ ES2016.object_assign_to_spread:Object.assign({}, a, b)→{ ...a, ...b }. First arg must be an object literal; refuses spread-element sources. Gated on tsconfig target ≥ ES2018.string_concat_to_template_literal:"…" + x + "…"→`…${x}…`. Refusesany/unknown/ non-primitive operands. Gated on tsconfig target ≥ ES2015.vue_set_delete_to_assignment:Vue.set/this.$set→ direct assignment;Vue.delete/this.$delete→delete obj.k..js/.tsonly;.vueSFC parsing is deferred to v0.4. Refusesdeletein expression context (return-value semantics differ). On Vue 2 codebases this is a semantic change (Vue.setis required for new reactive keys); caveat ships in the suggestion text.
pythonVersion: pin the Python target version ("3.9","3.11", …) for the four version-gated Python transforms. Auto-detected frompyproject.toml’srequires-pythonwhen unset; falls back to refusing version-gated transforms rather than guessing.
- Engine composition: multi-transform composition is now order-stable: when several transforms touch the same file, each emits its own
FileChangecarrying the cumulative content, and the last one per path is what’s written to disk. Fixes a silent-data-loss bug where only the LAST transform’s rewrite survived underrun --apply(PR #38).
May 18, 2026
Quality-of-life release for the analyze → run → document pipeline: boxed CLI
output, a real
rollback command, and a much more efficient document.Added- Bordered table output:
analyzerenders one box per file plus boxed TRANSFORMS / BY TRANSFORM / SUMMARY blocks;run --dry-runmatches, with a CHANGES table and a four-sided diff box per file. rollbackcommand: undo an applied refactor ordocumentrun; journal-based LIFO undo, drift-safe,--all/--force/--dry-run.run --applylive progress: gate-by-gate status and per-file verify/apply detail; batch-first with a per-file fallback when the batch fails.run --applyshort-circuit: exits early when no test runner is detected, instead of silently skipping the test gate.- Full report saved to disk:
analyze/run --dry-runwrite the complete report to.refactron/reports/. documentenrichments: inline comments, a per-run modernization report underdocs/refactron/, and a post-apply syntax re-check.
documentis far more efficient: docstring requests are batched with bounded concurrency and token-aware rate limiting; the LLM call count is nowO(source tokens / batch budget), notO(symbols).
documentproduced zero docstrings on large files: batches were sized by input tokens only, so the combined response overran the completion cap and truncated. Batches are now also capped by response size, and a truncated reply is salvaged entry-by-entry.documentsix-quote docstring bug (""""""…""""""); rate-limited runs that ground on for minutes; report / CHANGELOG paths normalized to forward slashes on Windows.analyze: old-string-format findings now anchor on the operator, not the opening quote;manual_typecheck_to_hintsno longer flags already-annotated parameters; the misleading “Fixable N/N” became an honest auto-fix-candidate count.deprecated_api_requests_to_httpxno longer emits runtime-broken code: it refuses files usingrequestsAPI that is not a safehttpxdrop-in.
May 16, 2026
Patch release: a large-file crash fix and two transform-coverage improvements.Fixed
analyzecrashed on files larger than ~32 KB: tree-sitter’s native binding rejects oversized string input. Parsing now uses the streaming callback-input form; a single unparseable file is skipped rather than aborting the run.var_to_const_letdropped whole files: reassignment checks matched identifiers by text across the entire file. Reference resolution is now scope-correct, and for-loopvar iinitializers are covered.
format_to_fstringnow converts the full printf grammar:%d,%.2f,%x,%o,%e,%g, width/precision specifiers, and%%. Mapping%(name)s, non-literal targets, and dynamic*widths are still conservatively skipped.
May 15, 2026
First public release of the v2.0 deterministic-refactoring rebuild.Added
- Engine: 10 deterministic AST transforms (5 Python via LibCST, 5 TypeScript via ts-morph) with cross-file preconditions.
- 3-gate verification: syntax + imports + tests on a shadow tree, with atomic batch write or rollback.
- Documentation engine: the only LLM-touching component, running only on already-verified diffs; 5 providers (Ollama, Groq, OpenAI, Anthropic, managed backend).
.refactronrc.jsonconfig: cosmiconfig + Ajv schema validation.- Authentication: OAuth device flow with
REFACTRON_TOKENsupport and long-lived API keys. - A Mintlify documentation site and reproducible performance benchmarks under
bench/.