@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,252 +1,252 @@
1
- 'use strict';
2
-
3
- /**
4
- * semantic-search.js
5
- *
6
- * Búsqueda híbrida con fallback automático:
7
- * - Default: Jaccard similarity sobre tokens normalizados (zero-deps).
8
- * - Opt-in: embeddings con @xenova/transformers (vía SWL_SEMANTIC_BACKEND=xenova).
9
- *
10
- * Patrón adaptado de `temp/cass_memory_system-main/src/semantic.ts`. Diferencias:
11
- * - Carga lazy de Xenova (no requiere instalación si no se usa).
12
- * - API uniforme: el caller no sabe qué backend está activo.
13
- * - Tokenización compatible con español (acentos preservados, stopwords es-MX).
14
- *
15
- * Cuándo usar:
16
- * - Buscar skills similares por significado, no solo keywords (150 skills).
17
- * - Encontrar instintos relacionados a un patrón observado.
18
- * - Detectar duplicados semánticos en APRENDIZAJES.md.
19
- *
20
- * API:
21
- * - jaccardSimilarity(a, b) → [0, 1]
22
- * - cosineSimilarity(a, b) → [-1, 1] (vectores numéricos)
23
- * - tokenize(text) → array de tokens
24
- * - semanticRank(query, items, opts) → items rankeados por similitud
25
- *
26
- * Backend: el módulo NO requiere Xenova. Si SWL_SEMANTIC_BACKEND=xenova y
27
- * @xenova/transformers está instalado, se usa para embeddings. Si no, Jaccard.
28
- *
29
- * Instalación opt-in del backend de embeddings:
30
- *
31
- * npm install -g @xenova/transformers
32
- * export SWL_SEMANTIC_BACKEND=xenova
33
- *
34
- * @xenova/transformers ya NO es dependencia del paquete swl-ses (v1.0.1+)
35
- * porque arrastra `prebuild-install@7.1.3` deprecated y sumaba ~50 MB al
36
- * install para una feature opt-in raramente usada. El usuario que quiera
37
- * el backend semántico lo instala manualmente.
38
- *
39
- * @module scripts/lib/semantic-search
40
- */
41
-
42
- // ── stopwords es-MX ───────────────────────────────────────────────────────────
43
-
44
- const STOPWORDS = new Set([
45
- 'a', 'al', 'algo', 'algunas', 'algunos', 'ante', 'antes', 'aunque',
46
- 'cada', 'cierta', 'ciertas', 'cierto', 'ciertos', 'como', 'con', 'contra',
47
- 'cual', 'cuando', 'de', 'del', 'desde', 'donde', 'durante',
48
- 'e', 'el', 'ella', 'ellas', 'ellos', 'en', 'entre', 'era', 'erais', 'eran',
49
- 'eras', 'eres', 'es', 'esa', 'esas', 'ese', 'eso', 'esos', 'esta', 'estaba',
50
- 'estaban', 'estado', 'estamos', 'estan', 'están', 'estar', 'estas', 'este',
51
- 'esto', 'estos', 'estoy', 'fue', 'fuera', 'fueron', 'fui', 'ha', 'habia',
52
- 'había', 'han', 'hasta', 'hay', 'haya', 'la', 'las', 'le', 'les', 'lo',
53
- 'los', 'me', 'mi', 'mis', 'mucho', 'muchos', 'muy', 'nada', 'ni', 'no',
54
- 'nos', 'nosotros', 'nuestra', 'nuestras', 'nuestro', 'nuestros', 'o', 'os',
55
- 'otra', 'otras', 'otro', 'otros', 'para', 'pero', 'poco', 'por', 'porque',
56
- 'que', 'quien', 'quienes', 'qué', 'se', 'sea', 'sean', 'ser', 'si', 'sí',
57
- 'sido', 'siendo', 'sin', 'sobre', 'soy', 'su', 'sus', 'también', 'tan',
58
- 'tanto', 'te', 'tener', 'ti', 'tiene', 'tienen', 'todo', 'todos', 'tu',
59
- 'tus', 'un', 'una', 'unas', 'uno', 'unos', 'usted', 'ustedes', 'va', 'vais',
60
- 'van', 'vas', 'ver', 'y', 'ya', 'yo',
61
- ]);
62
-
63
- // ── tokenización ──────────────────────────────────────────────────────────────
64
-
65
- /**
66
- * Tokeniza texto en español de México. Pasos:
67
- * 1. lowercase
68
- * 2. eliminar puntuación
69
- * 3. dividir en palabras
70
- * 4. eliminar stopwords
71
- * 5. eliminar tokens muy cortos (<2 chars)
72
- *
73
- * Preserva acentos (cá vs ca son distintos). No hace stemming.
74
- *
75
- * @param {string} texto
76
- * @returns {string[]} tokens
77
- */
78
- function tokenize(texto) {
79
- if (!texto || typeof texto !== 'string') return [];
80
- return texto
81
- .toLowerCase()
82
- .replace(/[^\p{L}\p{N}\s]/gu, ' ') // letras unicode + números
83
- .split(/\s+/)
84
- .filter(t => t.length >= 2 && !STOPWORDS.has(t));
85
- }
86
-
87
- // ── Jaccard ───────────────────────────────────────────────────────────────────
88
-
89
- /**
90
- * Jaccard similarity sobre conjuntos de tokens.
91
- * jaccard(A, B) = |A ∩ B| / |A ∪ B|
92
- *
93
- * Ambos textos vacíos → 0 (no hay similitud, no hay info).
94
- * Un texto vacío + uno con tokens → 0.
95
- *
96
- * @param {string} textA
97
- * @param {string} textB
98
- * @returns {number} en [0, 1]
99
- */
100
- function jaccardSimilarity(textA, textB) {
101
- const a = new Set(tokenize(textA));
102
- const b = new Set(tokenize(textB));
103
- if (a.size === 0 && b.size === 0) return 0;
104
- if (a.size === 0 || b.size === 0) return 0;
105
-
106
- let inter = 0;
107
- for (const t of a) if (b.has(t)) inter++;
108
- const union = a.size + b.size - inter;
109
- return inter / union;
110
- }
111
-
112
- // ── Cosine (para embeddings) ──────────────────────────────────────────────────
113
-
114
- /**
115
- * Cosine similarity entre dos vectores numéricos.
116
- * cos(A, B) = (A · B) / (||A|| × ||B||)
117
- *
118
- * @param {number[]} vecA
119
- * @param {number[]} vecB
120
- * @returns {number} en [-1, 1]
121
- */
122
- function cosineSimilarity(vecA, vecB) {
123
- if (!Array.isArray(vecA) || !Array.isArray(vecB)) return 0;
124
- if (vecA.length !== vecB.length || vecA.length === 0) return 0;
125
-
126
- let dot = 0, normA = 0, normB = 0;
127
- for (let i = 0; i < vecA.length; i++) {
128
- const a = vecA[i] || 0;
129
- const b = vecB[i] || 0;
130
- dot += a * b;
131
- normA += a * a;
132
- normB += b * b;
133
- }
134
- if (normA === 0 || normB === 0) return 0;
135
- return dot / (Math.sqrt(normA) * Math.sqrt(normB));
136
- }
137
-
138
- // ── backend de embeddings (Xenova lazy) ───────────────────────────────────────
139
-
140
- /**
141
- * Estado lazy del backend Xenova. Se carga la primera vez que se invoca
142
- * embeddingsBackend() y NO falla si no está instalado.
143
- */
144
- let _xenovaPipeline = null;
145
- let _xenovaTried = false;
146
-
147
- async function tryLoadXenova() {
148
- if (_xenovaPipeline || _xenovaTried) return _xenovaPipeline;
149
- _xenovaTried = true;
150
- try {
151
- // Carga dinámica para evitar require al inicio
152
- const transformers = require('@xenova/transformers');
153
- const { pipeline } = transformers;
154
- _xenovaPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
155
- return _xenovaPipeline;
156
- } catch (_) {
157
- // No instalado o error al cargar: silencioso. Caller usará fallback.
158
- return null;
159
- }
160
- }
161
-
162
- /**
163
- * Detecta el backend activo según SWL_SEMANTIC_BACKEND.
164
- * Sin variable o valor != 'xenova' → 'jaccard'.
165
- */
166
- function activeBackend() {
167
- const env = (process.env.SWL_SEMANTIC_BACKEND || '').toLowerCase().trim();
168
- return env === 'xenova' ? 'xenova' : 'jaccard';
169
- }
170
-
171
- /**
172
- * Embebe un texto en un vector. Solo disponible si Xenova está activo.
173
- * Retorna null si el backend no es xenova o no se pudo cargar.
174
- *
175
- * @param {string} texto
176
- * @returns {Promise<number[]|null>}
177
- */
178
- async function embed(texto) {
179
- if (activeBackend() !== 'xenova') return null;
180
- const pipe = await tryLoadXenova();
181
- if (!pipe) return null;
182
- try {
183
- const out = await pipe(texto, { pooling: 'mean', normalize: true });
184
- return Array.from(out.data);
185
- } catch (_) {
186
- return null;
187
- }
188
- }
189
-
190
- // ── ranking ──────────────────────────────────────────────────────────────────
191
-
192
- /**
193
- * Rankea items por similitud con la query.
194
- *
195
- * @param {string} query
196
- * @param {Array<{id: string, text: string, embedding?: number[]}>} items
197
- * @param {object} [opts]
198
- * @param {number} [opts.threshold=0] - filtra resultados con score < threshold
199
- * @param {number} [opts.limit=10] - limita resultados
200
- * @returns {Promise<Array<{id, text, score, backend}>>}
201
- */
202
- async function semanticRank(query, items, opts = {}) {
203
- const { threshold = 0, limit = 10 } = opts;
204
- if (!query || !Array.isArray(items)) return [];
205
-
206
- const backend = activeBackend();
207
- let results;
208
-
209
- if (backend === 'xenova') {
210
- const queryEmb = await embed(query);
211
- if (queryEmb) {
212
- results = await Promise.all(items.map(async (item) => {
213
- let itemEmb = item.embedding;
214
- if (!itemEmb) itemEmb = await embed(item.text || '');
215
- const score = (queryEmb && itemEmb) ? cosineSimilarity(queryEmb, itemEmb) : 0;
216
- return { id: item.id, text: item.text, score, backend: 'xenova' };
217
- }));
218
- } else {
219
- // Backend declarado pero no disponible → fallback transparente
220
- results = items.map(item => ({
221
- id: item.id,
222
- text: item.text,
223
- score: jaccardSimilarity(query, item.text || ''),
224
- backend: 'jaccard-fallback',
225
- }));
226
- }
227
- } else {
228
- results = items.map(item => ({
229
- id: item.id,
230
- text: item.text,
231
- score: jaccardSimilarity(query, item.text || ''),
232
- backend: 'jaccard',
233
- }));
234
- }
235
-
236
- return results
237
- .filter(r => r.score >= threshold)
238
- .sort((a, b) => b.score - a.score)
239
- .slice(0, limit);
240
- }
241
-
242
- // ── exports ───────────────────────────────────────────────────────────────────
243
-
244
- module.exports = {
245
- tokenize,
246
- jaccardSimilarity,
247
- cosineSimilarity,
248
- embed,
249
- semanticRank,
250
- activeBackend,
251
- STOPWORDS,
252
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * semantic-search.js
5
+ *
6
+ * Búsqueda híbrida con fallback automático:
7
+ * - Default: Jaccard similarity sobre tokens normalizados (zero-deps).
8
+ * - Opt-in: embeddings con @xenova/transformers (vía SWL_SEMANTIC_BACKEND=xenova).
9
+ *
10
+ * Patrón adaptado de `temp/cass_memory_system-main/src/semantic.ts`. Diferencias:
11
+ * - Carga lazy de Xenova (no requiere instalación si no se usa).
12
+ * - API uniforme: el caller no sabe qué backend está activo.
13
+ * - Tokenización compatible con español (acentos preservados, stopwords es-MX).
14
+ *
15
+ * Cuándo usar:
16
+ * - Buscar skills similares por significado, no solo keywords (150 skills).
17
+ * - Encontrar instintos relacionados a un patrón observado.
18
+ * - Detectar duplicados semánticos en APRENDIZAJES.md.
19
+ *
20
+ * API:
21
+ * - jaccardSimilarity(a, b) → [0, 1]
22
+ * - cosineSimilarity(a, b) → [-1, 1] (vectores numéricos)
23
+ * - tokenize(text) → array de tokens
24
+ * - semanticRank(query, items, opts) → items rankeados por similitud
25
+ *
26
+ * Backend: el módulo NO requiere Xenova. Si SWL_SEMANTIC_BACKEND=xenova y
27
+ * @xenova/transformers está instalado, se usa para embeddings. Si no, Jaccard.
28
+ *
29
+ * Instalación opt-in del backend de embeddings:
30
+ *
31
+ * npm install -g @xenova/transformers
32
+ * export SWL_SEMANTIC_BACKEND=xenova
33
+ *
34
+ * @xenova/transformers ya NO es dependencia del paquete swl-ses (v1.0.1+)
35
+ * porque arrastra `prebuild-install@7.1.3` deprecated y sumaba ~50 MB al
36
+ * install para una feature opt-in raramente usada. El usuario que quiera
37
+ * el backend semántico lo instala manualmente.
38
+ *
39
+ * @module scripts/lib/semantic-search
40
+ */
41
+
42
+ // ── stopwords es-MX ───────────────────────────────────────────────────────────
43
+
44
+ const STOPWORDS = new Set([
45
+ 'a', 'al', 'algo', 'algunas', 'algunos', 'ante', 'antes', 'aunque',
46
+ 'cada', 'cierta', 'ciertas', 'cierto', 'ciertos', 'como', 'con', 'contra',
47
+ 'cual', 'cuando', 'de', 'del', 'desde', 'donde', 'durante',
48
+ 'e', 'el', 'ella', 'ellas', 'ellos', 'en', 'entre', 'era', 'erais', 'eran',
49
+ 'eras', 'eres', 'es', 'esa', 'esas', 'ese', 'eso', 'esos', 'esta', 'estaba',
50
+ 'estaban', 'estado', 'estamos', 'estan', 'están', 'estar', 'estas', 'este',
51
+ 'esto', 'estos', 'estoy', 'fue', 'fuera', 'fueron', 'fui', 'ha', 'habia',
52
+ 'había', 'han', 'hasta', 'hay', 'haya', 'la', 'las', 'le', 'les', 'lo',
53
+ 'los', 'me', 'mi', 'mis', 'mucho', 'muchos', 'muy', 'nada', 'ni', 'no',
54
+ 'nos', 'nosotros', 'nuestra', 'nuestras', 'nuestro', 'nuestros', 'o', 'os',
55
+ 'otra', 'otras', 'otro', 'otros', 'para', 'pero', 'poco', 'por', 'porque',
56
+ 'que', 'quien', 'quienes', 'qué', 'se', 'sea', 'sean', 'ser', 'si', 'sí',
57
+ 'sido', 'siendo', 'sin', 'sobre', 'soy', 'su', 'sus', 'también', 'tan',
58
+ 'tanto', 'te', 'tener', 'ti', 'tiene', 'tienen', 'todo', 'todos', 'tu',
59
+ 'tus', 'un', 'una', 'unas', 'uno', 'unos', 'usted', 'ustedes', 'va', 'vais',
60
+ 'van', 'vas', 'ver', 'y', 'ya', 'yo',
61
+ ]);
62
+
63
+ // ── tokenización ──────────────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * Tokeniza texto en español de México. Pasos:
67
+ * 1. lowercase
68
+ * 2. eliminar puntuación
69
+ * 3. dividir en palabras
70
+ * 4. eliminar stopwords
71
+ * 5. eliminar tokens muy cortos (<2 chars)
72
+ *
73
+ * Preserva acentos (cá vs ca son distintos). No hace stemming.
74
+ *
75
+ * @param {string} texto
76
+ * @returns {string[]} tokens
77
+ */
78
+ function tokenize(texto) {
79
+ if (!texto || typeof texto !== 'string') return [];
80
+ return texto
81
+ .toLowerCase()
82
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ') // letras unicode + números
83
+ .split(/\s+/)
84
+ .filter(t => t.length >= 2 && !STOPWORDS.has(t));
85
+ }
86
+
87
+ // ── Jaccard ───────────────────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Jaccard similarity sobre conjuntos de tokens.
91
+ * jaccard(A, B) = |A ∩ B| / |A ∪ B|
92
+ *
93
+ * Ambos textos vacíos → 0 (no hay similitud, no hay info).
94
+ * Un texto vacío + uno con tokens → 0.
95
+ *
96
+ * @param {string} textA
97
+ * @param {string} textB
98
+ * @returns {number} en [0, 1]
99
+ */
100
+ function jaccardSimilarity(textA, textB) {
101
+ const a = new Set(tokenize(textA));
102
+ const b = new Set(tokenize(textB));
103
+ if (a.size === 0 && b.size === 0) return 0;
104
+ if (a.size === 0 || b.size === 0) return 0;
105
+
106
+ let inter = 0;
107
+ for (const t of a) if (b.has(t)) inter++;
108
+ const union = a.size + b.size - inter;
109
+ return inter / union;
110
+ }
111
+
112
+ // ── Cosine (para embeddings) ──────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Cosine similarity entre dos vectores numéricos.
116
+ * cos(A, B) = (A · B) / (||A|| × ||B||)
117
+ *
118
+ * @param {number[]} vecA
119
+ * @param {number[]} vecB
120
+ * @returns {number} en [-1, 1]
121
+ */
122
+ function cosineSimilarity(vecA, vecB) {
123
+ if (!Array.isArray(vecA) || !Array.isArray(vecB)) return 0;
124
+ if (vecA.length !== vecB.length || vecA.length === 0) return 0;
125
+
126
+ let dot = 0, normA = 0, normB = 0;
127
+ for (let i = 0; i < vecA.length; i++) {
128
+ const a = vecA[i] || 0;
129
+ const b = vecB[i] || 0;
130
+ dot += a * b;
131
+ normA += a * a;
132
+ normB += b * b;
133
+ }
134
+ if (normA === 0 || normB === 0) return 0;
135
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
136
+ }
137
+
138
+ // ── backend de embeddings (Xenova lazy) ───────────────────────────────────────
139
+
140
+ /**
141
+ * Estado lazy del backend Xenova. Se carga la primera vez que se invoca
142
+ * embeddingsBackend() y NO falla si no está instalado.
143
+ */
144
+ let _xenovaPipeline = null;
145
+ let _xenovaTried = false;
146
+
147
+ async function tryLoadXenova() {
148
+ if (_xenovaPipeline || _xenovaTried) return _xenovaPipeline;
149
+ _xenovaTried = true;
150
+ try {
151
+ // Carga dinámica para evitar require al inicio
152
+ const transformers = require('@xenova/transformers');
153
+ const { pipeline } = transformers;
154
+ _xenovaPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
155
+ return _xenovaPipeline;
156
+ } catch (_) {
157
+ // No instalado o error al cargar: silencioso. Caller usará fallback.
158
+ return null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Detecta el backend activo según SWL_SEMANTIC_BACKEND.
164
+ * Sin variable o valor != 'xenova' → 'jaccard'.
165
+ */
166
+ function activeBackend() {
167
+ const env = (process.env.SWL_SEMANTIC_BACKEND || '').toLowerCase().trim();
168
+ return env === 'xenova' ? 'xenova' : 'jaccard';
169
+ }
170
+
171
+ /**
172
+ * Embebe un texto en un vector. Solo disponible si Xenova está activo.
173
+ * Retorna null si el backend no es xenova o no se pudo cargar.
174
+ *
175
+ * @param {string} texto
176
+ * @returns {Promise<number[]|null>}
177
+ */
178
+ async function embed(texto) {
179
+ if (activeBackend() !== 'xenova') return null;
180
+ const pipe = await tryLoadXenova();
181
+ if (!pipe) return null;
182
+ try {
183
+ const out = await pipe(texto, { pooling: 'mean', normalize: true });
184
+ return Array.from(out.data);
185
+ } catch (_) {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ // ── ranking ──────────────────────────────────────────────────────────────────
191
+
192
+ /**
193
+ * Rankea items por similitud con la query.
194
+ *
195
+ * @param {string} query
196
+ * @param {Array<{id: string, text: string, embedding?: number[]}>} items
197
+ * @param {object} [opts]
198
+ * @param {number} [opts.threshold=0] - filtra resultados con score < threshold
199
+ * @param {number} [opts.limit=10] - limita resultados
200
+ * @returns {Promise<Array<{id, text, score, backend}>>}
201
+ */
202
+ async function semanticRank(query, items, opts = {}) {
203
+ const { threshold = 0, limit = 10 } = opts;
204
+ if (!query || !Array.isArray(items)) return [];
205
+
206
+ const backend = activeBackend();
207
+ let results;
208
+
209
+ if (backend === 'xenova') {
210
+ const queryEmb = await embed(query);
211
+ if (queryEmb) {
212
+ results = await Promise.all(items.map(async (item) => {
213
+ let itemEmb = item.embedding;
214
+ if (!itemEmb) itemEmb = await embed(item.text || '');
215
+ const score = (queryEmb && itemEmb) ? cosineSimilarity(queryEmb, itemEmb) : 0;
216
+ return { id: item.id, text: item.text, score, backend: 'xenova' };
217
+ }));
218
+ } else {
219
+ // Backend declarado pero no disponible → fallback transparente
220
+ results = items.map(item => ({
221
+ id: item.id,
222
+ text: item.text,
223
+ score: jaccardSimilarity(query, item.text || ''),
224
+ backend: 'jaccard-fallback',
225
+ }));
226
+ }
227
+ } else {
228
+ results = items.map(item => ({
229
+ id: item.id,
230
+ text: item.text,
231
+ score: jaccardSimilarity(query, item.text || ''),
232
+ backend: 'jaccard',
233
+ }));
234
+ }
235
+
236
+ return results
237
+ .filter(r => r.score >= threshold)
238
+ .sort((a, b) => b.score - a.score)
239
+ .slice(0, limit);
240
+ }
241
+
242
+ // ── exports ───────────────────────────────────────────────────────────────────
243
+
244
+ module.exports = {
245
+ tokenize,
246
+ jaccardSimilarity,
247
+ cosineSimilarity,
248
+ embed,
249
+ semanticRank,
250
+ activeBackend,
251
+ STOPWORDS,
252
+ };