agent-maintainer 0.1.0b1__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.
Files changed (60) hide show
  1. agent_maintainer/__init__.py +0 -0
  2. agent_maintainer/__main__.py +10 -0
  3. agent_maintainer/catalogs/__init__.py +1 -0
  4. agent_maintainer/catalogs/catalog.py +300 -0
  5. agent_maintainer/catalogs/docs.py +153 -0
  6. agent_maintainer/catalogs/python.py +234 -0
  7. agent_maintainer/catalogs/security.py +286 -0
  8. agent_maintainer/checks/__init__.py +0 -0
  9. agent_maintainer/checks/change_budget.py +366 -0
  10. agent_maintainer/checks/file_lengths.py +346 -0
  11. agent_maintainer/checks/pip_audit_config.py +21 -0
  12. agent_maintainer/checks/structure.py +255 -0
  13. agent_maintainer/checks/suppression_budget.py +215 -0
  14. agent_maintainer/checks/tach_config.py +38 -0
  15. agent_maintainer/cli.py +95 -0
  16. agent_maintainer/config/__init__.py +1 -0
  17. agent_maintainer/config/coercion.py +111 -0
  18. agent_maintainer/config/loader.py +192 -0
  19. agent_maintainer/config/modes.py +52 -0
  20. agent_maintainer/config/schema.py +246 -0
  21. agent_maintainer/core/__init__.py +0 -0
  22. agent_maintainer/core/args.py +240 -0
  23. agent_maintainer/core/bootstrap.py +269 -0
  24. agent_maintainer/core/config.py +55 -0
  25. agent_maintainer/core/executor.py +255 -0
  26. agent_maintainer/core/guidance.py +301 -0
  27. agent_maintainer/core/init_template_config.py +121 -0
  28. agent_maintainer/core/init_templates.py +319 -0
  29. agent_maintainer/core/initializer.py +66 -0
  30. agent_maintainer/core/layout.py +64 -0
  31. agent_maintainer/core/reporting.py +245 -0
  32. agent_maintainer/core/runtime.py +33 -0
  33. agent_maintainer/core/tool_capabilities.py +193 -0
  34. agent_maintainer/core/tool_capability_registry.py +104 -0
  35. agent_maintainer/core/tool_capability_types.py +39 -0
  36. agent_maintainer/doctor/__init__.py +0 -0
  37. agent_maintainer/doctor/cli.py +274 -0
  38. agent_maintainer/doctor/setup.py +268 -0
  39. agent_maintainer/doctor/support/__init__.py +0 -0
  40. agent_maintainer/doctor/support/hook_audit.py +157 -0
  41. agent_maintainer/doctor/support/logs.py +225 -0
  42. agent_maintainer/doctor/support/models.py +29 -0
  43. agent_maintainer/doctor/support/policy.py +134 -0
  44. agent_maintainer/models.py +50 -0
  45. agent_maintainer/runners/__init__.py +0 -0
  46. agent_maintainer/runners/bandit.py +109 -0
  47. agent_maintainer/runners/mutmut.py +65 -0
  48. agent_maintainer/runners/pyright.py +158 -0
  49. agent_maintainer/runners/ruff.py +113 -0
  50. agent_maintainer/runners/secret_scan.py +112 -0
  51. agent_maintainer/tach.py +202 -0
  52. agent_maintainer/verify/__init__.py +1 -0
  53. agent_maintainer/verify/artifacts.py +205 -0
  54. agent_maintainer/verify/quiet.py +146 -0
  55. agent_maintainer-0.1.0b1.dist-info/METADATA +381 -0
  56. agent_maintainer-0.1.0b1.dist-info/RECORD +60 -0
  57. agent_maintainer-0.1.0b1.dist-info/WHEEL +5 -0
  58. agent_maintainer-0.1.0b1.dist-info/entry_points.txt +2 -0
  59. agent_maintainer-0.1.0b1.dist-info/licenses/LICENSE +21 -0
  60. agent_maintainer-0.1.0b1.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,10 @@
1
+ """Module entrypoint for `python -m agent_maintainer`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from agent_maintainer.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1 @@
1
+ """Maintainer check catalog package."""
@@ -0,0 +1,300 @@
1
+ """Declarative catalog of maintainer checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from agent_maintainer import models
9
+ from agent_maintainer.catalogs import python as python_checks
10
+ from agent_maintainer.catalogs.docs import docs_config_checks
11
+ from agent_maintainer.catalogs.security import (
12
+ license_check_checks,
13
+ osv_scanner_checks,
14
+ sbom_checks,
15
+ secret_scan_checks,
16
+ semgrep_checks,
17
+ trivy_checks,
18
+ )
19
+ from agent_maintainer.config.schema import (
20
+ FRESH_STRICT_MODE,
21
+ IMPORT_LINTER_TOOL,
22
+ TACH_TOOL,
23
+ MaintainerConfig,
24
+ )
25
+ from agent_maintainer.core.config import existing_paths
26
+
27
+ CHANGE_BUDGET_PROFILES = (
28
+ models.FAST_PROFILE,
29
+ models.PRECOMMIT_PROFILE,
30
+ models.FULL_PROFILE,
31
+ models.CI_PROFILE,
32
+ )
33
+
34
+
35
+ def existing_or_configured(paths: tuple[str, ...]) -> tuple[str, ...]:
36
+ """Prefer existing paths while preserving configured values for diagnostics."""
37
+
38
+ existing = tuple(existing_paths(paths))
39
+ return existing if existing else paths
40
+
41
+
42
+ def architecture_checks(config: MaintainerConfig) -> list[models.Check]:
43
+ """Build the selected architecture contract checks."""
44
+
45
+ if config.architecture_tool == TACH_TOOL:
46
+ return tach_checks(config)
47
+ return [
48
+ models.Check(
49
+ IMPORT_LINTER_TOOL,
50
+ ["lint-imports"],
51
+ models.FULL_PROFILES,
52
+ required_executable="lint-imports",
53
+ optional_skip_reason=(
54
+ ".importlinter is absent; architecture contracts are not configured"
55
+ ),
56
+ )
57
+ ]
58
+
59
+
60
+ def tach_checks(config: MaintainerConfig) -> list[models.Check]:
61
+ """Build Tach config and architecture checks for the selected strictness mode."""
62
+
63
+ strict = config.mode == FRESH_STRICT_MODE
64
+ config_command = [sys.executable, "-m", "agent_maintainer.checks.tach_config"]
65
+ if strict:
66
+ config_command.append("--strict-root-module")
67
+ optional_skip_reason = None
68
+ if not strict:
69
+ optional_skip_reason = "tach.toml is absent; architecture contracts are not configured"
70
+ return [
71
+ models.Check(
72
+ "tach-config",
73
+ config_command,
74
+ models.FULL_PROFILES,
75
+ required_paths=("tach.toml",) if strict else (),
76
+ optional_skip_reason=optional_skip_reason,
77
+ ),
78
+ models.Check(
79
+ "tach",
80
+ ["tach", "check", "--exact"],
81
+ models.FULL_PROFILES,
82
+ required_paths=("tach.toml",) if strict else (),
83
+ required_executable="tach",
84
+ optional_skip_reason=optional_skip_reason,
85
+ ),
86
+ ]
87
+
88
+
89
+ def vulture_paths(config: MaintainerConfig, package_paths: tuple[str, ...]) -> tuple[str, ...]:
90
+ """Return existing vulture scan paths, falling back to package paths."""
91
+
92
+ paths = tuple(path for path in config.vulture_paths if Path(path).exists())
93
+ return paths or package_paths
94
+
95
+
96
+ def workflow_checks() -> list[models.Check]:
97
+ """Build GitHub Actions workflow quality and security checks."""
98
+
99
+ skip_reason = ".github/workflows is absent; GitHub Actions checks are not applicable"
100
+ return [
101
+ models.Check(
102
+ "actionlint",
103
+ ["actionlint", "-no-color", "-oneline"],
104
+ models.FULL_PROFILES,
105
+ required_executable="actionlint",
106
+ optional_skip_reason=skip_reason,
107
+ ),
108
+ models.Check(
109
+ "zizmor",
110
+ ["zizmor", "--offline", "--no-progress", ".github/workflows", ".github/dependabot.yml"],
111
+ models.FULL_PROFILES,
112
+ required_executable="zizmor",
113
+ optional_skip_reason=skip_reason,
114
+ ),
115
+ ]
116
+
117
+
118
+ def make_checks(
119
+ config: MaintainerConfig, base_ref: str, compare_branch: str, *, staged: bool = False
120
+ ) -> list[models.Check]:
121
+ """Build the complete check catalog for all verifier profiles."""
122
+
123
+ package_paths = existing_or_configured(config.package_paths)
124
+ file_length_paths = existing_or_configured(config.file_length_paths)
125
+ return [
126
+ models.Check(
127
+ "file-length",
128
+ file_length_command(config, file_length_paths),
129
+ models.ALL_PROFILES,
130
+ ),
131
+ models.Check(
132
+ "structure-cohesion",
133
+ structure_command(config),
134
+ models.ALL_PROFILES,
135
+ report_success_output=True,
136
+ ),
137
+ *change_budget_checks(config, base_ref, staged=staged),
138
+ models.Check(
139
+ "suppression-budget",
140
+ suppression_budget_command(base_ref, staged=staged),
141
+ models.ALL_PROFILES,
142
+ required_paths=(".git",),
143
+ ),
144
+ models.Check(
145
+ "ruff-format",
146
+ ["ruff", "format", "--check", "."],
147
+ models.LOCAL_GATE_PROFILES,
148
+ required_executable="ruff",
149
+ ),
150
+ python_checks.ruff_check(config),
151
+ python_checks.pyright_check(config),
152
+ python_checks.pytest_check(config),
153
+ models.Check(
154
+ "radon-cc-report",
155
+ ["radon", "cc", *package_paths, "-a", "-s"],
156
+ models.FULL_PROFILES,
157
+ required_executable="radon",
158
+ ),
159
+ models.Check(
160
+ "radon-mi-report",
161
+ ["radon", "mi", *package_paths, "-s"],
162
+ models.FULL_PROFILES,
163
+ required_executable="radon",
164
+ ),
165
+ models.Check(
166
+ "xenon-complexity-gate",
167
+ [
168
+ "xenon",
169
+ "--max-absolute",
170
+ config.xenon_max_absolute,
171
+ "--max-modules",
172
+ config.xenon_max_modules,
173
+ "--max-average",
174
+ config.xenon_max_average,
175
+ *package_paths,
176
+ ],
177
+ models.LOCAL_GATE_PROFILES,
178
+ required_executable="xenon",
179
+ ),
180
+ models.Check(
181
+ "pylint",
182
+ ["pylint", *package_paths, "--score=n"],
183
+ models.FULL_PROFILES,
184
+ required_executable="pylint",
185
+ ),
186
+ *architecture_checks(config),
187
+ models.Check(
188
+ "deptry",
189
+ ["deptry", "."],
190
+ models.FULL_PROFILES,
191
+ required_executable="deptry",
192
+ ),
193
+ models.Check(
194
+ "vulture",
195
+ ["vulture", *vulture_paths(config, package_paths)],
196
+ models.FULL_PROFILES,
197
+ required_executable="vulture",
198
+ ),
199
+ python_checks.bandit_check(config),
200
+ python_checks.pip_audit_check(config),
201
+ python_checks.mutmut_check(config),
202
+ *semgrep_checks(config),
203
+ *osv_scanner_checks(config),
204
+ *trivy_checks(config),
205
+ *sbom_checks(config),
206
+ *license_check_checks(config),
207
+ *secret_scan_checks(config, base_ref, staged=staged),
208
+ *workflow_checks(),
209
+ python_checks.wemake_check(config, package_paths),
210
+ python_checks.interrogate_check(config, package_paths),
211
+ *docs_config_checks(config),
212
+ python_checks.diff_cover_check(config, compare_branch),
213
+ ]
214
+
215
+
216
+ def change_budget_checks(
217
+ config: MaintainerConfig, base_ref: str, *, staged: bool
218
+ ) -> list[models.Check]:
219
+ """Build profile-specific change-budget checks."""
220
+
221
+ return [
222
+ models.Check(
223
+ "change-budget",
224
+ change_budget_command(
225
+ config,
226
+ base_ref,
227
+ staged=staged,
228
+ missing_test_change_as_error=(
229
+ profile in config.source_without_test_change_error_profiles
230
+ ),
231
+ ),
232
+ frozenset((profile,)),
233
+ required_paths=(".git",),
234
+ report_success_output=True,
235
+ )
236
+ for profile in CHANGE_BUDGET_PROFILES
237
+ ]
238
+
239
+
240
+ def file_length_command(config: MaintainerConfig, file_length_paths: tuple[str, ...]) -> list[str]:
241
+ """Build the file-length command with optional legacy-ratchet baseline."""
242
+
243
+ command = [sys.executable, "-m", "agent_maintainer.checks.file_lengths", *file_length_paths]
244
+ if config.file_length_baseline:
245
+ command.extend(["--baseline", config.file_length_baseline])
246
+ return command
247
+
248
+
249
+ def structure_command(config: MaintainerConfig) -> list[str]:
250
+ """Build structure cohesion command."""
251
+
252
+ paths = config.structure_paths or config.source_roots
253
+ command = [
254
+ sys.executable,
255
+ "-m",
256
+ "agent_maintainer.checks.structure",
257
+ "--warn-threshold",
258
+ str(config.folder_file_warn),
259
+ "--block-threshold",
260
+ str(config.folder_file_block if config.mode == FRESH_STRICT_MODE else 0),
261
+ "--cluster-min",
262
+ str(config.structure_cluster_min),
263
+ ]
264
+ for ignored_path in config.structure_ignore_paths:
265
+ command.extend(("--ignore", ignored_path))
266
+ for pattern in config.structure_hint_patterns:
267
+ command.extend(("--hint-pattern", pattern))
268
+ return [*command, *paths]
269
+
270
+
271
+ def change_budget_command(
272
+ config: MaintainerConfig,
273
+ base_ref: str,
274
+ *,
275
+ staged: bool,
276
+ missing_test_change_as_error: bool = False,
277
+ ) -> list[str]:
278
+ """Build the change-budget command with configured source and test roots."""
279
+
280
+ command = [sys.executable, "-m", "agent_maintainer.checks.change_budget", base_ref]
281
+ if staged:
282
+ command.append("--staged")
283
+ if missing_test_change_as_error:
284
+ command.append("--missing-test-change-as-error")
285
+ if config.allow_source_without_test_change:
286
+ command.append("--allow-source-without-test-change")
287
+ for root in config.source_roots:
288
+ command.extend(["--source-root", root])
289
+ for root in config.test_roots:
290
+ command.extend(["--test-root", root])
291
+ return command
292
+
293
+
294
+ def suppression_budget_command(base_ref: str, *, staged: bool) -> list[str]:
295
+ """Build the suppression-budget command for staged or ref-based diffs."""
296
+
297
+ command = [sys.executable, "-m", "agent_maintainer.checks.suppression_budget", base_ref]
298
+ if staged:
299
+ command.append("--staged")
300
+ return command
@@ -0,0 +1,153 @@
1
+ """Documentation and config hygiene check catalog helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from glob import glob
6
+ from pathlib import Path
7
+
8
+ from agent_maintainer import models
9
+ from agent_maintainer.config.schema import MaintainerConfig
10
+
11
+ IGNORED_PATH_PARTS = frozenset(
12
+ (
13
+ ".git",
14
+ ".mypy_cache",
15
+ ".pytest_cache",
16
+ ".ruff_cache",
17
+ ".venv",
18
+ ".verify-logs",
19
+ "__pycache__",
20
+ "build",
21
+ "dist",
22
+ "htmlcov",
23
+ "node_modules",
24
+ "venv",
25
+ )
26
+ )
27
+ MARKDOWNLINT_SKIP_REASON = (
28
+ "disabled by default; enable with AGENT_MAINTAINER_ENABLE_MARKDOWNLINT=1 or "
29
+ "[tool.agent_maintainer].enable_markdownlint = true"
30
+ )
31
+ YAMLLINT_SKIP_REASON = (
32
+ "disabled by default; enable with AGENT_MAINTAINER_ENABLE_YAMLLINT=1 or "
33
+ "[tool.agent_maintainer].enable_yamllint = true"
34
+ )
35
+ TAPLO_SKIP_REASON = (
36
+ "disabled by default; enable with AGENT_MAINTAINER_ENABLE_TAPLO=1 or "
37
+ "[tool.agent_maintainer].enable_taplo = true"
38
+ )
39
+ CHECK_JSONSCHEMA_SKIP_REASON = (
40
+ "disabled by default; enable with AGENT_MAINTAINER_ENABLE_CHECK_JSONSCHEMA=1 or "
41
+ "[tool.agent_maintainer].enable_check_jsonschema = true"
42
+ )
43
+
44
+
45
+ def docs_config_checks(config: MaintainerConfig) -> list[models.Check]:
46
+ """Build documentation and config hygiene checks."""
47
+ return [
48
+ markdownlint_check(config),
49
+ yamllint_check(config),
50
+ taplo_check(config),
51
+ check_jsonschema_check(config),
52
+ ]
53
+
54
+
55
+ def markdownlint_check(config: MaintainerConfig) -> models.Check:
56
+ """Build Markdown structure lint check."""
57
+ if not config.enable_markdownlint:
58
+ return disabled_check("markdownlint", "markdownlint-cli2", MARKDOWNLINT_SKIP_REASON)
59
+ paths = matching_paths(config.markdownlint_paths)
60
+ if not paths:
61
+ return no_files_check("markdownlint", "Markdown", config.markdownlint_paths)
62
+ return models.Check(
63
+ "markdownlint",
64
+ ["markdownlint-cli2", *paths],
65
+ models.FULL_PROFILES,
66
+ required_executable="markdownlint-cli2",
67
+ )
68
+
69
+
70
+ def yamllint_check(config: MaintainerConfig) -> models.Check:
71
+ """Build YAML structure lint check."""
72
+ if not config.enable_yamllint:
73
+ return disabled_check("yamllint", "yamllint", YAMLLINT_SKIP_REASON)
74
+ paths = matching_paths(config.yamllint_paths)
75
+ if not paths:
76
+ return no_files_check("yamllint", "YAML", config.yamllint_paths)
77
+ return models.Check(
78
+ "yamllint",
79
+ ["yamllint", *paths],
80
+ models.FULL_PROFILES,
81
+ required_executable="yamllint",
82
+ )
83
+
84
+
85
+ def taplo_check(config: MaintainerConfig) -> models.Check:
86
+ """Build TOML formatting check."""
87
+ if not config.enable_taplo:
88
+ return disabled_check("taplo", "taplo", TAPLO_SKIP_REASON)
89
+ paths = matching_paths(config.taplo_paths)
90
+ if not paths:
91
+ return no_files_check("taplo", "TOML", config.taplo_paths)
92
+ return models.Check(
93
+ "taplo",
94
+ ["taplo", "fmt", "--check", *paths],
95
+ models.FULL_PROFILES,
96
+ required_executable="taplo",
97
+ )
98
+
99
+
100
+ def check_jsonschema_check(config: MaintainerConfig) -> models.Check:
101
+ """Build JSON/YAML schema validation check."""
102
+ if not config.enable_check_jsonschema:
103
+ return disabled_check(
104
+ "check-jsonschema",
105
+ "check-jsonschema",
106
+ CHECK_JSONSCHEMA_SKIP_REASON,
107
+ )
108
+ if not config.check_jsonschema_args:
109
+ return models.Check(
110
+ "check-jsonschema",
111
+ ["check-jsonschema"],
112
+ models.FULL_PROFILES,
113
+ optional_skip_reason="enabled without schema arguments; no schema contracts configured",
114
+ )
115
+ return models.Check(
116
+ "check-jsonschema",
117
+ ["check-jsonschema", *config.check_jsonschema_args],
118
+ models.FULL_PROFILES,
119
+ required_executable="check-jsonschema",
120
+ )
121
+
122
+
123
+ def disabled_check(name: str, command: str, reason: str) -> models.Check:
124
+ """Return an explicit disabled optional check."""
125
+ return models.Check(name, [command], models.FULL_PROFILES, optional_skip_reason=reason)
126
+
127
+
128
+ def no_files_check(name: str, label: str, paths: tuple[str, ...]) -> models.Check:
129
+ """Return an explicit no-files optional check."""
130
+ configured = ", ".join(paths) or "<none>"
131
+ return models.Check(
132
+ name,
133
+ [name],
134
+ models.FULL_PROFILES,
135
+ optional_skip_reason=f"enabled but no {label} files matched: {configured}",
136
+ )
137
+
138
+
139
+ def matching_paths(patterns: tuple[str, ...]) -> tuple[str, ...]:
140
+ """Return existing paths matched by configured file or glob patterns."""
141
+ matches: list[str] = []
142
+ for pattern in patterns:
143
+ path = Path(pattern)
144
+ if path.exists() and not ignored(path):
145
+ matches.append(pattern)
146
+ continue
147
+ matches.extend(match for match in glob(pattern, recursive=True) if not ignored(Path(match)))
148
+ return tuple(sorted(set(matches)))
149
+
150
+
151
+ def ignored(path: Path) -> bool:
152
+ """Return whether path belongs to generated or cache output."""
153
+ return bool(IGNORED_PATH_PARTS.intersection(path.parts))
@@ -0,0 +1,234 @@
1
+ """Python quality and security checks for the maintenance catalog."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from agent_maintainer import models
9
+ from agent_maintainer.config.schema import FRESH_STRICT_MODE, MaintainerConfig
10
+
11
+
12
+ def pytest_command(config: MaintainerConfig) -> list[str]:
13
+ """Build the coverage-enforcing pytest command."""
14
+
15
+ artifacts_dir = Path(config.diagnostic_artifacts_dir)
16
+ coverage_json = artifacts_dir / "coverage.json"
17
+ junit_xml = artifacts_dir / "pytest-junit.xml"
18
+ command = ["pytest", "-q", "--tb=short", "--disable-warnings", "-p", "no:tach"]
19
+ command.extend(f"--cov={source}" for source in config.coverage_source)
20
+ command.extend(
21
+ [
22
+ "--cov-report=term-missing:skip-covered",
23
+ "--cov-report=xml",
24
+ f"--cov-report=json:{coverage_json}",
25
+ f"--junitxml={junit_xml}",
26
+ f"--cov-fail-under={config.coverage_fail_under}",
27
+ *config.test_roots,
28
+ ]
29
+ )
30
+ return command
31
+
32
+
33
+ def pytest_check(config: MaintainerConfig) -> models.Check:
34
+ """Build the pytest coverage check or its require-tests skip."""
35
+
36
+ if config.require_tests:
37
+ artifacts_dir = Path(config.diagnostic_artifacts_dir)
38
+ return models.Check(
39
+ "pytest-coverage",
40
+ pytest_command(config),
41
+ models.LOCAL_GATE_PROFILES,
42
+ required_executable="pytest",
43
+ artifact_paths=(
44
+ "coverage.xml",
45
+ str(artifacts_dir / "coverage.json"),
46
+ str(artifacts_dir / "pytest-junit.xml"),
47
+ ),
48
+ )
49
+ return models.Check(
50
+ "pytest-coverage",
51
+ ["pytest"],
52
+ models.LOCAL_GATE_PROFILES,
53
+ optional_skip_reason="tests are disabled by require_tests = false",
54
+ )
55
+
56
+
57
+ def diff_cover_check(config: MaintainerConfig, compare_branch: str) -> models.Check:
58
+ """Build the changed-code coverage check for CI profiles."""
59
+
60
+ if config.require_tests:
61
+ return models.Check(
62
+ "diff-cover",
63
+ [
64
+ "diff-cover",
65
+ "coverage.xml",
66
+ f"--compare-branch={compare_branch}",
67
+ f"--fail-under={config.diff_cover_fail_under}",
68
+ ],
69
+ models.CI_ONLY_PROFILES,
70
+ required_paths=("coverage.xml", ".git"),
71
+ required_executable="diff-cover",
72
+ )
73
+ return models.Check(
74
+ "diff-cover",
75
+ ["diff-cover"],
76
+ models.CI_ONLY_PROFILES,
77
+ optional_skip_reason="changed-code coverage is disabled because require_tests = false",
78
+ )
79
+
80
+
81
+ def pip_audit_check(config: MaintainerConfig) -> models.Check:
82
+ """Build the dependency vulnerability check or its explicit skip."""
83
+
84
+ if not config.enable_pip_audit:
85
+ return models.Check(
86
+ "pip-audit",
87
+ ["pip-audit"],
88
+ models.FULL_PROFILES,
89
+ optional_skip_reason=(
90
+ "disabled by default; enable with AGENT_MAINTAINER_ENABLE_PIP_AUDIT=1 or "
91
+ "[tool.agent_maintainer].enable_pip_audit = true"
92
+ ),
93
+ )
94
+ if not config.pip_audit_args:
95
+ if config.mode == FRESH_STRICT_MODE:
96
+ return models.Check(
97
+ "pip-audit",
98
+ [sys.executable, "-m", "agent_maintainer.checks.pip_audit_config"],
99
+ models.FULL_PROFILES,
100
+ )
101
+ return models.Check(
102
+ "pip-audit",
103
+ ["pip-audit"],
104
+ models.FULL_PROFILES,
105
+ optional_skip_reason=(
106
+ "enabled without pinned input; skipped to avoid auditing the active environment"
107
+ ),
108
+ )
109
+ return models.Check(
110
+ "pip-audit",
111
+ ["pip-audit", *config.pip_audit_args],
112
+ models.FULL_PROFILES,
113
+ required_executable="pip-audit",
114
+ )
115
+
116
+
117
+ def mutmut_check(config: MaintainerConfig) -> models.Check:
118
+ """Build mutation-testing check reserved for the manual profile."""
119
+
120
+ command = [sys.executable, "-m", "agent_maintainer.runners.mutmut", *config.mutmut_args]
121
+ if not config.enable_mutmut:
122
+ return models.Check(
123
+ "mutmut",
124
+ command,
125
+ models.MANUAL_PROFILES,
126
+ optional_skip_reason=(
127
+ "disabled by default; enable with AGENT_MAINTAINER_ENABLE_MUTMUT=1 or "
128
+ "[tool.agent_maintainer].enable_mutmut = true"
129
+ ),
130
+ )
131
+ return models.Check(
132
+ "mutmut",
133
+ command,
134
+ models.MANUAL_PROFILES,
135
+ required_executable="mutmut",
136
+ )
137
+
138
+
139
+ def pyright_check(config: MaintainerConfig) -> models.Check:
140
+ """Build the Pyright check through the generated-project wrapper."""
141
+
142
+ artifacts_dir = Path(config.diagnostic_artifacts_dir)
143
+ return models.Check(
144
+ "pyright",
145
+ [sys.executable, "-m", "agent_maintainer.runners.pyright"],
146
+ models.LOCAL_GATE_PROFILES,
147
+ required_executable="pyright",
148
+ artifact_paths=(
149
+ str(artifacts_dir / "pyright.json"),
150
+ str(artifacts_dir / "pyrightconfig.generated.json"),
151
+ ),
152
+ )
153
+
154
+
155
+ def ruff_check(config: MaintainerConfig) -> models.Check:
156
+ """Build the Ruff check through the JSON-artifact wrapper."""
157
+
158
+ artifacts_dir = Path(config.diagnostic_artifacts_dir)
159
+ return models.Check(
160
+ "ruff",
161
+ [sys.executable, "-m", "agent_maintainer.runners.ruff"],
162
+ models.ALL_PROFILES,
163
+ required_executable="ruff",
164
+ artifact_paths=(str(artifacts_dir / "ruff.json"),),
165
+ )
166
+
167
+
168
+ def bandit_check(config: MaintainerConfig) -> models.Check:
169
+ """Build the Bandit check through the JSON-artifact wrapper."""
170
+
171
+ artifacts_dir = Path(config.diagnostic_artifacts_dir)
172
+ return models.Check(
173
+ "bandit",
174
+ [sys.executable, "-m", "agent_maintainer.runners.bandit"],
175
+ models.FULL_PROFILES,
176
+ required_executable="bandit",
177
+ artifact_paths=(str(artifacts_dir / "bandit.json"),),
178
+ )
179
+
180
+
181
+ def wemake_check(config: MaintainerConfig, package_paths: tuple[str, ...]) -> models.Check:
182
+ """Build the wemake strict-style check or its explicit skip."""
183
+
184
+ if not config.enable_wemake:
185
+ return models.Check(
186
+ "wemake",
187
+ ["flake8"],
188
+ models.FULL_PROFILES,
189
+ optional_skip_reason=(
190
+ "disabled by default; enable with AGENT_MAINTAINER_ENABLE_WEMAKE=1 or "
191
+ "[tool.agent_maintainer].enable_wemake = true"
192
+ ),
193
+ )
194
+ return models.Check(
195
+ "wemake",
196
+ [
197
+ "flake8",
198
+ "--require-plugins",
199
+ "wemake-python-styleguide",
200
+ *package_paths,
201
+ ],
202
+ models.FULL_PROFILES,
203
+ required_executable="flake8",
204
+ )
205
+
206
+
207
+ def interrogate_check(config: MaintainerConfig, package_paths: tuple[str, ...]) -> models.Check:
208
+ """Build the docstring coverage check or its explicit optional skip."""
209
+
210
+ if not config.enable_interrogate:
211
+ return models.Check(
212
+ "interrogate",
213
+ ["interrogate"],
214
+ models.FULL_PROFILES,
215
+ optional_skip_reason=(
216
+ "disabled by default; enable with AGENT_MAINTAINER_ENABLE_INTERROGATE=1 or "
217
+ "[tool.agent_maintainer].enable_interrogate = true"
218
+ ),
219
+ )
220
+ return models.Check(
221
+ "interrogate",
222
+ [
223
+ "interrogate",
224
+ f"--fail-under={config.interrogate_fail_under}",
225
+ "--ignore-init-method",
226
+ "--ignore-init-module",
227
+ "--ignore-private",
228
+ "--ignore-semiprivate",
229
+ "--ignore-magic",
230
+ *package_paths,
231
+ ],
232
+ models.FULL_PROFILES,
233
+ required_executable="interrogate",
234
+ )