Skip to main content

DiffImpactScout

An incremental pre-push guard and AST blast-radius impact analyzer for git repositories.

  • Runs a battery of hygiene, syntax, and linter checks on just the files you changed, before you push.
  • Estimates the blast radius of a change: which Python symbols you touched and where they are referenced across Python, Django templates, and frontend code.
  • Installs as a git pre-push hook that is non-blocking by default, so it informs without getting in the way.

Features

  • Incremental checks: every check is scoped to the change-set or changed lines, not the whole repository.
  • Auto-fixing hygiene checks (line endings, trailing whitespace, end-of-file newline) that clean files in place.
  • Python AST symbol map: detects changed classes, functions, methods, module fields, and class fields, including deletions and renames.
  • Cross-layer impact linking: Python symbols to Django {% url %} template tags and frontend http.get()/post()-style calls.
  • Symbol cache (.impact_analysis_cache.json) so repeated runs skip unchanged files.
  • Profiles for generic, django, fastapi, python, and frontend projects.
  • Configurable pre-push hook installation that never silently clobbers an existing hook.

Core idea

DiffImpactScout answers one question for a developer about to push: did I break something?

  • Only your changes are analyzed. Every guard check and the impact report are computed against the diff between your branch and the resolved change base (upstream master or sprint branch when present, otherwise closest origin branch). Pre-existing violations that arrived from upstream or from sync merges are not re-reported, so a run answers "did I introduce this?" instead of "does this file have a problem?"
  • Non-blocking by default. A push is only blocked when strictness is explicitly requested (strict mode or an always-block check). Default runs inform without getting in the way.
  • Fixes over complaints. Hygiene problems that can be auto-fixed are fixed in place instead of failing the run.
  • Blast radius, not just lint. The impact report shows which code outside your diff could be affected by your change, so the "not my change" threshold is applied to blame, not to risk.

The change-set also handles the full breadth of realistic push shapes: direct pushes, force pushes, new branches, sync merges (merge, rebase, squash), stacked branches, shallow clones, repos without a remote anchor, and fork upstream sync. All are covered by the automated pre-push scenario suite, so a missing upstream anchor never silently means "nothing was checked".

Install

pip install diffimpactscout

The package is pure Python (standard library only at runtime) and requires Python 3.8 or newer and the git CLI on PATH.

Optional tools are only needed for the checks that use them: ruff, eslint (via npx), and prettier (via npx). If one is missing, the affected check emits a warning and the guard continues; it never blocks a push because a tool is absent.

Quick start

# 1. Write a .diffimpactscout.json config (optional; defaults are used without one)
diffimpactscout init

# 2. Run the guard checks on your change-set
diffimpactscout guard

# 3. Analyze the blast radius of your change
diffimpactscout impact

# 4. Install the pre-push hook (guard + impact report by default)
diffimpactscout install-hooks

init accepts --profile generic|django|fastapi|python|frontend to seed profile-appropriate settings. install-hooks detects the stack automatically (see Supported setups) and, unless the config's mode says otherwise, the hook runs both the guard and the impact report on every push.

Supported setups

install-hooks detects the project stack on first run and writes a .diffimpactscout.json that fits it:

Detection signal Profile Guard checks Impact globs
manage.py + settings.py/wsgi.py django default + ruff, ruff-format urls, templates, frontend
fastapi in deps or main.py fastapi default + ruff, ruff-format **/*.py routes + frontend globs
package.json + src/app *.ts/*.js frontend default + eslint, prettier templates + frontend globs
*.py present python default + ruff, ruff-format Python module references
anything else (or no signal) generic default from init none

Other ecosystems (Go, Rust, Java, Ruby, ...) are detected and reported as "planned for future releases" in the preview; the generic profile still installs the universal guard checks. Add a language linter with an external check.

Commands

The init command only needs a directory; every other command must be run inside a git repository.

diffimpactscout init

Writes a .diffimpactscout.json into the current directory. If the file already exists it is left unchanged.

Flag Description
--profile {generic,django,fastapi,python,frontend} Seed defaults from the named profile (default: generic).

diffimpactscout guard

Runs every check listed in guard.checks over the change-set and prints any issues. Exits non-zero only when the run is in blocking mode and a blocking check found issues (see Pre-push hook and Configuration).

Flag Description
--staged Check only staged files (git diff --cached).
--all Check every file tracked by git.
files... Check only the given paths.

With no flags, the change-set is the diff between your branch and the closest remote ref (upstream preferred, then origin), falling back to the union of committed, staged, and worktree changes when no remote base can be resolved.

diffimpactscout impact

Analyzes the blast radius of the change-set. In an interactive terminal it prints the report and asks Proceed with push? (Y/n); answering n or no exits non-zero.

Flag Description
--staged Analyze the staged diff (base ref becomes HEAD).
--fast Skip template and frontend scanning; limit Python scanning to the changed files plus import-linked files.
--json Emit the report as JSON ({changed_count, rows, endpoints, unresolved, hidden_count}) instead of a markdown table.
--expand Open rolled-up rows (e.g. "35 templates directly extend it") to list their child files.
--all Also show lower-confidence (heuristic/transitive) impacts.

diffimpactscout guard-and-impact-check

Runs the guard checks first, then always falls through to the impact blast-radius report so the developer sees the guard verdict and the affected areas together. This is the default pre-push hook scope, so one push runs the whole workflow. The command exits non-zero when the guard blocks in strict mode (or when impact is set to block), so a failing guard still surfaces the impact report but the push is rejected.

Flag Description
--staged Restrict both the guard and the impact analysis to staged files.
--fast Passes through to impact (skip template/frontend scanning).
--json Passes through to impact (emit the report as JSON).
--markdown Passes through to impact (emit a Markdown table).
--expand Passes through to impact (open rolled-up rows to list their child files).
--all Passes through to impact (also show lower-confidence impacts).

diffimpactscout dependency-check

Verifies that every external tool referenced by the configured guard checks is on PATH (ruff, eslint, prettier). Missing tools print an install hint and exit 1; a clean run prints an OK banner and exits 0. install-hooks also runs a non-blocking version of this check after installing a hook, warning about missing tools only when stdin is a terminal.

diffimpactscout check CHECK_ID [files...]

Runs a single check by id (see Checks reference). With no file arguments it runs against the change-set.

diffimpactscout install-hooks

Installs a pre-push git hook that runs the configured hook scope on push: diffimpactscout guard-and-impact-check by default (guard checks then the impact blast-radius report), or diffimpactscout guard when mode is pre-push.

On first run the command detects the stack, prints a preview of the .diffimpactscout.json it would write, and (when stdin is a terminal) asks for confirmation. Answer n to override profile, blocking mode, extra checks, impact, and the hook scope (mode). Use --yes in CI to skip prompts, --profile and --blocking to pre-select settings, and --reconfigure to rewrite an existing config. Without --reconfigure an existing config is left untouched (and its mode is reused for the hook).

Flag Description
--profile {generic,django,fastapi,python,frontend} Pre-select the setup profile; skips the stack question.
--blocking {warn,strict} Blocking mode for guard checks (default: warn).
--yes, -y Accept defaults and skip prompts (non-interactive).
--reconfigure Rewrite an existing .diffimpactscout.json.
--force Overwrite an existing pre-push hook that DiffImpactScout did not install.
--uninstall Remove the DiffImpactScout pre-push hook.

After a hook is installed a non-blocking dependency check runs: tools referenced by the guard checks (ruff, eslint, prettier) that are missing on PATH are reported with install hints, so e.g. a first-time ruff install is prompted instead of silently degrading the checks.

diffimpactscout --version

Prints the installed version.

Configuration

DiffImpactScout is configured by a .diffimpactscout.json file in the repository root. Without one, the defaults below are used.

{
  "version": 1,
  "mode": "guard-and-impact-check",
  "ignore_paths": [
    "**/node_modules/**",
    "**/venv/**",
    "**/.venv/**",
    "**/migrations/**",
    "**/staticfiles/**",
    "**/dist/**",
    "**/build/**",
    "**/.git/**"
  ],
  "use_gitignore": false,
  "guard": {
    "checks": [
      {"id": "hygiene/mixed-line-ending"},
      {"id": "hygiene/trailing-whitespace"},
      {"id": "hygiene/end-of-file-fixer"},
      {"id": "syntax/json-syntax"},
      {"id": "syntax/ast-syntax"},
      {"id": "syntax/merge-conflict"},
      {"id": "repo/large-files", "args": ["--maxkb=250000"]},
      {"id": "repo/private-key"}
    ],
    "blocking": "warn"
  },
  "impact": {
    "profile": "generic",
    "urls_globs": [],
    "template_globs": [],
    "frontend_globs": [],
    "cache_file": ".impact_analysis_cache.json",
    "fast_mode": false,
    "threads": 4
  }
}
Section Key Meaning
top-level mode Hook scope: guard-and-impact-check (default, runs the guard then the impact report on every push) or pre-push (guard only).
top-level ignore_paths Glob patterns (matching any path suffix) excluded from all checks and from impact scanning.
top-level use_gitignore When true, also excludes paths ignored by git check-ignore.
guard checks Ordered list of check entries; each {"id": ...} may add args, blocking, and always_block overrides.
guard blocking "warn" (default) or "strict". In strict mode every check marked blocking can fail the run.
impact profile generic, django, or fastapi; selects route extraction plus default globs.
impact urls_globs / template_globs / frontend_globs Glob patterns for route files, Django templates, and frontend sources.
impact cache_file Path of the symbol cache (relative to the repo root).
impact fast_mode, threads Reserved defaults from init; fast mode is currently selected with the impact --fast flag.
impact reverse true/false; when on, template-only and frontend-only diffs are analyzed back to render sites, views, routes, and referencing frontend files. Default: off for generic/python profiles, on for django/frontend.
impact experimental true/false; enables the endpoint chain (changed frontend HTTP calls to routes, handler views, and the service/utility functions they call). Advisory only. Default: off in all profiles.
impact asset_url_prefixes Template URL prefixes stripped before asset resolution. User entries are appended to the built-in defaults ({{ MEDIA_URL }}, {{ STATIC_URL }}, {% static ', url('); they cannot replace or clear the defaults.

A config entry can also declare a custom external check:

{"id": "my-lint", "type": "external", "command": ["make", "lint"]}

External checks run command (each {file} placeholder is replaced with the file path) and report a failed exit status as an issue. scoped can be "files" (command runs once per file) or "repo" (once for the whole repository).

Profiles

  • generic: no route or cross-layer globs; impact analysis reports only Python references.
  • python: same impact globs as generic; guard checks append ruff and ruff-format.
  • django: route files **/urls.py, templates **/templates/**/*.html, frontend **/src/**/*.ts and **/app/**/*.js; guard checks append ruff and ruff-format.
  • fastapi: routes extracted from **/*.py, same frontend globs; guard checks append ruff and ruff-format.
  • frontend: template globs **/*.html and frontend globs **/src/**/*.ts and **/app/**/*.js; guard checks append eslint and prettier.

Blocking behavior

  • guard.blocking is "warn" by default: issues are reported but the run exits 0. Only checks marked always_block (by default just repo/private-key) fail the run in warn mode.
  • Set guard.blocking to "strict" (or export IMPACT_CHECK_STRICT=1): every check marked {"blocking": true} that finds issues fails the run.
  • Override per check, for example:
{"id": "syntax/ast-syntax", "always_block": true}

Pre-push hook

diffimpactscout install-hooks writes a pre-push hook that runs the configured hook scope on every push — guard-and-impact-check by default (guard checks plus the impact report), guard only when mode is pre-push.

  • The hook directory is resolved from git rev-parse --git-common-dir and honors core.hooksPath.
  • The hook script is marked with # diffimpactscout pre-push hook. On --uninstall (or a re-install with --force), only a hook carrying this marker is touched.
  • If a pre-push hook already exists without the marker, installation refuses with a use --force to overwrite message.
  • If the tool is not found, the hook prints a notice and exits 0, so pushes are never blocked by a missing install.
  • Because the guard is warn-by-default, an installed hook reports issues without blocking the push unless you enable strict mode.

Environment variables

Variable Effect
DIFFIMPACTSCOUT_SKIP When set (any value), guard and impact skip entirely and exit 0.
IMPACT_CHECK_SKIP Legacy alias with the same behavior as DIFFIMPACTSCOUT_SKIP.
IMPACT_CHECK_STRICT Falsy values are 0, false, no, off, and empty; anything else puts guard in strict mode and makes impact non-interactive runs fail on High/Medium hits.
PRE_COMMIT_FROM_REF / PRE_COMMIT_TO_REF Override the diff range used to compute the change-set (e.g. set by a pre-commit-style wrapper).

Impact analysis

The impact command answers: if I push these changes, what else could break?

  1. It parses the change-set (git diff --find-renames) and extracts the Python entities you added, removed, or renamed: classes, functions, methods, module fields, and class fields.
  2. It builds an AST symbol map of every tracked Python file in the repo (skipping excluded paths), cached in impact.cache_file, and finds every reference to the changed symbols. Lookup kind follows the entity: class fields match attribute loads, methods match name and attribute loads, functions/classes also match imports. To keep the blast radius honest, entities are scoped to the diff hunks that actually changed (a one-line tweak inside a large function does not report the whole function), and same-name references in other files are only reported when the file imports the name from the changed module (views.orders counts, a bare orders in an unrelated file does not).
  3. Depending on the profile it extracts routes:
    • django: path()/re_path()/url() entries in urls_globs that carry a name=.
    • fastapi: get/post/put/delete/patch/options decorators on router/app-style objects in urls_globs.
  4. It links layers: Django templates using {% url 'name' %} resolve through the route to its handler; frontend calls (http.get(...), HttpClient.post(...), $http.delete(...)) resolve the quoted endpoint to a route by path. References that cannot be resolved to a route land in an "unresolved" bucket flagged for manual checking.
  5. Every impacted reference gets a severity:
Condition Severity
Symbol was deleted or renamed High
Symbol is referenced across 2+ layers (python / template / frontend) High
Reference is inside a file that is itself in the change-set Low
No reference path or no changed paths to compare Low
Otherwise Medium

Actions follow severity: High rows say review/verify, Medium rows verify, Low rows ok, and deleted symbols verify dangling references.

Example report excerpt

# Impact Analysis Report
| # | Impacted File Path | Module / Subsystem | Category | Detected Reference / Usage | Severity | Action Required |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | app/views/dashboard.py | app/views | python | render_dashboard (import at dashboard.py:12) | Medium | verify |
| 2 | app/templates/dashboard.html | app/templates | template | {% url 'dashboard' %} at app/templates/dashboard.html:8 | High | review/verify |
| 3 | frontend/src/api.ts | frontend/src | frontend | http.get("/dashboard/") at api.ts:41 | Medium | verify |

## Unresolved references (manual check required)
- {'file': 'frontend/src/api.ts', 'path': '/account/settings', 'dynamic': False}

Summary: 3 changed file(s); High: 1, Medium: 2, Low: 0

In a terminal, impact then asks Proceed with push? (Y/n). On a non-interactive run (for example after a CI trigger), the report is shown as a warning and the push is not blocked unless IMPACT_CHECK_STRICT is set.

Reverse chains

When the change-set touches no Python (or in addition to changed Python symbols), impact also runs reverse chains (impact.reverse must be on; they are skipped entirely under impact --fast):

  • Template chain: a changed Django template is matched (by exact template name) to the Python functions that render it, then through the existing route and frontend linking; templates that extend the changed template are reported too. Dynamic {% include var %} tags land in "unresolved"; dynamic {% extends %} targets are ignored.
  • Asset chain: a changed tracked JS/TS file is matched to the templates that load it (script src, {% static %}, link href), then to the views rendering those templates. A changed tracked asset with no referencing template lands in "unresolved"; untracked targets (bundles, CDNs) are ignored, never guessed.
  • Import chain: changed JS/TS exports are matched to other tracked frontend files that import them; deleted exports are flagged High. Minified and vendored files are excluded; unresolvable path aliases are skipped.
  • Endpoint chain (experimental): changed frontend HTTP literals are matched to Django routes, the handler view, and the service/utility functions it calls. Enable with "experimental": true in the impact section; advisory only, and never a sole grounds for blocking.

Reverse reports are confidence-tiered: frontend impacts (the changed template/asset/export and its direct frontend references) are shown first, then backend impacts that are certainly affected (functions that render a changed template through a known render API). Transitive descendants, render sites of descendant templates, and route bindings are hidden by default and summarized as a hidden count.

  • --expand opens rolled-up rows (e.g. "35 templates directly extend it") to list their child files.
  • --all additionally shows the lower-confidence (heuristic/transitive) tier.

Checks reference

All checks are incremental: file-scoped checks run on files in the change-set, and line-scoped checks flag only changed lines.

id What it checks Blocks by default?
hygiene/mixed-line-ending Converts CRLF/CR to LF (--fix=lf only); auto-fixes files in place. No (fixes)
hygiene/trailing-whitespace Strips trailing spaces/tabs and trailing blank lines; auto-fixes files in place. No (fixes)
hygiene/end-of-file-fixer Ensures files end with exactly one newline; auto-fixes files in place. No (fixes)
syntax/json-syntax Validates that .json files parse. No
syntax/ast-syntax Validates that .py files parse as Python. No
syntax/merge-conflict Flags <<<<<<< / ======= / >>>>>>> conflict markers on changed lines. No
repo/large-files Flags files over --maxkb (default 250000). No
repo/private-key Flags files containing private-key material (RSA, EC, OpenSSH, DSA, PGP blocks). Always
ruff Runs ruff check on changed lines of .py files (needs ruff). No
ruff-format Runs ruff format --check on changed lines (needs ruff). No
eslint Runs npx eslint on changed files, honoring a baseline (needs eslint). No
prettier Runs npx prettier --check on changed files under src/ or app/, honoring a baseline (needs prettier). No

"Always" means the check has always_block: true, so repo/private-key fails the guard even in warn mode: a private key should never be pushed. Every other blocking check only fails the run in strict mode (guard.blocking: "strict" or IMPACT_CHECK_STRICT=1).

Requirements

  • Python 3.8 or newer.
  • Standard library only at runtime; no third-party dependencies.
  • The git CLI on PATH.
  • Optional: ruff, eslint, and prettier for their respective checks (missing tools degrade to warnings).

FAQ

Why is nothing failing? DiffImpactScout is warn-by-default. guard reports issues but exists 0 unless a repo/private-key hit, or strict mode is active. The same applies to impact in non-interactive contexts.

How do I make it block? Set IMPACT_CHECK_STRICT=1 (or any truthy value), or set "blocking": "strict" in the guard section of .diffimpactscout.json. You can also force specific checks to always block with "always_block": true on a check entry.

I want to skip the guard, how? Export either DIFFIMPACTSCOUT_SKIP or IMPACT_CHECK_SKIP (any value); both guard and impact will exit 0 without doing anything.

I already have a pre-push hook. install-hooks refuses to overwrite a hook it did not install. Review the existing hook and re-run with --force to replace it, or call guard from your own hook.

My templates are in nested or unusual locations. Set impact.template_globs (and frontend_globs) in the config to cover your layout; use the django profile for a sensible starting point.

The hygiene checks modified my files. That is by design: mixed line endings, trailing whitespace, and missing final newlines are fixed in place. Checks that can fix do so rather than failing; run diffimpactscout check hygiene/end-of-file-fixer <path> to apply one fixer to specific files.

License

MIT. See LICENSE.

Metadata

Release files for diffimpactscout 0.6.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 diffimpactscout 0.6.0
File Size Uploaded
diffimpactscout-0.6.0.tar.gz 79.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for diffimpactscout 0.6.0
File Interpreter ABI Platform
diffimpactscout-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 162.0 kB

Release files / diffimpactscout-0.6.0.tar.gz

Download URL diffimpactscout-0.6.0.tar.gz
Size 79.5 kB
Tags Source
SHA-256 checksum
How to use checksums
6911048a1855625757ea4aa8acfa0d26fc2bc63b24d09fe35e1ecc087da61099
BLAKE2b-256 checksum
How to use checksums
8b9c342560b693fc711455d32636f0b78d35f65971e7197db5e4e18ebe404f30
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release files / diffimpactscout-0.6.0-py3-none-any.whl

Download URL diffimpactscout-0.6.0-py3-none-any.whl
Size 82.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
805b4c033d9b27fc258c1f872e3e08bb5d8d39203ece091a3d9f3f7a4c7fff26
BLAKE2b-256 checksum
How to use checksums
cf52ef79d21ccf036bc2cd2a5144da1a617952c1030648f3fc4b7d4ea7f41bd9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

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