invar-tools 1.8.0__py3-none-any.whl → 1.11.0__py3-none-any.whl
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.
- invar/__init__.py +8 -0
- invar/core/doc_edit.py +187 -0
- invar/core/doc_parser.py +563 -0
- invar/core/language.py +88 -0
- invar/core/models.py +106 -0
- invar/core/patterns/detector.py +6 -1
- invar/core/patterns/p0_exhaustive.py +15 -3
- invar/core/patterns/p0_literal.py +15 -3
- invar/core/patterns/p0_newtype.py +15 -3
- invar/core/patterns/p0_nonempty.py +15 -3
- invar/core/patterns/p0_validation.py +15 -3
- invar/core/patterns/registry.py +5 -1
- invar/core/patterns/types.py +5 -1
- invar/core/property_gen.py +4 -0
- invar/core/rules.py +84 -18
- invar/core/sync_helpers.py +27 -1
- invar/core/ts_parsers.py +286 -0
- invar/core/ts_sig_parser.py +310 -0
- invar/mcp/handlers.py +408 -0
- invar/mcp/server.py +288 -143
- invar/node_tools/MANIFEST +7 -0
- invar/node_tools/__init__.py +51 -0
- invar/node_tools/fc-runner/cli.js +77 -0
- invar/node_tools/quick-check/cli.js +28 -0
- invar/node_tools/ts-analyzer/cli.js +480 -0
- invar/shell/claude_hooks.py +35 -12
- invar/shell/commands/doc.py +409 -0
- invar/shell/commands/guard.py +41 -1
- invar/shell/commands/init.py +154 -16
- invar/shell/commands/perception.py +157 -33
- invar/shell/commands/skill.py +187 -0
- invar/shell/commands/template_sync.py +65 -13
- invar/shell/commands/uninstall.py +60 -12
- invar/shell/commands/update.py +6 -14
- invar/shell/contract_coverage.py +1 -0
- invar/shell/doc_tools.py +459 -0
- invar/shell/fs.py +67 -13
- invar/shell/pi_hooks.py +6 -0
- invar/shell/prove/crosshair.py +3 -0
- invar/shell/prove/guard_ts.py +902 -0
- invar/shell/skill_manager.py +355 -0
- invar/shell/template_engine.py +28 -4
- invar/shell/templates.py +4 -4
- invar/templates/claude-md/python/critical-rules.md +33 -0
- invar/templates/claude-md/python/quick-reference.md +24 -0
- invar/templates/claude-md/typescript/critical-rules.md +40 -0
- invar/templates/claude-md/typescript/quick-reference.md +24 -0
- invar/templates/claude-md/universal/check-in.md +25 -0
- invar/templates/claude-md/universal/skills.md +73 -0
- invar/templates/claude-md/universal/workflow.md +55 -0
- invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
- invar/templates/config/AGENT.md.jinja +58 -0
- invar/templates/config/CLAUDE.md.jinja +16 -209
- invar/templates/config/context.md.jinja +19 -0
- invar/templates/examples/{README.md → python/README.md} +2 -0
- invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
- invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
- invar/templates/examples/python/core_shell.py +227 -0
- invar/templates/examples/python/functional.py +613 -0
- invar/templates/examples/typescript/README.md +31 -0
- invar/templates/examples/typescript/contracts.ts +163 -0
- invar/templates/examples/typescript/core_shell.ts +374 -0
- invar/templates/examples/typescript/functional.ts +601 -0
- invar/templates/examples/typescript/workflow.md +95 -0
- invar/templates/hooks/PostToolUse.sh.jinja +10 -1
- invar/templates/hooks/PreToolUse.sh.jinja +38 -0
- invar/templates/hooks/Stop.sh.jinja +1 -1
- invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
- invar/templates/hooks/pi/invar.ts.jinja +9 -0
- invar/templates/manifest.toml +7 -6
- invar/templates/onboard/assessment.md.jinja +214 -0
- invar/templates/onboard/patterns/python.md +347 -0
- invar/templates/onboard/patterns/typescript.md +452 -0
- invar/templates/onboard/roadmap.md.jinja +168 -0
- invar/templates/protocol/INVAR.md.jinja +51 -0
- invar/templates/protocol/python/architecture-examples.md +41 -0
- invar/templates/protocol/python/contracts-syntax.md +56 -0
- invar/templates/protocol/python/markers.md +44 -0
- invar/templates/protocol/python/tools.md +24 -0
- invar/templates/protocol/python/troubleshooting.md +38 -0
- invar/templates/protocol/typescript/architecture-examples.md +52 -0
- invar/templates/protocol/typescript/contracts-syntax.md +73 -0
- invar/templates/protocol/typescript/markers.md +48 -0
- invar/templates/protocol/typescript/tools.md +65 -0
- invar/templates/protocol/typescript/troubleshooting.md +104 -0
- invar/templates/protocol/universal/architecture.md +36 -0
- invar/templates/protocol/universal/completion.md +14 -0
- invar/templates/protocol/universal/contracts-concept.md +37 -0
- invar/templates/protocol/universal/header.md +17 -0
- invar/templates/protocol/universal/session.md +17 -0
- invar/templates/protocol/universal/six-laws.md +10 -0
- invar/templates/protocol/universal/usbv.md +14 -0
- invar/templates/protocol/universal/visible-workflow.md +25 -0
- invar/templates/skills/develop/SKILL.md.jinja +85 -3
- invar/templates/skills/extensions/_registry.yaml +93 -0
- invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
- invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
- invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
- invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
- invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
- invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
- invar/templates/skills/extensions/security/SKILL.md +382 -0
- invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
- invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
- invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
- invar/templates/skills/review/SKILL.md.jinja +220 -248
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/METADATA +336 -12
- invar_tools-1.11.0.dist-info/RECORD +178 -0
- invar/templates/examples/core_shell.py +0 -127
- invar/templates/protocol/INVAR.md +0 -310
- invar_tools-1.8.0.dist-info/RECORD +0 -116
- /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
"""TypeScript Guard Orchestration.
|
|
2
|
+
|
|
3
|
+
Shell module: orchestrates TypeScript verification via subprocess calls.
|
|
4
|
+
Part of LX-06 TypeScript tooling support.
|
|
5
|
+
|
|
6
|
+
This module provides graceful degradation - if TypeScript tools are not
|
|
7
|
+
installed, it reports the missing dependency rather than failing hard.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Literal
|
|
19
|
+
|
|
20
|
+
from returns.result import Failure, Result, Success
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TypeScriptViolation:
|
|
25
|
+
"""A single TypeScript verification issue."""
|
|
26
|
+
|
|
27
|
+
file: str
|
|
28
|
+
line: int | None
|
|
29
|
+
column: int | None
|
|
30
|
+
rule: str
|
|
31
|
+
message: str
|
|
32
|
+
severity: Literal["error", "warning", "info"]
|
|
33
|
+
source: Literal["tsc", "eslint", "vitest"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ContractQuality:
|
|
38
|
+
"""Contract quality metrics from ts-analyzer."""
|
|
39
|
+
|
|
40
|
+
strong: int = 0
|
|
41
|
+
medium: int = 0
|
|
42
|
+
weak: int = 0
|
|
43
|
+
useless: int = 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class BlindSpot:
|
|
48
|
+
"""High-risk code without validation."""
|
|
49
|
+
|
|
50
|
+
function: str
|
|
51
|
+
file: str
|
|
52
|
+
line: int
|
|
53
|
+
risk: Literal["critical", "high", "medium", "low"]
|
|
54
|
+
reason: str
|
|
55
|
+
suggested_schema: str | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class EnhancedAnalysis:
|
|
60
|
+
"""Enhanced analysis from @invar/* Node components."""
|
|
61
|
+
|
|
62
|
+
quick_check_available: bool = False
|
|
63
|
+
ts_analyzer_available: bool = False
|
|
64
|
+
fc_runner_available: bool = False
|
|
65
|
+
contract_coverage: float | None = None
|
|
66
|
+
contract_quality: ContractQuality | None = None
|
|
67
|
+
blind_spots: list[BlindSpot] = field(default_factory=list)
|
|
68
|
+
property_tests_passed: bool | None = None
|
|
69
|
+
property_test_failures: list[dict] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class TypeScriptGuardResult:
|
|
74
|
+
"""Result of TypeScript verification."""
|
|
75
|
+
|
|
76
|
+
status: Literal["passed", "failed", "skipped"]
|
|
77
|
+
violations: list[TypeScriptViolation] = field(default_factory=list)
|
|
78
|
+
tsc_available: bool = False
|
|
79
|
+
eslint_available: bool = False
|
|
80
|
+
vitest_available: bool = False
|
|
81
|
+
error_count: int = 0
|
|
82
|
+
warning_count: int = 0
|
|
83
|
+
tool_errors: list[str] = field(default_factory=list)
|
|
84
|
+
enhanced: EnhancedAnalysis | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# @shell_orchestration: Transforms TypeScriptGuardResult to JSON for agent consumption
|
|
88
|
+
def format_typescript_guard_v2(result: TypeScriptGuardResult) -> dict:
|
|
89
|
+
"""Format TypeScript guard result as v2.0 JSON.
|
|
90
|
+
|
|
91
|
+
LX-06 Phase 3: Agent-optimized JSON output format with:
|
|
92
|
+
- Contract coverage and quality metrics
|
|
93
|
+
- Blind spot detection
|
|
94
|
+
- Property test results with counterexamples
|
|
95
|
+
- Structured fix suggestions
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
result: TypeScript guard result to format.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dict in v2.0 JSON format for agent consumption.
|
|
102
|
+
"""
|
|
103
|
+
# Count violations by source
|
|
104
|
+
tsc_errors = sum(1 for v in result.violations if v.source == "tsc" and v.severity == "error")
|
|
105
|
+
tsc_warnings = sum(1 for v in result.violations if v.source == "tsc" and v.severity == "warning")
|
|
106
|
+
eslint_errors = sum(1 for v in result.violations if v.source == "eslint" and v.severity == "error")
|
|
107
|
+
eslint_warnings = sum(1 for v in result.violations if v.source == "eslint" and v.severity == "warning")
|
|
108
|
+
vitest_failures = sum(1 for v in result.violations if v.source == "vitest")
|
|
109
|
+
|
|
110
|
+
# Count files checked (unique files in violations + estimate from available tools)
|
|
111
|
+
files_checked = len({v.file for v in result.violations}) if result.violations else 0
|
|
112
|
+
|
|
113
|
+
output: dict = {
|
|
114
|
+
"version": "2.0",
|
|
115
|
+
"language": "typescript",
|
|
116
|
+
"status": result.status,
|
|
117
|
+
"summary": {
|
|
118
|
+
"errors": result.error_count,
|
|
119
|
+
"warnings": result.warning_count,
|
|
120
|
+
"files_checked": files_checked,
|
|
121
|
+
},
|
|
122
|
+
"static": {
|
|
123
|
+
"tsc": {
|
|
124
|
+
"passed": tsc_errors == 0,
|
|
125
|
+
"available": result.tsc_available,
|
|
126
|
+
"errors": tsc_errors,
|
|
127
|
+
"warnings": tsc_warnings,
|
|
128
|
+
},
|
|
129
|
+
"eslint": {
|
|
130
|
+
"passed": eslint_errors == 0,
|
|
131
|
+
"available": result.eslint_available,
|
|
132
|
+
"errors": eslint_errors,
|
|
133
|
+
"warnings": eslint_warnings,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
"tests": {
|
|
137
|
+
"passed": vitest_failures == 0,
|
|
138
|
+
"available": result.vitest_available,
|
|
139
|
+
"failures": vitest_failures,
|
|
140
|
+
},
|
|
141
|
+
"violations": [
|
|
142
|
+
{
|
|
143
|
+
"file": v.file,
|
|
144
|
+
"line": v.line,
|
|
145
|
+
"column": v.column,
|
|
146
|
+
"rule": v.rule,
|
|
147
|
+
"message": v.message,
|
|
148
|
+
"severity": v.severity,
|
|
149
|
+
"source": v.source,
|
|
150
|
+
}
|
|
151
|
+
for v in result.violations
|
|
152
|
+
],
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Add enhanced analysis if available (LX-06 Phase 2)
|
|
156
|
+
if result.enhanced:
|
|
157
|
+
enhanced = result.enhanced
|
|
158
|
+
|
|
159
|
+
# Property tests section
|
|
160
|
+
if enhanced.fc_runner_available:
|
|
161
|
+
# Handle None (not run) vs False (failed) vs True (passed)
|
|
162
|
+
if enhanced.property_tests_passed is None:
|
|
163
|
+
pt_status = "skipped"
|
|
164
|
+
elif enhanced.property_tests_passed:
|
|
165
|
+
pt_status = "passed"
|
|
166
|
+
else:
|
|
167
|
+
pt_status = "failed"
|
|
168
|
+
|
|
169
|
+
property_tests: dict = {
|
|
170
|
+
"status": pt_status,
|
|
171
|
+
"confidence": "statistical",
|
|
172
|
+
"available": True,
|
|
173
|
+
}
|
|
174
|
+
if enhanced.property_test_failures:
|
|
175
|
+
property_tests["failures"] = [
|
|
176
|
+
{
|
|
177
|
+
"name": f.get("name", "unknown"),
|
|
178
|
+
"counterexample": f.get("counterexample"),
|
|
179
|
+
"analysis": f.get("analysis"),
|
|
180
|
+
}
|
|
181
|
+
for f in enhanced.property_test_failures
|
|
182
|
+
]
|
|
183
|
+
output["property_tests"] = property_tests
|
|
184
|
+
|
|
185
|
+
# Contracts section
|
|
186
|
+
if enhanced.ts_analyzer_available:
|
|
187
|
+
contracts: dict = {"available": True}
|
|
188
|
+
if enhanced.contract_coverage is not None:
|
|
189
|
+
contracts["coverage"] = enhanced.contract_coverage
|
|
190
|
+
if enhanced.contract_quality:
|
|
191
|
+
contracts["quality"] = {
|
|
192
|
+
"strong": enhanced.contract_quality.strong,
|
|
193
|
+
"medium": enhanced.contract_quality.medium,
|
|
194
|
+
"weak": enhanced.contract_quality.weak,
|
|
195
|
+
"useless": enhanced.contract_quality.useless,
|
|
196
|
+
}
|
|
197
|
+
if enhanced.blind_spots:
|
|
198
|
+
contracts["blind_spots"] = [
|
|
199
|
+
{
|
|
200
|
+
"function": bs.function,
|
|
201
|
+
"file": bs.file,
|
|
202
|
+
"line": bs.line,
|
|
203
|
+
"risk": bs.risk,
|
|
204
|
+
"reason": bs.reason,
|
|
205
|
+
"suggested_schema": bs.suggested_schema,
|
|
206
|
+
}
|
|
207
|
+
for bs in enhanced.blind_spots
|
|
208
|
+
]
|
|
209
|
+
output["contracts"] = contracts
|
|
210
|
+
|
|
211
|
+
# Add tool errors if any
|
|
212
|
+
if result.tool_errors:
|
|
213
|
+
output["tool_errors"] = result.tool_errors
|
|
214
|
+
|
|
215
|
+
# LX-06 Phase 3: Generate fix suggestions from violations
|
|
216
|
+
fixes = _generate_fix_suggestions(result.violations)
|
|
217
|
+
if fixes:
|
|
218
|
+
output["fixes"] = fixes
|
|
219
|
+
|
|
220
|
+
return output
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# @shell_orchestration: Helper for format_typescript_guard_v2 output assembly
|
|
224
|
+
def _generate_fix_suggestions(violations: list[TypeScriptViolation]) -> list[dict]:
|
|
225
|
+
"""Generate actionable fix suggestions from violations.
|
|
226
|
+
|
|
227
|
+
LX-06 Phase 3: Maps ESLint rule violations to repair code snippets.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
violations: List of TypeScriptViolation from ESLint/tsc.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of fix suggestions with repair code.
|
|
234
|
+
"""
|
|
235
|
+
fixes: list[dict] = []
|
|
236
|
+
fix_counter = 1
|
|
237
|
+
|
|
238
|
+
# Rule-specific fix generators mapping rule_id to (priority, action, code_template)
|
|
239
|
+
fix_generators: dict[str, tuple[str, str, str]] = {
|
|
240
|
+
"@invar/require-schema-validation": (
|
|
241
|
+
"high",
|
|
242
|
+
"insert",
|
|
243
|
+
"const validated = Schema.parse({param});",
|
|
244
|
+
),
|
|
245
|
+
"@invar/shell-result-type": (
|
|
246
|
+
"medium",
|
|
247
|
+
"replace",
|
|
248
|
+
"Result<{return_type}, Error>",
|
|
249
|
+
),
|
|
250
|
+
"@invar/no-io-in-core": (
|
|
251
|
+
"high",
|
|
252
|
+
"refactor",
|
|
253
|
+
"// Move to shell/ directory and import from there",
|
|
254
|
+
),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for v in violations:
|
|
258
|
+
if v.source != "eslint":
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
rule = v.rule or ""
|
|
262
|
+
if rule not in fix_generators:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
priority, action, code = fix_generators[rule]
|
|
266
|
+
|
|
267
|
+
# Customize code based on rule
|
|
268
|
+
if rule == "@invar/require-schema-validation":
|
|
269
|
+
# Extract param name from message (fragile: depends on ESLint message format)
|
|
270
|
+
# Falls back to "input" if extraction fails - user can adjust in fix suggestion
|
|
271
|
+
param_match = re.search(r'"(\w+)"', v.message)
|
|
272
|
+
param = param_match.group(1) if param_match else "input"
|
|
273
|
+
code = code.replace("{param}", param)
|
|
274
|
+
elif rule == "@invar/shell-result-type":
|
|
275
|
+
# Use 'T' as placeholder since actual type requires source analysis
|
|
276
|
+
code = code.replace("{return_type}", "T")
|
|
277
|
+
|
|
278
|
+
fix = {
|
|
279
|
+
"id": f"FIX-{fix_counter:03d}",
|
|
280
|
+
"priority": priority,
|
|
281
|
+
"issue": {
|
|
282
|
+
"type": rule.replace("@invar/", ""),
|
|
283
|
+
"message": v.message,
|
|
284
|
+
"location": {"file": v.file, "line": v.line, "column": v.column},
|
|
285
|
+
},
|
|
286
|
+
"repair": {
|
|
287
|
+
"action": action,
|
|
288
|
+
"target": {
|
|
289
|
+
"file": v.file,
|
|
290
|
+
"line": (v.line + 1) if v.line is not None else None,
|
|
291
|
+
},
|
|
292
|
+
"code": code,
|
|
293
|
+
"explanation": f"Fix for {rule}",
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
fixes.append(fix)
|
|
297
|
+
fix_counter += 1
|
|
298
|
+
|
|
299
|
+
return fixes
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _check_tool_available(tool: str, check_args: list[str]) -> bool:
|
|
303
|
+
"""Check if a tool is available in PATH.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
tool: Tool name (e.g., "npx", "tsc") - must be alphanumeric/dash/underscore
|
|
307
|
+
check_args: Arguments for version check
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
True if tool is available and responds to check.
|
|
311
|
+
"""
|
|
312
|
+
# Security: validate tool name to prevent command injection
|
|
313
|
+
if not tool or not all(c.isalnum() or c in "-_" for c in tool):
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
result = subprocess.run(
|
|
318
|
+
[tool, *check_args],
|
|
319
|
+
capture_output=True,
|
|
320
|
+
timeout=10,
|
|
321
|
+
)
|
|
322
|
+
return result.returncode == 0
|
|
323
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# =============================================================================
|
|
328
|
+
# @invar/* Node Component Integration (LX-06 Phase 2)
|
|
329
|
+
# =============================================================================
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# @shell_complexity: Path discovery with fallback logic
|
|
333
|
+
def _get_invar_package_cmd(package_name: str, project_path: Path) -> list[str]:
|
|
334
|
+
"""Get command to run an @invar/* package.
|
|
335
|
+
|
|
336
|
+
Priority order:
|
|
337
|
+
1. Embedded tools (pip install invar-tools includes these)
|
|
338
|
+
2. Local development (typescript/packages/*/dist/ in Invar repo)
|
|
339
|
+
3. npx fallback (if published to npm)
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
package_name: Package name without @invar/ prefix (e.g., "ts-analyzer")
|
|
343
|
+
project_path: Project path to check for local installation
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Command list for subprocess.run
|
|
347
|
+
"""
|
|
348
|
+
# Priority 1: Embedded tools (from pip install)
|
|
349
|
+
try:
|
|
350
|
+
from invar.node_tools import get_tool_path
|
|
351
|
+
|
|
352
|
+
if embedded := get_tool_path(package_name):
|
|
353
|
+
return ["node", str(embedded)]
|
|
354
|
+
except ImportError:
|
|
355
|
+
pass # node_tools module not available
|
|
356
|
+
|
|
357
|
+
# Priority 2: Local development setup (Invar repo itself)
|
|
358
|
+
local_cli = project_path / f"typescript/packages/{package_name}/dist/cli.js"
|
|
359
|
+
if local_cli.exists():
|
|
360
|
+
return ["node", str(local_cli)]
|
|
361
|
+
|
|
362
|
+
# Priority 2b: Walk up to find the Invar root (monorepo setup)
|
|
363
|
+
check_path = project_path
|
|
364
|
+
for _ in range(5): # Max 5 levels up
|
|
365
|
+
candidate = check_path / f"typescript/packages/{package_name}/dist/cli.js"
|
|
366
|
+
if candidate.exists():
|
|
367
|
+
return ["node", str(candidate)]
|
|
368
|
+
parent = check_path.parent
|
|
369
|
+
if parent == check_path:
|
|
370
|
+
break
|
|
371
|
+
check_path = parent
|
|
372
|
+
|
|
373
|
+
# Priority 3: npx fallback (requires package published to npm)
|
|
374
|
+
return ["npx", f"@invar/{package_name}"]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# @shell_complexity: Error handling branches for subprocess/JSON parsing
|
|
378
|
+
def run_ts_analyzer(project_path: Path) -> Result[dict, str]:
|
|
379
|
+
"""Run @invar/ts-analyzer for contract coverage analysis.
|
|
380
|
+
|
|
381
|
+
Calls the Node component via npx with JSON output mode.
|
|
382
|
+
Gracefully degrades if the package is not installed.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
project_path: Path to TypeScript project root.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Result containing analysis dict or error message.
|
|
389
|
+
"""
|
|
390
|
+
# Validate project path exists before subprocess call
|
|
391
|
+
if not project_path.exists():
|
|
392
|
+
return Failure(f"Project path does not exist: {project_path}")
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
cmd = _get_invar_package_cmd("ts-analyzer", project_path)
|
|
396
|
+
result = subprocess.run(
|
|
397
|
+
[*cmd, str(project_path), "--json"],
|
|
398
|
+
capture_output=True,
|
|
399
|
+
text=True,
|
|
400
|
+
timeout=60,
|
|
401
|
+
cwd=project_path,
|
|
402
|
+
)
|
|
403
|
+
# ts-analyzer exits non-zero when critical blind spots found, but still outputs valid JSON
|
|
404
|
+
# Try to parse JSON output regardless of exit code
|
|
405
|
+
if result.stdout.strip():
|
|
406
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
407
|
+
return Success(json.loads(result.stdout))
|
|
408
|
+
|
|
409
|
+
# Only report failure if no valid JSON output
|
|
410
|
+
if "not found" in result.stderr.lower() or "ENOENT" in result.stderr:
|
|
411
|
+
return Failure("@invar/ts-analyzer not installed")
|
|
412
|
+
return Failure(result.stderr or "ts-analyzer failed")
|
|
413
|
+
except FileNotFoundError:
|
|
414
|
+
return Failure("node/npx not available - is Node.js installed?")
|
|
415
|
+
except subprocess.TimeoutExpired:
|
|
416
|
+
return Failure("ts-analyzer timed out")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# @shell_complexity: Error handling branches for subprocess/JSON parsing
|
|
420
|
+
def run_fc_runner(
|
|
421
|
+
project_path: Path, *, seed: int = 42, num_runs: int = 100
|
|
422
|
+
) -> Result[dict, str]:
|
|
423
|
+
"""Run @invar/fc-runner for property-based testing.
|
|
424
|
+
|
|
425
|
+
Calls the Node component via npx with JSON output mode.
|
|
426
|
+
Gracefully degrades if the package is not installed.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
project_path: Path to TypeScript project root.
|
|
430
|
+
seed: Random seed for reproducibility.
|
|
431
|
+
num_runs: Number of test iterations.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Result containing test results dict or error message.
|
|
435
|
+
"""
|
|
436
|
+
# Validate project path exists before subprocess call
|
|
437
|
+
if not project_path.exists():
|
|
438
|
+
return Failure(f"Project path does not exist: {project_path}")
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
cmd = _get_invar_package_cmd("fc-runner", project_path)
|
|
442
|
+
result = subprocess.run(
|
|
443
|
+
[*cmd, "--json", "--seed", str(seed), "--num-runs", str(num_runs)],
|
|
444
|
+
capture_output=True,
|
|
445
|
+
text=True,
|
|
446
|
+
timeout=120, # Property tests can take longer
|
|
447
|
+
cwd=project_path,
|
|
448
|
+
)
|
|
449
|
+
if result.returncode == 0:
|
|
450
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
451
|
+
return Success(json.loads(result.stdout))
|
|
452
|
+
return Failure("Invalid JSON output from fc-runner")
|
|
453
|
+
if "not found" in result.stderr.lower() or "ENOENT" in result.stderr:
|
|
454
|
+
return Failure("@invar/fc-runner not installed")
|
|
455
|
+
# Return code 1 might mean test failures - try to parse output
|
|
456
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
457
|
+
data = json.loads(result.stdout)
|
|
458
|
+
if "properties" in data:
|
|
459
|
+
return Success(data) # Has results even if tests failed
|
|
460
|
+
return Failure(result.stderr or "fc-runner failed")
|
|
461
|
+
except FileNotFoundError:
|
|
462
|
+
return Failure("node/npx not available - is Node.js installed?")
|
|
463
|
+
except subprocess.TimeoutExpired:
|
|
464
|
+
return Failure("fc-runner timed out")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# @shell_complexity: Error handling branches for subprocess/JSON parsing
|
|
468
|
+
def run_quick_check(project_path: Path) -> Result[dict, str]:
|
|
469
|
+
"""Run @invar/quick-check for fast smoke testing.
|
|
470
|
+
|
|
471
|
+
Calls the Node component via npx with JSON output mode.
|
|
472
|
+
Gracefully degrades if the package is not installed.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
project_path: Path to TypeScript project root.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Result containing check results dict or error message.
|
|
479
|
+
"""
|
|
480
|
+
# Validate project path exists before subprocess call
|
|
481
|
+
if not project_path.exists():
|
|
482
|
+
return Failure(f"Project path does not exist: {project_path}")
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
cmd = _get_invar_package_cmd("quick-check", project_path)
|
|
486
|
+
result = subprocess.run(
|
|
487
|
+
[*cmd, str(project_path), "--json"],
|
|
488
|
+
capture_output=True,
|
|
489
|
+
text=True,
|
|
490
|
+
timeout=30, # Quick check should be fast
|
|
491
|
+
cwd=project_path,
|
|
492
|
+
)
|
|
493
|
+
# quick-check exits non-zero when checks fail, but still outputs valid JSON
|
|
494
|
+
if result.stdout.strip():
|
|
495
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
496
|
+
return Success(json.loads(result.stdout))
|
|
497
|
+
|
|
498
|
+
if "not found" in result.stderr.lower() or "ENOENT" in result.stderr:
|
|
499
|
+
return Failure("@invar/quick-check not installed")
|
|
500
|
+
return Failure(result.stderr or "quick-check failed")
|
|
501
|
+
except FileNotFoundError:
|
|
502
|
+
return Failure("node/npx not available - is Node.js installed?")
|
|
503
|
+
except subprocess.TimeoutExpired:
|
|
504
|
+
return Failure("quick-check timed out")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# @shell_orchestration: Helper for run_ts_analyzer - processes subprocess output
|
|
508
|
+
def _parse_ts_analyzer_result(
|
|
509
|
+
data: dict,
|
|
510
|
+
) -> tuple[float | None, ContractQuality | None, list[BlindSpot]]:
|
|
511
|
+
"""Parse ts-analyzer JSON output into typed structures.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
data: Raw JSON dict from ts-analyzer.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Tuple of (coverage, quality, blind_spots).
|
|
518
|
+
"""
|
|
519
|
+
coverage = data.get("coverage")
|
|
520
|
+
|
|
521
|
+
quality = None
|
|
522
|
+
if q := data.get("quality"):
|
|
523
|
+
quality = ContractQuality(
|
|
524
|
+
strong=q.get("strong", 0),
|
|
525
|
+
medium=q.get("medium", 0),
|
|
526
|
+
weak=q.get("weak", 0),
|
|
527
|
+
useless=q.get("useless", 0),
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
blind_spots = []
|
|
531
|
+
for bs in data.get("blindSpots", []):
|
|
532
|
+
blind_spots.append(
|
|
533
|
+
BlindSpot(
|
|
534
|
+
function=bs.get("function", "unknown"),
|
|
535
|
+
file=bs.get("file", "unknown"),
|
|
536
|
+
line=bs.get("line", 0),
|
|
537
|
+
risk=bs.get("risk", "medium"),
|
|
538
|
+
reason=bs.get("reason", ""),
|
|
539
|
+
suggested_schema=bs.get("suggestedSchema"),
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
return coverage, quality, blind_spots
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# @shell_orchestration: Helper for run_fc_runner - processes subprocess output
|
|
547
|
+
def _parse_fc_runner_result(data: dict) -> tuple[bool | None, list[dict]]:
|
|
548
|
+
"""Parse fc-runner JSON output into typed structures.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
data: Raw JSON dict from fc-runner.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
Tuple of (all_passed, failures).
|
|
555
|
+
"""
|
|
556
|
+
if "properties" not in data:
|
|
557
|
+
return None, []
|
|
558
|
+
|
|
559
|
+
failures = []
|
|
560
|
+
all_passed = True
|
|
561
|
+
|
|
562
|
+
for prop in data.get("properties", []):
|
|
563
|
+
if prop.get("status") == "failed":
|
|
564
|
+
all_passed = False
|
|
565
|
+
failures.append(
|
|
566
|
+
{
|
|
567
|
+
"name": prop.get("name", "unknown"),
|
|
568
|
+
"counterexample": prop.get("counterexample"),
|
|
569
|
+
"seed": prop.get("seed"),
|
|
570
|
+
"shrunk": prop.get("shrunk", False),
|
|
571
|
+
}
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
return all_passed, failures
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# =============================================================================
|
|
578
|
+
# Standard Tools (tsc, eslint, vitest)
|
|
579
|
+
# =============================================================================
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# @shell_complexity: CLI tool integration with error handling and output parsing
|
|
583
|
+
def run_tsc(project_path: Path) -> Result[list[TypeScriptViolation], str]:
|
|
584
|
+
"""Run TypeScript compiler for type checking.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
project_path: Path to TypeScript project root.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Result containing list of violations or error message.
|
|
591
|
+
"""
|
|
592
|
+
# Validate project path exists before subprocess call
|
|
593
|
+
if not project_path.exists():
|
|
594
|
+
return Failure(f"Project path does not exist: {project_path}")
|
|
595
|
+
|
|
596
|
+
tsconfig = project_path / "tsconfig.json"
|
|
597
|
+
if not tsconfig.exists():
|
|
598
|
+
return Failure("No tsconfig.json found")
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
result = subprocess.run(
|
|
602
|
+
["npx", "tsc", "--noEmit", "--pretty", "false"],
|
|
603
|
+
cwd=project_path,
|
|
604
|
+
capture_output=True,
|
|
605
|
+
text=True,
|
|
606
|
+
timeout=120,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
violations: list[TypeScriptViolation] = []
|
|
610
|
+
|
|
611
|
+
# Parse tsc output (format: file(line,col): error TSxxxx: message)
|
|
612
|
+
for line in result.stdout.splitlines():
|
|
613
|
+
if ": error TS" in line or ": warning TS" in line:
|
|
614
|
+
violation = _parse_tsc_line(line)
|
|
615
|
+
if violation:
|
|
616
|
+
violations.append(violation)
|
|
617
|
+
|
|
618
|
+
return Success(violations)
|
|
619
|
+
|
|
620
|
+
except FileNotFoundError:
|
|
621
|
+
return Failure("npx not found - is Node.js installed?")
|
|
622
|
+
except subprocess.TimeoutExpired:
|
|
623
|
+
return Failure("tsc timed out after 120 seconds")
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# @shell_orchestration: Parser helper for run_tsc subprocess output
|
|
627
|
+
def _parse_tsc_line(line: str) -> TypeScriptViolation | None:
|
|
628
|
+
"""Parse a single tsc output line into a violation.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
line: Raw tsc output line.
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Parsed violation or None if parsing fails.
|
|
635
|
+
|
|
636
|
+
Examples:
|
|
637
|
+
>>> v = _parse_tsc_line("src/foo.ts(10,5): error TS2322: Type mismatch")
|
|
638
|
+
>>> v.file if v else None
|
|
639
|
+
'src/foo.ts'
|
|
640
|
+
>>> v.line if v else None
|
|
641
|
+
10
|
|
642
|
+
>>> v.rule if v else None
|
|
643
|
+
'TS2322'
|
|
644
|
+
"""
|
|
645
|
+
# Pattern: file(line,col): severity TSxxxx: message
|
|
646
|
+
pattern = r"^(.+?)\((\d+),(\d+)\): (error|warning) (TS\d+): (.+)$"
|
|
647
|
+
match = re.match(pattern, line)
|
|
648
|
+
|
|
649
|
+
if not match:
|
|
650
|
+
return None
|
|
651
|
+
|
|
652
|
+
file_path, line_num, col, severity, code, message = match.groups()
|
|
653
|
+
|
|
654
|
+
return TypeScriptViolation(
|
|
655
|
+
file=file_path,
|
|
656
|
+
line=int(line_num),
|
|
657
|
+
column=int(col),
|
|
658
|
+
rule=code,
|
|
659
|
+
message=message,
|
|
660
|
+
severity="error" if severity == "error" else "warning",
|
|
661
|
+
source="tsc",
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# @shell_complexity: CLI tool integration with JSON parsing and error handling
|
|
666
|
+
def run_eslint(project_path: Path) -> Result[list[TypeScriptViolation], str]:
|
|
667
|
+
"""Run ESLint for code quality checks.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
project_path: Path to project root.
|
|
671
|
+
|
|
672
|
+
Returns:
|
|
673
|
+
Result containing list of violations or error message.
|
|
674
|
+
"""
|
|
675
|
+
# Validate project path exists before subprocess call
|
|
676
|
+
if not project_path.exists():
|
|
677
|
+
return Failure(f"Project path does not exist: {project_path}")
|
|
678
|
+
|
|
679
|
+
try:
|
|
680
|
+
result = subprocess.run(
|
|
681
|
+
["npx", "eslint", ".", "--format", "json", "--ext", ".ts,.tsx"],
|
|
682
|
+
cwd=project_path,
|
|
683
|
+
capture_output=True,
|
|
684
|
+
text=True,
|
|
685
|
+
timeout=120,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
violations: list[TypeScriptViolation] = []
|
|
689
|
+
|
|
690
|
+
try:
|
|
691
|
+
eslint_output = json.loads(result.stdout)
|
|
692
|
+
for file_result in eslint_output:
|
|
693
|
+
file_path = file_result.get("filePath", "")
|
|
694
|
+
# Make path relative
|
|
695
|
+
with contextlib.suppress(ValueError):
|
|
696
|
+
file_path = str(Path(file_path).relative_to(project_path))
|
|
697
|
+
|
|
698
|
+
for msg in file_result.get("messages", []):
|
|
699
|
+
severity_num = msg.get("severity", 1)
|
|
700
|
+
violations.append(
|
|
701
|
+
TypeScriptViolation(
|
|
702
|
+
file=file_path,
|
|
703
|
+
line=msg.get("line"),
|
|
704
|
+
column=msg.get("column"),
|
|
705
|
+
rule=msg.get("ruleId", "unknown"),
|
|
706
|
+
message=msg.get("message", ""),
|
|
707
|
+
severity="error" if severity_num == 2 else "warning",
|
|
708
|
+
source="eslint",
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
except json.JSONDecodeError:
|
|
712
|
+
# ESLint may output non-JSON on certain errors
|
|
713
|
+
if result.returncode != 0 and result.stderr:
|
|
714
|
+
return Failure(f"ESLint error: {result.stderr[:200]}")
|
|
715
|
+
|
|
716
|
+
return Success(violations)
|
|
717
|
+
|
|
718
|
+
except FileNotFoundError:
|
|
719
|
+
return Failure("npx not found - is Node.js installed?")
|
|
720
|
+
except subprocess.TimeoutExpired:
|
|
721
|
+
return Failure("eslint timed out after 120 seconds")
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
# @shell_complexity: Test runner with JSON result parsing and failure extraction
|
|
725
|
+
def run_vitest(project_path: Path) -> Result[list[TypeScriptViolation], str]:
|
|
726
|
+
"""Run Vitest for test execution.
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
project_path: Path to project root.
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
Result containing list of violations (test failures) or error message.
|
|
733
|
+
"""
|
|
734
|
+
# Validate project path exists before subprocess call
|
|
735
|
+
if not project_path.exists():
|
|
736
|
+
return Failure(f"Project path does not exist: {project_path}")
|
|
737
|
+
|
|
738
|
+
vitest_config = project_path / "vitest.config.ts"
|
|
739
|
+
if not vitest_config.exists() and not (project_path / "vitest.config.js").exists():
|
|
740
|
+
# Check if vitest is in package.json
|
|
741
|
+
pkg_json = project_path / "package.json"
|
|
742
|
+
if pkg_json.exists():
|
|
743
|
+
try:
|
|
744
|
+
pkg = json.loads(pkg_json.read_text())
|
|
745
|
+
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
746
|
+
if "vitest" not in deps:
|
|
747
|
+
return Success([]) # No vitest configured, skip
|
|
748
|
+
except json.JSONDecodeError:
|
|
749
|
+
pass
|
|
750
|
+
|
|
751
|
+
try:
|
|
752
|
+
result = subprocess.run(
|
|
753
|
+
["npx", "vitest", "run", "--reporter=json"],
|
|
754
|
+
cwd=project_path,
|
|
755
|
+
capture_output=True,
|
|
756
|
+
text=True,
|
|
757
|
+
timeout=300, # Tests may take longer
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
violations: list[TypeScriptViolation] = []
|
|
761
|
+
|
|
762
|
+
try:
|
|
763
|
+
vitest_output = json.loads(result.stdout)
|
|
764
|
+
for test_file in vitest_output.get("testResults", []):
|
|
765
|
+
file_path = test_file.get("name", "")
|
|
766
|
+
with contextlib.suppress(ValueError):
|
|
767
|
+
file_path = str(Path(file_path).relative_to(project_path))
|
|
768
|
+
|
|
769
|
+
for assertion in test_file.get("assertionResults", []):
|
|
770
|
+
if assertion.get("status") == "failed":
|
|
771
|
+
violations.append(
|
|
772
|
+
TypeScriptViolation(
|
|
773
|
+
file=file_path,
|
|
774
|
+
line=None,
|
|
775
|
+
column=None,
|
|
776
|
+
rule="test_failure",
|
|
777
|
+
message=assertion.get("title", "Test failed"),
|
|
778
|
+
severity="error",
|
|
779
|
+
source="vitest",
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
except json.JSONDecodeError:
|
|
783
|
+
# Non-JSON output usually means vitest itself failed
|
|
784
|
+
if result.returncode != 0:
|
|
785
|
+
return Failure(f"Vitest error: {result.stderr[:200]}")
|
|
786
|
+
|
|
787
|
+
return Success(violations)
|
|
788
|
+
|
|
789
|
+
except FileNotFoundError:
|
|
790
|
+
return Failure("npx not found - is Node.js installed?")
|
|
791
|
+
except subprocess.TimeoutExpired:
|
|
792
|
+
return Failure("vitest timed out after 300 seconds")
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
# @shell_complexity: Orchestrates multiple tools with graceful degradation
|
|
796
|
+
def run_typescript_guard(
|
|
797
|
+
project_path: Path,
|
|
798
|
+
*,
|
|
799
|
+
skip_tests: bool = False,
|
|
800
|
+
skip_enhanced: bool = False,
|
|
801
|
+
) -> Result[TypeScriptGuardResult, str]:
|
|
802
|
+
"""Run full TypeScript verification pipeline.
|
|
803
|
+
|
|
804
|
+
Orchestrates tsc, eslint, vitest, and @invar/* Node components
|
|
805
|
+
with graceful degradation if tools are unavailable.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
project_path: Path to TypeScript project root.
|
|
809
|
+
skip_tests: If True, skip vitest execution.
|
|
810
|
+
skip_enhanced: If True, skip @invar/* Node component analysis.
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Result containing guard result or error message.
|
|
814
|
+
"""
|
|
815
|
+
result = TypeScriptGuardResult(status="passed")
|
|
816
|
+
|
|
817
|
+
# Check tool availability
|
|
818
|
+
result.tsc_available = _check_tool_available("npx", ["tsc", "--version"])
|
|
819
|
+
result.eslint_available = _check_tool_available("npx", ["eslint", "--version"])
|
|
820
|
+
result.vitest_available = _check_tool_available("npx", ["vitest", "--version"])
|
|
821
|
+
|
|
822
|
+
all_violations: list[TypeScriptViolation] = []
|
|
823
|
+
|
|
824
|
+
# Run tsc
|
|
825
|
+
if result.tsc_available:
|
|
826
|
+
tsc_result = run_tsc(project_path)
|
|
827
|
+
match tsc_result:
|
|
828
|
+
case Success(violations):
|
|
829
|
+
all_violations.extend(violations)
|
|
830
|
+
case Failure(err):
|
|
831
|
+
# tsc failure is not fatal if it's just "no tsconfig"
|
|
832
|
+
if "No tsconfig.json" not in err:
|
|
833
|
+
pass # Log but continue
|
|
834
|
+
|
|
835
|
+
# Run eslint
|
|
836
|
+
if result.eslint_available:
|
|
837
|
+
eslint_result = run_eslint(project_path)
|
|
838
|
+
match eslint_result:
|
|
839
|
+
case Success(violations):
|
|
840
|
+
all_violations.extend(violations)
|
|
841
|
+
case Failure(_):
|
|
842
|
+
pass # ESLint errors are non-fatal
|
|
843
|
+
|
|
844
|
+
# Run vitest
|
|
845
|
+
if result.vitest_available and not skip_tests:
|
|
846
|
+
vitest_result = run_vitest(project_path)
|
|
847
|
+
match vitest_result:
|
|
848
|
+
case Success(violations):
|
|
849
|
+
all_violations.extend(violations)
|
|
850
|
+
case Failure(_):
|
|
851
|
+
pass # Test errors are non-fatal
|
|
852
|
+
|
|
853
|
+
# =========================================================================
|
|
854
|
+
# Enhanced analysis via @invar/* Node components (LX-06 Phase 2)
|
|
855
|
+
# =========================================================================
|
|
856
|
+
if not skip_enhanced:
|
|
857
|
+
enhanced = EnhancedAnalysis()
|
|
858
|
+
|
|
859
|
+
# Run ts-analyzer for contract coverage and blind spots
|
|
860
|
+
ts_analyzer_result = run_ts_analyzer(project_path)
|
|
861
|
+
match ts_analyzer_result:
|
|
862
|
+
case Success(data):
|
|
863
|
+
enhanced.ts_analyzer_available = True
|
|
864
|
+
coverage, quality, blind_spots = _parse_ts_analyzer_result(data)
|
|
865
|
+
enhanced.contract_coverage = coverage
|
|
866
|
+
enhanced.contract_quality = quality
|
|
867
|
+
enhanced.blind_spots = blind_spots
|
|
868
|
+
case Failure(_):
|
|
869
|
+
enhanced.ts_analyzer_available = False
|
|
870
|
+
|
|
871
|
+
# Run fc-runner for property-based testing
|
|
872
|
+
fc_runner_result = run_fc_runner(project_path)
|
|
873
|
+
match fc_runner_result:
|
|
874
|
+
case Success(data):
|
|
875
|
+
enhanced.fc_runner_available = True
|
|
876
|
+
passed, failures = _parse_fc_runner_result(data)
|
|
877
|
+
enhanced.property_tests_passed = passed
|
|
878
|
+
enhanced.property_test_failures = failures
|
|
879
|
+
case Failure(_):
|
|
880
|
+
enhanced.fc_runner_available = False
|
|
881
|
+
|
|
882
|
+
# Run quick-check for fast smoke testing
|
|
883
|
+
quick_check_result = run_quick_check(project_path)
|
|
884
|
+
match quick_check_result:
|
|
885
|
+
case Success(_):
|
|
886
|
+
enhanced.quick_check_available = True
|
|
887
|
+
case Failure(_):
|
|
888
|
+
enhanced.quick_check_available = False
|
|
889
|
+
|
|
890
|
+
result.enhanced = enhanced
|
|
891
|
+
|
|
892
|
+
# Aggregate results
|
|
893
|
+
result.violations = all_violations
|
|
894
|
+
result.error_count = sum(1 for v in all_violations if v.severity == "error")
|
|
895
|
+
result.warning_count = sum(1 for v in all_violations if v.severity == "warning")
|
|
896
|
+
|
|
897
|
+
if result.error_count > 0:
|
|
898
|
+
result.status = "failed"
|
|
899
|
+
elif not any([result.tsc_available, result.eslint_available]):
|
|
900
|
+
result.status = "skipped"
|
|
901
|
+
|
|
902
|
+
return Success(result)
|