# Deterministic code modernization with Factory

> Deterministic code modernization pairs recipe and codemod tools with coding agents. The tool makes the repeatable edits, Droids fix what no recipe covers, and compilers, tests, and a clean re-run verify the result. How Factory runs that loop in every language.

URL: https://factory.com/articles/deterministic-code-modernization
Published: 2026-11-14
Updated: 2026-11-14

Deterministic code modernization uses tools that apply the same transformation to the same input every time. OpenRewrite recipes for Java, clang-tidy fix-its for C++, Rector rules for PHP, and LibCST codemods for Python all work this way. Factory pairs those tools with Droids, its coding agents. The tool performs the bulk of a migration as a reviewable, rerunnable diff. Droids handle the residue the tool leaves behind, and the compiler, test suite, and a second run of the tool decide whether the work is done.

Factory's language support comes from running each ecosystem's own deterministic tools through hooks, skills, headless runs, and Missions, and from Droids writing and testing new recipes when no existing one covers the change. Factory does not ship a proprietary transpiler or a language-specific mode. The language guides near the end of this page show the specific tools and commands for each ecosystem.

## Where recipe tools stop and agents start

A recipe is a program that rewrites a syntax tree. [OpenRewrite](https://docs.openrewrite.org/), for example, applies recipes to a lossless semantic tree so that formatting and comments survive the rewrite. Because the transformation is code, it has properties a reviewer can rely on. Running it twice produces the same result. A reviewer can read the recipe once instead of reading ten thousand changed lines. After a rebase, the team reruns the recipe instead of resolving conflicts by hand.

Recipes stop where their authors' knowledge stops.

A framework upgrade recipe can rename packages and update method calls that have a known mapping. It cannot decide how to replace an API that was removed without a successor, repair a test that relied on old behavior, or choose between two plausible readings of ambiguous code. These leftovers are usually a small share of the changed lines and a large share of the engineering time. They are also the part of a migration that stalls for months, because each one needs someone to read the code, look up the release notes, and make a decision.

Agents have the opposite profile. A Droid can read a compile error, open the migration guide, and make a judgment call in context. Asked to rewrite thousands of files directly, an agent produces a diff that is hard to review and hard to reproduce, because two runs may differ in small ways.

Pairing the two assigns each part of the work to the component that handles it best.

- The deterministic tool performs the mechanical, repeatable bulk and produces one diff that can be regenerated.
- The Droid handles compile errors after the recipe, APIs with no recipe, ambiguous semantic choices, and test repairs.
- The Droid also writes new custom recipes or codemods, with before and after tests, when a pattern repeats. Once merged, that pattern becomes deterministic for every later run.
- Verification stays deterministic, through the compiler, type checker, test suite, characterization tests, and a dry run of the tool that must report zero remaining changes.

## The deterministic code modernization loop

The loop repeats per module or package. The gate after each pass uses the same commands the team already runs in CI, so the build judges the result instead of the agent's own account of its work.

Deterministic tool pass</span><br/><span style='color:#8A8380 !important'>recipe or codemod, committed alone</span>"]
    B["<span style='color:#FAFAFA !important;font-weight:600'>Build and test gate</span><br/><span style='color:#8A8380 !important'>compiler, type checker, test suite</span>"]
    C{"<span style='color:#FAFAFA !important;font-weight:600'>Gate passes?</span>"}
    D["<span style='color:#FAFAFA !important;font-weight:600'>Droid handles residue</span><br/><span style='color:#8A8380 !important'>compile errors, unmapped APIs, tests</span>"]
    E["<span style='color:#FAFAFA !important;font-weight:600'>Deterministic re-run</span><br/><span style='color:#8A8380 !important'>dry run reports zero changes</span>"]
    F["<span style='color:#FAFAFA !important;font-weight:600'>Human review</span><br/><span style='color:#8A8380 !important'>recipe commit and agent commit</span>"]
    A --> B --> C
    C -- no --> D --> B
    C -- yes --> E --> F
`}
/>

The final deterministic step carries the most weight. After the Droid's edits, run the tool again in check or dry-run mode, for example `mvn rewrite:dryRun` with OpenRewrite or `vendor/bin/rector process --dry-run` with Rector. An empty result shows that the agent did not reintroduce a pattern the recipe removes and that the recipe's work is complete. If the run does produce changes, they go back through the gate before review.

## Keep the tool in the loop on every edit

Factory [hooks](https://docs.factory.com/harness/hooks) run shell commands at fixed points in a Droid session. A `PostToolUse` hook runs immediately after a tool completes, so a project can run its formatter, linter, or compiler check after every file edit. When that hook exits with code 2, its stderr goes back to the Droid as corrective feedback. A `PreToolUse` hook that exits with code 2 blocks the tool call, which is useful for protecting generated files.

This project-level configuration, saved as `.factory/hooks.json`, runs a verification script after every file write.

```json
{
  "PostToolUse": [
    {
      "matcher": "Create|Edit|ApplyPatch",
      "hooks": [
        {
          "type": "command",
          "command": "\"$FACTORY_PROJECT_DIR\"/.factory/hooks/verify-edit.sh",
          "timeout": 120
        }
      ]
    }
  ]
}
```

The script is language-specific. In a Java repository it might compile the touched module. In a C++ repository it runs clang-tidy on the edited translation unit. Either way the check is unconditional, because it does not depend on the prompt or on the Droid remembering to run it.

The migration playbook belongs in a [skill](https://docs.factory.com/harness/skills). A skill lives at `.factory/skills/<name>/SKILL.md` and can name the tool to run, the order of passes, the verification commands, and the rule for separating commits. Exact build, test, and lint commands go in [AGENTS.md](https://docs.factory.com/harness/agents-md), which Droid reads before it writes code. With both committed, every interactive session and every CI run follows the same procedure. This example skill applies Ruff's pyupgrade rules to a Python package.

```markdown
---
name: python-upgrade-syntax
description: Upgrade Python syntax with Ruff UP rules, then fix residue. Use when raising the minimum Python version of a package.
---

1. Run `ruff check --select UP --target-version py312 --fix .` and commit the result alone.
2. Run `mypy src` and `pytest`. Fix failures in a separate commit.
3. Do not revert lines from the Ruff commit unless a test requires it.
4. Run `ruff check --select UP --target-version py312 .` again. It must report no violations.
```

## Fan out with Droid Exec and Missions

[Droid Exec](https://docs.factory.com/droid-exec/overview) runs Droid headless as a one-shot command. It is read-only by default, and `--auto low` allows project file edits without package installs or git writes. Factory's [lint autofix recipe](https://docs.factory.com/droid-exec/recipes/lint-autofix) shows the shape modernization work takes at scale. ESLint lists the files that violate a rule, a script runs one `droid exec --auto low` per file in parallel, and ESLint runs again on each file to confirm the fix. Replace ESLint with the compiler, clang-tidy, or Rector and the structure holds. The deterministic tool produces the target list and the verdict, and the Droid works only on the residue in between.

Migrations that span many modules or repositories fit [Missions](https://docs.factory.com/missions/overview). A Mission starts with a plan of features and milestones, and validation workers check each milestone before the next one begins. The Missions documentation lists brownfield migrations that preserve existing behavior as a primary use case. Hooks, skills, MCP integrations, and custom droids carry over into Missions, so the same deterministic checks run inside the orchestrated work.

These commands start each mode from the Droid CLI or a CI job.

<div className="not-prose my-6 flex flex-col gap-3 [&_button]:!w-auto">
  <div className="grid grid-cols-1 gap-y-5 md:grid-cols-[max-content_1fr] md:gap-x-4">
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Plan a migration.</strong> Breaks multi-module work into features and milestones with validation.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Capture the playbook.</strong> Turns the workflow from the current session into a reusable skill.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Fix residue headless.</strong> Runs one scoped repair that a script can verify afterward.</span>
  </div>
</div>

## Review the mechanical diff and the agent diff separately

Keep recipe output and agent edits in separate commits. A reviewer can check the recipe commit by reading the recipe and regenerating the diff, which should match exactly. The agent commit is smaller and gets line-by-line review. Mixing the two forces reviewers to inspect every mechanical change with the care that only the judgment calls need.

A successful build shows that the code is well-formed. It does not show that the code behaves the same.

Behavior needs its own evidence. Michael Feathers introduced the term characterization test in *Working Effectively with Legacy Code* for a test that records what the current system does, so a changed system can be held to it. For batch systems that means golden output files from representative inputs. For services it means recorded requests and responses. Write these before the migration starts, and treat any difference as a finding to explain rather than a test to update.

Roles can be split further with a [custom droid](https://docs.factory.com/harness/subagents). A subagent with a read-only tool policy can review the agent commit against the upstream migration guide without being able to edit it. Its single return value lists what the reviewer should look at.

Results at scale depend on scoping as much as tooling. [Comarch's published case study](https://factory.com/case-studies/comarch) reports an XSLT modernization completed in 45 days against a 10,000 person-day estimate, and a database migration estimated at 45 days that a Factory mission completed in six hours. These are separate projects compared with estimates, not a controlled benchmark.

## Language guides

Each guide below names the deterministic tools for one ecosystem, the exact commands to run them, and where Droids take over.

- [COBOL and mainframe modernization](https://factory.com/articles/cobol-modernization-deterministic)
- [Java modernization with OpenRewrite](https://factory.com/articles/java-modernization-openrewrite)
- [C# and .NET modernization with Roslyn](https://factory.com/articles/dotnet-modernization-roslyn)
- [Python modernization with codemods](https://factory.com/articles/python-modernization-codemods)
- [JavaScript and TypeScript codemods](https://factory.com/articles/javascript-typescript-codemods)
- [C and C++ modernization with clang-tidy](https://factory.com/articles/cpp-modernization-clang-tidy)
- [Go modernization with go fix](https://factory.com/articles/go-modernization-gofix)
- [PHP modernization with Rector](https://factory.com/articles/php-modernization-rector)
- [Ruby and Rails upgrades with RuboCop](https://factory.com/articles/ruby-rails-upgrade-rubocop)

## Frequently asked questions

### What is deterministic code modernization?

Deterministic code modernization uses tools that apply the same transformation to the same input every time, such as OpenRewrite recipes, clang-tidy fix-its, Rector rules, or LibCST codemods. The output is a reviewable diff that can be regenerated after a rebase.

### Does Factory ship a built-in OpenRewrite integration or a language mode?

No. Factory runs each ecosystem's own deterministic tools through hooks, skills, AGENTS.md, Droid Exec, and Missions. Droids also write and test new recipes or codemods when no existing one covers a change.

### What does the Droid do if the recipe already made most of the changes?

It handles the residue. That includes compile errors after the recipe, removed APIs with no recipe, ambiguous semantic choices, broken tests, and new custom recipes with before and after tests for patterns that repeat.

### How is an agent-assisted migration verified?

With deterministic checks. The compiler, type checker, and test suite must pass, characterization tests must match recorded behavior, and a dry run of the recipe or codemod must report zero remaining changes.

### Which languages does this approach cover?

Any language with deterministic refactoring tools. Factory has guides for COBOL, Java, C#, Python, JavaScript and TypeScript, C and C++, Go, PHP, and Ruby on Rails.

## Further reading

- [Enterprise software modernization beyond the demo](https://factory.com/articles/enterprise-software-modernization)
- [Legacy modernization with private AI agents](https://factory.com/articles/legacy-modernization-private-ai)
- [How to run a runtime upgrade with coding agents](https://factory.com/articles/coding-agent-runtime-upgrades)
- [Missions planning and validation](https://docs.factory.com/missions/planning)
