Metadata-Version: 2.5
Name: refactorlens
Version: 2.2.0
Summary: Metric-grounded AI code review CLI for Python codebases.
Project-URL: Homepage, https://github.com/okngms/RefactorLens
Project-URL: Source, https://github.com/okngms/RefactorLens
Project-URL: Issues, https://github.com/okngms/RefactorLens/issues
Author-email: Okan GUMUS <okanngumus4@gmail.com>
License: MIT License
        
        Copyright (c) 2026 RefactorLens contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: code-quality,llm,metrics,refactoring,static-analysis
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# RefactorLens

**Metric-grounded code review for Python.** RefactorLens computes
object-oriented design metrics from your codebase and reports them as evidence
— not opinions.

```bash
pipx install refactorlens
rlens scan .
```

```
your-project/src — 12 modules, 14 classes, 68 functions

Class metrics
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━┳━━━━━━━┳━━━━━┳━━━━━━┳━━━━━━┓
┃ Class                       ┃ NOM ┃ WMC ┃ LCOM4 ┃ DCC ┃  DAM ┃  CAM ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━╇━━━━━━━╇━━━━━╇━━━━━━╇━━━━━━┩
│ orders:OrderManager         │  25 │  49 │     4 │   8 │ 1.00 │    — │
│ reporting:ReportBuilder     │   6 │   7 │     2 │   0 │ 1.00 │    — │
│ models:Customer             │   6 │   6 │     3 │   0 │ 0.20 │ 1.00 │
└─────────────────────────────┴─────┴─────┴───────┴─────┴──────┴──────┘

— CAM not computed for 2 classes (no annotated parameters)
8 items over threshold.
```

> **Status: v2.2.0 released.** Four core commands: `scan` measures,
> `arch` maps layers and violations, `advise` asks an LLM for advice grounded in
> both, and `verify` checks whether the model's own prediction came true. A
> fifth, `explain`, is experimental. v2.1 checked every metric against a
> 26-project calibration corpus; v2.2 added Python-specific descriptive
> measures (dynamic opacity, duck-typing coupling); see the
> [changelog](https://github.com/okngms/RefactorLens/blob/main/CHANGELOG.md).
> Two experiments are done — [FINDINGS.md](https://github.com/okngms/RefactorLens/blob/main/FINDINGS.md) and
> [FINDINGS-2.md](https://github.com/okngms/RefactorLens/blob/main/FINDINGS-2.md).

## Why this exists

Most "let an AI review my code" tools hand the model raw source and hope for the
best. RefactorLens is built on a different bet: **give the model measurements,
then check its work.**

Two layers make that concrete.

**Metric-grounded prompting.** Before asking a model for advice, compute the
numbers. A suggestion that says "this class has four disjoint responsibilities
(LCOM4 = 4) and touches eight other classes (DCC = 8)" is checkable. A
suggestion that says "this feels messy" is not.

**The verify loop.** Every suggestion must come with a *measurable prediction* —
"this change lowers LCOM4 and leaves DCC unchanged". After the change is
applied, the metrics are recomputed and two questions get answered at once: did
quality actually improve, and **was the model's own prediction correct?**

The second question is the interesting one, and it has an answer now.

Across 13 verifiable predictions from three models, 6 were correct. The misses
are not scattered. Where the change removes something and nothing replaces it,
the models were right every time (CC, WMC, PARAMS — 6 of 6). Where the value
depends on what the change leaves behind — a delegating wrapper, a new
dependency — they were wrong every time (NOM, LCOM4, DCC, LOC — 0 of 7).

In one case a model predicted all four of its metrics correctly by producing a
change that deleted the class's entire public interface. Only the behaviour
tests caught it. Full report: [FINDINGS.md](https://github.com/okngms/RefactorLens/blob/main/FINDINGS.md).

**v2 asked what changes that.** Giving the model its architectural context works:
without it, 1 of 24 suggestions on a class with a layer violation mentions the
problem; with it, 13 of 25 do. Telling it how the metrics are computed does not:
accuracy moved from 50% to 53% while stated confidence rose from 0.76 to 0.83.

The confidence turns out to be unusable. Between 0.6 and 0.8 the models are
almost perfectly calibrated; above 0.8, where most predictions sit, they are
worse than chance — stated 0.89, actual 0.44.

And four suggestions across two models claimed to restore a layer boundary.
All four closed the violation on the target and opened the same one on the new
class. Full report: [FINDINGS-2.md](https://github.com/okngms/RefactorLens/blob/main/FINDINGS-2.md).

## Install

```bash
pipx install refactorlens     # recommended: isolated environment
pip install refactorlens      # or into your current environment
```

Requires Python 3.11 or newer. RefactorLens is a CLI tool rather than a library,
so `pipx` is the better fit.

## Usage

```bash
rlens scan .                          # scan the current project
rlens scan src/ --no-report           # print tables only, write nothing
rlens scan . --fail-on-violation      # exit 1 if anything is over threshold
rlens --version
```

`scan` prints two tables and writes a JSON report to `reports/`:

- **Class metrics** — every class, worst first
- **Functions over threshold** — only the ones that exceed a limit, because
  printing every function makes the output useless

Values shown as `—` were **not computed**, which is different from zero. The
footnote below the table says why.

### The full loop

The three commands are meant to be used in sequence.

```bash
# 1. Measure, establishing a baseline
rlens scan .

# 2. Ask for advice, grounded in those measurements
rlens advise .

# 3. Apply one suggestion by hand, then run YOUR tests

# 4. Re-measure and score the model's prediction
rlens verify . --applied "orders:OrderManager=1" \
                --advice reports/advice-20260829-153748.json
```

Step 3 is not optional. A refactoring that improves every metric while breaking
the code is a regression, not an improvement — `verify` reminds you of this in
every report, but it cannot check it for you.

### Asking for advice

```bash
rlens advise .                       # the 3 worst targets
rlens advise . --top-n 1             # just the worst one
rlens advise . --dry-run             # print the prompt, send nothing
rlens advise . --provider ollama --model llama3
```

`--dry-run` needs no API key and no network. It prints exactly what would be
sent, which is the honest way to decide whether you want to send it.

Configure the provider in `rlens.yaml` and put the key in `.env`
(see `.env.example`):

```yaml
provider:
  name: groq          # groq (cloud) or ollama (local)
  model: <model-id>   # from your provider's docs
```

Model names are never hard-coded. Providers change their catalogues often, and a
baked-in name breaks quietly when the package ages.

Every suggestion must name at least one metric and state a **measurable
prediction** — for example "LCOM4 down, DCC up". Suggestions that name no metric
are kept but tagged `unlinked` rather than dropped, so you can see how often the
model ignores the rule.

### Verifying

```bash
rlens verify .                                   # deltas only
rlens verify . --advice reports/advice-....json  # plus prediction scoring
rlens verify . --applied "orders:OrderManager=1" --advice ...
rlens verify . --fail-on-regression              # exit 1 if anything got worse
```

Without `--before`, the most recent scan report is used as the baseline.

`--applied` matters more than it looks. If a target got three suggestions and you
applied one, scoring all three punishes the model for advice you never followed.

Output looks like this:

```
god:OrderManager   improved   WMC 49→34, DCC 8→4
god:OrderRepository  added

Metric   Predicted  Actual
LCOM4    down       same     ✗
NOM      down       same     ✗
WMC      down       down     ✓
DCC      up         down     ✗

prediction accuracy: 1/4 (25%)
```

Two answers at once: the code did get measurably better, and the model was wrong
about three of its four predictions.

Predictions that cannot be checked — a metric that was never computable, a class
that no longer exists — are counted separately and **excluded** from the ratio.
Treating "we could not measure it" as "the model was wrong" would bias every
number.

`verify` refuses to compare reports with different scan schema versions: the same
code produces different numbers under different metric rules. After upgrading
RefactorLens, regenerate the `before` report with the new version. Schema 3
changed the meaning of `loc`, of `lcom4` for classes without methods (`null`,
previously `0`) and of `dcc`; see
[v2-tanim-kararlari.md](https://github.com/okngms/RefactorLens/blob/main/docs/v2-tanim-kararlari.md).

### Reading the measurements back (experimental)

```bash
rlens explain .                      # measure now, then describe
rlens explain . --report reports/scan-....json
rlens explain . --dry-run            # print the prompt, send nothing
```

`explain` asks the model to describe what the measurements say about the code
as it stands — which metrics point at the same structure, which classes share a
pattern — with no advice and no refactoring. Its output is **not scored** and
never enters an accuracy figure: a sentence like "this class carries too many
responsibilities" can be fluent and wrong, and nothing checks it. Each
observation is tagged with the metrics it rests on; one that names none is
marked `unlinked`.

It is experimental for a measured reason: in two runs the model still graded
values ("high", "low") it had been told not to grade, and its observations
mostly restated the table. A deterministic template layer is planned.

### Exit codes

| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Config error, unwritable report, or `--fail-on-violation` triggered |
| 2 | Invalid command usage |

## Configuration

RefactorLens works with no configuration. To customise, put `rlens.yaml` in your
project root — it is searched for upward from the scanned path.

```yaml
scan:
  include: ["."]                       # scan everything under the given path
  exclude: ["tests/", ".venv/", "migrations/"]
  output_dir: reports/

metrics:
  cam_min_annotation_coverage: 0.7     # below this, CAM reports null

thresholds:
  cyclomatic_complexity: {warn: 10, critical: 20}
  max_params: {warn: 5}
  max_nesting: {warn: 4}
  lcom4: {warn: 5, critical: 10}
  dcc: {warn: 7}
  wmc: {warn: 50}
  nom: {warn: 20}
```

The defaults are checked against a 26-project calibration corpus: in a typical
project each warning level flags at most about 7% of functions or classes, and
each critical level about 1%. LCOM4 was `{warn: 2, critical: 4}` before this
change; at 2 it flagged a third of all classes. Measurement:
[thresholds.md](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/thresholds.md).

Unknown keys are an **error**, not a warning. A typo like `max_nestings` would
otherwise leave you silently running on defaults.

## Metric definitions and adaptations

The metrics are inspired by the QMOOD family and classic complexity measures.
**Only a subset of QMOOD is implemented; this is not a complete QMOOD tool.**

Python is dynamically typed, so several object-oriented metrics can only be
computed by adaptation. Those adaptations are documented here rather than
hidden.

### Shared rule: which methods count

All six class metrics operate on the same method set: methods defined directly
in the class body, excluding dunder methods. `@property`, `@staticmethod` and
`@classmethod` are included. Nested functions and nested classes are not.

`__init__` is excluded, and this matters most for LCOM4: a constructor touches
every attribute by definition, so counting it would merge every class into a
single component and make the metric useless. Classic LCOM4 excludes
constructors for the same reason.

### Class-level metrics

| Metric | What it counts | Adaptation |
|---|---|---|
| **NOM** | Methods in the class | Dunders excluded |
| **WMC** | Sum of cyclomatic complexity over those methods | Same method set as NOM, for consistency |
| **LCOM4** | Connected components in the method–attribute graph | See limitation below |
| **DAM** | Ratio of private attributes | Reported twice: `dam` counts `_x` and `__x`; `dam_strict` counts only `__x` |
| **DCC** | Distinct project-internal classes referenced | Name-based resolution; see below |
| **CAM** | Mean ratio of each method's parameter types to the class-wide set | Computed only when annotation coverage is sufficient |
| `annotation_coverage` | Share of the class's method parameters that carry a type annotation | Descriptive only: no threshold, no smell; `null` when there are no parameters |

**Attribute set (used by DAM and LCOM4)** is the union of: class-level
assignments and annotations, `self.x = ...` in *any* method (not just
`__init__`), and names listed in `__slots__`. Names that are only ever read are
not attributes.

**DCC resolution is best-effort.** Python has no static type information, so a
name appearing in a class body is counted as a reference if it matches a class
defined anywhere in the project. String annotations (`"Order"`) and import
aliases from project modules (`from models import Order as O`) are resolved;
`Literal[...]` values are not references.

A class's own nested classes are part of the class, not dependencies, and are
not counted. Before scan schema 3 they were: Django's
`class Meta(BaseForm.Meta)` alone added a false dependency to hundreds of netbox
classes.

Measured by hand on 36 classes (152 references) from 12 open-source projects,
under schema 3: **97.4% precision and 96.1% recall**, with 31 of 36 classes
exactly right. The errors cluster in high-coupling classes, but none moved a
class across the `dcc` threshold. What goes wrong:

- **Name collisions count.** A third-party class, local variable or module
  with the same name as a project class is counted — `typing.Tuple` when the
  project defines `Tuple`, werkzeug's `Response` when the project defines its
  own.
- **Type aliases are not followed.** `Scheme = Union[APIKey, OAuth2]` used in
  an annotation contributes nothing.
- **Two project classes with the same name count once.**

Method, per-reference verdicts and the full breakdown:
[metric-accuracy.md](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/metric-accuracy.md).

**CAM is conditional.** The classic definition uses parameter *types*. Parameter
*names* measure something else entirely and would make the result incomparable
to the literature, so name similarity is never used as a fallback. Most Python
codebases on our calibration corpus are mostly annotated (median 82% of
parameters), but a large minority is not — 7 of 26 projects annotate fewer than
3%. Forcing a number out of those would feed the model
noise dressed up as evidence. If annotation coverage falls below
`metrics.cam_min_annotation_coverage` (default 0.7), CAM reports `null` and the
report records why.

### Known limitation: DAM and CAM on real code

Measured on a 26-project calibration corpus (per-project medians):

- **DAM largely reflects a project's naming convention.** Among classes with at
  least two attributes, a typical project puts 86.5% on a single DAM value, and
  in 17 of 19 projects that value is 0. Differences between projects explain
  between half (each project weighted equally) and three quarters (weighted by
  class count) of DAM's variance. In four of five
  web applications (and in fastapi, pipx, yolov5) DAM does not separate classes
  at all: at least 90% of them have no `_`-prefixed attribute. In six of seven
  libraries it does vary between classes. Read DAM against the project's own
  convention, not as an absolute score.
- **CAM is computed for about a quarter of classes** and carries information
  (two or more methods with parameters) for about one in six; in projects
  without annotations (yt-dlp, awscli, netbox) it is almost always `null`.
  Where it is computed, it does discriminate.

Both are kept rather than dropped: removing a field would invalidate earlier
reports and the experiment data built on them. Measurement:
[coverage.md](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/coverage.md).

### Known limitation: `god_class` and stateless methods

LCOM4 counts every method that touches no attribute as its own component. On
the 26-project calibration corpus, 44 of the 96 classes that pass the
`god_class` metric gate are mostly made of such methods.

- **Interfaces are exempt.** A class whose method names are at least half
  stubs (`pass`, `...`, `return None`, `raise NotImplementedError`) has no
  behaviour to split and gets no `god_class`; the report's `stub_methods`
  field shows the count. This removed three classes on the corpus, among them
  mypy's `NodeVisitor` (83 stub methods).
- **Still flagged, possibly wrongly:** GraphQL types whose resolvers are
  `@staticmethod` (five saleor classes), and large visitor or message-catalogue
  classes whose methods only delegate to a shared helper.

Two replacement gates from the literature (Hitz–Montazeri LCOM3 and
Lanza–Marinescu TCC) and two stateful-method variants were measured, and a
blind sample of 32 large classes was labelled — by an LLM (Claude Sonnet 5),
not a person, so treat this as a first signal. By those labels most large
classes are one responsibility with many methods (a visitor, a DataFrame-like
API, a framework base class), and only about one in ten classes flagged
`god_class` clearly holds several unrelated responsibilities. No alternative
gate did measurably better, so the gate is unchanged; treat a `god_class`
finding as "large, look closer" rather than "should be split". Measurement:
[density-gate.md](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/density-gate.md),
[stateless-gate.md](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/stateless-gate.md).

### Known limitation: LCOM4 and data classes

LCOM4 flags well-written data-holder classes as uncohesive. A class with one
accessor per field — `rename` touching `name`, `promote` touching `tier` — has
methods that share no state, so LCOM4 counts them as separate responsibilities.

This is a property of the metric, not a bug, and it is well documented in the
literature. RefactorLens reports the number as measured rather than
special-casing it away. When reading a report, treat a high LCOM4 on a small
class as a question rather than a verdict; **WMC and DCC separate genuinely
overloaded classes from plain data holders far more reliably.**

### Known limitation: LCOM4 and hub methods

The opposite failure also happens. LCOM4 treats a method call as a connection,
so a large class whose methods all call one shared helper (`self.add(...)`,
`self.fail(...)`, the visitor pattern) forms a single component and scores
LCOM4 = 1 — "perfectly cohesive" — however many unrelated responsibilities it
holds.

Because `god_class` requires `lcom4 >= 3`, such classes escape the smell. On
the hardening reference set, 36 of the 107 classes large enough to qualify
were filtered out by LCOM4, 34 of them only because of call edges; mypy's
217-method `TypeChecker` is one. When a class has very high NOM and WMC but no
`god_class` finding, check it by hand. Measurement:
[metric-accuracy.md §6](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/metric-accuracy.md).

### Function-level metrics

Cyclomatic complexity counts `if`/`elif`, loops, `except` handlers, ternaries,
each additional `and`/`or` operand, comprehension clauses, and `match` cases.
`else`, `with`, and `try` itself add nothing — they do not branch execution.

Nesting depth treats `elif` chains as flat: a ten-branch `elif` is not ten
levels deep.

Nested function definitions are never entered. A function containing a closure
does not inherit the closure's complexity.

LOC counts lines that contain code, from the `def` line to the last line.
Blank lines, comment-only lines and docstrings are not counted; decorators are
excluded. A tool that reports "improved" should not reward deleting a
function's documentation, and physical line counts do exactly that. Before scan
schema 3 LOC counted physical lines; on the calibration corpus the median
function is 15% shorter under the new rule, and a quarter of `long_method`
findings disappear because docstrings, comments and blank lines were what
carried them past 40 lines.

`too_many_params` is not reported for framework entry points: click/typer
commands, HTTP route handlers (`@app.get("/...")`, `@bp.route("/...")`), Django
and SQLAlchemy signal handlers, and pytest fixtures. Their parameters are the
command-line options or HTTP parameters the framework exposes, or a signature the
framework dictates; "reduce the parameter count" would mean changing that
interface. The parameter count itself is still measured and still coloured in the
terminal, where the row is marked (`cli.scan [cli]`); `advise` simply does not
treat it as a reason to pick the function. Recognition is deliberately narrow — a
Celery task's parameters are the author's own design and still count. On the
calibration corpus this affects 17 of 2,238 functions with five or more
parameters, but a single command can carry 29 of them.

Every function also reports `annotation_coverage` (the share of its parameters,
counted exactly as for the parameter count, that carry a type annotation;
`null` when it has none) and `returns_annotated`. These are descriptive: there
is no threshold and no smell, because "fewer annotations is worse" is not a
claim the tool can support. Types kept in separate `.pyi` stub files are not
read, so a package typed that way (attrs) looks unannotated. Measurement:
[annotation-coverage.md](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/annotation-coverage.md).

`dynamic_sites` counts the places in a function where static analysis cannot
see the target: `getattr`/`setattr`/`delattr`/`hasattr` with a non-constant
name, `eval`/`exec`, dynamic imports, and forwarding the function's own
`**kwargs` to another call. Classes report `dynamic_attribute_hooks` when they
define `__getattr__`, `__getattribute__` or `__setattr__`. Again descriptive:
forwarding `**kwargs` is usually the right design, so there is no threshold.
Hand-checked on 30 sites, 27 were genuinely opaque; the three that were not
looped over a literal list written in the same function. Measurement:
[dynamic-opacity.md](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/dynamic-opacity.md).

`duck_coupling` counts the distinct `(parameter, attribute)` pairs a function
touches on its arguments — `order.total`, `customer.notify()` — so it measures
how wide an interface the function expects from its collaborators without
resolving any types. It works for module-level functions, which DCC never
sees. Parameters that are reassigned inside the function are left out. Where
parameters are annotated with a project class, 90% of the attributes a function
touches are members of that class (median of 16 projects). Descriptive, no
threshold. Measurement:
[duck-coupling.md](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/duck-coupling.md).

## What RefactorLens does not do

- **It does not run your code.** Files are parsed with `ast`, never executed.
- **It does not fix anything.** `advise` suggests and `verify` checks; applying
  a suggestion is a human's job, on purpose. Automatic application would make
  the behaviour-test rule below unenforceable.
- **`scan` and `verify` send nothing anywhere.** They are entirely local.
  `advise` sends the selected class or function, plus the signatures of the
  project classes it depends on, to whichever provider you configure. Use
  `--dry-run` to see exactly what would go out, or the Ollama provider to keep
  everything on your machine.
- **Python only.** No Java or C#.
- **It does not measure code outside functions and classes.** Every metric
  works on a function or a class defined at the top of a module. Script-style
  code written at module level (a training loop in `train.py`), module-level
  calls (a CLI built with hundreds of `parser.add_argument(...)`), and
  functions or classes defined under `if`/`try` (`if TYPE_CHECKING:`) leave no
  trace in the report. On a 26-project corpus this is under 7% of logic lines
  for 22 projects, 8-13% for pydantic and rich, 18% for httpie and 56% for
  nanoGPT. Constant data tables are not counted as logic. Measurement:
  [corpus.md](https://github.com/okngms/RefactorLens/blob/main/experiments/hardening/corpus.md).

## Roadmap

| Phase | Contents | Version |
|---|---|---|
| 0 | Package skeleton, config, test foundation | — |
| 1 | Metric engine (`scan`) | — |
| 2 | First PyPI release | v0.1.0 |
| 3 | AI advisor (`advise`) | — |
| 4 | Verification loop (`verify`) | v0.2.0 |
| 5 | Experiment and findings | v1.0.0 |
| v2 | `arch`, smells, architectural context, calibration, second experiment | v2.0.0 |
| v2.1 | Metrics checked against a 26-project corpus: scan schema 3, percentile-based defaults, `explain` (experimental) | v2.1.0 |
| **v2.2** | **Python-specific measures, each pre-registered and checked on the corpus: dynamic opacity, duck-typing coupling** | **v2.2.0** |

Phases 3 and 4 shipped together in v0.2.0. Both experiments live in
[`experiments/`](https://github.com/okngms/RefactorLens/tree/main/experiments), with the raw data committed alongside them.

Next: a deterministic template layer for `explain`, and layer inference
instead of requiring layers to be declared — today they come from your config or from an
existing `import-linter` contract, and are reported `unknown` when neither is
present. **v3** closes the loop (`apply`, a
feedback round, a benchmark); **v4** adds history and other languages.

Ideas deliberately out of scope live in [FUTURE.md](https://github.com/okngms/RefactorLens/blob/main/FUTURE.md).
[STRUCTURE.md](https://github.com/okngms/RefactorLens/blob/main/STRUCTURE.md) maps the codebase file by file, and
[AGENTS.md](https://github.com/okngms/RefactorLens/blob/main/AGENTS.md) records the locked decisions and invariants behind it.

## Development

```bash
git clone https://github.com/okngms/RefactorLens
cd refactorlens
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

pytest tests                        # the package's own tests
pytest examples/messy_project/tests # the fixture's behaviour tests
ruff check . && ruff format --check .
```

`examples/messy_project` is a deliberately badly designed sample project used as
the test fixture; every metric is verified against hand-computed gold values on
it. See [SMELLS.md](https://github.com/okngms/RefactorLens/blob/main/examples/messy_project/SMELLS.md) for the inventory of
intentional smells and the reasoning behind each.

Its behaviour test suite exists for a specific reason: from phase 4 onward,
refactoring suggestions get applied by hand. If a refactoring breaks the code,
the metrics improve while the program stops working. The rule is therefore
absolute — **no metric delta counts unless the behaviour tests pass.**

## Licence

MIT
