# Refactoring intelligence

Refactoring intelligence names the specific fix: six deterministic detectors emit one graph-aware plan per opportunity, ranked by impact and effort.

A [code health](https://docs.repowise.dev/intelligence/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.

```bash
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 applies
```

It 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](https://docs.repowise.dev/intelligence/refactoring#code-generation)).

## 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](https://docs.repowise.dev/intelligence/code-health#performance-static-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.

> **Note**
>
> **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 `try` from its handlers, change how an exception leaves, move a Go
  `defer`, lose a local import, or share a local with a closure are refused. A
  TypeScript span is also refused when the helper would lose a narrowing the span
  relies on or would assign an outer name without taking or returning it; the plan
  records `narrowing_lost` or `outer_declaration`. A step whose helper takes a
  shared parameter object carries that object's declaration and the line that builds
  it, and a plan missing either is not offered. Apex has Extract Method plans too.
- **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` (`extractedHelper` where 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`:

```text
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.

> **Note**
>
> **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

```bash
repowise health --refactoring-targets            # ranked table
```

```python
# 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 recipe
```

Plans 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.

```text
# 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 detail
```

The 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`:

```yaml
refactoring:
  enabled: true
  detectors:
    disabled: []              # e.g. [move_method]
  min_confidence: medium      # low | medium | high
  llm:
    enabled: false            # set true to enable code generation
```

You 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 with `mcp.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.

```text
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 dashboard
```

With 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:

```json
{
  "rules": [
    { "path": "src/generated/**", "disabled_biomarkers": ["dry_violation"] }
  ]
}
```

## See also

- [Code health](https://docs.repowise.dev/intelligence/code-health), the markers and the three
  health signals the suggestions are built on.
- [`get_health`](https://docs.repowise.dev/mcp/get-health), the
  `get_health(include=["refactoring"])` response shape.
- [`repowise health`](https://docs.repowise.dev/cli/health), the full CLI reference.
