gruff-rs 0.3.0

Rust static analyzer and quality linter for CI: dead-code, complexity, security, secrets, and architecture rules with deterministic SARIF/JSON output and baseline support.
gruff-rs-0.3.0 is not a library.

gruff-rs

Crates.io Docs.rs License: MIT OR Apache-2.0

gruff-rs is an opinionated quality analyzer for Rust projects. It scans Rust source, Cargo metadata, and common project files, then emits deterministic reports for terminals, CI annotations, SARIF consumers, static HTML, and a local dashboard. It is heuristic static analysis; run it beside Clippy, cargo audit, rustfmt, tests, and code review, not instead of them.

Mission

gruff governs AI-generated code so a human who didn't write it can read, review, and trust it. Coding agents routinely produce code that superficially works while misunderstanding the requirement, and gruff exists to make that gap visible to a reviewer. Run as a coding-agent hook, it guides — or forces — the agent toward code a person can actually sign off on:

  • Verifiable — legible enough that a reviewer can confirm it does what was asked.
  • Secure — hardened where human review is weakest.
  • Genuinely tested — tests that exercise the contract, not low-signal bloat or ceremony.

Doc comments are mandatory even on a private one-liner: forcing the agent to state intent, usage, contract, and failure behaviour in prose gives the reviewer something to check the implementation against. A mismatch between the doc comment and the code is the signal that a change needs a deeper look.

Status At A Glance

Field Value
Release line Published 0.3.0 package line
Runtime Prebuilt binary, or Rust 1.82+ when building from source
Package gruff-rs on crates.io
Binary gruff-rs
Rule catalogue 87 rules across 11 pillars
Primary config .gruff-rs.yaml (requires schemaVersion: gruff-rs.config.v1)
Analysis schema gruff.analysis.v2
Baseline schema gruff.baseline.v1
Severity gate --fail-on with none, advisory, warning, error; per-subcommand defaults via minimumSeverity: in .gruff-rs.yaml
Dashboard 127.0.0.1:8766 by default

Rule IDs, fingerprints, baseline identity, JSON schema version, and SARIF behavior are the stable contract for the 0.3.x line.

Requirements

  • No Rust toolchain is needed when using a prebuilt binary through cargo-binstall.
  • Rust 1.82+ is required when building from source with Cargo.
  • Git is not used by default; Git-backed changed-region modes run only when --diff/--since is explicitly passed.

Install

Install into a repository-local tool directory:

cargo install gruff-rs --locked --version 0.3.0 --root ./.cargo-tools
./.cargo-tools/bin/gruff-rs init
./.cargo-tools/bin/gruff-rs summary .

Prebuilt binary, also into a repository-local tool directory:

cargo binstall gruff-rs --root ./.cargo-tools
./.cargo-tools/bin/gruff-rs init
./.cargo-tools/bin/gruff-rs summary .

From a source checkout:

cargo install --path . --locked --root ./.cargo-tools
./.cargo-tools/bin/gruff-rs --help

Quick Start

# Create a starter config.
./.cargo-tools/bin/gruff-rs init

# Review the current finding mix.
./.cargo-tools/bin/gruff-rs summary .

# Explore without failing because of findings.
./.cargo-tools/bin/gruff-rs analyse . --fail-on none

# Gate on warning and error findings.
./.cargo-tools/bin/gruff-rs analyse . --fail-on warning

# Emit SARIF for code scanning.
./.cargo-tools/bin/gruff-rs analyse . --format sarif --fail-on none > gruff-rs.sarif

# Generate a fresh-start baseline.
./.cargo-tools/bin/gruff-rs analyse . --generate-baseline --fail-on none

Commands

Command Purpose
analyse [paths...] Run the analyzer and print findings.
summary [paths...] Print compact score, pillar, rule, and file summaries.
report [paths...] Render an HTML or JSON report to stdout or --output.
init Generate a starter .gruff-rs.yaml.
list-rules Print rule metadata as text or JSON, optionally filtered by selector.
dashboard Serve the local browser dashboard.
completion [shell] Print a shell completion script.

Output Formats

analyse --format <fmt> accepts:

Format Use it for
text Human terminal output.
json Full gruff.analysis.v2 report.
sarif SARIF 2.1.0 for code scanning.
html Self-contained inspection report.
markdown Pull-request or issue comment summary.
github GitHub Actions workflow annotations.
hotspot gruff.hotspot.v1 file-offender JSON.

report --format <fmt> accepts html and json.

Exit Codes

Code Meaning
0 Run completed and no finding met --fail-on.
1 At least one finding met --fail-on.
2 Fatal diagnostic such as config failure, missing path, parse error, baseline error, diff failure, or invalid input.

analyse defaults to --fail-on advisory; report defaults to --fail-on none. Set per-project defaults with the minimumSeverity: block in .gruff-rs.yaml; the CLI flag always overrides the config value (see ADR-013).

CI Usage

Generic CI command:

./.cargo-tools/bin/gruff-rs analyse . --format github --fail-on warning --no-baseline

The repo ships a composite GitHub Action:

jobs:
  gruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: blundergoat/gruff-rs@v0.3.0
        with:
          args: analyse . --format sarif --fail-on warning --no-baseline

The action installs the matching binary via cargo-binstall and runs gruff-rs with the supplied args. Pin to a tag for reproducibility. See action.yml for inputs.

Configuration

gruff-rs reads .gruff-rs.yaml by default. Use --config <path> to pass another YAML file, or --no-config to ignore project config. Unknown keys, unknown rule IDs, unknown selectors, and invalid threshold shapes fail closed.

Every config must declare schemaVersion: gruff-rs.config.v1 as the first key. Configs without it are rejected at load time; run gruff-rs init --force to regenerate.

The optional minimumSeverity: block sets per-subcommand defaults for --fail-on so CI invocations can omit the flag. Accepted keys are analyse and report (the two commands that gate exit code); values are none, advisory, warning, or error. The CLI --fail-on flag always wins; if both the CLI flag and the config key are absent, the binary default (advisory for analyse, none for report) applies. See ADR-013 for the rationale and the gating-only accept-list rule.

schemaVersion: gruff-rs.config.v1

minimumSeverity:
  analyse: advisory  # CI gates on advisory+; CLI --fail-on always wins
  # report: none

paths:
  ignore:
    - target/**
    - fixtures/**

allowlists:
  acceptedAbbreviations: [id, db, io, ui]
  secretPreviews: []

rules:
  select: []
  ignore: []
  complexity.cognitive:
    threshold: 15
    severity: warning
  security.process-command:
    severity: error

exclude:
  - rule: security.process-command
    paths: ["tests/**"]
    message_contains: "Command::new"
    reason: "test-only synthetic command"

Selectors can target exact rule IDs, dotted prefixes such as security.*, or public pillars such as Security.

Unknown minimumSeverity: keys are rejected with a useful error: setting minimumSeverity.summary: advisory errors with unknown command "summary" in minimumSeverity: gruff-rs's summary does not gate exit code. Valid keys: analyse, report. The off-switch value is none (gruff-rs convention; sibling ports may use never).

Rules And Pillars

The catalogue contains 87 rules:

Pillar Rules
complexity 3
dead-code 3
design 3
documentation 11
maintainability 11
modernisation 6
naming 5
security 23
sensitive-data 11
size 3
test-quality 8

Use ./.cargo-tools/bin/gruff-rs list-rules --format json for the exact rule metadata. See Rules for rule families, limits, and deferred checks.

Generated default config keeps size.file-length enabled for Rust source over 600 lines and marks waste.unnecessary-clone-candidate as opt-in, because a clone can be the clearer ownership boundary. test-quality.long-test counts from the first assertion onward so fixture setup does not dilute the test-signal check.

Custom Rules

Top-level custom_rules entries register config-only regex rules under the reserved custom.<slug> namespace. They can be selected with exact IDs, custom.*, or their public pillar, and they use the normal fingerprint formula.

custom_rules:
  - id: custom.no-hack-comment
    pillar: Documentation
    severity: warning
    message: HACK comment marker
    scope: comments
    pattern: '(?m)^[ \t]*//[ \t]*HACK\b'

Custom rules are intentionally regex-only in 0.3.x; AST patterns, plugins, scripts, external runtimes, and Semgrep-style metavariables are out of scope.

Baselines And Changed-Code Scans

Baselines suppress reviewed findings by exact fingerprint, rule ID, and file path:

./.cargo-tools/bin/gruff-rs analyse src --generate-baseline --fail-on none
./.cargo-tools/bin/gruff-rs analyse src --baseline --fail-on warning
./.cargo-tools/bin/gruff-rs analyse src --no-baseline --fail-on none

A baseline run classifies every finding as new, unchanged (matched the baseline and suppressed from the default list), or resolved (a baseline entry no longer found). The counts surface in the scan card and in JSON under baseline.newCount / unchangedCount / absentCount; the default findings list still shows only new findings, so existing reports are byte-identical. See Quality Gates to fail CI on new findings only.

Changed-code scans keep findings whose location or enclosing declaration overlaps the changed hunk. JSON output includes suppressedCount for findings excluded as out of scope.

./.cargo-tools/bin/gruff-rs analyse --format json --changed-ranges "3-3,8-10" src/foo.rs
./.cargo-tools/bin/gruff-rs analyse --format json --since HEAD --diff-git-unsafe src/foo.rs
git diff | ./.cargo-tools/bin/gruff-rs analyse --format json --diff-patch - src/foo.rs

Use --changed-scope=hunk for line/hunk-only filtering; the default is --changed-scope=symbol.

When a baseline or diff comparison context is active, analyse and summary surface per-rule deltas before the composite-score line:

Top 5 improved: -12 docs.missing-public-doc, -7 size.method-length, ...
Top 5 regressed: +4 modernisation.semver-pin, +2 naming.identifier-quality, ...

The JSON output exposes the same data as a perRuleDeltas[] array ({ruleId, introduced, removed, net}). Both surfaces stay omitted on full-tree scans so the schema remains byte-identical for non-comparison runs.

Coding-agent hook. Coding agents can run gruff-rs after each edit so they only see findings on the code they just changed, not unrelated debt in the same file. The goat-flow project ships a PostToolUse hook (gruff-code-quality.sh) that runs gruff-rs analyse <file> --format json --fail-on none on the edited file and filters the JSON to the changed line ranges (from the agent's tool payload or git diff --unified=0), reporting a suppressed count for pre-existing same-file findings; see its gruff-code-quality.md playbook for setup and triage. gruff-rs's side of the contract is the stable per-file JSON (filePath, line, severity, ruleId) under gruff.analysis.v2; the --diff-patch / --diff <mode> flags above are the in-binary change-scoping equivalent for CI or single-binary use.

Config paths.ignore is authoritative in every invocation mode (walk, explicit file args, and all diff modes), so a hook can pass an ignored file and gruff-rs emits no findings for it — --include-ignored opts into git/default ignores only and never overrides paths.ignore. Ignored paths are reported under paths.ignoredPathDetails with their source and pattern. The check-ignore command lets a hook query the same decision per path without analysing (gruff-rs check-ignore --format json <path>...[{path, ignored, source, pattern}], git check-ignore exit codes). See CI Integration and ADR-018.

Quality Gates

Beyond the binary --fail-on <severity>, a gate: block in .gruff-rs.yaml gates on finding counts — per severity and/or a total cap:

gate:
  total: 200            # optional: fail when total findings exceed this
  severity:
    error: 0            # optional per-severity caps (omitted = unlimited)
    warning: 10
  onMatch: fail         # `fail` (default, exit 1) | `warn` (diagnostic only)
  scope: new            # optional: `new` | `all` (default: scope unset)

Every gated run records a gate diagnostic with the per-severity breakdown and the pass/trip/warn decision (rendered in text/json/sarif). When both the gate: block and --fail-on are set, the gate evaluates first; a trip with onMatch: fail exits 1. A malformed gate (unknown key, negative count, bad onMatch/scope) is a config error (exit 2) naming the offending path.

Fail only on new findings. --fail-on-new (equivalently gate.scope: new with a default error: 0 cap) fails the run on findings introduced since the baseline while keeping baselined debt visible in the gate diagnostic (… (baseline: N new, M unchanged)):

./.cargo-tools/bin/gruff-rs analyse src --baseline --fail-on-new

--fail-on-new requires a baseline; without one it is a config error (exit 2) — "new" is undefined with nothing to compare against. scope: all gates over every finding (new + baselined); the default (scope unset) preserves the historical behavior, which already counts only new findings when a baseline is applied. Resolved (absent) findings never count toward any gate.

Dashboard

./.cargo-tools/bin/gruff-rs dashboard --host 127.0.0.1 --port 8766 --project-root .

The dashboard renders HTML reports on demand. It has no authentication and must not be exposed to untrusted networks; keep the default loopback bind unless the environment is trusted.

In polyglot repositories, gruff-rs defaults to port 8766 while gruff-go, gruff-php, and gruff-py default to 8765; use --port when running multiple dashboards at the same time.

Trust Boundary

Default scans are source-only and local-only. gruff-rs does not execute target code, run Cargo build scripts, call Git unless a Git-backed changed-region mode is explicitly requested, query registries, read vulnerability feeds, or run benchmarks. Dependency checks read Cargo.toml and Cargo.lock as data. Candidate wording means the analyzer found a deterministic static signal, not type-aware or runtime certainty.

Stability Contract

0.3.x is the active release line. Rule IDs, finding fingerprints, baseline identity, JSON schema version gruff.analysis.v2, SARIF rendering, and CLI exit semantics are the contract for this line. Larger contract changes move to a later release line. See UPGRADING.md for the full contract.

How It Compares

Tool Relationship
Clippy Canonical Rust lint pass. gruff-rs adds deterministic reports, baselines, scoring, project-level rules, and SARIF. Run both.
cargo audit / cargo deny Vulnerability and dependency policy tools. gruff-rs does not query advisory feeds.
rustfmt Formatting only. gruff-rs does not format code.
rust-analyzer IDE language server. gruff-rs is a CI/CLI gate with stable fingerprints and reports.
Tests and review Still required; gruff findings are static review prompts, not runtime proof.

Development

bash scripts/preflight-checks.sh
cargo test
cargo clippy --all-targets -- -D warnings
bin/gruff-rs analyse . --format json --no-baseline

scripts/preflight-checks.sh runs formatting, Clippy, unit tests, rule listing, JSON and SARIF fixture scans, patch-input diff smoke tests, selector/exclusion/custom-rule smokes, and a dogfood scan of the whole project gated by minimumSeverity.analyse in .gruff-rs.yaml.

Documentation

Author

Built by Matthew Hansen.

License

Licensed under either of: