claude-dev-env 1.80.0 → 1.81.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/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
- package/audit-rubrics/CLAUDE.md +3 -2
- package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
- package/audit-rubrics/prompts/CLAUDE.md +2 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_annotations_length.py +59 -11
- package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
- package/hooks/blocking/code_rules_command_dispatch.py +140 -0
- package/hooks/blocking/code_rules_docstrings.py +93 -0
- package/hooks/blocking/code_rules_enforcer.py +54 -4
- package/hooks/blocking/code_rules_js_conventions.py +246 -0
- package/hooks/blocking/code_rules_naming_collection.py +5 -0
- package/hooks/blocking/code_rules_test_assertions.py +3 -0
- package/hooks/blocking/code_rules_unused_imports.py +0 -3
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
- package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +16 -3
- package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/blocking_check_limits.py +9 -0
- package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
- package/hooks/hooks_constants/js_conventions_constants.py +54 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +2 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
- package/skills/autoconverge/SKILL.md +2 -2
- package/skills/autoconverge/reference/convergence.md +3 -3
- package/skills/autoconverge/reference/stop-conditions.md +1 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +245 -1
- package/skills/autoconverge/workflow/converge.mjs +129 -23
|
@@ -95,10 +95,18 @@ def test_should_not_flag_name_containing_banned_substring() -> None:
|
|
|
95
95
|
assert issues == []
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def
|
|
98
|
+
def test_should_scope_test_file_banned_identifier_to_changed_lines() -> None:
|
|
99
|
+
content = "def test_thing():\n result = compute()\n assert result\n"
|
|
100
|
+
unchanged_only = check_banned_identifiers(content, TEST_FILE_PATH, {1, 3})
|
|
101
|
+
assert unchanged_only == []
|
|
102
|
+
on_changed_line = check_banned_identifiers(content, TEST_FILE_PATH, {2})
|
|
103
|
+
assert any("result" in each_issue for each_issue in on_changed_line)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_should_flag_newly_written_test_file_banned_identifier() -> None:
|
|
99
107
|
content = "def test_thing():\n result = compute()\n assert result\n"
|
|
100
108
|
issues = check_banned_identifiers(content, TEST_FILE_PATH)
|
|
101
|
-
assert
|
|
109
|
+
assert any("'result'" in each_issue for each_issue in issues)
|
|
102
110
|
|
|
103
111
|
|
|
104
112
|
def test_should_skip_hook_infrastructure() -> None:
|
|
@@ -117,11 +117,15 @@ def test_should_flag_btn_assignment() -> None:
|
|
|
117
117
|
)
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
def
|
|
120
|
+
def test_should_scope_import_alias_in_test_file_to_changed_lines() -> None:
|
|
121
121
|
content = "from config import rebase_constants as rc\n"
|
|
122
|
-
|
|
123
|
-
assert
|
|
124
|
-
f"
|
|
122
|
+
unchanged_only = check_banned_identifiers(content, TEST_FILE_PATH, {2})
|
|
123
|
+
assert unchanged_only == [], (
|
|
124
|
+
f"A banned alias on an untouched line must not block, got: {unchanged_only}"
|
|
125
|
+
)
|
|
126
|
+
on_changed_line = check_banned_identifiers(content, TEST_FILE_PATH, {1})
|
|
127
|
+
assert any("'rc'" in each_issue for each_issue in on_changed_line), (
|
|
128
|
+
f"A newly written banned alias in a test file must flag, got: {on_changed_line}"
|
|
125
129
|
)
|
|
126
130
|
|
|
127
131
|
|
|
@@ -10,8 +10,12 @@ orphan CSS class slip past the gate again.
|
|
|
10
10
|
This module reads ``validate_content``'s source and asserts every ``check_*``
|
|
11
11
|
attribute on the enforcer module appears in it. A check that is intentionally
|
|
12
12
|
not wired must be listed in ``KNOWN_UNDISPATCHED_CHECKS`` with a reason in this
|
|
13
|
-
docstring
|
|
14
|
-
|
|
13
|
+
docstring. ``check_unanchored_command_dispatch`` is listed there: it guards a
|
|
14
|
+
``hooks/blocking`` command classifier, and the whole ``validate_content`` verdict
|
|
15
|
+
stays off hook-infrastructure files, so the enforcer dispatches it from
|
|
16
|
+
``_hook_infrastructure_blocking_issues`` instead. The companion
|
|
17
|
+
``test_code_rules_enforcer_cap_meta.py`` guards the payload-cap convention; this
|
|
18
|
+
module guards the wiring.
|
|
15
19
|
"""
|
|
16
20
|
|
|
17
21
|
from __future__ import annotations
|
|
@@ -34,7 +38,9 @@ assert _hook_specification.loader is not None
|
|
|
34
38
|
_hook_module = importlib.util.module_from_spec(_hook_specification)
|
|
35
39
|
_hook_specification.loader.exec_module(_hook_module)
|
|
36
40
|
|
|
37
|
-
KNOWN_UNDISPATCHED_CHECKS: frozenset[str] = frozenset(
|
|
41
|
+
KNOWN_UNDISPATCHED_CHECKS: frozenset[str] = frozenset(
|
|
42
|
+
{"check_unanchored_command_dispatch"}
|
|
43
|
+
)
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
def _all_check_function_names() -> list[str]:
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Tests for check_docstring_names_absent_type_checking_gate — Category O6.
|
|
2
|
+
|
|
3
|
+
A module or function docstring that names a ``TYPE_CHECKING`` gate-detection step,
|
|
4
|
+
or a ``type-checking-gate`` helper family, drifts once no identifier in the body
|
|
5
|
+
handles TYPE_CHECKING: the prose points a reader at machinery the module does not
|
|
6
|
+
hold. This is the deterministic slice of Category O6 (docstring prose versus
|
|
7
|
+
implementation drift) for a TYPE_CHECKING gate claim. The check covers hook
|
|
8
|
+
infrastructure, where the import-scan gates that carry this drift class live.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import ModuleType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_enforcer_module() -> ModuleType:
|
|
19
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
20
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
21
|
+
assert spec is not None
|
|
22
|
+
assert spec.loader is not None
|
|
23
|
+
module = importlib.util.module_from_spec(spec)
|
|
24
|
+
spec.loader.exec_module(module)
|
|
25
|
+
return module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_type_checking_gate(content: str, file_path: str) -> list[str]:
|
|
32
|
+
return code_rules_enforcer.check_docstring_names_absent_type_checking_gate(content, file_path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PRODUCTION_HOOK_PATH = "/home/user/.claude/hooks/blocking/code_rules_unused_imports.py"
|
|
36
|
+
TEST_FILE_PATH = "/home/user/.claude/hooks/blocking/test_code_rules_unused_imports.py"
|
|
37
|
+
MYPY_GATE_PRODUCTION_PATH = "/home/user/project/scripts/run_mypy_gate.py"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_flags_module_docstring_naming_type_checking_gate_helpers() -> None:
|
|
41
|
+
content = (
|
|
42
|
+
'"""Unused module-level import check and its import-range and '
|
|
43
|
+
'type-checking-gate helpers."""\n'
|
|
44
|
+
"\n"
|
|
45
|
+
"import ast\n"
|
|
46
|
+
"\n"
|
|
47
|
+
"\n"
|
|
48
|
+
"def check_unused(content: str) -> list[str]:\n"
|
|
49
|
+
' """Flag unused imports."""\n'
|
|
50
|
+
" return []\n"
|
|
51
|
+
)
|
|
52
|
+
issues = check_type_checking_gate(content, PRODUCTION_HOOK_PATH)
|
|
53
|
+
assert len(issues) == 1
|
|
54
|
+
assert issues[0].startswith("Line 1:")
|
|
55
|
+
assert "type-checking-gate" in issues[0]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_flags_function_docstring_naming_type_checking_gate_detection() -> None:
|
|
59
|
+
content = (
|
|
60
|
+
"import ast\n"
|
|
61
|
+
"\n"
|
|
62
|
+
"\n"
|
|
63
|
+
"def check_unused_module_level_imports(content: str) -> list[str]:\n"
|
|
64
|
+
' """Flag module-level imports never referenced.\n'
|
|
65
|
+
"\n"
|
|
66
|
+
" When ``full_file_content`` is provided, the ``__all__`` /\n"
|
|
67
|
+
" ``TYPE_CHECKING`` gate detection and reference scanning run against\n"
|
|
68
|
+
" ``full_file_content``.\n"
|
|
69
|
+
' """\n'
|
|
70
|
+
" return []\n"
|
|
71
|
+
)
|
|
72
|
+
issues = check_type_checking_gate(content, PRODUCTION_HOOK_PATH)
|
|
73
|
+
assert len(issues) == 1
|
|
74
|
+
assert "type_checking gate" in issues[0]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_flags_both_module_and_function_docstrings_on_the_real_drift() -> None:
|
|
78
|
+
content = (
|
|
79
|
+
'"""Unused module-level import check and its import-range and '
|
|
80
|
+
'type-checking-gate helpers."""\n'
|
|
81
|
+
"\n"
|
|
82
|
+
"import ast\n"
|
|
83
|
+
"\n"
|
|
84
|
+
"\n"
|
|
85
|
+
"def check_unused_module_level_imports(content: str) -> list[str]:\n"
|
|
86
|
+
' """Flag module-level imports never referenced.\n'
|
|
87
|
+
"\n"
|
|
88
|
+
" The ``__all__`` / ``TYPE_CHECKING`` gate detection and reference\n"
|
|
89
|
+
" scanning run against ``full_file_content``.\n"
|
|
90
|
+
' """\n'
|
|
91
|
+
" return []\n"
|
|
92
|
+
)
|
|
93
|
+
issues = check_type_checking_gate(content, PRODUCTION_HOOK_PATH)
|
|
94
|
+
assert len(issues) == 2
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_passes_when_code_declares_a_type_checking_gate_helper() -> None:
|
|
98
|
+
content = (
|
|
99
|
+
'"""Unused module-level import check and its type-checking-gate helpers."""\n'
|
|
100
|
+
"\n"
|
|
101
|
+
"import ast\n"
|
|
102
|
+
"\n"
|
|
103
|
+
"\n"
|
|
104
|
+
"def _module_body_declares_type_checking_gate(tree: ast.Module) -> bool:\n"
|
|
105
|
+
' """Return True when the module guards imports behind TYPE_CHECKING."""\n'
|
|
106
|
+
" return False\n"
|
|
107
|
+
)
|
|
108
|
+
assert check_type_checking_gate(content, PRODUCTION_HOOK_PATH) == []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_passes_when_body_names_type_checking_guard() -> None:
|
|
112
|
+
content = (
|
|
113
|
+
"import ast\n"
|
|
114
|
+
"from typing import TYPE_CHECKING\n"
|
|
115
|
+
"\n"
|
|
116
|
+
"\n"
|
|
117
|
+
"def check_unused(content: str) -> list[str]:\n"
|
|
118
|
+
' """Skip imports the ``TYPE_CHECKING`` gate detection guards."""\n'
|
|
119
|
+
" if TYPE_CHECKING:\n"
|
|
120
|
+
" return []\n"
|
|
121
|
+
" return []\n"
|
|
122
|
+
)
|
|
123
|
+
assert check_type_checking_gate(content, PRODUCTION_HOOK_PATH) == []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_passes_when_no_docstring_names_the_gate() -> None:
|
|
127
|
+
content = (
|
|
128
|
+
'"""Unused module-level import check and its import-range helpers."""\n'
|
|
129
|
+
"\n"
|
|
130
|
+
"import ast\n"
|
|
131
|
+
"\n"
|
|
132
|
+
"\n"
|
|
133
|
+
"def check_unused(content: str) -> list[str]:\n"
|
|
134
|
+
' """Flag module-level imports never referenced."""\n'
|
|
135
|
+
" return []\n"
|
|
136
|
+
)
|
|
137
|
+
assert check_type_checking_gate(content, PRODUCTION_HOOK_PATH) == []
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_passes_on_mypy_static_type_checker_gate_docstring() -> None:
|
|
141
|
+
content = (
|
|
142
|
+
"\"\"\"Run the project's type-checking gate and report failures.\"\"\"\n"
|
|
143
|
+
"\n"
|
|
144
|
+
"import subprocess\n"
|
|
145
|
+
"\n"
|
|
146
|
+
"\n"
|
|
147
|
+
"def run_gate() -> int:\n"
|
|
148
|
+
' return subprocess.run(["mypy", "hooks"]).returncode\n'
|
|
149
|
+
)
|
|
150
|
+
assert check_type_checking_gate(content, MYPY_GATE_PRODUCTION_PATH) == []
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_test_files_are_exempt() -> None:
|
|
154
|
+
content = (
|
|
155
|
+
'"""Unused module-level import check and its type-checking-gate helpers."""\n'
|
|
156
|
+
"\n"
|
|
157
|
+
"import ast\n"
|
|
158
|
+
"\n"
|
|
159
|
+
"\n"
|
|
160
|
+
"def check_unused(content: str) -> list[str]:\n"
|
|
161
|
+
' """Flag unused imports."""\n'
|
|
162
|
+
" return []\n"
|
|
163
|
+
)
|
|
164
|
+
assert check_type_checking_gate(content, TEST_FILE_PATH) == []
|
|
@@ -190,15 +190,28 @@ def test_should_flag_dead_import_in_workflow_handler_file() -> None:
|
|
|
190
190
|
)
|
|
191
191
|
|
|
192
192
|
|
|
193
|
-
def
|
|
193
|
+
def test_should_flag_unused_import_in_test_file() -> None:
|
|
194
194
|
source = (
|
|
195
|
-
"
|
|
195
|
+
"import asyncio\n"
|
|
196
196
|
"\n"
|
|
197
197
|
"def test_thing() -> None:\n"
|
|
198
198
|
" assert True\n"
|
|
199
199
|
)
|
|
200
200
|
issues = check_unused_module_level_imports(source, TEST_FILE_PATH)
|
|
201
|
-
assert
|
|
201
|
+
assert any("asyncio" in each_issue for each_issue in issues), (
|
|
202
|
+
f"Unused import in a test file must be flagged, got: {issues}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_should_not_flag_used_import_in_test_file() -> None:
|
|
207
|
+
source = (
|
|
208
|
+
"import asyncio\n"
|
|
209
|
+
"\n"
|
|
210
|
+
"def test_thing() -> None:\n"
|
|
211
|
+
" asyncio.run(main())\n"
|
|
212
|
+
)
|
|
213
|
+
issues = check_unused_module_level_imports(source, TEST_FILE_PATH)
|
|
214
|
+
assert issues == [], f"Used import in a test file must not be flagged, got: {issues}"
|
|
202
215
|
|
|
203
216
|
|
|
204
217
|
def test_should_handle_syntax_error_gracefully() -> None:
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Behavioral tests for the JavaScript boolean-naming and banned-identifier checks."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
7
|
+
if _blocking_directory not in sys.path:
|
|
8
|
+
sys.path.insert(0, _blocking_directory)
|
|
9
|
+
|
|
10
|
+
from code_rules_js_conventions import ( # noqa: E402
|
|
11
|
+
check_js_banned_identifiers,
|
|
12
|
+
check_js_boolean_naming,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
CONVERGE_PATH = "packages/claude-dev-env/skills/autoconverge/workflow/converge.mjs"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_flags_unprefixed_boolean_let_declaration() -> None:
|
|
19
|
+
content = "let hardeningPrOpened = false\n"
|
|
20
|
+
issues = check_js_boolean_naming(content, CONVERGE_PATH)
|
|
21
|
+
assert len(issues) == 1
|
|
22
|
+
assert "hardeningPrOpened" in issues[0]
|
|
23
|
+
assert "Line 1" in issues[0]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_flags_unprefixed_boolean_negation_declaration() -> None:
|
|
27
|
+
content = "const readyState = !pending\n"
|
|
28
|
+
issues = check_js_boolean_naming(content, CONVERGE_PATH)
|
|
29
|
+
assert len(issues) == 1
|
|
30
|
+
assert "readyState" in issues[0]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_flags_pure_negation_declaration() -> None:
|
|
34
|
+
content = "const ready = !pending;\n"
|
|
35
|
+
issues = check_js_boolean_naming(content, CONVERGE_PATH)
|
|
36
|
+
assert len(issues) == 1
|
|
37
|
+
assert "ready" in issues[0]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_does_not_flag_ternary_negation_string_declaration() -> None:
|
|
41
|
+
content = 'const label = !isActive ? "on" : "off";\n'
|
|
42
|
+
assert check_js_boolean_naming(content, CONVERGE_PATH) == []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_does_not_flag_ternary_negation_number_declaration() -> None:
|
|
46
|
+
content = "const count = !arr.length ? 0 : arr.length;\n"
|
|
47
|
+
assert check_js_boolean_naming(content, CONVERGE_PATH) == []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_does_not_flag_negation_logical_and_declaration() -> None:
|
|
51
|
+
content = "const name = !err && getName();\n"
|
|
52
|
+
assert check_js_boolean_naming(content, CONVERGE_PATH) == []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_does_not_flag_negation_logical_or_declaration() -> None:
|
|
56
|
+
content = "const port = !custom || 8080;\n"
|
|
57
|
+
assert check_js_boolean_naming(content, CONVERGE_PATH) == []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_flags_unprefixed_boolean_jsdoc_param() -> None:
|
|
61
|
+
content = (
|
|
62
|
+
"/**\n"
|
|
63
|
+
" * @param {boolean} alreadyOpened whether the PR is open\n"
|
|
64
|
+
" */\n"
|
|
65
|
+
"function note(alreadyOpened) { return alreadyOpened }\n"
|
|
66
|
+
)
|
|
67
|
+
issues = check_js_boolean_naming(content, CONVERGE_PATH)
|
|
68
|
+
assert len(issues) == 1
|
|
69
|
+
assert "alreadyOpened" in issues[0]
|
|
70
|
+
assert "parameter" in issues[0].lower()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_accepts_prefixed_boolean_declarations() -> None:
|
|
74
|
+
content = (
|
|
75
|
+
"let isReady = true\n"
|
|
76
|
+
"const hasItems = false\n"
|
|
77
|
+
"let shouldRetry = !done\n"
|
|
78
|
+
"var wasOpened = true\n"
|
|
79
|
+
)
|
|
80
|
+
assert check_js_boolean_naming(content, CONVERGE_PATH) == []
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_accepts_prefixed_boolean_jsdoc_param() -> None:
|
|
84
|
+
content = (
|
|
85
|
+
"/**\n"
|
|
86
|
+
" * @param {boolean} isOpen whether the PR is open\n"
|
|
87
|
+
" */\n"
|
|
88
|
+
"function note(isOpen) { return isOpen }\n"
|
|
89
|
+
)
|
|
90
|
+
assert check_js_boolean_naming(content, CONVERGE_PATH) == []
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_ignores_boolean_declaration_inside_string_literal() -> None:
|
|
94
|
+
content = 'const label = "let flagged = false"\n'
|
|
95
|
+
assert check_js_boolean_naming(content, CONVERGE_PATH) == []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_ignores_boolean_declaration_inside_line_comment() -> None:
|
|
99
|
+
content = "// let flagged = false\nconst isReady = true\n"
|
|
100
|
+
assert check_js_boolean_naming(content, CONVERGE_PATH) == []
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_does_not_flag_falsely_named_non_boolean_declaration() -> None:
|
|
104
|
+
content = "const counted = falsely()\n"
|
|
105
|
+
assert check_js_boolean_naming(content, CONVERGE_PATH) == []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_boolean_naming_scoped_to_changed_lines() -> None:
|
|
109
|
+
content = "let alreadyRun = false\nconst isReady = true\n"
|
|
110
|
+
unchanged_only = check_js_boolean_naming(content, CONVERGE_PATH, {2})
|
|
111
|
+
assert unchanged_only == []
|
|
112
|
+
changed = check_js_boolean_naming(content, CONVERGE_PATH, {1})
|
|
113
|
+
assert len(changed) == 1
|
|
114
|
+
assert "alreadyRun" in changed[0]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_boolean_naming_defer_scope_returns_all() -> None:
|
|
118
|
+
content = "let alreadyRun = false\n"
|
|
119
|
+
deferred = check_js_boolean_naming(content, CONVERGE_PATH, None, True)
|
|
120
|
+
assert len(deferred) == 1
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_boolean_naming_exempts_test_files() -> None:
|
|
124
|
+
content = "let alreadyRun = false\n"
|
|
125
|
+
assert check_js_boolean_naming(content, "workflow/converge.test.mjs") == []
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_flags_banned_identifier_declaration() -> None:
|
|
129
|
+
content = "let data = fetchThings()\n"
|
|
130
|
+
issues = check_js_banned_identifiers(content, CONVERGE_PATH)
|
|
131
|
+
assert len(issues) == 1
|
|
132
|
+
assert "data" in issues[0]
|
|
133
|
+
assert "Line 1" in issues[0]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_accepts_descriptive_identifier_declaration() -> None:
|
|
137
|
+
content = "let allOrders = fetchThings()\n"
|
|
138
|
+
assert check_js_banned_identifiers(content, CONVERGE_PATH) == []
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_banned_identifier_message_carries_descriptive_name_guidance() -> None:
|
|
142
|
+
content = "let data = fetchThings()\n"
|
|
143
|
+
issues = check_js_banned_identifiers(content, CONVERGE_PATH)
|
|
144
|
+
assert "use descriptive name (see CODE_RULES Naming section)" in issues[0]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_flags_each_banned_identifier_name() -> None:
|
|
148
|
+
content = "const result = compute()\nlet response = call()\nvar ctx = build()\n"
|
|
149
|
+
issues = check_js_banned_identifiers(content, CONVERGE_PATH)
|
|
150
|
+
assert len(issues) == 3
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_banned_identifier_ignores_string_literal() -> None:
|
|
154
|
+
content = 'const label = "let data = 1"\n'
|
|
155
|
+
assert check_js_banned_identifiers(content, CONVERGE_PATH) == []
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_banned_identifier_scoped_to_changed_lines() -> None:
|
|
159
|
+
content = "let data = one()\nconst allOrders = two()\n"
|
|
160
|
+
assert check_js_banned_identifiers(content, CONVERGE_PATH, {2}) == []
|
|
161
|
+
changed = check_js_banned_identifiers(content, CONVERGE_PATH, {1})
|
|
162
|
+
assert len(changed) == 1
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_banned_identifier_exempts_test_files() -> None:
|
|
166
|
+
content = "let data = one()\n"
|
|
167
|
+
assert check_js_banned_identifiers(content, "workflow/converge.test.mjs") == []
|
|
@@ -16,6 +16,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
16
16
|
| `env_var_table_code_drift_constants.py` | Table patterns, env-var-name and code-file recognizers, scan budget, and block-message text for the env-var-table code-drift blocker |
|
|
17
17
|
| `code_rules_path_utils_constants.py` | Path-matching helpers used by the code-rules check modules |
|
|
18
18
|
| `code_verifier_spawn_preflight_gate_constants.py` | Subagent type, merge-tree command flags, timeouts, and deny-message text for the code-verifier spawn pre-flight gate |
|
|
19
|
+
| `command_dispatch_constants.py` | Command-word regex, command-key access pattern, tokenization pattern, and anchors for the unanchored command-dispatch meta-gate |
|
|
19
20
|
| `convergence_branch_constants.py` | Branch and worktree naming patterns for the convergence gate |
|
|
20
21
|
| `dead_argparse_argument_constants.py` | Patterns for detecting unused argparse arguments |
|
|
21
22
|
| `dead_config_field_constants.py` | Patterns for detecting unused `*Config` / `*Selectors` dataclass fields |
|
|
@@ -34,6 +35,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
34
35
|
| `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
|
|
35
36
|
| `html_companion_constants.py` | Blocked URL schemes and other config for the `.md`-to-`.html` companion hook |
|
|
36
37
|
| `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
|
|
38
|
+
| `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
|
|
37
39
|
| `md_to_html_blocker_constants.py` | Path exemptions and trigger patterns for the markdown-to-html blocker |
|
|
38
40
|
| `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
|
|
39
41
|
| `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
|
|
@@ -10,6 +10,8 @@ from __future__ import annotations
|
|
|
10
10
|
import re
|
|
11
11
|
|
|
12
12
|
MAX_BANNED_PREFIX_ISSUES: int = 3
|
|
13
|
+
MAX_POLARITY_CONTRADICTION_ISSUES: int = 3
|
|
14
|
+
MAX_VACUOUS_CLEANUP_ASSERTION_ISSUES: int = 3
|
|
13
15
|
MAX_STUB_IMPLEMENTATION_ISSUES: int = 3
|
|
14
16
|
MAX_TYPED_DICT_PAIR_ISSUES: int = 3
|
|
15
17
|
MAX_TEST_BRANCHING_ISSUES: int = 3
|
|
@@ -239,6 +241,13 @@ ALL_PATH_METADATA_ACCESS_METHOD_NAMES: frozenset[str] = frozenset(
|
|
|
239
241
|
{"is_file", "is_dir", "exists", "stat", "lstat"}
|
|
240
242
|
)
|
|
241
243
|
|
|
244
|
+
MAX_DOCSTRING_TYPE_CHECKING_GATE_ISSUES: int = 3
|
|
245
|
+
TYPE_CHECKING_IDENTIFIER_MARKER: str = "type_checking"
|
|
246
|
+
ALL_ABSENT_TYPE_CHECKING_GATE_DOCSTRING_PHRASES: tuple[str, ...] = (
|
|
247
|
+
"type_checking gate",
|
|
248
|
+
"type-checking-gate",
|
|
249
|
+
)
|
|
250
|
+
|
|
242
251
|
MAX_DOCSTRING_INLINE_LITERAL_CLAIM_ISSUES: int = 3
|
|
243
252
|
ALL_DOCSTRING_NO_INLINE_LITERAL_CLAIM_PHRASES: tuple[str, ...] = (
|
|
244
253
|
"no literals appear inline",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Constants for the unanchored command-dispatch meta-gate."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
COMMAND_DISPATCH_PATH_MARKER: str = "hooks/blocking"
|
|
6
|
+
|
|
7
|
+
COMMAND_DISPATCH_LITERAL_PATTERN: re.Pattern[str] = re.compile(
|
|
8
|
+
r"\b(?:gh|git|npm|npx|node|python3?|pwsh|powershell|docker|kubectl|pip|cargo"
|
|
9
|
+
r"|yarn|pnpm)\\s[+*]"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
COMMAND_KEY_ACCESS_PATTERN: re.Pattern[str] = re.compile(
|
|
13
|
+
r"""(?:\[\s*|\.get\(\s*)["']command["']"""
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
FIRST_TOKEN_TOKENIZATION_PATTERN: re.Pattern[str] = re.compile(
|
|
17
|
+
r"shlex\.split|\.split\("
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ALL_REGEX_START_ANCHOR_TOKENS: tuple[str, ...] = ("^", "\\A")
|
|
21
|
+
|
|
22
|
+
COMMAND_DISPATCH_MESSAGE_SUFFIX: str = (
|
|
23
|
+
"matches a multi-word command as a substring - anchor the pattern to the "
|
|
24
|
+
"start of the command (^) or tokenize the first word (shlex.split) so a "
|
|
25
|
+
"command like 'echo gh pr create' is not matched"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
MAX_COMMAND_DISPATCH_ISSUES: int = 20
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Configuration constants for the JavaScript convention checks in code_rules_enforcer."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
ALL_JAVASCRIPT_BANNED_IDENTIFIERS: frozenset[str] = frozenset(
|
|
6
|
+
{
|
|
7
|
+
"result",
|
|
8
|
+
"data",
|
|
9
|
+
"output",
|
|
10
|
+
"response",
|
|
11
|
+
"value",
|
|
12
|
+
"item",
|
|
13
|
+
"temp",
|
|
14
|
+
"ctx",
|
|
15
|
+
"cfg",
|
|
16
|
+
"msg",
|
|
17
|
+
"btn",
|
|
18
|
+
"idx",
|
|
19
|
+
"cnt",
|
|
20
|
+
"tmp",
|
|
21
|
+
"elem",
|
|
22
|
+
"val",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_JAVASCRIPT_NEGATION_OPERAND: str = (
|
|
27
|
+
r"[A-Za-z_$][\w$]*"
|
|
28
|
+
r"(?:\s*\.\s*[A-Za-z_$][\w$]*|\s*\((?:[^()]|\([^()]*\))*\))*"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
JAVASCRIPT_BOOLEAN_DECLARATION_PATTERN: re.Pattern[str] = re.compile(
|
|
32
|
+
r"\b(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*=\s*"
|
|
33
|
+
r"(?:(?:true|false)\b|!\s*" + _JAVASCRIPT_NEGATION_OPERAND + r"\s*(?:;|$))"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
JAVASCRIPT_BOOLEAN_JSDOC_PARAMETER_PATTERN: re.Pattern[str] = re.compile(
|
|
37
|
+
r"@param\s*\{\s*boolean\s*\}\s+(?P<name>[A-Za-z_$][\w$]*)"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
JAVASCRIPT_DECLARATION_NAME_PATTERN: re.Pattern[str] = re.compile(
|
|
41
|
+
r"\b(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*="
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
JAVASCRIPT_BOOLEAN_PREFIX_PATTERN: re.Pattern[str] = re.compile(
|
|
45
|
+
r"^(?:is|has|should|can|was|did)(?:[A-Z0-9_]|$)"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
BOOLEAN_PREFIX_GUIDANCE: str = "prefix with is/has/should/can/was/did"
|
|
49
|
+
|
|
50
|
+
SINGLE_CHARACTER_NAME_LENGTH: int = 1
|
|
51
|
+
|
|
52
|
+
MAX_JAVASCRIPT_BOOLEAN_NAMING_ISSUES: int = 20
|
|
53
|
+
|
|
54
|
+
MAX_JAVASCRIPT_BANNED_IDENTIFIER_ISSUES: int = 20
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
When a docstring enumerates the behaviors a body applies, the enumeration covers every behavior the body applies. A reader trusts the list to be complete: an item the code applies but the prose omits is a silent gap that misleads every future reader and reviewer.
|
|
8
8
|
|
|
9
|
-
The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names.
|
|
9
|
+
The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Ten more gate validators each cover one deterministic slice of the free-form prose. `check_docstring_names_absent_type_checking_gate` covers a module or function docstring that names a `TYPE_CHECKING` gate-detection step, or a `type-checking-gate` helper family, while no identifier in the module's code carries the `type_checking` marker — the drift where the prose points a reader at a gate the body never performs; drop the `TYPE_CHECKING` gate wording, or add the detection the prose describes. `check_docstring_length_constant_superlative_vs_exact_gate` covers a length-constant module whose docstring describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`) while the only code consuming the constant compares `len(...)` against it with `==`/`!=` — an exact-length gate that rejects every other length — and never with an ordered operator; the check scans the constant module's package tree (its own directory, or the parent package when the module sits in a `config/` subdirectory), so the mismatching consumer may sit in a sibling module. `check_docstring_fallback_branch_coverage` covers a summary that scopes a fallback to a single condition (`only when`, `falls back to ... when`) while the body routes to that same fallback call from two or more distinct early-return guards. `check_class_docstring_names_public_methods` covers a class whose docstring is a single summary line while the class exposes two or more public methods whose names the summary never spells out — the drift where a one-line class summary keeps naming its first feature after the class grows a second public entry point. `check_docstring_no_consumer_claim` covers a producer docstring asserting that no consumer reads its output yet (`producer-only artifact`, `no submission-run consumer reads it yet`) — a transitional claim that drifts the moment a reader lands and contradicts any companion `SKILL.md` that documents the consumer; this is the deterministic slice of the O8 companion-doc producer/consumer drift below. `check_docstring_returns_plural_cardinality` covers a `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) while the returned dict literal holds exactly one key in that family (`sheen_mid`) — the drift where a single-key family carries a plural noun, so the prose claims a cardinality of two or more that the dict does not hold. `check_docstring_args_single_line_scope_vs_span` covers an `Args:` entry whose prose scopes a finding to a single named line (`only when its block-anchor line is among the changed lines`) while the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper — the drift where the Args entry claims a narrower single-line scope than the span-intersection body applies, so an edit touching any non-anchor line of the span still blocks. `check_docstring_cardinal_count_matches_constant_family` covers a docstring that states a cardinal count of an outcome family (`Covers the four outcome branches: ...`) and lists those members, while the module references more members of the same `UPPER_SNAKE` constant family than the count names (`OUTCOME_OFFENDER_UNREADABLE` is imported and exercised, yet the summary stops at four) — the drift where a summary keeps the old count after the code grows another branch; this gate runs on test modules as well as production modules. `check_docstring_raises_unraisable_largezipfile` covers a `Raises:` clause that names `zipfile.LargeZipFile` while the function opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` left at its default of True — `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False, so a writer that allows ZIP64 documents an exception the body cannot produce. `check_docstring_no_network_claim_with_metadata_access` covers a function docstring promising a code path returns `without touching the network` (or a sibling no-network phrase) while the body calls a path-metadata method (`is_file`, `is_dir`, `exists`, `stat`, `lstat`) — on a network share each metadata call is a round-trip over the wire, so a cache-hit path the docstring swore avoids the network still pays a stat on every call; reword the claim to state the path is stat-checked on every call, or short-circuit to the cached path before the share is touched. The remaining free-form prose — `"a field counts as read when ..."`, `"resolves to shared temp only"`, `"strip ceremony, then drop blockquotes"`, and the broader module-responsibility paragraph outside the user-facing-text-scope slice the checklist below names — has no signature, method roster, or single structural shape to compare against, so the gate cannot catch its drift. This rule is the judgment standard for that prose; the audit lane below is the enforcement for everything outside the eleven gated slices.
|
|
10
10
|
|
|
11
11
|
## What to check before you write the docstring
|
|
12
12
|
|
|
@@ -28,6 +28,7 @@ Read the body and the docstring side by side:
|
|
|
28
28
|
- **Predicate breadth.** A boolean helper whose prose promises a narrow check accepts only the inputs the prose names — no broader input class the name and prose do not mention.
|
|
29
29
|
- **Exclusion-clause distinguisher.** A docstring sentence that says a named category of input "are not" / "is not" the thing the function flags (`plain logging, screenshot, or method-on-local calls inside a branch are not dispatch steps`) keys the exclusion to the same axis the body's classification keys on. When the body decides on one axis (a call sits in an `If.test` guard versus a plain statement) but the prose excludes on a different axis (the call's receiver shape — a method on a local), the exclusion clause names a category the body still flags: a guarded method-on-local call is flagged even though the prose lists method-on-local calls as excluded. Read the body's actual branch condition, then state the exclusion on that same axis (`plain (unguarded) calls inside a branch body are not dispatch steps`), so every member the prose excludes is a member the body also excludes.
|
|
30
30
|
- **Companion-doc ordering and content claims.** A `SKILL.md` (or sibling `.md`) sentence that names a produced artifact and claims its order (`sorted`, `alphabetical`, `in sorted order`) or its content (`the at-risk names`, `just the current set`) matches the producer function's docstring and body for that same artifact. A producer that builds the artifact by merging stored names with new names and appending — preserving file order, not re-sorting the union — leaves a doc that still says `sorted` drifted on both counts: the order claim is wrong, and the content claim hides the merged-in prior entries. When the producer's ordering or union changes, the same change updates the companion doc. The two move together in one commit, even when the producer edit does not touch the `.md` file.
|
|
31
|
+
- **TYPE_CHECKING gate claim vs code.** A docstring that names a `TYPE_CHECKING` gate-detection step, or a `type-checking-gate` helper family, matches a module whose code handles TYPE_CHECKING. When no identifier in the body carries the `type_checking` marker — no `TYPE_CHECKING` load, import alias, attribute, or helper name — the prose points a reader at a gate the module never performs. State what the module does, or add the detection the prose describes. The `check_docstring_names_absent_type_checking_gate` gate blocks this drift at Write/Edit time, and covers hook infrastructure, where the import-scan gates that carry this drift class live.
|
|
31
32
|
|
|
32
33
|
When the body changes the set of behaviors it applies, the same edit updates the prose enumeration. The two move together in one commit.
|
|
33
34
|
|