Skip to content

Latest commit

 

History

History
306 lines (256 loc) · 17.3 KB

REFACTORING.md

File metadata and controls

306 lines (256 loc) · 17.3 KB

Refactoring intelligence

A health score tells you a file is in trouble. Refactoring intelligence names the fix: split this class into these groups, move this method to the class its calls land in, cut this one import edge to break a cycle, lift these lines into a helper with this signature. Each plan is structured data computed from the same graph, class model and git history the health score uses.

Plans are built during the health pass of repowise init and repowise update. They need no LLM key, no network and no re-parse. Turning a plan into actual code is a separate, on-request step that does use an LLM (Code generation).

repowise code-health loop: markers fan into three signals, the graph and git history locate risk, and refactoring intelligence emits concrete plans an agent executes

Plan types

Type What the plan names
Extract Class The cohesive groups (methods and fields) an incohesive or god class splits into.
Extract Method A line span to lift out of a long or complex function, with the helper's parameters and return value.
Extract Helper Every occurrence of a duplicated block and where the shared helper belongs.
Move Method A method that uses another class more than its own, and the class it belongs in.
Break Cycle The smallest set of import edges to invert to break a runtime dependency cycle. Advisory: see below.
Split File The files an oversized module splits into, which symbols go where, and the import edits in each dependent.
Performance Fix One shared change for a performance problem, with the affected call sites and caller-to-sink paths.

When one copy of a duplicated block is already a whole function the other copies could call, the Extract Helper plan says so instead of asking for a new helper: each other site gets the call that replaces it, or is marked as a copy to delete and import. Plan detail carries the call text; lists stay the same size.

Each detector reports nothing when a signal it needs is missing, so a gap means "no suggestion", never a guessed one. Output is deterministic: the same commit gives the same plans and the same ids.

Quick start

repowise health --refactoring-targets                  # the stored queue, in serving order
repowise health --refactoring-targets --format json
repowise health --refactoring-targets --recompute      # analyze the working tree instead of the index
repowise health --refactoring-targets --generate-code 1   # code + diff for the top plan (LLM)
repowise health --plan refac...                        # one plan as the prompt an agent applies

From an agent, through MCP:

get_health(only=["fix_first"])                                      # refactorings ranked beside other fixes
get_health(include=["refactoring"], only=["refactoring_opportunities"], limit=6)
get_health(targets=["src/api/server.py"], include=["refactoring"])  # one file
get_health(opportunity_id="refop...")                               # one opportunity's steps
get_health(plan_id="refac...")                                      # one plan
get_health(plan_id="refac...", include=["recipe"])                  # ...as a recipe an agent applies

In the dashboard, the Refactoring page lists every type on one board, with filters for type, confidence and effort. A plan's drawer explains its rank, shows the tests that reach the code and the command to run them, and can export the structured plan for an agent. The page never edits code or runs tests on its own.

Tests and commands

A plan lists the tests closest to the change first: measured coverage, then tests named for the file, then the call graph. Tests that reach the file only through a widely imported hub module are left out. When the plan changes known functions, it quotes the few test functions nearest each one as runnable selectors (tests/test_orders.py::test_total), and its command runs only the files they live in.

When no test reaches the change, the plan has no command and asks for a characterization test first; it also asks when tests reach the file but none reaches the changed function. JavaScript, TypeScript, Go, Rust, Java, Kotlin, Ruby and C# tests get a command for the package or project that owns them; a test whose build file or runner the repository does not have gets no command. Per-language rules: architecture/refactoring.md.

In a plan with several steps, such as a performance fix at several call sites, plan detail adds a verify to each step whose tests differ from the plan's: the tests that reach that step's lines and the command to run them. A step without one is checked by the plan's own command.

Recipes

Any plan can be read as a recipe: what must hold before the edit (tests that pass, a characterization test to add, a decision to read), the edits in order, the checks and expected improvement after, and what the edit must not do. The dashboard's copy-prompt, repowise health --plan, plan detail with include=recipe and code generation all read that same recipe, so an agent sees one set of steps wherever it starts. The recipe and the prompt name the plan by the public id get_health(plan_id=...) takes; REST plan detail's own id field is the storage id.

Reading the results

A plan is one detector's output for one target. An opportunity is one file's refactoring work: the finding it leads with, and its plans as ordered steps that are safe to apply in sequence. Surfaces list opportunities; a step points to its plan by plan_id.

Field What it tells you
refactoring_type Which of the seven plan types this is.
plan The concrete change: groups, span and signature, destination class, cut edges, or file split.
evidence The measurements behind it, such as LCOM4, clone size, co-change count or modularity.
confidence low, medium or high. A high Extract Method is mechanical enough to hand to an agent.
effort_bucket S, M, L or XL, from the size of the target.
blast_radius What else has to change: callers, importing files, co-change partners. Extract Method is local.
impact_delta Health the plan would recover, in proportion to what it removes. 0 for Break Cycle, Split File and Performance Fix, which rank on their own evidence.
source_biomarker The health finding the plan answers, such as god_class or complex_method.

On an opportunity, each step carries applicability: mechanical (only extractions qualify) or judgment, with the facts behind the call. When an earlier step moves a symbol to another file, the later step carries relocated_by: its stored location is where the symbol was before, so find it again before applying. In the plan list a Split File plan stands for the plans it relocates in the same file: its row lists them as relocates, and they take no rows of their own.

Plan detail also lists risks, one sentence each, to read before applying the plan: an accepted decision that governs the file, callers or importers of what the plan moves, a target nothing reaches (delete it instead), recent commits on the same lines, and a test the plan runs that asserts on the function's own source text. When the plan changes a symbol a package exports to its consumers, the risk names the file exporting it. A governed plan's steps are judgment, and its blast_radius.co_change_partners names the files that usually change with it. A newly accepted decision shows here after the next index or update.

A plan the index stops detecting is resolved, and its detail then carries a payoff with the target's size before and after and the commit that resolved it. applied means the measures are consistent with the plan being applied: the target lost at least half the predicted complexity and a function the size of the extracted span appeared beside it (computed for Extract Method only). The other outcomes are file_deleted, superseded (the same plan found again under a new id), target_changed (renamed or rewritten another way) and unknown.

Plans and opportunities on Java, C#, C and C++ files are served as a preview: they are listed and counted like any other, and each one carries the measurement behind it, as preview on a REST row, in _meta.preview on get_health, and as a dim line under the CLI list. A language gated instead is left out unless the caller passes include_unverified, and counted in gated.

Plans and opportunities on test files are held back the same way on every plan surface, scope=all included, as Fix first already leaves them out. They are counted in gated under test.

Break Cycle is advisory. Its plan names edges to cut, not which symbols cross them or how to move them, so an opportunity carries it as evidence instead of as a numbered step: it never leads, and never reaches Fix first. The plan itself stays in the plan list. Cycles are found over runtime imports only; type-only and deferred imports do not close a cycle (rules). How the rollup counts plans: architecture/refactoring.md.

Names are never invented. A suggested helper or file name is null when nothing in the code anchors one. A Split File or Extract Class plan with an unnamed group is held as needs_design, like a cycle: it is not a step and never reaches Fix first. A plan led by Move Method or Extract Class stays in the full list but is kept out of Fix first and the default scope, counted as not yet audited, until that kind passes a per-language review. When such a step follows another lead, the Fix first item leaves it out and says how many steps it held back; the full plan keeps them. Code only moves within a language family, so Java, Kotlin and Scala are separate. A callback is named after the call it is passed to. An Extract Method helper gets the placeholder name extracted_helper, since the code rarely says what a block is for; the plan and its prompt ask you to rename it for what it returns or does when you apply it.

An empty list means no detector found work that clears its gates. It does not mean the code needs no attention: check the findings in CODE_HEALTH.md.

Scope and order

By default the open, repository-wide list shows only what Fix first would take: opportunities in shipped code that recover real health and name a concrete edit, outside dead code and outside functions a constant flag switches off. The response counts the rest and the reasons they were held back. Ask for the full inventory with scope=all (MCP: refactoring_scope="all"; dashboard: Full inventory). A call that names a file, or a triaged status, gets the full inventory automatically.

Many top opportunities score the same. The default order (diversified) round-robins over the lead finding, plan type and area so the first rows cover distinct problems. canonical returns the plain rank order. Test files always rank after production files.

Code generation

Code generation is off by default, only runs 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 with refactoring.llm.enabled: true. The switch names the provider and model it will use, so there is nothing else to configure. The CLI flag is itself an explicit request and does not read that setting.

You can ask from three places:

  • CLI: repowise health --refactoring-targets --generate-code <rank-or-symbol>.
  • 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=...). This tool is off the default MCP surface; add it with mcp.tools: ["+generate_refactoring_code"].

Generation reads the plan's real source spans from the working tree, sends the plan, the source and the graph and co-change context to your configured provider, and returns refactored code with a unified diff. Where a check is cheap it runs one: Extract Class compares LCOM4 before and after, Split File checks each new file is under the size floor with no symbol duplicated, and Extract Method checks the remaining function's complexity dropped. Results are cached by a hash of plan, source and model, so an unchanged plan is not paid for twice. Nothing is applied to your files.

The CLI uses the provider configured for page generation; the web and MCP use the chat model. With generation disabled, the MCP tool returns the plan with generation.available: false and the REST endpoint returns 403. With no key, MCP returns error: "no_provider".

Configuration

In .repowise/config.yaml:

refactoring:
  enabled: true              # set false to skip the refactoring layer
  detectors:
    disabled: []             # e.g. [move_method, split_file]
  min_confidence: medium     # low | medium | high; unset keeps every plan
  llm:
    enabled: false           # code generation; set true to enable

Detector names are the snake-case plan types: extract_class, extract_method, extract_helper, move_method, break_cycle, split_file, performance_fix.

Most plans answer a health finding, so per-path marker rules in .repowise/health-rules.json also remove the plans for those findings. See CODE_HEALTH.md.

Accuracy and limits

  • Plans are suggestions for review. Nothing is applied automatically, and generated code comes back as a diff.
  • Extract Method offers a span only when it can show the extraction keeps behavior, so it under-reports by design. Spans that would split a try from its handlers, move a Go defer, or carry the original finding into the helper are not offered. Full gates: architecture/refactoring.md.
  • A span must remove at least two decision points, hold at least 8 code lines and cover at most 60% of the function's lines. Estimated gain never exceeds the share of decision points removed.
  • When Extract Method declines a function, its finding records why in details.extract_method_refused.
  • An Extract Method plan writes out the new helper's header and the line that replaces the span, in the file's language, with parameter types where the code declares them; <type> marks a type for you to fill in. It flags an awaiting span with needs_async: true and a receiver-bound helper under new_symbol.
  • A TypeScript span is not offered when the helper would lose a narrowing the span relies on (if (!tc.result) continue; before tc.result.id), or would assign an outer name without taking or returning it; the finding records narrowing_lost or outer_declaration in details.extract_method_stage_refused. A narrowed parameter gets a note to type it as narrowed.
  • A step Repowise cannot express mechanically is a judgment call with a reason code: async_helper_unexpressible, receiver_unbound or receiver_copy_written.
  • A Python, TypeScript or JavaScript function too complex for one helper can get a staged plan: several helpers called in order, applied as direct edits one stage at a time, in batches of at most five with tests between batches.
  • A plan that would leave the function or a new helper above the break-up bar says so ("still above the break-up bar; more stages needed") and ranks after work that finishes the job. The mark is set at index time, so a plan ranked live is not marked.
  • Move Method never targets a class the method only instantiates, or an ancestor of its own class.
  • Split File works on any language with call resolution and suggests a split only when the groups are clearly separable. Python and TypeScript splits usually need a re-export shim, flagged as shim_required.
  • Plan quality follows graph quality. Where calls are not resolved for a language, Move Method and Split File have less to work with.
  • Generated code depends on the model you configure. The self-checks catch some structural mistakes, not behavior changes; review and test the diff.
  • There is no published accuracy benchmark for refactoring plans yet.

Reference

Flag Effect
--refactoring-targets Print the stored refactoring queue in the order MCP and the dashboard serve it.
--recompute Analyze the working tree in-process instead of reading the index. Slow on large repos.
--generate-code SELECTOR Generate code and a diff for one plan. SELECTOR is a 1-based rank or a symbol name. Needs an API key.
--plan ID Print one stored plan as the prompt an agent applies; with --format json, its recipe.
--format json Machine-readable output.

See also