anvil-dev-framework 0.1.6
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/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- package/scripts/verify.sh +255 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
verification_runner.py - Verification suite runner for /verify command (ANV-149)
|
|
4
|
+
|
|
5
|
+
Executes test, lint, and type check commands with output capture and error parsing.
|
|
6
|
+
Supports project-specific configuration via .claude/settings.yaml.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from verification_runner import VerificationRunner, VerificationResult
|
|
10
|
+
|
|
11
|
+
runner = VerificationRunner()
|
|
12
|
+
result = runner.run_all()
|
|
13
|
+
|
|
14
|
+
if result.passed:
|
|
15
|
+
print("All checks passed!")
|
|
16
|
+
else:
|
|
17
|
+
for error in result.errors:
|
|
18
|
+
print(f"{error.file}:{error.line} - {error.message}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
import time
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import yaml
|
|
31
|
+
HAS_YAML = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
HAS_YAML = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# Data Models
|
|
38
|
+
# =============================================================================
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class VerificationError:
|
|
42
|
+
"""A single verification error with location information."""
|
|
43
|
+
file: str
|
|
44
|
+
line: Optional[int] = None
|
|
45
|
+
column: Optional[int] = None
|
|
46
|
+
message: str = ""
|
|
47
|
+
severity: Literal["error", "warning"] = "error"
|
|
48
|
+
source: Literal["test", "lint", "types"] = "test"
|
|
49
|
+
raw_output: str = ""
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"file": self.file,
|
|
54
|
+
"line": self.line,
|
|
55
|
+
"column": self.column,
|
|
56
|
+
"message": self.message,
|
|
57
|
+
"severity": self.severity,
|
|
58
|
+
"source": self.source,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class CheckResult:
|
|
64
|
+
"""Result of a single verification check (test/lint/types)."""
|
|
65
|
+
check_type: Literal["test", "lint", "types"]
|
|
66
|
+
passed: bool
|
|
67
|
+
exit_code: int
|
|
68
|
+
duration_ms: int
|
|
69
|
+
output: str
|
|
70
|
+
errors: List[VerificationError] = field(default_factory=list)
|
|
71
|
+
count: Optional[int] = None # Number of tests passed, etc.
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
74
|
+
return {
|
|
75
|
+
"check_type": self.check_type,
|
|
76
|
+
"passed": self.passed,
|
|
77
|
+
"exit_code": self.exit_code,
|
|
78
|
+
"duration_ms": self.duration_ms,
|
|
79
|
+
"errors": [e.to_dict() for e in self.errors],
|
|
80
|
+
"count": self.count,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class VerificationResult:
|
|
86
|
+
"""Complete verification suite result."""
|
|
87
|
+
passed: bool
|
|
88
|
+
tests: Optional[CheckResult] = None
|
|
89
|
+
lint: Optional[CheckResult] = None
|
|
90
|
+
types: Optional[CheckResult] = None
|
|
91
|
+
total_duration_ms: int = 0
|
|
92
|
+
errors: List[VerificationError] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
95
|
+
return {
|
|
96
|
+
"passed": self.passed,
|
|
97
|
+
"tests": self.tests.to_dict() if self.tests else None,
|
|
98
|
+
"lint": self.lint.to_dict() if self.lint else None,
|
|
99
|
+
"types": self.types.to_dict() if self.types else None,
|
|
100
|
+
"total_duration_ms": self.total_duration_ms,
|
|
101
|
+
"error_count": len(self.errors),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def to_json(self) -> str:
|
|
105
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class VerificationConfig:
|
|
110
|
+
"""Configuration for verification commands."""
|
|
111
|
+
test_command: Optional[str] = None
|
|
112
|
+
lint_command: Optional[str] = None
|
|
113
|
+
types_command: Optional[str] = None
|
|
114
|
+
max_iterations: int = 3
|
|
115
|
+
required_for_completion: bool = True
|
|
116
|
+
timeout_seconds: int = 300
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_dict(cls, data: Dict[str, Any]) -> "VerificationConfig":
|
|
120
|
+
commands = data.get("commands", {})
|
|
121
|
+
return cls(
|
|
122
|
+
test_command=commands.get("test"),
|
|
123
|
+
lint_command=commands.get("lint"),
|
|
124
|
+
types_command=commands.get("types"),
|
|
125
|
+
max_iterations=data.get("max_iterations", 3),
|
|
126
|
+
required_for_completion=data.get("required_for_completion", True),
|
|
127
|
+
timeout_seconds=data.get("timeout_seconds", 300),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# =============================================================================
|
|
132
|
+
# Project Type Detection
|
|
133
|
+
# =============================================================================
|
|
134
|
+
|
|
135
|
+
class ProjectDetector:
|
|
136
|
+
"""Detect project type and default verification commands."""
|
|
137
|
+
|
|
138
|
+
NODEJS_DEFAULTS = {
|
|
139
|
+
"test": "npm test",
|
|
140
|
+
"lint": "npm run lint",
|
|
141
|
+
"types": "npm run typecheck",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
PYTHON_DEFAULTS = {
|
|
145
|
+
"test": "pytest",
|
|
146
|
+
"lint": "ruff check .",
|
|
147
|
+
"types": "mypy .",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
RUST_DEFAULTS = {
|
|
151
|
+
"test": "cargo test",
|
|
152
|
+
"lint": "cargo clippy",
|
|
153
|
+
"types": "cargo check",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
GO_DEFAULTS = {
|
|
157
|
+
"test": "go test ./...",
|
|
158
|
+
"lint": "golangci-lint run",
|
|
159
|
+
"types": None, # Go is statically typed at compile
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def detect(cls, project_root: Path) -> Tuple[str, Dict[str, Optional[str]]]:
|
|
164
|
+
"""Detect project type and return default commands."""
|
|
165
|
+
if (project_root / "package.json").exists():
|
|
166
|
+
return "nodejs", cls.NODEJS_DEFAULTS
|
|
167
|
+
|
|
168
|
+
if (project_root / "pyproject.toml").exists() or \
|
|
169
|
+
(project_root / "setup.py").exists() or \
|
|
170
|
+
(project_root / "requirements.txt").exists():
|
|
171
|
+
return "python", cls.PYTHON_DEFAULTS
|
|
172
|
+
|
|
173
|
+
if (project_root / "Cargo.toml").exists():
|
|
174
|
+
return "rust", cls.RUST_DEFAULTS
|
|
175
|
+
|
|
176
|
+
if (project_root / "go.mod").exists():
|
|
177
|
+
return "go", cls.GO_DEFAULTS
|
|
178
|
+
|
|
179
|
+
# Default to Node.js style
|
|
180
|
+
return "unknown", cls.NODEJS_DEFAULTS
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# =============================================================================
|
|
184
|
+
# Error Parsers
|
|
185
|
+
# =============================================================================
|
|
186
|
+
|
|
187
|
+
class ErrorParser:
|
|
188
|
+
"""Parse errors from command output."""
|
|
189
|
+
|
|
190
|
+
# Common patterns for extracting file:line:column
|
|
191
|
+
FILE_LINE_PATTERNS = [
|
|
192
|
+
# TypeScript/JavaScript: src/file.ts(10,5): error TS2345
|
|
193
|
+
re.compile(r"^(.+?)\((\d+),(\d+)\):\s*(.+)$", re.MULTILINE),
|
|
194
|
+
# ESLint/Ruff: src/file.ts:10:5: error message
|
|
195
|
+
re.compile(r"^(.+?):(\d+):(\d+):\s*(.+)$", re.MULTILINE),
|
|
196
|
+
# Python traceback: File "src/file.py", line 10
|
|
197
|
+
re.compile(r'File "(.+?)", line (\d+)', re.MULTILINE),
|
|
198
|
+
# Jest/Vitest: at Object.<anonymous> (src/file.test.ts:10:5)
|
|
199
|
+
re.compile(r"at .+? \((.+?):(\d+):(\d+)\)", re.MULTILINE),
|
|
200
|
+
# Generic: src/file.ts:10
|
|
201
|
+
re.compile(r"^(.+?):(\d+)(?::\d+)?(?:\s|$)", re.MULTILINE),
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
# Test failure patterns
|
|
205
|
+
TEST_FAILURE_PATTERNS = [
|
|
206
|
+
# Jest/Vitest: FAIL src/file.test.ts
|
|
207
|
+
re.compile(r"FAIL\s+(.+\.(?:test|spec)\.[jt]sx?)"),
|
|
208
|
+
# Pytest: FAILED test_file.py::test_name
|
|
209
|
+
re.compile(r"FAILED\s+(.+?)::", re.MULTILINE),
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def parse_errors(
|
|
214
|
+
cls,
|
|
215
|
+
output: str,
|
|
216
|
+
source: Literal["test", "lint", "types"]
|
|
217
|
+
) -> List[VerificationError]:
|
|
218
|
+
"""Parse errors from command output."""
|
|
219
|
+
errors = []
|
|
220
|
+
seen = set()
|
|
221
|
+
|
|
222
|
+
for pattern in cls.FILE_LINE_PATTERNS:
|
|
223
|
+
for match in pattern.finditer(output):
|
|
224
|
+
groups = match.groups()
|
|
225
|
+
file_path = groups[0] if groups else None
|
|
226
|
+
|
|
227
|
+
if not file_path or file_path in seen:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
# Skip common false positives
|
|
231
|
+
if any(skip in file_path for skip in [
|
|
232
|
+
"node_modules", ".git", "__pycache__", "site-packages"
|
|
233
|
+
]):
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
line = int(groups[1]) if len(groups) > 1 and groups[1] else None
|
|
237
|
+
column = int(groups[2]) if len(groups) > 2 and groups[2] and groups[2].isdigit() else None
|
|
238
|
+
message = groups[3] if len(groups) > 3 else ""
|
|
239
|
+
|
|
240
|
+
# Clean up message
|
|
241
|
+
message = message.strip()[:200]
|
|
242
|
+
|
|
243
|
+
errors.append(VerificationError(
|
|
244
|
+
file=file_path,
|
|
245
|
+
line=line,
|
|
246
|
+
column=column,
|
|
247
|
+
message=message,
|
|
248
|
+
source=source,
|
|
249
|
+
))
|
|
250
|
+
seen.add(file_path)
|
|
251
|
+
|
|
252
|
+
# Also check for test-specific failure patterns
|
|
253
|
+
if source == "test":
|
|
254
|
+
for pattern in cls.TEST_FAILURE_PATTERNS:
|
|
255
|
+
for match in pattern.finditer(output):
|
|
256
|
+
file_path = match.group(1)
|
|
257
|
+
if file_path and file_path not in seen:
|
|
258
|
+
errors.append(VerificationError(
|
|
259
|
+
file=file_path,
|
|
260
|
+
line=None,
|
|
261
|
+
message="Test failed",
|
|
262
|
+
source=source,
|
|
263
|
+
))
|
|
264
|
+
seen.add(file_path)
|
|
265
|
+
|
|
266
|
+
return errors
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def extract_test_count(cls, output: str) -> Optional[int]:
|
|
270
|
+
"""Extract number of passed tests from output."""
|
|
271
|
+
patterns = [
|
|
272
|
+
# Jest/Vitest: Tests: 47 passed
|
|
273
|
+
re.compile(r"Tests?:\s*(\d+)\s*passed"),
|
|
274
|
+
# Pytest: 47 passed
|
|
275
|
+
re.compile(r"(\d+)\s*passed"),
|
|
276
|
+
# Cargo: test result: ok. 47 passed
|
|
277
|
+
re.compile(r"(\d+)\s*passed"),
|
|
278
|
+
]
|
|
279
|
+
for pattern in patterns:
|
|
280
|
+
match = pattern.search(output)
|
|
281
|
+
if match:
|
|
282
|
+
return int(match.group(1))
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# =============================================================================
|
|
287
|
+
# Verification Runner
|
|
288
|
+
# =============================================================================
|
|
289
|
+
|
|
290
|
+
class VerificationRunner:
|
|
291
|
+
"""Run verification suite with output capture and error parsing."""
|
|
292
|
+
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
project_root: Optional[Path] = None,
|
|
296
|
+
config: Optional[VerificationConfig] = None,
|
|
297
|
+
):
|
|
298
|
+
self.project_root = project_root or Path.cwd()
|
|
299
|
+
self.config = config or self._load_config()
|
|
300
|
+
self.project_type, self.default_commands = ProjectDetector.detect(self.project_root)
|
|
301
|
+
|
|
302
|
+
def _load_config(self) -> VerificationConfig:
|
|
303
|
+
"""Load configuration from .claude/settings.yaml."""
|
|
304
|
+
config_path = self.project_root / ".claude" / "settings.yaml"
|
|
305
|
+
|
|
306
|
+
if not config_path.exists() or not HAS_YAML:
|
|
307
|
+
return VerificationConfig()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
with open(config_path) as f:
|
|
311
|
+
data = yaml.safe_load(f) or {}
|
|
312
|
+
verification_data = data.get("verification", {})
|
|
313
|
+
return VerificationConfig.from_dict(verification_data)
|
|
314
|
+
except Exception:
|
|
315
|
+
return VerificationConfig()
|
|
316
|
+
|
|
317
|
+
def _get_command(self, check_type: Literal["test", "lint", "types"]) -> Optional[str]:
|
|
318
|
+
"""Get command for a check type, preferring config over defaults."""
|
|
319
|
+
if check_type == "test":
|
|
320
|
+
return self.config.test_command or self.default_commands.get("test")
|
|
321
|
+
elif check_type == "lint":
|
|
322
|
+
return self.config.lint_command or self.default_commands.get("lint")
|
|
323
|
+
elif check_type == "types":
|
|
324
|
+
return self.config.types_command or self.default_commands.get("types")
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
def _run_command(
|
|
328
|
+
self,
|
|
329
|
+
command: str,
|
|
330
|
+
check_type: Literal["test", "lint", "types"],
|
|
331
|
+
) -> CheckResult:
|
|
332
|
+
"""Run a single verification command."""
|
|
333
|
+
start_time = time.time()
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
result = subprocess.run(
|
|
337
|
+
command,
|
|
338
|
+
shell=True,
|
|
339
|
+
capture_output=True,
|
|
340
|
+
text=True,
|
|
341
|
+
timeout=self.config.timeout_seconds,
|
|
342
|
+
cwd=str(self.project_root),
|
|
343
|
+
)
|
|
344
|
+
output = result.stdout + "\n" + result.stderr
|
|
345
|
+
exit_code = result.returncode
|
|
346
|
+
passed = exit_code == 0
|
|
347
|
+
|
|
348
|
+
except subprocess.TimeoutExpired:
|
|
349
|
+
output = f"Command timed out after {self.config.timeout_seconds}s"
|
|
350
|
+
exit_code = -1
|
|
351
|
+
passed = False
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
output = f"Command failed: {str(e)}"
|
|
355
|
+
exit_code = -1
|
|
356
|
+
passed = False
|
|
357
|
+
|
|
358
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
359
|
+
|
|
360
|
+
# Parse errors from output
|
|
361
|
+
errors = []
|
|
362
|
+
if not passed:
|
|
363
|
+
errors = ErrorParser.parse_errors(output, check_type)
|
|
364
|
+
|
|
365
|
+
# Extract test count if applicable
|
|
366
|
+
count = None
|
|
367
|
+
if check_type == "test" and passed:
|
|
368
|
+
count = ErrorParser.extract_test_count(output)
|
|
369
|
+
|
|
370
|
+
return CheckResult(
|
|
371
|
+
check_type=check_type,
|
|
372
|
+
passed=passed,
|
|
373
|
+
exit_code=exit_code,
|
|
374
|
+
duration_ms=duration_ms,
|
|
375
|
+
output=output,
|
|
376
|
+
errors=errors,
|
|
377
|
+
count=count,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def run_tests(self) -> Optional[CheckResult]:
|
|
381
|
+
"""Run test suite."""
|
|
382
|
+
command = self._get_command("test")
|
|
383
|
+
if not command:
|
|
384
|
+
return None
|
|
385
|
+
return self._run_command(command, "test")
|
|
386
|
+
|
|
387
|
+
def run_lint(self) -> Optional[CheckResult]:
|
|
388
|
+
"""Run linter."""
|
|
389
|
+
command = self._get_command("lint")
|
|
390
|
+
if not command:
|
|
391
|
+
return None
|
|
392
|
+
return self._run_command(command, "lint")
|
|
393
|
+
|
|
394
|
+
def run_types(self) -> Optional[CheckResult]:
|
|
395
|
+
"""Run type checker."""
|
|
396
|
+
command = self._get_command("types")
|
|
397
|
+
if not command:
|
|
398
|
+
return None
|
|
399
|
+
return self._run_command(command, "types")
|
|
400
|
+
|
|
401
|
+
def run_all(
|
|
402
|
+
self,
|
|
403
|
+
skip_tests: bool = False,
|
|
404
|
+
skip_lint: bool = False,
|
|
405
|
+
skip_types: bool = False,
|
|
406
|
+
) -> VerificationResult:
|
|
407
|
+
"""Run all verification checks."""
|
|
408
|
+
start_time = time.time()
|
|
409
|
+
|
|
410
|
+
tests = None if skip_tests else self.run_tests()
|
|
411
|
+
lint = None if skip_lint else self.run_lint()
|
|
412
|
+
types = None if skip_types else self.run_types()
|
|
413
|
+
|
|
414
|
+
# Collect all errors
|
|
415
|
+
all_errors = []
|
|
416
|
+
if tests and not tests.passed:
|
|
417
|
+
all_errors.extend(tests.errors)
|
|
418
|
+
if lint and not lint.passed:
|
|
419
|
+
all_errors.extend(lint.errors)
|
|
420
|
+
if types and not types.passed:
|
|
421
|
+
all_errors.extend(types.errors)
|
|
422
|
+
|
|
423
|
+
# Determine overall pass/fail
|
|
424
|
+
passed = True
|
|
425
|
+
if tests and not tests.passed:
|
|
426
|
+
passed = False
|
|
427
|
+
if lint and not lint.passed:
|
|
428
|
+
passed = False
|
|
429
|
+
if types and not types.passed:
|
|
430
|
+
passed = False
|
|
431
|
+
|
|
432
|
+
total_duration_ms = int((time.time() - start_time) * 1000)
|
|
433
|
+
|
|
434
|
+
return VerificationResult(
|
|
435
|
+
passed=passed,
|
|
436
|
+
tests=tests,
|
|
437
|
+
lint=lint,
|
|
438
|
+
types=types,
|
|
439
|
+
total_duration_ms=total_duration_ms,
|
|
440
|
+
errors=all_errors,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
def run_single(
|
|
444
|
+
self,
|
|
445
|
+
check_type: Literal["test", "lint", "types"]
|
|
446
|
+
) -> Optional[CheckResult]:
|
|
447
|
+
"""Run a single verification check."""
|
|
448
|
+
if check_type == "test":
|
|
449
|
+
return self.run_tests()
|
|
450
|
+
elif check_type == "lint":
|
|
451
|
+
return self.run_lint()
|
|
452
|
+
elif check_type == "types":
|
|
453
|
+
return self.run_types()
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# =============================================================================
|
|
458
|
+
# State Management
|
|
459
|
+
# =============================================================================
|
|
460
|
+
|
|
461
|
+
class VerificationState:
|
|
462
|
+
"""Manage verification state persistence."""
|
|
463
|
+
|
|
464
|
+
STATE_FILE = ".claude/verification-state.json"
|
|
465
|
+
|
|
466
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
467
|
+
self.project_root = project_root or Path.cwd()
|
|
468
|
+
self.state_path = self.project_root / self.STATE_FILE
|
|
469
|
+
|
|
470
|
+
def save(self, result: VerificationResult, iteration: int = 0) -> None:
|
|
471
|
+
"""Save verification state to file."""
|
|
472
|
+
from datetime import datetime
|
|
473
|
+
|
|
474
|
+
state = {
|
|
475
|
+
"last_run": datetime.now().isoformat(),
|
|
476
|
+
"status": "passed" if result.passed else "failed",
|
|
477
|
+
"iteration": iteration,
|
|
478
|
+
"results": result.to_dict(),
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
482
|
+
with open(self.state_path, "w") as f:
|
|
483
|
+
json.dump(state, f, indent=2)
|
|
484
|
+
|
|
485
|
+
def load(self) -> Optional[Dict[str, Any]]:
|
|
486
|
+
"""Load verification state from file."""
|
|
487
|
+
if not self.state_path.exists():
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
with open(self.state_path) as f:
|
|
492
|
+
return json.load(f)
|
|
493
|
+
except Exception:
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
def clear(self) -> None:
|
|
497
|
+
"""Clear verification state."""
|
|
498
|
+
if self.state_path.exists():
|
|
499
|
+
self.state_path.unlink()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# =============================================================================
|
|
503
|
+
# CLI Interface
|
|
504
|
+
# =============================================================================
|
|
505
|
+
|
|
506
|
+
def main():
|
|
507
|
+
"""CLI entry point for verification runner."""
|
|
508
|
+
import argparse
|
|
509
|
+
|
|
510
|
+
parser = argparse.ArgumentParser(description="Run verification suite")
|
|
511
|
+
parser.add_argument("--test-only", action="store_true", help="Run tests only")
|
|
512
|
+
parser.add_argument("--lint-only", action="store_true", help="Run lint only")
|
|
513
|
+
parser.add_argument("--types-only", action="store_true", help="Run types only")
|
|
514
|
+
parser.add_argument("--quick", action="store_true", help="Skip type checking")
|
|
515
|
+
parser.add_argument("--json", action="store_true", help="Output JSON")
|
|
516
|
+
parser.add_argument("--project", type=str, help="Project root path")
|
|
517
|
+
|
|
518
|
+
args = parser.parse_args()
|
|
519
|
+
|
|
520
|
+
project_root = Path(args.project) if args.project else Path.cwd()
|
|
521
|
+
runner = VerificationRunner(project_root=project_root)
|
|
522
|
+
|
|
523
|
+
if args.test_only:
|
|
524
|
+
result = runner.run_tests()
|
|
525
|
+
if result:
|
|
526
|
+
print(result.to_dict() if args.json else f"Tests: {'PASS' if result.passed else 'FAIL'}")
|
|
527
|
+
elif args.lint_only:
|
|
528
|
+
result = runner.run_lint()
|
|
529
|
+
if result:
|
|
530
|
+
print(result.to_dict() if args.json else f"Lint: {'PASS' if result.passed else 'FAIL'}")
|
|
531
|
+
elif args.types_only:
|
|
532
|
+
result = runner.run_types()
|
|
533
|
+
if result:
|
|
534
|
+
print(result.to_dict() if args.json else f"Types: {'PASS' if result.passed else 'FAIL'}")
|
|
535
|
+
else:
|
|
536
|
+
result = runner.run_all(skip_types=args.quick)
|
|
537
|
+
if args.json:
|
|
538
|
+
print(result.to_json())
|
|
539
|
+
else:
|
|
540
|
+
status = "PASS" if result.passed else "FAIL"
|
|
541
|
+
print(f"Verification: {status}")
|
|
542
|
+
if result.tests:
|
|
543
|
+
count_str = f" ({result.tests.count} passed)" if result.tests.count else ""
|
|
544
|
+
print(f" Tests: {'PASS' if result.tests.passed else 'FAIL'}{count_str}")
|
|
545
|
+
if result.lint:
|
|
546
|
+
print(f" Lint: {'PASS' if result.lint.passed else 'FAIL'}")
|
|
547
|
+
if result.types:
|
|
548
|
+
print(f" Types: {'PASS' if result.types.passed else 'FAIL'}")
|
|
549
|
+
if result.errors:
|
|
550
|
+
print(f"\nErrors ({len(result.errors)}):")
|
|
551
|
+
for err in result.errors[:10]:
|
|
552
|
+
loc = f"{err.file}:{err.line}" if err.line else err.file
|
|
553
|
+
print(f" - {loc}: {err.message[:80]}")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
if __name__ == "__main__":
|
|
557
|
+
main()
|