claude-dev-env 2.27.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/hitl/SKILL.md +45 -0
- 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/bin/ever-shipped-skills.mjs +1 -0
- 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 +99 -24
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/tests/test_codex_compat_materializer.py +70 -19
- 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,226 +0,0 @@
|
|
|
1
|
-
"""Tests for check_docstring_raises_unraisable_largezipfile — O6 Raises drift.
|
|
2
|
-
|
|
3
|
-
A function's docstring Raises clause lists ``zipfile.LargeZipFile`` while the
|
|
4
|
-
function opens its ``zipfile.ZipFile`` writer with ZIP64 permitted (``allowZip64``
|
|
5
|
-
left at its default of True). The stdlib raises ``LargeZipFile`` only when an
|
|
6
|
-
entry needs ZIP64 AND ``allowZip64`` is False; with ZIP64 permitted the writer
|
|
7
|
-
transparently uses it and never raises. The Raises entry therefore documents an
|
|
8
|
-
exception the body cannot produce — the deterministic slice of Category O6
|
|
9
|
-
docstring-prose-vs-implementation drift where a writer-opened-with-default-ZIP64
|
|
10
|
-
disagrees with a LargeZipFile Raises clause.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import importlib.util
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from types import ModuleType
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _load_enforcer_module() -> ModuleType:
|
|
21
|
-
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
22
|
-
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
23
|
-
assert spec is not None
|
|
24
|
-
assert spec.loader is not None
|
|
25
|
-
module = importlib.util.module_from_spec(spec)
|
|
26
|
-
spec.loader.exec_module(module)
|
|
27
|
-
return module
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
code_rules_enforcer = _load_enforcer_module()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def check_docstring_raises_unraisable_largezipfile(content: str, file_path: str) -> list[str]:
|
|
34
|
-
return code_rules_enforcer.check_docstring_raises_unraisable_largezipfile(content, file_path)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
38
|
-
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
PRODUCTION_FILE_PATH = "/project/src/stp_archive.py"
|
|
42
|
-
TEST_FILE_PATH = "/project/src/test_stp_archive.py"
|
|
43
|
-
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _default_zip64_writer_documenting_largezipfile() -> str:
|
|
47
|
-
return (
|
|
48
|
-
"import zipfile\n"
|
|
49
|
-
"from pathlib import Path\n"
|
|
50
|
-
"\n"
|
|
51
|
-
"def rewrite_stp_member_atomically(stp_path: Path, member_bytes: bytes) -> None:\n"
|
|
52
|
-
' """Rewrite one member of an STP in place via a temp sibling.\n'
|
|
53
|
-
"\n"
|
|
54
|
-
" Raises:\n"
|
|
55
|
-
" OSError: When the streaming write or the rename fails.\n"
|
|
56
|
-
" zipfile.BadZipFile: When the source archive is not a valid ZIP.\n"
|
|
57
|
-
" zipfile.LargeZipFile: When an entry needs ZIP64 the writer forbids.\n"
|
|
58
|
-
' """\n'
|
|
59
|
-
" with zipfile.ZipFile(stp_path, 'r') as source_archive:\n"
|
|
60
|
-
" with zipfile.ZipFile(stp_path, 'w', zipfile.ZIP_DEFLATED) as target_archive:\n"
|
|
61
|
-
" target_archive.writestr('IM/a.png', member_bytes)\n"
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_should_flag_largezipfile_over_a_default_zip64_writer() -> None:
|
|
66
|
-
issues = check_docstring_raises_unraisable_largezipfile(
|
|
67
|
-
_default_zip64_writer_documenting_largezipfile(), PRODUCTION_FILE_PATH
|
|
68
|
-
)
|
|
69
|
-
assert any("LargeZipFile" in each for each in issues), (
|
|
70
|
-
f"A LargeZipFile Raises entry over a default-ZIP64 writer must flag, got: {issues!r}"
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_should_report_category_o6_in_the_message() -> None:
|
|
75
|
-
issues = check_docstring_raises_unraisable_largezipfile(
|
|
76
|
-
_default_zip64_writer_documenting_largezipfile(), PRODUCTION_FILE_PATH
|
|
77
|
-
)
|
|
78
|
-
assert any("O6" in each for each in issues), (
|
|
79
|
-
f"Expected the Category O6 label in the message, got: {issues!r}"
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_should_not_flag_when_writer_forbids_zip64_via_keyword() -> None:
|
|
84
|
-
source = (
|
|
85
|
-
"import zipfile\n"
|
|
86
|
-
"from pathlib import Path\n"
|
|
87
|
-
"\n"
|
|
88
|
-
"def rewrite(stp_path: Path, member_bytes: bytes) -> None:\n"
|
|
89
|
-
' """Rewrite one member of an STP, forbidding ZIP64.\n'
|
|
90
|
-
"\n"
|
|
91
|
-
" Raises:\n"
|
|
92
|
-
" zipfile.LargeZipFile: When an entry needs ZIP64 the writer forbids.\n"
|
|
93
|
-
' """\n'
|
|
94
|
-
" with zipfile.ZipFile(stp_path, 'w', allowZip64=False) as target_archive:\n"
|
|
95
|
-
" target_archive.writestr('IM/a.png', member_bytes)\n"
|
|
96
|
-
)
|
|
97
|
-
issues = check_docstring_raises_unraisable_largezipfile(source, PRODUCTION_FILE_PATH)
|
|
98
|
-
assert issues == [], (
|
|
99
|
-
f"A writer that forbids ZIP64 can raise LargeZipFile, so the clause is valid, got: {issues!r}"
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def test_should_not_flag_when_writer_forbids_zip64_positionally() -> None:
|
|
104
|
-
source = (
|
|
105
|
-
"import zipfile\n"
|
|
106
|
-
"from pathlib import Path\n"
|
|
107
|
-
"\n"
|
|
108
|
-
"def rewrite(stp_path: Path, member_bytes: bytes) -> None:\n"
|
|
109
|
-
' """Rewrite one member of an STP, forbidding ZIP64.\n'
|
|
110
|
-
"\n"
|
|
111
|
-
" Raises:\n"
|
|
112
|
-
" zipfile.LargeZipFile: When an entry needs ZIP64 the writer forbids.\n"
|
|
113
|
-
' """\n'
|
|
114
|
-
" with zipfile.ZipFile(stp_path, 'w', zipfile.ZIP_DEFLATED, False) as target_archive:\n"
|
|
115
|
-
" target_archive.writestr('IM/a.png', member_bytes)\n"
|
|
116
|
-
)
|
|
117
|
-
issues = check_docstring_raises_unraisable_largezipfile(source, PRODUCTION_FILE_PATH)
|
|
118
|
-
assert issues == [], f"A positional allowZip64=False keeps the clause valid, got: {issues!r}"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def test_should_not_flag_when_function_opens_no_zip_writer() -> None:
|
|
122
|
-
source = (
|
|
123
|
-
"import zipfile\n"
|
|
124
|
-
"from pathlib import Path\n"
|
|
125
|
-
"\n"
|
|
126
|
-
"def patch_via_helper(stp_path: Path, member_bytes: bytes) -> None:\n"
|
|
127
|
-
' """Patch one member through a helper writer.\n'
|
|
128
|
-
"\n"
|
|
129
|
-
" Raises:\n"
|
|
130
|
-
" zipfile.LargeZipFile: When the helper writer forbids ZIP64.\n"
|
|
131
|
-
' """\n'
|
|
132
|
-
" write_member(stp_path, member_bytes)\n"
|
|
133
|
-
)
|
|
134
|
-
issues = check_docstring_raises_unraisable_largezipfile(source, PRODUCTION_FILE_PATH)
|
|
135
|
-
assert issues == [], (
|
|
136
|
-
f"With no visible writer the exception may propagate from a callee, got: {issues!r}"
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def test_should_not_flag_read_only_open() -> None:
|
|
141
|
-
source = (
|
|
142
|
-
"import zipfile\n"
|
|
143
|
-
"from pathlib import Path\n"
|
|
144
|
-
"\n"
|
|
145
|
-
"def read_member(stp_path: Path) -> bytes:\n"
|
|
146
|
-
' """Read one member of an STP.\n'
|
|
147
|
-
"\n"
|
|
148
|
-
" Raises:\n"
|
|
149
|
-
" zipfile.LargeZipFile: When an entry needs ZIP64 the writer forbids.\n"
|
|
150
|
-
' """\n'
|
|
151
|
-
" with zipfile.ZipFile(stp_path) as source_archive:\n"
|
|
152
|
-
" return source_archive.read('IM/a.png')\n"
|
|
153
|
-
)
|
|
154
|
-
issues = check_docstring_raises_unraisable_largezipfile(source, PRODUCTION_FILE_PATH)
|
|
155
|
-
assert issues == [], (
|
|
156
|
-
f"A read-only open has no write-mode writer, so it is left alone, got: {issues!r}"
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def test_should_not_flag_when_raises_omits_largezipfile() -> None:
|
|
161
|
-
source = (
|
|
162
|
-
"import zipfile\n"
|
|
163
|
-
"from pathlib import Path\n"
|
|
164
|
-
"\n"
|
|
165
|
-
"def rewrite(stp_path: Path, member_bytes: bytes) -> None:\n"
|
|
166
|
-
' """Rewrite one member of an STP in place.\n'
|
|
167
|
-
"\n"
|
|
168
|
-
" Raises:\n"
|
|
169
|
-
" OSError: When the streaming write or the rename fails.\n"
|
|
170
|
-
' """\n'
|
|
171
|
-
" with zipfile.ZipFile(stp_path, 'w', zipfile.ZIP_DEFLATED) as target_archive:\n"
|
|
172
|
-
" target_archive.writestr('IM/a.png', member_bytes)\n"
|
|
173
|
-
)
|
|
174
|
-
issues = check_docstring_raises_unraisable_largezipfile(source, PRODUCTION_FILE_PATH)
|
|
175
|
-
assert issues == [], f"A Raises clause without LargeZipFile must not flag, got: {issues!r}"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def test_should_flag_when_mode_passed_by_keyword() -> None:
|
|
179
|
-
source = (
|
|
180
|
-
"import zipfile\n"
|
|
181
|
-
"from pathlib import Path\n"
|
|
182
|
-
"\n"
|
|
183
|
-
"def rewrite(stp_path: Path, member_bytes: bytes) -> None:\n"
|
|
184
|
-
' """Rewrite one member of an STP in place.\n'
|
|
185
|
-
"\n"
|
|
186
|
-
" Raises:\n"
|
|
187
|
-
" zipfile.LargeZipFile: When an entry needs ZIP64 the writer forbids.\n"
|
|
188
|
-
' """\n'
|
|
189
|
-
" with zipfile.ZipFile(stp_path, mode='w') as target_archive:\n"
|
|
190
|
-
" target_archive.writestr('IM/a.png', member_bytes)\n"
|
|
191
|
-
)
|
|
192
|
-
issues = check_docstring_raises_unraisable_largezipfile(source, PRODUCTION_FILE_PATH)
|
|
193
|
-
assert any("LargeZipFile" in each for each in issues), (
|
|
194
|
-
f"A keyword mode='w' writer with default ZIP64 must flag, got: {issues!r}"
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def test_should_skip_test_file() -> None:
|
|
199
|
-
issues = check_docstring_raises_unraisable_largezipfile(
|
|
200
|
-
_default_zip64_writer_documenting_largezipfile(), TEST_FILE_PATH
|
|
201
|
-
)
|
|
202
|
-
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def test_should_skip_hook_infrastructure() -> None:
|
|
206
|
-
issues = check_docstring_raises_unraisable_largezipfile(
|
|
207
|
-
_default_zip64_writer_documenting_largezipfile(), HOOK_INFRASTRUCTURE_PATH
|
|
208
|
-
)
|
|
209
|
-
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def test_should_handle_syntax_error_gracefully() -> None:
|
|
213
|
-
issues = check_docstring_raises_unraisable_largezipfile("def rewrite(\n", PRODUCTION_FILE_PATH)
|
|
214
|
-
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def test_validate_content_surfaces_largezipfile_drift() -> None:
|
|
218
|
-
issues = validate_content(
|
|
219
|
-
_default_zip64_writer_documenting_largezipfile(),
|
|
220
|
-
PRODUCTION_FILE_PATH,
|
|
221
|
-
old_content="",
|
|
222
|
-
)
|
|
223
|
-
matching_issues = [each for each in issues if "LargeZipFile" in each and "O6" in each]
|
|
224
|
-
assert matching_issues, (
|
|
225
|
-
f"Expected validate_content to surface the O6 LargeZipFile drift, got: {issues!r}"
|
|
226
|
-
)
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"""Tests for check_docstring_returns_plural_cardinality — O6 plural-stop drift.
|
|
2
|
-
|
|
3
|
-
A function returns a dict literal whose keys carry prefix families, and its
|
|
4
|
-
Returns clause names one such family with a plural noun ("the sheen stops")
|
|
5
|
-
while only one key in that family exists ("sheen_mid"). The plural prose claims
|
|
6
|
-
two or more entries the dict no longer holds. This is the deterministic
|
|
7
|
-
single-key slice of Category O6 docstring-prose-vs-implementation drift, the
|
|
8
|
-
shape that appears when a producer removes the second key in a family but leaves
|
|
9
|
-
the plural prose untouched.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import importlib.util
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from types import ModuleType
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _load_enforcer_module() -> ModuleType:
|
|
20
|
-
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
21
|
-
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
22
|
-
assert spec is not None
|
|
23
|
-
assert spec.loader is not None
|
|
24
|
-
module = importlib.util.module_from_spec(spec)
|
|
25
|
-
spec.loader.exec_module(module)
|
|
26
|
-
return module
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
code_rules_enforcer = _load_enforcer_module()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def check_docstring_returns_plural_cardinality(content: str, file_path: str) -> list[str]:
|
|
33
|
-
return code_rules_enforcer.check_docstring_returns_plural_cardinality(content, file_path)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
37
|
-
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
PRODUCTION_FILE_PATH = "/project/src/phone_handset.py"
|
|
41
|
-
TEST_FILE_PATH = "/project/src/test_phone_handset.py"
|
|
42
|
-
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _drifted_single_sheen_function() -> str:
|
|
46
|
-
return (
|
|
47
|
-
"def _palette_substitutions(colors: PhoneHandsetColors) -> dict[str, str]:\n"
|
|
48
|
-
' """Derive every SVG color field from the three theme hero colors.\n'
|
|
49
|
-
"\n"
|
|
50
|
-
" Returns:\n"
|
|
51
|
-
" The color substitution fields the SVG template fills: the body form\n"
|
|
52
|
-
" stops (highlight, mid, shadow, inner-shadow), the sheen stops, the rim\n"
|
|
53
|
-
" highlight, the depth, and the specular core that lights the gloss.\n"
|
|
54
|
-
' """\n'
|
|
55
|
-
" return {\n"
|
|
56
|
-
' "body_highlight": derive_highlight(colors.body),\n'
|
|
57
|
-
' "body_mid": colors.body,\n'
|
|
58
|
-
' "body_shadow": derive_shadow(colors.body),\n'
|
|
59
|
-
' "body_inner_shadow": derive_inner(colors.body),\n'
|
|
60
|
-
' "sheen_mid": colors.sheen,\n'
|
|
61
|
-
' "rim_highlight": derive_rim(colors.sheen),\n'
|
|
62
|
-
' "specular_core": derive_specular(colors.sheen),\n'
|
|
63
|
-
' "depth_mid": colors.depth,\n'
|
|
64
|
-
" }\n"
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _singular_sheen_function() -> str:
|
|
69
|
-
return (
|
|
70
|
-
"def _palette_substitutions(colors: PhoneHandsetColors) -> dict[str, str]:\n"
|
|
71
|
-
' """Derive every SVG color field from the three theme hero colors.\n'
|
|
72
|
-
"\n"
|
|
73
|
-
" Returns:\n"
|
|
74
|
-
" The color substitution fields the SVG template fills: the body form\n"
|
|
75
|
-
" stops (highlight, mid, shadow, inner-shadow), the sheen stop, the rim\n"
|
|
76
|
-
" highlight, the depth, and the specular core that lights the gloss.\n"
|
|
77
|
-
' """\n'
|
|
78
|
-
" return {\n"
|
|
79
|
-
' "body_highlight": derive_highlight(colors.body),\n'
|
|
80
|
-
' "body_mid": colors.body,\n'
|
|
81
|
-
' "body_shadow": derive_shadow(colors.body),\n'
|
|
82
|
-
' "body_inner_shadow": derive_inner(colors.body),\n'
|
|
83
|
-
' "sheen_mid": colors.sheen,\n'
|
|
84
|
-
' "rim_highlight": derive_rim(colors.sheen),\n'
|
|
85
|
-
' "specular_core": derive_specular(colors.sheen),\n'
|
|
86
|
-
' "depth_mid": colors.depth,\n'
|
|
87
|
-
" }\n"
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _plural_family_with_two_keys_function() -> str:
|
|
92
|
-
return (
|
|
93
|
-
"def _palette_substitutions(colors: PhoneHandsetColors) -> dict[str, str]:\n"
|
|
94
|
-
' """Derive every SVG color field from the three theme hero colors.\n'
|
|
95
|
-
"\n"
|
|
96
|
-
" Returns:\n"
|
|
97
|
-
" The color substitution fields the SVG template fills: the body form\n"
|
|
98
|
-
" stops (highlight, mid, shadow, inner-shadow), the sheen stops, the rim\n"
|
|
99
|
-
" highlight, the depth, and the specular core that lights the gloss.\n"
|
|
100
|
-
' """\n'
|
|
101
|
-
" return {\n"
|
|
102
|
-
' "body_highlight": derive_highlight(colors.body),\n'
|
|
103
|
-
' "body_mid": colors.body,\n'
|
|
104
|
-
' "sheen_highlight": derive_sheen_highlight(colors.sheen),\n'
|
|
105
|
-
' "sheen_mid": colors.sheen,\n'
|
|
106
|
-
' "rim_highlight": derive_rim(colors.sheen),\n'
|
|
107
|
-
' "specular_core": derive_specular(colors.sheen),\n'
|
|
108
|
-
" }\n"
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def test_should_flag_plural_stops_with_single_family_key() -> None:
|
|
113
|
-
issues = check_docstring_returns_plural_cardinality(
|
|
114
|
-
_drifted_single_sheen_function(), PRODUCTION_FILE_PATH
|
|
115
|
-
)
|
|
116
|
-
assert any("sheen" in each for each in issues), (
|
|
117
|
-
f"The plural 'sheen stops' against a single sheen_mid key must be flagged, got: {issues!r}"
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def test_should_report_category_o6_in_the_message() -> None:
|
|
122
|
-
issues = check_docstring_returns_plural_cardinality(
|
|
123
|
-
_drifted_single_sheen_function(), PRODUCTION_FILE_PATH
|
|
124
|
-
)
|
|
125
|
-
assert any("O6" in each for each in issues), (
|
|
126
|
-
f"Expected the Category O6 label in the message, got: {issues!r}"
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def test_should_not_flag_singular_noun() -> None:
|
|
131
|
-
issues = check_docstring_returns_plural_cardinality(
|
|
132
|
-
_singular_sheen_function(), PRODUCTION_FILE_PATH
|
|
133
|
-
)
|
|
134
|
-
assert issues == [], (
|
|
135
|
-
f"A singular 'sheen stop' matching one key must not be flagged, got: {issues!r}"
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def test_should_not_flag_plural_family_with_two_keys() -> None:
|
|
140
|
-
issues = check_docstring_returns_plural_cardinality(
|
|
141
|
-
_plural_family_with_two_keys_function(), PRODUCTION_FILE_PATH
|
|
142
|
-
)
|
|
143
|
-
assert issues == [], (
|
|
144
|
-
f"A plural 'sheen stops' matching two sheen_ keys must not be flagged, got: {issues!r}"
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def test_should_not_flag_family_absent_from_dict() -> None:
|
|
149
|
-
source = (
|
|
150
|
-
"def build() -> dict[str, str]:\n"
|
|
151
|
-
' """Build the fields.\n'
|
|
152
|
-
"\n"
|
|
153
|
-
" Returns:\n"
|
|
154
|
-
" The body stops and the sheen stops the template fills.\n"
|
|
155
|
-
' """\n'
|
|
156
|
-
" return {\n"
|
|
157
|
-
' "body_mid": "a",\n'
|
|
158
|
-
' "rim_highlight": "b",\n'
|
|
159
|
-
" }\n"
|
|
160
|
-
)
|
|
161
|
-
issues = check_docstring_returns_plural_cardinality(source, PRODUCTION_FILE_PATH)
|
|
162
|
-
assert issues == [], (
|
|
163
|
-
f"A plural family with no matching dict keys must not be flagged, got: {issues!r}"
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def test_should_not_flag_when_no_returns_section() -> None:
|
|
168
|
-
source = (
|
|
169
|
-
"def build() -> dict[str, str]:\n"
|
|
170
|
-
' """Build the sheen stops without a Returns section."""\n'
|
|
171
|
-
" return {\n"
|
|
172
|
-
' "sheen_mid": "a",\n'
|
|
173
|
-
" }\n"
|
|
174
|
-
)
|
|
175
|
-
issues = check_docstring_returns_plural_cardinality(source, PRODUCTION_FILE_PATH)
|
|
176
|
-
assert issues == [], (
|
|
177
|
-
f"A plural noun outside a Returns section must not be flagged, got: {issues!r}"
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def test_should_skip_test_file() -> None:
|
|
182
|
-
issues = check_docstring_returns_plural_cardinality(
|
|
183
|
-
_drifted_single_sheen_function(), TEST_FILE_PATH
|
|
184
|
-
)
|
|
185
|
-
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def test_should_skip_hook_infrastructure() -> None:
|
|
189
|
-
issues = check_docstring_returns_plural_cardinality(
|
|
190
|
-
_drifted_single_sheen_function(), HOOK_INFRASTRUCTURE_PATH
|
|
191
|
-
)
|
|
192
|
-
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def test_should_handle_syntax_error_gracefully() -> None:
|
|
196
|
-
issues = check_docstring_returns_plural_cardinality("def fetch(\n", PRODUCTION_FILE_PATH)
|
|
197
|
-
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def test_validate_content_surfaces_plural_cardinality_drift() -> None:
|
|
201
|
-
issues = validate_content(
|
|
202
|
-
_drifted_single_sheen_function(), PRODUCTION_FILE_PATH, old_content=""
|
|
203
|
-
)
|
|
204
|
-
matching_issues = [each for each in issues if "sheen" in each and "O6" in each]
|
|
205
|
-
assert matching_issues, (
|
|
206
|
-
f"Expected validate_content to surface the O6 plural-cardinality drift, got: {issues!r}"
|
|
207
|
-
)
|