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.
Files changed (34) hide show
  1. package/CLAUDE.md +9 -0
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +426 -85
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +20 -0
  4. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +625 -21
  5. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +15 -0
  6. package/agents/clean-coder.md +7 -1
  7. package/agents/code-quality-agent.md +8 -5
  8. package/hooks/blocking/code_rules_enforcer.py +1562 -37
  9. package/hooks/blocking/open_questions_in_plans_blocker.py +249 -0
  10. package/hooks/blocking/test_code_rules_enforcer.py +1389 -0
  11. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +292 -0
  12. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +46 -8
  13. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +189 -0
  14. package/hooks/blocking/test_code_rules_enforcer_function_length.py +210 -0
  15. package/hooks/blocking/test_code_rules_enforcer_tests_isolate_home_temp.py +1512 -0
  16. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +9 -5
  17. package/hooks/blocking/test_open_questions_in_plans_blocker.py +790 -0
  18. package/hooks/hooks.json +10 -0
  19. package/hooks/hooks_constants/banned_identifiers_constants.py +19 -0
  20. package/hooks/hooks_constants/code_rules_enforcer_constants.py +129 -2
  21. package/hooks/hooks_constants/open_questions_in_plans_blocker_constants.py +35 -0
  22. package/hooks/hooks_constants/test_open_questions_in_plans_blocker_constants.py +125 -0
  23. package/package.json +1 -1
  24. package/skills/_shared/pr-loop/scripts/_path_resolver.py +34 -13
  25. package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -2
  26. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -4
  27. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +57 -0
  28. package/skills/_shared/pr-loop/scripts/test_init_loop_state.py +48 -0
  29. package/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +59 -0
  30. package/skills/bugteam/PROMPTS.md +48 -12
  31. package/skills/bugteam/reference/team-setup.md +4 -2
  32. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +487 -76
  33. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +22 -1
  34. 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)