@saulwade/swl-ses 1.4.1 → 1.5.0

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 (136) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +561 -560
  3. package/agentes/nemesis-auditor-swl.md +161 -161
  4. package/bin/swl-mcp-server.js +49 -22
  5. package/bin/swl-ses.js +74 -0
  6. package/comandos/swl/.evolved.json +22 -22
  7. package/comandos/swl/contribuir.md +233 -233
  8. package/comandos/swl/ejecutar-fase.md +33 -4
  9. package/comandos/swl/metricas.md +72 -0
  10. package/comandos/swl/nemesis.md +122 -122
  11. package/gateway/lib/event-channel.js +191 -191
  12. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  13. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  14. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  15. package/habilidades/discutir-fase/SKILL.md +50 -2
  16. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  17. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  18. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -0
  19. package/habilidades/eval-framework/SKILL.md +212 -212
  20. package/habilidades/feynman-auditor-swl/SKILL.md +123 -123
  21. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  22. package/habilidades/harness-claude-code/SKILL.md +299 -299
  23. package/habilidades/infra-github-actions/SKILL.md +166 -166
  24. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  25. package/habilidades/manejo-errores/.evolved.json +8 -8
  26. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  27. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  28. package/habilidades/patrones-python/SKILL.md +229 -229
  29. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  30. package/habilidades/planear-fase/SKILL.md +319 -319
  31. package/habilidades/protocolo-revision-swl/SKILL.md +276 -0
  32. package/habilidades/release-semver/.evolved.json +8 -8
  33. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -166
  34. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  35. package/habilidades/testing-python/SKILL.md +340 -340
  36. package/habilidades/verificar-trabajo/SKILL.md +49 -5
  37. package/habilidades/web-fetcher-routing/SKILL.md +75 -75
  38. package/hooks/claudemd-bloat-detector.js +161 -161
  39. package/hooks/lib/agent-routing.js +107 -107
  40. package/hooks/lib/auto-consolidator.js +335 -335
  41. package/hooks/lib/error-classifier.js +308 -308
  42. package/hooks/lib/merkle-audit.js +96 -96
  43. package/hooks/lib/provenance-tracker.js +191 -191
  44. package/hooks/lib/rate-limit-tracker.js +253 -253
  45. package/hooks/lib/resource-quota.js +122 -122
  46. package/hooks/lib/retry-jitter.js +165 -165
  47. package/hooks/lib/security-net.js +201 -201
  48. package/hooks/lib/skill-auditor.js +588 -588
  49. package/hooks/lib/sync-status.js +228 -228
  50. package/hooks/lib/taint-tracker.js +107 -107
  51. package/hooks/lib/text-similarity.js +241 -241
  52. package/hooks/lib/toon-compressor.js +245 -245
  53. package/hooks/registro-turnos.js +209 -209
  54. package/hooks/sugerir-regenerar-inventario.js +170 -170
  55. package/hooks/validar-formato-post-subagente.js +140 -140
  56. package/hooks/validar-memoria-hook.js +218 -218
  57. package/instintos/prompt-appendices.yaml +57 -57
  58. package/manifiestos/agent-output-schemas.json +57 -57
  59. package/manifiestos/modulos.json +1321 -1262
  60. package/manifiestos/perfiles.json +2 -1
  61. package/manifiestos/skills-lock.json +1114 -1114
  62. package/package.json +3 -3
  63. package/plantillas/auditor-veto-template.md +105 -105
  64. package/plantillas/github-workflows/README.md +47 -47
  65. package/plantillas/github-workflows/release-please.yml +44 -44
  66. package/plantillas/github-workflows/swl-ci.yml +107 -107
  67. package/plantillas/github-workflows/swl-security.yml +51 -51
  68. package/plugin.json +351 -343
  69. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  70. package/reglas/arreglar-al-detectar.md +147 -147
  71. package/reglas/fragmentos-compartidos.md +152 -152
  72. package/reglas/harness-claude-code.md +213 -213
  73. package/reglas/usar-context7.md +226 -226
  74. package/schemas/diary-entry.schema.json +80 -80
  75. package/scripts/audit-tools/audit-history.js +330 -330
  76. package/scripts/audit-tools/bundle-tracker.js +290 -290
  77. package/scripts/audit-tools/canary-monitor.js +352 -352
  78. package/scripts/audit-tools/code-profiler.js +605 -605
  79. package/scripts/audit-tools/dep-doctor.js +320 -320
  80. package/scripts/audit-tools/env-validator.js +206 -206
  81. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  82. package/scripts/audit-tools/lib/output.js +23 -23
  83. package/scripts/audit-tools/migration-checker.js +392 -392
  84. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  85. package/scripts/benchmark-memoria.js +167 -167
  86. package/scripts/configurar-branch-protection.js +418 -418
  87. package/scripts/derivar-feature-list.js +489 -0
  88. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  89. package/scripts/doctor.js +31 -4
  90. package/scripts/field-report.js +199 -199
  91. package/scripts/generar-checklists-consolidados.js +273 -273
  92. package/scripts/generar-inventario.js +420 -420
  93. package/scripts/generar-matriz-lenguajes.js +271 -271
  94. package/scripts/instalador.js +56 -5
  95. package/scripts/lib/artefactos-python.js +43 -43
  96. package/scripts/lib/benchmark-metrics.js +160 -160
  97. package/scripts/lib/budget-enforcer.js +252 -252
  98. package/scripts/lib/configurar-ci.js +380 -380
  99. package/scripts/lib/contadores-inventario.js +217 -217
  100. package/scripts/lib/detectar-runtime.js +75 -9
  101. package/scripts/lib/detectar-stack-detallado.js +307 -307
  102. package/scripts/lib/diary-entry.js +234 -234
  103. package/scripts/lib/estado.js +13 -1
  104. package/scripts/lib/eval-metrics-store.js +218 -218
  105. package/scripts/lib/eval-quality.js +171 -171
  106. package/scripts/lib/eval-schemas.js +144 -144
  107. package/scripts/lib/eval-self-correct.js +106 -106
  108. package/scripts/lib/eval-validator.js +185 -185
  109. package/scripts/lib/expandir-targets.js +71 -0
  110. package/scripts/lib/jaccard-similarity.js +98 -98
  111. package/scripts/lib/longmemeval-runner.js +125 -125
  112. package/scripts/lib/manifiestos.js +42 -1
  113. package/scripts/lib/npm-version.js +261 -261
  114. package/scripts/lib/paquetes-conocidos.js +50 -50
  115. package/scripts/lib/parsear-opciones.js +3 -0
  116. package/scripts/lib/prompt-builder.js +264 -264
  117. package/scripts/lib/rrf-fusion.js +175 -175
  118. package/scripts/lib/scoring-instintos.js +277 -277
  119. package/scripts/lib/semantic-search.js +252 -252
  120. package/scripts/lib/toml-merge.js +204 -0
  121. package/scripts/lib/transformadores/base.js +43 -9
  122. package/scripts/lib/transformadores/codex.js +375 -115
  123. package/scripts/lib/transformadores/cursor.js +359 -0
  124. package/scripts/lib/transformadores/index.js +2 -0
  125. package/scripts/limpiar-artefactos-python.js +131 -131
  126. package/scripts/mcp-server/README.md +122 -80
  127. package/scripts/mcp-server/auth.js +105 -0
  128. package/scripts/mcp-server/cache.js +106 -0
  129. package/scripts/mcp-server/handlers.js +386 -206
  130. package/scripts/mcp-server/telemetry.js +78 -0
  131. package/scripts/migrar-csv-a-array.js +168 -168
  132. package/scripts/migrar-fase-dominio.js +201 -201
  133. package/scripts/publicar.js +511 -511
  134. package/scripts/run-eval.js +141 -141
  135. package/scripts/validar-manifest.js +231 -195
  136. package/scripts/validar-userland-vacio.js +110 -110
@@ -411,6 +411,11 @@ async function install(opciones) {
411
411
  perfil,
412
412
  rutaBase: rutas.base,
413
413
  global: esGlobal,
414
+ // Persistir contexto de filtrado de reglas-lenguajes para que doctor
415
+ // verifique conteos contra el filtro real al instalar (no recalcule
416
+ // desde el cwd actual del doctor). Fix v1.4.3.
417
+ allLangs: allLangs === true,
418
+ stackInstalado: stackDetectado instanceof Set ? [...stackDetectado] : null,
414
419
  });
415
420
 
416
421
  let instalados = 0;
@@ -447,6 +452,11 @@ async function install(opciones) {
447
452
  }
448
453
  }
449
454
 
455
+ // Cargar transformador del target ANTES del loop de instalación —
456
+ // Sub-fase 11.5: permite que instalarArchivo invoque transformarAgente
457
+ // por archivo para targets que cambian formato (ej. codex .md → .toml).
458
+ const transformadorTarget = obtenerTransformador(target, runtime);
459
+
450
460
  // Instalar archivos core
451
461
  for (const archivo of resolucion.archivos) {
452
462
  try {
@@ -457,6 +467,7 @@ async function install(opciones) {
457
467
  estado,
458
468
  syncMode,
459
469
  flatNaming,
470
+ transformador: transformadorTarget,
460
471
  });
461
472
  if (resultado.instalado) {
462
473
  registrarArchivo(estado, {
@@ -612,6 +623,13 @@ async function install(opciones) {
612
623
  // transformador Claude pueda detectar stack/comandos/framework.
613
624
  // Otros transformadores ignoran este campo.
614
625
  dirProyecto: process.cwd(),
626
+ // ADR-0019 Sub-fase 1: el transformador Codex necesita saber el scope para
627
+ // resolver dirBase (~/.codex/ vs cwd) y registrar el MCP server.
628
+ // El runtime trae los paths absolutos calculados por detectar-runtime.js.
629
+ esGlobal,
630
+ dirRuntimeGlobal: runtime.global,
631
+ withMcp: Boolean(opciones.with_mcp) && !opciones.no_mcp,
632
+ swlBinPath: path.join(RAIZ_PKG, 'bin', 'swl-mcp-server.js'),
615
633
  });
616
634
 
617
635
  if (instrucciones) {
@@ -628,6 +646,7 @@ async function install(opciones) {
628
646
  console.log(' = CLAUDE.md no modificado (--no-claudemd)');
629
647
  } else if (instrucciones.merge && instrucciones.merge.tipo === 'marcadores') {
630
648
  // Merge idempotente con marcadores — preserva contenido del usuario.
649
+ // Aplica a CLAUDE.md (Claude Code) y AGENTS.md (Codex CLI) — ADR-0019 Sub-fase 1.
631
650
  if (!fs.existsSync(dirInstrucciones)) {
632
651
  fs.mkdirSync(dirInstrucciones, { recursive: true });
633
652
  }
@@ -637,12 +656,13 @@ async function install(opciones) {
637
656
  endTag: instrucciones.merge.endTag,
638
657
  beginPrefix: instrucciones.merge.beginPrefix,
639
658
  });
659
+ const nombreArchivo = instrucciones.rutaRelativa;
640
660
  const etiqueta = {
641
- 'creado': '+ CLAUDE.md creado con bloque SWL',
642
- 'append': '+ Bloque SWL agregado al final de CLAUDE.md (contenido del usuario preservado)',
643
- 'reemplazado': '* Bloque SWL actualizado en CLAUDE.md (contenido del usuario preservado)',
644
- 'sin-cambios': '= CLAUDE.md ya tenía el bloque SWL actualizado',
645
- 'error': '! Error al fusionar CLAUDE.md',
661
+ 'creado': `+ ${nombreArchivo} creado con bloque SWL`,
662
+ 'append': `+ Bloque SWL agregado al final de ${nombreArchivo} (contenido del usuario preservado)`,
663
+ 'reemplazado': `* Bloque SWL actualizado en ${nombreArchivo} (contenido del usuario preservado)`,
664
+ 'sin-cambios': `= ${nombreArchivo} ya tenía el bloque SWL actualizado`,
665
+ 'error': `! Error al fusionar ${nombreArchivo}`,
646
666
  }[resMerge.accion] || `${resMerge.accion} ${resMerge.archivo}`;
647
667
  console.log(` ${etiqueta}${resMerge.detalle ? ': ' + resMerge.detalle : ''}`);
648
668
  registrarArchivo(estado, {
@@ -850,6 +870,33 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
850
870
 
851
871
  let nombreArchivo = path.basename(archivo.origen);
852
872
 
873
+ // Sub-fase 11.5 v1.5.0: para tipo 'agentes', aplicar transformarAgente del
874
+ // transformador del target ANTES de la copia. Permite que codex emita TOML
875
+ // (.toml) en lugar del .md original, o que cursor normalice el frontmatter.
876
+ // Si transformarAgente devuelve `consolidar: true`, saltar — el archivo
877
+ // se incluirá en el archivoPrincipal (AGENTS.md) generado más adelante.
878
+ let contenidoTransformado = null;
879
+ if (archivo.tipo === 'agentes' && opciones.transformador && typeof opciones.transformador.transformarAgente === 'function') {
880
+ try {
881
+ const original = fs.readFileSync(archivo.origen, 'utf-8');
882
+ const t = opciones.transformador.transformarAgente(original, { nombreArchivo });
883
+ if (t && t.consolidar === true) {
884
+ // El transformador del target prefiere consolidar este agente en el
885
+ // archivoPrincipal en lugar de tener archivo individual. No instalar aquí.
886
+ return { instalado: false, razon: 'consolidado en archivoPrincipal' };
887
+ }
888
+ if (t && typeof t.contenido === 'string') {
889
+ contenidoTransformado = t.contenido;
890
+ if (t.rutaRelativa) {
891
+ // Usar el basename del rutaRelativa devuelto (puede cambiar extensión .md → .toml)
892
+ nombreArchivo = path.basename(t.rutaRelativa);
893
+ }
894
+ }
895
+ } catch (err) {
896
+ console.log(` ! Transformación de agente falló (${err.message}), copia literal del .md`);
897
+ }
898
+ }
899
+
853
900
  // Flat naming: convierte rutas jerárquicas en nombres planos con __
854
901
  // Ejemplo: habilidades/build-errors-python → build-errors-python (sin cambio, ya es plano)
855
902
  // Ejemplo: reglas/lenguajes/java/java-estilo.md → java__java-estilo.md
@@ -991,6 +1038,10 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
991
1038
  // Copy mode (default) o fallback de symlink
992
1039
  if (stat.isDirectory()) {
993
1040
  copiarDirectorio(archivo.origen, path.join(dirDestino, nombreArchivo));
1041
+ } else if (contenidoTransformado !== null) {
1042
+ // Sub-fase 11.5: el transformador del target reescribió el contenido
1043
+ // (ej. codex .md → .toml). Escribir el contenido transformado.
1044
+ fs.writeFileSync(destino, contenidoTransformado, 'utf-8');
994
1045
  } else {
995
1046
  fs.copyFileSync(archivo.origen, destino);
996
1047
  }
@@ -1,43 +1,43 @@
1
- 'use strict';
2
-
3
- /**
4
- * artefactos-python.js — fuente única de verdad para los artefactos
5
- * Python que NO deben distribuirse en el paquete npm.
6
- *
7
- * Por qué existe:
8
- * Tanto `scripts/limpiar-artefactos-python.js` (prepack) como
9
- * `scripts/publicar.js` (copiarDir hacia tmpDir del mirror GH)
10
- * necesitan saber qué directorios y extensiones excluir. Tener la
11
- * lista hardcodeada en cada archivo es violación DRY de conocimiento
12
- * — agregar `.pyd` o `.mypy_cache_v2` exigiría editar dos archivos
13
- * y mantenerlos sincronizados.
14
- */
15
-
16
- const DIRS_ARTEFACTOS_PYTHON = Object.freeze(new Set([
17
- '__pycache__',
18
- '.pytest_cache',
19
- '.mypy_cache',
20
- '.ruff_cache',
21
- ]));
22
-
23
- const EXTS_ARTEFACTOS_PYTHON = Object.freeze(new Set([
24
- '.pyc',
25
- '.pyo',
26
- ]));
27
-
28
- function esDirArtefacto(nombre) {
29
- return DIRS_ARTEFACTOS_PYTHON.has(nombre);
30
- }
31
-
32
- function esArchivoArtefacto(nombre) {
33
- const dot = nombre.lastIndexOf('.');
34
- if (dot < 0) return false;
35
- return EXTS_ARTEFACTOS_PYTHON.has(nombre.slice(dot).toLowerCase());
36
- }
37
-
38
- module.exports = {
39
- DIRS_ARTEFACTOS_PYTHON,
40
- EXTS_ARTEFACTOS_PYTHON,
41
- esDirArtefacto,
42
- esArchivoArtefacto,
43
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * artefactos-python.js — fuente única de verdad para los artefactos
5
+ * Python que NO deben distribuirse en el paquete npm.
6
+ *
7
+ * Por qué existe:
8
+ * Tanto `scripts/limpiar-artefactos-python.js` (prepack) como
9
+ * `scripts/publicar.js` (copiarDir hacia tmpDir del mirror GH)
10
+ * necesitan saber qué directorios y extensiones excluir. Tener la
11
+ * lista hardcodeada en cada archivo es violación DRY de conocimiento
12
+ * — agregar `.pyd` o `.mypy_cache_v2` exigiría editar dos archivos
13
+ * y mantenerlos sincronizados.
14
+ */
15
+
16
+ const DIRS_ARTEFACTOS_PYTHON = Object.freeze(new Set([
17
+ '__pycache__',
18
+ '.pytest_cache',
19
+ '.mypy_cache',
20
+ '.ruff_cache',
21
+ ]));
22
+
23
+ const EXTS_ARTEFACTOS_PYTHON = Object.freeze(new Set([
24
+ '.pyc',
25
+ '.pyo',
26
+ ]));
27
+
28
+ function esDirArtefacto(nombre) {
29
+ return DIRS_ARTEFACTOS_PYTHON.has(nombre);
30
+ }
31
+
32
+ function esArchivoArtefacto(nombre) {
33
+ const dot = nombre.lastIndexOf('.');
34
+ if (dot < 0) return false;
35
+ return EXTS_ARTEFACTOS_PYTHON.has(nombre.slice(dot).toLowerCase());
36
+ }
37
+
38
+ module.exports = {
39
+ DIRS_ARTEFACTOS_PYTHON,
40
+ EXTS_ARTEFACTOS_PYTHON,
41
+ esDirArtefacto,
42
+ esArchivoArtefacto,
43
+ };
@@ -1,160 +1,160 @@
1
- 'use strict';
2
-
3
- /**
4
- * benchmark-metrics.js — Métricas de retrieval para benchmark de memoria SWL.
5
- *
6
- * Patrón adoptado de `temp/agentmemory-main/benchmark/longmemeval-bench.ts`.
7
- * Funciones puras zero-deps. Adaptado a IDs SWL (aprendizaje, sesion,
8
- * instinto) en lugar de session_id de agentmemory.
9
- *
10
- * Métricas:
11
- * - recallAt(k): 1.0 si al menos un gold ID está en los top-k, sino 0.0
12
- * - precisionAt(k): proporción de top-k que son gold
13
- * - mrr: Mean Reciprocal Rank (1/rank del primer gold encontrado)
14
- * - ndcgAt(k): Normalized Discounted Cumulative Gain
15
- *
16
- * @module scripts/lib/benchmark-metrics
17
- */
18
-
19
- // ── helpers ───────────────────────────────────────────────────────────────────
20
-
21
- function asSet(arr) {
22
- return new Set(Array.isArray(arr) ? arr : []);
23
- }
24
-
25
- // ── métricas individuales ─────────────────────────────────────────────────────
26
-
27
- /**
28
- * Recall @ k: 1.0 si ALGÚN id gold está en los primeros k retrieved.
29
- *
30
- * @param {string[]} retrievedIds - IDs ordenados (mejor primero).
31
- * @param {string[]} goldIds - IDs correctos esperados.
32
- * @param {number} k
33
- * @returns {number} 0 o 1
34
- */
35
- function recallAt(retrievedIds, goldIds, k) {
36
- if (!Array.isArray(retrievedIds) || !Array.isArray(goldIds)) return 0;
37
- const topK = new Set(retrievedIds.slice(0, k));
38
- return goldIds.some(g => topK.has(g)) ? 1.0 : 0.0;
39
- }
40
-
41
- /**
42
- * Precision @ k: proporción de los primeros k retrieved que son gold.
43
- *
44
- * @param {string[]} retrievedIds
45
- * @param {string[]} goldIds
46
- * @param {number} k
47
- * @returns {number} en [0, 1]
48
- */
49
- function precisionAt(retrievedIds, goldIds, k) {
50
- if (!Array.isArray(retrievedIds) || !Array.isArray(goldIds) || k <= 0) return 0;
51
- const goldSet = asSet(goldIds);
52
- const topK = retrievedIds.slice(0, k);
53
- if (topK.length === 0) return 0;
54
- const hits = topK.filter(id => goldSet.has(id)).length;
55
- return hits / topK.length;
56
- }
57
-
58
- /**
59
- * Mean Reciprocal Rank: 1/rank del primer gold encontrado, o 0 si ninguno.
60
- *
61
- * @param {string[]} retrievedIds
62
- * @param {string[]} goldIds
63
- * @returns {number} en [0, 1]
64
- */
65
- function mrr(retrievedIds, goldIds) {
66
- if (!Array.isArray(retrievedIds) || !Array.isArray(goldIds)) return 0;
67
- const goldSet = asSet(goldIds);
68
- for (let i = 0; i < retrievedIds.length; i++) {
69
- if (goldSet.has(retrievedIds[i])) {
70
- return 1 / (i + 1);
71
- }
72
- }
73
- return 0;
74
- }
75
-
76
- function dcg(relevancias, k) {
77
- let suma = 0;
78
- for (let i = 0; i < Math.min(k, relevancias.length); i++) {
79
- suma += (relevancias[i] ? 1 : 0) / Math.log2(i + 2);
80
- }
81
- return suma;
82
- }
83
-
84
- /**
85
- * Normalized Discounted Cumulative Gain @ k.
86
- *
87
- * @param {string[]} retrievedIds
88
- * @param {string[]} goldIds
89
- * @param {number} k
90
- * @returns {number} en [0, 1]
91
- */
92
- function ndcgAt(retrievedIds, goldIds, k) {
93
- if (!Array.isArray(retrievedIds) || !Array.isArray(goldIds) || k <= 0) return 0;
94
- const goldSet = asSet(goldIds);
95
- const rels = retrievedIds.slice(0, k).map(id => goldSet.has(id));
96
- const idealRels = Array.from({ length: Math.min(k, goldSet.size) }, () => true);
97
- const idealDCG = dcg(idealRels, k);
98
- if (idealDCG === 0) return 0;
99
- return dcg(rels, k) / idealDCG;
100
- }
101
-
102
- // ── agregados ─────────────────────────────────────────────────────────────────
103
-
104
- /**
105
- * Calcula el conjunto completo de métricas para una query.
106
- *
107
- * @param {string[]} retrievedIds
108
- * @param {string[]} goldIds
109
- * @returns {{ recall_at_5, recall_at_10, recall_at_20, mrr, ndcg_at_10, precision_at_5 }}
110
- */
111
- function calcularMetricas(retrievedIds, goldIds) {
112
- return {
113
- recall_at_5: recallAt(retrievedIds, goldIds, 5),
114
- recall_at_10: recallAt(retrievedIds, goldIds, 10),
115
- recall_at_20: recallAt(retrievedIds, goldIds, 20),
116
- mrr: mrr(retrievedIds, goldIds),
117
- ndcg_at_10: ndcgAt(retrievedIds, goldIds, 10),
118
- precision_at_5: precisionAt(retrievedIds, goldIds, 5),
119
- };
120
- }
121
-
122
- /**
123
- * Promedia métricas sobre múltiples queries.
124
- *
125
- * @param {Array<object>} resultados - Array de objetos producidos por calcularMetricas().
126
- * @returns {object} Promedios con campo `n` (cantidad de queries).
127
- */
128
- function promediar(resultados) {
129
- if (!Array.isArray(resultados) || resultados.length === 0) {
130
- return {
131
- n: 0,
132
- recall_at_5: 0,
133
- recall_at_10: 0,
134
- recall_at_20: 0,
135
- mrr: 0,
136
- ndcg_at_10: 0,
137
- precision_at_5: 0,
138
- };
139
- }
140
-
141
- const claves = ['recall_at_5', 'recall_at_10', 'recall_at_20', 'mrr', 'ndcg_at_10', 'precision_at_5'];
142
- const promedio = { n: resultados.length };
143
- for (const k of claves) {
144
- const sum = resultados.reduce((a, r) => a + (r[k] || 0), 0);
145
- promedio[k] = sum / resultados.length;
146
- }
147
- return promedio;
148
- }
149
-
150
- // ── exports ───────────────────────────────────────────────────────────────────
151
-
152
- module.exports = {
153
- recallAt,
154
- precisionAt,
155
- mrr,
156
- ndcgAt,
157
- dcg,
158
- calcularMetricas,
159
- promediar,
160
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * benchmark-metrics.js — Métricas de retrieval para benchmark de memoria SWL.
5
+ *
6
+ * Patrón adoptado de `temp/agentmemory-main/benchmark/longmemeval-bench.ts`.
7
+ * Funciones puras zero-deps. Adaptado a IDs SWL (aprendizaje, sesion,
8
+ * instinto) en lugar de session_id de agentmemory.
9
+ *
10
+ * Métricas:
11
+ * - recallAt(k): 1.0 si al menos un gold ID está en los top-k, sino 0.0
12
+ * - precisionAt(k): proporción de top-k que son gold
13
+ * - mrr: Mean Reciprocal Rank (1/rank del primer gold encontrado)
14
+ * - ndcgAt(k): Normalized Discounted Cumulative Gain
15
+ *
16
+ * @module scripts/lib/benchmark-metrics
17
+ */
18
+
19
+ // ── helpers ───────────────────────────────────────────────────────────────────
20
+
21
+ function asSet(arr) {
22
+ return new Set(Array.isArray(arr) ? arr : []);
23
+ }
24
+
25
+ // ── métricas individuales ─────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Recall @ k: 1.0 si ALGÚN id gold está en los primeros k retrieved.
29
+ *
30
+ * @param {string[]} retrievedIds - IDs ordenados (mejor primero).
31
+ * @param {string[]} goldIds - IDs correctos esperados.
32
+ * @param {number} k
33
+ * @returns {number} 0 o 1
34
+ */
35
+ function recallAt(retrievedIds, goldIds, k) {
36
+ if (!Array.isArray(retrievedIds) || !Array.isArray(goldIds)) return 0;
37
+ const topK = new Set(retrievedIds.slice(0, k));
38
+ return goldIds.some(g => topK.has(g)) ? 1.0 : 0.0;
39
+ }
40
+
41
+ /**
42
+ * Precision @ k: proporción de los primeros k retrieved que son gold.
43
+ *
44
+ * @param {string[]} retrievedIds
45
+ * @param {string[]} goldIds
46
+ * @param {number} k
47
+ * @returns {number} en [0, 1]
48
+ */
49
+ function precisionAt(retrievedIds, goldIds, k) {
50
+ if (!Array.isArray(retrievedIds) || !Array.isArray(goldIds) || k <= 0) return 0;
51
+ const goldSet = asSet(goldIds);
52
+ const topK = retrievedIds.slice(0, k);
53
+ if (topK.length === 0) return 0;
54
+ const hits = topK.filter(id => goldSet.has(id)).length;
55
+ return hits / topK.length;
56
+ }
57
+
58
+ /**
59
+ * Mean Reciprocal Rank: 1/rank del primer gold encontrado, o 0 si ninguno.
60
+ *
61
+ * @param {string[]} retrievedIds
62
+ * @param {string[]} goldIds
63
+ * @returns {number} en [0, 1]
64
+ */
65
+ function mrr(retrievedIds, goldIds) {
66
+ if (!Array.isArray(retrievedIds) || !Array.isArray(goldIds)) return 0;
67
+ const goldSet = asSet(goldIds);
68
+ for (let i = 0; i < retrievedIds.length; i++) {
69
+ if (goldSet.has(retrievedIds[i])) {
70
+ return 1 / (i + 1);
71
+ }
72
+ }
73
+ return 0;
74
+ }
75
+
76
+ function dcg(relevancias, k) {
77
+ let suma = 0;
78
+ for (let i = 0; i < Math.min(k, relevancias.length); i++) {
79
+ suma += (relevancias[i] ? 1 : 0) / Math.log2(i + 2);
80
+ }
81
+ return suma;
82
+ }
83
+
84
+ /**
85
+ * Normalized Discounted Cumulative Gain @ k.
86
+ *
87
+ * @param {string[]} retrievedIds
88
+ * @param {string[]} goldIds
89
+ * @param {number} k
90
+ * @returns {number} en [0, 1]
91
+ */
92
+ function ndcgAt(retrievedIds, goldIds, k) {
93
+ if (!Array.isArray(retrievedIds) || !Array.isArray(goldIds) || k <= 0) return 0;
94
+ const goldSet = asSet(goldIds);
95
+ const rels = retrievedIds.slice(0, k).map(id => goldSet.has(id));
96
+ const idealRels = Array.from({ length: Math.min(k, goldSet.size) }, () => true);
97
+ const idealDCG = dcg(idealRels, k);
98
+ if (idealDCG === 0) return 0;
99
+ return dcg(rels, k) / idealDCG;
100
+ }
101
+
102
+ // ── agregados ─────────────────────────────────────────────────────────────────
103
+
104
+ /**
105
+ * Calcula el conjunto completo de métricas para una query.
106
+ *
107
+ * @param {string[]} retrievedIds
108
+ * @param {string[]} goldIds
109
+ * @returns {{ recall_at_5, recall_at_10, recall_at_20, mrr, ndcg_at_10, precision_at_5 }}
110
+ */
111
+ function calcularMetricas(retrievedIds, goldIds) {
112
+ return {
113
+ recall_at_5: recallAt(retrievedIds, goldIds, 5),
114
+ recall_at_10: recallAt(retrievedIds, goldIds, 10),
115
+ recall_at_20: recallAt(retrievedIds, goldIds, 20),
116
+ mrr: mrr(retrievedIds, goldIds),
117
+ ndcg_at_10: ndcgAt(retrievedIds, goldIds, 10),
118
+ precision_at_5: precisionAt(retrievedIds, goldIds, 5),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Promedia métricas sobre múltiples queries.
124
+ *
125
+ * @param {Array<object>} resultados - Array de objetos producidos por calcularMetricas().
126
+ * @returns {object} Promedios con campo `n` (cantidad de queries).
127
+ */
128
+ function promediar(resultados) {
129
+ if (!Array.isArray(resultados) || resultados.length === 0) {
130
+ return {
131
+ n: 0,
132
+ recall_at_5: 0,
133
+ recall_at_10: 0,
134
+ recall_at_20: 0,
135
+ mrr: 0,
136
+ ndcg_at_10: 0,
137
+ precision_at_5: 0,
138
+ };
139
+ }
140
+
141
+ const claves = ['recall_at_5', 'recall_at_10', 'recall_at_20', 'mrr', 'ndcg_at_10', 'precision_at_5'];
142
+ const promedio = { n: resultados.length };
143
+ for (const k of claves) {
144
+ const sum = resultados.reduce((a, r) => a + (r[k] || 0), 0);
145
+ promedio[k] = sum / resultados.length;
146
+ }
147
+ return promedio;
148
+ }
149
+
150
+ // ── exports ───────────────────────────────────────────────────────────────────
151
+
152
+ module.exports = {
153
+ recallAt,
154
+ precisionAt,
155
+ mrr,
156
+ ndcgAt,
157
+ dcg,
158
+ calcularMetricas,
159
+ promediar,
160
+ };