Developer Tools

RAFF

A Rust CLI that turns architecture rules into checks: component size, dependencies that point towards stability, and change hotspots from Git history. It runs as a pre-commit hook or in CI, so code written by agents has to pass the same rules as mine.

Active · 2025

Rust · CLI

RAFF

raff checks the shape of a Rust codebase. It measures how big each component is, which way the dependencies between point, and which parts keep changing. It runs as a CLI, a or a step, and it fails when a rule you set is broken.

I wrote the first version over three days in June 2025, after Mark Richards’ Architecture: The Hard Parts workshop at DDD Europe. The idea I took home was : architecture decisions written as automated checks, so they still hold after the people who made them stop watching. I wanted those for Rust.

What kept me working on it is . An agent will add a fourteenth responsibility to a module, or import a type from whichever crate happens to have it, and the tests still pass. Review catches some of that. raff catches the parts that can be counted, on every commit, before anyone reads the diff.

What it measures

CheckWhat raff computesFromFails when
Statement countEach crate’s (or top-level directory’s) share of all statementsThe syntax tree of every .rs fileA share passes --threshold (default 10%). Error.
CouplingCe, Ca and instability per crate and modulecargo metadata and use paths in the sourceA crate depends on a less stable crate. Warning.
VolatilityCommit touches + α × lines changed, per crateGit history of .rs filesA crate is in the top quarter by score. Warning.
rust-code-analysisCyclomatic complexity, Halstead metrics, lines of codeMozilla’s rust-code-analysis-cliNever. It reports.
Contributor reportCommits, lines and files per author, weighted towards recent onesGit historyNever. It reports.

Every command prints a table by default. Depending on the check it can also write JSON, YAML, CSV, HTML or . For CI there are two more: , which GitHub turns into pull-request annotations, and for everything else.

Size: share of statements

raff parses every file with and counts , so formatting and comments don’t move the number. At the root of a it sums them per crate, using cargo metadata to work out which crate owns each file. Anywhere else it sums them per top-level directory under --path. It fails if any one of them holds more than the threshold’s share of the total.

Here it is at the root of ripgrep:

Horizontal bar chart of each ripgrep crate's share of 12,434 statements. ripgrep has 30.5 percent, ignore 19.6, grep-printer 15.7 and grep-searcher 12.3, all above the dashed 10 percent threshold. grep-regex, globset, grep-index, grep-cli, grep-matcher, grep-pcre2 and grep are below it. A dotted line marks an even split at 9.1 percent.
ripgrep at commit 3fce3b5. The ripgrep crate builds from crates/core, and raff counts those files towards it. Four of eleven crates are over the default threshold.

ripgrep fails the default, and I wouldn’t call ripgrep badly structured. With eleven crates an even split is already 9.1%, so a 10% cap asks almost every crate to be smaller than average. The default is too tight for most repositories. Treat the threshold as a budget you set per repo and then defend. raff’s own config uses 15%.

Coupling: which way dependencies point

For each crate raff counts its (Ce, the crates it depends on) and its (Ca, the crates that depend on it). is Ce / (Ce + Ca). A crate at 0 is depended on and depends on nothing, so changing it is expensive. A crate at 1 is a leaf that nothing imports.

The first version of this rule was wrong. raff 0.1 warned about any crate with an instability above 0.7. In June 2026 I added it as a pre-commit hook to a private workspace of 106 crates. Ten hours later I made the hook advisory, because it was blocking every commit to three crates. The warnings were for the server binary (I = 1.00) and a background workers crate (I = 0.93). Nothing imports a binary. An instability of 1.0 is the correct number for it, and the rule was flagging good design.

Robert C. Martin, who defined the metric, never said instability should be low. His rule is the : depend in the direction of stability. So raff now checks every internal dependency A → B and warns when B is less stable than A. It also ignores , because a crate you only use in tests or benchmarks doesn’t shape the architecture.

WorkspaceCratesInternal dependencies, excluding devWarnings from raff 0.1.4 (I > 0.7)Stable Dependencies findings
ripgrep111630
Helix143731
Private (mine)106314175

The old rule warned about each workspace’s binaries, its top-level crates and its test harnesses. Eight of the seventeen private warnings were crates at exactly 1.0, and the new check flags none of them.

Helix’s one finding is small and real:

Dot plot of Helix's fourteen crates ordered by instability, from helix-dap-types, helix-event, helix-parsec and helix-stdx at 0 up to xtask at 1. A shaded band above 0.7 marks where the old rule warned, covering helix-term and xtask. A red arrow runs from helix-tui at 0.67 to helix-view at 0.70, labelled depends on a less stable crate. A right-hand column lists Ce and Ca for each crate.
Helix at commit 079a789e8, dev-dependencies excluded. 36 of 37 internal dependencies point towards stability.

helix-tui is the terminal-drawing library. helix-view is the editor’s model of documents, views and themes. You’d expect the drawing library to sit underneath, but helix-tui takes its geometry and style types from helix-view: 28 imports of helix_view::graphics, for Rect, Style and similar. The gap is 0.67 against 0.70, so no instability threshold would ever notice it. The fix is to move graphics into a crate below both.

The private workspace’s five findings were more useful. Four of them trace back to one crate: a transcription facade that re-exports a trait crate, one vendor’s implementation and a test mock. Two other crates, one of them a rival vendor’s adapter, depended on the facade while using only three types from the trait crate. So the rival adapter compiled its competitor’s client and a mock into production code. The fix is one line in each Cargo.toml.

Volatility: what keeps changing

Volatility scores each crate by how often and how much it changes:

score = commit touches + α × (lines added + lines deleted)

α defaults to 0.01, so 100 changed lines weigh the same as one extra commit. Checking this rule against Helix for this write-up turned up two bugs. The code computed the formula the other way round from its own help text, so the score was almost pure . It also counted every file in a crate’s directory, and helix-view keeps text-encoding test fixtures of about 24,000 lines each.

Rankraff 0.1.4Fixed
1helix-view, 1,141,607helix-term, 4,115 (2,636 commits)
2helix-term, 150,674helix-core, 1,471 (844 commits)
3helix-core, 63,229helix-view, 1,346 (962 commits)

The fixed ranking puts the terminal front end first, which is where most of Helix’s commits land. The old one was measuring test fixtures.

Wiring it into commits

repos:
  - repo: local
    hooks:
      - id: raff-architecture-check
        name: Architecture fitness functions
        entry: raff --profile pre-commit all
        language: system
        pass_filenames: false
        files: '(^|/)Cargo.(toml|lock)$|.rs$'

The pre-commit profile runs the fast checks on staged files only, prints one line when everything passes and a table when something doesn’t, and treats warnings as failures. It evaluates the Stable Dependencies check on the whole workspace graph but only reports edges that touch a crate you’ve staged, so an old violation elsewhere doesn’t block unrelated work. Statement share is skipped here, because a percentage of a partial tree means nothing. Run raff all in CI for that, with --ci-output sarif --output-file raff.sarif if you want findings on the pull request.

The failure text is written for whoever has to act on it:

Crate 'helix-tui' (I=0.67) depends on less stable crate 'helix-view' (I=0.70), violating the Stable Dependencies Principle

It names both crates and both numbers. A human can act on that, and so can an agent. Telling an agent to “keep the architecture clean” in a prompt gives you nothing to check. A hook that rejects the commit with that line gives the agent a specific dependency to fix.

Install

cargo install raff-cli
# or
brew install liamwh/raff/raff

The binary is raff. raff all runs every check against the current directory and prints an HTML report. The complexity metrics also need rust-code-analysis-cli on your PATH.

Open chat

Interested in working together? Reach out.

Strategy, architecture, and implementation — from workflow to production.

© 2026 Liam Woodleigh. All rights reserved.