claude-dev-env 1.0.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/LICENSE +21 -0
- package/README.md +219 -0
- package/agents/agent-writer.md +157 -0
- package/agents/clasp-deployment-orchestrator.md +609 -0
- package/agents/clean-coder.md +295 -0
- package/agents/code-quality-agent.md +40 -0
- package/agents/code-standards-agent.md +93 -0
- package/agents/config-centralizer.md +686 -0
- package/agents/config-extraction-agent.md +225 -0
- package/agents/doc-orchestrator.md +47 -0
- package/agents/docs-agent.md +112 -0
- package/agents/docx-agent.md +211 -0
- package/agents/git-commit-crafter.md +100 -0
- package/agents/magic-value-eliminator-agent.md +72 -0
- package/agents/mandatory-agent-workflow-agent.md +88 -0
- package/agents/parallel-workflow-coordinator.md +779 -0
- package/agents/pdf-agent.md +302 -0
- package/agents/plan-executor.md +226 -0
- package/agents/pr-description-writer.md +87 -0
- package/agents/project-context-loader.md +238 -0
- package/agents/project-docs-analyzer.md +54 -0
- package/agents/project-structure-organizer-agent.md +72 -0
- package/agents/readability-review-agent.md +76 -0
- package/agents/refactoring-specialist.md +69 -0
- package/agents/right-sized-engineer.md +129 -0
- package/agents/session-continuity-manager.md +53 -0
- package/agents/skill-to-agent-converter.md +371 -0
- package/agents/skill-writer-agent.md +470 -0
- package/agents/stub-detector-agent.md +140 -0
- package/agents/tdd-test-writer.md +62 -0
- package/agents/test-data-builder.md +68 -0
- package/agents/tooling-builder.md +78 -0
- package/agents/user-docs-writer.md +67 -0
- package/agents/validation-expert.md +71 -0
- package/agents/workflow-visual-documenter.md +82 -0
- package/agents/xlsx-agent.md +169 -0
- package/bin/install.mjs +256 -0
- package/commands/commit.md +28 -0
- package/commands/docupdate.md +322 -0
- package/commands/implement.md +102 -0
- package/commands/initialize.md +91 -0
- package/commands/plan.md +63 -0
- package/commands/pr-comments.md +47 -0
- package/commands/readability-review.md +20 -0
- package/commands/review-plan.md +7 -0
- package/commands/right-size.md +15 -0
- package/commands/stubcheck.md +89 -0
- package/commands/sum.md +30 -0
- package/docs/CODE_RULES.md +186 -0
- package/docs/DJANGO_PATTERNS.md +80 -0
- package/docs/REACT_PATTERNS.md +185 -0
- package/docs/TEST_QUALITY.md +104 -0
- package/hooks/advisory/migration-safety-advisor.py +49 -0
- package/hooks/advisory/refactor-guard.py +205 -0
- package/hooks/blocking/block-main-commit.py +168 -0
- package/hooks/blocking/code-rules-enforcer.py +549 -0
- package/hooks/blocking/destructive-command-blocker.py +107 -0
- package/hooks/blocking/docker-settings-guard.py +44 -0
- package/hooks/blocking/hedging-language-blocker.py +130 -0
- package/hooks/blocking/parallel-task-blocker.py +69 -0
- package/hooks/blocking/pr-description-enforcer.py +87 -0
- package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
- package/hooks/blocking/sensitive-file-protector.py +70 -0
- package/hooks/blocking/tdd-enforcer.py +62 -0
- package/hooks/blocking/test-preflight-check.py +343 -0
- package/hooks/blocking/write-existing-file-blocker.py +63 -0
- package/hooks/git-hooks/post-commit.py +103 -0
- package/hooks/github-action/test_workflow.py +33 -0
- package/hooks/hooks.json +246 -0
- package/hooks/lifecycle/config-change-guard.py +84 -0
- package/hooks/lifecycle/session-end-cleanup.py +59 -0
- package/hooks/notification/attention-needed-notify.py +63 -0
- package/hooks/notification/claude-notification-handler.py +59 -0
- package/hooks/notification/notification_utils.py +206 -0
- package/hooks/rewrite-plugin-paths.py +116 -0
- package/hooks/session/bulk-edit-reminder.py +30 -0
- package/hooks/session/code-rules-reminder.py +97 -0
- package/hooks/session/compact-context-reinject.py +39 -0
- package/hooks/session/hook-structure-context.py +140 -0
- package/hooks/session/plugin-data-dir-cleanup.py +39 -0
- package/hooks/validation/code-style-validator.py +145 -0
- package/hooks/validation/e2e-test-validator.py +142 -0
- package/hooks/validation/hook-format-validator.py +66 -0
- package/hooks/validation/mypy_validator.py +180 -0
- package/hooks/validators/README.md +125 -0
- package/hooks/validators/VALIDATION_REPORT.md +287 -0
- package/hooks/validators/__init__.py +19 -0
- package/hooks/validators/abbreviation_checks.py +82 -0
- package/hooks/validators/code_quality_checks.py +133 -0
- package/hooks/validators/comment_checks.py +188 -0
- package/hooks/validators/file_structure_checks.py +182 -0
- package/hooks/validators/git_checks.py +107 -0
- package/hooks/validators/health_check.py +214 -0
- package/hooks/validators/magic_value_checks.py +81 -0
- package/hooks/validators/mypy_integration.py +52 -0
- package/hooks/validators/output_formatter.py +266 -0
- package/hooks/validators/pr_reference_checks.py +72 -0
- package/hooks/validators/python_antipattern_checks.py +110 -0
- package/hooks/validators/python_style_checks.py +364 -0
- package/hooks/validators/react_checks.py +90 -0
- package/hooks/validators/ruff_integration.py +80 -0
- package/hooks/validators/run_all_validators.py +772 -0
- package/hooks/validators/security_checks.py +135 -0
- package/hooks/validators/test_abbreviation_checks.py +76 -0
- package/hooks/validators/test_bad.tsx +7 -0
- package/hooks/validators/test_code_quality_checks.py +129 -0
- package/hooks/validators/test_file_structure_checks.py +307 -0
- package/hooks/validators/test_files/01_basic_component.tsx +10 -0
- package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
- package/hooks/validators/test_files/03_pure_component.tsx +10 -0
- package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
- package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
- package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
- package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
- package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
- package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
- package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
- package/hooks/validators/test_files/11_ts_file.ts +10 -0
- package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
- package/hooks/validators/test_files/13_functional_component.tsx +8 -0
- package/hooks/validators/test_files/14_indented_class.tsx +13 -0
- package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
- package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
- package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
- package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
- package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
- package/hooks/validators/test_files/async_views.py +23 -0
- package/hooks/validators/test_files/async_with_imports.py +14 -0
- package/hooks/validators/test_files/bad_inline_imports.py +37 -0
- package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
- package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
- package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
- package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
- package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
- package/hooks/validators/test_files/not_management_command.py +10 -0
- package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
- package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
- package/hooks/validators/test_files/test_async_functions.py +45 -0
- package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
- package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
- package/hooks/validators/test_git_checks.py +295 -0
- package/hooks/validators/test_good.tsx +5 -0
- package/hooks/validators/test_health_check.py +57 -0
- package/hooks/validators/test_magic_value_checks.py +63 -0
- package/hooks/validators/test_mypy_integration.py +27 -0
- package/hooks/validators/test_output_formatter.py +150 -0
- package/hooks/validators/test_pr_reference_checks.py +41 -0
- package/hooks/validators/test_python_antipattern_checks.py +113 -0
- package/hooks/validators/test_python_style_checks.py +439 -0
- package/hooks/validators/test_react_checks.py +213 -0
- package/hooks/validators/test_results.txt +25 -0
- package/hooks/validators/test_ruff_integration.py +27 -0
- package/hooks/validators/test_run_all_validators.py +228 -0
- package/hooks/validators/test_run_all_validators_integration.py +48 -0
- package/hooks/validators/test_safety_checks.py +243 -0
- package/hooks/validators/test_security_checks.py +105 -0
- package/hooks/validators/test_test_safety_checks.py +321 -0
- package/hooks/validators/test_todo_checks.py +39 -0
- package/hooks/validators/test_type_safety_checks.py +85 -0
- package/hooks/validators/test_useless_test_checks.py +55 -0
- package/hooks/validators/test_validator_base.py +26 -0
- package/hooks/validators/test_verify_paths.py +34 -0
- package/hooks/validators/todo_checks.py +59 -0
- package/hooks/validators/type_safety_checks.py +101 -0
- package/hooks/validators/useless_test_checks.py +92 -0
- package/hooks/validators/validator_base.py +19 -0
- package/hooks/validators/verify_paths.py +57 -0
- package/hooks/workflow/auto-formatter.py +114 -0
- package/hooks/workflow/investigation-tracker-reset.py +46 -0
- package/package.json +30 -0
- package/rules/agent-spawn-protocol.md +47 -0
- package/rules/cleanup-temp-files.md +27 -0
- package/rules/code-reviews.md +11 -0
- package/rules/code-standards.md +43 -0
- package/rules/conservative-action.md +20 -0
- package/rules/context7.md +12 -0
- package/rules/explore-thoroughly.md +27 -0
- package/rules/git-workflow.md +42 -0
- package/rules/parallel-tools.md +23 -0
- package/rules/research-mode.md +23 -0
- package/rules/right-sized-engineering.md +28 -0
- package/rules/tdd.md +7 -0
- package/rules/testing.md +12 -0
- package/skills/agent-prompt/SKILL.md +102 -0
- package/skills/anthropic-plan/SKILL.md +107 -0
- package/skills/everything-search/SKILL.md +144 -0
- package/skills/ingest/SKILL.md +40 -0
- package/skills/npm-creator/SKILL.md +183 -0
- package/skills/pr-review-responder/EXAMPLES.md +590 -0
- package/skills/pr-review-responder/PRINCIPLES.md +539 -0
- package/skills/pr-review-responder/README.md +209 -0
- package/skills/pr-review-responder/SKILL.md +202 -0
- package/skills/pr-review-responder/TESTING.md +407 -0
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
- package/skills/pr-review-responder/update_skill.py +297 -0
- package/skills/prompt-generator/REFERENCE.md +150 -0
- package/skills/prompt-generator/SKILL.md +154 -0
- package/skills/readability-review/SKILL.md +127 -0
- package/skills/recall/SKILL.md +27 -0
- package/skills/remember/SKILL.md +63 -0
- package/skills/rule-audit/SKILL.md +307 -0
- package/skills/rule-creator/SKILL.md +150 -0
- package/skills/skill-writer/REFERENCE.md +246 -0
- package/skills/skill-writer/SKILL.md +270 -0
- package/skills/tdd-team/SKILL.md +128 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""File structure validation checks for pre-PR validation."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
EXCLUDED_DIRS = {".venv", "venv", "node_modules", ".git"}
|
|
10
|
+
|
|
11
|
+
# Django requires empty __init__.py in these directories for module discovery
|
|
12
|
+
DJANGO_REQUIRED_INIT_DIRS = {
|
|
13
|
+
"migrations",
|
|
14
|
+
"management",
|
|
15
|
+
"commands",
|
|
16
|
+
"templatetags",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Files that indicate a directory is a Django app
|
|
20
|
+
DJANGO_APP_INDICATORS = {
|
|
21
|
+
"apps.py",
|
|
22
|
+
"models.py",
|
|
23
|
+
"views.py",
|
|
24
|
+
"admin.py",
|
|
25
|
+
"settings.py",
|
|
26
|
+
"urls.py",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Violation:
|
|
32
|
+
"""Represents a validation violation."""
|
|
33
|
+
|
|
34
|
+
file: str
|
|
35
|
+
line: int
|
|
36
|
+
message: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _normalize_path(path: Path, project_root: Path) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Convert Path to normalized string with forward slashes.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
path: Path to normalize
|
|
45
|
+
project_root: Project root for relative path calculation
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Normalized path string with forward slashes
|
|
49
|
+
"""
|
|
50
|
+
relative = path.relative_to(project_root)
|
|
51
|
+
return relative.as_posix()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _should_exclude_path(path: Path, project_root: Path) -> bool:
|
|
55
|
+
"""Check if path should be excluded from validation."""
|
|
56
|
+
try:
|
|
57
|
+
relative = path.relative_to(project_root)
|
|
58
|
+
return any(part in EXCLUDED_DIRS for part in relative.parts)
|
|
59
|
+
except ValueError:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_multiple_requirements_txt(project_root: Path) -> List[Violation]:
|
|
64
|
+
"""
|
|
65
|
+
Check for multiple requirements.txt files in project.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
project_root: Root directory of the project
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of violations found (empty if single or no requirements.txt)
|
|
72
|
+
"""
|
|
73
|
+
requirements_files: List[Path] = []
|
|
74
|
+
|
|
75
|
+
for req_file in project_root.rglob("requirements.txt"):
|
|
76
|
+
if not _should_exclude_path(req_file, project_root):
|
|
77
|
+
requirements_files.append(req_file)
|
|
78
|
+
|
|
79
|
+
if len(requirements_files) <= 1:
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
extra_files = [
|
|
83
|
+
_normalize_path(f, project_root)
|
|
84
|
+
for f in requirements_files
|
|
85
|
+
if f != project_root / "requirements.txt"
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
message = (
|
|
89
|
+
f"Multiple requirements.txt files found. "
|
|
90
|
+
f"Project should have exactly one requirements.txt at root. "
|
|
91
|
+
f"Extra files: {', '.join(extra_files)}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return [Violation(file="requirements.txt", line=1, message=message)]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_django_required_init(init_file: Path) -> bool:
|
|
98
|
+
"""Check if __init__.py is in a Django-required directory or Django app."""
|
|
99
|
+
parent = init_file.parent
|
|
100
|
+
parent_name = parent.name
|
|
101
|
+
|
|
102
|
+
# Check if in known Django-required directory
|
|
103
|
+
if parent_name in DJANGO_REQUIRED_INIT_DIRS:
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# Check if this is a Django app directory (contains apps.py, models.py, etc.)
|
|
107
|
+
for indicator in DJANGO_APP_INDICATORS:
|
|
108
|
+
if (parent / indicator).exists():
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def check_empty_init_files(project_root: Path) -> List[Violation]:
|
|
115
|
+
"""
|
|
116
|
+
Check for empty __init__.py files that serve no purpose.
|
|
117
|
+
|
|
118
|
+
Excludes Django-required directories (migrations, management, commands, templatetags)
|
|
119
|
+
which need empty __init__.py for module discovery.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
project_root: Root directory of the project
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of violations found (one per empty __init__.py)
|
|
126
|
+
"""
|
|
127
|
+
violations: List[Violation] = []
|
|
128
|
+
|
|
129
|
+
for init_file in project_root.rglob("__init__.py"):
|
|
130
|
+
if _should_exclude_path(init_file, project_root):
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# Skip Django-required empty inits
|
|
134
|
+
if _is_django_required_init(init_file):
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
content = init_file.read_text(encoding="utf-8")
|
|
138
|
+
if not content.strip():
|
|
139
|
+
relative_path = _normalize_path(init_file, project_root)
|
|
140
|
+
message = (
|
|
141
|
+
"Empty __init__.py file serves no purpose. "
|
|
142
|
+
"Either add exports/initialization or delete the file."
|
|
143
|
+
)
|
|
144
|
+
violations.append(Violation(file=relative_path, line=1, message=message))
|
|
145
|
+
|
|
146
|
+
return violations
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main(args: List[str]) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Run all file structure checks and print violations.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
args: Command line arguments (expects project root path)
|
|
155
|
+
|
|
156
|
+
Exits:
|
|
157
|
+
0 if all checks pass, 1 if violations found or invalid usage
|
|
158
|
+
"""
|
|
159
|
+
if len(args) == 0:
|
|
160
|
+
print("Usage: python file_structure_checks.py <project_root>")
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
|
|
163
|
+
project_root = Path(args[0])
|
|
164
|
+
if not project_root.exists():
|
|
165
|
+
print(f"Error: Project root does not exist: {project_root}")
|
|
166
|
+
sys.exit(1)
|
|
167
|
+
|
|
168
|
+
all_violations: List[Violation] = []
|
|
169
|
+
|
|
170
|
+
all_violations.extend(check_multiple_requirements_txt(project_root))
|
|
171
|
+
all_violations.extend(check_empty_init_files(project_root))
|
|
172
|
+
|
|
173
|
+
if all_violations:
|
|
174
|
+
for violation in all_violations:
|
|
175
|
+
print(f"{violation.file}:{violation.line}: {violation.message}")
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
|
|
178
|
+
sys.exit(0)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main(sys.argv[1:])
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Git and GitHub validation checks for pre-push review."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SUBPROCESS_TIMEOUT_SECONDS = 30
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Violation:
|
|
15
|
+
"""Represents a validation violation."""
|
|
16
|
+
file: str
|
|
17
|
+
line: int
|
|
18
|
+
message: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_current_branch() -> str:
|
|
22
|
+
"""Get the current git branch name."""
|
|
23
|
+
try:
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
26
|
+
capture_output=True,
|
|
27
|
+
text=True,
|
|
28
|
+
check=True,
|
|
29
|
+
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
|
30
|
+
)
|
|
31
|
+
return result.stdout.strip()
|
|
32
|
+
except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_draft_pr_state() -> List[Violation]:
|
|
37
|
+
"""
|
|
38
|
+
Check that PR is in draft state.
|
|
39
|
+
|
|
40
|
+
Returns empty list if:
|
|
41
|
+
- No PR exists for current branch
|
|
42
|
+
- gh CLI not available
|
|
43
|
+
- PR is in draft state
|
|
44
|
+
|
|
45
|
+
Returns violation if:
|
|
46
|
+
- PR is not in draft state
|
|
47
|
+
"""
|
|
48
|
+
branch = get_current_branch()
|
|
49
|
+
if not branch:
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
pr_info = subprocess.run(
|
|
54
|
+
["gh", "pr", "list", "--head", branch, "--json", "number,isDraft"],
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
check=True,
|
|
58
|
+
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
|
59
|
+
)
|
|
60
|
+
except FileNotFoundError:
|
|
61
|
+
return []
|
|
62
|
+
except subprocess.CalledProcessError:
|
|
63
|
+
return []
|
|
64
|
+
except subprocess.TimeoutExpired:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
if not pr_info.stdout.strip():
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
pr_data = json.loads(pr_info.stdout)
|
|
72
|
+
except json.JSONDecodeError:
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
if not pr_data:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
is_draft = pr_data[0].get("isDraft", False)
|
|
79
|
+
|
|
80
|
+
if is_draft:
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
return [
|
|
84
|
+
Violation(
|
|
85
|
+
file="",
|
|
86
|
+
line=0,
|
|
87
|
+
message="PR must be in draft state before pushing. Run: gh pr ready --undo",
|
|
88
|
+
)
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main() -> None:
|
|
93
|
+
"""Run all git checks and exit with appropriate code."""
|
|
94
|
+
violations: List[Violation] = []
|
|
95
|
+
|
|
96
|
+
violations.extend(check_draft_pr_state())
|
|
97
|
+
|
|
98
|
+
if violations:
|
|
99
|
+
for violation in violations:
|
|
100
|
+
print(violation.message)
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
sys.exit(0)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
main()
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Health checks for validator availability and status.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- Validator file existence checks
|
|
5
|
+
- Dependency availability checks
|
|
6
|
+
- Version tracking
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
VALIDATOR_FILES = [
|
|
19
|
+
"python_style_checks.py",
|
|
20
|
+
"test_safety_checks.py",
|
|
21
|
+
"file_structure_checks.py",
|
|
22
|
+
"react_checks.py",
|
|
23
|
+
"git_checks.py",
|
|
24
|
+
"comment_checks.py",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ValidatorHealth:
|
|
30
|
+
"""Health status of a single validator."""
|
|
31
|
+
|
|
32
|
+
name: str
|
|
33
|
+
healthy: bool
|
|
34
|
+
error: Optional[str] = None
|
|
35
|
+
last_modified: Optional[datetime] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class SystemHealth:
|
|
40
|
+
"""Overall system health status."""
|
|
41
|
+
|
|
42
|
+
all_healthy: bool
|
|
43
|
+
validators: Dict[str, ValidatorHealth]
|
|
44
|
+
python_version: str
|
|
45
|
+
optional_tools: Dict[str, bool]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_validator_exists(validator_path: Path) -> ValidatorHealth:
|
|
49
|
+
"""Check if a validator file exists and is readable.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
validator_path: Path to validator Python file
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
ValidatorHealth with status
|
|
56
|
+
"""
|
|
57
|
+
name = validator_path.stem
|
|
58
|
+
|
|
59
|
+
if not validator_path.exists():
|
|
60
|
+
return ValidatorHealth(
|
|
61
|
+
name=name,
|
|
62
|
+
healthy=False,
|
|
63
|
+
error=f"Validator not found: {validator_path}",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
validator_path.read_text(encoding="utf-8")
|
|
68
|
+
mtime = datetime.fromtimestamp(validator_path.stat().st_mtime)
|
|
69
|
+
return ValidatorHealth(
|
|
70
|
+
name=name,
|
|
71
|
+
healthy=True,
|
|
72
|
+
last_modified=mtime,
|
|
73
|
+
)
|
|
74
|
+
except (IOError, OSError, PermissionError) as error:
|
|
75
|
+
return ValidatorHealth(
|
|
76
|
+
name=name,
|
|
77
|
+
healthy=False,
|
|
78
|
+
error=f"Cannot read validator: {error}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check_all_validators(validators_dir: Path) -> Dict[str, ValidatorHealth]:
|
|
83
|
+
"""Check health of all required validators.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
validators_dir: Directory containing validator files
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dict mapping validator names to health status
|
|
90
|
+
"""
|
|
91
|
+
results: Dict[str, ValidatorHealth] = {}
|
|
92
|
+
|
|
93
|
+
for validator_file in VALIDATOR_FILES:
|
|
94
|
+
validator_path = validators_dir / validator_file
|
|
95
|
+
health = check_validator_exists(validator_path)
|
|
96
|
+
results[health.name] = health
|
|
97
|
+
|
|
98
|
+
return results
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_optional_tool(tool_name: str) -> bool:
|
|
102
|
+
"""Check if an optional tool is available.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
tool_name: Name of tool to check (ruff, mypy, isort)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if tool is available
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
result = subprocess.run(
|
|
112
|
+
[tool_name, "--version"],
|
|
113
|
+
capture_output=True,
|
|
114
|
+
text=True,
|
|
115
|
+
)
|
|
116
|
+
return result.returncode == 0
|
|
117
|
+
except FileNotFoundError:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_validator_version(validators_dir: Optional[Path] = None) -> str:
|
|
122
|
+
"""Get a version string for the validator suite.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
validators_dir: Optional override for validators directory
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Version string based on file hashes
|
|
129
|
+
"""
|
|
130
|
+
if validators_dir is None:
|
|
131
|
+
validators_dir = Path(__file__).parent
|
|
132
|
+
hasher = hashlib.md5()
|
|
133
|
+
|
|
134
|
+
for validator_file in sorted(VALIDATOR_FILES):
|
|
135
|
+
validator_path = validators_dir / validator_file
|
|
136
|
+
if validator_path.exists():
|
|
137
|
+
content = validator_path.read_bytes()
|
|
138
|
+
hasher.update(content)
|
|
139
|
+
|
|
140
|
+
return hasher.hexdigest()[:8]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_system_health(validators_dir: Optional[Path] = None) -> SystemHealth:
|
|
144
|
+
"""Get complete system health status.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
validators_dir: Optional override for validators directory
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
SystemHealth with all status information
|
|
151
|
+
"""
|
|
152
|
+
if validators_dir is None:
|
|
153
|
+
validators_dir = Path(__file__).parent
|
|
154
|
+
|
|
155
|
+
validators = check_all_validators(validators_dir)
|
|
156
|
+
all_healthy = all(v.healthy for v in validators.values())
|
|
157
|
+
|
|
158
|
+
optional_tools = {
|
|
159
|
+
"ruff": check_optional_tool("ruff"),
|
|
160
|
+
"mypy": check_optional_tool("mypy"),
|
|
161
|
+
"isort": check_optional_tool("isort"),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return SystemHealth(
|
|
165
|
+
all_healthy=all_healthy,
|
|
166
|
+
validators=validators,
|
|
167
|
+
python_version=sys.version,
|
|
168
|
+
optional_tools=optional_tools,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def print_health_report(health: SystemHealth) -> None:
|
|
173
|
+
"""Print a formatted health report.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
health: SystemHealth to report
|
|
177
|
+
"""
|
|
178
|
+
print("=" * 60)
|
|
179
|
+
print("VALIDATOR HEALTH CHECK")
|
|
180
|
+
print("=" * 60)
|
|
181
|
+
print()
|
|
182
|
+
|
|
183
|
+
print(f"Python: {health.python_version.split()[0]}")
|
|
184
|
+
print(f"Version: {get_validator_version()}")
|
|
185
|
+
print()
|
|
186
|
+
|
|
187
|
+
print("Required Validators:")
|
|
188
|
+
for name, validator in sorted(health.validators.items()):
|
|
189
|
+
status = "[OK]" if validator.healthy else "[MISSING]"
|
|
190
|
+
print(f" {status} {name}")
|
|
191
|
+
if validator.error:
|
|
192
|
+
print(f" Error: {validator.error}")
|
|
193
|
+
print()
|
|
194
|
+
|
|
195
|
+
print("Optional Tools:")
|
|
196
|
+
for tool, available in sorted(health.optional_tools.items()):
|
|
197
|
+
status = "[OK]" if available else "[NOT INSTALLED]"
|
|
198
|
+
print(f" {status} {tool}")
|
|
199
|
+
print()
|
|
200
|
+
|
|
201
|
+
overall = "HEALTHY" if health.all_healthy else "DEGRADED"
|
|
202
|
+
print(f"Overall Status: {overall}")
|
|
203
|
+
print("=" * 60)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main() -> int:
|
|
207
|
+
"""Run health check and print report."""
|
|
208
|
+
health = get_system_health()
|
|
209
|
+
print_health_report(health)
|
|
210
|
+
return 0 if health.all_healthy else 1
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
sys.exit(main())
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Magic value detection validator.
|
|
2
|
+
|
|
3
|
+
Implements check 7: No hardcoded magic values.
|
|
4
|
+
Use named constants instead of magic numbers.
|
|
5
|
+
|
|
6
|
+
Note: Only checks for magic numbers. Magic string detection is not implemented.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Set
|
|
13
|
+
|
|
14
|
+
from validator_base import Violation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ALLOWED_NUMBERS: Set[int] = frozenset({-1, 0, 1, 2, 100})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_magic_values(tree: ast.AST, filename: str) -> List[Violation]:
|
|
21
|
+
violations: List[Violation] = []
|
|
22
|
+
|
|
23
|
+
constant_names: Set[str] = set()
|
|
24
|
+
for node in ast.walk(tree):
|
|
25
|
+
if isinstance(node, ast.Assign):
|
|
26
|
+
for target in node.targets:
|
|
27
|
+
if isinstance(target, ast.Name) and target.id.isupper():
|
|
28
|
+
constant_names.add(target.id)
|
|
29
|
+
|
|
30
|
+
for node in ast.walk(tree):
|
|
31
|
+
if isinstance(node, ast.Constant):
|
|
32
|
+
if isinstance(node.value, int) and node.value not in ALLOWED_NUMBERS:
|
|
33
|
+
if not _is_in_constant_definition(node, tree):
|
|
34
|
+
violations.append(
|
|
35
|
+
Violation(
|
|
36
|
+
filename,
|
|
37
|
+
node.lineno,
|
|
38
|
+
f"Magic number {node.value} - use named constant",
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return violations
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_in_constant_definition(node: ast.Constant, tree: ast.AST) -> bool:
|
|
46
|
+
for parent in ast.walk(tree):
|
|
47
|
+
if isinstance(parent, ast.Assign):
|
|
48
|
+
for target in parent.targets:
|
|
49
|
+
if isinstance(target, ast.Name) and target.id.isupper():
|
|
50
|
+
if parent.value is node:
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_file(file_path: Path) -> List[Violation]:
|
|
56
|
+
filename = str(file_path)
|
|
57
|
+
try:
|
|
58
|
+
source = file_path.read_text(encoding="utf-8")
|
|
59
|
+
tree = ast.parse(source)
|
|
60
|
+
except Exception as error:
|
|
61
|
+
return [Violation(filename, 0, f"Error: {error}")]
|
|
62
|
+
|
|
63
|
+
return check_magic_values(tree, filename)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main() -> int:
|
|
67
|
+
if len(sys.argv) < 2:
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
all_violations: List[Violation] = []
|
|
71
|
+
for file_arg in sys.argv[1:]:
|
|
72
|
+
all_violations.extend(validate_file(Path(file_arg)))
|
|
73
|
+
|
|
74
|
+
for violation in all_violations:
|
|
75
|
+
print(violation)
|
|
76
|
+
|
|
77
|
+
return 1 if all_violations else 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
sys.exit(main())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Mypy integration for static type checking."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class MypyResult:
|
|
10
|
+
passed: bool
|
|
11
|
+
output: str
|
|
12
|
+
error_count: int
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_mypy_available() -> bool:
|
|
16
|
+
"""Check if mypy is installed."""
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
["mypy", "--version"],
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
)
|
|
23
|
+
return result.returncode == 0
|
|
24
|
+
except FileNotFoundError:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_mypy_check(files: list[Path]) -> MypyResult:
|
|
29
|
+
"""Run mypy on files."""
|
|
30
|
+
if not files:
|
|
31
|
+
return MypyResult(passed=True, output="No files to check", error_count=0)
|
|
32
|
+
|
|
33
|
+
if not check_mypy_available():
|
|
34
|
+
return MypyResult(passed=True, output="Mypy not installed - skipping", error_count=0)
|
|
35
|
+
|
|
36
|
+
py_files = [str(f) for f in files if f.suffix == ".py"]
|
|
37
|
+
if not py_files:
|
|
38
|
+
return MypyResult(passed=True, output="No Python files", error_count=0)
|
|
39
|
+
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
["mypy", "--ignore-missing-imports", "--no-error-summary"] + py_files,
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
error_count = result.stdout.count(": error:")
|
|
47
|
+
|
|
48
|
+
return MypyResult(
|
|
49
|
+
passed=result.returncode == 0,
|
|
50
|
+
output=result.stdout or result.stderr or "No type errors",
|
|
51
|
+
error_count=error_count,
|
|
52
|
+
)
|