claude-dev-env 1.58.0 → 1.60.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/CLAUDE.md +2 -2
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  5. package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
  6. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
  7. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
  8. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
  9. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
  10. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  11. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  12. package/bin/install.mjs +100 -27
  13. package/bin/install.test.mjs +133 -1
  14. package/docs/CODE_RULES.md +3 -3
  15. package/hooks/blocking/code_rules_annotations_length.py +153 -0
  16. package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
  17. package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
  18. package/hooks/blocking/code_rules_duplicate_body.py +439 -0
  19. package/hooks/blocking/code_rules_enforcer.py +190 -21
  20. package/hooks/blocking/code_rules_magic_values.py +98 -0
  21. package/hooks/blocking/code_rules_shared.py +41 -0
  22. package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
  23. package/hooks/blocking/config/__init__.py +5 -0
  24. package/hooks/blocking/config/verified_commit_constants.py +106 -0
  25. package/hooks/blocking/destructive_command_blocker.py +1027 -12
  26. package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
  27. package/hooks/blocking/subprocess_budget_completeness.py +380 -0
  28. package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
  29. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  30. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
  31. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
  32. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
  33. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
  34. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
  35. package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
  36. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
  37. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
  38. package/hooks/blocking/test_destructive_command_blocker.py +622 -3
  39. package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
  40. package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
  41. package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
  42. package/hooks/blocking/test_verification_verdict_store.py +278 -0
  43. package/hooks/blocking/test_verified_commit_gate.py +368 -0
  44. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
  45. package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
  46. package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
  47. package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
  48. package/hooks/blocking/verification_verdict_store.py +446 -0
  49. package/hooks/blocking/verified_commit_gate.py +523 -0
  50. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
  51. package/hooks/blocking/verifier_verdict_minter.py +299 -0
  52. package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
  53. package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
  54. package/hooks/hooks.json +58 -1
  55. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  56. package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
  57. package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
  58. package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
  59. package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
  60. package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
  61. package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
  62. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  63. package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
  64. package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
  65. package/package.json +1 -1
  66. package/rules/docstring-prose-matches-implementation.md +43 -0
  67. package/rules/file-global-constants.md +7 -1
  68. package/rules/hook-prose-matches-detector.md +26 -0
  69. package/rules/no-cross-skill-duplicate-helpers.md +29 -0
  70. package/rules/no-inline-destructive-literals.md +11 -0
  71. package/rules/workflow-substitution-slots.md +7 -0
  72. package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
  73. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
  74. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
  75. package/skills/autoconverge/SKILL.md +67 -19
  76. package/skills/autoconverge/reference/closing-report.md +59 -17
  77. package/skills/autoconverge/reference/convergence.md +7 -3
  78. package/skills/autoconverge/reference/stop-conditions.md +7 -2
  79. package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
  80. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
  81. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
  82. package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
  83. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
  84. package/skills/autoconverge/workflow/converge.mjs +234 -42
  85. package/skills/autoconverge/workflow/convergence_summary.py +110 -0
  86. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
  87. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
  88. package/skills/autoconverge/workflow/render_report.py +488 -397
  89. package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
  90. package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
  91. package/skills/autoconverge/workflow/test_render_report.py +488 -259
  92. package/skills/pr-converge/reference/per-tick.md +28 -8
  93. package/skills/pr-converge/scripts/check_convergence.py +195 -64
  94. package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
  95. package/skills/rebase/SKILL.md +2 -4
  96. package/skills/update/SKILL.md +37 -5
  97. package/system-prompts/software-engineer.xml +2 -6
  98. package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
  99. package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
  100. package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
  101. package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
  102. package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
  103. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
  104. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
  105. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
  106. package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
@@ -30,6 +30,7 @@ if _HOOKS_DIRECTORY not in sys.path:
30
30
 
31
31
  from code_rules_annotations_length import ( # noqa: E402
32
32
  check_function_length,
33
+ check_known_pytest_fixture_annotations,
33
34
  check_parameter_annotations,
34
35
  check_return_annotations,
35
36
  )
@@ -50,10 +51,20 @@ from code_rules_constants_config import ( # noqa: E402
50
51
  check_constants_outside_config_advisory,
51
52
  check_file_global_constants_use_count,
52
53
  )
54
+ from code_rules_dead_dataclass_field import ( # noqa: E402
55
+ check_dead_dataclass_fields,
56
+ )
57
+ from code_rules_dead_module_constant import ( # noqa: E402
58
+ check_dead_module_constants,
59
+ )
53
60
  from code_rules_docstrings import ( # noqa: E402
54
61
  check_docstring_args_match_signature,
55
62
  check_docstring_format,
56
63
  )
64
+ from code_rules_duplicate_body import ( # noqa: E402
65
+ advise_cross_skill_duplicate_helper,
66
+ check_duplicate_function_body_across_files,
67
+ )
57
68
  from code_rules_imports_logging import ( # noqa: E402
58
69
  advise_file_line_count,
59
70
  check_e2e_test_naming,
@@ -113,6 +124,7 @@ from code_rules_typeddict_stub import ( # noqa: E402
113
124
  check_stub_implementations,
114
125
  check_thin_wrapper_files,
115
126
  check_typed_dict_encode_decode,
127
+ check_zero_payload_function_alias,
116
128
  )
117
129
  from code_rules_unused_imports import ( # noqa: E402
118
130
  check_unused_module_level_imports,
@@ -122,6 +134,7 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
122
134
  ALL_CODE_EXTENSIONS,
123
135
  ALL_JAVASCRIPT_EXTENSIONS,
124
136
  ALL_PYTHON_EXTENSIONS,
137
+ DENY_REASON_ISSUE_PREVIEW_COUNT,
125
138
  PRECHECK_USAGE_EXIT_CODE,
126
139
  PRECHECK_USAGE_MESSAGE,
127
140
  )
@@ -137,6 +150,7 @@ def validate_content(
137
150
  full_file_content: str | None = None,
138
151
  prior_full_file_content: str = "",
139
152
  defer_scope_to_caller: bool = False,
153
+ sibling_directory: Path | None = None,
140
154
  ) -> list[str]:
141
155
  """Run all applicable validators on content.
142
156
 
@@ -167,6 +181,12 @@ def validate_content(
167
181
  checks return their violations unscoped for the gate to classify.
168
182
  PreToolUse new-file or full-file writes leave this False: this
169
183
  enforcer is terminal, so it marks every violation in scope.
184
+ sibling_directory: The absolute directory the cross-file duplicate-body
185
+ check scans for sibling modules. The commit/push gate passes the
186
+ resolved file's parent so the on-disk sibling scan stays anchored to
187
+ the repository regardless of the gate process's working directory.
188
+ None (the PreToolUse default) derives the directory from
189
+ ``file_path``'s parent, which is already absolute on that path.
170
190
  """
171
191
  extension = get_file_extension(file_path)
172
192
  all_issues = []
@@ -188,6 +208,15 @@ def validate_content(
188
208
  all_issues.extend(check_constants_outside_config(content, file_path))
189
209
  all_issues.extend(check_constants_outside_config_advisory(content, file_path))
190
210
  all_issues.extend(check_file_global_constants_use_count(content, file_path))
211
+ all_issues.extend(
212
+ check_duplicate_function_body_across_files(
213
+ effective_content,
214
+ file_path,
215
+ all_changed_lines,
216
+ defer_scope_to_caller,
217
+ sibling_directory,
218
+ )
219
+ )
191
220
  all_issues.extend(check_type_escape_hatches(effective_content, file_path))
192
221
  all_issues.extend(check_banned_identifiers(content, file_path))
193
222
  all_issues.extend(
@@ -204,6 +233,7 @@ def validate_content(
204
233
  all_issues.extend(check_test_branching_in_production(effective_content, file_path))
205
234
  all_issues.extend(check_bare_except(effective_content, file_path))
206
235
  all_issues.extend(check_thin_wrapper_files(effective_content, file_path))
236
+ all_issues.extend(check_zero_payload_function_alias(effective_content, file_path))
207
237
  all_issues.extend(check_boundary_types(effective_content, file_path))
208
238
  all_issues.extend(check_docstring_format(effective_content, file_path))
209
239
  all_issues.extend(check_docstring_args_match_signature(effective_content, file_path))
@@ -242,8 +272,15 @@ def validate_content(
242
272
  all_issues.extend(
243
273
  check_unused_module_level_imports(content, file_path, full_file_content)
244
274
  )
275
+ all_issues.extend(
276
+ check_dead_dataclass_fields(content, file_path, full_file_content)
277
+ )
278
+ all_issues.extend(
279
+ check_dead_module_constants(content, file_path, full_file_content)
280
+ )
245
281
  all_issues.extend(check_library_print(content, file_path))
246
282
  all_issues.extend(check_parameter_annotations(content, file_path))
283
+ all_issues.extend(check_known_pytest_fixture_annotations(content, file_path))
247
284
  all_issues.extend(check_return_annotations(content, file_path))
248
285
  all_issues.extend(
249
286
  check_function_length(
@@ -259,6 +296,7 @@ def validate_content(
259
296
  all_issues.extend(check_string_literal_magic(content, file_path))
260
297
  check_incomplete_mocks(content, file_path)
261
298
  check_duplicated_format_patterns(content, file_path)
299
+ advise_cross_skill_duplicate_helper(effective_content, file_path)
262
300
 
263
301
  elif extension in ALL_JAVASCRIPT_EXTENSIONS:
264
302
  if not is_test_file(file_path):
@@ -334,6 +372,69 @@ def _is_validated_target(file_path: str) -> bool:
334
372
  return get_file_extension(file_path) in ALL_CODE_EXTENSIONS
335
373
 
336
374
 
375
+ def _is_hook_infrastructure_python_target(file_path: str) -> bool:
376
+ """Return whether the path is a hook-infrastructure Python file.
377
+
378
+ The full code-rules suite exempts hook-infrastructure files, but the
379
+ cross-file duplicate-body check must still guard them: a helper copied
380
+ across sibling hook modules is the exact violation it targets. This
381
+ predicate selects the hook ``.py`` files that route to that single check.
382
+
383
+ Args:
384
+ file_path: The destination path of the write, edit, or pre-check target.
385
+
386
+ Returns:
387
+ True when the path names a Python file inside hook infrastructure.
388
+ """
389
+ if not file_path:
390
+ return False
391
+ if not is_hook_infrastructure(file_path):
392
+ return False
393
+ return get_file_extension(file_path) in ALL_PYTHON_EXTENSIONS
394
+
395
+
396
+ def _hook_infrastructure_blocking_issues(
397
+ content: str,
398
+ file_path: str,
399
+ full_file_content: str | None = None,
400
+ prior_full_file_content: str = "",
401
+ ) -> list[str]:
402
+ """Run the checks that still guard a hook Python target.
403
+
404
+ The whole code-rules verdict stays off hook-infrastructure files, so this
405
+ runs the two checks that must still guard them: the cross-file duplicate-body
406
+ check, span-scoped to the lines an edit touched exactly as ``validate_content``
407
+ scopes it for production code; and the zero-payload alias check, whose
408
+ docstring names hook modules as its motivating case, run over the whole
409
+ post-edit file.
410
+
411
+ Args:
412
+ content: The fragment or whole-file body under validation.
413
+ file_path: The hook-infrastructure destination path.
414
+ full_file_content: The reconstructed post-edit file body on an Edit, or
415
+ None for a whole-file Write.
416
+ prior_full_file_content: The file content before the edit applied, used to
417
+ recover the changed lines on an Edit.
418
+
419
+ Returns:
420
+ The in-scope duplicate-body violations and the zero-payload alias
421
+ violations for the target.
422
+ """
423
+ effective_content = content if full_file_content is None else full_file_content
424
+ all_changed_lines = (
425
+ changed_line_numbers(prior_full_file_content, full_file_content)
426
+ if full_file_content is not None
427
+ else None
428
+ )
429
+ all_issues = check_duplicate_function_body_across_files(
430
+ effective_content,
431
+ file_path,
432
+ all_changed_lines,
433
+ )
434
+ all_issues.extend(check_zero_payload_function_alias(effective_content, file_path))
435
+ return all_issues
436
+
437
+
337
438
  def _without_line_prefix(violation_text: str) -> str:
338
439
  """Return the violation message body with its ``Line <n>: `` locator removed.
339
440
 
@@ -441,15 +542,22 @@ def _run_precheck(
441
542
  Exit code 1 when any violation exists or the candidate cannot be read,
442
543
  and 0 when the candidate is clean or the target is exempt.
443
544
  """
444
- if not _is_validated_target(target_path):
545
+ runs_full_verdict = _is_validated_target(target_path)
546
+ runs_hook_duplicate_body = _is_hook_infrastructure_python_target(target_path)
547
+ if not runs_full_verdict and not runs_hook_duplicate_body:
445
548
  return 0
446
549
  candidate_content = _read_existing_file_content(candidate_path)
447
550
  if candidate_content is None:
448
551
  error_stream.write(f"error: cannot read candidate file: {candidate_path}\n")
449
552
  return 1
450
553
  candidate_content = candidate_content.lstrip(UTF8_BYTE_ORDER_MARK)
451
- old_content = _read_existing_file_content(target_path) or ""
452
- all_issues = validate_content(candidate_content, target_path, old_content)
554
+ if runs_full_verdict:
555
+ old_content = _read_existing_file_content(target_path) or ""
556
+ all_issues = validate_content(candidate_content, target_path, old_content)
557
+ else:
558
+ all_issues = _hook_infrastructure_blocking_issues(
559
+ candidate_content, target_path
560
+ )
453
561
  for each_issue in all_issues:
454
562
  violation_stream.write(f"{each_issue}\n")
455
563
  return 1 if all_issues else 0
@@ -576,7 +684,7 @@ def _deny_reason_for_issues(
576
684
  Returns:
577
685
  The complete ``permissionDecisionReason`` text.
578
686
  """
579
- issue_list = "; ".join(all_blocking_issues[:10])
687
+ issue_list = "; ".join(all_blocking_issues[:DENY_REASON_ISSUE_PREVIEW_COUNT])
580
688
  deny_reason = (
581
689
  f"BLOCKED: [CODE_RULES] {len(all_blocking_issues)} violation(s): {issue_list}"
582
690
  )
@@ -593,7 +701,7 @@ def _deny_reason_for_issues(
593
701
  all_blocking_issues=all_blocking_issues,
594
702
  )
595
703
  if forecast_issues:
596
- forecast_list = "; ".join(forecast_issues[:10])
704
+ forecast_list = "; ".join(forecast_issues[:DENY_REASON_ISSUE_PREVIEW_COUNT])
597
705
  deny_reason += (
598
706
  f"; FULL-FILE FORECAST — {len(forecast_issues)} additional "
599
707
  "violation(s) elsewhere in this file will block future edits "
@@ -602,6 +710,24 @@ def _deny_reason_for_issues(
602
710
  return deny_reason + _precheck_hint()
603
711
 
604
712
 
713
+ def _write_deny_payload(deny_reason: str, deny_stream: TextIO) -> None:
714
+ """Write a PreToolUse deny payload carrying the given reason.
715
+
716
+ Args:
717
+ deny_reason: The composed ``permissionDecisionReason`` text.
718
+ deny_stream: The stream the JSON deny payload is written to.
719
+ """
720
+ deny_payload = {
721
+ "hookSpecificOutput": {
722
+ "hookEventName": "PreToolUse",
723
+ "permissionDecision": "deny",
724
+ "permissionDecisionReason": deny_reason,
725
+ }
726
+ }
727
+ deny_stream.write(json.dumps(deny_payload) + "\n")
728
+ deny_stream.flush()
729
+
730
+
605
731
  def _report_blocking_violations(
606
732
  content: str,
607
733
  tool_name: str,
@@ -632,21 +758,53 @@ def _report_blocking_violations(
632
758
  )
633
759
  if not all_blocking_issues:
634
760
  return
635
- deny_payload = {
636
- "hookSpecificOutput": {
637
- "hookEventName": "PreToolUse",
638
- "permissionDecision": "deny",
639
- "permissionDecisionReason": _deny_reason_for_issues(
640
- all_blocking_issues,
641
- tool_name,
642
- file_path,
643
- full_file_content_after_edit,
644
- prior_full_file_content,
645
- ),
646
- }
647
- }
648
- deny_stream.write(json.dumps(deny_payload) + "\n")
649
- deny_stream.flush()
761
+ _write_deny_payload(
762
+ _deny_reason_for_issues(
763
+ all_blocking_issues,
764
+ tool_name,
765
+ file_path,
766
+ full_file_content_after_edit,
767
+ prior_full_file_content,
768
+ ),
769
+ deny_stream,
770
+ )
771
+
772
+
773
+ def _report_hook_blocking_issues(
774
+ content: str,
775
+ file_path: str,
776
+ full_file_content_after_edit: str | None,
777
+ prior_full_file_content: str,
778
+ deny_stream: TextIO,
779
+ ) -> None:
780
+ """Write a deny payload when a hook target trips a check that still guards it.
781
+
782
+ The full code-rules verdict stays off hook-infrastructure files; this runs the
783
+ two checks that must still guard them — the cross-file duplicate-body check and
784
+ the zero-payload alias check — and emits the deny payload when either fires.
785
+
786
+ Args:
787
+ content: The fragment or whole-file body under validation.
788
+ file_path: The hook-infrastructure destination path.
789
+ full_file_content_after_edit: The reconstructed post-edit file body,
790
+ or None when the payload is not an Edit.
791
+ prior_full_file_content: The on-disk content before the edit.
792
+ deny_stream: The stream the JSON deny payload is written to.
793
+ """
794
+ all_blocking_issues = _hook_infrastructure_blocking_issues(
795
+ content,
796
+ file_path,
797
+ full_file_content_after_edit,
798
+ prior_full_file_content,
799
+ )
800
+ if not all_blocking_issues:
801
+ return
802
+ issue_list = "; ".join(all_blocking_issues[:DENY_REASON_ISSUE_PREVIEW_COUNT])
803
+ deny_reason = (
804
+ f"BLOCKED: [CODE_RULES] {len(all_blocking_issues)} violation(s): {issue_list}"
805
+ + _precheck_hint()
806
+ )
807
+ _write_deny_payload(deny_reason, deny_stream)
650
808
 
651
809
 
652
810
  def main(all_arguments: list[str]) -> None:
@@ -671,7 +829,8 @@ def main(all_arguments: list[str]) -> None:
671
829
  tool_input = pretooluse_payload.get("tool_input", {})
672
830
  file_path = tool_input.get("file_path", "")
673
831
 
674
- if not _is_validated_target(file_path):
832
+ runs_full_verdict = _is_validated_target(file_path)
833
+ if not runs_full_verdict and not _is_hook_infrastructure_python_target(file_path):
675
834
  sys.exit(0)
676
835
 
677
836
  validation_contents = _contents_for_validation(
@@ -690,6 +849,16 @@ def main(all_arguments: list[str]) -> None:
690
849
  if not content:
691
850
  sys.exit(0)
692
851
 
852
+ if not runs_full_verdict:
853
+ _report_hook_blocking_issues(
854
+ content,
855
+ file_path,
856
+ full_file_content_after_edit,
857
+ prior_full_file_content,
858
+ sys.stdout,
859
+ )
860
+ sys.exit(0)
861
+
693
862
  _report_blocking_violations(
694
863
  content,
695
864
  tool_name,
@@ -54,6 +54,101 @@ def _mask_string_literals_preserving_length(source_line: str) -> str:
54
54
  return string_literal_pattern.sub(_replace_string_literal, source_line)
55
55
 
56
56
 
57
+ def _carries_top_level_slice_colon(bracket_body: str) -> bool:
58
+ """Return True when ``bracket_body`` holds a slice colon at its own level.
59
+
60
+ The body is the text between one ``[...]`` pair. A slice colon is a ``:``
61
+ that sits at the body's top level — not nested inside any ``(``, ``[``,
62
+ or ``{`` opened within the body — and is not the ``:=`` walrus operator.
63
+ A slice writes ``[:N]`` or ``[1:N]``; a plain subscript (``[K]``), a
64
+ walrus subscript (``[(n := M)]``), and a lambda subscript
65
+ (``[(lambda: V)()]``) carry no such colon.
66
+ """
67
+ nesting_depth = 0
68
+ for each_position, each_character in enumerate(bracket_body):
69
+ if each_character in "([{":
70
+ nesting_depth += 1
71
+ continue
72
+ if each_character in ")]}":
73
+ nesting_depth -= 1
74
+ continue
75
+ if each_character == ":" and nesting_depth == 0:
76
+ next_position = each_position + 1
77
+ character_after_colon = bracket_body[next_position : next_position + 1]
78
+ follows_walrus = character_after_colon == "="
79
+ if not follows_walrus:
80
+ return True
81
+ return False
82
+
83
+
84
+ def _innermost_bracket_body_around(masked_line: str, occurrence_position: int) -> str | None:
85
+ """Return the body of the innermost ``[...]`` pair enclosing a position.
86
+
87
+ Walks the line tracking open-bracket positions on a stack. The bracket
88
+ pair directly enclosing ``occurrence_position`` is the one whose ``[``
89
+ is on top of the stack when that position is reached; its body runs from
90
+ just after that ``[`` to its matching ``]``. Returns ``None`` when the
91
+ position lies outside every ``[...]`` pair.
92
+ """
93
+ open_bracket_positions: list[int] = []
94
+ enclosing_open_position = -1
95
+ for each_position, each_character in enumerate(masked_line):
96
+ if each_position == occurrence_position:
97
+ enclosing_open_position = open_bracket_positions[-1] if open_bracket_positions else -1
98
+ if each_character == "[":
99
+ open_bracket_positions.append(each_position)
100
+ continue
101
+ if each_character == "]" and open_bracket_positions:
102
+ open_bracket_positions.pop()
103
+
104
+ if enclosing_open_position == -1:
105
+ return None
106
+
107
+ closing_position = _matching_close_bracket_position(masked_line, enclosing_open_position)
108
+ if closing_position == -1:
109
+ return None
110
+ return masked_line[enclosing_open_position + 1 : closing_position]
111
+
112
+
113
+ def _matching_close_bracket_position(masked_line: str, open_position: int) -> int:
114
+ """Return the index of the ``]`` that closes the ``[`` at ``open_position``.
115
+
116
+ Returns ``-1`` when the opening bracket has no matching close on the line.
117
+ """
118
+ depth = 0
119
+ for each_position in range(open_position, len(masked_line)):
120
+ each_character = masked_line[each_position]
121
+ if each_character == "[":
122
+ depth += 1
123
+ continue
124
+ if each_character == "]":
125
+ depth -= 1
126
+ if depth == 0:
127
+ return each_position
128
+ return -1
129
+
130
+
131
+ def _is_magic_number_inside_slice_bound(masked_line: str, number_text: str) -> bool:
132
+ """Return True when ``number_text`` appears as a slice bound on the line.
133
+
134
+ For each standalone-token occurrence of ``number_text``, the innermost
135
+ ``[...]`` pair directly enclosing it is located. The number is a slice
136
+ bound only when that innermost pair carries a top-level slice colon
137
+ (``sha[:N]``, ``ts[1:N]``). A plain subscript index (``items[K]``), the
138
+ inner index of a nested subscript (``a[b[I]:N]`` — the inner index sits
139
+ inside the plain inner pair), a walrus subscript (``arr[(n := M)]``), and
140
+ a lambda subscript (``seq[(lambda: V)()]``) are not slice bounds.
141
+ """
142
+ token_boundary_pattern = r"(?<![.\w])" + re.escape(number_text) + r"(?![.\w])"
143
+ for each_match in re.finditer(token_boundary_pattern, masked_line):
144
+ bracket_body = _innermost_bracket_body_around(masked_line, each_match.start())
145
+ if bracket_body is None:
146
+ continue
147
+ if _carries_top_level_slice_colon(bracket_body):
148
+ return True
149
+ return False
150
+
151
+
57
152
  def check_magic_values(content: str, file_path: str) -> list[str]:
58
153
  """Check for magic values in function bodies."""
59
154
  if is_config_file(file_path) or is_test_file(file_path):
@@ -93,6 +188,9 @@ def check_magic_values(content: str, file_path: str) -> list[str]:
93
188
  if each_number not in allowed_numbers:
94
189
  if "range(" in stripped_without_string_literals or "enumerate(" in stripped_without_string_literals:
95
190
  continue
191
+ if _is_magic_number_inside_slice_bound(stripped_without_string_literals, each_number):
192
+ issues.append(f"Line {each_line_number}: Magic value {each_number} - extract to named constant")
193
+ break
96
194
  if "[" in stripped_without_string_literals and "]" in stripped_without_string_literals:
97
195
  continue
98
196
  issues.append(f"Line {each_line_number}: Magic value {each_number} - extract to named constant")
@@ -115,6 +115,47 @@ def _collect_annotated_arguments(function_node: ast.FunctionDef | ast.AsyncFunct
115
115
  return all_annotated_arguments
116
116
 
117
117
 
118
+ def _collect_fixture_injection_arguments(
119
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef,
120
+ ) -> list[ast.arg]:
121
+ """Return only the named parameters pytest fills by fixture injection.
122
+
123
+ Pytest passes fixtures by keyword (``testfunction(**testargs)``), so a
124
+ parameter can receive a fixture only when both conditions hold: it is
125
+ reachable by keyword, and pytest is responsible for supplying its value.
126
+ Positional-only parameters are NOT injection slots — a keyword-passed
127
+ fixture can never bind to one, and ``def test_x(tmp_path, /)`` raises a
128
+ missing-argument ``TypeError`` under pytest. A ``*args`` star-argument or
129
+ ``**kwargs`` double-star-argument never names a single fixture either.
130
+ A parameter carrying a default is NOT injected — pytest leaves its default
131
+ in place rather than supplying the fixture. So this collector keeps only the
132
+ positional-or-keyword and keyword-only parameters that have no default, and
133
+ omits ``args.posonlyargs``, ``args.vararg``, and ``args.kwarg``.
134
+
135
+ Args:
136
+ function_node: The function definition AST node to inspect.
137
+
138
+ Returns:
139
+ The undefaulted positional-or-keyword and keyword-only argument nodes,
140
+ in declaration order.
141
+ """
142
+ arguments = function_node.args
143
+ defaulted_positional_count = len(arguments.defaults)
144
+ undefaulted_positional_arguments = (
145
+ arguments.args[:-defaulted_positional_count]
146
+ if defaulted_positional_count
147
+ else arguments.args
148
+ )
149
+ undefaulted_keyword_only_arguments = [
150
+ each_keyword_argument
151
+ for each_keyword_argument, each_default in zip(
152
+ arguments.kwonlyargs, arguments.kw_defaults
153
+ )
154
+ if each_default is None
155
+ ]
156
+ return [*undefaulted_positional_arguments, *undefaulted_keyword_only_arguments]
157
+
158
+
118
159
  def _collect_target_names(target: ast.expr) -> list[ast.Name]:
119
160
  """Return every ast.Name reachable through tuple/list/starred unpacking targets."""
120
161
  if isinstance(target, ast.Name):