claude-dev-env 1.92.0 → 1.93.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 +5 -39
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +340 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +165 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +7 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/rules/testing.md +0 -2
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +184 -0
- package/skills/orchestrator-refresh/SKILL.md +37 -0
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +32 -164
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- package/skills/verified-build/SKILL.md +4 -9
- package/skills/team-advisor-refresh/SKILL.md +0 -25
|
@@ -3,22 +3,39 @@
|
|
|
3
3
|
Implements four style checks:
|
|
4
4
|
1. Imports at top of file
|
|
5
5
|
2. No empty lines after decorators
|
|
6
|
-
3.
|
|
6
|
+
3. Two empty lines between top-level functions
|
|
7
7
|
4. View functions end with _view suffix
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import ast
|
|
11
|
+
import logging
|
|
11
12
|
import sys
|
|
12
13
|
from dataclasses import dataclass
|
|
13
14
|
from pathlib import Path
|
|
14
|
-
from typing import List
|
|
15
|
+
from typing import Iterator, List, Optional, Union
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from hooks_constants.python_style_checks_constants import (
|
|
19
|
+
EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS,
|
|
20
|
+
MINIMUM_ARGUMENT_COUNT,
|
|
21
|
+
)
|
|
22
|
+
except ModuleNotFoundError:
|
|
23
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
24
|
+
if _hooks_directory not in sys.path:
|
|
25
|
+
sys.path.insert(0, _hooks_directory)
|
|
26
|
+
from hooks_constants.python_style_checks_constants import (
|
|
27
|
+
EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS,
|
|
28
|
+
MINIMUM_ARGUMENT_COUNT,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
15
32
|
|
|
16
|
-
|
|
17
|
-
# Constants
|
|
18
33
|
VIEW_SUFFIX = "_view"
|
|
19
34
|
REQUEST_PARAM = "request"
|
|
20
35
|
VIEWS_FILENAME = "views.py"
|
|
21
36
|
|
|
37
|
+
FunctionNode = Union[ast.FunctionDef, ast.AsyncFunctionDef]
|
|
38
|
+
|
|
22
39
|
|
|
23
40
|
@dataclass
|
|
24
41
|
class Violation:
|
|
@@ -34,329 +51,367 @@ class Violation:
|
|
|
34
51
|
|
|
35
52
|
|
|
36
53
|
def check_imports_at_top(tree: ast.AST, filename: str) -> List[Violation]:
|
|
37
|
-
"""Check that all imports
|
|
38
|
-
|
|
39
|
-
Catches two violations:
|
|
40
|
-
1. Module-level imports after non-import statements
|
|
41
|
-
2. Imports inside functions/classes (inline imports)
|
|
54
|
+
"""Check that all imports sit at the top of the file.
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
filename: Name of file being checked
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
List of violations found
|
|
56
|
+
Flags module-level imports that follow other statements and imports
|
|
57
|
+
nested inside function or method bodies.
|
|
49
58
|
"""
|
|
50
59
|
violations: List[Violation] = []
|
|
60
|
+
violations.extend(_check_module_level_import_order(tree, filename))
|
|
61
|
+
violations.extend(_check_no_inline_imports(tree, filename))
|
|
62
|
+
return violations
|
|
63
|
+
|
|
51
64
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for child in tree.body:
|
|
56
|
-
if isinstance(child, (ast.Import, ast.ImportFrom)):
|
|
57
|
-
if seen_non_import:
|
|
58
|
-
violations.append(
|
|
59
|
-
Violation(
|
|
60
|
-
filename,
|
|
61
|
-
child.lineno,
|
|
62
|
-
"Import statement must be at top of file",
|
|
63
|
-
)
|
|
64
|
-
)
|
|
65
|
-
elif isinstance(child, ast.Expr) and isinstance(child.value, ast.Constant):
|
|
66
|
-
# Allow docstrings at top
|
|
67
|
-
if isinstance(child.value.value, str):
|
|
68
|
-
continue
|
|
69
|
-
seen_non_import = True
|
|
70
|
-
else:
|
|
71
|
-
seen_non_import = True
|
|
72
|
-
|
|
73
|
-
# Check 2: No imports inside functions or methods
|
|
74
|
-
for node in ast.walk(tree):
|
|
75
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
76
|
-
for child in ast.walk(node):
|
|
77
|
-
if isinstance(child, (ast.Import, ast.ImportFrom)):
|
|
78
|
-
violations.append(
|
|
79
|
-
Violation(
|
|
80
|
-
filename,
|
|
81
|
-
child.lineno,
|
|
82
|
-
"Import inside function - move to top of file",
|
|
83
|
-
)
|
|
84
|
-
)
|
|
65
|
+
def _is_import_statement(statement: ast.stmt) -> bool:
|
|
66
|
+
"""Return True when the statement is an import or from-import."""
|
|
67
|
+
return isinstance(statement, (ast.Import, ast.ImportFrom))
|
|
85
68
|
|
|
69
|
+
|
|
70
|
+
def _is_docstring_statement(statement: ast.stmt) -> bool:
|
|
71
|
+
"""Return True when the statement is a string-literal docstring."""
|
|
72
|
+
if not isinstance(statement, ast.Expr):
|
|
73
|
+
return False
|
|
74
|
+
literal = statement.value
|
|
75
|
+
return isinstance(literal, ast.Constant) and isinstance(literal.value, str)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _check_module_level_import_order(tree: ast.AST, filename: str) -> List[Violation]:
|
|
79
|
+
"""Flag module-level imports that appear after other statements."""
|
|
80
|
+
if not isinstance(tree, ast.Module):
|
|
81
|
+
return []
|
|
82
|
+
violations: List[Violation] = []
|
|
83
|
+
has_seen_non_import = False
|
|
84
|
+
for each_statement in tree.body:
|
|
85
|
+
is_import = _is_import_statement(each_statement)
|
|
86
|
+
if is_import and has_seen_non_import:
|
|
87
|
+
violations.append(
|
|
88
|
+
Violation(
|
|
89
|
+
filename,
|
|
90
|
+
each_statement.lineno,
|
|
91
|
+
"Import statement must be at top of file",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
if not is_import and not _is_docstring_statement(each_statement):
|
|
95
|
+
has_seen_non_import = True
|
|
86
96
|
return violations
|
|
87
97
|
|
|
88
98
|
|
|
89
|
-
def
|
|
90
|
-
"""
|
|
99
|
+
def _check_no_inline_imports(tree: ast.AST, filename: str) -> List[Violation]:
|
|
100
|
+
"""Flag import statements located inside function or method bodies."""
|
|
101
|
+
violations: List[Violation] = []
|
|
102
|
+
for each_function_node in _iter_function_definitions(tree):
|
|
103
|
+
violations.extend(_inline_imports_in(each_function_node, filename))
|
|
104
|
+
return violations
|
|
91
105
|
|
|
92
|
-
Args:
|
|
93
|
-
source: Source code as string
|
|
94
|
-
filename: Name of file being checked
|
|
95
106
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"""
|
|
107
|
+
def _inline_imports_in(function_node: FunctionNode, filename: str) -> List[Violation]:
|
|
108
|
+
"""Return violations for imports nested inside a single function."""
|
|
99
109
|
violations: List[Violation] = []
|
|
100
|
-
|
|
110
|
+
for each_descendant in ast.walk(function_node):
|
|
111
|
+
if isinstance(each_descendant, (ast.Import, ast.ImportFrom)):
|
|
112
|
+
violations.append(
|
|
113
|
+
Violation(
|
|
114
|
+
filename,
|
|
115
|
+
each_descendant.lineno,
|
|
116
|
+
"Import inside function - move to top of file",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return violations
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _iter_function_definitions(tree: ast.AST) -> Iterator[FunctionNode]:
|
|
123
|
+
"""Yield every function and async-function definition in the tree."""
|
|
124
|
+
for each_node in ast.walk(tree):
|
|
125
|
+
if isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
126
|
+
yield each_node
|
|
101
127
|
|
|
128
|
+
|
|
129
|
+
def check_no_empty_line_after_decorators(source: str, filename: str) -> List[Violation]:
|
|
130
|
+
"""Check that no empty line separates a decorator from its function."""
|
|
131
|
+
violations: List[Violation] = []
|
|
102
132
|
try:
|
|
103
133
|
tree = ast.parse(source)
|
|
104
134
|
except SyntaxError:
|
|
105
135
|
return violations
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if
|
|
109
|
-
|
|
110
|
-
last_decorator_line = max(d.lineno for d in node.decorator_list)
|
|
111
|
-
function_line = node.lineno
|
|
112
|
-
|
|
113
|
-
# Check if there's an empty line between decorator and function
|
|
114
|
-
if function_line - last_decorator_line > 1:
|
|
115
|
-
violations.append(
|
|
116
|
-
Violation(
|
|
117
|
-
filename,
|
|
118
|
-
last_decorator_line,
|
|
119
|
-
"No empty line allowed between decorator and function",
|
|
120
|
-
)
|
|
121
|
-
)
|
|
122
|
-
|
|
136
|
+
for each_function_node in _iter_function_definitions(tree):
|
|
137
|
+
violation = _decorator_gap_violation(each_function_node, filename)
|
|
138
|
+
if violation is not None:
|
|
139
|
+
violations.append(violation)
|
|
123
140
|
return violations
|
|
124
141
|
|
|
125
142
|
|
|
126
|
-
def
|
|
143
|
+
def _decorator_gap_violation(
|
|
144
|
+
function_node: FunctionNode, filename: str
|
|
145
|
+
) -> Optional[Violation]:
|
|
146
|
+
"""Return a violation when an empty line separates a decorator from its function."""
|
|
147
|
+
if not function_node.decorator_list:
|
|
148
|
+
return None
|
|
149
|
+
last_decorator_line = max(
|
|
150
|
+
each_decorator.lineno for each_decorator in function_node.decorator_list
|
|
151
|
+
)
|
|
152
|
+
if function_node.lineno - last_decorator_line <= 1:
|
|
153
|
+
return None
|
|
154
|
+
return Violation(
|
|
155
|
+
filename,
|
|
156
|
+
last_decorator_line,
|
|
157
|
+
"No empty line allowed between decorator and function",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def check_blank_lines_between_functions(
|
|
127
162
|
source: str, filename: str
|
|
128
163
|
) -> List[Violation]:
|
|
129
|
-
"""Check that
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
source: Source code as string
|
|
133
|
-
filename: Name of file being checked
|
|
134
|
-
|
|
135
|
-
Returns:
|
|
136
|
-
List of violations found
|
|
137
|
-
"""
|
|
164
|
+
"""Check that consecutive top-level functions carry the expected blank gap."""
|
|
138
165
|
violations: List[Violation] = []
|
|
139
|
-
|
|
166
|
+
source_lines = _real_newline_lines(source)
|
|
167
|
+
top_level_functions = _top_level_functions(source)
|
|
168
|
+
for each_current_function, each_next_function in zip(
|
|
169
|
+
top_level_functions, top_level_functions[1:]
|
|
170
|
+
):
|
|
171
|
+
violation = _spacing_violation(
|
|
172
|
+
each_current_function, each_next_function, source_lines, filename
|
|
173
|
+
)
|
|
174
|
+
if violation is not None:
|
|
175
|
+
violations.append(violation)
|
|
176
|
+
return violations
|
|
140
177
|
|
|
178
|
+
|
|
179
|
+
def _top_level_functions(source: str) -> List[FunctionNode]:
|
|
180
|
+
"""Return the module's top-level function definitions, ordered by line."""
|
|
141
181
|
try:
|
|
142
182
|
tree = ast.parse(source)
|
|
143
183
|
except SyntaxError:
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
functions = [
|
|
148
|
-
node
|
|
184
|
+
return []
|
|
185
|
+
if not isinstance(tree, ast.Module):
|
|
186
|
+
return []
|
|
187
|
+
functions: List[FunctionNode] = [
|
|
188
|
+
node
|
|
189
|
+
for node in tree.body
|
|
190
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
149
191
|
]
|
|
192
|
+
functions.sort(key=lambda function_node: function_node.lineno)
|
|
193
|
+
return functions
|
|
150
194
|
|
|
151
|
-
# Filter to only top-level functions (not nested)
|
|
152
|
-
if isinstance(tree, ast.Module):
|
|
153
|
-
top_level_functions = [
|
|
154
|
-
node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
155
|
-
]
|
|
156
|
-
else:
|
|
157
|
-
top_level_functions = []
|
|
158
|
-
|
|
159
|
-
# Sort by line number
|
|
160
|
-
top_level_functions.sort(key=lambda f: f.lineno)
|
|
161
|
-
|
|
162
|
-
# Check spacing between consecutive functions
|
|
163
|
-
for i in range(len(top_level_functions) - 1):
|
|
164
|
-
current_func = top_level_functions[i]
|
|
165
|
-
next_func = top_level_functions[i + 1]
|
|
166
|
-
|
|
167
|
-
# Find last line of current function
|
|
168
|
-
current_end = current_func.end_lineno
|
|
169
|
-
next_start = next_func.lineno
|
|
170
|
-
|
|
171
|
-
if current_end is not None:
|
|
172
|
-
# Calculate empty lines between functions
|
|
173
|
-
empty_lines = next_start - current_end - 1
|
|
174
|
-
|
|
175
|
-
if empty_lines != 1:
|
|
176
|
-
violations.append(
|
|
177
|
-
Violation(
|
|
178
|
-
filename,
|
|
179
|
-
current_end,
|
|
180
|
-
f"Expected 1 empty line between functions, found {empty_lines}",
|
|
181
|
-
)
|
|
182
|
-
)
|
|
183
195
|
|
|
184
|
-
|
|
196
|
+
def _real_newline_lines(source: str) -> List[str]:
|
|
197
|
+
"""Split source on CR, LF, and CRLF only, keeping each line ending.
|
|
185
198
|
|
|
199
|
+
``str.splitlines`` breaks on form feed and other control characters that
|
|
200
|
+
Python's line numbering ignores, so its indices drift from ``ast`` line
|
|
201
|
+
numbers. This splitter keeps line indices aligned with ``ast`` line numbers.
|
|
202
|
+
"""
|
|
203
|
+
lines: List[str] = []
|
|
204
|
+
line_start = 0
|
|
205
|
+
scan_index = 0
|
|
206
|
+
total_length = len(source)
|
|
207
|
+
while scan_index < total_length:
|
|
208
|
+
character = source[scan_index]
|
|
209
|
+
if character == "\r":
|
|
210
|
+
scan_index += 1
|
|
211
|
+
if scan_index < total_length and source[scan_index] == "\n":
|
|
212
|
+
scan_index += 1
|
|
213
|
+
elif character == "\n":
|
|
214
|
+
scan_index += 1
|
|
215
|
+
else:
|
|
216
|
+
scan_index += 1
|
|
217
|
+
continue
|
|
218
|
+
lines.append(source[line_start:scan_index])
|
|
219
|
+
line_start = scan_index
|
|
220
|
+
if line_start < total_length:
|
|
221
|
+
lines.append(source[line_start:])
|
|
222
|
+
return lines
|
|
186
223
|
|
|
187
|
-
def check_view_function_naming(tree: ast.AST, filename: str) -> List[Violation]:
|
|
188
|
-
"""Check that view functions end with _view suffix.
|
|
189
224
|
|
|
190
|
-
|
|
225
|
+
def _function_start_line(function_node: FunctionNode) -> int:
|
|
226
|
+
"""Return the first source line of a function, counting its decorators."""
|
|
227
|
+
if not function_node.decorator_list:
|
|
228
|
+
return function_node.lineno
|
|
229
|
+
return min(
|
|
230
|
+
each_decorator.lineno for each_decorator in function_node.decorator_list
|
|
231
|
+
)
|
|
191
232
|
|
|
192
|
-
Args:
|
|
193
|
-
tree: AST tree to check
|
|
194
|
-
filename: Name of file being checked
|
|
195
233
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
""
|
|
199
|
-
violations: List[Violation] = []
|
|
234
|
+
def _gap_is_blank_only(gap_lines: List[str]) -> bool:
|
|
235
|
+
"""Return True when every line between two functions is blank."""
|
|
236
|
+
return all(each_line.strip() == "" for each_line in gap_lines)
|
|
200
237
|
|
|
201
|
-
# Only check files named views.py
|
|
202
|
-
if not filename.endswith(VIEWS_FILENAME):
|
|
203
|
-
return violations
|
|
204
238
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
Violation(
|
|
213
|
-
filename,
|
|
214
|
-
node.lineno,
|
|
215
|
-
f"View function '{node.name}' must end with '{VIEW_SUFFIX}'",
|
|
216
|
-
)
|
|
217
|
-
)
|
|
239
|
+
def _spacing_violation(
|
|
240
|
+
current_function: FunctionNode,
|
|
241
|
+
next_function: FunctionNode,
|
|
242
|
+
source_lines: List[str],
|
|
243
|
+
filename: str,
|
|
244
|
+
) -> Optional[Violation]:
|
|
245
|
+
"""Return a violation when two functions hold the wrong blank-line count.
|
|
218
246
|
|
|
219
|
-
|
|
247
|
+
A run of purely empty lines is measured; a run holding any other content
|
|
248
|
+
is skipped, matching the fixer so a flagged file can be normalized.
|
|
249
|
+
"""
|
|
250
|
+
current_end = current_function.end_lineno
|
|
251
|
+
if current_end is None:
|
|
252
|
+
return None
|
|
253
|
+
next_start = _function_start_line(next_function)
|
|
254
|
+
gap_lines = source_lines[current_end : next_start - 1]
|
|
255
|
+
if not _gap_is_blank_only(gap_lines):
|
|
256
|
+
return None
|
|
257
|
+
blank_line_count = len(gap_lines)
|
|
258
|
+
if blank_line_count == EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS:
|
|
259
|
+
return None
|
|
260
|
+
return Violation(
|
|
261
|
+
filename,
|
|
262
|
+
current_end,
|
|
263
|
+
f"Expected {EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS} empty lines "
|
|
264
|
+
f"between functions, found {blank_line_count}",
|
|
265
|
+
)
|
|
220
266
|
|
|
221
267
|
|
|
222
|
-
def
|
|
223
|
-
"""
|
|
268
|
+
def check_view_function_naming(tree: ast.AST, filename: str) -> List[Violation]:
|
|
269
|
+
"""Check that request-handling functions in views.py end with _view."""
|
|
270
|
+
if not filename.endswith(VIEWS_FILENAME):
|
|
271
|
+
return []
|
|
272
|
+
violations: List[Violation] = []
|
|
273
|
+
for each_function_node in _iter_function_definitions(tree):
|
|
274
|
+
if _is_misnamed_view(each_function_node):
|
|
275
|
+
violations.append(
|
|
276
|
+
Violation(
|
|
277
|
+
filename,
|
|
278
|
+
each_function_node.lineno,
|
|
279
|
+
f"View function '{each_function_node.name}' must end with '{VIEW_SUFFIX}'",
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
return violations
|
|
224
283
|
|
|
225
|
-
Args:
|
|
226
|
-
source: Source code as string
|
|
227
284
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
285
|
+
def _is_misnamed_view(function_node: FunctionNode) -> bool:
|
|
286
|
+
"""Return True when a request handler is missing the _view suffix."""
|
|
287
|
+
arguments = function_node.args.args
|
|
288
|
+
if not arguments:
|
|
289
|
+
return False
|
|
290
|
+
if arguments[0].arg != REQUEST_PARAM:
|
|
291
|
+
return False
|
|
292
|
+
return not function_node.name.endswith(VIEW_SUFFIX)
|
|
234
293
|
|
|
235
|
-
for line in lines:
|
|
236
|
-
stripped = line.strip()
|
|
237
294
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
295
|
+
def fix_empty_lines_after_decorators(source: str) -> str:
|
|
296
|
+
"""Remove empty lines between decorators and their function definitions."""
|
|
297
|
+
result_lines: List[str] = []
|
|
298
|
+
should_skip_next_blank = False
|
|
299
|
+
for each_line in source.splitlines(keepends=True):
|
|
300
|
+
stripped = each_line.strip()
|
|
301
|
+
if should_skip_next_blank and stripped == "":
|
|
242
302
|
continue
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
result_lines.append(line)
|
|
246
|
-
|
|
303
|
+
should_skip_next_blank = stripped.startswith("@")
|
|
304
|
+
result_lines.append(each_line)
|
|
247
305
|
return "".join(result_lines)
|
|
248
306
|
|
|
249
307
|
|
|
250
|
-
def
|
|
251
|
-
"""
|
|
252
|
-
|
|
253
|
-
Args:
|
|
254
|
-
source: Source code as string
|
|
308
|
+
def _blank_line_for_source(source: str) -> str:
|
|
309
|
+
"""Return the blank-line string matching the source newline convention.
|
|
255
310
|
|
|
256
|
-
|
|
257
|
-
|
|
311
|
+
Every disk reader in this package loads source through
|
|
312
|
+
Path.read_text(), whose universal-newline translation turns a bare
|
|
313
|
+
\\r or a \\r\\n pair into \\n before the string reaches this function.
|
|
314
|
+
A bare \\r never arrives from a file on disk. The \\r\\n branch below
|
|
315
|
+
serves an in-memory caller that builds a CRLF string directly.
|
|
258
316
|
"""
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
317
|
+
if "\r\n" in source:
|
|
318
|
+
return "\r\n"
|
|
319
|
+
return "\n"
|
|
262
320
|
|
|
263
|
-
for line in lines:
|
|
264
|
-
if line.strip() == "":
|
|
265
|
-
blank_count += 1
|
|
266
|
-
if blank_count <= 1:
|
|
267
|
-
result_lines.append(line)
|
|
268
|
-
else:
|
|
269
|
-
blank_count = 0
|
|
270
|
-
result_lines.append(line)
|
|
271
321
|
|
|
272
|
-
|
|
322
|
+
def _normalized_gap(all_gap_lines: List[str], blank_line: str) -> List[str]:
|
|
323
|
+
"""Return the between-function gap normalized to the expected blank count.
|
|
273
324
|
|
|
325
|
+
A gap that is entirely blank becomes exactly the expected number of blank
|
|
326
|
+
lines. A gap that holds any non-blank line stays untouched so no comment,
|
|
327
|
+
class, or statement in it is lost.
|
|
328
|
+
"""
|
|
329
|
+
if not _gap_is_blank_only(all_gap_lines):
|
|
330
|
+
return all_gap_lines
|
|
331
|
+
return [blank_line] * EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def fix_function_spacing(source: str) -> str:
|
|
335
|
+
"""Normalize blank lines between top-level functions to exactly two."""
|
|
336
|
+
functions = _top_level_functions(source)
|
|
337
|
+
if len(functions) <= 1:
|
|
338
|
+
return source
|
|
339
|
+
source_lines = _real_newline_lines(source)
|
|
340
|
+
blank_line = _blank_line_for_source(source)
|
|
341
|
+
previous_end_line = functions[0].end_lineno
|
|
342
|
+
if previous_end_line is None:
|
|
343
|
+
return source
|
|
344
|
+
rebuilt: List[str] = list(source_lines[:previous_end_line])
|
|
345
|
+
for each_next_function in functions[1:]:
|
|
346
|
+
next_start_line = _function_start_line(each_next_function)
|
|
347
|
+
gap_lines = source_lines[previous_end_line : next_start_line - 1]
|
|
348
|
+
rebuilt.extend(_normalized_gap(gap_lines, blank_line))
|
|
349
|
+
next_end_line = each_next_function.end_lineno
|
|
350
|
+
if next_end_line is None:
|
|
351
|
+
return source
|
|
352
|
+
rebuilt.extend(source_lines[next_start_line - 1 : next_end_line])
|
|
353
|
+
previous_end_line = next_end_line
|
|
354
|
+
rebuilt.extend(source_lines[previous_end_line:])
|
|
355
|
+
return "".join(rebuilt)
|
|
274
356
|
|
|
275
|
-
def fix_file(file_path: Path) -> bool:
|
|
276
|
-
"""Apply all safe fixes to a file.
|
|
277
357
|
|
|
278
|
-
|
|
279
|
-
|
|
358
|
+
def fix_file(file_path: Path) -> bool:
|
|
359
|
+
"""Apply the safe blank-line fixes and report whether the file changed.
|
|
280
360
|
|
|
281
|
-
|
|
282
|
-
|
|
361
|
+
Reads the file through Path.read_text(), which applies
|
|
362
|
+
universal-newline translation and normalizes every line ending to
|
|
363
|
+
\\n before either fix function below inspects the text.
|
|
283
364
|
"""
|
|
284
365
|
try:
|
|
285
366
|
original = file_path.read_text(encoding="utf-8")
|
|
286
367
|
except Exception:
|
|
287
368
|
return False
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
file_path.write_text(fixed, encoding="utf-8")
|
|
295
|
-
return True
|
|
296
|
-
|
|
297
|
-
return False
|
|
369
|
+
decorators_fixed = fix_empty_lines_after_decorators(original)
|
|
370
|
+
blank_lines_fixed = fix_function_spacing(decorators_fixed)
|
|
371
|
+
if blank_lines_fixed == original:
|
|
372
|
+
return False
|
|
373
|
+
file_path.write_text(blank_lines_fixed, encoding="utf-8")
|
|
374
|
+
return True
|
|
298
375
|
|
|
299
376
|
|
|
300
377
|
def validate_file(file_path: Path) -> List[Violation]:
|
|
301
|
-
"""Validate a Python file
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
file_path: Path to Python file to validate
|
|
305
|
-
|
|
306
|
-
Returns:
|
|
307
|
-
List of all violations found
|
|
308
|
-
"""
|
|
378
|
+
"""Validate a Python file against every style check."""
|
|
309
379
|
violations: List[Violation] = []
|
|
310
380
|
filename = str(file_path)
|
|
311
|
-
|
|
312
381
|
try:
|
|
313
382
|
source = file_path.read_text(encoding="utf-8")
|
|
314
|
-
except Exception as
|
|
315
|
-
violations.append(Violation(filename, 0, f"Error reading file: {
|
|
383
|
+
except Exception as error:
|
|
384
|
+
violations.append(Violation(filename, 0, f"Error reading file: {error}"))
|
|
316
385
|
return violations
|
|
317
|
-
|
|
318
386
|
try:
|
|
319
387
|
tree = ast.parse(source)
|
|
320
|
-
except SyntaxError as
|
|
388
|
+
except SyntaxError as error:
|
|
321
389
|
violations.append(
|
|
322
|
-
Violation(filename,
|
|
390
|
+
Violation(filename, error.lineno or 0, f"Syntax error: {error.msg}")
|
|
323
391
|
)
|
|
324
392
|
return violations
|
|
325
|
-
|
|
326
|
-
# Run all checks
|
|
327
393
|
violations.extend(check_imports_at_top(tree, filename))
|
|
328
394
|
violations.extend(check_no_empty_line_after_decorators(source, filename))
|
|
329
|
-
violations.extend(
|
|
395
|
+
violations.extend(check_blank_lines_between_functions(source, filename))
|
|
330
396
|
violations.extend(check_view_function_naming(tree, filename))
|
|
331
|
-
|
|
332
397
|
return violations
|
|
333
398
|
|
|
334
399
|
|
|
335
400
|
def main() -> int:
|
|
336
|
-
"""
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
"""
|
|
341
|
-
if len(sys.argv) < 2:
|
|
342
|
-
print("Usage: python_style_checks.py <file1.py> [file2.py ...]", file=sys.stderr)
|
|
401
|
+
"""Run the style checks over the files named on the command line."""
|
|
402
|
+
minimum_argument_count = MINIMUM_ARGUMENT_COUNT
|
|
403
|
+
if len(sys.argv) < minimum_argument_count:
|
|
404
|
+
logger.error("Usage: %s <file1.py> [file2.py ...]", Path(sys.argv[0]).name)
|
|
343
405
|
return 1
|
|
344
|
-
|
|
345
406
|
all_violations: List[Violation] = []
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
file_path = Path(file_arg)
|
|
407
|
+
for each_file_arg in sys.argv[1:]:
|
|
408
|
+
file_path = Path(each_file_arg)
|
|
349
409
|
if not file_path.exists():
|
|
350
|
-
|
|
410
|
+
logger.error("File not found: %s", file_path)
|
|
351
411
|
return 1
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
# Print all violations
|
|
357
|
-
for violation in all_violations:
|
|
358
|
-
print(violation)
|
|
359
|
-
|
|
412
|
+
all_violations.extend(validate_file(file_path))
|
|
413
|
+
for each_violation in all_violations:
|
|
414
|
+
logger.error("%s", each_violation)
|
|
360
415
|
return 1 if all_violations else 0
|
|
361
416
|
|
|
362
417
|
|
|
@@ -32,6 +32,7 @@ _hooks_directory_on_path = str(hooks_dir.resolve())
|
|
|
32
32
|
if _hooks_directory_on_path not in sys.path:
|
|
33
33
|
sys.path.insert(0, _hooks_directory_on_path)
|
|
34
34
|
|
|
35
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
35
36
|
from hooks_constants.multi_edit_reconstruction import ( # noqa: E402
|
|
36
37
|
apply_edits,
|
|
37
38
|
edits_for_tool,
|
|
@@ -807,6 +808,11 @@ def _emit_pre_tool_use_deny(deny_reason: str) -> None:
|
|
|
807
808
|
"permissionDecisionReason": deny_reason,
|
|
808
809
|
}
|
|
809
810
|
}
|
|
811
|
+
log_hook_block(
|
|
812
|
+
calling_hook_name="run_all_validators.py",
|
|
813
|
+
hook_event="PreToolUse",
|
|
814
|
+
block_reason=deny_reason,
|
|
815
|
+
)
|
|
810
816
|
sys.stdout.write(json.dumps(deny_payload) + "\n")
|
|
811
817
|
sys.stdout.flush()
|
|
812
818
|
|