claude-dev-env 8.6.0 → 8.7.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 (106) hide show
  1. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  2. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
  3. package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
  4. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +4 -0
  6. package/_shared/pr-loop/scripts/terminology_sweep.py +155 -18
  7. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +130 -0
  8. package/bin/install.test.mjs +25 -0
  9. package/docs/high-trust-agent-delivery.md +132 -0
  10. package/hooks/advisory/refactor_guard.py +2 -2
  11. package/hooks/advisory/test_refactor_guard.py +21 -0
  12. package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
  13. package/hooks/blocking/code_rules_shared.py +4 -7
  14. package/hooks/blocking/conftest.py +22 -0
  15. package/hooks/blocking/state_description_blocker.py +232 -90
  16. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
  17. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +27 -54
  18. package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
  19. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
  20. package/hooks/blocking/test_code_rules_shared.py +18 -0
  21. package/hooks/blocking/test_conftest.py +35 -0
  22. package/hooks/blocking/test_state_description_blocker.py +59 -0
  23. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
  24. package/hooks/git-hooks/pull_request_handoff.py +2 -2
  25. package/hooks/git-hooks/test_config.py +10 -3
  26. package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
  27. package/hooks/git-hooks/test_verification_notice_state.py +50 -0
  28. package/hooks/git-hooks/verification_notice_state.py +3 -1
  29. package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
  30. package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
  31. package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
  32. package/hooks/hooks_constants/state_description_blocker_constants.py +2 -1
  33. package/hooks/hooks_constants/test_state_description_blocker_constants.py +87 -0
  34. package/hooks/validators/exempt_paths.py +3 -18
  35. package/hooks/validators/test_exempt_paths.py +20 -0
  36. package/hooks/validators/test_python_antipattern_checks.py +14 -0
  37. package/package.json +1 -1
  38. package/rules/agent-merges-its-own-green-pull-request.md +54 -0
  39. package/rules/archiving-agent-config.md +10 -0
  40. package/rules/code-standards.md +1 -0
  41. package/rules/correction-lens.md +134 -0
  42. package/rules/flag-non-breaking-findings.md +11 -0
  43. package/rules/no-contrast-framing.md +68 -0
  44. package/rules/review-closure-is-a-check.md +44 -0
  45. package/scripts/Invoke-PesterSuites.ps1 +42 -0
  46. package/scripts/agent_merge_check.py +381 -0
  47. package/scripts/check.ps1 +12 -1
  48. package/scripts/ci/install-playtest.mjs +291 -0
  49. package/scripts/ci/install-playtest.test.mjs +84 -0
  50. package/scripts/ci/installer-lifecycle.mjs +11 -30
  51. package/scripts/ci/installer-lifecycle.test.mjs +14 -0
  52. package/scripts/ci/pstack-command-shim.test.mjs +33 -0
  53. package/scripts/ci/scratch-home-install.mjs +73 -0
  54. package/scripts/ci/scratch-home-install.test.mjs +49 -0
  55. package/scripts/codex_compat_materializer.py +33 -16
  56. package/scripts/contrast_framing.py +101 -0
  57. package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
  58. package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
  59. package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
  60. package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
  61. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
  62. package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
  63. package/scripts/durable_post_lint.py +30 -4
  64. package/scripts/followup_cli.py +6 -10
  65. package/scripts/merge_gate_checks.py +236 -0
  66. package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
  67. package/scripts/policy_lint/adapter_detectors.py +1 -1
  68. package/scripts/policy_lint/adapter_support.py +3 -14
  69. package/scripts/policy_lint/adapters.py +30 -1
  70. package/scripts/policy_lint/config/constants.py +2 -0
  71. package/scripts/policy_lint/registry.py +5 -0
  72. package/scripts/repository_checks/config/constants.py +12 -0
  73. package/scripts/repository_checks/config/policy_document.py +28 -2
  74. package/scripts/repository_checks/followups.py +54 -0
  75. package/scripts/repository_checks/match_exemptions.py +67 -0
  76. package/scripts/repository_checks/models.py +46 -2
  77. package/scripts/repository_checks/reporting.py +23 -7
  78. package/scripts/repository_checks/tracked_secrets.py +2 -2
  79. package/scripts/repository_policy.py +4 -2
  80. package/scripts/review_closure.py +134 -0
  81. package/scripts/review_closure_github.py +345 -0
  82. package/scripts/review_closure_model.py +360 -0
  83. package/scripts/test_agent_merge_check.py +317 -0
  84. package/scripts/test_durable_post_lint.py +46 -0
  85. package/scripts/test_followup_cli.py +40 -0
  86. package/scripts/test_merge_gate_checks.py +92 -0
  87. package/scripts/tests/test_code_review_constants.py +78 -0
  88. package/scripts/tests/test_codex_compat_materializer.py +48 -4
  89. package/scripts/tests/test_contrast_framing.py +117 -0
  90. package/scripts/tests/test_followups.py +88 -0
  91. package/scripts/tests/test_match_exemptions.py +291 -0
  92. package/scripts/tests/test_models.py +67 -2
  93. package/scripts/tests/test_package_inventory.py +6 -4
  94. package/scripts/tests/test_pester_suites_run.py +53 -0
  95. package/scripts/tests/test_policy_document.py +39 -1
  96. package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
  97. package/scripts/tests/test_policy_lint_rules_engine.py +4 -1
  98. package/scripts/tests/test_reporting.py +18 -0
  99. package/scripts/tests/test_repository_policy.py +43 -1
  100. package/scripts/tests/test_review_closure.py +126 -0
  101. package/scripts/tests/test_review_closure_github.py +240 -0
  102. package/scripts/tests/test_review_closure_model.py +299 -0
  103. package/scripts/tests/test_tracked_secrets.py +42 -0
  104. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
  105. package/scripts/repository_checks/email_exemptions.py +0 -39
  106. package/scripts/tests/test_email_exemptions.py +0 -158
@@ -13,6 +13,7 @@ import json
13
13
  import os
14
14
  import sys
15
15
  from pathlib import Path
16
+ from re import Pattern
16
17
  from typing import TextIO
17
18
 
18
19
  try:
@@ -21,6 +22,10 @@ try:
21
22
  sys.path.insert(0, _hooks_dir)
22
23
 
23
24
  from blocking.code_rules_shared import is_ephemeral_path
25
+ from hooks_constants.code_rules_enforcer_constants import (
26
+ JAVASCRIPT_BLOCK_COMMENT_CLOSER,
27
+ JAVASCRIPT_BLOCK_COMMENT_OPENER,
28
+ )
24
29
  from hooks_constants.hook_block_logger import log_hook_block
25
30
  from hooks_constants.multi_edit_reconstruction import joined_new_strings
26
31
  from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
@@ -33,10 +38,11 @@ try:
33
38
  ALL_HASH_AND_SLASH_EXTENSIONS,
34
39
  ALL_HASH_ONLY_EXTENSIONS,
35
40
  ALL_MARKDOWN_EXTENSIONS,
36
- CODE_FENCE_PATTERN,
41
+ DOUBLE_QUOTE_BODY_GROUP,
37
42
  DOUBLE_QUOTED_SPAN_PATTERN,
38
43
  INLINE_CODE_PATTERN,
39
44
  PYTHON_EXTENSION,
45
+ SINGLE_QUOTE_BODY_GROUP,
40
46
  TRIPLE_QUOTED_BLOCK_PATTERN,
41
47
  )
42
48
  except ImportError as import_error:
@@ -77,15 +83,56 @@ def _get_inline_markers(extension: str) -> tuple[str, ...]:
77
83
  return ("//",)
78
84
 
79
85
 
80
- def _extract_comment_lines(text: str, extension: str = "") -> list[str]:
81
- """Extract comment lines from source code — Python (#), JS/TS/C/Rust/Go (//), and block comments."""
82
- all_comment_lines: list[str] = []
83
- all_lines = text.splitlines()
86
+ def _consume_block_comment_open(
87
+ each_line_number: int, stripped: str
88
+ ) -> tuple[list[tuple[int, str]], str | None, bool]:
89
+ """Extract the comment span a ``/*``-opening line carries.
90
+
91
+ Returns the extracted entries, the leftover code text to keep scanning
92
+ on this same line (``None`` when the whole line was consumed), and
93
+ whether the comment is still open past this line.
94
+ """
95
+ slash_star_index = stripped.find(JAVASCRIPT_BLOCK_COMMENT_OPENER)
96
+ close_star_index = stripped.find(
97
+ JAVASCRIPT_BLOCK_COMMENT_CLOSER, slash_star_index + len(JAVASCRIPT_BLOCK_COMMENT_OPENER)
98
+ )
99
+ if close_star_index < 0:
100
+ return [(each_line_number, stripped[slash_star_index:])], None, True
101
+ close_end = close_star_index + len(JAVASCRIPT_BLOCK_COMMENT_CLOSER)
102
+ entry = (each_line_number, stripped[slash_star_index:close_end])
103
+ after_close = stripped[close_end:].lstrip()
104
+ return [entry], (after_close or None), False
105
+
106
+
107
+ def _consume_block_comment_continuation(
108
+ each_line_number: int, stripped: str
109
+ ) -> tuple[list[tuple[int, str]], bool, bool]:
110
+ """Extract the comment span a line inside an already-open block carries.
111
+
112
+ Returns the extracted entries, whether the comment is still open past
113
+ this line, and whether the caller should move to the next source line.
114
+ """
115
+ close_index = stripped.find(JAVASCRIPT_BLOCK_COMMENT_CLOSER)
116
+ if close_index < 0:
117
+ return [(each_line_number, stripped)], True, True
118
+ close_end = close_index + len(JAVASCRIPT_BLOCK_COMMENT_CLOSER)
119
+ return [(each_line_number, stripped[:close_end])], False, False
120
+
121
+
122
+ def _extract_comment_lines(text: str, extension: str = "") -> list[tuple[int, str]]:
123
+ """Return each comment line as ``(source_line_number, comment_text)``.
124
+
125
+ Covers Python (``#``), JS/TS/C/Rust/Go (``//``), and block comments, and
126
+ tags every extracted line with the source line it came from so a
127
+ matching pattern can be pinned to that line rather than searched for
128
+ again.
129
+ """
130
+ all_comment_lines: list[tuple[int, str]] = []
84
131
 
85
132
  is_in_block_comment = False
86
133
  has_block_comments = extension in ALL_BLOCK_COMMENT_EXTENSIONS
87
134
  all_inline_markers = _get_inline_markers(extension)
88
- for each_line in all_lines:
135
+ for each_line_number, each_line in enumerate(text.splitlines(), 1):
89
136
  stripped = each_line.strip()
90
137
 
91
138
  if has_block_comments:
@@ -93,42 +140,33 @@ def _extract_comment_lines(text: str, extension: str = "") -> list[str]:
93
140
  stripped.startswith(each_marker)
94
141
  for each_marker in all_inline_markers
95
142
  ):
96
- all_comment_lines.append(stripped)
143
+ all_comment_lines.append((each_line_number, stripped))
97
144
  continue
98
- if "/*" in stripped and not is_in_block_comment:
99
- is_in_block_comment = True
100
- slash_star_index = stripped.find("/*")
101
- close_star_index = stripped.find("*/", slash_star_index + len("/*"))
102
- if close_star_index >= 0:
103
- all_comment_lines.append(
104
- stripped[slash_star_index : close_star_index + 2]
105
- )
106
- is_in_block_comment = False
107
- after_close = stripped[close_star_index + 2:].lstrip()
108
- if not after_close:
109
- continue
110
- stripped = after_close
111
- else:
112
- all_comment_lines.append(stripped[slash_star_index:])
145
+ if JAVASCRIPT_BLOCK_COMMENT_OPENER in stripped and not is_in_block_comment:
146
+ entries, leftover, is_in_block_comment = _consume_block_comment_open(
147
+ each_line_number, stripped
148
+ )
149
+ all_comment_lines.extend(entries)
150
+ if leftover is None:
113
151
  continue
152
+ stripped = leftover
114
153
  if is_in_block_comment:
115
- close_index = stripped.find("*/")
116
- if close_index >= 0:
117
- all_comment_lines.append(stripped[: close_index + 2])
118
- is_in_block_comment = False
119
- else:
120
- all_comment_lines.append(stripped)
154
+ entries, is_in_block_comment, should_continue = (
155
+ _consume_block_comment_continuation(each_line_number, stripped)
156
+ )
157
+ all_comment_lines.extend(entries)
158
+ if should_continue:
121
159
  continue
122
160
 
123
161
  if any(
124
162
  stripped.startswith(each_marker) for each_marker in all_inline_markers
125
163
  ):
126
- all_comment_lines.append(stripped)
164
+ all_comment_lines.append((each_line_number, stripped))
127
165
  continue
128
166
 
129
167
  inline_index = _find_inline_comment_start(stripped, all_inline_markers)
130
168
  if inline_index is not None and inline_index > 0:
131
- all_comment_lines.append(stripped[inline_index:])
169
+ all_comment_lines.append((each_line_number, stripped[inline_index:]))
132
170
  continue
133
171
 
134
172
  return all_comment_lines
@@ -154,16 +192,18 @@ def _find_inline_comment_start(stripped: str, all_markers: tuple[str, ...]) -> i
154
192
  return best_position
155
193
 
156
194
 
157
- def _extract_parsed_docstrings(tree: ast.Module) -> list[str]:
158
- """Collect the module, class, and function docstrings from a parsed tree.
195
+ def _extract_parsed_docstrings(tree: ast.Module) -> list[tuple[int, str]]:
196
+ """Return each docstring as ``(start_line, docstring_text)``.
159
197
 
160
198
  Args:
161
199
  tree: The parsed module tree to walk.
162
200
 
163
201
  Returns:
164
- Every docstring found on the module and its class/function definitions.
202
+ One entry per docstring found on the module and its class/function
203
+ definitions, tagged with the source line the docstring's opening
204
+ quotes start on.
165
205
  """
166
- all_found_docstrings: list[str] = []
206
+ all_found_docstrings: list[tuple[int, str]] = []
167
207
  for each_node in ast.walk(tree):
168
208
  if not isinstance(
169
209
  each_node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
@@ -171,102 +211,204 @@ def _extract_parsed_docstrings(tree: ast.Module) -> list[str]:
171
211
  continue
172
212
  maybe_docstring = ast.get_docstring(each_node, clean=False)
173
213
  if maybe_docstring:
174
- all_found_docstrings.append(maybe_docstring)
214
+ docstring_statement = each_node.body[0]
215
+ all_found_docstrings.append((docstring_statement.lineno, maybe_docstring))
175
216
  return all_found_docstrings
176
217
 
177
218
 
178
- def _extract_fragment_docstrings(text: str) -> list[str]:
179
- """Collect docstring-positioned triple-quoted blocks from an unparseable fragment.
219
+ def _is_docstring_position(leading_text: str) -> bool:
220
+ """Decide whether the text before a triple-quoted block marks a docstring slot.
180
221
 
181
222
  A triple-quoted block counts as a docstring when it opens the fragment
182
- (only whitespace before it) or when the nearest preceding non-blank line is
183
- a def/class header ending in a colon. A block assigned to a name or sitting
184
- elsewhere in the fragment is data and is skipped.
223
+ (only whitespace before it) or when the nearest preceding non-blank line
224
+ is a def/class header ending in a colon. A block assigned to a name or
225
+ sitting elsewhere in the fragment is data.
226
+ """
227
+ if not leading_text.strip():
228
+ return True
229
+ if leading_text.rpartition("\n")[2].strip():
230
+ return False
231
+ all_preceding_lines = [
232
+ each_line for each_line in leading_text.splitlines() if each_line.strip()
233
+ ]
234
+ if not all_preceding_lines:
235
+ return False
236
+ last_preceding_line = all_preceding_lines[-1].strip()
237
+ return last_preceding_line.startswith(
238
+ ALL_DEFINITION_HEADER_PREFIXES
239
+ ) and last_preceding_line.endswith(":")
240
+
241
+
242
+ def _extract_fragment_docstrings(text: str) -> list[tuple[int, str]]:
243
+ """Return docstring-positioned triple-quoted blocks from an unparseable fragment.
185
244
 
186
245
  Args:
187
246
  text: The Python source fragment that failed to parse.
188
247
 
189
248
  Returns:
190
- The body text of each docstring-positioned triple-quoted block.
249
+ One ``(start_line, body_text)`` pair per docstring-positioned
250
+ triple-quoted block, tagged with the source line its opening quotes
251
+ start on.
191
252
  """
192
- all_found_docstrings: list[str] = []
253
+ all_found_docstrings: list[tuple[int, str]] = []
193
254
  for each_match in TRIPLE_QUOTED_BLOCK_PATTERN.finditer(text):
194
- body = each_match.group(1) if each_match.group(1) is not None else each_match.group(2)
255
+ body = (
256
+ each_match.group(DOUBLE_QUOTE_BODY_GROUP)
257
+ if each_match.group(DOUBLE_QUOTE_BODY_GROUP) is not None
258
+ else each_match.group(SINGLE_QUOTE_BODY_GROUP)
259
+ )
260
+ start_line = text.count("\n", 0, each_match.start()) + 1
195
261
  leading_text = text[: each_match.start()]
196
- if not leading_text.strip():
197
- all_found_docstrings.append(body)
198
- continue
199
- if leading_text.rpartition("\n")[2].strip():
200
- continue
201
- all_preceding_lines = [
202
- each_line for each_line in leading_text.splitlines() if each_line.strip()
203
- ]
204
- last_preceding_line = all_preceding_lines[-1].strip()
205
- if last_preceding_line.startswith(
206
- ALL_DEFINITION_HEADER_PREFIXES
207
- ) and last_preceding_line.endswith(":"):
208
- all_found_docstrings.append(body)
262
+ if _is_docstring_position(leading_text):
263
+ all_found_docstrings.append((start_line, body))
209
264
  return all_found_docstrings
210
265
 
211
266
 
212
- def _extract_python_docstring_text(text: str) -> str:
213
- """Return the scannable docstring prose from Python source.
267
+ def _docstring_scan_lines(text: str) -> list[tuple[int, str]]:
268
+ """Return each docstring's prose as ``(source_line_number, line_text)`` pairs.
214
269
 
215
270
  Parses the source when valid, or falls back to the docstring-positioned
216
271
  triple-quoted blocks of a mid-edit fragment. Double-quoted and backticked
217
272
  spans inside each docstring are mentions rather than uses, so both are
218
- stripped from the returned prose.
273
+ stripped before the docstring is split back into its own source lines.
219
274
 
220
275
  Args:
221
276
  text: The Python source or fragment under scan.
222
277
 
223
278
  Returns:
224
- The docstring prose joined into one scannable string.
279
+ One entry per physical line of docstring prose, numbered against the
280
+ source file so a pattern match on that line carries the line the
281
+ rule matched rather than an unrelated occurrence elsewhere in the
282
+ file.
225
283
  """
226
284
  try:
227
285
  all_found_docstrings = _extract_parsed_docstrings(ast.parse(text))
228
286
  except (SyntaxError, ValueError):
229
287
  all_found_docstrings = _extract_fragment_docstrings(text)
230
- all_docstring_prose = [
231
- DOUBLE_QUOTED_SPAN_PATTERN.sub("", INLINE_CODE_PATTERN.sub("", each_docstring))
232
- for each_docstring in all_found_docstrings
233
- ]
234
- return "\n".join(all_docstring_prose)
288
+ all_scan_lines: list[tuple[int, str]] = []
289
+ for each_start_line, each_docstring in all_found_docstrings:
290
+ prose = DOUBLE_QUOTED_SPAN_PATTERN.sub(
291
+ "", INLINE_CODE_PATTERN.sub("", each_docstring)
292
+ )
293
+ for each_offset, each_prose_line in enumerate(prose.split("\n")):
294
+ all_scan_lines.append((each_start_line + each_offset, each_prose_line))
295
+ return all_scan_lines
235
296
 
236
297
 
237
- def find_violations(text: str, file_path: str) -> list[str]:
238
- """Return all violated patterns found in text for the given file.
298
+ def _markdown_scan_lines(text: str) -> list[tuple[int, str]]:
299
+ """Return each Markdown source line as ``(line_number, scannable_text)``.
239
300
 
240
- For .md files, scans the entire text. For code files, scans comment lines,
241
- and for Python files also scans module/class/function docstrings.
242
- Returns a list of matched pattern source strings.
301
+ A fenced code block's content is data rather than prose, so a line
302
+ between a pair of ``` fence markers is skipped. An inline code span is a
303
+ mention rather than a use, so it is blanked out of the line it appears
304
+ on, the same way it is blanked from Python docstring prose.
305
+
306
+ Args:
307
+ text: The Markdown source to scan.
308
+
309
+ Returns:
310
+ One entry per source line outside a fenced code block, with its
311
+ inline code spans removed.
312
+ """
313
+ all_scan_lines: list[tuple[int, str]] = []
314
+ is_inside_fence = False
315
+ for each_line_number, each_line in enumerate(text.splitlines(), 1):
316
+ if each_line.strip().startswith("```"):
317
+ is_inside_fence = not is_inside_fence
318
+ continue
319
+ if is_inside_fence:
320
+ continue
321
+ all_scan_lines.append(
322
+ (each_line_number, INLINE_CODE_PATTERN.sub("", each_line))
323
+ )
324
+ return all_scan_lines
325
+
326
+
327
+ def _first_pattern_match(
328
+ pattern: Pattern[str], all_scan_lines: list[tuple[int, str]]
329
+ ) -> tuple[str, int] | None:
330
+ """Return the first line a pattern matches, or None when it matches none."""
331
+ for each_line_number, each_line_text in all_scan_lines:
332
+ each_match = pattern.search(each_line_text)
333
+ if each_match is not None:
334
+ return (each_match.group(0).strip().lower(), each_line_number)
335
+ return None
336
+
337
+
338
+ def _scan_lines_for_file(
339
+ text: str, extension: str, file_path: str
340
+ ) -> list[tuple[int, str]] | None:
341
+ """Return the file's scannable lines, or ``None`` when its kind carries no prose.
342
+
343
+ A ``.md`` file scans every source line outside a fenced code block. A
344
+ code file scans its comment lines, and a Python file also scans its
345
+ module, class, and function docstrings.
243
346
  """
244
- extension = _get_file_extension(file_path)
245
347
  if is_markdown_file(file_path):
246
- scan_text = text
247
- elif is_comment_bearing_file(file_path):
248
- all_comment_lines = _extract_comment_lines(text, extension)
249
- scan_text = "\n".join(all_comment_lines)
250
- if extension == PYTHON_EXTENSION:
251
- scan_text = "\n".join([scan_text, _extract_python_docstring_text(text)])
252
- else:
253
- return []
348
+ return _markdown_scan_lines(text)
349
+ if not is_comment_bearing_file(file_path):
350
+ return None
351
+ all_scan_lines = _extract_comment_lines(text, extension)
352
+ if extension == PYTHON_EXTENSION:
353
+ return all_scan_lines + _docstring_scan_lines(text)
354
+ return all_scan_lines
355
+
356
+
357
+ def _matched_transition_patterns(
358
+ all_scan_lines: list[tuple[int, str]],
359
+ ) -> list[tuple[str, int]]:
360
+ """Return each transition pattern the scan lines match, with its line."""
361
+ all_detected: list[tuple[str, int]] = []
362
+ for each_pattern in ALL_COMMENT_TRANSITION_PATTERNS:
363
+ matched_entry = _first_pattern_match(each_pattern, all_scan_lines)
364
+ if matched_entry is not None:
365
+ all_detected.append(matched_entry)
366
+ return all_detected
254
367
 
255
- if is_markdown_file(file_path):
256
- scan_text = CODE_FENCE_PATTERN.sub("", scan_text)
257
- scan_text = INLINE_CODE_PATTERN.sub("", scan_text)
258
368
 
259
- if not scan_text.strip():
369
+ def find_violations_with_lines(text: str, file_path: str) -> list[tuple[str, int]]:
370
+ """Return each violated pattern with the source line the rule matched.
371
+
372
+ ::
373
+
374
+ # config.py
375
+ 1 fixture_text = "the field was `previously` required"
376
+ 4 def read_field():
377
+ 5 \"\"\"Read the field.
378
+ 7 `The field was previously required.`
379
+ 8 \"\"\"
380
+ flag: ("previously", 7) and ("was previously", 7) -- one per pattern
381
+ ok: line 1 never reported -- a plain string literal is fixture data, not prose
382
+
383
+ Each match is paired with the line it matched on, not an earlier mention
384
+ of the same words.
385
+
386
+ Args:
387
+ text: The file content to scan.
388
+ file_path: The path that selects the scan strategy for the file.
389
+
390
+ Returns:
391
+ One ``(matched_phrase, source_line_number)`` pair per matched
392
+ pattern, in pattern-declaration order, at most one match each.
393
+ """
394
+ extension = _get_file_extension(file_path)
395
+ all_scan_lines = _scan_lines_for_file(text, extension, file_path)
396
+ if all_scan_lines is None:
260
397
  return []
398
+ return _matched_transition_patterns(all_scan_lines)
261
399
 
262
- all_detected: list[str] = []
263
- all_transition_patterns = ALL_COMMENT_TRANSITION_PATTERNS
264
- for each_pattern in all_transition_patterns:
265
- all_matches = each_pattern.findall(scan_text)
266
- if all_matches:
267
- all_detected.append(all_matches[0].strip().lower())
268
400
 
269
- return all_detected
401
+ def find_violations(text: str, file_path: str) -> list[str]:
402
+ """Return all violated patterns found in text for the given file.
403
+
404
+ For .md files, scans the entire text. For code files, scans comment lines,
405
+ and for Python files also scans module/class/function docstrings.
406
+ Returns a list of matched pattern source strings.
407
+ """
408
+ return [
409
+ each_phrase
410
+ for each_phrase, _each_line_number in find_violations_with_lines(text, file_path)
411
+ ]
270
412
 
271
413
 
272
414
  def _build_deny_reason(file_path: str, all_detected_patterns: list[str]) -> str:
@@ -24,6 +24,8 @@ from collections.abc import Iterator
24
24
 
25
25
  import pytest
26
26
 
27
+ from code_rules_enforcer_test_support import STRIP_CODE_AND_QUOTES_SOURCE as SHARED_HELPER_SOURCE
28
+
27
29
  _HOOK_DIRECTORY = pathlib.Path(__file__).parent
28
30
  if str(_HOOK_DIRECTORY) not in sys.path:
29
31
  sys.path.insert(0, str(_HOOK_DIRECTORY))
@@ -39,17 +41,6 @@ _hook_spec.loader.exec_module(_hook_module)
39
41
  check_duplicate_function_body_across_files = _hook_module.check_duplicate_function_body_across_files
40
42
 
41
43
 
42
- SHARED_HELPER_SOURCE = (
43
- "import re\n"
44
- "\n"
45
- "def strip_code_and_quotes(text: str) -> str:\n"
46
- " without_fences = re.sub(r'```.*?```', '', text, flags=re.DOTALL)\n"
47
- " without_inline = re.sub(r'`[^`]*`', '', without_fences)\n"
48
- " without_quotes = re.sub(r'(?m)^>.*$', '', without_inline)\n"
49
- " return without_quotes.strip()\n"
50
- )
51
-
52
-
53
44
  @pytest.fixture
54
45
  def module_dir() -> Iterator[pathlib.Path]:
55
46
  base_directory = pathlib.Path(tempfile.mkdtemp())
@@ -1,25 +1,23 @@
1
- """Entry-point tests proving the duplicate-body check guards hook-infrastructure files.
1
+ """Entry-point tests proving the duplicate-body check's lane assignment for hook files.
2
2
 
3
3
  The cross-file duplicate-body check exists to catch a helper copied across sibling
4
4
  modules in the ``blocking/`` hook directory itself — the exact directory the rest of
5
- the code-rules suite exempts. These tests drive the real entry points (the ``main()``
6
- stdin path and the pre-check CLI) with a hook-infrastructure target so the deny fires
7
- on the same path a live Write would take, rather than calling the check function
8
- directly.
9
-
10
- Each test builds a temporary tree whose tail mirrors a production hook directory
11
- (``packages/claude-dev-env/hooks/blocking``) so ``is_hook_infrastructure`` matches the
12
- target path the same way it would for the real directory.
5
+ the code-rules suite exempts. CODE_RULES.md §11.6 assigns it by scope: it reads a
6
+ sibling file, so it runs on the full gate only, and a hook-infrastructure target's
7
+ live PreToolUse Write stays on the edit lane. These tests drive the shipped entry
8
+ points (the ``main()`` stdin path and the pre-check CLI) with a hook-infrastructure
9
+ target, so each lane's behavior is proven on the same path a live Write or a
10
+ pre-check run would take, rather than by calling the check function directly.
11
+
12
+ Each test takes the ``hook_blocking_dir`` fixture from ``conftest.py``, which builds
13
+ a temporary tree matching the production hook directory so ``is_hook_infrastructure``
14
+ routes a target inside it the same way it would for the production directory.
13
15
  """
14
16
 
15
17
  from __future__ import annotations
16
18
 
17
- import json
18
19
  import pathlib
19
- import shutil
20
20
  import sys
21
- import tempfile
22
- from collections.abc import Iterator
23
21
 
24
22
  import pytest
25
23
 
@@ -32,35 +30,13 @@ if str(_HOOKS_PARENT) not in sys.path:
32
30
 
33
31
  from code_rules_enforcer import main # noqa: E402
34
32
  from code_rules_enforcer_test_support import ( # noqa: E402
33
+ STRIP_CODE_AND_QUOTES_SOURCE as SHARED_HELPER_SOURCE,
35
34
  run_precheck,
36
35
  run_write_entrypoint,
37
36
  )
38
37
 
39
38
  pytestmark = pytest.mark.usefixtures("ephemeral_exempt_off")
40
39
 
41
- SHARED_HELPER_SOURCE = (
42
- "import re\n"
43
- "\n"
44
- "def strip_code_and_quotes(text: str) -> str:\n"
45
- " without_fences = re.sub(r'```.*?```', '', text, flags=re.DOTALL)\n"
46
- " without_inline = re.sub(r'`[^`]*`', '', without_fences)\n"
47
- " without_quotes = re.sub(r'(?m)^>.*$', '', without_inline)\n"
48
- " return without_quotes.strip()\n"
49
- )
50
-
51
- _HOOK_INFRASTRUCTURE_TAIL = pathlib.Path("packages") / "claude-dev-env" / "hooks" / "blocking"
52
-
53
-
54
- @pytest.fixture
55
- def hook_blocking_dir() -> Iterator[pathlib.Path]:
56
- base_directory = pathlib.Path(tempfile.mkdtemp())
57
- blocking_directory = base_directory / _HOOK_INFRASTRUCTURE_TAIL
58
- blocking_directory.mkdir(parents=True)
59
- try:
60
- yield blocking_directory
61
- finally:
62
- shutil.rmtree(base_directory, ignore_errors=False)
63
-
64
40
 
65
41
  def _run_main_with_write_payload(
66
42
  file_path: str,
@@ -75,27 +51,22 @@ def _run_main_with_write_payload(
75
51
  return captured_stdout
76
52
 
77
53
 
78
- def test_write_of_copied_helper_into_hook_directory_denies(
54
+ def test_write_of_copied_helper_into_hook_directory_does_not_deny(
79
55
  hook_blocking_dir: pathlib.Path,
80
56
  ) -> None:
81
- """A Write that copies a sibling helper into a second hook file is denied.
57
+ """A live Write that copies a sibling helper into a second hook file is not denied.
82
58
 
83
- The target lives under a hook-infrastructure path the rest of the code-rules
84
- suite exempts, so this proves the duplicate-body check still guards the exact
85
- directory its module docstring names as the primary target."""
59
+ Enforces CODE_RULES.md §11.6: "A check that reads a file other than the
60
+ target runs on the full gate." The cross-file duplicate-body check reads a
61
+ sibling module, so it is full-gate only; the live PreToolUse Write that
62
+ ``main()`` handles runs the edit lane, so it must not deny here even though
63
+ the copied helper is present."""
86
64
  (hook_blocking_dir / "existing_blocker.py").write_text(SHARED_HELPER_SOURCE, encoding="utf-8")
87
65
  new_file = hook_blocking_dir / "new_blocker.py"
88
66
  stdout = _run_main_with_write_payload(str(new_file), SHARED_HELPER_SOURCE)
89
- assert stdout != "", (
90
- "A copied helper written into a second hook-infrastructure file must "
91
- "produce a deny payload, got empty stdout"
92
- )
93
- deny_payload = json.loads(stdout)
94
- decision = deny_payload["hookSpecificOutput"]["permissionDecision"]
95
- reason = deny_payload["hookSpecificOutput"]["permissionDecisionReason"]
96
- assert decision == "deny", f"expected deny, got: {decision!r}"
97
- assert "strip_code_and_quotes" in reason, (
98
- f"the deny reason must name the duplicated helper, got: {reason!r}"
67
+ assert stdout == "", (
68
+ "the edit lane must not run the cross-file duplicate-body check, "
69
+ f"got stdout: {stdout!r}"
99
70
  )
100
71
 
101
72
 
@@ -124,9 +95,11 @@ def test_precheck_of_copied_helper_at_hook_target_exits_nonzero(
124
95
  ) -> None:
125
96
  """The pre-check CLI flags a copied helper judged at a hook-infrastructure target.
126
97
 
127
- Driving the real ``--check`` argv path proves the gate's pre-check mode also
128
- routes a hook ``.py`` target through the duplicate-body check rather than
129
- exiting clean on the blanket hook-infrastructure exemption."""
98
+ Enforces CODE_RULES.md §11.6: hook-infrastructure targets "run ... the
99
+ whole roster on the full gate." The pre-check CLI runs the full gate, so
100
+ driving the shipped ``--check`` argv path proves it still routes a hook
101
+ ``.py`` target through the duplicate-body check rather than exiting clean
102
+ on the blanket hook-infrastructure exemption."""
130
103
  (hook_blocking_dir / "existing_blocker.py").write_text(SHARED_HELPER_SOURCE, encoding="utf-8")
131
104
  staging_directory = tmp_path
132
105
  candidate_file = staging_directory / "candidate.py"
@@ -0,0 +1,21 @@
1
+ """Tests for the shared constants this directory's test-support module provides."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import cast
7
+
8
+ from code_rules_enforcer_test_support import STRIP_CODE_AND_QUOTES_SOURCE
9
+
10
+
11
+ def should_define_a_strip_code_and_quotes_function_that_removes_fenced_code() -> None:
12
+ module_namespace: dict[str, object] = {}
13
+ exec(STRIP_CODE_AND_QUOTES_SOURCE, module_namespace)
14
+ strip_code_and_quotes = cast("Callable[[str], str]", module_namespace["strip_code_and_quotes"])
15
+
16
+ stripped = strip_code_and_quotes("keep this\n```\ncode block\n```\n> a quote\nkeep this too")
17
+
18
+ assert "code block" not in stripped
19
+ assert "a quote" not in stripped
20
+ assert "keep this" in stripped
21
+ assert "keep this too" in stripped
@@ -7,19 +7,16 @@ through ``validate_content_for_edit_lane``, which hook files never reach at PreT
7
7
  tests drive the real ``main()`` stdin entry point and the pre-check CLI with a
8
8
  hook-infrastructure target.
9
9
 
10
- Each test builds a temporary tree whose tail mirrors a production hook directory
11
- (``packages/claude-dev-env/hooks/blocking``) so ``is_hook_infrastructure`` matches
12
- the target path the same way it would for the real directory.
10
+ Each test takes the ``hook_blocking_dir`` fixture from ``conftest.py``, which builds
11
+ a temporary tree matching the production hook directory so ``is_hook_infrastructure``
12
+ routes a target inside it the same way it would for the production directory.
13
13
  """
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
17
  import json
18
18
  import pathlib
19
- import shutil
20
19
  import sys
21
- import tempfile
22
- from collections.abc import Iterator
23
20
 
24
21
  import pytest
25
22
 
@@ -46,19 +43,6 @@ PASS_THROUGH_ALIAS_SOURCE = (
46
43
  " return find_bare_path_segments(content)\n"
47
44
  )
48
45
 
49
- _HOOK_INFRASTRUCTURE_TAIL = pathlib.Path("packages") / "claude-dev-env" / "hooks" / "blocking"
50
-
51
-
52
- @pytest.fixture
53
- def hook_blocking_dir() -> Iterator[pathlib.Path]:
54
- base_directory = pathlib.Path(tempfile.mkdtemp())
55
- blocking_directory = base_directory / _HOOK_INFRASTRUCTURE_TAIL
56
- blocking_directory.mkdir(parents=True)
57
- try:
58
- yield blocking_directory
59
- finally:
60
- shutil.rmtree(base_directory, ignore_errors=False)
61
-
62
46
 
63
47
  def test_write_of_pass_through_alias_into_hook_directory_denies(
64
48
  hook_blocking_dir: pathlib.Path,