File size: 9,455 Bytes
7d580fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
error_classifier.py β€” S404: Classificazione errori nel repair loop

Documento strategico: "Non basta 'vedere un errore'. Devi distinguere almeno:
selettore sbagliato, pagina diversa dal previsto, login fallito, errore runtime,
rete lenta, frame non caricato, stato UI incoerente."

Implementa la classificazione per consentire repair MIRATI invece di analisi generiche.

Integrazione:
  _reflective_debug β†’ classify_error(goal, errors) β†’ ErrorResult
  β†’ inject repair_strategy nel context prima del retry

Pipeline classificazione:
  1. Pattern matching regex (deterministico, zero latenza)
  2. Fallback: UNKNOWN β†’ generic strategic recovery

Non usa LLM per classificare β€” Γ¨ sincrono e costa zero latency.
"""

import re
from dataclasses import dataclass
from enum import Enum


class ErrorCategory(str, Enum):
    SELECTOR   = "selector"    # CSS/DOM selector non trovato
    NAVIGATION = "navigation"  # URL sbagliato, pagina non trovata, redirect
    AUTH       = "auth"        # Login fallito, token scaduto, 401/403
    RUNTIME    = "runtime"     # TypeError, ReferenceError, is not defined
    NETWORK    = "network"     # Timeout, ECONNREFUSED, offline, 503
    FRAME      = "frame"       # iframe non caricato, frame non trovato
    SYNTAX     = "syntax"      # SyntaxError, IndentationError, parse error
    LOGIC      = "logic"       # AssertionError, valore atteso β‰  ottenuto
    LIMIT      = "limit"       # Rate limit, quota, 429, OOM
    DB_ERROR   = "db_error"    # Sprint 3b: IntegrityError, FK, connection pool, deadlock
    UNKNOWN    = "unknown"     # Non classificato


# ── Strategie di repair per categoria ─────────────────────────────────────────
_REPAIR_STRATEGIES: dict[ErrorCategory, str] = {
    ErrorCategory.SELECTOR: (
        "SELETTORE DOM: usa ID espliciti (`#id`), aria-label (`[aria-label='...']`) "
        "o ruoli ARIA (`getByRole('button', {name:'...'})`) β€” evita classi dinamiche. "
        "Prima leggi il DOM attuale per trovare il selettore corretto."
    ),
    ErrorCategory.NAVIGATION: (
        "NAVIGAZIONE: verifica l'URL esatto prima di navigare. "
        "Attendi `domcontentloaded` o un elemento specifico prima di operare sulla pagina. "
        "Gestisci redirect e pagine di errore (404/500) esplicitamente."
    ),
    ErrorCategory.AUTH: (
        "AUTENTICAZIONE: gestisci il login PRIMA di qualsiasi azione protetta. "
        "Verifica il token, gestisci la scadenza della sessione e i redirect post-login. "
        "Non assumere che la sessione sia giΓ  attiva."
    ),
    ErrorCategory.RUNTIME: (
        "ERRORE RUNTIME: controlla undefined/null con optional chaining (`?.`) "
        "o guardie esplicite (`if (x == null) return`). "
        "Verifica i tipi degli argomenti prima di usarli. "
        "Aggiungi try/catch attorno alle operazioni rischiose."
    ),
    ErrorCategory.NETWORK: (
        "ERRORE RETE: aggiungi retry con exponential backoff (max 3 tentativi). "
        "Gestisci offline/timeout con fallback utili. "
        "Non bloccare la UI durante le chiamate β€” usa stato di loading."
    ),
    ErrorCategory.FRAME: (
        "IFRAME: aspetta che l'iframe sia caricato prima di accedere al suo DOM "
        "(usa `waitForLoadState` o `frameLocator`). "
        "Verifica che il frame esista con `page.frames()` prima di operare."
    ),
    ErrorCategory.SYNTAX: (
        "SINTASSI: correggi l'errore di sintassi PRIMA di qualsiasi altra cosa. "
        "Verifica l'indentazione (Python), le parentesi bilanciate e i punti e virgola (JS). "
        "Usa un linter per identificare tutti gli errori nella stessa sessione."
    ),
    ErrorCategory.LOGIC: (
        "LOGICA: rivedi i casi edge e le assunzioni. "
        "Aggiungi asserzioni esplicite sui valori attesi. "
        "Traccia il flusso di dati passo per passo per trovare dove diverge dal previsto."
    ),
    ErrorCategory.LIMIT: (
        "RATE LIMIT / QUOTA: implementa retry con backoff esponenziale e jitter. "
        "Riduci la frequenza delle chiamate. "
        "Considera caching dei risultati per evitare chiamate ridondanti."
    ),
    ErrorCategory.DB_ERROR: (
        "ERRORE DATABASE: controlla vincoli di integritΓ  (FK, UNIQUE, NOT NULL). "
        "Per IntegrityError: verifica che i dati rispettino i vincoli prima dell'insert. "
        "Per connection pool exhausted: chiudi le connessioni non usate, riduci max_connections. "
        "Per deadlock: usa retry con backoff, considera SKIP LOCKED per job queue. "
        "Per 'relation does not exist': esegui le migrazioni pendenti prima dell'avvio."
    ),
    ErrorCategory.UNKNOWN: (
        "Analizza il contesto completo dell'errore. "
        "Prova un approccio COMPLETAMENTE diverso da quello usato finora."
    ),
}

# ── Pattern regex per classificazione ─────────────────────────────────────────
# Ordinati per prioritΓ  (il primo match vince)
_PATTERNS: list[tuple[ErrorCategory, re.Pattern]] = [
    (ErrorCategory.SYNTAX, re.compile(
        r"SyntaxError|IndentationError|ParseError|parse error|"
        r"unexpected token|unexpected EOF|invalid syntax|"
        r"unterminated string|missing \)", re.IGNORECASE,
    )),
    (ErrorCategory.SELECTOR, re.compile(
        r"selector|querySelector|getElementById|no element|"
        r"element not found|locator|aria|getByRole|"
        r"TimeoutError.*waiting for|strict mode violation", re.IGNORECASE,
    )),
    (ErrorCategory.AUTH, re.compile(
        r"401|403|Unauthorized|Forbidden|Authentication|"
        r"token.*expired|session.*expired|login.*failed|"
        r"invalid.*credentials|permission denied", re.IGNORECASE,
    )),
    (ErrorCategory.NETWORK, re.compile(
        r"ECONNREFUSED|ENOTFOUND|ETIMEDOUT|timeout|"
        r"network error|fetch.*failed|connection refused|"
        r"503|502|504|unreachable|offline", re.IGNORECASE,
    )),
    (ErrorCategory.LIMIT, re.compile(
        r"429|rate.?limit|quota.*exceeded|too many requests|"
        r"OOM|out of memory|memory.*error", re.IGNORECASE,
    )),
    (ErrorCategory.FRAME, re.compile(
        r"frame|iframe|frameLocator|frame.*not found|"
        r"cross.?origin|sandboxed.*frame", re.IGNORECASE,
    )),
    (ErrorCategory.NAVIGATION, re.compile(
        r"404|page not found|navigation.*failed|goto.*timeout|"
        r"ERR_NAME_NOT_RESOLVED|net::ERR|redirect.*loop|"
        r"wrong.*page|URL.*invalid", re.IGNORECASE,
    )),
    (ErrorCategory.RUNTIME, re.compile(
        r"TypeError|ReferenceError|is not defined|"
        r"Cannot read|Cannot set|undefined is not|"
        r"null.*property|property.*null|"
        r"is not a function|is not iterable|"
        r"UnhandledPromiseRejection", re.IGNORECASE,
    )),
    (ErrorCategory.LOGIC, re.compile(
        r"AssertionError|assertion.*failed|expected.*got|"
        r"mismatch|wrong.*value|incorrect.*result|"
        r"test.*failed|expected.*received", re.IGNORECASE,
    )),
    # Sprint 3b: DB_ERROR β€” IntegrityError, FK, connection pool, deadlock
    (ErrorCategory.DB_ERROR, re.compile(
        r"IntegrityError|ForeignKey|ForeignKeyViolation|"
        r"connection pool|deadlock|"
        r"relation.*not.*exist|table.*not.*exist|"
        r"duplicate key|violates.*constraint|"
        r"UniqueViolation|CheckViolation|NotNullViolation|"
        r"OperationalError.*database|psycopg|asyncpg|"
        r"FATAL.*database|could not connect.*database",
        re.IGNORECASE,
    )),
]


@dataclass
class ErrorResult:
    category:        ErrorCategory
    repair_strategy: str
    confidence:      float     # 0.0-1.0 β€” quanto siamo sicuri della classificazione
    matched_pattern: str       # substring che ha fatto match (debug)


def classify_error(errors: list[str]) -> ErrorResult:
    """
    Classifica una lista di messaggi di errore nella categoria piΓΉ probabile.

    Algoritmo:
    1. Concatena gli errori (max 1500 chars)
    2. Prova i pattern in ordine di prioritΓ  (il primo match vince)
    3. Se nessun match β†’ UNKNOWN con confidence 0.0

    Non usa LLM β€” Γ¨ sincrono e a zero latency.
    Chiamato da _reflective_debug prima di invocare ARCHITECT.
    """
    combined = " | ".join(str(e)[:500] for e in errors[-4:])[:1500]  # S604: 400β†’500 per error string completa

    for category, pattern in _PATTERNS:
        m = pattern.search(combined)
        if m:
            matched = m.group(0)[:60]
            strategy = _REPAIR_STRATEGIES[category]
            return ErrorResult(
                category        = category,
                repair_strategy = strategy,
                confidence      = 0.85,
                matched_pattern = matched,
            )

    return ErrorResult(
        category        = ErrorCategory.UNKNOWN,
        repair_strategy = _REPAIR_STRATEGIES[ErrorCategory.UNKNOWN],
        confidence      = 0.0,
        matched_pattern = "",
    )


def format_for_context(result: ErrorResult) -> str:
    """
    Produce la stringa da iniettare nel context del loop prima del retry.
    Formato: [ERRORE CLASSIFICATO: <Categoria>] + strategia.
    """
    label = f"ERRORE CLASSIFICATO: {result.category.value.upper()}"
    if result.matched_pattern:
        label += f" (match: \"{result.matched_pattern}\")"
    return f"\n\n[{label}]\n{result.repair_strategy}"