claude-dev-env 1.82.0 → 1.83.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/CLAUDE.md +16 -13
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_constants_config.py +159 -1
- package/hooks/blocking/code_rules_docstrings.py +312 -9
- package/hooks/blocking/code_rules_enforcer.py +29 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_naming_collection.py +141 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/blocking_check_limits.py +43 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +4 -1
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +5 -0
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +1 -1
- package/skills/autoconverge/SKILL.md +10 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +69 -0
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +54 -18
- package/skills/autoconverge/workflow/converge.mjs +97 -33
- package/skills/everything-search/SKILL.md +5 -0
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: blocks Write/Edit containing historical/comparative language in comments and .md files.
|
|
2
|
+
"""PreToolUse hook: blocks Write/Edit containing historical/comparative language in comments, Python docstrings, and .md files.
|
|
3
3
|
|
|
4
4
|
Enforces the "describe current state only" rule — no "instead of", "previously",
|
|
5
|
-
"now uses", or similar transitional framing. Comments and
|
|
6
|
-
describe what IS, not what WAS or what CHANGED.
|
|
5
|
+
"now uses", or similar transitional framing. Comments, docstrings, and
|
|
6
|
+
documentation should describe what IS, not what WAS or what CHANGED. Inside a
|
|
7
|
+
docstring, a phrase wrapped in double quotes or backticks is a mention rather
|
|
8
|
+
than a use, so quoted spans are stripped before the scan.
|
|
7
9
|
"""
|
|
8
10
|
|
|
11
|
+
import ast
|
|
9
12
|
import json
|
|
10
13
|
import os
|
|
11
14
|
import sys
|
|
@@ -23,11 +26,15 @@ from hooks_constants.state_description_blocker_constants import ( # noqa: E402
|
|
|
23
26
|
ALL_BLOCK_COMMENT_ONLY_EXTENSIONS,
|
|
24
27
|
ALL_COMMENT_BEARING_EXTENSIONS,
|
|
25
28
|
ALL_COMMENT_TRANSITION_PATTERNS,
|
|
29
|
+
ALL_DEFINITION_HEADER_PREFIXES,
|
|
26
30
|
ALL_HASH_AND_SLASH_EXTENSIONS,
|
|
27
31
|
ALL_HASH_ONLY_EXTENSIONS,
|
|
28
32
|
ALL_MARKDOWN_EXTENSIONS,
|
|
29
33
|
CODE_FENCE_PATTERN,
|
|
34
|
+
DOUBLE_QUOTED_SPAN_PATTERN,
|
|
30
35
|
INLINE_CODE_PATTERN,
|
|
36
|
+
PYTHON_EXTENSION,
|
|
37
|
+
TRIPLE_QUOTED_BLOCK_PATTERN,
|
|
31
38
|
)
|
|
32
39
|
|
|
33
40
|
|
|
@@ -131,17 +138,101 @@ def _find_inline_comment_start(stripped: str, all_markers: tuple[str, ...]) -> i
|
|
|
131
138
|
return best_position
|
|
132
139
|
|
|
133
140
|
|
|
141
|
+
def _extract_parsed_docstrings(tree: ast.Module) -> list[str]:
|
|
142
|
+
"""Collect the module, class, and function docstrings from a parsed tree.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
tree: The parsed module tree to walk.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Every docstring found on the module and its class/function definitions.
|
|
149
|
+
"""
|
|
150
|
+
all_found_docstrings: list[str] = []
|
|
151
|
+
for each_node in ast.walk(tree):
|
|
152
|
+
if not isinstance(
|
|
153
|
+
each_node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
|
|
154
|
+
):
|
|
155
|
+
continue
|
|
156
|
+
maybe_docstring = ast.get_docstring(each_node, clean=False)
|
|
157
|
+
if maybe_docstring:
|
|
158
|
+
all_found_docstrings.append(maybe_docstring)
|
|
159
|
+
return all_found_docstrings
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _extract_fragment_docstrings(text: str) -> list[str]:
|
|
163
|
+
"""Collect docstring-positioned triple-quoted blocks from an unparseable fragment.
|
|
164
|
+
|
|
165
|
+
A triple-quoted block counts as a docstring when it opens the fragment
|
|
166
|
+
(only whitespace before it) or when the nearest preceding non-blank line is
|
|
167
|
+
a def/class header ending in a colon. A block assigned to a name or sitting
|
|
168
|
+
elsewhere in the fragment is data and is skipped.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
text: The Python source fragment that failed to parse.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
The body text of each docstring-positioned triple-quoted block.
|
|
175
|
+
"""
|
|
176
|
+
all_found_docstrings: list[str] = []
|
|
177
|
+
for each_match in TRIPLE_QUOTED_BLOCK_PATTERN.finditer(text):
|
|
178
|
+
body = each_match.group(1) if each_match.group(1) is not None else each_match.group(2)
|
|
179
|
+
leading_text = text[: each_match.start()]
|
|
180
|
+
if not leading_text.strip():
|
|
181
|
+
all_found_docstrings.append(body)
|
|
182
|
+
continue
|
|
183
|
+
if leading_text.rpartition("\n")[2].strip():
|
|
184
|
+
continue
|
|
185
|
+
all_preceding_lines = [
|
|
186
|
+
each_line for each_line in leading_text.splitlines() if each_line.strip()
|
|
187
|
+
]
|
|
188
|
+
last_preceding_line = all_preceding_lines[-1].strip()
|
|
189
|
+
if last_preceding_line.startswith(
|
|
190
|
+
ALL_DEFINITION_HEADER_PREFIXES
|
|
191
|
+
) and last_preceding_line.endswith(":"):
|
|
192
|
+
all_found_docstrings.append(body)
|
|
193
|
+
return all_found_docstrings
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _extract_python_docstring_text(text: str) -> str:
|
|
197
|
+
"""Return the scannable docstring prose from Python source.
|
|
198
|
+
|
|
199
|
+
Parses the source when valid, or falls back to the docstring-positioned
|
|
200
|
+
triple-quoted blocks of a mid-edit fragment. Double-quoted and backticked
|
|
201
|
+
spans inside each docstring are mentions rather than uses, so both are
|
|
202
|
+
stripped from the returned prose.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
text: The Python source or fragment under scan.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The docstring prose joined into one scannable string.
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
all_found_docstrings = _extract_parsed_docstrings(ast.parse(text))
|
|
212
|
+
except (SyntaxError, ValueError):
|
|
213
|
+
all_found_docstrings = _extract_fragment_docstrings(text)
|
|
214
|
+
all_docstring_prose = [
|
|
215
|
+
DOUBLE_QUOTED_SPAN_PATTERN.sub("", INLINE_CODE_PATTERN.sub("", each_docstring))
|
|
216
|
+
for each_docstring in all_found_docstrings
|
|
217
|
+
]
|
|
218
|
+
return "\n".join(all_docstring_prose)
|
|
219
|
+
|
|
220
|
+
|
|
134
221
|
def find_violations(text: str, file_path: str) -> list[str]:
|
|
135
222
|
"""Return all violated patterns found in text for the given file.
|
|
136
223
|
|
|
137
|
-
For .md files, scans the entire text. For code files, scans
|
|
224
|
+
For .md files, scans the entire text. For code files, scans comment lines,
|
|
225
|
+
and for Python files also scans module/class/function docstrings.
|
|
138
226
|
Returns a list of matched pattern source strings.
|
|
139
227
|
"""
|
|
228
|
+
extension = _get_file_extension(file_path)
|
|
140
229
|
if is_markdown_file(file_path):
|
|
141
230
|
scan_text = text
|
|
142
231
|
elif is_comment_bearing_file(file_path):
|
|
143
|
-
all_comment_lines = _extract_comment_lines(text,
|
|
232
|
+
all_comment_lines = _extract_comment_lines(text, extension)
|
|
144
233
|
scan_text = "\n".join(all_comment_lines)
|
|
234
|
+
if extension == PYTHON_EXTENSION:
|
|
235
|
+
scan_text = "\n".join([scan_text, _extract_python_docstring_text(text)])
|
|
145
236
|
else:
|
|
146
237
|
return []
|
|
147
238
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Tests for check_config_duplicate_path_anchor.
|
|
2
|
+
|
|
3
|
+
Directories come from ``tmp_path_factory.mktemp`` with a neutral name because
|
|
4
|
+
the per-test ``tmp_path`` embeds the test function name, and that name would
|
|
5
|
+
make the test-path exemption inside the validator swallow every case.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
ENFORCER_FILENAME = "code_rules_enforcer.py"
|
|
17
|
+
ENFORCER_MODULE_NAME = "code_rules_enforcer_duplicate_anchor_tests"
|
|
18
|
+
SIBLING_MODULE_SOURCE = """\
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
NEON_SUBMISSION_LOG_DIRECTORY = Path(__file__).resolve().parents[2] / "logs" / "theme_submissions"
|
|
22
|
+
"""
|
|
23
|
+
WRITTEN_MODULE_SOURCE = """\
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
LOG_BASE_DIR = Path(__file__).resolve().parents[2] / "logs"
|
|
27
|
+
THEME_SUBMISSIONS_LOG_FOLDER = "theme_submissions"
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_enforcer_module() -> ModuleType:
|
|
32
|
+
loader_path = Path(__file__).parent / ENFORCER_FILENAME
|
|
33
|
+
module_spec = importlib.util.spec_from_file_location(ENFORCER_MODULE_NAME, loader_path)
|
|
34
|
+
assert module_spec is not None
|
|
35
|
+
assert module_spec.loader is not None
|
|
36
|
+
loaded_module = importlib.util.module_from_spec(module_spec)
|
|
37
|
+
module_spec.loader.exec_module(loaded_module)
|
|
38
|
+
return loaded_module
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
enforcer = load_enforcer_module()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _config_directory_with_sibling(
|
|
45
|
+
tmp_path_factory: pytest.TempPathFactory, sibling_source: str
|
|
46
|
+
) -> Path:
|
|
47
|
+
config_directory = tmp_path_factory.mktemp("workspace_home") / "config"
|
|
48
|
+
config_directory.mkdir()
|
|
49
|
+
(config_directory / "neon_submission.py").write_text(sibling_source, encoding="utf-8")
|
|
50
|
+
return config_directory
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_flag_reanchored_base_already_built_by_sibling(
|
|
54
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
55
|
+
) -> None:
|
|
56
|
+
config_directory = _config_directory_with_sibling(tmp_path_factory, SIBLING_MODULE_SOURCE)
|
|
57
|
+
written_path = str(config_directory / "derived_paths.py")
|
|
58
|
+
issues = enforcer.check_config_duplicate_path_anchor(WRITTEN_MODULE_SOURCE, written_path)
|
|
59
|
+
assert len(issues) == 1
|
|
60
|
+
assert "neon_submission.py" in issues[0]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_flag_parent_chain_anchor_matching_sibling_parents_index(
|
|
64
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
65
|
+
) -> None:
|
|
66
|
+
config_directory = _config_directory_with_sibling(tmp_path_factory, SIBLING_MODULE_SOURCE)
|
|
67
|
+
written_source = """\
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
|
|
70
|
+
LOG_BASE_DIR = Path(__file__).resolve().parent.parent.parent / "logs"
|
|
71
|
+
"""
|
|
72
|
+
written_path = str(config_directory / "derived_paths.py")
|
|
73
|
+
issues = enforcer.check_config_duplicate_path_anchor(written_source, written_path)
|
|
74
|
+
assert len(issues) == 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_should_allow_anchor_with_different_first_segment(
|
|
78
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
79
|
+
) -> None:
|
|
80
|
+
config_directory = _config_directory_with_sibling(tmp_path_factory, SIBLING_MODULE_SOURCE)
|
|
81
|
+
written_source = """\
|
|
82
|
+
from pathlib import Path
|
|
83
|
+
|
|
84
|
+
SNAPSHOT_BASE_DIR = Path(__file__).resolve().parents[2] / "snapshots"
|
|
85
|
+
"""
|
|
86
|
+
written_path = str(config_directory / "derived_paths.py")
|
|
87
|
+
issues = enforcer.check_config_duplicate_path_anchor(written_source, written_path)
|
|
88
|
+
assert issues == []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_should_allow_anchor_with_different_depth(
|
|
92
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
93
|
+
) -> None:
|
|
94
|
+
config_directory = _config_directory_with_sibling(tmp_path_factory, SIBLING_MODULE_SOURCE)
|
|
95
|
+
written_source = """\
|
|
96
|
+
from pathlib import Path
|
|
97
|
+
|
|
98
|
+
LOG_BASE_DIR = Path(__file__).resolve().parents[3] / "logs"
|
|
99
|
+
"""
|
|
100
|
+
written_path = str(config_directory / "derived_paths.py")
|
|
101
|
+
issues = enforcer.check_config_duplicate_path_anchor(written_source, written_path)
|
|
102
|
+
assert issues == []
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_should_allow_module_with_no_sibling_config_files(
|
|
106
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
107
|
+
) -> None:
|
|
108
|
+
config_directory = tmp_path_factory.mktemp("workspace_home") / "config"
|
|
109
|
+
config_directory.mkdir()
|
|
110
|
+
written_path = str(config_directory / "derived_paths.py")
|
|
111
|
+
issues = enforcer.check_config_duplicate_path_anchor(WRITTEN_MODULE_SOURCE, written_path)
|
|
112
|
+
assert issues == []
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_should_skip_modules_outside_config_directories(
|
|
116
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
117
|
+
) -> None:
|
|
118
|
+
plain_directory = tmp_path_factory.mktemp("workspace_home") / "services"
|
|
119
|
+
plain_directory.mkdir()
|
|
120
|
+
(plain_directory / "neon_submission.py").write_text(SIBLING_MODULE_SOURCE, encoding="utf-8")
|
|
121
|
+
written_path = str(plain_directory / "derived_paths.py")
|
|
122
|
+
issues = enforcer.check_config_duplicate_path_anchor(WRITTEN_MODULE_SOURCE, written_path)
|
|
123
|
+
assert issues == []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_should_skip_unparseable_sibling_modules(
|
|
127
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
128
|
+
) -> None:
|
|
129
|
+
config_directory = _config_directory_with_sibling(tmp_path_factory, "def broken(:\n")
|
|
130
|
+
written_path = str(config_directory / "derived_paths.py")
|
|
131
|
+
issues = enforcer.check_config_duplicate_path_anchor(WRITTEN_MODULE_SOURCE, written_path)
|
|
132
|
+
assert issues == []
|
|
@@ -71,10 +71,12 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
|
|
|
71
71
|
"check_file_global_constants_use_count",
|
|
72
72
|
"check_imports_at_top",
|
|
73
73
|
"check_inline_literal_collections",
|
|
74
|
+
"check_join_separator_string_magic",
|
|
74
75
|
"check_known_pytest_fixture_annotations",
|
|
75
76
|
"check_library_print",
|
|
76
77
|
"check_loop_variable_naming",
|
|
77
78
|
"check_parameter_annotations",
|
|
79
|
+
"check_referenced_underscore_loop_variable",
|
|
78
80
|
"check_return_annotations",
|
|
79
81
|
"check_skip_decorators_in_tests",
|
|
80
82
|
"check_string_literal_magic",
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Tests for check_docstring_delegation_summary_enumeration_drift (O6).
|
|
2
|
+
|
|
3
|
+
A thin wrapper docstring that enumerates its actions and points at the home
|
|
4
|
+
of the real body drifts when the same-named function in that named neighbor
|
|
5
|
+
carries a summary enumeration omitting one of those actions. The gate fires
|
|
6
|
+
from both sides: saving the wrapper compares it against the named neighbor on
|
|
7
|
+
disk, and saving the delegated body compares it against every neighboring
|
|
8
|
+
wrapper docstring pointing at it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_enforcer_module() -> ModuleType:
|
|
20
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
21
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
22
|
+
assert spec is not None
|
|
23
|
+
assert spec.loader is not None
|
|
24
|
+
module = importlib.util.module_from_spec(spec)
|
|
25
|
+
spec.loader.exec_module(module)
|
|
26
|
+
return module
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_docstring_delegation_summary_enumeration_drift(
|
|
33
|
+
content: str, file_path: str
|
|
34
|
+
) -> list[str]:
|
|
35
|
+
return code_rules_enforcer.check_docstring_delegation_summary_enumeration_drift(
|
|
36
|
+
content, file_path
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
41
|
+
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
45
|
+
TARGET_FLOW_STEM = "listing_edit_flow"
|
|
46
|
+
PROCESSOR_FILE_NAME = "portal_processor.py"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _drifted_processor_source() -> str:
|
|
50
|
+
return (
|
|
51
|
+
"class PortalProcessor:\n"
|
|
52
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
53
|
+
' """Apply App Info, Russia, review note, publication edits;'
|
|
54
|
+
" full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
55
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _matching_processor_source() -> str:
|
|
60
|
+
return (
|
|
61
|
+
"class PortalProcessor:\n"
|
|
62
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
63
|
+
' """Apply Russia, review note, publication edits;'
|
|
64
|
+
" full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
65
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _target_flow_source() -> str:
|
|
70
|
+
return (
|
|
71
|
+
"async def _refresh_store_sections(processor: object, page: object) -> bool:\n"
|
|
72
|
+
' """Apply Russia uncheck, review note, and Publication edits.\n'
|
|
73
|
+
"\n"
|
|
74
|
+
" The App Info edit runs earlier in the binary phase and never\n"
|
|
75
|
+
" repeats here.\n"
|
|
76
|
+
' """\n'
|
|
77
|
+
" return True\n"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _target_flow_plain_summary() -> str:
|
|
82
|
+
return (
|
|
83
|
+
"async def _refresh_store_sections(processor: object, page: object) -> bool:\n"
|
|
84
|
+
' """Drive one theme end to end."""\n'
|
|
85
|
+
" return True\n"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _create_target_flow(directory: Path, source: str) -> Path:
|
|
90
|
+
created_path = directory / (TARGET_FLOW_STEM + ".py")
|
|
91
|
+
created_path.write_text(source, encoding="utf-8")
|
|
92
|
+
return created_path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_should_flag_listed_action_absent_from_the_named_neighbor(tmp_path: Path) -> None:
|
|
96
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
97
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
98
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
99
|
+
_drifted_processor_source(), checked_path
|
|
100
|
+
)
|
|
101
|
+
assert any("App Info" in each for each in issues), (
|
|
102
|
+
f"Expected 'App Info' to be flagged, got: {issues!r}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_should_pass_matching_summaries(tmp_path: Path) -> None:
|
|
107
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
108
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
109
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
110
|
+
_matching_processor_source(), checked_path
|
|
111
|
+
)
|
|
112
|
+
assert issues == [], f"Aligned summaries pass, got: {issues!r}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_should_flag_saved_body_that_strands_a_pointing_neighbor(tmp_path: Path) -> None:
|
|
116
|
+
checked_path = tmp_path / PROCESSOR_FILE_NAME
|
|
117
|
+
checked_path.write_text(_drifted_processor_source(), encoding="utf-8")
|
|
118
|
+
created_path = str(tmp_path / (TARGET_FLOW_STEM + ".py"))
|
|
119
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
120
|
+
_target_flow_source(), created_path
|
|
121
|
+
)
|
|
122
|
+
assert any(
|
|
123
|
+
"App Info" in each and PROCESSOR_FILE_NAME in each for each in issues
|
|
124
|
+
), f"Expected the stranded caller to be flagged, got: {issues!r}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_should_pass_plain_one_purpose_summary(tmp_path: Path) -> None:
|
|
128
|
+
_create_target_flow(tmp_path, _target_flow_plain_summary())
|
|
129
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
130
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
131
|
+
_drifted_processor_source(), checked_path
|
|
132
|
+
)
|
|
133
|
+
assert issues == [], (
|
|
134
|
+
f"A comma-free summary compares against nothing, got: {issues!r}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_should_pass_when_the_named_neighbor_does_not_exist(tmp_path: Path) -> None:
|
|
139
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
140
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
141
|
+
_drifted_processor_source(), checked_path
|
|
142
|
+
)
|
|
143
|
+
assert issues == [], f"Nothing on disk to compare against, got: {issues!r}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_should_pass_single_listed_action(tmp_path: Path) -> None:
|
|
147
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
148
|
+
solo_source = (
|
|
149
|
+
"class PortalProcessor:\n"
|
|
150
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
151
|
+
' """Apply the App Info edit;'
|
|
152
|
+
" full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
153
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
154
|
+
)
|
|
155
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
156
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
157
|
+
solo_source, checked_path
|
|
158
|
+
)
|
|
159
|
+
assert issues == [], f"A lone named action never fires, got: {issues!r}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_should_skip_strict_test_paths(tmp_path: Path) -> None:
|
|
163
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
164
|
+
skipped_path = str(tmp_path / "test_portal_processor.py")
|
|
165
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
166
|
+
_drifted_processor_source(), skipped_path
|
|
167
|
+
)
|
|
168
|
+
assert issues == [], f"Exempt, got: {issues!r}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
172
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
173
|
+
_drifted_processor_source(), HOOK_INFRASTRUCTURE_PATH
|
|
174
|
+
)
|
|
175
|
+
assert issues == [], f"Exempt, got: {issues!r}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_should_handle_syntax_error_gracefully(tmp_path: Path) -> None:
|
|
179
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
180
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
181
|
+
"def fetch(\n", checked_path
|
|
182
|
+
)
|
|
183
|
+
assert issues == [], f"A syntax error yields no issues, got: {issues!r}"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_should_handle_unparseable_neighbor(tmp_path: Path) -> None:
|
|
187
|
+
_create_target_flow(tmp_path, "def broken(\n")
|
|
188
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
189
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
190
|
+
_drifted_processor_source(), checked_path
|
|
191
|
+
)
|
|
192
|
+
assert issues == [], f"Unparseable neighbors yield no issues, got: {issues!r}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_validate_content_surfaces_the_drift(tmp_path: Path) -> None:
|
|
196
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
197
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
198
|
+
issues = validate_content(_drifted_processor_source(), checked_path, old_content="")
|
|
199
|
+
matching_issues = [each for each in issues if "App Info" in each and "O6" in each]
|
|
200
|
+
assert matching_issues, (
|
|
201
|
+
f"Expected validate_content to surface the O6 finding, got: {issues!r}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _drifted_processor_source_with_wrapped_summary() -> str:
|
|
206
|
+
return (
|
|
207
|
+
"class PortalProcessor:\n"
|
|
208
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
209
|
+
' """Apply App Info, Russia, review note, publication\n'
|
|
210
|
+
" edits; full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
211
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_should_flag_listed_action_when_the_summary_wraps_a_physical_line(
|
|
216
|
+
tmp_path: Path,
|
|
217
|
+
) -> None:
|
|
218
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
219
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
220
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
221
|
+
_drifted_processor_source_with_wrapped_summary(), checked_path
|
|
222
|
+
)
|
|
223
|
+
assert any("App Info" in each for each in issues), (
|
|
224
|
+
f"A summary wrapped onto a second physical line still names 'App Info', "
|
|
225
|
+
f"got: {issues!r}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _drifted_processor_source_with_capitalized_and_uneven_whitespace() -> str:
|
|
230
|
+
return (
|
|
231
|
+
"class PortalProcessor:\n"
|
|
232
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
233
|
+
' """Apply App Info, Russia, review note, And publication'
|
|
234
|
+
" edits; full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
235
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_should_flag_listed_action_when_the_conjunction_is_capitalized_with_uneven_whitespace(
|
|
240
|
+
tmp_path: Path,
|
|
241
|
+
) -> None:
|
|
242
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
243
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
244
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
245
|
+
_drifted_processor_source_with_capitalized_and_uneven_whitespace(), checked_path
|
|
246
|
+
)
|
|
247
|
+
assert any("App Info" in each for each in issues), (
|
|
248
|
+
f"A capitalized 'And' with uneven whitespace still splits into its own "
|
|
249
|
+
f"listed entry, got: {issues!r}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _write_filler_neighbor_modules(directory: Path, filler_count: int) -> None:
|
|
254
|
+
for each_index in range(filler_count):
|
|
255
|
+
filler_path = directory / f"filler_module_{each_index:03d}.py"
|
|
256
|
+
filler_path.write_text("PLACEHOLDER_VALUE = 1\n", encoding="utf-8")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_should_stop_scanning_neighbors_past_the_configured_limit(
|
|
260
|
+
tmp_path: Path,
|
|
261
|
+
) -> None:
|
|
262
|
+
neighbor_scan_limit = sys.modules["code_rules_docstrings"].NEIGHBOR_SCAN_FILE_LIMIT
|
|
263
|
+
_write_filler_neighbor_modules(tmp_path, filler_count=neighbor_scan_limit + 5)
|
|
264
|
+
stranded_wrapper_path = tmp_path / "zzz_stranded_processor.py"
|
|
265
|
+
stranded_wrapper_path.write_text(_drifted_processor_source(), encoding="utf-8")
|
|
266
|
+
created_path = str(tmp_path / (TARGET_FLOW_STEM + ".py"))
|
|
267
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
268
|
+
_target_flow_source(), created_path
|
|
269
|
+
)
|
|
270
|
+
assert issues == [], (
|
|
271
|
+
"The alphabetically-last wrapper sits past the neighbor scan limit, so "
|
|
272
|
+
f"the scan must stop before reaching it, got: {issues!r}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_should_still_flag_a_stranded_wrapper_within_the_scan_limit(
|
|
277
|
+
tmp_path: Path,
|
|
278
|
+
) -> None:
|
|
279
|
+
neighbor_scan_limit = sys.modules["code_rules_docstrings"].NEIGHBOR_SCAN_FILE_LIMIT
|
|
280
|
+
stranded_wrapper_path = tmp_path / "aaa_stranded_processor.py"
|
|
281
|
+
stranded_wrapper_path.write_text(_drifted_processor_source(), encoding="utf-8")
|
|
282
|
+
_write_filler_neighbor_modules(tmp_path, filler_count=neighbor_scan_limit + 5)
|
|
283
|
+
created_path = str(tmp_path / (TARGET_FLOW_STEM + ".py"))
|
|
284
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
285
|
+
_target_flow_source(), created_path
|
|
286
|
+
)
|
|
287
|
+
assert any(
|
|
288
|
+
"App Info" in each and "aaa_stranded_processor.py" in each for each in issues
|
|
289
|
+
), (
|
|
290
|
+
"The alphabetically-first wrapper sits within the neighbor scan limit "
|
|
291
|
+
f"and stays flagged even with extra filler neighbors, got: {issues!r}"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _target_flow_source_with_nested_duplicate_name() -> str:
|
|
296
|
+
return (
|
|
297
|
+
"async def _refresh_store_sections(processor: object, page: object) -> bool:\n"
|
|
298
|
+
' """Apply Russia uncheck, review note, and Publication edits.\n'
|
|
299
|
+
"\n"
|
|
300
|
+
" The App Info edit runs earlier in the binary phase and never\n"
|
|
301
|
+
" repeats here.\n"
|
|
302
|
+
' """\n'
|
|
303
|
+
" return True\n"
|
|
304
|
+
"\n"
|
|
305
|
+
"\n"
|
|
306
|
+
"def _outer_helper() -> bool:\n"
|
|
307
|
+
" def _refresh_store_sections() -> bool:\n"
|
|
308
|
+
' """A same-named nested helper that must not shadow the'
|
|
309
|
+
' top-level entry."""\n'
|
|
310
|
+
" return True\n"
|
|
311
|
+
"\n"
|
|
312
|
+
" return _refresh_store_sections()\n"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_should_not_let_a_nested_duplicate_name_hide_the_top_level_entry(
|
|
317
|
+
tmp_path: Path,
|
|
318
|
+
) -> None:
|
|
319
|
+
_create_target_flow(tmp_path, _target_flow_source_with_nested_duplicate_name())
|
|
320
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
321
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
322
|
+
_drifted_processor_source(), checked_path
|
|
323
|
+
)
|
|
324
|
+
assert any("App Info" in each for each in issues), (
|
|
325
|
+
"A nested helper sharing the top-level function's name must not "
|
|
326
|
+
f"delete the top-level entry from comparison, got: {issues!r}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _processor_source_with_nested_pointer_wrapper() -> str:
|
|
331
|
+
return (
|
|
332
|
+
"class PortalProcessor:\n"
|
|
333
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
334
|
+
' """Drive one theme end to end."""\n'
|
|
335
|
+
" return True\n"
|
|
336
|
+
"\n"
|
|
337
|
+
" def _run_nested_helper(self) -> bool:\n"
|
|
338
|
+
" async def _refresh_store_sections() -> bool:\n"
|
|
339
|
+
' """Apply App Info, Russia, review note, publication'
|
|
340
|
+
' edits; full doc on ``listing_edit_flow``.\"\"\"\n'
|
|
341
|
+
" return True\n"
|
|
342
|
+
" return True\n"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_should_not_flag_a_nested_wrapper_pointing_at_a_delegate(
|
|
347
|
+
tmp_path: Path,
|
|
348
|
+
) -> None:
|
|
349
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
350
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
351
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
352
|
+
_processor_source_with_nested_pointer_wrapper(), checked_path
|
|
353
|
+
)
|
|
354
|
+
assert issues == [], (
|
|
355
|
+
"A function-local wrapper is not a top-level function or class "
|
|
356
|
+
f"method, so it must never be compared, got: {issues!r}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _neighbor_source_with_nested_pointer_wrapper() -> str:
|
|
361
|
+
return (
|
|
362
|
+
"def _run_nested_helper() -> bool:\n"
|
|
363
|
+
" async def _refresh_store_sections() -> bool:\n"
|
|
364
|
+
' """Apply App Info, Russia, review note, publication edits;'
|
|
365
|
+
' full doc on ``listing_edit_flow``.\"\"\"\n'
|
|
366
|
+
" return True\n"
|
|
367
|
+
" return True\n"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_should_not_flag_a_nested_neighbor_wrapper_as_a_stranded_caller(
|
|
372
|
+
tmp_path: Path,
|
|
373
|
+
) -> None:
|
|
374
|
+
neighbor_path = tmp_path / "portal_helpers.py"
|
|
375
|
+
neighbor_path.write_text(
|
|
376
|
+
_neighbor_source_with_nested_pointer_wrapper(), encoding="utf-8"
|
|
377
|
+
)
|
|
378
|
+
created_path = str(tmp_path / (TARGET_FLOW_STEM + ".py"))
|
|
379
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
380
|
+
_target_flow_source(), created_path
|
|
381
|
+
)
|
|
382
|
+
assert issues == [], (
|
|
383
|
+
"A function-local wrapper in a neighbor module is not a pointing "
|
|
384
|
+
f"caller, so it must never be treated as stranded, got: {issues!r}"
|
|
385
|
+
)
|