claude-dev-env 2.30.1 → 2.31.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/agents/pr-description-writer.md +1 -1
- package/AGENTS.md +6 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +37 -0
- package/bin/install.prune.test.mjs +4 -2
- package/docs/CODE_RULES.md +12 -0
- package/hooks/advisory/migration_safety_advisor.py +25 -1
- package/hooks/advisory/refactor_guard.py +85 -18
- package/hooks/advisory/test_migration_safety_advisor.py +89 -5
- package/hooks/advisory/test_refactor_guard.py +87 -0
- package/hooks/advisory/test_refactor_guard_advisory.py +72 -1
- package/hooks/blocking/code_rules_constants_config.py +7 -6
- package/hooks/blocking/code_rules_duplicate_body.py +272 -100
- package/hooks/blocking/code_rules_enforcer.py +89 -45
- package/hooks/blocking/code_rules_shared.py +64 -14
- package/hooks/blocking/codex_apply_patch.py +136 -21
- package/hooks/blocking/config/codex_apply_patch_constants.py +13 -0
- package/hooks/blocking/conftest.py +70 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +26 -16
- package/hooks/blocking/hook_prose_detector_consistency.py +30 -20
- package/hooks/blocking/pii_payload_scan.py +61 -0
- package/hooks/blocking/pii_prevention_blocker.py +5 -0
- package/hooks/blocking/pr_description_writer_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +1 -1
- package/hooks/blocking/sensitive_file_protector.py +18 -3
- package/hooks/blocking/shell_substitution_blocker.py +25 -47
- package/hooks/blocking/stale_comment_reference_blocker.py +94 -24
- package/hooks/blocking/state_description_blocker.py +39 -26
- package/hooks/blocking/subprocess_budget_completeness.py +17 -43
- package/hooks/blocking/tdd_enforcer.py +63 -16
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +11 -1
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +180 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +6 -2
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +18 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +66 -1
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +143 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +12 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +63 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +81 -2
- package/hooks/blocking/test_code_rules_enforcer_codex_apply_patch.py +35 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +29 -16
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +12 -1
- package/hooks/blocking/test_code_rules_enforcer_ephemeral_repository_root.py +118 -0
- package/hooks/blocking/test_code_rules_enforcer_multi_edit.py +118 -0
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +244 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +29 -28
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +24 -0
- package/hooks/blocking/test_code_rules_enforcer_stage.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -0
- package/hooks/blocking/test_codex_apply_patch.py +43 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +68 -0
- package/hooks/blocking/test_hook_prose_detector_consistency.py +41 -0
- package/hooks/blocking/test_pii_payload_scan.py +65 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +80 -10
- package/hooks/blocking/test_pr_description_writer_gate.py +234 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +215 -25
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +18 -6
- package/hooks/blocking/test_sensitive_file_protector.py +42 -2
- package/hooks/blocking/test_stale_comment_reference_blocker.py +71 -0
- package/hooks/blocking/test_state_description_blocker.py +163 -49
- package/hooks/blocking/test_subprocess_budget_completeness.py +67 -0
- package/hooks/blocking/test_tdd_enforcer.py +246 -2
- package/hooks/blocking/test_windows_rmtree_blocker.py +63 -0
- package/hooks/blocking/test_write_existing_file_blocker.py +122 -0
- package/hooks/blocking/windows_rmtree_blocker.py +21 -7
- package/hooks/blocking/write_existing_file_blocker.py +75 -29
- package/hooks/conftest.py +47 -0
- package/hooks/hooks.json +7 -12
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +3 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +4 -0
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +3 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +7 -2
- package/hooks/hooks_constants/migration_safety_advisor_constants.py +7 -0
- package/hooks/hooks_constants/multi_edit_reconstruction.py +44 -3
- package/hooks/hooks_constants/pii_prevention_constants.py +9 -7
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +73 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +31 -14
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +15 -1
- package/hooks/hooks_constants/state_description_blocker_constants.py +1 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +71 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +2 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/test_multi_edit_reconstruction.py +103 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +139 -25
- package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +103 -0
- package/hooks/hooks_constants/validation_phase_constants.py +2 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +7 -3
- package/hooks/observability/pr_description_writer_spawn_tracker.py +113 -0
- package/hooks/observability/session_file_edit_tracker.py +25 -4
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +150 -0
- package/hooks/observability/test_session_file_edit_tracker.py +32 -0
- package/hooks/validation/hook_format_validator.py +23 -9
- package/hooks/validation/test_hook_format_validator.py +58 -0
- package/hooks/validators/README.md +1 -1
- package/hooks/validators/abbreviation_checks.py +6 -8
- package/hooks/validators/code_quality_checks.py +4 -4
- package/hooks/validators/config/abbreviation_checks_constants.py +3 -0
- package/hooks/validators/config/baseline_identity_constants.py +12 -0
- package/hooks/validators/config/directory_exemption_constants.py +44 -1
- package/hooks/validators/config/fast_save_validator_constants.py +32 -0
- package/hooks/validators/config/hook_timing_harness_constants.py +3 -1
- package/hooks/validators/config/test_abbreviation_checks_constants.py +11 -0
- package/hooks/validators/config/validator_base_constants.py +9 -0
- package/hooks/validators/fast_save_validators.py +275 -0
- package/hooks/validators/hook_timing_harness.py +8 -4
- package/hooks/validators/pr_reference_checks.py +2 -2
- package/hooks/validators/python_antipattern_checks.py +3 -3
- package/hooks/validators/python_style_checks.py +37 -62
- package/hooks/validators/python_style_helpers.py +16 -2
- package/hooks/validators/python_style_import_bootstrap.py +83 -0
- package/hooks/validators/react_checks.py +2 -9
- package/hooks/validators/run_all_validators.py +201 -155
- package/hooks/validators/security_checks.py +3 -3
- package/hooks/validators/test_abbreviation_checks.py +20 -0
- package/hooks/validators/test_directory_exemption_constants.py +30 -24
- package/hooks/validators/test_fast_save_validators.py +172 -0
- package/hooks/validators/test_hook_timing_harness.py +10 -1
- package/hooks/validators/test_python_style_checks.py +9 -0
- package/hooks/validators/test_python_style_checks_import_order.py +81 -0
- package/hooks/validators/test_python_style_helpers.py +35 -0
- package/hooks/validators/test_python_style_import_bootstrap.py +69 -0
- package/hooks/validators/test_run_all_validators.py +46 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +163 -0
- package/hooks/validators/test_run_all_validators_repository_root.py +202 -0
- package/hooks/validators/test_validator_base.py +51 -2
- package/hooks/validators/todo_checks.py +2 -2
- package/hooks/validators/type_safety_checks.py +3 -3
- package/hooks/validators/useless_test_checks.py +3 -3
- package/hooks/validators/validator_base.py +73 -1
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/git-workflow.md +2 -0
- package/rules/testing.md +4 -0
- package/scripts/codex_compat_materializer.py +5 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +15 -0
- package/scripts/grok_headless_runner.py +15 -12
- package/scripts/shared_tree_paths.py +70 -0
- package/scripts/test_shared_tree_paths.py +143 -0
- package/scripts/tests/test_codex_compat_materializer.py +37 -11
- package/hooks/blocking/tests/conftest.py +0 -10
|
@@ -19,6 +19,7 @@ from .run_all_validators import (
|
|
|
19
19
|
ValidatorResult,
|
|
20
20
|
_escapes_temporary_root,
|
|
21
21
|
_hooks_subprocess_working_directory_and_environment,
|
|
22
|
+
_scope_agnostic_message,
|
|
22
23
|
_scope_new_and_preexisting,
|
|
23
24
|
_temporary_path_preserving_directory_signal,
|
|
24
25
|
_violation_line_number,
|
|
@@ -26,11 +27,16 @@ from .run_all_validators import (
|
|
|
26
27
|
run_validators_entrypoint_subprocess,
|
|
27
28
|
)
|
|
28
29
|
|
|
30
|
+
pytestmark = pytest.mark.usefixtures("ephemeral_exempt_off")
|
|
31
|
+
|
|
29
32
|
CONFIG_DIR_TARGET_PATH = (
|
|
30
33
|
"CDP Automations/os_update_workflow/config/submission_constants.py"
|
|
31
34
|
)
|
|
32
35
|
PARENT_TRAVERSAL_TARGET_PATH = "../../escape_target.py"
|
|
33
36
|
RELATIVE_CONFIG_TARGET_PATH = "config/x.py"
|
|
37
|
+
PROJECT_ROOTED_PYTEST_SCRATCH_TARGET_PATH = (
|
|
38
|
+
"/srv/projects/demo/test_edit_introducing_new_viol0/legacy_module.py"
|
|
39
|
+
)
|
|
34
40
|
|
|
35
41
|
CLEAN_PYTHON_SOURCE = (
|
|
36
42
|
"def add_two_numbers(first_number: int, second_number: int) -> int:\n"
|
|
@@ -226,6 +232,38 @@ class TestTemporaryPathPreservingDirectorySignal:
|
|
|
226
232
|
)
|
|
227
233
|
assert staged_path == staging_root / "legacy_module.py"
|
|
228
234
|
|
|
235
|
+
def test_pytest_scratch_segment_stages_flat_outside_every_temporary_root(
|
|
236
|
+
self, tmp_path: Path
|
|
237
|
+
) -> None:
|
|
238
|
+
staging_root = tmp_path / "staging_root"
|
|
239
|
+
staging_root.mkdir()
|
|
240
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
241
|
+
staging_root, PROJECT_ROOTED_PYTEST_SCRATCH_TARGET_PATH
|
|
242
|
+
)
|
|
243
|
+
assert staged_path == staging_root / "legacy_module.py"
|
|
244
|
+
|
|
245
|
+
def test_test_helpers_directory_keeps_its_exemption_under_a_temporary_root(
|
|
246
|
+
self, tmp_path: Path
|
|
247
|
+
) -> None:
|
|
248
|
+
helpers_target = tmp_path / "pkg" / "test_helpers" / "worker.py"
|
|
249
|
+
staging_root = tmp_path / "staging_root"
|
|
250
|
+
staging_root.mkdir()
|
|
251
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
252
|
+
staging_root, str(helpers_target)
|
|
253
|
+
)
|
|
254
|
+
assert staged_path == staging_root / "test_helpers" / "worker.py"
|
|
255
|
+
|
|
256
|
+
def test_scripts_directory_keeps_its_exemption_under_a_temporary_root(
|
|
257
|
+
self, tmp_path: Path
|
|
258
|
+
) -> None:
|
|
259
|
+
scripts_target = tmp_path / "pkg" / "scripts" / "run_job.py"
|
|
260
|
+
staging_root = tmp_path / "staging_root"
|
|
261
|
+
staging_root.mkdir()
|
|
262
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
263
|
+
staging_root, str(scripts_target)
|
|
264
|
+
)
|
|
265
|
+
assert staged_path == staging_root / "scripts" / "run_job.py"
|
|
266
|
+
|
|
229
267
|
def test_relative_config_path_preserves_config_under_system_temp_cwd(
|
|
230
268
|
self, monkeypatch: pytest.MonkeyPatch
|
|
231
269
|
) -> None:
|
|
@@ -428,6 +466,100 @@ class TestBaselineScopedGate:
|
|
|
428
466
|
assert '"permissionDecision": "deny"' in completed.stdout
|
|
429
467
|
assert "Magic Values" in completed.stdout
|
|
430
468
|
|
|
469
|
+
def test_renaming_a_function_keeps_its_violation_grandfathered(
|
|
470
|
+
self, tmp_path: Path
|
|
471
|
+
) -> None:
|
|
472
|
+
target_file = tmp_path / "legacy_module.py"
|
|
473
|
+
target_file.write_text(
|
|
474
|
+
_over_long_function_source("over_long_function"), encoding="utf-8"
|
|
475
|
+
)
|
|
476
|
+
completed = run_gate(
|
|
477
|
+
{
|
|
478
|
+
"tool_name": "Edit",
|
|
479
|
+
"tool_input": {
|
|
480
|
+
"file_path": str(target_file),
|
|
481
|
+
"old_string": "def over_long_function(",
|
|
482
|
+
"new_string": "def accumulate_running_total(",
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
assert completed.returncode == 0, completed.stderr
|
|
487
|
+
assert '"permissionDecision": "deny"' not in completed.stdout
|
|
488
|
+
assert "Code Quality" in completed.stderr
|
|
489
|
+
|
|
490
|
+
def test_renaming_a_function_denies_a_second_violation_of_the_same_shape(
|
|
491
|
+
self, tmp_path: Path
|
|
492
|
+
) -> None:
|
|
493
|
+
target_file = tmp_path / "legacy_module.py"
|
|
494
|
+
target_file.write_text(
|
|
495
|
+
_over_long_function_source("over_long_function"), encoding="utf-8"
|
|
496
|
+
)
|
|
497
|
+
completed = run_gate(
|
|
498
|
+
{
|
|
499
|
+
"tool_name": "Edit",
|
|
500
|
+
"tool_input": {
|
|
501
|
+
"file_path": str(target_file),
|
|
502
|
+
"old_string": "def over_long_function(",
|
|
503
|
+
"new_string": (
|
|
504
|
+
_over_long_function_source("second_over_long_function")
|
|
505
|
+
+ "\n\n"
|
|
506
|
+
+ "def accumulate_running_total("
|
|
507
|
+
),
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
assert completed.returncode == 0, completed.stderr
|
|
512
|
+
assert '"permissionDecision": "deny"' in completed.stdout
|
|
513
|
+
assert "Code Quality" in completed.stdout
|
|
514
|
+
|
|
515
|
+
def test_renaming_a_function_denies_a_new_violation_of_another_validator(
|
|
516
|
+
self, tmp_path: Path
|
|
517
|
+
) -> None:
|
|
518
|
+
target_file = tmp_path / "legacy_module.py"
|
|
519
|
+
target_file.write_text(
|
|
520
|
+
_over_long_function_source("over_long_function"), encoding="utf-8"
|
|
521
|
+
)
|
|
522
|
+
completed = run_gate(
|
|
523
|
+
{
|
|
524
|
+
"tool_name": "Edit",
|
|
525
|
+
"tool_input": {
|
|
526
|
+
"file_path": str(target_file),
|
|
527
|
+
"old_string": "def over_long_function(",
|
|
528
|
+
"new_string": "import os\n\n\ndef accumulate_running_total(",
|
|
529
|
+
},
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
assert completed.returncode == 0, completed.stderr
|
|
533
|
+
assert '"permissionDecision": "deny"' in completed.stdout
|
|
534
|
+
assert "F401" in completed.stdout
|
|
535
|
+
|
|
536
|
+
def test_moving_a_violation_to_another_function_keeps_it_grandfathered(
|
|
537
|
+
self, tmp_path: Path
|
|
538
|
+
) -> None:
|
|
539
|
+
target_file = tmp_path / "legacy_module.py"
|
|
540
|
+
target_file.write_text(RUFF_DIRTY_FUNCTION_SOURCE, encoding="utf-8")
|
|
541
|
+
completed = run_gate(
|
|
542
|
+
{
|
|
543
|
+
"tool_name": "Edit",
|
|
544
|
+
"tool_input": {
|
|
545
|
+
"file_path": str(target_file),
|
|
546
|
+
"old_string": RUFF_DIRTY_FUNCTION_SOURCE,
|
|
547
|
+
"new_string": (
|
|
548
|
+
"def compute_total() -> int:\n"
|
|
549
|
+
" return 1\n"
|
|
550
|
+
"\n"
|
|
551
|
+
"\n"
|
|
552
|
+
"def compute_subtotal() -> int:\n"
|
|
553
|
+
" unused_intermediate = 1\n"
|
|
554
|
+
" return 1\n"
|
|
555
|
+
),
|
|
556
|
+
},
|
|
557
|
+
}
|
|
558
|
+
)
|
|
559
|
+
assert completed.returncode == 0, completed.stderr
|
|
560
|
+
assert '"permissionDecision": "deny"' not in completed.stdout
|
|
561
|
+
assert "F841" in completed.stderr
|
|
562
|
+
|
|
431
563
|
def test_new_module_scope_ruff_violation_with_dirty_function_denies(
|
|
432
564
|
self, tmp_path: Path
|
|
433
565
|
) -> None:
|
|
@@ -449,6 +581,37 @@ class TestBaselineScopedGate:
|
|
|
449
581
|
assert "F841" not in completed.stdout
|
|
450
582
|
|
|
451
583
|
|
|
584
|
+
class TestScopeAgnosticMessage:
|
|
585
|
+
def test_masks_the_enclosing_function_name_and_its_metrics(self) -> None:
|
|
586
|
+
masked_message = _scope_agnostic_message(
|
|
587
|
+
"over_long_function", "Function 'over_long_function' is 37 lines (max 30)"
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
assert masked_message == "Function '<name>' is <number> lines (max <number>)"
|
|
591
|
+
|
|
592
|
+
def test_keeps_the_numbers_of_a_message_that_never_names_its_function(self) -> None:
|
|
593
|
+
magic_value_message = "Magic number 199 - use named constant"
|
|
594
|
+
|
|
595
|
+
masked_message = _scope_agnostic_message("compute_total", magic_value_message)
|
|
596
|
+
|
|
597
|
+
assert masked_message == magic_value_message
|
|
598
|
+
|
|
599
|
+
def test_keeps_a_module_scope_message_unchanged(self) -> None:
|
|
600
|
+
ruff_message = "F401 `os` imported but unused"
|
|
601
|
+
|
|
602
|
+
masked_message = _scope_agnostic_message("", ruff_message)
|
|
603
|
+
|
|
604
|
+
assert masked_message == ruff_message
|
|
605
|
+
|
|
606
|
+
def test_masks_only_the_innermost_name_of_a_qualified_scope(self) -> None:
|
|
607
|
+
masked_message = _scope_agnostic_message(
|
|
608
|
+
"Ledger.record_entry",
|
|
609
|
+
"Parameter 'entry' in 'record_entry' missing type annotation",
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
assert masked_message == "Parameter 'entry' in '<name>' missing type annotation"
|
|
613
|
+
|
|
614
|
+
|
|
452
615
|
class TestScopeNewAndPreexisting:
|
|
453
616
|
def test_failed_result_without_located_lines_is_treated_as_new(self) -> None:
|
|
454
617
|
summary_only_result = ValidatorResult(
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Repository-root anchoring tests for the PreToolUse staging walk.
|
|
2
|
+
|
|
3
|
+
Staging mirrors the destination's exemption-signalling directories through a
|
|
4
|
+
temp path so the save-path checks read the same signals the real path carries.
|
|
5
|
+
The walk must read only the segments the project itself owns. A directory above
|
|
6
|
+
the project root — a home directory named ``tests``, a checkout parked under
|
|
7
|
+
``config`` — belongs to whoever laid out the disk, not to the project, and must
|
|
8
|
+
never lend a production file a test or config exemption.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from .run_all_validators import (
|
|
16
|
+
_temporary_path_preserving_directory_signal,
|
|
17
|
+
validate_proposed_file,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
pytestmark = pytest.mark.usefixtures("ephemeral_exempt_off")
|
|
21
|
+
|
|
22
|
+
GIT_MARKER_DIRECTORY_NAME = ".git"
|
|
23
|
+
MAGIC_VALUE_CHECK_NAME = "Magic Values"
|
|
24
|
+
PRODUCTION_CONTENT_WITH_MAGIC_VALUE = "def retry_budget_seconds() -> int:\n return 3600 * 7\n"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _project_root_under(parent_directory: Path) -> Path:
|
|
28
|
+
"""Create a git-marked project directory under *parent_directory*.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
parent_directory: Directory the project root is created inside.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The created project root, holding an empty ``.git`` marker directory.
|
|
35
|
+
"""
|
|
36
|
+
project_root = parent_directory / "project"
|
|
37
|
+
(project_root / GIT_MARKER_DIRECTORY_NAME).mkdir(parents=True)
|
|
38
|
+
return project_root
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _staged_relative_path(staging_directory: Path, destination_path: Path) -> str:
|
|
42
|
+
"""Return the posix staged path *destination_path* produces, relative to staging.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
staging_directory: Root of the ephemeral staging tree.
|
|
46
|
+
destination_path: Real destination the write or edit targets.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The staged path relative to *staging_directory*, in posix form.
|
|
50
|
+
"""
|
|
51
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
52
|
+
staging_directory, str(destination_path)
|
|
53
|
+
)
|
|
54
|
+
return staged_path.relative_to(staging_directory).as_posix()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _failed_check_names(destination_path: Path, proposed_content: str) -> list[str]:
|
|
58
|
+
"""Return the names of every save-path check that fails for a destination.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
destination_path: Real destination the write or edit targets.
|
|
62
|
+
proposed_content: Post-edit content the write would leave on disk.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The failing check names, in roster order.
|
|
66
|
+
"""
|
|
67
|
+
return [
|
|
68
|
+
each_result.name
|
|
69
|
+
for each_result in validate_proposed_file(str(destination_path), proposed_content)
|
|
70
|
+
if not each_result.passed
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_should_drop_tests_directory_above_the_project_root(tmp_path: Path) -> None:
|
|
75
|
+
"""A home directory named ``tests`` lends no test exemption to project code."""
|
|
76
|
+
parent_named_tests = tmp_path / "tests"
|
|
77
|
+
project_root = _project_root_under(parent_named_tests)
|
|
78
|
+
staging_directory = tmp_path / "staging"
|
|
79
|
+
staging_directory.mkdir()
|
|
80
|
+
|
|
81
|
+
staged_relative = _staged_relative_path(staging_directory, project_root / "src" / "worker.py")
|
|
82
|
+
|
|
83
|
+
assert staged_relative == "worker.py", (
|
|
84
|
+
f"a tests directory above the project must not stage into the path, got {staged_relative}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_should_drop_config_directory_above_the_project_root(tmp_path: Path) -> None:
|
|
89
|
+
"""A checkout parked under ``config`` lends no config exemption to project code."""
|
|
90
|
+
parent_named_config = tmp_path / "config"
|
|
91
|
+
project_root = _project_root_under(parent_named_config)
|
|
92
|
+
staging_directory = tmp_path / "staging"
|
|
93
|
+
staging_directory.mkdir()
|
|
94
|
+
|
|
95
|
+
staged_relative = _staged_relative_path(staging_directory, project_root / "src" / "worker.py")
|
|
96
|
+
|
|
97
|
+
assert staged_relative == "worker.py", (
|
|
98
|
+
f"a config directory above the project must not stage into the path, got {staged_relative}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_should_report_magic_value_for_production_file_under_a_tests_parent(
|
|
103
|
+
tmp_path: Path,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""The magic-value check grades project code under a ``tests`` parent directory."""
|
|
106
|
+
project_root = _project_root_under(tmp_path / "tests")
|
|
107
|
+
|
|
108
|
+
all_failed_names = _failed_check_names(
|
|
109
|
+
project_root / "src" / "worker.py", PRODUCTION_CONTENT_WITH_MAGIC_VALUE
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
assert MAGIC_VALUE_CHECK_NAME in all_failed_names, (
|
|
113
|
+
f"production code under a tests parent must still be graded, got {all_failed_names}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_should_report_magic_value_for_production_file_under_a_config_parent(
|
|
118
|
+
tmp_path: Path,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""The magic-value check grades project code under a ``config`` parent directory."""
|
|
121
|
+
project_root = _project_root_under(tmp_path / "config")
|
|
122
|
+
|
|
123
|
+
all_failed_names = _failed_check_names(
|
|
124
|
+
project_root / "src" / "worker.py", PRODUCTION_CONTENT_WITH_MAGIC_VALUE
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
assert MAGIC_VALUE_CHECK_NAME in all_failed_names, (
|
|
128
|
+
f"production code under a config parent must still be graded, got {all_failed_names}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_should_keep_test_helpers_directory_inside_the_project(tmp_path: Path) -> None:
|
|
133
|
+
"""A ``test_helpers`` package inside the project keeps its test exemption."""
|
|
134
|
+
project_root = _project_root_under(tmp_path / "tests")
|
|
135
|
+
staging_directory = tmp_path / "staging"
|
|
136
|
+
staging_directory.mkdir()
|
|
137
|
+
|
|
138
|
+
staged_relative = _staged_relative_path(
|
|
139
|
+
staging_directory, project_root / "pkg" / "test_helpers" / "worker.py"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
assert staged_relative == "test_helpers/worker.py", (
|
|
143
|
+
f"an in-project test_helpers directory must stage into the path, got {staged_relative}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_should_keep_scripts_directory_inside_the_project(tmp_path: Path) -> None:
|
|
148
|
+
"""A ``scripts`` directory inside the project keeps its CLI-marker signal."""
|
|
149
|
+
project_root = _project_root_under(tmp_path / "tests")
|
|
150
|
+
staging_directory = tmp_path / "staging"
|
|
151
|
+
staging_directory.mkdir()
|
|
152
|
+
|
|
153
|
+
staged_relative = _staged_relative_path(
|
|
154
|
+
staging_directory, project_root / "scripts" / "deploy.py"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
assert staged_relative == "scripts/deploy.py", (
|
|
158
|
+
f"an in-project scripts directory must stage into the path, got {staged_relative}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_should_keep_tests_directory_inside_the_project(tmp_path: Path) -> None:
|
|
163
|
+
"""A genuine ``tests`` directory inside the project keeps its test exemption."""
|
|
164
|
+
project_root = _project_root_under(tmp_path / "config")
|
|
165
|
+
staging_directory = tmp_path / "staging"
|
|
166
|
+
staging_directory.mkdir()
|
|
167
|
+
|
|
168
|
+
staged_relative = _staged_relative_path(
|
|
169
|
+
staging_directory, project_root / "tests" / "helpers" / "data.py"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
assert staged_relative == "tests/helpers/data.py", (
|
|
173
|
+
f"an in-project tests directory must stage into the path, got {staged_relative}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_should_keep_config_directory_inside_the_project(tmp_path: Path) -> None:
|
|
178
|
+
"""A ``config`` directory inside the project keeps its magic-value exemption."""
|
|
179
|
+
project_root = _project_root_under(tmp_path / "tests")
|
|
180
|
+
|
|
181
|
+
all_failed_names = _failed_check_names(
|
|
182
|
+
project_root / "config" / "timing.py", PRODUCTION_CONTENT_WITH_MAGIC_VALUE
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
assert MAGIC_VALUE_CHECK_NAME not in all_failed_names, (
|
|
186
|
+
f"an in-project config module must stay magic-value exempt, got {all_failed_names}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_should_walk_every_segment_for_a_target_with_no_project_root(
|
|
191
|
+
tmp_path: Path,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""An install-tree target outside every project keeps the whole-path walk."""
|
|
194
|
+
install_target = tmp_path / ".claude" / "hooks" / "blocking" / "gate.py"
|
|
195
|
+
staging_directory = tmp_path / "staging"
|
|
196
|
+
staging_directory.mkdir()
|
|
197
|
+
|
|
198
|
+
staged_relative = _staged_relative_path(staging_directory, install_target)
|
|
199
|
+
|
|
200
|
+
assert staged_relative == ".claude/hooks/blocking/gate.py", (
|
|
201
|
+
f"a target with no project root must keep its exemption tail, got {staged_relative}"
|
|
202
|
+
)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
"""Tests for shared validator base types."""
|
|
1
|
+
"""Tests for shared validator base types and its shared read and parse."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
2
5
|
|
|
3
6
|
import pytest
|
|
4
7
|
|
|
5
|
-
from .validator_base import Violation
|
|
8
|
+
from .validator_base import Violation, source_text, syntax_tree
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
class TestViolation:
|
|
@@ -24,3 +27,49 @@ class TestViolation:
|
|
|
24
27
|
violation = Violation(file="test.py", line=42, message="Test message")
|
|
25
28
|
violation_set = {violation}
|
|
26
29
|
assert violation in violation_set
|
|
30
|
+
|
|
31
|
+
def test_source_text_reads_the_file_once_for_repeated_callers(tmp_path: Path) -> None:
|
|
32
|
+
"""Two callers asking for the same unchanged file cause one read."""
|
|
33
|
+
target = tmp_path / "module.py"
|
|
34
|
+
target.write_text("value = 1\n", encoding="utf-8")
|
|
35
|
+
first = source_text(target)
|
|
36
|
+
second = source_text(target)
|
|
37
|
+
assert first == second == "value = 1\n"
|
|
38
|
+
assert source_text.__module__ == "validators.validator_base"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_source_text_reads_again_after_the_file_changes(tmp_path: Path) -> None:
|
|
42
|
+
"""An edited file is read again rather than served from the cache.
|
|
43
|
+
|
|
44
|
+
The cache key carries the modification time and the size, so a rewrite
|
|
45
|
+
between two calls reaches disk a second time.
|
|
46
|
+
"""
|
|
47
|
+
target = tmp_path / "module.py"
|
|
48
|
+
target.write_text("value = 1\n", encoding="utf-8")
|
|
49
|
+
assert source_text(target) == "value = 1\n"
|
|
50
|
+
target.write_text("value = 2\nvalue = 3\n", encoding="utf-8")
|
|
51
|
+
assert source_text(target) == "value = 2\nvalue = 3\n"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_source_text_raises_for_a_missing_file(tmp_path: Path) -> None:
|
|
55
|
+
"""A caller's own error handling still sees the read failure."""
|
|
56
|
+
with pytest.raises(OSError):
|
|
57
|
+
source_text(tmp_path / "absent.py")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_syntax_tree_returns_the_same_tree_for_the_same_source() -> None:
|
|
61
|
+
"""One parse serves every caller asking about the same text."""
|
|
62
|
+
source = "def handler() -> int:\n return 1\n"
|
|
63
|
+
assert syntax_tree(source) is syntax_tree(source)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_syntax_tree_parses_what_ast_parse_parses() -> None:
|
|
67
|
+
"""The shared parse agrees with a direct ast.parse of the same text."""
|
|
68
|
+
source = "value = 1\n"
|
|
69
|
+
assert ast.dump(syntax_tree(source)) == ast.dump(ast.parse(source))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_syntax_tree_raises_syntax_error_for_unparseable_source() -> None:
|
|
73
|
+
"""A caller's own SyntaxError branch still fires."""
|
|
74
|
+
with pytest.raises(SyntaxError):
|
|
75
|
+
syntax_tree("def broken(:\n")
|
|
@@ -8,7 +8,7 @@ import sys
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import List
|
|
10
10
|
|
|
11
|
-
from .validator_base import Violation
|
|
11
|
+
from .validator_base import Violation, source_text
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
TODO_PATTERN = re.compile(r"#\s*(TODO|FIXME)\b(?!.*#\d+)", re.IGNORECASE)
|
|
@@ -34,7 +34,7 @@ def check_untracked_todos(source: str, filename: str) -> List[Violation]:
|
|
|
34
34
|
def validate_file(file_path: Path) -> List[Violation]:
|
|
35
35
|
filename = str(file_path)
|
|
36
36
|
try:
|
|
37
|
-
source = file_path
|
|
37
|
+
source = source_text(file_path)
|
|
38
38
|
except Exception as error:
|
|
39
39
|
return [Violation(filename, 0, f"Error reading file: {error}")]
|
|
40
40
|
|
|
@@ -10,7 +10,7 @@ import sys
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import List
|
|
12
12
|
|
|
13
|
-
from .validator_base import Violation
|
|
13
|
+
from .validator_base import Violation, source_text, syntax_tree
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def check_missing_type_hints(tree: ast.AST, filename: str) -> List[Violation]:
|
|
@@ -66,8 +66,8 @@ def validate_file(file_path: Path) -> List[Violation]:
|
|
|
66
66
|
filename = str(file_path)
|
|
67
67
|
|
|
68
68
|
try:
|
|
69
|
-
source = file_path
|
|
70
|
-
tree =
|
|
69
|
+
source = source_text(file_path)
|
|
70
|
+
tree = syntax_tree(source)
|
|
71
71
|
except Exception as error:
|
|
72
72
|
return [Violation(filename, 0, f"Error: {error}")]
|
|
73
73
|
|
|
@@ -9,7 +9,7 @@ import sys
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import List
|
|
11
11
|
|
|
12
|
-
from .validator_base import Violation
|
|
12
|
+
from .validator_base import Violation, source_text, syntax_tree
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def check_useless_tests(tree: ast.AST, filename: str) -> List[Violation]:
|
|
@@ -66,8 +66,8 @@ def validate_file(file_path: Path) -> List[Violation]:
|
|
|
66
66
|
return []
|
|
67
67
|
|
|
68
68
|
try:
|
|
69
|
-
source = file_path
|
|
70
|
-
tree =
|
|
69
|
+
source = source_text(file_path)
|
|
70
|
+
tree = syntax_tree(source)
|
|
71
71
|
except Exception as error:
|
|
72
72
|
return [Violation(filename, 0, f"Error: {error}")]
|
|
73
73
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"""Base classes for validators.
|
|
2
2
|
|
|
3
|
-
Provides shared dataclasses used across all validator modules
|
|
3
|
+
Provides shared dataclasses used across all validator modules, and the one
|
|
4
|
+
read and the one parse each of those modules shares for a given file.
|
|
4
5
|
"""
|
|
5
6
|
|
|
7
|
+
import ast
|
|
6
8
|
from dataclasses import dataclass
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .config.validator_base_constants import SOURCE_CACHE_ENTRIES, SOURCE_ENCODING
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
@dataclass(frozen=True)
|
|
@@ -17,3 +23,69 @@ class Violation:
|
|
|
17
23
|
def __str__(self) -> str:
|
|
18
24
|
"""Format as file:line: message."""
|
|
19
25
|
return f"{self.file}:{self.line}: {self.message}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ValidatorResult:
|
|
30
|
+
"""Result from running a validator."""
|
|
31
|
+
|
|
32
|
+
name: str
|
|
33
|
+
checks: str
|
|
34
|
+
passed: bool
|
|
35
|
+
output: str
|
|
36
|
+
skipped: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@lru_cache(maxsize=SOURCE_CACHE_ENTRIES)
|
|
40
|
+
def _source_text_for_stat(
|
|
41
|
+
file_path_text: str, modified_nanoseconds: int, size_in_bytes: int
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Read one file, keyed on the stat that identifies this version of it."""
|
|
44
|
+
return Path(file_path_text).read_text(encoding=SOURCE_ENCODING)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def source_text(file_path: Path) -> str:
|
|
48
|
+
"""Return the file's text, reading it once however many checks ask.
|
|
49
|
+
|
|
50
|
+
Every check the save-path gate runs reads the same file. The gate hands
|
|
51
|
+
each of them one freshly written target, so the read is repeated work.
|
|
52
|
+
The cache key carries the modification time and size, so an edited file
|
|
53
|
+
is read again.
|
|
54
|
+
|
|
55
|
+
That key has one blind spot: a rewrite that leaves the size unchanged
|
|
56
|
+
inside a single modification-time tick is served from the cache. A hook
|
|
57
|
+
process handles one payload, so it cannot happen there; a long CLI pass
|
|
58
|
+
over many files is where a reader needs to know.
|
|
59
|
+
|
|
60
|
+
Raises whatever ``read_text`` raises, so a caller's own error handling
|
|
61
|
+
is unchanged.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
file_path: The file to read.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The file's decoded text.
|
|
68
|
+
"""
|
|
69
|
+
file_status = file_path.stat()
|
|
70
|
+
return _source_text_for_stat(
|
|
71
|
+
str(file_path), file_status.st_mtime_ns, file_status.st_size
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@lru_cache(maxsize=SOURCE_CACHE_ENTRIES)
|
|
76
|
+
def syntax_tree(source: str) -> ast.Module:
|
|
77
|
+
"""Return the parsed tree for source, parsing it once however many checks ask.
|
|
78
|
+
|
|
79
|
+
No check mutates the tree; each one walks it. So one parse serves all of
|
|
80
|
+
them, and the tree is shared rather than copied.
|
|
81
|
+
|
|
82
|
+
Raises ``SyntaxError`` exactly as ``ast.parse`` does, so a caller's own
|
|
83
|
+
error handling is unchanged.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
source: The file's text.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The parsed module.
|
|
90
|
+
"""
|
|
91
|
+
return ast.parse(source)
|
package/package.json
CHANGED
|
@@ -34,7 +34,7 @@ Many deterministic shapes of this drift have Write/Edit gates in `~/.claude/hook
|
|
|
34
34
|
|
|
35
35
|
A hook module is the sharpest case of the same rule: its docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags, and claim no broader trigger surface than the regex enforces.
|
|
36
36
|
|
|
37
|
-
`hook_prose_detector_consistency` (PreToolUse on Write|Edit of hook modules and their `*_constants.py` companions) blocks prose that claims a trigger the detector never fires on, and names the fix.
|
|
37
|
+
`hook_prose_detector_consistency` (PreToolUse on Write|Edit|MultiEdit of hook modules and their `*_constants.py` companions) blocks prose that claims a trigger the detector never fires on, and names the fix.
|
|
38
38
|
|
|
39
39
|
After writing a hook, ask: would a token matching every word of this message actually trip the detector? When the message names a shape the regex skips, rewrite the message to name only what the regex catches. The path-shape case is the common overstatement — a detector that keys off a path separator must not claim it blocks an "output-key segment". The corrective message spells the rewrite.
|
|
40
40
|
|
package/rules/git-workflow.md
CHANGED
|
@@ -17,6 +17,8 @@ User-level rule: applies to **every** git repo that uses GitHub with `gh` (no ex
|
|
|
17
17
|
|
|
18
18
|
**ALWAYS create PRs as DRAFT:** Use `gh pr create --draft` for ALL PRs
|
|
19
19
|
|
|
20
|
+
**The pr-description-writer agent writes the body:** Spawn the `pr-description-writer` agent (Agent tool, `subagent_type` `pr-description-writer`), then publish what it returns with `--body-file`. `pr_description_writer_gate.py` (PreToolUse on Bash and PowerShell, hosted by `bash_pre_tool_use_dispatcher`) denies a `gh pr create` when the session records no such spawn. A trailing `# pr-description-skip` comment on the command opts out for a body you write yourself.
|
|
21
|
+
|
|
20
22
|
## Git Golden Rules (NON-NEGOTIABLE)
|
|
21
23
|
|
|
22
24
|
1. **DRAFT BEFORE PUSH**: When pushing ANYTHING to a PR, it MUST be in draft state first
|
package/rules/testing.md
CHANGED
|
@@ -22,3 +22,7 @@ Incomplete mocks make it impossible to distinguish "broken code" from "missing d
|
|
|
22
22
|
## Tests Exercise Real Behavior
|
|
23
23
|
|
|
24
24
|
Tests exercise real behavior, real data, and production code paths. A test that asserts on a stand-in for the production path proves the stand-in works.
|
|
25
|
+
|
|
26
|
+
## The File-Level TDD Gate Reads Content
|
|
27
|
+
|
|
28
|
+
`tdd_enforcer.py` records each candidate test's content hash at every sighting and compares that hash on the next write. A first sighting requires content that differs from HEAD.
|
|
@@ -64,6 +64,8 @@ codex_hook_dependency_manifest = (
|
|
|
64
64
|
"hooks/blocking/code_rules_comments.py",
|
|
65
65
|
"hooks/blocking/code_rules_constants_config.py",
|
|
66
66
|
"hooks/blocking/codex_apply_patch.py",
|
|
67
|
+
"hooks/blocking/config/__init__.py",
|
|
68
|
+
"hooks/blocking/config/codex_apply_patch_constants.py",
|
|
67
69
|
"hooks/blocking/code_rules_docstrings.py",
|
|
68
70
|
"hooks/blocking/code_rules_duplicate_body.py",
|
|
69
71
|
"hooks/blocking/code_rules_enforcer.py",
|
|
@@ -101,14 +103,17 @@ codex_hook_dependency_manifest = (
|
|
|
101
103
|
"hooks/hooks_constants/hook_block_logger.py",
|
|
102
104
|
"hooks/hooks_constants/inline_tuple_string_magic_constants.py",
|
|
103
105
|
"hooks/hooks_constants/js_conventions_constants.py",
|
|
106
|
+
"hooks/hooks_constants/multi_edit_reconstruction.py",
|
|
104
107
|
"hooks/hooks_constants/orphan_css_class_constants.py",
|
|
105
108
|
"hooks/hooks_constants/paired_test_coverage_constants.py",
|
|
106
109
|
"hooks/hooks_constants/setup_project_paths_constants.py",
|
|
107
110
|
"hooks/hooks_constants/stuttering_check_config.py",
|
|
108
111
|
"hooks/hooks_constants/stuttering_import_binding_constants.py",
|
|
112
|
+
"hooks/hooks_constants/subprocess_budget_completeness_content.py",
|
|
109
113
|
"hooks/hooks_constants/sys_path_insert_constants.py",
|
|
110
114
|
"hooks/hooks_constants/test_layout_constants.py",
|
|
111
115
|
"hooks/hooks_constants/unused_module_import_constants.py",
|
|
116
|
+
"hooks/hooks_constants/validation_phase_constants.py",
|
|
112
117
|
)
|
|
113
118
|
full_prune_opt_in_flag = "--allow-prune-all"
|
|
114
119
|
unreadable_source_root_message = (
|