Diffrat
Local CLI for developers and reviewers who want structured assistance when assessing pull-request diffs using git context.
Purpose
Diffrat reads a bounded git diff (not the whole repository) and produces a
review-oriented report: change summary, focus areas, and git metadata. It runs
locally without a web UI. Terminal output is the default; use --json when
scripting.
Status
1.0.0 is the first product release — v1 review CLI core plus Phase 3
optional LLM. Published on PyPI as diffrat
(formerly developed as Numbat; see D-008):
diffrat reviewwith unstaged,--staged,--base, and--rangemodes; optional--json- Bounded diff hunks, git context, file categories, deterministic Focus/Risk hints (including CI/workflow path hints with suggested commands, and content-based typo hints for known CI validator patterns)
- Optional
--checkfor path-scoped local validators and tests - Optional LLM-backed analysis when
DIFFRAT_LLM_*env vars are set (ADR-0001 / D-005); heuristics-only report remains the default without API keys
Phase 4 (integrations) is deferred. See .ai/project/roadmap.md.
Current capabilities
- Installable Python package with
diffratCLI entry point --helpand--versiondiffrat review— analyze unstaged or staged local git diffs and print a human-readable report (file list with coarse categories, per-file +/- counts, bounded diff hunks, summary, deterministic Focus/Risk hints)diffrat review --json— same analysis as structured JSON on stdout for scriptingdiffrat review --check— run applicable local validators/tests for touched pathsdiffrat review --base <ref>— compare the current branch to a base ref and include git context (branch, base, commits since base)diffrat review --range <A..B>— compare two git refs using two-dot range semantics (e.g.main..feature) and include range git context- Dev tooling: pytest, ruff, mypy
Setup
Requires Python 3.11+ and git on PATH.
pip install diffrat
diffrat --version
From source (development):
git clone https://github.com/szymoniwacz/diffrat.git
cd diffrat
pip install -e .
diffrat --version
diffrat review needs a real diff. On a clean main with no local changes,
--base main exits 2 (no changes on branch since main) — that is expected.
Use unstaged/staged edits or a feature branch, then:
diffrat review
diffrat review --base main
For local development (tests, lint, typecheck), and if you will use
diffrat review --check, install the optional extras:
pip install -e ".[dev]"
External dogfood sessions: docs/feedback-checklist.md.
Run
diffrat --help
python -m diffrat --help
Review a local diff
Run from inside a git repository:
# Unstaged changes (working tree vs index) — default
diffrat review
# Staged changes (index vs HEAD)
diffrat review --staged
# Branch vs base (merge-base with ref through HEAD; default base is main)
diffrat review --base main
diffrat review --base
# Two-dot commit range (changes reachable from B not from A)
diffrat review --range main..feature
diffrat review --help
JSON output for scripting
Use --json to write a structured document to stdout instead of the
human-readable report. The schema_version field identifies the output format;
breaking changes require bumping that version. When LLM analysis is enabled and
succeeds, JSON includes an additive top-level llm_findings string; the key is
omitted when LLM is disabled or the request fails.
# Unstaged diff as JSON
diffrat review --json
# Staged or branch-vs-base JSON
diffrat review --staged --json
diffrat review --base main --json
# Example: file count from a branch review
diffrat review --base main --json | python -c "import sys,json; print(json.load(sys.stdin)['summary']['file_count'])"
Errors and empty-diff messages still go to stderr with the same exit codes as the default report.
Focus / Risk hints and file categories
Every successful review assigns each changed file a coarse category:
source, tests, config, docs, ci, or other.
The report also includes deterministic Focus/Risk hints derived from paths and
diff size (for example large diffs, tests touched, config/dependency changes,
docs-only changes, CI/workflow path changes with suggested validator commands,
security-sensitive path names, rename/copy detection (rename_or_move),
category-composition signals (source_without_tests, tests_only,
ci_without_tests), size and deletion signals
(large_single_file, deletions_heavy), generated-artifact detection
(generated_file_touched), missing mapped test files for changed
src/diffrat modules, lockfile/manifest consistency hints (lockfile_without_manifest,
manifest_without_lockfile), git-context hints on branch/range reviews
(many_commits, wip_commits) and cross-area diffs (mixed_concerns), and
content-based hints from added hunk lines on
source and ci paths). Content-based codes include possible_secret,
debug_leftover, dangerous_call, broad_exception, and hardcoded_url_or_ip,
plus validator-specific typo hints for known CI patterns such as
PROJECT_EXECUTOR_COMMENT_FILTER. No network or API key is
required. JSON
output includes additive category fields on each file and a top-level
focus_risk array while keeping schema_version at "1". Each hint carries a
code, message, and severity (risk, warn, or info) from the central
registry in src/diffrat/scoring.py; unknown codes default to info. Content-derived
hints may also include optional path (repository-relative file path) and line
(1-based line number in the new file) when the location can be resolved from the
diff; those keys are omitted when unset. Hints are sorted by severity (risk first),
then by code, in both text and JSON reports.
File risk scores and ordering
Each changed file receives a deterministic non-negative integer risk_score
computed in src/diffrat/scoring.py. Files in the text Files list and JSON
files[] array are sorted by descending risk_score; ties break by path name.
The Changes section follows the same order.
The text Files section groups paths by category (source, tests, ci,
config, docs, other) in that fixed order. Within each category subsection,
files keep the same risk_score sort. A Review order section lists up to
five highest-priority paths (by risk_score) with rank, category, and line
counts. JSON output adds top-level review_order (up to five paths) and
files_by_category (category → path lists in the same order).
Text reports show risk=<score> on each file line (for example
src/a.py [source] risk=42 +4 -1). Binary files use a fixed score of 5.
| Signal | Weight constant | Points |
|---|---|---|
| Line share of non-binary diff | RISK_WEIGHT_LINE_SHARE_MAX (50) |
scaled by file lines ÷ total |
| Security-sensitive path | RISK_WEIGHT_SECURITY_SENSITIVE |
40 |
source without tests in diff |
RISK_WEIGHT_SOURCE_WITHOUT_TESTS |
25 |
ci category |
RISK_WEIGHT_CI_CATEGORY |
20 |
config category |
RISK_WEIGHT_CONFIG_CATEGORY |
10 |
| Binary file | RISK_WEIGHT_BINARY |
5 (fixed) |
JSON output includes additive risk_score on each file entry while keeping
schema_version at "1".
Changes section (diff hunks)
Text reports include a Changes section with unified-diff hunks for each
changed file (after the file list). JSON output includes a top-level changes
object with the same bounded content per file (path, hunks with header
and lines, plus binary / truncated flags).
Output is bounded to keep reports readable:
| Limit | Value |
|---|---|
| Max files shown in Changes | 20 |
| Max diff lines per file | 100 |
When limits apply, the report notes truncation. Limits are documented in
diffrat review --help and echoed in JSON under changes.limits.
Optional local checks (--check)
Use --check to run applicable repo validators/tests for touched paths and
include results in the report:
| Touched path pattern | Command run |
|---|---|
ci/, .github/workflows/, or validate-workflow-contracts.py |
python ci/validate-workflow-contracts.py --mode project |
src/diffrat/<module>.py |
pytest tests/test_<module>.py, mypy src/diffrat/<module>.py, and bandit -r src/diffrat/<module>.py when bandit is on PATH |
tests/test_<name>.py |
pytest tests/test_<name>.py |
other tests/ files (e.g. conftest.py) |
pytest tests |
pyproject.toml |
ruff check . and pip-audit when pip-audit is on PATH |
lockfile or dependency manifest paths (e.g. poetry.lock, requirements.txt) |
pip-audit when pip-audit is on PATH |
Multiple touched modules are deduplicated into one pytest, one mypy, and one
bandit invocation with all target paths. Source and test changes that map to the
same module run that test file once.
bandit and pip-audit are optional host tools. When a check applies by path
but the executable is not on PATH, the report records a skipped result and
the review run does not fail solely because the tool is missing.
Text reports add a Local checks section. JSON output includes an additive
top-level checks array with code, command, passed, output, and
optional skipped fields.
Failed checks are echoed to stderr with the command and output. Exit code 3
means at least one check failed (distinct from git errors and empty diffs).
diffrat review --check
diffrat review --staged --check
diffrat review --base main --check --json
Scriptable gate (--fail-on)
Use --fail-on with comma-separated hint codes (no spaces) to fail the review
when any requested code appears in Focus/Risk hints. This is an advisory gate on
hint presence only — it does not run extra subprocesses beyond --check.
| Exit code | Meaning |
|---|---|
0 |
Success (no requested codes matched) |
1 |
Git error, usage error, or invalid --fail-on token |
2 |
Empty diff (evaluated before --fail-on) |
3 |
--check subprocess failure (takes precedence over exit 4) |
4 |
At least one requested hint code matched |
# Fail when typo or secret hints appear (human report)
diffrat review --base main --fail-on=regex_typo,possible_secret
# Pre-push hook: JSON + gate
diffrat review --base main --json --fail-on=regex_typo,possible_secret
When --json and --fail-on are both used, JSON output includes a top-level
fail_on object with requested and matched arrays so scripts can read
matches without parsing stderr.
Single-file deep diff (--hunks-for)
Use --hunks-for=<path> to show Changes hunks for one repository-relative
path only. Files, Review order, and Focus / Risk still reflect the
full diff. The selected file uses a higher line budget (500 diff lines vs the
default 100). When the path is not in the current diff, the command exits 1
with stderr path not in diff: <path>.
With --json, changes.files contains only the requested path and
changes.limits.max_lines_per_file reflects the elevated limit (500).
diffrat review --staged --hunks-for=src/foo.py
diffrat review --base main --hunks-for=src/foo.py --json
Tests and quality
pytest
ruff check .
mypy .
Configuration
Diffrat is offline and deterministic by default (D-005). No API keys or LLM
credentials are required for the heuristic report. Repositories without
[tool.diffrat] behave exactly as before — built-in check commands and content
heuristics apply unchanged.
Optional LLM analysis (Phase 3)
LLM calls are opt-in only. With no DIFFRAT_LLM_* variables set, Diffrat
makes no network requests and sends no diff content to external services. The
heuristic Focus/Risk report is unchanged.
When both provider and API key are set, Diffrat sends diff-scoped prompts
(bounded hunks from the current review only — never a whole-repo scan) to an
OpenAI-compatible chat-completions endpoint. Successful responses appear as an
LLM analysis section in the text report and as additive llm_findings in
--json output. Failed or disabled LLM paths leave output unchanged.
| Variable | Required | Purpose |
|---|---|---|
DIFFRAT_LLM_PROVIDER |
When LLM enabled | Provider id (e.g. openai, ollama) |
DIFFRAT_LLM_API_KEY |
When LLM enabled | API key or token for the provider |
DIFFRAT_LLM_BASE_URL |
Optional | Custom base URL for local runtimes or proxies |
Cloud provider default endpoints are selected from DIFFRAT_LLM_PROVIDER.
DIFFRAT_LLM_BASE_URL is for local or custom OpenAI-compatible endpoints only.
Privacy: diff content leaves the machine only when you explicitly set these
variables. Store keys in your environment or secret manager — never commit them.
See ADR-0001 (.ai/architecture/adr-0001-llm-analysis-layer.md) and D-005 in
.ai/project/decisions.md.
Optional per-repository rules live in TOML at the git repository root (or
cwd when not inside a git repo):
pyproject.toml→[tool.diffrat](base).diffrat.tomlat the repo root overrides duplicate keys when both exist
Parsing uses stdlib tomllib only (Python 3.11+). Invalid regex in a content
rule emits a stderr warning and skips that rule; review does not crash.
[tool.diffrat.checks]
Map check code → display command string. Commands are parsed safely into argv
(no shell=True). A leading python token maps to sys.executable.
In v1, only ci_validator may be overridden. Built-in defaults for pytest,
ruff, mypy, bandit, and pip-audit remain when a key is omitted.
[tool.diffrat.checks]
ci_validator = "python ci/validate-workflow-contracts.py --mode project"
[tool.diffrat.content_rules]
Declarative regex rules scanned on added diff-hunk lines (before built-in
production heuristics). The hint code is the TOML table key (for example
regex_typo).
Shorthand — one string per rule code:
[tool.diffrat.content_rules]
regex_typo = "continue-projec(?!t) → continue-project"
Table form — supports path scoping and multiple entries per code:
[[tool.diffrat.content_rules.regex_typo]]
paths = ["ci/validate-workflow-contracts.py"]
pattern = "execute-projec(?!t)"
expected = "execute-project"
When paths is empty or omitted, the rule applies to all non-binary,
non-test, non-doc files (same skip rules as built-in production hints). Path
entries match as prefix or glob-style patterns.
Example (this repository)
This repo dogfoods [tool.diffrat.content_rules] for CI validator typo patterns
and PROJECT_EXECUTOR_COMMENT_FILTER checks — see pyproject.toml:
[[tool.diffrat.content_rules.regex_typo]]
paths = ["ci/validate-workflow-contracts.py"]
pattern = "continue-projec(?!t)"
expected = "continue-project"
[[tool.diffrat.content_rules.suspicious_constant_change]]
paths = ["ci/validate-workflow-contracts.py"]
pattern = "PROJECT_EXECUTOR_COMMENT_FILTER\\s*=(?!.*continue-project)(?!.*continue-projec)"
expected = "continue-project"
See D-006 in .ai/project/decisions.md for format scope and precedence.
Architecture and context
.ai/project/product-context.md— product identity and workflows.ai/project/scope.md— in-scope and deferred work.ai/docs/architecture-direction.md— CLI component boundaries
How this project is built
This repository is developed with a documentation-first AI delivery workflow
used to plan, review, and land changes in small steps. That system is private
and not part of the installable CLI — you only need the Setup section above to
run diffrat.
Limitations
- No CI integration or GitHub App (Phase 4 deferred)
- LLM analysis requires explicit env configuration; non-OpenAI-shaped APIs need a compatibility layer or future adapter work (ADR-0001)
- The PyPI name
numbatwas already taken; this product usesdiffratfor package, CLI, and import (D-008)
License
MIT — see LICENSE.
Contact and contributions
Maintained by Szymon Iwacz. Contributions via pull request; agents never merge.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file diffrat-1.0.0.tar.gz.
File metadata
- Download URL: diffrat-1.0.0.tar.gz
- Upload date:
- Size: 39.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60509d07a383abcb06f2c0c20510805791726e5b42ee24e1be97ed3bc9578617
|
|
| MD5 |
969c37cc005bf6d32707ca6688fab067
|
|
| BLAKE2b-256 |
ca74aadf1810b7f7c89574b2793fa27b84fd7eda606f4ebe340561c0b3ab2d61
|
File details
Details for the file diffrat-1.0.0-py3-none-any.whl.
File metadata
- Download URL: diffrat-1.0.0-py3-none-any.whl
- Upload date:
- Size: 38.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f6fc464ade10619e860647f02579053383ca02f894a7782e4b29bce479bfbba
|
|
| MD5 |
f4b69eb8396dbd3f5888f7a50adf1cdc
|
|
| BLAKE2b-256 |
2e8939c57bdd13ff001e1373a150c8eff1f0a0b5974ce9045775537c170309f2
|