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,425 +0,0 @@
|
|
|
1
|
-
"""Span-scope tests for docstring checks.
|
|
2
|
-
|
|
3
|
-
Covers two surfaces:
|
|
4
|
-
|
|
5
|
-
1. ``check_docstring_args_single_line_scope_vs_span`` (Category O6) — an Args
|
|
6
|
-
entry that claims single-line (block-anchor) scope while the body builds a
|
|
7
|
-
multi-line span and routes it through span intersection.
|
|
8
|
-
|
|
9
|
-
2. Changed-span grading for ``check_docstring_runon_sentence`` and
|
|
10
|
-
``check_docstring_prose_wall_without_illustration`` (issue #237) — far-away
|
|
11
|
-
no-op grandfathering, introduced-violation blocking, and
|
|
12
|
-
``defer_scope_to_caller`` pass-through.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import importlib.util
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from types import ModuleType
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _load_enforcer_module() -> ModuleType:
|
|
23
|
-
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
24
|
-
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
25
|
-
assert spec is not None
|
|
26
|
-
assert spec.loader is not None
|
|
27
|
-
module = importlib.util.module_from_spec(spec)
|
|
28
|
-
spec.loader.exec_module(module)
|
|
29
|
-
return module
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
code_rules_enforcer = _load_enforcer_module()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def check_docstring_args_single_line_scope_vs_span(content: str, file_path: str) -> list[str]:
|
|
36
|
-
return code_rules_enforcer.check_docstring_args_single_line_scope_vs_span(content, file_path)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
PRODUCTION_FILE_PATH = "/project/scripts/check_import_block_sorted.py"
|
|
40
|
-
TEST_FILE_PATH = "/project/scripts/test_check_import_block_sorted.py"
|
|
41
|
-
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
SPAN_SCOPING_BODY = (
|
|
45
|
-
" span_range = range(line_number, block_end_line_number + 1)\n"
|
|
46
|
-
" all_violations.append((span_range, message))\n"
|
|
47
|
-
" return _scope_violations_to_changed_lines(\n"
|
|
48
|
-
" all_violations, all_changed_lines, defer_scope_to_caller\n"
|
|
49
|
-
" )\n"
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_flags_anchor_line_scope_against_span_intersection_body() -> None:
|
|
54
|
-
content = (
|
|
55
|
-
"def check_import_block_sorted(\n"
|
|
56
|
-
" content: str, file_path: str, all_changed_lines: set[int] | None\n"
|
|
57
|
-
") -> list[str]:\n"
|
|
58
|
-
' """Flag an unsorted import block scoped to the changed lines.\n'
|
|
59
|
-
"\n"
|
|
60
|
-
" A finding is returned when any line in that block span is among\n"
|
|
61
|
-
" all_changed_lines.\n"
|
|
62
|
-
"\n"
|
|
63
|
-
" Args:\n"
|
|
64
|
-
" content: The full file content the write would leave on disk.\n"
|
|
65
|
-
" file_path: The destination path used to gate by extension.\n"
|
|
66
|
-
" all_changed_lines: Post-edit line numbers the current edit touched, or\n"
|
|
67
|
-
" None to treat the whole file as in scope. When provided, a finding\n"
|
|
68
|
-
" blocks only when its block-anchor line is among the changed lines.\n"
|
|
69
|
-
"\n"
|
|
70
|
-
" Returns:\n"
|
|
71
|
-
" One issue string per detected finding.\n"
|
|
72
|
-
' """\n' + SPAN_SCOPING_BODY
|
|
73
|
-
)
|
|
74
|
-
issues = check_docstring_args_single_line_scope_vs_span(content, PRODUCTION_FILE_PATH)
|
|
75
|
-
assert len(issues) == 1
|
|
76
|
-
assert "check_import_block_sorted" in issues[0]
|
|
77
|
-
assert "all_changed_lines" in issues[0]
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def test_flags_single_line_scope_against_stepped_range_span_body() -> None:
|
|
81
|
-
content = (
|
|
82
|
-
"def check_import_block_sorted(\n"
|
|
83
|
-
" content: str, file_path: str, all_changed_lines: set[int] | None\n"
|
|
84
|
-
") -> list[str]:\n"
|
|
85
|
-
' """Flag an unsorted import block scoped to the changed lines.\n'
|
|
86
|
-
"\n"
|
|
87
|
-
" Args:\n"
|
|
88
|
-
" content: The full file content the write would leave on disk.\n"
|
|
89
|
-
" file_path: The destination path used to gate by extension.\n"
|
|
90
|
-
" all_changed_lines: Post-edit line numbers, or None. When provided,\n"
|
|
91
|
-
" a finding blocks only when its block-anchor line is among the\n"
|
|
92
|
-
" changed lines.\n"
|
|
93
|
-
"\n"
|
|
94
|
-
" Returns:\n"
|
|
95
|
-
" One issue string per detected finding.\n"
|
|
96
|
-
' """\n'
|
|
97
|
-
" span_range = range(line_number, block_end_line_number + 1, 1)\n"
|
|
98
|
-
" all_violations.append((span_range, message))\n"
|
|
99
|
-
" return _scope_violations_to_changed_lines(\n"
|
|
100
|
-
" all_violations, all_changed_lines, defer_scope_to_caller\n"
|
|
101
|
-
" )\n"
|
|
102
|
-
)
|
|
103
|
-
issues = check_docstring_args_single_line_scope_vs_span(content, PRODUCTION_FILE_PATH)
|
|
104
|
-
assert len(issues) == 1
|
|
105
|
-
assert "check_import_block_sorted" in issues[0]
|
|
106
|
-
assert "all_changed_lines" in issues[0]
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def test_flags_the_line_is_among_phrasing() -> None:
|
|
110
|
-
content = (
|
|
111
|
-
"def check_block(\n"
|
|
112
|
-
" content: str, file_path: str, all_changed_lines: set[int] | None\n"
|
|
113
|
-
") -> list[str]:\n"
|
|
114
|
-
' """Flag an unsorted block scoped to the diff.\n'
|
|
115
|
-
"\n"
|
|
116
|
-
" Args:\n"
|
|
117
|
-
" content: The file content.\n"
|
|
118
|
-
" all_changed_lines: When provided, a finding blocks only when the\n"
|
|
119
|
-
" anchor line is among the changed lines.\n"
|
|
120
|
-
' """\n' + SPAN_SCOPING_BODY
|
|
121
|
-
)
|
|
122
|
-
assert len(check_docstring_args_single_line_scope_vs_span(content, PRODUCTION_FILE_PATH)) == 1
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def test_passes_when_args_says_any_line_of_the_span() -> None:
|
|
126
|
-
content = (
|
|
127
|
-
"def check_import_block_sorted(\n"
|
|
128
|
-
" content: str, file_path: str, all_changed_lines: set[int] | None\n"
|
|
129
|
-
") -> list[str]:\n"
|
|
130
|
-
' """Flag an unsorted import block scoped to the changed lines.\n'
|
|
131
|
-
"\n"
|
|
132
|
-
" Args:\n"
|
|
133
|
-
" content: The full file content.\n"
|
|
134
|
-
" all_changed_lines: Post-edit line numbers the current edit touched, or\n"
|
|
135
|
-
" None to treat the whole file as in scope. When provided, a finding\n"
|
|
136
|
-
" blocks only when any line of its block span is among the changed lines.\n"
|
|
137
|
-
' """\n' + SPAN_SCOPING_BODY
|
|
138
|
-
)
|
|
139
|
-
assert check_docstring_args_single_line_scope_vs_span(content, PRODUCTION_FILE_PATH) == []
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def test_passes_when_body_scopes_by_single_line_not_a_span() -> None:
|
|
143
|
-
content = (
|
|
144
|
-
"def check_one_line(\n"
|
|
145
|
-
" content: str, file_path: str, all_changed_lines: set[int] | None\n"
|
|
146
|
-
") -> list[str]:\n"
|
|
147
|
-
' """Flag a violation on a single line scoped to the diff.\n'
|
|
148
|
-
"\n"
|
|
149
|
-
" Args:\n"
|
|
150
|
-
" content: The file content.\n"
|
|
151
|
-
" all_changed_lines: When provided, a finding blocks only when its\n"
|
|
152
|
-
" anchor line is among the changed lines.\n"
|
|
153
|
-
' """\n'
|
|
154
|
-
" if all_changed_lines is not None and line_number not in all_changed_lines:\n"
|
|
155
|
-
" return []\n"
|
|
156
|
-
" return [message]\n"
|
|
157
|
-
)
|
|
158
|
-
assert check_docstring_args_single_line_scope_vs_span(content, PRODUCTION_FILE_PATH) == []
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def test_passes_when_no_single_line_scope_phrase() -> None:
|
|
162
|
-
content = (
|
|
163
|
-
"def check_import_block_sorted(\n"
|
|
164
|
-
" content: str, file_path: str, all_changed_lines: set[int] | None\n"
|
|
165
|
-
") -> list[str]:\n"
|
|
166
|
-
' """Flag an unsorted import block scoped to the changed lines.\n'
|
|
167
|
-
"\n"
|
|
168
|
-
" Args:\n"
|
|
169
|
-
" content: The full file content.\n"
|
|
170
|
-
" all_changed_lines: Post-edit line numbers the current edit touched.\n"
|
|
171
|
-
' """\n' + SPAN_SCOPING_BODY
|
|
172
|
-
)
|
|
173
|
-
assert check_docstring_args_single_line_scope_vs_span(content, PRODUCTION_FILE_PATH) == []
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def test_test_files_are_exempt() -> None:
|
|
177
|
-
content = (
|
|
178
|
-
"def check_import_block_sorted(\n"
|
|
179
|
-
" content: str, file_path: str, all_changed_lines: set[int] | None\n"
|
|
180
|
-
") -> list[str]:\n"
|
|
181
|
-
' """Flag an unsorted import block.\n'
|
|
182
|
-
"\n"
|
|
183
|
-
" Args:\n"
|
|
184
|
-
" all_changed_lines: A finding blocks only when its anchor line is\n"
|
|
185
|
-
" among the changed lines.\n"
|
|
186
|
-
' """\n' + SPAN_SCOPING_BODY
|
|
187
|
-
)
|
|
188
|
-
assert check_docstring_args_single_line_scope_vs_span(content, TEST_FILE_PATH) == []
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def test_hook_infrastructure_is_not_exempt() -> None:
|
|
192
|
-
content = (
|
|
193
|
-
"def check_import_block_sorted(\n"
|
|
194
|
-
" content: str, file_path: str, all_changed_lines: set[int] | None\n"
|
|
195
|
-
") -> list[str]:\n"
|
|
196
|
-
' """Flag an unsorted import block.\n'
|
|
197
|
-
"\n"
|
|
198
|
-
" Args:\n"
|
|
199
|
-
" all_changed_lines: A finding blocks only when its block-anchor line is\n"
|
|
200
|
-
" among the changed lines.\n"
|
|
201
|
-
' """\n' + SPAN_SCOPING_BODY
|
|
202
|
-
)
|
|
203
|
-
assert (
|
|
204
|
-
len(check_docstring_args_single_line_scope_vs_span(content, HOOK_INFRASTRUCTURE_PATH)) == 1
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
# ---------------------------------------------------------------------------
|
|
208
|
-
# Changed-span grading for the two whole-file docstring checks (issue #237 / P-41)
|
|
209
|
-
# check_docstring_runon_sentence and check_docstring_prose_wall_without_illustration
|
|
210
|
-
# ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def check_docstring_runon_sentence(
|
|
214
|
-
content: str,
|
|
215
|
-
file_path: str,
|
|
216
|
-
all_changed_lines: set[int] | None = None,
|
|
217
|
-
defer_scope_to_caller: bool = False,
|
|
218
|
-
) -> list[str]:
|
|
219
|
-
return code_rules_enforcer.check_docstring_runon_sentence(
|
|
220
|
-
content, file_path, all_changed_lines, defer_scope_to_caller
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def check_docstring_prose_wall_without_illustration(
|
|
225
|
-
content: str,
|
|
226
|
-
file_path: str,
|
|
227
|
-
all_changed_lines: set[int] | None = None,
|
|
228
|
-
defer_scope_to_caller: bool = False,
|
|
229
|
-
) -> list[str]:
|
|
230
|
-
return code_rules_enforcer.check_docstring_prose_wall_without_illustration(
|
|
231
|
-
content, file_path, all_changed_lines, defer_scope_to_caller
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
_CLEAN_HELPER_TAIL = (
|
|
236
|
-
"\n"
|
|
237
|
-
"def clean_helper() -> str:\n"
|
|
238
|
-
' """Return a short status token for the board.\n'
|
|
239
|
-
"\n"
|
|
240
|
-
" Each call names one vessel and its final port.\n"
|
|
241
|
-
' """\n'
|
|
242
|
-
' return "ok"\n'
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def _module_with_far_away_runon_and_clean_helper() -> str:
|
|
247
|
-
return (
|
|
248
|
-
'"""Owns the SIGINT install/restore/installability check, the atexit terminal-record\n'
|
|
249
|
-
"registration, and the interrupted-run finalizer — the non-promoter-specific\n"
|
|
250
|
-
"machinery that brackets a run so the JSONL artifact always carries a terminal\n"
|
|
251
|
-
"record and an in-flight theme record on interrupt.\n"
|
|
252
|
-
'"""\n'
|
|
253
|
-
+ _CLEAN_HELPER_TAIL
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def _module_with_far_away_prose_wall_and_clean_helper() -> str:
|
|
258
|
-
return (
|
|
259
|
-
'"""Assemble the nightly voyage tally from the harbor scans.\n'
|
|
260
|
-
"\n"
|
|
261
|
-
"A scan names one vessel and where it dropped anchor.\n"
|
|
262
|
-
"The tally walks the scans in arrival order and keeps that order.\n"
|
|
263
|
-
"A calm voyage ends well for every vessel it carried.\n"
|
|
264
|
-
"A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
265
|
-
"A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
266
|
-
"The tally groups the vessels by their final port for the harbor.\n"
|
|
267
|
-
"The harbor reads the tally and sees every arrival at a glance.\n"
|
|
268
|
-
'"""\n'
|
|
269
|
-
+ _CLEAN_HELPER_TAIL
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def test_runon_far_away_no_op_is_grandfathered() -> None:
|
|
274
|
-
content = _module_with_far_away_runon_and_clean_helper()
|
|
275
|
-
clean_helper_line = content.splitlines().index("def clean_helper() -> str:") + 1
|
|
276
|
-
all_changed_lines = set(range(clean_helper_line, clean_helper_line + 6))
|
|
277
|
-
unscoped = check_docstring_runon_sentence(content, PRODUCTION_FILE_PATH)
|
|
278
|
-
assert any("run-on" in each for each in unscoped), (
|
|
279
|
-
f"control: unscoped must still see the far-away run-on, got {unscoped!r}"
|
|
280
|
-
)
|
|
281
|
-
scoped = check_docstring_runon_sentence(
|
|
282
|
-
content, PRODUCTION_FILE_PATH, all_changed_lines=all_changed_lines
|
|
283
|
-
)
|
|
284
|
-
assert scoped == [], (
|
|
285
|
-
f"far-away run-on must be grandfathered on a clean-helper edit, got {scoped!r}"
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def test_runon_introduced_violation_blocks() -> None:
|
|
290
|
-
content = _module_with_far_away_runon_and_clean_helper()
|
|
291
|
-
unscoped = check_docstring_runon_sentence(content, PRODUCTION_FILE_PATH)
|
|
292
|
-
assert any("run-on" in each for each in unscoped)
|
|
293
|
-
scoped = check_docstring_runon_sentence(
|
|
294
|
-
content, PRODUCTION_FILE_PATH, all_changed_lines={1}
|
|
295
|
-
)
|
|
296
|
-
assert any("run-on" in each for each in scoped), (
|
|
297
|
-
f"introduced/touched run-on must block, got {scoped!r}"
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def test_prose_wall_far_away_no_op_is_grandfathered() -> None:
|
|
302
|
-
content = _module_with_far_away_prose_wall_and_clean_helper()
|
|
303
|
-
clean_helper_line = content.splitlines().index("def clean_helper() -> str:") + 1
|
|
304
|
-
all_changed_lines = set(range(clean_helper_line, clean_helper_line + 6))
|
|
305
|
-
unscoped = check_docstring_prose_wall_without_illustration(
|
|
306
|
-
content, PRODUCTION_FILE_PATH
|
|
307
|
-
)
|
|
308
|
-
assert any("worked example" in each for each in unscoped), (
|
|
309
|
-
f"control: unscoped must still see the far-away wall, got {unscoped!r}"
|
|
310
|
-
)
|
|
311
|
-
scoped = check_docstring_prose_wall_without_illustration(
|
|
312
|
-
content, PRODUCTION_FILE_PATH, all_changed_lines=all_changed_lines
|
|
313
|
-
)
|
|
314
|
-
assert scoped == [], (
|
|
315
|
-
f"far-away wall must be grandfathered on a clean-helper edit, got {scoped!r}"
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def test_prose_wall_introduced_violation_blocks() -> None:
|
|
320
|
-
content = _module_with_far_away_prose_wall_and_clean_helper()
|
|
321
|
-
unscoped = check_docstring_prose_wall_without_illustration(
|
|
322
|
-
content, PRODUCTION_FILE_PATH
|
|
323
|
-
)
|
|
324
|
-
assert any("worked example" in each for each in unscoped)
|
|
325
|
-
scoped = check_docstring_prose_wall_without_illustration(
|
|
326
|
-
content, PRODUCTION_FILE_PATH, all_changed_lines={1}
|
|
327
|
-
)
|
|
328
|
-
assert any("worked example" in each for each in scoped), (
|
|
329
|
-
f"introduced/touched wall must block, got {scoped!r}"
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
def test_runon_defer_scope_returns_all_violations() -> None:
|
|
334
|
-
content = _module_with_far_away_runon_and_clean_helper()
|
|
335
|
-
deferred = check_docstring_runon_sentence(
|
|
336
|
-
content,
|
|
337
|
-
PRODUCTION_FILE_PATH,
|
|
338
|
-
all_changed_lines={999},
|
|
339
|
-
defer_scope_to_caller=True,
|
|
340
|
-
)
|
|
341
|
-
assert any("run-on" in each for each in deferred)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def _function_with_runon_docstring() -> str:
|
|
345
|
-
return (
|
|
346
|
-
"def clean_helper() -> str:\n"
|
|
347
|
-
' """Owns the SIGINT install/restore/installability check, the atexit terminal-record\n'
|
|
348
|
-
" registration, and the interrupted-run finalizer — the non-promoter-specific\n"
|
|
349
|
-
" machinery that brackets a run so the JSONL artifact always carries a terminal\n"
|
|
350
|
-
" record and an in-flight theme record on interrupt.\n"
|
|
351
|
-
' """\n'
|
|
352
|
-
' return "ok"\n'
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
def test_runon_docstring_body_edit_blocks_without_def_line() -> None:
|
|
357
|
-
content = _function_with_runon_docstring()
|
|
358
|
-
unscoped = check_docstring_runon_sentence(content, PRODUCTION_FILE_PATH)
|
|
359
|
-
assert any("run-on" in each for each in unscoped), (
|
|
360
|
-
f"control: unscoped must see the function run-on, got {unscoped!r}"
|
|
361
|
-
)
|
|
362
|
-
docstring_body_lines = {2, 3, 4, 5}
|
|
363
|
-
scoped = check_docstring_runon_sentence(
|
|
364
|
-
content, PRODUCTION_FILE_PATH, all_changed_lines=docstring_body_lines
|
|
365
|
-
)
|
|
366
|
-
assert any("run-on" in each for each in scoped), (
|
|
367
|
-
f"docstring-body edit must block even when the def line is untouched, got {scoped!r}"
|
|
368
|
-
)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
def test_runon_post_docstring_body_edit_is_grandfathered() -> None:
|
|
372
|
-
content = _function_with_runon_docstring()
|
|
373
|
-
return_line = content.splitlines().index(' return "ok"') + 1
|
|
374
|
-
scoped = check_docstring_runon_sentence(
|
|
375
|
-
content, PRODUCTION_FILE_PATH, all_changed_lines={return_line}
|
|
376
|
-
)
|
|
377
|
-
assert scoped == [], (
|
|
378
|
-
f"post-docstring body edit must grandfather the pre-existing run-on, got {scoped!r}"
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def _function_with_prose_wall_docstring() -> str:
|
|
383
|
-
return (
|
|
384
|
-
"def clean_helper() -> str:\n"
|
|
385
|
-
' """Assemble the nightly voyage tally from the harbor scans.\n'
|
|
386
|
-
"\n"
|
|
387
|
-
" A scan names one vessel and where it dropped anchor.\n"
|
|
388
|
-
" The tally walks the scans in arrival order and keeps that order.\n"
|
|
389
|
-
" A calm voyage ends well for every vessel it carried.\n"
|
|
390
|
-
" A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
391
|
-
" A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
392
|
-
" The tally groups the vessels by their final port for the harbor.\n"
|
|
393
|
-
" The harbor reads the tally and sees every arrival at a glance.\n"
|
|
394
|
-
' """\n'
|
|
395
|
-
' return "ok"\n'
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
def test_prose_wall_docstring_body_edit_blocks_without_def_line() -> None:
|
|
400
|
-
content = _function_with_prose_wall_docstring()
|
|
401
|
-
unscoped = check_docstring_prose_wall_without_illustration(
|
|
402
|
-
content, PRODUCTION_FILE_PATH
|
|
403
|
-
)
|
|
404
|
-
assert any("worked example" in each for each in unscoped), (
|
|
405
|
-
f"control: unscoped must see the function wall, got {unscoped!r}"
|
|
406
|
-
)
|
|
407
|
-
docstring_body_lines = set(range(2, 12))
|
|
408
|
-
scoped = check_docstring_prose_wall_without_illustration(
|
|
409
|
-
content, PRODUCTION_FILE_PATH, all_changed_lines=docstring_body_lines
|
|
410
|
-
)
|
|
411
|
-
assert any("worked example" in each for each in scoped), (
|
|
412
|
-
f"docstring-body edit must block even when the def line is untouched, got {scoped!r}"
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def test_prose_wall_defer_scope_returns_all_violations() -> None:
|
|
417
|
-
content = _module_with_far_away_prose_wall_and_clean_helper()
|
|
418
|
-
deferred = check_docstring_prose_wall_without_illustration(
|
|
419
|
-
content,
|
|
420
|
-
PRODUCTION_FILE_PATH,
|
|
421
|
-
all_changed_lines={999},
|
|
422
|
-
defer_scope_to_caller=True,
|
|
423
|
-
)
|
|
424
|
-
assert any("worked example" in each for each in deferred)
|
|
425
|
-
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
"""Tests for check_docstring_cardinal_count_matches_constant_family — O6 drift.
|
|
2
|
-
|
|
3
|
-
A docstring states a cardinal count of an outcome family ("Covers the four
|
|
4
|
-
outcome branches: ...") and enumerates the family members in prose, while the
|
|
5
|
-
module references more members of that constant family than the count names. The
|
|
6
|
-
prose under-describes the code: a reader trusts "four" and the list to be the
|
|
7
|
-
full set, but the module imports and exercises a fifth outcome. This is the
|
|
8
|
-
deterministic cardinal-count slice of Category O6 docstring-prose-vs-
|
|
9
|
-
implementation drift, the shape that appears when a producer adds an outcome
|
|
10
|
-
branch but leaves the test-module summary at the old count. The check runs on
|
|
11
|
-
test files because the drift class lives in a test-module docstring.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import importlib.util
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from types import ModuleType
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _load_enforcer_module() -> ModuleType:
|
|
22
|
-
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
23
|
-
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
24
|
-
assert spec is not None
|
|
25
|
-
assert spec.loader is not None
|
|
26
|
-
module = importlib.util.module_from_spec(spec)
|
|
27
|
-
spec.loader.exec_module(module)
|
|
28
|
-
return module
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
code_rules_enforcer = _load_enforcer_module()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def check_docstring_cardinal_count_matches_constant_family(
|
|
35
|
-
content: str, file_path: str
|
|
36
|
-
) -> list[str]:
|
|
37
|
-
return code_rules_enforcer.check_docstring_cardinal_count_matches_constant_family(
|
|
38
|
-
content, file_path
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
43
|
-
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
PRODUCTION_FILE_PATH = "/project/src/dropdown_fix_rule.py"
|
|
47
|
-
TEST_FILE_PATH = "/project/src/tests/test_dropdown_fix_rule.py"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _test_module(summary_line: str, enumeration_line: str) -> str:
|
|
51
|
-
return (
|
|
52
|
-
'"""Behavioral tests for classify_dropdown_fix.\n'
|
|
53
|
-
"\n"
|
|
54
|
-
f"{summary_line}\n"
|
|
55
|
-
f"{enumeration_line}\n"
|
|
56
|
-
'"""\n'
|
|
57
|
-
"\n"
|
|
58
|
-
"from stp_dropdown_color_fix.config.status_codes import (\n"
|
|
59
|
-
" OUTCOME_ALREADY_OK,\n"
|
|
60
|
-
" OUTCOME_OFFENDER_LOW_CONTRAST,\n"
|
|
61
|
-
" OUTCOME_OFFENDER_MISSING,\n"
|
|
62
|
-
" OUTCOME_OFFENDER_UNREADABLE,\n"
|
|
63
|
-
" OUTCOME_SKIPPED_NO_SAFE_SOURCE,\n"
|
|
64
|
-
")\n"
|
|
65
|
-
"\n"
|
|
66
|
-
"def test_skipped() -> None:\n"
|
|
67
|
-
" assert classify() == OUTCOME_SKIPPED_NO_SAFE_SOURCE\n"
|
|
68
|
-
"\n"
|
|
69
|
-
"def test_missing() -> None:\n"
|
|
70
|
-
" assert classify() == OUTCOME_OFFENDER_MISSING\n"
|
|
71
|
-
"\n"
|
|
72
|
-
"def test_low_contrast() -> None:\n"
|
|
73
|
-
" assert classify() == OUTCOME_OFFENDER_LOW_CONTRAST\n"
|
|
74
|
-
"\n"
|
|
75
|
-
"def test_already_ok() -> None:\n"
|
|
76
|
-
" assert classify() == OUTCOME_ALREADY_OK\n"
|
|
77
|
-
"\n"
|
|
78
|
-
"def test_unreadable() -> None:\n"
|
|
79
|
-
" assert classify() == OUTCOME_OFFENDER_UNREADABLE\n"
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _drifted_module() -> str:
|
|
84
|
-
return _test_module(
|
|
85
|
-
"Covers the four outcome branches: skipped_no_safe_source,",
|
|
86
|
-
"offender_missing (case A), offender_low_contrast (case B), already_ok.",
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _matching_module() -> str:
|
|
91
|
-
return _test_module(
|
|
92
|
-
"Covers the five outcome branches: skipped_no_safe_source, offender_missing,",
|
|
93
|
-
"offender_low_contrast, already_ok, offender_unreadable.",
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def test_should_flag_test_module_omitting_an_outcome() -> None:
|
|
98
|
-
issues = check_docstring_cardinal_count_matches_constant_family(
|
|
99
|
-
_drifted_module(), TEST_FILE_PATH
|
|
100
|
-
)
|
|
101
|
-
assert any("offender_unreadable" in each for each in issues), (
|
|
102
|
-
"A test-module docstring claiming four outcome branches while the module "
|
|
103
|
-
f"references five OUTCOME_ constants must be flagged, got: {issues!r}"
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def test_should_report_category_o6_in_the_message() -> None:
|
|
108
|
-
issues = check_docstring_cardinal_count_matches_constant_family(
|
|
109
|
-
_drifted_module(), TEST_FILE_PATH
|
|
110
|
-
)
|
|
111
|
-
assert any("O6" in each for each in issues), (
|
|
112
|
-
f"Expected the Category O6 label in the message, got: {issues!r}"
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_should_also_flag_production_module() -> None:
|
|
117
|
-
issues = check_docstring_cardinal_count_matches_constant_family(
|
|
118
|
-
_drifted_module(), PRODUCTION_FILE_PATH
|
|
119
|
-
)
|
|
120
|
-
assert issues != [], (
|
|
121
|
-
f"The cardinal drift must be flagged on a production file too, got: {issues!r}"
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def test_should_not_flag_when_count_and_enumeration_match_family() -> None:
|
|
126
|
-
issues = check_docstring_cardinal_count_matches_constant_family(
|
|
127
|
-
_matching_module(), TEST_FILE_PATH
|
|
128
|
-
)
|
|
129
|
-
assert issues == [], (
|
|
130
|
-
f"A docstring naming all five referenced outcomes must not be flagged, got: {issues!r}"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def test_should_not_flag_without_a_cardinal_phrase() -> None:
|
|
135
|
-
source = _test_module(
|
|
136
|
-
"Covers these outcome branches: skipped_no_safe_source,",
|
|
137
|
-
"offender_missing, offender_low_contrast, already_ok.",
|
|
138
|
-
)
|
|
139
|
-
issues = check_docstring_cardinal_count_matches_constant_family(source, TEST_FILE_PATH)
|
|
140
|
-
assert issues == [], (
|
|
141
|
-
f"An enumeration with no cardinal count word must not bind, got: {issues!r}"
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def test_should_not_flag_single_passing_mention() -> None:
|
|
146
|
-
source = _test_module(
|
|
147
|
-
"Asserts the four guard paths reach already_ok when the dropdown is",
|
|
148
|
-
"absent and the dialer keeps its painted swatch.",
|
|
149
|
-
)
|
|
150
|
-
issues = check_docstring_cardinal_count_matches_constant_family(source, TEST_FILE_PATH)
|
|
151
|
-
assert issues == [], f"A single overlapping token must not bind the family, got: {issues!r}"
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def test_should_not_flag_when_family_has_one_referenced_member() -> None:
|
|
155
|
-
source = (
|
|
156
|
-
'"""Covers the four outcome branches: already_ok, more, words, here."""\n'
|
|
157
|
-
"from status_codes import OUTCOME_ALREADY_OK\n"
|
|
158
|
-
"\n"
|
|
159
|
-
"def test_only() -> None:\n"
|
|
160
|
-
" assert classify() == OUTCOME_ALREADY_OK\n"
|
|
161
|
-
)
|
|
162
|
-
issues = check_docstring_cardinal_count_matches_constant_family(source, TEST_FILE_PATH)
|
|
163
|
-
assert issues == [], f"A family with one referenced member must not bind, got: {issues!r}"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def test_should_handle_syntax_error_gracefully() -> None:
|
|
167
|
-
issues = check_docstring_cardinal_count_matches_constant_family("def fetch(\n", TEST_FILE_PATH)
|
|
168
|
-
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def test_validate_content_surfaces_cardinal_family_drift() -> None:
|
|
172
|
-
issues = validate_content(_drifted_module(), TEST_FILE_PATH, old_content="")
|
|
173
|
-
matching_issues = [each for each in issues if "offender_unreadable" in each and "O6" in each]
|
|
174
|
-
assert matching_issues, (
|
|
175
|
-
f"Expected validate_content to surface the O6 cardinal-family drift, got: {issues!r}"
|
|
176
|
-
)
|