CLI · commit a0ece2a

reviewroute

CODEOWNERS linter, coverage analyzer & smart reviewer suggester

Download reviewroute.zip
Quickstart
unzip reviewroute.zip && cd reviewroute
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

reviewroute

CODEOWNERS files rot: patterns that match nothing, owners who left, files nobody owns, and a few people drowning in review requests. reviewroute lints your CODEOWNERS against the real repository, roster and git history, measures ownership coverage, and suggests a minimal, load-balanced set of reviewers for a change.

Features

Install

uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'

Requires Python 3.12+, git on PATH.

Quickstart

.venv/bin/python examples/make_repo.py            # builds examples/demo-repo (6 teams, 15 devs, 40 dirs)
export PATH="$PWD/.venv/bin:$PATH"
reviewroute --repo examples/demo-repo lint
reviewroute --repo examples/demo-repo coverage
reviewroute --repo examples/demo-repo who services/billing/main.py
reviewroute --repo examples/demo-repo suggest --base main --author carla --load examples/demo-repo/load.csv
reviewroute --repo examples/demo-repo generate

The demo repo has a deliberately flawed .github/CODEOWNERS, an owners.yaml roster (three departed developers), an unavailable.yaml vacation list and a load.csv of open review counts. Its checked-out branch feature/refund-flow is one commit (touching four files) ahead of main. Outputs below were captured with --now 2026-10-16 (and make_repo.py --now 2026-10-16) so they are reproducible.

Usage

Global options go before the subcommand:

Option Meaning
--repo PATH repository root (default .)
--codeowners FILE default: .github/CODEOWNERS, CODEOWNERS, docs/CODEOWNERS, .gitlab/CODEOWNERS
--roster FILE owners.yaml; default <repo>/owners.yaml if present
--gitlab parse GitLab sections and approval counts
--now YYYY-MM-DD reference date for decay, inactivity and vacations (default today)

lint

reviewroute lint [--format rich|json|github] [--fail-on error|warning|never] [--inactive-days 180] [--github-validate]

Exit code 1 when a finding at or above --fail-on exists (default error), 2 on usage errors.

┏━━━━━━┳━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Line ┃ Code  ┃ Severity ┃ Message                                                                          ┃
┡━━━━━━╇━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 2    │ RR010 │ warning  │ team `@acme/platform` still lists departed member(s): erin                       │
│ 3    │ RR006 │ warning  │ pattern `/docs/` is overridden by the identical pattern at line 35               │
│ 5    │ RR007 │ error    │ catch-all `*` placed after 3 specific rule(s) (lines 2, 3, 4); it overrides      │
│      │       │          │ them. Move it to the top.                                                        │
│ 7    │ RR005 │ error    │ owner `@erin` is inactive (departed)                                             │
│ 8    │ RR008 │ warning  │ owner `@alice` made no changes in the 7 file(s) it owns during the last 180 days │
│ 9    │ RR010 │ warning  │ team `@acme/security` still lists departed member(s): omar                       │
│ 11   │ RR002 │ warning  │ pattern `/services/payment-gateway/` matches no files in the repository          │
│ 12   │ RR004 │ error    │ owner `@acme/sre-oncall` is not in the roster                                    │
│ 17   │ RR008 │ warning  │ owner `@acme/platform` made no changes in the 4 file(s) it owns during the last  │
│      │       │          │ 180 days                                                                         │
│ 19   │ RR004 │ error    │ owner `@pavel-contractor` is not in the roster                                   │
│ 20   │ RR005 │ error    │ owner `@kira` is inactive (departed)                                             │
│ 20   │ RR010 │ warning  │ team `@acme/mobile` still lists departed member(s): kira                         │
│ 27   │ RR003 │ warning  │ rule `/infra/k8s/` never takes effect: every file it matches is claimed by later │
│      │       │          │ rule(s) at line 28                                                               │
│ 29   │ RR008 │ warning  │ owner `@acme/platform` made no changes in the 6 file(s) it owns during the last  │
│      │       │          │ 180 days                                                                         │
│ 30   │ RR005 │ error    │ owner `@omar` is inactive (departed)                                             │
│ 31   │ RR005 │ error    │ owner `@erin` is inactive (departed)                                             │
│ 34   │ RR002 │ warning  │ pattern `*.proto` matches no files in the repository                             │
│ 36   │ RR001 │ error    │ negation with `!` is not supported in CODEOWNERS                                 │
│ 37   │ RR001 │ error    │ invalid owner `platform-team` (use @user, @org/team or an email)                 │
└──────┴───────┴──────────┴──────────────────────────────────────────────────────────────────────────────────┘
9 error(s), 10 warning(s) in .github/CODEOWNERS
Code Severity Meaning
RR001 error invalid syntax (negation, bad owner, bad pattern, bad section header)
RR002 warning pattern matches no files
RR003 warning rule never takes effect (fully shadowed by later rules)
RR004 error owner not in roster (or not found via GitHub API)
RR005 error owner is inactive (departed) in the roster
RR006 warning duplicate pattern
RR007 error catch-all placed after specific rules
RR008 warning owner made no changes in the files it owns during --inactive-days
RR009 warning syntax that parses but is unsupported ([a-z] ranges, embedded **)
RR010 warning team lists departed members

--format github prints workflow annotations such as ::error file=.github/CODEOWNERS,line=5,title=RR007::catch-all ....

coverage

Files owned: 257/276 (93.1%)
Lines owned: 10169/11151 (91.2%)
Ownership kind: team-only 191, individual-only 21, mixed 45 (team ratio 90%)

          Unowned hotspots (by churn)           
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┓
┃ File                         ┃ Churn ┃ Lines ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━┩
│ legacy/reports/schema.sql    │   154 │   102 │
│ apps/docs-site/main.py       │   148 │   112 │
│ apps/docs-site/util.py       │   136 │   100 │
│ apps/docs-site/README.md     │   112 │    64 │
│ data/dashboards/test_main.py │   108 │    68 │
└──────────────────────────────┴───────┴───────┘
             Owner concentration              
┏━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓
┃ Owner          ┃ Files ┃ % of repo ┃ Lines ┃
┡━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━┩
│ @acme/platform │    89 │     32.2% │  2299 │
│ @acme/web      │    43 │     15.6% │  1752 │
│ @acme/payments │    29 │     10.5% │  1626 │
│ @acme/data     │    28 │     10.1% │  1150 │
│ @acme/security │    22 │      8.0% │  1098 │
└────────────────┴───────┴───────────┴───────┘
              Single-owner directories (bus factor 1)               
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━━┓
┃ Directory            ┃ Owner          ┃ Files ┃ Churn ┃ Critical ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━━┩
│ services/ledger/     │ @alice         │ 7     │ 710   │ yes      │
│ apps/mobile-android/ │ @acme/mobile   │ 8     │ 656   │ yes      │
│ services/auth/       │ @acme/security │ 8     │ 598   │ yes      │
│ legacy/billing-v1/   │ @erin          │ 8     │ 526   │ no       │
│ libs/core/           │ @farid         │ 6     │ 178   │ no       │
└──────────────────────┴────────────────┴───────┴───────┴──────────┘

--format json emits the same data (plus percentages) for dashboards.

who

line 5    *  ->  @acme/platform  overridden
line 7    /services/billing/  ->  @acme/payments @erin (inactive)  WINS

services/billing/main.py is owned by: @acme/payments @erin

line 5    *  ->  @acme/platform  overridden
line 10   /services/gateway/  ->  @acme/platform  WINS

services/gateway/util.py is owned by: @acme/platform

suggest

reviewroute suggest (--base REF | --files PATH... | --files-from FILE) [--author LOGIN] [--unavailable FILE] [--load CSV | --github-repo OWNER/NAME] [--half-life 90] [--load-penalty 0.1] [--format rich|json]

4 changed file(s), 4 required owner group(s)

               Required owner groups                
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ Group (any one of) ┃ Files ┃ Eligible candidates ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ @acme/web          │ 1     │ gwen, hiro, ines    │
│ @acme/platform     │ 1     │ dmitri, farid       │
│ @alice             │ 1     │ alice               │
│ @acme/payments     │ 1     │ alice               │
└────────────────────┴───────┴─────────────────────┘
Suggested reviewers
  @alice: covers @alice; @acme/payments. expertise 0.25 (4 decayed lines, last touched 262d ago), 4 open 
review(s), score -0.15
  @gwen: covers @acme/web. expertise 1.00 (16 decayed lines, last touched 48d ago), 2 open review(s), score 
0.80
  @farid: covers @acme/platform. expertise 0.46 (7 decayed lines, last touched 188d ago), 1 open review(s), 
score 0.36
Excluded @bram: unavailable until 2026-10-26 (vacation)
Excluded @carla: is the change author
Excluded @erin: inactive in roster

Here @alice covers both the @alice and @acme/payments groups, so she is chosen even though her expertise is low and her load high: the set is minimal first, scored second. @bram is on vacation, @carla is the author, @erin has left. See docs/scoring.md.

With --gitlab, section approval counts are honoured ([Payments][2] needs two distinct reviewers) and optional ^[Sections] require nothing:

2 changed file(s), 2 required owner group(s)

               Required owner groups                
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ Group (any one of) ┃ Files ┃ Eligible candidates ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ @acme/platform     │ 2     │ dmitri, farid       │
│ @acme/payments     │ 1     │ alice, carla        │
└────────────────────┴───────┴─────────────────────┘
Suggested reviewers
  @carla: covers @acme/payments. expertise 1.00 (23 decayed lines, last touched 19d ago), 0 open review(s), 
score 1.00
  @farid: covers @acme/platform. expertise 0.87 (20 decayed lines, last touched 13d ago), 0 open review(s), 
score 0.87
  @alice: covers @acme/payments. expertise 0.18 (4 decayed lines, last touched 262d ago), 0 open review(s), 
score 0.18
Excluded @bram: unavailable until 2026-10-26 (vacation)
Excluded @erin: inactive in roster

generate

reviewroute generate [--depth 2] [--min-share 0.25] [--min-commits 2] [--max-owners 3] [--team-share 0.6] [--half-life 180] [--default-owner @org/team] [-o FILE]

# Generated by reviewroute 0.1.0 from git history (half-life 180d, min share 25%). Review before committing.

/.github/              @acme/platform  # 100% dmitri
/apps/admin/           @acme/web  # 50% ines, 30% gwen, 18% hiro
/apps/docs-site/       @acme/web  # 44% hiro, 33% gwen, 22% ines
/apps/mobile-android/  @acme/mobile  # 100% jonas
/apps/mobile-ios/      @acme/mobile  # 93% jonas, 6% dmitri
/apps/web/             @acme/web  # 34% hiro, 27% ines, 27% gwen
/data/                 @acme/data  # 40% liam, 37% mira, 8% noor
/docs/                 @acme/web  # 32% gwen, 28% ines, 23% hiro
/docs/runbooks/        @acme/platform  # 100% dmitri
/infra/                @acme/platform  # 42% farid, 38% dmitri, 10% liam
/legacy/billing-v1/    @acme/payments  # 44% alice, 29% carla, 26% bram
/legacy/reports/       @acme/data  # 47% mira, 41% liam, 10% dmitri
/libs/config/          @acme/platform  # 100% farid
/libs/core/            @acme/platform @alice  # 37% farid, 36% dmitri, 25% alice
/libs/db/              @acme/mobile @dmitri  # 61% jonas, 38% dmitri
/libs/http/            @acme/platform  # 63% farid, 36% dmitri
/libs/logging/         @acme/platform  # 70% farid, 29% dmitri
/libs/testing/         @acme/platform  # 100% dmitri
/security/             @acme/security  # 100% noor
/services/             @acme/payments  # 30% bram, 19% carla, 13% alice
/services/auth/        @acme/security  # 100% noor
/services/gateway/     @acme/platform  # 61% farid, 38% dmitri
/services/search/      @acme/data  # 72% liam, 27% mira
/tests/                @acme/web  # 32% ines, 27% hiro, 23% gwen
/tests/perf/           @acme/platform  # 100% dmitri
/tools/                @acme/platform  # 65% dmitri, 19% farid, 15% noor

Departed and non-roster authors are ignored, a directory collapses to a team when the team's members together reach --team-share, shallow directories without a clear owner defer to their children, and a child is only emitted when it differs from its parent.

Input files

owners.yaml

users:
  alice: {name: Alice Ng, emails: [alice@acme.example], active: true}
  erin:  {name: Erin Walsh, emails: [erin@acme.example], active: false}
teams:
  acme/payments: {members: [alice, bram, carla]}

Emails map git authors to logins. Teams are referenced in CODEOWNERS as @acme/payments.

unavailable.yaml

unavailable:
  - {user: bram, from: 2026-10-13, until: 2026-10-26, reason: vacation}

load.csv: user,open_reviews per line.

Configuration

Everything is command-line options; the GitHub API is optional and needs GITHUB_TOKEN: lint --github-validate checks @user and @org/team owners when no roster is given, and suggest --github-repo owner/name counts requested reviewers on open pull requests. Tests never touch the network.

Architecture

Module Role
patterns.py CODEOWNERS pattern to regex compiler, catch-all detection
parser.py line parser, owners, GitLab sections, rule resolution
ownership.py resolves every repo file against the rules once (hits, winners, per-rule matches)
roster.py owners.yaml, unavailable.yaml, owner expansion
history.py git helpers: file list, diff, log --numstat into change events
lint.py all lint checks
coverage.py coverage, hotspots, concentration, bus factor
scoring.py exponential-decay expertise
suggest.py required groups, greedy set cover, explanations
generate.py CODEOWNERS draft generation
github.py optional API access behind an injectable fetch
cli.py click commands and rich/JSON/annotation output

Deeper notes: docs/codeowners-semantics.md, docs/scoring.md.

Tests

.venv/bin/python -m pytest -q

Covers a pattern-matching table drawn from the GitHub docs, last-match-wins, GitLab sections, every lint code, coverage math, decay scoring, set cover (required groups covered, author and vacation excluded, approval counts), generation, the GitHub client with a fake transport, and the CLI end to end on the synthetic repo.

Limitations