Skip to main content

gruff-py

gruff-py is the Python implementation of gruff, an opinionated quality analyser built to govern AI-generated code so a human can review and trust it. It walks Python projects, scores findings across quality pillars, and emits reports for terminals, CI annotations, SARIF consumers, static HTML, and a local dashboard.

Mission

gruff-py exists to govern AI-generated code so a human reviewer can sign off on it. The premise: a coding agent wrote the change, and a person who did not write it has to read, review, and trust it. Every rule, severity, and score is tuned for that reviewer's confidence rather than for abstract style. Wired in as a coding-agent hook, gruff-py guides the agent through advisory findings and forces it through --fail-on-gated warning and error findings toward code that is:

  • Legible enough to verify — a reviewer can follow the control flow and confirm it does what was asked, without holding the whole function in their head.
  • Secure where the eye fails — it flags the dangerous patterns and leaked secrets human review is worst at catching, because those are the failures the eye skips.
  • Tested for real, not padded — it rewards tests that exercise behaviour and flags low-signal ceremony (assertion-free tests, tautologies, mock-only theatre) that inflates coverage without earning trust.

Why doc comments are mandatory, even on a private one-liner: coding agents routinely produce code that superficially works while misunderstanding the requirement. Forcing the agent to state intent, usage, contract, and failure behaviour in prose gives the reviewer something to check the implementation against — and a mismatch between the doc comment and the code is itself a signal that the change needs a deeper look.

gruff-py is heuristic static analysis: it complements ruff, mypy, pytest, security scanners, and human review — it does not replace them.

See docs/mission.md for the full statement.

Status At A Glance

Field Value
Release line 0.5.0 package line
Runtime Python 3.11+
Package gruff-py
Import package gruffpy with py.typed
Binary gruff-py
Rule catalogue Generated from RuleRegistry.defaults(); see Rules
Primary config .gruff-py.yaml; [tool.gruff-py] in pyproject.toml is also supported
Analysis schema gruff.analysis.v2
Baseline schema gruff-py.baseline.v1; legacy gruff.baseline.v1 can be read
Severity gate --fail-on with none, advisory, warning, error; project default via minimumSeverity: in .gruff-py.yaml / pyproject.toml
Dashboard 127.0.0.1:8765 by default

Finding fingerprints are 16-character SHA-256 derivatives kept compatible with the PHP implementation where the rule identity and finding identity match. Analysis JSON uses the shared gruff.analysis.v2 schema string; baseline, hotspot, and config schemas remain language-prefixed. Each JSON finding also exposes a stableIdentity field — a line-insensitive companion to fingerprint for external diff tooling that needs to match "the same logical finding across line shifts" without re-baselining a moved violation; see docs/output-formats.md for the input set.

Requirements

  • Python 3.11+.
  • uv for source-checkout development.
  • Git only for diff and branch-review modes.
  • Optional external mutation tooling only when mutation flags are explicitly used.

Install

Install as a project dev dependency:

uv add --dev gruff-py
uv run gruff-py init
uv run gruff-py summary

From a source checkout:

uv sync
uv run gruff-py --help
./bin/gruff-py --help

Quick Start

# Create the project config.
uv run gruff-py init

# Review the current finding mix.
uv run gruff-py summary

# Explore without failing because of findings.
uv run gruff-py analyse src/ --fail-on none

# Gate on warning and error findings.
uv run gruff-py analyse src/ --fail-on warning

# Emit SARIF for code scanning.
uv run gruff-py analyse src/ --format sarif --fail-on none > gruff.sarif

# Generate a fresh-start baseline.
uv run gruff-py analyse src/ --generate-baseline-path gruff-baseline.json --fail-on none

# Start the local dashboard.
uv run gruff-py dashboard src/ --report-interactive

Open http://127.0.0.1:8765/ for the dashboard.

Commands

Command Purpose
init Write a default .gruff-py.yaml to the current directory.
analyse [paths...] Run the analyser 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.
list-rules [rule-id] Print rule metadata as text or JSON; pass a rule id for explain mode.
check-ignore [paths...] Report whether each path is ignored, and why (exit codes mirror git check-ignore).
hook [paths...] Run analysis for a coding-agent hook; emits gruff.hook.v1 JSON.
migrate-config Rewrite legacy config keys to the current schema; --dry-run prints the diff.
dashboard [paths...] Serve the local browser dashboard.
completion [shell] Print a shell completion script.
list, help Show command lists and command-specific help.

Global options mirror the broader gruff CLI surface: --silent, --quiet, --version, --ansi / --no-ansi, --no-interaction, and --verbose.

Output Formats

analyse --format <fmt> accepts:

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

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 input, parse, configuration, baseline, or diff failure.

--fail-on gates findings only. A parse error is a fatal diagnostic and exits 2 even with --fail-on none; none makes findings report-only, not diagnostics.

analyse defaults to --fail-on advisory. Set minimumSeverity.analyse in .gruff-py.yaml to change the default per-project (see docs/configuration.md).

CI Usage

Generic CI command:

uv run gruff-py analyse src tests --format github --fail-on warning

SARIF jobs can write an artifact for code scanning:

uv run gruff-py analyse src tests --format sarif --fail-on none > gruff-py.sarif

Use --no-baseline for security-focused gates where adoption baselines should not hide new error-severity findings.

Configuration

Config is optional. Precedence is:

  1. --config <path>
  2. .gruff-py.yaml in the project root
  3. [tool.gruff-py] in pyproject.toml
  4. Built-in defaults

Example .gruff-py.yaml:

schemaVersion: gruff-py.config.v0.1

paths:
  ignore:
    - "tests/fixtures/**"

selection:
  excludeRules:
    - docs.missing-module-docstring

rules:
  size.file-length:
    threshold: 900
    severity: error

See Configuration for the full shape.

Rules And Pillars

docs/rules.md is the generated source for current rule IDs, declared pillars, defaults, and catalog totals. Its header and pillar table come from RuleRegistry.defaults() rather than hand-maintained README values.

Verify the committed catalog without rewriting it:

uv run python -m gruffpy.command.rule_docs --check docs/rules.md

coupling, architecture, and mutation are reserved schema or future catalogue names; they do not have shipping rules in 0.5.0. See Rules for rule IDs, defaults, and remediation guidance.

Baselines And Changed-Code Scans

Baselines suppress reviewed findings by fingerprint:

uv run gruff-py analyse src/ --generate-baseline-path gruff-baseline.json --fail-on none
uv run gruff-py analyse src/ --baseline-path gruff-baseline.json --fail-on warning
uv run gruff-py analyse src/ --no-baseline --fail-on none

Changed-code scans are changed-region aware: a finding is kept when its location or enclosing function/class overlaps the changed hunk. JSON output includes suppressedCount for findings excluded as out of scope.

uv run gruff-py analyse --format json --changed-ranges "3-3,8-10" src/foo.py
uv run gruff-py analyse --format json --since HEAD src/foo.py
git diff | uv run gruff-py analyse --format json --diff - src/foo.py

Narrow path scans that leave project-wide rules enabled include a partial-context caveat in JSON run metadata and text output because conclusions can differ from a full-project scan.

Display filters such as --min-severity, --include-pillar, and --exclude-rule reduce rendered analyse output without changing which rules execute, the score, or the exit code. Text output discloses when findings are hidden by these filters.

Dashboard

uv run gruff-py dashboard src/ --host 127.0.0.1 --port 8765 --report-interactive

The dashboard serves a local browser UI for repeated scans. It has no authentication and is intended for local development. Non-loopback hosts are refused unless you pass --allow-public; that acknowledgment warns that remote users can scan any directory readable by the server process. See Dashboard for supported controls and safety notes.

In polyglot repositories, remember that gruff-go, gruff-php, and gruff-py all default to port 8765; use --port when running multiple dashboards at the same time.

Trust Boundary

Default scans are local source inspections. gruff-py parses Python source and selected project metadata; it does not execute target application code, run tests, query vulnerability feeds, or contact package registries. Git is used only for explicit diff modes. External mutation tooling is used only when explicitly requested by mutation flags. Sensitive-data previews are redacted before they reach terminal, JSON, SARIF, GitHub, Markdown, hotspot, or HTML output.

Stability Contract

Through the 0.x line, rule IDs, finding fingerprints, baseline identity, gruff.analysis.v2, gruff-py.baseline.v1, gruff-py.hotspot.v1, gruff.hook.v1, SARIF rendering, and CLI exit semantics are compatibility-sensitive. Pre-1.0, a minor bump (0.4.x to 0.5.0) is permitted to break them; every break carries a BREAKING: marker and a migration path in CHANGELOG.md.

How It Compares

Tool Relationship
ruff Fast linting and formatting. gruff-py adds scoring, baselines, reports, dashboard, and project-quality rules.
mypy / Pyright Type checking. gruff-py does not prove type correctness.
pytest Runtime tests. gruff-py can flag test-quality smells but does not prove behavior.
Bandit / Semgrep / vulnerability scanners Security-focused checks. gruff-py reports local static signals and does not replace specialized scanners.
Vulture / dead-code tools Focused unused-code detection. gruff-py includes broader quality scoring and reporting.

Development

uv sync --extra dev
uv run ruff check src tests
uv run ruff format --check src tests
uv run mypy src
uv run pytest
make check

make check uses the lint target, which runs ruff check --fix. Use the explicit commands above when you need non-mutating release verification.

Documentation

Author

Built by Matthew Hansen.

License

MIT

Metadata

Release files for gruff-py 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gruff-py 0.5.0
File Size Uploaded
gruff_py-0.5.0.tar.gz 700.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gruff-py 0.5.0
File Interpreter ABI Platform
gruff_py-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.3 MB

Release files / gruff_py-0.5.0.tar.gz

Download URL gruff_py-0.5.0.tar.gz
Size 700.6 kB
Tags Source
SHA-256 checksum
How to use checksums
b8a4454c1237a27596bedf06f9a12ca321b73316c2547be5ece6c9dfe95c15ec
BLAKE2b-256 checksum
How to use checksums
0d0b15378a64e47b868398e63a2a77d1bf863d9e083b1e0e9c30c46ec503894c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / gruff_py-0.5.0-py3-none-any.whl

Download URL gruff_py-0.5.0-py3-none-any.whl
Size 574.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
39fe2dd544c8ed3dc7fe3855bb17edd8ed3225f2e753e600d9573fa9d1cc410a
BLAKE2b-256 checksum
How to use checksums
8d8d2e8d2db7c306c9db3b9fa9f5ab3a82a24e537766f0cb9338790b3ec89201
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page