claude-dev-env 1.44.0 → 1.45.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 +9 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +426 -85
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +20 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +625 -21
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +15 -0
- package/agents/clean-coder.md +7 -1
- package/agents/code-quality-agent.md +8 -5
- package/hooks/blocking/code_rules_enforcer.py +1562 -37
- package/hooks/blocking/open_questions_in_plans_blocker.py +249 -0
- package/hooks/blocking/test_code_rules_enforcer.py +1389 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +292 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +46 -8
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +189 -0
- package/hooks/blocking/test_code_rules_enforcer_function_length.py +210 -0
- package/hooks/blocking/test_code_rules_enforcer_tests_isolate_home_temp.py +1512 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +9 -5
- package/hooks/blocking/test_open_questions_in_plans_blocker.py +790 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/banned_identifiers_constants.py +19 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +129 -2
- package/hooks/hooks_constants/open_questions_in_plans_blocker_constants.py +35 -0
- package/hooks/hooks_constants/test_open_questions_in_plans_blocker_constants.py +125 -0
- package/package.json +1 -1
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +34 -13
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -2
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -4
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +57 -0
- package/skills/_shared/pr-loop/scripts/test_init_loop_state.py +48 -0
- package/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +59 -0
- package/skills/bugteam/PROMPTS.md +48 -12
- package/skills/bugteam/reference/team-setup.md +4 -2
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +487 -76
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +22 -1
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +597 -12
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Tests for ``check_function_length``.
|
|
2
|
+
|
|
3
|
+
Functions whose definition span (signature line through last body statement,
|
|
4
|
+
inclusive) is at or above ``FUNCTION_LENGTH_BLOCKING_THRESHOLD`` (60) block the
|
|
5
|
+
write (small-function basis: Robert C. Martin, Clean Code Ch. 3 "Functions";
|
|
6
|
+
Google Python Style Guide ~40-line function review hint). Spans below the
|
|
7
|
+
threshold pass silently.
|
|
8
|
+
|
|
9
|
+
Cited SYNTHESIS evidence: pa#143 F4, F9, F14 (three recurrences in one PR);
|
|
10
|
+
pa#136 F20.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import importlib.util
|
|
16
|
+
import pathlib
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
20
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
22
|
+
|
|
23
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
24
|
+
"code_rules_enforcer",
|
|
25
|
+
_HOOK_DIR / "code_rules_enforcer.py",
|
|
26
|
+
)
|
|
27
|
+
assert hook_spec is not None
|
|
28
|
+
assert hook_spec.loader is not None
|
|
29
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
30
|
+
hook_spec.loader.exec_module(hook_module)
|
|
31
|
+
check_function_length = hook_module.check_function_length
|
|
32
|
+
|
|
33
|
+
PRODUCTION_FILE_PATH = "/project/src/long_module.py"
|
|
34
|
+
TEST_FILE_PATH = "/project/src/test_long_module.py"
|
|
35
|
+
MIGRATION_FILE_PATH = "/project/src/migrations/0001_initial.py"
|
|
36
|
+
HOOK_INFRASTRUCTURE_PATH = "/packages/claude-dev-env/hooks/blocking/example.py"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _build_function_source(name: str, body_line_count: int) -> str:
|
|
40
|
+
body_lines = [
|
|
41
|
+
f" statement_{each_index} = {each_index}" for each_index in range(body_line_count)
|
|
42
|
+
]
|
|
43
|
+
body_block = "\n".join(body_lines)
|
|
44
|
+
return f"def {name}() -> None:\n{body_block}\n"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_should_not_flag_short_function() -> None:
|
|
48
|
+
source = _build_function_source("compact_helper", body_line_count=5)
|
|
49
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
50
|
+
assert issues == []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_not_block_mid_band_function_under_blocking_threshold() -> None:
|
|
54
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 5
|
|
55
|
+
source = _build_function_source("mid_helper", body_line_count=body_line_count)
|
|
56
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
57
|
+
assert issues == []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_should_block_at_sixty_lines() -> None:
|
|
61
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
62
|
+
source = _build_function_source("oversized_helper", body_line_count=body_line_count)
|
|
63
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
64
|
+
assert any("oversized_helper" in each_issue for each_issue in issues)
|
|
65
|
+
assert any("blocking" in each_issue.lower() for each_issue in issues)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_should_not_block_at_fifty_nine_line_span() -> None:
|
|
69
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 2
|
|
70
|
+
source = _build_function_source("boundary_helper", body_line_count=body_line_count)
|
|
71
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
72
|
+
assert issues == []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_should_handle_async_function_definitions() -> None:
|
|
76
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
77
|
+
body_lines = [
|
|
78
|
+
f" statement_{each_index} = {each_index}" for each_index in range(body_line_count)
|
|
79
|
+
]
|
|
80
|
+
source = "async def long_async_helper() -> None:\n" + "\n".join(body_lines) + "\n"
|
|
81
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
82
|
+
assert any("long_async_helper" in each_issue for each_issue in issues)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_should_skip_test_files() -> None:
|
|
86
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
87
|
+
source = _build_function_source("test_long_scenario", body_line_count=body_line_count)
|
|
88
|
+
issues = check_function_length(source, TEST_FILE_PATH)
|
|
89
|
+
assert issues == []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_should_skip_migrations() -> None:
|
|
93
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
94
|
+
source = _build_function_source("operation_body", body_line_count=body_line_count)
|
|
95
|
+
issues = check_function_length(source, MIGRATION_FILE_PATH)
|
|
96
|
+
assert issues == []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
100
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
101
|
+
source = _build_function_source("hook_helper", body_line_count=body_line_count)
|
|
102
|
+
issues = check_function_length(source, HOOK_INFRASTRUCTURE_PATH)
|
|
103
|
+
assert issues == []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_should_skip_when_source_does_not_parse() -> None:
|
|
107
|
+
source = "def broken(:\n"
|
|
108
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
109
|
+
assert issues == []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_edit_drops_every_out_of_scope_long_function() -> None:
|
|
113
|
+
"""An edit that touches none of the oversized functions reports nothing —
|
|
114
|
+
every violation is out of scope (untouched code must not block a single-file
|
|
115
|
+
edit)."""
|
|
116
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
117
|
+
body_lines = [
|
|
118
|
+
f" statement_{each_index} = {each_index}" for each_index in range(body_line_count)
|
|
119
|
+
]
|
|
120
|
+
body_block = "\n".join(body_lines)
|
|
121
|
+
function_count = 8
|
|
122
|
+
chunks = [
|
|
123
|
+
f"def f_{each_index}() -> None:\n{body_block}\n" for each_index in range(function_count)
|
|
124
|
+
]
|
|
125
|
+
source = "\n".join(chunks)
|
|
126
|
+
untouched_line_far_outside_any_span = 100000
|
|
127
|
+
issues = check_function_length(
|
|
128
|
+
source,
|
|
129
|
+
PRODUCTION_FILE_PATH,
|
|
130
|
+
all_changed_lines={untouched_line_far_outside_any_span},
|
|
131
|
+
)
|
|
132
|
+
assert issues == []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_new_file_reports_every_long_function_uncapped() -> None:
|
|
136
|
+
"""On a new file (``all_changed_lines is None``) every line is in scope, so
|
|
137
|
+
every long function is reported with no ceiling on the count."""
|
|
138
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
139
|
+
body_lines = [
|
|
140
|
+
f" statement_{each_index} = {each_index}" for each_index in range(body_line_count)
|
|
141
|
+
]
|
|
142
|
+
body_block = "\n".join(body_lines)
|
|
143
|
+
function_count = 8
|
|
144
|
+
chunks = [
|
|
145
|
+
f"def f_{each_index}() -> None:\n{body_block}\n" for each_index in range(function_count)
|
|
146
|
+
]
|
|
147
|
+
source = "\n".join(chunks)
|
|
148
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
149
|
+
assert len(issues) == function_count
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_should_block_nested_function_over_blocking_threshold() -> None:
|
|
153
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
154
|
+
inner_body = "\n".join(
|
|
155
|
+
f" inner_statement_{each_index} = {each_index}"
|
|
156
|
+
for each_index in range(body_line_count)
|
|
157
|
+
)
|
|
158
|
+
source = f"def outer() -> None:\n def inner() -> None:\n{inner_body}\n"
|
|
159
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
160
|
+
assert any("inner" in each_issue for each_issue in issues)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_blocking_message_does_not_cite_file_length_section() -> None:
|
|
164
|
+
assert "6.5" not in hook_module.FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX
|
|
165
|
+
assert "Clean Code" in hook_module.FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _oversized_source(name: str) -> str:
|
|
169
|
+
return _build_function_source(
|
|
170
|
+
name, body_line_count=hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_changed_lines_scope_skips_untouched_long_function() -> None:
|
|
175
|
+
"""loop5-1: with changed_lines naming only the short helper, an untouched
|
|
176
|
+
oversized function above it must not appear in the issues."""
|
|
177
|
+
untouched_long = _oversized_source("untouched_long")
|
|
178
|
+
short_helper = "def short_helper() -> int:\n return 2\n"
|
|
179
|
+
full_source = untouched_long + "\n" + short_helper
|
|
180
|
+
short_helper_line = len(full_source.splitlines())
|
|
181
|
+
issues = check_function_length(
|
|
182
|
+
full_source, PRODUCTION_FILE_PATH, all_changed_lines={short_helper_line}
|
|
183
|
+
)
|
|
184
|
+
assert issues == [], f"untouched long function must not be in scope, got: {issues!r}"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_changed_lines_scope_keeps_touched_long_function() -> None:
|
|
188
|
+
"""loop5-1: when a changed line falls inside the oversized function's span,
|
|
189
|
+
the violation must remain in the issues."""
|
|
190
|
+
long_function = _oversized_source("grows_now")
|
|
191
|
+
issues = check_function_length(
|
|
192
|
+
long_function, PRODUCTION_FILE_PATH, all_changed_lines={2}
|
|
193
|
+
)
|
|
194
|
+
assert any("grows_now" in each_issue for each_issue in issues)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_reports_only_in_scope_violation_among_untouched_ones() -> None:
|
|
198
|
+
"""loop5-2: an in-scope violation appearing after several untouched
|
|
199
|
+
out-of-scope violations is still reported, while the untouched ones stay out
|
|
200
|
+
of scope."""
|
|
201
|
+
leading_count = 5
|
|
202
|
+
leading = "\n".join(_oversized_source(f"leading_{each_index}") for each_index in range(leading_count))
|
|
203
|
+
target = _oversized_source("target_function")
|
|
204
|
+
full_source = leading + "\n" + target
|
|
205
|
+
target_definition_line = len(leading.splitlines()) + 2
|
|
206
|
+
issues = check_function_length(
|
|
207
|
+
full_source, PRODUCTION_FILE_PATH, all_changed_lines={target_definition_line}
|
|
208
|
+
)
|
|
209
|
+
assert any("target_function" in each_issue for each_issue in issues)
|
|
210
|
+
assert not any("leading_" in each_issue for each_issue in issues)
|