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 checks the shape of a Rust codebase. It measures how big each component is, which way the dependencies between Crate Rust's unit of compilation and packaging, roughly one library or one program. Each crate lists the crates it depends on in its Cargo.toml. point, and which parts keep changing. It runs as a CLI, a Pre-commit hook A script Git runs before it records a commit. If the script fails, the commit does not happen. or a CI Continuous integration. A server builds and tests every push, so problems show up before code is merged. 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 Fitness function An automated check that scores how well a system keeps an architectural property you care about, such as small components or dependencies that point one way. The term comes from evolutionary computing, by way of the book Building Evolutionary Architectures.: 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 Coding agent An AI model given tools to edit files and run commands, so it can carry out a programming task end to end instead of only suggesting code.. 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
| Check | What raff computes | From | Fails when |
|---|---|---|---|
| Statement count | Each crate’s (or top-level directory’s) share of all statements | The syntax tree of every .rs file | A share passes --threshold (default 10%). Error. |
| Coupling | Ce, Ca and instability per crate and module | cargo metadata and use paths in the source | A crate depends on a less stable crate. Warning. |
| Volatility | Commit touches + α × lines changed, per crate | Git history of .rs files | A crate is in the top quarter by score. Warning. |
| rust-code-analysis | Cyclomatic complexity, Halstead metrics, lines of code | Mozilla’s rust-code-analysis-cli | Never. It reports. |
| Contributor report | Commits, lines and files per author, weighted towards recent ones | Git history | Never. It reports. |
Every command prints a table by default. Depending on the check it can also write JSON, YAML, CSV, HTML or Graphviz DOT A plain-text format for describing graphs. Graphviz turns it into a diagram of boxes and arrows.. For CI there are two more: SARIF Static Analysis Results Interchange Format, a JSON format for code-analysis findings. GitHub reads it and shows each finding as an annotation on the pull request., which GitHub turns into pull-request annotations, and JUnit XML A test-report format that started with Java's JUnit. Most CI systems, including Azure DevOps and GitLab, display it as a list of passed and failed checks. for everything else.
Size: share of statements
raff parses every file with syn The standard Rust library for parsing Rust source code into an AST. raff uses it to count statements and to find which modules refer to which. and counts Statement One instruction in the code, such as a let binding or a function call. Counting statements measures size more fairly than counting lines, because formatting and comments do not change the count., so formatting and comments don’t move the number. At the root of a Workspace A set of Rust crates in one repository that share a build and a lockfile. Large projects split into dozens of crates this way. 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:
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 Efferent coupling (Ce) How many other components this one depends on. Efferent means outgoing. (Ce, the crates it depends on) and its Afferent coupling (Ca) How many other components depend on this one. Afferent means incoming. (Ca, the crates that depend on it). Instability (I) Robert C. Martin's ratio Ce / (Ce + Ca). At 0, others depend on the component and it depends on nothing, so changing it is expensive. At 1, nothing depends on it and it can change freely. Neither end is bad by itself. 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 Stable Dependencies Principle Robert C. Martin's rule that a component should depend only on components more stable than itself. When a stable crate depends on an unstable one, every change to the unstable crate ripples into code that many others rely on.: 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 Dev-dependency A crate needed only for tests, examples or benchmarks. It never ends up in the shipped program, so it says nothing about the architecture., because a crate you only use in tests or benchmarks doesn’t shape the architecture.
| Workspace | Crates | Internal dependencies, excluding dev | Warnings from raff 0.1.4 (I > 0.7) | Stable Dependencies findings |
|---|---|---|---|---|
| ripgrep | 11 | 16 | 3 | 0 |
| Helix | 14 | 37 | 3 | 1 |
| Private (mine) | 106 | 314 | 17 | 5 |
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:
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 Churn Lines added plus lines deleted in version control over a period. It measures how much code moved, not how often it changed.. It also counted every file in a crate’s directory, and helix-view keeps text-encoding test fixtures of about 24,000 lines each.
| Rank | raff 0.1.4 | Fixed |
|---|---|---|
| 1 | helix-view, 1,141,607 | helix-term, 4,115 (2,636 commits) |
| 2 | helix-term, 150,674 | helix-core, 1,471 (844 commits) |
| 3 | helix-core, 63,229 | helix-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.