File size: 10,809 Bytes
95eb75a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/**
 * 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,
  };
}