claude-dev-env 1.76.0 → 1.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -0
  2. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +8 -4
  3. package/hooks/blocking/CLAUDE.md +3 -1
  4. package/hooks/blocking/code_rules_dead_module_constant.py +215 -59
  5. package/hooks/blocking/code_rules_dead_split_branch.py +225 -0
  6. package/hooks/blocking/code_rules_docstrings.py +730 -0
  7. package/hooks/blocking/code_rules_enforcer.py +39 -0
  8. package/hooks/blocking/code_rules_paired_test.py +299 -0
  9. package/hooks/blocking/code_rules_string_magic.py +71 -1
  10. package/hooks/blocking/convergence_gate_blocker.py +24 -15
  11. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +89 -2
  12. package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +105 -0
  13. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +174 -0
  14. package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +176 -0
  15. package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +262 -0
  16. package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +226 -0
  17. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +267 -0
  18. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +149 -0
  19. package/hooks/blocking/test_code_rules_enforcer_whitespace_indentation_magic.py +74 -0
  20. package/hooks/blocking/test_convergence_gate_blocker.py +71 -0
  21. package/hooks/hooks_constants/CLAUDE.md +1 -0
  22. package/hooks/hooks_constants/blocking_check_limits.py +51 -0
  23. package/hooks/hooks_constants/code_rules_enforcer_constants.py +49 -0
  24. package/hooks/hooks_constants/paired_test_coverage_constants.py +27 -0
  25. package/package.json +1 -1
  26. package/rules/CLAUDE.md +2 -0
  27. package/rules/docstring-prose-matches-implementation.md +5 -1
  28. package/rules/file-global-constants.md +2 -2
  29. package/rules/package-inventory-stale-entry.md +8 -0
  30. package/rules/paired-test-coverage.md +28 -0
  31. package/rules/plain-illustrative-docstrings.md +56 -0
@@ -0,0 +1,225 @@
1
+ """Dead-conditional-branch check for a truthiness test on an always-non-empty split."""
2
+
3
+ import ast
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ _blocking_directory = str(Path(__file__).resolve().parent)
8
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
9
+ if _blocking_directory not in sys.path:
10
+ sys.path.insert(0, _blocking_directory)
11
+ if _hooks_directory not in sys.path:
12
+ sys.path.insert(0, _hooks_directory)
13
+
14
+ from code_rules_path_utils import ( # noqa: E402
15
+ is_config_file,
16
+ )
17
+ from code_rules_shared import ( # noqa: E402
18
+ _collect_annotated_arguments,
19
+ _walk_skipping_nested_function_defs,
20
+ is_test_file,
21
+ )
22
+
23
+ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
24
+ ALL_ALWAYS_NONEMPTY_SPLIT_METHOD_NAMES,
25
+ DEAD_SPLIT_BRANCH_MESSAGE_SUFFIX,
26
+ MAX_DEAD_SPLIT_BRANCH_ISSUES,
27
+ )
28
+
29
+
30
+ def _is_separator_split_call(call_node: ast.Call) -> bool:
31
+ """Return True for an ``x.split(sep)`` / ``x.rsplit(sep)`` with a non-empty separator.
32
+
33
+ ``str.split`` and ``bytes.split`` return a list holding at least one element
34
+ whenever a separator argument is supplied, so the result is always truthy.
35
+ Requiring a non-empty string or bytes literal separator keeps the guarantee
36
+ airtight: ``split()`` with no argument can return an empty list, and an
37
+ empty separator raises at runtime. The receiver is restricted to a bare
38
+ ``Name`` or a string/bytes literal so an intermediate attribute chain such
39
+ as a pandas ``s.str.split(",")`` — whose result is a Series, not a list —
40
+ is not treated as an always-non-empty split.
41
+
42
+ Args:
43
+ call_node: The call expression on the right-hand side of an assignment.
44
+
45
+ Returns:
46
+ True when the call is a separator-bearing split that never returns an
47
+ empty list.
48
+ """
49
+ function_node = call_node.func
50
+ if not isinstance(function_node, ast.Attribute):
51
+ return False
52
+ if function_node.attr not in ALL_ALWAYS_NONEMPTY_SPLIT_METHOD_NAMES:
53
+ return False
54
+ receiver_node = function_node.value
55
+ if isinstance(receiver_node, ast.Constant):
56
+ if not isinstance(receiver_node.value, (str, bytes)):
57
+ return False
58
+ elif not isinstance(receiver_node, ast.Name):
59
+ return False
60
+ if not call_node.args:
61
+ return False
62
+ first_argument = call_node.args[0]
63
+ if not isinstance(first_argument, ast.Constant):
64
+ return False
65
+ separator_value = first_argument.value
66
+ if not isinstance(separator_value, (str, bytes)):
67
+ return False
68
+ return len(separator_value) > 0
69
+
70
+
71
+ def _separator_split_target_names(
72
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef,
73
+ ) -> set[str]:
74
+ """Return names bound exactly once in the function from a separator split.
75
+
76
+ A name qualifies when its single binding in the function body is an
77
+ assignment whose value is a separator-bearing ``split`` / ``rsplit`` call,
78
+ so the bound list is always truthy. A name bound more than once, or also a
79
+ parameter, is excluded because a later rebinding could make it falsy.
80
+
81
+ Args:
82
+ function_node: The function whose body is inspected.
83
+
84
+ Returns:
85
+ The set of always-truthy split-result names safe to reason about.
86
+ """
87
+ all_store_counts: dict[str, int] = {}
88
+ all_split_names: set[str] = set()
89
+ for each_statement in function_node.body:
90
+ for each_node in _walk_skipping_nested_function_defs(each_statement):
91
+ if isinstance(each_node, ast.Name) and isinstance(each_node.ctx, ast.Store):
92
+ all_store_counts[each_node.id] = (
93
+ all_store_counts.get(each_node.id, 0) + 1
94
+ )
95
+ if not isinstance(each_node, ast.Assign):
96
+ continue
97
+ if len(each_node.targets) != 1:
98
+ continue
99
+ assignment_target = each_node.targets[0]
100
+ if not isinstance(assignment_target, ast.Name):
101
+ continue
102
+ if isinstance(each_node.value, ast.Call) and _is_separator_split_call(
103
+ each_node.value
104
+ ):
105
+ all_split_names.add(assignment_target.id)
106
+ all_parameter_names = {
107
+ each_argument.arg
108
+ for each_argument in _collect_annotated_arguments(function_node)
109
+ }
110
+ return {
111
+ each_name
112
+ for each_name in all_split_names
113
+ if all_store_counts.get(each_name, 0) == 1
114
+ and each_name not in all_parameter_names
115
+ }
116
+
117
+
118
+ def _truthiness_target(
119
+ test_node: ast.expr, all_candidate_names: set[str]
120
+ ) -> str | None:
121
+ """Return the candidate name a bare ``Name`` truthiness test reads, else None."""
122
+ if isinstance(test_node, ast.Name) and test_node.id in all_candidate_names:
123
+ return test_node.id
124
+ return None
125
+
126
+
127
+ def _negated_truthiness_target(
128
+ test_node: ast.expr, all_candidate_names: set[str]
129
+ ) -> str | None:
130
+ """Return the candidate name a ``not Name`` test reads, else None."""
131
+ if not isinstance(test_node, ast.UnaryOp) or not isinstance(test_node.op, ast.Not):
132
+ return None
133
+ operand = test_node.operand
134
+ if isinstance(operand, ast.Name) and operand.id in all_candidate_names:
135
+ return operand.id
136
+ return None
137
+
138
+
139
+ def _branch_finding_for_node(
140
+ node: ast.AST, all_candidate_names: set[str]
141
+ ) -> tuple[int, str] | None:
142
+ """Return ``(line, name)`` when a node carries a dead branch, else None.
143
+
144
+ A conditional expression always carries an else arm, so any truthiness test
145
+ on a candidate name is a dead branch. An ``if`` statement only carries a
146
+ dead else when it has one, while an ``if not name`` statement always has a
147
+ dead body.
148
+ """
149
+ if isinstance(node, ast.IfExp):
150
+ truthy_name = _truthiness_target(node.test, all_candidate_names)
151
+ if truthy_name is not None:
152
+ return node.lineno, truthy_name
153
+ return _negated_branch_finding(node.test, node.lineno, all_candidate_names)
154
+ if isinstance(node, ast.If):
155
+ truthy_name = _truthiness_target(node.test, all_candidate_names)
156
+ if truthy_name is not None and node.orelse:
157
+ return node.lineno, truthy_name
158
+ return _negated_branch_finding(node.test, node.lineno, all_candidate_names)
159
+ return None
160
+
161
+
162
+ def _negated_branch_finding(
163
+ test_node: ast.expr, line_number: int, all_candidate_names: set[str]
164
+ ) -> tuple[int, str] | None:
165
+ negated_name = _negated_truthiness_target(test_node, all_candidate_names)
166
+ if negated_name is not None:
167
+ return line_number, negated_name
168
+ return None
169
+
170
+
171
+ def _dead_branch_findings(
172
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef,
173
+ all_candidate_names: set[str],
174
+ ) -> list[tuple[int, str]]:
175
+ """Return every ``(line, name)`` dead-branch finding in the function body."""
176
+ all_findings: list[tuple[int, str]] = []
177
+ for each_statement in function_node.body:
178
+ for each_node in _walk_skipping_nested_function_defs(each_statement):
179
+ finding = _branch_finding_for_node(each_node, all_candidate_names)
180
+ if finding is not None:
181
+ all_findings.append(finding)
182
+ return all_findings
183
+
184
+
185
+ def check_dead_split_truthiness_branch(content: str, file_path: str) -> list[str]:
186
+ """Flag a conditional whose branch is unreachable after a separator split.
187
+
188
+ ``str.split(sep)`` and ``bytes.split(sep)`` with a separator always return a
189
+ list holding at least one element, so a value bound from such a call is
190
+ always truthy. A conditional that tests that value's truthiness carries a
191
+ dead branch: ``parts[0] if parts else fallback`` never reaches ``fallback``,
192
+ and ``if not parts:`` never runs its body. Both the conditional-expression
193
+ form and the ``if`` / ``if not`` statement form are reported, scoped to a
194
+ value bound exactly once in the function from a separator split. Config
195
+ files and test files are exempt.
196
+
197
+ Args:
198
+ content: The source text to inspect.
199
+ file_path: The path the source will be written to, used for exemptions.
200
+
201
+ Returns:
202
+ One issue per dead conditional branch found, capped at the module limit.
203
+ """
204
+ if is_test_file(file_path) or is_config_file(file_path):
205
+ return []
206
+ try:
207
+ tree = ast.parse(content)
208
+ except SyntaxError:
209
+ return []
210
+ issues: list[str] = []
211
+ for each_function_node in ast.walk(tree):
212
+ if not isinstance(each_function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
213
+ continue
214
+ all_candidate_names = _separator_split_target_names(each_function_node)
215
+ if not all_candidate_names:
216
+ continue
217
+ for each_line, each_name in _dead_branch_findings(
218
+ each_function_node, all_candidate_names
219
+ ):
220
+ issues.append(
221
+ f"Line {each_line}: {each_name!r} {DEAD_SPLIT_BRANCH_MESSAGE_SUFFIX}"
222
+ )
223
+ if len(issues) >= MAX_DEAD_SPLIT_BRANCH_ISSUES:
224
+ return issues
225
+ return issues