@mainahq/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/package.json +37 -0
- package/src/ai/__tests__/ai.test.ts +207 -0
- package/src/ai/__tests__/design-approaches.test.ts +192 -0
- package/src/ai/__tests__/spec-questions.test.ts +191 -0
- package/src/ai/__tests__/tiers.test.ts +110 -0
- package/src/ai/commit-msg.ts +28 -0
- package/src/ai/design-approaches.ts +76 -0
- package/src/ai/index.ts +205 -0
- package/src/ai/pr-summary.ts +60 -0
- package/src/ai/spec-questions.ts +74 -0
- package/src/ai/tiers.ts +52 -0
- package/src/ai/try-generate.ts +89 -0
- package/src/ai/validate.ts +66 -0
- package/src/benchmark/__tests__/reporter.test.ts +525 -0
- package/src/benchmark/__tests__/runner.test.ts +113 -0
- package/src/benchmark/__tests__/story-loader.test.ts +152 -0
- package/src/benchmark/reporter.ts +332 -0
- package/src/benchmark/runner.ts +91 -0
- package/src/benchmark/story-loader.ts +88 -0
- package/src/benchmark/types.ts +95 -0
- package/src/cache/__tests__/keys.test.ts +97 -0
- package/src/cache/__tests__/manager.test.ts +312 -0
- package/src/cache/__tests__/ttl.test.ts +94 -0
- package/src/cache/keys.ts +44 -0
- package/src/cache/manager.ts +231 -0
- package/src/cache/ttl.ts +77 -0
- package/src/config/__tests__/config.test.ts +376 -0
- package/src/config/index.ts +198 -0
- package/src/context/__tests__/budget.test.ts +179 -0
- package/src/context/__tests__/engine.test.ts +163 -0
- package/src/context/__tests__/episodic.test.ts +291 -0
- package/src/context/__tests__/relevance.test.ts +323 -0
- package/src/context/__tests__/retrieval.test.ts +143 -0
- package/src/context/__tests__/selector.test.ts +174 -0
- package/src/context/__tests__/semantic.test.ts +252 -0
- package/src/context/__tests__/treesitter.test.ts +229 -0
- package/src/context/__tests__/working.test.ts +236 -0
- package/src/context/budget.ts +130 -0
- package/src/context/engine.ts +394 -0
- package/src/context/episodic.ts +251 -0
- package/src/context/relevance.ts +325 -0
- package/src/context/retrieval.ts +325 -0
- package/src/context/selector.ts +93 -0
- package/src/context/semantic.ts +331 -0
- package/src/context/treesitter.ts +216 -0
- package/src/context/working.ts +192 -0
- package/src/db/__tests__/db.test.ts +151 -0
- package/src/db/index.ts +211 -0
- package/src/db/schema.ts +84 -0
- package/src/design/__tests__/design.test.ts +310 -0
- package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
- package/src/design/__tests__/review.test.ts +561 -0
- package/src/design/index.ts +297 -0
- package/src/design/review.ts +327 -0
- package/src/explain/__tests__/explain.test.ts +173 -0
- package/src/explain/index.ts +181 -0
- package/src/features/__tests__/analyzer.test.ts +358 -0
- package/src/features/__tests__/checklist.test.ts +454 -0
- package/src/features/__tests__/numbering.test.ts +319 -0
- package/src/features/__tests__/quality.test.ts +295 -0
- package/src/features/__tests__/traceability.test.ts +147 -0
- package/src/features/analyzer.ts +445 -0
- package/src/features/checklist.ts +366 -0
- package/src/features/index.ts +18 -0
- package/src/features/numbering.ts +404 -0
- package/src/features/quality.ts +349 -0
- package/src/features/test-stubs.ts +157 -0
- package/src/features/traceability.ts +260 -0
- package/src/feedback/__tests__/async-feedback.test.ts +52 -0
- package/src/feedback/__tests__/collector.test.ts +219 -0
- package/src/feedback/__tests__/compress.test.ts +150 -0
- package/src/feedback/__tests__/preferences.test.ts +169 -0
- package/src/feedback/collector.ts +135 -0
- package/src/feedback/compress.ts +92 -0
- package/src/feedback/preferences.ts +108 -0
- package/src/git/__tests__/git.test.ts +62 -0
- package/src/git/index.ts +110 -0
- package/src/hooks/__tests__/runner.test.ts +266 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/runner.ts +130 -0
- package/src/index.ts +356 -0
- package/src/init/__tests__/init.test.ts +228 -0
- package/src/init/index.ts +364 -0
- package/src/language/__tests__/detect.test.ts +77 -0
- package/src/language/__tests__/profile.test.ts +51 -0
- package/src/language/detect.ts +70 -0
- package/src/language/profile.ts +110 -0
- package/src/prompts/__tests__/defaults.test.ts +52 -0
- package/src/prompts/__tests__/engine.test.ts +183 -0
- package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
- package/src/prompts/__tests__/evolution.test.ts +187 -0
- package/src/prompts/__tests__/loader.test.ts +105 -0
- package/src/prompts/candidates/review-v2.md +55 -0
- package/src/prompts/defaults/ai-review.md +49 -0
- package/src/prompts/defaults/commit.md +30 -0
- package/src/prompts/defaults/context.md +26 -0
- package/src/prompts/defaults/design-approaches.md +57 -0
- package/src/prompts/defaults/design-hld-lld.md +55 -0
- package/src/prompts/defaults/design.md +53 -0
- package/src/prompts/defaults/explain.md +31 -0
- package/src/prompts/defaults/fix.md +32 -0
- package/src/prompts/defaults/index.ts +38 -0
- package/src/prompts/defaults/review.md +41 -0
- package/src/prompts/defaults/spec-questions.md +59 -0
- package/src/prompts/defaults/tests.md +72 -0
- package/src/prompts/engine.ts +137 -0
- package/src/prompts/evolution.ts +409 -0
- package/src/prompts/loader.ts +71 -0
- package/src/review/__tests__/review.test.ts +288 -0
- package/src/review/comprehensive.ts +362 -0
- package/src/review/index.ts +417 -0
- package/src/stats/__tests__/tracker.test.ts +323 -0
- package/src/stats/index.ts +11 -0
- package/src/stats/tracker.ts +492 -0
- package/src/ticket/__tests__/ticket.test.ts +273 -0
- package/src/ticket/index.ts +185 -0
- package/src/utils.ts +87 -0
- package/src/verify/__tests__/ai-review.test.ts +242 -0
- package/src/verify/__tests__/coverage.test.ts +83 -0
- package/src/verify/__tests__/detect.test.ts +175 -0
- package/src/verify/__tests__/diff-filter.test.ts +338 -0
- package/src/verify/__tests__/fix.test.ts +478 -0
- package/src/verify/__tests__/linters/clippy.test.ts +45 -0
- package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
- package/src/verify/__tests__/linters/ruff.test.ts +64 -0
- package/src/verify/__tests__/mutation.test.ts +141 -0
- package/src/verify/__tests__/pipeline.test.ts +553 -0
- package/src/verify/__tests__/proof.test.ts +97 -0
- package/src/verify/__tests__/secretlint.test.ts +190 -0
- package/src/verify/__tests__/semgrep.test.ts +217 -0
- package/src/verify/__tests__/slop.test.ts +366 -0
- package/src/verify/__tests__/sonar.test.ts +113 -0
- package/src/verify/__tests__/syntax-guard.test.ts +227 -0
- package/src/verify/__tests__/trivy.test.ts +191 -0
- package/src/verify/__tests__/visual.test.ts +139 -0
- package/src/verify/ai-review.ts +276 -0
- package/src/verify/coverage.ts +134 -0
- package/src/verify/detect.ts +171 -0
- package/src/verify/diff-filter.ts +183 -0
- package/src/verify/fix.ts +317 -0
- package/src/verify/linters/clippy.ts +52 -0
- package/src/verify/linters/go-vet.ts +32 -0
- package/src/verify/linters/ruff.ts +47 -0
- package/src/verify/mutation.ts +143 -0
- package/src/verify/pipeline.ts +328 -0
- package/src/verify/proof.ts +277 -0
- package/src/verify/secretlint.ts +168 -0
- package/src/verify/semgrep.ts +170 -0
- package/src/verify/slop.ts +493 -0
- package/src/verify/sonar.ts +146 -0
- package/src/verify/syntax-guard.ts +251 -0
- package/src/verify/trivy.ts +161 -0
- package/src/verify/visual.ts +460 -0
- package/src/workflow/__tests__/context.test.ts +110 -0
- package/src/workflow/context.ts +81 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fix Generation — generates minimal code fixes for verification findings.
|
|
3
|
+
*
|
|
4
|
+
* Takes findings from the Verify Engine, checks cache first, then uses the
|
|
5
|
+
* Prompt Engine + AI to generate unified-diff fixes. Results are cached so
|
|
6
|
+
* identical findings never hit the AI twice.
|
|
7
|
+
*
|
|
8
|
+
* Response format expected from AI:
|
|
9
|
+
* ### Fix for finding: <tool>/<ruleId> at <file>:<line>
|
|
10
|
+
* **Explanation:** <why this fix works>
|
|
11
|
+
* **Confidence:** high|medium|low
|
|
12
|
+
* ```diff
|
|
13
|
+
* --- a/<file>
|
|
14
|
+
* +++ b/<file>
|
|
15
|
+
* @@ ... @@
|
|
16
|
+
* context
|
|
17
|
+
* -old line
|
|
18
|
+
* +new line
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { generate } from "../ai/index";
|
|
23
|
+
import { hashContent } from "../cache/keys";
|
|
24
|
+
import { createCacheManager } from "../cache/manager";
|
|
25
|
+
import { buildSystemPrompt } from "../prompts/engine";
|
|
26
|
+
import type { Finding } from "./diff-filter";
|
|
27
|
+
|
|
28
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export interface FixSuggestion {
|
|
31
|
+
finding: Finding;
|
|
32
|
+
diff: string;
|
|
33
|
+
explanation: string;
|
|
34
|
+
confidence: "high" | "medium" | "low";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FixResult {
|
|
38
|
+
suggestions: FixSuggestion[];
|
|
39
|
+
cached: boolean;
|
|
40
|
+
model?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface FixOptions {
|
|
44
|
+
mainaDir: string;
|
|
45
|
+
cwd?: string;
|
|
46
|
+
contextText?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Hashing ──────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Deterministic hash of a finding for cache key generation.
|
|
53
|
+
* Includes tool, file, line, message, and ruleId.
|
|
54
|
+
*/
|
|
55
|
+
export function hashFinding(finding: Finding): string {
|
|
56
|
+
const input = [
|
|
57
|
+
finding.tool,
|
|
58
|
+
finding.file,
|
|
59
|
+
String(finding.line),
|
|
60
|
+
finding.message,
|
|
61
|
+
finding.ruleId ?? "",
|
|
62
|
+
].join("|");
|
|
63
|
+
return hashContent(input);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a composite cache key from multiple findings.
|
|
68
|
+
*/
|
|
69
|
+
function buildFixCacheKey(findings: Finding[]): string {
|
|
70
|
+
const hashes = findings.map(hashFinding).sort();
|
|
71
|
+
return hashContent(`fix:${hashes.join(":")}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Response Parsing ─────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Match a parsed fix block to its original finding by matching tool/ruleId
|
|
78
|
+
* and file:line from the header.
|
|
79
|
+
*/
|
|
80
|
+
function matchFinding(
|
|
81
|
+
header: string,
|
|
82
|
+
findings: Finding[],
|
|
83
|
+
): Finding | undefined {
|
|
84
|
+
// Header format: "### Fix for finding: <tool>/<ruleId> at <file>:<line>"
|
|
85
|
+
// or "### Fix for finding: <tool> at <file>:<line>" (no ruleId)
|
|
86
|
+
const headerMatch = header.match(
|
|
87
|
+
/### Fix for finding:\s*(\S+?)(?:\/(\S+))?\s+at\s+(\S+?):(\d+)/,
|
|
88
|
+
);
|
|
89
|
+
if (!headerMatch) {
|
|
90
|
+
// Fall back to index-based matching (handled by caller)
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const [, tool, ruleId, file, lineStr] = headerMatch;
|
|
95
|
+
const line = Number.parseInt(lineStr ?? "0", 10);
|
|
96
|
+
|
|
97
|
+
// Try exact match first (tool + ruleId + file + line)
|
|
98
|
+
for (const f of findings) {
|
|
99
|
+
if (f.tool === tool && f.file === file && f.line === line) {
|
|
100
|
+
return f;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Try matching by tool/ruleId and file
|
|
105
|
+
for (const f of findings) {
|
|
106
|
+
const fTool = f.ruleId ? `${f.tool}/${f.ruleId}` : f.tool;
|
|
107
|
+
const headerTool = ruleId ? `${tool}/${ruleId}` : (tool ?? "");
|
|
108
|
+
if (fTool === headerTool && f.file === file) {
|
|
109
|
+
return f;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Try matching by file and line only
|
|
114
|
+
for (const f of findings) {
|
|
115
|
+
if (f.file === file && f.line === line) {
|
|
116
|
+
return f;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse the structured AI response into FixSuggestion objects.
|
|
125
|
+
*
|
|
126
|
+
* Expected format per fix:
|
|
127
|
+
* ### Fix for finding: <tool>/<ruleId> at <file>:<line>
|
|
128
|
+
* **Explanation:** <text>
|
|
129
|
+
* **Confidence:** high|medium|low
|
|
130
|
+
* ```diff
|
|
131
|
+
* <unified diff content>
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export function parseFixResponse(
|
|
135
|
+
response: string,
|
|
136
|
+
findings: Finding[],
|
|
137
|
+
): FixSuggestion[] {
|
|
138
|
+
if (!response.trim()) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const suggestions: FixSuggestion[] = [];
|
|
143
|
+
|
|
144
|
+
// Split response into fix blocks by "### Fix for finding:" headers
|
|
145
|
+
const blocks = response.split(/(?=### Fix for finding:)/);
|
|
146
|
+
|
|
147
|
+
let blockIndex = 0;
|
|
148
|
+
for (const block of blocks) {
|
|
149
|
+
if (!block.includes("### Fix for finding:")) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Extract explanation
|
|
154
|
+
const explanationMatch = block.match(
|
|
155
|
+
/\*\*Explanation:\*\*\s*(.+?)(?:\n\n|\n\*\*)/s,
|
|
156
|
+
);
|
|
157
|
+
const explanation = explanationMatch?.[1]?.trim() ?? "";
|
|
158
|
+
|
|
159
|
+
// Extract confidence
|
|
160
|
+
const confidenceMatch = block.match(
|
|
161
|
+
/\*\*Confidence:\*\*\s*(high|medium|low)/i,
|
|
162
|
+
);
|
|
163
|
+
const confidence = (confidenceMatch?.[1]?.toLowerCase() ??
|
|
164
|
+
"low") as FixSuggestion["confidence"];
|
|
165
|
+
|
|
166
|
+
// Extract diff
|
|
167
|
+
const diffMatch = block.match(/```diff\n([\s\S]*?)```/);
|
|
168
|
+
const diff = diffMatch?.[1]?.trim() ?? "";
|
|
169
|
+
|
|
170
|
+
if (!diff) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Match to finding
|
|
175
|
+
const header = block.split("\n")[0] ?? "";
|
|
176
|
+
const finding = matchFinding(header, findings) ?? findings[blockIndex];
|
|
177
|
+
|
|
178
|
+
if (!finding) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
suggestions.push({
|
|
183
|
+
finding,
|
|
184
|
+
diff,
|
|
185
|
+
explanation,
|
|
186
|
+
confidence,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
blockIndex++;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return suggestions;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Prompt Formatting ───────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Format findings into a text block for the prompt template.
|
|
199
|
+
*/
|
|
200
|
+
function formatFindings(findings: Finding[]): string {
|
|
201
|
+
return findings
|
|
202
|
+
.map((f, i) => {
|
|
203
|
+
const ruleInfo = f.ruleId ? ` (${f.ruleId})` : "";
|
|
204
|
+
const colInfo = f.column ? `:${f.column}` : "";
|
|
205
|
+
return `${i + 1}. [${f.severity.toUpperCase()}] ${f.tool}${ruleInfo} — ${f.file}:${f.line}${colInfo}\n ${f.message}`;
|
|
206
|
+
})
|
|
207
|
+
.join("\n\n");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build the user prompt instructing the AI how to format its response.
|
|
212
|
+
*/
|
|
213
|
+
function buildUserPrompt(findings: Finding[]): string {
|
|
214
|
+
const formatted = formatFindings(findings);
|
|
215
|
+
return [
|
|
216
|
+
"Generate fixes for the following findings. Use this exact format for each fix:",
|
|
217
|
+
"",
|
|
218
|
+
"### Fix for finding: <tool>/<ruleId> at <file>:<line>",
|
|
219
|
+
"",
|
|
220
|
+
"**Explanation:** <why this fix works>",
|
|
221
|
+
"",
|
|
222
|
+
"**Confidence:** high|medium|low",
|
|
223
|
+
"",
|
|
224
|
+
"```diff",
|
|
225
|
+
"--- a/<file>",
|
|
226
|
+
"+++ b/<file>",
|
|
227
|
+
"@@ ... @@",
|
|
228
|
+
" context",
|
|
229
|
+
"-old line",
|
|
230
|
+
"+new line",
|
|
231
|
+
"```",
|
|
232
|
+
"",
|
|
233
|
+
"---",
|
|
234
|
+
"",
|
|
235
|
+
"Findings:",
|
|
236
|
+
"",
|
|
237
|
+
formatted,
|
|
238
|
+
].join("\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Main Entry Point ────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Generate fixes for the given findings using AI.
|
|
245
|
+
*
|
|
246
|
+
* 1. Return early for empty findings.
|
|
247
|
+
* 2. Check cache by findings hash — return cached result if found.
|
|
248
|
+
* 3. Build system prompt via Prompt Engine.
|
|
249
|
+
* 4. Call AI with formatted findings and context.
|
|
250
|
+
* 5. Parse response into FixSuggestion objects.
|
|
251
|
+
* 6. Cache the result and return.
|
|
252
|
+
*/
|
|
253
|
+
export async function generateFixes(
|
|
254
|
+
findings: Finding[],
|
|
255
|
+
options: FixOptions,
|
|
256
|
+
): Promise<FixResult> {
|
|
257
|
+
// No findings, no work
|
|
258
|
+
if (findings.length === 0) {
|
|
259
|
+
return { suggestions: [], cached: false };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const { mainaDir, contextText } = options;
|
|
263
|
+
|
|
264
|
+
// Build cache key from all findings
|
|
265
|
+
const cacheKey = buildFixCacheKey(findings);
|
|
266
|
+
|
|
267
|
+
// Check cache first
|
|
268
|
+
const cache = createCacheManager(mainaDir);
|
|
269
|
+
const cachedEntry = cache.get(cacheKey);
|
|
270
|
+
if (cachedEntry !== null) {
|
|
271
|
+
try {
|
|
272
|
+
const parsed = JSON.parse(cachedEntry.value) as FixResult;
|
|
273
|
+
return { ...parsed, cached: true };
|
|
274
|
+
} catch {
|
|
275
|
+
// Corrupted cache entry — fall through to regenerate
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Build system prompt via Prompt Engine
|
|
280
|
+
const sourceText = contextText ?? "No additional source context available.";
|
|
281
|
+
const findingsText = formatFindings(findings);
|
|
282
|
+
|
|
283
|
+
const builtPrompt = await buildSystemPrompt("fix", mainaDir, {
|
|
284
|
+
findings: findingsText,
|
|
285
|
+
source: sourceText,
|
|
286
|
+
conventions: "",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Build user prompt
|
|
290
|
+
const userPrompt = buildUserPrompt(findings);
|
|
291
|
+
|
|
292
|
+
// Call AI (single call for all findings — batched)
|
|
293
|
+
const aiResult = await generate({
|
|
294
|
+
task: "fix",
|
|
295
|
+
systemPrompt: builtPrompt.prompt,
|
|
296
|
+
userPrompt,
|
|
297
|
+
files: findings.map((f) => f.file),
|
|
298
|
+
mainaDir,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Parse the response
|
|
302
|
+
const suggestions = parseFixResponse(aiResult.text, findings);
|
|
303
|
+
|
|
304
|
+
const result: FixResult = {
|
|
305
|
+
suggestions,
|
|
306
|
+
cached: false,
|
|
307
|
+
model: aiResult.model,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Cache the result (TTL 0 = forever for fix tasks, keyed by content hash)
|
|
311
|
+
cache.set(cacheKey, JSON.stringify(result), {
|
|
312
|
+
ttl: 0,
|
|
313
|
+
model: aiResult.model,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clippy output parser for Rust linting.
|
|
3
|
+
* Parses `cargo clippy --message-format=json` output.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SyntaxDiagnostic } from "../syntax-guard";
|
|
7
|
+
|
|
8
|
+
export function parseClippyOutput(output: string): SyntaxDiagnostic[] {
|
|
9
|
+
const diagnostics: SyntaxDiagnostic[] = [];
|
|
10
|
+
const lines = output.split("\n");
|
|
11
|
+
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
if (!line.trim()) continue;
|
|
14
|
+
|
|
15
|
+
let parsed: Record<string, unknown>;
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(line);
|
|
18
|
+
} catch {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (parsed.reason !== "compiler-message") continue;
|
|
23
|
+
|
|
24
|
+
const msg = parsed.message as Record<string, unknown> | undefined;
|
|
25
|
+
if (!msg) continue;
|
|
26
|
+
|
|
27
|
+
const level = (msg.level as string) ?? "warning";
|
|
28
|
+
const message = (msg.message as string) ?? "";
|
|
29
|
+
const code = (msg.code as Record<string, string> | undefined)?.code ?? "";
|
|
30
|
+
const spans = msg.spans as Array<Record<string, unknown>> | undefined;
|
|
31
|
+
|
|
32
|
+
if (!spans || spans.length === 0) continue;
|
|
33
|
+
|
|
34
|
+
const span = spans[0] as Record<string, unknown>;
|
|
35
|
+
const fileName = (span.file_name as string) ?? "";
|
|
36
|
+
const lineStart = (span.line_start as number) ?? 0;
|
|
37
|
+
const columnStart = (span.column_start as number) ?? 0;
|
|
38
|
+
|
|
39
|
+
const severity: "error" | "warning" =
|
|
40
|
+
level === "error" ? "error" : "warning";
|
|
41
|
+
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
file: fileName,
|
|
44
|
+
line: lineStart,
|
|
45
|
+
column: columnStart,
|
|
46
|
+
message: code ? `${code}: ${message}` : message,
|
|
47
|
+
severity,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return diagnostics;
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go vet output parser.
|
|
3
|
+
* Parses `go vet` stderr text output (format: file:line:col: message).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SyntaxDiagnostic } from "../syntax-guard";
|
|
7
|
+
|
|
8
|
+
export function parseGoVetOutput(output: string): SyntaxDiagnostic[] {
|
|
9
|
+
const diagnostics: SyntaxDiagnostic[] = [];
|
|
10
|
+
const lines = output.split("\n");
|
|
11
|
+
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
if (!line.trim() || line.startsWith("#") || line.startsWith("vet:"))
|
|
14
|
+
continue;
|
|
15
|
+
|
|
16
|
+
const match = line.match(/^(.+?):(\d+):(\d+):\s+(.+)$/);
|
|
17
|
+
if (!match) continue;
|
|
18
|
+
|
|
19
|
+
const [, file, lineStr, colStr, message] = match;
|
|
20
|
+
if (!file || !lineStr || !message) continue;
|
|
21
|
+
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
file,
|
|
24
|
+
line: Number.parseInt(lineStr, 10),
|
|
25
|
+
column: Number.parseInt(colStr ?? "0", 10),
|
|
26
|
+
message,
|
|
27
|
+
severity: "error",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return diagnostics;
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruff output parser for Python syntax/lint checking.
|
|
3
|
+
* Parses `ruff check --output-format=json` output.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SyntaxDiagnostic } from "../syntax-guard";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse ruff JSON output into SyntaxDiagnostic[].
|
|
10
|
+
* F-codes (pyflakes) are errors, E-codes (pycodestyle) are warnings.
|
|
11
|
+
*/
|
|
12
|
+
export function parseRuffOutput(json: string): SyntaxDiagnostic[] {
|
|
13
|
+
let items: unknown[];
|
|
14
|
+
try {
|
|
15
|
+
items = JSON.parse(json);
|
|
16
|
+
} catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!Array.isArray(items)) return [];
|
|
21
|
+
|
|
22
|
+
const diagnostics: SyntaxDiagnostic[] = [];
|
|
23
|
+
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
const i = item as Record<string, unknown>;
|
|
26
|
+
const code = (i.code as string) ?? "";
|
|
27
|
+
const message = (i.message as string) ?? "";
|
|
28
|
+
const filename = (i.filename as string) ?? "";
|
|
29
|
+
const location = i.location as Record<string, number> | undefined;
|
|
30
|
+
const row = location?.row ?? 0;
|
|
31
|
+
const column = location?.column ?? 0;
|
|
32
|
+
|
|
33
|
+
const severity: "error" | "warning" = code.startsWith("F")
|
|
34
|
+
? "error"
|
|
35
|
+
: "warning";
|
|
36
|
+
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
file: filename,
|
|
39
|
+
line: row,
|
|
40
|
+
column,
|
|
41
|
+
message: `${code}: ${message}`,
|
|
42
|
+
severity,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return diagnostics;
|
|
47
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stryker Mutation Testing Integration for the Verify Engine.
|
|
3
|
+
*
|
|
4
|
+
* Runs Stryker and parses the JSON report for survived mutants.
|
|
5
|
+
* Survived mutants indicate untested code paths.
|
|
6
|
+
* Gracefully skips if stryker is not installed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isToolAvailable } from "./detect";
|
|
10
|
+
import type { Finding } from "./diff-filter";
|
|
11
|
+
|
|
12
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface MutationOptions {
|
|
15
|
+
cwd?: string;
|
|
16
|
+
/** Pre-resolved availability — skips redundant detection if provided. */
|
|
17
|
+
available?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MutationResult {
|
|
21
|
+
findings: Finding[];
|
|
22
|
+
skipped: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── JSON Parsing ─────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse Stryker JSON report into Finding[].
|
|
29
|
+
* Only survived mutants become findings — killed/timeout/no-coverage are ignored.
|
|
30
|
+
*
|
|
31
|
+
* Expected format:
|
|
32
|
+
* ```json
|
|
33
|
+
* {
|
|
34
|
+
* "files": {
|
|
35
|
+
* "src/app.ts": {
|
|
36
|
+
* "mutants": [{
|
|
37
|
+
* "id": "1",
|
|
38
|
+
* "mutatorName": "ConditionalExpression",
|
|
39
|
+
* "status": "Survived",
|
|
40
|
+
* "location": { "start": { "line": 10, "column": 5 } },
|
|
41
|
+
* "description": "Replaced x > 0 with false"
|
|
42
|
+
* }]
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function parseStrykerReport(json: string): Finding[] {
|
|
49
|
+
let parsed: Record<string, unknown>;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(json) as Record<string, unknown>;
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const files = parsed.files;
|
|
57
|
+
if (!files || typeof files !== "object") {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const findings: Finding[] = [];
|
|
62
|
+
|
|
63
|
+
for (const [filePath, fileData] of Object.entries(
|
|
64
|
+
files as Record<string, unknown>,
|
|
65
|
+
)) {
|
|
66
|
+
const data = fileData as Record<string, unknown>;
|
|
67
|
+
const mutants = data.mutants;
|
|
68
|
+
if (!Array.isArray(mutants)) continue;
|
|
69
|
+
|
|
70
|
+
for (const mutant of mutants) {
|
|
71
|
+
const m = mutant as Record<string, unknown>;
|
|
72
|
+
const status = (m.status as string) ?? "";
|
|
73
|
+
|
|
74
|
+
// Only report survived mutants — they indicate untested code
|
|
75
|
+
if (status !== "Survived") continue;
|
|
76
|
+
|
|
77
|
+
const mutatorName = (m.mutatorName as string) ?? "Unknown";
|
|
78
|
+
const description = (m.description as string) ?? "";
|
|
79
|
+
const location = m.location as Record<string, unknown> | undefined;
|
|
80
|
+
const start = location?.start as Record<string, unknown> | undefined;
|
|
81
|
+
const line = (start?.line as number) ?? 0;
|
|
82
|
+
|
|
83
|
+
findings.push({
|
|
84
|
+
tool: "stryker",
|
|
85
|
+
file: filePath,
|
|
86
|
+
line,
|
|
87
|
+
message: `Survived mutant: ${description} (${mutatorName})`,
|
|
88
|
+
severity: "warning",
|
|
89
|
+
ruleId: `stryker/${mutatorName}`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return findings;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Runner ───────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Run Stryker mutation testing and return parsed findings.
|
|
101
|
+
*
|
|
102
|
+
* If stryker is not installed, returns `{ findings: [], skipped: true }`.
|
|
103
|
+
* If stryker fails, returns `{ findings: [], skipped: false }`.
|
|
104
|
+
*/
|
|
105
|
+
export async function runMutation(
|
|
106
|
+
options?: MutationOptions,
|
|
107
|
+
): Promise<MutationResult> {
|
|
108
|
+
const toolAvailable =
|
|
109
|
+
options?.available ?? (await isToolAvailable("stryker"));
|
|
110
|
+
if (!toolAvailable) {
|
|
111
|
+
return { findings: [], skipped: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
115
|
+
|
|
116
|
+
const args = ["stryker", "run", "--reporters", "json"];
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const proc = Bun.spawn(args, {
|
|
120
|
+
cwd,
|
|
121
|
+
stdout: "pipe",
|
|
122
|
+
stderr: "pipe",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await new Response(proc.stdout).text();
|
|
126
|
+
await new Response(proc.stderr).text();
|
|
127
|
+
await proc.exited;
|
|
128
|
+
|
|
129
|
+
// Read the generated report file
|
|
130
|
+
const reportPath = `${cwd}/reports/mutation/mutation.json`;
|
|
131
|
+
const reportFile = Bun.file(reportPath);
|
|
132
|
+
const exists = await reportFile.exists();
|
|
133
|
+
if (!exists) {
|
|
134
|
+
return { findings: [], skipped: false };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const reportJson = await reportFile.text();
|
|
138
|
+
const findings = parseStrykerReport(reportJson);
|
|
139
|
+
return { findings, skipped: false };
|
|
140
|
+
} catch {
|
|
141
|
+
return { findings: [], skipped: false };
|
|
142
|
+
}
|
|
143
|
+
}
|