diffimpactscout 0.1.0__tar.gz

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.
Files changed (40) hide show
  1. diffimpactscout-0.1.0/LICENSE +21 -0
  2. diffimpactscout-0.1.0/PKG-INFO +314 -0
  3. diffimpactscout-0.1.0/README.md +297 -0
  4. diffimpactscout-0.1.0/pyproject.toml +7 -0
  5. diffimpactscout-0.1.0/setup.cfg +4 -0
  6. diffimpactscout-0.1.0/setup.py +22 -0
  7. diffimpactscout-0.1.0/src/diffimpactscout/__init__.py +3 -0
  8. diffimpactscout-0.1.0/src/diffimpactscout/__main__.py +8 -0
  9. diffimpactscout-0.1.0/src/diffimpactscout/base.py +95 -0
  10. diffimpactscout-0.1.0/src/diffimpactscout/checks/__init__.py +20 -0
  11. diffimpactscout-0.1.0/src/diffimpactscout/checks/base.py +206 -0
  12. diffimpactscout-0.1.0/src/diffimpactscout/checks/eslint.py +164 -0
  13. diffimpactscout-0.1.0/src/diffimpactscout/checks/hygiene.py +104 -0
  14. diffimpactscout-0.1.0/src/diffimpactscout/checks/prettier.py +103 -0
  15. diffimpactscout-0.1.0/src/diffimpactscout/checks/repo_checks.py +95 -0
  16. diffimpactscout-0.1.0/src/diffimpactscout/checks/ruff.py +210 -0
  17. diffimpactscout-0.1.0/src/diffimpactscout/checks/syntax.py +133 -0
  18. diffimpactscout-0.1.0/src/diffimpactscout/cli.py +220 -0
  19. diffimpactscout-0.1.0/src/diffimpactscout/config.py +205 -0
  20. diffimpactscout-0.1.0/src/diffimpactscout/env.py +29 -0
  21. diffimpactscout-0.1.0/src/diffimpactscout/gitrun.py +68 -0
  22. diffimpactscout-0.1.0/src/diffimpactscout/guard.py +116 -0
  23. diffimpactscout-0.1.0/src/diffimpactscout/impact/__init__.py +1 -0
  24. diffimpactscout-0.1.0/src/diffimpactscout/impact/cache.py +55 -0
  25. diffimpactscout-0.1.0/src/diffimpactscout/impact/diff_parser.py +181 -0
  26. diffimpactscout-0.1.0/src/diffimpactscout/impact/impact.py +345 -0
  27. diffimpactscout-0.1.0/src/diffimpactscout/impact/python_analyzer.py +284 -0
  28. diffimpactscout-0.1.0/src/diffimpactscout/impact/reporter.py +107 -0
  29. diffimpactscout-0.1.0/src/diffimpactscout/impact/route_linker.py +363 -0
  30. diffimpactscout-0.1.0/src/diffimpactscout/launcher.py +108 -0
  31. diffimpactscout-0.1.0/src/diffimpactscout/profiles/django.json +14 -0
  32. diffimpactscout-0.1.0/src/diffimpactscout/profiles/fastapi.json +14 -0
  33. diffimpactscout-0.1.0/src/diffimpactscout/profiles/plain.json +8 -0
  34. diffimpactscout-0.1.0/src/diffimpactscout/scope.py +210 -0
  35. diffimpactscout-0.1.0/src/diffimpactscout.egg-info/PKG-INFO +314 -0
  36. diffimpactscout-0.1.0/src/diffimpactscout.egg-info/SOURCES.txt +38 -0
  37. diffimpactscout-0.1.0/src/diffimpactscout.egg-info/dependency_links.txt +1 -0
  38. diffimpactscout-0.1.0/src/diffimpactscout.egg-info/entry_points.txt +2 -0
  39. diffimpactscout-0.1.0/src/diffimpactscout.egg-info/top_level.txt +1 -0
  40. diffimpactscout-0.1.0/tests/test_fixture_smoke.py +115 -0
@@ -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,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,297 @@
1
+ # DiffImpactScout
2
+
3
+ An incremental pre-push guard and AST blast-radius impact analyzer for git repositories.
4
+
5
+ - Runs a battery of hygiene, syntax, and linter checks on just the files you changed, before you push.
6
+ - Estimates the blast radius of a change: which Python symbols you touched and where they are referenced across Python, Django templates, and frontend code.
7
+ - Installs as a git pre-push hook that is non-blocking by default, so it informs without getting in the way.
8
+
9
+ ## Features
10
+
11
+ - Incremental checks: every check is scoped to the change-set or changed lines, not the whole repository.
12
+ - Auto-fixing hygiene checks (line endings, trailing whitespace, end-of-file newline) that clean files in place.
13
+ - Python AST symbol map: detects changed classes, functions, methods, module fields, and class fields, including deletions and renames.
14
+ - Cross-layer impact linking: Python symbols to Django `{% url %}` template tags and frontend `http.get()/post()`-style calls.
15
+ - Symbol cache (`.impact_analysis_cache.json`) so repeated runs skip unchanged files.
16
+ - Profiles for `plain`, `django`, and `fastapi` projects.
17
+ - Configurable pre-push hook installation that never silently clobbers an existing hook.
18
+
19
+ ## Core idea
20
+
21
+ DiffImpactScout answers one question for a developer about to push: did I break something?
22
+
23
+ - **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?"
24
+ - **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.
25
+ - **Fixes over complaints.** Hygiene problems that can be auto-fixed are fixed in place instead of failing the run.
26
+ - **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.
27
+
28
+ 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".
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ pip install diffimpactscout
34
+ ```
35
+
36
+ The package is pure Python (standard library only at runtime) and requires Python 3.6 or newer and the `git` CLI on `PATH`.
37
+
38
+ 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.
39
+
40
+ ## Quick start
41
+
42
+ ```sh
43
+ # 1. Write a .diffimpactscout.json config (optional; defaults are used without one)
44
+ diffimpactscout init
45
+
46
+ # 2. Run the guard checks on your change-set
47
+ diffimpactscout guard
48
+
49
+ # 3. Analyze the blast radius of your change
50
+ diffimpactscout impact
51
+
52
+ # 4. Run the guard automatically on every push
53
+ diffimpactscout install-hooks
54
+ ```
55
+
56
+ `init` accepts `--profile plain|django|fastapi` to seed profile-appropriate settings.
57
+
58
+ ## Commands
59
+
60
+ The `init` command only needs a directory; every other command must be run inside a git repository.
61
+
62
+ ### `diffimpactscout init`
63
+
64
+ Writes a `.diffimpactscout.json` into the current directory. If the file already exists it is left unchanged.
65
+
66
+ | Flag | Description |
67
+ | --- | --- |
68
+ | `--profile {plain,django,fastapi}` | Seed defaults from the named profile (default: `plain`). |
69
+
70
+ ### `diffimpactscout guard`
71
+
72
+ 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)).
73
+
74
+ | Flag | Description |
75
+ | --- | --- |
76
+ | `--staged` | Check only staged files (`git diff --cached`). |
77
+ | `--all` | Check every file tracked by git. |
78
+ | `files...` | Check only the given paths. |
79
+
80
+ 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.
81
+
82
+ ### `diffimpactscout impact`
83
+
84
+ 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.
85
+
86
+ | Flag | Description |
87
+ | --- | --- |
88
+ | `--staged` | Analyze the staged diff (base ref becomes `HEAD`). |
89
+ | `--fast` | Skip template and frontend scanning; limit Python scanning to the changed files plus import-linked files. |
90
+ | `--json` | Emit the report as JSON (`{changed_count, rows, unresolved}`) instead of a markdown table. |
91
+
92
+ ### `diffimpactscout check CHECK_ID [files...]`
93
+
94
+ Runs a single check by id (see [Checks reference](#checks-reference)). With no file arguments it runs against the change-set.
95
+
96
+ ### `diffimpactscout install-hooks`
97
+
98
+ Installs a `pre-push` git hook that runs `diffimpactscout guard` on push.
99
+
100
+ | Flag | Description |
101
+ | --- | --- |
102
+ | `--force` | Overwrite an existing `pre-push` hook that DiffImpactScout did not install. |
103
+ | `--uninstall` | Remove the DiffImpactScout `pre-push` hook. |
104
+
105
+ ### `diffimpactscout --version`
106
+
107
+ Prints the installed version.
108
+
109
+ ## Configuration
110
+
111
+ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repository root. Without one, the defaults below are used.
112
+
113
+ ```json
114
+ {
115
+ "version": 1,
116
+ "mode": "pre-push",
117
+ "ignore_paths": [
118
+ "**/node_modules/**",
119
+ "**/venv/**",
120
+ "**/.venv/**",
121
+ "**/migrations/**",
122
+ "**/staticfiles/**",
123
+ "**/dist/**",
124
+ "**/build/**",
125
+ "**/.git/**"
126
+ ],
127
+ "use_gitignore": false,
128
+ "guard": {
129
+ "checks": [
130
+ {"id": "hygiene/mixed-line-ending"},
131
+ {"id": "hygiene/trailing-whitespace"},
132
+ {"id": "hygiene/end-of-file-fixer"},
133
+ {"id": "syntax/json-syntax"},
134
+ {"id": "syntax/ast-syntax"},
135
+ {"id": "syntax/merge-conflict"},
136
+ {"id": "repo/large-files", "args": ["--maxkb=250000"]},
137
+ {"id": "repo/private-key"}
138
+ ],
139
+ "blocking": "warn"
140
+ },
141
+ "impact": {
142
+ "profile": "plain",
143
+ "urls_globs": [],
144
+ "template_globs": [],
145
+ "frontend_globs": [],
146
+ "cache_file": ".impact_analysis_cache.json",
147
+ "fast_mode": false,
148
+ "threads": 4
149
+ }
150
+ }
151
+ ```
152
+
153
+ | Section | Key | Meaning |
154
+ | --- | --- | --- |
155
+ | top-level | `ignore_paths` | Glob patterns (matching any path suffix) excluded from all checks and from impact scanning. |
156
+ | top-level | `use_gitignore` | When `true`, also excludes paths ignored by `git check-ignore`. |
157
+ | `guard` | `checks` | Ordered list of check entries; each `{"id": ...}` may add `args`, `blocking`, and `always_block` overrides. |
158
+ | `guard` | `blocking` | `"warn"` (default) or `"strict"`. In strict mode every check marked blocking can fail the run. |
159
+ | `impact` | `profile` | `plain`, `django`, or `fastapi`; selects route extraction plus default globs. |
160
+ | `impact` | `urls_globs` / `template_globs` / `frontend_globs` | Glob patterns for route files, Django templates, and frontend sources. |
161
+ | `impact` | `cache_file` | Path of the symbol cache (relative to the repo root). |
162
+ | `impact` | `fast_mode`, `threads` | Reserved defaults from `init`; fast mode is currently selected with the `impact --fast` flag. |
163
+
164
+ A config entry can also declare a custom external check:
165
+
166
+ ```json
167
+ {"id": "my-lint", "type": "external", "command": ["make", "lint"]}
168
+ ```
169
+
170
+ 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).
171
+
172
+ ### Profiles
173
+
174
+ - `plain`: no route or cross-layer globs; impact analysis reports only Python references.
175
+ - `django`: route files `**/urls.py`, templates `**/templates/**/*.html`, frontend `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `ruff` and `ruff-format`.
176
+ - `fastapi`: routes extracted from `**/*.py`, same frontend globs; guard checks append `ruff` and `ruff-format`.
177
+
178
+ ### Blocking behavior
179
+
180
+ - `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.
181
+ - Set `guard.blocking` to `"strict"` (or export `IMPACT_CHECK_STRICT=1`): every check marked `{"blocking": true}` that finds issues fails the run.
182
+ - Override per check, for example:
183
+
184
+ ```json
185
+ {"id": "syntax/ast-syntax", "always_block": true}
186
+ ```
187
+
188
+ ## Pre-push hook
189
+
190
+ `diffimpactscout install-hooks` writes a `pre-push` hook that runs `diffimpactscout guard` on every push.
191
+
192
+ - The hook directory is resolved from `git rev-parse --git-common-dir` and honors `core.hooksPath`.
193
+ - 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.
194
+ - If a `pre-push` hook already exists without the marker, installation refuses with a `use --force to overwrite` message.
195
+ - 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.
196
+ - Because the guard is warn-by-default, an installed hook reports issues without blocking the push unless you enable strict mode.
197
+
198
+ ### Environment variables
199
+
200
+ | Variable | Effect |
201
+ | --- | --- |
202
+ | `DIFFIMPACTSCOUT_SKIP` | When set (any value), `guard` and `impact` skip entirely and exit `0`. |
203
+ | `IMPACT_CHECK_SKIP` | Legacy alias with the same behavior as `DIFFIMPACTSCOUT_SKIP`. |
204
+ | `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. |
205
+ | `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). |
206
+
207
+ ## Impact analysis
208
+
209
+ The `impact` command answers: if I push these changes, what else could break?
210
+
211
+ 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.
212
+ 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.
213
+ 3. Depending on the profile it extracts routes:
214
+ - `django`: `path()`/`re_path()`/`url()` entries in `urls_globs` that carry a `name=`.
215
+ - `fastapi`: `get`/`post`/`put`/`delete`/`patch`/`options` decorators on `router`/`app`-style objects in `urls_globs`.
216
+ 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.
217
+ 5. Every impacted reference gets a severity:
218
+
219
+ | Condition | Severity |
220
+ | --- | --- |
221
+ | Symbol was deleted or renamed | High |
222
+ | Symbol is referenced across 2+ layers (python / template / frontend) | High |
223
+ | Reference is inside a file that is itself in the change-set | Low |
224
+ | No reference path or no changed paths to compare | Low |
225
+ | Otherwise | Medium |
226
+
227
+ Actions follow severity: High rows say `review/verify`, Medium rows `verify`, Low rows `ok`, and deleted symbols `verify dangling references`.
228
+
229
+ ### Example report excerpt
230
+
231
+ ```
232
+ # Impact Analysis Report
233
+ | # | Impacted File Path | Module / Subsystem | Category | Detected Reference / Usage | Severity | Action Required |
234
+ | --- | --- | --- | --- | --- | --- | --- |
235
+ | 1 | app/views/dashboard.py | app/views | python | render_dashboard (import at dashboard.py:12) | Medium | verify |
236
+ | 2 | app/templates/dashboard.html | app/templates | template | {% url 'dashboard' %} at app/templates/dashboard.html:8 | High | review/verify |
237
+ | 3 | frontend/src/api.ts | frontend/src | frontend | http.get("/dashboard/") at api.ts:41 | Medium | verify |
238
+
239
+ ## Unresolved references (manual check required)
240
+ - {'file': 'frontend/src/api.ts', 'path': '/account/settings', 'dynamic': False}
241
+
242
+ Summary: 3 changed file(s); High: 1, Medium: 2, Low: 0
243
+ ```
244
+
245
+ 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.
246
+
247
+ ## Checks reference
248
+
249
+ All checks are incremental: file-scoped checks run on files in the change-set, and line-scoped checks flag only changed lines.
250
+
251
+ | id | What it checks | Blocks by default? |
252
+ | --- | --- | --- |
253
+ | `hygiene/mixed-line-ending` | Converts CRLF/CR to LF (`--fix=lf` only); auto-fixes files in place. | No (fixes) |
254
+ | `hygiene/trailing-whitespace` | Strips trailing spaces/tabs and trailing blank lines; auto-fixes files in place. | No (fixes) |
255
+ | `hygiene/end-of-file-fixer` | Ensures files end with exactly one newline; auto-fixes files in place. | No (fixes) |
256
+ | `syntax/json-syntax` | Validates that `.json` files parse. | No |
257
+ | `syntax/ast-syntax` | Validates that `.py` files parse as Python. | No |
258
+ | `syntax/merge-conflict` | Flags `<<<<<<<` / `=======` / `>>>>>>>` conflict markers on changed lines. | No |
259
+ | `repo/large-files` | Flags files over `--maxkb` (default 250000). | No |
260
+ | `repo/private-key` | Flags files containing private-key material (RSA, EC, OpenSSH, DSA, PGP blocks). | **Always** |
261
+ | `ruff` | Runs `ruff check` on changed lines of `.py` files (needs `ruff`). | No |
262
+ | `ruff-format` | Runs `ruff format --check` on changed lines (needs `ruff`). | No |
263
+ | `eslint` | Runs `npx eslint` on changed files, honoring a baseline (needs `eslint`). | No |
264
+ | `prettier` | Runs `npx prettier --check` on changed files under `src/` or `app/`, honoring a baseline (needs `prettier`). | No |
265
+
266
+ "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`).
267
+
268
+ ## Requirements
269
+
270
+ - Python 3.6 or newer.
271
+ - Standard library only at runtime; no third-party dependencies.
272
+ - The `git` CLI on `PATH`.
273
+ - Optional: `ruff`, `eslint`, and `prettier` for their respective checks (missing tools degrade to warnings).
274
+
275
+ ## FAQ
276
+
277
+ **Why is nothing failing?**
278
+ 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.
279
+
280
+ **How do I make it block?**
281
+ 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.
282
+
283
+ **I want to skip the guard, how?**
284
+ Export either `DIFFIMPACTSCOUT_SKIP` or `IMPACT_CHECK_SKIP` (any value); both `guard` and `impact` will exit `0` without doing anything.
285
+
286
+ **I already have a pre-push hook.**
287
+ `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.
288
+
289
+ **My templates are in nested or unusual locations.**
290
+ Set `impact.template_globs` (and `frontend_globs`) in the config to cover your layout; use the `django` profile for a sensible starting point.
291
+
292
+ **The hygiene checks modified my files.**
293
+ 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.
294
+
295
+ ## License
296
+
297
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,7 @@
1
+ [build-system]
2
+ requires = [
3
+ 'setuptools>=61 ; python_version >= "3.7"',
4
+ 'setuptools==59.6.0 ; python_version < "3.7"',
5
+ 'wheel',
6
+ ]
7
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+