agent-gauntlet 0.10.0 → 0.11.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 +25 -23
- package/dist/index.js +9226 -0
- package/dist/index.js.map +65 -0
- package/dist/scripts/status.js +280 -0
- package/dist/scripts/status.js.map +10 -0
- package/package.json +22 -8
- package/src/built-in-reviews/code-quality.md +0 -25
- package/src/built-in-reviews/index.ts +0 -28
- package/src/bun-plugins.d.ts +0 -4
- package/src/cli-adapters/claude.ts +0 -327
- package/src/cli-adapters/codex.ts +0 -290
- package/src/cli-adapters/cursor.ts +0 -128
- package/src/cli-adapters/gemini.ts +0 -510
- package/src/cli-adapters/github-copilot.ts +0 -141
- package/src/cli-adapters/index.ts +0 -250
- package/src/cli-adapters/thinking-budget.ts +0 -23
- package/src/commands/check.ts +0 -311
- package/src/commands/ci/index.ts +0 -15
- package/src/commands/ci/init.ts +0 -96
- package/src/commands/ci/list-jobs.ts +0 -90
- package/src/commands/clean.ts +0 -54
- package/src/commands/detect.ts +0 -173
- package/src/commands/health.ts +0 -169
- package/src/commands/help.ts +0 -34
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -1878
- package/src/commands/list.ts +0 -33
- package/src/commands/review.ts +0 -311
- package/src/commands/run.ts +0 -29
- package/src/commands/shared.ts +0 -267
- package/src/commands/stop-hook.ts +0 -567
- package/src/commands/validate.ts +0 -20
- package/src/commands/wait-ci.ts +0 -518
- package/src/config/ci-loader.ts +0 -33
- package/src/config/ci-schema.ts +0 -28
- package/src/config/global.ts +0 -87
- package/src/config/loader.ts +0 -301
- package/src/config/schema.ts +0 -165
- package/src/config/stop-hook-config.ts +0 -130
- package/src/config/types.ts +0 -65
- package/src/config/validator.ts +0 -592
- package/src/core/change-detector.ts +0 -137
- package/src/core/diff-stats.ts +0 -442
- package/src/core/entry-point.ts +0 -190
- package/src/core/job.ts +0 -96
- package/src/core/run-executor.ts +0 -621
- package/src/core/runner.ts +0 -290
- package/src/gates/check.ts +0 -118
- package/src/gates/resolve-check-command.ts +0 -21
- package/src/gates/result.ts +0 -54
- package/src/gates/review.ts +0 -1333
- package/src/hooks/adapters/claude-stop-hook.ts +0 -99
- package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
- package/src/hooks/adapters/types.ts +0 -94
- package/src/hooks/stop-hook-handler.ts +0 -748
- package/src/index.ts +0 -47
- package/src/output/app-logger.ts +0 -214
- package/src/output/console-log.ts +0 -168
- package/src/output/console.ts +0 -359
- package/src/output/logger.ts +0 -126
- package/src/output/sinks/console-sink.ts +0 -59
- package/src/output/sinks/file-sink.ts +0 -110
- package/src/scripts/status.ts +0 -433
- package/src/templates/workflow.yml +0 -79
- package/src/types/gauntlet-status.ts +0 -79
- package/src/utils/debug-log.ts +0 -392
- package/src/utils/diff-parser.ts +0 -103
- package/src/utils/execution-state.ts +0 -472
- package/src/utils/log-parser.ts +0 -696
- package/src/utils/sanitizer.ts +0 -3
- package/src/utils/session-ref.ts +0 -91
package/src/utils/log-parser.ts
DELETED
|
@@ -1,696 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { ReviewFullJsonOutput } from "../gates/result.js";
|
|
4
|
-
import { getCategoryLogger } from "../output/app-logger.js";
|
|
5
|
-
|
|
6
|
-
export type { PreviousViolation } from "../gates/result.js";
|
|
7
|
-
|
|
8
|
-
import type { PreviousViolation } from "../gates/result.js";
|
|
9
|
-
|
|
10
|
-
const log = getCategoryLogger("log-parser");
|
|
11
|
-
|
|
12
|
-
export interface AdapterFailure {
|
|
13
|
-
adapterName: string; // e.g., 'claude', 'gemini'
|
|
14
|
-
reviewIndex?: number; // 1-based review index from @N in filename
|
|
15
|
-
violations: PreviousViolation[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface PassedSlot {
|
|
19
|
-
reviewIndex: number; // 1-based review index
|
|
20
|
-
passIteration: number; // Iteration number when this slot passed
|
|
21
|
-
adapter: string; // Adapter name that passed the review
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Result from findPreviousFailures that includes both failures and passed slots.
|
|
26
|
-
* passedSlots maps jobId -> reviewIndex -> { adapter, passIteration }
|
|
27
|
-
*/
|
|
28
|
-
export interface PreviousFailuresResult {
|
|
29
|
-
failures: GateFailures[];
|
|
30
|
-
passedSlots: Map<string, Map<number, PassedSlot>>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface GateFailures {
|
|
34
|
-
jobId: string; // This will be the sanitized Job ID (filename without extension)
|
|
35
|
-
gateName: string; // Parsed or empty
|
|
36
|
-
entryPoint: string; // Parsed or empty
|
|
37
|
-
adapterFailures: AdapterFailure[]; // Failures grouped by adapter
|
|
38
|
-
logPath: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Parse a review filename to extract the job ID, adapter, review index, and run number.
|
|
43
|
-
* Pattern: <jobId>_<adapter>@<reviewIndex>.<runNumber>.(log|json)
|
|
44
|
-
* Returns null if the filename doesn't match the review pattern.
|
|
45
|
-
*/
|
|
46
|
-
export function parseReviewFilename(filename: string): {
|
|
47
|
-
jobId: string;
|
|
48
|
-
adapter: string;
|
|
49
|
-
reviewIndex: number;
|
|
50
|
-
runNumber: number;
|
|
51
|
-
ext: string;
|
|
52
|
-
} | null {
|
|
53
|
-
// Match: <prefix>_<adapter>@<index>.<runNum>.(log|json)
|
|
54
|
-
const m = filename.match(/^(.+)_([^@]+)@(\d+)\.(\d+)\.(log|json)$/);
|
|
55
|
-
if (!m) return null;
|
|
56
|
-
const [, jobId, adapter, indexStr, runStr, ext] = m;
|
|
57
|
-
if (!jobId || !adapter || !indexStr || !runStr || !ext) return null;
|
|
58
|
-
return {
|
|
59
|
-
jobId,
|
|
60
|
-
adapter,
|
|
61
|
-
reviewIndex: parseInt(indexStr, 10),
|
|
62
|
-
runNumber: parseInt(runStr, 10),
|
|
63
|
-
ext,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Parses a JSON review file.
|
|
69
|
-
*/
|
|
70
|
-
export async function parseJsonReviewFile(
|
|
71
|
-
jsonPath: string,
|
|
72
|
-
): Promise<GateFailures | null> {
|
|
73
|
-
try {
|
|
74
|
-
const content = await fs.readFile(jsonPath, "utf-8");
|
|
75
|
-
const data: ReviewFullJsonOutput = JSON.parse(content);
|
|
76
|
-
const filename = path.basename(jsonPath);
|
|
77
|
-
|
|
78
|
-
// Extract jobId: strip the _adapter@index.runNum.json suffix
|
|
79
|
-
const parsed = parseReviewFilename(filename);
|
|
80
|
-
const jobId = parsed ? parsed.jobId : filename.replace(/\.\d+\.json$/, "");
|
|
81
|
-
|
|
82
|
-
if (data.status === "pass" || data.status === "skipped_prior_pass") {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const violations = (data.violations || []).map((v) => ({
|
|
87
|
-
...v,
|
|
88
|
-
status: v.status || "new",
|
|
89
|
-
}));
|
|
90
|
-
|
|
91
|
-
if (violations.length === 0 && data.status === "fail") {
|
|
92
|
-
violations.push({
|
|
93
|
-
file: "unknown",
|
|
94
|
-
line: "?",
|
|
95
|
-
issue: "Previous run failed but no violations found in JSON",
|
|
96
|
-
status: "new",
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (violations.length === 0) return null;
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
jobId,
|
|
104
|
-
gateName: "",
|
|
105
|
-
entryPoint: "",
|
|
106
|
-
adapterFailures: [
|
|
107
|
-
{
|
|
108
|
-
adapterName: data.adapter,
|
|
109
|
-
reviewIndex: parsed?.reviewIndex,
|
|
110
|
-
violations,
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
logPath: jsonPath.replace(/\.json$/, ".log"),
|
|
114
|
-
};
|
|
115
|
-
} catch (error) {
|
|
116
|
-
log.warn(`Failed to parse JSON review file: ${jsonPath} - ${error}`);
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Extract the log prefix (job ID) from a numbered log filename.
|
|
123
|
-
* Handles both patterns:
|
|
124
|
-
* - Check: `check_src_test.2.log` -> `check_src_test`
|
|
125
|
-
* - Review (new): `review_src_claude@1.2.log` -> `review_src_claude@1`
|
|
126
|
-
*/
|
|
127
|
-
export function extractPrefix(filename: string): string {
|
|
128
|
-
// Pattern: <prefix>.<number>.(log|json)
|
|
129
|
-
const m = filename.match(/^(.+)\.\d+\.(log|json)$/);
|
|
130
|
-
if (m?.[1]) return m[1];
|
|
131
|
-
// Fallback for non-numbered files
|
|
132
|
-
return filename.replace(/\.(log|json)$/, "");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Parses a single log file to extract failures per adapter.
|
|
137
|
-
* Processes both review and check gates.
|
|
138
|
-
*/
|
|
139
|
-
export async function parseLogFile(
|
|
140
|
-
logPath: string,
|
|
141
|
-
): Promise<GateFailures | null> {
|
|
142
|
-
try {
|
|
143
|
-
const content = await fs.readFile(logPath, "utf-8");
|
|
144
|
-
const filename = path.basename(logPath);
|
|
145
|
-
|
|
146
|
-
// Try to parse as review filename with @index pattern
|
|
147
|
-
const parsed = parseReviewFilename(filename);
|
|
148
|
-
const jobId = parsed ? parsed.jobId : extractPrefix(filename);
|
|
149
|
-
|
|
150
|
-
// Check if it's a review log
|
|
151
|
-
if (content.includes("--- Review Output")) {
|
|
152
|
-
const adapterFailures: AdapterFailure[] = [];
|
|
153
|
-
const sectionRegex = /--- Review Output \(([^)]+)\) ---/g;
|
|
154
|
-
|
|
155
|
-
let match: RegExpExecArray | null;
|
|
156
|
-
const sections: { adapter: string; startIndex: number }[] = [];
|
|
157
|
-
|
|
158
|
-
for (;;) {
|
|
159
|
-
match = sectionRegex.exec(content);
|
|
160
|
-
if (!match || !match[1]) break;
|
|
161
|
-
sections.push({
|
|
162
|
-
adapter: match[1],
|
|
163
|
-
startIndex: match.index,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (sections.length === 0) return null;
|
|
168
|
-
|
|
169
|
-
for (let i = 0; i < sections.length; i++) {
|
|
170
|
-
const currentSection = sections[i];
|
|
171
|
-
if (!currentSection) continue;
|
|
172
|
-
const nextSection = sections[i + 1];
|
|
173
|
-
const endIndex = nextSection ? nextSection.startIndex : content.length;
|
|
174
|
-
const sectionContent = content.substring(
|
|
175
|
-
currentSection.startIndex,
|
|
176
|
-
endIndex,
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
const violations: PreviousViolation[] = [];
|
|
180
|
-
const parsedResultMatch = sectionContent.match(
|
|
181
|
-
/---\s*Parsed Result(?:\s+\(([^)]+)\))?\s*---([\s\S]*?)(?:$|---)/,
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
if (parsedResultMatch?.[2]) {
|
|
185
|
-
const parsedContent = parsedResultMatch[2];
|
|
186
|
-
if (parsedContent.includes("Status: PASS")) continue;
|
|
187
|
-
const violationRegex = /^\d+\.\s+(.+?):(\d+|NaN|\?)\s+-\s+(.+)$/gm;
|
|
188
|
-
let vMatch: RegExpExecArray | null;
|
|
189
|
-
for (;;) {
|
|
190
|
-
vMatch = violationRegex.exec(parsedContent);
|
|
191
|
-
if (!vMatch || !vMatch[1] || !vMatch[2] || !vMatch[3]) break;
|
|
192
|
-
const file = vMatch[1].trim();
|
|
193
|
-
let line: number | string = vMatch[2];
|
|
194
|
-
if (line !== "NaN" && line !== "?")
|
|
195
|
-
line = parseInt(line as string, 10);
|
|
196
|
-
const issue = vMatch[3].trim();
|
|
197
|
-
let fix: string | undefined;
|
|
198
|
-
const remainder = parsedContent.substring(
|
|
199
|
-
vMatch.index + vMatch[0].length,
|
|
200
|
-
);
|
|
201
|
-
const fixMatch = remainder.match(/^\s+Fix:\s+(.+)$/m);
|
|
202
|
-
const nextViolationIndex = remainder.search(/^\d+\./m);
|
|
203
|
-
if (
|
|
204
|
-
fixMatch?.index !== undefined &&
|
|
205
|
-
fixMatch[1] &&
|
|
206
|
-
(nextViolationIndex === -1 || fixMatch.index < nextViolationIndex)
|
|
207
|
-
) {
|
|
208
|
-
fix = fixMatch[1].trim();
|
|
209
|
-
}
|
|
210
|
-
violations.push({ file, line, issue, fix });
|
|
211
|
-
}
|
|
212
|
-
} else {
|
|
213
|
-
// Fallback JSON
|
|
214
|
-
const firstBrace = sectionContent.indexOf("{");
|
|
215
|
-
const lastBrace = sectionContent.lastIndexOf("}");
|
|
216
|
-
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
217
|
-
try {
|
|
218
|
-
const jsonStr = sectionContent.substring(
|
|
219
|
-
firstBrace,
|
|
220
|
-
lastBrace + 1,
|
|
221
|
-
);
|
|
222
|
-
const json = JSON.parse(jsonStr);
|
|
223
|
-
if (json.violations && Array.isArray(json.violations)) {
|
|
224
|
-
for (const v of json.violations) {
|
|
225
|
-
if (v.file && v.issue) {
|
|
226
|
-
violations.push({
|
|
227
|
-
file: v.file,
|
|
228
|
-
line: v.line || 0,
|
|
229
|
-
issue: v.issue,
|
|
230
|
-
fix: v.fix,
|
|
231
|
-
status: v.status,
|
|
232
|
-
result: v.result,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
} catch (_e) {}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (violations.length > 0) {
|
|
242
|
-
adapterFailures.push({
|
|
243
|
-
adapterName: currentSection.adapter,
|
|
244
|
-
reviewIndex: parsed?.reviewIndex,
|
|
245
|
-
violations,
|
|
246
|
-
});
|
|
247
|
-
} else if (parsedResultMatch?.[2]?.includes("Status: FAIL")) {
|
|
248
|
-
adapterFailures.push({
|
|
249
|
-
adapterName: currentSection.adapter,
|
|
250
|
-
reviewIndex: parsed?.reviewIndex,
|
|
251
|
-
violations: [
|
|
252
|
-
{
|
|
253
|
-
file: "unknown",
|
|
254
|
-
line: "?",
|
|
255
|
-
issue:
|
|
256
|
-
"Previous run failed but specific violations could not be parsed",
|
|
257
|
-
},
|
|
258
|
-
],
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (adapterFailures.length === 0) return null;
|
|
264
|
-
return { jobId, gateName: "", entryPoint: "", adapterFailures, logPath };
|
|
265
|
-
} else {
|
|
266
|
-
// Check log
|
|
267
|
-
if (content.includes("Result: pass")) return null;
|
|
268
|
-
|
|
269
|
-
const hasFailure =
|
|
270
|
-
content.includes("Result: fail") ||
|
|
271
|
-
content.includes("Result: error") ||
|
|
272
|
-
content.includes("Command failed:");
|
|
273
|
-
|
|
274
|
-
if (!hasFailure) return null;
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
jobId,
|
|
278
|
-
gateName: "",
|
|
279
|
-
entryPoint: "",
|
|
280
|
-
adapterFailures: [
|
|
281
|
-
{
|
|
282
|
-
adapterName: "check",
|
|
283
|
-
violations: [{ file: "check", line: 0, issue: "Check failed" }],
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
logPath,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
} catch (_error) {
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export interface RunIteration {
|
|
295
|
-
iteration: number;
|
|
296
|
-
fixed: Array<{
|
|
297
|
-
jobId: string;
|
|
298
|
-
adapter?: string;
|
|
299
|
-
details: string;
|
|
300
|
-
}>;
|
|
301
|
-
skipped: Array<{
|
|
302
|
-
jobId: string;
|
|
303
|
-
adapter?: string;
|
|
304
|
-
file: string;
|
|
305
|
-
line: number | string;
|
|
306
|
-
issue: string;
|
|
307
|
-
result?: string | null;
|
|
308
|
-
}>;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Reconstructs the history of fixes and skips after all iterations.
|
|
313
|
-
*/
|
|
314
|
-
export async function reconstructHistory(
|
|
315
|
-
logDir: string,
|
|
316
|
-
): Promise<RunIteration[]> {
|
|
317
|
-
try {
|
|
318
|
-
const files = await fs.readdir(logDir);
|
|
319
|
-
const runNumbers = new Set<number>();
|
|
320
|
-
for (const file of files) {
|
|
321
|
-
const m = file.match(/\.(\d+)\.(log|json)$/);
|
|
322
|
-
if (m?.[1]) runNumbers.add(parseInt(m[1], 10));
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const sortedRuns = Array.from(runNumbers).sort((a, b) => a - b);
|
|
326
|
-
const iterations: RunIteration[] = [];
|
|
327
|
-
|
|
328
|
-
let previousFailuresByJob = new Map<string, PreviousViolation[]>();
|
|
329
|
-
|
|
330
|
-
for (const runNum of sortedRuns) {
|
|
331
|
-
const currentFailuresByJob = new Map<string, PreviousViolation[]>();
|
|
332
|
-
const iteration: RunIteration = {
|
|
333
|
-
iteration: runNum,
|
|
334
|
-
fixed: [],
|
|
335
|
-
skipped: [],
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const runFiles = files.filter((f) => f.includes(`.${runNum}.`));
|
|
339
|
-
const prefixes = new Set(runFiles.map((f) => extractPrefix(f)));
|
|
340
|
-
|
|
341
|
-
for (const prefix of prefixes) {
|
|
342
|
-
const jsonFile = runFiles.find(
|
|
343
|
-
(f) => f.startsWith(`${prefix}.${runNum}.`) && f.endsWith(".json"),
|
|
344
|
-
);
|
|
345
|
-
const logFile = runFiles.find(
|
|
346
|
-
(f) => f.startsWith(`${prefix}.${runNum}.`) && f.endsWith(".log"),
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
let failure: GateFailures | null = null;
|
|
350
|
-
if (jsonFile) {
|
|
351
|
-
failure = await parseJsonReviewFile(path.join(logDir, jsonFile));
|
|
352
|
-
} else if (logFile) {
|
|
353
|
-
failure = await parseLogFile(path.join(logDir, logFile));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (failure) {
|
|
357
|
-
for (const af of failure.adapterFailures) {
|
|
358
|
-
const key = af.reviewIndex
|
|
359
|
-
? `${failure.jobId}:${af.reviewIndex}`
|
|
360
|
-
: `${failure.jobId}:${af.adapterName}`;
|
|
361
|
-
currentFailuresByJob.set(key, af.violations);
|
|
362
|
-
|
|
363
|
-
for (const v of af.violations) {
|
|
364
|
-
if (v.status === "skipped") {
|
|
365
|
-
iteration.skipped.push({
|
|
366
|
-
jobId: failure.jobId,
|
|
367
|
-
adapter: af.adapterName,
|
|
368
|
-
file: v.file,
|
|
369
|
-
line: v.line,
|
|
370
|
-
issue: v.issue,
|
|
371
|
-
result: v.result,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
for (const [key, prevViolations] of previousFailuresByJob.entries()) {
|
|
380
|
-
const current = currentFailuresByJob.get(key);
|
|
381
|
-
const sep = key.lastIndexOf(":");
|
|
382
|
-
const jobId = key.substring(0, sep);
|
|
383
|
-
const adapter = key.substring(sep + 1);
|
|
384
|
-
|
|
385
|
-
const trulyFixed = prevViolations.filter((pv) => {
|
|
386
|
-
if (pv.status === "skipped") return false;
|
|
387
|
-
return !current?.some(
|
|
388
|
-
(cv) =>
|
|
389
|
-
cv.file === pv.file &&
|
|
390
|
-
cv.line === pv.line &&
|
|
391
|
-
cv.issue === pv.issue,
|
|
392
|
-
);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
if (trulyFixed.length > 0) {
|
|
396
|
-
if (jobId.startsWith("check_")) {
|
|
397
|
-
iteration.fixed.push({
|
|
398
|
-
jobId,
|
|
399
|
-
details: `${trulyFixed.length} violations resolved`,
|
|
400
|
-
});
|
|
401
|
-
} else {
|
|
402
|
-
for (const f of trulyFixed) {
|
|
403
|
-
iteration.fixed.push({
|
|
404
|
-
jobId,
|
|
405
|
-
adapter,
|
|
406
|
-
details: `${f.file}:${f.line} ${f.issue}`,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
iterations.push(iteration);
|
|
414
|
-
previousFailuresByJob = currentFailuresByJob;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
return iterations;
|
|
418
|
-
} catch (_e) {
|
|
419
|
-
return [];
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Checks if a JSON review file has status "pass" or "skipped_prior_pass".
|
|
425
|
-
* Skipped slots are treated as passing since they represent a previously-passed review.
|
|
426
|
-
*/
|
|
427
|
-
async function isJsonReviewPassing(jsonPath: string): Promise<boolean> {
|
|
428
|
-
try {
|
|
429
|
-
const content = await fs.readFile(jsonPath, "utf-8");
|
|
430
|
-
const data: ReviewFullJsonOutput = JSON.parse(content);
|
|
431
|
-
return data.status === "pass" || data.status === "skipped_prior_pass";
|
|
432
|
-
} catch {
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Checks if a log file represents a passing review.
|
|
439
|
-
* Treats both "Status: PASS" and "Status: skipped_prior_pass" as passing.
|
|
440
|
-
*/
|
|
441
|
-
async function isLogReviewPassing(logPath: string): Promise<boolean> {
|
|
442
|
-
try {
|
|
443
|
-
const content = await fs.readFile(logPath, "utf-8");
|
|
444
|
-
// Check for skipped review log (skipped slots are treated as passing)
|
|
445
|
-
if (content.includes("Status: skipped_prior_pass")) {
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
// Check for review log passing
|
|
449
|
-
if (content.includes("--- Review Output")) {
|
|
450
|
-
return content.includes("Status: PASS");
|
|
451
|
-
}
|
|
452
|
-
// Check for check log passing
|
|
453
|
-
return content.includes("Result: pass");
|
|
454
|
-
} catch {
|
|
455
|
-
return false;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Finds all previous failures and passed slots from the log directory.
|
|
461
|
-
* For review gates with the @<index> pattern, groups by (jobId, reviewIndex)
|
|
462
|
-
* and returns the highest-numbered run for each index.
|
|
463
|
-
* The resulting Map keys are the review index (as string) for lookup by the review gate.
|
|
464
|
-
*
|
|
465
|
-
* Also returns passedSlots: a map of jobId -> reviewIndex -> passIteration
|
|
466
|
-
* for slots that passed in their most recent run.
|
|
467
|
-
*/
|
|
468
|
-
export async function findPreviousFailures(
|
|
469
|
-
logDir: string,
|
|
470
|
-
gateFilter?: string,
|
|
471
|
-
): Promise<GateFailures[]>;
|
|
472
|
-
export async function findPreviousFailures(
|
|
473
|
-
logDir: string,
|
|
474
|
-
gateFilter: string | undefined,
|
|
475
|
-
includePassedSlots: true,
|
|
476
|
-
): Promise<PreviousFailuresResult>;
|
|
477
|
-
export async function findPreviousFailures(
|
|
478
|
-
logDir: string,
|
|
479
|
-
gateFilter?: string,
|
|
480
|
-
includePassedSlots?: boolean,
|
|
481
|
-
): Promise<GateFailures[] | PreviousFailuresResult> {
|
|
482
|
-
try {
|
|
483
|
-
const files = await fs.readdir(logDir);
|
|
484
|
-
const gateFailures: GateFailures[] = [];
|
|
485
|
-
// Map: jobId -> reviewIndex -> { adapter, passIteration }
|
|
486
|
-
const passedSlots = new Map<string, Map<number, PassedSlot>>();
|
|
487
|
-
|
|
488
|
-
// Separate review files (with @index) from check files
|
|
489
|
-
// Group review files by (jobId, reviewIndex) -> highest run number
|
|
490
|
-
const reviewSlotMap = new Map<
|
|
491
|
-
string,
|
|
492
|
-
{ filename: string; runNumber: number; ext: string }
|
|
493
|
-
>();
|
|
494
|
-
const checkPrefixMap = new Map<string, Map<number, Set<string>>>();
|
|
495
|
-
|
|
496
|
-
for (const file of files) {
|
|
497
|
-
const isLog = file.endsWith(".log");
|
|
498
|
-
const isJson = file.endsWith(".json");
|
|
499
|
-
if (!isLog && !isJson) continue;
|
|
500
|
-
if (gateFilter && !file.includes(gateFilter)) continue;
|
|
501
|
-
|
|
502
|
-
const parsed = parseReviewFilename(file);
|
|
503
|
-
if (parsed) {
|
|
504
|
-
// Review file with @index pattern
|
|
505
|
-
const slotKey = `${parsed.jobId}:${parsed.reviewIndex}`;
|
|
506
|
-
const existing = reviewSlotMap.get(slotKey);
|
|
507
|
-
// Update if: no existing entry, higher run number, or same run number but .json (prefer .json over .log)
|
|
508
|
-
const shouldUpdate =
|
|
509
|
-
!existing ||
|
|
510
|
-
parsed.runNumber > existing.runNumber ||
|
|
511
|
-
(parsed.runNumber === existing.runNumber &&
|
|
512
|
-
parsed.ext === "json" &&
|
|
513
|
-
existing.ext === "log");
|
|
514
|
-
if (shouldUpdate) {
|
|
515
|
-
reviewSlotMap.set(slotKey, {
|
|
516
|
-
filename: file,
|
|
517
|
-
runNumber: parsed.runNumber,
|
|
518
|
-
ext: parsed.ext,
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
} else {
|
|
522
|
-
// Check file or legacy review file
|
|
523
|
-
const m = file.match(/^(.+)\.(\d+)\.(log|json)$/);
|
|
524
|
-
if (!m || !m[1] || !m[2] || !m[3]) continue;
|
|
525
|
-
|
|
526
|
-
const prefix = m[1];
|
|
527
|
-
const runNum = parseInt(m[2], 10);
|
|
528
|
-
const ext = m[3];
|
|
529
|
-
|
|
530
|
-
let runMap = checkPrefixMap.get(prefix);
|
|
531
|
-
if (!runMap) {
|
|
532
|
-
runMap = new Map();
|
|
533
|
-
checkPrefixMap.set(prefix, runMap);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
let exts = runMap.get(runNum);
|
|
537
|
-
if (!exts) {
|
|
538
|
-
exts = new Set();
|
|
539
|
-
runMap.set(runNum, exts);
|
|
540
|
-
}
|
|
541
|
-
exts.add(ext);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Process review files grouped by slot (jobId + reviewIndex)
|
|
546
|
-
// Group by jobId to produce a single GateFailures per job
|
|
547
|
-
const jobReviewFailures = new Map<string, AdapterFailure[]>();
|
|
548
|
-
|
|
549
|
-
for (const [slotKey, fileInfo] of reviewSlotMap.entries()) {
|
|
550
|
-
const sepIdx = slotKey.lastIndexOf(":");
|
|
551
|
-
const jobId = slotKey.substring(0, sepIdx);
|
|
552
|
-
const reviewIndex = parseInt(slotKey.substring(sepIdx + 1), 10);
|
|
553
|
-
|
|
554
|
-
// Extract adapter from filename
|
|
555
|
-
const parsed = parseReviewFilename(fileInfo.filename);
|
|
556
|
-
const adapter = parsed?.adapter || "unknown";
|
|
557
|
-
|
|
558
|
-
// Check if this slot passed
|
|
559
|
-
const filePath = path.join(logDir, fileInfo.filename);
|
|
560
|
-
let isPassing = false;
|
|
561
|
-
if (fileInfo.ext === "json") {
|
|
562
|
-
isPassing = await isJsonReviewPassing(filePath);
|
|
563
|
-
} else {
|
|
564
|
-
isPassing = await isLogReviewPassing(filePath);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (isPassing && includePassedSlots) {
|
|
568
|
-
// Record this as a passed slot with adapter info
|
|
569
|
-
let jobSlots = passedSlots.get(jobId);
|
|
570
|
-
if (!jobSlots) {
|
|
571
|
-
jobSlots = new Map();
|
|
572
|
-
passedSlots.set(jobId, jobSlots);
|
|
573
|
-
}
|
|
574
|
-
jobSlots.set(reviewIndex, {
|
|
575
|
-
reviewIndex,
|
|
576
|
-
passIteration: fileInfo.runNumber,
|
|
577
|
-
adapter,
|
|
578
|
-
});
|
|
579
|
-
continue; // Don't process as failure
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
let failure: GateFailures | null = null;
|
|
583
|
-
if (fileInfo.ext === "json") {
|
|
584
|
-
failure = await parseJsonReviewFile(filePath);
|
|
585
|
-
} else {
|
|
586
|
-
failure = await parseLogFile(filePath);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (failure) {
|
|
590
|
-
// Apply status filtering
|
|
591
|
-
for (const af of failure.adapterFailures) {
|
|
592
|
-
af.reviewIndex = reviewIndex;
|
|
593
|
-
const filteredViolations: PreviousViolation[] = [];
|
|
594
|
-
for (const v of af.violations) {
|
|
595
|
-
const status = v.status || "new";
|
|
596
|
-
if (status === "skipped") continue;
|
|
597
|
-
if (
|
|
598
|
-
status !== "new" &&
|
|
599
|
-
status !== "fixed" &&
|
|
600
|
-
status !== "skipped"
|
|
601
|
-
) {
|
|
602
|
-
log.warn(
|
|
603
|
-
`Unexpected status "${status}" for violation in ${jobId}. Treating as "new".`,
|
|
604
|
-
);
|
|
605
|
-
v.status = "new";
|
|
606
|
-
}
|
|
607
|
-
filteredViolations.push(v);
|
|
608
|
-
}
|
|
609
|
-
af.violations = filteredViolations;
|
|
610
|
-
|
|
611
|
-
if (af.violations.length > 0) {
|
|
612
|
-
let failures = jobReviewFailures.get(jobId);
|
|
613
|
-
if (!failures) {
|
|
614
|
-
failures = [];
|
|
615
|
-
jobReviewFailures.set(jobId, failures);
|
|
616
|
-
}
|
|
617
|
-
failures.push(af);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
for (const [jobId, adapterFailures] of jobReviewFailures.entries()) {
|
|
624
|
-
gateFailures.push({
|
|
625
|
-
jobId,
|
|
626
|
-
gateName: "",
|
|
627
|
-
entryPoint: "",
|
|
628
|
-
adapterFailures,
|
|
629
|
-
logPath: path.join(logDir, `${jobId}.log`),
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Process check files (non-review)
|
|
634
|
-
for (const [prefix, runMap] of checkPrefixMap.entries()) {
|
|
635
|
-
const latestRun = Math.max(...runMap.keys());
|
|
636
|
-
const exts = runMap.get(latestRun);
|
|
637
|
-
if (!exts) continue;
|
|
638
|
-
|
|
639
|
-
let failure: GateFailures | null = null;
|
|
640
|
-
if (exts.has("json")) {
|
|
641
|
-
failure = await parseJsonReviewFile(
|
|
642
|
-
path.join(logDir, `${prefix}.${latestRun}.json`),
|
|
643
|
-
);
|
|
644
|
-
} else if (exts.has("log")) {
|
|
645
|
-
failure = await parseLogFile(
|
|
646
|
-
path.join(logDir, `${prefix}.${latestRun}.log`),
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (failure) {
|
|
651
|
-
for (const af of failure.adapterFailures) {
|
|
652
|
-
const filteredViolations: PreviousViolation[] = [];
|
|
653
|
-
for (const v of af.violations) {
|
|
654
|
-
const status = v.status || "new";
|
|
655
|
-
if (status === "skipped") continue;
|
|
656
|
-
if (
|
|
657
|
-
status !== "new" &&
|
|
658
|
-
status !== "fixed" &&
|
|
659
|
-
status !== "skipped"
|
|
660
|
-
) {
|
|
661
|
-
log.warn(
|
|
662
|
-
`Unexpected status "${status}" for violation in ${failure.jobId}. Treating as "new".`,
|
|
663
|
-
);
|
|
664
|
-
v.status = "new";
|
|
665
|
-
}
|
|
666
|
-
filteredViolations.push(v);
|
|
667
|
-
}
|
|
668
|
-
af.violations = filteredViolations;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const totalViolations = failure.adapterFailures.reduce(
|
|
672
|
-
(sum, af) => sum + af.violations.length,
|
|
673
|
-
0,
|
|
674
|
-
);
|
|
675
|
-
if (totalViolations > 0) {
|
|
676
|
-
gateFailures.push(failure);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (includePassedSlots) {
|
|
682
|
-
return { failures: gateFailures, passedSlots };
|
|
683
|
-
}
|
|
684
|
-
return gateFailures;
|
|
685
|
-
} catch (error: unknown) {
|
|
686
|
-
if (
|
|
687
|
-
typeof error === "object" &&
|
|
688
|
-
error !== null &&
|
|
689
|
-
"code" in error &&
|
|
690
|
-
(error as { code: string }).code === "ENOENT"
|
|
691
|
-
) {
|
|
692
|
-
return includePassedSlots ? { failures: [], passedSlots: new Map() } : [];
|
|
693
|
-
}
|
|
694
|
-
return includePassedSlots ? { failures: [], passedSlots: new Map() } : [];
|
|
695
|
-
}
|
|
696
|
-
}
|
package/src/utils/sanitizer.ts
DELETED