diffimpactscout 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ """DevScope: which files and which lines the developer changed relative to the resolved change base.
2
+
3
+ Example: after syncing from upstream, only the developer's own commit files
4
+ appear in the change-set, so upstream-merged files are never re-checked.
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import sys
10
+ import zlib
11
+
12
+ import diffimpactscout.base as base
13
+ import diffimpactscout.gitrun as gitrun
14
+
15
+ CACHE_DIRNAME = "diffimpactscout"
16
+ NONE_SENTINEL = "none"
17
+
18
+ _HUNK_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
19
+
20
+
21
+ def default_cache_dir():
22
+ base_dir = os.environ.get("XDG_CACHE_HOME")
23
+ if not base_dir:
24
+ base_dir = os.path.join(os.path.expanduser("~"), ".cache")
25
+ return os.path.join(base_dir, CACHE_DIRNAME)
26
+
27
+
28
+ def cache_key(gitdir, from_ref, to_ref):
29
+ data = "%s|%s|%s" % (gitdir or "none", from_ref or "", to_ref or "")
30
+ return "%x" % (zlib.crc32(data.encode("utf-8")) & 0xFFFFFFFF)
31
+
32
+
33
+ def parse_hunks(text):
34
+ hunks = []
35
+ for line in text.split("\n"):
36
+ m = _HUNK_RE.match(line)
37
+ if m:
38
+ hunks.append(
39
+ (
40
+ int(m.group(1)),
41
+ int(m.group(2) or 1),
42
+ int(m.group(3)),
43
+ int(m.group(4) or 1),
44
+ )
45
+ )
46
+ return hunks
47
+
48
+
49
+ def hunk_new_lines(text):
50
+ lines = set()
51
+ for _old_start, _old_count, new_start, new_count in parse_hunks(text):
52
+ for i in range(new_start, new_start + new_count):
53
+ lines.add(i)
54
+ return lines
55
+
56
+
57
+ class DevScope(object):
58
+ def __init__(self, root, cache_dir=None):
59
+ self.root = root
60
+ self.cache_dir = cache_dir if cache_dir is not None else default_cache_dir()
61
+ self._gitdir_cache = None
62
+
63
+ def _gitdir(self):
64
+ if self._gitdir_cache is None:
65
+ self._gitdir_cache = gitrun.git_out(
66
+ ["rev-parse", "--absolute-git-dir"], self.root
67
+ )
68
+ return self._gitdir_cache
69
+
70
+ def _cache_path(self, kind, from_ref, to_ref):
71
+ return os.path.join(
72
+ self.cache_dir,
73
+ "%s.%s" % (kind, cache_key(self._gitdir(), from_ref, to_ref)),
74
+ )
75
+
76
+ def _write(self, path, text):
77
+ try:
78
+ if not os.path.isdir(self.cache_dir):
79
+ os.makedirs(self.cache_dir)
80
+ tmp = path + ".tmp"
81
+ with open(tmp, "w") as fh:
82
+ fh.write(text)
83
+ os.replace(tmp, path)
84
+ except OSError:
85
+ pass
86
+
87
+ def _write_list(self, path, files):
88
+ try:
89
+ if not os.path.isdir(self.cache_dir):
90
+ os.makedirs(self.cache_dir)
91
+ tmp = path + ".tmp"
92
+ with open(tmp, "w") as fh:
93
+ if files:
94
+ fh.write("\n".join(files) + "\n")
95
+ os.replace(tmp, path)
96
+ except OSError:
97
+ pass
98
+
99
+ def _read(self, path):
100
+ try:
101
+ with open(path, "r") as fh:
102
+ return fh.read()
103
+ except (OSError, ValueError):
104
+ return None
105
+
106
+ def _read_list(self, path):
107
+ content = self._read(path)
108
+ if content is None:
109
+ return None
110
+ return [line for line in content.split("\n") if line]
111
+
112
+ def _warn_no_anchor(self):
113
+ sys.stderr.write(
114
+ "diffimpactscout: no default upstream ref; falling back to push range\n"
115
+ )
116
+
117
+ def scope_base(self, from_ref=None, to_ref=None):
118
+ """Resolve the default anchor ref, cached on the (from, to) range."""
119
+ if from_ref and to_ref:
120
+ cached = self._cache_path("anchor", from_ref, to_ref)
121
+ value = self._read(cached)
122
+ if value is not None:
123
+ value = value.strip()
124
+ if value == NONE_SENTINEL:
125
+ self._warn_no_anchor()
126
+ return None
127
+ return value
128
+ ref = base.resolve_change_base(self.root)
129
+ if ref is None:
130
+ self._warn_no_anchor()
131
+ self._write(cached, NONE_SENTINEL)
132
+ return None
133
+ self._write(cached, ref)
134
+ return ref
135
+ ref = base.resolve_change_base(self.root)
136
+ if ref is None:
137
+ self._warn_no_anchor()
138
+ return ref
139
+
140
+ def dev_files(self, anchor, from_ref=None, to_ref=None):
141
+ """Dev files for the change, cached on the (from, to) range only.
142
+
143
+ The anchor is not part of the cache key, so a different anchor with
144
+ the same range hits a stale cache.
145
+ """
146
+ if from_ref and to_ref:
147
+ cached = self._cache_path("devset", from_ref, to_ref)
148
+ files = self._read_list(cached)
149
+ if files is not None:
150
+ return files
151
+ files = self._dev_files_uncached(anchor, from_ref, to_ref)
152
+ self._write_list(cached, files)
153
+ return files
154
+ return self._dev_files_uncached(anchor, from_ref, to_ref)
155
+
156
+ def _dev_files_uncached(self, anchor, from_ref, to_ref):
157
+ if anchor:
158
+ return gitrun.git_nul(
159
+ ["diff", "--name-only", "-z", "--diff-filter=ACMRT", anchor, "HEAD"],
160
+ self.root,
161
+ )
162
+ if from_ref and to_ref:
163
+ return gitrun.git_nul(
164
+ [
165
+ "diff",
166
+ "--name-only",
167
+ "-z",
168
+ "--diff-filter=ACMRT",
169
+ "%s...%s" % (from_ref, to_ref),
170
+ ],
171
+ self.root,
172
+ )
173
+ files = set()
174
+ for args in (
175
+ ["diff", "--name-only", "-z", "--diff-filter=ACMRT", "HEAD"],
176
+ ["diff", "--cached", "--name-only", "-z", "--diff-filter=ACMRT"],
177
+ ["diff", "--name-only", "-z", "--diff-filter=ACMRT"],
178
+ ):
179
+ for f in gitrun.git_nul(args, self.root):
180
+ files.add(f)
181
+ return sorted(files)
182
+
183
+ def changed_lines(self, anchor, path, from_ref=None, to_ref=None):
184
+ if not self.is_tracked(path):
185
+ return None
186
+ if anchor:
187
+ text = gitrun.git_out(
188
+ ["diff", "--unified=0", anchor, "HEAD", "--", path], self.root
189
+ )
190
+ return hunk_new_lines(text)
191
+ if from_ref and to_ref:
192
+ text = gitrun.git_out(
193
+ ["diff", "--unified=0", "%s...%s" % (from_ref, to_ref), "--", path],
194
+ self.root,
195
+ )
196
+ return hunk_new_lines(text)
197
+ lines = set()
198
+ for args in (
199
+ ["diff", "--unified=0", "HEAD", "--", path],
200
+ ["diff", "--cached", "--unified=0", "--", path],
201
+ ["diff", "--unified=0", "--", path],
202
+ ):
203
+ lines |= hunk_new_lines(gitrun.git_out(args, self.root))
204
+ return lines
205
+
206
+ def is_tracked(self, path):
207
+ return gitrun.git_ok(["ls-files", "--error-unmatch", "--", path], self.root)
208
+
209
+ def contains(self, anchor, path, from_ref=None, to_ref=None):
210
+ return path in self.dev_files(anchor, from_ref, to_ref)
@@ -0,0 +1,314 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffimpactscout
3
+ Version: 0.1.0
4
+ Summary: Incremental pre-push guard and AST blast-radius impact analyzer for git repositories.
5
+ Author: DiffImpactScout contributors
6
+ License: MIT
7
+ Requires-Python: >=3.6
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: author
11
+ Dynamic: description
12
+ Dynamic: description-content-type
13
+ Dynamic: license
14
+ Dynamic: license-file
15
+ Dynamic: requires-python
16
+ Dynamic: summary
17
+
18
+ # DiffImpactScout
19
+
20
+ An incremental pre-push guard and AST blast-radius impact analyzer for git repositories.
21
+
22
+ - Runs a battery of hygiene, syntax, and linter checks on just the files you changed, before you push.
23
+ - Estimates the blast radius of a change: which Python symbols you touched and where they are referenced across Python, Django templates, and frontend code.
24
+ - Installs as a git pre-push hook that is non-blocking by default, so it informs without getting in the way.
25
+
26
+ ## Features
27
+
28
+ - Incremental checks: every check is scoped to the change-set or changed lines, not the whole repository.
29
+ - Auto-fixing hygiene checks (line endings, trailing whitespace, end-of-file newline) that clean files in place.
30
+ - Python AST symbol map: detects changed classes, functions, methods, module fields, and class fields, including deletions and renames.
31
+ - Cross-layer impact linking: Python symbols to Django `{% url %}` template tags and frontend `http.get()/post()`-style calls.
32
+ - Symbol cache (`.impact_analysis_cache.json`) so repeated runs skip unchanged files.
33
+ - Profiles for `plain`, `django`, and `fastapi` projects.
34
+ - Configurable pre-push hook installation that never silently clobbers an existing hook.
35
+
36
+ ## Core idea
37
+
38
+ DiffImpactScout answers one question for a developer about to push: did I break something?
39
+
40
+ - **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?"
41
+ - **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.
42
+ - **Fixes over complaints.** Hygiene problems that can be auto-fixed are fixed in place instead of failing the run.
43
+ - **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.
44
+
45
+ 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".
46
+
47
+ ## Install
48
+
49
+ ```sh
50
+ pip install diffimpactscout
51
+ ```
52
+
53
+ The package is pure Python (standard library only at runtime) and requires Python 3.6 or newer and the `git` CLI on `PATH`.
54
+
55
+ 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.
56
+
57
+ ## Quick start
58
+
59
+ ```sh
60
+ # 1. Write a .diffimpactscout.json config (optional; defaults are used without one)
61
+ diffimpactscout init
62
+
63
+ # 2. Run the guard checks on your change-set
64
+ diffimpactscout guard
65
+
66
+ # 3. Analyze the blast radius of your change
67
+ diffimpactscout impact
68
+
69
+ # 4. Run the guard automatically on every push
70
+ diffimpactscout install-hooks
71
+ ```
72
+
73
+ `init` accepts `--profile plain|django|fastapi` to seed profile-appropriate settings.
74
+
75
+ ## Commands
76
+
77
+ The `init` command only needs a directory; every other command must be run inside a git repository.
78
+
79
+ ### `diffimpactscout init`
80
+
81
+ Writes a `.diffimpactscout.json` into the current directory. If the file already exists it is left unchanged.
82
+
83
+ | Flag | Description |
84
+ | --- | --- |
85
+ | `--profile {plain,django,fastapi}` | Seed defaults from the named profile (default: `plain`). |
86
+
87
+ ### `diffimpactscout guard`
88
+
89
+ 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](#pre-push-hook) and [Configuration](#configuration)).
90
+
91
+ | Flag | Description |
92
+ | --- | --- |
93
+ | `--staged` | Check only staged files (`git diff --cached`). |
94
+ | `--all` | Check every file tracked by git. |
95
+ | `files...` | Check only the given paths. |
96
+
97
+ 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.
98
+
99
+ ### `diffimpactscout impact`
100
+
101
+ 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.
102
+
103
+ | Flag | Description |
104
+ | --- | --- |
105
+ | `--staged` | Analyze the staged diff (base ref becomes `HEAD`). |
106
+ | `--fast` | Skip template and frontend scanning; limit Python scanning to the changed files plus import-linked files. |
107
+ | `--json` | Emit the report as JSON (`{changed_count, rows, unresolved}`) instead of a markdown table. |
108
+
109
+ ### `diffimpactscout check CHECK_ID [files...]`
110
+
111
+ Runs a single check by id (see [Checks reference](#checks-reference)). With no file arguments it runs against the change-set.
112
+
113
+ ### `diffimpactscout install-hooks`
114
+
115
+ Installs a `pre-push` git hook that runs `diffimpactscout guard` on push.
116
+
117
+ | Flag | Description |
118
+ | --- | --- |
119
+ | `--force` | Overwrite an existing `pre-push` hook that DiffImpactScout did not install. |
120
+ | `--uninstall` | Remove the DiffImpactScout `pre-push` hook. |
121
+
122
+ ### `diffimpactscout --version`
123
+
124
+ Prints the installed version.
125
+
126
+ ## Configuration
127
+
128
+ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repository root. Without one, the defaults below are used.
129
+
130
+ ```json
131
+ {
132
+ "version": 1,
133
+ "mode": "pre-push",
134
+ "ignore_paths": [
135
+ "**/node_modules/**",
136
+ "**/venv/**",
137
+ "**/.venv/**",
138
+ "**/migrations/**",
139
+ "**/staticfiles/**",
140
+ "**/dist/**",
141
+ "**/build/**",
142
+ "**/.git/**"
143
+ ],
144
+ "use_gitignore": false,
145
+ "guard": {
146
+ "checks": [
147
+ {"id": "hygiene/mixed-line-ending"},
148
+ {"id": "hygiene/trailing-whitespace"},
149
+ {"id": "hygiene/end-of-file-fixer"},
150
+ {"id": "syntax/json-syntax"},
151
+ {"id": "syntax/ast-syntax"},
152
+ {"id": "syntax/merge-conflict"},
153
+ {"id": "repo/large-files", "args": ["--maxkb=250000"]},
154
+ {"id": "repo/private-key"}
155
+ ],
156
+ "blocking": "warn"
157
+ },
158
+ "impact": {
159
+ "profile": "plain",
160
+ "urls_globs": [],
161
+ "template_globs": [],
162
+ "frontend_globs": [],
163
+ "cache_file": ".impact_analysis_cache.json",
164
+ "fast_mode": false,
165
+ "threads": 4
166
+ }
167
+ }
168
+ ```
169
+
170
+ | Section | Key | Meaning |
171
+ | --- | --- | --- |
172
+ | top-level | `ignore_paths` | Glob patterns (matching any path suffix) excluded from all checks and from impact scanning. |
173
+ | top-level | `use_gitignore` | When `true`, also excludes paths ignored by `git check-ignore`. |
174
+ | `guard` | `checks` | Ordered list of check entries; each `{"id": ...}` may add `args`, `blocking`, and `always_block` overrides. |
175
+ | `guard` | `blocking` | `"warn"` (default) or `"strict"`. In strict mode every check marked blocking can fail the run. |
176
+ | `impact` | `profile` | `plain`, `django`, or `fastapi`; selects route extraction plus default globs. |
177
+ | `impact` | `urls_globs` / `template_globs` / `frontend_globs` | Glob patterns for route files, Django templates, and frontend sources. |
178
+ | `impact` | `cache_file` | Path of the symbol cache (relative to the repo root). |
179
+ | `impact` | `fast_mode`, `threads` | Reserved defaults from `init`; fast mode is currently selected with the `impact --fast` flag. |
180
+
181
+ A config entry can also declare a custom external check:
182
+
183
+ ```json
184
+ {"id": "my-lint", "type": "external", "command": ["make", "lint"]}
185
+ ```
186
+
187
+ 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).
188
+
189
+ ### Profiles
190
+
191
+ - `plain`: no route or cross-layer globs; impact analysis reports only Python references.
192
+ - `django`: route files `**/urls.py`, templates `**/templates/**/*.html`, frontend `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `ruff` and `ruff-format`.
193
+ - `fastapi`: routes extracted from `**/*.py`, same frontend globs; guard checks append `ruff` and `ruff-format`.
194
+
195
+ ### Blocking behavior
196
+
197
+ - `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.
198
+ - Set `guard.blocking` to `"strict"` (or export `IMPACT_CHECK_STRICT=1`): every check marked `{"blocking": true}` that finds issues fails the run.
199
+ - Override per check, for example:
200
+
201
+ ```json
202
+ {"id": "syntax/ast-syntax", "always_block": true}
203
+ ```
204
+
205
+ ## Pre-push hook
206
+
207
+ `diffimpactscout install-hooks` writes a `pre-push` hook that runs `diffimpactscout guard` on every push.
208
+
209
+ - The hook directory is resolved from `git rev-parse --git-common-dir` and honors `core.hooksPath`.
210
+ - 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.
211
+ - If a `pre-push` hook already exists without the marker, installation refuses with a `use --force to overwrite` message.
212
+ - The hook runs `diffimpactscout guard`; if the tool is not found, it prints a notice and exits `0`, so pushes are never blocked by a missing install.
213
+ - Because the guard is warn-by-default, an installed hook reports issues without blocking the push unless you enable strict mode.
214
+
215
+ ### Environment variables
216
+
217
+ | Variable | Effect |
218
+ | --- | --- |
219
+ | `DIFFIMPACTSCOUT_SKIP` | When set (any value), `guard` and `impact` skip entirely and exit `0`. |
220
+ | `IMPACT_CHECK_SKIP` | Legacy alias with the same behavior as `DIFFIMPACTSCOUT_SKIP`. |
221
+ | `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. |
222
+ | `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). |
223
+
224
+ ## Impact analysis
225
+
226
+ The `impact` command answers: if I push these changes, what else could break?
227
+
228
+ 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.
229
+ 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.
230
+ 3. Depending on the profile it extracts routes:
231
+ - `django`: `path()`/`re_path()`/`url()` entries in `urls_globs` that carry a `name=`.
232
+ - `fastapi`: `get`/`post`/`put`/`delete`/`patch`/`options` decorators on `router`/`app`-style objects in `urls_globs`.
233
+ 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.
234
+ 5. Every impacted reference gets a severity:
235
+
236
+ | Condition | Severity |
237
+ | --- | --- |
238
+ | Symbol was deleted or renamed | High |
239
+ | Symbol is referenced across 2+ layers (python / template / frontend) | High |
240
+ | Reference is inside a file that is itself in the change-set | Low |
241
+ | No reference path or no changed paths to compare | Low |
242
+ | Otherwise | Medium |
243
+
244
+ Actions follow severity: High rows say `review/verify`, Medium rows `verify`, Low rows `ok`, and deleted symbols `verify dangling references`.
245
+
246
+ ### Example report excerpt
247
+
248
+ ```
249
+ # Impact Analysis Report
250
+ | # | Impacted File Path | Module / Subsystem | Category | Detected Reference / Usage | Severity | Action Required |
251
+ | --- | --- | --- | --- | --- | --- | --- |
252
+ | 1 | app/views/dashboard.py | app/views | python | render_dashboard (import at dashboard.py:12) | Medium | verify |
253
+ | 2 | app/templates/dashboard.html | app/templates | template | {% url 'dashboard' %} at app/templates/dashboard.html:8 | High | review/verify |
254
+ | 3 | frontend/src/api.ts | frontend/src | frontend | http.get("/dashboard/") at api.ts:41 | Medium | verify |
255
+
256
+ ## Unresolved references (manual check required)
257
+ - {'file': 'frontend/src/api.ts', 'path': '/account/settings', 'dynamic': False}
258
+
259
+ Summary: 3 changed file(s); High: 1, Medium: 2, Low: 0
260
+ ```
261
+
262
+ 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.
263
+
264
+ ## Checks reference
265
+
266
+ All checks are incremental: file-scoped checks run on files in the change-set, and line-scoped checks flag only changed lines.
267
+
268
+ | id | What it checks | Blocks by default? |
269
+ | --- | --- | --- |
270
+ | `hygiene/mixed-line-ending` | Converts CRLF/CR to LF (`--fix=lf` only); auto-fixes files in place. | No (fixes) |
271
+ | `hygiene/trailing-whitespace` | Strips trailing spaces/tabs and trailing blank lines; auto-fixes files in place. | No (fixes) |
272
+ | `hygiene/end-of-file-fixer` | Ensures files end with exactly one newline; auto-fixes files in place. | No (fixes) |
273
+ | `syntax/json-syntax` | Validates that `.json` files parse. | No |
274
+ | `syntax/ast-syntax` | Validates that `.py` files parse as Python. | No |
275
+ | `syntax/merge-conflict` | Flags `<<<<<<<` / `=======` / `>>>>>>>` conflict markers on changed lines. | No |
276
+ | `repo/large-files` | Flags files over `--maxkb` (default 250000). | No |
277
+ | `repo/private-key` | Flags files containing private-key material (RSA, EC, OpenSSH, DSA, PGP blocks). | **Always** |
278
+ | `ruff` | Runs `ruff check` on changed lines of `.py` files (needs `ruff`). | No |
279
+ | `ruff-format` | Runs `ruff format --check` on changed lines (needs `ruff`). | No |
280
+ | `eslint` | Runs `npx eslint` on changed files, honoring a baseline (needs `eslint`). | No |
281
+ | `prettier` | Runs `npx prettier --check` on changed files under `src/` or `app/`, honoring a baseline (needs `prettier`). | No |
282
+
283
+ "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`).
284
+
285
+ ## Requirements
286
+
287
+ - Python 3.6 or newer.
288
+ - Standard library only at runtime; no third-party dependencies.
289
+ - The `git` CLI on `PATH`.
290
+ - Optional: `ruff`, `eslint`, and `prettier` for their respective checks (missing tools degrade to warnings).
291
+
292
+ ## FAQ
293
+
294
+ **Why is nothing failing?**
295
+ 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.
296
+
297
+ **How do I make it block?**
298
+ 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.
299
+
300
+ **I want to skip the guard, how?**
301
+ Export either `DIFFIMPACTSCOUT_SKIP` or `IMPACT_CHECK_SKIP` (any value); both `guard` and `impact` will exit `0` without doing anything.
302
+
303
+ **I already have a pre-push hook.**
304
+ `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.
305
+
306
+ **My templates are in nested or unusual locations.**
307
+ Set `impact.template_globs` (and `frontend_globs`) in the config to cover your layout; use the `django` profile for a sensible starting point.
308
+
309
+ **The hygiene checks modified my files.**
310
+ 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.
311
+
312
+ ## License
313
+
314
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,34 @@
1
+ diffimpactscout/__init__.py,sha256=ao0cR4JLPPl_brU_aligQBsmcTkFf_KYHpJNc4tZOXM,110
2
+ diffimpactscout/__main__.py,sha256=br2c-2UJuPrikT004vbqyoAOZAW-W8JISsWLpa_oXwU,168
3
+ diffimpactscout/base.py,sha256=MlYBHpBDZKqfBfQYzgSISxwlxwio6LrA6qyxdcDcjJU,3000
4
+ diffimpactscout/cli.py,sha256=2V63FMg7NOUHd671nGS74YU1Q2KnZ7gUALHAP-jVOvY,6531
5
+ diffimpactscout/config.py,sha256=oxVqRV_LCKb4a4qyZj7tD4_qtL7BASDkRMR_1cWYQnc,5829
6
+ diffimpactscout/env.py,sha256=xP2BIfU5LBsObRCFC3-Ml7GWAbpVlt2CDCY8dzkqi1o,691
7
+ diffimpactscout/gitrun.py,sha256=k57mvt-QCVsDhAsXqdhWgfqzvdXSXzz4fqZjbPyXHC4,1483
8
+ diffimpactscout/guard.py,sha256=B977IFYJ4qF1vudWVqZxfEm9f6ceViC6XtOu-G8sf1c,3155
9
+ diffimpactscout/launcher.py,sha256=zM6BzzqK-_EgPTiILRsbmX-nmaISoRWfOsx5sCPBmCk,3437
10
+ diffimpactscout/scope.py,sha256=4SkqUs8qDOmZAMkemadTQBA_LmZVEzybDy8keV4jqRA,6956
11
+ diffimpactscout/checks/__init__.py,sha256=Ywv2BePUBkLQusUJsYMeG016TiKWWL2C5oL1Uxoab5E,518
12
+ diffimpactscout/checks/base.py,sha256=Fgq-cxH-eouWcUP6mZS2S2yZ_bUQGVhYMFgD3hYV3B4,6655
13
+ diffimpactscout/checks/eslint.py,sha256=VZBm-FsuMnvkUeiyT1j6XMogtDjI1mqAopy-bC1l23k,5005
14
+ diffimpactscout/checks/hygiene.py,sha256=fH-2tsiAMeqj471hjbsqZlck9oPChPXfdOtBEQ2ObaI,3044
15
+ diffimpactscout/checks/prettier.py,sha256=eDHLQjS_KJqm6e81MIXSHBUz4M1Qdij3uc_PjREGqqo,2798
16
+ diffimpactscout/checks/repo_checks.py,sha256=vGv5Yi8qbQgJksuB6XQ6Txy-4lTiVDUfjlv-_1GzbUo,2875
17
+ diffimpactscout/checks/ruff.py,sha256=XZwHtOO7nEH-BvELVADA0zbjpkP7VzJ2cDmsplN-b-o,6327
18
+ diffimpactscout/checks/syntax.py,sha256=2VWEdMj9lQeWCAUNrS2TeR3Rkfwtmk-ALmnoVTHHHc0,4010
19
+ diffimpactscout/impact/__init__.py,sha256=RUvKbfuJVE76KZYJTByPkmke6CO_l9PKUVt5ESLXWs8,89
20
+ diffimpactscout/impact/cache.py,sha256=t-wAjUR_PPYhv8plohMQEShreIfzjyJSCccGp3ubx6U,1413
21
+ diffimpactscout/impact/diff_parser.py,sha256=APtpgxpIbaUnEP1v-9MUdO9V7vB5cN0NxwfJUk5pVJw,5230
22
+ diffimpactscout/impact/impact.py,sha256=vlqMTLI5GJk0oIvZdF54qiZJpLt5iAL47dz1n3NnSdE,10800
23
+ diffimpactscout/impact/python_analyzer.py,sha256=cuAiqBS5mcTtCLL9DyTZFb4qwoNiGPA76mudIoaGNIQ,9527
24
+ diffimpactscout/impact/reporter.py,sha256=cl0yAS4Wedu9dT7dMl3rlQfOacVRoJcf9RQL7563c4U,3045
25
+ diffimpactscout/impact/route_linker.py,sha256=plIwIcIXi16QCpge7V8Te6VMyilz5rVGuieMy1Fg1EA,10747
26
+ diffimpactscout/profiles/django.json,sha256=YoX3a6JOCyiM3w8WUat2OHMOqVOY8JrPQbnfU24V0Ec,279
27
+ diffimpactscout/profiles/fastapi.json,sha256=rpOqdv6ng_ow4eCd_GuvJmHMnEV80hkAMk_vFFg3Ulc,253
28
+ diffimpactscout/profiles/plain.json,sha256=mfmuJPlP2vbGrlk8_GSQEwtCGF9_eXfzfLnlk2sm59M,118
29
+ diffimpactscout-0.1.0.dist-info/licenses/LICENSE,sha256=XOOr2DhCQTG-_JwvRPXRAqz7q6hcNnJILmmMePr-myY,1084
30
+ diffimpactscout-0.1.0.dist-info/METADATA,sha256=pufne3x3ThKJjwJFJVzsm_2p1RFkcX9CwhQaqkeZrcE,16705
31
+ diffimpactscout-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
32
+ diffimpactscout-0.1.0.dist-info/entry_points.txt,sha256=I2a_ge2N1ixBJaKGyHLRS3Fox8ifhPnj0hV9NTmrY1g,61
33
+ diffimpactscout-0.1.0.dist-info/top_level.txt,sha256=jpUTpkFX7Ivccy7bkkdBPly0YPx9q3s-4kIG-qxuncc,16
34
+ diffimpactscout-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ diffimpactscout = diffimpactscout.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DiffImpactScout contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ diffimpactscout