claude-dev-env 2.31.0 → 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/docs/CODE_RULES.md +2 -1
- package/hooks/blocking/code_rules_duplicate_body.py +272 -100
- package/hooks/blocking/code_rules_enforcer.py +32 -14
- package/hooks/blocking/code_rules_shared.py +64 -14
- package/hooks/blocking/pr_description_writer_gate.py +18 -25
- package/hooks/blocking/tdd_enforcer.py +8 -4
- 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_same_file_inline_duplicate.py +244 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +22 -0
- package/hooks/blocking/test_pr_description_writer_gate.py +21 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +9 -2
- package/hooks/blocking/test_state_description_blocker.py +99 -53
- package/hooks/blocking/test_tdd_enforcer.py +64 -0
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
- package/hooks/hooks_constants/test_multi_edit_reconstruction.py +1 -0
- package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +33 -0
- package/hooks/observability/pr_description_writer_spawn_tracker.py +4 -12
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +6 -2
- package/hooks/validators/abbreviation_checks.py +3 -3
- package/hooks/validators/code_quality_checks.py +4 -4
- package/hooks/validators/config/baseline_identity_constants.py +12 -0
- package/hooks/validators/config/directory_exemption_constants.py +44 -1
- package/hooks/validators/config/validator_base_constants.py +9 -0
- package/hooks/validators/fast_save_validators.py +23 -80
- 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 +3 -2
- package/hooks/validators/react_checks.py +2 -9
- package/hooks/validators/run_all_validators.py +190 -145
- package/hooks/validators/security_checks.py +3 -3
- package/hooks/validators/test_directory_exemption_constants.py +30 -24
- package/hooks/validators/test_fast_save_validators.py +16 -16
- package/hooks/validators/test_hook_timing_harness.py +10 -1
- package/hooks/validators/test_run_all_validators_pretooluse.py +161 -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/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/docs/CODE_RULES.md
CHANGED
|
@@ -126,7 +126,8 @@ Scope assigns the lane. A check that reads a file other than the target runs on
|
|
|
126
126
|
|
|
127
127
|
Hook-infrastructure targets run three checks in the edit lane — `check_same_file_inline_duplicate_body`, `check_zero_payload_function_alias`, `check_unanchored_command_dispatch` — and the whole roster on the full gate. `ALL_HOOK_INFRASTRUCTURE_EDIT_LANE_CHECK_NAMES` holds that set.
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
Three surfaces report on the roster, and each reports a specific thing:
|
|
130
130
|
|
|
131
131
|
- `hooks/validators/hook_timing_harness.py` builds a `Write` payload against a target that already holds content. `_contents_for_validation` returns `None` for that payload, so the harness times interpreter start and hook dispatch. Time an `Edit` payload against a real file to measure the checks.
|
|
132
132
|
- `~/.claude/logs/hook-blocks.log` records the denials raised by fixtures in `test_code_rules_enforcer_*.py` and by the timing harness's default target.
|
|
133
|
+
- `hooks/validators/run_all_validators.py` suppresses exemption-directory substring matching for a target under a system temporary root, so pytest's `test_<name>0` parents stay scaffolding. A pytest `--basetemp` outside those roots restores the matching, and the staged copy takes a test-file exemption. Run this suite under the default temporary root.
|
|
@@ -54,6 +54,7 @@ import ast
|
|
|
54
54
|
import copy
|
|
55
55
|
import sys
|
|
56
56
|
from pathlib import Path
|
|
57
|
+
from typing import NamedTuple
|
|
57
58
|
|
|
58
59
|
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
59
60
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
@@ -84,6 +85,21 @@ from hooks_constants.duplicate_function_body_constants import ( # noqa: E402
|
|
|
84
85
|
)
|
|
85
86
|
|
|
86
87
|
|
|
88
|
+
_ModuleScopeFunction = ast.FunctionDef | ast.AsyncFunctionDef
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _FunctionScanProfile(NamedTuple):
|
|
92
|
+
"""The three readings of one module-scope function the inline scan compares.
|
|
93
|
+
|
|
94
|
+
Every reading is a property of that function alone, so each is taken once per
|
|
95
|
+
file and read by each pairing the scan weighs.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
reachable_statement_count: int
|
|
99
|
+
all_sorted_body_dumps: list[str]
|
|
100
|
+
all_block_dumps: list[list[str]]
|
|
101
|
+
|
|
102
|
+
|
|
87
103
|
def _body_statements_without_docstring(
|
|
88
104
|
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
89
105
|
) -> list[ast.stmt]:
|
|
@@ -384,6 +400,37 @@ def _normalized_statement_dump(statement: ast.stmt) -> str:
|
|
|
384
400
|
return ast.dump(canonical_statement, annotate_fields=False)
|
|
385
401
|
|
|
386
402
|
|
|
403
|
+
def _memoized_statement_dump(
|
|
404
|
+
statement: ast.stmt,
|
|
405
|
+
dump_by_statement: dict[ast.stmt, str],
|
|
406
|
+
) -> str:
|
|
407
|
+
"""Return the normalized dump of one statement, computing it at most once.
|
|
408
|
+
|
|
409
|
+
The inline scan reads the same statement's dump many times over: once for its
|
|
410
|
+
enclosing function's block window, once for that function's twin-test
|
|
411
|
+
multiset, and again for every helper the function is weighed against. Dumping
|
|
412
|
+
deep-copies the statement subtree before canonicalizing string constants, so
|
|
413
|
+
this memo serves the repeat reads.
|
|
414
|
+
|
|
415
|
+
An ``ast`` node defines no ``__eq__``, so the key is the node's identity and
|
|
416
|
+
the dict holds a strong reference to it: a key cannot be freed and a later
|
|
417
|
+
node cannot reuse its slot. The caller owns one memo per scanned file and
|
|
418
|
+
drops it when the scan returns, so no entry outlives its parse tree.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
statement: The statement node to fingerprint.
|
|
422
|
+
dump_by_statement: The file's statement-to-dump memo, mutated in place.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
The value ``_normalized_statement_dump`` returns for this statement.
|
|
426
|
+
"""
|
|
427
|
+
memoized_dump = dump_by_statement.get(statement)
|
|
428
|
+
if memoized_dump is None:
|
|
429
|
+
memoized_dump = _normalized_statement_dump(statement)
|
|
430
|
+
dump_by_statement[statement] = memoized_dump
|
|
431
|
+
return memoized_dump
|
|
432
|
+
|
|
433
|
+
|
|
387
434
|
def _statement_blocks_in_function(
|
|
388
435
|
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
389
436
|
) -> list[list[ast.stmt]]:
|
|
@@ -443,76 +490,6 @@ def _total_reachable_statement_count(
|
|
|
443
490
|
return total_statement_count
|
|
444
491
|
|
|
445
492
|
|
|
446
|
-
def _normalized_body_dump_multiset(
|
|
447
|
-
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
448
|
-
) -> list[str]:
|
|
449
|
-
"""Return the function's docstring-stripped statement dumps, sorted.
|
|
450
|
-
|
|
451
|
-
Used to test whether two functions are structural twins — same statements in
|
|
452
|
-
any arrangement under string normalization. Sorting makes the comparison
|
|
453
|
-
order-independent, so two peer helpers that read different inputs but share
|
|
454
|
-
the same statement shapes compare equal and are left to the cross-file check.
|
|
455
|
-
|
|
456
|
-
Args:
|
|
457
|
-
function_node: The function whose body to fingerprint.
|
|
458
|
-
|
|
459
|
-
Returns:
|
|
460
|
-
The sorted per-statement normalized dumps of the docstring-stripped body.
|
|
461
|
-
"""
|
|
462
|
-
body_statements = _body_statements_without_docstring(function_node)
|
|
463
|
-
return sorted(_normalized_statement_dump(each) for each in body_statements)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
def _function_inlines_window(
|
|
467
|
-
helper_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
468
|
-
enclosing_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
469
|
-
all_helper_window_dumps: list[str],
|
|
470
|
-
) -> bool:
|
|
471
|
-
"""Return whether a function inlines a helper window inside a larger body.
|
|
472
|
-
|
|
473
|
-
Slides a window the length of ``all_helper_window_dumps`` over each reachable
|
|
474
|
-
statement block in the enclosing function; a window whose per-statement dumps
|
|
475
|
-
match the helper's, in order, is the inlined copy. Two guards keep the report
|
|
476
|
-
to the genuine "helper inlined inside a larger function" shape:
|
|
477
|
-
|
|
478
|
-
- The enclosing function carries more total reachable statements than the
|
|
479
|
-
window, so a copy that fills a whole peer function is not reported here. The
|
|
480
|
-
count spans every nested block, so a window wrapped inside one top-level
|
|
481
|
-
compound — a ``try``/``finally`` cleanup, an ``except`` handler, or a single
|
|
482
|
-
``if`` guard — still clears the guard and is scanned.
|
|
483
|
-
- The two functions are not structural twins — their docstring-stripped
|
|
484
|
-
statement multisets differ — so two peer helpers that share a statement
|
|
485
|
-
shape but read different inputs are left to the cross-file whole-function
|
|
486
|
-
check ``check_duplicate_function_body_across_files`` rather than reported as
|
|
487
|
-
an inline duplicate.
|
|
488
|
-
|
|
489
|
-
Args:
|
|
490
|
-
helper_node: The candidate helper whose window is sought.
|
|
491
|
-
enclosing_node: The candidate enclosing function to scan.
|
|
492
|
-
all_helper_window_dumps: The helper's substantive-block per-statement dumps.
|
|
493
|
-
|
|
494
|
-
Returns:
|
|
495
|
-
True when some block in the enclosing function contains the helper window
|
|
496
|
-
verbatim as a contiguous run inside a strictly larger, non-twin body.
|
|
497
|
-
"""
|
|
498
|
-
window_length = len(all_helper_window_dumps)
|
|
499
|
-
if _total_reachable_statement_count(enclosing_node) <= window_length:
|
|
500
|
-
return False
|
|
501
|
-
if _normalized_body_dump_multiset(helper_node) == _normalized_body_dump_multiset(
|
|
502
|
-
enclosing_node
|
|
503
|
-
):
|
|
504
|
-
return False
|
|
505
|
-
for each_block in _statement_blocks_in_function(enclosing_node):
|
|
506
|
-
if len(each_block) < window_length:
|
|
507
|
-
continue
|
|
508
|
-
block_dumps = [_normalized_statement_dump(each) for each in each_block]
|
|
509
|
-
for each_start_index in range(len(block_dumps) - window_length + 1):
|
|
510
|
-
window = block_dumps[each_start_index : each_start_index + window_length]
|
|
511
|
-
if window == all_helper_window_dumps:
|
|
512
|
-
return True
|
|
513
|
-
return False
|
|
514
|
-
|
|
515
|
-
|
|
516
493
|
def _helper_match_window_dumps(
|
|
517
494
|
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
518
495
|
) -> list[str] | None:
|
|
@@ -616,40 +593,235 @@ def check_same_file_inline_duplicate_body(
|
|
|
616
593
|
for each_node in tree.body
|
|
617
594
|
if isinstance(each_node, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
618
595
|
]
|
|
596
|
+
return _scope_violations_to_changed_lines(
|
|
597
|
+
_inline_duplicate_violations(all_top_level_functions, MAX_DUPLICATE_BODY_ISSUES),
|
|
598
|
+
all_changed_lines,
|
|
599
|
+
defer_scope_to_caller,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _block_dumps_in_function(
|
|
604
|
+
function_node: _ModuleScopeFunction,
|
|
605
|
+
dump_by_statement: dict[ast.stmt, str],
|
|
606
|
+
) -> list[list[str]]:
|
|
607
|
+
"""Return the per-statement dumps of each reachable block in a function.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
function_node: The module-scope function to read.
|
|
611
|
+
dump_by_statement: The file's statement-to-dump memo, mutated in place.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
One dump list per reachable block, in block-collection order.
|
|
615
|
+
"""
|
|
616
|
+
return [
|
|
617
|
+
[
|
|
618
|
+
_memoized_statement_dump(each_statement, dump_by_statement)
|
|
619
|
+
for each_statement in each_block
|
|
620
|
+
]
|
|
621
|
+
for each_block in _statement_blocks_in_function(function_node)
|
|
622
|
+
]
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _function_scan_profile(
|
|
626
|
+
function_node: _ModuleScopeFunction,
|
|
627
|
+
dump_by_statement: dict[ast.stmt, str],
|
|
628
|
+
) -> _FunctionScanProfile:
|
|
629
|
+
"""Take the three readings the inline scan needs from one function.
|
|
630
|
+
|
|
631
|
+
The sorted body dumps carry the docstring-stripped statement multiset that
|
|
632
|
+
tests whether two functions are structural twins. Sorting makes that
|
|
633
|
+
comparison order-independent, so two peer helpers that read different inputs
|
|
634
|
+
but share the same statement shapes compare equal and are left to the
|
|
635
|
+
cross-file check.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
function_node: The module-scope function to read.
|
|
639
|
+
dump_by_statement: The file's statement-to-dump memo, mutated in place.
|
|
640
|
+
|
|
641
|
+
Returns:
|
|
642
|
+
The function's reachable statement count, sorted body dumps, and
|
|
643
|
+
per-block statement dumps.
|
|
644
|
+
"""
|
|
645
|
+
return _FunctionScanProfile(
|
|
646
|
+
reachable_statement_count=_total_reachable_statement_count(function_node),
|
|
647
|
+
all_sorted_body_dumps=sorted(
|
|
648
|
+
_memoized_statement_dump(each_statement, dump_by_statement)
|
|
649
|
+
for each_statement in _body_statements_without_docstring(function_node)
|
|
650
|
+
),
|
|
651
|
+
all_block_dumps=_block_dumps_in_function(function_node, dump_by_statement),
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _block_dumps_contain_window(
|
|
656
|
+
all_block_dumps: list[str],
|
|
657
|
+
all_window_dumps: list[str],
|
|
658
|
+
) -> bool:
|
|
659
|
+
"""Return whether one block carries the window as a contiguous run.
|
|
660
|
+
|
|
661
|
+
A block shorter than the window holds no run and answers False.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
all_block_dumps: One reachable block's per-statement dumps.
|
|
665
|
+
all_window_dumps: The helper's window dumps to seek.
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
True when the window appears in the block as a contiguous run.
|
|
669
|
+
"""
|
|
670
|
+
window_length = len(all_window_dumps)
|
|
671
|
+
last_start_index = len(all_block_dumps) - window_length
|
|
672
|
+
for each_start_index in range(last_start_index + 1):
|
|
673
|
+
window_end = each_start_index + window_length
|
|
674
|
+
if all_block_dumps[each_start_index:window_end] == all_window_dumps:
|
|
675
|
+
return True
|
|
676
|
+
return False
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _profile_inlines_window(
|
|
680
|
+
helper_profile: _FunctionScanProfile,
|
|
681
|
+
enclosing_profile: _FunctionScanProfile,
|
|
682
|
+
all_helper_window_dumps: list[str],
|
|
683
|
+
) -> bool:
|
|
684
|
+
"""Return whether a function inlines a helper window inside a larger body.
|
|
685
|
+
|
|
686
|
+
A run inside one of the enclosing function's reachable blocks whose dumps
|
|
687
|
+
match the helper's, in order, is the inlined copy. Two guards hold the report
|
|
688
|
+
to a genuine inlining: the enclosing function carries more reachable
|
|
689
|
+
statements than the window, and the two functions are not structural twins,
|
|
690
|
+
so peer helpers sharing a statement shape are left to the cross-file check.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
helper_profile: The candidate helper's readings, for the twin guard.
|
|
694
|
+
enclosing_profile: The candidate enclosing function's readings.
|
|
695
|
+
all_helper_window_dumps: The helper's substantive-block statement dumps.
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
True when a block carries the window inside a larger, non-twin body.
|
|
699
|
+
"""
|
|
700
|
+
if enclosing_profile.reachable_statement_count <= len(all_helper_window_dumps):
|
|
701
|
+
return False
|
|
702
|
+
if helper_profile.all_sorted_body_dumps == enclosing_profile.all_sorted_body_dumps:
|
|
703
|
+
return False
|
|
704
|
+
return any(
|
|
705
|
+
_block_dumps_contain_window(each_block_dumps, all_helper_window_dumps)
|
|
706
|
+
for each_block_dumps in enclosing_profile.all_block_dumps
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _first_enclosing_inliner(
|
|
711
|
+
helper_node: _ModuleScopeFunction,
|
|
712
|
+
all_helper_window_dumps: list[str],
|
|
713
|
+
profile_by_function: dict[_ModuleScopeFunction, _FunctionScanProfile],
|
|
714
|
+
) -> _ModuleScopeFunction | None:
|
|
715
|
+
"""Return the first other module-scope function that inlines this window.
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
helper_node: The candidate helper whose window is sought.
|
|
719
|
+
all_helper_window_dumps: The helper's substantive-block statement dumps.
|
|
720
|
+
profile_by_function: Each function's readings, keyed in source order.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
The first function in source order that carries the window, or None.
|
|
724
|
+
"""
|
|
725
|
+
helper_profile = profile_by_function[helper_node]
|
|
726
|
+
for each_enclosing, each_profile in profile_by_function.items():
|
|
727
|
+
if each_enclosing is helper_node:
|
|
728
|
+
continue
|
|
729
|
+
if _profile_inlines_window(
|
|
730
|
+
helper_profile, each_profile, all_helper_window_dumps
|
|
731
|
+
):
|
|
732
|
+
return each_enclosing
|
|
733
|
+
return None
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _inline_duplicate_violation(
|
|
737
|
+
helper_node: _ModuleScopeFunction,
|
|
738
|
+
enclosing_node: _ModuleScopeFunction,
|
|
739
|
+
) -> tuple[frozenset[int], str]:
|
|
740
|
+
"""Return the scoped lines and report text for one inlined-helper finding.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
helper_node: The helper whose body appears inline elsewhere.
|
|
744
|
+
enclosing_node: The function carrying the inlined copy.
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
The union of both function spans, and the message naming both.
|
|
748
|
+
"""
|
|
749
|
+
helper_span = _function_definition_span(helper_node)
|
|
750
|
+
enclosing_span = _function_definition_span(enclosing_node)
|
|
751
|
+
span_suffix = SAME_FILE_INLINE_DUPLICATE_SPAN_SUFFIX_TEMPLATE.format(
|
|
752
|
+
helper_start=helper_span.start,
|
|
753
|
+
helper_length=len(helper_span),
|
|
754
|
+
enclosing_start=enclosing_span.start,
|
|
755
|
+
enclosing_length=len(enclosing_span),
|
|
756
|
+
)
|
|
757
|
+
message = (
|
|
758
|
+
f"Function {helper_node.name!r} duplicates an inline block in "
|
|
759
|
+
f"{enclosing_node.name!r} — {SAME_FILE_INLINE_DUPLICATE_GUIDANCE} "
|
|
760
|
+
f"{span_suffix}"
|
|
761
|
+
)
|
|
762
|
+
return (frozenset(helper_span) | frozenset(enclosing_span), message)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _violations_for_qualifying_helpers(
|
|
766
|
+
window_by_function: dict[_ModuleScopeFunction, list[str] | None],
|
|
767
|
+
profile_by_function: dict[_ModuleScopeFunction, _FunctionScanProfile],
|
|
768
|
+
maximum_issue_count: int,
|
|
769
|
+
) -> list[tuple[frozenset[int], str]]:
|
|
770
|
+
"""Walk the helpers in source order and report the first inliner of each.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
window_by_function: Each function's helper match window, or None.
|
|
774
|
+
profile_by_function: Each function's readings, keyed in source order.
|
|
775
|
+
maximum_issue_count: The report ceiling the walk stops at.
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
The in-scope lines and message of each report, in walk order.
|
|
779
|
+
"""
|
|
619
780
|
all_violations_in_walk_order: list[tuple[frozenset[int], str]] = []
|
|
620
|
-
for each_helper in
|
|
621
|
-
|
|
622
|
-
if helper_window_dumps is None:
|
|
781
|
+
for each_helper, each_window_dumps in window_by_function.items():
|
|
782
|
+
if each_window_dumps is None:
|
|
623
783
|
continue
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
continue
|
|
631
|
-
helper_span = _function_definition_span(each_helper)
|
|
632
|
-
enclosing_span = _function_definition_span(each_enclosing)
|
|
633
|
-
in_scope_lines = frozenset(helper_span) | frozenset(enclosing_span)
|
|
634
|
-
span_suffix = SAME_FILE_INLINE_DUPLICATE_SPAN_SUFFIX_TEMPLATE.format(
|
|
635
|
-
helper_start=helper_span.start,
|
|
636
|
-
helper_length=len(helper_span),
|
|
637
|
-
enclosing_start=enclosing_span.start,
|
|
638
|
-
enclosing_length=len(enclosing_span),
|
|
639
|
-
)
|
|
640
|
-
message = (
|
|
641
|
-
f"Function {each_helper.name!r} duplicates an inline block in "
|
|
642
|
-
f"{each_enclosing.name!r} — {SAME_FILE_INLINE_DUPLICATE_GUIDANCE} "
|
|
643
|
-
f"{span_suffix}"
|
|
784
|
+
each_enclosing = _first_enclosing_inliner(
|
|
785
|
+
each_helper, each_window_dumps, profile_by_function
|
|
786
|
+
)
|
|
787
|
+
if each_enclosing is not None:
|
|
788
|
+
all_violations_in_walk_order.append(
|
|
789
|
+
_inline_duplicate_violation(each_helper, each_enclosing)
|
|
644
790
|
)
|
|
645
|
-
|
|
791
|
+
if len(all_violations_in_walk_order) >= maximum_issue_count:
|
|
646
792
|
break
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
793
|
+
return all_violations_in_walk_order
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _inline_duplicate_violations(
|
|
797
|
+
all_top_level_functions: list[_ModuleScopeFunction],
|
|
798
|
+
maximum_issue_count: int,
|
|
799
|
+
) -> list[tuple[frozenset[int], str]]:
|
|
800
|
+
"""Report every module-scope helper whose body is inlined in another function.
|
|
801
|
+
|
|
802
|
+
The dump memo and the per-function readings live for this one call and are
|
|
803
|
+
dropped when it returns, so no reading outlives the parse tree it describes.
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
all_top_level_functions: The module-scope functions in source order.
|
|
807
|
+
maximum_issue_count: The report ceiling the walk stops at.
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
The in-scope lines and message of each report, in walk order.
|
|
811
|
+
"""
|
|
812
|
+
dump_by_statement: dict[ast.stmt, str] = {}
|
|
813
|
+
window_by_function = {
|
|
814
|
+
each_function: _helper_match_window_dumps(each_function)
|
|
815
|
+
for each_function in all_top_level_functions
|
|
816
|
+
}
|
|
817
|
+
if all(each_window is None for each_window in window_by_function.values()):
|
|
818
|
+
return []
|
|
819
|
+
profile_by_function = {
|
|
820
|
+
each_function: _function_scan_profile(each_function, dump_by_statement)
|
|
821
|
+
for each_function in all_top_level_functions
|
|
822
|
+
}
|
|
823
|
+
return _violations_for_qualifying_helpers(
|
|
824
|
+
window_by_function, profile_by_function, maximum_issue_count
|
|
653
825
|
)
|
|
654
826
|
|
|
655
827
|
|
|
@@ -247,11 +247,11 @@ def _validated_phase(phase: str) -> str:
|
|
|
247
247
|
return phase
|
|
248
248
|
|
|
249
249
|
|
|
250
|
-
def _codex_patch_issues(each_patch_file: CodexPatchFile) -> list[str]:
|
|
250
|
+
def _codex_patch_issues(each_patch_file: CodexPatchFile, repository_root: str = "") -> list[str]:
|
|
251
251
|
"""Run the existing code-rules verdict over one Codex patch view."""
|
|
252
252
|
if not each_patch_file.post_content and each_patch_file.operation == "delete":
|
|
253
253
|
return []
|
|
254
|
-
if _is_hook_infrastructure_python_target(each_patch_file.file_path):
|
|
254
|
+
if _is_hook_infrastructure_python_target(each_patch_file.file_path, repository_root):
|
|
255
255
|
all_issues = _hook_infrastructure_blocking_issues(
|
|
256
256
|
each_patch_file.post_content,
|
|
257
257
|
each_patch_file.file_path,
|
|
@@ -259,7 +259,7 @@ def _codex_patch_issues(each_patch_file: CodexPatchFile) -> list[str]:
|
|
|
259
259
|
each_patch_file.prior_content,
|
|
260
260
|
phase=EDIT_LANE_PHASE,
|
|
261
261
|
)
|
|
262
|
-
elif _is_validated_target(each_patch_file.file_path):
|
|
262
|
+
elif _is_validated_target(each_patch_file.file_path, repository_root):
|
|
263
263
|
all_issues = validate_content_for_edit_lane(
|
|
264
264
|
each_patch_file.post_content,
|
|
265
265
|
each_patch_file.file_path,
|
|
@@ -306,7 +306,7 @@ def _report_codex_patch_payload(
|
|
|
306
306
|
all_issues = [
|
|
307
307
|
each_issue
|
|
308
308
|
for each_patch_file in all_patch_files
|
|
309
|
-
for each_issue in _codex_patch_issues(each_patch_file)
|
|
309
|
+
for each_issue in _codex_patch_issues(each_patch_file, working_directory or "")
|
|
310
310
|
]
|
|
311
311
|
if all_issues:
|
|
312
312
|
_write_deny_payload(
|
|
@@ -708,14 +708,16 @@ def validate_content_for_full_gate(
|
|
|
708
708
|
|
|
709
709
|
|
|
710
710
|
def prior_and_post_edit_content(
|
|
711
|
-
file_path: str, old_string: str, new_string: str,
|
|
711
|
+
file_path: str, old_string: str, new_string: str, is_replace_all: bool = False,
|
|
712
712
|
) -> tuple[str | None, str | None]:
|
|
713
713
|
"""Return the pre-edit and post-edit file content from a single disk read.
|
|
714
714
|
|
|
715
715
|
Reads ``file_path`` once and derives both views from that single read so the
|
|
716
716
|
prior and the reconstruction never diverge across two independent reads.
|
|
717
|
-
The post-edit view
|
|
718
|
-
|
|
717
|
+
The post-edit view comes from apply_edits, the same reconstruction the
|
|
718
|
+
MultiEdit lane uses, so one payload never reconstructs two ways. It replaces
|
|
719
|
+
the first occurrence of ``old_string`` with ``new_string``, or every occurrence
|
|
720
|
+
when the Edit carries ``replace_all``, mirroring how the Edit tool applies a
|
|
719
721
|
replacement.
|
|
720
722
|
|
|
721
723
|
Returns ``(None, None)`` when the file cannot be read, ``old_string`` is
|
|
@@ -729,6 +731,7 @@ def prior_and_post_edit_content(
|
|
|
729
731
|
file_path: The path of the file the Edit targets.
|
|
730
732
|
old_string: The Edit's ``old_string`` fragment.
|
|
731
733
|
new_string: The Edit's ``new_string`` fragment.
|
|
734
|
+
is_replace_all: Whether the Edit carries ``replace_all``.
|
|
732
735
|
|
|
733
736
|
Returns:
|
|
734
737
|
A ``(prior_content, post_edit_content)`` pair, or ``(None, None)`` when
|
|
@@ -741,14 +744,22 @@ def prior_and_post_edit_content(
|
|
|
741
744
|
return None, None
|
|
742
745
|
if old_string not in existing_content:
|
|
743
746
|
return None, None
|
|
744
|
-
|
|
747
|
+
one_edit = {
|
|
748
|
+
"old_string": old_string,
|
|
749
|
+
"new_string": new_string,
|
|
750
|
+
"replace_all": is_replace_all,
|
|
751
|
+
}
|
|
752
|
+
return existing_content, apply_edits(existing_content, [one_edit])
|
|
745
753
|
|
|
746
754
|
|
|
747
|
-
def _is_validated_target(file_path: str) -> bool:
|
|
755
|
+
def _is_validated_target(file_path: str, repository_root: str = "") -> bool:
|
|
748
756
|
"""Return whether the path is subject to code-rules validation.
|
|
749
757
|
|
|
750
758
|
Args:
|
|
751
759
|
file_path: The destination path of the write, edit, or pre-check target.
|
|
760
|
+
repository_root: The session's working directory (the PreToolUse
|
|
761
|
+
payload's ``cwd``), so a path inside it is never exempted as
|
|
762
|
+
scratch merely because the repository sits under ``/tmp``.
|
|
752
763
|
|
|
753
764
|
Returns:
|
|
754
765
|
True when the path is non-empty, outside hook infrastructure, and
|
|
@@ -756,14 +767,14 @@ def _is_validated_target(file_path: str) -> bool:
|
|
|
756
767
|
"""
|
|
757
768
|
if not file_path:
|
|
758
769
|
return False
|
|
759
|
-
if is_ephemeral_script_path(file_path):
|
|
770
|
+
if is_ephemeral_script_path(file_path, repository_root):
|
|
760
771
|
return False
|
|
761
772
|
if is_hook_infrastructure(file_path):
|
|
762
773
|
return False
|
|
763
774
|
return get_file_extension(file_path) in ALL_CODE_EXTENSIONS
|
|
764
775
|
|
|
765
776
|
|
|
766
|
-
def _is_hook_infrastructure_python_target(file_path: str) -> bool:
|
|
777
|
+
def _is_hook_infrastructure_python_target(file_path: str, repository_root: str = "") -> bool:
|
|
767
778
|
"""Return whether the path is a hook-infrastructure Python file.
|
|
768
779
|
|
|
769
780
|
The full code-rules suite exempts hook-infrastructure files, but the
|
|
@@ -773,13 +784,16 @@ def _is_hook_infrastructure_python_target(file_path: str) -> bool:
|
|
|
773
784
|
|
|
774
785
|
Args:
|
|
775
786
|
file_path: The destination path of the write, edit, or pre-check target.
|
|
787
|
+
repository_root: The session's working directory (the PreToolUse
|
|
788
|
+
payload's ``cwd``), so a path inside it is never exempted as
|
|
789
|
+
scratch merely because the repository sits under ``/tmp``.
|
|
776
790
|
|
|
777
791
|
Returns:
|
|
778
792
|
True when the path names a Python file inside hook infrastructure.
|
|
779
793
|
"""
|
|
780
794
|
if not file_path:
|
|
781
795
|
return False
|
|
782
|
-
if is_ephemeral_script_path(file_path):
|
|
796
|
+
if is_ephemeral_script_path(file_path, repository_root):
|
|
783
797
|
return False
|
|
784
798
|
if not is_hook_infrastructure(file_path):
|
|
785
799
|
return False
|
|
@@ -1170,6 +1184,7 @@ def _contents_for_validation(
|
|
|
1170
1184
|
if tool_name == "Edit":
|
|
1171
1185
|
prior_content, full_file_content_after_edit = prior_and_post_edit_content(
|
|
1172
1186
|
file_path, old_string, new_string,
|
|
1187
|
+
is_replace_all=all_tool_input.get("replace_all") is True,
|
|
1173
1188
|
)
|
|
1174
1189
|
if full_file_content_after_edit is None:
|
|
1175
1190
|
full_file_content_after_edit = existing_file_content(file_path)
|
|
@@ -1372,8 +1387,11 @@ def main(all_arguments: list[str]) -> None:
|
|
|
1372
1387
|
if is_under_session_scratchpad(file_path, pretooluse_payload):
|
|
1373
1388
|
sys.exit(0)
|
|
1374
1389
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1390
|
+
repository_root = str(pretooluse_payload.get("cwd") or "")
|
|
1391
|
+
runs_full_verdict = _is_validated_target(file_path, repository_root)
|
|
1392
|
+
if not runs_full_verdict and not _is_hook_infrastructure_python_target(
|
|
1393
|
+
file_path, repository_root
|
|
1394
|
+
):
|
|
1377
1395
|
sys.exit(0)
|
|
1378
1396
|
|
|
1379
1397
|
validation_contents = _contents_for_validation(
|