AUDIT / src /lib /agentLoop /errorSignatureBuilder.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
10.8 kB
/**
* errorSignatureBuilder.ts — S697: Structured error diagnosis for self-healing
*
* Builds a rich ErrorSignature from raw error strings:
* - 16 categories vs 8 flat regex (previous adaptiveSolver)
* - Python traceback frame parsing (File "x.py", line N, in fn)
* - JS/TS stack trace parsing (at fn (file:line:col))
* - missingModule / missingSymbol extraction
* - repairComplexity: trivial → simple → complex → requires_rewrite
* - isTransient flag: timeout/network = retry; syntax = must change code
* - Anti-loop fingerprint (djb2 hash, Safari-safe, no crypto)
*/
export type ErrorCategory =
| "import_error"
| "type_error"
| "syntax_error"
| "reference_error"
| "assertion_fail"
| "runtime_crash"
| "timeout"
| "network_fail"
| "cors"
| "permission"
| "rate_limit"
| "not_found"
| "dependency"
| "build_fail"
| "quota"
| "unknown";
export type RepairComplexity = "trivial" | "simple" | "complex" | "requires_rewrite";
export interface TracebackFrame {
file: string;
line: number;
column?: number;
fn?: string;
}
export interface ErrorSignature {
raw: string;
category: ErrorCategory;
exitCode?: number;
stderrLines: string[];
errorType?: string;
errorMessage?: string;
failedFile?: string;
failedLine?: number;
failedColumn?: number;
missingModule?: string;
missingSymbol?: string;
tracebackFrames: TracebackFrame[];
isTransient: boolean;
repairComplexity: RepairComplexity;
tool: string;
args: Record<string, unknown>;
iteration: number;
attemptNumber: number;
isKnown: boolean;
knownFix?: string;
fingerprint: string;
}
// ─── Frame parsers ────────────────────────────────────────────────────────────
function parsePythonTraceback(raw: string): TracebackFrame[] {
const frames: TracebackFrame[] = [];
const re = /File\s+"([^"]+)",\s+line\s+(\d+)(?:,\s+in\s+(\S+))?/g;
let m: RegExpExecArray | null;
while ((m = re.exec(raw)) !== null) {
frames.push({ file: m[1], line: parseInt(m[2]), fn: m[3] });
}
return frames;
}
function parseJsStack(raw: string): TracebackFrame[] {
const frames: TracebackFrame[] = [];
const re = /at\s+(?:(\S+)\s+\()?([^\s:()]+):(\d+):(\d+)\)?/g;
let m: RegExpExecArray | null;
while ((m = re.exec(raw)) !== null) {
frames.push({ fn: m[1] ?? undefined, file: m[2], line: parseInt(m[3]), column: parseInt(m[4]) });
}
return frames.slice(0, 10);
}
// ─── Extractors ───────────────────────────────────────────────────────────────
function extractMissingModule(raw: string): string | undefined {
const jsM = raw.match(/(?:Cannot find module|Could not resolve|Module not found)[^'"]*['"]([^'"]+)['"]/i);
if (jsM) return jsM[1];
const pyM = raw.match(/(?:No module named|ModuleNotFoundError)[^'"]*['"]?([a-zA-Z0-9_.\-]+)['"]?/i);
if (pyM) return pyM[1];
const pyI = raw.match(/cannot import name\s+['"]([^'"]+)['"]/i);
if (pyI) return pyI[1];
return undefined;
}
function extractMissingSymbol(raw: string): string | undefined {
const r1 = raw.match(/ReferenceError:\s+(\S+)\s+is not defined/i);
if (r1) return r1[1];
const r2 = raw.match(/TypeError:\s+(\S+)\s+is not a (?:function|constructor)/i);
if (r2) return r2[1];
const r3 = raw.match(/Cannot (?:read|set) properties of (?:undefined|null) \((?:reading|setting) '([^']+)'\)/i);
if (r3) return r3[1];
return undefined;
}
function extractFailedLine(raw: string): number | undefined {
const m = raw.match(/(?::(\d+):\d+|at line (\d+)|line (\d+),|\((\d+)\))/);
if (m) return parseInt(m[1] ?? m[2] ?? m[3] ?? m[4]);
return undefined;
}
function extractFailedFile(raw: string): string | undefined {
const m = raw.match(/(?:File "([^"]+)"|at\s+\S+\s+\(([^:)]+):\d+:\d+\)|Error in ([^\n:]+\.(?:ts|js|tsx|jsx|py)))/);
if (m) return m[1] ?? m[2] ?? m[3];
return undefined;
}
function djb2Fingerprint(category: ErrorCategory, msg: string, tool: string): string {
const key = `${category}:${msg.slice(0, 80).replace(/\s+/g, " ")}:${tool}`;
let h = 5381;
for (let i = 0; i < key.length; i++) {
h = (((h << 5) + h) ^ key.charCodeAt(i)) >>> 0;
}
return h.toString(16);
}
// ─── Main classifier ──────────────────────────────────────────────────────────
export function buildErrorSignature(
raw: string,
tool: string,
args: Record<string, unknown>,
iteration: number,
attemptNumber: number,
): ErrorSignature {
const lines = raw.split("\n").filter(l => l.trim().length > 0);
let category: ErrorCategory = "unknown";
let errorType: string | undefined;
let errorMessage: string | undefined;
let isTransient = false;
let repairComplexity: RepairComplexity = "complex";
let missingModule: string | undefined;
let missingSymbol: string | undefined;
const tracebackFrames: TracebackFrame[] = [];
// ── Classification priority order ─────────────────────────────────────────
if (/cannot find module|could not resolve|module not found|no module named|modulenotfounderror|importerror|cannot import name/i.test(raw)) {
category = "import_error"; missingModule = extractMissingModule(raw);
repairComplexity = missingModule ? "trivial" : "simple"; errorType = "ImportError";
} else if (/referenceerror|is not defined/i.test(raw)) {
category = "reference_error"; missingSymbol = extractMissingSymbol(raw);
repairComplexity = missingSymbol ? "trivial" : "simple"; errorType = "ReferenceError";
} else if (/typeerror|is not a function|cannot read propert|cannot set propert|is not a constructor/i.test(raw)) {
category = "type_error"; missingSymbol = extractMissingSymbol(raw);
repairComplexity = missingSymbol ? "simple" : "complex"; errorType = "TypeError";
} else if (/syntaxerror|unexpected token|unterminated string|unexpected end|parse error|invalid syntax/i.test(raw)) {
category = "syntax_error"; repairComplexity = "simple"; errorType = "SyntaxError";
} else if (/assertionerror|assertion failed|expected.*to.*equal|test.*failed/i.test(raw)) {
category = "assertion_fail"; repairComplexity = "requires_rewrite"; errorType = "AssertionError";
} else if (/429|rate.?limit|too many requests|ratelimitexceeded/i.test(raw)) {
category = "rate_limit"; isTransient = true; repairComplexity = "trivial";
} else if (/401|403|forbidden|unauthorized|access denied|permission denied|eperm/i.test(raw)) {
category = "permission"; repairComplexity = "simple";
} else if (/cors|cross.?origin|access.control.allow.origin|blocked by cors/i.test(raw)) {
category = "cors"; isTransient = true; repairComplexity = "trivial";
} else if (/timeout|timed.?out|etimedout|operation.*timeout|abortError/i.test(raw)) {
category = "timeout"; isTransient = true; repairComplexity = "trivial";
} else if (/econnrefused|enotfound|failed to fetch|network error|net::err|fetch.*failed|connection.*refused/i.test(raw)) {
category = "network_fail"; isTransient = true; repairComplexity = "trivial";
} else if (/404|enoent|file not found|no such file|path.*does not exist/i.test(raw)) {
category = "not_found"; repairComplexity = "simple";
} else if (/quota|storage full|no space left|disk.*full|exceeded.*storage/i.test(raw)) {
category = "quota"; repairComplexity = "complex";
} else if (/peer dependency|version mismatch|incompatible.*version|dependency.*conflict/i.test(raw)) {
category = "dependency"; repairComplexity = "simple";
} else if (/build failed|compilation error|tsc.*error|vite.*build.*error|rollup.*error/i.test(raw)) {
category = "build_fail"; repairComplexity = "complex";
} else if (/exit code [^0]|exited with \d+|uncaught exception|unhandled.*rejection|segfault/i.test(raw)) {
category = "runtime_crash"; repairComplexity = "complex";
}
// ── Traceback frames ───────────────────────────────────────────────────────
if (/traceback \(most recent call last\)/i.test(raw)) {
tracebackFrames.push(...parsePythonTraceback(raw));
} else if (/\bat\s+(?:\S+\s+)?\([^)]+:\d+:\d+\)/.test(raw)) {
tracebackFrames.push(...parseJsStack(raw));
}
// ── Error type/message from first error line ───────────────────────────────
const firstErrLine = lines.find(l => /Error|error|Exception/i.test(l)) ?? lines[0] ?? "";
const typeMatch = firstErrLine.match(/^([A-Z][a-zA-Z]+Error|[A-Z][a-zA-Z]+Exception):\s*(.+)/);
if (typeMatch && !errorType) {
errorType = typeMatch[1];
errorMessage = typeMatch[2]?.trim().slice(0, 200);
}
// ── Numeric fields ─────────────────────────────────────────────────────────
const failedLine = extractFailedLine(raw);
const failedFile = extractFailedFile(raw);
const colMatch = raw.match(/:(\d+):(\d+)/);
const failedColumn = colMatch ? parseInt(colMatch[2]) : undefined;
// ── SelfLearning integration (best-effort) ─────────────────────────────────
let isKnown = false; let knownFix: string | undefined;
try {
// Dynamic import to avoid circular dependency
const sl = (globalThis as Record<string, unknown>)._selfLearningCache as { preCheck?: (t: string, a: Record<string, unknown>) => { hasKnownIssue: boolean; suggestion?: string } } | undefined;
if (sl?.preCheck) { const pc = sl.preCheck(tool, args); if (pc.hasKnownIssue) { isKnown = true; knownFix = pc.suggestion; } }
} catch { /* non-blocking */ }
const fingerprint = djb2Fingerprint(category, errorMessage ?? raw.slice(0, 80), tool);
return {
raw: raw.slice(0, 500),
category,
stderrLines: lines.slice(0, 20),
errorType,
errorMessage,
failedFile,
failedLine,
failedColumn,
missingModule,
missingSymbol,
tracebackFrames,
isTransient,
repairComplexity,
tool,
args,
iteration,
attemptNumber,
isKnown,
knownFix,
fingerprint,
};
}