A code health score tells you a file is in
trouble. Refactoring intelligence names the specific fix. Every other tool
stops at the score, or prints the same static sentence for every god class in
every repo. repowise emits one structured plan per opportunity: split
GraphBuilder into these three cohesive groups, move resolve_call to the
resolvers class where its calls actually land, break the pipeline <-> update
import cycle by inverting this one edge, decompose this 900-line module into
these four files and rewrite the imports in the six that depend on it. Each is
computed deterministically from the same graph, class model, and git data the
score is built on.
repowise health --refactoring-targets # the stored queue, in serving order
repowise health --refactoring-targets --format json
repowise health --plan refac... # one plan as the recipe an agent appliesIt runs inside the health pass (init / update), reusing data already
computed: no re-parse, no LLM, no network, inside the same sub-30-second
budget. Repowise never applies a plan to your files: an agent or a person does.
The LLM layer (code generation) is a separate step, off by default, that runs
only on explicit request (below).
The six detectors
Each detector is a self-contained module registered into a registry (adding a refactoring type is a new file plus a registry entry, like the marker registry). A detector degrades to "no suggestion" on any missing signal, never a wrong one, and produces stable-sorted, deterministic output.
| Type | What it names | Detection (deterministic) |
|---|---|---|
| Extract Class | The cohesion groups an incohesive / god class should split into: the exact methods + fields per group. | LCOM4 union-find components (each disconnected component is a candidate class), with the god-class shape confirmed via Lanza-Marinescu (WMC = Σ McCabe, TCC). |
| Extract Helper | A clone's exact occurrences and where the shared helper belongs. | Rabin-Karp clone pairs (line ranges, token count, co-change). The extraction site is the community centroid of the involved files; transitive clones (A<->B, B<->C) are clustered into one suggestion, not pairwise nags. |
| Move Method | A feature-envy method and the class it actually belongs to. | The method's entity set (fields / methods it touches, class-qualified) is built from the call graph; Jaccard distance to each class. Fires only when a foreign class is clearly nearer than its own. Never targets a class the method only instantiates (that class's producer isn't envy) or an ancestor of the method's own class (calling an inherited method is calling your own member). |
| Break Cycle | The minimal set of import edges to invert to break a dependency cycle. | A strongly-connected component in the import graph, then a greedy minimum feedback arc set (MFAS) over the real edges picks the smallest cut. |
| Split File | The cohesive files an oversized module should decompose into: which top-level symbols move to each new file, plus the import edits in every dependent. | Community detection (Leiden, Louvain fallback) over a weighted intra-file symbol graph (direct calls, shared local helpers, shared foreign modules); emits only when the partition's modularity clears a decomposability gate. The file-level analog of Extract Class. |
| Extract Method | The exact line span to lift out of an oversized/complex method, with the helper's inferred signature: the parameters it needs (IN) and the value it must return (OUT). | Intra-procedural dataflow (CFG + def/use + reaching definitions) over methods a large_method / brain_method / complex_method finding already flagged. Only single-exit, statement-boundary spans that remove real complexity qualify; IN/OUT comes from liveness over the def/use facts, so the suggested helper is behavior-preserving by construction. |
The algorithms are derived from public academic literature (Fokaefs-Tsantalis HAC for class splitting, Bavota feature-envy distance, MFAS for cycle breaking, Newman-Girvan modularity for module decomposition), not from any product.
A seventh, narrower category rides the same ranked list: performance_fix
plans, generated from performance-risk
findings, not from their own graph detector. Each one only emits when
the underlying marker's evidence supports a specific, named strategy (batch
the I/O, parallelize independent awaits, buffer the string accumulation); a
finding with real evidence but no safely describable fix stays a health
finding and does not become a plan.
Split File is the cross-file wedge made concrete. It is language-agnostic:
it reads only the already-built graph (defines / calls edges), so it works
the same on every language with call resolution, and it covers the gap LCOM4
leaves for Go (top-level functions, not class methods). Splitting Go files in
the same package is near-zero blast radius (no import edits); Python / TS get a
back-compat re-export shim, surfaced as shim_required on the plan.
Extract Method plans
Extract Method is the plan an agent can apply from the plan text alone.
- Spans worth making. A span must cover at most 60% of the function, hold
at least 8 code lines and remove at least 2 decision points. Spans that would
split a
tryfrom its handlers, change how an exception leaves, move a Godefer, lose a local import, or share a local with a closure are refused. - Header and call. Plan detail writes out the new helper's header and the line that replaces the span, in the file's language (Python, TS, JS, Go, Java, Rust, C, C++, C#), with parameter types where the code declares them, whether it awaits, and what fields it writes.
- The helper's name is a placeholder. The header, call and stage chaining
use
extracted_helper(extractedHelperwhere the language writes camelCase), numbered for later stages, with one note asking whoever applies the plan to rename it for what it returns or does. Judged blind on 107 real refactorings from 19 repositories, names chosen by the applying agent were accepted about 74% of the time. - Staged plans. A function one helper cannot fix gets several helpers called in order, each stage at most CCN 10 and 60 lines, anchored by its first and last line text, served in batches of at most five with the tests to run between them. A plan that would still leave the function above the break-up bar says so ("more stages needed") and ranks after plans that finish the job.
Tests to run
A plan lists the tests closest to the change first (measured coverage, then
tests named for the file, then the call graph), quotes up to 5 nearest test
functions as runnable selectors (tests/test_orders.py::test_total), and
carries the command to run them for the package or project that owns them:
| Language | Command |
|---|---|
| Python | pytest on the selected files |
| JavaScript, TypeScript | The owning package's test runner |
| Go | go test on the package |
| Rust | cargo test -p on the crate |
| Java, Kotlin | Maven or Gradle module tests |
| Ruby | bundle exec rspec or rake test |
| C# | dotnet test on the owning test project or solution |
Commands are stored on the plan in validation.test_runs, so batches rebuild
them without a checkout. When no test reaches the change, the plan has no
command and asks for a characterization test first.
Anatomy of a suggestion
Every suggestion is structured data, not a string: the structure is the source of truth; human-readable text is rendered only at the edges (CLI / MCP / web).
| Field | Meaning |
|---|---|
refactoring_type | extract_class | extract_helper | move_method | break_cycle | split_file | extract_method | performance_fix |
file_path, target_symbol, line_start, line_end | What the refactoring acts on. |
plan | The concrete, type-specific plan: the split groups (methods + fields), the move {method, from_class, to_class}, the clone occurrences + suggested_site, the cycle + cut_edges, the file-split groups ({name, symbols, suggested_file}) + residual core + shim_required, or the method-extraction span + params + returns + suggested_name. |
evidence | The signals that justify it: lcom4, wmc, clone token / line counts + co_change_count, Jaccard distances, cycle size, or the split's modularity + symbol_count + group_count + intra / cut edge counts. |
impact_delta | The health score the refactoring would recover (the deduction of the marker it answers); 0 for the graph-native types that answer no marker. |
effort_bucket | S | M | L | XL, from the target's size. |
blast_radius | What else must move: the callers, co-change partners, and importing files. Extract Method carries {"scope": "local"} instead: extraction adds a private helper and changes no signature, so nothing outside the file moves and there is no count to make. |
confidence | low | medium | high; drives the min_confidence surface gate. |
source_biomarker | The finding this answers (e.g. low_cohesion, god_class, dry_violation). |
Ranking: graph-aware, not churn-only
Each detector sorts its own output, but the surfaces show one mixed list, so the
global order is what matters. Every plan gets a rank_score:
rank_score = benefit × (1 + leverage) / (1 + cost + risk)Benefit multiplies, it does not offset, so a plan with no evidence of a gain scores zero and can never outrank one that recovers real health, however popular or cheap it is.
- benefit is the health score the plan would recover (
impact_delta), or, for the graph-native types that answer no marker, a detector-native structural measure (cycle size, the envy gap for Move Method, group count for Split File). - leverage scales that benefit by the host file's own condition: its
weighted health deficit (
(8.0 - score) x lines of code, log-compressed) plus its dependent count (importer / in-degree, also log-compressed), with a small bonus when the target is reachable from a reliable entry point. - cost is the effort bucket's price (S=1, M=2, L=3, XL=5).
- risk combines change surface (blast radius, log-compressed) and confidence, plus a penalty when the supporting call-graph evidence is weak or the test-validation basis is unmeasured.
A plan on a central, unhealthy hub file outranks the same plan on a healthy leaf. Ties break on rank, then type, then file, then target, so the order is fully deterministic.
The wedge. The leading commercial code-health tool ranks refactoring targets by churn alone, generates code within-function only, and ignores its own coupling signal at generation time. repowise ranks by graph centrality, works across files (class splits, method moves, cycle breaks), and feeds the co-change + graph context straight into the plan.
Surfaces
repowise health --refactoring-targets # ranked table# MCP: the same structured plans, ranked
get_health(only=["fix_first"]) # refactorings ranked beside other fixes
get_health(include=["refactoring"])
get_health(targets=["src/api/server.py"]) # one file
get_health(plan_id="refac...", include=["recipe"]) # one plan as a recipePlans and opportunities on Java, C#, C and C++ files carry a Preview label
with the measurement behind it (preview on a REST row, _meta.preview in
get_health, a dim line in the CLI), because a hand-judged sample there is
still below the bar. Plans on test files are held back on every plan surface.
Break Cycle is advisory: it is shown beside a file's steps, never as one, and
never reaches Fix first.
# REST: what the web tab reads
GET /api/repos/{repo_id}/refactoring/targets?refactoring_type=extract_class&min_confidence=high
GET /api/repos/{repo_id}/refactoring/{suggestion_id} # one plan + blast-radius detailThe web Refactoring tab renders each plan as a card (the split groups as a small tree, the move arrow, the clone occurrences with line links, the file-split groups tree with its residual core and import-rewrite list) over an impact / effort quadrant, with per-type filter chips (URL-synced) and a distinct accent color per type carried consistently across the quadrant dots, card rails, and chips. Each card has a copy-to-agent button that exports the structured plan + source spans + blast radius as a prompt a coding agent can execute.
Code generation
The deterministic plan is the product. Code generation is off by default,
runs only when you ask for it, and never runs during indexing. Turn it on with
the code-generation switch on the repo Settings page or in an open plan, or in
.repowise/config.yaml:
refactoring:
enabled: true
detectors:
disabled: [] # e.g. [move_method]
min_confidence: medium # low | medium | high
llm:
enabled: false # set true to enable code generationYou can ask from three places:
- CLI:
repowise health --refactoring-targets --generate-code <rank-or-symbol>. The flag is itself an explicit request and does not read the setting. - Dashboard: the Generate code action on a plan, when the server runs on your machine with the checkout on disk.
- MCP:
generate_refactoring_code(suggestion_id=...), off the default MCP surface; add it withmcp.tools: ["+generate_refactoring_code"].
Generation reads the plan's real source spans off the working tree, sends the plan, the source and the graph and co-change context to your configured provider, and returns refactored code and a unified diff. Where a self-check is cheap it runs one: Extract Class compares LCOM4 before and after, Split File checks each new file is below the size floor with no symbol duplicated, and Extract Method checks the remaining function's complexity dropped. Results are cached on disk by a content hash (plan + source + model), so the same plan never pays twice.
POST /api/repos/{repo_id}/refactoring/{suggestion_id}/generate-code
GET /api/repos/{repo_id}/refactoring/settings # read llm.enabled + provider/model
PUT /api/repos/{repo_id}/refactoring/settings # toggle it from the dashboardWith generation disabled the REST endpoint returns 403, and 404 when the
repo has no accessible checkout. Nothing is applied to your files: the
output is a reviewable diff.
Configuration
Per-path disables reuse the existing .repowise/health-rules.json glob
mechanism, so a refactoring type can be silenced for generated or vendored paths
the same way a marker is:
{
"rules": [
{ "path": "src/generated/**", "disabled_biomarkers": ["dry_violation"] }
]
}See also
- Code health, the markers and the three health signals the suggestions are built on.
get_health, theget_health(include=["refactoring"])response shape.repowise health, the full CLI reference.