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,470 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
quality_service.py - Quality gate data aggregation for HUD (ANV-103/104)
|
|
4
|
+
|
|
5
|
+
Runs local quality checks (tests, lint, types) and aggregates results
|
|
6
|
+
for display in the Anvil HUD Quality Gates panel.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from quality_service import QualityService
|
|
10
|
+
|
|
11
|
+
service = QualityService("/path/to/project")
|
|
12
|
+
results = service.run_checks()
|
|
13
|
+
print(results["tests"]["passed"], results["tests"]["failed"])
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import subprocess
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Dict, Optional, TypedDict
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CheckResult(TypedDict, total=False):
|
|
24
|
+
"""Result of a single quality check."""
|
|
25
|
+
status: str # "passed", "failed", "running", "skipped"
|
|
26
|
+
passed: int # Number passed
|
|
27
|
+
failed: int # Number failed
|
|
28
|
+
warnings: int # Number of warnings
|
|
29
|
+
errors: int # Number of errors
|
|
30
|
+
duration_ms: int # How long the check took
|
|
31
|
+
message: str # Human-readable message
|
|
32
|
+
last_run: float # Timestamp of last run
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class QualityGateResult(TypedDict, total=False):
|
|
36
|
+
"""Aggregated quality gate results for a project."""
|
|
37
|
+
project_path: str
|
|
38
|
+
branch: str
|
|
39
|
+
tests: CheckResult
|
|
40
|
+
lint: CheckResult
|
|
41
|
+
types: CheckResult
|
|
42
|
+
ready_to_merge: bool # All gates passed
|
|
43
|
+
blocking_issues: int # Count of blocking problems
|
|
44
|
+
last_check: float # Timestamp of last full check
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Cache TTL in seconds
|
|
48
|
+
CACHE_TTL = 30.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class QualityService:
|
|
52
|
+
"""Service for running and caching quality checks (ANV-103/104)."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, project_path: Optional[str] = None):
|
|
55
|
+
"""Initialize the service.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
project_path: Path to project root. Defaults to cwd.
|
|
59
|
+
"""
|
|
60
|
+
self.project_path = Path(project_path) if project_path else Path.cwd()
|
|
61
|
+
self._cache: Dict[str, QualityGateResult] = {}
|
|
62
|
+
self._last_check: Dict[str, float] = {}
|
|
63
|
+
|
|
64
|
+
def get_results(self, force_refresh: bool = False) -> QualityGateResult:
|
|
65
|
+
"""Get quality gate results, using cache if fresh.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
force_refresh: Force running checks even if cache is fresh
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
QualityGateResult with all check statuses
|
|
72
|
+
"""
|
|
73
|
+
cache_key = str(self.project_path)
|
|
74
|
+
now = time.time()
|
|
75
|
+
|
|
76
|
+
# Check cache
|
|
77
|
+
if not force_refresh and cache_key in self._cache:
|
|
78
|
+
last = self._last_check.get(cache_key, 0)
|
|
79
|
+
if now - last < CACHE_TTL:
|
|
80
|
+
return self._cache[cache_key]
|
|
81
|
+
|
|
82
|
+
# Run fresh checks
|
|
83
|
+
result = self.run_checks()
|
|
84
|
+
self._cache[cache_key] = result
|
|
85
|
+
self._last_check[cache_key] = now
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
def run_checks(self) -> QualityGateResult:
|
|
90
|
+
"""Run all quality checks and return aggregated results."""
|
|
91
|
+
branch = self._get_branch()
|
|
92
|
+
|
|
93
|
+
# Run checks in sequence (could be parallelized for speed)
|
|
94
|
+
tests = self._run_tests()
|
|
95
|
+
lint = self._run_lint()
|
|
96
|
+
types = self._run_typecheck()
|
|
97
|
+
|
|
98
|
+
# Calculate blocking issues and merge readiness
|
|
99
|
+
blocking = 0
|
|
100
|
+
if tests.get("failed", 0) > 0:
|
|
101
|
+
blocking += tests["failed"]
|
|
102
|
+
if lint.get("errors", 0) > 0:
|
|
103
|
+
blocking += lint["errors"]
|
|
104
|
+
if types.get("errors", 0) > 0:
|
|
105
|
+
blocking += types["errors"]
|
|
106
|
+
|
|
107
|
+
ready = (
|
|
108
|
+
tests.get("status") == "passed" and
|
|
109
|
+
lint.get("status") == "passed" and
|
|
110
|
+
types.get("status") == "passed"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"project_path": str(self.project_path),
|
|
115
|
+
"branch": branch,
|
|
116
|
+
"tests": tests,
|
|
117
|
+
"lint": lint,
|
|
118
|
+
"types": types,
|
|
119
|
+
"ready_to_merge": ready,
|
|
120
|
+
"blocking_issues": blocking,
|
|
121
|
+
"last_check": time.time(),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def _get_branch(self) -> str:
|
|
125
|
+
"""Get current git branch name."""
|
|
126
|
+
try:
|
|
127
|
+
result = subprocess.run(
|
|
128
|
+
["git", "branch", "--show-current"],
|
|
129
|
+
cwd=self.project_path,
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True,
|
|
132
|
+
timeout=5,
|
|
133
|
+
)
|
|
134
|
+
return result.stdout.strip() or "unknown"
|
|
135
|
+
except Exception:
|
|
136
|
+
return "unknown"
|
|
137
|
+
|
|
138
|
+
def _run_tests(self) -> CheckResult:
|
|
139
|
+
"""Run test suite and parse results."""
|
|
140
|
+
start = time.time()
|
|
141
|
+
|
|
142
|
+
# Check which test command is available
|
|
143
|
+
package_json = self.project_path / "package.json"
|
|
144
|
+
if package_json.exists():
|
|
145
|
+
try:
|
|
146
|
+
pkg = json.loads(package_json.read_text())
|
|
147
|
+
scripts = pkg.get("scripts", {})
|
|
148
|
+
|
|
149
|
+
# Try npm test first
|
|
150
|
+
if "test" in scripts:
|
|
151
|
+
return self._run_npm_test(start)
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# No tests configured
|
|
156
|
+
return {
|
|
157
|
+
"status": "skipped",
|
|
158
|
+
"passed": 0,
|
|
159
|
+
"failed": 0,
|
|
160
|
+
"warnings": 0,
|
|
161
|
+
"errors": 0,
|
|
162
|
+
"duration_ms": int((time.time() - start) * 1000),
|
|
163
|
+
"message": "No test runner configured",
|
|
164
|
+
"last_run": time.time(),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def _run_npm_test(self, start: float) -> CheckResult:
|
|
168
|
+
"""Run npm test and parse output."""
|
|
169
|
+
try:
|
|
170
|
+
result = subprocess.run(
|
|
171
|
+
["npm", "test", "--", "--passWithNoTests"],
|
|
172
|
+
cwd=self.project_path,
|
|
173
|
+
capture_output=True,
|
|
174
|
+
text=True,
|
|
175
|
+
timeout=120, # 2 minute timeout
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
duration = int((time.time() - start) * 1000)
|
|
179
|
+
output = result.stdout + result.stderr
|
|
180
|
+
|
|
181
|
+
# Parse test results from output
|
|
182
|
+
passed = 0
|
|
183
|
+
failed = 0
|
|
184
|
+
|
|
185
|
+
# Look for common test output patterns
|
|
186
|
+
import re
|
|
187
|
+
|
|
188
|
+
# Jest/Vitest pattern: "Tests: X passed, Y failed"
|
|
189
|
+
match = re.search(r"Tests?:\s*(\d+)\s*passed", output)
|
|
190
|
+
if match:
|
|
191
|
+
passed = int(match.group(1))
|
|
192
|
+
|
|
193
|
+
match = re.search(r"Tests?:\s*\d+\s*passed,\s*(\d+)\s*failed", output)
|
|
194
|
+
if match:
|
|
195
|
+
failed = int(match.group(1))
|
|
196
|
+
elif re.search(r"(\d+)\s*failed", output):
|
|
197
|
+
match = re.search(r"(\d+)\s*failed", output)
|
|
198
|
+
if match:
|
|
199
|
+
failed = int(match.group(1))
|
|
200
|
+
|
|
201
|
+
# Alternative patterns
|
|
202
|
+
if passed == 0:
|
|
203
|
+
match = re.search(r"(\d+)\s*passing", output)
|
|
204
|
+
if match:
|
|
205
|
+
passed = int(match.group(1))
|
|
206
|
+
|
|
207
|
+
if failed == 0:
|
|
208
|
+
match = re.search(r"(\d+)\s*failing", output)
|
|
209
|
+
if match:
|
|
210
|
+
failed = int(match.group(1))
|
|
211
|
+
|
|
212
|
+
status = "passed" if result.returncode == 0 else "failed"
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
"status": status,
|
|
216
|
+
"passed": passed,
|
|
217
|
+
"failed": failed,
|
|
218
|
+
"warnings": 0,
|
|
219
|
+
"errors": failed,
|
|
220
|
+
"duration_ms": duration,
|
|
221
|
+
"message": f"{passed} passed" + (f", {failed} failed" if failed else ""),
|
|
222
|
+
"last_run": time.time(),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
except subprocess.TimeoutExpired:
|
|
226
|
+
return {
|
|
227
|
+
"status": "failed",
|
|
228
|
+
"passed": 0,
|
|
229
|
+
"failed": 0,
|
|
230
|
+
"warnings": 0,
|
|
231
|
+
"errors": 1,
|
|
232
|
+
"duration_ms": 120000,
|
|
233
|
+
"message": "Tests timed out",
|
|
234
|
+
"last_run": time.time(),
|
|
235
|
+
}
|
|
236
|
+
except Exception as e:
|
|
237
|
+
return {
|
|
238
|
+
"status": "failed",
|
|
239
|
+
"passed": 0,
|
|
240
|
+
"failed": 0,
|
|
241
|
+
"warnings": 0,
|
|
242
|
+
"errors": 1,
|
|
243
|
+
"duration_ms": int((time.time() - start) * 1000),
|
|
244
|
+
"message": str(e)[:50],
|
|
245
|
+
"last_run": time.time(),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
def _run_lint(self) -> CheckResult:
|
|
249
|
+
"""Run linter and parse results."""
|
|
250
|
+
start = time.time()
|
|
251
|
+
|
|
252
|
+
# Check for eslint config
|
|
253
|
+
has_eslint = any([
|
|
254
|
+
(self.project_path / f).exists()
|
|
255
|
+
for f in [".eslintrc", ".eslintrc.js", ".eslintrc.json", ".eslintrc.cjs", "eslint.config.js", "eslint.config.mjs"]
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
if not has_eslint:
|
|
259
|
+
# Check package.json for lint script
|
|
260
|
+
package_json = self.project_path / "package.json"
|
|
261
|
+
if package_json.exists():
|
|
262
|
+
try:
|
|
263
|
+
pkg = json.loads(package_json.read_text())
|
|
264
|
+
if "lint" not in pkg.get("scripts", {}):
|
|
265
|
+
return {
|
|
266
|
+
"status": "skipped",
|
|
267
|
+
"passed": 0,
|
|
268
|
+
"failed": 0,
|
|
269
|
+
"warnings": 0,
|
|
270
|
+
"errors": 0,
|
|
271
|
+
"duration_ms": int((time.time() - start) * 1000),
|
|
272
|
+
"message": "No linter configured",
|
|
273
|
+
"last_run": time.time(),
|
|
274
|
+
}
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
result = subprocess.run(
|
|
280
|
+
["npm", "run", "lint", "--", "--format", "json"],
|
|
281
|
+
cwd=self.project_path,
|
|
282
|
+
capture_output=True,
|
|
283
|
+
text=True,
|
|
284
|
+
timeout=60,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
duration = int((time.time() - start) * 1000)
|
|
288
|
+
|
|
289
|
+
errors = 0
|
|
290
|
+
warnings = 0
|
|
291
|
+
|
|
292
|
+
# Try to parse JSON output
|
|
293
|
+
try:
|
|
294
|
+
# ESLint JSON output
|
|
295
|
+
lint_data = json.loads(result.stdout)
|
|
296
|
+
if isinstance(lint_data, list):
|
|
297
|
+
for file_result in lint_data:
|
|
298
|
+
errors += file_result.get("errorCount", 0)
|
|
299
|
+
warnings += file_result.get("warningCount", 0)
|
|
300
|
+
except json.JSONDecodeError:
|
|
301
|
+
# Fall back to parsing text output
|
|
302
|
+
import re
|
|
303
|
+
|
|
304
|
+
# ESLint text pattern: "X errors and Y warnings"
|
|
305
|
+
match = re.search(r"(\d+)\s*error", result.stdout + result.stderr)
|
|
306
|
+
if match:
|
|
307
|
+
errors = int(match.group(1))
|
|
308
|
+
|
|
309
|
+
match = re.search(r"(\d+)\s*warning", result.stdout + result.stderr)
|
|
310
|
+
if match:
|
|
311
|
+
warnings = int(match.group(1))
|
|
312
|
+
|
|
313
|
+
status = "passed" if errors == 0 else "failed"
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"status": status,
|
|
317
|
+
"passed": 0,
|
|
318
|
+
"failed": 0,
|
|
319
|
+
"warnings": warnings,
|
|
320
|
+
"errors": errors,
|
|
321
|
+
"duration_ms": duration,
|
|
322
|
+
"message": f"{errors} errors, {warnings} warnings",
|
|
323
|
+
"last_run": time.time(),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
except subprocess.TimeoutExpired:
|
|
327
|
+
return {
|
|
328
|
+
"status": "failed",
|
|
329
|
+
"passed": 0,
|
|
330
|
+
"failed": 0,
|
|
331
|
+
"warnings": 0,
|
|
332
|
+
"errors": 1,
|
|
333
|
+
"duration_ms": 60000,
|
|
334
|
+
"message": "Lint timed out",
|
|
335
|
+
"last_run": time.time(),
|
|
336
|
+
}
|
|
337
|
+
except Exception:
|
|
338
|
+
return {
|
|
339
|
+
"status": "skipped",
|
|
340
|
+
"passed": 0,
|
|
341
|
+
"failed": 0,
|
|
342
|
+
"warnings": 0,
|
|
343
|
+
"errors": 0,
|
|
344
|
+
"duration_ms": int((time.time() - start) * 1000),
|
|
345
|
+
"message": "Lint not available",
|
|
346
|
+
"last_run": time.time(),
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
def _run_typecheck(self) -> CheckResult:
|
|
350
|
+
"""Run TypeScript type check."""
|
|
351
|
+
start = time.time()
|
|
352
|
+
|
|
353
|
+
# Check for tsconfig
|
|
354
|
+
tsconfig = self.project_path / "tsconfig.json"
|
|
355
|
+
if not tsconfig.exists():
|
|
356
|
+
return {
|
|
357
|
+
"status": "skipped",
|
|
358
|
+
"passed": 0,
|
|
359
|
+
"failed": 0,
|
|
360
|
+
"warnings": 0,
|
|
361
|
+
"errors": 0,
|
|
362
|
+
"duration_ms": int((time.time() - start) * 1000),
|
|
363
|
+
"message": "TypeScript not configured",
|
|
364
|
+
"last_run": time.time(),
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
# Try npm run typecheck first, then fall back to npx tsc
|
|
369
|
+
result = subprocess.run(
|
|
370
|
+
["npm", "run", "typecheck", "--if-present"],
|
|
371
|
+
cwd=self.project_path,
|
|
372
|
+
capture_output=True,
|
|
373
|
+
text=True,
|
|
374
|
+
timeout=60,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Only fall back to npx tsc if typecheck script is missing
|
|
378
|
+
# (don't fallback on non-zero exit - that means typecheck ran but found errors)
|
|
379
|
+
if "missing script" in result.stderr.lower():
|
|
380
|
+
result = subprocess.run(
|
|
381
|
+
["npx", "tsc", "--noEmit"],
|
|
382
|
+
cwd=self.project_path,
|
|
383
|
+
capture_output=True,
|
|
384
|
+
text=True,
|
|
385
|
+
timeout=60,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
duration = int((time.time() - start) * 1000)
|
|
389
|
+
output = result.stdout + result.stderr
|
|
390
|
+
|
|
391
|
+
# Count errors in output
|
|
392
|
+
import re
|
|
393
|
+
errors = len(re.findall(r"error TS\d+:", output))
|
|
394
|
+
|
|
395
|
+
# If no pattern matches but exit code is non-zero, count as 1 error
|
|
396
|
+
if errors == 0 and result.returncode != 0:
|
|
397
|
+
errors = 1
|
|
398
|
+
|
|
399
|
+
status = "passed" if result.returncode == 0 else "failed"
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
"status": status,
|
|
403
|
+
"passed": 0,
|
|
404
|
+
"failed": 0,
|
|
405
|
+
"warnings": 0,
|
|
406
|
+
"errors": errors,
|
|
407
|
+
"duration_ms": duration,
|
|
408
|
+
"message": f"{errors} errors" if errors else "No errors",
|
|
409
|
+
"last_run": time.time(),
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
except subprocess.TimeoutExpired:
|
|
413
|
+
return {
|
|
414
|
+
"status": "failed",
|
|
415
|
+
"passed": 0,
|
|
416
|
+
"failed": 0,
|
|
417
|
+
"warnings": 0,
|
|
418
|
+
"errors": 1,
|
|
419
|
+
"duration_ms": 60000,
|
|
420
|
+
"message": "Type check timed out",
|
|
421
|
+
"last_run": time.time(),
|
|
422
|
+
}
|
|
423
|
+
except Exception:
|
|
424
|
+
return {
|
|
425
|
+
"status": "skipped",
|
|
426
|
+
"passed": 0,
|
|
427
|
+
"failed": 0,
|
|
428
|
+
"warnings": 0,
|
|
429
|
+
"errors": 0,
|
|
430
|
+
"duration_ms": int((time.time() - start) * 1000),
|
|
431
|
+
"message": "Type check not available",
|
|
432
|
+
"last_run": time.time(),
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
def clear_cache(self) -> None:
|
|
436
|
+
"""Clear cached results."""
|
|
437
|
+
self._cache.clear()
|
|
438
|
+
self._last_check.clear()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# Singleton services keyed by project path
|
|
442
|
+
_services: Dict[str, QualityService] = {}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def get_quality_service(project_path: Optional[str] = None) -> QualityService:
|
|
446
|
+
"""Get or create a quality service for a project path."""
|
|
447
|
+
path = str(Path(project_path) if project_path else Path.cwd())
|
|
448
|
+
if path not in _services:
|
|
449
|
+
_services[path] = QualityService(project_path)
|
|
450
|
+
return _services[path]
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
if __name__ == "__main__":
|
|
454
|
+
# Simple test
|
|
455
|
+
import sys
|
|
456
|
+
|
|
457
|
+
project = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
458
|
+
service = QualityService(project)
|
|
459
|
+
results = service.run_checks()
|
|
460
|
+
|
|
461
|
+
print(f"Project: {results['project_path']}")
|
|
462
|
+
print(f"Branch: {results['branch']}")
|
|
463
|
+
print(f"\nTests: {results['tests']['status']}")
|
|
464
|
+
print(f" {results['tests']['message']}")
|
|
465
|
+
print(f"\nLint: {results['lint']['status']}")
|
|
466
|
+
print(f" {results['lint']['message']}")
|
|
467
|
+
print(f"\nTypes: {results['types']['status']}")
|
|
468
|
+
print(f" {results['types']['message']}")
|
|
469
|
+
print(f"\nReady to merge: {results['ready_to_merge']}")
|
|
470
|
+
print(f"Blocking issues: {results['blocking_issues']}")
|