@saulwade/swl-ses 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +4 -3
- package/README.md +15 -14
- package/agentes/nemesis-auditor-swl.md +161 -0
- package/bin/swl-mcp-server.js +187 -187
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/nemesis.md +122 -0
- package/comandos/swl/salud.md +34 -0
- package/comandos/swl/verificar.md +45 -0
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/feynman-auditor-swl/SKILL.md +123 -0
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -0
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -0
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -0
- package/habilidades/testing-python/SKILL.md +340 -340
- package/habilidades/web-fetcher-routing/SKILL.md +75 -0
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -0
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/modulos.json +41 -6
- package/manifiestos/perfiles.json +2 -1
- package/manifiestos/skills-lock.json +30 -9
- package/package.json +2 -2
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +10 -2
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/usar-context7.md +226 -226
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/audit-tools/audit-history.js +330 -0
- package/scripts/audit-tools/bundle-tracker.js +290 -0
- package/scripts/audit-tools/canary-monitor.js +352 -0
- package/scripts/audit-tools/code-profiler.js +605 -0
- package/scripts/audit-tools/dep-doctor.js +320 -0
- package/scripts/audit-tools/env-validator.js +206 -0
- package/scripts/audit-tools/lib/fs-walk.js +48 -0
- package/scripts/audit-tools/lib/output.js +23 -0
- package/scripts/audit-tools/migration-checker.js +392 -0
- package/scripts/audit-tools/pentest-scanner.js +1436 -0
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/manifiestos.js +42 -1
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/README.md +128 -128
- package/scripts/mcp-server/handlers.js +206 -206
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-manifest.js +231 -195
- package/scripts/validar-userland-vacio.js +110 -110
|
@@ -1,308 +1,308 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Error Classifier — Clasificación tipada de errores de API con hints de recuperación.
|
|
5
|
-
*
|
|
6
|
-
* Patrón adoptado de Hermes Agent (agent/error_classifier.py).
|
|
7
|
-
* Clasifica errores HTTP y de red en categorías con hints de acción:
|
|
8
|
-
* - retryable: ¿vale la pena reintentar?
|
|
9
|
-
* - shouldCompress: ¿hay que comprimir contexto?
|
|
10
|
-
* - shouldFallback: ¿cambiar de modelo?
|
|
11
|
-
* - shouldRotateCredential: ¿rotar API key?
|
|
12
|
-
*
|
|
13
|
-
* Zero dependencias externas.
|
|
14
|
-
*
|
|
15
|
-
* Uso:
|
|
16
|
-
* const { clasificarError, RAZON } = require('./lib/error-classifier');
|
|
17
|
-
*
|
|
18
|
-
* try {
|
|
19
|
-
* await llamadaAPI();
|
|
20
|
-
* } catch (error) {
|
|
21
|
-
* const clasificado = clasificarError(error);
|
|
22
|
-
* if (clasificado.retryable) {
|
|
23
|
-
* // reintentar con jitteredBackoff
|
|
24
|
-
* } else if (clasificado.shouldCompress) {
|
|
25
|
-
* // comprimir contexto y reintentar
|
|
26
|
-
* } else if (clasificado.shouldFallback) {
|
|
27
|
-
* // cambiar a modelo alternativo
|
|
28
|
-
* }
|
|
29
|
-
* }
|
|
30
|
-
*
|
|
31
|
-
* @module hooks/lib/error-classifier
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Razones de error (enum-like)
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Razones de error clasificadas.
|
|
40
|
-
* Cada razón tiene un significado semántico y sugiere una acción de recuperación.
|
|
41
|
-
*
|
|
42
|
-
* @readonly
|
|
43
|
-
* @enum {string}
|
|
44
|
-
*/
|
|
45
|
-
const RAZON = Object.freeze({
|
|
46
|
-
// Autenticación
|
|
47
|
-
AUTH: 'auth', // 401/403 transitorio — rotar credential
|
|
48
|
-
AUTH_PERMANENTE: 'auth_permanente', // 401/403 persistente — abortar
|
|
49
|
-
|
|
50
|
-
// Facturación / cuota
|
|
51
|
-
BILLING: 'billing', // 402 o créditos agotados — rotar inmediato
|
|
52
|
-
RATE_LIMIT: 'rate_limit', // 429 o throttling — backoff + rotar
|
|
53
|
-
|
|
54
|
-
// Servidor
|
|
55
|
-
SOBRECARGADO: 'sobrecargado', // 503/529 — backoff largo
|
|
56
|
-
ERROR_SERVIDOR: 'error_servidor', // 500/502 — retry corto
|
|
57
|
-
|
|
58
|
-
// Transporte
|
|
59
|
-
TIMEOUT: 'timeout', // Timeout de conexión/lectura
|
|
60
|
-
|
|
61
|
-
// Contexto / payload
|
|
62
|
-
CONTEXTO_EXCEDIDO: 'contexto_excedido', // Context length exceeded — comprimir
|
|
63
|
-
PAYLOAD_GRANDE: 'payload_grande', // 413 — comprimir
|
|
64
|
-
|
|
65
|
-
// Modelo
|
|
66
|
-
MODELO_NO_ENCONTRADO: 'modelo_no_encontrado', // 404/invalid model — fallback
|
|
67
|
-
|
|
68
|
-
// Formato de request
|
|
69
|
-
ERROR_FORMATO: 'error_formato', // 400 — revisar request
|
|
70
|
-
|
|
71
|
-
// Catch-all
|
|
72
|
-
DESCONOCIDO: 'desconocido', // No clasificable — retry con backoff
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
// Patrones de detección por mensaje de error
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
/** @type {Array<{ patron: RegExp, razon: string }>} */
|
|
80
|
-
const PATRONES_BILLING = [
|
|
81
|
-
{ patron: /insufficient\s+credits?/i, razon: RAZON.BILLING },
|
|
82
|
-
{ patron: /credit\s+balance/i, razon: RAZON.BILLING },
|
|
83
|
-
{ patron: /billing\s+hard\s+limit/i, razon: RAZON.BILLING },
|
|
84
|
-
{ patron: /payment\s+required/i, razon: RAZON.BILLING },
|
|
85
|
-
{ patron: /quota\s+exceeded/i, razon: RAZON.BILLING },
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
const PATRONES_RATE_LIMIT = [
|
|
89
|
-
{ patron: /rate\s+limit/i, razon: RAZON.RATE_LIMIT },
|
|
90
|
-
{ patron: /too\s+many\s+requests/i, razon: RAZON.RATE_LIMIT },
|
|
91
|
-
{ patron: /throttl/i, razon: RAZON.RATE_LIMIT },
|
|
92
|
-
{ patron: /requests?\s+per\s+(minute|second|hour)/i, razon: RAZON.RATE_LIMIT },
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
const PATRONES_CONTEXTO = [
|
|
96
|
-
{ patron: /context\s+length/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
97
|
-
{ patron: /context\s+size/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
98
|
-
{ patron: /maximum\s+context/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
99
|
-
{ patron: /token\s+limit/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
100
|
-
{ patron: /max_tokens/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
101
|
-
{ patron: /too\s+long/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
const PATRONES_PAYLOAD = [
|
|
105
|
-
{ patron: /request\s+entity\s+too\s+large/i, razon: RAZON.PAYLOAD_GRANDE },
|
|
106
|
-
{ patron: /error\s+code:\s*413/i, razon: RAZON.PAYLOAD_GRANDE },
|
|
107
|
-
{ patron: /payload\s+too\s+large/i, razon: RAZON.PAYLOAD_GRANDE },
|
|
108
|
-
];
|
|
109
|
-
|
|
110
|
-
const PATRONES_TIMEOUT = [
|
|
111
|
-
{ patron: /timed?\s*out/i, razon: RAZON.TIMEOUT },
|
|
112
|
-
{ patron: /ETIMEDOUT/i, razon: RAZON.TIMEOUT },
|
|
113
|
-
{ patron: /ECONNRESET/i, razon: RAZON.TIMEOUT },
|
|
114
|
-
{ patron: /ECONNREFUSED/i, razon: RAZON.TIMEOUT },
|
|
115
|
-
{ patron: /socket\s+hang\s+up/i, razon: RAZON.TIMEOUT },
|
|
116
|
-
{ patron: /network\s+error/i, razon: RAZON.TIMEOUT },
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
const PATRONES_MODELO = [
|
|
120
|
-
{ patron: /model\s+not\s+found/i, razon: RAZON.MODELO_NO_ENCONTRADO },
|
|
121
|
-
{ patron: /invalid\s+model/i, razon: RAZON.MODELO_NO_ENCONTRADO },
|
|
122
|
-
{ patron: /does\s+not\s+exist/i, razon: RAZON.MODELO_NO_ENCONTRADO },
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
const PATRONES_SOBRECARGA = [
|
|
126
|
-
{ patron: /overloaded/i, razon: RAZON.SOBRECARGADO },
|
|
127
|
-
{ patron: /service\s+unavailable/i, razon: RAZON.SOBRECARGADO },
|
|
128
|
-
{ patron: /capacity/i, razon: RAZON.SOBRECARGADO },
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
const TODOS_LOS_PATRONES = [
|
|
132
|
-
...PATRONES_BILLING,
|
|
133
|
-
...PATRONES_RATE_LIMIT,
|
|
134
|
-
...PATRONES_CONTEXTO,
|
|
135
|
-
...PATRONES_PAYLOAD,
|
|
136
|
-
...PATRONES_TIMEOUT,
|
|
137
|
-
...PATRONES_MODELO,
|
|
138
|
-
...PATRONES_SOBRECARGA,
|
|
139
|
-
];
|
|
140
|
-
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
// Clasificación por status code
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Clasifica un error basándose en el status code HTTP.
|
|
147
|
-
*
|
|
148
|
-
* @param {number} statusCode
|
|
149
|
-
* @returns {string|null} Razón o null si no hay match directo
|
|
150
|
-
*/
|
|
151
|
-
function _clasificarPorStatus(statusCode) {
|
|
152
|
-
switch (statusCode) {
|
|
153
|
-
case 400: return RAZON.ERROR_FORMATO;
|
|
154
|
-
case 401: return RAZON.AUTH;
|
|
155
|
-
case 402: return RAZON.BILLING;
|
|
156
|
-
case 403: return RAZON.AUTH;
|
|
157
|
-
case 404: return RAZON.MODELO_NO_ENCONTRADO;
|
|
158
|
-
case 408: return RAZON.TIMEOUT;
|
|
159
|
-
case 413: return RAZON.PAYLOAD_GRANDE;
|
|
160
|
-
case 429: return RAZON.RATE_LIMIT;
|
|
161
|
-
case 500: return RAZON.ERROR_SERVIDOR;
|
|
162
|
-
case 502: return RAZON.ERROR_SERVIDOR;
|
|
163
|
-
case 503: return RAZON.SOBRECARGADO;
|
|
164
|
-
case 504: return RAZON.TIMEOUT;
|
|
165
|
-
case 529: return RAZON.SOBRECARGADO;
|
|
166
|
-
default: return null;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Clasifica un error basándose en el mensaje.
|
|
172
|
-
*
|
|
173
|
-
* @param {string} mensaje
|
|
174
|
-
* @returns {string|null} Razón o null
|
|
175
|
-
*/
|
|
176
|
-
function _clasificarPorMensaje(mensaje) {
|
|
177
|
-
if (!mensaje) return null;
|
|
178
|
-
for (const { patron, razon } of TODOS_LOS_PATRONES) {
|
|
179
|
-
if (patron.test(mensaje)) return razon;
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
// Hints de recuperación por razón
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* @typedef {object} HintsRecuperacion
|
|
190
|
-
* @property {boolean} retryable - ¿Reintentar?
|
|
191
|
-
* @property {boolean} shouldCompress - ¿Comprimir contexto?
|
|
192
|
-
* @property {boolean} shouldFallback - ¿Cambiar de modelo?
|
|
193
|
-
* @property {boolean} shouldRotateCredential - ¿Rotar API key?
|
|
194
|
-
*/
|
|
195
|
-
|
|
196
|
-
/** @type {Object.<string, HintsRecuperacion>} */
|
|
197
|
-
const HINTS_POR_RAZON = {
|
|
198
|
-
[RAZON.AUTH]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: true },
|
|
199
|
-
[RAZON.AUTH_PERMANENTE]: { retryable: false, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
200
|
-
[RAZON.BILLING]: { retryable: false, shouldCompress: false, shouldFallback: true, shouldRotateCredential: true },
|
|
201
|
-
[RAZON.RATE_LIMIT]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
202
|
-
[RAZON.SOBRECARGADO]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
203
|
-
[RAZON.ERROR_SERVIDOR]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
204
|
-
[RAZON.TIMEOUT]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
205
|
-
[RAZON.CONTEXTO_EXCEDIDO]: { retryable: false, shouldCompress: true, shouldFallback: false, shouldRotateCredential: false },
|
|
206
|
-
[RAZON.PAYLOAD_GRANDE]: { retryable: false, shouldCompress: true, shouldFallback: false, shouldRotateCredential: false },
|
|
207
|
-
[RAZON.MODELO_NO_ENCONTRADO]: { retryable: false, shouldCompress: false, shouldFallback: true, shouldRotateCredential: false },
|
|
208
|
-
[RAZON.ERROR_FORMATO]: { retryable: false, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
209
|
-
[RAZON.DESCONOCIDO]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
// API pública
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* @typedef {object} ErrorClasificado
|
|
218
|
-
* @property {string} razon - Razón del error (valor de RAZON)
|
|
219
|
-
* @property {number|null} statusCode - Status code HTTP (si disponible)
|
|
220
|
-
* @property {string} mensaje - Mensaje del error original
|
|
221
|
-
* @property {boolean} retryable - ¿Reintentar?
|
|
222
|
-
* @property {boolean} shouldCompress - ¿Comprimir contexto?
|
|
223
|
-
* @property {boolean} shouldFallback - ¿Cambiar de modelo?
|
|
224
|
-
* @property {boolean} shouldRotateCredential - ¿Rotar API key?
|
|
225
|
-
* @property {string} descripcion - Descripción legible de la clasificación
|
|
226
|
-
*/
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Clasifica un error de API y retorna hints de recuperación.
|
|
230
|
-
*
|
|
231
|
-
* Acepta objetos Error nativos, respuestas HTTP, o strings.
|
|
232
|
-
* La clasificación combina status code + análisis de mensaje.
|
|
233
|
-
*
|
|
234
|
-
* @param {Error|object|string} error - Error a clasificar
|
|
235
|
-
* @param {object} [contexto]
|
|
236
|
-
* @param {string} [contexto.provider] - Provider que originó el error
|
|
237
|
-
* @param {string} [contexto.modelo] - Modelo usado
|
|
238
|
-
* @returns {ErrorClasificado}
|
|
239
|
-
*/
|
|
240
|
-
function clasificarError(error, contexto = {}) {
|
|
241
|
-
// Extraer statusCode y mensaje del error
|
|
242
|
-
let statusCode = null;
|
|
243
|
-
let mensaje = '';
|
|
244
|
-
|
|
245
|
-
if (typeof error === 'string') {
|
|
246
|
-
mensaje = error;
|
|
247
|
-
} else if (error instanceof Error) {
|
|
248
|
-
mensaje = error.message || '';
|
|
249
|
-
statusCode = error.status || error.statusCode || error.code || null;
|
|
250
|
-
if (typeof statusCode === 'string') {
|
|
251
|
-
const parsed = parseInt(statusCode, 10);
|
|
252
|
-
statusCode = isNaN(parsed) ? null : parsed;
|
|
253
|
-
}
|
|
254
|
-
} else if (error && typeof error === 'object') {
|
|
255
|
-
mensaje = error.message || error.error || JSON.stringify(error);
|
|
256
|
-
statusCode = error.status || error.statusCode || null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Clasificar: primero por status, luego por mensaje
|
|
260
|
-
let razon = null;
|
|
261
|
-
if (statusCode) {
|
|
262
|
-
razon = _clasificarPorStatus(statusCode);
|
|
263
|
-
}
|
|
264
|
-
if (!razon) {
|
|
265
|
-
razon = _clasificarPorMensaje(mensaje);
|
|
266
|
-
}
|
|
267
|
-
if (!razon) {
|
|
268
|
-
razon = RAZON.DESCONOCIDO;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const hints = HINTS_POR_RAZON[razon] || HINTS_POR_RAZON[RAZON.DESCONOCIDO];
|
|
272
|
-
|
|
273
|
-
// Descripción legible
|
|
274
|
-
const descripciones = {
|
|
275
|
-
[RAZON.AUTH]: 'Error de autenticación — verificar o rotar credencial',
|
|
276
|
-
[RAZON.AUTH_PERMANENTE]: 'Autenticación rechazada permanentemente — abortar',
|
|
277
|
-
[RAZON.BILLING]: 'Créditos agotados o límite de facturación — cambiar provider/cuenta',
|
|
278
|
-
[RAZON.RATE_LIMIT]: 'Límite de tasa excedido — aplicar backoff',
|
|
279
|
-
[RAZON.SOBRECARGADO]: 'Servidor sobrecargado — esperar y reintentar',
|
|
280
|
-
[RAZON.ERROR_SERVIDOR]: 'Error interno del servidor — reintentar',
|
|
281
|
-
[RAZON.TIMEOUT]: 'Timeout de conexión/lectura — reintentar con timeout mayor',
|
|
282
|
-
[RAZON.CONTEXTO_EXCEDIDO]: 'Contexto excede el límite del modelo — comprimir',
|
|
283
|
-
[RAZON.PAYLOAD_GRANDE]: 'Payload demasiado grande — comprimir o dividir',
|
|
284
|
-
[RAZON.MODELO_NO_ENCONTRADO]: 'Modelo no encontrado — usar modelo alternativo',
|
|
285
|
-
[RAZON.ERROR_FORMATO]: 'Error de formato en request — revisar parámetros',
|
|
286
|
-
[RAZON.DESCONOCIDO]: 'Error no clasificado — reintentar con backoff conservador',
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
razon,
|
|
291
|
-
statusCode,
|
|
292
|
-
mensaje,
|
|
293
|
-
provider: contexto.provider || null,
|
|
294
|
-
modelo: contexto.modelo || null,
|
|
295
|
-
...hints,
|
|
296
|
-
descripcion: descripciones[razon] || descripciones[RAZON.DESCONOCIDO],
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
// Exports
|
|
302
|
-
// ---------------------------------------------------------------------------
|
|
303
|
-
|
|
304
|
-
module.exports = {
|
|
305
|
-
clasificarError,
|
|
306
|
-
RAZON,
|
|
307
|
-
HINTS_POR_RAZON,
|
|
308
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error Classifier — Clasificación tipada de errores de API con hints de recuperación.
|
|
5
|
+
*
|
|
6
|
+
* Patrón adoptado de Hermes Agent (agent/error_classifier.py).
|
|
7
|
+
* Clasifica errores HTTP y de red en categorías con hints de acción:
|
|
8
|
+
* - retryable: ¿vale la pena reintentar?
|
|
9
|
+
* - shouldCompress: ¿hay que comprimir contexto?
|
|
10
|
+
* - shouldFallback: ¿cambiar de modelo?
|
|
11
|
+
* - shouldRotateCredential: ¿rotar API key?
|
|
12
|
+
*
|
|
13
|
+
* Zero dependencias externas.
|
|
14
|
+
*
|
|
15
|
+
* Uso:
|
|
16
|
+
* const { clasificarError, RAZON } = require('./lib/error-classifier');
|
|
17
|
+
*
|
|
18
|
+
* try {
|
|
19
|
+
* await llamadaAPI();
|
|
20
|
+
* } catch (error) {
|
|
21
|
+
* const clasificado = clasificarError(error);
|
|
22
|
+
* if (clasificado.retryable) {
|
|
23
|
+
* // reintentar con jitteredBackoff
|
|
24
|
+
* } else if (clasificado.shouldCompress) {
|
|
25
|
+
* // comprimir contexto y reintentar
|
|
26
|
+
* } else if (clasificado.shouldFallback) {
|
|
27
|
+
* // cambiar a modelo alternativo
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* @module hooks/lib/error-classifier
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Razones de error (enum-like)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Razones de error clasificadas.
|
|
40
|
+
* Cada razón tiene un significado semántico y sugiere una acción de recuperación.
|
|
41
|
+
*
|
|
42
|
+
* @readonly
|
|
43
|
+
* @enum {string}
|
|
44
|
+
*/
|
|
45
|
+
const RAZON = Object.freeze({
|
|
46
|
+
// Autenticación
|
|
47
|
+
AUTH: 'auth', // 401/403 transitorio — rotar credential
|
|
48
|
+
AUTH_PERMANENTE: 'auth_permanente', // 401/403 persistente — abortar
|
|
49
|
+
|
|
50
|
+
// Facturación / cuota
|
|
51
|
+
BILLING: 'billing', // 402 o créditos agotados — rotar inmediato
|
|
52
|
+
RATE_LIMIT: 'rate_limit', // 429 o throttling — backoff + rotar
|
|
53
|
+
|
|
54
|
+
// Servidor
|
|
55
|
+
SOBRECARGADO: 'sobrecargado', // 503/529 — backoff largo
|
|
56
|
+
ERROR_SERVIDOR: 'error_servidor', // 500/502 — retry corto
|
|
57
|
+
|
|
58
|
+
// Transporte
|
|
59
|
+
TIMEOUT: 'timeout', // Timeout de conexión/lectura
|
|
60
|
+
|
|
61
|
+
// Contexto / payload
|
|
62
|
+
CONTEXTO_EXCEDIDO: 'contexto_excedido', // Context length exceeded — comprimir
|
|
63
|
+
PAYLOAD_GRANDE: 'payload_grande', // 413 — comprimir
|
|
64
|
+
|
|
65
|
+
// Modelo
|
|
66
|
+
MODELO_NO_ENCONTRADO: 'modelo_no_encontrado', // 404/invalid model — fallback
|
|
67
|
+
|
|
68
|
+
// Formato de request
|
|
69
|
+
ERROR_FORMATO: 'error_formato', // 400 — revisar request
|
|
70
|
+
|
|
71
|
+
// Catch-all
|
|
72
|
+
DESCONOCIDO: 'desconocido', // No clasificable — retry con backoff
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Patrones de detección por mensaje de error
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/** @type {Array<{ patron: RegExp, razon: string }>} */
|
|
80
|
+
const PATRONES_BILLING = [
|
|
81
|
+
{ patron: /insufficient\s+credits?/i, razon: RAZON.BILLING },
|
|
82
|
+
{ patron: /credit\s+balance/i, razon: RAZON.BILLING },
|
|
83
|
+
{ patron: /billing\s+hard\s+limit/i, razon: RAZON.BILLING },
|
|
84
|
+
{ patron: /payment\s+required/i, razon: RAZON.BILLING },
|
|
85
|
+
{ patron: /quota\s+exceeded/i, razon: RAZON.BILLING },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const PATRONES_RATE_LIMIT = [
|
|
89
|
+
{ patron: /rate\s+limit/i, razon: RAZON.RATE_LIMIT },
|
|
90
|
+
{ patron: /too\s+many\s+requests/i, razon: RAZON.RATE_LIMIT },
|
|
91
|
+
{ patron: /throttl/i, razon: RAZON.RATE_LIMIT },
|
|
92
|
+
{ patron: /requests?\s+per\s+(minute|second|hour)/i, razon: RAZON.RATE_LIMIT },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const PATRONES_CONTEXTO = [
|
|
96
|
+
{ patron: /context\s+length/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
97
|
+
{ patron: /context\s+size/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
98
|
+
{ patron: /maximum\s+context/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
99
|
+
{ patron: /token\s+limit/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
100
|
+
{ patron: /max_tokens/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
101
|
+
{ patron: /too\s+long/i, razon: RAZON.CONTEXTO_EXCEDIDO },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const PATRONES_PAYLOAD = [
|
|
105
|
+
{ patron: /request\s+entity\s+too\s+large/i, razon: RAZON.PAYLOAD_GRANDE },
|
|
106
|
+
{ patron: /error\s+code:\s*413/i, razon: RAZON.PAYLOAD_GRANDE },
|
|
107
|
+
{ patron: /payload\s+too\s+large/i, razon: RAZON.PAYLOAD_GRANDE },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const PATRONES_TIMEOUT = [
|
|
111
|
+
{ patron: /timed?\s*out/i, razon: RAZON.TIMEOUT },
|
|
112
|
+
{ patron: /ETIMEDOUT/i, razon: RAZON.TIMEOUT },
|
|
113
|
+
{ patron: /ECONNRESET/i, razon: RAZON.TIMEOUT },
|
|
114
|
+
{ patron: /ECONNREFUSED/i, razon: RAZON.TIMEOUT },
|
|
115
|
+
{ patron: /socket\s+hang\s+up/i, razon: RAZON.TIMEOUT },
|
|
116
|
+
{ patron: /network\s+error/i, razon: RAZON.TIMEOUT },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const PATRONES_MODELO = [
|
|
120
|
+
{ patron: /model\s+not\s+found/i, razon: RAZON.MODELO_NO_ENCONTRADO },
|
|
121
|
+
{ patron: /invalid\s+model/i, razon: RAZON.MODELO_NO_ENCONTRADO },
|
|
122
|
+
{ patron: /does\s+not\s+exist/i, razon: RAZON.MODELO_NO_ENCONTRADO },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const PATRONES_SOBRECARGA = [
|
|
126
|
+
{ patron: /overloaded/i, razon: RAZON.SOBRECARGADO },
|
|
127
|
+
{ patron: /service\s+unavailable/i, razon: RAZON.SOBRECARGADO },
|
|
128
|
+
{ patron: /capacity/i, razon: RAZON.SOBRECARGADO },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const TODOS_LOS_PATRONES = [
|
|
132
|
+
...PATRONES_BILLING,
|
|
133
|
+
...PATRONES_RATE_LIMIT,
|
|
134
|
+
...PATRONES_CONTEXTO,
|
|
135
|
+
...PATRONES_PAYLOAD,
|
|
136
|
+
...PATRONES_TIMEOUT,
|
|
137
|
+
...PATRONES_MODELO,
|
|
138
|
+
...PATRONES_SOBRECARGA,
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Clasificación por status code
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Clasifica un error basándose en el status code HTTP.
|
|
147
|
+
*
|
|
148
|
+
* @param {number} statusCode
|
|
149
|
+
* @returns {string|null} Razón o null si no hay match directo
|
|
150
|
+
*/
|
|
151
|
+
function _clasificarPorStatus(statusCode) {
|
|
152
|
+
switch (statusCode) {
|
|
153
|
+
case 400: return RAZON.ERROR_FORMATO;
|
|
154
|
+
case 401: return RAZON.AUTH;
|
|
155
|
+
case 402: return RAZON.BILLING;
|
|
156
|
+
case 403: return RAZON.AUTH;
|
|
157
|
+
case 404: return RAZON.MODELO_NO_ENCONTRADO;
|
|
158
|
+
case 408: return RAZON.TIMEOUT;
|
|
159
|
+
case 413: return RAZON.PAYLOAD_GRANDE;
|
|
160
|
+
case 429: return RAZON.RATE_LIMIT;
|
|
161
|
+
case 500: return RAZON.ERROR_SERVIDOR;
|
|
162
|
+
case 502: return RAZON.ERROR_SERVIDOR;
|
|
163
|
+
case 503: return RAZON.SOBRECARGADO;
|
|
164
|
+
case 504: return RAZON.TIMEOUT;
|
|
165
|
+
case 529: return RAZON.SOBRECARGADO;
|
|
166
|
+
default: return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clasifica un error basándose en el mensaje.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} mensaje
|
|
174
|
+
* @returns {string|null} Razón o null
|
|
175
|
+
*/
|
|
176
|
+
function _clasificarPorMensaje(mensaje) {
|
|
177
|
+
if (!mensaje) return null;
|
|
178
|
+
for (const { patron, razon } of TODOS_LOS_PATRONES) {
|
|
179
|
+
if (patron.test(mensaje)) return razon;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Hints de recuperación por razón
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @typedef {object} HintsRecuperacion
|
|
190
|
+
* @property {boolean} retryable - ¿Reintentar?
|
|
191
|
+
* @property {boolean} shouldCompress - ¿Comprimir contexto?
|
|
192
|
+
* @property {boolean} shouldFallback - ¿Cambiar de modelo?
|
|
193
|
+
* @property {boolean} shouldRotateCredential - ¿Rotar API key?
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
/** @type {Object.<string, HintsRecuperacion>} */
|
|
197
|
+
const HINTS_POR_RAZON = {
|
|
198
|
+
[RAZON.AUTH]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: true },
|
|
199
|
+
[RAZON.AUTH_PERMANENTE]: { retryable: false, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
200
|
+
[RAZON.BILLING]: { retryable: false, shouldCompress: false, shouldFallback: true, shouldRotateCredential: true },
|
|
201
|
+
[RAZON.RATE_LIMIT]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
202
|
+
[RAZON.SOBRECARGADO]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
203
|
+
[RAZON.ERROR_SERVIDOR]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
204
|
+
[RAZON.TIMEOUT]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
205
|
+
[RAZON.CONTEXTO_EXCEDIDO]: { retryable: false, shouldCompress: true, shouldFallback: false, shouldRotateCredential: false },
|
|
206
|
+
[RAZON.PAYLOAD_GRANDE]: { retryable: false, shouldCompress: true, shouldFallback: false, shouldRotateCredential: false },
|
|
207
|
+
[RAZON.MODELO_NO_ENCONTRADO]: { retryable: false, shouldCompress: false, shouldFallback: true, shouldRotateCredential: false },
|
|
208
|
+
[RAZON.ERROR_FORMATO]: { retryable: false, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
209
|
+
[RAZON.DESCONOCIDO]: { retryable: true, shouldCompress: false, shouldFallback: false, shouldRotateCredential: false },
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// API pública
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @typedef {object} ErrorClasificado
|
|
218
|
+
* @property {string} razon - Razón del error (valor de RAZON)
|
|
219
|
+
* @property {number|null} statusCode - Status code HTTP (si disponible)
|
|
220
|
+
* @property {string} mensaje - Mensaje del error original
|
|
221
|
+
* @property {boolean} retryable - ¿Reintentar?
|
|
222
|
+
* @property {boolean} shouldCompress - ¿Comprimir contexto?
|
|
223
|
+
* @property {boolean} shouldFallback - ¿Cambiar de modelo?
|
|
224
|
+
* @property {boolean} shouldRotateCredential - ¿Rotar API key?
|
|
225
|
+
* @property {string} descripcion - Descripción legible de la clasificación
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Clasifica un error de API y retorna hints de recuperación.
|
|
230
|
+
*
|
|
231
|
+
* Acepta objetos Error nativos, respuestas HTTP, o strings.
|
|
232
|
+
* La clasificación combina status code + análisis de mensaje.
|
|
233
|
+
*
|
|
234
|
+
* @param {Error|object|string} error - Error a clasificar
|
|
235
|
+
* @param {object} [contexto]
|
|
236
|
+
* @param {string} [contexto.provider] - Provider que originó el error
|
|
237
|
+
* @param {string} [contexto.modelo] - Modelo usado
|
|
238
|
+
* @returns {ErrorClasificado}
|
|
239
|
+
*/
|
|
240
|
+
function clasificarError(error, contexto = {}) {
|
|
241
|
+
// Extraer statusCode y mensaje del error
|
|
242
|
+
let statusCode = null;
|
|
243
|
+
let mensaje = '';
|
|
244
|
+
|
|
245
|
+
if (typeof error === 'string') {
|
|
246
|
+
mensaje = error;
|
|
247
|
+
} else if (error instanceof Error) {
|
|
248
|
+
mensaje = error.message || '';
|
|
249
|
+
statusCode = error.status || error.statusCode || error.code || null;
|
|
250
|
+
if (typeof statusCode === 'string') {
|
|
251
|
+
const parsed = parseInt(statusCode, 10);
|
|
252
|
+
statusCode = isNaN(parsed) ? null : parsed;
|
|
253
|
+
}
|
|
254
|
+
} else if (error && typeof error === 'object') {
|
|
255
|
+
mensaje = error.message || error.error || JSON.stringify(error);
|
|
256
|
+
statusCode = error.status || error.statusCode || null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Clasificar: primero por status, luego por mensaje
|
|
260
|
+
let razon = null;
|
|
261
|
+
if (statusCode) {
|
|
262
|
+
razon = _clasificarPorStatus(statusCode);
|
|
263
|
+
}
|
|
264
|
+
if (!razon) {
|
|
265
|
+
razon = _clasificarPorMensaje(mensaje);
|
|
266
|
+
}
|
|
267
|
+
if (!razon) {
|
|
268
|
+
razon = RAZON.DESCONOCIDO;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const hints = HINTS_POR_RAZON[razon] || HINTS_POR_RAZON[RAZON.DESCONOCIDO];
|
|
272
|
+
|
|
273
|
+
// Descripción legible
|
|
274
|
+
const descripciones = {
|
|
275
|
+
[RAZON.AUTH]: 'Error de autenticación — verificar o rotar credencial',
|
|
276
|
+
[RAZON.AUTH_PERMANENTE]: 'Autenticación rechazada permanentemente — abortar',
|
|
277
|
+
[RAZON.BILLING]: 'Créditos agotados o límite de facturación — cambiar provider/cuenta',
|
|
278
|
+
[RAZON.RATE_LIMIT]: 'Límite de tasa excedido — aplicar backoff',
|
|
279
|
+
[RAZON.SOBRECARGADO]: 'Servidor sobrecargado — esperar y reintentar',
|
|
280
|
+
[RAZON.ERROR_SERVIDOR]: 'Error interno del servidor — reintentar',
|
|
281
|
+
[RAZON.TIMEOUT]: 'Timeout de conexión/lectura — reintentar con timeout mayor',
|
|
282
|
+
[RAZON.CONTEXTO_EXCEDIDO]: 'Contexto excede el límite del modelo — comprimir',
|
|
283
|
+
[RAZON.PAYLOAD_GRANDE]: 'Payload demasiado grande — comprimir o dividir',
|
|
284
|
+
[RAZON.MODELO_NO_ENCONTRADO]: 'Modelo no encontrado — usar modelo alternativo',
|
|
285
|
+
[RAZON.ERROR_FORMATO]: 'Error de formato en request — revisar parámetros',
|
|
286
|
+
[RAZON.DESCONOCIDO]: 'Error no clasificado — reintentar con backoff conservador',
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
razon,
|
|
291
|
+
statusCode,
|
|
292
|
+
mensaje,
|
|
293
|
+
provider: contexto.provider || null,
|
|
294
|
+
modelo: contexto.modelo || null,
|
|
295
|
+
...hints,
|
|
296
|
+
descripcion: descripciones[razon] || descripciones[RAZON.DESCONOCIDO],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Exports
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
module.exports = {
|
|
305
|
+
clasificarError,
|
|
306
|
+
RAZON,
|
|
307
|
+
HINTS_POR_RAZON,
|
|
308
|
+
};
|