claude-dev-env 1.34.1 → 1.36.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/agents/clean-coder.md +109 -1
- package/agents/docs-agent.md +1 -1
- package/agents/project-docs-analyzer.md +0 -1
- package/agents/skill-to-agent-converter.md +0 -1
- package/bin/install.mjs +28 -8
- package/bin/install.test.mjs +9 -1
- package/commands/initialize.md +0 -1
- package/commands/readability-review.md +4 -4
- package/commands/review-plan.md +2 -4
- package/commands/stubcheck.md +1 -2
- package/docs/CODE_RULES.md +3 -0
- package/docs/agents-md-alignment-plan.md +123 -0
- package/hooks/blocking/code_rules_enforcer.py +686 -60
- package/hooks/blocking/es_exe_path_rewriter.py +10 -4
- package/hooks/blocking/test_code_rules_enforcer.py +273 -39
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +328 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +33 -11
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +194 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -13
- package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
- package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
- package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
- package/hooks/blocking/windows_rmtree_blocker.py +23 -6
- package/hooks/config/banned_identifiers_constants.py +24 -0
- package/hooks/config/hardcoded_user_path_constants.py +12 -0
- package/hooks/config/hook_log_extractor_constants.py +1 -1
- package/hooks/config/pre_tool_use_stdin.py +48 -0
- package/hooks/config/setup_project_paths_constants.py +4 -0
- package/hooks/config/stuttering_check_config.py +14 -0
- package/hooks/config/stuttering_import_binding_constants.py +11 -0
- package/hooks/config/sys_path_insert_constants.py +4 -0
- package/hooks/config/test_banned_identifiers_constants.py +48 -0
- package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
- package/hooks/config/test_hook_log_extractor_constants.py +3 -3
- package/hooks/config/test_pre_tool_use_stdin.py +80 -0
- package/hooks/config/unused_module_import_constants.py +7 -0
- package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
- package/hooks/git-hooks/config.py +3 -3
- package/hooks/git-hooks/test_gate_utils.py +10 -10
- package/hooks/mypy.ini +2 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +125 -0
- package/skills/bugteam/CONSTRAINTS.md +12 -6
- package/skills/bugteam/PROMPTS.md +0 -39
- package/skills/bugteam/SKILL.md +93 -125
- package/skills/bugteam/SKILL_EVALS.md +25 -23
- package/skills/bugteam/reference/README.md +2 -0
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
- package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
- package/skills/bugteam/test_skill_additions.py +13 -4
- package/skills/bugteam/test_team_lifecycle.py +94 -0
- package/skills/findbugs/SKILL.md +3 -3
- package/skills/fixbugs/SKILL.md +4 -4
- package/skills/monitor-open-prs/SKILL.md +32 -2
- package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
- package/skills/pr-converge/SKILL.md +576 -95
- package/skills/pr-converge/scripts/README.md +145 -0
- package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
- package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
- package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
- package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
- package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
- package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
- package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
- package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
- package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
- package/skills/pr-converge/scripts/view_pr_context.py +47 -0
- package/skills/pr-converge/test_team_lifecycle.py +47 -0
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
- package/skills/qbug/SKILL.md +4 -4
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
- package/skills/resume-review/SKILL.md +261 -0
- package/agents/agent-writer.md +0 -157
- package/agents/config-centralizer.md +0 -686
- package/agents/config-extraction-agent.md +0 -225
- package/agents/doc-orchestrator.md +0 -47
- package/agents/docx-agent.md +0 -211
- package/agents/magic-value-eliminator-agent.md +0 -72
- package/agents/mandatory-agent-workflow-agent.md +0 -88
- package/agents/parallel-workflow-coordinator.md +0 -779
- package/agents/pdf-agent.md +0 -302
- package/agents/project-context-loader.md +0 -238
- package/agents/readability-review-agent.md +0 -76
- package/agents/refactoring-specialist.md +0 -69
- package/agents/right-sized-engineer.md +0 -129
- package/agents/session-continuity-manager.md +0 -53
- package/agents/stub-detector-agent.md +0 -140
- package/agents/tdd-test-writer.md +0 -62
- package/agents/test-data-builder.md +0 -68
- package/agents/tooling-builder.md +0 -78
- package/agents/validation-expert.md +0 -71
- package/agents/xlsx-agent.md +0 -169
- package/skills/bugteam/scripts/README.md +0 -58
- package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
- package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
- package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
- package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
- /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
|
@@ -26,6 +26,7 @@ def _insert_hooks_tree_for_imports() -> None:
|
|
|
26
26
|
_insert_hooks_tree_for_imports()
|
|
27
27
|
|
|
28
28
|
from config.dynamic_stderr_handler import DynamicStderrHandler
|
|
29
|
+
from config.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
29
30
|
from config.path_rewriter_constants import (
|
|
30
31
|
BASH_TOOL_NAME,
|
|
31
32
|
HOOK_EVENT_NAME,
|
|
@@ -135,12 +136,17 @@ def _build_allow_response(rewritten_command: str, original_tool_input: dict) ->
|
|
|
135
136
|
|
|
136
137
|
def main() -> None:
|
|
137
138
|
try:
|
|
138
|
-
hook_input =
|
|
139
|
-
|
|
139
|
+
hook_input = read_hook_input_dictionary_from_stdin()
|
|
140
|
+
if hook_input is None:
|
|
141
|
+
sys.exit(0)
|
|
142
|
+
raw_tool_name = hook_input.get("tool_name", "")
|
|
143
|
+
raw_tool_input = hook_input.get("tool_input", {})
|
|
144
|
+
tool_name = raw_tool_name if isinstance(raw_tool_name, str) else ""
|
|
145
|
+
tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {}
|
|
140
146
|
if tool_name != BASH_TOOL_NAME:
|
|
141
147
|
sys.exit(0)
|
|
142
|
-
|
|
143
|
-
command =
|
|
148
|
+
raw_command = tool_input.get("command", "")
|
|
149
|
+
command = raw_command if isinstance(raw_command, str) else ""
|
|
144
150
|
if not command_invokes_es_exe(command):
|
|
145
151
|
sys.exit(0)
|
|
146
152
|
known_registry = load_registry()
|
|
@@ -33,14 +33,82 @@ def _load_enforcer_module() -> ModuleType:
|
|
|
33
33
|
code_rules_enforcer = _load_enforcer_module()
|
|
34
34
|
|
|
35
35
|
_BLOCKING_DIR = Path(__file__).resolve().parent
|
|
36
|
+
_HOOKS_TREE_DIR = _BLOCKING_DIR.parent
|
|
36
37
|
if str(_BLOCKING_DIR) not in sys.path:
|
|
37
38
|
sys.path.insert(0, str(_BLOCKING_DIR))
|
|
39
|
+
if str(_HOOKS_TREE_DIR) not in sys.path:
|
|
40
|
+
sys.path.insert(0, str(_HOOKS_TREE_DIR))
|
|
38
41
|
|
|
39
42
|
from code_rules_path_utils import is_config_file as path_utils_is_config_file # noqa: E402
|
|
43
|
+
from config.banned_identifiers_constants import ( # noqa: E402
|
|
44
|
+
ALL_BANNED_IDENTIFIERS as config_all_banned_identifiers,
|
|
45
|
+
BANNED_IDENTIFIER_MESSAGE_SUFFIX as config_banned_identifier_message_suffix,
|
|
46
|
+
BANNED_IDENTIFIER_SKIP_ADVISORY as config_banned_identifier_skip_advisory,
|
|
47
|
+
MAX_BANNED_IDENTIFIER_ISSUES as config_max_banned_identifier_issues,
|
|
48
|
+
)
|
|
49
|
+
from config.hardcoded_user_path_constants import ( # noqa: E402
|
|
50
|
+
HARDCODED_USER_PATH_GUIDANCE as config_hardcoded_user_path_guidance,
|
|
51
|
+
HARDCODED_USER_PATH_PATTERN as config_hardcoded_user_path_pattern,
|
|
52
|
+
MAX_HARDCODED_USER_PATH_ISSUES as config_max_hardcoded_user_path_issues,
|
|
53
|
+
)
|
|
54
|
+
from config.stuttering_check_config import ( # noqa: E402
|
|
55
|
+
MAX_STUTTERING_PREFIX_ISSUES as config_max_stuttering_prefix_issues,
|
|
56
|
+
STUTTERING_ALL_PREFIX_PATTERN as config_stuttering_all_prefix_pattern,
|
|
57
|
+
)
|
|
40
58
|
|
|
41
59
|
PRODUCTION_FILE_PATH = "packages/claude-dev-env/hooks/blocking/example_production.py"
|
|
42
60
|
|
|
43
61
|
|
|
62
|
+
def test_should_expose_all_banned_identifiers_from_config() -> None:
|
|
63
|
+
expected_banned_identifiers = frozenset({
|
|
64
|
+
"result", "data", "output", "response", "value", "item", "temp",
|
|
65
|
+
"argv", "args", "kwargs", "argc",
|
|
66
|
+
})
|
|
67
|
+
actual_banned_identifiers = getattr(
|
|
68
|
+
code_rules_enforcer, "ALL_BANNED_IDENTIFIERS", None
|
|
69
|
+
)
|
|
70
|
+
assert actual_banned_identifiers is not None, (
|
|
71
|
+
"Renamed constant ALL_BANNED_IDENTIFIERS must be importable from "
|
|
72
|
+
"config/banned_identifiers_constants.py and re-exposed on the "
|
|
73
|
+
f"enforcer module, got: {actual_banned_identifiers!r}"
|
|
74
|
+
)
|
|
75
|
+
assert expected_banned_identifiers <= actual_banned_identifiers, (
|
|
76
|
+
"ALL_BANNED_IDENTIFIERS must contain every expected banned identifier; "
|
|
77
|
+
f"missing: {expected_banned_identifiers - actual_banned_identifiers!r}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_should_source_banned_identifier_companion_constants_from_config() -> None:
|
|
82
|
+
assert (
|
|
83
|
+
code_rules_enforcer.MAX_BANNED_IDENTIFIER_ISSUES
|
|
84
|
+
is config_max_banned_identifier_issues
|
|
85
|
+
)
|
|
86
|
+
assert (
|
|
87
|
+
code_rules_enforcer.BANNED_IDENTIFIER_MESSAGE_SUFFIX
|
|
88
|
+
is config_banned_identifier_message_suffix
|
|
89
|
+
)
|
|
90
|
+
assert (
|
|
91
|
+
code_rules_enforcer.BANNED_IDENTIFIER_SKIP_ADVISORY
|
|
92
|
+
is config_banned_identifier_skip_advisory
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_should_reexport_hardcoded_user_path_pattern_from_config() -> None:
|
|
97
|
+
assert code_rules_enforcer.HARDCODED_USER_PATH_PATTERN is config_hardcoded_user_path_pattern
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_should_reexport_max_hardcoded_user_path_issues_from_config() -> None:
|
|
101
|
+
assert code_rules_enforcer.MAX_HARDCODED_USER_PATH_ISSUES == config_max_hardcoded_user_path_issues
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_should_reexport_hardcoded_user_path_guidance_from_config() -> None:
|
|
105
|
+
assert code_rules_enforcer.HARDCODED_USER_PATH_GUIDANCE == config_hardcoded_user_path_guidance
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_should_reexport_all_banned_identifiers_from_config() -> None:
|
|
109
|
+
assert code_rules_enforcer.ALL_BANNED_IDENTIFIERS is config_all_banned_identifiers
|
|
110
|
+
|
|
111
|
+
|
|
44
112
|
def test_should_flag_constant_used_only_in_class_level_decorator() -> None:
|
|
45
113
|
source = (
|
|
46
114
|
"TIMEOUT = 5\n"
|
|
@@ -188,34 +256,6 @@ def test_should_flag_when_every_call_passes_the_exact_default() -> None:
|
|
|
188
256
|
)
|
|
189
257
|
|
|
190
258
|
|
|
191
|
-
def test_check_unused_optional_parameters_stops_at_max_issues_per_check() -> None:
|
|
192
|
-
source = (
|
|
193
|
-
"def make_url_one(path: str, prefix: str = '/api') -> str:\n"
|
|
194
|
-
" return f'{prefix}{path}'\n"
|
|
195
|
-
"def make_url_two(path: str, prefix: str = '/api') -> str:\n"
|
|
196
|
-
" return f'{prefix}{path}'\n"
|
|
197
|
-
"def make_url_three(path: str, prefix: str = '/api') -> str:\n"
|
|
198
|
-
" return f'{prefix}{path}'\n"
|
|
199
|
-
"def make_url_four(path: str, prefix: str = '/api') -> str:\n"
|
|
200
|
-
" return f'{prefix}{path}'\n"
|
|
201
|
-
"def make_url_five(path: str, prefix: str = '/api') -> str:\n"
|
|
202
|
-
" return f'{prefix}{path}'\n"
|
|
203
|
-
"\n"
|
|
204
|
-
"def call_all() -> None:\n"
|
|
205
|
-
" make_url_one('/a')\n"
|
|
206
|
-
" make_url_two('/b')\n"
|
|
207
|
-
" make_url_three('/c')\n"
|
|
208
|
-
" make_url_four('/d')\n"
|
|
209
|
-
" make_url_five('/e')\n"
|
|
210
|
-
)
|
|
211
|
-
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
212
|
-
source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
|
|
213
|
-
)
|
|
214
|
-
assert len(issues) == code_rules_enforcer.MAX_ISSUES_PER_CHECK, (
|
|
215
|
-
f"Expected exactly MAX_ISSUES_PER_CHECK issues, got {len(issues)}: {issues}"
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
219
259
|
INCOMPLETE_MOCK_TEST_FILE_PATH = "packages/app/tests/test_orders.py"
|
|
220
260
|
INCOMPLETE_MOCK_PRODUCTION_FILE_PATH = "packages/app/services/orders.py"
|
|
221
261
|
|
|
@@ -675,22 +715,20 @@ def test_advisory_should_still_flag_actual_method_body_constant() -> None:
|
|
|
675
715
|
assert "MAXIMUM_RETRIES" in advisory_issues[0]
|
|
676
716
|
|
|
677
717
|
|
|
678
|
-
def
|
|
679
|
-
|
|
680
|
-
"def
|
|
681
|
-
"
|
|
682
|
-
"
|
|
683
|
-
" GAMMA_CONSTANT = 3\n"
|
|
684
|
-
" DELTA_CONSTANT = 4\n"
|
|
685
|
-
" EPSILON_CONSTANT = 5\n"
|
|
718
|
+
def test_advisory_should_flag_annotated_function_body_constant() -> None:
|
|
719
|
+
source_with_annotated_function_body_constant = (
|
|
720
|
+
"def example_function() -> None:\n"
|
|
721
|
+
" MAXIMUM_RETRIES: int = 3\n"
|
|
722
|
+
" return None\n"
|
|
686
723
|
)
|
|
687
724
|
advisory_issues = code_rules_enforcer.check_constants_outside_config_advisory(
|
|
688
|
-
|
|
725
|
+
source_with_annotated_function_body_constant,
|
|
689
726
|
"example_module.py",
|
|
690
727
|
)
|
|
691
|
-
assert len(advisory_issues) ==
|
|
692
|
-
"
|
|
728
|
+
assert len(advisory_issues) == 1, (
|
|
729
|
+
"Annotated function-body UPPER_SNAKE constant (PEP 526) must surface as advisory"
|
|
693
730
|
)
|
|
731
|
+
assert "MAXIMUM_RETRIES" in advisory_issues[0]
|
|
694
732
|
|
|
695
733
|
|
|
696
734
|
def test_advisory_should_flag_outer_constants_after_nested_def() -> None:
|
|
@@ -958,3 +996,199 @@ def test_should_still_advise_when_duplicated_fstring_literal_is_long(capsys: obj
|
|
|
958
996
|
"Expected the existing /api/<x> path-shape advisory to still fire, "
|
|
959
997
|
f"got: {captured.err!r}"
|
|
960
998
|
)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
LOOP_NAMING_PRODUCTION_FILE_PATH = "packages/app/services/loop_naming.py"
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def test_check_loop_variable_naming_flags_missing_each_prefix() -> None:
|
|
1005
|
+
source = (
|
|
1006
|
+
"def consume() -> None:\n"
|
|
1007
|
+
" for marker in []:\n"
|
|
1008
|
+
" return None\n"
|
|
1009
|
+
)
|
|
1010
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
1011
|
+
source, LOOP_NAMING_PRODUCTION_FILE_PATH
|
|
1012
|
+
)
|
|
1013
|
+
assert any("marker" in each_issue for each_issue in issues), (
|
|
1014
|
+
f"Expected 'marker' loop variable flagged, got: {issues}"
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
INLINE_LITERAL_PRODUCTION_FILE_PATH = "packages/app/services/inline_literal.py"
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def test_check_inline_literal_collections_flags_three_string_set_in_function() -> None:
|
|
1022
|
+
source = (
|
|
1023
|
+
"def is_known(value: str) -> bool:\n"
|
|
1024
|
+
" return value in {'true', 'false', 'none'}\n"
|
|
1025
|
+
)
|
|
1026
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
1027
|
+
source, INLINE_LITERAL_PRODUCTION_FILE_PATH
|
|
1028
|
+
)
|
|
1029
|
+
assert len(issues) == 1, f"Expected 3-element string set flagged, got: {issues}"
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
STRING_MAGIC_PRODUCTION_FILE_PATH = "packages/app/services/string_magic.py"
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def test_check_string_literal_magic_flags_env_var_name() -> None:
|
|
1036
|
+
source = (
|
|
1037
|
+
"import os\n"
|
|
1038
|
+
"\n"
|
|
1039
|
+
"def fetch_secret() -> str:\n"
|
|
1040
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
1041
|
+
)
|
|
1042
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
1043
|
+
source, STRING_MAGIC_PRODUCTION_FILE_PATH
|
|
1044
|
+
)
|
|
1045
|
+
assert any("STRIPE_SECRET" in each_issue for each_issue in issues), (
|
|
1046
|
+
f"Expected env-var name flagged, got: {issues}"
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH = "packages/app/services/encoding.py"
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def test_check_constants_outside_config_flags_annotated_assignment() -> None:
|
|
1054
|
+
source = "TEXT_FILE_ENCODING: str = 'utf-8'\n"
|
|
1055
|
+
issues = code_rules_enforcer.check_constants_outside_config(
|
|
1056
|
+
source, CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH
|
|
1057
|
+
)
|
|
1058
|
+
assert any("TEXT_FILE_ENCODING" in each_issue for each_issue in issues), (
|
|
1059
|
+
f"Expected annotated UPPER_SNAKE assignment flagged, got: {issues}"
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def test_check_constants_outside_config_reports_more_than_three_constants() -> None:
|
|
1064
|
+
source = (
|
|
1065
|
+
"ALPHA_VALUE = 1\n"
|
|
1066
|
+
"BETA_VALUE = 2\n"
|
|
1067
|
+
"GAMMA_VALUE = 3\n"
|
|
1068
|
+
"DELTA_VALUE = 4\n"
|
|
1069
|
+
"EPSILON_VALUE = 5\n"
|
|
1070
|
+
"\n"
|
|
1071
|
+
"def consumer() -> int:\n"
|
|
1072
|
+
" return ALPHA_VALUE + BETA_VALUE\n"
|
|
1073
|
+
)
|
|
1074
|
+
issues = code_rules_enforcer.check_constants_outside_config(
|
|
1075
|
+
source, CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH
|
|
1076
|
+
)
|
|
1077
|
+
expected_constant_count = 5
|
|
1078
|
+
assert len(issues) == expected_constant_count, (
|
|
1079
|
+
f"Expected all {expected_constant_count} constants reported, got {len(issues)}: {issues}"
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def test_stuttering_collection_prefix_flags_function_name_loop1_1() -> None:
|
|
1084
|
+
source = "def all_all_process() -> None:\n return None\n"
|
|
1085
|
+
issues = code_rules_enforcer.check_stuttering_collection_prefix(
|
|
1086
|
+
source, "packages/app/services/foo.py"
|
|
1087
|
+
)
|
|
1088
|
+
assert any("all_all_process" in each_issue for each_issue in issues), (
|
|
1089
|
+
f"loop1-1: stuttering function name must be flagged, got: {issues}"
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def test_stuttering_collection_prefix_flags_with_as_binding_loop3_1() -> None:
|
|
1094
|
+
source = "def f() -> None:\n with open('x') as all_all_context:\n pass\n"
|
|
1095
|
+
issues = code_rules_enforcer.check_stuttering_collection_prefix(
|
|
1096
|
+
source, "packages/app/services/foo.py"
|
|
1097
|
+
)
|
|
1098
|
+
assert any("all_all_context" in each_issue for each_issue in issues), (
|
|
1099
|
+
f"loop3-1: stuttering with-as binding must be flagged, got: {issues}"
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def test_stuttering_collection_prefix_flags_except_as_binding_loop3_1() -> None:
|
|
1104
|
+
source = (
|
|
1105
|
+
"def f() -> None:\n"
|
|
1106
|
+
" try:\n"
|
|
1107
|
+
" pass\n"
|
|
1108
|
+
" except Exception as all_all_error:\n"
|
|
1109
|
+
" pass\n"
|
|
1110
|
+
)
|
|
1111
|
+
issues = code_rules_enforcer.check_stuttering_collection_prefix(
|
|
1112
|
+
source, "packages/app/services/foo.py"
|
|
1113
|
+
)
|
|
1114
|
+
assert any("all_all_error" in each_issue for each_issue in issues), (
|
|
1115
|
+
f"loop3-1: stuttering except-as binding must be flagged, got: {issues}"
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def test_stuttering_constants_live_under_config_subpackage() -> None:
|
|
1120
|
+
"""Stuttering-prefix constants must be sourced from the hooks-tree config package.
|
|
1121
|
+
|
|
1122
|
+
Per CODE_RULES, module-level UPPER_SNAKE constants must live under a
|
|
1123
|
+
directory segment named ``config``. This test pins the move so the
|
|
1124
|
+
constants cannot regress to inline definition at the enforcer module's
|
|
1125
|
+
top level. The enforcer's own bootstrap inserts the hooks tree onto
|
|
1126
|
+
``sys.path`` so ``config.stuttering_check_config`` resolves at runtime.
|
|
1127
|
+
"""
|
|
1128
|
+
assert (
|
|
1129
|
+
code_rules_enforcer.STUTTERING_ALL_PREFIX_PATTERN
|
|
1130
|
+
is config_stuttering_all_prefix_pattern
|
|
1131
|
+
), "Enforcer must reuse the hooks-tree config STUTTERING_ALL_PREFIX_PATTERN object"
|
|
1132
|
+
assert (
|
|
1133
|
+
code_rules_enforcer.MAX_STUTTERING_PREFIX_ISSUES
|
|
1134
|
+
== config_max_stuttering_prefix_issues
|
|
1135
|
+
), "Enforcer must reuse the hooks-tree config MAX_STUTTERING_PREFIX_ISSUES value"
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
SYS_PATH_INSERT_PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
|
|
1139
|
+
SYS_PATH_INSERT_HOOK_INFRASTRUCTURE_FILE_PATH = "/repo/.claude/hooks/blocking/some_hook.py"
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def test_sys_path_insert_should_flag_mismatched_guard_path() -> None:
|
|
1143
|
+
source = (
|
|
1144
|
+
"import sys\n"
|
|
1145
|
+
'if "wrong_path" not in sys.path:\n'
|
|
1146
|
+
' sys.path.insert(0, "actual_path")\n'
|
|
1147
|
+
)
|
|
1148
|
+
issues = code_rules_enforcer.check_sys_path_insert_deduplication_guard(
|
|
1149
|
+
source, SYS_PATH_INSERT_PRODUCTION_FILE_PATH
|
|
1150
|
+
)
|
|
1151
|
+
assert any("sys.path.insert" in each_issue for each_issue in issues), (
|
|
1152
|
+
"Guard testing a different value than what is inserted must be flagged, "
|
|
1153
|
+
f"got: {issues}"
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def test_sys_path_insert_should_not_flag_matching_guard_path() -> None:
|
|
1158
|
+
source = (
|
|
1159
|
+
"import sys\n"
|
|
1160
|
+
'if "correct_path" not in sys.path:\n'
|
|
1161
|
+
' sys.path.insert(0, "correct_path")\n'
|
|
1162
|
+
)
|
|
1163
|
+
issues = code_rules_enforcer.check_sys_path_insert_deduplication_guard(
|
|
1164
|
+
source, SYS_PATH_INSERT_PRODUCTION_FILE_PATH
|
|
1165
|
+
)
|
|
1166
|
+
assert issues == [], (
|
|
1167
|
+
f"Guard testing the same value that is inserted must not be flagged, got: {issues}"
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def test_sys_path_insert_should_not_flag_guarded_insert_in_class_body() -> None:
|
|
1172
|
+
source = (
|
|
1173
|
+
"import sys\n"
|
|
1174
|
+
"class Configurator:\n"
|
|
1175
|
+
" target = '/some/path'\n"
|
|
1176
|
+
" if target not in sys.path:\n"
|
|
1177
|
+
" sys.path.insert(0, target)\n"
|
|
1178
|
+
)
|
|
1179
|
+
issues = code_rules_enforcer.check_sys_path_insert_deduplication_guard(
|
|
1180
|
+
source, SYS_PATH_INSERT_PRODUCTION_FILE_PATH
|
|
1181
|
+
)
|
|
1182
|
+
assert issues == [], (
|
|
1183
|
+
f"Guarded sys.path.insert directly in a class body must not be flagged, got: {issues}"
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def test_sys_path_insert_should_skip_hook_infrastructure_files() -> None:
|
|
1188
|
+
source = "import sys\nsys.path.insert(0, '/some/path')\n"
|
|
1189
|
+
issues = code_rules_enforcer.check_sys_path_insert_deduplication_guard(
|
|
1190
|
+
source, SYS_PATH_INSERT_HOOK_INFRASTRUCTURE_FILE_PATH
|
|
1191
|
+
)
|
|
1192
|
+
assert issues == [], (
|
|
1193
|
+
f"Hook infrastructure files are exempt from this rule, got: {issues}"
|
|
1194
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location(
|
|
8
|
+
"code_rules_enforcer", ENFORCER_PATH
|
|
9
|
+
)
|
|
10
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
11
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
12
|
+
|
|
13
|
+
PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
|
|
14
|
+
TEST_FILE_PATH = "packages/app/tests/test_foo.py"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_should_flag_parameter_without_annotation() -> None:
|
|
18
|
+
source = "def consume(value) -> None:\n return None\n"
|
|
19
|
+
issues = code_rules_enforcer.check_parameter_annotations(
|
|
20
|
+
source, PRODUCTION_FILE_PATH
|
|
21
|
+
)
|
|
22
|
+
assert any("value" in each_issue for each_issue in issues), (
|
|
23
|
+
f"Expected unannotated parameter flagged, got: {issues}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_should_not_flag_annotated_parameters() -> None:
|
|
28
|
+
source = (
|
|
29
|
+
"def consume(value: int, label: str = 'default') -> None:\n return None\n"
|
|
30
|
+
)
|
|
31
|
+
issues = code_rules_enforcer.check_parameter_annotations(
|
|
32
|
+
source, PRODUCTION_FILE_PATH
|
|
33
|
+
)
|
|
34
|
+
assert issues == [], f"Expected no issues for annotated params, got: {issues}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_should_exempt_self_and_cls_parameters() -> None:
|
|
38
|
+
source = (
|
|
39
|
+
"class Foo:\n"
|
|
40
|
+
" def method(self, value: int) -> None:\n"
|
|
41
|
+
" return None\n"
|
|
42
|
+
" @classmethod\n"
|
|
43
|
+
" def factory(cls, value: int) -> 'Foo':\n"
|
|
44
|
+
" return cls()\n"
|
|
45
|
+
)
|
|
46
|
+
issues = code_rules_enforcer.check_parameter_annotations(
|
|
47
|
+
source, PRODUCTION_FILE_PATH
|
|
48
|
+
)
|
|
49
|
+
assert issues == [], (
|
|
50
|
+
f"self/cls must be exempt from annotation requirement, got: {issues}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_should_flag_class_method_parameter_without_annotation() -> None:
|
|
55
|
+
source = "class Foo:\n def method(self, value) -> None:\n return None\n"
|
|
56
|
+
issues = code_rules_enforcer.check_parameter_annotations(
|
|
57
|
+
source, PRODUCTION_FILE_PATH
|
|
58
|
+
)
|
|
59
|
+
assert any("value" in each_issue for each_issue in issues), (
|
|
60
|
+
f"Expected method param flagged, got: {issues}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_should_skip_parameter_check_in_test_files() -> None:
|
|
65
|
+
source = "def consume(value) -> None:\n return None\n"
|
|
66
|
+
issues = code_rules_enforcer.check_parameter_annotations(source, TEST_FILE_PATH)
|
|
67
|
+
assert issues == [], f"Test files must be exempt, got: {issues}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_should_flag_function_without_return_annotation() -> None:
|
|
71
|
+
source = "def fetch(url: str):\n return url\n"
|
|
72
|
+
issues = code_rules_enforcer.check_return_annotations(source, PRODUCTION_FILE_PATH)
|
|
73
|
+
assert any("fetch" in each_issue for each_issue in issues), (
|
|
74
|
+
f"Expected function without return type flagged, got: {issues}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_should_not_flag_function_with_return_annotation() -> None:
|
|
79
|
+
source = "def fetch(url: str) -> str:\n return url\n"
|
|
80
|
+
issues = code_rules_enforcer.check_return_annotations(source, PRODUCTION_FILE_PATH)
|
|
81
|
+
assert issues == [], f"Function with return type must not be flagged, got: {issues}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_should_flag_async_function_without_return_annotation() -> None:
|
|
85
|
+
source = "async def fetch(url: str):\n return url\n"
|
|
86
|
+
issues = code_rules_enforcer.check_return_annotations(source, PRODUCTION_FILE_PATH)
|
|
87
|
+
assert any("fetch" in each_issue for each_issue in issues), (
|
|
88
|
+
f"Expected async function without return type flagged, got: {issues}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_should_skip_return_check_in_test_files() -> None:
|
|
93
|
+
source = "def fetch(url: str):\n return url\n"
|
|
94
|
+
issues = code_rules_enforcer.check_return_annotations(source, TEST_FILE_PATH)
|
|
95
|
+
assert issues == [], f"Test files must be exempt, got: {issues}"
|
|
96
|
+
|
|
97
|
+
|
|
@@ -229,3 +229,109 @@ def test_should_emit_stderr_advisory_on_syntax_error(
|
|
|
229
229
|
captured = capsys.readouterr() # type: ignore[attr-defined]
|
|
230
230
|
assert "banned-identifier check skipped" in captured.err
|
|
231
231
|
assert PRODUCTION_FILE_PATH in captured.err
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_should_flag_argv_assignment() -> None:
|
|
235
|
+
content = "def parse_command():\n argv = collect()\n return argv\n"
|
|
236
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
237
|
+
assert any("'argv'" in each_issue for each_issue in issues), (
|
|
238
|
+
f"Expected 'argv' flagged — use arguments_list, got: {issues}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_should_flag_args_assignment() -> None:
|
|
243
|
+
content = "def parse_command():\n args = collect()\n return args\n"
|
|
244
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
245
|
+
assert any("'args'" in each_issue for each_issue in issues), (
|
|
246
|
+
f"Expected 'args' flagged — use arguments, got: {issues}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_should_not_flag_args_assigned_parse_args_call() -> None:
|
|
251
|
+
content = (
|
|
252
|
+
"def main():\n"
|
|
253
|
+
" parser = build_parser()\n"
|
|
254
|
+
" args = parser.parse_args()\n"
|
|
255
|
+
" return args\n"
|
|
256
|
+
)
|
|
257
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
258
|
+
assert issues == [], (
|
|
259
|
+
"args = parser.parse_args() is established argparse idiom; must not flag, "
|
|
260
|
+
f"got: {issues}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_should_not_flag_args_annotated_parse_args_call() -> None:
|
|
265
|
+
content = (
|
|
266
|
+
"def main():\n"
|
|
267
|
+
" parser = build_parser()\n"
|
|
268
|
+
" args: object = parser.parse_args()\n"
|
|
269
|
+
" return args\n"
|
|
270
|
+
)
|
|
271
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
272
|
+
assert issues == [], f"Annotated parse_args binding must not flag, got: {issues}"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_should_not_flag_args_walrus_parse_args_call() -> None:
|
|
276
|
+
content = (
|
|
277
|
+
"def main():\n"
|
|
278
|
+
" parser = build_parser()\n"
|
|
279
|
+
" if (args := parser.parse_args()):\n"
|
|
280
|
+
" return args\n"
|
|
281
|
+
" return None\n"
|
|
282
|
+
)
|
|
283
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
284
|
+
assert issues == [], f"Walrus parse_args binding must not flag, got: {issues}"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_should_flag_args_assigned_parse_args_method_reference() -> None:
|
|
288
|
+
content = (
|
|
289
|
+
"def main():\n"
|
|
290
|
+
" parser = build_parser()\n"
|
|
291
|
+
" args = parser.parse_args\n"
|
|
292
|
+
" return args\n"
|
|
293
|
+
)
|
|
294
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
295
|
+
assert any("'args'" in each_issue for each_issue in issues), (
|
|
296
|
+
"Method reference (no call) is not the namespace idiom; must flag, "
|
|
297
|
+
f"got: {issues}"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_should_flag_kwargs_assignment() -> None:
|
|
302
|
+
content = "def parse_command():\n kwargs = collect()\n return kwargs\n"
|
|
303
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
304
|
+
assert any("'kwargs'" in each_issue for each_issue in issues), (
|
|
305
|
+
f"Expected 'kwargs' flagged — use keyword_arguments, got: {issues}"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_should_flag_argc_assignment() -> None:
|
|
310
|
+
content = "def parse_command():\n argc = count()\n return argc\n"
|
|
311
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
312
|
+
assert any("'argc'" in each_issue for each_issue in issues), (
|
|
313
|
+
f"Expected 'argc' flagged — use argument_count, got: {issues}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_should_not_flag_args_as_function_parameter() -> None:
|
|
318
|
+
content = (
|
|
319
|
+
"def passthrough(*args, **kwargs):\n"
|
|
320
|
+
" return args, kwargs\n"
|
|
321
|
+
)
|
|
322
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
323
|
+
assert issues == [], (
|
|
324
|
+
f"*args/**kwargs parameters are Python convention, must not flag, got: {issues}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_should_not_flag_argv_substring_in_local_name() -> None:
|
|
329
|
+
content = (
|
|
330
|
+
"def parse_command():\n"
|
|
331
|
+
" parsed_argv_entries = []\n"
|
|
332
|
+
" return parsed_argv_entries\n"
|
|
333
|
+
)
|
|
334
|
+
issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
|
|
335
|
+
assert issues == [], (
|
|
336
|
+
f"Substring 'argv' inside parsed_argv_entries must not flag, got: {issues}"
|
|
337
|
+
)
|