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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/cli-adapters/claude.ts +13 -1
- package/src/cli-adapters/gemini.ts +17 -2
- package/src/commands/check.ts +108 -12
- package/src/commands/ci/list-jobs.ts +3 -2
- package/src/commands/clean.ts +29 -0
- package/src/commands/help.ts +1 -1
- package/src/commands/index.ts +2 -1
- package/src/commands/init.ts +4 -4
- package/src/commands/review.ts +108 -12
- package/src/commands/run.ts +109 -12
- package/src/commands/shared.ts +56 -10
- package/src/commands/validate.ts +20 -0
- package/src/config/schema.ts +5 -0
- package/src/config/validator.ts +6 -13
- package/src/core/change-detector.ts +1 -0
- package/src/core/entry-point.ts +48 -7
- package/src/core/runner.ts +90 -56
- package/src/gates/result.ts +32 -0
- package/src/gates/review.ts +428 -162
- package/src/index.ts +4 -2
- package/src/output/console-log.ts +146 -0
- package/src/output/console.ts +103 -9
- package/src/output/logger.ts +52 -8
- package/src/templates/run_gauntlet.template.md +20 -13
- package/src/utils/log-parser.ts +498 -162
- package/src/utils/session-ref.ts +82 -0
- package/src/commands/check.test.ts +0 -29
- package/src/commands/detect.test.ts +0 -43
- package/src/commands/health.test.ts +0 -93
- package/src/commands/help.test.ts +0 -44
- package/src/commands/init.test.ts +0 -130
- package/src/commands/list.test.ts +0 -121
- package/src/commands/rerun.ts +0 -160
- package/src/commands/review.test.ts +0 -31
- package/src/commands/run.test.ts +0 -27
- package/src/config/loader.test.ts +0 -151
- package/src/core/entry-point.test.ts +0 -61
- package/src/gates/review.test.ts +0 -291
package/src/utils/log-parser.ts
CHANGED
|
@@ -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
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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
|
|
29
|
-
|
|
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(
|
|
33
|
-
const
|
|
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
|
-
|
|
36
|
-
if (!content.includes("--- Review Output")) {
|
|
62
|
+
if (data.status === "pass") {
|
|
37
63
|
return null;
|
|
38
64
|
}
|
|
39
65
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 (
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
80
|
+
if (violations.length === 0) return null;
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
file
|
|
180
|
-
line
|
|
181
|
-
issue
|
|
182
|
-
|
|
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
|
-
|
|
190
|
-
|
|
391
|
+
iterations.push(iteration);
|
|
392
|
+
previousFailuresByJob = currentFailuresByJob;
|
|
191
393
|
}
|
|
192
394
|
|
|
193
|
-
return
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
const
|
|
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
|
-
|
|
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
|
}
|