claude-dev-env 2.28.0 → 2.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  2. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
  3. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  4. package/docs/CODE_RULES.md +3 -3
  5. package/docs/agent-spawn-protocol.md +1 -1
  6. package/docs/references/prose-style-enforcement.md +5 -9
  7. package/hooks/blocking/AGENTS.md +3 -13
  8. package/hooks/blocking/code_rules_docstrings.py +10 -2138
  9. package/hooks/blocking/code_rules_enforcer.py +0 -121
  10. package/hooks/blocking/code_rules_imports_logging.py +1 -236
  11. package/hooks/blocking/code_rules_shared.py +23 -0
  12. package/hooks/blocking/code_rules_test_layout.py +8 -8
  13. package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
  14. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
  15. package/hooks/blocking/precommit_code_rules_gate.py +8 -43
  16. package/hooks/blocking/state_description_blocker.py +1 -7
  17. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  18. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
  19. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
  20. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
  21. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
  22. package/hooks/blocking/test_code_rules_shared.py +12 -0
  23. package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
  24. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
  25. package/hooks/blocking/test_state_description_blocker.py +6 -4
  26. package/hooks/blocking/test_stop_dispatcher.py +5 -29
  27. package/hooks/hooks_constants/AGENTS.md +1 -6
  28. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
  29. package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
  30. package/hooks/hooks_constants/messages.py +0 -2
  31. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
  32. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
  33. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
  34. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
  35. package/hooks/hooks_constants/test_messages.py +5 -3
  36. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
  37. package/package.json +1 -1
  38. package/rules/AGENTS.md +3 -6
  39. package/rules/claims-as-quotes.md +0 -10
  40. package/rules/code-standards.md +6 -6
  41. package/rules/explore-thoroughly.md +0 -1
  42. package/rules/failure-blast-radius.md +0 -8
  43. package/rules/falsify-before-green.md +0 -8
  44. package/rules/file-global-constants.md +2 -2
  45. package/rules/filesystem-search.md +1 -1
  46. package/rules/git-workflow.md +1 -9
  47. package/rules/hedging-claims.md +2 -6
  48. package/rules/long-horizon-autonomy.md +1 -1
  49. package/rules/measurement-denominators.md +0 -9
  50. package/rules/research-mode.md +0 -6
  51. package/rules/verify-before-asking.md +0 -5
  52. package/rules/verify-runtime-state.md +0 -5
  53. package/scripts/codex_compat_materializer.py +0 -12
  54. package/scripts/sync_to_cursor/rules.py +0 -10
  55. package/scripts/tests/test_engine.py +0 -1
  56. package/scripts/tests/test_rules.py +0 -1
  57. package/scripts/tests/test_sync_to_cursor.py +0 -1
  58. package/system-prompts/software-engineer.xml +3 -3
  59. package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
  60. package/hooks/blocking/code_rules_dead_config_field.py +0 -568
  61. package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
  62. package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
  63. package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
  64. package/hooks/blocking/code_rules_mock_completeness.py +0 -295
  65. package/hooks/blocking/code_rules_scope_binding.py +0 -151
  66. package/hooks/blocking/code_rules_unused_imports.py +0 -197
  67. package/hooks/blocking/hedging_language_blocker.py +0 -221
  68. package/hooks/blocking/intent_only_ending_blocker.py +0 -148
  69. package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
  70. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
  71. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
  72. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
  73. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
  74. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
  75. package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
  76. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
  77. package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
  78. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
  79. package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
  80. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
  81. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
  82. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
  83. package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
  84. package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
  86. package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
  87. package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
  88. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
  89. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
  90. package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
  91. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
  92. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
  93. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
  94. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
  95. package/hooks/blocking/test_hedging_language_blocker.py +0 -261
  96. package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
  97. package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
  98. package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
  99. package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
  100. package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
  101. package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
  102. package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
  103. package/rules/conservative-action.md +0 -17
  104. package/rules/context7.md +0 -8
  105. package/rules/few-words.md +0 -3
  106. package/rules/parallel-tools.md +0 -23
@@ -63,45 +63,14 @@ from code_rules_constants_config import ( # noqa: E402
63
63
  check_constants_outside_config_advisory,
64
64
  check_file_global_constants_use_count,
65
65
  )
66
- from code_rules_dead_argparse_argument import ( # noqa: E402
67
- check_dead_argparse_arguments,
68
- )
69
- from code_rules_dead_config_field import ( # noqa: E402
70
- check_dead_config_dataclass_fields,
71
- )
72
- from code_rules_dead_dataclass_field import ( # noqa: E402
73
- check_dead_dataclass_fields,
74
- )
75
- from code_rules_dead_module_constant import ( # noqa: E402
76
- check_dead_module_constants,
77
- )
78
- from code_rules_dead_split_branch import ( # noqa: E402
79
- check_dead_split_truthiness_branch,
80
- )
81
66
  from code_rules_docstrings import ( # noqa: E402
82
67
  check_class_docstring_names_public_methods,
83
68
  check_docstring_args_match_signature,
84
- check_docstring_args_single_line_scope_vs_span,
85
- check_docstring_cardinal_count_matches_constant_family,
86
- check_docstring_delegation_summary_enumeration_drift,
87
69
  check_docstring_documents_unreferenced_parameter,
88
- check_docstring_fallback_branch_coverage,
89
- check_docstring_field_runmode_outcome,
90
70
  check_docstring_format,
91
- check_docstring_length_constant_superlative_vs_exact_gate,
92
- check_docstring_names_absent_type_checking_gate,
93
71
  check_docstring_names_undefined_constant,
94
- check_docstring_no_consumer_claim,
95
- check_docstring_no_inline_literal_claim,
96
- check_docstring_no_network_claim_with_metadata_access,
97
72
  check_docstring_prose_wall_without_illustration,
98
- check_docstring_punctuation_mark_enumeration_coverage,
99
- check_docstring_raises_unraisable_largezipfile,
100
- check_docstring_returns_plural_cardinality,
101
73
  check_docstring_runon_sentence,
102
- check_docstring_step_enumeration_dispatch_coverage,
103
- check_docstring_tuple_enumeration_match,
104
- check_docstring_unguarded_malformed_payload_claim,
105
74
  check_module_docstring_names_public_checks,
106
75
  check_module_docstring_scope_omits_data_schema_constants,
107
76
  )
@@ -113,7 +82,6 @@ from code_rules_duplicate_body import ( # noqa: E402
113
82
  from code_rules_imports_logging import ( # noqa: E402
114
83
  advise_file_line_count,
115
84
  check_e2e_test_naming,
116
- check_import_block_sorted,
117
85
  check_imports_at_top,
118
86
  check_js_bare_flag_return_directive,
119
87
  check_js_resume_task_enumeration_coverage,
@@ -134,9 +102,6 @@ from code_rules_magic_values import ( # noqa: E402
134
102
  check_fstring_structural_literals,
135
103
  check_magic_values,
136
104
  )
137
- from code_rules_mock_completeness import ( # noqa: E402
138
- check_incomplete_mocks,
139
- )
140
105
  from code_rules_naming_collection import ( # noqa: E402
141
106
  check_collection_prefix,
142
107
  check_loop_variable_naming,
@@ -203,9 +168,6 @@ from code_rules_typeddict_stub import ( # noqa: E402
203
168
  check_typed_dict_encode_decode,
204
169
  check_zero_payload_function_alias,
205
170
  )
206
- from code_rules_unused_imports import ( # noqa: E402
207
- check_unused_module_level_imports,
208
- )
209
171
  from codex_apply_patch import ( # noqa: E402
210
172
  CodexPatchError,
211
173
  CodexPatchFile,
@@ -355,14 +317,6 @@ def validate_content(
355
317
  if not is_test_file(file_path):
356
318
  all_issues.extend(check_comment_changes(old_content, content, file_path))
357
319
  all_issues.extend(check_imports_at_top(content))
358
- all_issues.extend(
359
- check_import_block_sorted(
360
- effective_content,
361
- file_path,
362
- all_changed_lines,
363
- defer_scope_to_caller,
364
- )
365
- )
366
320
  all_issues.extend(check_logging_fstrings(content))
367
321
  all_issues.extend(check_logging_printf_tokens(content, file_path))
368
322
  all_issues.extend(check_logging_adjacent_string_literals(content, file_path))
@@ -440,21 +394,6 @@ def validate_content(
440
394
  all_issues.extend(
441
395
  check_docstring_documents_unreferenced_parameter(effective_content, file_path)
442
396
  )
443
- all_issues.extend(check_docstring_fallback_branch_coverage(effective_content, file_path))
444
- all_issues.extend(check_docstring_no_consumer_claim(effective_content, file_path))
445
- all_issues.extend(
446
- check_docstring_no_network_claim_with_metadata_access(
447
- effective_content, file_path
448
- )
449
- )
450
- all_issues.extend(
451
- check_docstring_unguarded_malformed_payload_claim(
452
- effective_content, file_path
453
- )
454
- )
455
- all_issues.extend(
456
- check_docstring_no_inline_literal_claim(effective_content, file_path)
457
- )
458
397
  all_issues.extend(
459
398
  check_class_docstring_names_public_methods(effective_content, file_path)
460
399
  )
@@ -482,52 +421,9 @@ def validate_content(
482
421
  effective_content, file_path
483
422
  )
484
423
  )
485
- all_issues.extend(
486
- check_docstring_tuple_enumeration_match(effective_content, file_path)
487
- )
488
- all_issues.extend(
489
- check_docstring_punctuation_mark_enumeration_coverage(
490
- effective_content, file_path
491
- )
492
- )
493
- all_issues.extend(
494
- check_docstring_step_enumeration_dispatch_coverage(
495
- effective_content, file_path
496
- )
497
- )
498
- all_issues.extend(
499
- check_docstring_returns_plural_cardinality(effective_content, file_path)
500
- )
501
- all_issues.extend(
502
- check_docstring_length_constant_superlative_vs_exact_gate(
503
- effective_content, file_path
504
- )
505
- )
506
- all_issues.extend(
507
- check_docstring_field_runmode_outcome(effective_content, file_path)
508
- )
509
- all_issues.extend(
510
- check_docstring_raises_unraisable_largezipfile(effective_content, file_path)
511
- )
512
- all_issues.extend(
513
- check_docstring_cardinal_count_matches_constant_family(
514
- effective_content, file_path
515
- )
516
- )
517
424
  all_issues.extend(
518
425
  check_docstring_names_undefined_constant(effective_content, file_path)
519
426
  )
520
- all_issues.extend(
521
- check_docstring_names_absent_type_checking_gate(effective_content, file_path)
522
- )
523
- all_issues.extend(
524
- check_docstring_args_single_line_scope_vs_span(effective_content, file_path)
525
- )
526
- all_issues.extend(
527
- check_docstring_delegation_summary_enumeration_drift(
528
- effective_content, file_path
529
- )
530
- )
531
427
  all_issues.extend(
532
428
  check_boolean_naming(
533
429
  effective_content,
@@ -563,22 +459,6 @@ def validate_content(
563
459
  all_issues.extend(check_stuttering_collection_prefix(content, file_path))
564
460
  all_issues.extend(check_hardcoded_user_paths(content, file_path))
565
461
  all_issues.extend(check_sys_path_insert_deduplication_guard(content, file_path))
566
- all_issues.extend(
567
- check_unused_module_level_imports(content, file_path, full_file_content)
568
- )
569
- all_issues.extend(
570
- check_dead_dataclass_fields(content, file_path, full_file_content)
571
- )
572
- all_issues.extend(
573
- check_dead_argparse_arguments(content, file_path, full_file_content)
574
- )
575
- all_issues.extend(
576
- check_dead_config_dataclass_fields(content, file_path, full_file_content)
577
- )
578
- all_issues.extend(
579
- check_dead_module_constants(content, file_path, full_file_content)
580
- )
581
- all_issues.extend(check_dead_split_truthiness_branch(content, file_path))
582
462
  all_issues.extend(check_library_print(content, file_path))
583
463
  all_issues.extend(check_parameter_annotations(content, file_path))
584
464
  all_issues.extend(check_known_pytest_fixture_annotations(content, file_path))
@@ -640,7 +520,6 @@ def validate_content(
640
520
  )
641
521
  all_issues.extend(check_whitespace_indentation_magic(content, file_path))
642
522
  all_issues.extend(check_orphan_css_classes(effective_content, file_path))
643
- check_incomplete_mocks(content, file_path)
644
523
  check_duplicated_format_patterns(content, file_path)
645
524
  advise_cross_skill_duplicate_helper(effective_content, file_path)
646
525
 
@@ -1,9 +1,7 @@
1
- """Imports-at-top, import-block-sorted, logging f-string, logging printf-token, win32gui None, naive datetime construction, E2E spec naming, JS resume-task enumeration coverage, JS returns-object schema-less branch, JS sibling return-object key drift, JS bare-flag return-directive, file-length advisory, and library-print checks."""
1
+ """Import and logging convention checks: check_imports_at_top, check_logging_fstrings, check_logging_printf_tokens, check_logging_adjacent_string_literals, check_windows_api_none, check_naive_datetime_construction, check_e2e_test_naming, check_js_resume_task_enumeration_coverage, check_js_returns_object_schemaless_branch, check_js_sibling_return_object_key_drift, check_js_bare_flag_return_directive, and check_library_print."""
2
2
 
3
3
  import ast
4
- import json
5
4
  import re
6
- import subprocess
7
5
  import sys
8
6
  from pathlib import Path
9
7
 
@@ -28,14 +26,9 @@ from code_rules_shared import ( # noqa: E402
28
26
  from hooks_constants.blocking_check_limits import ( # noqa: E402
29
27
  ALL_ALWAYS_NAIVE_DATETIME_CONSTRUCTORS,
30
28
  ALL_FORMAT_LOGGER_FUNCTION_NAMES,
31
- ALL_IMPORT_BLOCK_SORT_RUFF_COMMAND_PREFIX,
32
- ALL_RUFF_STANDALONE_CONFIG_FILENAMES,
33
29
  DATETIME_CLASS_ATTRIBUTE_NAME,
34
30
  FROMTIMESTAMP_POSITIONAL_TIMEZONE_ARGUMENT_COUNT,
35
- IMPORT_BLOCK_SORT_RUFF_TIMEOUT_SECONDS,
36
- IMPORT_BLOCK_SORT_RULE_CODE,
37
31
  MAX_E2E_TEST_NAMING_ISSUES,
38
- MAX_IMPORT_BLOCK_SORT_ISSUES,
39
32
  MAX_JS_BARE_FLAG_RETURN_DIRECTIVE_ISSUES,
40
33
  MAX_JS_RESUME_TASK_ENUMERATION_ISSUES,
41
34
  MAX_JS_RETURNS_OBJECT_SCHEMALESS_ISSUES,
@@ -51,9 +44,6 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
51
44
  NAIVE_DATETIME_TIMEZONE_KEYWORD,
52
45
  NAIVE_DATETIME_UTCFROMTIMESTAMP_CONSTRUCTOR,
53
46
  NAIVE_DATETIME_UTCNOW_CONSTRUCTOR,
54
- RUFF_PYPROJECT_CONFIG_FILENAME,
55
- RUFF_PYPROJECT_TOOL_TABLE_MARKER,
56
- RUFF_STDIN_ENCODING,
57
47
  SIBLING_RETURN_OBJECT_EXACT_MISSING_KEY_COUNT,
58
48
  SIBLING_RETURN_OBJECT_SINGLE_AGREED_MISSING_KEY_COUNT,
59
49
  )
@@ -234,231 +224,6 @@ def check_imports_at_top(content: str) -> list[str]:
234
224
  return issues
235
225
 
236
226
 
237
- def check_import_block_sorted(
238
- content: str,
239
- file_path: str,
240
- all_changed_lines: set[int] | None = None,
241
- defer_scope_to_caller: bool = False,
242
- ) -> list[str]:
243
- """Flag a Python import block left un-sorted per isort rules (ruff I001).
244
-
245
- ruff/isort treats a contiguous run of ``import`` and ``from`` statements as
246
- one sortable unit. An edit that re-touches such a block pulls the whole block
247
- into the gate's scope, so a pre-existing mis-ordering in an untouched line of
248
- that block becomes a quality-gate failure (``check.ps1`` runs ruff over
249
- ``hooks/blocking``). This catches that drift at Write/Edit time by delegating
250
- to ruff itself, the same sorter the quality gate uses, so the two surfaces
251
- never disagree.
252
-
253
- The check resolves *file_path* to an absolute path and runs ruff against
254
- *content* on stdin under that path, so ruff walks parents to find the repo's
255
- ``[tool.ruff]`` config. Without a discoverable config ruff falls back to
256
- default isort first-party grouping, which orders sibling imports differently
257
- and produces false positives, so the check fails open when no ruff config is
258
- found above the file. Non-Python files and test files are exempt. When ruff
259
- is not installed, times out, or returns output the parser cannot read, the
260
- check also fails open and reports nothing — a missing or slow tool never
261
- blocks a write.
262
-
263
- An I001 finding covers the whole sortable block, so each finding carries a
264
- span from the block's anchor line (``location.row``) through its last line
265
- (``end_location.row``) scoped through the shared
266
- ``_scope_violations_to_changed_lines`` helper. On a terminal diff-scoped Edit
267
- a finding is returned when any line in that block span is among
268
- ``all_changed_lines``, so an edit touching any line of an unsorted block
269
- blocks while an edit far from the block does not. On a new-file or full-file
270
- write (``all_changed_lines`` None) every finding is in scope;
271
- ``defer_scope_to_caller`` returns every finding so the commit/push gate
272
- scopes by added line through its own ``Line N:`` partitioning.
273
-
274
- Args:
275
- content: The full file content the Write or Edit would leave on disk.
276
- file_path: The destination path, used both to gate by extension and to
277
- give ruff the filename it needs for config discovery.
278
- all_changed_lines: Post-edit line numbers the current edit touched, or
279
- None to treat the whole file as in scope. When provided, a finding
280
- blocks only when any line of its block span is among the changed
281
- lines.
282
- defer_scope_to_caller: When True, return every finding so the commit/push
283
- gate's ``Line N:`` partitioning scopes by added line.
284
-
285
- Returns:
286
- One issue string per detected I001 finding, capped at
287
- ``MAX_IMPORT_BLOCK_SORT_ISSUES``; an empty list when the block sorts
288
- cleanly, every finding falls outside the changed lines, or the check
289
- fails open.
290
- """
291
- if get_file_extension(file_path) not in ALL_PYTHON_EXTENSIONS:
292
- return []
293
- if is_test_file(file_path):
294
- return []
295
-
296
- absolute_file_path = Path(file_path).resolve()
297
- if not _ruff_config_discoverable(absolute_file_path):
298
- return []
299
-
300
- ruff_findings = _run_ruff_import_sort_check(content, str(absolute_file_path))
301
- if ruff_findings is None:
302
- return []
303
-
304
- span_end_line_offset = 1
305
- all_violations_in_finding_order: list[tuple[range, str]] = []
306
- for each_finding in ruff_findings:
307
- if each_finding.get("code") != IMPORT_BLOCK_SORT_RULE_CODE:
308
- continue
309
- line_number = _finding_line_number(each_finding.get("location"))
310
- block_end_line_number = _finding_block_end_line_number(
311
- each_finding.get("end_location"), line_number
312
- )
313
- span_range = range(line_number, block_end_line_number + span_end_line_offset)
314
- message = (
315
- f"Line {line_number}: Import block is un-sorted (ruff I001) - "
316
- f"run 'ruff check --fix' to sort the block before writing"
317
- )
318
- all_violations_in_finding_order.append((span_range, message))
319
- if len(all_violations_in_finding_order) >= MAX_IMPORT_BLOCK_SORT_ISSUES:
320
- break
321
-
322
- return _scope_violations_to_changed_lines(
323
- all_violations_in_finding_order,
324
- all_changed_lines,
325
- defer_scope_to_caller,
326
- )
327
-
328
-
329
- def _run_ruff_import_sort_check(content: str, file_path: str) -> list[dict[str, object]] | None:
330
- """Run ruff's I001 check over *content* and return its parsed JSON findings.
331
-
332
- Args:
333
- content: The file content fed to ruff on stdin.
334
- file_path: The filename ruff is told the stdin content belongs to, so it
335
- discovers the surrounding ``[tool.ruff]`` config.
336
-
337
- Returns:
338
- The list of ruff finding dictionaries, or ``None`` when ruff is absent,
339
- times out, or emits output that is not the expected JSON list.
340
- """
341
- ruff_command = [
342
- *ALL_IMPORT_BLOCK_SORT_RUFF_COMMAND_PREFIX,
343
- "--stdin-filename",
344
- file_path,
345
- "-",
346
- ]
347
- try:
348
- completed_process = subprocess.run(
349
- ruff_command,
350
- input=content.encode(RUFF_STDIN_ENCODING),
351
- capture_output=True,
352
- timeout=IMPORT_BLOCK_SORT_RUFF_TIMEOUT_SECONDS,
353
- check=False,
354
- )
355
- except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
356
- return None
357
-
358
- try:
359
- ruff_stdout = completed_process.stdout.decode(RUFF_STDIN_ENCODING)
360
- except UnicodeDecodeError:
361
- return None
362
-
363
- try:
364
- parsed_findings = json.loads(ruff_stdout)
365
- except json.JSONDecodeError:
366
- return None
367
-
368
- if not isinstance(parsed_findings, list):
369
- return None
370
- return [each_finding for each_finding in parsed_findings if isinstance(each_finding, dict)]
371
-
372
-
373
- def _ruff_config_discoverable(absolute_file_path: Path) -> bool:
374
- """Return whether a ruff config sits above *absolute_file_path*.
375
-
376
- Walks the file's parent directories looking for a standalone ``ruff.toml`` /
377
- ``.ruff.toml`` or a ``pyproject.toml`` that declares a ``[tool.ruff`` table.
378
- ruff uses this same nearest-ancestor walk to resolve its settings; mirroring
379
- it lets the check confirm ruff will see the project's isort configuration
380
- rather than its built-in defaults before trusting an I001 finding.
381
-
382
- Args:
383
- absolute_file_path: The resolved path of the file under validation.
384
-
385
- Returns:
386
- True when a ruff config is found in the file's directory or any ancestor;
387
- False when the walk reaches the filesystem root without one.
388
- """
389
- for each_directory in absolute_file_path.parents:
390
- for each_standalone_name in ALL_RUFF_STANDALONE_CONFIG_FILENAMES:
391
- if (each_directory / each_standalone_name).is_file():
392
- return True
393
- pyproject_path = each_directory / RUFF_PYPROJECT_CONFIG_FILENAME
394
- if _pyproject_declares_ruff_table(pyproject_path):
395
- return True
396
- return False
397
-
398
-
399
- def _pyproject_declares_ruff_table(pyproject_path: Path) -> bool:
400
- """Return whether *pyproject_path* exists and declares a ``[tool.ruff`` table.
401
-
402
- Args:
403
- pyproject_path: The candidate ``pyproject.toml`` path to inspect.
404
-
405
- Returns:
406
- True when the file exists and its text contains the ``[tool.ruff`` table
407
- marker; False when the file is absent or unreadable.
408
- """
409
- if not pyproject_path.is_file():
410
- return False
411
- try:
412
- pyproject_text = pyproject_path.read_text(encoding=RUFF_STDIN_ENCODING)
413
- except OSError:
414
- return False
415
- return RUFF_PYPROJECT_TOOL_TABLE_MARKER in pyproject_text
416
-
417
-
418
- def _finding_line_number(location: object) -> int:
419
- """Return the source line a ruff finding's location anchors to.
420
-
421
- Args:
422
- location: The ``location`` value from one ruff JSON finding, expected to
423
- be a mapping carrying an integer ``row``; any other shape yields a
424
- zero line number.
425
-
426
- Returns:
427
- The ``row`` value as an integer, or a zero line number when the location
428
- is absent or carries no readable row.
429
- """
430
- if not isinstance(location, dict):
431
- return 0
432
- row = location.get("row")
433
- if not isinstance(row, int):
434
- return 0
435
- return row
436
-
437
-
438
- def _finding_block_end_line_number(end_location: object, anchor_line_number: int) -> int:
439
- """Return the last source line a ruff finding's block spans.
440
-
441
- ruff anchors an I001 finding at its block's first line and records the
442
- block's last line in ``end_location.row``, so the two together bound the
443
- whole sortable run. The end row falls back to the anchor line when the
444
- ``end_location`` is absent or carries no readable row, or when it would land
445
- above the anchor — keeping the span to at least the anchor line itself.
446
-
447
- Args:
448
- end_location: The ``end_location`` value from one ruff JSON finding,
449
- expected to be a mapping carrying an integer ``row``.
450
- anchor_line_number: The finding's block-anchor line, used as the floor
451
- for the returned end line.
452
-
453
- Returns:
454
- The block's last line as an integer, never below *anchor_line_number*.
455
- """
456
- if not isinstance(end_location, dict):
457
- return anchor_line_number
458
- row = end_location.get("row")
459
- if not isinstance(row, int) or row < anchor_line_number:
460
- return anchor_line_number
461
- return row
462
227
 
463
228
 
464
229
  def _update_triple_quote_state_for_line(
@@ -19,6 +19,8 @@ from hooks_constants.code_rules_enforcer_constants import (
19
19
  ALL_WORKFLOW_REGISTRY_PATTERNS,
20
20
  CLAUDE_JOB_DIR_ENVIRONMENT_VARIABLE_NAME,
21
21
  CLAUDE_JOB_DIR_SCRATCH_SUBDIRECTORY,
22
+ CONFIG_DIRECTORY_SEGMENT,
23
+ CONSTANTS_MODULE_SUFFIX,
22
24
  EPHEMERAL_EXEMPT_DISABLE_ENVIRONMENT_VARIABLE_NAME,
23
25
  LEADING_DRIVE_LETTER_PATTERN,
24
26
  STRICT_TEST_FILE_BASENAME_PATTERN,
@@ -447,6 +449,27 @@ def is_ephemeral_path(file_path: str, hook_payload: dict | None = None) -> bool:
447
449
  return is_under_session_scratchpad(file_path, hook_payload or {})
448
450
 
449
451
 
452
+ def _is_dedicated_constants_module(file_path: str) -> bool:
453
+ """Return whether a path is a dedicated constants module.
454
+
455
+ A dedicated constants module is one whose filename ends in
456
+ ``_constants.py`` or whose path includes a ``config`` directory segment.
457
+ These modules export named values to importers, so a same-file reference
458
+ count cannot prove one of their constants dead.
459
+
460
+ Args:
461
+ file_path: The destination path of the write.
462
+
463
+ Returns:
464
+ True for a constants-suffixed module or a module under ``config/``.
465
+ """
466
+ normalized_path = file_path.replace("\\", "/").lower()
467
+ if normalized_path.endswith(CONSTANTS_MODULE_SUFFIX):
468
+ return True
469
+ path_segments = normalized_path.split("/")
470
+ return CONFIG_DIRECTORY_SEGMENT in path_segments[:-1]
471
+
472
+
450
473
  def is_migration_file(file_path: str) -> bool:
451
474
  """Check if file is a Django migration (must be self-contained)."""
452
475
  path_lower = file_path.lower().replace("\\", "/")
@@ -10,15 +10,14 @@ test functions, which exports nothing, so a single-file scan proves a constant
10
10
  no other line reads is dead. A ``conftest.py``, a shared helper module reached
11
11
  only through the ``tests/`` directory gate, and a dedicated constants or
12
12
  ``config/`` module all export their constants to importer modules, so the
13
- cross-module ``check_dead_module_constants`` governs their constants and the
14
- dead-constant check leaves them alone.
13
+ audit lane governs their constants cross-module (see
14
+ rules/file-global-constants.md) and this write-time check leaves them alone.
15
15
  """
16
16
 
17
17
  import ast
18
18
  from typing import TypeGuard
19
19
 
20
- from code_rules_dead_module_constant import _is_dedicated_constants_module
21
- from code_rules_shared import is_strict_test_file
20
+ from code_rules_shared import _is_dedicated_constants_module, is_strict_test_file
22
21
  from hooks_constants.test_layout_constants import (
23
22
  CLASS_METHOD_FIRST_PARAMETER_NAME,
24
23
  DEAD_TEST_CONSTANT_GUIDANCE,
@@ -55,7 +54,7 @@ def _is_dead_constant_scan_target(file_path: str) -> bool:
55
54
  constant no other line reads is dead. A ``conftest.py``, a shared helper
56
55
  module reached only through the ``tests/`` directory gate, and a dedicated
57
56
  constants or ``config/`` module all export to importer modules, where the
58
- cross-module ``check_dead_module_constants`` governs their constants.
57
+ cross-module audit lane governs their constants (see rules/file-global-constants.md).
59
58
 
60
59
  Args:
61
60
  file_path: The destination path of the write.
@@ -78,8 +77,8 @@ def _is_private_constant_name(name: str) -> bool:
78
77
  EXPECTED_TOTAL -> public constant, left alone
79
78
 
80
79
  Only a leading-underscore name qualifies. A public module constant exports
81
- to importer modules, so the cross-module ``check_dead_module_constants``
82
- governs it and this single-file scan leaves it alone.
80
+ to importer modules, so the cross-module audit lane governs it (see
81
+ rules/file-global-constants.md) and this single-file scan leaves it alone.
83
82
  """
84
83
  if not name.startswith(PRIVATE_NAME_PREFIX):
85
84
  return False
@@ -164,7 +163,8 @@ def check_dead_test_module_constant(content: str, file_path: str) -> list[str]:
164
163
  dead scaffolding an edit stranded. A public constant, a ``conftest.py``, a
165
164
  shared helper module under ``tests/``, and a dedicated constants or
166
165
  ``config/`` module all export to importer modules, so the cross-module
167
- ``check_dead_module_constants`` judges those and this check skips them.
166
+ audit lane judges those (see rules/file-global-constants.md) and this
167
+ check skips them.
168
168
 
169
169
  Args:
170
170
  content: The post-edit file content under validation.
@@ -6,8 +6,10 @@
6
6
  (unset) -> PROSE_STYLE_ENFORCEMENT_ENABLED False
7
7
 
8
8
  Structural AskUserQuestion lean-block validation stays always on. The flag
9
- arms only opinionated prose gates (heavy-word swaps, hedging, historical
10
- state phrasing, intent-only endings, hook prose-detector consistency).
9
+ arms only two opinionated prose gates: `plain_language_blocker.py` (heavy-word
10
+ swaps) and `hook_prose_detector_consistency.py`. Historical-state phrasing
11
+ (`state_description_blocker.py`) runs unconditionally and does not read this
12
+ flag.
11
13
  """
12
14
 
13
15
  from __future__ import annotations
@@ -1,4 +1,8 @@
1
- """Tests for the prose-style enforcement opt-in flag (default off)."""
1
+ """Tests for the prose-style enforcement opt-in flag.
2
+
3
+ Gates ``plain_language_blocker.py`` and ``hook_prose_detector_consistency.py``;
4
+ default off.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -1,11 +1,13 @@
1
- """PreToolUse hook that passes Git commit validation to Git hooks.
2
-
3
- Intercepts Bash `git commit` invocations (including `git -C <path> commit`),
4
- emits neutral evidence, and returns control to Git. Configured Git hooks
5
- receive the commit at the commit boundary.
1
+ """Library helper: resolve a directory's Git repository root.
2
+
3
+ ``pii_prevention_blocker.py`` and ``pii_payload_scan.py`` import
4
+ ``resolve_repository_root`` to find the repository a staged-commit scan
5
+ targets, and ``session_edit_stage_gate.py`` imports it to find the
6
+ repository a commit's unstaged-edit check targets. This module carries no
7
+ hook entry point of its own; the native ``git-hooks/pre_commit.py`` runs
8
+ the real commit-time CODE_RULES enforcement.
6
9
  """
7
10
 
8
- import re
9
11
  import subprocess
10
12
  import sys
11
13
  from pathlib import Path
@@ -14,36 +16,12 @@ _hooks_directory = str(Path(__file__).resolve().parent.parent)
14
16
  if _hooks_directory not in sys.path:
15
17
  sys.path.insert(0, _hooks_directory)
16
18
 
17
- from block_main_commit import ( # noqa: E402
18
- is_commit_command,
19
- parse_bash_command_from_stdin,
20
- )
21
-
22
19
  from hooks_constants.precommit_code_rules_gate_constants import ( # noqa: E402
23
20
  ALL_GIT_REPOSITORY_ROOT_COMMAND,
24
21
  GIT_COMMAND_TIMEOUT_SECONDS,
25
- GIT_DASH_C_COMMIT_PATTERN,
26
22
  )
27
23
 
28
24
 
29
- def is_git_commit_invocation(bash_command: str) -> bool:
30
- """Report whether *bash_command* runs a git commit.
31
-
32
- Matches both the plain ``git commit`` substring form and the
33
- ``git -C <path> commit`` form, where the directory flag sits between
34
- the two words.
35
-
36
- Args:
37
- bash_command: The Bash tool command string from the hook payload.
38
-
39
- Returns:
40
- True when the command invokes git commit; False otherwise.
41
- """
42
- if is_commit_command(bash_command):
43
- return True
44
- return re.search(GIT_DASH_C_COMMIT_PATTERN, bash_command) is not None
45
-
46
-
47
25
  def resolve_repository_root(working_directory: str | None) -> Path | None:
48
26
  """Resolve the Git repository root for a directory.
49
27
 
@@ -70,16 +48,3 @@ def resolve_repository_root(working_directory: str | None) -> Path | None:
70
48
  if not top_level_text:
71
49
  return None
72
50
  return Path(top_level_text)
73
-
74
-
75
- def main() -> None:
76
- """Pass Git commit validation through to configured Git hooks."""
77
- bash_command = parse_bash_command_from_stdin()
78
- if not is_git_commit_invocation(bash_command):
79
- sys.exit(0)
80
- sys.stderr.write("Git commit proceeds to configured Git hooks.\n")
81
- sys.exit(0)
82
-
83
-
84
- if __name__ == "__main__":
85
- main()
@@ -20,9 +20,6 @@ if _hooks_dir not in sys.path:
20
20
  sys.path.insert(0, _hooks_dir)
21
21
 
22
22
  from blocking.code_rules_shared import is_ephemeral_path # noqa: E402
23
- from blocking.config.prose_style_enforcement_constants import ( # noqa: E402
24
- prose_style_enforcement_enabled_in_environment,
25
- )
26
23
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
27
24
  from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin # noqa: E402
28
25
  from hooks_constants.state_description_blocker_constants import ( # noqa: E402
@@ -281,8 +278,7 @@ def evaluate(payload_by_key: dict[str, object]) -> str | None:
281
278
 
282
279
  Applies the same tool-name gate, file-extension gate, content selection, and
283
280
  pattern scan the standalone hook applies. Returns the deny-reason text when a
284
- historical phrase is found, or None to allow. Opinionated prose enforcement
285
- is default-off via ``prose_style_enforcement_enabled_in_environment()``.
281
+ historical phrase is found, or None to allow. The check is unconditional.
286
282
 
287
283
  Args:
288
284
  payload_by_key: The PreToolUse payload with tool_name and tool_input.
@@ -291,8 +287,6 @@ def evaluate(payload_by_key: dict[str, object]) -> str | None:
291
287
  The permissionDecisionReason text when the write is denied, or None when
292
288
  the write is allowed.
293
289
  """
294
- if not prose_style_enforcement_enabled_in_environment():
295
- return None
296
290
  raw_tool_name = payload_by_key.get("tool_name", "")
297
291
  tool_name = raw_tool_name if isinstance(raw_tool_name, str) else ""
298
292
  if tool_name not in ("Write", "Edit"):
@@ -47,7 +47,6 @@ VOID_ADVISORY_CHECK_FUNCTION_NAMES: frozenset[str] = frozenset(
47
47
  {
48
48
  "check_duplicated_format_patterns",
49
49
  "check_flag_gated_scenario_test_naming",
50
- "check_incomplete_mocks",
51
50
  }
52
51
  )
53
52