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
|
@@ -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
|
|
@@ -413,7 +418,9 @@ def run_comment_checks(files: List[Path]) -> ValidatorResult:
|
|
|
413
418
|
)
|
|
414
419
|
|
|
415
420
|
|
|
416
|
-
def run_ruff_checks(
|
|
421
|
+
def run_ruff_checks(
|
|
422
|
+
files: List[Path], config_source_path: Optional[Path] = None
|
|
423
|
+
) -> ValidatorResult:
|
|
417
424
|
"""Run ruff for fast Python linting."""
|
|
418
425
|
if not check_ruff_available():
|
|
419
426
|
return ValidatorResult(
|
|
@@ -423,7 +430,7 @@ def run_ruff_checks(files: List[Path]) -> ValidatorResult:
|
|
|
423
430
|
output="Ruff not installed - skipping",
|
|
424
431
|
)
|
|
425
432
|
|
|
426
|
-
result = run_ruff_check(files)
|
|
433
|
+
result = run_ruff_check(files, config_source_path)
|
|
427
434
|
|
|
428
435
|
return ValidatorResult(
|
|
429
436
|
name="Ruff",
|
|
@@ -433,7 +440,9 @@ def run_ruff_checks(files: List[Path]) -> ValidatorResult:
|
|
|
433
440
|
)
|
|
434
441
|
|
|
435
442
|
|
|
436
|
-
def run_mypy_checks(
|
|
443
|
+
def run_mypy_checks(
|
|
444
|
+
files: List[Path], config_source_path: Optional[Path] = None
|
|
445
|
+
) -> ValidatorResult:
|
|
437
446
|
"""Run mypy for static type checking."""
|
|
438
447
|
if not check_mypy_available():
|
|
439
448
|
return ValidatorResult(
|
|
@@ -443,7 +452,7 @@ def run_mypy_checks(files: List[Path]) -> ValidatorResult:
|
|
|
443
452
|
output="Mypy not installed - skipping",
|
|
444
453
|
)
|
|
445
454
|
|
|
446
|
-
result = run_mypy_check(files)
|
|
455
|
+
result = run_mypy_check(files, config_source_path)
|
|
447
456
|
|
|
448
457
|
return ValidatorResult(
|
|
449
458
|
name="Mypy",
|
|
@@ -727,15 +736,16 @@ def reconstruct_proposed_content(
|
|
|
727
736
|
return apply_edits(existing_content, edits_for_tool(tool_name, dict(tool_input)))
|
|
728
737
|
|
|
729
738
|
|
|
730
|
-
def run_file_scoped_validators(
|
|
731
|
-
|
|
739
|
+
def run_file_scoped_validators(
|
|
740
|
+
all_files: List[Path], config_source_path: Optional[Path] = None
|
|
741
|
+
) -> List[ValidatorResult]:
|
|
742
|
+
"""Run every file-scoped validator against *all_files*.
|
|
732
743
|
|
|
733
|
-
Excludes the branch-scoped File Structure and Git validators
|
|
734
|
-
the whole project rather than a single proposed file.
|
|
744
|
+
Excludes the branch-scoped File Structure and Git validators.
|
|
735
745
|
|
|
736
746
|
Args:
|
|
737
|
-
all_files: The files under validation —
|
|
738
|
-
|
|
747
|
+
all_files: The files under validation — one reconstructed file in gate mode.
|
|
748
|
+
config_source_path: Original path ruff and mypy resolve their config from.
|
|
739
749
|
|
|
740
750
|
Returns:
|
|
741
751
|
One ValidatorResult per file-scoped validator, in run order.
|
|
@@ -744,8 +754,8 @@ def run_file_scoped_validators(all_files: List[Path]) -> List[ValidatorResult]:
|
|
|
744
754
|
run_python_style_checks(all_files),
|
|
745
755
|
run_test_safety_checks(all_files),
|
|
746
756
|
run_react_checks(all_files),
|
|
747
|
-
run_ruff_checks(all_files),
|
|
748
|
-
run_mypy_checks(all_files),
|
|
757
|
+
run_ruff_checks(all_files, config_source_path),
|
|
758
|
+
run_mypy_checks(all_files, config_source_path),
|
|
749
759
|
run_abbreviation_checks(all_files),
|
|
750
760
|
run_pr_reference_checks(all_files),
|
|
751
761
|
run_magic_value_checks(all_files),
|
|
@@ -758,27 +768,221 @@ def run_file_scoped_validators(all_files: List[Path]) -> List[ValidatorResult]:
|
|
|
758
768
|
]
|
|
759
769
|
|
|
760
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
|
+
|
|
761
950
|
def validate_proposed_file(
|
|
762
|
-
file_path: str,
|
|
951
|
+
file_path: str,
|
|
952
|
+
proposed_content: str,
|
|
953
|
+
config_source_path: Optional[Path] = None,
|
|
763
954
|
) -> List[ValidatorResult]:
|
|
764
955
|
"""Validate *proposed_content* as if written to *file_path*.
|
|
765
956
|
|
|
766
|
-
Writes the content to a temporary file that
|
|
767
|
-
|
|
768
|
-
|
|
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.
|
|
769
963
|
|
|
770
964
|
Args:
|
|
771
965
|
file_path: The destination path the write or edit targets.
|
|
772
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.
|
|
773
969
|
|
|
774
970
|
Returns:
|
|
775
971
|
One ValidatorResult per file-scoped validator.
|
|
776
972
|
"""
|
|
777
|
-
base_name = Path(file_path).name
|
|
778
973
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
779
|
-
temporary_file =
|
|
974
|
+
temporary_file = _temporary_path_preserving_directory_signal(
|
|
975
|
+
Path(temporary_directory), file_path
|
|
976
|
+
)
|
|
780
977
|
temporary_file.write_text(proposed_content, encoding="utf-8")
|
|
781
|
-
|
|
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
|
+
)
|
|
782
986
|
|
|
783
987
|
|
|
784
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
|
|
@@ -165,6 +165,37 @@ def test_run_mypy_check_accepts_relative_path_under_nested_root(
|
|
|
165
165
|
assert mypy_result.passed, mypy_result.output
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def test_run_mypy_check_applies_config_resolved_from_config_source_path(
|
|
169
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
170
|
+
) -> None:
|
|
171
|
+
"""A staged file is checked under the config resolved from the original path.
|
|
172
|
+
|
|
173
|
+
A strict ``[tool.mypy]`` config flags an untyped def only when
|
|
174
|
+
``config_source_path`` resolves it; native discovery from the detached
|
|
175
|
+
file finds no config and passes.
|
|
176
|
+
"""
|
|
177
|
+
strict_repo = tmp_path / "strict_repo"
|
|
178
|
+
strict_repo.mkdir()
|
|
179
|
+
(strict_repo / "pyproject.toml").write_text(
|
|
180
|
+
"[tool.mypy]\ndisallow_untyped_defs = true\n", encoding="utf-8"
|
|
181
|
+
)
|
|
182
|
+
detached_file = tmp_path / "detached" / "untyped.py"
|
|
183
|
+
detached_file.parent.mkdir(parents=True)
|
|
184
|
+
detached_file.write_text(
|
|
185
|
+
"def annotate_nothing(value):\n return value\n", encoding="utf-8"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
monkeypatch.chdir(tmp_path)
|
|
189
|
+
|
|
190
|
+
without_source = run_mypy_check([detached_file])
|
|
191
|
+
with_source = run_mypy_check(
|
|
192
|
+
[detached_file], config_source_path=strict_repo / "pyproject.toml"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
assert without_source.passed, without_source.output
|
|
196
|
+
assert not with_source.passed
|
|
197
|
+
|
|
198
|
+
|
|
168
199
|
def test_mypy_result_dataclass() -> None:
|
|
169
200
|
"""Test MypyResult dataclass creation."""
|
|
170
201
|
result = MypyResult(passed=True, output="test", error_count=0)
|
|
@@ -183,6 +214,7 @@ def test_run_mypy_check_returns_passed_for_empty_files() -> None:
|
|
|
183
214
|
"""Test that run_mypy_check passes with no files."""
|
|
184
215
|
result = run_mypy_check([])
|
|
185
216
|
assert result.passed is True
|
|
217
|
+
assert result.error_count == 0
|
|
186
218
|
assert "No files" in result.output
|
|
187
219
|
|
|
188
220
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Tests for the shared pyproject config-discovery walk-up primitive.
|
|
2
|
+
|
|
3
|
+
The primitive walks up from a starting path and returns the first pyproject.toml
|
|
4
|
+
whose ``[tool.<name>]`` table exists, so each tool matches only a config that
|
|
5
|
+
actually configures it — a mypy-only pyproject is no ruff config.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from hooks_constants.pyproject_config_discovery_constants import (
|
|
11
|
+
MYPY_TOOL_TABLE_NAME,
|
|
12
|
+
RUFF_TOOL_TABLE_NAME,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .pyproject_config_discovery import (
|
|
16
|
+
ancestor_directories,
|
|
17
|
+
find_pyproject_configuring_tool,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_ancestor_directories_lists_directory_then_parents_nearest_first(
|
|
22
|
+
tmp_path: Path,
|
|
23
|
+
) -> None:
|
|
24
|
+
nested_directory = tmp_path / "outer" / "inner"
|
|
25
|
+
nested_directory.mkdir(parents=True)
|
|
26
|
+
nested_file = nested_directory / "module.py"
|
|
27
|
+
nested_file.write_text("sample_number: int = 1\n", encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
walked = ancestor_directories(nested_file)
|
|
30
|
+
|
|
31
|
+
assert walked[0] == nested_directory.resolve()
|
|
32
|
+
assert walked[1] == (tmp_path / "outer").resolve()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_find_pyproject_configuring_tool_returns_table_owning_pyproject(
|
|
36
|
+
tmp_path: Path,
|
|
37
|
+
) -> None:
|
|
38
|
+
project_root = tmp_path / "project"
|
|
39
|
+
nested_directory = project_root / "src" / "deep"
|
|
40
|
+
nested_directory.mkdir(parents=True)
|
|
41
|
+
owning_pyproject = project_root / "pyproject.toml"
|
|
42
|
+
owning_pyproject.write_text("[tool.ruff.lint]\nselect = ['B']\n", encoding="utf-8")
|
|
43
|
+
target_file = nested_directory / "module.py"
|
|
44
|
+
|
|
45
|
+
found = find_pyproject_configuring_tool(target_file, RUFF_TOOL_TABLE_NAME)
|
|
46
|
+
|
|
47
|
+
assert found == owning_pyproject
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_find_pyproject_configuring_tool_skips_pyproject_without_the_table(
|
|
51
|
+
tmp_path: Path,
|
|
52
|
+
) -> None:
|
|
53
|
+
outer_root = tmp_path / "outer"
|
|
54
|
+
inner_root = outer_root / "inner"
|
|
55
|
+
inner_root.mkdir(parents=True)
|
|
56
|
+
outer_pyproject = outer_root / "pyproject.toml"
|
|
57
|
+
outer_pyproject.write_text("[tool.mypy]\nignore_missing_imports = true\n", encoding="utf-8")
|
|
58
|
+
(inner_root / "pyproject.toml").write_text("[project]\nname = 'inner'\n", encoding="utf-8")
|
|
59
|
+
target_file = inner_root / "module.py"
|
|
60
|
+
|
|
61
|
+
found = find_pyproject_configuring_tool(target_file, MYPY_TOOL_TABLE_NAME)
|
|
62
|
+
|
|
63
|
+
assert found == outer_pyproject
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_find_pyproject_configuring_tool_is_table_specific(tmp_path: Path) -> None:
|
|
67
|
+
project_root = tmp_path / "mypy_only"
|
|
68
|
+
project_root.mkdir()
|
|
69
|
+
(project_root / "pyproject.toml").write_text(
|
|
70
|
+
"[tool.mypy]\nignore_missing_imports = true\n", encoding="utf-8"
|
|
71
|
+
)
|
|
72
|
+
target_file = project_root / "module.py"
|
|
73
|
+
|
|
74
|
+
assert find_pyproject_configuring_tool(target_file, MYPY_TOOL_TABLE_NAME) is not None
|
|
75
|
+
assert find_pyproject_configuring_tool(target_file, RUFF_TOOL_TABLE_NAME) is None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_find_pyproject_configuring_tool_returns_none_when_no_match(tmp_path: Path) -> None:
|
|
79
|
+
isolated_directory = tmp_path / "isolated"
|
|
80
|
+
isolated_directory.mkdir()
|
|
81
|
+
target_file = isolated_directory / "module.py"
|
|
82
|
+
|
|
83
|
+
assert find_pyproject_configuring_tool(target_file, RUFF_TOOL_TABLE_NAME) is None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_find_pyproject_configuring_tool_treats_malformed_toml_as_no_match(
|
|
87
|
+
tmp_path: Path,
|
|
88
|
+
) -> None:
|
|
89
|
+
project_root = tmp_path / "broken"
|
|
90
|
+
project_root.mkdir()
|
|
91
|
+
(project_root / "pyproject.toml").write_text("[tool.ruff\nbroken = true\n", encoding="utf-8")
|
|
92
|
+
target_file = project_root / "module.py"
|
|
93
|
+
|
|
94
|
+
assert find_pyproject_configuring_tool(target_file, RUFF_TOOL_TABLE_NAME) is None
|