@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.
Files changed (116) hide show
  1. package/CLAUDE.md +4 -3
  2. package/README.md +15 -14
  3. package/agentes/nemesis-auditor-swl.md +161 -0
  4. package/bin/swl-mcp-server.js +187 -187
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/contribuir.md +233 -233
  7. package/comandos/swl/nemesis.md +122 -0
  8. package/comandos/swl/salud.md +34 -0
  9. package/comandos/swl/verificar.md +45 -0
  10. package/gateway/lib/event-channel.js +191 -191
  11. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  12. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  13. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  14. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  15. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  16. package/habilidades/eval-framework/SKILL.md +212 -212
  17. package/habilidades/feynman-auditor-swl/SKILL.md +123 -0
  18. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -0
  19. package/habilidades/harness-claude-code/SKILL.md +299 -299
  20. package/habilidades/infra-github-actions/SKILL.md +166 -166
  21. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  22. package/habilidades/manejo-errores/.evolved.json +8 -8
  23. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  24. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  25. package/habilidades/patrones-python/SKILL.md +229 -229
  26. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  27. package/habilidades/planear-fase/SKILL.md +319 -319
  28. package/habilidades/release-semver/.evolved.json +8 -8
  29. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -0
  30. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -0
  31. package/habilidades/testing-python/SKILL.md +340 -340
  32. package/habilidades/web-fetcher-routing/SKILL.md +75 -0
  33. package/hooks/claudemd-bloat-detector.js +161 -161
  34. package/hooks/lib/agent-routing.js +107 -107
  35. package/hooks/lib/auto-consolidator.js +335 -335
  36. package/hooks/lib/error-classifier.js +308 -308
  37. package/hooks/lib/merkle-audit.js +96 -96
  38. package/hooks/lib/provenance-tracker.js +191 -191
  39. package/hooks/lib/rate-limit-tracker.js +253 -253
  40. package/hooks/lib/resource-quota.js +122 -122
  41. package/hooks/lib/retry-jitter.js +165 -165
  42. package/hooks/lib/security-net.js +201 -0
  43. package/hooks/lib/skill-auditor.js +588 -588
  44. package/hooks/lib/sync-status.js +228 -228
  45. package/hooks/lib/taint-tracker.js +107 -107
  46. package/hooks/lib/text-similarity.js +241 -241
  47. package/hooks/lib/toon-compressor.js +245 -245
  48. package/hooks/registro-turnos.js +209 -209
  49. package/hooks/sugerir-regenerar-inventario.js +170 -170
  50. package/hooks/validar-formato-post-subagente.js +140 -140
  51. package/hooks/validar-memoria-hook.js +218 -218
  52. package/instintos/prompt-appendices.yaml +57 -57
  53. package/manifiestos/agent-output-schemas.json +57 -57
  54. package/manifiestos/modulos.json +41 -6
  55. package/manifiestos/perfiles.json +2 -1
  56. package/manifiestos/skills-lock.json +30 -9
  57. package/package.json +2 -2
  58. package/plantillas/auditor-veto-template.md +105 -105
  59. package/plantillas/github-workflows/README.md +47 -47
  60. package/plantillas/github-workflows/release-please.yml +44 -44
  61. package/plantillas/github-workflows/swl-ci.yml +107 -107
  62. package/plantillas/github-workflows/swl-security.yml +51 -51
  63. package/plugin.json +10 -2
  64. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  65. package/reglas/arreglar-al-detectar.md +147 -147
  66. package/reglas/fragmentos-compartidos.md +152 -152
  67. package/reglas/harness-claude-code.md +213 -213
  68. package/reglas/usar-context7.md +226 -226
  69. package/schemas/diary-entry.schema.json +80 -80
  70. package/scripts/audit-tools/audit-history.js +330 -0
  71. package/scripts/audit-tools/bundle-tracker.js +290 -0
  72. package/scripts/audit-tools/canary-monitor.js +352 -0
  73. package/scripts/audit-tools/code-profiler.js +605 -0
  74. package/scripts/audit-tools/dep-doctor.js +320 -0
  75. package/scripts/audit-tools/env-validator.js +206 -0
  76. package/scripts/audit-tools/lib/fs-walk.js +48 -0
  77. package/scripts/audit-tools/lib/output.js +23 -0
  78. package/scripts/audit-tools/migration-checker.js +392 -0
  79. package/scripts/audit-tools/pentest-scanner.js +1436 -0
  80. package/scripts/benchmark-memoria.js +167 -167
  81. package/scripts/configurar-branch-protection.js +418 -418
  82. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  83. package/scripts/field-report.js +199 -199
  84. package/scripts/generar-checklists-consolidados.js +273 -273
  85. package/scripts/generar-inventario.js +420 -420
  86. package/scripts/generar-matriz-lenguajes.js +271 -271
  87. package/scripts/lib/artefactos-python.js +43 -43
  88. package/scripts/lib/benchmark-metrics.js +160 -160
  89. package/scripts/lib/budget-enforcer.js +252 -252
  90. package/scripts/lib/configurar-ci.js +380 -380
  91. package/scripts/lib/contadores-inventario.js +217 -217
  92. package/scripts/lib/detectar-stack-detallado.js +307 -307
  93. package/scripts/lib/diary-entry.js +234 -234
  94. package/scripts/lib/eval-metrics-store.js +218 -218
  95. package/scripts/lib/eval-quality.js +171 -171
  96. package/scripts/lib/eval-schemas.js +144 -144
  97. package/scripts/lib/eval-self-correct.js +106 -106
  98. package/scripts/lib/eval-validator.js +185 -185
  99. package/scripts/lib/jaccard-similarity.js +98 -98
  100. package/scripts/lib/longmemeval-runner.js +125 -125
  101. package/scripts/lib/manifiestos.js +42 -1
  102. package/scripts/lib/npm-version.js +261 -261
  103. package/scripts/lib/paquetes-conocidos.js +50 -50
  104. package/scripts/lib/prompt-builder.js +264 -264
  105. package/scripts/lib/rrf-fusion.js +175 -175
  106. package/scripts/lib/scoring-instintos.js +277 -277
  107. package/scripts/lib/semantic-search.js +252 -252
  108. package/scripts/limpiar-artefactos-python.js +131 -131
  109. package/scripts/mcp-server/README.md +128 -128
  110. package/scripts/mcp-server/handlers.js +206 -206
  111. package/scripts/migrar-csv-a-array.js +168 -168
  112. package/scripts/migrar-fase-dominio.js +201 -201
  113. package/scripts/publicar.js +511 -511
  114. package/scripts/run-eval.js +141 -141
  115. package/scripts/validar-manifest.js +231 -195
  116. package/scripts/validar-userland-vacio.js +110 -110
@@ -1,122 +1,122 @@
1
- 'use strict';
2
-
3
- /**
4
- * Resource Quota — Control de consumo por ventana temporal.
5
- *
6
- * Adoptado de OpenFang (crates/openfang-kernel/src/metering.rs).
7
- * Implementa quotas rolling por hora, dia y mes para tokens y costo USD.
8
- *
9
- * Zero dependencias externas.
10
- *
11
- * Uso:
12
- * const { quota } = require('./lib/resource-quota');
13
- * quota.registrar({ tokens: 1500, costoUsd: 0.005, agente: 'implementador-swl' });
14
- * const check = quota.verificar();
15
- * if (check.excedido) console.log(check.mensaje);
16
- *
17
- * @module hooks/lib/resource-quota
18
- */
19
-
20
- const DEFAULTS = {
21
- maxTokensPorHora: 200000,
22
- maxCostoPorHoraUsd: 2.00,
23
- maxCostoPorDiaUsd: 10.00,
24
- maxCostoPorMesUsd: 100.00,
25
- alertAt: 0.80,
26
- };
27
-
28
- const _registros = [];
29
- const MAX_REGISTROS = 10000;
30
-
31
- function _ahora() { return Date.now(); }
32
- function _haceUnaHora() { return _ahora() - 3600000; }
33
- function _haceUnDia() { return _ahora() - 86400000; }
34
- function _haceUnMes() { return _ahora() - 2592000000; }
35
-
36
- const quota = {
37
- configurar(opciones) {
38
- Object.assign(DEFAULTS, opciones);
39
- },
40
-
41
- registrar(datos) {
42
- _registros.push({
43
- tokens: datos.tokens || 0,
44
- costoUsd: datos.costoUsd || 0,
45
- agente: datos.agente || 'desconocido',
46
- timestamp: _ahora(),
47
- });
48
- if (_registros.length > MAX_REGISTROS) {
49
- _registros.splice(0, _registros.length - MAX_REGISTROS);
50
- }
51
- },
52
-
53
- _sumarVentana(desdeMs) {
54
- let tokens = 0, costo = 0;
55
- for (let i = _registros.length - 1; i >= 0; i--) {
56
- const r = _registros[i];
57
- if (r.timestamp < desdeMs) break;
58
- tokens += r.tokens;
59
- costo += r.costoUsd;
60
- }
61
- return { tokens, costo };
62
- },
63
-
64
- verificar() {
65
- const hora = this._sumarVentana(_haceUnaHora());
66
- const dia = this._sumarVentana(_haceUnDia());
67
- const mes = this._sumarVentana(_haceUnMes());
68
-
69
- const violations = [];
70
-
71
- if (hora.tokens >= DEFAULTS.maxTokensPorHora) {
72
- violations.push({ tipo: 'tokens_hora', valor: hora.tokens, limite: DEFAULTS.maxTokensPorHora });
73
- }
74
- if (hora.costo >= DEFAULTS.maxCostoPorHoraUsd) {
75
- violations.push({ tipo: 'costo_hora', valor: hora.costo, limite: DEFAULTS.maxCostoPorHoraUsd });
76
- }
77
- if (dia.costo >= DEFAULTS.maxCostoPorDiaUsd) {
78
- violations.push({ tipo: 'costo_dia', valor: dia.costo, limite: DEFAULTS.maxCostoPorDiaUsd });
79
- }
80
- if (mes.costo >= DEFAULTS.maxCostoPorMesUsd) {
81
- violations.push({ tipo: 'costo_mes', valor: mes.costo, limite: DEFAULTS.maxCostoPorMesUsd });
82
- }
83
-
84
- const warnings = [];
85
- const alertThreshold = DEFAULTS.alertAt;
86
-
87
- if (hora.costo >= DEFAULTS.maxCostoPorHoraUsd * alertThreshold && !violations.find(v => v.tipo === 'costo_hora')) {
88
- warnings.push({ tipo: 'costo_hora', valor: hora.costo, limite: DEFAULTS.maxCostoPorHoraUsd, pct: Math.round(hora.costo / DEFAULTS.maxCostoPorHoraUsd * 100) });
89
- }
90
- if (dia.costo >= DEFAULTS.maxCostoPorDiaUsd * alertThreshold && !violations.find(v => v.tipo === 'costo_dia')) {
91
- warnings.push({ tipo: 'costo_dia', valor: dia.costo, limite: DEFAULTS.maxCostoPorDiaUsd, pct: Math.round(dia.costo / DEFAULTS.maxCostoPorDiaUsd * 100) });
92
- }
93
-
94
- return {
95
- excedido: violations.length > 0,
96
- violations,
97
- warnings,
98
- consumo: { hora, dia, mes },
99
- mensaje: violations.length > 0
100
- ? 'Quota excedida: ' + violations.map(v => v.tipo + ' (' + v.valor.toFixed(2) + '/' + v.limite + ')').join(', ')
101
- : warnings.length > 0
102
- ? 'Alerta: ' + warnings.map(w => w.tipo + ' al ' + w.pct + '%').join(', ')
103
- : 'Dentro de quota',
104
- };
105
- },
106
-
107
- resumen() {
108
- const hora = this._sumarVentana(_haceUnaHora());
109
- const dia = this._sumarVentana(_haceUnDia());
110
- const mes = this._sumarVentana(_haceUnMes());
111
- return {
112
- hora: { tokens: hora.tokens, costoUsd: Math.round(hora.costo * 10000) / 10000 },
113
- dia: { tokens: dia.tokens, costoUsd: Math.round(dia.costo * 10000) / 10000 },
114
- mes: { tokens: mes.tokens, costoUsd: Math.round(mes.costo * 10000) / 10000 },
115
- registros: _registros.length,
116
- };
117
- },
118
-
119
- reset() { _registros.length = 0; },
120
- };
121
-
122
- module.exports = { quota, DEFAULTS };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Resource Quota — Control de consumo por ventana temporal.
5
+ *
6
+ * Adoptado de OpenFang (crates/openfang-kernel/src/metering.rs).
7
+ * Implementa quotas rolling por hora, dia y mes para tokens y costo USD.
8
+ *
9
+ * Zero dependencias externas.
10
+ *
11
+ * Uso:
12
+ * const { quota } = require('./lib/resource-quota');
13
+ * quota.registrar({ tokens: 1500, costoUsd: 0.005, agente: 'implementador-swl' });
14
+ * const check = quota.verificar();
15
+ * if (check.excedido) console.log(check.mensaje);
16
+ *
17
+ * @module hooks/lib/resource-quota
18
+ */
19
+
20
+ const DEFAULTS = {
21
+ maxTokensPorHora: 200000,
22
+ maxCostoPorHoraUsd: 2.00,
23
+ maxCostoPorDiaUsd: 10.00,
24
+ maxCostoPorMesUsd: 100.00,
25
+ alertAt: 0.80,
26
+ };
27
+
28
+ const _registros = [];
29
+ const MAX_REGISTROS = 10000;
30
+
31
+ function _ahora() { return Date.now(); }
32
+ function _haceUnaHora() { return _ahora() - 3600000; }
33
+ function _haceUnDia() { return _ahora() - 86400000; }
34
+ function _haceUnMes() { return _ahora() - 2592000000; }
35
+
36
+ const quota = {
37
+ configurar(opciones) {
38
+ Object.assign(DEFAULTS, opciones);
39
+ },
40
+
41
+ registrar(datos) {
42
+ _registros.push({
43
+ tokens: datos.tokens || 0,
44
+ costoUsd: datos.costoUsd || 0,
45
+ agente: datos.agente || 'desconocido',
46
+ timestamp: _ahora(),
47
+ });
48
+ if (_registros.length > MAX_REGISTROS) {
49
+ _registros.splice(0, _registros.length - MAX_REGISTROS);
50
+ }
51
+ },
52
+
53
+ _sumarVentana(desdeMs) {
54
+ let tokens = 0, costo = 0;
55
+ for (let i = _registros.length - 1; i >= 0; i--) {
56
+ const r = _registros[i];
57
+ if (r.timestamp < desdeMs) break;
58
+ tokens += r.tokens;
59
+ costo += r.costoUsd;
60
+ }
61
+ return { tokens, costo };
62
+ },
63
+
64
+ verificar() {
65
+ const hora = this._sumarVentana(_haceUnaHora());
66
+ const dia = this._sumarVentana(_haceUnDia());
67
+ const mes = this._sumarVentana(_haceUnMes());
68
+
69
+ const violations = [];
70
+
71
+ if (hora.tokens >= DEFAULTS.maxTokensPorHora) {
72
+ violations.push({ tipo: 'tokens_hora', valor: hora.tokens, limite: DEFAULTS.maxTokensPorHora });
73
+ }
74
+ if (hora.costo >= DEFAULTS.maxCostoPorHoraUsd) {
75
+ violations.push({ tipo: 'costo_hora', valor: hora.costo, limite: DEFAULTS.maxCostoPorHoraUsd });
76
+ }
77
+ if (dia.costo >= DEFAULTS.maxCostoPorDiaUsd) {
78
+ violations.push({ tipo: 'costo_dia', valor: dia.costo, limite: DEFAULTS.maxCostoPorDiaUsd });
79
+ }
80
+ if (mes.costo >= DEFAULTS.maxCostoPorMesUsd) {
81
+ violations.push({ tipo: 'costo_mes', valor: mes.costo, limite: DEFAULTS.maxCostoPorMesUsd });
82
+ }
83
+
84
+ const warnings = [];
85
+ const alertThreshold = DEFAULTS.alertAt;
86
+
87
+ if (hora.costo >= DEFAULTS.maxCostoPorHoraUsd * alertThreshold && !violations.find(v => v.tipo === 'costo_hora')) {
88
+ warnings.push({ tipo: 'costo_hora', valor: hora.costo, limite: DEFAULTS.maxCostoPorHoraUsd, pct: Math.round(hora.costo / DEFAULTS.maxCostoPorHoraUsd * 100) });
89
+ }
90
+ if (dia.costo >= DEFAULTS.maxCostoPorDiaUsd * alertThreshold && !violations.find(v => v.tipo === 'costo_dia')) {
91
+ warnings.push({ tipo: 'costo_dia', valor: dia.costo, limite: DEFAULTS.maxCostoPorDiaUsd, pct: Math.round(dia.costo / DEFAULTS.maxCostoPorDiaUsd * 100) });
92
+ }
93
+
94
+ return {
95
+ excedido: violations.length > 0,
96
+ violations,
97
+ warnings,
98
+ consumo: { hora, dia, mes },
99
+ mensaje: violations.length > 0
100
+ ? 'Quota excedida: ' + violations.map(v => v.tipo + ' (' + v.valor.toFixed(2) + '/' + v.limite + ')').join(', ')
101
+ : warnings.length > 0
102
+ ? 'Alerta: ' + warnings.map(w => w.tipo + ' al ' + w.pct + '%').join(', ')
103
+ : 'Dentro de quota',
104
+ };
105
+ },
106
+
107
+ resumen() {
108
+ const hora = this._sumarVentana(_haceUnaHora());
109
+ const dia = this._sumarVentana(_haceUnDia());
110
+ const mes = this._sumarVentana(_haceUnMes());
111
+ return {
112
+ hora: { tokens: hora.tokens, costoUsd: Math.round(hora.costo * 10000) / 10000 },
113
+ dia: { tokens: dia.tokens, costoUsd: Math.round(dia.costo * 10000) / 10000 },
114
+ mes: { tokens: mes.tokens, costoUsd: Math.round(mes.costo * 10000) / 10000 },
115
+ registros: _registros.length,
116
+ };
117
+ },
118
+
119
+ reset() { _registros.length = 0; },
120
+ };
121
+
122
+ module.exports = { quota, DEFAULTS };
@@ -1,165 +1,165 @@
1
- 'use strict';
2
-
3
- /**
4
- * Retry con Jitter Decorrelado — Backoff exponencial con decorrelación.
5
- *
6
- * Patrón adoptado de Hermes Agent (agent/retry_utils.py).
7
- * Usa seed basado en time + counter con golden ratio para decorrelación
8
- * entre sesiones concurrentes — evita thundering herd.
9
- *
10
- * Zero dependencias externas — usa crypto nativo de Node.js.
11
- *
12
- * Uso:
13
- * const { jitteredBackoff, crearRetrier } = require('./lib/retry-jitter');
14
- *
15
- * // Calcular delay para un intento específico
16
- * const delay = jitteredBackoff(2, { baseDelay: 5, maxDelay: 120 });
17
- *
18
- * // Usar retrier completo
19
- * const retrier = crearRetrier({ maxIntentos: 3, baseDelay: 5 });
20
- * const resultado = await retrier.ejecutar(async () => {
21
- * return await llamadaAPI();
22
- * });
23
- *
24
- * @module hooks/lib/retry-jitter
25
- */
26
-
27
- const crypto = require('crypto');
28
-
29
- // ---------------------------------------------------------------------------
30
- // Constantes
31
- // ---------------------------------------------------------------------------
32
-
33
- /** Golden ratio × 2^32 — constante de dispersión de Fibonacci hashing. */
34
- const GOLDEN_RATIO_32 = 0x9E3779B9;
35
-
36
- /** Contador global para decorrelación entre llamadas concurrentes. */
37
- let _jitterCounter = 0;
38
-
39
- // ---------------------------------------------------------------------------
40
- // Funciones core
41
- // ---------------------------------------------------------------------------
42
-
43
- /**
44
- * Calcula delay con backoff exponencial + jitter decorrelado.
45
- *
46
- * Delay = min(base × 2^(intento-1), maxDelay) + jitter
47
- * Jitter = random(0, jitterRatio × delay)
48
- *
49
- * El seed para el random combina timestamp con un contador global
50
- * multiplicado por la constante golden ratio, lo que garantiza
51
- * que llamadas concurrentes desde diferentes hilos/procesos
52
- * NO coincidan en el mismo delay.
53
- *
54
- * @param {number} intento - Número de intento (1-indexed)
55
- * @param {object} [opciones]
56
- * @param {number} [opciones.baseDelay=5] - Delay base en segundos
57
- * @param {number} [opciones.maxDelay=120] - Delay máximo en segundos
58
- * @param {number} [opciones.jitterRatio=0.5] - Proporción de jitter (0-1)
59
- * @returns {number} Delay en segundos (con decimales)
60
- */
61
- function jitteredBackoff(intento, opciones = {}) {
62
- const {
63
- baseDelay = 5,
64
- maxDelay = 120,
65
- jitterRatio = 0.5,
66
- } = opciones;
67
-
68
- _jitterCounter += 1;
69
- const tick = _jitterCounter;
70
-
71
- // Backoff exponencial acotado
72
- const exponente = Math.max(0, intento - 1);
73
- const delay = Math.min(baseDelay * Math.pow(2, exponente), maxDelay);
74
-
75
- // Seed decorrelado: timestamp XOR (counter × golden_ratio)
76
- const nowNs = BigInt(Date.now()) * 1_000_000n; // ms → pseudo-ns
77
- const seed = Number((nowNs ^ BigInt(tick * GOLDEN_RATIO_32)) & 0xFFFFFFFFn);
78
-
79
- // PRNG determinista basado en seed (sin crypto para velocidad)
80
- const hash = crypto.createHash('md5').update(String(seed)).digest();
81
- const pseudoRandom = hash.readUInt32LE(0) / 0xFFFFFFFF;
82
-
83
- const jitter = pseudoRandom * jitterRatio * delay;
84
-
85
- return delay + jitter;
86
- }
87
-
88
- /**
89
- * Espera un número de segundos (para uso con async/await).
90
- *
91
- * @param {number} segundos
92
- * @returns {Promise<void>}
93
- */
94
- function esperar(segundos) {
95
- return new Promise(resolve => setTimeout(resolve, Math.round(segundos * 1000)));
96
- }
97
-
98
- /**
99
- * Crea un retrier configurable con jitter decorrelado.
100
- *
101
- * @param {object} [opciones]
102
- * @param {number} [opciones.maxIntentos=3] - Máximo de intentos
103
- * @param {number} [opciones.baseDelay=5] - Delay base en segundos
104
- * @param {number} [opciones.maxDelay=120] - Delay máximo en segundos
105
- * @param {number} [opciones.jitterRatio=0.5] - Proporción de jitter
106
- * @param {Function} [opciones.esRetryable] - Función que determina si un error es retryable
107
- * @param {Function} [opciones.onReintento] - Callback antes de cada reintento
108
- * @returns {object} Retrier con método ejecutar()
109
- */
110
- function crearRetrier(opciones = {}) {
111
- const {
112
- maxIntentos = 3,
113
- baseDelay = 5,
114
- maxDelay = 120,
115
- jitterRatio = 0.5,
116
- esRetryable = () => true,
117
- onReintento = null,
118
- } = opciones;
119
-
120
- return {
121
- /**
122
- * Ejecuta una función con reintentos automáticos.
123
- *
124
- * @template T
125
- * @param {() => Promise<T>} fn - Función async a ejecutar
126
- * @returns {Promise<T>} Resultado de la función
127
- * @throws {Error} Error del último intento si todos fallan
128
- */
129
- async ejecutar(fn) {
130
- let ultimoError;
131
-
132
- for (let intento = 1; intento <= maxIntentos; intento++) {
133
- try {
134
- return await fn();
135
- } catch (error) {
136
- ultimoError = error;
137
-
138
- if (intento >= maxIntentos || !esRetryable(error)) {
139
- throw error;
140
- }
141
-
142
- const delay = jitteredBackoff(intento, { baseDelay, maxDelay, jitterRatio });
143
-
144
- if (onReintento) {
145
- onReintento({ intento, maxIntentos, delay, error });
146
- }
147
-
148
- await esperar(delay);
149
- }
150
- }
151
-
152
- throw ultimoError;
153
- },
154
- };
155
- }
156
-
157
- // ---------------------------------------------------------------------------
158
- // Exports
159
- // ---------------------------------------------------------------------------
160
-
161
- module.exports = {
162
- jitteredBackoff,
163
- esperar,
164
- crearRetrier,
165
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Retry con Jitter Decorrelado — Backoff exponencial con decorrelación.
5
+ *
6
+ * Patrón adoptado de Hermes Agent (agent/retry_utils.py).
7
+ * Usa seed basado en time + counter con golden ratio para decorrelación
8
+ * entre sesiones concurrentes — evita thundering herd.
9
+ *
10
+ * Zero dependencias externas — usa crypto nativo de Node.js.
11
+ *
12
+ * Uso:
13
+ * const { jitteredBackoff, crearRetrier } = require('./lib/retry-jitter');
14
+ *
15
+ * // Calcular delay para un intento específico
16
+ * const delay = jitteredBackoff(2, { baseDelay: 5, maxDelay: 120 });
17
+ *
18
+ * // Usar retrier completo
19
+ * const retrier = crearRetrier({ maxIntentos: 3, baseDelay: 5 });
20
+ * const resultado = await retrier.ejecutar(async () => {
21
+ * return await llamadaAPI();
22
+ * });
23
+ *
24
+ * @module hooks/lib/retry-jitter
25
+ */
26
+
27
+ const crypto = require('crypto');
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Constantes
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /** Golden ratio × 2^32 — constante de dispersión de Fibonacci hashing. */
34
+ const GOLDEN_RATIO_32 = 0x9E3779B9;
35
+
36
+ /** Contador global para decorrelación entre llamadas concurrentes. */
37
+ let _jitterCounter = 0;
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Funciones core
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Calcula delay con backoff exponencial + jitter decorrelado.
45
+ *
46
+ * Delay = min(base × 2^(intento-1), maxDelay) + jitter
47
+ * Jitter = random(0, jitterRatio × delay)
48
+ *
49
+ * El seed para el random combina timestamp con un contador global
50
+ * multiplicado por la constante golden ratio, lo que garantiza
51
+ * que llamadas concurrentes desde diferentes hilos/procesos
52
+ * NO coincidan en el mismo delay.
53
+ *
54
+ * @param {number} intento - Número de intento (1-indexed)
55
+ * @param {object} [opciones]
56
+ * @param {number} [opciones.baseDelay=5] - Delay base en segundos
57
+ * @param {number} [opciones.maxDelay=120] - Delay máximo en segundos
58
+ * @param {number} [opciones.jitterRatio=0.5] - Proporción de jitter (0-1)
59
+ * @returns {number} Delay en segundos (con decimales)
60
+ */
61
+ function jitteredBackoff(intento, opciones = {}) {
62
+ const {
63
+ baseDelay = 5,
64
+ maxDelay = 120,
65
+ jitterRatio = 0.5,
66
+ } = opciones;
67
+
68
+ _jitterCounter += 1;
69
+ const tick = _jitterCounter;
70
+
71
+ // Backoff exponencial acotado
72
+ const exponente = Math.max(0, intento - 1);
73
+ const delay = Math.min(baseDelay * Math.pow(2, exponente), maxDelay);
74
+
75
+ // Seed decorrelado: timestamp XOR (counter × golden_ratio)
76
+ const nowNs = BigInt(Date.now()) * 1_000_000n; // ms → pseudo-ns
77
+ const seed = Number((nowNs ^ BigInt(tick * GOLDEN_RATIO_32)) & 0xFFFFFFFFn);
78
+
79
+ // PRNG determinista basado en seed (sin crypto para velocidad)
80
+ const hash = crypto.createHash('md5').update(String(seed)).digest();
81
+ const pseudoRandom = hash.readUInt32LE(0) / 0xFFFFFFFF;
82
+
83
+ const jitter = pseudoRandom * jitterRatio * delay;
84
+
85
+ return delay + jitter;
86
+ }
87
+
88
+ /**
89
+ * Espera un número de segundos (para uso con async/await).
90
+ *
91
+ * @param {number} segundos
92
+ * @returns {Promise<void>}
93
+ */
94
+ function esperar(segundos) {
95
+ return new Promise(resolve => setTimeout(resolve, Math.round(segundos * 1000)));
96
+ }
97
+
98
+ /**
99
+ * Crea un retrier configurable con jitter decorrelado.
100
+ *
101
+ * @param {object} [opciones]
102
+ * @param {number} [opciones.maxIntentos=3] - Máximo de intentos
103
+ * @param {number} [opciones.baseDelay=5] - Delay base en segundos
104
+ * @param {number} [opciones.maxDelay=120] - Delay máximo en segundos
105
+ * @param {number} [opciones.jitterRatio=0.5] - Proporción de jitter
106
+ * @param {Function} [opciones.esRetryable] - Función que determina si un error es retryable
107
+ * @param {Function} [opciones.onReintento] - Callback antes de cada reintento
108
+ * @returns {object} Retrier con método ejecutar()
109
+ */
110
+ function crearRetrier(opciones = {}) {
111
+ const {
112
+ maxIntentos = 3,
113
+ baseDelay = 5,
114
+ maxDelay = 120,
115
+ jitterRatio = 0.5,
116
+ esRetryable = () => true,
117
+ onReintento = null,
118
+ } = opciones;
119
+
120
+ return {
121
+ /**
122
+ * Ejecuta una función con reintentos automáticos.
123
+ *
124
+ * @template T
125
+ * @param {() => Promise<T>} fn - Función async a ejecutar
126
+ * @returns {Promise<T>} Resultado de la función
127
+ * @throws {Error} Error del último intento si todos fallan
128
+ */
129
+ async ejecutar(fn) {
130
+ let ultimoError;
131
+
132
+ for (let intento = 1; intento <= maxIntentos; intento++) {
133
+ try {
134
+ return await fn();
135
+ } catch (error) {
136
+ ultimoError = error;
137
+
138
+ if (intento >= maxIntentos || !esRetryable(error)) {
139
+ throw error;
140
+ }
141
+
142
+ const delay = jitteredBackoff(intento, { baseDelay, maxDelay, jitterRatio });
143
+
144
+ if (onReintento) {
145
+ onReintento({ intento, maxIntentos, delay, error });
146
+ }
147
+
148
+ await esperar(delay);
149
+ }
150
+ }
151
+
152
+ throw ultimoError;
153
+ },
154
+ };
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Exports
159
+ // ---------------------------------------------------------------------------
160
+
161
+ module.exports = {
162
+ jitteredBackoff,
163
+ esperar,
164
+ crearRetrier,
165
+ };