File size: 3,825 Bytes
763ef0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Error classifier - identifies error categories from tracebacks/output.
Used by repair engine to produce targeted repair plans.
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Optional


@dataclass
class ErrorClass:
    category: str
    detail: str
    suggested_fix: str


RULES = [
    # python module / pip
    (r"ModuleNotFoundError: No module named ['\"]([\w\.\-]+)['\"]", "missing_python_module"),
    (r"ImportError: No module named ([\w\.\-]+)", "missing_python_module"),
    (r"pip(?:3)?: command not found", "missing_pip"),
    # node / npm
    (r"command not found: (npm|node|npx)", "missing_node"),
    (r"npm ERR! code E?ENOENT", "npm_failure"),
    (r"npm ERR! code (E\w+)", "npm_failure"),
    # playwright
    (r"playwright[^\s]*: command not found", "missing_playwright"),
    (r"Executable doesn't exist at .+(chrom|firefox|webkit)", "playwright_browsers_missing"),
    (r"BrowserType\.launch:.*Host system is missing dependencies", "playwright_missing_deps"),
    # git
    (r"fatal: unable to auto-detect email address", "git_identity_missing"),
    (r"Please tell me who you are\.", "git_identity_missing"),
    (r"fatal: not a git repository", "not_a_git_repo"),
    (r"Authentication failed", "git_auth_failed"),
    # python version / build
    (r"greenlet[^\n]*failed to build", "greenlet_build_failure"),
    (r"Could not build wheels for ([\w\-]+)", "python_build_failure"),
    (r"requires Python ['\"][^'\"]+['\"]", "python_version_mismatch"),
    # network
    (r"Could not resolve host", "network_failure"),
    (r"Connection refused", "network_failure"),
    (r"Read timed out", "network_failure"),
    # http
    (r"\b429\b", "rate_limited"),
    (r"\b401\b|\b403\b", "auth_failure"),
]


def classify(output: str) -> Optional[ErrorClass]:
    if not output:
        return None
    for pattern, category in RULES:
        m = re.search(pattern, output)
        if m:
            detail = m.group(0)
            return ErrorClass(category=category, detail=detail, suggested_fix=_fix_for(category, m))
    # Generic exception detection
    if "Traceback (most recent call last):" in output:
        return ErrorClass(category="python_exception", detail="Unclassified Python exception", suggested_fix="inspect traceback and retry")
    return None


def _fix_for(category: str, m: re.Match) -> str:
    if category == "missing_python_module":
        return f"pip install {m.group(1)}"
    if category == "missing_pip":
        return "ensure python3-pip is installed"
    if category == "missing_node":
        return "install node + npm"
    if category == "npm_failure":
        return "delete node_modules and reinstall"
    if category == "missing_playwright":
        return "pip install playwright && python -m playwright install"
    if category == "playwright_browsers_missing":
        return "python -m playwright install chromium"
    if category == "playwright_missing_deps":
        return "python -m playwright install-deps chromium"
    if category == "git_identity_missing":
        return "git config user.email and user.name"
    if category == "not_a_git_repo":
        return "git init or cd into repo"
    if category == "git_auth_failed":
        return "verify GITHUB_TOKEN and remote URL"
    if category == "greenlet_build_failure":
        return "pin Python 3.11 and install build essentials"
    if category == "python_build_failure":
        return "install build-essential and retry"
    if category == "python_version_mismatch":
        return "use Python 3.11"
    if category == "network_failure":
        return "retry after backoff"
    if category == "rate_limited":
        return "rotate provider key or wait"
    if category == "auth_failure":
        return "rotate API key"
    return "retry"