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.
@@ -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
- const m = TS_LINE.exec(line);
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
- // Test runner.
33
- if (/Test Suites:|FAIL\s|Tests:.*failed|✘|✗/.test(combined)) {
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
- return { type: 'test', files, raw: combined, exitCode };
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
- if (/error\s+at\s+|eslint.*problem|✖/.test(combined)) {
46
- return { type: 'lint', files: extractFirstLines(combined, 5), raw: combined, exitCode };
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
+ };