agent-gauntlet 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/cli-adapters/claude.ts +13 -1
  4. package/src/cli-adapters/gemini.ts +17 -2
  5. package/src/commands/check.ts +108 -12
  6. package/src/commands/ci/list-jobs.ts +3 -2
  7. package/src/commands/clean.ts +29 -0
  8. package/src/commands/help.ts +1 -1
  9. package/src/commands/index.ts +2 -1
  10. package/src/commands/init.ts +4 -4
  11. package/src/commands/review.ts +108 -12
  12. package/src/commands/run.ts +109 -12
  13. package/src/commands/shared.ts +56 -10
  14. package/src/commands/validate.ts +20 -0
  15. package/src/config/schema.ts +5 -0
  16. package/src/config/validator.ts +6 -13
  17. package/src/core/change-detector.ts +1 -0
  18. package/src/core/entry-point.ts +48 -7
  19. package/src/core/runner.ts +90 -56
  20. package/src/gates/result.ts +32 -0
  21. package/src/gates/review.ts +428 -162
  22. package/src/index.ts +4 -2
  23. package/src/output/console-log.ts +146 -0
  24. package/src/output/console.ts +103 -9
  25. package/src/output/logger.ts +52 -8
  26. package/src/templates/run_gauntlet.template.md +20 -13
  27. package/src/utils/log-parser.ts +498 -162
  28. package/src/utils/session-ref.ts +82 -0
  29. package/src/commands/check.test.ts +0 -29
  30. package/src/commands/detect.test.ts +0 -43
  31. package/src/commands/health.test.ts +0 -93
  32. package/src/commands/help.test.ts +0 -44
  33. package/src/commands/init.test.ts +0 -130
  34. package/src/commands/list.test.ts +0 -121
  35. package/src/commands/rerun.ts +0 -160
  36. package/src/commands/review.test.ts +0 -31
  37. package/src/commands/run.test.ts +0 -27
  38. package/src/config/loader.test.ts +0 -151
  39. package/src/core/entry-point.test.ts +0 -61
  40. package/src/gates/review.test.ts +0 -291
@@ -1,15 +1,14 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import type { ReviewFullJsonOutput } from "../gates/result.js";
3
4
 
4
- export interface PreviousViolation {
5
- file: string;
6
- line: number | string;
7
- issue: string;
8
- fix?: string;
9
- }
5
+ export type { PreviousViolation } from "../gates/result.js";
6
+
7
+ import type { PreviousViolation } from "../gates/result.js";
10
8
 
11
9
  export interface AdapterFailure {
12
10
  adapterName: string; // e.g., 'claude', 'gemini'
11
+ reviewIndex?: number; // 1-based review index from @N in filename
13
12
  violations: PreviousViolation[];
14
13
  }
15
14
 
@@ -22,189 +21,388 @@ export interface GateFailures {
22
21
  }
23
22
 
24
23
  /**
25
- * Parses a single log file to extract failures per adapter.
26
- * Only processes review gates (ignores check gates).
24
+ * Parse a review filename to extract the job ID, adapter, review index, and run number.
25
+ * Pattern: <jobId>_<adapter>@<reviewIndex>.<runNumber>.(log|json)
26
+ * Returns null if the filename doesn't match the review pattern.
27
27
  */
28
- export async function parseLogFile(
29
- logPath: string,
28
+ export function parseReviewFilename(filename: string): {
29
+ jobId: string;
30
+ adapter: string;
31
+ reviewIndex: number;
32
+ runNumber: number;
33
+ ext: string;
34
+ } | null {
35
+ // Match: <prefix>_<adapter>@<index>.<runNum>.(log|json)
36
+ const m = filename.match(/^(.+)_([^@]+)@(\d+)\.(\d+)\.(log|json)$/);
37
+ if (!m) return null;
38
+ return {
39
+ jobId: m[1]!,
40
+ adapter: m[2]!,
41
+ reviewIndex: parseInt(m[3]!, 10),
42
+ runNumber: parseInt(m[4]!, 10),
43
+ ext: m[5]!,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Parses a JSON review file.
49
+ */
50
+ export async function parseJsonReviewFile(
51
+ jsonPath: string,
30
52
  ): Promise<GateFailures | null> {
31
53
  try {
32
- const content = await fs.readFile(logPath, "utf-8");
33
- const filename = path.basename(logPath);
54
+ const content = await fs.readFile(jsonPath, "utf-8");
55
+ const data: ReviewFullJsonOutput = JSON.parse(content);
56
+ const filename = path.basename(jsonPath);
57
+
58
+ // Extract jobId: strip the _adapter@index.runNum.json suffix
59
+ const parsed = parseReviewFilename(filename);
60
+ const jobId = parsed ? parsed.jobId : filename.replace(/\.\d+\.json$/, "");
34
61
 
35
- // Check if it's a review log by content marker
36
- if (!content.includes("--- Review Output")) {
62
+ if (data.status === "pass") {
37
63
  return null;
38
64
  }
39
65
 
40
- // Use the sanitized filename as the Job ID key
41
- const jobId = filename.replace(/\.log$/, "");
42
-
43
- // We can't reliably parse entryPoint/gateName from sanitized filename
44
- // leaving them empty for now as they aren't critical for the map lookup
45
- const gateName = "";
46
- const entryPoint = "";
47
-
48
- const adapterFailures: AdapterFailure[] = [];
49
-
50
- // Split by sections using `--- Review Output (adapterName) ---` markers
51
- const sectionRegex = /--- Review Output \(([^)]+)\) ---/g;
52
-
53
- let match: RegExpExecArray | null;
54
- const sections: { adapter: string; startIndex: number }[] = [];
55
-
56
- for (;;) {
57
- match = sectionRegex.exec(content);
58
- if (!match) break;
59
- sections.push({
60
- adapter: match[1],
61
- startIndex: match.index,
66
+ const violations = (data.violations || []).map((v) => ({
67
+ ...v,
68
+ status: v.status || "new",
69
+ }));
70
+
71
+ if (violations.length === 0 && data.status === "fail") {
72
+ violations.push({
73
+ file: "unknown",
74
+ line: "?",
75
+ issue: "Previous run failed but no violations found in JSON",
76
+ status: "new",
62
77
  });
63
78
  }
64
79
 
65
- if (sections.length === 0) {
66
- return null;
67
- }
80
+ if (violations.length === 0) return null;
68
81
 
69
- for (let i = 0; i < sections.length; i++) {
70
- const currentSection = sections[i];
71
- const nextSection = sections[i + 1];
72
- const endIndex = nextSection ? nextSection.startIndex : content.length;
73
- const sectionContent = content.substring(
74
- currentSection.startIndex,
75
- endIndex,
76
- );
82
+ return {
83
+ jobId,
84
+ gateName: "",
85
+ entryPoint: "",
86
+ adapterFailures: [
87
+ {
88
+ adapterName: data.adapter,
89
+ reviewIndex: parsed?.reviewIndex,
90
+ violations,
91
+ },
92
+ ],
93
+ logPath: jsonPath.replace(/\.json$/, ".log"),
94
+ };
95
+ } catch (error) {
96
+ console.warn("Warning: Failed to parse JSON review file:", jsonPath, error);
97
+ return null;
98
+ }
99
+ }
77
100
 
78
- const violations: PreviousViolation[] = [];
101
+ /**
102
+ * Extract the log prefix (job ID) from a numbered log filename.
103
+ * Handles both patterns:
104
+ * - Check: `check_src_test.2.log` -> `check_src_test`
105
+ * - Review (new): `review_src_claude@1.2.log` -> `review_src_claude@1`
106
+ */
107
+ export function extractPrefix(filename: string): string {
108
+ // Pattern: <prefix>.<number>.(log|json)
109
+ const m = filename.match(/^(.+)\.\d+\.(log|json)$/);
110
+ if (m) return m[1]!;
111
+ // Fallback for non-numbered files
112
+ return filename.replace(/\.(log|json)$/, "");
113
+ }
79
114
 
80
- // 1. Look for "--- Parsed Result ---"
81
- const parsedResultMatch = sectionContent.match(
82
- /---\s*Parsed Result(?:\s+\(([^)]+)\))?\s*---([\s\S]*?)(?:$|---)/,
83
- );
115
+ /**
116
+ * Parses a single log file to extract failures per adapter.
117
+ * Processes both review and check gates.
118
+ */
119
+ export async function parseLogFile(
120
+ logPath: string,
121
+ ): Promise<GateFailures | null> {
122
+ try {
123
+ const content = await fs.readFile(logPath, "utf-8");
124
+ const filename = path.basename(logPath);
84
125
 
85
- if (parsedResultMatch) {
86
- const parsedContent = parsedResultMatch[2];
126
+ // Try to parse as review filename with @index pattern
127
+ const parsed = parseReviewFilename(filename);
128
+ const jobId = parsed ? parsed.jobId : extractPrefix(filename);
87
129
 
88
- // Check status
89
- if (parsedContent.includes("Status: PASS")) {
90
- continue; // No violations for this adapter
91
- }
130
+ // Check if it's a review log
131
+ if (content.includes("--- Review Output")) {
132
+ const adapterFailures: AdapterFailure[] = [];
133
+ const sectionRegex = /--- Review Output \(([^)]+)\) ---/g;
134
+
135
+ let match: RegExpExecArray | null;
136
+ const sections: { adapter: string; startIndex: number }[] = [];
92
137
 
93
- // Extract violations
94
- // Pattern: 1. src/app.ts:42 - Missing error handling
95
- // Pattern: 1. src/app.ts:? - Missing error handling
96
- // Pattern: 1. src/app.ts:NaN - Missing error handling
97
- /**
98
- * Extract violations from the parsed result section.
99
- * Pattern matches "1. file:line - issue" where line can be a number, NaN, or ?.
100
- */
101
- const violationRegex = /^\d+\.\s+(.+?):(\d+|NaN|\?)\s+-\s+(.+)$/gm;
102
- let vMatch: RegExpExecArray | null;
103
-
104
- for (;;) {
105
- vMatch = violationRegex.exec(parsedContent);
106
- if (!vMatch) break;
107
- const file = vMatch[1].trim();
108
- let line: number | string = vMatch[2];
109
- if (line !== "NaN" && line !== "?") {
110
- line = parseInt(line, 10);
138
+ for (;;) {
139
+ match = sectionRegex.exec(content);
140
+ if (!match) break;
141
+ sections.push({
142
+ adapter: match[1]!,
143
+ startIndex: match.index,
144
+ });
145
+ }
146
+
147
+ if (sections.length === 0) return null;
148
+
149
+ for (let i = 0; i < sections.length; i++) {
150
+ const currentSection = sections[i]!;
151
+ const nextSection = sections[i + 1];
152
+ const endIndex = nextSection ? nextSection.startIndex : content.length;
153
+ const sectionContent = content.substring(
154
+ currentSection.startIndex,
155
+ endIndex,
156
+ );
157
+
158
+ const violations: PreviousViolation[] = [];
159
+ const parsedResultMatch = sectionContent.match(
160
+ /---\s*Parsed Result(?:\s+\(([^)]+)\))?\s*---([\s\S]*?)(?:$|---)/,
161
+ );
162
+
163
+ if (parsedResultMatch) {
164
+ const parsedContent = parsedResultMatch[2]!;
165
+ if (parsedContent.includes("Status: PASS")) continue;
166
+ const violationRegex = /^\d+\.\s+(.+?):(\d+|NaN|\?)\s+-\s+(.+)$/gm;
167
+ let vMatch: RegExpExecArray | null;
168
+ for (;;) {
169
+ vMatch = violationRegex.exec(parsedContent);
170
+ if (!vMatch) break;
171
+ const file = vMatch[1]!.trim();
172
+ let line: number | string = vMatch[2]!;
173
+ if (line !== "NaN" && line !== "?")
174
+ line = parseInt(line as string, 10);
175
+ const issue = vMatch[3]!.trim();
176
+ let fix: string | undefined;
177
+ const remainder = parsedContent.substring(
178
+ vMatch.index + vMatch[0].length,
179
+ );
180
+ const fixMatch = remainder.match(/^\s+Fix:\s+(.+)$/m);
181
+ const nextViolationIndex = remainder.search(/^\d+\./m);
182
+ if (
183
+ fixMatch?.index !== undefined &&
184
+ (nextViolationIndex === -1 || fixMatch.index < nextViolationIndex)
185
+ ) {
186
+ fix = fixMatch[1]!.trim();
187
+ }
188
+ violations.push({ file, line, issue, fix });
111
189
  }
112
- const issue = vMatch[3].trim();
190
+ } else {
191
+ // Fallback JSON
192
+ const firstBrace = sectionContent.indexOf("{");
193
+ const lastBrace = sectionContent.lastIndexOf("}");
194
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
195
+ try {
196
+ const jsonStr = sectionContent.substring(
197
+ firstBrace,
198
+ lastBrace + 1,
199
+ );
200
+ const json = JSON.parse(jsonStr);
201
+ if (json.violations && Array.isArray(json.violations)) {
202
+ for (const v of json.violations) {
203
+ if (v.file && v.issue) {
204
+ violations.push({
205
+ file: v.file,
206
+ line: v.line || 0,
207
+ issue: v.issue,
208
+ fix: v.fix,
209
+ status: v.status,
210
+ result: v.result,
211
+ });
212
+ }
213
+ }
214
+ }
215
+ } catch (_e) {}
216
+ }
217
+ }
113
218
 
114
- // Look for fix in the next line(s)
115
- let fix: string | undefined;
116
- const remainder = parsedContent.substring(
117
- vMatch.index + vMatch[0].length,
118
- );
219
+ if (violations.length > 0) {
220
+ adapterFailures.push({
221
+ adapterName: currentSection.adapter,
222
+ reviewIndex: parsed?.reviewIndex,
223
+ violations,
224
+ });
225
+ } else if (parsedResultMatch?.[2]?.includes("Status: FAIL")) {
226
+ adapterFailures.push({
227
+ adapterName: currentSection.adapter,
228
+ reviewIndex: parsed?.reviewIndex,
229
+ violations: [
230
+ {
231
+ file: "unknown",
232
+ line: "?",
233
+ issue:
234
+ "Previous run failed but specific violations could not be parsed",
235
+ },
236
+ ],
237
+ });
238
+ }
239
+ }
119
240
 
120
- const fixMatch = remainder.match(/^\s+Fix:\s+(.+)$/m);
121
- const nextViolationIndex = remainder.search(/^\d+\./m);
241
+ if (adapterFailures.length === 0) return null;
242
+ return { jobId, gateName: "", entryPoint: "", adapterFailures, logPath };
243
+ } else {
244
+ // Check log
245
+ if (content.includes("Result: pass")) return null;
246
+
247
+ const hasFailure =
248
+ content.includes("Result: fail") ||
249
+ content.includes("Result: error") ||
250
+ content.includes("Command failed:");
251
+
252
+ if (!hasFailure) return null;
253
+
254
+ return {
255
+ jobId,
256
+ gateName: "",
257
+ entryPoint: "",
258
+ adapterFailures: [
259
+ {
260
+ adapterName: "check",
261
+ violations: [{ file: "check", line: 0, issue: "Check failed" }],
262
+ },
263
+ ],
264
+ logPath,
265
+ };
266
+ }
267
+ } catch (_error) {
268
+ return null;
269
+ }
270
+ }
122
271
 
123
- const isFixBelongingToCurrentViolation =
124
- fixMatch?.index !== undefined &&
125
- (nextViolationIndex === -1 || fixMatch.index < nextViolationIndex);
272
+ export interface RunIteration {
273
+ iteration: number;
274
+ fixed: Array<{
275
+ jobId: string;
276
+ adapter?: string;
277
+ details: string;
278
+ }>;
279
+ skipped: Array<{
280
+ jobId: string;
281
+ adapter?: string;
282
+ file: string;
283
+ line: number | string;
284
+ issue: string;
285
+ result?: string | null;
286
+ }>;
287
+ }
126
288
 
127
- if (isFixBelongingToCurrentViolation && fixMatch) {
128
- fix = fixMatch[1].trim();
129
- }
289
+ /**
290
+ * Reconstructs the history of fixes and skips after all iterations.
291
+ */
292
+ export async function reconstructHistory(
293
+ logDir: string,
294
+ ): Promise<RunIteration[]> {
295
+ try {
296
+ const files = await fs.readdir(logDir);
297
+ const runNumbers = new Set<number>();
298
+ for (const file of files) {
299
+ const m = file.match(/\.(\d+)\.(log|json)$/);
300
+ if (m) runNumbers.add(parseInt(m[1]!, 10));
301
+ }
130
302
 
131
- violations.push({
132
- file,
133
- line,
134
- issue,
135
- fix,
136
- });
303
+ const sortedRuns = Array.from(runNumbers).sort((a, b) => a - b);
304
+ const iterations: RunIteration[] = [];
305
+
306
+ let previousFailuresByJob = new Map<string, PreviousViolation[]>();
307
+
308
+ for (const runNum of sortedRuns) {
309
+ const currentFailuresByJob = new Map<string, PreviousViolation[]>();
310
+ const iteration: RunIteration = {
311
+ iteration: runNum,
312
+ fixed: [],
313
+ skipped: [],
314
+ };
315
+
316
+ const runFiles = files.filter((f) => f.includes(`.${runNum}.`));
317
+ const prefixes = new Set(runFiles.map((f) => extractPrefix(f)));
318
+
319
+ for (const prefix of prefixes) {
320
+ const jsonFile = runFiles.find(
321
+ (f) => f.startsWith(`${prefix}.${runNum}.`) && f.endsWith(".json"),
322
+ );
323
+ const logFile = runFiles.find(
324
+ (f) => f.startsWith(`${prefix}.${runNum}.`) && f.endsWith(".log"),
325
+ );
326
+
327
+ let failure: GateFailures | null = null;
328
+ if (jsonFile) {
329
+ failure = await parseJsonReviewFile(path.join(logDir, jsonFile));
330
+ } else if (logFile) {
331
+ failure = await parseLogFile(path.join(logDir, logFile));
137
332
  }
138
- } else {
139
- // Fallback: Try to parse JSON
140
- // Extract JSON using first '{' and last '}' to capture the full object
141
- const firstBrace = sectionContent.indexOf("{");
142
- const lastBrace = sectionContent.lastIndexOf("}");
143
-
144
- if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
145
- try {
146
- const jsonStr = sectionContent.substring(firstBrace, lastBrace + 1);
147
- // Try to find the valid JSON object
148
- const json = JSON.parse(jsonStr);
149
-
150
- if (json.violations && Array.isArray(json.violations)) {
151
- for (const v of json.violations) {
152
- if (v.file && v.issue) {
153
- violations.push({
154
- file: v.file,
155
- line: v.line || 0,
156
- issue: v.issue,
157
- fix: v.fix,
158
- });
159
- }
333
+
334
+ if (failure) {
335
+ for (const af of failure.adapterFailures) {
336
+ const key = af.reviewIndex
337
+ ? `${failure.jobId}:${af.reviewIndex}`
338
+ : `${failure.jobId}:${af.adapterName}`;
339
+ currentFailuresByJob.set(key, af.violations);
340
+
341
+ for (const v of af.violations) {
342
+ if (v.status === "skipped") {
343
+ iteration.skipped.push({
344
+ jobId: failure.jobId,
345
+ adapter: af.adapterName,
346
+ file: v.file,
347
+ line: v.line,
348
+ issue: v.issue,
349
+ result: v.result,
350
+ });
160
351
  }
161
352
  }
162
- } catch (_e: unknown) {
163
- // Log warning for debugging (commented out to reduce noise in production)
164
- // console.warn(`Warning: Failed to parse JSON for ${currentSection.adapter} in ${jobId}: ${e.message}`);
165
353
  }
166
354
  }
167
355
  }
168
- if (violations.length > 0) {
169
- adapterFailures.push({
170
- adapterName: currentSection.adapter,
171
- violations,
172
- });
173
- } else if (parsedResultMatch?.[1].includes("Status: FAIL")) {
174
- // Track failure even if violations couldn't be parsed
175
- adapterFailures.push({
176
- adapterName: currentSection.adapter,
177
- violations: [
178
- {
179
- file: "unknown",
180
- line: "?",
181
- issue:
182
- "Previous run failed but specific violations could not be parsed",
183
- },
184
- ],
356
+
357
+ for (const [key, prevViolations] of previousFailuresByJob.entries()) {
358
+ const current = currentFailuresByJob.get(key);
359
+ const sep = key.lastIndexOf(":");
360
+ const jobId = key.substring(0, sep);
361
+ const adapter = key.substring(sep + 1);
362
+
363
+ const trulyFixed = prevViolations.filter((pv) => {
364
+ if (pv.status === "skipped") return false;
365
+ return !current?.some(
366
+ (cv) =>
367
+ cv.file === pv.file &&
368
+ cv.line === pv.line &&
369
+ cv.issue === pv.issue,
370
+ );
185
371
  });
372
+
373
+ if (trulyFixed.length > 0) {
374
+ if (jobId.startsWith("check_")) {
375
+ iteration.fixed.push({
376
+ jobId,
377
+ details: `${trulyFixed.length} violations resolved`,
378
+ });
379
+ } else {
380
+ for (const f of trulyFixed) {
381
+ iteration.fixed.push({
382
+ jobId,
383
+ adapter,
384
+ details: `${f.file}:${f.line} ${f.issue}`,
385
+ });
386
+ }
387
+ }
388
+ }
186
389
  }
187
- }
188
390
 
189
- if (adapterFailures.length === 0) {
190
- return null;
391
+ iterations.push(iteration);
392
+ previousFailuresByJob = currentFailuresByJob;
191
393
  }
192
394
 
193
- return {
194
- jobId,
195
- gateName,
196
- entryPoint,
197
- adapterFailures,
198
- logPath,
199
- };
200
- } catch (_error) {
201
- // console.warn(`Error parsing log file ${logPath}:`, error);
202
- return null;
395
+ return iterations;
396
+ } catch (_e) {
397
+ return [];
203
398
  }
204
399
  }
205
400
 
206
401
  /**
207
402
  * Finds all previous failures from the log directory.
403
+ * For review gates with the @<index> pattern, groups by (jobId, reviewIndex)
404
+ * and returns the highest-numbered run for each index.
405
+ * The resulting Map keys are the review index (as string) for lookup by the review gate.
208
406
  */
209
407
  export async function findPreviousFailures(
210
408
  logDir: string,
@@ -214,26 +412,166 @@ export async function findPreviousFailures(
214
412
  const files = await fs.readdir(logDir);
215
413
  const gateFailures: GateFailures[] = [];
216
414
 
415
+ // Separate review files (with @index) from check files
416
+ // Group review files by (jobId, reviewIndex) -> highest run number
417
+ const reviewSlotMap = new Map<
418
+ string,
419
+ { filename: string; runNumber: number; ext: string }
420
+ >();
421
+ const checkPrefixMap = new Map<string, Map<number, Set<string>>>();
422
+
217
423
  for (const file of files) {
218
- if (!file.endsWith(".log")) continue;
424
+ const isLog = file.endsWith(".log");
425
+ const isJson = file.endsWith(".json");
426
+ if (!isLog && !isJson) continue;
427
+ if (gateFilter && !file.includes(gateFilter)) continue;
428
+
429
+ const parsed = parseReviewFilename(file);
430
+ if (parsed) {
431
+ // Review file with @index pattern
432
+ const slotKey = `${parsed.jobId}:${parsed.reviewIndex}`;
433
+ const existing = reviewSlotMap.get(slotKey);
434
+ if (!existing || parsed.runNumber > existing.runNumber) {
435
+ reviewSlotMap.set(slotKey, {
436
+ filename: file,
437
+ runNumber: parsed.runNumber,
438
+ ext: parsed.ext,
439
+ });
440
+ }
441
+ } else {
442
+ // Check file or legacy review file
443
+ const m = file.match(/^(.+)\.(\d+)\.(log|json)$/);
444
+ if (!m) continue;
445
+
446
+ const prefix = m[1]!;
447
+ const runNum = parseInt(m[2]!, 10);
448
+ const ext = m[3]!;
449
+
450
+ let runMap = checkPrefixMap.get(prefix);
451
+ if (!runMap) {
452
+ runMap = new Map();
453
+ checkPrefixMap.set(prefix, runMap);
454
+ }
219
455
 
220
- // If gate filter provided, check if filename matches
221
- // filename is sanitized, so we do a loose check
222
- if (gateFilter && !file.includes(gateFilter)) {
223
- continue;
456
+ let exts = runMap.get(runNum);
457
+ if (!exts) {
458
+ exts = new Set();
459
+ runMap.set(runNum, exts);
460
+ }
461
+ exts.add(ext);
224
462
  }
463
+ }
464
+
465
+ // Process review files grouped by slot (jobId + reviewIndex)
466
+ // Group by jobId to produce a single GateFailures per job
467
+ const jobReviewFailures = new Map<string, AdapterFailure[]>();
225
468
 
226
- const logPath = path.join(logDir, file);
227
- const failure = await parseLogFile(logPath);
469
+ for (const [slotKey, fileInfo] of reviewSlotMap.entries()) {
470
+ const sepIdx = slotKey.lastIndexOf(":");
471
+ const jobId = slotKey.substring(0, sepIdx);
472
+ const reviewIndex = parseInt(slotKey.substring(sepIdx + 1), 10);
473
+
474
+ let failure: GateFailures | null = null;
475
+ if (fileInfo.ext === "json") {
476
+ failure = await parseJsonReviewFile(
477
+ path.join(logDir, fileInfo.filename),
478
+ );
479
+ } else {
480
+ failure = await parseLogFile(path.join(logDir, fileInfo.filename));
481
+ }
228
482
 
229
483
  if (failure) {
230
- gateFailures.push(failure);
484
+ // Apply status filtering
485
+ for (const af of failure.adapterFailures) {
486
+ af.reviewIndex = reviewIndex;
487
+ const filteredViolations: PreviousViolation[] = [];
488
+ for (const v of af.violations) {
489
+ const status = v.status || "new";
490
+ if (status === "skipped") continue;
491
+ if (
492
+ status !== "new" &&
493
+ status !== "fixed" &&
494
+ status !== "skipped"
495
+ ) {
496
+ console.warn(
497
+ `Warning: Unexpected status "${status}" for violation in ${jobId}. Treating as "new".`,
498
+ );
499
+ v.status = "new";
500
+ }
501
+ filteredViolations.push(v);
502
+ }
503
+ af.violations = filteredViolations;
504
+
505
+ if (af.violations.length > 0) {
506
+ if (!jobReviewFailures.has(jobId)) {
507
+ jobReviewFailures.set(jobId, []);
508
+ }
509
+ jobReviewFailures.get(jobId)!.push(af);
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ for (const [jobId, adapterFailures] of jobReviewFailures.entries()) {
516
+ gateFailures.push({
517
+ jobId,
518
+ gateName: "",
519
+ entryPoint: "",
520
+ adapterFailures,
521
+ logPath: path.join(logDir, `${jobId}.log`),
522
+ });
523
+ }
524
+
525
+ // Process check files (non-review)
526
+ for (const [prefix, runMap] of checkPrefixMap.entries()) {
527
+ const latestRun = Math.max(...runMap.keys());
528
+ const exts = runMap.get(latestRun);
529
+ if (!exts) continue;
530
+
531
+ let failure: GateFailures | null = null;
532
+ if (exts.has("json")) {
533
+ failure = await parseJsonReviewFile(
534
+ path.join(logDir, `${prefix}.${latestRun}.json`),
535
+ );
536
+ } else if (exts.has("log")) {
537
+ failure = await parseLogFile(
538
+ path.join(logDir, `${prefix}.${latestRun}.log`),
539
+ );
540
+ }
541
+
542
+ if (failure) {
543
+ for (const af of failure.adapterFailures) {
544
+ const filteredViolations: PreviousViolation[] = [];
545
+ for (const v of af.violations) {
546
+ const status = v.status || "new";
547
+ if (status === "skipped") continue;
548
+ if (
549
+ status !== "new" &&
550
+ status !== "fixed" &&
551
+ status !== "skipped"
552
+ ) {
553
+ console.warn(
554
+ `Warning: Unexpected status "${status}" for violation in ${failure.jobId}. Treating as "new".`,
555
+ );
556
+ v.status = "new";
557
+ }
558
+ filteredViolations.push(v);
559
+ }
560
+ af.violations = filteredViolations;
561
+ }
562
+
563
+ const totalViolations = failure.adapterFailures.reduce(
564
+ (sum, af) => sum + af.violations.length,
565
+ 0,
566
+ );
567
+ if (totalViolations > 0) {
568
+ gateFailures.push(failure);
569
+ }
231
570
  }
232
571
  }
233
572
 
234
573
  return gateFailures;
235
574
  } catch (error: unknown) {
236
- // If directory doesn't exist, return empty
237
575
  if (
238
576
  typeof error === "object" &&
239
577
  error !== null &&
@@ -242,8 +580,6 @@ export async function findPreviousFailures(
242
580
  ) {
243
581
  return [];
244
582
  }
245
- // Otherwise log and return empty
246
- // console.warn(`Error reading log directory ${logDir}:`, error instanceof Error ? error.message : String(error));
247
583
  return [];
248
584
  }
249
585
  }