claude-dev-env 2.33.1 → 2.33.2

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.
@@ -251,6 +251,16 @@ def _report_empty_file_set() -> int:
251
251
  return EMPTY_FILE_SET_EXIT_CODE
252
252
 
253
253
 
254
+ def _report_empty_diff() -> int:
255
+ """Print inspected 0 and return 0 when git has no changed files.
256
+
257
+ HEAD equals the chosen base and nothing is untracked. That is a clean
258
+ tree, so the gate stays quiet.
259
+ """
260
+ sys.stderr.write(INSPECTED_COUNT_MESSAGE.format(inspected_count=0) + "\n")
261
+ return 0
262
+
263
+
254
264
  def _run_explicit_paths_mode(
255
265
  validate_content: enforcer_loading.ValidateContentCallable,
256
266
  arguments: argparse.Namespace,
@@ -327,7 +337,7 @@ def _run_diff_mode(
327
337
  + paths_from_git_untracked(repository_root)
328
338
  )
329
339
  if not all_candidate_paths:
330
- return _report_empty_file_set()
340
+ return _report_empty_diff()
331
341
  file_paths = filter_paths_under_prefixes(
332
342
  all_candidate_paths, repository_root, arguments.only_under
333
343
  )
@@ -96,26 +96,23 @@ def temporary_git_repository(tmp_path: Path) -> Path:
96
96
  return repository_root
97
97
 
98
98
 
99
- def test_diff_mode_with_empty_file_set_exits_non_zero_and_says_so(
99
+ def test_diff_mode_with_empty_file_set_exits_zero_and_says_so(
100
100
  temporary_git_repository: Path,
101
101
  monkeypatch: pytest.MonkeyPatch,
102
102
  capsys: pytest.CaptureFixture[str],
103
103
  ) -> None:
104
- """HEAD == base and nothing untracked: the gate refuses to vouch for nothing.
104
+ """HEAD equals base and nothing is untracked, so there is no diff.
105
105
 
106
- A diff that resolves zero candidate files means the gate inspected
107
- nothing — a bad merge base or a wrong directory looks exactly like this.
108
- Issue #62's contract: that run exits non-zero and says so, because a
109
- silent pass over zero files is trusted like a real pass.
106
+ Report inspected 0 and exit 0. A bad merge base or a wrong directory
107
+ can also look empty, but a clean tree is the case this test covers.
110
108
  """
111
109
  monkeypatch.chdir(temporary_git_repository)
112
110
 
113
111
  exit_code = gate_module.main(["--base", "HEAD"])
114
112
 
115
113
  captured = capsys.readouterr()
116
- assert exit_code != 0
114
+ assert exit_code == 0
117
115
  assert "inspected 0 file(s)" in captured.err
118
- assert "empty" in captured.err.lower()
119
116
 
120
117
 
121
118
  def test_diff_mode_changes_outside_only_under_prefixes_exit_clean_and_report(
@@ -2949,6 +2949,23 @@ def test_diff_mode_untracked_png_does_not_flood_stderr(
2949
2949
  assert "untracked_icon.png" not in captured.err
2950
2950
 
2951
2951
 
2952
+ def test_diff_mode_empty_diff_exits_zero(
2953
+ temporary_git_repository: Path,
2954
+ monkeypatch: pytest.MonkeyPatch,
2955
+ capsys: pytest.CaptureFixture[str],
2956
+ ) -> None:
2957
+ write_file(temporary_git_repository / "seed.txt", "seed\n")
2958
+ commit_all_files(temporary_git_repository, "seed")
2959
+ monkeypatch.chdir(temporary_git_repository)
2960
+
2961
+ exit_code = gate_module.main(["--base", "HEAD"])
2962
+
2963
+ captured = capsys.readouterr()
2964
+ assert exit_code == 0
2965
+ assert "inspected 0 file(s)" in captured.err
2966
+ assert gate_module.EMPTY_FILE_SET_MESSAGE not in captured.err
2967
+
2968
+
2952
2969
  def test_diff_mode_image_only_surface_exits_zero_not_empty_file_set(
2953
2970
  temporary_git_repository: Path,
2954
2971
  monkeypatch: pytest.MonkeyPatch,
@@ -8,7 +8,6 @@ import tokenize
8
8
  from collections import Counter
9
9
  from collections.abc import Iterator
10
10
  from pathlib import Path
11
- from typing import cast
12
11
 
13
12
  _blocking_directory = str(Path(__file__).resolve().parent)
14
13
  _hooks_directory = str(Path(__file__).resolve().parent.parent)
@@ -100,49 +99,41 @@ def check_comments_javascript(content: str) -> list[str]:
100
99
  return issues
101
100
 
102
101
 
103
- def extract_comment_texts(
102
+ def _comment_occurrences(
104
103
  content: str, file_path: str, include_directive_comments: bool = False
105
- ) -> tuple[set[str], set[str]]:
106
- """Extract normalized comment text strings from content for comparison.
104
+ ) -> tuple[list[tuple[str, int, bool]], bool]:
105
+ """List comments as text, line number, and inline vs standalone.
107
106
 
108
- Returns:
109
- Tuple of (inline_comments, standalone_comments).
110
- Inline comments appear after code on the same line.
111
- Standalone comments are lines where the entire line is a comment.
107
+ Python tokenize failure returns an empty list and False. JavaScript
108
+ returns the shared scanner's list and True. Other extensions return
109
+ an empty list and True.
112
110
  """
113
111
  extension = get_file_extension(file_path)
114
- inline_comments: set[str] = set()
115
- standalone_comments: set[str] = set()
116
- if not content:
117
- return inline_comments, standalone_comments
118
-
119
112
  if extension in ALL_PYTHON_EXTENSIONS:
120
- inline_comments, standalone_comments, _ = _extract_python_comment_sets(
113
+ return _python_comment_occurrences(content, include_directive_comments)
114
+ if extension in ALL_JAVASCRIPT_EXTENSIONS:
115
+ all_occurrences = extract_javascript_comment_occurrences(
121
116
  content, include_directive_comments
122
117
  )
123
- return inline_comments, standalone_comments
118
+ return [tuple(each_occurrence) for each_occurrence in all_occurrences], True
119
+ return [], True
124
120
 
125
- if extension in ALL_JAVASCRIPT_EXTENSIONS:
126
- all_occurrences: list[tuple[str, int, bool]] = cast(
127
- list[tuple[str, int, bool]],
128
- extract_javascript_comment_occurrences(content, include_directive_comments),
129
- )
130
- inline_comments = {
131
- each_text
132
- for each_text, _each_line_number, is_inline in all_occurrences
133
- if is_inline
134
- }
135
- standalone_comments = {
136
- each_text
137
- for each_text, _each_line_number, is_inline in all_occurrences
138
- if not is_inline
139
- }
140
121
 
141
- return inline_comments, standalone_comments
122
+ def extract_comment_texts(
123
+ content: str, file_path: str, include_directive_comments: bool = False
124
+ ) -> tuple[set[str], set[str]]:
125
+ """Extract normalized comment text strings from content for comparison."""
126
+ all_occurrences, _ = _comment_occurrences(
127
+ content, file_path, include_directive_comments
128
+ )
129
+ return (
130
+ {each_text for each_text, _each_line, each_is_inline in all_occurrences if each_is_inline},
131
+ {each_text for each_text, _each_line, each_is_inline in all_occurrences if not each_is_inline},
132
+ )
142
133
 
143
134
 
144
135
  def _python_comment_occurrences(
145
- content: str,
136
+ content: str, include_directive_comments: bool = True
146
137
  ) -> tuple[list[tuple[str, int, bool]], bool]:
147
138
  """Return Python comments with exact lines and a tokenize status."""
148
139
  source_lines = content.split("\n")
@@ -156,6 +147,8 @@ def _python_comment_occurrences(
156
147
  continue
157
148
  if each_token.string.startswith("#!") and each_token.start == (1, 0):
158
149
  continue
150
+ if not include_directive_comments and _is_exempt_python_comment(each_token):
151
+ continue
159
152
  source_line = source_lines[each_token.start[0] - 1]
160
153
  is_inline = bool(source_line[: each_token.start[1]].strip())
161
154
  all_occurrences.append(
@@ -182,8 +175,8 @@ def check_comment_changes(old_content: str, new_content: str, file_path: str) ->
182
175
 
183
176
  extension = get_file_extension(file_path)
184
177
  if extension in ALL_PYTHON_EXTENSIONS:
185
- old_occurrences, old_tokenize_ok = _python_comment_occurrences(old_content)
186
- new_occurrences, new_tokenize_ok = _python_comment_occurrences(new_content)
178
+ old_occurrences, old_tokenize_ok = _python_comment_occurrences(old_content, True)
179
+ new_occurrences, new_tokenize_ok = _python_comment_occurrences(new_content, True)
187
180
  if not (old_tokenize_ok and new_tokenize_ok):
188
181
  return issues
189
182
  else:
@@ -223,25 +216,70 @@ def _changed_line_numbers(old_content: str, new_content: str) -> set[int]:
223
216
  }
224
217
 
225
218
 
219
+ def _deleted_line_numbers(old_content: str, new_content: str) -> set[int]:
220
+ """Old-file line numbers that a delete or replace covers."""
221
+ matcher = difflib.SequenceMatcher(
222
+ None, old_content.splitlines(), new_content.splitlines(), autojunk=False
223
+ )
224
+ return {
225
+ each_line_number
226
+ for each_tag, each_old_start, each_old_end, _, _ in matcher.get_opcodes()
227
+ if each_tag in {"delete", "replace"}
228
+ for each_line_number in range(each_old_start + 1, each_old_end + 1)
229
+ }
230
+
231
+
232
+ def _comment_lines_by_key(
233
+ all_occurrences: list[tuple[str, int, bool]],
234
+ ) -> dict[tuple[str, bool], list[int]]:
235
+ """Group line numbers by comment text and inline vs standalone."""
236
+ lines_by_key: dict[tuple[str, bool], list[int]] = {}
237
+ for each_text, each_line_number, each_is_inline in all_occurrences:
238
+ lines_by_key.setdefault((each_text, each_is_inline), []).append(each_line_number)
239
+ return lines_by_key
240
+
241
+
242
+ def _is_attached_to_changed_code(
243
+ each_line_number: int,
244
+ each_is_inline: bool,
245
+ old_line_number: int,
246
+ all_changed_lines: set[int],
247
+ all_deleted_lines: set[int],
248
+ ) -> bool:
249
+ """Check if this leftover comment still sits on a changed line.
250
+
251
+ Inline comments count when their own line changed. Standalone comments
252
+ count when the next line changed, or when the next line in the old file
253
+ was deleted.
254
+ """
255
+ if each_is_inline:
256
+ return each_line_number in all_changed_lines
257
+ return each_line_number + 1 in all_changed_lines or old_line_number + 1 in all_deleted_lines
258
+
259
+
226
260
  def _retained_comment_issues(
227
261
  old_content: str, new_content: str, file_path: str
228
262
  ) -> list[str]:
229
- old_inline, old_standalone = extract_comment_texts(old_content, file_path, True)
230
- new_inline, new_standalone = extract_comment_texts(new_content, file_path, True)
231
- shared_inline = set(Counter(old_inline) & Counter(new_inline))
232
- shared_standalone = old_standalone & new_standalone
233
- if not shared_inline and not shared_standalone:
263
+ old_occurrences, old_tokenize_ok = _comment_occurrences(old_content, file_path, True)
264
+ new_occurrences, new_tokenize_ok = _comment_occurrences(new_content, file_path, True)
265
+ if not (old_tokenize_ok and new_tokenize_ok):
234
266
  return []
235
- changed_lines = _changed_line_numbers(old_content, new_content)
236
- new_lines = new_content.splitlines()
267
+ all_changed_lines = _changed_line_numbers(old_content, new_content)
268
+ all_deleted_lines = _deleted_line_numbers(old_content, new_content)
269
+ old_line_by_key = _comment_lines_by_key(old_occurrences)
237
270
  issues: list[str] = []
238
- for each_line_number in sorted(changed_lines):
239
- inline_comment = next((each_comment for each_comment in shared_inline if each_comment in new_lines[each_line_number - 1]), None)
240
- if inline_comment is not None:
241
- issues.append(f"Line {each_line_number}: Inline comment retained on changed code: {inline_comment} - remove the comment")
242
- preceding_line_number = each_line_number - 1
243
- if preceding_line_number > 0 and new_lines[preceding_line_number - 1].strip() in shared_standalone:
244
- issues.append(f"Line {each_line_number}: Standalone comment retained on changed code at line {preceding_line_number}: {new_lines[preceding_line_number - 1].strip()} - remove the comment")
271
+ for each_text, each_line_number, each_is_inline in new_occurrences:
272
+ matching_old_lines = old_line_by_key.get((each_text, each_is_inline), [])
273
+ if not matching_old_lines:
274
+ continue
275
+ old_line_number = matching_old_lines.pop(0)
276
+ if not _is_attached_to_changed_code(each_line_number, each_is_inline, old_line_number, all_changed_lines, all_deleted_lines):
277
+ continue
278
+ comment_kind = "Inline" if each_is_inline else "Standalone"
279
+ issues.append(
280
+ f"Line {each_line_number}: {comment_kind} comment still on the changed lines: "
281
+ f"{each_text} - remove the comment"
282
+ )
245
283
  if len(issues) >= MAX_COMMENT_ISSUES:
246
284
  break
247
285
  return issues[:MAX_COMMENT_ISSUES]
@@ -263,12 +301,11 @@ def _python_tokens(source: str) -> Iterator[tokenize.TokenInfo]:
263
301
  def _comment_tokens(source: str) -> Iterator[tokenize.TokenInfo]:
264
302
  """Yield COMMENT tokens from *source* one at a time.
265
303
 
266
- Streams from ``_python_tokens`` so consumers that early-exit (e.g.
267
- ``check_comments_python`` caps at ``MAX_COMMENT_ISSUES``) avoid
268
- materializing the entire token list. Silently stops on tokenize
269
- failure so callers receive only valid comment tokens — no
270
- indeterminate signal is exposed at this layer because the consumers
271
- that need it (``_extract_python_comment_sets``) bypass this helper.
304
+ Streams from ``_python_tokens`` so consumers that early-exit, such as
305
+ ``check_comments_python`` capping at ``MAX_COMMENT_ISSUES``, avoid
306
+ materializing the entire token list. Tokenize failure stops the
307
+ stream with no extra flag. Callers that need to know tokenize failed
308
+ use ``_python_comment_occurrences``.
272
309
  """
273
310
  try:
274
311
  for each_token in _python_tokens(source):
@@ -356,41 +393,3 @@ def _starts_with_bounded_token_anchored_directive(directive_body: str) -> bool:
356
393
  if next_character in ALL_TOKEN_ANCHORED_DIRECTIVE_BOUNDARY_CHARACTERS:
357
394
  return True
358
395
  return False
359
-
360
-
361
- def _extract_python_comment_sets(
362
- content: str, include_directive_comments: bool = False
363
- ) -> tuple[set[str], set[str], bool]:
364
- """Return (inline_comments, standalone_comments, tokenize_succeeded).
365
-
366
- Streams *content* once via ``_python_tokens``. A tokenize failure
367
- (mid-edit fragment, syntax error) returns empty sets and ``False``
368
- so callers can treat the situation as indeterminate rather than as
369
- "no comments present". Inline vs standalone is decided by inspecting
370
- the column offset of each ``COMMENT`` token against its source
371
- line: an all-whitespace prefix means standalone.
372
- """
373
- inline_comments: set[str] = set()
374
- standalone_comments: set[str] = set()
375
- lines = content.split("\n")
376
- try:
377
- for each_token in _python_tokens(content):
378
- if each_token.type != tokenize.COMMENT:
379
- continue
380
- is_shebang = each_token.string.startswith("#!") and each_token.start == (1, 0)
381
- if is_shebang or (
382
- not include_directive_comments and _is_exempt_python_comment(each_token)
383
- ):
384
- continue
385
- line_number = each_token.start[0]
386
- column_offset = each_token.start[1]
387
- source_line = lines[line_number - 1] if line_number - 1 < len(lines) else ""
388
- text_before_comment = source_line[:column_offset]
389
- normalized_comment_text = each_token.string.strip()
390
- if not text_before_comment.strip():
391
- standalone_comments.add(normalized_comment_text)
392
- else:
393
- inline_comments.add(normalized_comment_text)
394
- except ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS:
395
- return set(), set(), False
396
- return inline_comments, standalone_comments, True
@@ -236,7 +236,7 @@ class _ValidationContext(NamedTuple):
236
236
  defer_scope_to_caller: bool
237
237
  sibling_directory: Path | None
238
238
  phase: str
239
- include_comment_policy: bool = True
239
+ include_comment_policy: bool = False
240
240
 
241
241
 
242
242
  def _validated_phase(phase: str) -> str:
@@ -653,7 +653,7 @@ def validate_content_for_phase(
653
653
  sibling_directory: Path | None = None,
654
654
  *,
655
655
  phase: str,
656
- include_comment_policy: bool = True,
656
+ include_comment_policy: bool = False,
657
657
  ) -> list[str]:
658
658
  """Run all applicable validators on content for one named validation phase."""
659
659
  validated_phase = _validated_phase(phase)
@@ -703,7 +703,7 @@ def validate_content_for_full_gate(
703
703
  defer_scope_to_caller: bool = False,
704
704
  sibling_directory: Path | None = None,
705
705
  *,
706
- include_comment_policy: bool = True,
706
+ include_comment_policy: bool = False,
707
707
  ) -> list[str]:
708
708
  """Run the full-gate phase, including the six checks that read sibling files."""
709
709
  return validate_content_for_phase(
@@ -1,14 +1,11 @@
1
1
  """Extract JavaScript comments without matching comment-like strings."""
2
2
 
3
- import io
4
3
  import re
5
- import tokenize
6
4
  from typing import NamedTuple
7
5
 
8
6
  from hooks_constants.code_rules_enforcer_constants import (
9
7
  ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES,
10
8
  ALL_JAVASCRIPT_EXEMPT_INLINE_COMMENT_PREFIXES,
11
- ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS,
12
9
  )
13
10
 
14
11
 
@@ -72,28 +69,3 @@ def extract_javascript_comment_occurrences(
72
69
  JavaScriptCommentOccurrence(comment_text, line_number, is_inline)
73
70
  )
74
71
  return all_occurrences
75
-
76
-
77
- def extract_python_comment_occurrences(
78
- content: str,
79
- ) -> tuple[list[JavaScriptCommentOccurrence], bool]:
80
- """Return Python comments with exact lines and a tokenize status."""
81
- source_lines = content.split("\n")
82
- try:
83
- all_tokens = list(tokenize.generate_tokens(io.StringIO(content).readline))
84
- except ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS:
85
- return [], False
86
- all_occurrences: list[JavaScriptCommentOccurrence] = []
87
- for each_token in all_tokens:
88
- if each_token.type != tokenize.COMMENT:
89
- continue
90
- if each_token.string.startswith("#!") and each_token.start == (1, 0):
91
- continue
92
- source_line = source_lines[each_token.start[0] - 1]
93
- is_inline = bool(source_line[: each_token.start[1]].strip())
94
- all_occurrences.append(
95
- JavaScriptCommentOccurrence(
96
- each_token.string.strip(), each_token.start[0], is_inline
97
- )
98
- )
99
- return all_occurrences, True
@@ -208,7 +208,12 @@ def test_full_gate_blocks_python_inline_comment_in_test_file() -> None:
208
208
  sibling_directory=None,
209
209
  phase="edit",
210
210
  )
211
- issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
211
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(
212
+ context.content,
213
+ context.file_path,
214
+ context.old_content,
215
+ include_comment_policy=True,
216
+ )
212
217
  assert any("Inline comment added" in each_issue for each_issue in issues)
213
218
 
214
219
 
@@ -223,7 +228,7 @@ def test_full_gate_blocks_javascript_inline_comment_in_test_file() -> None:
223
228
  sibling_directory=None,
224
229
  phase="edit",
225
230
  )
226
- issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
231
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
227
232
  assert any("Inline comment added" in each_issue for each_issue in issues)
228
233
 
229
234
 
@@ -238,7 +243,7 @@ def test_full_gate_blocks_javascript_inline_block_comment_in_test_file() -> None
238
243
  sibling_directory=None,
239
244
  phase="edit",
240
245
  )
241
- issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
246
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
242
247
  assert any("Inline comment added" in each_issue for each_issue in issues)
243
248
 
244
249
 
@@ -253,7 +258,7 @@ def test_full_gate_blocks_javascript_directive_comment_in_test_file() -> None:
253
258
  sibling_directory=None,
254
259
  phase="edit",
255
260
  )
256
- issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
261
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
257
262
  assert any("Inline comment added" in each_issue for each_issue in issues)
258
263
 
259
264
 
@@ -268,7 +273,7 @@ def test_full_gate_blocks_python_directive_comment_in_test_file() -> None:
268
273
  sibling_directory=None,
269
274
  phase="edit",
270
275
  )
271
- issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
276
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
272
277
  assert any("comment added" in each_issue.lower() for each_issue in issues)
273
278
 
274
279
 
@@ -286,7 +291,7 @@ def test_full_gate_blocks_changed_marker_comments_in_test_file(each_marker: str)
286
291
  sibling_directory=None,
287
292
  phase="edit",
288
293
  )
289
- issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
294
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
290
295
  assert any("comment added" in each_issue.lower() for each_issue in issues)
291
296
 
292
297
 
@@ -301,7 +306,7 @@ def test_full_gate_blocks_standalone_comment_in_test_file() -> None:
301
306
  sibling_directory=None,
302
307
  phase="edit",
303
308
  )
304
- issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
309
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
305
310
  assert any("Standalone comment added" in each_issue for each_issue in issues)
306
311
 
307
312
 
@@ -316,7 +321,7 @@ def test_full_gate_preserves_shebang_and_docstring_in_test_file() -> None:
316
321
  sibling_directory=None,
317
322
  phase="edit",
318
323
  )
319
- issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
324
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
320
325
  assert not any("comment added" in each_issue.lower() for each_issue in issues)
321
326
 
322
327
 
@@ -333,7 +338,7 @@ def test_full_gate_allows_existing_comment_removal_without_advisory(
333
338
  sibling_directory=None,
334
339
  phase="edit",
335
340
  )
336
- issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
341
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
337
342
  assert not any("Existing comment removed" in each_issue for each_issue in issues)
338
343
  assert capsys.readouterr().err == ""
339
344
 
@@ -373,11 +378,11 @@ def test_check_comment_changes_blocks_retained_comments_on_changed_code(
373
378
  file_path: str, old_content: str, new_content: str
374
379
  ) -> None:
375
380
  issues = code_rules_enforcer.check_comment_changes(old_content, new_content, file_path)
376
- assert any("retained on changed code" in each_issue for each_issue in issues)
381
+ assert any("still on the changed lines" in each_issue for each_issue in issues)
377
382
 
378
383
 
379
384
  def test_check_comment_changes_accepts_distant_untouched_comment() -> None:
380
385
  old_content = "# TODO #999: distant note\n\ntotal = 1\n"
381
386
  new_content = "# TODO #999: distant note\n\ntotal = 2\n"
382
387
  issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "totals.py")
383
- assert not any("retained on changed code" in each_issue for each_issue in issues)
388
+ assert not any("still on the changed lines" in each_issue for each_issue in issues)
@@ -13,9 +13,9 @@ if _BLOCKING_DIRECTORY not in sys.path:
13
13
  if _HOOKS_DIRECTORY not in sys.path:
14
14
  sys.path.insert(0, _HOOKS_DIRECTORY)
15
15
 
16
- check_comment_changes = importlib.import_module(
17
- "code_rules_comments"
18
- ).check_comment_changes
16
+ comment_rules_module = importlib.import_module("code_rules_comments")
17
+ check_comment_changes = comment_rules_module.check_comment_changes
18
+ code_rules_enforcer_module = importlib.import_module("code_rules_enforcer")
19
19
 
20
20
 
21
21
  def test_check_comment_changes_ignores_javascript_url_string() -> None:
@@ -42,3 +42,33 @@ def test_check_comment_changes_reports_duplicate_identical_comment_occurrence()
42
42
  )
43
43
 
44
44
  assert any("Line 2: Inline comment added" in each_issue for each_issue in issues)
45
+
46
+
47
+ def test_check_comment_changes_does_not_match_comment_text_inside_changed_string() -> None:
48
+ issues = check_comment_changes(
49
+ "# note\n\ntotal = 1\n",
50
+ '"# note"\ntotal = 2\n',
51
+ "totals.py",
52
+ )
53
+
54
+ assert not any("still on the changed lines" in each_issue for each_issue in issues)
55
+
56
+
57
+ def test_check_comment_changes_flags_comment_attached_to_deleted_code() -> None:
58
+ issues = check_comment_changes(
59
+ "# attached\nold_total = 1\nnew_total = 2\n",
60
+ "# attached\nnew_total = 2\n",
61
+ "totals.py",
62
+ )
63
+
64
+ assert any("Standalone comment still on the changed lines" in each_issue for each_issue in issues)
65
+
66
+
67
+ def test_full_gate_skips_comment_policy_by_default() -> None:
68
+ issues = code_rules_enforcer_module.validate_content_for_full_gate(
69
+ "total = 1 # added\n",
70
+ "totals.py",
71
+ "total = 1\n",
72
+ )
73
+
74
+ assert not any("comment" in each_issue.lower() for each_issue in issues)
@@ -723,14 +723,12 @@ def test_dispatcher_edit_applies_both_groups() -> None:
723
723
  """
724
724
  all_edit_entries = _applicable_entries_for_tool(EDIT_TOOL_NAME)
725
725
  all_edit_script_paths = {each_entry.script_relative_path for each_entry in all_edit_entries}
726
- assert "blocking/stale_comment_reference_blocker.py" in all_edit_script_paths, (
727
- "stale_comment_reference_blocker belongs in the Edit applicable set"
728
- )
726
+ assert "blocking/stale_comment_reference_blocker.py" not in all_edit_script_paths
729
727
  assert "advisory/refactor_guard.py" in all_edit_script_paths, (
730
728
  "refactor_guard is Edit-scoped and hosted, so it belongs in the Edit applicable set"
731
729
  )
732
- assert len(all_edit_entries) == 21, (
733
- f"expected 21 Edit entries, got {len(all_edit_entries)}"
730
+ assert len(all_edit_entries) == 20, (
731
+ f"expected 20 Edit entries, got {len(all_edit_entries)}"
734
732
  )
735
733
 
736
734
 
@@ -130,10 +130,6 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
130
130
  applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
131
131
  native_module_name=STATE_DESCRIPTION_BLOCKER_MODULE_NAME,
132
132
  ),
133
- HostedHookEntry(
134
- script_relative_path="blocking/stale_comment_reference_blocker.py",
135
- applicable_tool_names=frozenset({EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME}),
136
- ),
137
133
  HostedHookEntry(
138
134
  script_relative_path="blocking/subprocess_budget_completeness.py",
139
135
  applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
@@ -165,7 +165,6 @@ _ALL_DEFERRED_LINT_SCRIPT_PATHS = (
165
165
  "blocking/duplicate_rmtree_helper_blocker.py",
166
166
  "validation/hook_format_validator.py",
167
167
  "blocking/hook_prose_detector_consistency.py",
168
- "blocking/stale_comment_reference_blocker.py",
169
168
  "blocking/subprocess_budget_completeness.py",
170
169
  "blocking/windows_rmtree_blocker.py",
171
170
  "advisory/refactor_guard.py",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.33.1",
3
+ "version": "2.33.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/check.ps1 CHANGED
@@ -61,13 +61,22 @@ function Resolve-CommentPolicyBase {
61
61
  }
62
62
  throw "Comment-policy baseline '$ExplicitBase' does not resolve in the repository."
63
63
  }
64
- foreach ($candidateBase in @('origin/main', 'HEAD^1')) {
64
+
65
+ $headCommit = & git -C $repositoryRoot rev-parse --verify --quiet 'HEAD^{commit}' 2>$null
66
+ $ciBases = @($env:GITHUB_BASE_SHA, $env:GITHUB_EVENT_BEFORE)
67
+ foreach ($candidateBase in @($ciBases + 'origin/main' + 'HEAD^1')) {
68
+ if (-not $candidateBase) {
69
+ continue
70
+ }
65
71
  $resolvedCandidateBase = & git -C $repositoryRoot rev-parse --verify --quiet "${candidateBase}^{commit}" 2>$null
66
72
  if ($LASTEXITCODE -eq 0 -and $resolvedCandidateBase) {
73
+ if ($candidateBase -eq 'origin/main' -and $resolvedCandidateBase.Trim() -eq $headCommit.Trim()) {
74
+ continue
75
+ }
67
76
  return $candidateBase
68
77
  }
69
78
  }
70
- throw 'Comment-policy baseline does not resolve to origin/main or HEAD^1.'
79
+ throw 'Comment-policy baseline does not resolve to a CI base, origin/main, or HEAD^1.'
71
80
  }
72
81
 
73
82
  $commentPolicyBase = Resolve-CommentPolicyBase -ExplicitBase $CommentPolicyBase