claude-dev-env 1.58.0 → 1.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_duplicate_body.py +287 -0
- package/hooks/blocking/code_rules_enforcer.py +175 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +17 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/autoconverge/SKILL.md +13 -2
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +106 -36
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/update/SKILL.md +37 -5
|
@@ -54,6 +54,101 @@ def _mask_string_literals_preserving_length(source_line: str) -> str:
|
|
|
54
54
|
return string_literal_pattern.sub(_replace_string_literal, source_line)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def _carries_top_level_slice_colon(bracket_body: str) -> bool:
|
|
58
|
+
"""Return True when ``bracket_body`` holds a slice colon at its own level.
|
|
59
|
+
|
|
60
|
+
The body is the text between one ``[...]`` pair. A slice colon is a ``:``
|
|
61
|
+
that sits at the body's top level — not nested inside any ``(``, ``[``,
|
|
62
|
+
or ``{`` opened within the body — and is not the ``:=`` walrus operator.
|
|
63
|
+
A slice writes ``[:N]`` or ``[1:N]``; a plain subscript (``[K]``), a
|
|
64
|
+
walrus subscript (``[(n := M)]``), and a lambda subscript
|
|
65
|
+
(``[(lambda: V)()]``) carry no such colon.
|
|
66
|
+
"""
|
|
67
|
+
nesting_depth = 0
|
|
68
|
+
for each_position, each_character in enumerate(bracket_body):
|
|
69
|
+
if each_character in "([{":
|
|
70
|
+
nesting_depth += 1
|
|
71
|
+
continue
|
|
72
|
+
if each_character in ")]}":
|
|
73
|
+
nesting_depth -= 1
|
|
74
|
+
continue
|
|
75
|
+
if each_character == ":" and nesting_depth == 0:
|
|
76
|
+
next_position = each_position + 1
|
|
77
|
+
character_after_colon = bracket_body[next_position : next_position + 1]
|
|
78
|
+
follows_walrus = character_after_colon == "="
|
|
79
|
+
if not follows_walrus:
|
|
80
|
+
return True
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _innermost_bracket_body_around(masked_line: str, occurrence_position: int) -> str | None:
|
|
85
|
+
"""Return the body of the innermost ``[...]`` pair enclosing a position.
|
|
86
|
+
|
|
87
|
+
Walks the line tracking open-bracket positions on a stack. The bracket
|
|
88
|
+
pair directly enclosing ``occurrence_position`` is the one whose ``[``
|
|
89
|
+
is on top of the stack when that position is reached; its body runs from
|
|
90
|
+
just after that ``[`` to its matching ``]``. Returns ``None`` when the
|
|
91
|
+
position lies outside every ``[...]`` pair.
|
|
92
|
+
"""
|
|
93
|
+
open_bracket_positions: list[int] = []
|
|
94
|
+
enclosing_open_position = -1
|
|
95
|
+
for each_position, each_character in enumerate(masked_line):
|
|
96
|
+
if each_position == occurrence_position:
|
|
97
|
+
enclosing_open_position = open_bracket_positions[-1] if open_bracket_positions else -1
|
|
98
|
+
if each_character == "[":
|
|
99
|
+
open_bracket_positions.append(each_position)
|
|
100
|
+
continue
|
|
101
|
+
if each_character == "]" and open_bracket_positions:
|
|
102
|
+
open_bracket_positions.pop()
|
|
103
|
+
|
|
104
|
+
if enclosing_open_position == -1:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
closing_position = _matching_close_bracket_position(masked_line, enclosing_open_position)
|
|
108
|
+
if closing_position == -1:
|
|
109
|
+
return None
|
|
110
|
+
return masked_line[enclosing_open_position + 1 : closing_position]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _matching_close_bracket_position(masked_line: str, open_position: int) -> int:
|
|
114
|
+
"""Return the index of the ``]`` that closes the ``[`` at ``open_position``.
|
|
115
|
+
|
|
116
|
+
Returns ``-1`` when the opening bracket has no matching close on the line.
|
|
117
|
+
"""
|
|
118
|
+
depth = 0
|
|
119
|
+
for each_position in range(open_position, len(masked_line)):
|
|
120
|
+
each_character = masked_line[each_position]
|
|
121
|
+
if each_character == "[":
|
|
122
|
+
depth += 1
|
|
123
|
+
continue
|
|
124
|
+
if each_character == "]":
|
|
125
|
+
depth -= 1
|
|
126
|
+
if depth == 0:
|
|
127
|
+
return each_position
|
|
128
|
+
return -1
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _is_magic_number_inside_slice_bound(masked_line: str, number_text: str) -> bool:
|
|
132
|
+
"""Return True when ``number_text`` appears as a slice bound on the line.
|
|
133
|
+
|
|
134
|
+
For each standalone-token occurrence of ``number_text``, the innermost
|
|
135
|
+
``[...]`` pair directly enclosing it is located. The number is a slice
|
|
136
|
+
bound only when that innermost pair carries a top-level slice colon
|
|
137
|
+
(``sha[:N]``, ``ts[1:N]``). A plain subscript index (``items[K]``), the
|
|
138
|
+
inner index of a nested subscript (``a[b[I]:N]`` — the inner index sits
|
|
139
|
+
inside the plain inner pair), a walrus subscript (``arr[(n := M)]``), and
|
|
140
|
+
a lambda subscript (``seq[(lambda: V)()]``) are not slice bounds.
|
|
141
|
+
"""
|
|
142
|
+
token_boundary_pattern = r"(?<![.\w])" + re.escape(number_text) + r"(?![.\w])"
|
|
143
|
+
for each_match in re.finditer(token_boundary_pattern, masked_line):
|
|
144
|
+
bracket_body = _innermost_bracket_body_around(masked_line, each_match.start())
|
|
145
|
+
if bracket_body is None:
|
|
146
|
+
continue
|
|
147
|
+
if _carries_top_level_slice_colon(bracket_body):
|
|
148
|
+
return True
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
|
|
57
152
|
def check_magic_values(content: str, file_path: str) -> list[str]:
|
|
58
153
|
"""Check for magic values in function bodies."""
|
|
59
154
|
if is_config_file(file_path) or is_test_file(file_path):
|
|
@@ -93,6 +188,9 @@ def check_magic_values(content: str, file_path: str) -> list[str]:
|
|
|
93
188
|
if each_number not in allowed_numbers:
|
|
94
189
|
if "range(" in stripped_without_string_literals or "enumerate(" in stripped_without_string_literals:
|
|
95
190
|
continue
|
|
191
|
+
if _is_magic_number_inside_slice_bound(stripped_without_string_literals, each_number):
|
|
192
|
+
issues.append(f"Line {each_line_number}: Magic value {each_number} - extract to named constant")
|
|
193
|
+
break
|
|
96
194
|
if "[" in stripped_without_string_literals and "]" in stripped_without_string_literals:
|
|
97
195
|
continue
|
|
98
196
|
issues.append(f"Line {each_line_number}: Magic value {each_number} - extract to named constant")
|
|
@@ -115,6 +115,47 @@ def _collect_annotated_arguments(function_node: ast.FunctionDef | ast.AsyncFunct
|
|
|
115
115
|
return all_annotated_arguments
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def _collect_fixture_injection_arguments(
|
|
119
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
120
|
+
) -> list[ast.arg]:
|
|
121
|
+
"""Return only the named parameters pytest fills by fixture injection.
|
|
122
|
+
|
|
123
|
+
Pytest passes fixtures by keyword (``testfunction(**testargs)``), so a
|
|
124
|
+
parameter can receive a fixture only when both conditions hold: it is
|
|
125
|
+
reachable by keyword, and pytest is responsible for supplying its value.
|
|
126
|
+
Positional-only parameters are NOT injection slots — a keyword-passed
|
|
127
|
+
fixture can never bind to one, and ``def test_x(tmp_path, /)`` raises a
|
|
128
|
+
missing-argument ``TypeError`` under pytest. A ``*args`` star-argument or
|
|
129
|
+
``**kwargs`` double-star-argument never names a single fixture either.
|
|
130
|
+
A parameter carrying a default is NOT injected — pytest leaves its default
|
|
131
|
+
in place rather than supplying the fixture. So this collector keeps only the
|
|
132
|
+
positional-or-keyword and keyword-only parameters that have no default, and
|
|
133
|
+
omits ``args.posonlyargs``, ``args.vararg``, and ``args.kwarg``.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
function_node: The function definition AST node to inspect.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The undefaulted positional-or-keyword and keyword-only argument nodes,
|
|
140
|
+
in declaration order.
|
|
141
|
+
"""
|
|
142
|
+
arguments = function_node.args
|
|
143
|
+
defaulted_positional_count = len(arguments.defaults)
|
|
144
|
+
undefaulted_positional_arguments = (
|
|
145
|
+
arguments.args[:-defaulted_positional_count]
|
|
146
|
+
if defaulted_positional_count
|
|
147
|
+
else arguments.args
|
|
148
|
+
)
|
|
149
|
+
undefaulted_keyword_only_arguments = [
|
|
150
|
+
each_keyword_argument
|
|
151
|
+
for each_keyword_argument, each_default in zip(
|
|
152
|
+
arguments.kwonlyargs, arguments.kw_defaults
|
|
153
|
+
)
|
|
154
|
+
if each_default is None
|
|
155
|
+
]
|
|
156
|
+
return [*undefaulted_positional_arguments, *undefaulted_keyword_only_arguments]
|
|
157
|
+
|
|
158
|
+
|
|
118
159
|
def _collect_target_names(target: ast.expr) -> list[ast.Name]:
|
|
119
160
|
"""Return every ast.Name reachable through tuple/list/starred unpacking targets."""
|
|
120
161
|
if isinstance(target, ast.Name):
|