diffimpactscout 0.1.0__tar.gz → 0.2.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 (43) hide show
  1. {diffimpactscout-0.1.0/src/diffimpactscout.egg-info → diffimpactscout-0.2.0}/PKG-INFO +32 -10
  2. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/README.md +30 -8
  3. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/setup.py +2 -2
  4. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/__init__.py +1 -1
  5. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/checks/ruff.py +16 -1
  6. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/cli.py +197 -1
  7. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/config.py +2 -1
  8. diffimpactscout-0.2.0/src/diffimpactscout/detect.py +91 -0
  9. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/impact/impact.py +1 -1
  10. diffimpactscout-0.2.0/src/diffimpactscout/profiles/frontend.json +14 -0
  11. diffimpactscout-0.1.0/src/diffimpactscout/profiles/plain.json → diffimpactscout-0.2.0/src/diffimpactscout/profiles/generic.json +1 -1
  12. diffimpactscout-0.2.0/src/diffimpactscout/profiles/python.json +14 -0
  13. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0/src/diffimpactscout.egg-info}/PKG-INFO +32 -10
  14. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout.egg-info/SOURCES.txt +4 -1
  15. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/LICENSE +0 -0
  16. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/pyproject.toml +0 -0
  17. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/setup.cfg +0 -0
  18. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/__main__.py +0 -0
  19. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/base.py +0 -0
  20. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/checks/__init__.py +0 -0
  21. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/checks/base.py +0 -0
  22. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/checks/eslint.py +0 -0
  23. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/checks/hygiene.py +0 -0
  24. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/checks/prettier.py +0 -0
  25. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/checks/repo_checks.py +0 -0
  26. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/checks/syntax.py +0 -0
  27. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/env.py +0 -0
  28. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/gitrun.py +0 -0
  29. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/guard.py +0 -0
  30. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/impact/__init__.py +0 -0
  31. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/impact/cache.py +0 -0
  32. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/impact/diff_parser.py +0 -0
  33. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/impact/python_analyzer.py +0 -0
  34. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/impact/reporter.py +0 -0
  35. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/impact/route_linker.py +0 -0
  36. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/launcher.py +0 -0
  37. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/profiles/django.json +0 -0
  38. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/profiles/fastapi.json +0 -0
  39. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout/scope.py +0 -0
  40. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout.egg-info/dependency_links.txt +0 -0
  41. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout.egg-info/entry_points.txt +0 -0
  42. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/src/diffimpactscout.egg-info/top_level.txt +0 -0
  43. {diffimpactscout-0.1.0 → diffimpactscout-0.2.0}/tests/test_fixture_smoke.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffimpactscout
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Incremental pre-push guard and AST blast-radius impact analyzer for git repositories.
5
5
  Author: DiffImpactScout contributors
6
6
  License: MIT
7
- Requires-Python: >=3.6
7
+ Requires-Python: >=3.8
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Dynamic: author
@@ -30,7 +30,7 @@ An incremental pre-push guard and AST blast-radius impact analyzer for git repos
30
30
  - Python AST symbol map: detects changed classes, functions, methods, module fields, and class fields, including deletions and renames.
31
31
  - Cross-layer impact linking: Python symbols to Django `{% url %}` template tags and frontend `http.get()/post()`-style calls.
32
32
  - Symbol cache (`.impact_analysis_cache.json`) so repeated runs skip unchanged files.
33
- - Profiles for `plain`, `django`, and `fastapi` projects.
33
+ - Profiles for `generic`, `django`, `fastapi`, `python`, and `frontend` projects.
34
34
  - Configurable pre-push hook installation that never silently clobbers an existing hook.
35
35
 
36
36
  ## Core idea
@@ -50,7 +50,7 @@ The change-set also handles the full breadth of realistic push shapes: direct pu
50
50
  pip install diffimpactscout
51
51
  ```
52
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`.
53
+ The package is pure Python (standard library only at runtime) and requires Python 3.8 or newer and the `git` CLI on `PATH`.
54
54
 
55
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
56
 
@@ -70,7 +70,21 @@ diffimpactscout impact
70
70
  diffimpactscout install-hooks
71
71
  ```
72
72
 
73
- `init` accepts `--profile plain|django|fastapi` to seed profile-appropriate settings.
73
+ `init` accepts `--profile generic|django|fastapi|python|frontend` to seed profile-appropriate settings. `install-hooks` detects the stack automatically (see [Supported setups](#supported-setups)).
74
+
75
+ ## Supported setups
76
+
77
+ `install-hooks` detects the project stack on first run and writes a `.diffimpactscout.json` that fits it:
78
+
79
+ | Detection signal | Profile | Guard checks | Impact globs |
80
+ | --- | --- | --- | --- |
81
+ | `manage.py` + `settings.py`/`wsgi.py` | django | default + `ruff`, `ruff-format` | urls, templates, frontend |
82
+ | `fastapi` in deps or `main.py` | fastapi | default + `ruff`, `ruff-format` | `**/*.py` routes + frontend globs |
83
+ | `package.json` + `src`/`app` `*.ts`/`*.js` | frontend | default + `eslint`, `prettier` | templates + frontend globs |
84
+ | `*.py` present | python | default + `ruff`, `ruff-format` | Python module references |
85
+ | anything else (or no signal) | generic | default from `init` | none |
86
+
87
+ 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](#configuration).
74
88
 
75
89
  ## Commands
76
90
 
@@ -82,7 +96,7 @@ Writes a `.diffimpactscout.json` into the current directory. If the file already
82
96
 
83
97
  | Flag | Description |
84
98
  | --- | --- |
85
- | `--profile {plain,django,fastapi}` | Seed defaults from the named profile (default: `plain`). |
99
+ | `--profile {generic,django,fastapi,python,frontend}` | Seed defaults from the named profile (default: `generic`). |
86
100
 
87
101
  ### `diffimpactscout guard`
88
102
 
@@ -114,8 +128,14 @@ Runs a single check by id (see [Checks reference](#checks-reference)). With no f
114
128
 
115
129
  Installs a `pre-push` git hook that runs `diffimpactscout guard` on push.
116
130
 
131
+ 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, and impact. 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.
132
+
117
133
  | Flag | Description |
118
134
  | --- | --- |
135
+ | `--profile {generic,django,fastapi,python,frontend}` | Pre-select the setup profile; skips the stack question. |
136
+ | `--blocking {warn,strict}` | Blocking mode for guard checks (default: `warn`). |
137
+ | `--yes`, `-y` | Accept defaults and skip prompts (non-interactive). |
138
+ | `--reconfigure` | Rewrite an existing `.diffimpactscout.json`. |
119
139
  | `--force` | Overwrite an existing `pre-push` hook that DiffImpactScout did not install. |
120
140
  | `--uninstall` | Remove the DiffImpactScout `pre-push` hook. |
121
141
 
@@ -156,7 +176,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
156
176
  "blocking": "warn"
157
177
  },
158
178
  "impact": {
159
- "profile": "plain",
179
+ "profile": "generic",
160
180
  "urls_globs": [],
161
181
  "template_globs": [],
162
182
  "frontend_globs": [],
@@ -173,7 +193,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
173
193
  | top-level | `use_gitignore` | When `true`, also excludes paths ignored by `git check-ignore`. |
174
194
  | `guard` | `checks` | Ordered list of check entries; each `{"id": ...}` may add `args`, `blocking`, and `always_block` overrides. |
175
195
  | `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. |
196
+ | `impact` | `profile` | `generic`, `django`, or `fastapi`; selects route extraction plus default globs. |
177
197
  | `impact` | `urls_globs` / `template_globs` / `frontend_globs` | Glob patterns for route files, Django templates, and frontend sources. |
178
198
  | `impact` | `cache_file` | Path of the symbol cache (relative to the repo root). |
179
199
  | `impact` | `fast_mode`, `threads` | Reserved defaults from `init`; fast mode is currently selected with the `impact --fast` flag. |
@@ -188,9 +208,11 @@ External checks run `command` (each `{file}` placeholder is replaced with the fi
188
208
 
189
209
  ### Profiles
190
210
 
191
- - `plain`: no route or cross-layer globs; impact analysis reports only Python references.
211
+ - `generic`: no route or cross-layer globs; impact analysis reports only Python references.
212
+ - `python`: same impact globs as `generic`; guard checks append `ruff` and `ruff-format`.
192
213
  - `django`: route files `**/urls.py`, templates `**/templates/**/*.html`, frontend `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `ruff` and `ruff-format`.
193
214
  - `fastapi`: routes extracted from `**/*.py`, same frontend globs; guard checks append `ruff` and `ruff-format`.
215
+ - `frontend`: template globs `**/*.html` and frontend globs `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `eslint` and `prettier`.
194
216
 
195
217
  ### Blocking behavior
196
218
 
@@ -284,7 +306,7 @@ All checks are incremental: file-scoped checks run on files in the change-set, a
284
306
 
285
307
  ## Requirements
286
308
 
287
- - Python 3.6 or newer.
309
+ - Python 3.8 or newer.
288
310
  - Standard library only at runtime; no third-party dependencies.
289
311
  - The `git` CLI on `PATH`.
290
312
  - Optional: `ruff`, `eslint`, and `prettier` for their respective checks (missing tools degrade to warnings).
@@ -13,7 +13,7 @@ An incremental pre-push guard and AST blast-radius impact analyzer for git repos
13
13
  - Python AST symbol map: detects changed classes, functions, methods, module fields, and class fields, including deletions and renames.
14
14
  - Cross-layer impact linking: Python symbols to Django `{% url %}` template tags and frontend `http.get()/post()`-style calls.
15
15
  - Symbol cache (`.impact_analysis_cache.json`) so repeated runs skip unchanged files.
16
- - Profiles for `plain`, `django`, and `fastapi` projects.
16
+ - Profiles for `generic`, `django`, `fastapi`, `python`, and `frontend` projects.
17
17
  - Configurable pre-push hook installation that never silently clobbers an existing hook.
18
18
 
19
19
  ## Core idea
@@ -33,7 +33,7 @@ The change-set also handles the full breadth of realistic push shapes: direct pu
33
33
  pip install diffimpactscout
34
34
  ```
35
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`.
36
+ The package is pure Python (standard library only at runtime) and requires Python 3.8 or newer and the `git` CLI on `PATH`.
37
37
 
38
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
39
 
@@ -53,7 +53,21 @@ diffimpactscout impact
53
53
  diffimpactscout install-hooks
54
54
  ```
55
55
 
56
- `init` accepts `--profile plain|django|fastapi` to seed profile-appropriate settings.
56
+ `init` accepts `--profile generic|django|fastapi|python|frontend` to seed profile-appropriate settings. `install-hooks` detects the stack automatically (see [Supported setups](#supported-setups)).
57
+
58
+ ## Supported setups
59
+
60
+ `install-hooks` detects the project stack on first run and writes a `.diffimpactscout.json` that fits it:
61
+
62
+ | Detection signal | Profile | Guard checks | Impact globs |
63
+ | --- | --- | --- | --- |
64
+ | `manage.py` + `settings.py`/`wsgi.py` | django | default + `ruff`, `ruff-format` | urls, templates, frontend |
65
+ | `fastapi` in deps or `main.py` | fastapi | default + `ruff`, `ruff-format` | `**/*.py` routes + frontend globs |
66
+ | `package.json` + `src`/`app` `*.ts`/`*.js` | frontend | default + `eslint`, `prettier` | templates + frontend globs |
67
+ | `*.py` present | python | default + `ruff`, `ruff-format` | Python module references |
68
+ | anything else (or no signal) | generic | default from `init` | none |
69
+
70
+ 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](#configuration).
57
71
 
58
72
  ## Commands
59
73
 
@@ -65,7 +79,7 @@ Writes a `.diffimpactscout.json` into the current directory. If the file already
65
79
 
66
80
  | Flag | Description |
67
81
  | --- | --- |
68
- | `--profile {plain,django,fastapi}` | Seed defaults from the named profile (default: `plain`). |
82
+ | `--profile {generic,django,fastapi,python,frontend}` | Seed defaults from the named profile (default: `generic`). |
69
83
 
70
84
  ### `diffimpactscout guard`
71
85
 
@@ -97,8 +111,14 @@ Runs a single check by id (see [Checks reference](#checks-reference)). With no f
97
111
 
98
112
  Installs a `pre-push` git hook that runs `diffimpactscout guard` on push.
99
113
 
114
+ 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, and impact. 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.
115
+
100
116
  | Flag | Description |
101
117
  | --- | --- |
118
+ | `--profile {generic,django,fastapi,python,frontend}` | Pre-select the setup profile; skips the stack question. |
119
+ | `--blocking {warn,strict}` | Blocking mode for guard checks (default: `warn`). |
120
+ | `--yes`, `-y` | Accept defaults and skip prompts (non-interactive). |
121
+ | `--reconfigure` | Rewrite an existing `.diffimpactscout.json`. |
102
122
  | `--force` | Overwrite an existing `pre-push` hook that DiffImpactScout did not install. |
103
123
  | `--uninstall` | Remove the DiffImpactScout `pre-push` hook. |
104
124
 
@@ -139,7 +159,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
139
159
  "blocking": "warn"
140
160
  },
141
161
  "impact": {
142
- "profile": "plain",
162
+ "profile": "generic",
143
163
  "urls_globs": [],
144
164
  "template_globs": [],
145
165
  "frontend_globs": [],
@@ -156,7 +176,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
156
176
  | top-level | `use_gitignore` | When `true`, also excludes paths ignored by `git check-ignore`. |
157
177
  | `guard` | `checks` | Ordered list of check entries; each `{"id": ...}` may add `args`, `blocking`, and `always_block` overrides. |
158
178
  | `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. |
179
+ | `impact` | `profile` | `generic`, `django`, or `fastapi`; selects route extraction plus default globs. |
160
180
  | `impact` | `urls_globs` / `template_globs` / `frontend_globs` | Glob patterns for route files, Django templates, and frontend sources. |
161
181
  | `impact` | `cache_file` | Path of the symbol cache (relative to the repo root). |
162
182
  | `impact` | `fast_mode`, `threads` | Reserved defaults from `init`; fast mode is currently selected with the `impact --fast` flag. |
@@ -171,9 +191,11 @@ External checks run `command` (each `{file}` placeholder is replaced with the fi
171
191
 
172
192
  ### Profiles
173
193
 
174
- - `plain`: no route or cross-layer globs; impact analysis reports only Python references.
194
+ - `generic`: no route or cross-layer globs; impact analysis reports only Python references.
195
+ - `python`: same impact globs as `generic`; guard checks append `ruff` and `ruff-format`.
175
196
  - `django`: route files `**/urls.py`, templates `**/templates/**/*.html`, frontend `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `ruff` and `ruff-format`.
176
197
  - `fastapi`: routes extracted from `**/*.py`, same frontend globs; guard checks append `ruff` and `ruff-format`.
198
+ - `frontend`: template globs `**/*.html` and frontend globs `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `eslint` and `prettier`.
177
199
 
178
200
  ### Blocking behavior
179
201
 
@@ -267,7 +289,7 @@ All checks are incremental: file-scoped checks run on files in the change-set, a
267
289
 
268
290
  ## Requirements
269
291
 
270
- - Python 3.6 or newer.
292
+ - Python 3.8 or newer.
271
293
  - Standard library only at runtime; no third-party dependencies.
272
294
  - The `git` CLI on `PATH`.
273
295
  - Optional: `ruff`, `eslint`, and `prettier` for their respective checks (missing tools degrade to warnings).
@@ -7,13 +7,13 @@ with io.open("README.md", encoding="utf-8") as fh:
7
7
 
8
8
  setup(
9
9
  name="diffimpactscout",
10
- version="0.1.0",
10
+ version="0.2.0",
11
11
  description="Incremental pre-push guard and AST blast-radius impact analyzer for git repositories.",
12
12
  long_description=long_description,
13
13
  long_description_content_type="text/markdown",
14
14
  license="MIT",
15
15
  author="DiffImpactScout contributors",
16
- python_requires=">=3.6",
16
+ python_requires=">=3.8",
17
17
  packages=find_packages("src"),
18
18
  package_dir={"": "src"},
19
19
  package_data={"diffimpactscout": ["profiles/*.json"]},
@@ -1,3 +1,3 @@
1
1
  """diffimpactscout: incremental pre-push guard and AST blast-radius impact analyzer."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
@@ -18,6 +18,21 @@ from diffimpactscout.checks.base import (
18
18
  _HUNK_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
19
19
 
20
20
 
21
+ def _split_hunks(text):
22
+ hunks = []
23
+ current = []
24
+ for line in text.split("\n"):
25
+ if line.startswith("@@"):
26
+ if current:
27
+ hunks.append("\n".join(current))
28
+ current = [line]
29
+ else:
30
+ current.append(line)
31
+ if current:
32
+ hunks.append("\n".join(current))
33
+ return hunks
34
+
35
+
21
36
  def _truncate(text, limit=30):
22
37
  lines = text.split("\n")
23
38
  if len(lines) <= limit:
@@ -55,7 +70,7 @@ def _filter_diff_by_lines(diff, changed):
55
70
  i += 1
56
71
  out = list(header)
57
72
  body = "\n".join(lines[i:])
58
- for hunk in re.split(r"(?=^@@)", body, flags=re.MULTILINE):
73
+ for hunk in _split_hunks(body):
59
74
  if not hunk.startswith("@@"):
60
75
  continue
61
76
  m = _HUNK_RE.match(hunk)
@@ -6,6 +6,7 @@ import os
6
6
  import sys
7
7
 
8
8
  import diffimpactscout.config as config
9
+ import diffimpactscout.detect as detect
9
10
  import diffimpactscout.env as env
10
11
  import diffimpactscout.gitrun as gitrun
11
12
  import diffimpactscout.guard as guard
@@ -28,7 +29,7 @@ def _parser():
28
29
  p_init = sub.add_parser("init", help="write a .diffimpactscout.json config")
29
30
  p_init.add_argument(
30
31
  "--profile",
31
- choices=("plain", "django", "fastapi"),
32
+ choices=config.PROFILE_CHOICES,
32
33
  default=config.DEFAULT_PROFILE,
33
34
  )
34
35
  p_init.set_defaults(func=_cmd_init)
@@ -67,6 +68,24 @@ def _parser():
67
68
  action="store_true",
68
69
  help="remove the diffimpactscout pre-push hook",
69
70
  )
71
+ p_hooks.add_argument(
72
+ "--profile",
73
+ choices=config.PROFILE_CHOICES,
74
+ help="pre-select the setup profile (skips the stack question)",
75
+ )
76
+ p_hooks.add_argument(
77
+ "--blocking",
78
+ choices=("warn", "strict"),
79
+ help="blocking mode for guard checks",
80
+ )
81
+ p_hooks.add_argument(
82
+ "--yes", "-y", action="store_true", help="accept defaults without prompts"
83
+ )
84
+ p_hooks.add_argument(
85
+ "--reconfigure",
86
+ action="store_true",
87
+ help="rewrite an existing .diffimpactscout.json",
88
+ )
70
89
  p_hooks.set_defaults(func=_cmd_install_hooks)
71
90
 
72
91
  return parser
@@ -190,6 +209,153 @@ def _cmd_check(args):
190
209
  return 1 if result.has_issues() else 0
191
210
 
192
211
 
212
+ def _is_tty():
213
+ try:
214
+ return sys.stdin.isatty()
215
+ except (OSError, ValueError):
216
+ return False
217
+
218
+
219
+ def _read_answer():
220
+ try:
221
+ return (sys.stdin.readline() or "").strip().lower()
222
+ except (OSError, ValueError):
223
+ return ""
224
+
225
+
226
+ def _prompt_yes_default(text, default):
227
+ sys.stdout.write(text + " ")
228
+ sys.stdout.flush()
229
+ answer = _read_answer()
230
+ if answer in ("y", "yes"):
231
+ return True
232
+ if answer in ("n", "no"):
233
+ return False
234
+ return default
235
+
236
+
237
+ def _prompt_choice(text, choices, default):
238
+ sys.stdout.write("%s [%s] (default: %s): " % (text, "/".join(choices), default))
239
+ sys.stdout.flush()
240
+ answer = _read_answer()
241
+ if answer in choices:
242
+ return answer
243
+ return default
244
+
245
+
246
+ def _check_id(entry):
247
+ if isinstance(entry, dict):
248
+ return entry.get("id", "?")
249
+ return str(entry)
250
+
251
+
252
+ def _resolve_profile(args, detected):
253
+ if args.profile:
254
+ return args.profile
255
+ if detected in config.PROFILE_CHOICES:
256
+ return detected
257
+ return config.DEFAULT_PROFILE
258
+
259
+
260
+ def _render_preview(detected, cfg):
261
+ profile = cfg["impact"].get("profile") or config.DEFAULT_PROFILE
262
+ if profile not in config.PROFILE_CHOICES:
263
+ profile = config.DEFAULT_PROFILE
264
+ checks = cfg["guard"]["checks"]
265
+ impact_on = profile != config.DEFAULT_PROFILE
266
+ blocking = cfg["guard"].get("blocking", "warn")
267
+ lines = []
268
+ if detected == profile:
269
+ lines.append("detected framework: %s" % detected)
270
+ lines.append("")
271
+ lines.append("diffimpactscout will write .diffimpactscout.json")
272
+ lines.append(" profile : %s" % profile)
273
+ lines.append(" blocking : %s" % blocking)
274
+ lines.append(" impact : %s" % ("on" if impact_on else "off"))
275
+ lines.append(" checks : %d" % len(checks))
276
+ for entry in checks:
277
+ lines.append(" - %s" % _check_id(entry))
278
+ lines.append("")
279
+ lines.append(
280
+ " then installs the pre-push hook -> runs `diffimpactscout guard` on each push."
281
+ )
282
+ lines.append("")
283
+ return "\n".join(lines)
284
+
285
+
286
+ def _other_stack_note(root, profile):
287
+ if profile != config.DEFAULT_PROFILE:
288
+ return None
289
+ other = detect.describe_other_stack(root)
290
+ if not other:
291
+ return None
292
+ return (
293
+ "detected %s. Language-specific lint and impact for %s is planned "
294
+ "for future releases; the generic profile installs the universal "
295
+ "guard checks instead." % (other, other)
296
+ )
297
+
298
+
299
+ def _build_config_data(profile, args):
300
+ data = config._defaults()
301
+ data = config._deep_merge(data, config.load_profile(profile))
302
+ if args.blocking:
303
+ data["guard"]["blocking"] = args.blocking
304
+ return data
305
+
306
+
307
+ def _write_config(path, data):
308
+ try:
309
+ with open(path, "w") as fh:
310
+ json.dump(data, fh, indent=2)
311
+ fh.write("\n")
312
+ except OSError as exc:
313
+ sys.stderr.write("diffimpactscout: cannot write %s: %s\n" % (path, exc))
314
+ return 1
315
+ return 0
316
+
317
+
318
+ _LINT_HINT = {
319
+ "django": "ruff + ruff-format",
320
+ "fastapi": "ruff + ruff-format",
321
+ "python": "ruff + ruff-format",
322
+ "frontend": "eslint + prettier",
323
+ }
324
+
325
+
326
+ def _ask_overrides(data, args):
327
+ profile = data["impact"].get("profile") or config.DEFAULT_PROFILE
328
+ if profile not in config.PROFILE_CHOICES:
329
+ profile = config.DEFAULT_PROFILE
330
+ profile = _prompt_choice("Profile", list(config.PROFILE_CHOICES), profile)
331
+ merged = config._deep_merge(config._defaults(), config.load_profile(profile))
332
+ if args.blocking:
333
+ merged["guard"]["blocking"] = args.blocking
334
+ data["guard"] = merged["guard"]
335
+ data["impact"] = merged["impact"]
336
+ if not args.blocking:
337
+ strict = _prompt_yes_default(
338
+ "Block the push when checks report issues? (strict mode) [y/N]", False
339
+ )
340
+ data["guard"]["blocking"] = "strict" if strict else "warn"
341
+ hint = _LINT_HINT.get(profile)
342
+ if hint:
343
+ keep = _prompt_yes_default(
344
+ "Keep the extra lint checks? (%s) [Y/n]" % hint, True
345
+ )
346
+ if not keep:
347
+ data["guard"]["checks"] = [dict(c) for c in config.DEFAULT_GUARD_CHECKS]
348
+ impact_on = _prompt_yes_default(
349
+ "Enable impact analysis? [Y/n]",
350
+ merged["impact"].get("profile") != config.DEFAULT_PROFILE,
351
+ )
352
+ if not impact_on:
353
+ generic = config._deep_merge(
354
+ config._defaults(), config.load_profile("generic")
355
+ )
356
+ data["impact"] = generic["impact"]
357
+
358
+
193
359
  def _cmd_install_hooks(args):
194
360
  root = _require_repo()
195
361
  if root is None:
@@ -207,6 +373,36 @@ def _cmd_install_hooks(args):
207
373
  else:
208
374
  print("nothing to remove")
209
375
  return 0
376
+ cfg_path = os.path.join(root, config.CFG_NAME)
377
+ has_config = os.path.exists(cfg_path)
378
+ proceed = True
379
+ if has_config and not args.reconfigure:
380
+ print(
381
+ "diffimpactscout: %s already exists; leaving it unchanged "
382
+ "(pass --reconfigure to rewrite)" % config.CFG_NAME
383
+ )
384
+ else:
385
+ detected = detect.detect_stack(root)
386
+ profile = _resolve_profile(args, detected)
387
+ data = _build_config_data(profile, args)
388
+ sys.stdout.write(_render_preview(detected, data))
389
+ note = _other_stack_note(root, profile)
390
+ if note:
391
+ sys.stderr.write("diffimpactscout: %s\n" % note)
392
+ interactive = not args.yes and _is_tty()
393
+ if interactive:
394
+ proceed = _prompt_yes_default("Proceed? [Y/n]", True)
395
+ if not proceed:
396
+ _ask_overrides(data, args)
397
+ sys.stdout.write(_render_preview(detected, data))
398
+ proceed = _prompt_yes_default("Proceed? [Y/n]", True)
399
+ if proceed:
400
+ rc = _write_config(cfg_path, data)
401
+ if rc != 0:
402
+ return rc
403
+ if not proceed:
404
+ print("diffimpactscout: setup skipped; no changes made")
405
+ return 0
210
406
  try:
211
407
  installed = launcher.install_hook(root, force=args.force)
212
408
  except OSError as exc:
@@ -8,7 +8,8 @@ import sys
8
8
  import diffimpactscout.gitrun as gitrun
9
9
 
10
10
  CFG_NAME = ".diffimpactscout.json"
11
- DEFAULT_PROFILE = "plain"
11
+ DEFAULT_PROFILE = "generic"
12
+ PROFILE_CHOICES = ("generic", "django", "fastapi", "python", "frontend")
12
13
  MODE_PRE_PUSH = "pre-push"
13
14
 
14
15
  DEFAULT_IGNORE_PATHS = [
@@ -0,0 +1,91 @@
1
+ """Detect the project stack for first-run setup.
2
+
3
+ Example: a repo with manage.py plus settings.py resolves to 'django'; a
4
+ repo containing only go.mod falls back to the generic profile.
5
+ """
6
+
7
+ import os
8
+
9
+ _IGNORED_DIRS = {".git", "node_modules", "venv", ".venv", "env", "dist", "build"}
10
+
11
+ _OTHER_STACKS = (
12
+ ("Go", "go.mod"),
13
+ ("Rust", "Cargo.toml"),
14
+ ("Java", "pom.xml"),
15
+ ("Ruby", "Gemfile"),
16
+ )
17
+
18
+
19
+ def _walk(root):
20
+ for base, dirs, files in os.walk(root):
21
+ dirs[:] = [d for d in dirs if d not in _IGNORED_DIRS]
22
+ for name in files:
23
+ yield os.path.join(base, name)
24
+
25
+
26
+ def _has(root, rel):
27
+ return os.path.exists(os.path.join(root, rel))
28
+
29
+
30
+ def _mentions(root, name, token):
31
+ path = os.path.join(root, name)
32
+ if not os.path.exists(path):
33
+ return False
34
+ with open(path) as fh:
35
+ return token in fh.read()
36
+
37
+
38
+ def _has_django(root):
39
+ if not _has(root, "manage.py"):
40
+ return False
41
+ for path in _walk(root):
42
+ if path.endswith("settings.py") or path.endswith("wsgi.py"):
43
+ return True
44
+ return False
45
+
46
+
47
+ def _has_fastapi(root):
48
+ for name in ("requirements.txt", "requirements-dev.txt", "pyproject.toml"):
49
+ if _mentions(root, name, "fastapi"):
50
+ return True
51
+ main = os.path.join(root, "main.py")
52
+ if os.path.exists(main):
53
+ with open(main) as fh:
54
+ return "fastapi" in fh.read()
55
+ return False
56
+
57
+
58
+ def _has_frontend(root):
59
+ if not _has(root, "package.json"):
60
+ return False
61
+ for path in _walk(root):
62
+ parts = path.split(os.sep)
63
+ if ("src" in parts or "app" in parts) and path.endswith((".ts", ".js")):
64
+ return True
65
+ return False
66
+
67
+
68
+ def _has_python(root):
69
+ for path in _walk(root):
70
+ if path.endswith(".py"):
71
+ return True
72
+ return False
73
+
74
+
75
+ def detect_stack(root):
76
+ if _has_django(root):
77
+ return "django"
78
+ if _has_fastapi(root):
79
+ return "fastapi"
80
+ if _has_frontend(root):
81
+ return "frontend"
82
+ if _has_python(root):
83
+ return "python"
84
+ return "generic"
85
+
86
+
87
+ def describe_other_stack(root):
88
+ for label, marker in _OTHER_STACKS:
89
+ if _has(root, marker):
90
+ return label
91
+ return None
@@ -34,7 +34,7 @@ def run_impact(root, cfg, staged=False, fast=False, json_out=False):
34
34
  if _skip_requested():
35
35
  return 0
36
36
  impact_cfg = cfg.get("impact") or {}
37
- profile = impact_cfg.get("profile") or "plain"
37
+ profile = impact_cfg.get("profile") or "generic"
38
38
  from_ref, to_ref = env.pre_commit_refs()
39
39
  anchor = None
40
40
  if not staged:
@@ -0,0 +1,14 @@
1
+ {
2
+ "impact": {
3
+ "profile": "frontend",
4
+ "urls_globs": [],
5
+ "template_globs": ["**/*.html"],
6
+ "frontend_globs": ["**/src/**/*.ts", "**/app/**/*.js"]
7
+ },
8
+ "guard": {
9
+ "checks": [
10
+ {"id": "eslint"},
11
+ {"id": "prettier"}
12
+ ]
13
+ }
14
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "impact": {
3
- "profile": "plain",
3
+ "profile": "generic",
4
4
  "urls_globs": [],
5
5
  "template_globs": [],
6
6
  "frontend_globs": []
@@ -0,0 +1,14 @@
1
+ {
2
+ "impact": {
3
+ "profile": "python",
4
+ "urls_globs": [],
5
+ "template_globs": [],
6
+ "frontend_globs": []
7
+ },
8
+ "guard": {
9
+ "checks": [
10
+ {"id": "ruff"},
11
+ {"id": "ruff-format"}
12
+ ]
13
+ }
14
+ }
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffimpactscout
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Incremental pre-push guard and AST blast-radius impact analyzer for git repositories.
5
5
  Author: DiffImpactScout contributors
6
6
  License: MIT
7
- Requires-Python: >=3.6
7
+ Requires-Python: >=3.8
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Dynamic: author
@@ -30,7 +30,7 @@ An incremental pre-push guard and AST blast-radius impact analyzer for git repos
30
30
  - Python AST symbol map: detects changed classes, functions, methods, module fields, and class fields, including deletions and renames.
31
31
  - Cross-layer impact linking: Python symbols to Django `{% url %}` template tags and frontend `http.get()/post()`-style calls.
32
32
  - Symbol cache (`.impact_analysis_cache.json`) so repeated runs skip unchanged files.
33
- - Profiles for `plain`, `django`, and `fastapi` projects.
33
+ - Profiles for `generic`, `django`, `fastapi`, `python`, and `frontend` projects.
34
34
  - Configurable pre-push hook installation that never silently clobbers an existing hook.
35
35
 
36
36
  ## Core idea
@@ -50,7 +50,7 @@ The change-set also handles the full breadth of realistic push shapes: direct pu
50
50
  pip install diffimpactscout
51
51
  ```
52
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`.
53
+ The package is pure Python (standard library only at runtime) and requires Python 3.8 or newer and the `git` CLI on `PATH`.
54
54
 
55
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
56
 
@@ -70,7 +70,21 @@ diffimpactscout impact
70
70
  diffimpactscout install-hooks
71
71
  ```
72
72
 
73
- `init` accepts `--profile plain|django|fastapi` to seed profile-appropriate settings.
73
+ `init` accepts `--profile generic|django|fastapi|python|frontend` to seed profile-appropriate settings. `install-hooks` detects the stack automatically (see [Supported setups](#supported-setups)).
74
+
75
+ ## Supported setups
76
+
77
+ `install-hooks` detects the project stack on first run and writes a `.diffimpactscout.json` that fits it:
78
+
79
+ | Detection signal | Profile | Guard checks | Impact globs |
80
+ | --- | --- | --- | --- |
81
+ | `manage.py` + `settings.py`/`wsgi.py` | django | default + `ruff`, `ruff-format` | urls, templates, frontend |
82
+ | `fastapi` in deps or `main.py` | fastapi | default + `ruff`, `ruff-format` | `**/*.py` routes + frontend globs |
83
+ | `package.json` + `src`/`app` `*.ts`/`*.js` | frontend | default + `eslint`, `prettier` | templates + frontend globs |
84
+ | `*.py` present | python | default + `ruff`, `ruff-format` | Python module references |
85
+ | anything else (or no signal) | generic | default from `init` | none |
86
+
87
+ 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](#configuration).
74
88
 
75
89
  ## Commands
76
90
 
@@ -82,7 +96,7 @@ Writes a `.diffimpactscout.json` into the current directory. If the file already
82
96
 
83
97
  | Flag | Description |
84
98
  | --- | --- |
85
- | `--profile {plain,django,fastapi}` | Seed defaults from the named profile (default: `plain`). |
99
+ | `--profile {generic,django,fastapi,python,frontend}` | Seed defaults from the named profile (default: `generic`). |
86
100
 
87
101
  ### `diffimpactscout guard`
88
102
 
@@ -114,8 +128,14 @@ Runs a single check by id (see [Checks reference](#checks-reference)). With no f
114
128
 
115
129
  Installs a `pre-push` git hook that runs `diffimpactscout guard` on push.
116
130
 
131
+ 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, and impact. 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.
132
+
117
133
  | Flag | Description |
118
134
  | --- | --- |
135
+ | `--profile {generic,django,fastapi,python,frontend}` | Pre-select the setup profile; skips the stack question. |
136
+ | `--blocking {warn,strict}` | Blocking mode for guard checks (default: `warn`). |
137
+ | `--yes`, `-y` | Accept defaults and skip prompts (non-interactive). |
138
+ | `--reconfigure` | Rewrite an existing `.diffimpactscout.json`. |
119
139
  | `--force` | Overwrite an existing `pre-push` hook that DiffImpactScout did not install. |
120
140
  | `--uninstall` | Remove the DiffImpactScout `pre-push` hook. |
121
141
 
@@ -156,7 +176,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
156
176
  "blocking": "warn"
157
177
  },
158
178
  "impact": {
159
- "profile": "plain",
179
+ "profile": "generic",
160
180
  "urls_globs": [],
161
181
  "template_globs": [],
162
182
  "frontend_globs": [],
@@ -173,7 +193,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
173
193
  | top-level | `use_gitignore` | When `true`, also excludes paths ignored by `git check-ignore`. |
174
194
  | `guard` | `checks` | Ordered list of check entries; each `{"id": ...}` may add `args`, `blocking`, and `always_block` overrides. |
175
195
  | `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. |
196
+ | `impact` | `profile` | `generic`, `django`, or `fastapi`; selects route extraction plus default globs. |
177
197
  | `impact` | `urls_globs` / `template_globs` / `frontend_globs` | Glob patterns for route files, Django templates, and frontend sources. |
178
198
  | `impact` | `cache_file` | Path of the symbol cache (relative to the repo root). |
179
199
  | `impact` | `fast_mode`, `threads` | Reserved defaults from `init`; fast mode is currently selected with the `impact --fast` flag. |
@@ -188,9 +208,11 @@ External checks run `command` (each `{file}` placeholder is replaced with the fi
188
208
 
189
209
  ### Profiles
190
210
 
191
- - `plain`: no route or cross-layer globs; impact analysis reports only Python references.
211
+ - `generic`: no route or cross-layer globs; impact analysis reports only Python references.
212
+ - `python`: same impact globs as `generic`; guard checks append `ruff` and `ruff-format`.
192
213
  - `django`: route files `**/urls.py`, templates `**/templates/**/*.html`, frontend `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `ruff` and `ruff-format`.
193
214
  - `fastapi`: routes extracted from `**/*.py`, same frontend globs; guard checks append `ruff` and `ruff-format`.
215
+ - `frontend`: template globs `**/*.html` and frontend globs `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `eslint` and `prettier`.
194
216
 
195
217
  ### Blocking behavior
196
218
 
@@ -284,7 +306,7 @@ All checks are incremental: file-scoped checks run on files in the change-set, a
284
306
 
285
307
  ## Requirements
286
308
 
287
- - Python 3.6 or newer.
309
+ - Python 3.8 or newer.
288
310
  - Standard library only at runtime; no third-party dependencies.
289
311
  - The `git` CLI on `PATH`.
290
312
  - Optional: `ruff`, `eslint`, and `prettier` for their respective checks (missing tools degrade to warnings).
@@ -7,6 +7,7 @@ src/diffimpactscout/__main__.py
7
7
  src/diffimpactscout/base.py
8
8
  src/diffimpactscout/cli.py
9
9
  src/diffimpactscout/config.py
10
+ src/diffimpactscout/detect.py
10
11
  src/diffimpactscout/env.py
11
12
  src/diffimpactscout/gitrun.py
12
13
  src/diffimpactscout/guard.py
@@ -34,5 +35,7 @@ src/diffimpactscout/impact/reporter.py
34
35
  src/diffimpactscout/impact/route_linker.py
35
36
  src/diffimpactscout/profiles/django.json
36
37
  src/diffimpactscout/profiles/fastapi.json
37
- src/diffimpactscout/profiles/plain.json
38
+ src/diffimpactscout/profiles/frontend.json
39
+ src/diffimpactscout/profiles/generic.json
40
+ src/diffimpactscout/profiles/python.json
38
41
  tests/test_fixture_smoke.py
File without changes