claude-dev-env 1.80.0 → 1.82.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 +3 -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/conventional_pr_title_gate.py +444 -0
- 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/blocking/test_conventional_pr_title_gate.py +640 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -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/conventional_pr_title_gate_constants.py +58 -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 +92 -7
- package/skills/autoconverge/reference/convergence.md +3 -3
- package/skills/autoconverge/reference/gotchas.md +5 -5
- package/skills/autoconverge/reference/stop-conditions.md +1 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +161 -29
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +342 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +178 -32
- package/skills/autoconverge/workflow/converge_multi.mjs +6 -2
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +26 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Meta-gate flagging an unanchored multi-word command pattern in a blocker.
|
|
2
|
+
|
|
3
|
+
A hook that classifies a shell command reads the command text and matches it
|
|
4
|
+
against a regex. When that regex names a multi-word command such as
|
|
5
|
+
``gh\\s+pr\\s+(create|edit)`` and is matched with a bare ``re.search`` — no
|
|
6
|
+
start anchor and no first-word tokenization — the pattern matches the command
|
|
7
|
+
as a substring anywhere in the string. A benign command like
|
|
8
|
+
``echo gh pr create --title x`` then trips the gate. This check flags that shape
|
|
9
|
+
at write time on files under ``hooks/blocking/`` so a new blocker anchors its
|
|
10
|
+
command match to the start of the command or tokenizes the first word.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
18
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
19
|
+
if _blocking_directory not in sys.path:
|
|
20
|
+
sys.path.insert(0, _blocking_directory)
|
|
21
|
+
if _hooks_directory not in sys.path:
|
|
22
|
+
sys.path.insert(0, _hooks_directory)
|
|
23
|
+
|
|
24
|
+
from code_rules_shared import ( # noqa: E402
|
|
25
|
+
_scope_violations_to_changed_lines,
|
|
26
|
+
is_test_file,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from hooks_constants.command_dispatch_constants import ( # noqa: E402
|
|
30
|
+
ALL_REGEX_START_ANCHOR_TOKENS,
|
|
31
|
+
COMMAND_DISPATCH_LITERAL_PATTERN,
|
|
32
|
+
COMMAND_DISPATCH_MESSAGE_SUFFIX,
|
|
33
|
+
COMMAND_DISPATCH_PATH_MARKER,
|
|
34
|
+
COMMAND_KEY_ACCESS_PATTERN,
|
|
35
|
+
FIRST_TOKEN_TOKENIZATION_PATTERN,
|
|
36
|
+
MAX_COMMAND_DISPATCH_ISSUES,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_under_hooks_blocking(file_path: str) -> bool:
|
|
41
|
+
"""Return whether the path sits under the ``hooks/blocking`` directory."""
|
|
42
|
+
return COMMAND_DISPATCH_PATH_MARKER in file_path.replace("\\", "/")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _command_literal_is_anchored(literal_value: str, match_start: int) -> bool:
|
|
46
|
+
"""Return whether a start anchor precedes the command word in the pattern.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
literal_value: The regex string literal the command word appears in.
|
|
50
|
+
match_start: The index where the command word match begins.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True when a ``^`` or ``\\A`` anchor appears before the command word, so
|
|
54
|
+
the pattern binds the command to the start of the string.
|
|
55
|
+
"""
|
|
56
|
+
prefix = literal_value[:match_start]
|
|
57
|
+
return any(each_anchor in prefix for each_anchor in ALL_REGEX_START_ANCHOR_TOKENS)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _unanchored_command_literals(
|
|
61
|
+
parsed_tree: ast.AST,
|
|
62
|
+
) -> list[tuple[range, str]]:
|
|
63
|
+
"""Return one span-tagged violation per unanchored command-regex literal.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
parsed_tree: The parsed module to scan for string-constant nodes.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
``(line_range, message)`` pairs in source order.
|
|
70
|
+
"""
|
|
71
|
+
all_violations: list[tuple[range, str]] = []
|
|
72
|
+
for each_node in ast.walk(parsed_tree):
|
|
73
|
+
if not isinstance(each_node, ast.Constant) or not isinstance(
|
|
74
|
+
each_node.value, str
|
|
75
|
+
):
|
|
76
|
+
continue
|
|
77
|
+
literal_match = COMMAND_DISPATCH_LITERAL_PATTERN.search(each_node.value)
|
|
78
|
+
if literal_match is None:
|
|
79
|
+
continue
|
|
80
|
+
if _command_literal_is_anchored(each_node.value, literal_match.start()):
|
|
81
|
+
continue
|
|
82
|
+
end_line = each_node.end_lineno or each_node.lineno
|
|
83
|
+
message = (
|
|
84
|
+
f"Line {each_node.lineno}: command pattern {each_node.value!r} "
|
|
85
|
+
f"{COMMAND_DISPATCH_MESSAGE_SUFFIX}"
|
|
86
|
+
)
|
|
87
|
+
all_violations.append((range(each_node.lineno, end_line + 1), message))
|
|
88
|
+
return all_violations
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def check_unanchored_command_dispatch(
|
|
92
|
+
content: str,
|
|
93
|
+
file_path: str,
|
|
94
|
+
all_changed_lines: set[int] | None = None,
|
|
95
|
+
defer_scope_to_caller: bool = False,
|
|
96
|
+
) -> list[str]:
|
|
97
|
+
"""Flag an unanchored multi-word command regex in a hooks/blocking file.
|
|
98
|
+
|
|
99
|
+
The check fires only on a file under ``hooks/blocking/`` that reads a
|
|
100
|
+
``command`` key (a shell-command classifier) and does not tokenize the
|
|
101
|
+
command's first word (no ``shlex.split`` or ``.split(`` nearby). Under those
|
|
102
|
+
conditions, a regex string literal naming a known command word followed by
|
|
103
|
+
``\\s+`` without a leading ``^``/``\\A`` anchor is flagged. Findings scope to
|
|
104
|
+
*all_changed_lines* so a pre-existing pattern on an untouched line does not
|
|
105
|
+
block an edit while a newly written one does.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
content: The source text to inspect — the reconstructed full file on an
|
|
109
|
+
Edit so the parse succeeds.
|
|
110
|
+
file_path: The path the source will be written to, used for scoping to
|
|
111
|
+
``hooks/blocking`` and skipping test files.
|
|
112
|
+
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
113
|
+
None to treat the whole file as in scope.
|
|
114
|
+
defer_scope_to_caller: When True, return every violation so a downstream
|
|
115
|
+
scoper classifies by added line.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
One issue per unanchored command-regex literal, scoped to the changed
|
|
119
|
+
lines unless *defer_scope_to_caller* is True or *all_changed_lines* is
|
|
120
|
+
None, capped at the module limit.
|
|
121
|
+
"""
|
|
122
|
+
if is_test_file(file_path) or not _is_under_hooks_blocking(file_path):
|
|
123
|
+
return []
|
|
124
|
+
if COMMAND_KEY_ACCESS_PATTERN.search(content) is None:
|
|
125
|
+
return []
|
|
126
|
+
if FIRST_TOKEN_TOKENIZATION_PATTERN.search(content) is not None:
|
|
127
|
+
return []
|
|
128
|
+
try:
|
|
129
|
+
parsed_tree = ast.parse(content)
|
|
130
|
+
except SyntaxError:
|
|
131
|
+
return []
|
|
132
|
+
all_violations_in_source_order = _unanchored_command_literals(parsed_tree)
|
|
133
|
+
scoped_issues = _scope_violations_to_changed_lines(
|
|
134
|
+
all_violations_in_source_order,
|
|
135
|
+
all_changed_lines,
|
|
136
|
+
defer_scope_to_caller,
|
|
137
|
+
)
|
|
138
|
+
if defer_scope_to_caller:
|
|
139
|
+
return scoped_issues
|
|
140
|
+
return scoped_issues[:MAX_COMMAND_DISPATCH_ISSUES]
|
|
@@ -22,6 +22,7 @@ from code_rules_shared import ( # noqa: E402
|
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
25
|
+
ALL_ABSENT_TYPE_CHECKING_GATE_DOCSTRING_PHRASES,
|
|
25
26
|
ALL_DATA_SCHEMA_CONSTANT_NAME_MARKERS,
|
|
26
27
|
ALL_DATA_SCHEMA_DOCSTRING_ACKNOWLEDGEMENT_PHRASES,
|
|
27
28
|
ALL_DOCSTRING_EXCLUSIVE_SCOPE_PHRASES,
|
|
@@ -74,6 +75,7 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
|
74
75
|
MAX_DOCSTRING_RUNON_SENTENCE_ISSUES,
|
|
75
76
|
MAX_DOCSTRING_STEP_DISPATCH_ISSUES,
|
|
76
77
|
MAX_DOCSTRING_TUPLE_ENUMERATION_ISSUES,
|
|
78
|
+
MAX_DOCSTRING_TYPE_CHECKING_GATE_ISSUES,
|
|
77
79
|
MAX_DOCSTRING_UNDEFINED_CONSTANT_ISSUES,
|
|
78
80
|
MAX_DOCSTRING_UNGUARDED_PAYLOAD_CLAIM_ISSUES,
|
|
79
81
|
MAX_LENGTH_CONSTANT_SUPERLATIVE_ISSUES,
|
|
@@ -90,6 +92,7 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
|
90
92
|
MODULE_DOCSTRING_DATA_SCHEMA_CONSTANT_SAMPLE_LIMIT,
|
|
91
93
|
PYTHON_MODULE_FILE_SUFFIX,
|
|
92
94
|
SINGLE_DICT_KEY_COUNT_FOR_PLURAL_CARDINALITY_DRIFT,
|
|
95
|
+
TYPE_CHECKING_IDENTIFIER_MARKER,
|
|
93
96
|
WORD_BOUNDARY_REGEX,
|
|
94
97
|
ZIPFILE_ALLOW_ZIP64_KEYWORD,
|
|
95
98
|
ZIPFILE_ALLOW_ZIP64_POSITIONAL_INDEX,
|
|
@@ -959,6 +962,96 @@ def check_docstring_no_network_claim_with_metadata_access(
|
|
|
959
962
|
return issues[:MAX_DOCSTRING_NO_NETWORK_CLAIM_ISSUES]
|
|
960
963
|
|
|
961
964
|
|
|
965
|
+
def _module_code_references_type_checking(parsed_tree: ast.Module) -> bool:
|
|
966
|
+
"""Return True when a code identifier in the module handles TYPE_CHECKING.
|
|
967
|
+
|
|
968
|
+
A module that names TYPE_CHECKING in code genuinely gates on it. That covers
|
|
969
|
+
a Name reference (any context — load, store, or delete), an import alias, an
|
|
970
|
+
attribute access, or a function or class definition whose name carries the
|
|
971
|
+
type_checking marker. Docstring text is an ast.Constant and never counts, so
|
|
972
|
+
the check can still flag a gate the prose names while the body performs none.
|
|
973
|
+
"""
|
|
974
|
+
for each_node in ast.walk(parsed_tree):
|
|
975
|
+
if isinstance(each_node, ast.Name) and (
|
|
976
|
+
TYPE_CHECKING_IDENTIFIER_MARKER in each_node.id.lower()
|
|
977
|
+
):
|
|
978
|
+
return True
|
|
979
|
+
if isinstance(each_node, ast.Attribute) and (
|
|
980
|
+
TYPE_CHECKING_IDENTIFIER_MARKER in each_node.attr.lower()
|
|
981
|
+
):
|
|
982
|
+
return True
|
|
983
|
+
if isinstance(each_node, ast.alias) and any(
|
|
984
|
+
each_alias_name and TYPE_CHECKING_IDENTIFIER_MARKER in each_alias_name.lower()
|
|
985
|
+
for each_alias_name in (each_node.name, each_node.asname)
|
|
986
|
+
):
|
|
987
|
+
return True
|
|
988
|
+
if isinstance(
|
|
989
|
+
each_node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
990
|
+
) and TYPE_CHECKING_IDENTIFIER_MARKER in each_node.name.lower():
|
|
991
|
+
return True
|
|
992
|
+
return False
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def _docstring_names_absent_type_checking_gate(docstring_text: str) -> str:
|
|
996
|
+
collapsed_docstring = " ".join(docstring_text.replace("`", " ").lower().split())
|
|
997
|
+
for each_phrase in ALL_ABSENT_TYPE_CHECKING_GATE_DOCSTRING_PHRASES:
|
|
998
|
+
if each_phrase in collapsed_docstring:
|
|
999
|
+
return each_phrase
|
|
1000
|
+
return ""
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def check_docstring_names_absent_type_checking_gate(
|
|
1004
|
+
content: str, file_path: str
|
|
1005
|
+
) -> list[str]:
|
|
1006
|
+
"""Flag a docstring naming a TYPE_CHECKING gate the module's code never runs.
|
|
1007
|
+
|
|
1008
|
+
Picture a hook whose docstring advertises a `TYPE_CHECKING` gate-detection
|
|
1009
|
+
step, or a module summary that names `type-checking-gate` helpers, while no
|
|
1010
|
+
identifier in the body handles TYPE_CHECKING. A reader trusts the prose and
|
|
1011
|
+
looks for the gate, but the code performs none, so the claim points at
|
|
1012
|
+
machinery the module does not hold.
|
|
1013
|
+
|
|
1014
|
+
This is the deterministic slice of Category O6 docstring-prose-vs-
|
|
1015
|
+
implementation drift for a TYPE_CHECKING gate claim. The check reads the
|
|
1016
|
+
module docstring and every function and class docstring. It fires only when
|
|
1017
|
+
a docstring names a `TYPE_CHECKING` gate or a `type-checking-gate` helper
|
|
1018
|
+
family while no code identifier in the module carries the `type_checking`
|
|
1019
|
+
marker, so a module that genuinely gates on TYPE_CHECKING is left alone.
|
|
1020
|
+
Hook infrastructure is in scope, since the import-scan hooks that carry this
|
|
1021
|
+
drift class are themselves hooks. Test files are exempt.
|
|
1022
|
+
|
|
1023
|
+
Args:
|
|
1024
|
+
content: The source text to inspect.
|
|
1025
|
+
file_path: The path the source will be written to, used for exemptions.
|
|
1026
|
+
|
|
1027
|
+
Returns:
|
|
1028
|
+
One issue per docstring naming a TYPE_CHECKING gate the code never
|
|
1029
|
+
performs, capped at the module limit.
|
|
1030
|
+
"""
|
|
1031
|
+
if is_strict_test_file(file_path):
|
|
1032
|
+
return []
|
|
1033
|
+
try:
|
|
1034
|
+
parsed_tree = ast.parse(content)
|
|
1035
|
+
except SyntaxError:
|
|
1036
|
+
return []
|
|
1037
|
+
if _module_code_references_type_checking(parsed_tree):
|
|
1038
|
+
return []
|
|
1039
|
+
issues: list[str] = []
|
|
1040
|
+
for each_line_number, each_docstring in _documentable_nodes_with_docstrings(parsed_tree):
|
|
1041
|
+
matched_phrase = _docstring_names_absent_type_checking_gate(each_docstring)
|
|
1042
|
+
if not matched_phrase:
|
|
1043
|
+
continue
|
|
1044
|
+
issues.append(
|
|
1045
|
+
f"Line {each_line_number}: docstring names a '{matched_phrase}' the module's "
|
|
1046
|
+
"code never performs — no identifier in the body handles TYPE_CHECKING, so the "
|
|
1047
|
+
"gate-detection claim is stale; drop the TYPE_CHECKING gate wording or add the "
|
|
1048
|
+
"detection (Category O6 docstring-vs-implementation drift)"
|
|
1049
|
+
)
|
|
1050
|
+
if len(issues) >= MAX_DOCSTRING_TYPE_CHECKING_GATE_ISSUES:
|
|
1051
|
+
break
|
|
1052
|
+
return issues[:MAX_DOCSTRING_TYPE_CHECKING_GATE_ISSUES]
|
|
1053
|
+
|
|
1054
|
+
|
|
962
1055
|
def _module_docstring_claims_no_inline_literal(module_docstring: str) -> str:
|
|
963
1056
|
collapsed_docstring = " ".join(module_docstring.lower().split())
|
|
964
1057
|
for each_phrase in ALL_DOCSTRING_NO_INLINE_LITERAL_CLAIM_PHRASES:
|
|
@@ -44,6 +44,9 @@ from code_rules_boolean_mustcheck import ( # noqa: E402
|
|
|
44
44
|
check_boolean_naming,
|
|
45
45
|
check_ignored_must_check_return,
|
|
46
46
|
)
|
|
47
|
+
from code_rules_command_dispatch import ( # noqa: E402
|
|
48
|
+
check_unanchored_command_dispatch,
|
|
49
|
+
)
|
|
47
50
|
from code_rules_comments import ( # noqa: E402
|
|
48
51
|
check_comment_changes,
|
|
49
52
|
)
|
|
@@ -77,6 +80,7 @@ from code_rules_docstrings import ( # noqa: E402
|
|
|
77
80
|
check_docstring_field_runmode_outcome,
|
|
78
81
|
check_docstring_format,
|
|
79
82
|
check_docstring_length_constant_superlative_vs_exact_gate,
|
|
83
|
+
check_docstring_names_absent_type_checking_gate,
|
|
80
84
|
check_docstring_names_undefined_constant,
|
|
81
85
|
check_docstring_no_consumer_claim,
|
|
82
86
|
check_docstring_no_inline_literal_claim,
|
|
@@ -108,6 +112,10 @@ from code_rules_imports_logging import ( # noqa: E402
|
|
|
108
112
|
check_logging_printf_tokens,
|
|
109
113
|
check_windows_api_none,
|
|
110
114
|
)
|
|
115
|
+
from code_rules_js_conventions import ( # noqa: E402
|
|
116
|
+
check_js_banned_identifiers,
|
|
117
|
+
check_js_boolean_naming,
|
|
118
|
+
)
|
|
111
119
|
from code_rules_magic_values import ( # noqa: E402
|
|
112
120
|
check_fstring_structural_literals,
|
|
113
121
|
check_magic_values,
|
|
@@ -284,7 +292,14 @@ def validate_content(
|
|
|
284
292
|
)
|
|
285
293
|
)
|
|
286
294
|
all_issues.extend(check_type_escape_hatches(effective_content, file_path))
|
|
287
|
-
all_issues.extend(
|
|
295
|
+
all_issues.extend(
|
|
296
|
+
check_banned_identifiers(
|
|
297
|
+
effective_content,
|
|
298
|
+
file_path,
|
|
299
|
+
all_changed_lines,
|
|
300
|
+
defer_scope_to_caller,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
288
303
|
all_issues.extend(
|
|
289
304
|
check_banned_noun_word_boundary(
|
|
290
305
|
effective_content,
|
|
@@ -368,6 +383,9 @@ def validate_content(
|
|
|
368
383
|
all_issues.extend(
|
|
369
384
|
check_docstring_names_undefined_constant(effective_content, file_path)
|
|
370
385
|
)
|
|
386
|
+
all_issues.extend(
|
|
387
|
+
check_docstring_names_absent_type_checking_gate(effective_content, file_path)
|
|
388
|
+
)
|
|
371
389
|
all_issues.extend(
|
|
372
390
|
check_docstring_args_single_line_scope_vs_span(effective_content, file_path)
|
|
373
391
|
)
|
|
@@ -428,7 +446,14 @@ def validate_content(
|
|
|
428
446
|
all_issues.extend(
|
|
429
447
|
check_unused_known_pytest_fixture_parameters(content, file_path)
|
|
430
448
|
)
|
|
431
|
-
all_issues.extend(
|
|
449
|
+
all_issues.extend(
|
|
450
|
+
check_return_annotations(
|
|
451
|
+
effective_content,
|
|
452
|
+
file_path,
|
|
453
|
+
all_changed_lines,
|
|
454
|
+
defer_scope_to_caller,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
432
457
|
all_issues.extend(
|
|
433
458
|
check_function_length(
|
|
434
459
|
effective_content,
|
|
@@ -466,6 +491,22 @@ def validate_content(
|
|
|
466
491
|
if not is_test_file(file_path):
|
|
467
492
|
all_issues.extend(check_comment_changes(old_content, content, file_path))
|
|
468
493
|
all_issues.extend(check_e2e_test_naming(content, file_path))
|
|
494
|
+
all_issues.extend(
|
|
495
|
+
check_js_boolean_naming(
|
|
496
|
+
effective_content,
|
|
497
|
+
file_path,
|
|
498
|
+
all_changed_lines,
|
|
499
|
+
defer_scope_to_caller,
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
all_issues.extend(
|
|
503
|
+
check_js_banned_identifiers(
|
|
504
|
+
effective_content,
|
|
505
|
+
file_path,
|
|
506
|
+
all_changed_lines,
|
|
507
|
+
defer_scope_to_caller,
|
|
508
|
+
)
|
|
509
|
+
)
|
|
469
510
|
all_issues.extend(
|
|
470
511
|
check_js_resume_task_enumeration_coverage(content, file_path)
|
|
471
512
|
)
|
|
@@ -579,8 +620,10 @@ def _hook_infrastructure_blocking_issues(
|
|
|
579
620
|
runs the checks that must still guard them: the cross-file duplicate-body
|
|
580
621
|
check and the same-file inline-duplicate-body check, each span-scoped to the
|
|
581
622
|
lines an edit touched exactly as ``validate_content`` scopes it for production
|
|
582
|
-
code;
|
|
583
|
-
its motivating case, run over the whole post-edit file
|
|
623
|
+
code; the zero-payload alias check, whose docstring names hook modules as
|
|
624
|
+
its motivating case, run over the whole post-edit file; and the
|
|
625
|
+
unanchored command-dispatch check, which guards a ``hooks/blocking``
|
|
626
|
+
command classifier against matching a multi-word command as a substring.
|
|
584
627
|
|
|
585
628
|
Args:
|
|
586
629
|
content: The fragment or whole-file body under validation.
|
|
@@ -613,6 +656,13 @@ def _hook_infrastructure_blocking_issues(
|
|
|
613
656
|
)
|
|
614
657
|
)
|
|
615
658
|
all_issues.extend(check_zero_payload_function_alias(effective_content, file_path))
|
|
659
|
+
all_issues.extend(
|
|
660
|
+
check_unanchored_command_dispatch(
|
|
661
|
+
effective_content,
|
|
662
|
+
file_path,
|
|
663
|
+
all_changed_lines,
|
|
664
|
+
)
|
|
665
|
+
)
|
|
616
666
|
return all_issues
|
|
617
667
|
|
|
618
668
|
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Boolean-naming and banned-identifier checks for JavaScript and TypeScript source.
|
|
2
|
+
|
|
3
|
+
These mirror the Python ``check_boolean_naming`` and ``check_banned_identifiers``
|
|
4
|
+
rules for the ``.mjs`` / ``.js`` family so a JavaScript declaration receives the
|
|
5
|
+
same naming discipline the Python checks apply. Detection is line-regex based
|
|
6
|
+
over a copy of the source whose string, comment, and regex regions are blanked,
|
|
7
|
+
so a boolean assignment inside a prompt string or a comment never fires. The
|
|
8
|
+
``@param {boolean}`` scan reads the raw source instead, because JSDoc lives
|
|
9
|
+
inside a block comment the blanking step removes.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
16
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
17
|
+
if _blocking_directory not in sys.path:
|
|
18
|
+
sys.path.insert(0, _blocking_directory)
|
|
19
|
+
if _hooks_directory not in sys.path:
|
|
20
|
+
sys.path.insert(0, _hooks_directory)
|
|
21
|
+
|
|
22
|
+
from code_rules_imports_logging import ( # noqa: E402
|
|
23
|
+
_blank_non_code_regions,
|
|
24
|
+
)
|
|
25
|
+
from code_rules_shared import ( # noqa: E402
|
|
26
|
+
_scope_violations_to_changed_lines,
|
|
27
|
+
get_file_extension,
|
|
28
|
+
is_hook_infrastructure,
|
|
29
|
+
is_test_file,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from hooks_constants.banned_identifiers_constants import ( # noqa: E402
|
|
33
|
+
BANNED_IDENTIFIER_MESSAGE_SUFFIX,
|
|
34
|
+
)
|
|
35
|
+
from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
36
|
+
ALL_JAVASCRIPT_EXTENSIONS,
|
|
37
|
+
)
|
|
38
|
+
from hooks_constants.js_conventions_constants import ( # noqa: E402
|
|
39
|
+
ALL_JAVASCRIPT_BANNED_IDENTIFIERS,
|
|
40
|
+
BOOLEAN_PREFIX_GUIDANCE,
|
|
41
|
+
JAVASCRIPT_BOOLEAN_DECLARATION_PATTERN,
|
|
42
|
+
JAVASCRIPT_BOOLEAN_JSDOC_PARAMETER_PATTERN,
|
|
43
|
+
JAVASCRIPT_BOOLEAN_PREFIX_PATTERN,
|
|
44
|
+
JAVASCRIPT_DECLARATION_NAME_PATTERN,
|
|
45
|
+
MAX_JAVASCRIPT_BANNED_IDENTIFIER_ISSUES,
|
|
46
|
+
MAX_JAVASCRIPT_BOOLEAN_NAMING_ISSUES,
|
|
47
|
+
SINGLE_CHARACTER_NAME_LENGTH,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_javascript_target(file_path: str) -> bool:
|
|
52
|
+
"""Return whether a check should run on this path.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
file_path: The destination path of the write or edit.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True when the path carries a JavaScript extension and is neither a test
|
|
59
|
+
file nor hook infrastructure; False for every exempt path.
|
|
60
|
+
"""
|
|
61
|
+
if is_test_file(file_path) or is_hook_infrastructure(file_path):
|
|
62
|
+
return False
|
|
63
|
+
return get_file_extension(file_path) in ALL_JAVASCRIPT_EXTENSIONS
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _boolean_name_lacks_prefix(name: str) -> bool:
|
|
67
|
+
"""Return whether a boolean name needs a naming prefix it does not carry.
|
|
68
|
+
|
|
69
|
+
A single-character name, an all-uppercase constant name, and a name already
|
|
70
|
+
carrying an ``is``/``has``/``should``/``can``/``was``/``did`` prefix are all
|
|
71
|
+
accepted.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: The declared boolean identifier.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True when the name should carry a boolean prefix and does not.
|
|
78
|
+
"""
|
|
79
|
+
if len(name) <= SINGLE_CHARACTER_NAME_LENGTH:
|
|
80
|
+
return False
|
|
81
|
+
if name.isupper():
|
|
82
|
+
return False
|
|
83
|
+
return JAVASCRIPT_BOOLEAN_PREFIX_PATTERN.match(name) is None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _boolean_declaration_violations(
|
|
87
|
+
blanked_content: str,
|
|
88
|
+
) -> list[tuple[range, str]]:
|
|
89
|
+
"""Return one span-tagged violation per unprefixed boolean declaration.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
blanked_content: The source with string, comment, and regex regions
|
|
93
|
+
replaced by spaces so only structural code is scanned.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
``(line_range, message)`` pairs in source order.
|
|
97
|
+
"""
|
|
98
|
+
all_violations: list[tuple[range, str]] = []
|
|
99
|
+
for each_line_number, each_line in enumerate(blanked_content.split("\n"), 1):
|
|
100
|
+
for each_match in JAVASCRIPT_BOOLEAN_DECLARATION_PATTERN.finditer(each_line):
|
|
101
|
+
name = each_match.group("name")
|
|
102
|
+
if not _boolean_name_lacks_prefix(name):
|
|
103
|
+
continue
|
|
104
|
+
message = (
|
|
105
|
+
f"Line {each_line_number}: Boolean {name} - {BOOLEAN_PREFIX_GUIDANCE}"
|
|
106
|
+
)
|
|
107
|
+
all_violations.append(
|
|
108
|
+
(range(each_line_number, each_line_number + 1), message)
|
|
109
|
+
)
|
|
110
|
+
return all_violations
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _boolean_jsdoc_parameter_violations(content: str) -> list[tuple[range, str]]:
|
|
114
|
+
"""Return one span-tagged violation per unprefixed ``@param {boolean}`` name.
|
|
115
|
+
|
|
116
|
+
The raw source is scanned rather than the blanked copy because JSDoc lives
|
|
117
|
+
inside a block comment the blanking step removes.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
content: The raw source text.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
``(line_range, message)`` pairs in source order.
|
|
124
|
+
"""
|
|
125
|
+
all_violations: list[tuple[range, str]] = []
|
|
126
|
+
for each_line_number, each_line in enumerate(content.split("\n"), 1):
|
|
127
|
+
for each_match in JAVASCRIPT_BOOLEAN_JSDOC_PARAMETER_PATTERN.finditer(
|
|
128
|
+
each_line
|
|
129
|
+
):
|
|
130
|
+
name = each_match.group("name")
|
|
131
|
+
if not _boolean_name_lacks_prefix(name):
|
|
132
|
+
continue
|
|
133
|
+
message = (
|
|
134
|
+
f"Line {each_line_number}: Boolean parameter {name} - "
|
|
135
|
+
f"{BOOLEAN_PREFIX_GUIDANCE}"
|
|
136
|
+
)
|
|
137
|
+
all_violations.append(
|
|
138
|
+
(range(each_line_number, each_line_number + 1), message)
|
|
139
|
+
)
|
|
140
|
+
return all_violations
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_js_boolean_naming(
|
|
144
|
+
content: str,
|
|
145
|
+
file_path: str,
|
|
146
|
+
all_changed_lines: set[int] | None = None,
|
|
147
|
+
defer_scope_to_caller: bool = False,
|
|
148
|
+
) -> list[str]:
|
|
149
|
+
"""Flag JavaScript boolean declarations and JSDoc params lacking a prefix.
|
|
150
|
+
|
|
151
|
+
A ``const``/``let``/``var`` declaration whose right-hand side is a boolean
|
|
152
|
+
literal, or a negation that is the entire right-hand side (``= !ready;`` or
|
|
153
|
+
``= !obj.prop`` or ``= !call(...)``, ending the statement with no ``?``,
|
|
154
|
+
``&&``, ``||``, or ``,`` combining the negated operand), and a
|
|
155
|
+
``@param {boolean}`` JSDoc entry, all name a boolean. A ``!``-headed side
|
|
156
|
+
that a ternary or logical operator continues (``!active ? "on" : "off"``,
|
|
157
|
+
``!err && getName()``) is an ordinary expression, not a boolean, and is left
|
|
158
|
+
alone. When the boolean name lacks an ``is``/``has``/``should``/``can``/
|
|
159
|
+
``was``/``did`` prefix (camelCase forms such as ``isReady``), the check flags
|
|
160
|
+
it, so a reader learns a value is a boolean from its name. Findings scope to
|
|
161
|
+
*all_changed_lines* so an edit blocks on the unprefixed boolean it just
|
|
162
|
+
introduced while a pre-existing one on an untouched line does not block.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
content: The source text to inspect.
|
|
166
|
+
file_path: The path the source will be written to, used for exemptions.
|
|
167
|
+
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
168
|
+
None to treat the whole file as in scope. When provided, a violation
|
|
169
|
+
blocks only when its line intersects the changed lines.
|
|
170
|
+
defer_scope_to_caller: When True, return every violation so the
|
|
171
|
+
commit/push gate scopes by added line through its ``Line N:``
|
|
172
|
+
partitioning.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
One issue per unprefixed boolean declaration and JSDoc parameter, scoped
|
|
176
|
+
to the changed lines unless *defer_scope_to_caller* is True or
|
|
177
|
+
*all_changed_lines* is None, capped at the module limit.
|
|
178
|
+
"""
|
|
179
|
+
if not _is_javascript_target(file_path):
|
|
180
|
+
return []
|
|
181
|
+
blanked_content = _blank_non_code_regions(content)
|
|
182
|
+
all_violations_in_source_order = _boolean_declaration_violations(blanked_content)
|
|
183
|
+
all_violations_in_source_order.extend(_boolean_jsdoc_parameter_violations(content))
|
|
184
|
+
scoped_issues = _scope_violations_to_changed_lines(
|
|
185
|
+
all_violations_in_source_order,
|
|
186
|
+
all_changed_lines,
|
|
187
|
+
defer_scope_to_caller,
|
|
188
|
+
)
|
|
189
|
+
if defer_scope_to_caller:
|
|
190
|
+
return scoped_issues
|
|
191
|
+
return scoped_issues[:MAX_JAVASCRIPT_BOOLEAN_NAMING_ISSUES]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def check_js_banned_identifiers(
|
|
195
|
+
content: str,
|
|
196
|
+
file_path: str,
|
|
197
|
+
all_changed_lines: set[int] | None = None,
|
|
198
|
+
defer_scope_to_caller: bool = False,
|
|
199
|
+
) -> list[str]:
|
|
200
|
+
"""Flag JavaScript declarations bound to a banned identifier name.
|
|
201
|
+
|
|
202
|
+
A ``const``/``let``/``var`` declaration whose name is one of the banned
|
|
203
|
+
placeholder names (``result``, ``data``, ``response``, ``ctx``, and the rest)
|
|
204
|
+
is flagged so the author picks a domain-specific name. Findings scope to
|
|
205
|
+
*all_changed_lines* so a pre-existing ``result`` on an untouched line never
|
|
206
|
+
blocks while a newly written one does.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
content: The source text to inspect.
|
|
210
|
+
file_path: The path the source will be written to, used for exemptions.
|
|
211
|
+
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
212
|
+
None to treat the whole file as in scope. When provided, a violation
|
|
213
|
+
blocks only when its line intersects the changed lines.
|
|
214
|
+
defer_scope_to_caller: When True, return every violation so the
|
|
215
|
+
commit/push gate scopes by added line through its ``Line N:``
|
|
216
|
+
partitioning.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
One issue per banned declaration name, scoped to the changed lines unless
|
|
220
|
+
*defer_scope_to_caller* is True or *all_changed_lines* is None, capped at
|
|
221
|
+
the module limit.
|
|
222
|
+
"""
|
|
223
|
+
if not _is_javascript_target(file_path):
|
|
224
|
+
return []
|
|
225
|
+
blanked_content = _blank_non_code_regions(content)
|
|
226
|
+
all_violations_in_source_order: list[tuple[range, str]] = []
|
|
227
|
+
for each_line_number, each_line in enumerate(blanked_content.split("\n"), 1):
|
|
228
|
+
for each_match in JAVASCRIPT_DECLARATION_NAME_PATTERN.finditer(each_line):
|
|
229
|
+
name = each_match.group("name")
|
|
230
|
+
if name not in ALL_JAVASCRIPT_BANNED_IDENTIFIERS:
|
|
231
|
+
continue
|
|
232
|
+
message = (
|
|
233
|
+
f"Line {each_line_number}: Banned identifier '{name}' - "
|
|
234
|
+
f"{BANNED_IDENTIFIER_MESSAGE_SUFFIX}"
|
|
235
|
+
)
|
|
236
|
+
all_violations_in_source_order.append(
|
|
237
|
+
(range(each_line_number, each_line_number + 1), message)
|
|
238
|
+
)
|
|
239
|
+
scoped_issues = _scope_violations_to_changed_lines(
|
|
240
|
+
all_violations_in_source_order,
|
|
241
|
+
all_changed_lines,
|
|
242
|
+
defer_scope_to_caller,
|
|
243
|
+
)
|
|
244
|
+
if defer_scope_to_caller:
|
|
245
|
+
return scoped_issues
|
|
246
|
+
return scoped_issues[:MAX_JAVASCRIPT_BANNED_IDENTIFIER_ISSUES]
|
|
@@ -33,6 +33,9 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
|
33
33
|
POLARITY_TOKEN_BOUNDARY_PATTERN,
|
|
34
34
|
UPPER_SNAKE_CONSTANT_PATTERN,
|
|
35
35
|
)
|
|
36
|
+
from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
37
|
+
MAX_POLARITY_CONTRADICTION_ISSUES,
|
|
38
|
+
)
|
|
36
39
|
from hooks_constants.stuttering_check_config import ( # noqa: E402
|
|
37
40
|
MAX_STUTTERING_PREFIX_ISSUES,
|
|
38
41
|
STUTTERING_ALL_PREFIX_PATTERN,
|
|
@@ -336,4 +339,6 @@ def check_polarity_name_contradiction(content: str, file_path: str) -> list[str]
|
|
|
336
339
|
f" {called_name!r} (says {called_token!r}) - the name contradicts the value; rename the"
|
|
337
340
|
f" callee to a neutral form so each call site reads truthfully (CODE_RULES §5)"
|
|
338
341
|
)
|
|
342
|
+
if len(issues) >= MAX_POLARITY_CONTRADICTION_ISSUES:
|
|
343
|
+
break
|
|
339
344
|
return issues
|
|
@@ -51,6 +51,7 @@ from code_rules_shared import ( # noqa: E402
|
|
|
51
51
|
|
|
52
52
|
from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
53
53
|
MAX_STALE_TEST_NAME_TARGET_ISSUES,
|
|
54
|
+
MAX_VACUOUS_CLEANUP_ASSERTION_ISSUES,
|
|
54
55
|
STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT,
|
|
55
56
|
)
|
|
56
57
|
from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
@@ -387,6 +388,8 @@ def check_vacuous_cleanup_assertion_tests(content: str, file_path: str) -> list[
|
|
|
387
388
|
f" failure (e.g. monkeypatch os.replace to raise after the temp exists)"
|
|
388
389
|
f" then assert the temp was removed"
|
|
389
390
|
)
|
|
391
|
+
if len(issues) >= MAX_VACUOUS_CLEANUP_ASSERTION_ISSUES:
|
|
392
|
+
break
|
|
390
393
|
|
|
391
394
|
return issues
|
|
392
395
|
|
|
@@ -19,7 +19,6 @@ from code_rules_scope_binding import ( # noqa: E402
|
|
|
19
19
|
from code_rules_shared import ( # noqa: E402
|
|
20
20
|
_build_parent_map,
|
|
21
21
|
is_migration_file,
|
|
22
|
-
is_test_file,
|
|
23
22
|
)
|
|
24
23
|
|
|
25
24
|
from hooks_constants.unused_module_import_constants import ( # noqa: E402
|
|
@@ -149,8 +148,6 @@ def check_unused_module_level_imports(
|
|
|
149
148
|
Edit applies). This prevents false-positive flags on imports added in the
|
|
150
149
|
same Edit as their consumers.
|
|
151
150
|
"""
|
|
152
|
-
if is_test_file(file_path):
|
|
153
|
-
return []
|
|
154
151
|
if is_migration_file(file_path):
|
|
155
152
|
return []
|
|
156
153
|
try:
|