claude-dev-env 1.59.0 → 1.61.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 +4 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
- package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/code_rules_annotations_length.py +189 -10
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +152 -0
- package/hooks/blocking/code_rules_enforcer.py +38 -15
- package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +118 -0
- package/hooks/blocking/destructive_command_blocker.py +483 -61
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_destructive_command_blocker.py +213 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +490 -0
- package/hooks/blocking/test_verified_commit_gate.py +495 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +193 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +686 -0
- package/hooks/blocking/verified_commit_gate.py +535 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +221 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +43 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
- package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/validation/mypy_validator.py +59 -7
- package/hooks/validation/test_mypy_validator.py +94 -0
- package/package.json +1 -1
- package/rules/file-global-constants.md +7 -1
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/orphan-css-class.md +23 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +54 -17
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +192 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +395 -206
- package/skills/autoconverge/workflow/converge.mjs +520 -57
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +518 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/rebase/SKILL.md +2 -4
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Orphan-CSS-class check: class attributes in markup with no matching selector."""
|
|
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_shared import ( # noqa: E402
|
|
15
|
+
is_test_file,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from hooks_constants.orphan_css_class_constants import ( # noqa: E402
|
|
19
|
+
CLASS_ATTRIBUTE_PATTERN,
|
|
20
|
+
CSS_CLASS_SELECTOR_PATTERN,
|
|
21
|
+
MAX_ORPHAN_CSS_CLASS_ISSUES,
|
|
22
|
+
MAX_SIBLING_MODULES_SCANNED,
|
|
23
|
+
ORPHAN_CSS_CLASS_MESSAGE_SUFFIX,
|
|
24
|
+
PYTHON_MODULE_GLOB,
|
|
25
|
+
STYLE_BLOCK_PATTERN,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _string_literals_with_lines(tree: ast.Module) -> list[tuple[str, int]]:
|
|
30
|
+
"""Return every string-constant value in the tree paired with its line number.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
tree: The parsed module to walk.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A list of ``(string_value, line_number)`` pairs, one per string constant.
|
|
37
|
+
"""
|
|
38
|
+
literals: list[tuple[str, int]] = []
|
|
39
|
+
for each_node in ast.walk(tree):
|
|
40
|
+
if isinstance(each_node, ast.Constant) and isinstance(each_node.value, str):
|
|
41
|
+
literals.append((each_node.value, each_node.lineno))
|
|
42
|
+
return literals
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _class_names_in_attribute(attribute_text: str) -> list[str]:
|
|
46
|
+
"""Return the individual class names in a single ``class="..."`` attribute.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
attribute_text: The whitespace-separated class list from one attribute.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Each non-empty class token, in order.
|
|
53
|
+
"""
|
|
54
|
+
return [each_token for each_token in attribute_text.split() if each_token]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _class_references_with_lines(
|
|
58
|
+
all_string_literals: list[tuple[str, int]],
|
|
59
|
+
) -> list[tuple[str, int]]:
|
|
60
|
+
"""Return every class name referenced in a ``class="..."`` attribute.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
all_string_literals: The ``(literal_text, line_number)`` constants to scan.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A list of ``(class_name, line_number)`` pairs, one per referenced class.
|
|
67
|
+
"""
|
|
68
|
+
references: list[tuple[str, int]] = []
|
|
69
|
+
for each_text, each_line in all_string_literals:
|
|
70
|
+
for each_match in CLASS_ATTRIBUTE_PATTERN.finditer(each_text):
|
|
71
|
+
for each_class_name in _class_names_in_attribute(each_match.group(1)):
|
|
72
|
+
references.append((each_class_name, each_line))
|
|
73
|
+
return references
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _defined_class_selectors(all_string_literals: list[tuple[str, int]]) -> set[str]:
|
|
77
|
+
"""Return every CSS class name defined by a selector inside a ``<style>`` block.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
all_string_literals: The ``(literal_text, line_number)`` constants to scan.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The set of class names that carry a matching ``.<class>`` selector.
|
|
84
|
+
"""
|
|
85
|
+
defined: set[str] = set()
|
|
86
|
+
for each_text, _ in all_string_literals:
|
|
87
|
+
for each_style_match in STYLE_BLOCK_PATTERN.finditer(each_text):
|
|
88
|
+
for each_selector in CSS_CLASS_SELECTOR_PATTERN.finditer(
|
|
89
|
+
each_style_match.group(1)
|
|
90
|
+
):
|
|
91
|
+
defined.add(each_selector.group(1))
|
|
92
|
+
return defined
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _sibling_module_paths(file_path: str) -> list[Path]:
|
|
96
|
+
"""Return the importable sibling Python modules near *file_path*.
|
|
97
|
+
|
|
98
|
+
Scans the file's own directory and its immediate child directories, since a
|
|
99
|
+
markup module commonly imports its ``<style>`` constant from a companion
|
|
100
|
+
package directory beside it. The scan is bounded so a large tree never
|
|
101
|
+
stalls a write.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
file_path: The absolute path of the file under validation.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The sibling ``.py`` paths to read for cross-module selector resolution,
|
|
108
|
+
excluding the file itself, capped at the scan budget.
|
|
109
|
+
"""
|
|
110
|
+
target = Path(file_path)
|
|
111
|
+
base_directory = target.parent
|
|
112
|
+
if not base_directory.is_dir():
|
|
113
|
+
return []
|
|
114
|
+
siblings: list[Path] = []
|
|
115
|
+
for each_path in sorted(base_directory.rglob(PYTHON_MODULE_GLOB)):
|
|
116
|
+
if each_path.resolve() == target.resolve():
|
|
117
|
+
continue
|
|
118
|
+
siblings.append(each_path)
|
|
119
|
+
if len(siblings) >= MAX_SIBLING_MODULES_SCANNED:
|
|
120
|
+
break
|
|
121
|
+
return siblings
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _selectors_from_sibling_modules(file_path: str) -> set[str]:
|
|
125
|
+
"""Return CSS class selectors defined in ``<style>`` blocks of sibling modules.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
file_path: The absolute path of the file under validation.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The union of class names whose selectors appear in any readable sibling
|
|
132
|
+
module's string literals.
|
|
133
|
+
"""
|
|
134
|
+
selectors: set[str] = set()
|
|
135
|
+
for each_sibling in _sibling_module_paths(file_path):
|
|
136
|
+
try:
|
|
137
|
+
sibling_source = each_sibling.read_text(encoding="utf-8")
|
|
138
|
+
except (OSError, UnicodeDecodeError):
|
|
139
|
+
continue
|
|
140
|
+
try:
|
|
141
|
+
sibling_tree = ast.parse(sibling_source)
|
|
142
|
+
except SyntaxError:
|
|
143
|
+
continue
|
|
144
|
+
selectors |= _defined_class_selectors(_string_literals_with_lines(sibling_tree))
|
|
145
|
+
return selectors
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def check_orphan_css_classes(content: str, file_path: str) -> list[str]:
|
|
149
|
+
"""Flag ``class="..."`` markup whose class has no matching CSS selector.
|
|
150
|
+
|
|
151
|
+
A module that emits HTML names each class it references with a matching
|
|
152
|
+
``.<class>`` selector, either in a ``<style>`` block in the same file or in
|
|
153
|
+
a companion module beside it. A referenced class with no selector anywhere
|
|
154
|
+
is a dead attribute (or a missing rule), so this flags it. The check only
|
|
155
|
+
fires for a file that itself emits markup, and only after a ``<style>``
|
|
156
|
+
block exists in the file or a sibling — a file with markup but no style
|
|
157
|
+
source nearby is left alone, since its stylesheet lives outside the scan.
|
|
158
|
+
Test files are exempt, since a fixture may carry intentional orphan markup.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
content: The new or whole-file content being written.
|
|
162
|
+
file_path: The destination path of the write or edit.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
One issue per orphan class reference, capped at the issue budget.
|
|
166
|
+
"""
|
|
167
|
+
if is_test_file(file_path):
|
|
168
|
+
return []
|
|
169
|
+
try:
|
|
170
|
+
tree = ast.parse(content)
|
|
171
|
+
except SyntaxError:
|
|
172
|
+
return []
|
|
173
|
+
all_string_literals = _string_literals_with_lines(tree)
|
|
174
|
+
class_references = _class_references_with_lines(all_string_literals)
|
|
175
|
+
if not class_references:
|
|
176
|
+
return []
|
|
177
|
+
defined_selectors = _defined_class_selectors(all_string_literals)
|
|
178
|
+
defined_selectors |= _selectors_from_sibling_modules(file_path)
|
|
179
|
+
if not defined_selectors:
|
|
180
|
+
return []
|
|
181
|
+
issues: list[str] = []
|
|
182
|
+
reported_classes: set[str] = set()
|
|
183
|
+
for each_class_name, each_line in class_references:
|
|
184
|
+
if each_class_name in defined_selectors:
|
|
185
|
+
continue
|
|
186
|
+
if each_class_name in reported_classes:
|
|
187
|
+
continue
|
|
188
|
+
reported_classes.add(each_class_name)
|
|
189
|
+
issues.append(
|
|
190
|
+
f"Line {each_line}: CSS class {each_class_name!r} used in markup"
|
|
191
|
+
f" has no matching '.{each_class_name}' selector - "
|
|
192
|
+
f"{ORPHAN_CSS_CLASS_MESSAGE_SUFFIX}"
|
|
193
|
+
)
|
|
194
|
+
if len(issues) >= MAX_ORPHAN_CSS_CLASS_ISSUES:
|
|
195
|
+
break
|
|
196
|
+
return issues
|
|
@@ -26,6 +26,7 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
|
26
26
|
MAX_STUB_IMPLEMENTATION_ISSUES,
|
|
27
27
|
MAX_THIN_WRAPPER_ISSUES,
|
|
28
28
|
MAX_TYPED_DICT_PAIR_ISSUES,
|
|
29
|
+
MAX_ZERO_PAYLOAD_ALIAS_ISSUES,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
def _pascal_to_snake_case(pascal_name: str) -> str:
|
|
@@ -58,6 +59,16 @@ def _collect_module_function_names(parsed_tree: ast.AST) -> set[str]:
|
|
|
58
59
|
return module_function_names
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
def _collect_module_function_nodes_by_name(
|
|
63
|
+
parsed_tree: ast.AST,
|
|
64
|
+
) -> dict[str, ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
65
|
+
function_node_by_name: dict[str, ast.FunctionDef | ast.AsyncFunctionDef] = {}
|
|
66
|
+
for each_statement in parsed_tree.body:
|
|
67
|
+
if isinstance(each_statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
68
|
+
function_node_by_name[each_statement.name] = each_statement
|
|
69
|
+
return function_node_by_name
|
|
70
|
+
|
|
71
|
+
|
|
61
72
|
def _is_init_file(file_path: str) -> bool:
|
|
62
73
|
return file_path.replace("\\", "/").rsplit("/", 1)[-1] == "__init__.py"
|
|
63
74
|
|
|
@@ -126,6 +137,167 @@ def check_thin_wrapper_files(content: str, file_path: str) -> list[str]:
|
|
|
126
137
|
return issues[:MAX_THIN_WRAPPER_ISSUES]
|
|
127
138
|
|
|
128
139
|
|
|
140
|
+
def _function_parameter_names_in_order(
|
|
141
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
142
|
+
) -> list[str]:
|
|
143
|
+
arguments = function_node.args
|
|
144
|
+
positional_arguments = [*arguments.posonlyargs, *arguments.args]
|
|
145
|
+
return [each_argument.arg for each_argument in positional_arguments]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _has_only_positional_parameters(
|
|
149
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
150
|
+
) -> bool:
|
|
151
|
+
arguments = function_node.args
|
|
152
|
+
has_no_parameter_defaults = not arguments.defaults and not arguments.kw_defaults
|
|
153
|
+
return (
|
|
154
|
+
not arguments.kwonlyargs
|
|
155
|
+
and arguments.vararg is None
|
|
156
|
+
and arguments.kwarg is None
|
|
157
|
+
and has_no_parameter_defaults
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _single_return_call(
|
|
162
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
163
|
+
) -> ast.Call | None:
|
|
164
|
+
body_statements = function_node.body
|
|
165
|
+
statements_after_docstring = (
|
|
166
|
+
body_statements[1:]
|
|
167
|
+
if body_statements and _statement_is_docstring(body_statements[0])
|
|
168
|
+
else body_statements
|
|
169
|
+
)
|
|
170
|
+
if len(statements_after_docstring) != 1:
|
|
171
|
+
return None
|
|
172
|
+
only_statement = statements_after_docstring[0]
|
|
173
|
+
if not isinstance(only_statement, ast.Return):
|
|
174
|
+
return None
|
|
175
|
+
return only_statement.value if isinstance(only_statement.value, ast.Call) else None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _forwards_parameters_unchanged(call_node: ast.Call, all_parameter_names: list[str]) -> bool:
|
|
179
|
+
if call_node.keywords:
|
|
180
|
+
return False
|
|
181
|
+
if len(call_node.args) != len(all_parameter_names):
|
|
182
|
+
return False
|
|
183
|
+
for each_argument, each_parameter_name in zip(call_node.args, all_parameter_names):
|
|
184
|
+
if not isinstance(each_argument, ast.Name) or each_argument.id != each_parameter_name:
|
|
185
|
+
return False
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _function_is_async(function_node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
190
|
+
return isinstance(function_node, ast.AsyncFunctionDef)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _alias_target_name(call_node: ast.Call, all_sibling_function_names: set[str]) -> str:
|
|
194
|
+
callee = call_node.func
|
|
195
|
+
if not isinstance(callee, ast.Name):
|
|
196
|
+
return ""
|
|
197
|
+
return callee.id if callee.id in all_sibling_function_names else ""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _module_string_literal_values(parsed_tree: ast.AST) -> set[str]:
|
|
201
|
+
string_literal_values: set[str] = set()
|
|
202
|
+
for each_node in ast.walk(parsed_tree):
|
|
203
|
+
if isinstance(each_node, ast.Constant) and isinstance(each_node.value, str):
|
|
204
|
+
string_literal_values.add(each_node.value)
|
|
205
|
+
return string_literal_values
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _target_accepts_forwarded_positional_call(
|
|
209
|
+
target_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
210
|
+
forwarded_argument_count: int,
|
|
211
|
+
) -> bool:
|
|
212
|
+
arguments = target_node.args
|
|
213
|
+
if any(default is None for default in arguments.kw_defaults):
|
|
214
|
+
return False
|
|
215
|
+
positional_parameters = [*arguments.posonlyargs, *arguments.args]
|
|
216
|
+
total_positional_count = len(positional_parameters)
|
|
217
|
+
required_positional_count = total_positional_count - len(arguments.defaults)
|
|
218
|
+
if arguments.vararg is not None:
|
|
219
|
+
return forwarded_argument_count >= required_positional_count
|
|
220
|
+
return required_positional_count <= forwarded_argument_count <= total_positional_count
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def check_zero_payload_function_alias(content: str, file_path: str) -> list[str]:
|
|
224
|
+
"""Flag a module-level function that only forwards its parameters to a sibling.
|
|
225
|
+
|
|
226
|
+
A function whose entire body (after an optional docstring) is a single
|
|
227
|
+
`return sibling_function(first_param, second_param, ...)` that forwards its
|
|
228
|
+
own parameters unchanged to another function defined in the same module is a
|
|
229
|
+
second name for one behavior — indirection without payload, which CODE_RULES
|
|
230
|
+
discourages. Callers should invoke the sibling directly. Both `def` and
|
|
231
|
+
`async def` forwarders are inspected.
|
|
232
|
+
|
|
233
|
+
A forwarder is left unflagged when any of these makes a direct call to the
|
|
234
|
+
target not equivalent to the alias: a decorator (caching, `@property`, route
|
|
235
|
+
registration); a parameter carrying a default value the target rejects; a
|
|
236
|
+
keyword-only / `*args` / `**kwargs` parameter on the alias; an awaitability
|
|
237
|
+
mismatch where one of the alias and target is `async def` and the other is
|
|
238
|
+
not; a forwarded positional call the live target's signature rejects (the
|
|
239
|
+
target has a keyword-only parameter without a default, or its positional
|
|
240
|
+
arity does not admit the forwarded argument count); or a name dispatched by a
|
|
241
|
+
string literal elsewhere in the module, where the named handler must exist for
|
|
242
|
+
a registry to resolve it.
|
|
243
|
+
|
|
244
|
+
Hook infrastructure is intentionally NOT exempt — pass-through aliases inside
|
|
245
|
+
hook modules are the motivating case. Test files and config files are exempt
|
|
246
|
+
because re-binding aliases are legitimate scaffolding there.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
content: The source under inspection.
|
|
250
|
+
file_path: Path to the file, used for the test and config exemptions.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
One issue string per pass-through alias, capped at
|
|
254
|
+
MAX_ZERO_PAYLOAD_ALIAS_ISSUES.
|
|
255
|
+
"""
|
|
256
|
+
if is_test_file(file_path) or is_config_file(file_path):
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
parsed_tree = ast.parse(content)
|
|
261
|
+
except SyntaxError:
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
function_node_by_name = _collect_module_function_nodes_by_name(parsed_tree)
|
|
265
|
+
all_sibling_function_names = set(function_node_by_name)
|
|
266
|
+
all_string_literal_values = _module_string_literal_values(parsed_tree)
|
|
267
|
+
issues: list[str] = []
|
|
268
|
+
for each_statement in parsed_tree.body:
|
|
269
|
+
if not isinstance(each_statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
270
|
+
continue
|
|
271
|
+
if each_statement.decorator_list:
|
|
272
|
+
continue
|
|
273
|
+
if each_statement.name in all_string_literal_values:
|
|
274
|
+
continue
|
|
275
|
+
if not _has_only_positional_parameters(each_statement):
|
|
276
|
+
continue
|
|
277
|
+
call_node = _single_return_call(each_statement)
|
|
278
|
+
if call_node is None:
|
|
279
|
+
continue
|
|
280
|
+
target_name = _alias_target_name(call_node, all_sibling_function_names)
|
|
281
|
+
if not target_name or target_name == each_statement.name:
|
|
282
|
+
continue
|
|
283
|
+
target_node = function_node_by_name[target_name]
|
|
284
|
+
if _function_is_async(each_statement) != _function_is_async(target_node):
|
|
285
|
+
continue
|
|
286
|
+
forwarded_parameter_names = _function_parameter_names_in_order(each_statement)
|
|
287
|
+
if not _forwards_parameters_unchanged(call_node, forwarded_parameter_names):
|
|
288
|
+
continue
|
|
289
|
+
if not _target_accepts_forwarded_positional_call(
|
|
290
|
+
target_node, len(forwarded_parameter_names)
|
|
291
|
+
):
|
|
292
|
+
continue
|
|
293
|
+
issues.append(
|
|
294
|
+
f"Line {each_statement.lineno}: {file_path}: zero-payload alias — "
|
|
295
|
+
f"{each_statement.name} only forwards its parameters to {target_name}; "
|
|
296
|
+
f"callers should call {target_name} directly (indirection without payload)"
|
|
297
|
+
)
|
|
298
|
+
return issues[:MAX_ZERO_PAYLOAD_ALIAS_ISSUES]
|
|
299
|
+
|
|
300
|
+
|
|
129
301
|
def check_typed_dict_encode_decode(content: str, file_path: str) -> list[str]:
|
|
130
302
|
"""Flag TypedDict declarations missing companion `_encode_*` / `_decode_*` functions."""
|
|
131
303
|
if (
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Constants for the verified-commit gate hook family.
|
|
2
|
+
|
|
3
|
+
Shared by ``verification_verdict_store.py``, ``verified_commit_gate.py``,
|
|
4
|
+
and ``verifier_verdict_minter.py`` so every tunable lives in one place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
GIT_TIMEOUT_SECONDS = 30
|
|
10
|
+
ROOT_KEY_HEX_LENGTH = 16
|
|
11
|
+
VERDICT_JSON_INDENT = 2
|
|
12
|
+
CLAUDE_HOME_DIRECTORY_NAME = ".claude"
|
|
13
|
+
VERDICT_DIRECTORY_NAME = "verification"
|
|
14
|
+
VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN = r"['\"\\/,\s]+"
|
|
15
|
+
VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN = r"(?=['\"]*[\\/,])"
|
|
16
|
+
RELATIVE_VERDICT_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))verification[\\/]"
|
|
17
|
+
VERDICT_PATH_GLUE_PATTERN = r"['\"+\\/\s]*[\\/]['\"+\\/\s]*"
|
|
18
|
+
VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?verification[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
19
|
+
VERDICT_PATH_JOINED_VARIABLE_PATTERN = r"\$\{?(\w+)\}?[\\/]|[\\/]\$\{?(\w+)\}?"
|
|
20
|
+
VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN = r"(?:^|(?<=[\s;&|(]))%s=(\S+)"
|
|
21
|
+
VERDICT_FILE_RELATIVE_REFERENCE_PATTERN = (
|
|
22
|
+
rf"(?:^|(?<=[\s;&|(='\"\\/]))verification[\\/][0-9a-f]{{{ROOT_KEY_HEX_LENGTH}}}\.json"
|
|
23
|
+
)
|
|
24
|
+
PATH_OBFUSCATION_PRIMITIVE_PATTERN = (
|
|
25
|
+
r"chr\s*\(|bytes\.fromhex\s*\(|b64decode\s*\(|codecs\.decode\s*\("
|
|
26
|
+
r"|(?:bytes|bytearray)\s*\(\s*\[|\[char\[?\]?\]"
|
|
27
|
+
)
|
|
28
|
+
ALL_VERDICT_PATH_SEGMENT_NAMES = (".claude", "verification")
|
|
29
|
+
ALL_VERDICT_PATH_SEGMENT_BODIES = ("claude", "verification")
|
|
30
|
+
HEX_TOKEN_PATTERN = r"(?<![0-9a-fx])([0-9a-f]{6,})(?![0-9a-f])"
|
|
31
|
+
BASE64_TOKEN_PATTERN = r"[A-Za-z0-9+/]{8,}={0,2}"
|
|
32
|
+
CHARACTER_CODE_SEQUENCE_PATTERN = r"\d{1,3}(?:\s*,\s*\d{1,3})+"
|
|
33
|
+
CHR_CALL_CHAIN_PATTERN = r"chr\(\s*\d{1,3}\s*\)(?:\s*\+\s*chr\(\s*\d{1,3}\s*\))+"
|
|
34
|
+
CHR_CALL_CODE_PATTERN = r"chr\(\s*(\d{1,3})\s*\)"
|
|
35
|
+
HEX_DIGITS_PER_BYTE = 2
|
|
36
|
+
FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
37
|
+
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
38
|
+
r"|Out-File|Set-Content|Add-Content|\btee\b|>"
|
|
39
|
+
)
|
|
40
|
+
NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
41
|
+
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
42
|
+
r"|Out-File|Set-Content|Add-Content|\btee\b"
|
|
43
|
+
)
|
|
44
|
+
WRITE_CALL_REGION_PATTERN = (
|
|
45
|
+
r"(?:\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
46
|
+
r"|Out-File|Set-Content|Add-Content|\btee\b)[^;&|\n]*"
|
|
47
|
+
)
|
|
48
|
+
VERDICT_KEY_ALL_PASS = "all_pass"
|
|
49
|
+
VERDICT_KEY_MANIFEST_SHA256 = "manifest_sha256"
|
|
50
|
+
VERDICT_KEY_FINDINGS = "findings"
|
|
51
|
+
SUBAGENTS_DIRECTORY_NAME = "subagents"
|
|
52
|
+
AGENT_TRANSCRIPT_GLOB = "agent-*.jsonl"
|
|
53
|
+
AGENT_META_SIDECAR_SUFFIX = ".meta.json"
|
|
54
|
+
AGENT_META_TYPE_KEY = "agentType"
|
|
55
|
+
TRANSCRIPT_ENTRY_TYPE_KEY = "type"
|
|
56
|
+
TRANSCRIPT_ASSISTANT_ENTRY_TYPE = "assistant"
|
|
57
|
+
TRANSCRIPT_MESSAGE_KEY = "message"
|
|
58
|
+
TRANSCRIPT_CONTENT_KEY = "content"
|
|
59
|
+
TRANSCRIPT_CONTENT_TYPE_KEY = "type"
|
|
60
|
+
TRANSCRIPT_TEXT_CONTENT_TYPE = "text"
|
|
61
|
+
TRANSCRIPT_TEXT_KEY = "text"
|
|
62
|
+
VERDICT_FENCE_PATTERN = r"```verdict\s*\n(.*?)```"
|
|
63
|
+
MANIFEST_HASH_CLI_FLAG = "--manifest-hash"
|
|
64
|
+
DOCS_ONLY_EXTENSIONS = frozenset(
|
|
65
|
+
{".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
|
|
66
|
+
)
|
|
67
|
+
PYTHON_EXTENSION = ".py"
|
|
68
|
+
TEST_FILE_PREFIX = "test_"
|
|
69
|
+
TEST_FILE_SUFFIX = "_test.py"
|
|
70
|
+
CONFTEST_FILE_NAME = "conftest.py"
|
|
71
|
+
MINIMUM_STATUS_FIELD_COUNT = 2
|
|
72
|
+
ALL_FALLBACK_BASE_REFERENCES = ("origin/main", "origin/master")
|
|
73
|
+
ALL_TOOLING_STATE_PREFIXES = (
|
|
74
|
+
".claude/verification/",
|
|
75
|
+
".claude/worktrees/",
|
|
76
|
+
".claude/daemon/",
|
|
77
|
+
".claude/teams/",
|
|
78
|
+
".claude/sessions/",
|
|
79
|
+
".cursor/worktrees/",
|
|
80
|
+
)
|
|
81
|
+
GATED_GIT_SUBCOMMANDS = frozenset({"commit", "push"})
|
|
82
|
+
ALL_GIT_BINARY_NAMES = frozenset({"git", "git.exe"})
|
|
83
|
+
VALUE_TAKING_GIT_OPTIONS = frozenset({"-C", "-c", "--git-dir", "--work-tree", "--namespace"})
|
|
84
|
+
REPO_DIRECTORY_OPTION = "-C"
|
|
85
|
+
WORK_TREE_OPTION = "--work-tree"
|
|
86
|
+
DIRECTORY_CHANGE_VERBS = frozenset({"cd", "pushd", "set-location", "sl"})
|
|
87
|
+
DIRECTORY_CHANGE_PATH_OPTIONS = frozenset({"-path", "-literalpath"})
|
|
88
|
+
DIRECTORY_CHANGE_OPTION_TERMINATOR = "--"
|
|
89
|
+
DIRECTORY_CHANGE_PATTERN_PREFIX = r"(?:^|(?<=[\s;&|(]))(?:"
|
|
90
|
+
DIRECTORY_CHANGE_PATTERN_SUFFIX = r")(?=\s|$)"
|
|
91
|
+
DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN = r"(?:[ \t]+(?:%s)(?=\s|$))*"
|
|
92
|
+
DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?\S*"
|
|
93
|
+
CLAUDE_HOME_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
94
|
+
VERDICT_DIRECTORY_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
95
|
+
COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN = r"[;&|\n][\s]*\S"
|
|
96
|
+
OPTION_WITH_VALUE_STEP = 2
|
|
97
|
+
ALL_GATED_TOOL_NAMES = ("Bash", "PowerShell")
|
|
98
|
+
HASH_PREVIEW_LENGTH = 16
|
|
99
|
+
MINTING_AGENT_TYPE = "code-verifier"
|
|
100
|
+
VERDICT_DIRECTORY_GUARD_MESSAGE = (
|
|
101
|
+
"BLOCKED: [VERDICT_DIRECTORY_GUARD] Shell access to the verification "
|
|
102
|
+
"verdict directory (~/.claude/verification/) is denied. Only the "
|
|
103
|
+
"verifier_verdict_minter.py SubagentStop hook writes verdict files; a "
|
|
104
|
+
"shell write here would forge a passing verdict and defeat the "
|
|
105
|
+
"verified-commit gate. Spawn the code-verifier agent to earn a verdict "
|
|
106
|
+
"instead of writing one."
|
|
107
|
+
)
|
|
108
|
+
CORRECTIVE_MESSAGE = (
|
|
109
|
+
"BLOCKED: [VERIFIED_COMMIT_GATE] This branch surface has no passing "
|
|
110
|
+
"verification verdict. Spawn the code-verifier agent (Agent tool, "
|
|
111
|
+
"subagent_type 'code-verifier') with the task texts, the diff scope, "
|
|
112
|
+
"and recorded baselines; when it finishes with a clean verdict the "
|
|
113
|
+
"SubagentStop hook mints the verdict and this command will pass. Any "
|
|
114
|
+
"file change after verification invalidates the verdict, so verify "
|
|
115
|
+
"last. Exempt automatically: docs/image files, pytest test files, and "
|
|
116
|
+
"Python files whose docstring- and comment-stripped AST is unchanged "
|
|
117
|
+
"(comment-only edits in non-Python files are not exempt)."
|
|
118
|
+
)
|