claude-dev-env 2.28.0 → 2.28.1
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/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/docs/CODE_RULES.md +3 -3
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/references/prose-style-enforcement.md +5 -9
- package/hooks/blocking/AGENTS.md +3 -13
- package/hooks/blocking/code_rules_docstrings.py +10 -2138
- package/hooks/blocking/code_rules_enforcer.py +0 -121
- package/hooks/blocking/code_rules_imports_logging.py +1 -236
- package/hooks/blocking/code_rules_shared.py +23 -0
- package/hooks/blocking/code_rules_test_layout.py +8 -8
- package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
- package/hooks/blocking/precommit_code_rules_gate.py +8 -43
- package/hooks/blocking/state_description_blocker.py +1 -7
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
- package/hooks/blocking/test_code_rules_shared.py +12 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
- package/hooks/blocking/test_state_description_blocker.py +6 -4
- package/hooks/blocking/test_stop_dispatcher.py +5 -29
- package/hooks/hooks_constants/AGENTS.md +1 -6
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/messages.py +0 -2
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/test_messages.py +5 -3
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
- package/package.json +1 -1
- package/rules/AGENTS.md +3 -6
- package/rules/claims-as-quotes.md +0 -10
- package/rules/code-standards.md +6 -6
- package/rules/explore-thoroughly.md +0 -1
- package/rules/failure-blast-radius.md +0 -8
- package/rules/falsify-before-green.md +0 -8
- package/rules/file-global-constants.md +2 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +1 -9
- package/rules/hedging-claims.md +2 -6
- package/rules/long-horizon-autonomy.md +1 -1
- package/rules/measurement-denominators.md +0 -9
- package/rules/research-mode.md +0 -6
- package/rules/verify-before-asking.md +0 -5
- package/rules/verify-runtime-state.md +0 -5
- package/scripts/codex_compat_materializer.py +0 -12
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/tests/test_engine.py +0 -1
- package/scripts/tests/test_rules.py +0 -1
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/system-prompts/software-engineer.xml +3 -3
- package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
- package/hooks/blocking/code_rules_dead_config_field.py +0 -568
- package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
- package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
- package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
- package/hooks/blocking/code_rules_mock_completeness.py +0 -295
- package/hooks/blocking/code_rules_scope_binding.py +0 -151
- package/hooks/blocking/code_rules_unused_imports.py +0 -197
- package/hooks/blocking/hedging_language_blocker.py +0 -221
- package/hooks/blocking/intent_only_ending_blocker.py +0 -148
- package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
- package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
- package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
- package/hooks/blocking/test_hedging_language_blocker.py +0 -261
- package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
- package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
- package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
- package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
- package/rules/conservative-action.md +0 -17
- package/rules/context7.md +0 -8
- package/rules/few-words.md +0 -3
- package/rules/parallel-tools.md +0 -23
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
"""Tests for hedging_language_blocker hook response shape."""
|
|
2
|
-
|
|
3
|
-
import importlib.util
|
|
4
|
-
import io
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import subprocess
|
|
8
|
-
import sys
|
|
9
|
-
import tempfile
|
|
10
|
-
|
|
11
|
-
import pytest
|
|
12
|
-
|
|
13
|
-
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "hedging_language_blocker.py")
|
|
14
|
-
_HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
|
|
15
|
-
_HOOKS_ROOT = os.path.join(_HOOKS_DIR, "..")
|
|
16
|
-
_HOOK_CONFIG_DIR = os.path.join(_HOOKS_ROOT, "hooks_constants")
|
|
17
|
-
if _HOOKS_DIR not in sys.path:
|
|
18
|
-
sys.path.insert(0, _HOOKS_DIR)
|
|
19
|
-
if _HOOKS_ROOT not in sys.path:
|
|
20
|
-
sys.path.insert(0, _HOOKS_ROOT)
|
|
21
|
-
import hedging_language_blocker
|
|
22
|
-
from hooks_constants.messages import USER_FACING_NOTICE
|
|
23
|
-
from hooks_constants.text_stripping import strip_code_and_quotes
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_blocker_uses_shared_strip_code_and_quotes() -> None:
|
|
27
|
-
assert hedging_language_blocker.strip_code_and_quotes is strip_code_and_quotes
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
RESEARCH_MODE_SKILL_BODY_MARKER = "Three anti-hallucination constraints are ALWAYS active."
|
|
31
|
-
HEDGING_MESSAGE = "This is likely correct."
|
|
32
|
-
CLEAN_MESSAGE = "This is verified by the source document."
|
|
33
|
-
EMPTY_MESSAGE = ""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def run_hook_with_message(
|
|
37
|
-
assistant_message: str, *, is_prose_style_enabled: bool = True
|
|
38
|
-
) -> subprocess.CompletedProcess:
|
|
39
|
-
hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
|
|
40
|
-
environment_by_key = os.environ.copy()
|
|
41
|
-
if is_prose_style_enabled:
|
|
42
|
-
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
43
|
-
else:
|
|
44
|
-
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
45
|
-
return subprocess.run(
|
|
46
|
-
[sys.executable, HOOK_SCRIPT_PATH],
|
|
47
|
-
input=hook_input_payload,
|
|
48
|
-
capture_output=True,
|
|
49
|
-
text=True,
|
|
50
|
-
check=False,
|
|
51
|
-
env=environment_by_key,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def run_hook_with_patched_search_paths(
|
|
56
|
-
assistant_message: str,
|
|
57
|
-
search_paths: list[str],
|
|
58
|
-
) -> subprocess.CompletedProcess:
|
|
59
|
-
"""Run the hook with RESEARCH_MODE_SKILL_SEARCH_PATHS overridden via a wrapper script."""
|
|
60
|
-
wrapper_script = (
|
|
61
|
-
"import sys, json, os\n"
|
|
62
|
-
f"sys.path.insert(0, {repr(os.path.dirname(HOOK_SCRIPT_PATH))})\n"
|
|
63
|
-
"import hedging_language_blocker as blocker\n"
|
|
64
|
-
f"blocker.RESEARCH_MODE_SKILL_SEARCH_PATHS = {repr(search_paths)}\n"
|
|
65
|
-
"blocker.main()\n"
|
|
66
|
-
)
|
|
67
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as wrapper_file:
|
|
68
|
-
wrapper_file.write(wrapper_script)
|
|
69
|
-
wrapper_file_path = wrapper_file.name
|
|
70
|
-
|
|
71
|
-
hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
|
|
72
|
-
environment_by_key = os.environ.copy()
|
|
73
|
-
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
74
|
-
try:
|
|
75
|
-
completed_process = subprocess.run(
|
|
76
|
-
[sys.executable, wrapper_file_path],
|
|
77
|
-
input=hook_input_payload,
|
|
78
|
-
capture_output=True,
|
|
79
|
-
text=True,
|
|
80
|
-
check=False,
|
|
81
|
-
env=environment_by_key,
|
|
82
|
-
)
|
|
83
|
-
finally:
|
|
84
|
-
os.unlink(wrapper_file_path)
|
|
85
|
-
return completed_process
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def test_user_facing_notice_matches_config_messages_module():
|
|
89
|
-
config_messages_path = os.path.join(_HOOK_CONFIG_DIR, "messages.py")
|
|
90
|
-
specification = importlib.util.spec_from_file_location("messages", config_messages_path)
|
|
91
|
-
module = importlib.util.module_from_spec(specification)
|
|
92
|
-
specification.loader.exec_module(module)
|
|
93
|
-
|
|
94
|
-
assert module.USER_FACING_NOTICE == USER_FACING_NOTICE
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def test_hedging_scan_is_default_off() -> None:
|
|
98
|
-
completed_process = run_hook_with_message(
|
|
99
|
-
HEDGING_MESSAGE, is_prose_style_enabled=False
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
assert completed_process.returncode == 0
|
|
103
|
-
assert completed_process.stdout == ""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_default_off_emits_privacy_safe_advisory_candidates(
|
|
107
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
108
|
-
) -> None:
|
|
109
|
-
all_emitted: list[tuple[str, str, str]] = []
|
|
110
|
-
|
|
111
|
-
def _record_advisory(
|
|
112
|
-
matcher_id: str, surface: str, context_text: str, **_kwargs: object
|
|
113
|
-
) -> dict[str, object]:
|
|
114
|
-
all_emitted.append((matcher_id, surface, context_text))
|
|
115
|
-
return {}
|
|
116
|
-
|
|
117
|
-
monkeypatch.setattr(
|
|
118
|
-
hedging_language_blocker, "emit_advisory_candidate", _record_advisory
|
|
119
|
-
)
|
|
120
|
-
monkeypatch.setattr(
|
|
121
|
-
hedging_language_blocker,
|
|
122
|
-
"prose_style_enforcement_enabled_in_environment",
|
|
123
|
-
lambda: False,
|
|
124
|
-
)
|
|
125
|
-
monkeypatch.setattr(
|
|
126
|
-
hedging_language_blocker.sys,
|
|
127
|
-
"stdin",
|
|
128
|
-
io.StringIO(json.dumps({"last_assistant_message": HEDGING_MESSAGE})),
|
|
129
|
-
)
|
|
130
|
-
with pytest.raises(SystemExit) as exit_info:
|
|
131
|
-
hedging_language_blocker.main()
|
|
132
|
-
assert exit_info.value.code == 0
|
|
133
|
-
assert all_emitted
|
|
134
|
-
matcher_id, surface, context_text = all_emitted[0]
|
|
135
|
-
assert matcher_id == "hedging_word"
|
|
136
|
-
assert surface == "Stop"
|
|
137
|
-
assert "likely" in context_text
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def test_hedging_message_emits_block_with_short_user_notice():
|
|
141
|
-
completed_process = run_hook_with_message(HEDGING_MESSAGE)
|
|
142
|
-
|
|
143
|
-
assert completed_process.returncode == 0
|
|
144
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
145
|
-
|
|
146
|
-
assert parsed_response["decision"] == "block"
|
|
147
|
-
assert parsed_response["systemMessage"] == USER_FACING_NOTICE
|
|
148
|
-
assert parsed_response["suppressOutput"] is True
|
|
149
|
-
assert "likely" in parsed_response["reason"]
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def test_hedging_reason_contains_not_installed_notice_when_skill_absent():
|
|
153
|
-
completed_process = run_hook_with_patched_search_paths(
|
|
154
|
-
HEDGING_MESSAGE,
|
|
155
|
-
["/nonexistent/path/one/SKILL.md", "/nonexistent/path/two/SKILL.md"],
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
assert completed_process.returncode == 0
|
|
159
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
160
|
-
|
|
161
|
-
assert parsed_response["decision"] == "block"
|
|
162
|
-
assert "no research-mode skill installed" in parsed_response["reason"]
|
|
163
|
-
assert "verify with sources or prompt the user via AskUserQuestion" in parsed_response["reason"]
|
|
164
|
-
assert "SKILL.md" not in parsed_response["reason"]
|
|
165
|
-
assert RESEARCH_MODE_SKILL_BODY_MARKER not in parsed_response["reason"]
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def test_hedging_reason_contains_skill_path_when_skill_present():
|
|
169
|
-
with tempfile.TemporaryDirectory() as skill_dir:
|
|
170
|
-
skill_file_path = os.path.join(skill_dir, "SKILL.md")
|
|
171
|
-
with open(skill_file_path, "w") as skill_file:
|
|
172
|
-
skill_file.write("# Research Mode Skill\n")
|
|
173
|
-
|
|
174
|
-
completed_process = run_hook_with_patched_search_paths(
|
|
175
|
-
HEDGING_MESSAGE,
|
|
176
|
-
["/nonexistent/path/SKILL.md", skill_file_path],
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
assert completed_process.returncode == 0
|
|
180
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
181
|
-
|
|
182
|
-
assert parsed_response["decision"] == "block"
|
|
183
|
-
assert "SKILL.md" in parsed_response["reason"]
|
|
184
|
-
assert "no research-mode skill installed" not in parsed_response["reason"]
|
|
185
|
-
assert RESEARCH_MODE_SKILL_BODY_MARKER not in parsed_response["reason"]
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def test_clean_message_passes_through_with_no_output():
|
|
189
|
-
completed_process = run_hook_with_message(CLEAN_MESSAGE)
|
|
190
|
-
|
|
191
|
-
assert completed_process.returncode == 0
|
|
192
|
-
assert completed_process.stdout == ""
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def test_empty_message_passes_through_with_no_output():
|
|
196
|
-
completed_process = run_hook_with_message(EMPTY_MESSAGE)
|
|
197
|
-
|
|
198
|
-
assert completed_process.returncode == 0
|
|
199
|
-
assert completed_process.stdout == ""
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def test_explicit_unverified_label_in_same_sentence_passes() -> None:
|
|
203
|
-
completed_process = run_hook_with_message(
|
|
204
|
-
"This claim is unverified; the deploy is probably blocked."
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
assert completed_process.returncode == 0
|
|
208
|
-
assert completed_process.stdout == ""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def test_i_dont_know_label_in_same_sentence_passes() -> None:
|
|
212
|
-
completed_process = run_hook_with_message(
|
|
213
|
-
"I don't know whether the port is probably open."
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
assert completed_process.returncode == 0
|
|
217
|
-
assert completed_process.stdout == ""
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def test_supported_probability_without_hedge_word_passes() -> None:
|
|
221
|
-
completed_process = run_hook_with_message(
|
|
222
|
-
"The suite reports 0.92 precision on the labeled fixture set in "
|
|
223
|
-
"test_prose_matcher_advisory.py."
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
assert completed_process.returncode == 0
|
|
227
|
-
assert completed_process.stdout == ""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def test_label_in_one_sentence_does_not_exempt_bare_hedge_in_another() -> None:
|
|
231
|
-
completed_process = run_hook_with_message(
|
|
232
|
-
"This claim is unverified. The deploy is probably blocked."
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
assert completed_process.returncode == 0
|
|
236
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
237
|
-
assert parsed_response["decision"] == "block"
|
|
238
|
-
assert "probably" in parsed_response["reason"]
|
|
239
|
-
assert "explicit uncertainty label" in parsed_response["reason"]
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def test_bare_probably_still_blocks_with_positive_corrective() -> None:
|
|
243
|
-
completed_process = run_hook_with_message("The deploy is probably blocked.")
|
|
244
|
-
|
|
245
|
-
assert completed_process.returncode == 0
|
|
246
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
247
|
-
assert parsed_response["decision"] == "block"
|
|
248
|
-
assert "probably" in parsed_response["reason"]
|
|
249
|
-
assert "label that claim unverified" in parsed_response["reason"]
|
|
250
|
-
assert "AskUserQuestion" in parsed_response["reason"]
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def test_find_blocking_hedging_terms_is_sentence_scoped() -> None:
|
|
254
|
-
bare = hedging_language_blocker.find_blocking_hedging_terms(
|
|
255
|
-
"This claim is unverified. The deploy is probably blocked."
|
|
256
|
-
)
|
|
257
|
-
labeled = hedging_language_blocker.find_blocking_hedging_terms(
|
|
258
|
-
"This claim is unverified; the deploy is probably blocked."
|
|
259
|
-
)
|
|
260
|
-
assert bare == ["probably"]
|
|
261
|
-
assert labeled == []
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
"""Tests for intent_only_ending_blocker hook response shape."""
|
|
2
|
-
|
|
3
|
-
import importlib.util
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import subprocess
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "intent_only_ending_blocker.py")
|
|
10
|
-
_HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
|
|
11
|
-
_HOOKS_ROOT = os.path.join(_HOOKS_DIR, "..")
|
|
12
|
-
_HOOK_CONFIG_DIR = os.path.join(_HOOKS_ROOT, "hooks_constants")
|
|
13
|
-
if _HOOKS_DIR not in sys.path:
|
|
14
|
-
sys.path.insert(0, _HOOKS_DIR)
|
|
15
|
-
if _HOOKS_ROOT not in sys.path:
|
|
16
|
-
sys.path.insert(0, _HOOKS_ROOT)
|
|
17
|
-
import intent_only_ending_blocker
|
|
18
|
-
from hooks_constants.messages import USER_FACING_INTENT_ENDING_NOTICE
|
|
19
|
-
from hooks_constants.text_stripping import strip_code_and_quotes
|
|
20
|
-
|
|
21
|
-
def test_blocker_uses_shared_strip_code_and_quotes() -> None:
|
|
22
|
-
assert intent_only_ending_blocker.strip_code_and_quotes is strip_code_and_quotes
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
INTENT_ENDING_MESSAGE = "I'll now run the test suite and fix any failures that come up."
|
|
26
|
-
NEXT_STEPS_MESSAGE = "Next steps:"
|
|
27
|
-
COMPLETED_WORK_MESSAGE = "Done - all tests pass. The fix is in place."
|
|
28
|
-
BENIGN_PAST_TENSE_MESSAGE = "I implemented the parser and verified it against the fixtures."
|
|
29
|
-
CLEAN_MESSAGE = "The function returns the parsed payload to its caller."
|
|
30
|
-
COMMITMENT_PHRASING_MESSAGE = "I'll commit to keeping the API stable across the next release."
|
|
31
|
-
NAMING_COMMITMENT_MESSAGE = "Let me commit to a clear naming convention for these helpers."
|
|
32
|
-
GIT_COMMIT_INTENT_MESSAGE = "I'll commit the changes once the tests pass."
|
|
33
|
-
DEFERRED_TO_USER_CI_RUNS_MESSAGE = (
|
|
34
|
-
"Let me know if this looks right. The CI will run automatically on push."
|
|
35
|
-
)
|
|
36
|
-
DEFERRED_TO_USER_EITHER_WAY_MESSAGE = (
|
|
37
|
-
"Let me know which option you want. I can check either way."
|
|
38
|
-
)
|
|
39
|
-
USER_DIRECTED_NEXT_STEPS_MESSAGE = (
|
|
40
|
-
"All done. The PR is merged.\n\nNext steps: you should deploy when ready."
|
|
41
|
-
)
|
|
42
|
-
EMPTY_MESSAGE = ""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def run_hook_with_message(
|
|
46
|
-
assistant_message: str, *, is_prose_style_enabled: bool = True
|
|
47
|
-
) -> subprocess.CompletedProcess:
|
|
48
|
-
hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
|
|
49
|
-
environment_by_key = os.environ.copy()
|
|
50
|
-
if is_prose_style_enabled:
|
|
51
|
-
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
52
|
-
else:
|
|
53
|
-
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
54
|
-
return subprocess.run(
|
|
55
|
-
[sys.executable, HOOK_SCRIPT_PATH],
|
|
56
|
-
input=hook_input_payload,
|
|
57
|
-
capture_output=True,
|
|
58
|
-
text=True,
|
|
59
|
-
check=False,
|
|
60
|
-
env=environment_by_key,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def run_hook_with_payload(
|
|
65
|
-
hook_input_payload: dict, *, is_prose_style_enabled: bool = True
|
|
66
|
-
) -> subprocess.CompletedProcess:
|
|
67
|
-
environment_by_key = os.environ.copy()
|
|
68
|
-
if is_prose_style_enabled:
|
|
69
|
-
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
70
|
-
else:
|
|
71
|
-
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
72
|
-
return subprocess.run(
|
|
73
|
-
[sys.executable, HOOK_SCRIPT_PATH],
|
|
74
|
-
input=json.dumps(hook_input_payload),
|
|
75
|
-
capture_output=True,
|
|
76
|
-
text=True,
|
|
77
|
-
check=False,
|
|
78
|
-
env=environment_by_key,
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_user_facing_notice_matches_config_messages_module():
|
|
83
|
-
config_messages_path = os.path.join(_HOOK_CONFIG_DIR, "messages.py")
|
|
84
|
-
specification = importlib.util.spec_from_file_location("messages", config_messages_path)
|
|
85
|
-
module = importlib.util.module_from_spec(specification)
|
|
86
|
-
specification.loader.exec_module(module)
|
|
87
|
-
|
|
88
|
-
assert module.USER_FACING_INTENT_ENDING_NOTICE == USER_FACING_INTENT_ENDING_NOTICE
|
|
89
|
-
assert (
|
|
90
|
-
intent_only_ending_blocker.USER_FACING_INTENT_ENDING_NOTICE
|
|
91
|
-
== module.USER_FACING_INTENT_ENDING_NOTICE
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def test_intent_scan_is_default_off() -> None:
|
|
96
|
-
completed_process = run_hook_with_message(
|
|
97
|
-
INTENT_ENDING_MESSAGE, is_prose_style_enabled=False
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
assert completed_process.returncode == 0
|
|
101
|
-
assert completed_process.stdout == ""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def test_intent_ending_message_emits_block_with_short_user_notice():
|
|
105
|
-
completed_process = run_hook_with_message(INTENT_ENDING_MESSAGE)
|
|
106
|
-
|
|
107
|
-
assert completed_process.returncode == 0
|
|
108
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
109
|
-
|
|
110
|
-
assert parsed_response["decision"] == "block"
|
|
111
|
-
assert parsed_response["systemMessage"] == USER_FACING_INTENT_ENDING_NOTICE
|
|
112
|
-
assert parsed_response["suppressOutput"] is True
|
|
113
|
-
assert "long-horizon-autonomy" in parsed_response["reason"]
|
|
114
|
-
assert "This turn ends with a promise of future work." in parsed_response["reason"]
|
|
115
|
-
assert "Complete that work with tool calls during this turn." in parsed_response[
|
|
116
|
-
"reason"
|
|
117
|
-
]
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def test_next_steps_lead_in_emits_block():
|
|
121
|
-
completed_process = run_hook_with_message(NEXT_STEPS_MESSAGE)
|
|
122
|
-
|
|
123
|
-
assert completed_process.returncode == 0
|
|
124
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
125
|
-
|
|
126
|
-
assert parsed_response["decision"] == "block"
|
|
127
|
-
assert parsed_response["systemMessage"] == USER_FACING_INTENT_ENDING_NOTICE
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def test_completed_work_summary_passes_through_with_no_output():
|
|
131
|
-
completed_process = run_hook_with_message(COMPLETED_WORK_MESSAGE)
|
|
132
|
-
|
|
133
|
-
assert completed_process.returncode == 0
|
|
134
|
-
assert completed_process.stdout == ""
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def test_past_tense_summary_passes_through_with_no_output():
|
|
138
|
-
completed_process = run_hook_with_message(BENIGN_PAST_TENSE_MESSAGE)
|
|
139
|
-
|
|
140
|
-
assert completed_process.returncode == 0
|
|
141
|
-
assert completed_process.stdout == ""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def test_commitment_phrasing_passes_through_with_no_output():
|
|
145
|
-
completed_process = run_hook_with_message(COMMITMENT_PHRASING_MESSAGE)
|
|
146
|
-
|
|
147
|
-
assert completed_process.returncode == 0
|
|
148
|
-
assert completed_process.stdout == ""
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def test_naming_commitment_passes_through_with_no_output():
|
|
152
|
-
completed_process = run_hook_with_message(NAMING_COMMITMENT_MESSAGE)
|
|
153
|
-
|
|
154
|
-
assert completed_process.returncode == 0
|
|
155
|
-
assert completed_process.stdout == ""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def test_git_commit_intent_emits_block():
|
|
159
|
-
completed_process = run_hook_with_message(GIT_COMMIT_INTENT_MESSAGE)
|
|
160
|
-
|
|
161
|
-
assert completed_process.returncode == 0
|
|
162
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
163
|
-
|
|
164
|
-
assert parsed_response["decision"] == "block"
|
|
165
|
-
assert parsed_response["systemMessage"] == USER_FACING_INTENT_ENDING_NOTICE
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def test_benign_opener_with_unrelated_work_verb_passes_through_with_no_output():
|
|
169
|
-
completed_process = run_hook_with_message(DEFERRED_TO_USER_CI_RUNS_MESSAGE)
|
|
170
|
-
|
|
171
|
-
assert completed_process.returncode == 0
|
|
172
|
-
assert completed_process.stdout == ""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def test_deferral_to_user_with_later_verb_passes_through_with_no_output():
|
|
176
|
-
completed_process = run_hook_with_message(DEFERRED_TO_USER_EITHER_WAY_MESSAGE)
|
|
177
|
-
|
|
178
|
-
assert completed_process.returncode == 0
|
|
179
|
-
assert completed_process.stdout == ""
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def test_user_directed_next_steps_passes_through_with_no_output():
|
|
183
|
-
completed_process = run_hook_with_message(USER_DIRECTED_NEXT_STEPS_MESSAGE)
|
|
184
|
-
|
|
185
|
-
assert completed_process.returncode == 0
|
|
186
|
-
assert completed_process.stdout == ""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def test_clean_message_passes_through_with_no_output():
|
|
190
|
-
completed_process = run_hook_with_message(CLEAN_MESSAGE)
|
|
191
|
-
|
|
192
|
-
assert completed_process.returncode == 0
|
|
193
|
-
assert completed_process.stdout == ""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def test_empty_message_passes_through_with_no_output():
|
|
197
|
-
completed_process = run_hook_with_message(EMPTY_MESSAGE)
|
|
198
|
-
|
|
199
|
-
assert completed_process.returncode == 0
|
|
200
|
-
assert completed_process.stdout == ""
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def test_stop_hook_active_short_circuits_with_no_output():
|
|
204
|
-
completed_process = run_hook_with_payload(
|
|
205
|
-
{"last_assistant_message": INTENT_ENDING_MESSAGE, "stop_hook_active": True}
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
assert completed_process.returncode == 0
|
|
209
|
-
assert completed_process.stdout == ""
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"""Constants for the dead argparse-argument detector in ``code_rules_enforcer``.
|
|
2
|
-
|
|
3
|
-
Lives under the hooks-tree ``hooks_constants`` package so module-level
|
|
4
|
-
UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
|
|
5
|
-
requirement and share a home with the other hook-tree configuration.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
ADD_ARGUMENT_METHOD_NAME: str = "add_argument"
|
|
9
|
-
ALL_PARSE_METHOD_NAMES: frozenset[str] = frozenset({"parse_args", "parse_known_args"})
|
|
10
|
-
DEST_KEYWORD_NAME: str = "dest"
|
|
11
|
-
NAMESPACE_KEYWORD_NAME: str = "namespace"
|
|
12
|
-
ACTION_KEYWORD_NAME: str = "action"
|
|
13
|
-
ALL_SUPPRESSED_ACTION_NAMES: frozenset[str] = frozenset({"help", "version"})
|
|
14
|
-
GETATTR_FUNCTION_NAME: str = "getattr"
|
|
15
|
-
GETATTR_NAME_ARGUMENT_MINIMUM: int = 2
|
|
16
|
-
ATTRGETTER_FUNCTION_NAME: str = "attrgetter"
|
|
17
|
-
NAMESPACE_DICT_ATTRIBUTE_NAME: str = "__dict__"
|
|
18
|
-
OPTION_PREFIX: str = "-"
|
|
19
|
-
LONG_OPTION_PREFIX: str = "--"
|
|
20
|
-
DEST_WORD_SEPARATOR: str = "-"
|
|
21
|
-
DEST_WORD_JOINER: str = "_"
|
|
22
|
-
EXPORTED_NAMES_ATTRIBUTE: str = "__all__"
|
|
23
|
-
MAX_DEAD_ARGPARSE_ARGUMENT_ISSUES: int = 25
|
|
24
|
-
DEAD_ARGPARSE_ARGUMENT_GUIDANCE: str = (
|
|
25
|
-
"optional CLI flag whose parsed value is never read in this file - remove the"
|
|
26
|
-
" add_argument call (argparse silently accepts an unused flag), or read the"
|
|
27
|
-
" parsed value where it is needed (CODE_RULES §9.8)"
|
|
28
|
-
)
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""Constants for the dead config-dataclass field detector in ``code_rules_enforcer``.
|
|
2
|
-
|
|
3
|
-
Lives under the hooks-tree ``hooks_constants`` package so module-level
|
|
4
|
-
UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
|
|
5
|
-
requirement and share a home with the other hook-tree configuration.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from hooks_constants.dead_dataclass_field_constants import (
|
|
9
|
-
ALL_REFLECTIVE_FIELD_CONSUMER_NAMES,
|
|
10
|
-
WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME,
|
|
11
|
-
)
|
|
12
|
-
from hooks_constants.dead_module_constant_constants import (
|
|
13
|
-
CONFIG_DIRECTORY_SEGMENT,
|
|
14
|
-
DUNDER_INIT_FILENAME,
|
|
15
|
-
MAX_SCAN_ROOT_FILE_COUNT,
|
|
16
|
-
PYTHON_SOURCE_SUFFIX,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
ALL_CONFIG_CLASS_NAME_SUFFIXES: tuple[str, ...] = ("Config", "Selectors")
|
|
20
|
-
DATACLASSES_MODULE_NAME: str = "dataclasses"
|
|
21
|
-
MAX_DEAD_CONFIG_FIELD_ISSUES: int = 25
|
|
22
|
-
DEAD_CONFIG_FIELD_GUIDANCE: str = (
|
|
23
|
-
"config or selectors dataclass field is defined but read by no production"
|
|
24
|
-
" module in the enclosing package tree - remove the dead field, or read it"
|
|
25
|
-
" where the value is needed (CODE_RULES §9.8)"
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
__all__ = [
|
|
29
|
-
"ALL_CONFIG_CLASS_NAME_SUFFIXES",
|
|
30
|
-
"ALL_REFLECTIVE_FIELD_CONSUMER_NAMES",
|
|
31
|
-
"CONFIG_DIRECTORY_SEGMENT",
|
|
32
|
-
"DATACLASSES_MODULE_NAME",
|
|
33
|
-
"DEAD_CONFIG_FIELD_GUIDANCE",
|
|
34
|
-
"DUNDER_INIT_FILENAME",
|
|
35
|
-
"MAX_DEAD_CONFIG_FIELD_ISSUES",
|
|
36
|
-
"MAX_SCAN_ROOT_FILE_COUNT",
|
|
37
|
-
"PYTHON_SOURCE_SUFFIX",
|
|
38
|
-
"WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME",
|
|
39
|
-
]
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"""Constants for the dead dataclass-field detector in ``code_rules_enforcer``.
|
|
2
|
-
|
|
3
|
-
Lives under the hooks-tree ``hooks_constants`` package so module-level
|
|
4
|
-
UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
|
|
5
|
-
requirement and share a home with the other hook-tree configuration.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
ALL_DATACLASS_DECORATOR_NAMES: frozenset[str] = frozenset({"dataclass", "dataclasses"})
|
|
9
|
-
ATTRGETTER_FUNCTION_NAME: str = "attrgetter"
|
|
10
|
-
CLASSVAR_ANNOTATION_NAME: str = "ClassVar"
|
|
11
|
-
GETATTR_FUNCTION_NAME: str = "getattr"
|
|
12
|
-
GETATTR_NAME_ARGUMENT_MINIMUM: int = 2
|
|
13
|
-
ALL_REFLECTIVE_FIELD_CONSUMER_NAMES: frozenset[str] = frozenset(
|
|
14
|
-
{"asdict", "astuple", "fields", "replace", "vars"}
|
|
15
|
-
)
|
|
16
|
-
WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME: str = "__dict__"
|
|
17
|
-
ALL_WHOLE_INSTANCE_STRINGIFY_NAMES: frozenset[str] = frozenset(
|
|
18
|
-
{"str", "repr", "format"}
|
|
19
|
-
)
|
|
20
|
-
MAX_DEAD_DATACLASS_FIELD_ISSUES: int = 25
|
|
21
|
-
DEAD_DATACLASS_FIELD_GUIDANCE: str = (
|
|
22
|
-
"field is assigned but never read in this file - remove the field and the code"
|
|
23
|
-
" that only exists to populate it, or read it where the value is needed"
|
|
24
|
-
" (CODE_RULES §9.8)"
|
|
25
|
-
)
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""Constants for the dead module-level constant detector in ``code_rules_enforcer``.
|
|
2
|
-
|
|
3
|
-
Lives under the hooks-tree ``hooks_constants`` package so module-level
|
|
4
|
-
UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
|
|
5
|
-
requirement and share a home with the other hook-tree configuration.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
PYTHON_SOURCE_SUFFIX: str = ".py"
|
|
9
|
-
DUNDER_INIT_FILENAME: str = "__init__.py"
|
|
10
|
-
CONSTANTS_MODULE_SUFFIX: str = "_constants.py"
|
|
11
|
-
CONFIG_DIRECTORY_SEGMENT: str = "config"
|
|
12
|
-
DUNDER_ALL_NAME: str = "__all__"
|
|
13
|
-
GIT_DIRECTORY_NAME: str = ".git"
|
|
14
|
-
MINIMUM_UPPER_SNAKE_LENGTH: int = 2
|
|
15
|
-
MAX_DEAD_MODULE_CONSTANT_ISSUES: int = 25
|
|
16
|
-
MAX_SCAN_ROOT_FILE_COUNT: int = 2000
|
|
17
|
-
MAX_SCAN_ROOT_READ_COUNT: int = 20000
|
|
18
|
-
DEAD_MODULE_CONSTANT_GUIDANCE: str = (
|
|
19
|
-
"module-level constant is defined here but never imported or read by any"
|
|
20
|
-
" module in the enclosing package tree - remove the constant, or reference it"
|
|
21
|
-
" where its value is needed (CODE_RULES §9.8)"
|
|
22
|
-
)
|
|
23
|
-
DEAD_MODULE_CONSTANT_RETRY_GUIDANCE: str = (
|
|
24
|
-
"To land a constant a consumer will read, break the write-order deadlock by"
|
|
25
|
-
" writing the consumer that reads it first: that write completes even though a"
|
|
26
|
-
" transient mypy attr-defined advisory flags the not-yet-defined name (a"
|
|
27
|
-
" non-blocking post-write check), then re-issue this write, which passes once"
|
|
28
|
-
" the consumer is on disk. A constant no module ever reads stays flagged on"
|
|
29
|
-
" every attempt."
|
|
30
|
-
)
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"""Explicit uncertainty labels that co-occur with hedge words in one sentence.
|
|
2
|
-
|
|
3
|
-
::
|
|
4
|
-
|
|
5
|
-
ok: This claim is unverified; the port is probably down.
|
|
6
|
-
flag: The port is probably down. (no label in that sentence)
|
|
7
|
-
|
|
8
|
-
A response-wide "unverified" label does not exempt a bare hedge in another
|
|
9
|
-
sentence. Classification of matcher precision (OP-07B) uses the same blocking
|
|
10
|
-
surface: only hedges that survive this filter count toward hard-block evidence.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import re
|
|
16
|
-
|
|
17
|
-
# Sentence ends at . ! ? when followed by space or end; keep the terminator
|
|
18
|
-
# attached to the sentence so labels next to periods still match.
|
|
19
|
-
SENTENCE_BOUNDARY_PATTERN = re.compile(r"(?<=[.!?])\s+")
|
|
20
|
-
|
|
21
|
-
ALL_EXPLICIT_UNCERTAINTY_LABEL_PATTERNS = (
|
|
22
|
-
re.compile(r"\bunverified\b", re.IGNORECASE),
|
|
23
|
-
re.compile(r"\bi don't know\b", re.IGNORECASE),
|
|
24
|
-
re.compile(r"\bi do not know\b", re.IGNORECASE),
|
|
25
|
-
re.compile(r"\bno source for this claim\b", re.IGNORECASE),
|
|
26
|
-
re.compile(r"\bwithout a (credible )?source\b", re.IGNORECASE),
|
|
27
|
-
re.compile(r"\bnamed (as )?unverified\b", re.IGNORECASE),
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
HEDGING_TERM_LIST_SEPARATOR = ", "
|
|
31
|
-
|
|
32
|
-
POSITIVE_CORRECTIVE_GUIDANCE = (
|
|
33
|
-
"State the claim with evidence that backs it, or label that claim unverified "
|
|
34
|
-
"in the same sentence as the uncertainty word. "
|
|
35
|
-
"Gather a source, run a live probe, or ask the user through AskUserQuestion — "
|
|
36
|
-
"then re-output the complete revised response."
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
BLOCK_REASON_PREFIX = (
|
|
40
|
-
"ANTI-HALLUCINATION GUARDRAIL: Your response contains hedging language without "
|
|
41
|
-
"an explicit uncertainty label in the same sentence: "
|
|
42
|
-
)
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"""Regression tests proving the dispatcher constants docstrings clear the O6 gate.
|
|
2
|
-
|
|
3
|
-
The PreToolUse and PostToolUse dispatcher constants modules each carry a module
|
|
4
|
-
docstring stating what the module centralizes. Neither docstring may assert that
|
|
5
|
-
no literals appear inline in its companion dispatcher script — that completeness
|
|
6
|
-
claim about a sibling file is the deterministic Category O6 drift the enforcer's
|
|
7
|
-
check_docstring_no_inline_literal_claim blocks. These tests load the real
|
|
8
|
-
modules' source and assert the check returns no issues for either.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import importlib.util
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from types import ModuleType
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _load_enforcer_module() -> ModuleType:
|
|
19
|
-
enforcer_path = Path(__file__).resolve().parent.parent / "blocking" / "code_rules_enforcer.py"
|
|
20
|
-
enforcer_spec = importlib.util.spec_from_file_location("code_rules_enforcer", enforcer_path)
|
|
21
|
-
assert enforcer_spec is not None
|
|
22
|
-
assert enforcer_spec.loader is not None
|
|
23
|
-
enforcer_module = importlib.util.module_from_spec(enforcer_spec)
|
|
24
|
-
enforcer_spec.loader.exec_module(enforcer_module)
|
|
25
|
-
return enforcer_module
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
code_rules_enforcer = _load_enforcer_module()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _check_issues_for(module_filename: str) -> list[str]:
|
|
32
|
-
module_path = Path(__file__).resolve().parent / module_filename
|
|
33
|
-
module_source = module_path.read_text(encoding="utf-8")
|
|
34
|
-
return code_rules_enforcer.check_docstring_no_inline_literal_claim(
|
|
35
|
-
module_source, str(module_path)
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_pre_tool_use_dispatcher_constants_docstring_clears_o6_gate() -> None:
|
|
40
|
-
assert _check_issues_for("pre_tool_use_dispatcher_constants.py") == []
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_post_tool_use_dispatcher_constants_docstring_clears_o6_gate() -> None:
|
|
44
|
-
assert _check_issues_for("post_tool_use_dispatcher_constants.py") == []
|