claude-dev-env 1.58.0 → 1.59.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 +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_duplicate_body.py +287 -0
- package/hooks/blocking/code_rules_enforcer.py +175 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +17 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/autoconverge/SKILL.md +13 -2
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +106 -36
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/update/SKILL.md +37 -5
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Unit tests for hook_prose_detector_consistency PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import pathlib
|
|
7
|
+
import sys
|
|
8
|
+
from unittest import mock
|
|
9
|
+
|
|
10
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
11
|
+
_HOOKS_ROOT = _HOOK_DIR.parent
|
|
12
|
+
for _each_root in (str(_HOOK_DIR), str(_HOOKS_ROOT)):
|
|
13
|
+
if _each_root not in sys.path:
|
|
14
|
+
sys.path.insert(0, _each_root)
|
|
15
|
+
|
|
16
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
17
|
+
"hook_prose_detector_consistency",
|
|
18
|
+
_HOOK_DIR / "hook_prose_detector_consistency.py",
|
|
19
|
+
)
|
|
20
|
+
assert hook_spec is not None
|
|
21
|
+
assert hook_spec.loader is not None
|
|
22
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
23
|
+
hook_spec.loader.exec_module(hook_module)
|
|
24
|
+
|
|
25
|
+
content_has_violation = hook_module.content_has_violation
|
|
26
|
+
claims_output_key_trigger = hook_module.claims_output_key_trigger
|
|
27
|
+
detects_only_path_shape = hook_module.detects_only_path_shape
|
|
28
|
+
is_constants_module = hook_module.is_constants_module
|
|
29
|
+
is_hook_python_module = hook_module.is_hook_python_module
|
|
30
|
+
is_own_detector_family = hook_module.is_own_detector_family
|
|
31
|
+
written_content = hook_module.written_content
|
|
32
|
+
|
|
33
|
+
_BLOCKER_MODULE_PATH = "/repo/hooks/blocking/some_blocker.py"
|
|
34
|
+
_CONSTANTS_MODULE_PATH = "/repo/hooks/hooks_constants/some_blocker_constants.py"
|
|
35
|
+
|
|
36
|
+
_OWN_HOOK_PATH = "/repo/packages/x/hooks/blocking/hook_prose_detector_consistency.py"
|
|
37
|
+
_OWN_CONSTANTS_PATH = (
|
|
38
|
+
"/repo/packages/x/hooks/hooks_constants/hook_prose_detector_consistency_constants.py"
|
|
39
|
+
)
|
|
40
|
+
_OWN_TEST_PATH = "/repo/packages/x/hooks/blocking/test_hook_prose_detector_consistency.py"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_OVERSTATED_MESSAGE_MODULE = (
|
|
44
|
+
'path_context = re.compile(r"(?:[\\\\/]\\s*([A-Za-z][\\w]*?_[ijk])")\n'
|
|
45
|
+
"CORRECTIVE_MESSAGE = (\n"
|
|
46
|
+
' "A bare per-iteration index token (for example `cand_i`) appears as a path "\n'
|
|
47
|
+
' "or output-key segment inside a looping block."\n'
|
|
48
|
+
")\n"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_FIXED_MESSAGE_MODULE = (
|
|
52
|
+
'path_context = re.compile(r"(?:[\\\\/]\\s*([A-Za-z][\\w]*?_[ijk])")\n'
|
|
53
|
+
"CORRECTIVE_MESSAGE = (\n"
|
|
54
|
+
' "A bare per-iteration index token (for example `cand_i`) appears as a "\n'
|
|
55
|
+
' "per-iteration path segment inside a looping block."\n'
|
|
56
|
+
")\n"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_overstated_path_shape_module_is_flagged() -> None:
|
|
61
|
+
assert content_has_violation(_OVERSTATED_MESSAGE_MODULE, _BLOCKER_MODULE_PATH) is True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_fixed_path_shape_module_passes() -> None:
|
|
65
|
+
assert content_has_violation(_FIXED_MESSAGE_MODULE, _BLOCKER_MODULE_PATH) is False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_output_key_claim_without_path_detector_in_blocker_passes() -> None:
|
|
69
|
+
no_path_detector = (
|
|
70
|
+
'pattern = re.compile(r"[A-Za-z]+")\n'
|
|
71
|
+
'CORRECTIVE_MESSAGE = "blocks an output-key segment"\n'
|
|
72
|
+
)
|
|
73
|
+
assert content_has_violation(no_path_detector, _BLOCKER_MODULE_PATH) is False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_output_key_claim_alone_in_constants_module_is_flagged() -> None:
|
|
77
|
+
constants_only = 'CORRECTIVE_MESSAGE = "appears as a path or output-key segment"\n'
|
|
78
|
+
assert content_has_violation(constants_only, _CONSTANTS_MODULE_PATH) is True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_constants_module_without_output_key_claim_passes() -> None:
|
|
82
|
+
clean_constants = 'CORRECTIVE_MESSAGE = "appears as a per-iteration path segment"\n'
|
|
83
|
+
assert content_has_violation(clean_constants, _CONSTANTS_MODULE_PATH) is False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_path_detector_without_output_key_claim_passes() -> None:
|
|
87
|
+
path_only = 'path_context = re.compile(r"(?:[\\\\/]\\s*([A-Za-z][\\w]*?_[ijk])")\n'
|
|
88
|
+
assert content_has_violation(path_only, _BLOCKER_MODULE_PATH) is False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_space_separated_output_key_phrase_is_flagged() -> None:
|
|
92
|
+
space_variant = (
|
|
93
|
+
'path_context = re.compile(r"(?:[\\\\/]\\s*(token))")\n'
|
|
94
|
+
'message = "appears as a path or output key segment"\n'
|
|
95
|
+
)
|
|
96
|
+
assert content_has_violation(space_variant, _BLOCKER_MODULE_PATH) is True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_own_hook_module_is_exempt_from_self_lockout() -> None:
|
|
100
|
+
own_hook_content = pathlib.Path(hook_module.__file__).read_text(encoding="utf-8")
|
|
101
|
+
assert content_has_violation(own_hook_content, _OWN_HOOK_PATH) is False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_own_constants_module_is_exempt_from_self_lockout() -> None:
|
|
105
|
+
own_constants_path = (
|
|
106
|
+
_HOOKS_ROOT
|
|
107
|
+
/ "hooks_constants"
|
|
108
|
+
/ "hook_prose_detector_consistency_constants.py"
|
|
109
|
+
)
|
|
110
|
+
own_constants_content = own_constants_path.read_text(encoding="utf-8")
|
|
111
|
+
assert content_has_violation(own_constants_content, _OWN_CONSTANTS_PATH) is False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_own_test_module_is_exempt_from_self_lockout() -> None:
|
|
115
|
+
own_test_content = pathlib.Path(__file__).read_text(encoding="utf-8")
|
|
116
|
+
assert content_has_violation(own_test_content, _OWN_TEST_PATH) is False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_slot_blocker_constants_companion_passes_at_its_real_path() -> None:
|
|
120
|
+
slot_constants_path = (
|
|
121
|
+
_HOOKS_ROOT
|
|
122
|
+
/ "hooks_constants"
|
|
123
|
+
/ "workflow_substitution_slot_blocker_constants.py"
|
|
124
|
+
)
|
|
125
|
+
slot_constants_content = slot_constants_path.read_text(encoding="utf-8")
|
|
126
|
+
assert content_has_violation(slot_constants_content, str(slot_constants_path)) is False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_is_own_detector_family_recognizes_hook_module() -> None:
|
|
130
|
+
assert is_own_detector_family(_OWN_HOOK_PATH) is True
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_is_own_detector_family_recognizes_constants_companion() -> None:
|
|
134
|
+
assert is_own_detector_family(_OWN_CONSTANTS_PATH) is True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_is_own_detector_family_recognizes_test_module() -> None:
|
|
138
|
+
assert is_own_detector_family(_OWN_TEST_PATH) is True
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_is_own_detector_family_rejects_unrelated_blocker() -> None:
|
|
142
|
+
assert is_own_detector_family(_BLOCKER_MODULE_PATH) is False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_unrelated_constants_module_still_flagged_after_exemption() -> None:
|
|
146
|
+
constants_only = 'CORRECTIVE_MESSAGE = "appears as a path or output-key segment"\n'
|
|
147
|
+
assert content_has_violation(constants_only, _CONSTANTS_MODULE_PATH) is True
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_is_constants_module_accepts_constants_suffix() -> None:
|
|
151
|
+
assert is_constants_module(_CONSTANTS_MODULE_PATH) is True
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_is_constants_module_rejects_blocker_module() -> None:
|
|
155
|
+
assert is_constants_module(_BLOCKER_MODULE_PATH) is False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_claims_output_key_trigger_matches_hyphen_form() -> None:
|
|
159
|
+
assert claims_output_key_trigger("a path or output-key segment here") is True
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_claims_output_key_trigger_ignores_unrelated_output_word() -> None:
|
|
163
|
+
assert claims_output_key_trigger("the output is written to disk") is False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_detects_only_path_shape_finds_separator_class() -> None:
|
|
167
|
+
assert detects_only_path_shape('re.compile(r"[\\\\/]token")') is True
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_detects_only_path_shape_finds_backslash_only_class() -> None:
|
|
171
|
+
assert detects_only_path_shape(r'pat = re.compile(r"[\\]token")') is True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_detects_only_path_shape_false_without_separator_class() -> None:
|
|
175
|
+
assert detects_only_path_shape('re.compile(r"[A-Za-z]+")') is False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_is_hook_python_module_accepts_hooks_path() -> None:
|
|
179
|
+
assert is_hook_python_module("/repo/packages/x/hooks/blocking/some_blocker.py") is True
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_is_hook_python_module_rejects_non_hook_path() -> None:
|
|
183
|
+
assert is_hook_python_module("/repo/src/blocking/some_blocker.py") is False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_is_hook_python_module_rejects_non_python_file() -> None:
|
|
187
|
+
assert is_hook_python_module("/repo/hooks/blocking/notes.md") is False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_written_content_reads_edit_new_string() -> None:
|
|
191
|
+
edit_input = {"new_string": "edited body"}
|
|
192
|
+
assert written_content("Edit", edit_input) == "edited body"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _run_main_with_io(input_text: str) -> str:
|
|
196
|
+
with mock.patch("sys.stdin", io.StringIO(input_text)):
|
|
197
|
+
with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
|
|
198
|
+
try:
|
|
199
|
+
hook_module.main()
|
|
200
|
+
except SystemExit:
|
|
201
|
+
pass
|
|
202
|
+
return mock_stdout.getvalue()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_main_blocks_overstated_hook_module_write() -> None:
|
|
206
|
+
hook_input = {
|
|
207
|
+
"tool_name": "Write",
|
|
208
|
+
"tool_input": {
|
|
209
|
+
"file_path": "/repo/hooks/hooks_constants/some_blocker_constants.py",
|
|
210
|
+
"content": _OVERSTATED_MESSAGE_MODULE,
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
output_text = _run_main_with_io(json.dumps(hook_input))
|
|
214
|
+
payload = json.loads(output_text)
|
|
215
|
+
assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_main_blocks_overstated_hook_module_edit() -> None:
|
|
219
|
+
hook_input = {
|
|
220
|
+
"tool_name": "Edit",
|
|
221
|
+
"tool_input": {
|
|
222
|
+
"file_path": "/repo/hooks/hooks_constants/some_blocker_constants.py",
|
|
223
|
+
"new_string": _OVERSTATED_MESSAGE_MODULE,
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
output_text = _run_main_with_io(json.dumps(hook_input))
|
|
227
|
+
payload = json.loads(output_text)
|
|
228
|
+
assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_main_passes_fixed_hook_module_write() -> None:
|
|
232
|
+
hook_input = {
|
|
233
|
+
"tool_name": "Write",
|
|
234
|
+
"tool_input": {
|
|
235
|
+
"file_path": "/repo/hooks/hooks_constants/some_blocker_constants.py",
|
|
236
|
+
"content": _FIXED_MESSAGE_MODULE,
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
assert _run_main_with_io(json.dumps(hook_input)) == ""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_main_passes_non_hook_path() -> None:
|
|
243
|
+
hook_input = {
|
|
244
|
+
"tool_name": "Write",
|
|
245
|
+
"tool_input": {
|
|
246
|
+
"file_path": "/repo/src/some_blocker.py",
|
|
247
|
+
"content": _OVERSTATED_MESSAGE_MODULE,
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
assert _run_main_with_io(json.dumps(hook_input)) == ""
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_main_passes_wrong_tool_name() -> None:
|
|
254
|
+
hook_input = {
|
|
255
|
+
"tool_name": "Bash",
|
|
256
|
+
"tool_input": {
|
|
257
|
+
"file_path": "/repo/hooks/blocking/x.py",
|
|
258
|
+
"command": "echo output-key segment",
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
assert _run_main_with_io(json.dumps(hook_input)) == ""
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_main_passes_malformed_json() -> None:
|
|
265
|
+
assert _run_main_with_io("not valid json {{{") == ""
|