/** * 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; 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, 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)._selfLearningCache as { preCheck?: (t: string, a: Record) => { 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, }; }