claude-dev-env 2.4.0 → 2.5.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.
- package/CLAUDE.md +53 -49
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +2 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +0 -1
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -44
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +167 -0
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/scripts/test_invoke_code_review.py +0 -966
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Ruff integration for fast Python linting."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
5
6
|
from dataclasses import dataclass
|
|
@@ -16,13 +17,25 @@ except ModuleNotFoundError:
|
|
|
16
17
|
from pyproject_config_discovery import find_pyproject_configuring_tool
|
|
17
18
|
|
|
18
19
|
try:
|
|
20
|
+
from hooks_constants.blocking_check_limits import RUFF_STDIN_ENCODING
|
|
19
21
|
from hooks_constants.mypy_integration_constants import PYTHON_SOURCE_SUFFIX
|
|
20
22
|
from hooks_constants.pyproject_config_discovery_constants import RUFF_TOOL_TABLE_NAME
|
|
23
|
+
from hooks_constants.ruff_integration_constants import (
|
|
24
|
+
FORCE_COLOR_ENVIRONMENT_VARIABLE_NAME,
|
|
25
|
+
NO_COLOR_ENABLED_VALUE,
|
|
26
|
+
NO_COLOR_ENVIRONMENT_VARIABLE_NAME,
|
|
27
|
+
)
|
|
21
28
|
except ModuleNotFoundError:
|
|
22
29
|
if _hooks_directory not in sys.path:
|
|
23
30
|
sys.path.insert(0, _hooks_directory)
|
|
31
|
+
from hooks_constants.blocking_check_limits import RUFF_STDIN_ENCODING
|
|
24
32
|
from hooks_constants.mypy_integration_constants import PYTHON_SOURCE_SUFFIX
|
|
25
33
|
from hooks_constants.pyproject_config_discovery_constants import RUFF_TOOL_TABLE_NAME
|
|
34
|
+
from hooks_constants.ruff_integration_constants import (
|
|
35
|
+
FORCE_COLOR_ENVIRONMENT_VARIABLE_NAME,
|
|
36
|
+
NO_COLOR_ENABLED_VALUE,
|
|
37
|
+
NO_COLOR_ENVIRONMENT_VARIABLE_NAME,
|
|
38
|
+
)
|
|
26
39
|
|
|
27
40
|
|
|
28
41
|
@dataclass
|
|
@@ -46,6 +59,19 @@ def check_ruff_available() -> bool:
|
|
|
46
59
|
return False
|
|
47
60
|
|
|
48
61
|
|
|
62
|
+
def _ruff_subprocess_environment() -> dict[str, str]:
|
|
63
|
+
"""Return an environment that forces plain, non-ANSI ruff diagnostics.
|
|
64
|
+
|
|
65
|
+
The PreToolUse gate parses ``path:line:col:`` prefixes. ANSI color codes
|
|
66
|
+
wrap those fields when FORCE_COLOR is set, so line numbers no longer parse
|
|
67
|
+
and baseline scoping fail-closes every ruff finding as new.
|
|
68
|
+
"""
|
|
69
|
+
ruff_environment = os.environ.copy()
|
|
70
|
+
ruff_environment[NO_COLOR_ENVIRONMENT_VARIABLE_NAME] = NO_COLOR_ENABLED_VALUE
|
|
71
|
+
ruff_environment.pop(FORCE_COLOR_ENVIRONMENT_VARIABLE_NAME, None)
|
|
72
|
+
return ruff_environment
|
|
73
|
+
|
|
74
|
+
|
|
49
75
|
def find_pyproject_with_ruff_config(starting_file: Path) -> Path | None:
|
|
50
76
|
"""Walk up from a starting file to a pyproject.toml that configures ruff.
|
|
51
77
|
|
|
@@ -108,9 +134,14 @@ def _config_relative_stdin_filename(target_path: Path, config_directory: Path) -
|
|
|
108
134
|
The config-relative POSIX path when the target sits under the config
|
|
109
135
|
directory, else the target's basename.
|
|
110
136
|
"""
|
|
137
|
+
try:
|
|
138
|
+
return target_path.relative_to(config_directory).as_posix()
|
|
139
|
+
except ValueError:
|
|
140
|
+
pass
|
|
111
141
|
resolved_target = target_path.resolve()
|
|
142
|
+
resolved_config_directory = config_directory.resolve()
|
|
112
143
|
try:
|
|
113
|
-
return resolved_target.relative_to(
|
|
144
|
+
return resolved_target.relative_to(resolved_config_directory).as_posix()
|
|
114
145
|
except ValueError:
|
|
115
146
|
return resolved_target.name
|
|
116
147
|
|
|
@@ -151,14 +182,25 @@ def _staged_ruff_command(resolved_pyproject: Path, stdin_filename: str) -> list[
|
|
|
151
182
|
def _staged_ruff_result(
|
|
152
183
|
resolved_pyproject: Path, stdin_filename: str, staged_content: str
|
|
153
184
|
) -> RuffResult:
|
|
154
|
-
"""Pipe *staged_content* to ruff from the config directory and wrap the result.
|
|
185
|
+
"""Pipe *staged_content* to ruff from the config directory and wrap the result.
|
|
186
|
+
|
|
187
|
+
``encoding`` pins the stdin encode to UTF-8, which ruff requires, regardless
|
|
188
|
+
of the host locale codec. ``errors="replace"`` keeps the stdout/stderr decode
|
|
189
|
+
tolerant: a stray non-UTF-8 byte in ruff's output becomes the replacement
|
|
190
|
+
character rather than raising, which under strict decoding drops the whole
|
|
191
|
+
stream on Windows and raises out of ``subprocess.run`` on POSIX. ``replace``
|
|
192
|
+
is chosen over ``surrogateescape`` so the decoded text never plants a lone
|
|
193
|
+
surrogate that would raise at a later strict re-encode.
|
|
194
|
+
"""
|
|
155
195
|
result = subprocess.run(
|
|
156
196
|
_staged_ruff_command(resolved_pyproject, stdin_filename),
|
|
157
197
|
check=False,
|
|
158
198
|
capture_output=True,
|
|
159
|
-
|
|
199
|
+
encoding=RUFF_STDIN_ENCODING,
|
|
200
|
+
errors="replace",
|
|
160
201
|
input=staged_content,
|
|
161
202
|
cwd=str(resolved_pyproject.parent),
|
|
203
|
+
env=_ruff_subprocess_environment(),
|
|
162
204
|
)
|
|
163
205
|
return RuffResult(
|
|
164
206
|
passed=result.returncode == 0,
|
|
@@ -204,6 +246,7 @@ def _run_native_ruff_check(
|
|
|
204
246
|
check=False,
|
|
205
247
|
capture_output=True,
|
|
206
248
|
text=True,
|
|
249
|
+
env=_ruff_subprocess_environment(),
|
|
207
250
|
)
|
|
208
251
|
return RuffResult(
|
|
209
252
|
passed=result.returncode == 0,
|
|
@@ -250,12 +293,12 @@ def _parse_fixed_count(fix_output: str) -> int:
|
|
|
250
293
|
return 0
|
|
251
294
|
|
|
252
295
|
|
|
253
|
-
def run_ruff_fix(
|
|
296
|
+
def run_ruff_fix(all_files: list[Path]) -> RuffResult:
|
|
254
297
|
"""Run ruff with --fix to auto-fix violations."""
|
|
255
298
|
if not check_ruff_available():
|
|
256
299
|
return RuffResult(passed=True, output="Ruff not installed", fixed_count=0)
|
|
257
300
|
py_files = [
|
|
258
|
-
str(each_file) for each_file in
|
|
301
|
+
str(each_file) for each_file in all_files if each_file.suffix == PYTHON_SOURCE_SUFFIX
|
|
259
302
|
]
|
|
260
303
|
if not py_files:
|
|
261
304
|
return RuffResult(passed=True, output="No Python files", fixed_count=0)
|
|
@@ -264,6 +307,7 @@ def run_ruff_fix(files: list[Path]) -> RuffResult:
|
|
|
264
307
|
check=False,
|
|
265
308
|
capture_output=True,
|
|
266
309
|
text=True,
|
|
310
|
+
env=_ruff_subprocess_environment(),
|
|
267
311
|
)
|
|
268
312
|
return RuffResult(
|
|
269
313
|
passed=result.returncode == 0,
|
|
@@ -19,6 +19,11 @@ from datetime import datetime
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import Callable, Dict, List, Optional, Tuple
|
|
21
21
|
|
|
22
|
+
from .config.directory_exemption_constants import (
|
|
23
|
+
ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES,
|
|
24
|
+
ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS,
|
|
25
|
+
ALL_SYSTEM_TEMPORARY_ROOT_ENVIRONMENT_VARIABLE_NAMES,
|
|
26
|
+
)
|
|
22
27
|
from .health_check import get_system_health, get_validator_version, print_health_report
|
|
23
28
|
from .mypy_integration import check_mypy_available, run_mypy_check
|
|
24
29
|
from .output_formatter import OutputFormatter, OutputMode, ValidatorResultDict
|
|
@@ -763,29 +768,221 @@ def run_file_scoped_validators(
|
|
|
763
768
|
]
|
|
764
769
|
|
|
765
770
|
|
|
771
|
+
def _escapes_temporary_root(path_part: str) -> bool:
|
|
772
|
+
"""Return True when a path part would climb out of or re-anchor the temp root.
|
|
773
|
+
|
|
774
|
+
::
|
|
775
|
+
|
|
776
|
+
ok: "scripts" -> False (stays inside)
|
|
777
|
+
flag: ".." -> True (climbs a level)
|
|
778
|
+
flag: "/etc" -> True (absolute, re-anchors the join)
|
|
779
|
+
|
|
780
|
+
A ``..`` component escapes upward and an absolute or anchored component
|
|
781
|
+
re-roots the join away from the temporary directory. Both are dropped so a
|
|
782
|
+
staged file cannot land outside the ephemeral root.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
path_part: A single path component drawn from the destination path.
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
True when the component must be dropped to keep staging contained.
|
|
789
|
+
"""
|
|
790
|
+
if path_part == os.pardir:
|
|
791
|
+
return True
|
|
792
|
+
part_as_path = Path(path_part)
|
|
793
|
+
return part_as_path.is_absolute() or bool(part_as_path.anchor)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _all_system_temporary_roots() -> tuple[Path, ...]:
|
|
797
|
+
"""Return resolved roots that count as system temporary directories.
|
|
798
|
+
|
|
799
|
+
::
|
|
800
|
+
|
|
801
|
+
gettempdir() plus TEMP / TMP / TMPDIR / RUNNER_TEMP when set.
|
|
802
|
+
|
|
803
|
+
GitHub Actions puts pytest basetemp under ``RUNNER_TEMP``
|
|
804
|
+
(``/home/runner/work/_temp``) while ``tempfile.gettempdir()`` is ``/tmp``.
|
|
805
|
+
Both must count so pytest-shaped ``test_*`` parents disable substring
|
|
806
|
+
exemption matching during staging.
|
|
807
|
+
"""
|
|
808
|
+
all_candidate_roots: list[str] = [tempfile.gettempdir()]
|
|
809
|
+
for each_environment_name in ALL_SYSTEM_TEMPORARY_ROOT_ENVIRONMENT_VARIABLE_NAMES:
|
|
810
|
+
environment_value = os.environ.get(each_environment_name)
|
|
811
|
+
if environment_value:
|
|
812
|
+
all_candidate_roots.append(environment_value)
|
|
813
|
+
all_resolved_roots: list[Path] = []
|
|
814
|
+
for each_candidate in all_candidate_roots:
|
|
815
|
+
try:
|
|
816
|
+
all_resolved_roots.append(Path(each_candidate).resolve())
|
|
817
|
+
except OSError:
|
|
818
|
+
continue
|
|
819
|
+
return tuple(all_resolved_roots)
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def _is_absolute_path_under_system_temporary_directory(file_path: str) -> bool:
|
|
823
|
+
"""Return True when *file_path* is absolute and resolves under an OS temp root.
|
|
824
|
+
|
|
825
|
+
::
|
|
826
|
+
|
|
827
|
+
ok: "C:/Users/.../Temp/pytest-of-x/test_foo0/a.py" -> True
|
|
828
|
+
ok: "/home/runner/work/_temp/pytest-basetemp/.../a.py" when RUNNER_TEMP
|
|
829
|
+
flag: "packages/demo/test_helpers/worker.py" -> False (relative)
|
|
830
|
+
flag: "C:/repo/pkg/test_helpers/worker.py" -> False (project)
|
|
831
|
+
|
|
832
|
+
Absolute paths under system temporary directories often carry pytest
|
|
833
|
+
``tmp_path`` segments named ``test_*``. Those segments must not be treated
|
|
834
|
+
as project exemption signals, or a production file staged from a pytest
|
|
835
|
+
path would inherit a false test-file exemption.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
file_path: Destination path the write or edit targets.
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
True when *file_path* is absolute and lives under ``tempfile.gettempdir()``
|
|
842
|
+
or a known temporary-root environment variable (``TEMP``, ``TMP``,
|
|
843
|
+
``TMPDIR``, ``RUNNER_TEMP``).
|
|
844
|
+
"""
|
|
845
|
+
destination_path = Path(file_path)
|
|
846
|
+
if not destination_path.is_absolute():
|
|
847
|
+
return False
|
|
848
|
+
try:
|
|
849
|
+
resolved_destination = destination_path.resolve()
|
|
850
|
+
except OSError:
|
|
851
|
+
return False
|
|
852
|
+
return any(
|
|
853
|
+
resolved_destination.is_relative_to(each_temporary_root)
|
|
854
|
+
for each_temporary_root in _all_system_temporary_roots()
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _directory_segment_signals_exemption(
|
|
859
|
+
segment_lower: str, *, is_substring_match_enabled: bool
|
|
860
|
+
) -> bool:
|
|
861
|
+
"""Return True when a directory segment carries a path-exemption signal.
|
|
862
|
+
|
|
863
|
+
::
|
|
864
|
+
|
|
865
|
+
ok: "test_helpers" (substring on) -> True (contains the ``test_`` fragment)
|
|
866
|
+
ok: "scripts" -> True (exact CLI-marker segment name)
|
|
867
|
+
flag: "latest" -> False (neither exact name nor a fragment)
|
|
868
|
+
flag: "test_edit_foo0" (substring off) -> False (pytest-shaped; exact-only mode)
|
|
869
|
+
|
|
870
|
+
A segment is kept during staging when its name exactly matches an exemption
|
|
871
|
+
directory name. When *is_substring_match_enabled* is True, a separator-free
|
|
872
|
+
substring pattern that ``is_test_file`` matches anywhere in a path also
|
|
873
|
+
counts — that is what preserves ``pkg/test_helpers/``. Substring matching is
|
|
874
|
+
disabled for absolute paths under the system temporary directory so pytest
|
|
875
|
+
``tmp_path`` parents named ``test_*`` do not falsely trip test exemptions.
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
segment_lower: One lowercased directory component of the target path.
|
|
879
|
+
is_substring_match_enabled: When False, only exact exemption names count.
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
True when staging must keep this segment to reproduce the exemption.
|
|
883
|
+
"""
|
|
884
|
+
if segment_lower in ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES:
|
|
885
|
+
return True
|
|
886
|
+
if not is_substring_match_enabled:
|
|
887
|
+
return False
|
|
888
|
+
return any(
|
|
889
|
+
each_pattern in segment_lower
|
|
890
|
+
for each_pattern in ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def _temporary_path_preserving_directory_signal(
|
|
895
|
+
temporary_directory: Path, file_path: str
|
|
896
|
+
) -> Path:
|
|
897
|
+
"""Build a temp path that keeps exemption directory tails and the basename.
|
|
898
|
+
|
|
899
|
+
Directory-name exemptions (``/scripts/``, ``/tests/``) and separator-free
|
|
900
|
+
path fragments (``test_`` inside ``test_helpers``) match substrings of the
|
|
901
|
+
real file path. Staging under a flat temp basename drops those segments.
|
|
902
|
+
Mirroring the first exemption-signaling directory through the basename
|
|
903
|
+
restores that signal. Substring fragment matching is skipped for absolute
|
|
904
|
+
paths under the system temporary directory so pytest ``tmp_path`` parents
|
|
905
|
+
that contain ``test_`` do not falsely trip test-file exemptions; exact
|
|
906
|
+
exemption directory names (``config``, ``scripts``, ``tests``, ...) still
|
|
907
|
+
anchor staging on those absolute temp paths.
|
|
908
|
+
|
|
909
|
+
Args:
|
|
910
|
+
temporary_directory: Root of the ephemeral staging tree.
|
|
911
|
+
file_path: Real destination path the write or edit targets.
|
|
912
|
+
|
|
913
|
+
Returns:
|
|
914
|
+
Path under *temporary_directory* ending in the exemption directory tail
|
|
915
|
+
(when present) and the real basename.
|
|
916
|
+
"""
|
|
917
|
+
destination_path = Path(file_path)
|
|
918
|
+
path_parts = destination_path.parts
|
|
919
|
+
if destination_path.anchor:
|
|
920
|
+
path_parts = path_parts[1:]
|
|
921
|
+
if not path_parts:
|
|
922
|
+
path_parts = (destination_path.name,)
|
|
923
|
+
|
|
924
|
+
is_substring_match_enabled = not _is_absolute_path_under_system_temporary_directory(
|
|
925
|
+
file_path
|
|
926
|
+
)
|
|
927
|
+
start_index = len(path_parts) - 1
|
|
928
|
+
for each_index, each_part in enumerate(path_parts[:-1]):
|
|
929
|
+
part_lower = each_part.lower()
|
|
930
|
+
if not _directory_segment_signals_exemption(
|
|
931
|
+
part_lower, is_substring_match_enabled=is_substring_match_enabled
|
|
932
|
+
):
|
|
933
|
+
continue
|
|
934
|
+
start_index = each_index
|
|
935
|
+
break
|
|
936
|
+
|
|
937
|
+
selected_parts = path_parts[start_index:]
|
|
938
|
+
contained_parts = tuple(
|
|
939
|
+
each_part for each_part in selected_parts if not _escapes_temporary_root(each_part)
|
|
940
|
+
)
|
|
941
|
+
if not contained_parts:
|
|
942
|
+
contained_parts = (destination_path.name,)
|
|
943
|
+
temporary_file = temporary_directory.joinpath(*contained_parts)
|
|
944
|
+
if not temporary_file.resolve().is_relative_to(temporary_directory.resolve()):
|
|
945
|
+
temporary_file = temporary_directory / destination_path.name
|
|
946
|
+
temporary_file.parent.mkdir(parents=True, exist_ok=True)
|
|
947
|
+
return temporary_file
|
|
948
|
+
|
|
949
|
+
|
|
766
950
|
def validate_proposed_file(
|
|
767
|
-
file_path: str,
|
|
951
|
+
file_path: str,
|
|
952
|
+
proposed_content: str,
|
|
953
|
+
config_source_path: Optional[Path] = None,
|
|
768
954
|
) -> List[ValidatorResult]:
|
|
769
955
|
"""Validate *proposed_content* as if written to *file_path*.
|
|
770
956
|
|
|
771
|
-
Writes the content to a temporary file that
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
957
|
+
Writes the content to a temporary file that preserves the exemption-relevant
|
|
958
|
+
directory tail and basename so directory-based exemptions, suffix-based
|
|
959
|
+
filtering, and test-name-based filtering match the real path, then runs the
|
|
960
|
+
file-scoped validators against it. Ruff and mypy resolve their config by
|
|
961
|
+
walking up from the config source path, so the staged copy is graded under
|
|
962
|
+
the project config the real path sits in rather than the temp directory's.
|
|
776
963
|
|
|
777
964
|
Args:
|
|
778
965
|
file_path: The destination path the write or edit targets.
|
|
779
966
|
proposed_content: The reconstructed post-edit content of that file.
|
|
967
|
+
config_source_path: Path ruff and mypy resolve their config from;
|
|
968
|
+
defaults to *file_path* when the caller passes nothing.
|
|
780
969
|
|
|
781
970
|
Returns:
|
|
782
971
|
One ValidatorResult per file-scoped validator.
|
|
783
972
|
"""
|
|
784
|
-
base_name = Path(file_path).name
|
|
785
973
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
786
|
-
temporary_file =
|
|
974
|
+
temporary_file = _temporary_path_preserving_directory_signal(
|
|
975
|
+
Path(temporary_directory), file_path
|
|
976
|
+
)
|
|
787
977
|
temporary_file.write_text(proposed_content, encoding="utf-8")
|
|
788
|
-
|
|
978
|
+
resolved_config_source_path = (
|
|
979
|
+
config_source_path
|
|
980
|
+
if config_source_path is not None
|
|
981
|
+
else Path(file_path)
|
|
982
|
+
)
|
|
983
|
+
return run_file_scoped_validators(
|
|
984
|
+
[temporary_file], config_source_path=resolved_config_source_path
|
|
985
|
+
)
|
|
789
986
|
|
|
790
987
|
|
|
791
988
|
def _validator_summaries(results: List[ValidatorResult]) -> str:
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Tests for directory-exemption segment derivation.
|
|
2
|
+
|
|
3
|
+
Proves segment names come from authoritative path-exemption patterns and CLI
|
|
4
|
+
markers rather than hand-copied directory-name literals.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from hooks_constants.code_rules_enforcer_constants import ALL_CLI_FILE_PATH_MARKERS
|
|
14
|
+
from hooks_constants.code_rules_path_utils_constants import ALL_CONFIG_DIRECTORY_NAMES
|
|
15
|
+
from validators.config.directory_exemption_constants import (
|
|
16
|
+
ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES,
|
|
17
|
+
ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS,
|
|
18
|
+
ALL_DIRECTORY_SEGMENTS_FROM_PATH_PATTERNS,
|
|
19
|
+
all_directory_segments_in_path_pattern,
|
|
20
|
+
directory_segment_names_from_path_patterns,
|
|
21
|
+
is_filename_like_path_segment,
|
|
22
|
+
substring_patterns_from_path_patterns,
|
|
23
|
+
)
|
|
24
|
+
from validators.exempt_paths import (
|
|
25
|
+
HOOK_INFRASTRUCTURE_PATTERNS,
|
|
26
|
+
MIGRATION_PATH_PATTERNS,
|
|
27
|
+
TEST_PATH_PATTERNS,
|
|
28
|
+
WORKFLOW_REGISTRY_PATTERNS,
|
|
29
|
+
)
|
|
30
|
+
from validators.run_all_validators import _temporary_path_preserving_directory_signal
|
|
31
|
+
|
|
32
|
+
ALL_KNOWN_DIRECTORY_EXEMPTION_SEGMENTS = frozenset(
|
|
33
|
+
{"scripts", "tests", "migrations", "workflow", "hooks"}
|
|
34
|
+
)
|
|
35
|
+
ALL_SYNTHETIC_DIRECTORY_PATTERNS = frozenset(
|
|
36
|
+
{"/foo/", "\\bar\\", "/baz.py", "plain", "/.hidden/qux/"}
|
|
37
|
+
)
|
|
38
|
+
ALL_EXPECTED_SYNTHETIC_SEGMENTS = frozenset({"foo", "bar", ".hidden", "qux"})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_is_filename_like_path_segment_detects_extensions() -> None:
|
|
42
|
+
assert is_filename_like_path_segment("states.py") is True
|
|
43
|
+
assert is_filename_like_path_segment("cli.py") is True
|
|
44
|
+
assert is_filename_like_path_segment("tests") is False
|
|
45
|
+
assert is_filename_like_path_segment(".claude") is False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_all_directory_segments_in_path_pattern_keeps_directory_tokens_only() -> None:
|
|
49
|
+
assert all_directory_segments_in_path_pattern("/.claude/hooks/") == [
|
|
50
|
+
".claude",
|
|
51
|
+
"hooks",
|
|
52
|
+
]
|
|
53
|
+
assert all_directory_segments_in_path_pattern("\\tests\\") == ["tests"]
|
|
54
|
+
assert all_directory_segments_in_path_pattern("/tests.py") == []
|
|
55
|
+
assert all_directory_segments_in_path_pattern("conftest") == []
|
|
56
|
+
assert all_directory_segments_in_path_pattern("_tab.py") == []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_directory_segment_names_from_path_patterns_extracts_new_directory_token() -> None:
|
|
60
|
+
all_segment_names = directory_segment_names_from_path_patterns(
|
|
61
|
+
ALL_SYNTHETIC_DIRECTORY_PATTERNS
|
|
62
|
+
)
|
|
63
|
+
assert all_segment_names == ALL_EXPECTED_SYNTHETIC_SEGMENTS
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_derived_segments_cover_authoritative_directory_patterns() -> None:
|
|
67
|
+
all_authoritative_patterns = (
|
|
68
|
+
set(TEST_PATH_PATTERNS)
|
|
69
|
+
| set(WORKFLOW_REGISTRY_PATTERNS)
|
|
70
|
+
| set(MIGRATION_PATH_PATTERNS)
|
|
71
|
+
| set(HOOK_INFRASTRUCTURE_PATTERNS)
|
|
72
|
+
| set(ALL_CLI_FILE_PATH_MARKERS)
|
|
73
|
+
)
|
|
74
|
+
all_expected_segments = directory_segment_names_from_path_patterns(
|
|
75
|
+
all_authoritative_patterns
|
|
76
|
+
)
|
|
77
|
+
assert all_expected_segments == ALL_DIRECTORY_SEGMENTS_FROM_PATH_PATTERNS
|
|
78
|
+
assert all_expected_segments <= ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
|
|
79
|
+
assert ALL_CONFIG_DIRECTORY_NAMES <= ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
|
|
80
|
+
assert ALL_KNOWN_DIRECTORY_EXEMPTION_SEGMENTS <= ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_temporary_path_preserves_scripts_directory_segment(tmp_path: Path) -> None:
|
|
84
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
85
|
+
tmp_path, "packages/demo/scripts/run_job.py"
|
|
86
|
+
)
|
|
87
|
+
assert staged_path == tmp_path / "scripts" / "run_job.py"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_temporary_path_preserves_claude_hooks_parent_and_child(tmp_path: Path) -> None:
|
|
91
|
+
staged_hooks_path = _temporary_path_preserving_directory_signal(
|
|
92
|
+
tmp_path, "home/.claude/hooks/blocking/gate.py"
|
|
93
|
+
)
|
|
94
|
+
assert staged_hooks_path == (
|
|
95
|
+
tmp_path / ".claude" / "hooks" / "blocking" / "gate.py"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_temporary_path_preserves_tests_directory_segment(tmp_path: Path) -> None:
|
|
100
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
101
|
+
tmp_path, "packages/demo/tests/helper_functions.py"
|
|
102
|
+
)
|
|
103
|
+
assert staged_path == tmp_path / "tests" / "helper_functions.py"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_temporary_path_preserves_substring_test_helpers_directory(
|
|
107
|
+
tmp_path: Path,
|
|
108
|
+
) -> None:
|
|
109
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
110
|
+
tmp_path, "packages/demo/test_helpers/worker.py"
|
|
111
|
+
)
|
|
112
|
+
assert staged_path == tmp_path / "test_helpers" / "worker.py"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_temporary_path_preserves_absolute_project_test_helpers_directory(
|
|
116
|
+
tmp_path: Path,
|
|
117
|
+
) -> None:
|
|
118
|
+
absolute_helpers_path = "C:/workspace/pkg/test_helpers/worker.py"
|
|
119
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
120
|
+
tmp_path, absolute_helpers_path
|
|
121
|
+
)
|
|
122
|
+
assert staged_path == tmp_path / "test_helpers" / "worker.py"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_absolute_system_temp_pytest_shaped_path_stages_flat_basename(
|
|
126
|
+
tmp_path: Path,
|
|
127
|
+
) -> None:
|
|
128
|
+
pytest_shaped_target = (
|
|
129
|
+
tmp_path / "test_edit_introducing_new_viol0" / "legacy_module.py"
|
|
130
|
+
)
|
|
131
|
+
staging_root = tmp_path / "staging_root"
|
|
132
|
+
staging_root.mkdir()
|
|
133
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
134
|
+
staging_root, str(pytest_shaped_target)
|
|
135
|
+
)
|
|
136
|
+
assert staged_path == staging_root / "legacy_module.py"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_runner_temp_pytest_shaped_path_stages_flat_basename_when_gettempdir_differs(
|
|
140
|
+
tmp_path: Path,
|
|
141
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""GHA basetemp lives under RUNNER_TEMP while gettempdir is often /tmp."""
|
|
144
|
+
runner_temp_root = tmp_path / "runner_temp"
|
|
145
|
+
os_gettemp = tmp_path / "os_gettemp"
|
|
146
|
+
runner_temp_root.mkdir()
|
|
147
|
+
os_gettemp.mkdir()
|
|
148
|
+
monkeypatch.setenv("RUNNER_TEMP", str(runner_temp_root))
|
|
149
|
+
monkeypatch.setattr(
|
|
150
|
+
"validators.run_all_validators.tempfile.gettempdir",
|
|
151
|
+
lambda: str(os_gettemp),
|
|
152
|
+
)
|
|
153
|
+
pytest_shaped_target = (
|
|
154
|
+
runner_temp_root
|
|
155
|
+
/ "pytest-basetemp-pkg"
|
|
156
|
+
/ "test_edit_introducing_new_viol0"
|
|
157
|
+
/ "legacy_module.py"
|
|
158
|
+
)
|
|
159
|
+
staging_root = tmp_path / "staging_root"
|
|
160
|
+
staging_root.mkdir()
|
|
161
|
+
staged_path = _temporary_path_preserving_directory_signal(
|
|
162
|
+
staging_root, str(pytest_shaped_target)
|
|
163
|
+
)
|
|
164
|
+
assert staged_path == staging_root / "legacy_module.py"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_substring_patterns_from_path_patterns_skips_directory_tokens() -> None:
|
|
168
|
+
all_substring_patterns = substring_patterns_from_path_patterns(
|
|
169
|
+
{"test_", "/tests/", "conftest", "\\tests\\"}
|
|
170
|
+
)
|
|
171
|
+
assert all_substring_patterns == frozenset({"test_", "conftest"})
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_directory_exemption_substring_patterns_are_non_empty_and_separator_free() -> None:
|
|
175
|
+
assert ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS
|
|
176
|
+
for each_pattern in ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS:
|
|
177
|
+
assert "/" not in each_pattern
|
|
178
|
+
assert "\\" not in each_pattern
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_directory_exemption_substring_patterns_include_test_fragments() -> None:
|
|
182
|
+
assert "test_" in ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS
|
|
183
|
+
assert "_test." in ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS
|
|
184
|
+
assert ".spec." in ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS
|
|
185
|
+
assert "conftest" in ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS
|