klyro 0.1.16 → 0.1.18
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/dist/agent/runtime.d.ts +1 -0
- package/dist/agent/runtime.js +139 -4
- package/dist/cli/eval.js +15 -0
- package/dist/cli/repl.js +36 -39
- package/dist/cli/run.d.ts +1 -0
- package/dist/cli/run.js +17 -0
- package/dist/cli/slash/parser.d.ts +2 -0
- package/dist/cli/slash/parser.js +2 -1
- package/dist/index.js +2 -0
- package/dist/tui/app.d.ts +6 -4
- package/dist/tui/app.js +49 -49
- package/dist/verification/baseline.d.ts +17 -0
- package/dist/verification/baseline.js +108 -0
- package/dist/verification/classify.d.ts +22 -0
- package/dist/verification/classify.js +108 -0
- package/dist/verification/detect.js +175 -10
- package/dist/verification/registry.d.ts +23 -0
- package/dist/verification/registry.js +130 -0
- package/dist/verification/scoped.d.ts +22 -0
- package/dist/verification/scoped.js +214 -0
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 6.1 — Baseline run (pre-existing failure cache per HEAD)
|
|
3
|
+
* Before first edit, run primary verifier and cache result keyed by git HEAD.
|
|
4
|
+
* Used to distinguish introduced vs pre-existing failures (6.4 classify).
|
|
5
|
+
*/
|
|
6
|
+
export interface BaselineResult {
|
|
7
|
+
head: string;
|
|
8
|
+
command: string;
|
|
9
|
+
ok: boolean;
|
|
10
|
+
exitCode: number;
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
capturedAt: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function getBaseline(cwd: string, command?: string): Promise<BaselineResult | null>;
|
|
16
|
+
export declare function runBaseline(cwd: string, command?: string, timeoutMs?: number): Promise<BaselineResult | null>;
|
|
17
|
+
export declare function ensureBaseline(cwd: string, command?: string): Promise<BaselineResult | null>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 6.1 — Baseline run (pre-existing failure cache per HEAD)
|
|
3
|
+
* Before first edit, run primary verifier and cache result keyed by git HEAD.
|
|
4
|
+
* Used to distinguish introduced vs pre-existing failures (6.4 classify).
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as crypto from 'node:crypto';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { primaryVerifyCommand } from './registry.js';
|
|
11
|
+
async function gitHead(cwd) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const child = spawn('git', ['rev-parse', 'HEAD'], { cwd, shell: false });
|
|
14
|
+
let out = '';
|
|
15
|
+
child.stdout.on('data', (b) => { out += b.toString(); });
|
|
16
|
+
child.on('close', () => resolve(out.trim() || 'no-head'));
|
|
17
|
+
child.on('error', () => resolve('no-head'));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function baselinePath(cwd, head) {
|
|
21
|
+
const safe = head.slice(0, 12) || 'no-head';
|
|
22
|
+
return path.join(cwd, '.klyro', 'baselines', `${safe}.json`);
|
|
23
|
+
}
|
|
24
|
+
export async function getBaseline(cwd, command) {
|
|
25
|
+
const cmd = command ?? primaryVerifyCommand(cwd);
|
|
26
|
+
if (!cmd)
|
|
27
|
+
return null;
|
|
28
|
+
const head = await gitHead(cwd);
|
|
29
|
+
const p = baselinePath(cwd, head);
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
32
|
+
const parsed = JSON.parse(raw);
|
|
33
|
+
if (parsed.head === head && parsed.command === cmd)
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
catch { /* miss */ }
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export async function runBaseline(cwd, command, timeoutMs = 90_000) {
|
|
40
|
+
const cmd = command ?? primaryVerifyCommand(cwd);
|
|
41
|
+
if (!cmd)
|
|
42
|
+
return null;
|
|
43
|
+
const head = await gitHead(cwd);
|
|
44
|
+
const p = baselinePath(cwd, head);
|
|
45
|
+
// if cached, return
|
|
46
|
+
const cached = await getBaseline(cwd, cmd);
|
|
47
|
+
if (cached)
|
|
48
|
+
return cached;
|
|
49
|
+
const result = await runCmd(cwd, cmd, timeoutMs);
|
|
50
|
+
const baseline = {
|
|
51
|
+
head,
|
|
52
|
+
command: cmd,
|
|
53
|
+
ok: result.ok,
|
|
54
|
+
exitCode: result.exitCode,
|
|
55
|
+
stdout: result.stdout.slice(0, 8000),
|
|
56
|
+
stderr: result.stderr.slice(0, 8000),
|
|
57
|
+
capturedAt: Date.now(),
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
61
|
+
const tmp = `${p}.tmp-${crypto.randomBytes(4).toString('hex')}`;
|
|
62
|
+
fs.writeFileSync(tmp, JSON.stringify(baseline, null, 2));
|
|
63
|
+
fs.renameSync(tmp, p);
|
|
64
|
+
}
|
|
65
|
+
catch { /* ignore */ }
|
|
66
|
+
return baseline;
|
|
67
|
+
}
|
|
68
|
+
function runCmd(cwd, command, timeoutMs) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const child = spawn(command, { cwd, shell: true, env: process.env });
|
|
71
|
+
let stdout = '';
|
|
72
|
+
let stderr = '';
|
|
73
|
+
let done = false;
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
if (done)
|
|
76
|
+
return;
|
|
77
|
+
done = true;
|
|
78
|
+
try {
|
|
79
|
+
child.kill();
|
|
80
|
+
}
|
|
81
|
+
catch { /* ignore */ }
|
|
82
|
+
resolve({ ok: false, exitCode: -1, stdout, stderr: stderr + '\n[baseline timeout]' });
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
child.stdout.on('data', (b) => { stdout += b.toString(); });
|
|
85
|
+
child.stderr.on('data', (b) => { stderr += b.toString(); });
|
|
86
|
+
child.on('close', (code) => {
|
|
87
|
+
if (done)
|
|
88
|
+
return;
|
|
89
|
+
done = true;
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
const exit = typeof code === 'number' ? code : -1;
|
|
92
|
+
resolve({ ok: exit === 0, exitCode: exit, stdout, stderr });
|
|
93
|
+
});
|
|
94
|
+
child.on('error', (err) => {
|
|
95
|
+
if (done)
|
|
96
|
+
return;
|
|
97
|
+
done = true;
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
resolve({ ok: false, exitCode: -1, stdout, stderr: String(err) });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export async function ensureBaseline(cwd, command) {
|
|
104
|
+
const existing = await getBaseline(cwd, command);
|
|
105
|
+
if (existing)
|
|
106
|
+
return existing;
|
|
107
|
+
return runBaseline(cwd, command);
|
|
108
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Failure } from './detect.js';
|
|
2
|
+
import type { BaselineResult } from './baseline.js';
|
|
3
|
+
export type FailureClass = 'introduced' | 'pre_existing' | 'flaky' | 'env';
|
|
4
|
+
export declare function classifyFailure(current: {
|
|
5
|
+
failure?: Failure;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
}, baseline: BaselineResult | null, flakyRerunOk?: boolean): FailureClass;
|
|
9
|
+
export declare function rerunOnce(cwd: string, command: string, timeoutMs?: number): Promise<boolean>;
|
|
10
|
+
export interface RepairContext {
|
|
11
|
+
failingTests: {
|
|
12
|
+
file: string;
|
|
13
|
+
content: string;
|
|
14
|
+
}[];
|
|
15
|
+
hunks: string;
|
|
16
|
+
blame: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function gatherRepairContext(cwd: string, failure: Failure | undefined): Promise<RepairContext>;
|
|
19
|
+
export declare function guardRepair(diff: string, editedFiles: string[]): {
|
|
20
|
+
blocked: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 6.4 — Repair classifier + context gather + guard
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
export function classifyFailure(current, baseline, flakyRerunOk) {
|
|
8
|
+
const combined = (current.stderr + '\n' + current.stdout).toLowerCase();
|
|
9
|
+
// env: missing binary, network, permission, no such file, EACCES etc
|
|
10
|
+
if (/enoent|command not found|no such file|network|econn|etimedout|eacces|permission denied|env/.test(combined) && /error/i.test(combined)) {
|
|
11
|
+
// only if not a real test failure but env
|
|
12
|
+
if (!current.failure || current.failure.type === 'unknown')
|
|
13
|
+
return 'env';
|
|
14
|
+
// even test failures can be env if message hints
|
|
15
|
+
if (/enoent|not found/.test(combined))
|
|
16
|
+
return 'env';
|
|
17
|
+
}
|
|
18
|
+
if (flakyRerunOk)
|
|
19
|
+
return 'flaky';
|
|
20
|
+
if (baseline && !baseline.ok) {
|
|
21
|
+
const baseRaw = (baseline.stderr + '\n' + baseline.stdout);
|
|
22
|
+
// if current failure files overlap baseline failure raw, treat as pre-existing
|
|
23
|
+
if (current.failure && baseline.stdout + baseline.stderr) {
|
|
24
|
+
const curPaths = new Set(current.failure.files.map((f) => f.path).filter(Boolean));
|
|
25
|
+
const baseContains = [...curPaths].some((p) => baseRaw.includes(p));
|
|
26
|
+
if (baseContains)
|
|
27
|
+
return 'pre_existing';
|
|
28
|
+
// also if same exit code and type, likely pre-existing
|
|
29
|
+
if (baseRaw.includes(current.failure.files[0]?.message ?? '') && baseRaw.length > 0)
|
|
30
|
+
return 'pre_existing';
|
|
31
|
+
}
|
|
32
|
+
// if baseline failed and current also fails with similar raw length, assume pre-existing
|
|
33
|
+
if (current.failure && baseline.stderr.length > 0 && combined.includes(baseline.stderr.slice(0, 200).toLowerCase()))
|
|
34
|
+
return 'pre_existing';
|
|
35
|
+
}
|
|
36
|
+
return 'introduced';
|
|
37
|
+
}
|
|
38
|
+
export async function rerunOnce(cwd, command, timeoutMs = 45_000) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const child = spawn(command, { cwd, shell: true, env: process.env });
|
|
41
|
+
let done = false;
|
|
42
|
+
const t = setTimeout(() => { if (!done) {
|
|
43
|
+
done = true;
|
|
44
|
+
try {
|
|
45
|
+
child.kill();
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
resolve(false);
|
|
49
|
+
} }, timeoutMs);
|
|
50
|
+
child.on('close', (code) => { if (done)
|
|
51
|
+
return; done = true; clearTimeout(t); resolve(code === 0); });
|
|
52
|
+
child.on('error', () => { if (done)
|
|
53
|
+
return; done = true; clearTimeout(t); resolve(false); });
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function gatherRepairContext(cwd, failure) {
|
|
57
|
+
const failingTests = [];
|
|
58
|
+
if (failure) {
|
|
59
|
+
for (const f of failure.files.slice(0, 3)) {
|
|
60
|
+
if (!f.path)
|
|
61
|
+
continue;
|
|
62
|
+
const full = path.join(cwd, f.path);
|
|
63
|
+
try {
|
|
64
|
+
const raw = fs.readFileSync(full, 'utf-8');
|
|
65
|
+
// slice around failing line if available
|
|
66
|
+
const lines = raw.split('\n');
|
|
67
|
+
const start = Math.max(0, (f.line ?? 1) - 20);
|
|
68
|
+
const end = Math.min(lines.length, (f.line ?? 1) + 20);
|
|
69
|
+
failingTests.push({ file: f.path, content: lines.slice(start, end).join('\n') });
|
|
70
|
+
}
|
|
71
|
+
catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// hunks: git diff --stat + --unified=2 for changed files
|
|
75
|
+
const hunks = await execCapture(cwd, 'git diff --stat && echo "---" && git diff -U2 2>&1 | head -n 300');
|
|
76
|
+
const blame = failure?.files[0]?.path
|
|
77
|
+
? await execCapture(cwd, `git blame "${failure.files[0].path}" 2>&1 | head -n 20`)
|
|
78
|
+
: '';
|
|
79
|
+
return { failingTests, hunks, blame };
|
|
80
|
+
}
|
|
81
|
+
function execCapture(cwd, cmd) {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const child = spawn(cmd, { cwd, shell: true, env: process.env });
|
|
84
|
+
let out = '';
|
|
85
|
+
child.stdout.on('data', (b) => { out += b.toString(); });
|
|
86
|
+
child.stderr.on('data', (b) => { out += b.toString(); });
|
|
87
|
+
child.on('close', () => resolve(out.slice(0, 4000)));
|
|
88
|
+
child.on('error', () => resolve(''));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Guard: does diff touch assertions or add skips?
|
|
92
|
+
const ASSERT_RE = /(?:expect\s*\(|assert\.|assert\(|should\.|chai\.)/;
|
|
93
|
+
const SKIP_RE = /(?:\.skip\(|\.todo\(|xdescribe\(|xit\(|xtest\(|@pytest\.mark\.skip|pytest\.skip|:\s*skip\b)/i;
|
|
94
|
+
export function guardRepair(diff, editedFiles) {
|
|
95
|
+
if (!diff)
|
|
96
|
+
return { blocked: false };
|
|
97
|
+
const addedLines = diff.split('\n').filter((l) => l.startsWith('+') && !l.startsWith('+++'));
|
|
98
|
+
const touchesAssert = addedLines.some((l) => ASSERT_RE.test(l));
|
|
99
|
+
const addsSkip = addedLines.some((l) => SKIP_RE.test(l));
|
|
100
|
+
if (touchesAssert && addedLines.length < 20) {
|
|
101
|
+
// touching assertions in a small diff likely means editing test expectations — require approval
|
|
102
|
+
return { blocked: true, reason: 'repair touches test assertions — requires explicit approval (edit expectations directly is discouraged)' };
|
|
103
|
+
}
|
|
104
|
+
if (addsSkip) {
|
|
105
|
+
return { blocked: true, reason: 'repair adds skip/todo — requires explicit approval (skipping tests is not a fix)' };
|
|
106
|
+
}
|
|
107
|
+
return { blocked: false };
|
|
108
|
+
}
|
|
@@ -11,39 +11,178 @@
|
|
|
11
11
|
* the raw output; we just pre-structure the high-signal lines.
|
|
12
12
|
*/
|
|
13
13
|
const TS_LINE = /^(.+?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
|
|
14
|
+
const TSC_LINE2 = /^(.+?):(\d+):(\d+)\s+-\s+error\s+(TS\d+):\s+(.+)$/;
|
|
14
15
|
const TEST_FAIL = /^\s*✘|FAIL\s+(\S+)|✗\s+(\S+)|×\s+(.+?)\s/;
|
|
15
|
-
const LINT_LINE = /^(.+?)\s*$/;
|
|
16
16
|
const RUNTIME_LINE = /^(?:Error|TypeError|ReferenceError):\s+(.+)$/;
|
|
17
|
+
// 6.2 — dedicated patterns for each runner/linter/compiler
|
|
18
|
+
const PYTEST_FAIL = /FAILED\s+(\S+?)(?:::\S+)?\s+-\s+(.+)/;
|
|
19
|
+
const PYTEST_ASSERT = /AssertionError:\s*(.+)/;
|
|
20
|
+
const GO_FAIL = /^---\s+FAIL:\s+(\S+)\s+\((.+)\)/;
|
|
21
|
+
const GO_PKG_FAIL = /^FAIL\s+(\S+)\s/;
|
|
22
|
+
const CARGO_FAIL = /^test\s+(\S+)\s+\.\.\.\s+FAILED/;
|
|
23
|
+
const CARGO_PANIC = /thread\s+'(.+?)'\s+panicked at\s+'(.+?)',\s+(.+?):(\d+):(\d+)/;
|
|
24
|
+
const MOCHA_FAIL = /^\s*\d+\)\s+(.+)$/;
|
|
25
|
+
const JUNIT_FAIL = /<(?:failure|error)[\s>]/;
|
|
26
|
+
const ESLINT_LINE = /^(.+?):(\d+):(\d+):\s+(error|warning)\s+(.+?)\s+\((.+?)\)\s*$/;
|
|
27
|
+
const RUFF_LINE = /^(.+?):(\d+):(\d+):\s+([A-Z]\d+)\s+(.+)$/;
|
|
28
|
+
const MYPY_LINE = /^(.+?):(\d+):\s+error:\s+(.+?)\s+\[(.+?)\]\s*$/;
|
|
29
|
+
const GCC_LINE = /^(.+?):(\d+):(\d+):\s+(?:fatal\s+)?error:\s+(.+)$/;
|
|
30
|
+
const VITE_ERROR = /ERROR\s+in\s+(.+)|Module build failed.+/;
|
|
31
|
+
const DOTNET_CS = /^(.+?)\((\d+),(\d+)\):\s+error\s+(CS\d+):\s+(.+)$/;
|
|
32
|
+
const GENERIC_FILE_LINE = /^(.+?):(\d+)(?::(\d+))?:\s*(.+)$/;
|
|
17
33
|
export function detect(stdout, stderr, exitCode) {
|
|
18
34
|
const combined = stderr + '\n' + stdout;
|
|
19
35
|
if (exitCode === 0) {
|
|
20
36
|
return { type: 'unknown', files: [], raw: combined, exitCode };
|
|
21
37
|
}
|
|
22
|
-
// TypeScript tsc
|
|
38
|
+
// TypeScript tsc (both formats)
|
|
23
39
|
const tsMatches = [];
|
|
24
40
|
for (const line of combined.split(/\r?\n/)) {
|
|
25
|
-
|
|
41
|
+
let m = TS_LINE.exec(line);
|
|
26
42
|
if (m && m[1] && m[2] && m[3] && m[4] && m[5]) {
|
|
27
43
|
tsMatches.push({ path: m[1], line: Number(m[2]), column: Number(m[3]), code: m[4], message: m[5] });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
m = TSC_LINE2.exec(line);
|
|
47
|
+
if (m && m[1] && m[2] && m[3] && m[4] && m[5]) {
|
|
48
|
+
tsMatches.push({ path: m[1], line: Number(m[2]), column: Number(m[3]), code: m[4], message: m[5] });
|
|
49
|
+
}
|
|
50
|
+
const dm = DOTNET_CS.exec(line);
|
|
51
|
+
if (dm && dm[1] && dm[2] && dm[3] && dm[4] && dm[5]) {
|
|
52
|
+
tsMatches.push({ path: dm[1], line: Number(dm[2]), column: Number(dm[3]), code: dm[4], message: dm[5] });
|
|
28
53
|
}
|
|
29
54
|
}
|
|
30
55
|
if (tsMatches.length > 0)
|
|
31
|
-
return { type: 'type', files: tsMatches, raw: combined, exitCode };
|
|
32
|
-
//
|
|
33
|
-
if (
|
|
56
|
+
return { type: 'type', files: dedupe(tsMatches), raw: combined, exitCode };
|
|
57
|
+
// JUnit XML
|
|
58
|
+
if (JUNIT_FAIL.test(combined)) {
|
|
59
|
+
const files = [];
|
|
60
|
+
for (const line of combined.split(/\r?\n/)) {
|
|
61
|
+
if (JUNIT_FAIL.test(line))
|
|
62
|
+
files.push({ path: '', message: line.trim() });
|
|
63
|
+
}
|
|
64
|
+
// also extract file:line fallback
|
|
65
|
+
if (files.length === 0)
|
|
66
|
+
return { type: 'test', files: extractGeneric(combined, 5), raw: combined, exitCode };
|
|
67
|
+
return { type: 'test', files: dedupe(files), raw: combined, exitCode };
|
|
68
|
+
}
|
|
69
|
+
// pytest
|
|
70
|
+
if (/FAILED\s+\S+|AssertionError|pytest/.test(combined)) {
|
|
71
|
+
const files = [];
|
|
72
|
+
for (const line of combined.split(/\r?\n/)) {
|
|
73
|
+
let m = PYTEST_FAIL.exec(line);
|
|
74
|
+
if (m && m[1]) {
|
|
75
|
+
files.push({ path: m[1], message: m[2] ?? line.trim() });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
m = PYTEST_ASSERT.exec(line);
|
|
79
|
+
if (m) {
|
|
80
|
+
files.push({ path: '', message: m[0] });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (files.length > 0)
|
|
85
|
+
return { type: 'test', files: dedupe(files), raw: combined, exitCode };
|
|
86
|
+
}
|
|
87
|
+
// go test
|
|
88
|
+
if (/---\s+FAIL:|FAIL\s+\S+/.test(combined)) {
|
|
89
|
+
const files = [];
|
|
90
|
+
for (const line of combined.split(/\r?\n/)) {
|
|
91
|
+
let m = GO_FAIL.exec(line);
|
|
92
|
+
if (m && m[1]) {
|
|
93
|
+
files.push({ path: m[1], message: line.trim() });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
m = GO_PKG_FAIL.exec(line);
|
|
97
|
+
if (m && m[1]) {
|
|
98
|
+
files.push({ path: m[1], message: line.trim() });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (files.length > 0)
|
|
102
|
+
return { type: 'test', files: dedupe(files), raw: combined, exitCode };
|
|
103
|
+
}
|
|
104
|
+
// cargo test
|
|
105
|
+
if (/test result: FAILED|FAILED.*cargo|panicked at/.test(combined)) {
|
|
106
|
+
const files = [];
|
|
107
|
+
for (const line of combined.split(/\r?\n/)) {
|
|
108
|
+
let m = CARGO_FAIL.exec(line);
|
|
109
|
+
if (m && m[1]) {
|
|
110
|
+
files.push({ path: m[1], message: line.trim() });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
m = CARGO_PANIC.exec(line);
|
|
114
|
+
if (m && m[3] && m[4] && m[5]) {
|
|
115
|
+
files.push({ path: m[3], line: Number(m[4]), column: Number(m[5]), message: m[2] ?? line.trim() });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (files.length > 0)
|
|
119
|
+
return { type: 'test', files: dedupe(files), raw: combined, exitCode };
|
|
120
|
+
}
|
|
121
|
+
// Generic test runner (vitest/jest/mocha)
|
|
122
|
+
if (/Test Suites:|FAIL\s|Tests:.*failed|✘|✗|×\s+/.test(combined) || MOCHA_FAIL.test(combined)) {
|
|
34
123
|
const files = [];
|
|
35
124
|
for (const line of combined.split(/\r?\n/)) {
|
|
36
125
|
const m = TEST_FAIL.exec(line);
|
|
37
126
|
if (m) {
|
|
38
127
|
const target = m[1] ?? m[2] ?? m[3] ?? line.trim();
|
|
39
128
|
files.push({ path: target, message: line.trim() });
|
|
129
|
+
continue;
|
|
40
130
|
}
|
|
131
|
+
const mm = MOCHA_FAIL.exec(line);
|
|
132
|
+
if (mm && mm[1])
|
|
133
|
+
files.push({ path: '', message: mm[1] });
|
|
41
134
|
}
|
|
42
|
-
|
|
135
|
+
if (files.length > 0)
|
|
136
|
+
return { type: 'test', files: dedupe(files), raw: combined, exitCode };
|
|
137
|
+
// fallback: any FAIL line
|
|
138
|
+
if (/FAIL/.test(combined))
|
|
139
|
+
return { type: 'test', files: extractGeneric(combined, 5), raw: combined, exitCode };
|
|
43
140
|
}
|
|
44
|
-
// ESLint
|
|
45
|
-
|
|
46
|
-
|
|
141
|
+
// ESLint
|
|
142
|
+
{
|
|
143
|
+
const es = [];
|
|
144
|
+
for (const line of combined.split(/\r?\n/)) {
|
|
145
|
+
const m = ESLINT_LINE.exec(line);
|
|
146
|
+
if (m && m[1] && m[2] && m[3] && m[5])
|
|
147
|
+
es.push({ path: m[1], line: Number(m[2]), column: Number(m[3]), code: m[6], message: m[5] });
|
|
148
|
+
}
|
|
149
|
+
if (es.length > 0)
|
|
150
|
+
return { type: 'lint', files: dedupe(es), raw: combined, exitCode };
|
|
151
|
+
}
|
|
152
|
+
// ruff / flake8
|
|
153
|
+
{
|
|
154
|
+
const rf = [];
|
|
155
|
+
for (const line of combined.split(/\r?\n/)) {
|
|
156
|
+
const m = RUFF_LINE.exec(line);
|
|
157
|
+
if (m && m[1] && m[2] && m[3] && m[4] && m[5])
|
|
158
|
+
rf.push({ path: m[1], line: Number(m[2]), column: Number(m[3]), code: m[4], message: m[5] });
|
|
159
|
+
}
|
|
160
|
+
if (rf.length > 0)
|
|
161
|
+
return { type: 'lint', files: dedupe(rf), raw: combined, exitCode };
|
|
162
|
+
}
|
|
163
|
+
// mypy
|
|
164
|
+
{
|
|
165
|
+
const mp = [];
|
|
166
|
+
for (const line of combined.split(/\r?\n/)) {
|
|
167
|
+
const m = MYPY_LINE.exec(line);
|
|
168
|
+
if (m && m[1] && m[2] && m[3] && m[4])
|
|
169
|
+
mp.push({ path: m[1], line: Number(m[2]), code: m[4], message: m[3] });
|
|
170
|
+
}
|
|
171
|
+
if (mp.length > 0)
|
|
172
|
+
return { type: 'type', files: dedupe(mp), raw: combined, exitCode };
|
|
173
|
+
}
|
|
174
|
+
// gcc/clang, vite/webpack
|
|
175
|
+
{
|
|
176
|
+
const gc = [];
|
|
177
|
+
for (const line of combined.split(/\r?\n/)) {
|
|
178
|
+
const m = GCC_LINE.exec(line);
|
|
179
|
+
if (m && m[1] && m[2] && m[3] && m[4])
|
|
180
|
+
gc.push({ path: m[1], line: Number(m[2]), column: Number(m[3]), message: m[4] });
|
|
181
|
+
else if (VITE_ERROR.test(line))
|
|
182
|
+
gc.push({ path: '', message: line.trim() });
|
|
183
|
+
}
|
|
184
|
+
if (gc.length > 0)
|
|
185
|
+
return { type: 'build', files: dedupe(gc), raw: combined, exitCode };
|
|
47
186
|
}
|
|
48
187
|
// Runtime exception.
|
|
49
188
|
const lines = combined.split(/\r?\n/);
|
|
@@ -53,10 +192,36 @@ export function detect(stdout, stderr, exitCode) {
|
|
|
53
192
|
}
|
|
54
193
|
// Generic build.
|
|
55
194
|
if (/error/i.test(combined)) {
|
|
195
|
+
const gf = extractGeneric(combined, 5);
|
|
196
|
+
if (gf.length > 0)
|
|
197
|
+
return { type: 'build', files: gf, raw: combined, exitCode };
|
|
56
198
|
return { type: 'build', files: extractFirstLines(combined, 5), raw: combined, exitCode };
|
|
57
199
|
}
|
|
58
200
|
return { type: 'unknown', files: extractFirstLines(combined, 3), raw: combined, exitCode };
|
|
59
201
|
}
|
|
202
|
+
function dedupe(files) {
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const f of files) {
|
|
206
|
+
const key = `${f.path}:${f.line ?? ''}:${f.column ?? ''}:${f.message}`;
|
|
207
|
+
if (!seen.has(key)) {
|
|
208
|
+
seen.add(key);
|
|
209
|
+
out.push(f);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
function extractGeneric(s, n) {
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const line of s.split(/\r?\n/)) {
|
|
217
|
+
const m = GENERIC_FILE_LINE.exec(line);
|
|
218
|
+
if (m && m[1] && m[2] && m[4])
|
|
219
|
+
out.push({ path: m[1], line: Number(m[2]), column: m[3] ? Number(m[3]) : undefined, message: m[4] });
|
|
220
|
+
if (out.length >= n)
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
60
225
|
function extractFirstLines(s, n) {
|
|
61
226
|
return s
|
|
62
227
|
.split(/\r?\n/)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 6.1 — Verifier registry
|
|
3
|
+
* Detects available verifiers from project heuristics (package.json / Makefile / pyproject / Cargo).
|
|
4
|
+
* Each verifier is a named command that can be run via `verify` engine.
|
|
5
|
+
*/
|
|
6
|
+
export type VerifierKind = 'tests' | 'typecheck' | 'lint' | 'build' | 'format-check' | 'custom';
|
|
7
|
+
export interface Verifier {
|
|
8
|
+
id: VerifierKind;
|
|
9
|
+
label: string;
|
|
10
|
+
command: string;
|
|
11
|
+
priority: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function detectVerifiers(cwd: string): Verifier[];
|
|
14
|
+
export declare function primaryVerifyCommand(cwd: string): string | null;
|
|
15
|
+
export interface VerifySettings {
|
|
16
|
+
commands?: string[];
|
|
17
|
+
onEdit?: boolean;
|
|
18
|
+
afterChange?: boolean;
|
|
19
|
+
beforeDone?: boolean;
|
|
20
|
+
maxRepairs?: number;
|
|
21
|
+
requireVerify?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function resolveVerifySettings(raw: Record<string, unknown> | undefined): VerifySettings;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 6.1 — Verifier registry
|
|
3
|
+
* Detects available verifiers from project heuristics (package.json / Makefile / pyproject / Cargo).
|
|
4
|
+
* Each verifier is a named command that can be run via `verify` engine.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
function exists(p) {
|
|
9
|
+
try {
|
|
10
|
+
return fs.existsSync(p);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readJson(p) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function pm(cwd) {
|
|
26
|
+
if (exists(path.join(cwd, 'pnpm-lock.yaml')))
|
|
27
|
+
return 'pnpm';
|
|
28
|
+
if (exists(path.join(cwd, 'yarn.lock')))
|
|
29
|
+
return 'yarn';
|
|
30
|
+
if (exists(path.join(cwd, 'bun.lockb')))
|
|
31
|
+
return 'bun';
|
|
32
|
+
return 'npm';
|
|
33
|
+
}
|
|
34
|
+
function runCmd(base, cwd) {
|
|
35
|
+
const m = pm(cwd);
|
|
36
|
+
if (base === 'test')
|
|
37
|
+
return `${m} test`;
|
|
38
|
+
if (base === 'build')
|
|
39
|
+
return `${m} run build`;
|
|
40
|
+
if (base === 'lint')
|
|
41
|
+
return `${m} run lint`;
|
|
42
|
+
return `${m} run ${base}`;
|
|
43
|
+
}
|
|
44
|
+
export function detectVerifiers(cwd) {
|
|
45
|
+
const out = [];
|
|
46
|
+
const pkg = readJson(path.join(cwd, 'package.json'));
|
|
47
|
+
const hasPkg = !!pkg;
|
|
48
|
+
// tests — highest priority
|
|
49
|
+
if (hasPkg && pkg?.scripts?.test) {
|
|
50
|
+
out.push({ id: 'tests', label: 'tests', command: runCmd('test', cwd), priority: 10 });
|
|
51
|
+
}
|
|
52
|
+
else if (exists(path.join(cwd, 'pyproject.toml')) || exists(path.join(cwd, 'requirements.txt')) || exists(path.join(cwd, 'setup.py'))) {
|
|
53
|
+
// pytest heuristic — check pyproject for pytest
|
|
54
|
+
out.push({ id: 'tests', label: 'pytest', command: 'pytest', priority: 10 });
|
|
55
|
+
}
|
|
56
|
+
else if (exists(path.join(cwd, 'go.mod'))) {
|
|
57
|
+
out.push({ id: 'tests', label: 'go test', command: 'go test ./...', priority: 10 });
|
|
58
|
+
}
|
|
59
|
+
else if (exists(path.join(cwd, 'Cargo.toml'))) {
|
|
60
|
+
out.push({ id: 'tests', label: 'cargo test', command: 'cargo test', priority: 10 });
|
|
61
|
+
}
|
|
62
|
+
else if (exists(path.join(cwd, 'Makefile')) || exists(path.join(cwd, 'makefile'))) {
|
|
63
|
+
try {
|
|
64
|
+
const mk = fs.readFileSync(path.join(cwd, exists(path.join(cwd, 'Makefile')) ? 'Makefile' : 'makefile'), 'utf-8');
|
|
65
|
+
if (/^test:/m.test(mk))
|
|
66
|
+
out.push({ id: 'tests', label: 'make test', command: 'make test', priority: 10 });
|
|
67
|
+
}
|
|
68
|
+
catch { /* ignore */ }
|
|
69
|
+
}
|
|
70
|
+
// typecheck
|
|
71
|
+
if (exists(path.join(cwd, 'tsconfig.json'))) {
|
|
72
|
+
out.push({ id: 'typecheck', label: 'tsc', command: 'npx tsc --noEmit', priority: 20 });
|
|
73
|
+
}
|
|
74
|
+
else if (exists(path.join(cwd, 'pyproject.toml'))) {
|
|
75
|
+
try {
|
|
76
|
+
const py = fs.readFileSync(path.join(cwd, 'pyproject.toml'), 'utf-8');
|
|
77
|
+
if (py.includes('mypy'))
|
|
78
|
+
out.push({ id: 'typecheck', label: 'mypy', command: 'mypy .', priority: 20 });
|
|
79
|
+
}
|
|
80
|
+
catch { /* ignore */ }
|
|
81
|
+
}
|
|
82
|
+
// lint — check for eslint config or lint script
|
|
83
|
+
if (hasPkg && pkg?.scripts?.lint) {
|
|
84
|
+
out.push({ id: 'lint', label: 'lint', command: runCmd('lint', cwd), priority: 30 });
|
|
85
|
+
}
|
|
86
|
+
else if (exists(path.join(cwd, '.eslintrc.json')) || exists(path.join(cwd, '.eslintrc.js')) || exists(path.join(cwd, '.eslintrc.cjs')) || exists(path.join(cwd, 'eslint.config.js')) || exists(path.join(cwd, 'eslint.config.cjs')) || exists(path.join(cwd, 'eslint.config.mjs'))) {
|
|
87
|
+
out.push({ id: 'lint', label: 'eslint', command: 'npx eslint .', priority: 30 });
|
|
88
|
+
}
|
|
89
|
+
else if (exists(path.join(cwd, 'ruff.toml')) || exists(path.join(cwd, '.ruff.toml')) || exists(path.join(cwd, 'pyproject.toml'))) {
|
|
90
|
+
try {
|
|
91
|
+
if (exists(path.join(cwd, 'pyproject.toml'))) {
|
|
92
|
+
const py2 = fs.readFileSync(path.join(cwd, 'pyproject.toml'), 'utf-8');
|
|
93
|
+
if (py2.includes('[tool.ruff]'))
|
|
94
|
+
out.push({ id: 'lint', label: 'ruff', command: 'ruff check .', priority: 30 });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch { /* ignore */ }
|
|
98
|
+
}
|
|
99
|
+
// build
|
|
100
|
+
if (hasPkg && pkg?.scripts?.build) {
|
|
101
|
+
out.push({ id: 'build', label: 'build', command: runCmd('build', cwd), priority: 40 });
|
|
102
|
+
}
|
|
103
|
+
// format-check
|
|
104
|
+
if (exists(path.join(cwd, '.prettierrc')) || exists(path.join(cwd, '.prettierrc.json')) || exists(path.join(cwd, 'prettier.config.js')) || exists(path.join(cwd, '.prettierrc.cjs'))) {
|
|
105
|
+
out.push({ id: 'format-check', label: 'prettier', command: 'npx prettier --check .', priority: 50 });
|
|
106
|
+
}
|
|
107
|
+
else if (hasPkg && pkg?.scripts?.['format:check']) {
|
|
108
|
+
out.push({ id: 'format-check', label: 'format:check', command: runCmd('format:check', cwd), priority: 50 });
|
|
109
|
+
}
|
|
110
|
+
return out.sort((a, b) => a.priority - b.priority);
|
|
111
|
+
}
|
|
112
|
+
export function primaryVerifyCommand(cwd) {
|
|
113
|
+
const v = detectVerifiers(cwd);
|
|
114
|
+
return v[0]?.command ?? null;
|
|
115
|
+
}
|
|
116
|
+
export function resolveVerifySettings(raw) {
|
|
117
|
+
if (!raw || typeof raw !== 'object')
|
|
118
|
+
return {};
|
|
119
|
+
const v = raw.verify;
|
|
120
|
+
if (!v || typeof v !== 'object')
|
|
121
|
+
return {};
|
|
122
|
+
return {
|
|
123
|
+
commands: Array.isArray(v.commands) ? v.commands : undefined,
|
|
124
|
+
onEdit: typeof v.onEdit === 'boolean' ? v.onEdit : undefined,
|
|
125
|
+
afterChange: typeof v.afterChange === 'boolean' ? v.afterChange : undefined,
|
|
126
|
+
beforeDone: typeof v.beforeDone === 'boolean' ? v.beforeDone : undefined,
|
|
127
|
+
maxRepairs: typeof v.maxRepairs === 'number' ? v.maxRepairs : undefined,
|
|
128
|
+
requireVerify: typeof v.requireVerify === 'boolean' ? v.requireVerify : undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 6.3 — Scoped runs & sanity checks
|
|
3
|
+
* Edited files → related tests (name match), scoped verify command, syntax + import checks.
|
|
4
|
+
*/
|
|
5
|
+
export declare function findRelatedTests(cwd: string, editedFiles: string[]): string[];
|
|
6
|
+
export declare function buildScopedCommand(cwd: string, baseCommand: string, relatedTests: string[]): string | null;
|
|
7
|
+
export declare function runScopedVerify(cwd: string, command: string, timeoutMs?: number): Promise<{
|
|
8
|
+
ok: boolean;
|
|
9
|
+
exitCode: number;
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
}>;
|
|
13
|
+
/** Quick syntax check: try to parse file with node --check or tsc snippet */
|
|
14
|
+
export declare function syntaxCheck(cwd: string, file: string): Promise<{
|
|
15
|
+
ok: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
}>;
|
|
18
|
+
/** Import-path existence check (TS/JS): read file, extract imports, verify targets exist */
|
|
19
|
+
export declare function checkImports(cwd: string, file: string): {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
missing: string[];
|
|
22
|
+
};
|