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.
Files changed (148) hide show
  1. package/agents/clean-coder.md +109 -1
  2. package/agents/docs-agent.md +1 -1
  3. package/agents/project-docs-analyzer.md +0 -1
  4. package/agents/skill-to-agent-converter.md +0 -1
  5. package/bin/install.mjs +28 -8
  6. package/bin/install.test.mjs +9 -1
  7. package/commands/initialize.md +0 -1
  8. package/commands/readability-review.md +4 -4
  9. package/commands/review-plan.md +2 -4
  10. package/commands/stubcheck.md +1 -2
  11. package/docs/CODE_RULES.md +3 -0
  12. package/docs/agents-md-alignment-plan.md +123 -0
  13. package/hooks/blocking/code_rules_enforcer.py +686 -60
  14. package/hooks/blocking/es_exe_path_rewriter.py +10 -4
  15. package/hooks/blocking/test_code_rules_enforcer.py +273 -39
  16. package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
  17. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
  18. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
  19. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +328 -0
  20. package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
  21. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +33 -11
  22. package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
  23. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
  24. package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
  25. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +194 -0
  26. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -13
  27. package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
  28. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
  29. package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
  30. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
  31. package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
  32. package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
  33. package/hooks/blocking/windows_rmtree_blocker.py +23 -6
  34. package/hooks/config/banned_identifiers_constants.py +24 -0
  35. package/hooks/config/hardcoded_user_path_constants.py +12 -0
  36. package/hooks/config/hook_log_extractor_constants.py +1 -1
  37. package/hooks/config/pre_tool_use_stdin.py +48 -0
  38. package/hooks/config/setup_project_paths_constants.py +4 -0
  39. package/hooks/config/stuttering_check_config.py +14 -0
  40. package/hooks/config/stuttering_import_binding_constants.py +11 -0
  41. package/hooks/config/sys_path_insert_constants.py +4 -0
  42. package/hooks/config/test_banned_identifiers_constants.py +48 -0
  43. package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
  44. package/hooks/config/test_hook_log_extractor_constants.py +3 -3
  45. package/hooks/config/test_pre_tool_use_stdin.py +80 -0
  46. package/hooks/config/unused_module_import_constants.py +7 -0
  47. package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
  48. package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
  49. package/hooks/git-hooks/config.py +3 -3
  50. package/hooks/git-hooks/test_gate_utils.py +10 -10
  51. package/hooks/mypy.ini +2 -0
  52. package/package.json +1 -1
  53. package/rules/gh-paginate.md +125 -0
  54. package/skills/bugteam/CONSTRAINTS.md +12 -6
  55. package/skills/bugteam/PROMPTS.md +0 -39
  56. package/skills/bugteam/SKILL.md +93 -125
  57. package/skills/bugteam/SKILL_EVALS.md +25 -23
  58. package/skills/bugteam/reference/README.md +2 -0
  59. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  60. package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
  61. package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
  62. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
  63. package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
  64. package/skills/bugteam/test_skill_additions.py +13 -4
  65. package/skills/bugteam/test_team_lifecycle.py +94 -0
  66. package/skills/findbugs/SKILL.md +3 -3
  67. package/skills/fixbugs/SKILL.md +4 -4
  68. package/skills/monitor-open-prs/SKILL.md +32 -2
  69. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  70. package/skills/pr-converge/SKILL.md +576 -95
  71. package/skills/pr-converge/scripts/README.md +145 -0
  72. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  73. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  74. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  75. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  76. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  77. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  78. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  79. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  80. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  81. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  82. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  83. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  84. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  85. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  86. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  87. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  88. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  89. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  90. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  91. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  92. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  93. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  94. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  95. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  96. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  97. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  98. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  99. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  100. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  101. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  102. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  103. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  104. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  105. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  106. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  107. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  108. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  109. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  110. package/skills/pr-converge/test_team_lifecycle.py +47 -0
  111. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  112. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  113. package/skills/qbug/SKILL.md +4 -4
  114. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  115. package/skills/resume-review/SKILL.md +261 -0
  116. package/agents/agent-writer.md +0 -157
  117. package/agents/config-centralizer.md +0 -686
  118. package/agents/config-extraction-agent.md +0 -225
  119. package/agents/doc-orchestrator.md +0 -47
  120. package/agents/docx-agent.md +0 -211
  121. package/agents/magic-value-eliminator-agent.md +0 -72
  122. package/agents/mandatory-agent-workflow-agent.md +0 -88
  123. package/agents/parallel-workflow-coordinator.md +0 -779
  124. package/agents/pdf-agent.md +0 -302
  125. package/agents/project-context-loader.md +0 -238
  126. package/agents/readability-review-agent.md +0 -76
  127. package/agents/refactoring-specialist.md +0 -69
  128. package/agents/right-sized-engineer.md +0 -129
  129. package/agents/session-continuity-manager.md +0 -53
  130. package/agents/stub-detector-agent.md +0 -140
  131. package/agents/tdd-test-writer.md +0 -62
  132. package/agents/test-data-builder.md +0 -68
  133. package/agents/tooling-builder.md +0 -78
  134. package/agents/validation-expert.md +0 -71
  135. package/agents/xlsx-agent.md +0 -169
  136. package/skills/bugteam/scripts/README.md +0 -58
  137. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  138. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  139. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  140. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  141. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  142. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  143. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  144. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  145. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  146. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  147. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  148. /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 = json.load(sys.stdin)
139
- tool_name = hook_input.get("tool_name", "")
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
- tool_input = hook_input.get("tool_input", {})
143
- command = tool_input.get("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 test_advisory_cap_matches_max_issues_per_check_constant() -> None:
679
- many_constants_source = (
680
- "def crowded_function():\n"
681
- " ALPHA_CONSTANT = 1\n"
682
- " BETA_CONSTANT = 2\n"
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
- many_constants_source,
725
+ source_with_annotated_function_body_constant,
689
726
  "example_module.py",
690
727
  )
691
- assert len(advisory_issues) == code_rules_enforcer.MAX_ISSUES_PER_CHECK, (
692
- "Advisory cap must equal MAX_ISSUES_PER_CHECK, not a hardcoded literal"
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
+ )