claude-dev-env 1.35.0 → 1.36.1

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 (115) hide show
  1. package/agents/clean-coder.md +109 -1
  2. package/bin/install.mjs +28 -8
  3. package/bin/install.test.mjs +9 -1
  4. package/docs/CODE_RULES.md +3 -0
  5. package/docs/agents-md-alignment-plan.md +123 -0
  6. package/hooks/blocking/code_rules_enforcer.py +451 -39
  7. package/hooks/blocking/es_exe_path_rewriter.py +10 -4
  8. package/hooks/blocking/test_code_rules_enforcer.py +182 -0
  9. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
  10. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
  11. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +191 -0
  12. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +40 -0
  13. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
  14. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +87 -3
  15. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -0
  16. package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
  17. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
  18. package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
  19. package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
  20. package/hooks/blocking/windows_rmtree_blocker.py +23 -6
  21. package/hooks/config/banned_identifiers_constants.py +24 -0
  22. package/hooks/config/hardcoded_user_path_constants.py +12 -0
  23. package/hooks/config/hook_log_extractor_constants.py +1 -1
  24. package/hooks/config/pre_tool_use_stdin.py +48 -0
  25. package/hooks/config/setup_project_paths_constants.py +4 -0
  26. package/hooks/config/stuttering_check_config.py +14 -0
  27. package/hooks/config/stuttering_import_binding_constants.py +11 -0
  28. package/hooks/config/sys_path_insert_constants.py +4 -0
  29. package/hooks/config/test_banned_identifiers_constants.py +48 -0
  30. package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
  31. package/hooks/config/test_hook_log_extractor_constants.py +3 -3
  32. package/hooks/config/test_pre_tool_use_stdin.py +80 -0
  33. package/hooks/config/unused_module_import_constants.py +7 -0
  34. package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
  35. package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
  36. package/hooks/git-hooks/config.py +3 -3
  37. package/hooks/git-hooks/test_gate_utils.py +10 -10
  38. package/hooks/mypy.ini +2 -0
  39. package/package.json +1 -1
  40. package/rules/gh-paginate.md +125 -0
  41. package/skills/bugteam/CONSTRAINTS.md +12 -6
  42. package/skills/bugteam/SKILL.md +364 -154
  43. package/skills/bugteam/SKILL_EVALS.md +25 -23
  44. package/skills/bugteam/reference/README.md +2 -0
  45. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  46. package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
  47. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
  48. package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
  49. package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
  50. package/skills/bugteam/test_skill_additions.py +13 -4
  51. package/skills/bugteam/test_team_lifecycle.py +103 -0
  52. package/skills/findbugs/SKILL.md +3 -3
  53. package/skills/fixbugs/SKILL.md +4 -4
  54. package/skills/monitor-open-prs/SKILL.md +32 -2
  55. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  56. package/skills/pr-converge/SKILL.md +1206 -131
  57. package/skills/pr-converge/scripts/README.md +145 -0
  58. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  59. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  60. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  61. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  62. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  63. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  64. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  65. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  66. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  67. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  68. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  69. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  70. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  71. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  72. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  73. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  74. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  75. package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
  76. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  77. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  78. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  79. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  80. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  81. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  82. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  83. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  84. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  85. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  86. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  87. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  88. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  89. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  90. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  91. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  92. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  93. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  94. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  95. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  96. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  97. package/skills/pr-converge/test_team_lifecycle.py +56 -0
  98. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  99. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  100. package/skills/qbug/SKILL.md +4 -4
  101. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  102. package/skills/resume-review/SKILL.md +261 -0
  103. package/skills/bugteam/scripts/README.md +0 -58
  104. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  105. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  106. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  107. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  108. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  109. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  110. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  111. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  112. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  113. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  114. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  115. /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
@@ -110,3 +110,43 @@ def test_should_not_flag_non_test_function() -> None:
110
110
  assert issues == [], f"Expected no issues for non-test function, got: {issues}"
111
111
 
112
112
 
113
+ def test_should_flag_literal_on_left_with_constant_on_right() -> None:
114
+ source = (
115
+ 'CACHE_DIR = "cache"\n'
116
+ "\n"
117
+ "def test_cache_dir_right() -> None:\n"
118
+ ' assert "cache" == CACHE_DIR\n'
119
+ )
120
+ issues = code_rules_enforcer.check_constant_equality_tests(source, TEST_FILE_PATH)
121
+ assert any("constant" in issue.lower() for issue in issues), (
122
+ f"Expected 'cache' == CACHE_DIR flagged equally — equality is commutative, got: {issues}"
123
+ )
124
+
125
+
126
+ def test_should_flag_numeric_literal_on_left_with_constant_on_right() -> None:
127
+ source = (
128
+ "MAX_SIZE = 100\n"
129
+ "\n"
130
+ "def test_max_size_right() -> None:\n"
131
+ " assert 100 == MAX_SIZE\n"
132
+ )
133
+ issues = code_rules_enforcer.check_constant_equality_tests(source, TEST_FILE_PATH)
134
+ assert any("constant" in issue.lower() for issue in issues), (
135
+ f"Expected 100 == MAX_SIZE flagged equally, got: {issues}"
136
+ )
137
+
138
+
139
+ def test_should_not_flag_two_upper_snake_constants_compared() -> None:
140
+ source = (
141
+ "EXPECTED = 5\n"
142
+ "ACTUAL = 5\n"
143
+ "\n"
144
+ "def test_two_constants() -> None:\n"
145
+ " assert EXPECTED == ACTUAL\n"
146
+ )
147
+ issues = code_rules_enforcer.check_constant_equality_tests(source, TEST_FILE_PATH)
148
+ assert issues == [], (
149
+ f"Two-constant comparison is not constant-vs-literal, must not flag, got: {issues}"
150
+ )
151
+
152
+
@@ -0,0 +1,291 @@
1
+ """Tests for hardcoded user path detection.
2
+
3
+ Bot reviewers on PR #257 flagged 6+ instances of 'C:/Users/jon/' embedded
4
+ in production source code, which breaks portability across machines.
5
+ The new rule flags any string literal in production code that names a
6
+ specific user's home directory.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ import pathlib
13
+ import sys
14
+
15
+
16
+ _HOOK_DIRECTORY = pathlib.Path(__file__).parent
17
+ if str(_HOOK_DIRECTORY) not in sys.path:
18
+ sys.path.insert(0, str(_HOOK_DIRECTORY))
19
+
20
+ _hook_spec = importlib.util.spec_from_file_location(
21
+ "code_rules_enforcer",
22
+ _HOOK_DIRECTORY / "code_rules_enforcer.py",
23
+ )
24
+ assert _hook_spec is not None
25
+ assert _hook_spec.loader is not None
26
+ _hook_module = importlib.util.module_from_spec(_hook_spec)
27
+ _hook_spec.loader.exec_module(_hook_module)
28
+ check_hardcoded_user_paths = _hook_module.check_hardcoded_user_paths
29
+ HARDCODED_USER_PATH_PATTERN = _hook_module.HARDCODED_USER_PATH_PATTERN
30
+
31
+
32
+ PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
33
+ TEST_FILE_PATH = "packages/app/tests/test_loader.py"
34
+ CONFIG_FILE_PATH = "packages/app/config/paths.py"
35
+ HOOK_INFRASTRUCTURE_FILE_PATH = "/repo/packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
36
+
37
+
38
+ def test_should_match_user_directory_without_consuming_following_separator() -> None:
39
+ windows = HARDCODED_USER_PATH_PATTERN.search("C:/Users/jon/more")
40
+ macos = HARDCODED_USER_PATH_PATTERN.search("/Users/bob/more")
41
+ linux = HARDCODED_USER_PATH_PATTERN.search("/home/alice/more")
42
+ assert windows is not None and windows.group(0) == "C:/Users/jon"
43
+ assert macos is not None and macos.group(0) == "/Users/bob"
44
+ assert linux is not None and linux.group(0) == "/home/alice"
45
+
46
+
47
+ def test_should_flag_windows_user_path_with_forward_slashes() -> None:
48
+ source = 'def find() -> str:\n return "C:/Users/jon/notes.md"\n'
49
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
50
+ assert any("C:/Users/jon" in each_issue for each_issue in issues), (
51
+ f"Expected Windows user path flagged, got: {issues}"
52
+ )
53
+
54
+
55
+ def test_should_flag_windows_user_path_when_users_segment_is_not_title_case() -> None:
56
+ source = 'def find() -> str:\n return "c:/users/jon/notes.md"\n'
57
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
58
+ assert any("users" in each_issue.lower() for each_issue in issues), (
59
+ f"Expected Windows user path flagged (case-insensitive Users segment), got: {issues}"
60
+ )
61
+
62
+
63
+ def test_should_flag_windows_user_path_with_backslashes() -> None:
64
+ source = 'def find() -> str:\n return "C:\\\\Users\\\\jon\\\\notes.md"\n'
65
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
66
+ assert any("Users" in each_issue for each_issue in issues), (
67
+ f"Expected Windows backslash user path flagged, got: {issues}"
68
+ )
69
+
70
+
71
+ def test_should_flag_unix_home_path() -> None:
72
+ source = 'def find() -> str:\n return "/home/alice/notes.md"\n'
73
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
74
+ assert any("/home/alice" in each_issue for each_issue in issues), (
75
+ f"Expected Unix home path flagged, got: {issues}"
76
+ )
77
+
78
+
79
+ def test_should_flag_macos_user_path() -> None:
80
+ source = 'def find() -> str:\n return "/Users/bob/Documents/data.json"\n'
81
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
82
+ assert any("/Users/bob" in each_issue for each_issue in issues), (
83
+ f"Expected macOS user path flagged, got: {issues}"
84
+ )
85
+
86
+
87
+ def test_should_flag_macos_user_path_when_home_is_entire_path() -> None:
88
+ source = 'def find() -> str:\n return "/Users/bob"\n'
89
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
90
+ assert any("/Users/bob" in each_issue for each_issue in issues), (
91
+ f"Expected macOS user home literal flagged without trailing slash, got: {issues}"
92
+ )
93
+
94
+
95
+ def test_should_not_flag_tilde_home_alias() -> None:
96
+ source = 'def find() -> str:\n return "~/notes.md"\n'
97
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
98
+ assert issues == [], f"Tilde alias is portable, must not flag, got: {issues}"
99
+
100
+
101
+ def test_should_not_flag_users_directory_without_specific_user() -> None:
102
+ source = 'def root_dir() -> str:\n return "C:/Users"\n'
103
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
104
+ assert issues == [], (
105
+ f"Generic /Users root with no specific user has no portability cost, got: {issues}"
106
+ )
107
+
108
+
109
+ def test_should_skip_test_files() -> None:
110
+ source = 'def test_path() -> None:\n fixture = "C:/Users/jon/scratch.txt"\n'
111
+ issues = check_hardcoded_user_paths(source, TEST_FILE_PATH)
112
+ assert issues == [], (
113
+ f"Test files exempt — fixtures often need real paths, got: {issues}"
114
+ )
115
+
116
+
117
+ def test_should_skip_config_files() -> None:
118
+ source = 'DEFAULT_PATH = "C:/Users/jon/notes.md"\n'
119
+ issues = check_hardcoded_user_paths(source, CONFIG_FILE_PATH)
120
+ assert issues == [], (
121
+ f"Config files exempt — that is the right place for paths, got: {issues}"
122
+ )
123
+
124
+
125
+ def test_should_include_line_number_in_issue() -> None:
126
+ source = '\n\ndef find() -> str:\n return "C:/Users/jon/notes.md"\n'
127
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
128
+ assert any("Line 4" in each_issue for each_issue in issues), (
129
+ f"Expected line 4 reference, got: {issues}"
130
+ )
131
+
132
+
133
+ def test_should_handle_syntax_error_gracefully() -> None:
134
+ source = "def broken(\n not python\n"
135
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
136
+ assert issues == [], f"Parse failure must return empty, got: {issues}"
137
+
138
+
139
+ def test_should_suggest_path_home_or_expanduser_in_message() -> None:
140
+ source = 'def find() -> str:\n return "/home/alice/x"\n'
141
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
142
+ assert any(
143
+ "Path.home" in each_issue or "expanduser" in each_issue for each_issue in issues
144
+ ), (
145
+ f"Error message should suggest Path.home() or os.path.expanduser('~'), got: {issues}"
146
+ )
147
+
148
+ def test_should_flag_standalone_home_segment_for_symmetry_with_macos() -> None:
149
+ source = 'def route() -> str:\n return "/home/dashboard"\n'
150
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
151
+ assert any("/home/dashboard" in each_issue for each_issue in issues), (
152
+ f"Linux '/home/<segment>' is structurally indistinguishable from a real"
153
+ f" home directory and must flag for symmetry with macOS, got: {issues}"
154
+ )
155
+
156
+
157
+ def test_should_not_flag_standalone_users_segment_without_trailing_path() -> None:
158
+ source = 'def system_path() -> str:\n return "/Users/Shared"\n'
159
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
160
+ assert issues == [], (
161
+ f"'/Users/Shared' without trailing path component is not navigating into a user home, got: {issues}"
162
+ )
163
+
164
+
165
+ def test_should_skip_hook_infrastructure_files() -> None:
166
+ source = (
167
+ 'HARDCODED_USER_PATH_PATTERN = "/Users/[^/]+|/home/[^/]+"\n'
168
+ 'def find() -> str:\n'
169
+ ' return "C:/Users/jon/notes.md"\n'
170
+ )
171
+ issues = check_hardcoded_user_paths(source, HOOK_INFRASTRUCTURE_FILE_PATH)
172
+ assert issues == [], (
173
+ f"Hook infrastructure files exempt — the enforcer itself encodes user-path"
174
+ f" patterns and would otherwise self-block, got: {issues}"
175
+ )
176
+
177
+
178
+ def test_should_not_flag_docstring_mentioning_user_path() -> None:
179
+ source = (
180
+ 'def load_data() -> None:\n'
181
+ ' """Reads from /home/alice/data for testing."""\n'
182
+ ' pass\n'
183
+ )
184
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
185
+ assert issues == [], (
186
+ f"Docstrings are allowed to mention paths, got: {issues}"
187
+ )
188
+
189
+
190
+ def test_should_flag_linux_home_path_when_home_is_entire_path() -> None:
191
+ source = 'def find() -> str:\n return "/home/alice"\n'
192
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
193
+ assert any("/home/alice" in each_issue for each_issue in issues), (
194
+ f"Expected Linux home literal flagged without trailing slash, got: {issues}"
195
+ )
196
+
197
+
198
+ def test_should_not_flag_windows_public_shared_folder() -> None:
199
+ source = 'def find() -> str:\n return "C:/Users/Public/Documents"\n'
200
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
201
+ assert issues == [], (
202
+ f"Windows 'C:/Users/Public' is a system shared folder, not a user home, got: {issues}"
203
+ )
204
+
205
+
206
+ def test_should_not_flag_windows_shared_folder() -> None:
207
+ source = 'def find() -> str:\n return "C:/Users/Shared/data"\n'
208
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
209
+ assert issues == [], (
210
+ f"Windows 'C:/Users/Shared' is a system shared folder, not a user home, got: {issues}"
211
+ )
212
+
213
+
214
+ def test_should_not_flag_windows_all_users_folder() -> None:
215
+ source = 'def find() -> str:\n return "C:/Users/All Users/AppData"\n'
216
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
217
+ assert issues == [], (
218
+ f"Windows 'C:/Users/All Users' is a legacy shared folder, not a user home, got: {issues}"
219
+ )
220
+
221
+
222
+ def test_should_not_flag_macos_public_shared_folder() -> None:
223
+ source = 'def find() -> str:\n return "/Users/Public/Documents"\n'
224
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
225
+ assert issues == [], (
226
+ f"macOS '/Users/Public' is a default shared folder on every macOS install,"
227
+ f" not a user home — symmetry with the Windows exclusion, got: {issues}"
228
+ )
229
+
230
+
231
+ def test_should_not_flag_windows_lowercase_public_shared_folder() -> None:
232
+ source = 'def find() -> str:\n return "c:/users/public/Documents"\n'
233
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
234
+ assert issues == [], (
235
+ f"Windows 'c:/users/public' is the same shared folder regardless of case,"
236
+ f" the exclusion list must be case-insensitive, got: {issues}"
237
+ )
238
+
239
+
240
+ def test_should_not_flag_windows_lowercase_shared_folder() -> None:
241
+ source = 'def find() -> str:\n return "c:/users/shared/data"\n'
242
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
243
+ assert issues == [], (
244
+ f"Windows 'c:/users/shared' is a system shared folder regardless of case,"
245
+ f" got: {issues}"
246
+ )
247
+
248
+
249
+ def test_should_not_flag_windows_lowercase_all_users_folder() -> None:
250
+ source = 'def find() -> str:\n return "c:/users/all users/AppData"\n'
251
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
252
+ assert issues == [], (
253
+ f"Windows 'c:/users/all users' is a legacy shared folder regardless of case,"
254
+ f" got: {issues}"
255
+ )
256
+
257
+
258
+ def test_should_not_flag_windows_mixed_case_public_shared_folder() -> None:
259
+ source = 'def find() -> str:\n return "C:/Users/PuBlIc/Documents"\n'
260
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
261
+ assert issues == [], (
262
+ f"Windows 'C:/Users/PuBlIc' is the same shared folder in any casing,"
263
+ f" got: {issues}"
264
+ )
265
+
266
+
267
+ def test_should_not_flag_windows_uppercase_public_shared_folder() -> None:
268
+ source = 'def find() -> str:\n return "C:/Users/PUBLIC/Documents"\n'
269
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
270
+ assert issues == [], (
271
+ f"Windows 'C:/Users/PUBLIC' is the same shared folder in any casing,"
272
+ f" got: {issues}"
273
+ )
274
+
275
+
276
+ def test_should_not_flag_macos_lowercase_shared_folder() -> None:
277
+ source = 'def find() -> str:\n return "/Users/shared/data"\n'
278
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
279
+ assert issues == [], (
280
+ f"macOS '/Users/shared' is a system shared folder regardless of case,"
281
+ f" got: {issues}"
282
+ )
283
+
284
+
285
+ def test_should_not_flag_macos_lowercase_public_shared_folder() -> None:
286
+ source = 'def find() -> str:\n return "/Users/public/Documents"\n'
287
+ issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
288
+ assert issues == [], (
289
+ f"macOS '/Users/public' is a default shared folder regardless of case,"
290
+ f" got: {issues}"
291
+ )
@@ -56,16 +56,100 @@ def test_should_flag_bare_each_without_subject() -> None:
56
56
  )
57
57
 
58
58
 
59
- def test_should_not_flag_tuple_unpacking_targets() -> None:
59
+ def test_should_flag_tuple_unpacking_targets_lacking_each_prefix() -> None:
60
60
  source = (
61
61
  "def consume() -> None:\n"
62
- " for key, value in {}.items():\n"
62
+ " for accessed_field, access_line in []:\n"
63
63
  " return None\n"
64
64
  )
65
65
  issues = code_rules_enforcer.check_loop_variable_naming(
66
66
  source, PRODUCTION_FILE_PATH
67
67
  )
68
- assert issues == [], f"Tuple-unpack targets exempt, got: {issues}"
68
+ assert any("accessed_field" in each_issue for each_issue in issues), (
69
+ f"Expected 'accessed_field' tuple-unpack target flagged, got: {issues}"
70
+ )
71
+ assert any("access_line" in each_issue for each_issue in issues), (
72
+ f"Expected 'access_line' tuple-unpack target flagged, got: {issues}"
73
+ )
74
+
75
+
76
+ def test_should_not_flag_tuple_unpacking_when_all_targets_have_each_prefix() -> None:
77
+ source = (
78
+ "def consume() -> None:\n"
79
+ " for each_key, each_value in {}.items():\n"
80
+ " return None\n"
81
+ )
82
+ issues = code_rules_enforcer.check_loop_variable_naming(
83
+ source, PRODUCTION_FILE_PATH
84
+ )
85
+ assert issues == [], (
86
+ f"Tuple-unpack with each_ prefix on all targets must pass, got: {issues}"
87
+ )
88
+
89
+
90
+ def test_should_exempt_underscore_inside_tuple_unpacking() -> None:
91
+ source = (
92
+ "def consume() -> None:\n"
93
+ " for _, each_position in []:\n"
94
+ " return None\n"
95
+ )
96
+ issues = code_rules_enforcer.check_loop_variable_naming(
97
+ source, PRODUCTION_FILE_PATH
98
+ )
99
+ assert issues == [], (
100
+ f"'_' must remain exempt inside tuple unpacking, got: {issues}"
101
+ )
102
+
103
+
104
+ def test_should_flag_partially_compliant_tuple_unpacking() -> None:
105
+ source = (
106
+ "def consume() -> None:\n"
107
+ " for each_key, raw_value in {}.items():\n"
108
+ " return None\n"
109
+ )
110
+ issues = code_rules_enforcer.check_loop_variable_naming(
111
+ source, PRODUCTION_FILE_PATH
112
+ )
113
+ assert any("raw_value" in each_issue for each_issue in issues), (
114
+ f"Mixed-compliance tuple unpack must flag the offender, got: {issues}"
115
+ )
116
+ assert not any("each_key" in each_issue for each_issue in issues), (
117
+ f"each_key compliant target must not be flagged, got: {issues}"
118
+ )
119
+
120
+
121
+ def test_should_flag_nested_tuple_unpacking_targets() -> None:
122
+ source = (
123
+ "def consume() -> None:\n"
124
+ " for outer_label, (inner_first, inner_second) in []:\n"
125
+ " return None\n"
126
+ )
127
+ issues = code_rules_enforcer.check_loop_variable_naming(
128
+ source, PRODUCTION_FILE_PATH
129
+ )
130
+ assert any("inner_first" in each_issue for each_issue in issues), (
131
+ f"Nested tuple-unpack targets must be inspected, got: {issues}"
132
+ )
133
+ assert any("inner_second" in each_issue for each_issue in issues), (
134
+ f"Nested tuple-unpack targets must be inspected, got: {issues}"
135
+ )
136
+
137
+
138
+ def test_should_flag_starred_tuple_unpacking_target() -> None:
139
+ source = (
140
+ "def consume() -> None:\n"
141
+ " for first, *rest in []:\n"
142
+ " return None\n"
143
+ )
144
+ issues = code_rules_enforcer.check_loop_variable_naming(
145
+ source, PRODUCTION_FILE_PATH
146
+ )
147
+ assert any("first" in each_issue for each_issue in issues), (
148
+ f"First tuple-unpack target must be flagged, got: {issues}"
149
+ )
150
+ assert any("rest" in each_issue for each_issue in issues), (
151
+ f"Starred tuple-unpack target must be flagged, got: {issues}"
152
+ )
69
153
 
70
154
 
71
155
  def test_should_not_flag_list_comprehension_target() -> None:
@@ -161,3 +161,52 @@ def test_validate_content_invokes_boolean_naming_check() -> None:
161
161
  assert matching_issues, (
162
162
  f"expected validate_content to surface the boolean-naming issue, got {issues!r}"
163
163
  )
164
+
165
+
166
+ def test_should_flag_substring_is_when_not_at_prefix_position() -> None:
167
+ source = "def f() -> None:\n left_is_upper_snake = True\n"
168
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
169
+ _assert_flags_name(issues, "left_is_upper_snake", 2)
170
+ assert len(issues) == 1, (
171
+ f"'is_' in middle position must not satisfy the prefix rule, got: {issues}"
172
+ )
173
+
174
+
175
+ def test_should_flag_substring_has_when_not_at_prefix_position() -> None:
176
+ source = "def f() -> None:\n user_has_permission = True\n"
177
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
178
+ _assert_flags_name(issues, "user_has_permission", 2)
179
+ assert len(issues) == 1, (
180
+ f"'has_' in middle position must not satisfy the prefix rule, got: {issues}"
181
+ )
182
+
183
+
184
+ def test_should_flag_substring_should_when_not_at_prefix_position() -> None:
185
+ source = "def f() -> None:\n user_should_retry = True\n"
186
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
187
+ _assert_flags_name(issues, "user_should_retry", 2)
188
+ assert len(issues) == 1
189
+
190
+
191
+ def test_should_flag_substring_can_when_not_at_prefix_position() -> None:
192
+ source = "def f() -> None:\n user_can_edit = True\n"
193
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
194
+ _assert_flags_name(issues, "user_can_edit", 2)
195
+ assert len(issues) == 1
196
+
197
+
198
+ def test_should_flag_right_is_literal_substring_match() -> None:
199
+ source = "def f() -> None:\n right_is_literal = False\n"
200
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
201
+ _assert_flags_name(issues, "right_is_literal", 2)
202
+ assert len(issues) == 1, (
203
+ f"PR #232 finding: substring 'is_' in 'right_is_literal' must be flagged, got: {issues}"
204
+ )
205
+
206
+
207
+ def test_should_allow_is_prefix_at_start_when_compound_word_follows() -> None:
208
+ source = "def f() -> None:\n is_left_upper_snake = True\n"
209
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
210
+ assert issues == [], (
211
+ f"is_left_upper_snake has prefix at position 0, must pass, got: {issues}"
212
+ )
@@ -0,0 +1,157 @@
1
+ """Tests for sys.path.insert dedup-guard rule.
2
+
3
+ Bot reviewers on PR #289 flagged grant_project_claude_permissions.py:13
4
+ and revoke_project_claude_permissions.py for unconditionally calling
5
+ sys.path.insert(0, X) without checking whether X was already present.
6
+ The convention in the rest of the repo is to guard the call with
7
+ `if str(X) not in sys.path:` (or equivalent) to avoid pushing the
8
+ same path repeatedly when modules get reloaded.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import pathlib
15
+ import sys
16
+
17
+
18
+ _HOOK_DIRECTORY = pathlib.Path(__file__).parent
19
+ if str(_HOOK_DIRECTORY) not in sys.path:
20
+ sys.path.insert(0, str(_HOOK_DIRECTORY))
21
+
22
+ _hook_spec = importlib.util.spec_from_file_location(
23
+ "code_rules_enforcer",
24
+ _HOOK_DIRECTORY / "code_rules_enforcer.py",
25
+ )
26
+ assert _hook_spec is not None
27
+ assert _hook_spec.loader is not None
28
+ _hook_module = importlib.util.module_from_spec(_hook_spec)
29
+ _hook_spec.loader.exec_module(_hook_module)
30
+ check_sys_path_insert_deduplication_guard = _hook_module.check_sys_path_insert_deduplication_guard
31
+
32
+
33
+ PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
34
+ TEST_FILE_PATH = "packages/app/tests/test_loader.py"
35
+
36
+
37
+ def test_should_flag_unguarded_module_level_insert() -> None:
38
+ source = (
39
+ "import sys\n"
40
+ "from pathlib import Path\n"
41
+ "REPOSITORY_ROOT = Path(__file__).resolve().parent\n"
42
+ "sys.path.insert(0, str(REPOSITORY_ROOT))\n"
43
+ )
44
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
45
+ assert any("sys.path.insert" in each_issue for each_issue in issues), (
46
+ f"Expected unguarded sys.path.insert flagged, got: {issues}"
47
+ )
48
+
49
+
50
+ def test_should_not_flag_when_preceded_by_membership_guard() -> None:
51
+ source = (
52
+ "import sys\n"
53
+ "from pathlib import Path\n"
54
+ "REPOSITORY_ROOT = str(Path(__file__).resolve().parent)\n"
55
+ "if REPOSITORY_ROOT not in sys.path:\n"
56
+ " sys.path.insert(0, REPOSITORY_ROOT)\n"
57
+ )
58
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
59
+ assert issues == [], (
60
+ f"Guarded insert (if X not in sys.path) must not be flagged, got: {issues}"
61
+ )
62
+
63
+
64
+ def test_should_flag_unguarded_function_local_insert() -> None:
65
+ source = (
66
+ "import sys\ndef configure() -> None:\n sys.path.insert(0, '/some/path')\n"
67
+ )
68
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
69
+ assert any("sys.path.insert" in each_issue for each_issue in issues), (
70
+ f"Function-local unguarded insert must be flagged, got: {issues}"
71
+ )
72
+
73
+
74
+ def test_should_not_flag_function_local_when_guarded() -> None:
75
+ source = (
76
+ "import sys\n"
77
+ "def configure() -> None:\n"
78
+ " target = '/some/path'\n"
79
+ " if target not in sys.path:\n"
80
+ " sys.path.insert(0, target)\n"
81
+ )
82
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
83
+ assert issues == [], f"Guarded function-local insert must pass, got: {issues}"
84
+
85
+
86
+ def test_should_not_flag_sys_path_append_or_extend() -> None:
87
+ source = (
88
+ "import sys\nsys.path.append('/some/path')\nsys.path.extend(['/a', '/b'])\n"
89
+ )
90
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
91
+ assert issues == [], (
92
+ f"This rule targets sys.path.insert specifically, append/extend exempt, got: {issues}"
93
+ )
94
+
95
+
96
+ def test_should_skip_test_files() -> None:
97
+ source = "import sys\nsys.path.insert(0, '/some/path')\n"
98
+ issues = check_sys_path_insert_deduplication_guard(source, TEST_FILE_PATH)
99
+ assert issues == [], (
100
+ f"Test files exempt — fixtures often manipulate sys.path, got: {issues}"
101
+ )
102
+
103
+
104
+ def test_should_handle_syntax_error_gracefully() -> None:
105
+ source = "import sys\nsys.path.insert(\n not python\n"
106
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
107
+ assert issues == [], f"Parse failure must return empty, got: {issues}"
108
+
109
+
110
+ def test_should_recognize_str_call_around_path_in_guard() -> None:
111
+ source = (
112
+ "import sys\n"
113
+ "from pathlib import Path\n"
114
+ "ROOT = Path('/x')\n"
115
+ "if str(ROOT) not in sys.path:\n"
116
+ " sys.path.insert(0, str(ROOT))\n"
117
+ )
118
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
119
+ assert issues == [], (
120
+ f"str(ROOT) wrapped in both guard and insert must not flag, got: {issues}"
121
+ )
122
+
123
+
124
+ def test_should_include_line_number_in_issue() -> None:
125
+ source = "import sys\n\n\nsys.path.insert(0, '/x')\n"
126
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
127
+ assert any("Line 4" in each_issue for each_issue in issues), (
128
+ f"Expected line 4 reference, got: {issues}"
129
+ )
130
+
131
+
132
+ def test_should_flag_inverted_membership_guard_inserting_in_then_branch() -> None:
133
+ source = (
134
+ "import sys\n"
135
+ "TARGET = '/some/path'\n"
136
+ "if TARGET in sys.path:\n"
137
+ " sys.path.insert(0, TARGET)\n"
138
+ )
139
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
140
+ assert any("sys.path.insert" in each_issue for each_issue in issues), (
141
+ "`if X in sys.path: sys.path.insert(0, X)` inserts a duplicate when X "
142
+ f"is already present; only `not in` guards then-branch inserts. Got: {issues}"
143
+ )
144
+
145
+
146
+ def test_should_flag_inverted_membership_guard_with_str_wrapper() -> None:
147
+ source = (
148
+ "import sys\n"
149
+ "from pathlib import Path\n"
150
+ "ROOT = Path('/x')\n"
151
+ "if str(ROOT) in sys.path:\n"
152
+ " sys.path.insert(0, str(ROOT))\n"
153
+ )
154
+ issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
155
+ assert any("sys.path.insert" in each_issue for each_issue in issues), (
156
+ f"Inverted membership guard with str() wrapper must still flag, got: {issues}"
157
+ )