claude-dev-env 2.30.1 → 2.31.0
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.
- package/.agents/agents/pr-description-writer.md +1 -1
- package/AGENTS.md +6 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +37 -0
- package/bin/install.prune.test.mjs +4 -2
- package/docs/CODE_RULES.md +11 -0
- package/hooks/advisory/migration_safety_advisor.py +25 -1
- package/hooks/advisory/refactor_guard.py +85 -18
- package/hooks/advisory/test_migration_safety_advisor.py +89 -5
- package/hooks/advisory/test_refactor_guard.py +87 -0
- package/hooks/advisory/test_refactor_guard_advisory.py +72 -1
- package/hooks/blocking/code_rules_constants_config.py +7 -6
- package/hooks/blocking/code_rules_enforcer.py +57 -31
- package/hooks/blocking/codex_apply_patch.py +136 -21
- package/hooks/blocking/config/codex_apply_patch_constants.py +13 -0
- package/hooks/blocking/conftest.py +70 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +26 -16
- package/hooks/blocking/hook_prose_detector_consistency.py +30 -20
- package/hooks/blocking/pii_payload_scan.py +61 -0
- package/hooks/blocking/pii_prevention_blocker.py +5 -0
- package/hooks/blocking/pr_description_writer_gate.py +263 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +1 -1
- package/hooks/blocking/sensitive_file_protector.py +18 -3
- package/hooks/blocking/shell_substitution_blocker.py +25 -47
- package/hooks/blocking/stale_comment_reference_blocker.py +94 -24
- package/hooks/blocking/state_description_blocker.py +39 -26
- package/hooks/blocking/subprocess_budget_completeness.py +17 -43
- package/hooks/blocking/tdd_enforcer.py +57 -14
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +11 -1
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +180 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +6 -2
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +18 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +66 -1
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +143 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +12 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +63 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +81 -2
- package/hooks/blocking/test_code_rules_enforcer_codex_apply_patch.py +35 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +29 -16
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_multi_edit.py +118 -0
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +29 -28
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_stage.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -0
- package/hooks/blocking/test_codex_apply_patch.py +43 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +68 -0
- package/hooks/blocking/test_hook_prose_detector_consistency.py +41 -0
- package/hooks/blocking/test_pii_payload_scan.py +65 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +80 -10
- package/hooks/blocking/test_pr_description_writer_gate.py +216 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +215 -25
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +9 -4
- package/hooks/blocking/test_sensitive_file_protector.py +42 -2
- package/hooks/blocking/test_stale_comment_reference_blocker.py +71 -0
- package/hooks/blocking/test_state_description_blocker.py +68 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +67 -0
- package/hooks/blocking/test_tdd_enforcer.py +182 -2
- package/hooks/blocking/test_windows_rmtree_blocker.py +63 -0
- package/hooks/blocking/test_write_existing_file_blocker.py +122 -0
- package/hooks/blocking/windows_rmtree_blocker.py +21 -7
- package/hooks/blocking/write_existing_file_blocker.py +75 -29
- package/hooks/conftest.py +47 -0
- package/hooks/hooks.json +7 -12
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +3 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +4 -0
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +3 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +7 -2
- package/hooks/hooks_constants/migration_safety_advisor_constants.py +7 -0
- package/hooks/hooks_constants/multi_edit_reconstruction.py +44 -3
- package/hooks/hooks_constants/pii_prevention_constants.py +9 -7
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +39 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +31 -14
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +15 -1
- package/hooks/hooks_constants/state_description_blocker_constants.py +1 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +71 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +2 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/test_multi_edit_reconstruction.py +102 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +139 -25
- package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +70 -0
- package/hooks/hooks_constants/validation_phase_constants.py +2 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +7 -3
- package/hooks/observability/pr_description_writer_spawn_tracker.py +121 -0
- package/hooks/observability/session_file_edit_tracker.py +25 -4
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +146 -0
- package/hooks/observability/test_session_file_edit_tracker.py +32 -0
- package/hooks/validation/hook_format_validator.py +23 -9
- package/hooks/validation/test_hook_format_validator.py +58 -0
- package/hooks/validators/README.md +1 -1
- package/hooks/validators/abbreviation_checks.py +3 -5
- package/hooks/validators/config/abbreviation_checks_constants.py +3 -0
- package/hooks/validators/config/fast_save_validator_constants.py +32 -0
- package/hooks/validators/config/hook_timing_harness_constants.py +3 -1
- package/hooks/validators/config/test_abbreviation_checks_constants.py +11 -0
- package/hooks/validators/fast_save_validators.py +332 -0
- package/hooks/validators/hook_timing_harness.py +8 -4
- package/hooks/validators/python_style_checks.py +34 -60
- package/hooks/validators/python_style_helpers.py +16 -2
- package/hooks/validators/python_style_import_bootstrap.py +83 -0
- package/hooks/validators/run_all_validators.py +42 -41
- package/hooks/validators/test_abbreviation_checks.py +20 -0
- package/hooks/validators/test_fast_save_validators.py +172 -0
- package/hooks/validators/test_python_style_checks.py +9 -0
- package/hooks/validators/test_python_style_checks_import_order.py +81 -0
- package/hooks/validators/test_python_style_helpers.py +35 -0
- package/hooks/validators/test_python_style_import_bootstrap.py +69 -0
- package/hooks/validators/test_run_all_validators.py +46 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +2 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/git-workflow.md +2 -0
- package/rules/testing.md +4 -0
- package/scripts/codex_compat_materializer.py +5 -0
- package/scripts/tests/test_codex_compat_materializer.py +37 -11
- package/hooks/blocking/tests/conftest.py +0 -10
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""In-process, Mypy-free validator roster for the Write/Edit save-path gate.
|
|
2
|
+
|
|
3
|
+
``run_all_validators.py``'s PreToolUse gate calls ``run_fast_save_validators``
|
|
4
|
+
in place of the CLI/full mode's per-check subprocess and its Mypy pass.
|
|
5
|
+
|
|
6
|
+
::
|
|
7
|
+
|
|
8
|
+
save path (this module): 12 checks, in-process, no Mypy, no subprocess
|
|
9
|
+
CLI / full mode (run_all_validators.main): the same 12 checks, plus Mypy
|
|
10
|
+
and Ruff, each still its own subprocess -- unchanged by this module
|
|
11
|
+
|
|
12
|
+
Every check here calls the exact same ``validate_file`` (or check) function
|
|
13
|
+
the CLI mode calls. A save-path finding matches the CLI mode's finding for
|
|
14
|
+
the same validator; only Mypy and the process-per-check overhead are gone.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from collections.abc import Callable, Sequence
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from hooks_constants.mypy_integration_constants import PYTHON_SOURCE_SUFFIX
|
|
22
|
+
|
|
23
|
+
from .abbreviation_checks import validate_file as validate_abbreviation_file
|
|
24
|
+
from .code_quality_checks import validate_file as validate_code_quality_file
|
|
25
|
+
from .config.fast_save_validator_constants import (
|
|
26
|
+
ALL_CODE_REFERENCE_FILE_SUFFIXES,
|
|
27
|
+
ALL_REACT_COMPONENT_FILE_SUFFIXES,
|
|
28
|
+
NO_CODE_REFERENCE_FILES_MESSAGE,
|
|
29
|
+
NO_PYTHON_FILES_MESSAGE,
|
|
30
|
+
NO_REACT_FILES_MESSAGE,
|
|
31
|
+
NO_TEST_FILES_MESSAGE,
|
|
32
|
+
NO_VIOLATIONS_FOUND_MESSAGE,
|
|
33
|
+
VIOLATION_REPORT_LINE_SEPARATOR,
|
|
34
|
+
)
|
|
35
|
+
from .magic_value_checks import validate_file as validate_magic_literal_file
|
|
36
|
+
from .pr_reference_checks import validate_file as validate_pr_reference_file
|
|
37
|
+
from .python_antipattern_checks import validate_file as validate_python_antipattern_file
|
|
38
|
+
from .python_style_checks import validate_file as validate_python_style_file
|
|
39
|
+
from .react_checks import check_no_class_components
|
|
40
|
+
from .security_checks import validate_file as validate_security_file
|
|
41
|
+
from .test_safety_checks import (
|
|
42
|
+
check_debug_guard_in_dev_scripts,
|
|
43
|
+
check_no_skip_decorators,
|
|
44
|
+
)
|
|
45
|
+
from .todo_checks import validate_file as validate_todo_file
|
|
46
|
+
from .type_safety_checks import validate_file as validate_type_safety_file
|
|
47
|
+
from .useless_test_checks import validate_file as validate_useless_test_file
|
|
48
|
+
|
|
49
|
+
FilePredicate = Callable[[Path], bool]
|
|
50
|
+
FileViolationLineCollector = Callable[[Path], "list[str]"]
|
|
51
|
+
_RawFileValidator = Callable[[Path], "Sequence[object]"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _violation_lines_from(raw_validate_file: _RawFileValidator) -> FileViolationLineCollector:
|
|
55
|
+
"""Wrap a ``validate_file`` returning check-specific Violation objects.
|
|
56
|
+
|
|
57
|
+
::
|
|
58
|
+
|
|
59
|
+
wrap(abbreviation_checks.validate_file) -> Callable[[Path], list[str]]
|
|
60
|
+
|
|
61
|
+
Each check module defines its own ``Violation``-shaped dataclass with a
|
|
62
|
+
matching ``__str__``, so formatting through ``str()`` here needs no shared
|
|
63
|
+
base type across the four differently-typed modules that define one.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
raw_validate_file: A module's own ``validate_file(path) -> [Violation, ...]``.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A collector returning each violation's ``str()`` line, not the
|
|
70
|
+
object itself.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def _collect(file_path: Path) -> list[str]:
|
|
74
|
+
return [str(each_violation) for each_violation in raw_validate_file(file_path)]
|
|
75
|
+
|
|
76
|
+
return _collect
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class FastSaveCheckOutcome:
|
|
81
|
+
"""One save-path check's finding.
|
|
82
|
+
|
|
83
|
+
The caller wraps this into its own reporting type. This module never
|
|
84
|
+
imports that type back, so no circular import exists between the two
|
|
85
|
+
modules.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
display_name: str
|
|
89
|
+
checks: str
|
|
90
|
+
is_clean: bool
|
|
91
|
+
violation_report: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_python_file(file_path: Path) -> bool:
|
|
95
|
+
return file_path.suffix == PYTHON_SOURCE_SUFFIX
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _is_test_python_file(file_path: Path) -> bool:
|
|
99
|
+
return "test" in file_path.name.lower() and file_path.suffix == PYTHON_SOURCE_SUFFIX
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_react_file(file_path: Path) -> bool:
|
|
103
|
+
return file_path.suffix in ALL_REACT_COMPONENT_FILE_SUFFIXES
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_code_reference_file(file_path: Path) -> bool:
|
|
107
|
+
return file_path.suffix in ALL_CODE_REFERENCE_FILE_SUFFIXES
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _collect_test_safety_violation_lines(file_path: Path) -> "list[str]":
|
|
111
|
+
"""Adapt test_safety_checks's whole-batch ``main`` into one file at a time."""
|
|
112
|
+
try:
|
|
113
|
+
code = file_path.read_text(encoding="utf-8")
|
|
114
|
+
except OSError:
|
|
115
|
+
return []
|
|
116
|
+
filepath = str(file_path)
|
|
117
|
+
all_violations = [
|
|
118
|
+
*check_no_skip_decorators(code, filepath),
|
|
119
|
+
*check_debug_guard_in_dev_scripts(code, filepath),
|
|
120
|
+
]
|
|
121
|
+
return [str(each_violation) for each_violation in all_violations]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _collect_react_violation_lines(file_path: Path) -> "list[str]":
|
|
125
|
+
"""Adapt react_checks's whole-batch ``check_no_class_components`` to one file.
|
|
126
|
+
|
|
127
|
+
react_checks.Violation carries no ``__str__`` override, so this formats
|
|
128
|
+
each violation the same way react_checks's own ``main`` prints one.
|
|
129
|
+
"""
|
|
130
|
+
all_violations = check_no_class_components([str(file_path)])
|
|
131
|
+
return [
|
|
132
|
+
f"{each_violation.file}:{each_violation.line}: {each_violation.message}"
|
|
133
|
+
for each_violation in all_violations
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class _FastSaveValidatorSpecification:
|
|
139
|
+
"""One save-path validator: display name, check numbers, and how to run it."""
|
|
140
|
+
|
|
141
|
+
display_name: str
|
|
142
|
+
checks: str
|
|
143
|
+
applies_to_file: FilePredicate
|
|
144
|
+
collect_violation_lines: FileViolationLineCollector
|
|
145
|
+
no_matching_files_message: str
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _python_naming_and_syntax_specifications() -> (
|
|
149
|
+
"list[_FastSaveValidatorSpecification]"
|
|
150
|
+
):
|
|
151
|
+
"""Return the save-path checks for Python naming, style, and constants."""
|
|
152
|
+
return [
|
|
153
|
+
_FastSaveValidatorSpecification(
|
|
154
|
+
"Python Style",
|
|
155
|
+
"1,2,3,4",
|
|
156
|
+
_is_python_file,
|
|
157
|
+
_violation_lines_from(validate_python_style_file),
|
|
158
|
+
NO_PYTHON_FILES_MESSAGE,
|
|
159
|
+
),
|
|
160
|
+
_FastSaveValidatorSpecification(
|
|
161
|
+
"Abbreviations",
|
|
162
|
+
"5",
|
|
163
|
+
_is_python_file,
|
|
164
|
+
_violation_lines_from(validate_abbreviation_file),
|
|
165
|
+
NO_PYTHON_FILES_MESSAGE,
|
|
166
|
+
),
|
|
167
|
+
_FastSaveValidatorSpecification(
|
|
168
|
+
"Magic Values",
|
|
169
|
+
"7",
|
|
170
|
+
_is_python_file,
|
|
171
|
+
_violation_lines_from(validate_magic_literal_file),
|
|
172
|
+
NO_PYTHON_FILES_MESSAGE,
|
|
173
|
+
),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _python_quality_and_tracking_specifications() -> (
|
|
178
|
+
"list[_FastSaveValidatorSpecification]"
|
|
179
|
+
):
|
|
180
|
+
"""Return the save-path checks for Python code quality and TODO tracking."""
|
|
181
|
+
return [
|
|
182
|
+
_FastSaveValidatorSpecification(
|
|
183
|
+
"TODO Tracking",
|
|
184
|
+
"36",
|
|
185
|
+
_is_python_file,
|
|
186
|
+
_violation_lines_from(validate_todo_file),
|
|
187
|
+
NO_PYTHON_FILES_MESSAGE,
|
|
188
|
+
),
|
|
189
|
+
_FastSaveValidatorSpecification(
|
|
190
|
+
"Security",
|
|
191
|
+
"27,28,29",
|
|
192
|
+
_is_python_file,
|
|
193
|
+
_violation_lines_from(validate_security_file),
|
|
194
|
+
NO_PYTHON_FILES_MESSAGE,
|
|
195
|
+
),
|
|
196
|
+
_FastSaveValidatorSpecification(
|
|
197
|
+
"Code Quality",
|
|
198
|
+
"30,31,32",
|
|
199
|
+
_is_python_file,
|
|
200
|
+
_violation_lines_from(validate_code_quality_file),
|
|
201
|
+
NO_PYTHON_FILES_MESSAGE,
|
|
202
|
+
),
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _python_safety_and_typing_specifications() -> (
|
|
207
|
+
"list[_FastSaveValidatorSpecification]"
|
|
208
|
+
):
|
|
209
|
+
"""Return the save-path checks for Python anti-patterns and typing."""
|
|
210
|
+
return [
|
|
211
|
+
_FastSaveValidatorSpecification(
|
|
212
|
+
"Python Anti-patterns",
|
|
213
|
+
"33,34,35",
|
|
214
|
+
_is_python_file,
|
|
215
|
+
_violation_lines_from(validate_python_antipattern_file),
|
|
216
|
+
NO_PYTHON_FILES_MESSAGE,
|
|
217
|
+
),
|
|
218
|
+
_FastSaveValidatorSpecification(
|
|
219
|
+
"Type Safety",
|
|
220
|
+
"39,40",
|
|
221
|
+
_is_python_file,
|
|
222
|
+
_violation_lines_from(validate_type_safety_file),
|
|
223
|
+
NO_PYTHON_FILES_MESSAGE,
|
|
224
|
+
),
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _test_scoped_fast_save_specifications() -> "list[_FastSaveValidatorSpecification]":
|
|
229
|
+
"""Return the save-path checks scoped to a ``test_*.py``-shaped file."""
|
|
230
|
+
return [
|
|
231
|
+
_FastSaveValidatorSpecification(
|
|
232
|
+
"Test Safety",
|
|
233
|
+
"11,21",
|
|
234
|
+
_is_test_python_file,
|
|
235
|
+
_collect_test_safety_violation_lines,
|
|
236
|
+
NO_TEST_FILES_MESSAGE,
|
|
237
|
+
),
|
|
238
|
+
_FastSaveValidatorSpecification(
|
|
239
|
+
"Useless Tests",
|
|
240
|
+
"12",
|
|
241
|
+
_is_test_python_file,
|
|
242
|
+
_violation_lines_from(validate_useless_test_file),
|
|
243
|
+
NO_TEST_FILES_MESSAGE,
|
|
244
|
+
),
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _cross_language_fast_save_specifications() -> (
|
|
249
|
+
"list[_FastSaveValidatorSpecification]"
|
|
250
|
+
):
|
|
251
|
+
"""Return the save-path checks that read a non-Python or multi-language file."""
|
|
252
|
+
return [
|
|
253
|
+
_FastSaveValidatorSpecification(
|
|
254
|
+
"React",
|
|
255
|
+
"17",
|
|
256
|
+
_is_react_file,
|
|
257
|
+
_collect_react_violation_lines,
|
|
258
|
+
NO_REACT_FILES_MESSAGE,
|
|
259
|
+
),
|
|
260
|
+
_FastSaveValidatorSpecification(
|
|
261
|
+
"PR References",
|
|
262
|
+
"6",
|
|
263
|
+
_is_code_reference_file,
|
|
264
|
+
_violation_lines_from(validate_pr_reference_file),
|
|
265
|
+
NO_CODE_REFERENCE_FILES_MESSAGE,
|
|
266
|
+
),
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _all_fast_save_validator_specifications() -> (
|
|
271
|
+
"list[_FastSaveValidatorSpecification]"
|
|
272
|
+
):
|
|
273
|
+
"""Return the save-path roster: every non-Mypy, non-Ruff file-scoped check."""
|
|
274
|
+
return [
|
|
275
|
+
*_python_naming_and_syntax_specifications(),
|
|
276
|
+
*_python_quality_and_tracking_specifications(),
|
|
277
|
+
*_python_safety_and_typing_specifications(),
|
|
278
|
+
*_test_scoped_fast_save_specifications(),
|
|
279
|
+
*_cross_language_fast_save_specifications(),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _fast_save_outcome(
|
|
284
|
+
specification: _FastSaveValidatorSpecification,
|
|
285
|
+
is_clean: bool,
|
|
286
|
+
violation_report: str,
|
|
287
|
+
) -> FastSaveCheckOutcome:
|
|
288
|
+
"""Build one outcome from a specification's display name and check numbers."""
|
|
289
|
+
return FastSaveCheckOutcome(
|
|
290
|
+
display_name=specification.display_name,
|
|
291
|
+
checks=specification.checks,
|
|
292
|
+
is_clean=is_clean,
|
|
293
|
+
violation_report=violation_report,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _run_specification(
|
|
298
|
+
specification: _FastSaveValidatorSpecification, files: "list[Path]"
|
|
299
|
+
) -> FastSaveCheckOutcome:
|
|
300
|
+
"""Run one validator's violation collector in-process over its matching files."""
|
|
301
|
+
matching_files = [
|
|
302
|
+
each_file for each_file in files if specification.applies_to_file(each_file)
|
|
303
|
+
]
|
|
304
|
+
if not matching_files:
|
|
305
|
+
return _fast_save_outcome(
|
|
306
|
+
specification, True, specification.no_matching_files_message
|
|
307
|
+
)
|
|
308
|
+
all_violation_lines = [
|
|
309
|
+
each_line
|
|
310
|
+
for each_file in matching_files
|
|
311
|
+
for each_line in specification.collect_violation_lines(each_file)
|
|
312
|
+
]
|
|
313
|
+
if not all_violation_lines:
|
|
314
|
+
return _fast_save_outcome(specification, True, NO_VIOLATIONS_FOUND_MESSAGE)
|
|
315
|
+
violation_report = VIOLATION_REPORT_LINE_SEPARATOR.join(all_violation_lines)
|
|
316
|
+
return _fast_save_outcome(specification, False, violation_report)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def run_fast_save_validators(files: "list[Path]") -> "list[FastSaveCheckOutcome]":
|
|
320
|
+
"""Run every save-path validator in-process, skipping Mypy and Ruff.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
files: The files under validation -- one reconstructed file in gate mode.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
One outcome per in-process validator, in roster order. The caller adds
|
|
327
|
+
Ruff and never adds Mypy.
|
|
328
|
+
"""
|
|
329
|
+
return [
|
|
330
|
+
_run_specification(each_specification, files)
|
|
331
|
+
for each_specification in _all_fast_save_validator_specifications()
|
|
332
|
+
]
|
|
@@ -13,10 +13,14 @@ count, and reported as p50 and p95 milliseconds.
|
|
|
13
13
|
post_tool_use_dispatcher p50=80.1ms p95=1200.5ms
|
|
14
14
|
session_file_edit_tracker p50=31.0ms p95=34.2ms
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
This harness refuses a target under the OS temp root, and runs the whole
|
|
17
|
+
command line ``hooks.json`` registers.
|
|
18
|
+
|
|
19
|
+
The payload names the Write tool and carries the target's own current content.
|
|
20
|
+
``code_rules_enforcer`` takes its early return for a write over an existing
|
|
21
|
+
file, so the ``pre_tool_use_dispatcher`` figure covers interpreter start and
|
|
22
|
+
hook dispatch. Time an Edit payload against a real file to measure the
|
|
23
|
+
CODE_RULES checks themselves.
|
|
20
24
|
|
|
21
25
|
The timed payload carries no ``session_id``, so ``session_file_edit_tracker``
|
|
22
26
|
writes its edit record to the default-session file rather than to a
|
|
@@ -14,42 +14,27 @@ from dataclasses import dataclass
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
16
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
real_newline_lines,
|
|
39
|
-
top_level_functions,
|
|
40
|
-
)
|
|
41
|
-
except ModuleNotFoundError:
|
|
42
|
-
if _hooks_directory not in sys.path:
|
|
43
|
-
sys.path.insert(0, _hooks_directory)
|
|
44
|
-
from validators.python_style_helpers import (
|
|
45
|
-
FunctionNode,
|
|
46
|
-
blank_line_for_source,
|
|
47
|
-
function_start_line,
|
|
48
|
-
gap_is_blank_only,
|
|
49
|
-
iter_function_definitions,
|
|
50
|
-
real_newline_lines,
|
|
51
|
-
top_level_functions,
|
|
52
|
-
)
|
|
17
|
+
if _hooks_directory not in sys.path:
|
|
18
|
+
sys.path.insert(0, _hooks_directory)
|
|
19
|
+
|
|
20
|
+
from hooks_constants.python_style_checks_constants import ( # noqa: E402
|
|
21
|
+
EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS,
|
|
22
|
+
MINIMUM_ARGUMENT_COUNT,
|
|
23
|
+
)
|
|
24
|
+
from validators.python_style_helpers import ( # noqa: E402
|
|
25
|
+
FunctionNode,
|
|
26
|
+
blank_line_for_source,
|
|
27
|
+
function_start_line,
|
|
28
|
+
gap_is_blank_only,
|
|
29
|
+
is_docstring_statement,
|
|
30
|
+
is_import_statement,
|
|
31
|
+
iter_function_definitions,
|
|
32
|
+
real_newline_lines,
|
|
33
|
+
top_level_functions,
|
|
34
|
+
)
|
|
35
|
+
from validators.python_style_import_bootstrap import ( # noqa: E402
|
|
36
|
+
is_sys_path_bootstrap_prelude,
|
|
37
|
+
)
|
|
53
38
|
|
|
54
39
|
logger = logging.getLogger(__name__)
|
|
55
40
|
|
|
@@ -83,37 +68,26 @@ def check_imports_at_top(tree: ast.AST, filename: str) -> list[Violation]:
|
|
|
83
68
|
return violations
|
|
84
69
|
|
|
85
70
|
|
|
86
|
-
def _is_import_statement(statement: ast.stmt) -> bool:
|
|
87
|
-
"""Return True when the statement is an import or from-import."""
|
|
88
|
-
return isinstance(statement, (ast.Import, ast.ImportFrom))
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _is_docstring_statement(statement: ast.stmt) -> bool:
|
|
92
|
-
"""Return True when the statement is a string-literal docstring."""
|
|
93
|
-
if not isinstance(statement, ast.Expr):
|
|
94
|
-
return False
|
|
95
|
-
literal = statement.value
|
|
96
|
-
return isinstance(literal, ast.Constant) and isinstance(literal.value, str)
|
|
97
|
-
|
|
98
|
-
|
|
99
71
|
def _check_module_level_import_order(tree: ast.AST, filename: str) -> list[Violation]:
|
|
100
|
-
"""Flag module-level imports
|
|
72
|
+
"""Flag module-level imports after other statements, exempting a sys.path bootstrap run."""
|
|
101
73
|
if not isinstance(tree, ast.Module):
|
|
102
74
|
return []
|
|
103
75
|
violations: list[Violation] = []
|
|
104
76
|
has_seen_non_import = False
|
|
77
|
+
pending_prelude_statements: list[ast.stmt] = []
|
|
105
78
|
for each_statement in tree.body:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
)
|
|
115
|
-
if not is_import and not _is_docstring_statement(each_statement):
|
|
79
|
+
if not is_import_statement(each_statement):
|
|
80
|
+
if not is_docstring_statement(each_statement):
|
|
81
|
+
pending_prelude_statements.append(each_statement)
|
|
82
|
+
continue
|
|
83
|
+
if pending_prelude_statements and not is_sys_path_bootstrap_prelude(
|
|
84
|
+
pending_prelude_statements
|
|
85
|
+
):
|
|
116
86
|
has_seen_non_import = True
|
|
87
|
+
pending_prelude_statements = []
|
|
88
|
+
if has_seen_non_import:
|
|
89
|
+
message = "Import statement must be at top of file"
|
|
90
|
+
violations.append(Violation(filename, each_statement.lineno, message))
|
|
117
91
|
return violations
|
|
118
92
|
|
|
119
93
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Shared source-line and function-discovery helpers for the style checks.
|
|
2
2
|
|
|
3
3
|
These pure helpers underlie the style checks and the blank-line fixers:
|
|
4
|
-
splitting source into ast-aligned lines, locating function definitions,
|
|
5
|
-
matching the source newline
|
|
4
|
+
splitting source into ast-aligned lines, locating function definitions,
|
|
5
|
+
classifying a module-level statement, and matching the source newline
|
|
6
|
+
convention.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import ast
|
|
@@ -93,3 +94,16 @@ def real_newline_lines(source: str) -> list[str]:
|
|
|
93
94
|
if line_start < total_length:
|
|
94
95
|
lines.append(source[line_start:])
|
|
95
96
|
return lines
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_import_statement(statement: ast.stmt) -> bool:
|
|
100
|
+
"""Return True when the statement is an import or from-import."""
|
|
101
|
+
return isinstance(statement, (ast.Import, ast.ImportFrom))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def is_docstring_statement(statement: ast.stmt) -> bool:
|
|
105
|
+
"""Return True when the statement is a string-literal docstring."""
|
|
106
|
+
if not isinstance(statement, ast.Expr):
|
|
107
|
+
return False
|
|
108
|
+
literal = statement.value
|
|
109
|
+
return isinstance(literal, ast.Constant) and isinstance(literal.value, str)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Recognizer for the repo's sys.path bootstrap-guard idiom.
|
|
2
|
+
|
|
3
|
+
Split out of python_style_checks.py to keep that file under its line cap.
|
|
4
|
+
Tells the import-order check whether a run of module-level statements is that
|
|
5
|
+
idiom rather than ordinary code: `check_sys_path_insert_deduplication_guard`
|
|
6
|
+
(code_rules_paths_syspath.py) requires every `sys.path.insert` call to sit
|
|
7
|
+
behind `if <path> not in sys.path:`, and the import-order check must not
|
|
8
|
+
penalize a file for following that guard.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_sys_path_insert_expression(expression: ast.expr) -> bool:
|
|
15
|
+
"""Return True for a `sys.path.insert(...)` call expression."""
|
|
16
|
+
if not isinstance(expression, ast.Call):
|
|
17
|
+
return False
|
|
18
|
+
function_reference = expression.func
|
|
19
|
+
if not isinstance(function_reference, ast.Attribute) or function_reference.attr != "insert":
|
|
20
|
+
return False
|
|
21
|
+
receiver = function_reference.value
|
|
22
|
+
if not isinstance(receiver, ast.Attribute) or receiver.attr != "path":
|
|
23
|
+
return False
|
|
24
|
+
receiver_value = receiver.value
|
|
25
|
+
return isinstance(receiver_value, ast.Name) and receiver_value.id == "sys"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_sys_path_bootstrap_guard(statement: ast.stmt) -> bool:
|
|
29
|
+
"""Return True for `if <expr> not in sys.path: sys.path.insert(...)`.
|
|
30
|
+
|
|
31
|
+
Only the then-branch is inspected, matching the scope
|
|
32
|
+
`check_sys_path_insert_deduplication_guard` itself checks.
|
|
33
|
+
"""
|
|
34
|
+
if not isinstance(statement, ast.If):
|
|
35
|
+
return False
|
|
36
|
+
membership_test = statement.test
|
|
37
|
+
if not isinstance(membership_test, ast.Compare) or len(membership_test.ops) != 1:
|
|
38
|
+
return False
|
|
39
|
+
if not isinstance(membership_test.ops[0], ast.NotIn):
|
|
40
|
+
return False
|
|
41
|
+
membership_target = membership_test.comparators[0]
|
|
42
|
+
if not isinstance(membership_target, ast.Attribute) or membership_target.attr != "path":
|
|
43
|
+
return False
|
|
44
|
+
if not isinstance(membership_target.value, ast.Name) or membership_target.value.id != "sys":
|
|
45
|
+
return False
|
|
46
|
+
return any(
|
|
47
|
+
isinstance(each_body_statement, ast.Expr)
|
|
48
|
+
and _is_sys_path_insert_expression(each_body_statement.value)
|
|
49
|
+
for each_body_statement in statement.body
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_plain_assignment_statement(statement: ast.stmt) -> bool:
|
|
54
|
+
"""Return True for a single-target module-level `name = value` assignment."""
|
|
55
|
+
return (
|
|
56
|
+
isinstance(statement, ast.Assign)
|
|
57
|
+
and len(statement.targets) == 1
|
|
58
|
+
and isinstance(statement.targets[0], ast.Name)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_sys_path_bootstrap_prelude(all_pending_statements: list[ast.stmt]) -> bool:
|
|
63
|
+
"""Return True when *all_pending_statements* form the repo's sys.path bootstrap idiom.
|
|
64
|
+
|
|
65
|
+
Writing the mandated dedup guard takes a preceding `<path> = ...`
|
|
66
|
+
assignment and the guarded `if` itself — both ordinary, non-import
|
|
67
|
+
statements that would otherwise read as ordinary code to an import-order
|
|
68
|
+
check and flag every import that follows.
|
|
69
|
+
|
|
70
|
+
A run of statements between two import blocks is accepted as this
|
|
71
|
+
bootstrap prelude, rather than as ordinary code, when it contains at
|
|
72
|
+
least one real guard and every other statement in the same run is a
|
|
73
|
+
plain assignment — the shape a `path = ...` line or an unrelated
|
|
74
|
+
same-block constant both take. A bare `sys.path.insert` with no guard,
|
|
75
|
+
or any def/class/call statement, is not part of this shape and still
|
|
76
|
+
counts as ordinary code.
|
|
77
|
+
"""
|
|
78
|
+
if not any(_is_sys_path_bootstrap_guard(each) for each in all_pending_statements):
|
|
79
|
+
return False
|
|
80
|
+
return all(
|
|
81
|
+
_is_sys_path_bootstrap_guard(each) or _is_plain_assignment_statement(each)
|
|
82
|
+
for each in all_pending_statements
|
|
83
|
+
)
|