claude-dev-env 2.3.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 -48
- 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 +3 -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 +3 -4
- 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/skill-writer-agent.md +84 -0
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- 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 +2 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +5 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +107 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +9 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +198 -0
- package/hooks/blocking/code_review_push_gate.py +145 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -155
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
- package/hooks/blocking/test_code_review_push_gate.py +205 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +974 -903
- 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/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1014
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +128 -0
- package/hooks/hooks.json +26 -1
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -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/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- 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 +2 -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/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +257 -25
- package/hooks/validators/run_all_validators.py +223 -19
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +160 -2
- package/hooks/validators/test_run_all_validators.py +140 -68
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- 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 -67
- package/rules/durable-post-artifacts.md +7 -0
- 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/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +23 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +626 -619
- 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_codec.py +77 -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/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -421
- package/skills/autoconverge/reference/convergence.md +26 -4
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
- package/skills/autoconverge/workflow/converge.mjs +191 -8
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -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/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -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/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- 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/skills/team-advisor/SKILL.md +2 -2
- package/scripts/test_invoke_code_review.py +0 -672
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -1,178 +1,185 @@
|
|
|
1
|
-
"""Unit tests for the enter_worktree_origin_prefetch PreToolUse hook.
|
|
2
|
-
|
|
3
|
-
Uses real git repositories (a "remote" repo cloned into a "local" one) so the
|
|
4
|
-
fetch behavior is exercised against actual git plumbing, not mocked calls.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import importlib.util
|
|
10
|
-
import io
|
|
11
|
-
import json
|
|
12
|
-
import pathlib
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
19
|
-
_HOOKS_TREE = _HOOK_DIR.parent
|
|
20
|
-
for each_path in (str(_HOOK_DIR), str(_HOOKS_TREE)):
|
|
21
|
-
if each_path not in sys.path:
|
|
22
|
-
sys.path.insert(0, each_path)
|
|
23
|
-
|
|
24
|
-
hook_spec = importlib.util.spec_from_file_location(
|
|
25
|
-
"enter_worktree_origin_prefetch",
|
|
26
|
-
_HOOK_DIR / "enter_worktree_origin_prefetch.py",
|
|
27
|
-
)
|
|
28
|
-
assert hook_spec is not None
|
|
29
|
-
assert hook_spec.loader is not None
|
|
30
|
-
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
31
|
-
hook_spec.loader.exec_module(hook_module)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _run_git(repo_directory: pathlib.Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
35
|
-
return subprocess.run(
|
|
36
|
-
["git", *args],
|
|
37
|
-
cwd=repo_directory,
|
|
38
|
-
capture_output=True,
|
|
39
|
-
text=True,
|
|
40
|
-
check=True,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _init_repo_with_commit(repo_directory: pathlib.Path) -> None:
|
|
45
|
-
repo_directory.mkdir(parents=True, exist_ok=True)
|
|
46
|
-
_run_git(repo_directory, "init", "--quiet", "--initial-branch=main")
|
|
47
|
-
_run_git(repo_directory, "config", "user.email", "test@example.com")
|
|
48
|
-
_run_git(repo_directory, "config", "user.name", "Test")
|
|
49
|
-
_run_git(repo_directory, "commit", "--allow-empty", "--quiet", "-m", "init")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _clone_as_local(remote_directory: pathlib.Path, local_directory: pathlib.Path) -> None:
|
|
53
|
-
subprocess.run(
|
|
54
|
-
["git", "clone", "--quiet", str(remote_directory), str(local_directory)],
|
|
55
|
-
capture_output=True,
|
|
56
|
-
text=True,
|
|
57
|
-
check=True,
|
|
58
|
-
)
|
|
59
|
-
_run_git(local_directory, "config", "user.email", "test@example.com")
|
|
60
|
-
_run_git(local_directory, "config", "user.name", "Test")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_is_enter_worktree_creation_true_when_no_path_given() -> None:
|
|
64
|
-
payload = {"tool_name": "EnterWorktree", "tool_input": {}}
|
|
65
|
-
assert hook_module.is_enter_worktree_creation(payload) is True
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def test_is_enter_worktree_creation_true_with_name_only() -> None:
|
|
69
|
-
payload = {"tool_name": "EnterWorktree", "tool_input": {"name": "my-branch"}}
|
|
70
|
-
assert hook_module.is_enter_worktree_creation(payload) is True
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def test_is_enter_worktree_creation_false_when_path_given() -> None:
|
|
74
|
-
payload = {"tool_name": "EnterWorktree", "tool_input": {"path": "/some/worktree"}}
|
|
75
|
-
assert hook_module.is_enter_worktree_creation(payload) is False
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_is_enter_worktree_creation_false_for_other_tool() -> None:
|
|
79
|
-
payload = {"tool_name": "Bash", "tool_input": {}}
|
|
80
|
-
assert hook_module.is_enter_worktree_creation(payload) is False
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_resolve_origin_default_branch_reads_cloned_head(tmp_path: pathlib.Path) -> None:
|
|
84
|
-
remote_directory = tmp_path / "remote"
|
|
85
|
-
local_directory = tmp_path / "local"
|
|
86
|
-
_init_repo_with_commit(remote_directory)
|
|
87
|
-
_clone_as_local(remote_directory, local_directory)
|
|
88
|
-
|
|
89
|
-
resolved_branch = hook_module.resolve_origin_default_branch(str(local_directory))
|
|
90
|
-
|
|
91
|
-
assert resolved_branch == "main"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_resolve_origin_default_branch_returns_none_without_origin(
|
|
95
|
-
tmp_path: pathlib.Path,
|
|
96
|
-
) -> None:
|
|
97
|
-
solo_directory = tmp_path / "solo"
|
|
98
|
-
_init_repo_with_commit(solo_directory)
|
|
99
|
-
|
|
100
|
-
resolved_branch = hook_module.resolve_origin_default_branch(str(solo_directory))
|
|
101
|
-
|
|
102
|
-
assert resolved_branch is None
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def test_fetch_origin_branch_updates_stale_local_ref(tmp_path: pathlib.Path) -> None:
|
|
106
|
-
remote_directory = tmp_path / "remote"
|
|
107
|
-
local_directory = tmp_path / "local"
|
|
108
|
-
_init_repo_with_commit(remote_directory)
|
|
109
|
-
_clone_as_local(remote_directory, local_directory)
|
|
110
|
-
|
|
111
|
-
_run_git(remote_directory, "commit", "--allow-empty", "--quiet", "-m", "second")
|
|
112
|
-
remote_head_sha = _run_git(remote_directory, "rev-parse", "HEAD").stdout.strip()
|
|
113
|
-
local_cached_sha_before = _run_git(
|
|
114
|
-
local_directory, "rev-parse", "refs/remotes/origin/main"
|
|
115
|
-
).stdout.strip()
|
|
116
|
-
assert local_cached_sha_before != remote_head_sha
|
|
117
|
-
|
|
118
|
-
hook_module.fetch_origin_branch(str(local_directory), "main")
|
|
119
|
-
|
|
120
|
-
local_cached_sha_after = _run_git(
|
|
121
|
-
local_directory, "rev-parse", "refs/remotes/origin/main"
|
|
122
|
-
).stdout.strip()
|
|
123
|
-
assert local_cached_sha_after == remote_head_sha
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def test_fetch_origin_branch_never_raises_when_remote_missing(tmp_path: pathlib.Path) -> None:
|
|
127
|
-
solo_directory = tmp_path / "solo"
|
|
128
|
-
_init_repo_with_commit(solo_directory)
|
|
129
|
-
|
|
130
|
-
hook_module.fetch_origin_branch(str(solo_directory), "main")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_main_fetches_stale_ref_and_exits_zero(
|
|
134
|
-
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
135
|
-
) -> None:
|
|
136
|
-
remote_directory = tmp_path / "remote"
|
|
137
|
-
local_directory = tmp_path / "local"
|
|
138
|
-
_init_repo_with_commit(remote_directory)
|
|
139
|
-
_clone_as_local(remote_directory, local_directory)
|
|
140
|
-
_run_git(remote_directory, "commit", "--allow-empty", "--quiet", "-m", "second")
|
|
141
|
-
remote_head_sha = _run_git(remote_directory, "rev-parse", "HEAD").stdout.strip()
|
|
142
|
-
|
|
143
|
-
payload = {
|
|
144
|
-
"tool_name": "EnterWorktree",
|
|
145
|
-
"tool_input": {},
|
|
146
|
-
"cwd": str(local_directory),
|
|
147
|
-
}
|
|
148
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
149
|
-
|
|
150
|
-
with pytest.raises(SystemExit) as exit_info:
|
|
151
|
-
hook_module.main()
|
|
152
|
-
|
|
153
|
-
assert exit_info.value.code == 0
|
|
154
|
-
local_cached_sha_after = _run_git(
|
|
155
|
-
local_directory, "rev-parse", "refs/remotes/origin/main"
|
|
156
|
-
).stdout.strip()
|
|
157
|
-
assert local_cached_sha_after == remote_head_sha
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def test_main_exits_zero_for_non_enter_worktree_tool(
|
|
161
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
162
|
-
) -> None:
|
|
163
|
-
payload = {"tool_name": "Bash", "tool_input": {"command": "ls"}, "cwd": "/tmp"}
|
|
164
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
165
|
-
|
|
166
|
-
with pytest.raises(SystemExit) as exit_info:
|
|
167
|
-
hook_module.main()
|
|
168
|
-
|
|
169
|
-
assert exit_info.value.code == 0
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
1
|
+
"""Unit tests for the enter_worktree_origin_prefetch PreToolUse hook.
|
|
2
|
+
|
|
3
|
+
Uses real git repositories (a "remote" repo cloned into a "local" one) so the
|
|
4
|
+
fetch behavior is exercised against actual git plumbing, not mocked calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib.util
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import pathlib
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
19
|
+
_HOOKS_TREE = _HOOK_DIR.parent
|
|
20
|
+
for each_path in (str(_HOOK_DIR), str(_HOOKS_TREE)):
|
|
21
|
+
if each_path not in sys.path:
|
|
22
|
+
sys.path.insert(0, each_path)
|
|
23
|
+
|
|
24
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
25
|
+
"enter_worktree_origin_prefetch",
|
|
26
|
+
_HOOK_DIR / "enter_worktree_origin_prefetch.py",
|
|
27
|
+
)
|
|
28
|
+
assert hook_spec is not None
|
|
29
|
+
assert hook_spec.loader is not None
|
|
30
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
31
|
+
hook_spec.loader.exec_module(hook_module)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _run_git(repo_directory: pathlib.Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
35
|
+
return subprocess.run(
|
|
36
|
+
["git", *args],
|
|
37
|
+
cwd=repo_directory,
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _init_repo_with_commit(repo_directory: pathlib.Path) -> None:
|
|
45
|
+
repo_directory.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
_run_git(repo_directory, "init", "--quiet", "--initial-branch=main")
|
|
47
|
+
_run_git(repo_directory, "config", "user.email", "test@example.com")
|
|
48
|
+
_run_git(repo_directory, "config", "user.name", "Test")
|
|
49
|
+
_run_git(repo_directory, "commit", "--allow-empty", "--quiet", "-m", "init")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _clone_as_local(remote_directory: pathlib.Path, local_directory: pathlib.Path) -> None:
|
|
53
|
+
subprocess.run(
|
|
54
|
+
["git", "clone", "--quiet", str(remote_directory), str(local_directory)],
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
check=True,
|
|
58
|
+
)
|
|
59
|
+
_run_git(local_directory, "config", "user.email", "test@example.com")
|
|
60
|
+
_run_git(local_directory, "config", "user.name", "Test")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_is_enter_worktree_creation_true_when_no_path_given() -> None:
|
|
64
|
+
payload = {"tool_name": "EnterWorktree", "tool_input": {}}
|
|
65
|
+
assert hook_module.is_enter_worktree_creation(payload) is True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_is_enter_worktree_creation_true_with_name_only() -> None:
|
|
69
|
+
payload = {"tool_name": "EnterWorktree", "tool_input": {"name": "my-branch"}}
|
|
70
|
+
assert hook_module.is_enter_worktree_creation(payload) is True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_is_enter_worktree_creation_false_when_path_given() -> None:
|
|
74
|
+
payload = {"tool_name": "EnterWorktree", "tool_input": {"path": "/some/worktree"}}
|
|
75
|
+
assert hook_module.is_enter_worktree_creation(payload) is False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_is_enter_worktree_creation_false_for_other_tool() -> None:
|
|
79
|
+
payload = {"tool_name": "Bash", "tool_input": {}}
|
|
80
|
+
assert hook_module.is_enter_worktree_creation(payload) is False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_resolve_origin_default_branch_reads_cloned_head(tmp_path: pathlib.Path) -> None:
|
|
84
|
+
remote_directory = tmp_path / "remote"
|
|
85
|
+
local_directory = tmp_path / "local"
|
|
86
|
+
_init_repo_with_commit(remote_directory)
|
|
87
|
+
_clone_as_local(remote_directory, local_directory)
|
|
88
|
+
|
|
89
|
+
resolved_branch = hook_module.resolve_origin_default_branch(str(local_directory))
|
|
90
|
+
|
|
91
|
+
assert resolved_branch == "main"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_resolve_origin_default_branch_returns_none_without_origin(
|
|
95
|
+
tmp_path: pathlib.Path,
|
|
96
|
+
) -> None:
|
|
97
|
+
solo_directory = tmp_path / "solo"
|
|
98
|
+
_init_repo_with_commit(solo_directory)
|
|
99
|
+
|
|
100
|
+
resolved_branch = hook_module.resolve_origin_default_branch(str(solo_directory))
|
|
101
|
+
|
|
102
|
+
assert resolved_branch is None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_fetch_origin_branch_updates_stale_local_ref(tmp_path: pathlib.Path) -> None:
|
|
106
|
+
remote_directory = tmp_path / "remote"
|
|
107
|
+
local_directory = tmp_path / "local"
|
|
108
|
+
_init_repo_with_commit(remote_directory)
|
|
109
|
+
_clone_as_local(remote_directory, local_directory)
|
|
110
|
+
|
|
111
|
+
_run_git(remote_directory, "commit", "--allow-empty", "--quiet", "-m", "second")
|
|
112
|
+
remote_head_sha = _run_git(remote_directory, "rev-parse", "HEAD").stdout.strip()
|
|
113
|
+
local_cached_sha_before = _run_git(
|
|
114
|
+
local_directory, "rev-parse", "refs/remotes/origin/main"
|
|
115
|
+
).stdout.strip()
|
|
116
|
+
assert local_cached_sha_before != remote_head_sha
|
|
117
|
+
|
|
118
|
+
hook_module.fetch_origin_branch(str(local_directory), "main")
|
|
119
|
+
|
|
120
|
+
local_cached_sha_after = _run_git(
|
|
121
|
+
local_directory, "rev-parse", "refs/remotes/origin/main"
|
|
122
|
+
).stdout.strip()
|
|
123
|
+
assert local_cached_sha_after == remote_head_sha
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_fetch_origin_branch_never_raises_when_remote_missing(tmp_path: pathlib.Path) -> None:
|
|
127
|
+
solo_directory = tmp_path / "solo"
|
|
128
|
+
_init_repo_with_commit(solo_directory)
|
|
129
|
+
|
|
130
|
+
hook_module.fetch_origin_branch(str(solo_directory), "main")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_main_fetches_stale_ref_and_exits_zero(
|
|
134
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
135
|
+
) -> None:
|
|
136
|
+
remote_directory = tmp_path / "remote"
|
|
137
|
+
local_directory = tmp_path / "local"
|
|
138
|
+
_init_repo_with_commit(remote_directory)
|
|
139
|
+
_clone_as_local(remote_directory, local_directory)
|
|
140
|
+
_run_git(remote_directory, "commit", "--allow-empty", "--quiet", "-m", "second")
|
|
141
|
+
remote_head_sha = _run_git(remote_directory, "rev-parse", "HEAD").stdout.strip()
|
|
142
|
+
|
|
143
|
+
payload = {
|
|
144
|
+
"tool_name": "EnterWorktree",
|
|
145
|
+
"tool_input": {},
|
|
146
|
+
"cwd": str(local_directory),
|
|
147
|
+
}
|
|
148
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
149
|
+
|
|
150
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
151
|
+
hook_module.main()
|
|
152
|
+
|
|
153
|
+
assert exit_info.value.code == 0
|
|
154
|
+
local_cached_sha_after = _run_git(
|
|
155
|
+
local_directory, "rev-parse", "refs/remotes/origin/main"
|
|
156
|
+
).stdout.strip()
|
|
157
|
+
assert local_cached_sha_after == remote_head_sha
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_main_exits_zero_for_non_enter_worktree_tool(
|
|
161
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
162
|
+
) -> None:
|
|
163
|
+
payload = {"tool_name": "Bash", "tool_input": {"command": "ls"}, "cwd": "/tmp"}
|
|
164
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
165
|
+
|
|
166
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
167
|
+
hook_module.main()
|
|
168
|
+
|
|
169
|
+
assert exit_info.value.code == 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@pytest.mark.parametrize(
|
|
173
|
+
"stdin_text",
|
|
174
|
+
["not json", "", " \n\t ", "[]", "42"],
|
|
175
|
+
ids=["malformed-json", "empty", "whitespace-only", "json-array-root", "json-scalar-root"],
|
|
176
|
+
)
|
|
177
|
+
def test_main_exits_zero_on_fail_soft_stdin(
|
|
178
|
+
stdin_text: str, monkeypatch: pytest.MonkeyPatch
|
|
179
|
+
) -> None:
|
|
180
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(stdin_text))
|
|
181
|
+
|
|
182
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
183
|
+
hook_module.main()
|
|
184
|
+
|
|
185
|
+
assert exit_info.value.code == 0
|
package/hooks/pyproject.toml
CHANGED
|
@@ -12,6 +12,7 @@ A library of check modules used by the validation hooks. Each module focuses on
|
|
|
12
12
|
| `output_formatter.py` | Formats `Violation` lists into human-readable output |
|
|
13
13
|
| `run_all_validators.py` | Entry point — runs every check module and aggregates results |
|
|
14
14
|
| `health_check.py` | Verifies that all validator dependencies (ruff, mypy) are reachable |
|
|
15
|
+
| `pyproject_config_discovery.py` | Shared walk-up primitive that resolves a tool's pyproject.toml config from an original target path, matching the `[tool.<name>]` table the tool owns |
|
|
15
16
|
| `python_style_helpers.py` | Shared source-line splitting and function-discovery helpers imported by `python_style_checks.py` |
|
|
16
17
|
|
|
17
18
|
## Check modules
|
|
@@ -39,6 +40,7 @@ A library of check modules used by the validation hooks. Each module focuses on
|
|
|
39
40
|
|
|
40
41
|
| Directory | Role |
|
|
41
42
|
|---|---|
|
|
43
|
+
| `config/` | Constants module (`directory_exemption_constants.py`) holding the exemption directory-segment names that `validate_proposed_file` staging keeps |
|
|
42
44
|
| `test_files/` | Fixture files used by the validator tests — not checked-in test code |
|
|
43
45
|
|
|
44
46
|
## Conventions
|
|
File without changes
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Directory-segment signals that anchor validator temp-path staging.
|
|
2
|
+
|
|
3
|
+
``validate_proposed_file`` keeps the target's path from the first directory
|
|
4
|
+
segment that either matches an exemption directory name exactly or contains a
|
|
5
|
+
separator-free substring pattern (``test_``, ``conftest``, ``.spec.``, and
|
|
6
|
+
siblings). Exact names come from the authoritative path-exemption pattern sets
|
|
7
|
+
and CLI markers, unioned with the config-directory allowlist. Substring
|
|
8
|
+
patterns come from the separator-free members of those same pattern sets so a
|
|
9
|
+
directory such as ``pkg/test_helpers/`` stages with the same test exemption as
|
|
10
|
+
the real path.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
|
|
17
|
+
from hooks_constants.code_rules_enforcer_constants import ALL_CLI_FILE_PATH_MARKERS
|
|
18
|
+
from hooks_constants.code_rules_path_utils_constants import ALL_CONFIG_DIRECTORY_NAMES
|
|
19
|
+
|
|
20
|
+
from ..exempt_paths import (
|
|
21
|
+
HOOK_INFRASTRUCTURE_PATTERNS,
|
|
22
|
+
MIGRATION_PATH_PATTERNS,
|
|
23
|
+
TEST_PATH_PATTERNS,
|
|
24
|
+
WORKFLOW_REGISTRY_PATTERNS,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
POSIX_DIRECTORY_SEPARATOR = "/"
|
|
28
|
+
WINDOWS_DIRECTORY_SEPARATOR = "\\"
|
|
29
|
+
|
|
30
|
+
ALL_SYSTEM_TEMPORARY_ROOT_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
|
|
31
|
+
{
|
|
32
|
+
"TEMP",
|
|
33
|
+
"TMP",
|
|
34
|
+
"TMPDIR",
|
|
35
|
+
"RUNNER_TEMP",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
ALL_AUTHORITATIVE_DIRECTORY_PATH_PATTERN_SETS: tuple[Iterable[str], ...] = (
|
|
40
|
+
TEST_PATH_PATTERNS,
|
|
41
|
+
WORKFLOW_REGISTRY_PATTERNS,
|
|
42
|
+
MIGRATION_PATH_PATTERNS,
|
|
43
|
+
HOOK_INFRASTRUCTURE_PATTERNS,
|
|
44
|
+
ALL_CLI_FILE_PATH_MARKERS,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_filename_like_path_segment(path_segment: str) -> bool:
|
|
49
|
+
"""Return True when *path_segment* looks like a file name, not a directory.
|
|
50
|
+
|
|
51
|
+
::
|
|
52
|
+
|
|
53
|
+
is_filename_like_path_segment("states.py") # True
|
|
54
|
+
is_filename_like_path_segment(".claude") # False
|
|
55
|
+
is_filename_like_path_segment("tests") # False
|
|
56
|
+
|
|
57
|
+
A leading-dot name is treated as a directory (``.claude``). Any other
|
|
58
|
+
segment that contains a dot is treated as a filename (``states.py``).
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
path_segment: One path component with no separators.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True when the segment should be ignored as a directory token.
|
|
65
|
+
"""
|
|
66
|
+
if path_segment.startswith("."):
|
|
67
|
+
return False
|
|
68
|
+
return "." in path_segment
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def all_directory_segments_in_path_pattern(path_pattern: str) -> list[str]:
|
|
72
|
+
"""Return lowercased directory tokens from one path-exemption pattern.
|
|
73
|
+
|
|
74
|
+
::
|
|
75
|
+
|
|
76
|
+
all_directory_segments_in_path_pattern("/.claude/hooks/")
|
|
77
|
+
# ['.claude', 'hooks']
|
|
78
|
+
|
|
79
|
+
all_directory_segments_in_path_pattern("/tests.py")
|
|
80
|
+
# []
|
|
81
|
+
|
|
82
|
+
all_directory_segments_in_path_pattern("conftest")
|
|
83
|
+
# []
|
|
84
|
+
|
|
85
|
+
Separators normalize to POSIX. Filename-like segments and empty splits
|
|
86
|
+
are dropped so only directory tokens remain.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
path_pattern: One entry from an exemption or CLI marker pattern set.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Lowercased directory segment names in path order.
|
|
93
|
+
"""
|
|
94
|
+
normalized_pattern = path_pattern.replace(
|
|
95
|
+
WINDOWS_DIRECTORY_SEPARATOR, POSIX_DIRECTORY_SEPARATOR
|
|
96
|
+
)
|
|
97
|
+
if POSIX_DIRECTORY_SEPARATOR not in normalized_pattern:
|
|
98
|
+
return []
|
|
99
|
+
all_directory_segments: list[str] = []
|
|
100
|
+
for each_segment in normalized_pattern.split(POSIX_DIRECTORY_SEPARATOR):
|
|
101
|
+
if not each_segment:
|
|
102
|
+
continue
|
|
103
|
+
if is_filename_like_path_segment(each_segment):
|
|
104
|
+
continue
|
|
105
|
+
all_directory_segments.append(each_segment.lower())
|
|
106
|
+
return all_directory_segments
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def directory_segment_names_from_path_patterns(
|
|
110
|
+
all_path_patterns: Iterable[str],
|
|
111
|
+
) -> frozenset[str]:
|
|
112
|
+
"""Collect unique directory segment names from path-exemption patterns.
|
|
113
|
+
|
|
114
|
+
::
|
|
115
|
+
|
|
116
|
+
directory_segment_names_from_path_patterns({"/tests/", "\\workflow\\", "/cli.py"})
|
|
117
|
+
# frozenset({'tests', 'workflow'})
|
|
118
|
+
|
|
119
|
+
Basename markers without a directory token contribute nothing. Adding a
|
|
120
|
+
new ``/foo/`` pattern to an authoritative set surfaces ``foo`` here
|
|
121
|
+
automatically.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
all_path_patterns: Path substrings used by exemption or CLI checks.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Lowercased directory segment names present in the patterns.
|
|
128
|
+
"""
|
|
129
|
+
all_segment_names: set[str] = set()
|
|
130
|
+
for each_pattern in all_path_patterns:
|
|
131
|
+
all_segment_names.update(all_directory_segments_in_path_pattern(each_pattern))
|
|
132
|
+
return frozenset(all_segment_names)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def substring_patterns_from_path_patterns(
|
|
136
|
+
all_path_patterns: Iterable[str],
|
|
137
|
+
) -> frozenset[str]:
|
|
138
|
+
"""Collect separator-free substring patterns from path-exemption patterns.
|
|
139
|
+
|
|
140
|
+
::
|
|
141
|
+
|
|
142
|
+
substring_patterns_from_path_patterns({"test_", "/tests/", "conftest"})
|
|
143
|
+
# frozenset({'test_', 'conftest'})
|
|
144
|
+
|
|
145
|
+
``is_test_file`` and its siblings match these fragments anywhere in a path,
|
|
146
|
+
including inside a directory segment name such as ``pkg/test_helpers/``.
|
|
147
|
+
Patterns carrying a separator are directory tokens handled by
|
|
148
|
+
``directory_segment_names_from_path_patterns`` and are skipped here.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
all_path_patterns: Path substrings used by exemption or CLI checks.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Lowercased separator-free substring patterns.
|
|
155
|
+
"""
|
|
156
|
+
all_substring_patterns: set[str] = set()
|
|
157
|
+
for each_pattern in all_path_patterns:
|
|
158
|
+
if (
|
|
159
|
+
POSIX_DIRECTORY_SEPARATOR in each_pattern
|
|
160
|
+
or WINDOWS_DIRECTORY_SEPARATOR in each_pattern
|
|
161
|
+
):
|
|
162
|
+
continue
|
|
163
|
+
all_substring_patterns.add(each_pattern.lower())
|
|
164
|
+
return frozenset(all_substring_patterns)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
ALL_DIRECTORY_SEGMENTS_FROM_PATH_PATTERNS: frozenset[str] = frozenset().union(
|
|
168
|
+
*(
|
|
169
|
+
directory_segment_names_from_path_patterns(each_pattern_set)
|
|
170
|
+
for each_pattern_set in ALL_AUTHORITATIVE_DIRECTORY_PATH_PATTERN_SETS
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES: frozenset[str] = (
|
|
175
|
+
ALL_CONFIG_DIRECTORY_NAMES | ALL_DIRECTORY_SEGMENTS_FROM_PATH_PATTERNS
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS: frozenset[str] = frozenset().union(
|
|
179
|
+
*(
|
|
180
|
+
substring_patterns_from_path_patterns(each_pattern_set)
|
|
181
|
+
for each_pattern_set in ALL_AUTHORITATIVE_DIRECTORY_PATH_PATTERN_SETS
|
|
182
|
+
)
|
|
183
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Tests for the derived directory-exemption segment set."""
|
|
2
|
+
|
|
3
|
+
from hooks_constants.code_rules_path_utils_constants import ALL_CONFIG_DIRECTORY_NAMES
|
|
4
|
+
|
|
5
|
+
from .directory_exemption_constants import ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_exemption_set_covers_every_config_directory_name() -> None:
|
|
9
|
+
missing_names = ALL_CONFIG_DIRECTORY_NAMES - ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
|
|
10
|
+
assert missing_names == frozenset()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_exemption_set_covers_constants_package_directories() -> None:
|
|
14
|
+
assert "hooks_constants" in ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
|
|
15
|
+
assert "pr_loop_shared_constants" in ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
|
|
16
|
+
assert "git_hooks_constants" in ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_exemption_set_keeps_path_marker_segments() -> None:
|
|
20
|
+
for each_marker in ("scripts", "tests", "migrations", "workflow", "hooks"):
|
|
21
|
+
assert each_marker in ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
|
|
@@ -5,6 +5,10 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
VALIDATORS_DIRECTORY = Path(__file__).resolve().parent
|
|
8
|
+
HOOKS_DIRECTORY = VALIDATORS_DIRECTORY.parent
|
|
8
9
|
|
|
9
10
|
if str(VALIDATORS_DIRECTORY) not in sys.path:
|
|
10
11
|
sys.path.insert(0, str(VALIDATORS_DIRECTORY))
|
|
12
|
+
|
|
13
|
+
if str(HOOKS_DIRECTORY) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(HOOKS_DIRECTORY))
|