@delegance/claude-autopilot 1.0.0-alpha.6 → 1.0.0-alpha.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-alpha.7
4
+
5
+ ### Added
6
+
7
+ - **`autopilot hook install`** — writes a `pre-push` git hook that runs `autoregress run` before every push; `hook uninstall` removes it; `hook status` shows current state; `--force` overwrites existing hook
8
+ - **`autoregress diff`** — colored snapshot viewer showing line-by-line JSON diffs between current output and baselines; exits 1 if any diffs found (never modifies baselines — use `update` for that)
9
+ - **`autoregress generate --files <list>`** — explicit comma-separated file list bypasses git detection; generates baselines for any src file on demand
10
+ - **Real baselines** — `tests/snapshots/*.snap.ts` + baselines for `serializer.ts`, `import-scanner.ts`, `impact-selector.ts`, and `sarif.ts` — alpha.6 infrastructure now self-testing via snapshots
11
+
3
12
  ## 1.0.0-alpha.6
4
13
 
5
14
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delegance/claude-autopilot",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.7",
4
4
  "type": "module",
5
5
  "description": "Claude Code automation pipeline: spec → plan → implement → validate → PR",
6
6
  "keywords": ["claude", "autopilot", "ai", "pipeline", "code-review", "cli"],
@@ -2,6 +2,7 @@
2
2
  // scripts/autoregress.ts
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
+ import * as os from 'node:os';
5
6
  import { execSync, spawnSync } from 'node:child_process';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { selectSnapshots } from '../src/snapshots/impact-selector.ts';
@@ -19,6 +20,22 @@ function loadJson<T>(p: string, fallback: T): T {
19
20
  try { return JSON.parse(fs.readFileSync(p, 'utf8')) as T; } catch { return fallback; }
20
21
  }
21
22
 
23
+ export function diffBaselines(baselineJson: string, currentJson: string): string[] {
24
+ if (baselineJson === currentJson) return [];
25
+ const baselineLines = baselineJson.split('\n');
26
+ const currentLines = currentJson.split('\n');
27
+ const lines: string[] = [];
28
+ const maxLen = Math.max(baselineLines.length, currentLines.length);
29
+ for (let i = 0; i < maxLen; i++) {
30
+ const bLine = baselineLines[i];
31
+ const cLine = currentLines[i];
32
+ if (bLine === cLine) continue;
33
+ if (bLine !== undefined) lines.push(`- ${bLine}`);
34
+ if (cLine !== undefined) lines.push(`+ ${cLine}`);
35
+ }
36
+ return lines;
37
+ }
38
+
22
39
  function getChangedFiles(since?: string): string[] | null {
23
40
  try {
24
41
  const base = since
@@ -155,12 +172,15 @@ Write a snapshot test file. Requirements:
155
172
  5. Baseline loading pattern (use slug {slug}):
156
173
  const SLUG = '{slug}';
157
174
  import { fileURLToPath } from 'node:url';
175
+ import * as path from 'node:path';
158
176
  const baselineRaw = process.env.CAPTURE_BASELINE === '1' ? '{}' : fs.readFileSync(fileURLToPath(new URL('./baselines/{slug}.json', import.meta.url)), 'utf8');
159
177
  const baseline = JSON.parse(baselineRaw);
160
178
  const captured: Record<string, unknown> = {};
161
179
  process.on('exit', () => {
162
180
  if (process.env.CAPTURE_BASELINE === '1') {
163
- const p = fileURLToPath(new URL('./baselines/{slug}.json', import.meta.url));
181
+ const p = process.env.AUTOREGRESS_TEMP_BASELINE_DIR
182
+ ? path.join(process.env.AUTOREGRESS_TEMP_BASELINE_DIR, '{slug}.json')
183
+ : fileURLToPath(new URL('./baselines/{slug}.json', import.meta.url));
164
184
  fs.writeFileSync(p, JSON.stringify(captured, null, 2), 'utf8');
165
185
  }
166
186
  });
@@ -175,13 +195,24 @@ async function cmdGenerate(args: string[]): Promise<number> {
175
195
 
176
196
  const sinceIdx = args.indexOf('--since');
177
197
  const since = sinceIdx >= 0 ? args[sinceIdx + 1] : undefined;
178
- const changed = getChangedFiles(since);
179
- if (!changed) { console.error('[autoregress generate] could not determine changed files'); return 1; }
180
-
181
- const srcFiles = changed.filter(f => f.startsWith('src/') && f.endsWith('.ts'));
182
- if (srcFiles.length === 0) {
183
- console.log('[autoregress generate] no src/*.ts files changed — nothing to generate');
184
- return 0;
198
+ const filesIdx = args.indexOf('--files');
199
+ const filesArg = filesIdx >= 0 ? args[filesIdx + 1] : undefined;
200
+
201
+ let srcFiles: string[];
202
+ if (filesArg) {
203
+ srcFiles = filesArg.split(',').map(f => f.trim()).filter(f => f.startsWith('src/') && f.endsWith('.ts'));
204
+ if (srcFiles.length === 0) {
205
+ console.error('[autoregress generate] --files must contain at least one src/*.ts path');
206
+ return 1;
207
+ }
208
+ } else {
209
+ const changed = getChangedFiles(since);
210
+ if (!changed) { console.error('[autoregress generate] could not determine changed files'); return 1; }
211
+ srcFiles = changed.filter(f => f.startsWith('src/') && f.endsWith('.ts'));
212
+ if (srcFiles.length === 0) {
213
+ console.log('[autoregress generate] no src/*.ts files changed — nothing to generate');
214
+ return 0;
215
+ }
185
216
  }
186
217
 
187
218
  console.log(`[autoregress generate] generating snapshots for ${srcFiles.length} file(s)`);
@@ -257,12 +288,98 @@ async function cmdGenerate(args: string[]): Promise<number> {
257
288
  return 0;
258
289
  }
259
290
 
260
- const [,, subcmd, ...rest] = process.argv;
261
- switch (subcmd) {
262
- case 'run': process.exit(cmdRun(rest)); break;
263
- case 'update': process.exit(cmdUpdate(rest)); break;
264
- case 'generate': process.exit(await cmdGenerate(rest)); break;
265
- default:
266
- console.error(`[autoregress] unknown subcommand: ${subcmd ?? '(none)'}`);
267
- process.exit(1);
291
+ function cmdDiff(args: string[]): number {
292
+ const runAll = args.includes('--all');
293
+ const snapIdx = args.indexOf('--snapshot');
294
+ const slug = snapIdx >= 0 ? args[snapIdx + 1] : undefined;
295
+ const sinceIdx = args.indexOf('--since');
296
+ const since = sinceIdx >= 0 ? args[sinceIdx + 1] : undefined;
297
+
298
+ const index = loadJson<Record<string, string[]>>(INDEX_PATH, {});
299
+ const importMap = loadJson<Record<string, string[]>>(IMPORT_MAP_PATH, {});
300
+ const snapFiles = slug
301
+ ? [path.join('tests', 'snapshots', `${slug}.snap.ts`)]
302
+ : allSnapFiles();
303
+
304
+ let selected: string[];
305
+ if (runAll || slug || snapFiles.length === 0) {
306
+ selected = snapFiles;
307
+ } else {
308
+ const changed = getChangedFiles(since);
309
+ selected = changed ? selectSnapshots(changed, snapFiles, index, importMap).selected : snapFiles;
310
+ }
311
+
312
+ if (selected.length === 0) {
313
+ console.log('[autoregress diff] no snapshots to diff');
314
+ return 0;
315
+ }
316
+
317
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
318
+ const red = useColor ? '\x1b[31m' : '';
319
+ const green = useColor ? '\x1b[32m' : '';
320
+ const reset = useColor ? '\x1b[0m' : '';
321
+
322
+ let changedCount = 0;
323
+ for (const snap of selected) {
324
+ const slug_ = path.basename(snap, '.snap.ts');
325
+ const baselinePath = path.join(BASELINES_DIR, `${slug_}.json`);
326
+
327
+ if (!fs.existsSync(baselinePath)) {
328
+ console.log(` ${snap} — ${red}no baseline${reset}`);
329
+ continue;
330
+ }
331
+
332
+ const tmpBaselinesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ar-diff-'));
333
+ const tmpBaselinePath = path.join(tmpBaselinesDir, `${slug_}.json`);
334
+ fs.copyFileSync(baselinePath, tmpBaselinePath);
335
+
336
+ const absSnap = path.join(ROOT, snap);
337
+ const captureResult = spawnSync('node', ['--test', '--import', 'tsx', absSnap], {
338
+ stdio: ['ignore', 'pipe', 'pipe'],
339
+ cwd: ROOT,
340
+ env: { ...process.env, CAPTURE_BASELINE: '1', AUTOREGRESS_TEMP_BASELINE_DIR: tmpBaselinesDir },
341
+ });
342
+
343
+ const baselineJson = fs.readFileSync(baselinePath, 'utf8');
344
+ const captureOk = fs.existsSync(tmpBaselinePath);
345
+ const currentJson = captureOk ? fs.readFileSync(tmpBaselinePath, 'utf8') : null;
346
+ if (!captureOk && captureResult.status !== 0) {
347
+ const stderr = captureResult.stderr?.toString().trim();
348
+ if (stderr) console.error(` ${stderr.slice(0, 120)}`);
349
+ }
350
+ fs.rmSync(tmpBaselinesDir, { recursive: true, force: true });
351
+
352
+ if (!currentJson) {
353
+ console.log(` ${snap} — ${red}capture failed${reset}`);
354
+ continue;
355
+ }
356
+
357
+ const diffLines = diffBaselines(baselineJson, currentJson);
358
+ if (diffLines.length === 0) {
359
+ console.log(` ${snap} — ${green}✓ no changes${reset}`);
360
+ } else {
361
+ changedCount++;
362
+ console.log(` ${snap}`);
363
+ for (const line of diffLines) {
364
+ if (line.startsWith('-')) console.log(` ${red}${line}${reset}`);
365
+ else if (line.startsWith('+')) console.log(` ${green}${line}${reset}`);
366
+ else console.log(` ${line}`);
367
+ }
368
+ }
369
+ }
370
+
371
+ return changedCount > 0 ? 1 : 0;
372
+ }
373
+
374
+ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? '')) {
375
+ const [,, subcmd, ...rest] = process.argv;
376
+ switch (subcmd) {
377
+ case 'run': process.exit(cmdRun(rest)); break;
378
+ case 'update': process.exit(cmdUpdate(rest)); break;
379
+ case 'generate': process.exit(await cmdGenerate(rest)); break;
380
+ case 'diff': process.exit(cmdDiff(rest)); break;
381
+ default:
382
+ console.error(`[autoregress] unknown subcommand: ${subcmd ?? '(none)'}`);
383
+ process.exit(1);
384
+ }
268
385
  }
@@ -0,0 +1,79 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ const HOOK_CONTENT = `#!/bin/sh
5
+ # autopilot pre-push hook — runs impact-selected snapshots before push
6
+ npx tsx scripts/autoregress.ts run
7
+ `;
8
+
9
+ function findGitDir(cwd: string): string | null {
10
+ let dir = path.resolve(cwd);
11
+ for (let i = 0; i < 10; i++) {
12
+ const candidate = path.join(dir, '.git');
13
+ if (fs.existsSync(candidate)) {
14
+ const stat = fs.statSync(candidate);
15
+ if (stat.isDirectory()) return candidate;
16
+ if (stat.isFile()) {
17
+ const content = fs.readFileSync(candidate, 'utf8');
18
+ const match = content.match(/^gitdir:\s*(.+)/m);
19
+ if (match) return path.resolve(dir, match[1]!.trim());
20
+ }
21
+ }
22
+ const parent = path.dirname(dir);
23
+ if (parent === dir) return null;
24
+ dir = parent;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ export async function runHook(
30
+ sub: string,
31
+ options: { cwd?: string; force?: boolean } = {},
32
+ ): Promise<number> {
33
+ const cwd = options.cwd ?? process.cwd();
34
+ const gitDir = findGitDir(cwd);
35
+
36
+ if (!gitDir) {
37
+ console.error('[hook] not inside a git repository');
38
+ return 1;
39
+ }
40
+
41
+ const hookPath = path.join(gitDir, 'hooks', 'pre-push');
42
+
43
+ switch (sub) {
44
+ case 'install': {
45
+ if (fs.existsSync(hookPath) && !options.force) {
46
+ console.error(`[hook] pre-push hook already exists at ${hookPath}`);
47
+ console.error(' Use --force to overwrite.');
48
+ return 1;
49
+ }
50
+ fs.mkdirSync(path.dirname(hookPath), { recursive: true });
51
+ fs.writeFileSync(hookPath, HOOK_CONTENT, 'utf8');
52
+ fs.chmodSync(hookPath, 0o755);
53
+ console.log(`[hook] installed pre-push hook at ${hookPath}`);
54
+ return 0;
55
+ }
56
+ case 'uninstall': {
57
+ if (!fs.existsSync(hookPath)) {
58
+ console.log('[hook] no pre-push hook installed');
59
+ return 0;
60
+ }
61
+ fs.rmSync(hookPath);
62
+ console.log(`[hook] removed ${hookPath}`);
63
+ return 0;
64
+ }
65
+ case 'status': {
66
+ if (fs.existsSync(hookPath)) {
67
+ console.log(`[hook] installed at ${hookPath}`);
68
+ console.log(fs.readFileSync(hookPath, 'utf8'));
69
+ } else {
70
+ console.log('[hook] not installed');
71
+ }
72
+ return 0;
73
+ }
74
+ default:
75
+ console.error(`[hook] unknown subcommand: ${sub}`);
76
+ console.error('Usage: autopilot hook <install|uninstall|status> [--force]');
77
+ return 1;
78
+ }
79
+ }
package/src/cli/index.ts CHANGED
@@ -118,6 +118,15 @@ switch (subcommand) {
118
118
  break;
119
119
  }
120
120
 
121
+ case 'hook': {
122
+ const { runHook } = await import('./hook.ts');
123
+ const hookSub = args[1] ?? 'status';
124
+ const force = boolFlag('force');
125
+ const code = await runHook(hookSub, { force });
126
+ process.exit(code);
127
+ break;
128
+ }
129
+
121
130
  default:
122
131
  console.error(`\x1b[31m[autopilot] Unknown subcommand: "${subcommand}"\x1b[0m`);
123
132
  printUsage();
@@ -0,0 +1,210 @@
1
+ {
2
+ "normalizeSarifUri handles relative absolute and parent escaping paths": {
3
+ "rel": "src/index.ts",
4
+ "absInside": "lib/file.ts",
5
+ "absOutside": "/other/place/file.ts",
6
+ "dotted": "scripts/run.ts",
7
+ "windows": "folder/nested/file.ts"
8
+ },
9
+ "toSarif emits schema tool metadata rules and mapped severities": {
10
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
11
+ "version": "2.1.0",
12
+ "runs": [
13
+ {
14
+ "tool": {
15
+ "driver": {
16
+ "name": "claude-autopilot",
17
+ "version": "9.9.9",
18
+ "informationUri": "https://github.com/axledbetter/claude-autopilot",
19
+ "rules": [
20
+ {
21
+ "id": "no-secrets",
22
+ "name": "no-secrets",
23
+ "shortDescription": {
24
+ "text": "no-secrets"
25
+ }
26
+ },
27
+ {
28
+ "id": "style-warning",
29
+ "name": "style-warning",
30
+ "shortDescription": {
31
+ "text": "style-warning"
32
+ }
33
+ },
34
+ {
35
+ "id": "info-note",
36
+ "name": "info-note",
37
+ "shortDescription": {
38
+ "text": "info-note"
39
+ }
40
+ }
41
+ ]
42
+ }
43
+ },
44
+ "results": [
45
+ {
46
+ "ruleId": "no-secrets",
47
+ "level": "error",
48
+ "message": {
49
+ "text": "Hardcoded secret detected"
50
+ },
51
+ "locations": [
52
+ {
53
+ "physicalLocation": {
54
+ "artifactLocation": {
55
+ "uri": "src/a.ts",
56
+ "uriBaseId": "%SRCROOT%"
57
+ },
58
+ "region": {
59
+ "startLine": 10
60
+ }
61
+ }
62
+ }
63
+ ],
64
+ "fixes": [
65
+ {
66
+ "description": {
67
+ "text": "Use environment variables"
68
+ }
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ "ruleId": "style-warning",
74
+ "level": "warning",
75
+ "message": {
76
+ "text": "Potentially confusing naming"
77
+ },
78
+ "locations": [
79
+ {
80
+ "physicalLocation": {
81
+ "artifactLocation": {
82
+ "uri": "src/b.ts",
83
+ "uriBaseId": "%SRCROOT%"
84
+ },
85
+ "region": {
86
+ "startLine": 3
87
+ }
88
+ }
89
+ }
90
+ ]
91
+ },
92
+ {
93
+ "ruleId": "info-note",
94
+ "level": "note",
95
+ "message": {
96
+ "text": "Informational hint"
97
+ },
98
+ "locations": [
99
+ {
100
+ "physicalLocation": {
101
+ "artifactLocation": {
102
+ "uri": "src/c.ts",
103
+ "uriBaseId": "%SRCROOT%"
104
+ }
105
+ }
106
+ }
107
+ ]
108
+ },
109
+ {
110
+ "ruleId": "no-secrets",
111
+ "level": "error",
112
+ "message": {
113
+ "text": "Another secret"
114
+ },
115
+ "locations": [
116
+ {
117
+ "physicalLocation": {
118
+ "artifactLocation": {
119
+ "uri": "src/d.ts",
120
+ "uriBaseId": "%SRCROOT%"
121
+ },
122
+ "region": {
123
+ "startLine": 20
124
+ }
125
+ }
126
+ }
127
+ ]
128
+ }
129
+ ]
130
+ }
131
+ ]
132
+ },
133
+ "toSarif includes optional fix and region only when provided": {
134
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
135
+ "version": "2.1.0",
136
+ "runs": [
137
+ {
138
+ "tool": {
139
+ "driver": {
140
+ "name": "claude-autopilot",
141
+ "version": "1.2.3",
142
+ "informationUri": "https://github.com/axledbetter/claude-autopilot",
143
+ "rules": [
144
+ {
145
+ "id": "cat-a",
146
+ "name": "cat-a",
147
+ "shortDescription": {
148
+ "text": "cat-a"
149
+ }
150
+ },
151
+ {
152
+ "id": "cat-b",
153
+ "name": "cat-b",
154
+ "shortDescription": {
155
+ "text": "cat-b"
156
+ }
157
+ }
158
+ ]
159
+ }
160
+ },
161
+ "results": [
162
+ {
163
+ "ruleId": "cat-a",
164
+ "level": "warning",
165
+ "message": {
166
+ "text": "Has line and suggestion"
167
+ },
168
+ "locations": [
169
+ {
170
+ "physicalLocation": {
171
+ "artifactLocation": {
172
+ "uri": "src/with.ts",
173
+ "uriBaseId": "%SRCROOT%"
174
+ },
175
+ "region": {
176
+ "startLine": 42
177
+ }
178
+ }
179
+ }
180
+ ],
181
+ "fixes": [
182
+ {
183
+ "description": {
184
+ "text": "Apply quick fix"
185
+ }
186
+ }
187
+ ]
188
+ },
189
+ {
190
+ "ruleId": "cat-b",
191
+ "level": "warning",
192
+ "message": {
193
+ "text": "No line no suggestion"
194
+ },
195
+ "locations": [
196
+ {
197
+ "physicalLocation": {
198
+ "artifactLocation": {
199
+ "uri": "src/without.ts",
200
+ "uriBaseId": "%SRCROOT%"
201
+ }
202
+ }
203
+ }
204
+ ]
205
+ }
206
+ ]
207
+ }
208
+ ]
209
+ }
210
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "volume override triggers full run": {
3
+ "selected": [
4
+ "a.snap",
5
+ "b.snap",
6
+ "c.snap"
7
+ ],
8
+ "fullRun": true,
9
+ "reason": "volume override (>10 files changed)"
10
+ },
11
+ "high-impact path match triggers full run": {
12
+ "selected": [
13
+ "x.snap",
14
+ "y.snap"
15
+ ],
16
+ "fullRun": true,
17
+ "reason": "high-impact path matched: src/core/pipeline/runner.ts"
18
+ },
19
+ "selects snapshots via direct and importer mapping": {
20
+ "selected": [
21
+ "snap-a.snap",
22
+ "snap-b.snap"
23
+ ],
24
+ "fullRun": false,
25
+ "reason": "2 snapshot(s) selected"
26
+ },
27
+ "returns no matches reason when nothing selected": {
28
+ "selected": [],
29
+ "fullRun": false,
30
+ "reason": "no snapshots matched changed files"
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "captures relative import and export-from edges": {
3
+ "feature/util.ts": [
4
+ "feature/index.ts"
5
+ ]
6
+ },
7
+ "ignores non-relative imports and deduplicates importers per target": {
8
+ "lib/shared.ts": [
9
+ "a.ts",
10
+ "b.ts"
11
+ ]
12
+ },
13
+ "excludes imports that resolve outside srcDir": {
14
+ "pkg/inside.ts": [
15
+ "pkg/consumer.ts"
16
+ ],
17
+ "outside.ts": [
18
+ "pkg/consumer.ts"
19
+ ]
20
+ }
21
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "normalizes timestamps uuids and cwd-prefixed paths": {
3
+ "id": "<uuid>",
4
+ "path": "src/index.ts",
5
+ "ts": "<timestamp>",
6
+ "untouched": "/other/place/file.ts"
7
+ },
8
+ "recursively normalizes arrays and nested objects": {
9
+ "items": [
10
+ "<timestamp>",
11
+ {
12
+ "file": "a/b.txt",
13
+ "uid": "<uuid>"
14
+ },
15
+ [
16
+ "plain",
17
+ "c/d.ts"
18
+ ]
19
+ ]
20
+ },
21
+ "sorts object keys deterministically at all levels": {
22
+ "a": {
23
+ "b": 2,
24
+ "c": 3,
25
+ "d": 4
26
+ },
27
+ "m": [
28
+ {
29
+ "x": 1,
30
+ "y": 2
31
+ },
32
+ {
33
+ "a": 1,
34
+ "b": 2
35
+ }
36
+ ],
37
+ "z": 1
38
+ }
39
+ }
@@ -1 +1,138 @@
1
- {}
1
+ {
2
+ "src/core/errors.ts": [
3
+ "src/adapters/loader.ts",
4
+ "src/adapters/review-engine/codex.ts",
5
+ "src/adapters/vcs-host/github.ts",
6
+ "src/core/config/loader.ts",
7
+ "src/core/config/preset-resolver.ts",
8
+ "src/core/runtime/lock.ts",
9
+ "src/core/runtime/state.ts",
10
+ "src/core/shell.ts"
11
+ ],
12
+ "src/adapters/base.ts": [
13
+ "src/adapters/loader.ts",
14
+ "src/adapters/migration-runner/supabase.ts",
15
+ "src/adapters/migration-runner/types.ts",
16
+ "src/adapters/review-bot-parser/declarative-base.ts",
17
+ "src/adapters/review-bot-parser/types.ts",
18
+ "src/adapters/review-engine/codex.ts",
19
+ "src/adapters/review-engine/types.ts",
20
+ "src/adapters/vcs-host/github.ts",
21
+ "src/adapters/vcs-host/types.ts",
22
+ "src/core/cache/cached-engine.ts"
23
+ ],
24
+ "src/core/shell.ts": [
25
+ "src/adapters/migration-runner/supabase.ts",
26
+ "src/adapters/vcs-host/github.ts",
27
+ "src/cli/preflight.ts",
28
+ "src/core/git/touched-files.ts"
29
+ ],
30
+ "src/adapters/migration-runner/types.ts": [
31
+ "src/adapters/migration-runner/supabase.ts"
32
+ ],
33
+ "src/adapters/review-bot-parser/declarative-base.ts": [
34
+ "src/adapters/review-bot-parser/cursor.ts"
35
+ ],
36
+ "src/core/findings/types.ts": [
37
+ "src/adapters/review-bot-parser/declarative-base.ts",
38
+ "src/adapters/review-bot-parser/types.ts",
39
+ "src/adapters/review-engine/codex.ts",
40
+ "src/adapters/review-engine/types.ts",
41
+ "src/core/findings/dedup.ts",
42
+ "src/core/phases/static-rules.ts",
43
+ "src/core/phases/tests.ts",
44
+ "src/core/pipeline/review-phase.ts",
45
+ "src/core/pipeline/run.ts",
46
+ "src/formatters/github-annotations.ts",
47
+ "src/formatters/sarif.ts"
48
+ ],
49
+ "src/adapters/vcs-host/types.ts": [
50
+ "src/adapters/review-bot-parser/declarative-base.ts",
51
+ "src/adapters/review-bot-parser/types.ts",
52
+ "src/adapters/vcs-host/github.ts"
53
+ ],
54
+ "src/adapters/review-bot-parser/types.ts": [
55
+ "src/adapters/review-bot-parser/declarative-base.ts"
56
+ ],
57
+ "src/adapters/review-engine/types.ts": [
58
+ "src/adapters/review-engine/codex.ts",
59
+ "src/cli/run.ts",
60
+ "src/cli/watch.ts",
61
+ "src/core/cache/cached-engine.ts",
62
+ "src/core/cache/review-cache.ts",
63
+ "src/core/chunking/index.ts",
64
+ "src/core/pipeline/review-phase.ts",
65
+ "src/core/pipeline/run.ts"
66
+ ],
67
+ "src/cli/init.ts": [
68
+ "src/cli/index.ts"
69
+ ],
70
+ "src/cli/run.ts": [
71
+ "src/cli/index.ts"
72
+ ],
73
+ "src/cli/watch.ts": [
74
+ "src/cli/index.ts"
75
+ ],
76
+ "src/core/config/loader.ts": [
77
+ "src/cli/run.ts",
78
+ "src/cli/watch.ts",
79
+ "src/core/config/preset-resolver.ts"
80
+ ],
81
+ "src/core/config/preset-resolver.ts": [
82
+ "src/cli/run.ts",
83
+ "src/cli/watch.ts"
84
+ ],
85
+ "src/adapters/loader.ts": [
86
+ "src/cli/run.ts",
87
+ "src/cli/watch.ts"
88
+ ],
89
+ "src/core/pipeline/run.ts": [
90
+ "src/cli/run.ts",
91
+ "src/cli/watch.ts",
92
+ "src/formatters/sarif.ts"
93
+ ],
94
+ "src/core/git/touched-files.ts": [
95
+ "src/cli/run.ts"
96
+ ],
97
+ "src/core/config/types.ts": [
98
+ "src/cli/run.ts",
99
+ "src/cli/watch.ts",
100
+ "src/core/chunking/index.ts",
101
+ "src/core/config/loader.ts",
102
+ "src/core/config/preset-resolver.ts",
103
+ "src/core/pipeline/review-phase.ts",
104
+ "src/core/pipeline/run.ts"
105
+ ],
106
+ "src/formatters/sarif.ts": [
107
+ "src/cli/run.ts",
108
+ "src/formatters/index.ts"
109
+ ],
110
+ "src/formatters/github-annotations.ts": [
111
+ "src/cli/run.ts",
112
+ "src/formatters/index.ts"
113
+ ],
114
+ "src/core/cache/review-cache.ts": [
115
+ "src/core/cache/cached-engine.ts"
116
+ ],
117
+ "src/core/config/schema.ts": [
118
+ "src/core/config/loader.ts"
119
+ ],
120
+ "src/core/logging/redaction.ts": [
121
+ "src/core/logging/ndjson-writer.ts"
122
+ ],
123
+ "src/core/findings/dedup.ts": [
124
+ "src/core/phases/static-rules.ts"
125
+ ],
126
+ "src/core/chunking/index.ts": [
127
+ "src/core/pipeline/review-phase.ts"
128
+ ],
129
+ "src/core/phases/static-rules.ts": [
130
+ "src/core/pipeline/run.ts"
131
+ ],
132
+ "src/core/phases/tests.ts": [
133
+ "src/core/pipeline/run.ts"
134
+ ],
135
+ "src/core/pipeline/review-phase.ts": [
136
+ "src/core/pipeline/run.ts"
137
+ ]
138
+ }
@@ -1 +1,14 @@
1
- {}
1
+ {
2
+ "tests/snapshots/src-formatters-sarif.snap.ts": [
3
+ "src/formatters/sarif.ts"
4
+ ],
5
+ "tests/snapshots/src-snapshots-impact-selector.snap.ts": [
6
+ "src/snapshots/impact-selector.ts"
7
+ ],
8
+ "tests/snapshots/src-snapshots-import-scanner.snap.ts": [
9
+ "src/snapshots/import-scanner.ts"
10
+ ],
11
+ "tests/snapshots/src-snapshots-serializer.snap.ts": [
12
+ "src/snapshots/serializer.ts"
13
+ ]
14
+ }
@@ -0,0 +1,132 @@
1
+ // @snapshot-for: src/formatters/sarif.ts
2
+ // @generated-at: 2026-04-21T17:42:06.431Z
3
+ // @source-commit: d207869
4
+ // @generator-version: 1.0.0-alpha.6
5
+
6
+ import fs from 'node:fs';
7
+ import { describe, it } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { fileURLToPath } from 'node:url';
10
+ import * as path from 'node:path';
11
+
12
+ import { normalizeSarifUri, toSarif } from '../../src/formatters/sarif.ts';
13
+ import { normalizeSnapshot } from '../../src/snapshots/serializer.ts';
14
+
15
+ const SLUG = 'src-formatters-sarif';
16
+ const baselineRaw =
17
+ process.env.CAPTURE_BASELINE === '1'
18
+ ? '{}'
19
+ : fs.readFileSync(
20
+ fileURLToPath(new URL('./baselines/src-formatters-sarif.json', import.meta.url)),
21
+ 'utf8',
22
+ );
23
+ const baseline = JSON.parse(baselineRaw);
24
+ const captured: Record<string, unknown> = {};
25
+ process.on('exit', () => {
26
+ if (process.env.CAPTURE_BASELINE === '1') {
27
+ const p = process.env.AUTOREGRESS_TEMP_BASELINE_DIR
28
+ ? path.join(process.env.AUTOREGRESS_TEMP_BASELINE_DIR, 'src-formatters-sarif.json')
29
+ : fileURLToPath(new URL('./baselines/src-formatters-sarif.json', import.meta.url));
30
+ fs.writeFileSync(p, JSON.stringify(captured, null, 2), 'utf8');
31
+ }
32
+ });
33
+
34
+ describe(SLUG, () => {
35
+ it('normalizeSarifUri handles relative absolute and parent escaping paths', () => {
36
+ const cwd = '/repo';
37
+ const result = {
38
+ rel: normalizeSarifUri('src/index.ts', cwd),
39
+ absInside: normalizeSarifUri('/repo/lib/file.ts', cwd),
40
+ absOutside: normalizeSarifUri('/other/place/file.ts', cwd),
41
+ dotted: normalizeSarifUri('./scripts/run.ts', cwd),
42
+ windows: normalizeSarifUri('folder\\nested\\file.ts', cwd),
43
+ };
44
+
45
+ if (process.env.CAPTURE_BASELINE === '1') {
46
+ captured['normalizeSarifUri handles relative absolute and parent escaping paths'] = result;
47
+ return;
48
+ }
49
+ assert.equal(
50
+ normalizeSnapshot(result),
51
+ normalizeSnapshot(baseline['normalizeSarifUri handles relative absolute and parent escaping paths']),
52
+ );
53
+ });
54
+
55
+ it('toSarif emits schema tool metadata rules and mapped severities', () => {
56
+ const runResult = {
57
+ allFindings: [
58
+ {
59
+ category: 'no-secrets',
60
+ severity: 'critical',
61
+ message: 'Hardcoded secret detected',
62
+ file: '/repo/src/a.ts',
63
+ line: 10,
64
+ suggestion: 'Use environment variables',
65
+ },
66
+ {
67
+ category: 'style-warning',
68
+ severity: 'warning',
69
+ message: 'Potentially confusing naming',
70
+ file: '/repo/src/b.ts',
71
+ line: 3,
72
+ },
73
+ {
74
+ category: 'info-note',
75
+ severity: 'info',
76
+ message: 'Informational hint',
77
+ file: '/repo/src/c.ts',
78
+ },
79
+ {
80
+ category: 'no-secrets',
81
+ severity: 'critical',
82
+ message: 'Another secret',
83
+ file: '/repo/src/d.ts',
84
+ line: 20,
85
+ },
86
+ ],
87
+ } as any;
88
+
89
+ const result = toSarif(runResult, { toolVersion: '9.9.9', cwd: '/repo' });
90
+
91
+ if (process.env.CAPTURE_BASELINE === '1') {
92
+ captured['toSarif emits schema tool metadata rules and mapped severities'] = result;
93
+ return;
94
+ }
95
+ assert.equal(
96
+ normalizeSnapshot(result),
97
+ normalizeSnapshot(baseline['toSarif emits schema tool metadata rules and mapped severities']),
98
+ );
99
+ });
100
+
101
+ it('toSarif includes optional fix and region only when provided', () => {
102
+ const runResult = {
103
+ allFindings: [
104
+ {
105
+ category: 'cat-a',
106
+ severity: 'warning',
107
+ message: 'Has line and suggestion',
108
+ file: 'src/with.ts',
109
+ line: 42,
110
+ suggestion: 'Apply quick fix',
111
+ },
112
+ {
113
+ category: 'cat-b',
114
+ severity: 'warning',
115
+ message: 'No line no suggestion',
116
+ file: 'src/without.ts',
117
+ },
118
+ ],
119
+ } as any;
120
+
121
+ const result = toSarif(runResult, { toolVersion: '1.2.3', cwd: '/repo' });
122
+
123
+ if (process.env.CAPTURE_BASELINE === '1') {
124
+ captured['toSarif includes optional fix and region only when provided'] = result;
125
+ return;
126
+ }
127
+ assert.equal(
128
+ normalizeSnapshot(result),
129
+ normalizeSnapshot(baseline['toSarif includes optional fix and region only when provided']),
130
+ );
131
+ });
132
+ });
@@ -0,0 +1,95 @@
1
+ // @snapshot-for: src/snapshots/impact-selector.ts
2
+ // @generated-at: 2026-04-21T17:42:06.431Z
3
+ // @source-commit: d207869
4
+ // @generator-version: 1.0.0-alpha.6
5
+
6
+ import fs from 'node:fs';
7
+ import { describe, it } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { fileURLToPath } from 'node:url';
10
+ import * as path from 'node:path';
11
+
12
+ import { selectSnapshots } from '../../src/snapshots/impact-selector.ts';
13
+ import { normalizeSnapshot } from '../../src/snapshots/serializer.ts';
14
+
15
+ const SLUG = 'src-snapshots-impact-selector';
16
+ const baselineRaw = process.env.CAPTURE_BASELINE === '1' ? '{}' : fs.readFileSync(fileURLToPath(new URL('./baselines/src-snapshots-impact-selector.json', import.meta.url)), 'utf8');
17
+ const baseline = JSON.parse(baselineRaw);
18
+ const captured: Record<string, unknown> = {};
19
+
20
+ process.on('exit', () => {
21
+ if (process.env.CAPTURE_BASELINE === '1') {
22
+ const p = process.env.AUTOREGRESS_TEMP_BASELINE_DIR
23
+ ? path.join(process.env.AUTOREGRESS_TEMP_BASELINE_DIR, 'src-snapshots-impact-selector.json')
24
+ : fileURLToPath(new URL('./baselines/src-snapshots-impact-selector.json', import.meta.url));
25
+ fs.writeFileSync(p, JSON.stringify(captured, null, 2), 'utf8');
26
+ }
27
+ });
28
+
29
+ describe(SLUG, () => {
30
+ it('volume override triggers full run', () => {
31
+ const changedFiles = Array.from({ length: 11 }, (_, i) => `src/feature/file-${i}.ts`);
32
+ const allSnapshotFiles = ['a.snap', 'b.snap', 'c.snap'];
33
+
34
+ const result = selectSnapshots(changedFiles, allSnapshotFiles, {}, {}, {});
35
+
36
+ if (process.env.CAPTURE_BASELINE === '1') {
37
+ captured['volume override triggers full run'] = result;
38
+ return;
39
+ }
40
+ assert.equal(normalizeSnapshot(result), normalizeSnapshot(baseline['volume override triggers full run']));
41
+ });
42
+
43
+ it('high-impact path match triggers full run', () => {
44
+ const changedFiles = ['src/core/pipeline/runner.ts'];
45
+ const allSnapshotFiles = ['x.snap', 'y.snap'];
46
+
47
+ const result = selectSnapshots(changedFiles, allSnapshotFiles, {}, {}, {});
48
+
49
+ if (process.env.CAPTURE_BASELINE === '1') {
50
+ captured['high-impact path match triggers full run'] = result;
51
+ return;
52
+ }
53
+ assert.equal(normalizeSnapshot(result), normalizeSnapshot(baseline['high-impact path match triggers full run']));
54
+ });
55
+
56
+ it('selects snapshots via direct and importer mapping', () => {
57
+ const changedFiles = ['src/features/a.ts'];
58
+ const allSnapshotFiles = ['snap-a.snap', 'snap-b.snap', 'snap-c.snap'];
59
+ const index = {
60
+ 'snap-a.snap': ['src/features/a.ts'],
61
+ 'snap-b.snap': ['src/features/b.ts'],
62
+ 'snap-c.snap': ['src/features/c.ts'],
63
+ };
64
+ const importMap = {
65
+ 'src/features/a.ts': ['src/features/b.ts'],
66
+ };
67
+
68
+ const result = selectSnapshots(changedFiles, allSnapshotFiles, index, importMap, {
69
+ highImpactPatterns: [],
70
+ volumeThreshold: 50,
71
+ });
72
+
73
+ if (process.env.CAPTURE_BASELINE === '1') {
74
+ captured['selects snapshots via direct and importer mapping'] = result;
75
+ return;
76
+ }
77
+ assert.equal(normalizeSnapshot(result), normalizeSnapshot(baseline['selects snapshots via direct and importer mapping']));
78
+ });
79
+
80
+ it('returns no matches reason when nothing selected', () => {
81
+ const result = selectSnapshots(
82
+ ['src/unknown/file.ts'],
83
+ ['only.snap'],
84
+ { 'only.snap': ['src/other/file.ts'] },
85
+ {},
86
+ { highImpactPatterns: [], volumeThreshold: 50 },
87
+ );
88
+
89
+ if (process.env.CAPTURE_BASELINE === '1') {
90
+ captured['returns no matches reason when nothing selected'] = result;
91
+ return;
92
+ }
93
+ assert.equal(normalizeSnapshot(result), normalizeSnapshot(baseline['returns no matches reason when nothing selected']));
94
+ });
95
+ });
@@ -0,0 +1,126 @@
1
+ // @snapshot-for: src/snapshots/import-scanner.ts
2
+ // @generated-at: 2026-04-21T17:42:06.431Z
3
+ // @source-commit: d207869
4
+ // @generator-version: 1.0.0-alpha.6
5
+
6
+ import fs from 'node:fs';
7
+ import { describe, it } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { fileURLToPath } from 'node:url';
10
+ import * as path from 'node:path';
11
+
12
+ import { buildImportMap } from '../../src/snapshots/import-scanner.ts';
13
+ import { normalizeSnapshot } from '../../src/snapshots/serializer.ts';
14
+
15
+ const SLUG = 'src-snapshots-import-scanner';
16
+ void SLUG;
17
+ const baselineRaw =
18
+ process.env.CAPTURE_BASELINE === '1'
19
+ ? '{}'
20
+ : fs.readFileSync(
21
+ fileURLToPath(new URL('./baselines/src-snapshots-import-scanner.json', import.meta.url)),
22
+ 'utf8',
23
+ );
24
+ const baseline = JSON.parse(baselineRaw);
25
+ const captured: Record<string, unknown> = {};
26
+ process.on('exit', () => {
27
+ if (process.env.CAPTURE_BASELINE === '1') {
28
+ const p = process.env.AUTOREGRESS_TEMP_BASELINE_DIR
29
+ ? path.join(process.env.AUTOREGRESS_TEMP_BASELINE_DIR, 'src-snapshots-import-scanner.json')
30
+ : fileURLToPath(new URL('./baselines/src-snapshots-import-scanner.json', import.meta.url));
31
+ fs.writeFileSync(p, JSON.stringify(captured, null, 2), 'utf8');
32
+ }
33
+ });
34
+
35
+ function mkTempDir(name: string): string {
36
+ return fs.mkdtempSync(path.join(process.cwd(), `.tmp-${name}-`));
37
+ }
38
+
39
+ describe('buildImportMap snapshots', () => {
40
+ it('captures relative import and export-from edges', () => {
41
+ const dir = mkTempDir('import-scanner-basic');
42
+ fs.mkdirSync(path.join(dir, 'feature'), { recursive: true });
43
+
44
+ fs.writeFileSync(
45
+ path.join(dir, 'feature', 'util.ts'),
46
+ `export const util = 1;\n`,
47
+ 'utf8',
48
+ );
49
+ fs.writeFileSync(
50
+ path.join(dir, 'feature', 'index.ts'),
51
+ `import { util } from './util';
52
+ export { util as u } from './util';
53
+ `,
54
+ 'utf8',
55
+ );
56
+
57
+ const result = buildImportMap(dir);
58
+
59
+ if (process.env.CAPTURE_BASELINE === '1') {
60
+ captured['captures relative import and export-from edges'] = result;
61
+ return;
62
+ }
63
+ assert.equal(
64
+ normalizeSnapshot(result),
65
+ normalizeSnapshot(baseline['captures relative import and export-from edges']),
66
+ );
67
+ });
68
+
69
+ it('ignores non-relative imports and deduplicates importers per target', () => {
70
+ const dir = mkTempDir('import-scanner-dedupe');
71
+ fs.mkdirSync(path.join(dir, 'lib'), { recursive: true });
72
+
73
+ fs.writeFileSync(path.join(dir, 'lib', 'shared.ts'), `export const x = 1;\n`, 'utf8');
74
+ fs.writeFileSync(
75
+ path.join(dir, 'a.ts'),
76
+ `import { readFileSync } from 'node:fs';
77
+ import { x } from './lib/shared';
78
+ import { y } from "./lib/shared";
79
+ `,
80
+ 'utf8',
81
+ );
82
+ fs.writeFileSync(
83
+ path.join(dir, 'b.ts'),
84
+ `export { x } from './lib/shared';\n`,
85
+ 'utf8',
86
+ );
87
+
88
+ const result = buildImportMap(dir);
89
+
90
+ if (process.env.CAPTURE_BASELINE === '1') {
91
+ captured['ignores non-relative imports and deduplicates importers per target'] = result;
92
+ return;
93
+ }
94
+ assert.equal(
95
+ normalizeSnapshot(result),
96
+ normalizeSnapshot(baseline['ignores non-relative imports and deduplicates importers per target']),
97
+ );
98
+ });
99
+
100
+ it('excludes imports that resolve outside srcDir', () => {
101
+ const root = mkTempDir('import-scanner-outside');
102
+ const src = path.join(root, 'src');
103
+ fs.mkdirSync(path.join(src, 'pkg'), { recursive: true });
104
+
105
+ fs.writeFileSync(path.join(src, 'pkg', 'inside.ts'), `export const ok = true;\n`, 'utf8');
106
+ fs.writeFileSync(
107
+ path.join(src, 'pkg', 'consumer.ts'),
108
+ `import { ok } from './inside';
109
+ import x from '../outside';
110
+ import y from '../../totally-out';
111
+ `,
112
+ 'utf8',
113
+ );
114
+
115
+ const result = buildImportMap(src);
116
+
117
+ if (process.env.CAPTURE_BASELINE === '1') {
118
+ captured['excludes imports that resolve outside srcDir'] = result;
119
+ return;
120
+ }
121
+ assert.equal(
122
+ normalizeSnapshot(result),
123
+ normalizeSnapshot(baseline['excludes imports that resolve outside srcDir']),
124
+ );
125
+ });
126
+ });
@@ -0,0 +1,64 @@
1
+ // @snapshot-for: src/snapshots/serializer.ts
2
+ // @generated-at: 2026-04-21T17:42:06.431Z
3
+ // @source-commit: d207869
4
+ // @generator-version: 1.0.0-alpha.6
5
+
6
+ import fs from 'node:fs';
7
+ import { describe, it } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { fileURLToPath } from 'node:url';
10
+ import * as path from 'node:path';
11
+ import { normalizeSnapshot } from '../../src/snapshots/serializer.ts';
12
+
13
+ const SLUG = 'src-snapshots-serializer';
14
+ const baselineRaw = process.env.CAPTURE_BASELINE === '1' ? '{}' : fs.readFileSync(fileURLToPath(new URL('./baselines/src-snapshots-serializer.json', import.meta.url)), 'utf8');
15
+ const baseline = JSON.parse(baselineRaw);
16
+ const captured: Record<string, unknown> = {};
17
+ process.on('exit', () => {
18
+ if (process.env.CAPTURE_BASELINE === '1') {
19
+ const p = process.env.AUTOREGRESS_TEMP_BASELINE_DIR
20
+ ? path.join(process.env.AUTOREGRESS_TEMP_BASELINE_DIR, 'src-snapshots-serializer.json')
21
+ : fileURLToPath(new URL('./baselines/src-snapshots-serializer.json', import.meta.url));
22
+ fs.writeFileSync(p, JSON.stringify(captured, null, 2), 'utf8');
23
+ }
24
+ });
25
+
26
+ describe('normalizeSnapshot', () => {
27
+ it('normalizes timestamps uuids and cwd-prefixed paths', () => {
28
+ const cwd = '/repo/project';
29
+ const result = JSON.parse(normalizeSnapshot({
30
+ ts: '2026-04-21T17:42:06.431Z',
31
+ id: '550e8400-e29b-41d4-a716-446655440000',
32
+ path: '/repo/project/src/index.ts',
33
+ untouched: '/other/place/file.ts',
34
+ }, cwd));
35
+
36
+ if (process.env.CAPTURE_BASELINE === '1') { captured['normalizes timestamps uuids and cwd-prefixed paths'] = result; return; }
37
+ assert.equal(normalizeSnapshot(result), normalizeSnapshot(baseline['normalizes timestamps uuids and cwd-prefixed paths']));
38
+ });
39
+
40
+ it('recursively normalizes arrays and nested objects', () => {
41
+ const cwd = '/work';
42
+ const result = JSON.parse(normalizeSnapshot({
43
+ items: [
44
+ '2024-01-02T03:04:05.000Z',
45
+ { uid: '123e4567-e89b-12d3-a456-426614174000', file: '/work/a/b.txt' },
46
+ ['plain', '/work/c/d.ts'],
47
+ ],
48
+ }, cwd));
49
+
50
+ if (process.env.CAPTURE_BASELINE === '1') { captured['recursively normalizes arrays and nested objects'] = result; return; }
51
+ assert.equal(normalizeSnapshot(result), normalizeSnapshot(baseline['recursively normalizes arrays and nested objects']));
52
+ });
53
+
54
+ it('sorts object keys deterministically at all levels', () => {
55
+ const result = JSON.parse(normalizeSnapshot({
56
+ z: 1,
57
+ a: { d: 4, b: 2, c: 3 },
58
+ m: [{ y: 2, x: 1 }, { b: 2, a: 1 }],
59
+ }));
60
+
61
+ if (process.env.CAPTURE_BASELINE === '1') { captured['sorts object keys deterministically at all levels'] = result; return; }
62
+ assert.equal(normalizeSnapshot(result), normalizeSnapshot(baseline['sorts object keys deterministically at all levels']));
63
+ });
64
+ });