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.
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/docs/CODE_RULES.md +3 -3
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/references/prose-style-enforcement.md +5 -9
- package/hooks/blocking/AGENTS.md +3 -13
- package/hooks/blocking/code_rules_docstrings.py +10 -2138
- package/hooks/blocking/code_rules_enforcer.py +0 -121
- package/hooks/blocking/code_rules_imports_logging.py +1 -236
- package/hooks/blocking/code_rules_shared.py +23 -0
- package/hooks/blocking/code_rules_test_layout.py +8 -8
- package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
- package/hooks/blocking/precommit_code_rules_gate.py +8 -43
- package/hooks/blocking/state_description_blocker.py +1 -7
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
- package/hooks/blocking/test_code_rules_shared.py +12 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
- package/hooks/blocking/test_state_description_blocker.py +6 -4
- package/hooks/blocking/test_stop_dispatcher.py +5 -29
- package/hooks/hooks_constants/AGENTS.md +1 -6
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/messages.py +0 -2
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/test_messages.py +5 -3
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
- package/package.json +1 -1
- package/rules/AGENTS.md +3 -6
- package/rules/claims-as-quotes.md +0 -10
- package/rules/code-standards.md +6 -6
- package/rules/explore-thoroughly.md +0 -1
- package/rules/failure-blast-radius.md +0 -8
- package/rules/falsify-before-green.md +0 -8
- package/rules/file-global-constants.md +2 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +1 -9
- package/rules/hedging-claims.md +2 -6
- package/rules/long-horizon-autonomy.md +1 -1
- package/rules/measurement-denominators.md +0 -9
- package/rules/research-mode.md +0 -6
- package/rules/verify-before-asking.md +0 -5
- package/rules/verify-runtime-state.md +0 -5
- package/scripts/codex_compat_materializer.py +0 -12
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/tests/test_engine.py +0 -1
- package/scripts/tests/test_rules.py +0 -1
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/system-prompts/software-engineer.xml +3 -3
- package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
- package/hooks/blocking/code_rules_dead_config_field.py +0 -568
- package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
- package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
- package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
- package/hooks/blocking/code_rules_mock_completeness.py +0 -295
- package/hooks/blocking/code_rules_scope_binding.py +0 -151
- package/hooks/blocking/code_rules_unused_imports.py +0 -197
- package/hooks/blocking/hedging_language_blocker.py +0 -221
- package/hooks/blocking/intent_only_ending_blocker.py +0 -148
- package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
- package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
- package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
- package/hooks/blocking/test_hedging_language_blocker.py +0 -261
- package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
- package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
- package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
- package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
- package/rules/conservative-action.md +0 -17
- package/rules/context7.md +0 -8
- package/rules/few-words.md +0 -3
- 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
|
-
"""
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
|
|
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
|
|
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
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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"):
|