claude-dev-env 2.29.0 → 2.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) 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/.agents/skills/e-simplify/SKILL.md +20 -8
  7. package/_shared/pr-loop/code-rules-gate.md +1 -1
  8. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -2
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +8 -8
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +2 -2
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +1 -1
  12. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +15 -15
  13. package/docs/CODE_RULES.md +10 -0
  14. package/hooks/blocking/code_rules_enforcer.py +435 -318
  15. package/hooks/blocking/shell_substitution_blocker.py +106 -30
  16. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +14 -0
  17. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +1 -1
  18. package/hooks/blocking/test_code_rules_enforcer_class_docstring_methods.py +1 -1
  19. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +117 -22
  20. package/hooks/blocking/test_code_rules_enforcer_docstring_args_signature.py +1 -1
  21. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +1 -1
  22. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +1 -1
  23. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +1 -1
  24. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +1 -1
  25. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +1 -1
  26. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +1 -1
  27. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +2 -2
  28. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +3 -3
  29. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +1 -1
  30. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +1 -1
  31. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +117 -38
  32. package/hooks/blocking/test_code_rules_enforcer_output_snapshot.py +121 -0
  33. package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +17 -15
  34. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +4 -2
  35. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +1 -1
  36. package/hooks/blocking/test_code_rules_enforcer_stage.py +1 -1
  37. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +4 -4
  38. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +2 -2
  39. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +2 -2
  40. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -2
  41. package/hooks/blocking/test_code_rules_validation_phase.py +332 -0
  42. package/hooks/blocking/test_fable_spawn_gate.py +1 -1
  43. package/hooks/blocking/test_pre_tool_use_dispatcher.py +26 -11
  44. package/hooks/blocking/test_shell_substitution_blocker.py +69 -0
  45. package/hooks/hooks.json +1 -1
  46. package/hooks/hooks_constants/code_rules_enforcer_constants.py +7 -0
  47. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +4 -1
  48. package/hooks/hooks_constants/task_tool_prompt_constants.py +13 -0
  49. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +12 -0
  50. package/hooks/hooks_constants/validation_phase_constants.py +34 -0
  51. package/hooks/session/task_tool_prompt.py +37 -0
  52. package/hooks/session/test_task_tool_prompt.py +65 -0
  53. package/hooks/validators/README.md +1 -1
  54. package/hooks/validators/config/hook_timing_harness_constants.py +16 -0
  55. package/hooks/validators/hook_timing_harness.py +287 -0
  56. package/hooks/validators/test_hook_timing_harness.py +251 -0
  57. package/package.json +1 -1
  58. package/hooks/hooks_constants/task_list_loop_starter_constants.py +0 -19
  59. package/hooks/session/task_list_loop_starter.py +0 -45
  60. package/hooks/session/test_task_list_loop_starter.py +0 -63
@@ -20,7 +20,7 @@ if _HOOKS_DIRECTORY not in sys.path:
20
20
 
21
21
  from code_rules_enforcer import ( # noqa: E402
22
22
  main,
23
- validate_content,
23
+ validate_content_for_full_gate as validate_content,
24
24
  )
25
25
 
26
26
 
@@ -165,47 +165,126 @@ def _validate_narrow_edit(narrow_edit_fixture: NarrowEditFixture) -> list[str]:
165
165
  )
166
166
 
167
167
 
168
- def _scope_aware_rule_names_from_source() -> frozenset[str]:
169
- source_tree = ast.parse(inspect.getsource(validate_content))
170
- all_rule_names: set[str] = set()
171
- for each_call in ast.walk(source_tree):
172
- if not isinstance(each_call, ast.Call):
168
+ _SCOPE_CARRYING_ATTRIBUTE_NAMES = frozenset({"all_changed_lines", "defer_scope_to_caller"})
169
+
170
+
171
+ def _assignment_target_names(target: ast.expr) -> list[str]:
172
+ """Return the plain names one assignment target binds, Name or Tuple-of-Name."""
173
+ if isinstance(target, ast.Name):
174
+ return [target.id]
175
+ if isinstance(target, ast.Tuple):
176
+ return [each.id for each in target.elts if isinstance(each, ast.Name)]
177
+ return []
178
+
179
+
180
+ def _scope_aliases_for_pair(target_names: list[str], all_values: list[ast.expr]) -> dict[str, str]:
181
+ """Return the local-name-to-scope-attribute map one assignment pair contributes."""
182
+ aliases: dict[str, str] = {}
183
+ for each_name, each_value in zip(target_names, all_values):
184
+ if not isinstance(each_value, ast.Attribute):
173
185
  continue
174
- if not isinstance(each_call.func, ast.Name):
186
+ if each_value.attr not in _SCOPE_CARRYING_ATTRIBUTE_NAMES:
175
187
  continue
176
- if each_call.func.id == "_fragment_or_deferred_check":
177
- all_argument_names = {
178
- each_name.id
179
- for each_argument in each_call.args[1:]
180
- for each_name in ast.walk(each_argument)
181
- if isinstance(each_name, ast.Name)
182
- }
183
- if (
184
- "defer_scope_to_caller" in all_argument_names
185
- and each_call.args
186
- and isinstance(each_call.args[0], ast.Name)
187
- ):
188
- all_rule_names.add(each_call.args[0].id)
188
+ aliases[each_name] = each_value.attr
189
+ return aliases
190
+
191
+
192
+ def _context_alias_map(function_node: ast.AST) -> dict[str, str]:
193
+ """Map each local name one function binds from ``context.<scope attribute>``.
194
+
195
+ A dispatch helper unpacks ``context.all_changed_lines`` and
196
+ ``context.defer_scope_to_caller`` into a short local name (``changed``,
197
+ ``defer``) before passing it on, so a call carrying that local name still
198
+ carries scope, even though the local name spells out none of it.
199
+ """
200
+ aliases: dict[str, str] = {}
201
+ for each_node in ast.walk(function_node):
202
+ if not isinstance(each_node, ast.Assign) or len(each_node.targets) != 1:
189
203
  continue
190
- if not each_call.func.id.startswith("check_"):
204
+ target_names = _assignment_target_names(each_node.targets[0])
205
+ if not target_names:
191
206
  continue
192
- all_argument_names = {
193
- each_name.id
194
- for each_argument in each_call.args
195
- for each_name in ast.walk(each_argument)
196
- if isinstance(each_name, ast.Name)
197
- }
198
- all_argument_names.update(
199
- each_name.id
200
- for each_keyword in each_call.keywords
201
- for each_name in ast.walk(each_keyword.value)
202
- if isinstance(each_name, ast.Name)
203
- )
204
- if (
205
- "all_changed_lines" in all_argument_names
206
- or "defer_scope_to_caller" in all_argument_names
207
- ):
208
- all_rule_names.add(each_call.func.id)
207
+ value = each_node.value
208
+ all_values = value.elts if isinstance(value, ast.Tuple) else [value]
209
+ aliases.update(_scope_aliases_for_pair(target_names, all_values))
210
+ return aliases
211
+
212
+
213
+ def _scope_names_for_reference(reference: ast.AST, aliases: dict[str, str]) -> set[str]:
214
+ """Return the scope-carrying attribute name one AST reference resolves to, if any."""
215
+ if isinstance(reference, ast.Name) and reference.id in aliases:
216
+ return {aliases[reference.id]}
217
+ if isinstance(reference, ast.Attribute) and reference.attr in _SCOPE_CARRYING_ATTRIBUTE_NAMES:
218
+ return {reference.attr}
219
+ return set()
220
+
221
+
222
+ def _referenced_scope_attribute_names(node: ast.AST, aliases: dict[str, str]) -> set[str]:
223
+ """Return every scope-carrying attribute name *node* references, direct or aliased."""
224
+ found: set[str] = set()
225
+ for each_reference in ast.walk(node):
226
+ found.update(_scope_names_for_reference(each_reference, aliases))
227
+ return found
228
+
229
+
230
+ def _fragment_deferred_rule_name(call_node: ast.Call, aliases: dict[str, str]) -> str | None:
231
+ """Return the rule name one ``_fragment_or_deferred_check`` call selects, if scoped."""
232
+ argument_scope_names = {
233
+ each_scope_name
234
+ for each_argument in call_node.args[1:]
235
+ for each_scope_name in _referenced_scope_attribute_names(each_argument, aliases)
236
+ }
237
+ if "defer_scope_to_caller" not in argument_scope_names:
238
+ return None
239
+ if not call_node.args or not isinstance(call_node.args[0], ast.Name):
240
+ return None
241
+ return call_node.args[0].id
242
+
243
+
244
+ def _check_call_rule_name(call_node: ast.Call, aliases: dict[str, str]) -> str | None:
245
+ """Return the rule name one direct ``check_*`` call selects, if it carries scope."""
246
+ func_name = call_node.func.id # type: ignore[attr-defined] # caller already narrowed func to ast.Name
247
+ if not func_name.startswith("check_"):
248
+ return None
249
+ all_call_arguments = (*call_node.args, *(each_keyword.value for each_keyword in call_node.keywords))
250
+ argument_scope_names = {
251
+ each_scope_name
252
+ for each_argument in all_call_arguments
253
+ for each_scope_name in _referenced_scope_attribute_names(each_argument, aliases)
254
+ }
255
+ return func_name if argument_scope_names else None
256
+
257
+
258
+ def _rule_names_from_function(function_node: ast.FunctionDef) -> set[str]:
259
+ """Return the check_* names *function_node* calls with scope-carrying arguments."""
260
+ aliases = _context_alias_map(function_node)
261
+ rule_names: set[str] = set()
262
+ for each_call in ast.walk(function_node):
263
+ if not isinstance(each_call, ast.Call) or not isinstance(each_call.func, ast.Name):
264
+ continue
265
+ if each_call.func.id == "_fragment_or_deferred_check":
266
+ rule_name = _fragment_deferred_rule_name(each_call, aliases)
267
+ else:
268
+ rule_name = _check_call_rule_name(each_call, aliases)
269
+ if rule_name is not None:
270
+ rule_names.add(rule_name)
271
+ return rule_names
272
+
273
+
274
+ def _scope_aware_rule_names_from_source() -> frozenset[str]:
275
+ """Return every check_* name dispatched anywhere in the enforcer with scope.
276
+
277
+ Walks every function in the enforcer module rather than only
278
+ ``validate_content``, since the extension-issue helper functions hold the
279
+ actual dispatch calls today.
280
+ """
281
+ enforcer_module = inspect.getmodule(validate_content)
282
+ assert enforcer_module is not None
283
+ module_tree = ast.parse(inspect.getsource(enforcer_module))
284
+ all_rule_names: set[str] = set()
285
+ for each_function in ast.walk(module_tree):
286
+ if isinstance(each_function, ast.FunctionDef):
287
+ all_rule_names.update(_rule_names_from_function(each_function))
209
288
  return frozenset(all_rule_names)
210
289
 
211
290
 
@@ -0,0 +1,121 @@
1
+ """Golden-output snapshot for the phase-split validators, guarding both phases.
2
+
3
+ Records the full-gate phase's ordered issue list for three representative
4
+ fixtures — a Python cross-file duplicate-body hit, a JavaScript boolean-naming
5
+ hit, and a Python-built-HTML orphan-CSS-class hit — so a decomposition that
6
+ reorders or drops a dispatch line shows up as a changed list, not a
7
+ still-green suite. Each fixture also records the edit-lane phase's list, which
8
+ matches the full-gate list except for the cross-file finding the edit lane
9
+ skips.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+
18
+
19
+ def _load_module_from_path(module_name: str, module_path: Path) -> ModuleType:
20
+ """Load and execute the module found at module_path, under module_name."""
21
+ specification = importlib.util.spec_from_file_location(module_name, module_path)
22
+ assert specification is not None
23
+ assert specification.loader is not None
24
+ module = importlib.util.module_from_spec(specification)
25
+ specification.loader.exec_module(module)
26
+ return module
27
+
28
+
29
+ _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
30
+ code_rules_enforcer = _load_module_from_path(
31
+ "code_rules_enforcer", _BLOCKING_DIRECTORY / "code_rules_enforcer.py"
32
+ )
33
+
34
+ _DUPLICATE_FUNCTION_SOURCE = (
35
+ "def compute_shipping_total(order_total: float, tax_rate: float) -> float:\n"
36
+ " taxed_total = order_total * (1 + tax_rate)\n"
37
+ " rounded_total = round(taxed_total, 2)\n"
38
+ " return rounded_total\n"
39
+ )
40
+ _JS_BOOLEAN_SOURCE = "const active = true;\n"
41
+ _CSS_ORPHAN_CLASS_SOURCE = (
42
+ '"""Render a status badge."""\n\n'
43
+ "STATUS_MARKUP = '<div class=\"status-badge\">Active</div>'\n"
44
+ "STYLE_BLOCK = '<style>.status-pill { color: green; }</style>'\n"
45
+ )
46
+
47
+ EXPECTED_PYTHON_CROSS_FILE_ISSUES = [
48
+ "Line 3: Magic value 2 - extract to named constant",
49
+ "Function 'compute_shipping_total' duplicates shipping_helper.py::compute_shipping_total"
50
+ " — this function body is identical to one in a sibling module; extract a single shared"
51
+ " helper (for example in hooks_constants/) and import it from both modules instead of"
52
+ " copying it (Reuse before create / DRY) (duplicate body span at line 1, spanning 4 lines)",
53
+ ]
54
+ EXPECTED_PYTHON_CROSS_FILE_EDIT_LANE_ISSUES = [
55
+ "Line 3: Magic value 2 - extract to named constant",
56
+ ]
57
+ EXPECTED_JS_ISSUES = [
58
+ "Line 1: Boolean active - prefix with is/has/should/can/was/did",
59
+ ]
60
+ EXPECTED_CSS_ISSUES = [
61
+ "Line 3: Constant STATUS_MARKUP - move to config/",
62
+ "Line 4: Constant STYLE_BLOCK - move to config/",
63
+ "Line 3: CSS class 'status-badge' used in markup has no matching '.status-badge' selector"
64
+ " - add a matching '.<class>' selector to the <style> block, or drop the unused class"
65
+ " attribute (CODE_RULES self-documenting markup)",
66
+ ]
67
+ EXPECTED_CSS_EDIT_LANE_ISSUES = [
68
+ "Line 3: Constant STATUS_MARKUP - move to config/",
69
+ "Line 4: Constant STYLE_BLOCK - move to config/",
70
+ ]
71
+
72
+
73
+ def test_python_cross_file_duplicate_snapshot_is_unchanged(tmp_path: Path) -> None:
74
+ """Both phases' ordered output for a cross-file duplicate stays fixed.
75
+
76
+ ``file_path`` is a fixed classification-only path, never opened, so the
77
+ real ``tmp_path`` sibling scan (which embeds this test's own name, a
78
+ ``test_`` match) never misclassifies the fixture as a test file.
79
+ """
80
+ sibling_dir = tmp_path / "sibling_dir"
81
+ sibling_dir.mkdir()
82
+ (sibling_dir / "shipping_helper.py").write_text(_DUPLICATE_FUNCTION_SOURCE, encoding="utf-8")
83
+ classification_only_file_path = "/repo/packages/demo/shipping/shipping_target.py"
84
+
85
+ full_gate_issues = code_rules_enforcer.validate_content_for_full_gate(
86
+ _DUPLICATE_FUNCTION_SOURCE,
87
+ classification_only_file_path,
88
+ "",
89
+ sibling_directory=sibling_dir,
90
+ )
91
+ edit_lane_issues = code_rules_enforcer.validate_content_for_edit_lane(
92
+ _DUPLICATE_FUNCTION_SOURCE,
93
+ classification_only_file_path,
94
+ "",
95
+ sibling_directory=sibling_dir,
96
+ )
97
+
98
+ assert full_gate_issues == EXPECTED_PYTHON_CROSS_FILE_ISSUES
99
+ assert edit_lane_issues == EXPECTED_PYTHON_CROSS_FILE_EDIT_LANE_ISSUES
100
+
101
+
102
+ def test_javascript_boolean_naming_snapshot_is_unchanged() -> None:
103
+ full_gate_issues = code_rules_enforcer.validate_content_for_full_gate(
104
+ _JS_BOOLEAN_SOURCE, "packages/app/frontend/status.js", ""
105
+ )
106
+ edit_lane_issues = code_rules_enforcer.validate_content_for_edit_lane(
107
+ _JS_BOOLEAN_SOURCE, "packages/app/frontend/status.js", ""
108
+ )
109
+ assert full_gate_issues == EXPECTED_JS_ISSUES
110
+ assert edit_lane_issues == EXPECTED_JS_ISSUES
111
+
112
+
113
+ def test_python_orphan_css_class_snapshot_is_unchanged() -> None:
114
+ full_gate_issues = code_rules_enforcer.validate_content_for_full_gate(
115
+ _CSS_ORPHAN_CLASS_SOURCE, "packages/app/widgets/status_widget.py", ""
116
+ )
117
+ edit_lane_issues = code_rules_enforcer.validate_content_for_edit_lane(
118
+ _CSS_ORPHAN_CLASS_SOURCE, "packages/app/widgets/status_widget.py", ""
119
+ )
120
+ assert full_gate_issues == EXPECTED_CSS_ISSUES
121
+ assert edit_lane_issues == EXPECTED_CSS_EDIT_LANE_ISSUES
@@ -7,6 +7,8 @@ import subprocess
7
7
  import sys
8
8
  from pathlib import Path
9
9
 
10
+ import pytest
11
+
10
12
  _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
11
13
  _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
12
14
  if _BLOCKING_DIRECTORY not in sys.path:
@@ -16,7 +18,7 @@ if _HOOKS_DIRECTORY not in sys.path:
16
18
 
17
19
  from code_rules_enforcer import ( # noqa: E402
18
20
  main,
19
- validate_content,
21
+ validate_content_for_full_gate as validate_content,
20
22
  )
21
23
  from code_rules_enforcer_test_support import ( # noqa: E402
22
24
  run_enforcer_cli,
@@ -115,7 +117,7 @@ def test_precheck_stream_parameter_carries_no_banned_noun() -> None:
115
117
 
116
118
 
117
119
  def test_precheck_reports_violations_and_exits_nonzero_for_production_candidate(
118
- tmp_path_factory: object,
120
+ tmp_path_factory: pytest.TempPathFactory,
119
121
  ) -> None:
120
122
  """A violating candidate judged at a production target prints each violation to
121
123
  stdout and exits 1."""
@@ -138,7 +140,7 @@ def test_precheck_reports_violations_and_exits_nonzero_for_production_candidate(
138
140
 
139
141
 
140
142
  def test_precheck_emits_no_output_and_exits_zero_for_clean_candidate(
141
- tmp_path_factory: object,
143
+ tmp_path_factory: pytest.TempPathFactory,
142
144
  ) -> None:
143
145
  """A clean candidate produces no stdout and exits 0."""
144
146
  staging_directory = getattr(tmp_path_factory, "mktemp")("staging")
@@ -155,7 +157,7 @@ def test_precheck_emits_no_output_and_exits_zero_for_clean_candidate(
155
157
 
156
158
 
157
159
  def test_precheck_target_path_drives_test_file_exemptions(
158
- tmp_path_factory: object,
160
+ tmp_path_factory: pytest.TempPathFactory,
159
161
  ) -> None:
160
162
  """Production-shaped content judged against a ``test_*.py`` target passes
161
163
  because test-file exemptions apply through the target path."""
@@ -172,7 +174,7 @@ def test_precheck_target_path_drives_test_file_exemptions(
172
174
 
173
175
 
174
176
  def test_precheck_noncode_target_exits_zero_with_no_output(
175
- tmp_path_factory: object,
177
+ tmp_path_factory: pytest.TempPathFactory,
176
178
  ) -> None:
177
179
  """A non-code target extension is exempt: the pre-check exits 0 with no output."""
178
180
  staging_directory = getattr(tmp_path_factory, "mktemp")("staging")
@@ -186,7 +188,7 @@ def test_precheck_noncode_target_exits_zero_with_no_output(
186
188
 
187
189
 
188
190
  def test_precheck_missing_candidate_errors_on_stderr_without_traceback(
189
- tmp_path_factory: object,
191
+ tmp_path_factory: pytest.TempPathFactory,
190
192
  ) -> None:
191
193
  """A nonexistent candidate path prints a one-line stderr error and exits
192
194
  nonzero without a Python traceback."""
@@ -204,7 +206,7 @@ def test_precheck_missing_candidate_errors_on_stderr_without_traceback(
204
206
 
205
207
 
206
208
  def test_edit_deny_reason_includes_forecast_and_precheck_hint(
207
- tmp_path_factory: object,
209
+ tmp_path_factory: pytest.TempPathFactory,
208
210
  ) -> None:
209
211
  """An Edit whose new_string introduces a violation on a file that already
210
212
  contains a separate violation elsewhere blocks on the fragment violation and
@@ -236,7 +238,7 @@ def test_edit_deny_reason_includes_forecast_and_precheck_hint(
236
238
 
237
239
 
238
240
  def test_edit_clean_fragment_on_dirty_file_produces_no_deny_payload(
239
- tmp_path_factory: object,
241
+ tmp_path_factory: pytest.TempPathFactory,
240
242
  ) -> None:
241
243
  """A clean Edit fragment on a file that is dirty elsewhere must not block —
242
244
  the forecast never converts a clean-fragment edit into a deny."""
@@ -258,7 +260,7 @@ def test_edit_clean_fragment_on_dirty_file_produces_no_deny_payload(
258
260
 
259
261
 
260
262
  def test_every_deny_reason_carries_the_precheck_hint(
261
- tmp_path_factory: object,
263
+ tmp_path_factory: pytest.TempPathFactory,
262
264
  ) -> None:
263
265
  """A deny with no forecast still appends the pre-check hint to the reason."""
264
266
  production_directory = getattr(tmp_path_factory, "mktemp")("production_pkg")
@@ -287,7 +289,7 @@ def test_every_deny_reason_carries_the_precheck_hint(
287
289
 
288
290
 
289
291
  def test_forecast_skipped_when_edit_prior_is_unreconstructable(
290
- tmp_path_factory: object,
292
+ tmp_path_factory: pytest.TempPathFactory,
291
293
  ) -> None:
292
294
  """An Edit whose old_string is absent from the file has no reliable prior to
293
295
  diff against, so the full-file forecast must not run: a pre-existing inline
@@ -313,7 +315,7 @@ def test_forecast_skipped_when_edit_prior_is_unreconstructable(
313
315
 
314
316
 
315
317
  def test_forecast_omits_the_fragment_introduced_violation(
316
- tmp_path_factory: object,
318
+ tmp_path_factory: pytest.TempPathFactory,
317
319
  ) -> None:
318
320
  """An Edit that introduces the file's only print() blocks on it without the
319
321
  forecast re-listing that same violation under its full-file line number."""
@@ -353,7 +355,7 @@ def test_precheck_missing_candidate_value_exits_two_with_usage() -> None:
353
355
 
354
356
 
355
357
  def test_precheck_flag_shaped_candidate_value_exits_two_with_usage(
356
- tmp_path_factory: object,
358
+ tmp_path_factory: pytest.TempPathFactory,
357
359
  ) -> None:
358
360
  """``--check`` immediately followed by ``--as`` is a usage error rather than
359
361
  an attempt to read a file literally named ``--as``."""
@@ -370,7 +372,7 @@ def test_precheck_flag_shaped_candidate_value_exits_two_with_usage(
370
372
 
371
373
 
372
374
  def test_precheck_rejects_unrecognized_trailing_token_with_usage(
373
- tmp_path_factory: object,
375
+ tmp_path_factory: pytest.TempPathFactory,
374
376
  ) -> None:
375
377
  """A pre-check vector carrying a token beyond the supported
376
378
  ``--check <candidate> [--as <target>]`` shape is a usage error: an extra
@@ -393,7 +395,7 @@ def test_precheck_rejects_unrecognized_trailing_token_with_usage(
393
395
 
394
396
 
395
397
  def test_precheck_strips_candidate_byte_order_mark_before_validation(
396
- tmp_path_factory: object,
398
+ tmp_path_factory: pytest.TempPathFactory,
397
399
  ) -> None:
398
400
  """A UTF-8 byte-order mark on the candidate must not hide AST-based
399
401
  violations: the candidate is judged exactly as its decoded content would
@@ -415,7 +417,7 @@ def test_precheck_strips_candidate_byte_order_mark_before_validation(
415
417
 
416
418
 
417
419
  def test_precheck_strips_every_leading_byte_order_mark_before_validation(
418
- tmp_path_factory: object,
420
+ tmp_path_factory: pytest.TempPathFactory,
419
421
  ) -> None:
420
422
  """Stacked byte-order marks must not hide AST-based violations: every
421
423
  leading mark is stripped, so a double-BOM candidate fails exactly like its
@@ -6,6 +6,8 @@ import sys
6
6
  from pathlib import Path
7
7
  from types import SimpleNamespace
8
8
 
9
+ import pytest
10
+
9
11
  _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
10
12
  _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
11
13
  if _BLOCKING_DIRECTORY not in sys.path:
@@ -17,7 +19,7 @@ from code_rules_annotations_length import ( # noqa: E402
17
19
  FUNCTION_LENGTH_BLOCKING_THRESHOLD,
18
20
  )
19
21
  from code_rules_enforcer import ( # noqa: E402
20
- validate_content,
22
+ validate_content_for_full_gate as validate_content,
21
23
  )
22
24
 
23
25
  code_rules_enforcer = SimpleNamespace(
@@ -38,7 +40,7 @@ def _oversized_function_source(name: str) -> str:
38
40
 
39
41
 
40
42
  def test_should_emit_advisory_for_duplicated_format_patterns_via_validate_content(
41
- capsys: object,
43
+ capsys: pytest.CaptureFixture[str],
42
44
  ) -> None:
43
45
  repeated_pattern_source = (
44
46
  "def get_user(user_id: str) -> str:\n"
@@ -22,7 +22,7 @@ from code_rules_annotations_length import ( # noqa: E402
22
22
  from code_rules_enforcer import ( # noqa: E402
23
23
  main,
24
24
  prior_and_post_edit_content,
25
- validate_content,
25
+ validate_content_for_full_gate as validate_content,
26
26
  )
27
27
  from code_rules_enforcer_test_support import ( # noqa: E402
28
28
  run_serialized_payload_entrypoint,
@@ -25,7 +25,7 @@ if _PR_LOOP_SCRIPTS_DIRECTORY not in sys.path:
25
25
  from code_rules_enforcer import ( # noqa: E402
26
26
  _fragment_or_deferred_check,
27
27
  main,
28
- validate_content,
28
+ validate_content_for_full_gate as validate_content,
29
29
  )
30
30
  from code_rules_gate_parts import gate_running # noqa: E402
31
31
  from code_rules_gate_parts.tests._repo_test_helpers import ( # noqa: E402
@@ -248,7 +248,7 @@ def test_should_grandfather_unchanged_string_magic_in_wide_edit() -> None:
248
248
  " # wide edit that leaves the magic string in place\n"
249
249
  " return os.environ['STRIPE_SECRET']\n"
250
250
  )
251
- issues = code_rules_enforcer.validate_content(
251
+ issues = code_rules_enforcer.validate_content_for_full_gate(
252
252
  proposed_fragment,
253
253
  PRODUCTION_FILE_PATH,
254
254
  prior_fragment,
@@ -272,7 +272,7 @@ def test_should_block_new_string_magic_in_wide_edit() -> None:
272
272
  "def fetch_secret() -> str:\n"
273
273
  " return os.environ['STRIPE_SECRET']\n"
274
274
  )
275
- issues = code_rules_enforcer.validate_content(
275
+ issues = code_rules_enforcer.validate_content_for_full_gate(
276
276
  proposed_fragment,
277
277
  PRODUCTION_FILE_PATH,
278
278
  prior_fragment,
@@ -300,7 +300,7 @@ def test_should_block_second_identical_string_magic_per_occurrence() -> None:
300
300
  "def fetch_backup() -> str:\n"
301
301
  " return os.environ['STRIPE_SECRET']\n"
302
302
  )
303
- issues = code_rules_enforcer.validate_content(
303
+ issues = code_rules_enforcer.validate_content_for_full_gate(
304
304
  proposed_fragment,
305
305
  PRODUCTION_FILE_PATH,
306
306
  prior_fragment,
@@ -320,7 +320,7 @@ def test_should_surface_magic_when_gate_defers_scope_with_head_as_old() -> None:
320
320
  " old_number = 9999\n"
321
321
  " return old_number\n"
322
322
  )
323
- issues = code_rules_enforcer.validate_content(
323
+ issues = code_rules_enforcer.validate_content_for_full_gate(
324
324
  source,
325
325
  PRODUCTION_FILE_PATH,
326
326
  source,
@@ -149,7 +149,7 @@ def test_should_flag_thin_wrapper_with_module_docstring() -> None:
149
149
  )
150
150
 
151
151
 
152
- def test_validate_content_uses_empty_full_file_content_over_pre_edit_fragment() -> None:
152
+ def test_validate_content_for_full_gate_uses_empty_full_file_content_over_pre_edit_fragment() -> None:
153
153
  """An empty-string `full_file_content` must be honored, not silently replaced with `content`.
154
154
 
155
155
  Regression for loop1-8: the `or` short-circuit at line 3775 collapsed
@@ -159,7 +159,7 @@ def test_validate_content_uses_empty_full_file_content_over_pre_edit_fragment()
159
159
  not a thin wrapper, but a pre-edit fragment with imports + __all__ is.
160
160
  """
161
161
  pre_edit_fragment = "from real_module import do_thing\n__all__ = ['do_thing']\n"
162
- issues = code_rules_enforcer.validate_content(
162
+ issues = code_rules_enforcer.validate_content_for_full_gate(
163
163
  pre_edit_fragment,
164
164
  PRODUCTION_FILE_PATH,
165
165
  full_file_content="",
@@ -240,7 +240,7 @@ def test_should_not_flag_recursive_self_call() -> None:
240
240
  )
241
241
 
242
242
 
243
- def test_should_flag_through_validate_content_for_hook_file() -> None:
243
+ def test_should_flag_through_validate_content_for_full_gate_for_hook_file() -> None:
244
244
  source = (
245
245
  "def find_bare_path_segments(content: str) -> set[str]:\n"
246
246
  " return {part for part in content.split() if part}\n"
@@ -248,7 +248,7 @@ def test_should_flag_through_validate_content_for_hook_file() -> None:
248
248
  "def find_bare_index_segments(content: str) -> set[str]:\n"
249
249
  " return find_bare_path_segments(content)\n"
250
250
  )
251
- issues = code_rules_enforcer.validate_content(source, HOOK_INFRASTRUCTURE_PATH)
251
+ issues = code_rules_enforcer.validate_content_for_full_gate(source, HOOK_INFRASTRUCTURE_PATH)
252
252
  assert any("find_bare_index_segments" in each for each in issues), (
253
253
  f"validate_content must surface the alias for hook files, got: {issues!r}"
254
254
  )
@@ -3,7 +3,7 @@
3
3
  A pass-through alias inside a hook module is the motivating case for the
4
4
  zero-payload-alias check, so the deny must fire on the same PreToolUse path a
5
5
  live Write into ``packages/claude-dev-env/hooks/blocking`` would take — not only
6
- through ``validate_content``, which hook files never reach at PreToolUse. These
6
+ through ``validate_content_for_edit_lane``, which hook files never reach at PreToolUse. These
7
7
  tests drive the real ``main()`` stdin entry point and the pre-check CLI with a
8
8
  hook-infrastructure target.
9
9
 
@@ -66,7 +66,7 @@ def test_write_of_pass_through_alias_into_hook_directory_denies(
66
66
  The target lives under a hook-infrastructure path the full code-rules suite
67
67
  exempts, so this proves the zero-payload-alias check still fires on the exact
68
68
  directory its docstring names as the motivating case — at the PreToolUse Write
69
- point, not only through ``validate_content``."""
69
+ point, not only through ``validate_content_for_edit_lane``."""
70
70
  new_file = hook_blocking_dir / "new_blocker.py"
71
71
  captured_stdout, _exit_code = run_write_entrypoint(
72
72
  main,