claude-dev-env 1.41.0 → 1.43.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 (214) hide show
  1. package/CLAUDE.md +8 -0
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +232 -8
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +293 -8
  4. package/_shared/pr-loop/scripts/fix_hookspath.py +96 -5
  5. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +124 -20
  6. package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +90 -0
  8. package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +2 -0
  9. package/_shared/pr-loop/scripts/preflight.py +13 -31
  10. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -16
  11. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +76 -33
  12. package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
  13. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
  14. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
  15. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +37 -2
  16. package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +4 -2
  17. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +4 -2
  18. package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +6 -2
  19. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +2 -2
  20. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1 -2
  21. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +4 -2
  22. package/_shared/pr-loop/scripts/tests/test_preflight.py +17 -52
  23. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +6 -2
  24. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +5 -3
  25. package/agents/pr-description-writer.md +50 -140
  26. package/docs/PR_DESCRIPTION_GUIDE.md +101 -102
  27. package/hooks/_gh_pr_author_swap_utils.py +1 -1
  28. package/hooks/blocking/bot_mention_comment_blocker.py +4 -10
  29. package/hooks/blocking/code_rules_enforcer.py +217 -99
  30. package/hooks/blocking/code_rules_path_utils.py +8 -1
  31. package/hooks/blocking/destructive_command_blocker.py +1 -1
  32. package/hooks/blocking/es_exe_path_rewriter.py +7 -13
  33. package/hooks/blocking/gh_body_arg_blocker.py +6 -1
  34. package/hooks/blocking/gh_pr_author_enforcer.py +5 -5
  35. package/hooks/blocking/gh_pr_author_restore.py +5 -5
  36. package/hooks/blocking/hedging_language_blocker.py +4 -10
  37. package/hooks/blocking/md_path_exemptions.py +205 -0
  38. package/hooks/blocking/md_to_html_blocker.py +48 -20
  39. package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -11
  40. package/hooks/blocking/pr_description_enforcer.py +626 -41
  41. package/hooks/blocking/question_to_user_enforcer.py +4 -10
  42. package/hooks/blocking/state_description_blocker.py +6 -12
  43. package/hooks/blocking/tdd_enforcer.py +1 -1
  44. package/hooks/blocking/test_bot_mention_comment_blocker.py +1 -1
  45. package/hooks/blocking/test_code_rules_enforcer.py +3 -3
  46. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +1 -1
  47. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -2
  48. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +184 -0
  49. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +82 -0
  50. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +29 -29
  51. package/hooks/blocking/test_gh_body_arg_blocker.py +7 -8
  52. package/hooks/blocking/test_gh_pr_author_enforcer.py +1 -1
  53. package/hooks/blocking/test_gh_pr_author_restore.py +1 -1
  54. package/hooks/blocking/test_hedging_language_blocker.py +2 -2
  55. package/hooks/blocking/test_md_to_html_blocker.py +463 -8
  56. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +1 -1
  57. package/hooks/blocking/test_pr_description_enforcer.py +1210 -13
  58. package/hooks/blocking/test_question_to_user_enforcer.py +1 -1
  59. package/hooks/blocking/windows_rmtree_blocker.py +5 -11
  60. package/hooks/diagnostic/hook_log_extractor.py +1 -1
  61. package/hooks/diagnostic/hook_log_init.py +1 -1
  62. package/hooks/diagnostic/hook_log_stop_wrapper.py +1 -1
  63. package/hooks/diagnostic/test_hook_log_extractor.py +1 -1
  64. package/hooks/diagnostic/test_hook_log_init.py +2 -2
  65. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +1 -1
  66. package/hooks/git-hooks/gate_utils.py +1 -1
  67. package/hooks/git-hooks/pre_commit.py +1 -1
  68. package/hooks/git-hooks/pre_push.py +1 -1
  69. package/hooks/git-hooks/test_config.py +5 -5
  70. package/hooks/git-hooks/test_pre_push.py +6 -6
  71. package/hooks/{config → hooks_constants}/code_rules_enforcer_constants.py +37 -0
  72. package/hooks/hooks_constants/code_rules_path_utils_constants.py +28 -0
  73. package/hooks/hooks_constants/md_to_html_blocker_constants.py +82 -0
  74. package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_state.py +1 -1
  75. package/hooks/hooks_constants/pr_description_enforcer_constants.py +154 -0
  76. package/hooks/{config → hooks_constants}/pre_tool_use_stdin.py +1 -1
  77. package/hooks/{config → hooks_constants}/project_paths_reader.py +2 -2
  78. package/hooks/{config → hooks_constants}/test_banned_identifiers_constants.py +1 -1
  79. package/hooks/{config → hooks_constants}/test_dynamic_stderr_handler.py +1 -1
  80. package/hooks/{config → hooks_constants}/test_hardcoded_user_path_constants.py +1 -1
  81. package/hooks/{config → hooks_constants}/test_hook_log_extractor_constants.py +2 -2
  82. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +110 -0
  83. package/hooks/{config → hooks_constants}/test_messages.py +2 -6
  84. package/hooks/{config → hooks_constants}/test_path_rewriter_constants.py +1 -1
  85. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +292 -0
  86. package/hooks/{config → hooks_constants}/test_pre_tool_use_stdin.py +2 -2
  87. package/hooks/{config → hooks_constants}/test_project_paths_reader.py +3 -3
  88. package/hooks/{config → hooks_constants}/test_session_env_cleanup_constants.py +1 -1
  89. package/hooks/{config → hooks_constants}/test_setup_project_paths_constants.py +2 -2
  90. package/hooks/{config → hooks_constants}/test_unused_module_import_constants.py +1 -1
  91. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +5 -11
  92. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +1 -1
  93. package/hooks/session/gh_pr_author_session_cleanup.py +5 -6
  94. package/hooks/session/session_env_cleanup.py +4 -10
  95. package/hooks/session/test_gh_pr_author_session_cleanup.py +1 -1
  96. package/hooks/session/test_untracked_repo_detector.py +2 -2
  97. package/hooks/session/untracked_repo_detector.py +6 -12
  98. package/hooks/test__gh_pr_author_swap_utils.py +1 -1
  99. package/hooks/validators/run_all_validators.py +16 -5
  100. package/hooks/validators/test_output_formatter.py +46 -0
  101. package/hooks/workflow/doc_gist_auto_publish.py +1 -1
  102. package/hooks/workflow/md_to_html_companion.py +8 -15
  103. package/hooks/workflow/test_md_to_html_companion.py +184 -23
  104. package/package.json +1 -1
  105. package/rules/ask-user-question-required.md +1 -1
  106. package/rules/vault-context.md +1 -1
  107. package/scripts/{config → dev_env_scripts_constants}/timing.py +1 -1
  108. package/scripts/setup_project_paths.py +49 -11
  109. package/scripts/sweep_empty_dirs.py +10 -1
  110. package/scripts/test_setup_project_paths.py +2 -2
  111. package/scripts/test_sweep_empty_dirs.py +2 -6
  112. package/skills/_shared/pr-loop/scripts/_path_resolver.py +1 -1
  113. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  114. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  115. package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -1
  116. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -1
  117. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  118. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  119. package/skills/bugteam/PROMPTS.md +1 -1
  120. package/skills/bugteam/SKILL.md +1 -1
  121. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  122. package/skills/bugteam/scripts/{_claude_permissions_common.py → _bugteam_permissions_common.py} +110 -13
  123. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +1 -13
  124. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +1 -16
  125. package/skills/bugteam/scripts/bugteam_preflight.py +1 -13
  126. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +69 -0
  127. package/skills/bugteam/scripts/grant_project_claude_permissions.py +117 -12
  128. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +1 -1
  129. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  130. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +71 -25
  131. package/skills/bugteam/scripts/{test__claude_permissions_common.py → test__bugteam_permissions_common.py} +4 -4
  132. package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
  133. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -26
  134. package/skills/bugteam/scripts/{test_claude_permissions_common.py → test_bugteam_permissions_common.py} +3 -66
  135. package/skills/bugteam/scripts/test_bugteam_preflight.py +2 -27
  136. package/skills/bugteam/scripts/windows_safe_rmtree.py +1 -1
  137. package/skills/doc-gist/SKILL.md +1 -1
  138. package/skills/doc-gist/scripts/gist_upload.py +1 -1
  139. package/skills/implement/SKILL.md +66 -0
  140. package/skills/implement/scripts/append_note.py +133 -0
  141. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  142. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +12 -0
  143. package/skills/implement/scripts/test_append_note.py +191 -0
  144. package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
  145. package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +6 -1
  146. package/skills/pr-converge/scripts/check_bugbot_ci.py +2 -2
  147. package/skills/pr-converge/scripts/check_convergence.py +175 -29
  148. package/skills/pr-converge/scripts/check_pending_reviews.py +2 -2
  149. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +2 -2
  150. package/skills/pr-converge/scripts/post_fix_reply.py +2 -2
  151. package/skills/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
  152. package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py +1 -1
  153. package/skills/pr-converge/scripts/reflow_skill_md.py +90 -16
  154. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
  155. package/skills/pr-converge/scripts/test_check_convergence.py +324 -0
  156. package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
  157. package/skills/refine/SKILL.md +257 -0
  158. package/skills/refine/templates/implementation-notes-template.html +56 -0
  159. package/skills/refine/templates/plan-template.md +60 -0
  160. package/skills/session-log/SKILL.md +98 -233
  161. package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +0 -36
  162. package/hooks/config/pr_description_enforcer_constants.py +0 -19
  163. package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
  164. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +0 -20
  165. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
  166. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
  167. package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
  168. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
  169. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
  170. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
  171. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
  172. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
  173. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
  174. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
  175. /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
  176. /package/hooks/{config → hooks_constants}/__init__.py +0 -0
  177. /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
  178. /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
  179. /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
  180. /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
  181. /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
  182. /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
  183. /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
  184. /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
  185. /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
  186. /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
  187. /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
  188. /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
  189. /package/hooks/{config → hooks_constants}/messages.py +0 -0
  190. /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
  191. /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
  192. /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
  193. /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
  194. /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
  195. /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
  196. /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
  197. /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
  198. /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
  199. /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
  200. /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
  201. /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
  202. /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
  203. /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
  204. /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
  205. /package/skills/{pr-converge/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
  206. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
  207. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
  208. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
  209. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
  210. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
  211. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
  212. /package/skills/{pr-converge/scripts/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
  213. /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
  214. /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
@@ -15,17 +15,11 @@ import re
15
15
  import sys
16
16
  from pathlib import Path
17
17
 
18
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
19
+ if _hooks_dir not in sys.path:
20
+ sys.path.insert(0, _hooks_dir)
18
21
 
19
- def _insert_hooks_tree_for_imports() -> None:
20
- hooks_tree = Path(__file__).resolve().parent.parent
21
- hooks_tree_string = str(hooks_tree)
22
- if hooks_tree_string not in sys.path:
23
- sys.path.insert(0, hooks_tree_string)
24
-
25
-
26
- _insert_hooks_tree_for_imports()
27
-
28
- from config.messages import USER_FACING_ASKUSERQUESTION_NOTICE
22
+ from hooks_constants.messages import USER_FACING_ASKUSERQUESTION_NOTICE # noqa: E402
29
23
 
30
24
 
31
25
  def strip_code_and_quotes(text: str) -> str:
@@ -6,23 +6,17 @@ Enforces the "describe current state only" rule — no "instead of", "previously
6
6
  describe what IS, not what WAS or what CHANGED.
7
7
  """
8
8
 
9
- import io
10
9
  import json
11
10
  import os
12
11
  import sys
13
12
  from pathlib import Path
13
+ from typing import TextIO
14
14
 
15
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
16
+ if _hooks_dir not in sys.path:
17
+ sys.path.insert(0, _hooks_dir)
15
18
 
16
- def _insert_hooks_tree_for_imports() -> None:
17
- hooks_tree = Path(__file__).absolute().parent.parent
18
- hooks_tree_string = str(hooks_tree)
19
- if hooks_tree_string not in sys.path:
20
- sys.path.insert(0, hooks_tree_string)
21
-
22
-
23
- _insert_hooks_tree_for_imports()
24
-
25
- from config.state_description_blocker_constants import (
19
+ from hooks_constants.state_description_blocker_constants import ( # noqa: E402
26
20
  ALL_BLOCK_COMMENT_EXTENSIONS,
27
21
  ALL_BLOCK_COMMENT_ONLY_EXTENSIONS,
28
22
  ALL_COMMENT_BEARING_EXTENSIONS,
@@ -233,7 +227,7 @@ def main() -> None:
233
227
  sys.exit(0)
234
228
 
235
229
 
236
- def _emit_hook_result(all_hook_data: dict, output_stream: io.TextIOBase) -> None:
230
+ def _emit_hook_result(all_hook_data: dict, output_stream: TextIO) -> None:
237
231
  """Write the hook result JSON to the given output stream."""
238
232
  output_stream.write(json.dumps(all_hook_data) + "\n")
239
233
  output_stream.flush()
@@ -18,7 +18,7 @@ _hooks_root_path_string = str(Path(__file__).resolve().parent.parent)
18
18
  if _hooks_root_path_string not in sys.path:
19
19
  sys.path.insert(0, _hooks_root_path_string)
20
20
 
21
- from config.messages import USER_FACING_TDD_NOTICE
21
+ from hooks_constants.messages import USER_FACING_TDD_NOTICE
22
22
 
23
23
  PRODUCTION_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx'}
24
24
  SKIP_PATTERNS = {
@@ -22,7 +22,7 @@ hook_spec.loader.exec_module(hook_module)
22
22
  _detect_bot_mention = hook_module._detect_bot_mention
23
23
  _body_contains_token = hook_module._body_contains_token
24
24
 
25
- from config.bot_mention_comment_blocker_constants import (
25
+ from hooks_constants.bot_mention_comment_blocker_constants import (
26
26
  CORRECTIVE_MESSAGE_COPILOT,
27
27
  CORRECTIVE_MESSAGE_CURSOR,
28
28
  CURSOR_MENTION_TOKEN,
@@ -40,18 +40,18 @@ if str(_HOOKS_TREE_DIR) not in sys.path:
40
40
  sys.path.insert(0, str(_HOOKS_TREE_DIR))
41
41
 
42
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
43
+ from hooks_constants.banned_identifiers_constants import ( # noqa: E402
44
44
  ALL_BANNED_IDENTIFIERS as config_all_banned_identifiers,
45
45
  BANNED_IDENTIFIER_MESSAGE_SUFFIX as config_banned_identifier_message_suffix,
46
46
  BANNED_IDENTIFIER_SKIP_ADVISORY as config_banned_identifier_skip_advisory,
47
47
  MAX_BANNED_IDENTIFIER_ISSUES as config_max_banned_identifier_issues,
48
48
  )
49
- from config.hardcoded_user_path_constants import ( # noqa: E402
49
+ from hooks_constants.hardcoded_user_path_constants import ( # noqa: E402
50
50
  HARDCODED_USER_PATH_GUIDANCE as config_hardcoded_user_path_guidance,
51
51
  HARDCODED_USER_PATH_PATTERN as config_hardcoded_user_path_pattern,
52
52
  MAX_HARDCODED_USER_PATH_ISSUES as config_max_hardcoded_user_path_issues,
53
53
  )
54
- from config.stuttering_check_config import ( # noqa: E402
54
+ from hooks_constants.stuttering_check_config import ( # noqa: E402
55
55
  MAX_STUTTERING_PREFIX_ISSUES as config_max_stuttering_prefix_issues,
56
56
  STUTTERING_ALL_PREFIX_PATTERN as config_stuttering_all_prefix_pattern,
57
57
  )
@@ -56,7 +56,7 @@ def test_should_still_flag_in_regular_module() -> None:
56
56
 
57
57
 
58
58
  def test_any_type_config_module_exists_and_exposes_constant() -> None:
59
- config_module_path = Path(__file__).parent.parent / "config" / "any_type_config.py"
59
+ config_module_path = Path(__file__).parent.parent / "hooks_constants" / "any_type_config.py"
60
60
  assert config_module_path.is_file(), f"Missing: {config_module_path}"
61
61
  spec = importlib.util.spec_from_file_location("any_type_config_under_test", config_module_path)
62
62
  assert spec is not None
@@ -48,8 +48,6 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
48
48
  "check_boolean_naming",
49
49
  "check_collection_prefix",
50
50
  "check_comment_changes",
51
- "check_comments_javascript",
52
- "check_comments_python",
53
51
  "check_constant_equality_tests",
54
52
  "check_constants_outside_config",
55
53
  "check_constants_outside_config_advisory",
@@ -0,0 +1,184 @@
1
+ """Tests pinning string-aware ``#`` detection in the Python comment checks.
2
+
3
+ ``#`` characters appear inside string literals in three common shapes:
4
+ hex color codes (``"#FFFFFF"``), URL fragments
5
+ (``"https://x#section"``), and f-string interpolation patterns. None of
6
+ those ``#`` characters belong to a comment token. ``check_comments_python``
7
+ and the Python branch of ``extract_comment_texts`` route their ``#``
8
+ detection through ``tokenize.generate_tokens`` so only true ``COMMENT``
9
+ tokens are considered. These tests pin both halves of that contract:
10
+ ``#``-in-strings is exempt; real inline comments that land AFTER such
11
+ a string still flag.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import importlib.util
17
+ from pathlib import Path
18
+ from types import ModuleType
19
+
20
+
21
+ def _load_enforcer_module() -> ModuleType:
22
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
23
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
24
+ assert spec is not None
25
+ assert spec.loader is not None
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ code_rules_enforcer = _load_enforcer_module()
32
+
33
+
34
+ def test_python_check_should_not_flag_hex_color_literal() -> None:
35
+ content = 'palette_primary = "#FFFFFF"\n'
36
+ issues = code_rules_enforcer.check_comments_python(content)
37
+ assert issues == []
38
+
39
+
40
+ def test_python_check_should_not_flag_url_fragment_in_string() -> None:
41
+ content = 'docs_link = "https://example.com/guide#installation"\n'
42
+ issues = code_rules_enforcer.check_comments_python(content)
43
+ assert issues == []
44
+
45
+
46
+ def test_python_check_should_not_flag_hash_inside_fstring_interpolation() -> None:
47
+ content = 'rendered = f"prefix #{count} suffix"\n'
48
+ issues = code_rules_enforcer.check_comments_python(content)
49
+ assert issues == []
50
+
51
+
52
+ def test_python_check_should_not_flag_hash_inside_triple_quoted_string() -> None:
53
+ content = 'message = """use # for inline comments"""\n'
54
+ issues = code_rules_enforcer.check_comments_python(content)
55
+ assert issues == []
56
+
57
+
58
+ def test_python_check_should_flag_real_inline_comment_after_string_with_hash() -> None:
59
+ content = 'name = "user" # this comment must still flag\n'
60
+ issues = code_rules_enforcer.check_comments_python(content)
61
+ assert len(issues) == 1
62
+ assert "Comment found" in issues[0]
63
+
64
+
65
+ def test_python_check_should_flag_real_comment_after_hex_color_literal() -> None:
66
+ content = 'palette_primary = "#FFFFFF" # accidentally added comment\n'
67
+ issues = code_rules_enforcer.check_comments_python(content)
68
+ assert len(issues) == 1
69
+ assert "Comment found" in issues[0]
70
+
71
+
72
+ def test_extract_should_not_classify_hex_color_as_inline_comment() -> None:
73
+ content = 'palette_primary = "#FFFFFF"\n'
74
+ inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
75
+ assert inline == set()
76
+ assert standalone == set()
77
+
78
+
79
+ def test_extract_should_classify_real_inline_comment_after_string_with_hash() -> None:
80
+ content = 'name = "user" # real comment\n'
81
+ inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
82
+ assert len(inline) == 1
83
+ assert "# real comment" in next(iter(inline))
84
+ assert standalone == set()
85
+
86
+
87
+ def test_extract_should_classify_standalone_comment_correctly() -> None:
88
+ content = "# standalone comment\nx = 1\n"
89
+ inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
90
+ assert "# standalone comment" in standalone
91
+ assert inline == set()
92
+
93
+
94
+ def test_extract_should_distinguish_inline_from_standalone_in_same_file() -> None:
95
+ content = '# standalone first\nx = "#FFFFFF" # inline real comment\n# standalone second\n'
96
+ inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
97
+ assert "# inline real comment" in next(iter(inline))
98
+ assert "# standalone first" in standalone
99
+ assert "# standalone second" in standalone
100
+
101
+
102
+ def test_check_comment_changes_skips_removal_when_new_python_un_parseable() -> None:
103
+ old_content = 'x = 1 # existing comment\n'
104
+ new_content = '"""unterminated multi-line string\n'
105
+ issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
106
+ assert issues == []
107
+
108
+
109
+ def test_check_comment_changes_skips_removal_when_old_python_un_parseable() -> None:
110
+ old_content = '"""unterminated multi-line string\n'
111
+ new_content = 'x = 1 # newly added comment\n'
112
+ issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
113
+ assert issues == []
114
+
115
+
116
+ def test_check_comment_changes_still_detects_removal_on_parseable_python() -> None:
117
+ old_content = 'x = 1 # existing comment\n'
118
+ new_content = 'x = 1\n'
119
+ issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
120
+ assert any("Existing comment removed" in each_issue for each_issue in issues)
121
+
122
+
123
+ def test_python_check_should_exempt_directive_without_space_after_hash() -> None:
124
+ for each_directive in ("#noqa", "#type: ignore", "#pylint: disable", "#pragma: no cover"):
125
+ content = f"{each_directive}\n"
126
+ issues = code_rules_enforcer.check_comments_python(content)
127
+ assert issues == [], f"expected exempt for {each_directive!r}"
128
+
129
+
130
+ def test_python_check_should_exempt_directive_with_tab_after_hash() -> None:
131
+ content = "#\tnoqa: F401\n"
132
+ issues = code_rules_enforcer.check_comments_python(content)
133
+ assert issues == []
134
+
135
+
136
+ def test_python_check_should_still_flag_unrelated_no_space_comment() -> None:
137
+ content = "#realcomment text\n"
138
+ issues = code_rules_enforcer.check_comments_python(content)
139
+ assert len(issues) == 1
140
+
141
+
142
+ def test_extract_should_exempt_directive_without_space_after_hash() -> None:
143
+ content = "#noqa\nx = 1\n"
144
+ inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
145
+ assert standalone == set()
146
+ assert inline == set()
147
+
148
+
149
+ def test_python_check_should_exempt_bare_hash_comment() -> None:
150
+ for each_content in ("#\n", "# \n", "x = 1 #\n"):
151
+ issues = code_rules_enforcer.check_comments_python(each_content)
152
+ assert issues == [], f"expected exempt for bare hash in {each_content!r}"
153
+
154
+
155
+ def test_extract_should_not_classify_bare_hash_as_comment() -> None:
156
+ content = "#\nx = 1 #\n"
157
+ inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
158
+ assert standalone == set()
159
+ assert inline == set()
160
+
161
+
162
+ def test_python_check_should_exempt_true_shebang_on_line_one() -> None:
163
+ content = "#!/usr/bin/env python3\nx = 1\n"
164
+ issues = code_rules_enforcer.check_comments_python(content)
165
+ assert issues == []
166
+
167
+
168
+ def test_python_check_should_flag_inline_shebang_lookalike() -> None:
169
+ content = "x = 1 #! hidden comment\n"
170
+ issues = code_rules_enforcer.check_comments_python(content)
171
+ assert len(issues) == 1
172
+
173
+
174
+ def test_python_check_should_flag_shebang_on_later_standalone_line() -> None:
175
+ content = "x = 1\n#! not a real shebang\n"
176
+ issues = code_rules_enforcer.check_comments_python(content)
177
+ assert len(issues) == 1
178
+
179
+
180
+ def test_extract_should_classify_inline_shebang_lookalike_as_inline_comment() -> None:
181
+ content = "x = 1 #! hidden\n"
182
+ inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
183
+ assert inline != set()
184
+ assert standalone == set()
@@ -110,3 +110,85 @@ def test_should_track_only_innermost_type_checking_block() -> None:
110
110
  assert len(issues) == 1
111
111
  assert issues[0].startswith("Line 7:")
112
112
  assert "Import inside function" in issues[0]
113
+
114
+
115
+ def test_should_skip_docstring_lines_starting_with_import_keyword() -> None:
116
+ """Docstring sentences that incidentally start with ``from `` or ``import `` after
117
+ stripping must not trigger the import-inside-function check.
118
+ """
119
+ content = (
120
+ "def helper():\n"
121
+ ' """Apply the priority queue atomically.\n'
122
+ "\n"
123
+ " from a rename within the trailing-revenue window the duplicate\n"
124
+ " import the loaders for the cycle so the writer can advance.\n"
125
+ ' """\n'
126
+ " return 42\n"
127
+ )
128
+ issues = check_imports_at_top(content)
129
+ assert issues == []
130
+
131
+
132
+ def test_should_still_flag_real_import_after_docstring_closes() -> None:
133
+ """An actual import statement after a one-line docstring closes must still flag."""
134
+ content = (
135
+ "def helper():\n"
136
+ ' """One-line docstring."""\n'
137
+ " import os\n"
138
+ " return os\n"
139
+ )
140
+ issues = check_imports_at_top(content)
141
+ assert len(issues) == 1
142
+ assert "Import inside function" in issues[0]
143
+
144
+
145
+ def test_should_skip_triple_single_quoted_docstring_lines() -> None:
146
+ """Triple-single-quote (''') docstrings exempt their interior lines too."""
147
+ content = (
148
+ "def helper():\n"
149
+ " '''Apply the cycle reset.\n"
150
+ "\n"
151
+ " from a rename within the cycle window the writer would advance.\n"
152
+ " '''\n"
153
+ " return 1\n"
154
+ )
155
+ issues = check_imports_at_top(content)
156
+ assert issues == []
157
+
158
+
159
+ def test_should_flag_real_import_after_multi_line_docstring_closes() -> None:
160
+ """Real imports landing AFTER a multi-line docstring closes must still flag,
161
+ confirming the triple-quote state correctly transitions back to ``None``.
162
+ """
163
+ content = (
164
+ "def helper():\n"
165
+ ' """Apply the priority queue atomically.\n'
166
+ "\n"
167
+ " from a rename within the cycle window.\n"
168
+ ' """\n'
169
+ " import os\n"
170
+ " return os\n"
171
+ )
172
+ issues = check_imports_at_top(content)
173
+ assert len(issues) == 1
174
+ assert "Import inside function" in issues[0]
175
+
176
+
177
+ def test_should_skip_module_level_docstring_text() -> None:
178
+ """A module-level docstring containing ``from ``/``import `` text must not flag.
179
+
180
+ The check ignores top-level lines regardless of triple-quote state because
181
+ function-tracking is the only path that produces issues, but this exercises
182
+ the entry-condition path where the docstring opens on line 1.
183
+ """
184
+ content = (
185
+ '"""Module docstring opener.\n'
186
+ "\n"
187
+ "from a rename within the trailing-revenue window.\n"
188
+ '"""\n'
189
+ "\n"
190
+ "def helper():\n"
191
+ " return 1\n"
192
+ )
193
+ issues = check_imports_at_top(content)
194
+ assert issues == []
@@ -39,7 +39,7 @@ TEST_FILE_PATH = "packages/app/tests/test_loader.py"
39
39
 
40
40
  def test_should_flag_unused_from_import() -> None:
41
41
  source = (
42
- "from config.preflight_constants import VENV_DIRECTORY_NAME\n"
42
+ "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
43
43
  "\n"
44
44
  "def run() -> None:\n"
45
45
  " return None\n"
@@ -52,7 +52,7 @@ def test_should_flag_unused_from_import() -> None:
52
52
 
53
53
  def test_should_not_flag_used_from_import() -> None:
54
54
  source = (
55
- "from config.preflight_constants import VENV_DIRECTORY_NAME\n"
55
+ "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
56
56
  "\n"
57
57
  "def run() -> str:\n"
58
58
  " return VENV_DIRECTORY_NAME\n"
@@ -79,7 +79,7 @@ def test_should_not_flag_when_alias_is_used() -> None:
79
79
 
80
80
  def test_should_skip_file_with_dunder_all() -> None:
81
81
  source = (
82
- "from config.preflight_constants import VENV_DIRECTORY_NAME\n"
82
+ "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
83
83
  "\n"
84
84
  "__all__ = ['something']\n"
85
85
  )
@@ -91,7 +91,7 @@ def test_should_skip_file_with_dunder_all() -> None:
91
91
 
92
92
  def test_should_skip_file_with_dunder_all_annotated_assignment() -> None:
93
93
  source = (
94
- "from config.preflight_constants import VENV_DIRECTORY_NAME\n"
94
+ "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
95
95
  "\n"
96
96
  '__all__: list[str] = ["VENV_DIRECTORY_NAME"]\n'
97
97
  )
@@ -105,7 +105,7 @@ def test_should_skip_file_with_dunder_all_annotated_assignment() -> None:
105
105
  def test_should_skip_file_using_type_checking_block() -> None:
106
106
  source = (
107
107
  "from typing import TYPE_CHECKING\n"
108
- "from config.constants import UNUSED_NAME\n"
108
+ "from hooks_constants.constants import UNUSED_NAME\n"
109
109
  "\n"
110
110
  "if TYPE_CHECKING:\n"
111
111
  " from somewhere import OtherName\n"
@@ -121,7 +121,7 @@ def test_should_skip_file_using_type_checking_block() -> None:
121
121
 
122
122
  def test_should_skip_test_files() -> None:
123
123
  source = (
124
- "from config.constants import UNUSED_NAME\n"
124
+ "from hooks_constants.constants import UNUSED_NAME\n"
125
125
  "\n"
126
126
  "def test_thing() -> None:\n"
127
127
  " assert True\n"
@@ -139,7 +139,7 @@ def test_should_handle_syntax_error_gracefully() -> None:
139
139
  def test_should_include_line_number_in_issue() -> None:
140
140
  source = (
141
141
  "\n"
142
- "from config.preflight_constants import VENV_DIRECTORY_NAME\n"
142
+ "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
143
143
  "\n"
144
144
  "def run() -> None:\n"
145
145
  " return None\n"
@@ -152,7 +152,7 @@ def test_should_include_line_number_in_issue() -> None:
152
152
 
153
153
  def test_should_flag_each_unused_in_multi_import() -> None:
154
154
  source = (
155
- "from config.constants import USED_ONE, UNUSED_TWO\n"
155
+ "from hooks_constants.constants import USED_ONE, UNUSED_TWO\n"
156
156
  "\n"
157
157
  "def run() -> str:\n"
158
158
  " return USED_ONE\n"
@@ -178,7 +178,7 @@ def test_should_not_flag_when_referenced_in_annotation() -> None:
178
178
 
179
179
  def test_should_skip_noqa_marked_imports() -> None:
180
180
  source = (
181
- "from config.constants import UNUSED_BUT_DELIBERATE # noqa: F401\n"
181
+ "from hooks_constants.constants import UNUSED_BUT_DELIBERATE # noqa: F401\n"
182
182
  "\n"
183
183
  "def run() -> None:\n"
184
184
  " return None\n"
@@ -191,7 +191,7 @@ def test_should_skip_noqa_marked_imports() -> None:
191
191
 
192
192
  def test_should_skip_noqa_on_from_keyword_line_for_multiline_import() -> None:
193
193
  source = (
194
- "from config.constants import ( # noqa: F401\n"
194
+ "from hooks_constants.constants import ( # noqa: F401\n"
195
195
  " SOME_CONSTANT,\n"
196
196
  " ANOTHER_CONSTANT,\n"
197
197
  ")\n"
@@ -261,7 +261,7 @@ def test_should_flag_when_name_only_appears_in_comment() -> None:
261
261
 
262
262
  def test_should_not_skip_when_type_checking_only_in_string_constant() -> None:
263
263
  source = (
264
- 'from config.constants import UNUSED_NAME\n'
264
+ 'from hooks_constants.constants import UNUSED_NAME\n'
265
265
  '\n'
266
266
  'HELP_TEXT = "See TYPE_CHECKING docs"\n'
267
267
  '\n'
@@ -276,7 +276,7 @@ def test_should_not_skip_when_type_checking_only_in_string_constant() -> None:
276
276
 
277
277
  def test_should_flag_when_noqa_lists_only_non_f401_codes() -> None:
278
278
  source = (
279
- "from config.constants import UNUSED # noqa: E402\n"
279
+ "from hooks_constants.constants import UNUSED # noqa: E402\n"
280
280
  "\n"
281
281
  "def run() -> None:\n"
282
282
  " return None\n"
@@ -289,7 +289,7 @@ def test_should_flag_when_noqa_lists_only_non_f401_codes() -> None:
289
289
 
290
290
  def test_should_skip_when_noqa_is_bare() -> None:
291
291
  source = (
292
- "from config.constants import UNUSED # noqa\n"
292
+ "from hooks_constants.constants import UNUSED # noqa\n"
293
293
  "\n"
294
294
  "def run() -> None:\n"
295
295
  " return None\n"
@@ -299,9 +299,9 @@ def test_should_skip_when_noqa_is_bare() -> None:
299
299
 
300
300
 
301
301
  def test_should_not_flag_imports_referenced_only_in_full_file_content() -> None:
302
- fragment = "from config.constants import NEW_NAME\n"
302
+ fragment = "from hooks_constants.constants import NEW_NAME\n"
303
303
  full_file = (
304
- "from config.constants import NEW_NAME\n"
304
+ "from hooks_constants.constants import NEW_NAME\n"
305
305
  "\n"
306
306
  "def existing_function() -> str:\n"
307
307
  " return NEW_NAME\n"
@@ -315,9 +315,9 @@ def test_should_not_flag_imports_referenced_only_in_full_file_content() -> None:
315
315
 
316
316
 
317
317
  def test_should_flag_imports_unused_in_full_file_content() -> None:
318
- fragment = "from config.constants import TRULY_UNUSED\n"
318
+ fragment = "from hooks_constants.constants import TRULY_UNUSED\n"
319
319
  full_file = (
320
- "from config.constants import TRULY_UNUSED\n"
320
+ "from hooks_constants.constants import TRULY_UNUSED\n"
321
321
  "\n"
322
322
  "def existing_function() -> None:\n"
323
323
  " return None\n"
@@ -331,10 +331,10 @@ def test_should_flag_imports_unused_in_full_file_content() -> None:
331
331
 
332
332
 
333
333
  def test_should_only_flag_imports_in_fragment_not_full_file() -> None:
334
- fragment = "from config.constants import FRAGMENT_IMPORT\n"
334
+ fragment = "from hooks_constants.constants import FRAGMENT_IMPORT\n"
335
335
  full_file = (
336
- "from config.other import PRE_EXISTING_UNUSED\n"
337
- "from config.constants import FRAGMENT_IMPORT\n"
336
+ "from hooks_constants.other import PRE_EXISTING_UNUSED\n"
337
+ "from hooks_constants.constants import FRAGMENT_IMPORT\n"
338
338
  "\n"
339
339
  "def existing_function() -> str:\n"
340
340
  " return FRAGMENT_IMPORT\n"
@@ -349,9 +349,9 @@ def test_should_only_flag_imports_in_fragment_not_full_file() -> None:
349
349
 
350
350
 
351
351
  def test_should_skip_when_full_file_declares_dunder_all() -> None:
352
- fragment = "from config.constants import NEW_NAME\n"
352
+ fragment = "from hooks_constants.constants import NEW_NAME\n"
353
353
  full_file = (
354
- "from config.constants import NEW_NAME\n"
354
+ "from hooks_constants.constants import NEW_NAME\n"
355
355
  "\n"
356
356
  "__all__ = ['NEW_NAME']\n"
357
357
  )
@@ -365,10 +365,10 @@ def test_should_skip_when_full_file_declares_dunder_all() -> None:
365
365
 
366
366
 
367
367
  def test_should_skip_when_full_file_uses_type_checking_gate() -> None:
368
- fragment = "from config.constants import NEW_NAME\n"
368
+ fragment = "from hooks_constants.constants import NEW_NAME\n"
369
369
  full_file = (
370
370
  "from typing import TYPE_CHECKING\n"
371
- "from config.constants import NEW_NAME\n"
371
+ "from hooks_constants.constants import NEW_NAME\n"
372
372
  "\n"
373
373
  "if TYPE_CHECKING:\n"
374
374
  " from somewhere import OtherName\n"
@@ -387,7 +387,7 @@ def test_should_skip_when_full_file_uses_type_checking_gate() -> None:
387
387
 
388
388
  def test_should_fall_back_to_content_when_full_file_content_is_none() -> None:
389
389
  source = (
390
- "from config.constants import VENV_DIRECTORY_NAME\n"
390
+ "from hooks_constants.constants import VENV_DIRECTORY_NAME\n"
391
391
  "\n"
392
392
  "def run() -> None:\n"
393
393
  " return None\n"
@@ -400,7 +400,7 @@ def test_should_fall_back_to_content_when_full_file_content_is_none() -> None:
400
400
 
401
401
 
402
402
  def test_should_fall_back_when_full_file_content_has_syntax_error() -> None:
403
- fragment = "from config.constants import NEW_NAME\n"
403
+ fragment = "from hooks_constants.constants import NEW_NAME\n"
404
404
  full_file_with_syntax_error = "from config import (\n not python\n"
405
405
  issues = check_unused_module_level_imports(
406
406
  fragment, PRODUCTION_FILE_PATH, full_file_content=full_file_with_syntax_error,
@@ -471,7 +471,7 @@ def test_should_flag_import_when_only_shadowed_local_name_is_loaded() -> None:
471
471
  def test_should_skip_when_type_checking_uses_imported_alias() -> None:
472
472
  source = (
473
473
  "from typing import TYPE_CHECKING as IS_TYPE_CHECKING\n"
474
- "from config.constants import UNUSED_NAME\n"
474
+ "from hooks_constants.constants import UNUSED_NAME\n"
475
475
  "\n"
476
476
  "if IS_TYPE_CHECKING:\n"
477
477
  " from somewhere import OtherName\n"
@@ -488,7 +488,7 @@ def test_should_skip_when_type_checking_uses_imported_alias() -> None:
488
488
  def test_should_skip_when_type_checking_uses_module_alias() -> None:
489
489
  source = (
490
490
  "import typing as t\n"
491
- "from config.constants import UNUSED_NAME\n"
491
+ "from hooks_constants.constants import UNUSED_NAME\n"
492
492
  "\n"
493
493
  "if t.TYPE_CHECKING:\n"
494
494
  " from somewhere import OtherName\n"
@@ -517,7 +517,7 @@ def test_should_not_flag_when_referenced_in_quoted_annotation() -> None:
517
517
 
518
518
  def test_should_flag_when_noqa_only_appears_inside_string_literal() -> None:
519
519
  source = (
520
- "from config.constants import UNUSED; MARKER = '# noqa: F401'\n"
520
+ "from hooks_constants.constants import UNUSED; MARKER = '# noqa: F401'\n"
521
521
  "\n"
522
522
  "def run() -> None:\n"
523
523
  " return None\n"
@@ -19,7 +19,13 @@ hook_spec.loader.exec_module(hook_module)
19
19
  _uses_body_string_arg = hook_module._uses_body_string_arg
20
20
  _has_backtick = hook_module._has_backtick
21
21
 
22
- from _gh_body_arg_utils import iter_significant_tokens
22
+ from blocking._gh_body_arg_utils import (
23
+ _all_equals_prefixes_for_skip,
24
+ _quoted_value_starts_split,
25
+ all_body_flag_prefixes,
26
+ count_extra_tokens_to_skip_for_split_quoted_value,
27
+ iter_significant_tokens,
28
+ )
23
29
 
24
30
 
25
31
  def test_blocks_issue_create_with_body_string() -> None:
@@ -337,40 +343,33 @@ def test_space_form_value_flag_remaining_excludes_consumed_value() -> None:
337
343
 
338
344
 
339
345
  def test_quoted_value_starts_split_unclosed_single_quote() -> None:
340
- from _gh_body_arg_utils import _quoted_value_starts_split
341
346
  assert _quoted_value_starts_split("'it") is True
342
347
 
343
348
 
344
349
  def test_quoted_value_starts_split_fully_closed() -> None:
345
- from _gh_body_arg_utils import _quoted_value_starts_split
346
350
  assert _quoted_value_starts_split("'hello'") is False
347
351
 
348
352
 
349
353
  def test_quoted_value_starts_split_double_quote_unclosed() -> None:
350
- from _gh_body_arg_utils import _quoted_value_starts_split
351
354
  assert _quoted_value_starts_split('"hello') is True
352
355
 
353
356
 
354
357
  def test_count_extra_tokens_returns_none_when_exhausted() -> None:
355
- from _gh_body_arg_utils import count_extra_tokens_to_skip_for_split_quoted_value
356
358
  result = count_extra_tokens_to_skip_for_split_quoted_value([], "'unclosed")
357
359
  assert result is None
358
360
 
359
361
 
360
362
  def test_count_extra_tokens_returns_none_no_closing_in_remaining() -> None:
361
- from _gh_body_arg_utils import count_extra_tokens_to_skip_for_split_quoted_value
362
363
  result = count_extra_tokens_to_skip_for_split_quoted_value(["word", "another"], "'unclosed")
363
364
  assert result is None
364
365
 
365
366
 
366
367
  def test_count_extra_tokens_returns_zero_for_self_contained() -> None:
367
- from _gh_body_arg_utils import count_extra_tokens_to_skip_for_split_quoted_value
368
368
  result = count_extra_tokens_to_skip_for_split_quoted_value(["next"], "'complete'")
369
369
  assert result == 0
370
370
 
371
371
 
372
372
  def test_all_body_flag_prefixes_used_for_equals_skip() -> None:
373
- from _gh_body_arg_utils import _all_equals_prefixes_for_skip, all_body_flag_prefixes
374
373
  for each_prefix in all_body_flag_prefixes:
375
374
  assert each_prefix in _all_equals_prefixes_for_skip
376
375