> ## Documentation Index
> Fetch the complete documentation index at: https://roe.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# roe health

> Flag complex, oversized, and tangled code, and rank change hotspots.

Flags code that's hard to work with rather than code that's unused: methods
that are too complex or too long, types and files that have grown too big,
and types that depend on each other in a cycle. Opt in to `--hotspots` to
also rank the files that are both complex and frequently changed.

```bash theme={null}
roe health [PATH]
```

Truncated to the two worst findings with `--limit 2`:

```text theme={null}
Simulation/AI/ActionGenerator.cs (Simulation)
    11:35  Simulation.AI.ActionGenerator.GenerateActions
           cyclomatic 46/10 · cognitive 86/15 · 242/40 lines

Simulation/Scenarios/TenementScenario.cs (Simulation)
    19:28  Simulation.Scenarios.TenementScenario.Build
           cyclomatic 12/10 · cognitive 19/15 · 195/40 lines
  … and 70 more — re-run with --limit 0 to see all

circular dependencies
  Simulation.Core.GameState → Simulation.Core.MoveValidator → Simulation.Core.GameState
    Simulation/Core/GameState.cs 15:21
    Simulation/Core/MoveValidator.cs 12:21

found 119 issues across 72 locations in 45 files — 3 project(s), 160 file(s), 1688 symbol(s) scanned in 281 ms
  32 complex methods · 18 hard-to-follow methods · 49 long methods · 10 over-parameterized methods · 1 large file · 8 large types · 1 circular dependency
```

Each declaration gets one entry listing every check it tripped, and each
metric is printed as `actual/threshold` so you can see how far past the line
it sits. The worst offenders come first.

## Options

| Option                       | Description                                                                                                                                                                                                                                                            |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PATH`                       | Path to the codebase root — a directory, `.sln` file, or `.csproj` file. Defaults to the current directory.                                                                                                                                                            |
| `-f, --format <human\|json>` | Output format. Defaults to `human`; `json` emits the stable v1 schema described in [JSON output](/reference/json-output).                                                                                                                                              |
| `--max-complexity <N>`       | Flag methods and properties above this cyclomatic complexity. Defaults to `10`.                                                                                                                                                                                        |
| `--max-cognitive <N>`        | Flag methods and properties above this cognitive complexity. Defaults to `15`.                                                                                                                                                                                         |
| `--max-method-lines <N>`     | Flag methods and properties whose body spans more than this many lines. Defaults to `40`.                                                                                                                                                                              |
| `--max-parameters <N>`       | Flag methods, operators, and indexers with more than this many *required* parameters. Defaults to `5`.                                                                                                                                                                 |
| `--max-file-lines <N>`       | Flag files longer than this many lines. Defaults to `750`.                                                                                                                                                                                                             |
| `--max-type-members <N>`     | Flag types with more than this many members. Defaults to `20`.                                                                                                                                                                                                         |
| `--exclude-tests`            | Skip test projects, including any circular dependency that touches one. Off by default. Excluded projects are subtracted from the scanned counts and named on the footer, so you can confirm the flag took effect — see [What "scanned" counts](#what-scanned-counts). |
| `--sort <severity\|path>`    | Order findings by how far past their threshold they sit, or by file path. Defaults to `severity`.                                                                                                                                                                      |
| `--limit <N>`                | Print at most this many findings; `0` prints all of them. Defaults to `0`.                                                                                                                                                                                             |
| `--hotspots`                 | Also rank files that are both complex and frequently changed, read from git history.                                                                                                                                                                                   |
| `--top <N>`                  | How many hotspots to list. Defaults to `10`. Requires `--hotspots`.                                                                                                                                                                                                    |
| `--baseline <PATH>`          | Hide the findings recorded in this [baseline file](#baselines), so only new ones are reported.                                                                                                                                                                         |
| `--write-baseline <PATH>`    | Record today's findings to this path and exit `0`. Conflicts with `--baseline`.                                                                                                                                                                                        |
| `--config <PATH>`            | Use this `roe.json`/`roe.yaml`/`roe.yml` instead of [auto-discovery](/configuration).                                                                                                                                                                                  |

Every threshold can also be set persistently in a
[config file](/configuration), which is usually what you want in CI rather
than repeating six flags on each invocation.

## The checks

| Check                     | Flags                                                                                                                                                                                                                                                        |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Complex method            | A method or property whose cyclomatic complexity — the number of independent paths through it — exceeds `--max-complexity`. One point per `if`, loop, `catch`, `switch` arm, ternary, and `&&`/`\|\|`, plus one baseline.                                    |
| Hard-to-follow method     | A method or property whose cognitive complexity exceeds `--max-cognitive`. Like cyclomatic, but weighted by nesting depth, so deeply nested code scores worse than the same number of flat branches.                                                         |
| Long method               | A method or property whose body spans more than `--max-method-lines` lines.                                                                                                                                                                                  |
| Over-parameterized method | A method, operator, or indexer with more than `--max-parameters` **required** parameters. Defaulted parameters, `params` arrays, and `out` parameters don't count towards the limit — see [Only required parameters count](#only-required-parameters-count). |
| Large file                | A file longer than `--max-file-lines` lines.                                                                                                                                                                                                                 |
| Large type                | A type with more than `--max-type-members` members.                                                                                                                                                                                                          |
| Circular dependency       | Two or more types that reference each other in a cycle. Has no threshold and can't be turned off.                                                                                                                                                            |

Generated files are excluded from every check, as are files matched by the
config's [`ignore` globs](/configuration). A circular dependency that touches
a generated type is dropped whole rather than reported with that type left
out — like `--exclude-tests`, since a path with a hole in it would name edges
that aren't there.

### `??` is not a branch

The null-coalescing operator doesn't count towards cyclomatic complexity.
It's a defaulting idiom, not control flow the reader has to trace — nobody
writes a second test case for the null side of `name ?? "anonymous"`. Counting
it made hand-rolled `with`-style methods score wildly:

```csharp theme={null}
public Unit Copy(string? name = null, int? hp = null, /* … */)
    => new(name ?? Name, hp ?? Hp, /* …eleven more… */);
```

That used to score cyclomatic 14 against a limit of 10 while cognitive
complexity — the metric that actually models comprehension — scored it 0.
Now both agree it's trivial.

Any real branching hidden behind a `??` still shows up: the right-hand side is
walked like any other expression, so `Load() ?? throw new InvalidOperationException()`
or a `??` whose fallback contains a ternary is counted on its own merits.
`??=` never counted, since the grammar treats it as an assignment.

<Note>
  `&&` and `||` are unchanged and still cost one point each — that's McCabe's
  definition, and unlike `??` they genuinely add a path a test has to cover.
  Cognitive complexity continues to collapse a run of them to a single point.
</Note>

### What "scanned" counts

The footer's `N project(s), N file(s), N symbol(s) scanned` counts what was
*eligible to be reported*, not what was parsed. Anything ruled out up front —
test projects under `--exclude-tests`, files matched by the config's
[`ignore` globs](/configuration) (top-level or `health.ignore`) — is
subtracted, and named on a line of its own:

```text theme={null}
found 8 issues across 7 locations in 6 files — 2 project(s), 118 file(s), 1204 symbol(s) scanned in 153 ms
  2 complex methods · 0 hard-to-follow methods · 1 long method · 3 over-parameterized methods · 0 large files · 2 large types · 0 circular dependencies
  excluded: 1 test project (Lib.Tests), 4 ignored files
```

The `excluded:` line only appears when something was actually excluded, so a
plain run is unchanged. Up to three project names are listed; past that the
line says `+N more`.

This matters most when the exclusion comes from a
[config file](/configuration), where there is no command line to eyeball:
`health.excludeTests: true` is otherwise invisible, and the footer is the one
place a reader can confirm both that the setting was picked up *and* that
their test project was recognized as one.

<Note>
  Generated files are not reported as an exclusion. They are never eligible
  for any check, under any setting, so there is no setting for a reader to
  confirm — unlike `--exclude-tests` and `ignore`, which are choices that can
  silently fail to apply.
</Note>

### Sorting and severity

Severity is simply how many times over its threshold a finding sits —
`metric / threshold` — which makes a cyclomatic complexity of 46 against a
limit of 10 directly comparable to a 242-line body against a limit of 40.
A declaration's severity is the worst of its checks, and a file's is the
worst of its declarations.

`--sort severity` (the default) puts the worst thing in the codebase first.
`--sort path` groups by file path instead, which is stable regardless of the
metrics and is the better choice when diffing two runs. Both are
deterministic: severity ties break on location.

`--limit` caps the human report only. When it bites, roe says how many
findings it held back:

```text theme={null}
  … and 70 more — re-run with --limit 0 to see all
```

<Note>
  `--sort` and `--limit` are presentation only and don't apply to
  `--format json`, which always emits every finding. Tooling does its own
  ordering, and a silently truncated array would be actively misleading.
</Note>

### Large types report a breakdown

Thirty auto-properties is a data holder; thirty methods is a god class. roe
doesn't guess which one you have — it prints the composition and lets you
judge:

```text theme={null}
     6:21  Simulation.Units.Unit
           34/20 members (19 properties, 15 methods)
```

Enum cases don't count towards a type's size. An enum's cases are its whole
point, and counting them would report a 25-case enum as a god class.

`const` fields don't count either, for the same reason: a `const` has no
behaviour and no state at runtime — it's a name for a literal, inlined at
every call site — so it can't be part of the cohesion problem this check
looks for. A class holding fifty tuning constants is a lookup table, not a god
class. `static readonly` fields *do* still count: roe has no type analysis, so
it can't tell a `static readonly float` tuning value from a `static readonly
HttpClient` that the type genuinely depends on.

### Only required parameters count

The point of a parameter limit is call-site burden: how much a caller has to
supply, and how much they have to keep straight while doing it. A parameter
the caller can leave out costs them nothing, so it isn't counted.

A parameter is **required** when the caller has to pass something and think
about what. That means:

| Parameter                    | Counts | Why                                                                             |
| ---------------------------- | ------ | ------------------------------------------------------------------------------- |
| `int amount`                 | Yes    | The caller has to supply it.                                                    |
| `ref int total`              | Yes    | Supplied, and the caller has to reason about it being mutated.                  |
| `in ReadOnlySpan<byte> data` | Yes    | Supplied; `in` is a calling convention, not an escape hatch.                    |
| `this Widget widget`         | Yes    | The extension receiver — written as `widget.Method(…)`, but still an argument.  |
| `int retries = 3`            | No     | Omit it and the default applies.                                                |
| `params object[] extras`     | No     | Omit it and it's empty.                                                         |
| `out int result`             | No     | The caller supplies nothing; it's a return value wearing a parameter's clothes. |

Findings print the whole picture, so nothing is hidden — the metric is the
required count, and the declared signature follows in parentheses:

```text theme={null}
    42:17  App.Reporting.Builder.Build
           8/5 params (11 declared: 8 required, 2 optional, 1 out)
```

When every parameter is required, the parenthetical is dropped: repeating
`6 declared: 6 required` next to `6/5 params` would be noise.

<Note>
  A method with a dozen `out` parameters is still worth a second look — it's
  usually asking to return a `record` instead. roe just doesn't report it
  under a check whose stated meaning is "too much to pass in". The declared
  total is always printed, so a long signature is still visible in the report
  and in `--format json`.
</Note>

### Overloads

Overloads share a name, so two flagged rows could otherwise be
indistinguishable. Where that collision actually happens, roe appends a
Roslyn-style arity suffix — and only there, since an arity on a name with no
overloads is noise:

```text theme={null}
    95:28  Simulation.Core.GameSimulation.TryBeginActivation/0
   197:28  Simulation.Core.GameSimulation.TryBeginActivation/1
```

### Circular dependencies

A cycle is reported as a chain of real references, each type pointing at the
next and the last pointing back at the first. Larger tangles usually contain
more types than any single loop through them touches; those are listed
separately rather than being spliced into the path, which would imply
references that don't exist:

```text theme={null}
  App.Orders.Order → App.Orders.Invoice → App.Orders.Order
    src/App/Orders/Order.cs 12:14
    src/App/Orders/Invoice.cs 8:14
    + 2 more types in this cycle: App.Orders.Line, App.Orders.Tax
```

## Hotspots

`--hotspots` reads git history and ranks files by complexity multiplied by
recent churn — the files that are both hard to understand and constantly
being changed, which is where refactoring pays off most. Commits are weighted
by recency on a 90-day half-life, so last month's churn counts for more than
last year's.

```bash theme={null}
roe health --hotspots --top 3
```

```text theme={null}
hotspots (complexity × churn over 52 commit(s))
     37  Simulation.Cli/AsciiRenderer.cs  complexity 87 over 328 line(s), 8.7 weighted commit(s)
     34  Simulation/AI/MovePlanner.cs  complexity 126 over 651 line(s), 10.8 weighted commit(s)
     30  Simulation/Units/Unit.cs  complexity 43 over 166 line(s), 7.1 weighted commit(s)
```

Scores are relative: the riskiest file in the run scores `100` and everything
else is measured against it, so they compare files within one run rather than
across runs or repositories.

Hotspots are informational and **never affect the exit code** — every
codebase has a riskiest file, and failing a build over the existence of a
ranking would make the check useless as a CI gate.

<Note>
  This is the one part of roe that reads git history, so the analysis root
  must be inside a git repository. If it isn't, `--hotspots` fails with exit
  `2` rather than silently reporting nothing.
</Note>

## Baselines

Turning `roe health` on over a codebase that didn't have it from day one
means starting at a few hundred findings, which fails every build until
someone fixes all of them. A baseline records what's already there so CI can
gate on **new** debt from the first day, and the existing debt gets paid down
on its own schedule.

```bash theme={null}
roe health --write-baseline roe-baseline.json   # accept today's findings
roe health --baseline roe-baseline.json         # fail only on new ones
```

`--write-baseline` writes the file, reports what it recorded on stderr, and
exits `0` without printing a report:

```text theme={null}
wrote 119 finding(s) and 1 cycle(s) to roe-baseline.json
```

Commit that file. From then on, `--baseline roe-baseline.json` reports and
exits on new findings only, and the footer says how many it hid:

```text theme={null}
✓ no health issues found · 3 project(s), 160 file(s) scanned in 274 ms
  120 baselined finding(s) hidden
```

The two flags conflict — writing a baseline through a baseline would record a
filtered picture of the codebase.

### The file

```json roe-baseline.json theme={null}
{
  "version": 1,
  "findings": [
    {
      "kind": "too-many-parameters",
      "name": "App.Units.Unit..ctor",
      "file": "src/App/Units/Unit.cs",
      "metric": 16
    }
  ],
  "cycles": [
    { "members": ["App.Orders.Invoice", "App.Orders.Order"] }
  ]
}
```

Entries are written sorted by `kind` and then `name`, so the file diffs
cleanly and a regenerated baseline shows only what actually changed. Unknown
fields and unknown `version`s are rejected rather than ignored, the same way
[config files](/configuration) are.

### What matches

A finding is hidden when its **kind and name** match a baseline entry. The
line number is deliberately not part of the match: a baselined method stays
baselined when something above it in the file grows by three lines, which is
the whole point of recording it once. `file` is written for readability and
diffing and isn't matched on either.

Cycles match on their set of member names, regardless of the order the path
happens to be printed in.

### Metric regressions still report

A matched finding whose metric is **higher** than the baselined value is
reported anyway. A method going from cyclomatic 12 to 30 is new debt in an
old place, and a baseline that hid it would let a rewrite land unexamined
under the cover of an entry that was about something much smaller. Equal or
lower stays hidden.

### Stale entries

A baseline entry that no longer matches anything — the method was fixed,
renamed, or deleted — is reported as a warning, never a failure:

```text theme={null}
warning: 4 stale entries in roe-baseline.json no longer match any finding — regenerate it with `roe health --write-baseline roe-baseline.json`
```

Regenerating after a green run is the ratchet: fixed debt can't come back
under the entry that used to cover it.

### In CI

```yaml theme={null}
- name: Check code health
  run: roe health --baseline roe-baseline.json
```

The step passes while the codebase only carries its known debt, and fails the
moment a pull request adds a finding that isn't in the file. Set
`health.baseline` in a [config file](/configuration) instead if you'd rather
a bare `roe` and `roe check` picked it up too.

## Suppressing findings

Individual findings take [inline suppression
comments](/suppressing-findings) using the rule names `high-complexity`,
`high-cognitive-complexity`, `long-method`, `too-many-parameters`,
`large-file`, and `large-type`:

```csharp theme={null}
// roe-ignore-next-line high-complexity
public void ParseEverything(string input)
```

Circular dependencies span multiple files, so — like duplicates — they have
no inline comment. Use ignore globs in a [config file](/configuration)
instead — `health.ignore` scopes the suppression to health alone, so the
file keeps its dead-code and dupes coverage.
