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.
Files changed (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. 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']}")