claude-dev-env 2.29.0 → 2.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.agents/agents/pr-description-writer.md +21 -6
  2. package/.agents/agents/reference/README.md +6 -0
  3. package/.agents/agents/reference/pr-description-illustrative-voice.md +45 -0
  4. package/.agents/agents/reference/pr-description-verification.md +19 -0
  5. package/.agents/agents/test_pr_description_writer_style.py +47 -0
  6. package/_shared/pr-loop/code-rules-gate.md +1 -1
  7. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -2
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +8 -8
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +2 -2
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +1 -1
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +15 -15
  12. package/docs/CODE_RULES.md +10 -0
  13. package/hooks/blocking/code_rules_enforcer.py +433 -313
  14. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +1 -1
  15. package/hooks/blocking/test_code_rules_enforcer_class_docstring_methods.py +1 -1
  16. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +117 -22
  17. package/hooks/blocking/test_code_rules_enforcer_docstring_args_signature.py +1 -1
  18. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +1 -1
  19. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +1 -1
  20. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +1 -1
  21. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +1 -1
  22. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +1 -1
  23. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +1 -1
  24. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +2 -2
  25. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +3 -3
  26. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +1 -1
  27. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +1 -1
  28. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +117 -38
  29. package/hooks/blocking/test_code_rules_enforcer_output_snapshot.py +121 -0
  30. package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +17 -15
  31. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +4 -2
  32. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +1 -1
  33. package/hooks/blocking/test_code_rules_enforcer_stage.py +1 -1
  34. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +4 -4
  35. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +2 -2
  36. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +2 -2
  37. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -2
  38. package/hooks/blocking/test_code_rules_validation_phase.py +281 -0
  39. package/hooks/blocking/test_fable_spawn_gate.py +1 -1
  40. package/hooks/blocking/test_pre_tool_use_dispatcher.py +26 -11
  41. package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
  42. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +21 -0
  43. package/hooks/hooks_constants/validation_phase_constants.py +34 -0
  44. package/hooks/validators/README.md +1 -1
  45. package/hooks/validators/config/hook_timing_harness_constants.py +16 -0
  46. package/hooks/validators/hook_timing_harness.py +287 -0
  47. package/hooks/validators/test_hook_timing_harness.py +251 -0
  48. package/package.json +1 -1
@@ -1,19 +1,34 @@
1
1
  ---
2
2
  name: pr-description-writer
3
- description: "Optional agent that drafts factual pull request descriptions and comments from the current diff and validates each statement against the master review guides."
3
+ description: "Optional agent that writes factual pull request titles, descriptions, and comments in plain illustrative language from the current diff."
4
4
  tools: Read,Grep,Glob,Bash
5
5
  ---
6
6
 
7
7
  # PR Description Writer
8
8
 
9
- Write the body from the current pull request diff and task. Resolve the active managed root and active agents home before reading guides: `~/.claude` is the default root, `CLAUDE_CONFIG_DIR` selects another root, and `--target DIR` takes precedence; the default `.claude` root uses sibling `~/.agents`, while another root uses sibling `<root-name>.agents`. Use `<agents-home>/skills/pr-title-description/SKILL.md` (source fallback: `packages/claude-dev-env/.agents/skills/pr-title-description/SKILL.md`) for required content and shape. Use the current review findings and task guidance for a review comment. Do not assume `~/.claude` or `~/.agents` for a named profile or explicit target.
9
+ Write from the current pull request diff and task. Resolve the active managed root and active agents home before reading guides: `~/.claude` is the default root, `CLAUDE_CONFIG_DIR` selects another root, and `--target DIR` takes precedence; the default `.claude` root uses sibling `~/.agents`, while another root uses sibling `<root-name>.agents`. Use `<agents-home>/skills/pr-title-description/SKILL.md` (source fallback: `packages/claude-dev-env/.agents/skills/pr-title-description/SKILL.md`) for required content and shape. Use the current review findings and task guidance for a review comment. Do not assume `~/.claude` or `~/.agents` for a named profile or explicit target.
10
+
11
+ ## Voice
12
+
13
+ Explain the change so a smart reader who knows nothing about the code can picture it on the first read.
14
+
15
+ - Start with a concrete scene or action.
16
+ - Prefer a tiny Before / After story when the change is about how something is found, asked for, checked, saved, retried, or stopped.
17
+ - Use small words and picture words.
18
+ - Replace jargon with the action it represents. When one technical term matters, explain it immediately in plain words.
19
+ - Keep the main behavior in `What this adds`, the reason in `Why`, and proof in `Verification`.
20
+ - Keep titles concrete. Say what someone gives it, what happens, or what comes back.
21
+
22
+ For a concrete voice sample only when needed, read `reference/pr-description-illustrative-voice.md` beside this agent. For a concrete verification sample only when needed, read `reference/pr-description-verification.md`. These samples guide the shape and voice; they are not content to copy into unrelated pull requests.
10
23
 
11
24
  ## Draft the body
12
25
 
13
26
  1. Inspect the cumulative diff, the current pull request body, and the validation results.
14
- 2. State the behavior change, its scope, and the validation that ran.
15
- 3. Preserve accurate issue links and curated sections from the current body.
16
- 4. Use headings when they help a reviewer scan the change.
27
+ 2. Find the main behavior a user, caller, operator, or reviewer can see.
28
+ 3. Write that behavior first in plain illustrative language.
29
+ 4. Preserve accurate issue links and curated sections from the current body when they remain useful.
30
+ 5. Use headings when they help a reviewer scan the change.
31
+ 6. In `Verification`, lead with what a person can open, see, click, compare, or try for themselves. Put test results in one short supporting line when useful.
17
32
 
18
33
  ## Publish through GitHub CLI
19
34
 
@@ -21,4 +36,4 @@ Place markdown in a BOM-free temporary file and pass its path with `--body-file`
21
36
 
22
37
  ## Check the draft
23
38
 
24
- Confirm every statement matches the diff and current pull request state. Return the final markdown and the file path ready for publication.
39
+ Confirm every statement matches the diff and current pull request state. Confirm the title and body stay clear without requiring the reader to decode repository jargon. Confirm `Verification` gives the reviewer a visible check they can perform themselves whenever the change has one. Return the final markdown and the file path ready for publication.
@@ -0,0 +1,6 @@
1
+ # PR writer references
2
+
3
+ These files are optional examples for `pr-description-writer.md`.
4
+
5
+ - `pr-description-illustrative-voice.md` — plain, picture-first title and description examples.
6
+ - `pr-description-verification.md` — human-checkable verification examples.
@@ -0,0 +1,45 @@
1
+ # PR description illustrative voice
2
+
3
+ Use this only when the PR writer needs a concrete voice sample.
4
+
5
+ ## Core pattern
6
+
7
+ Start with something a person can picture.
8
+
9
+ Before: show the old action in a tiny scene.
10
+
11
+ After: show the new action in a tiny scene.
12
+
13
+ Keep machine words out when a plain action works. If one technical term matters, explain it in the same breath with a simple picture.
14
+
15
+ ## Sample 1 — call the right helper directly
16
+
17
+ The theme package has a phone icon somewhere. We need to find it.
18
+
19
+ Before: yell, “anyone know how to find icons?” and wait for a hand.
20
+
21
+ After: call the finder by its name tag: “icon finder, go.”
22
+
23
+ That is all the operation name is here — the finder’s name tag.
24
+
25
+ ## Sample 2 — ask for a deeper review
26
+
27
+ You start the review loop. It looks at the code, then looks again, until it has nothing new to say.
28
+
29
+ Before: each look could be a quick glance.
30
+
31
+ After: each look uses the deepest review setting the model has.
32
+
33
+ The person running the loop asked for a full look, so every pass gets a full look.
34
+
35
+ ## Writing rules
36
+
37
+ - Few words. Small words. Clear on first read.
38
+ - Lead with the thing someone can see or do.
39
+ - Prefer Before / After when the change is about how something is asked, found, checked, saved, retried, or stopped.
40
+ - Use picture words: hand it, open it, find it, check it, save it, skip it, stop, call by name.
41
+ - Prefer spoken results: “found it,” “missing,” “duplicated,” “couldn’t.”
42
+ - Keep the main behavior in `What this adds`.
43
+ - Keep `Why` short and concrete.
44
+ - Put proof only in `Verification`.
45
+ - Explain jargon immediately or replace it.
@@ -0,0 +1,19 @@
1
+ # Human-checkable PR verification
2
+
3
+ Use this only when the PR writer needs a concrete verification sample.
4
+
5
+ Verification should lead with something a reviewer can open, see, click, compare, or try.
6
+
7
+ Good:
8
+
9
+ Open the generated PR description. The first paragraph should tell the story in plain words before it names any code detail. If the change has a visible result, point to the exact file, screen, output, or behavior the reviewer can inspect.
10
+
11
+ Tests: focused checks pass.
12
+
13
+ Weak:
14
+
15
+ - Contract test passed.
16
+ - Static analysis passed.
17
+ - Internal schema assertion passed.
18
+
19
+ Machine checks can support the claim. They should not be the whole proof when a person can inspect the result directly.
@@ -0,0 +1,47 @@
1
+ """Contract tests for the PR description writer voice."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ AGENT_DIRECTORY = Path(__file__).parent
7
+ AGENT_PATH = AGENT_DIRECTORY / "pr-description-writer.md"
8
+ VOICE_REFERENCE_PATH = AGENT_DIRECTORY / "reference" / "pr-description-illustrative-voice.md"
9
+ VERIFICATION_REFERENCE_PATH = AGENT_DIRECTORY / "reference" / "pr-description-verification.md"
10
+
11
+
12
+ def test_pr_description_writer_uses_repo_owned_progressive_samples() -> None:
13
+ agent_text = AGENT_PATH.read_text(encoding="utf-8")
14
+
15
+ assert "reference/pr-description-illustrative-voice.md" in agent_text
16
+ assert "reference/pr-description-verification.md" in agent_text
17
+ assert "only when needed" in agent_text
18
+ assert "PR #2562" not in agent_text
19
+ assert "PR #1150" not in agent_text
20
+
21
+
22
+ def test_voice_reference_keeps_the_plain_illustrative_shape() -> None:
23
+ reference_text = VOICE_REFERENCE_PATH.read_text(encoding="utf-8")
24
+
25
+ for required_text in (
26
+ "Before:",
27
+ "After:",
28
+ "finder’s name tag",
29
+ "found it",
30
+ "missing",
31
+ "duplicated",
32
+ "What this adds",
33
+ "Why",
34
+ "Verification",
35
+ ):
36
+ assert required_text in reference_text
37
+
38
+
39
+ def test_verification_leads_with_a_human_visible_check() -> None:
40
+ agent_text = AGENT_PATH.read_text(encoding="utf-8")
41
+ reference_text = VERIFICATION_REFERENCE_PATH.read_text(encoding="utf-8")
42
+
43
+ for visible_action in ("open", "see", "click", "compare", "try"):
44
+ assert visible_action in agent_text
45
+ assert visible_action in reference_text
46
+ assert "Tests: focused checks pass." in reference_text
47
+ assert "Machine checks can support the claim." in reference_text
@@ -1,6 +1,6 @@
1
1
  # CODE_RULES gate
2
2
 
3
- Pre-audit validator run before each AUDIT (and pre-commit when applicable). Wraps `validate_content` from `~/.claude/hooks/blocking/code_rules_enforcer.py`.
3
+ Pre-audit validator run before each AUDIT (and pre-commit when applicable). Wraps `validate_content_for_full_gate` from `~/.claude/hooks/blocking/code_rules_enforcer.py`.
4
4
 
5
5
  ## Script location
6
6
 
@@ -63,7 +63,7 @@ except ImportError as import_error:
63
63
  subprocess = git_file_sets.subprocess
64
64
 
65
65
  resolve_claude_dev_env_root = enforcer_loading.resolve_claude_dev_env_root
66
- load_validate_content = enforcer_loading.load_validate_content
66
+ load_validate_content_for_full_gate = enforcer_loading.load_validate_content_for_full_gate
67
67
  ValidateContentCallable = enforcer_loading.ValidateContentCallable
68
68
 
69
69
  resolve_merge_base = git_file_sets.resolve_merge_base
@@ -357,7 +357,7 @@ def main(all_arguments: list[str]) -> int:
357
357
  repository_root = (
358
358
  arguments.repo_root.resolve() if arguments.repo_root is not None else Path.cwd().resolve()
359
359
  )
360
- validate_content = load_validate_content()
360
+ validate_content = load_validate_content_for_full_gate()
361
361
  if arguments.paths:
362
362
  return _run_explicit_paths_mode(validate_content, arguments, repository_root)
363
363
  if arguments.immediate:
@@ -1,8 +1,8 @@
1
- """Load the code-rules enforcer's ``validate_content`` for in-process use.
1
+ """Load the code-rules enforcer's ``validate_content_for_full_gate`` for in-process use.
2
2
 
3
- The gate runs the same ``validate_content`` the PreToolUse enforcer runs, so it
4
- locates the ``hooks/blocking/code_rules_enforcer.py`` module from disk, executes
5
- it with the hooks directory on ``sys.path``, and hands back its callable.
3
+ The gate runs the enforcer's full-gate validation phase, so it locates the
4
+ ``hooks/blocking/code_rules_enforcer.py`` module from disk, executes it with
5
+ the hooks directory on ``sys.path``, and hands back its callable.
6
6
  """
7
7
 
8
8
  import importlib.machinery
@@ -138,11 +138,11 @@ def _exec_enforcer_with_hooks_on_path(
138
138
  _restore_hooks_constants_modules(all_saved_modules, hooks_root_path)
139
139
 
140
140
 
141
- def load_validate_content() -> ValidateContentCallable:
142
- """Load ``code_rules_enforcer.validate_content`` for in-process use.
141
+ def load_validate_content_for_full_gate() -> ValidateContentCallable:
142
+ """Load ``code_rules_enforcer.validate_content_for_full_gate`` for in-process use.
143
143
 
144
144
  Returns:
145
- The ``validate_content`` callable from the enforcer module.
145
+ The ``validate_content_for_full_gate`` callable from the enforcer module.
146
146
 
147
147
  Raises:
148
148
  SystemExit: When the package root or the enforcer module cannot load.
@@ -153,7 +153,7 @@ def load_validate_content() -> ValidateContentCallable:
153
153
  )
154
154
  module = importlib.util.module_from_spec(specification)
155
155
  _exec_enforcer_with_hooks_on_path(specification, module)
156
- return module.validate_content
156
+ return module.validate_content_for_full_gate
157
157
 
158
158
 
159
159
  def _restore_hooks_constants_modules(
@@ -10,8 +10,8 @@ def test_resolve_claude_dev_env_root_finds_enforcer_bearing_root() -> None:
10
10
  assert (resolved_root / "hooks" / "blocking" / "code_rules_enforcer.py").is_file()
11
11
 
12
12
 
13
- def test_load_validate_content_returns_callable_passing_a_clean_module() -> None:
14
- validate_content = enforcer_loading.load_validate_content()
13
+ def test_load_validate_content_for_full_gate_returns_callable_passing_a_clean_module() -> None:
14
+ validate_content = enforcer_loading.load_validate_content_for_full_gate()
15
15
  clean_module = '"""Clean module."""\n\n\ndef ping() -> str:\n return "pong"\n'
16
16
  issues = validate_content(clean_module, "sample.py", "")
17
17
  assert issues == []
@@ -130,7 +130,7 @@ def test_gate_caller_scans_staged_content_with_commit_stage_scope(
130
130
 
131
131
  resolved_file_path = file_path.resolve()
132
132
  exit_code = gate_running.run_gate(
133
- validate_content=enforcer_loading.load_validate_content(),
133
+ validate_content=enforcer_loading.load_validate_content_for_full_gate(),
134
134
  all_file_paths=[resolved_file_path],
135
135
  repository_root=repository_root,
136
136
  all_added_lines_by_path={resolved_file_path: {2, 3}},
@@ -735,7 +735,7 @@ def test_run_gate_flags_copied_sibling_when_cwd_is_outside_repo_root(
735
735
  copied_file = package_directory / "copied_helper.py"
736
736
  existing_file.write_text(_DUPLICATE_HELPER_SOURCE, encoding="utf-8")
737
737
  copied_file.write_text(_DUPLICATE_HELPER_SOURCE, encoding="utf-8")
738
- validate_content = gate_module.load_validate_content()
738
+ validate_content = gate_module.load_validate_content_for_full_gate()
739
739
 
740
740
  monkeypatch.chdir(temporary_git_repository.parent)
741
741
  blocking_by_file, _advisory_by_file, _skipped = (
@@ -813,7 +813,7 @@ def test_duplicate_body_span_range_covers_the_definition_through_last_body_line(
813
813
  assert span == range(definition_line, last_body_line + 1)
814
814
 
815
815
 
816
- def test_split_violations_blocks_duplicate_body_when_span_intersects_added_lines(
816
+ def test_split_violations_by_scope_blocks_duplicate_body_when_span_intersects_added_lines(
817
817
  tmp_path: Path,
818
818
  ) -> None:
819
819
  """A duplicate-body issue whose copied-function span overlaps the diff's added
@@ -829,7 +829,7 @@ def test_split_violations_blocks_duplicate_body_when_span_intersects_added_lines
829
829
  assert advisory == []
830
830
 
831
831
 
832
- def test_split_violations_advises_duplicate_body_when_span_misses_added_lines(
832
+ def test_split_violations_by_scope_advises_duplicate_body_when_span_misses_added_lines(
833
833
  tmp_path: Path,
834
834
  ) -> None:
835
835
  """A duplicate-body issue for an untouched pre-existing copy — whose span does
@@ -1002,7 +1002,7 @@ def test_collect_partitioned_violations_advises_pre_existing_sibling_duplicate(
1002
1002
  _DUPLICATE_HELPER_SOURCE + "unrelated_constant = 1\n", encoding="utf-8"
1003
1003
  )
1004
1004
  unrelated_added_line = _DUPLICATE_HELPER_SOURCE.count("\n") + 1
1005
- validate_content = gate_module.load_validate_content()
1005
+ validate_content = gate_module.load_validate_content_for_full_gate()
1006
1006
 
1007
1007
  resolved_copied = copied_file.resolve()
1008
1008
  blocking_by_file, advisory_by_file, _skipped = (
@@ -1055,7 +1055,7 @@ def test_collect_partitioned_violations_blocks_sibling_duplicate_in_added_region
1055
1055
  definition_line = 3
1056
1056
  last_body_line = 15
1057
1057
  all_copied_function_lines = set(range(definition_line, last_body_line + 1))
1058
- validate_content = gate_module.load_validate_content()
1058
+ validate_content = gate_module.load_validate_content_for_full_gate()
1059
1059
 
1060
1060
  resolved_copied = copied_file.resolve()
1061
1061
  blocking_by_file, _advisory_by_file, _skipped = (
@@ -1549,10 +1549,10 @@ def test_main_blocks_when_function_body_grows_past_threshold_with_def_line_untou
1549
1549
  assert exit_code == 1
1550
1550
 
1551
1551
 
1552
- def test_split_violations_blocks_function_length_when_span_intersects_added_lines() -> None:
1552
+ def test_split_violations_by_scope_blocks_function_length_when_span_intersects_added_lines() -> None:
1553
1553
  """A function-length issue whose declared span overlaps the diff's added
1554
1554
  lines is blocking — the body grew, which is exactly Finding B's intent."""
1555
- validate_content = gate_module.load_validate_content()
1555
+ validate_content = gate_module.load_validate_content_for_full_gate()
1556
1556
  long_function = _build_function_module(
1557
1557
  "oversized", body_line_count=70, leading_lines=3
1558
1558
  )
@@ -1571,12 +1571,12 @@ def test_split_violations_blocks_function_length_when_span_intersects_added_line
1571
1571
  assert advisory == []
1572
1572
 
1573
1573
 
1574
- def test_split_violations_advises_function_length_when_span_misses_added_lines() -> None:
1574
+ def test_split_violations_by_scope_advises_function_length_when_span_misses_added_lines() -> None:
1575
1575
  """A function-length issue for an untouched pre-existing function — whose
1576
1576
  declared span does not overlap any added line — is advisory, not blocking.
1577
1577
  This prevents the over-block regression where every pre-existing >=60-line
1578
1578
  function in a touched file was forced into the blocking payload."""
1579
- validate_content = gate_module.load_validate_content()
1579
+ validate_content = gate_module.load_validate_content_for_full_gate()
1580
1580
  long_function = _build_function_module(
1581
1581
  "oversized", body_line_count=70, leading_lines=3
1582
1582
  )
@@ -1595,7 +1595,7 @@ def test_split_violations_advises_function_length_when_span_misses_added_lines()
1595
1595
 
1596
1596
 
1597
1597
  def _isolation_issues_for_home_probe_test() -> list[str]:
1598
- validate_content = gate_module.load_validate_content()
1598
+ validate_content = gate_module.load_validate_content_for_full_gate()
1599
1599
  header = "from pathlib import Path\n"
1600
1600
  test_body = (
1601
1601
  "def test_reads_home() -> None:\n"
@@ -1606,7 +1606,7 @@ def _isolation_issues_for_home_probe_test() -> list[str]:
1606
1606
  return [each_issue for each_issue in issues if "probes" in each_issue]
1607
1607
 
1608
1608
 
1609
- def test_split_violations_blocks_isolation_when_function_span_intersects_added_lines() -> None:
1609
+ def test_split_violations_by_scope_blocks_isolation_when_function_span_intersects_added_lines() -> None:
1610
1610
  """An isolation issue whose enclosing test-function span overlaps the diff's
1611
1611
  added lines is blocking — a signature-line change that un-isolates an
1612
1612
  unchanged-body probe must block, matching the enforcer's terminal path."""
@@ -1621,7 +1621,7 @@ def test_split_violations_blocks_isolation_when_function_span_intersects_added_l
1621
1621
  assert advisory == []
1622
1622
 
1623
1623
 
1624
- def test_split_violations_advises_isolation_when_function_span_misses_added_lines() -> None:
1624
+ def test_split_violations_by_scope_advises_isolation_when_function_span_misses_added_lines() -> None:
1625
1625
  """An isolation issue for an untouched pre-existing probe — whose enclosing
1626
1626
  test-function span does not overlap any added line — is advisory, not
1627
1627
  blocking, mirroring the function-length scope contract."""
@@ -1814,7 +1814,7 @@ def test_check_wrapper_plumb_through_stays_under_function_length_threshold() ->
1814
1814
 
1815
1815
 
1816
1816
  def _banned_noun_parameter_issues() -> list[str]:
1817
- validate_content = gate_module.load_validate_content()
1817
+ validate_content = gate_module.load_validate_content_for_full_gate()
1818
1818
  source = (
1819
1819
  "def aggregate(canned_results: int) -> int:\n"
1820
1820
  " doubled = canned_results * 2\n"
@@ -1824,7 +1824,7 @@ def _banned_noun_parameter_issues() -> list[str]:
1824
1824
  return [each_issue for each_issue in issues if "banned noun" in each_issue]
1825
1825
 
1826
1826
 
1827
- def test_split_violations_blocks_banned_noun_when_binding_line_is_added() -> None:
1827
+ def test_split_violations_by_scope_blocks_banned_noun_when_binding_line_is_added() -> None:
1828
1828
  """A banned-noun binding is blocking when its own binding line is among the
1829
1829
  added lines. The gate reconstructs the one-line binding span through the
1830
1830
  same shared extractor registry it uses for function-length and isolation,
@@ -1840,7 +1840,7 @@ def test_split_violations_blocks_banned_noun_when_binding_line_is_added() -> Non
1840
1840
  assert advisory == []
1841
1841
 
1842
1842
 
1843
- def test_split_violations_advises_banned_noun_when_binding_line_untouched() -> None:
1843
+ def test_split_violations_by_scope_advises_banned_noun_when_binding_line_untouched() -> None:
1844
1844
  """A banned-noun binding whose own line is not among the added lines is
1845
1845
  advisory — editing an unrelated body line does not pull a pre-existing
1846
1846
  binding into scope, mirroring the companion exact-match identifier check."""
@@ -109,3 +109,13 @@ If you already have the data, don't fetch it again.
109
109
  ## 11. ENFORCEMENT SURFACES
110
110
 
111
111
  ⚡ **Hooks** block pattern-matchable violations at Write/Edit time. 🤖 **Prompt context** carries judgment principles (SRP, Right-Sized Engineering, research-first action on ambiguous intent, BDD discovery, docstring-prose-matches-implementation). 👥 **Audit rubrics** (`/check`, `packages/claude-dev-env/audit-rubrics/` categories A–Q) cover cross-file architectural concerns. Rules with documented-but-pending hook coverage live in `~/.claude/rules/*.md`; each names its own promotion path. The docstring-prose standard (free-form enumerations match the body) lives in `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md`, enforced via Category O6 audit. The diagram-first docstring standard (a summary line, then a `::` example or doctest, then a couple of short prose lines) lives in `packages/claude-dev-env/rules/plain-illustrative-docstrings.md`, enforced by the `check_docstring_runon_sentence` and `check_docstring_prose_wall_without_illustration` backstop hooks and Category O9 audit.
112
+
113
+ ## 11.5 VALIDATION-PHASE PRECEDENCE
114
+
115
+ `code_rules_enforcer.py` decides what a run checks and reports along three independent axes. Each axis filters a narrower scope than the one before it; none widens what the axis before it already decided.
116
+
117
+ 1. **Phase selects the roster.** `EDIT_LANE_PHASE` or `FULL_GATE_PHASE` decides which checks exist in the lane at all. `validate_content_for_phase` takes `phase` keyword-only with no default, so every caller names its lane explicitly.
118
+ 2. **Target classification filters within a lane.** The hook-infrastructure patterns and the ephemeral-path check decide whether a target is validated, and with which subset. Classification narrows a lane; it never adds a check the phase already excluded.
119
+ 3. **Changed-line scope filters only the report.** `defer_scope_to_caller` and the changed-line set decide which found violations block. Scope filters findings after every check in the roster already ran; it adds or removes no check.
120
+
121
+ `hooks/hooks_constants/validation_phase_constants.py` is the single source for all three axes: the phase names (`EDIT_LANE_PHASE`, `FULL_GATE_PHASE`), the full-gate-only roster (`ALL_FULL_GATE_ONLY_CHECK_NAMES`), and the hook-infrastructure edit-lane roster (`ALL_HOOK_INFRASTRUCTURE_EDIT_LANE_CHECK_NAMES`).