@saulwade/swl-ses 1.3.8 → 1.4.1

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 (148) hide show
  1. package/CLAUDE.md +15 -6
  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/bin/swl-webhook-server.js +198 -0
  6. package/comandos/swl/.evolved.json +22 -22
  7. package/comandos/swl/adoptar-proyecto.md +21 -1
  8. package/comandos/swl/claudemd.md +14 -1
  9. package/comandos/swl/contribuir.md +233 -233
  10. package/comandos/swl/exportar-vault.md +108 -0
  11. package/comandos/swl/nemesis.md +122 -0
  12. package/comandos/swl/nuevo-proyecto.md +24 -2
  13. package/comandos/swl/salud.md +34 -0
  14. package/comandos/swl/verificar.md +45 -0
  15. package/gateway/adapters/base.js +109 -0
  16. package/gateway/adapters/discord.js +167 -0
  17. package/gateway/adapters/email.js +221 -0
  18. package/gateway/adapters/slack.js +192 -0
  19. package/gateway/adapters/telegram.js +183 -0
  20. package/gateway/adapters/webhook.js +113 -0
  21. package/gateway/adapters/whatsapp.js +214 -0
  22. package/gateway/agent-executor.js +322 -0
  23. package/gateway/command-relay.js +271 -0
  24. package/gateway/cron/jobs.js +263 -0
  25. package/gateway/cron/scheduler.js +322 -0
  26. package/gateway/cron/store.js +335 -0
  27. package/gateway/index.js +320 -0
  28. package/gateway/lib/event-channel.js +191 -0
  29. package/gateway/session.js +131 -0
  30. package/gateway/webhook-server.js +324 -0
  31. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  32. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  33. package/habilidades/build-errors-nextjs/SKILL.md +55 -1
  34. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  35. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  36. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  37. package/habilidades/eval-framework/SKILL.md +212 -212
  38. package/habilidades/extractor-de-aprendizajes/SKILL.md +20 -10
  39. package/habilidades/feynman-auditor-swl/SKILL.md +123 -0
  40. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -0
  41. package/habilidades/harness-claude-code/SKILL.md +299 -299
  42. package/habilidades/infra-github-actions/SKILL.md +166 -166
  43. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  44. package/habilidades/manejo-errores/.evolved.json +8 -8
  45. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  46. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  47. package/habilidades/nextjs-testing/SKILL.md +89 -5
  48. package/habilidades/node-experto/SKILL.md +37 -1
  49. package/habilidades/patrones-python/SKILL.md +229 -229
  50. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  51. package/habilidades/planear-fase/SKILL.md +319 -319
  52. package/habilidades/react-experto/SKILL.md +45 -4
  53. package/habilidades/release-semver/.evolved.json +8 -8
  54. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -0
  55. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -0
  56. package/habilidades/tdd-workflow/SKILL.md +36 -4
  57. package/habilidades/testing-python/SKILL.md +340 -340
  58. package/habilidades/web-fetcher-routing/SKILL.md +75 -0
  59. package/hooks/claudemd-bloat-detector.js +161 -161
  60. package/hooks/inyeccion-contexto.js +8 -3
  61. package/hooks/lib/agent-routing.js +107 -107
  62. package/hooks/lib/auto-consolidator.js +335 -335
  63. package/hooks/lib/error-classifier.js +308 -308
  64. package/hooks/lib/merkle-audit.js +96 -96
  65. package/hooks/lib/provenance-tracker.js +191 -191
  66. package/hooks/lib/rate-limit-ip.js +177 -0
  67. package/hooks/lib/rate-limit-tracker.js +253 -253
  68. package/hooks/lib/resource-quota.js +122 -122
  69. package/hooks/lib/retry-jitter.js +165 -165
  70. package/hooks/lib/security-net.js +201 -0
  71. package/hooks/lib/skill-auditor.js +588 -588
  72. package/hooks/lib/sync-status.js +228 -228
  73. package/hooks/lib/taint-tracker.js +107 -107
  74. package/hooks/lib/text-similarity.js +241 -241
  75. package/hooks/lib/toon-compressor.js +245 -245
  76. package/hooks/lib/webhook-dedup.js +184 -0
  77. package/hooks/lib/webhook-verify.js +123 -0
  78. package/hooks/proteccion-rutas.js +120 -15
  79. package/hooks/registro-turnos.js +209 -209
  80. package/hooks/sugerir-regenerar-inventario.js +170 -170
  81. package/hooks/validar-formato-post-subagente.js +140 -140
  82. package/hooks/validar-memoria-hook.js +218 -218
  83. package/instintos/prompt-appendices.yaml +57 -57
  84. package/manifiestos/agent-output-schemas.json +57 -57
  85. package/manifiestos/modulos.json +31 -0
  86. package/manifiestos/skills-lock.json +1114 -1093
  87. package/package.json +6 -4
  88. package/plantillas/auditor-veto-template.md +105 -105
  89. package/plantillas/github-workflows/README.md +47 -47
  90. package/plantillas/github-workflows/release-please.yml +44 -44
  91. package/plantillas/github-workflows/swl-ci.yml +107 -107
  92. package/plantillas/github-workflows/swl-security.yml +51 -51
  93. package/plugin.json +2 -2
  94. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  95. package/reglas/arreglar-al-detectar.md +147 -147
  96. package/reglas/fragmentos-compartidos.md +152 -152
  97. package/reglas/harness-claude-code.md +213 -213
  98. package/reglas/usar-context7.md +226 -226
  99. package/reglas/usar-sistema-swl.md +251 -0
  100. package/schemas/diary-entry.schema.json +80 -80
  101. package/scripts/audit-tools/audit-history.js +330 -0
  102. package/scripts/audit-tools/bundle-tracker.js +290 -0
  103. package/scripts/audit-tools/canary-monitor.js +352 -0
  104. package/scripts/audit-tools/code-profiler.js +605 -0
  105. package/scripts/audit-tools/dep-doctor.js +320 -0
  106. package/scripts/audit-tools/env-validator.js +206 -0
  107. package/scripts/audit-tools/lib/fs-walk.js +48 -0
  108. package/scripts/audit-tools/lib/output.js +23 -0
  109. package/scripts/audit-tools/migration-checker.js +392 -0
  110. package/scripts/audit-tools/pentest-scanner.js +1436 -0
  111. package/scripts/benchmark-memoria.js +167 -167
  112. package/scripts/comandos/skills.js +251 -2
  113. package/scripts/configurar-branch-protection.js +418 -418
  114. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  115. package/scripts/field-report.js +199 -199
  116. package/scripts/generar-checklists-consolidados.js +273 -273
  117. package/scripts/generar-inventario.js +420 -420
  118. package/scripts/generar-matriz-lenguajes.js +271 -271
  119. package/scripts/lib/artefactos-python.js +43 -43
  120. package/scripts/lib/benchmark-metrics.js +160 -160
  121. package/scripts/lib/budget-enforcer.js +252 -252
  122. package/scripts/lib/configurar-ci.js +380 -380
  123. package/scripts/lib/contadores-inventario.js +217 -217
  124. package/scripts/lib/detectar-stack-detallado.js +307 -307
  125. package/scripts/lib/diary-entry.js +234 -234
  126. package/scripts/lib/eval-metrics-store.js +218 -218
  127. package/scripts/lib/eval-quality.js +171 -171
  128. package/scripts/lib/eval-schemas.js +144 -144
  129. package/scripts/lib/eval-self-correct.js +106 -106
  130. package/scripts/lib/eval-validator.js +185 -185
  131. package/scripts/lib/jaccard-similarity.js +98 -98
  132. package/scripts/lib/longmemeval-runner.js +125 -125
  133. package/scripts/lib/npm-version.js +261 -261
  134. package/scripts/lib/paquetes-conocidos.js +50 -50
  135. package/scripts/lib/prompt-builder.js +264 -264
  136. package/scripts/lib/rrf-fusion.js +175 -175
  137. package/scripts/lib/scoring-instintos.js +277 -277
  138. package/scripts/lib/semantic-search.js +252 -252
  139. package/scripts/limpiar-artefactos-python.js +131 -131
  140. package/scripts/mcp-server/README.md +128 -128
  141. package/scripts/mcp-server/handlers.js +206 -206
  142. package/scripts/migrar-csv-a-array.js +168 -168
  143. package/scripts/migrar-fase-dominio.js +201 -201
  144. package/scripts/publicar.js +511 -511
  145. package/scripts/run-eval.js +141 -141
  146. package/scripts/validar-manifest.js +195 -195
  147. package/scripts/validar-userland-vacio.js +110 -110
  148. package/scripts/verificar-release.js +110 -0
@@ -1,217 +1,217 @@
1
- 'use strict';
2
-
3
- /**
4
- * contadores-inventario.js
5
- *
6
- * Funciones puras zero-deps para validar consistencia entre los contadores
7
- * de inventario (agentes, skills, comandos, hooks, reglas) declarados en
8
- * `INVENTARIO.md` (fuente de verdad regenerada por
9
- * `scripts/generar-inventario.js`) y los contadores citados en los demás
10
- * documentos canónicos del repositorio.
11
- *
12
- * Motivado por el drift recurrente detectado el 2026-04-29: MAPEO_SKILLS_AGENTES.md
13
- * decía "153 skills" cuando la realidad eran 150; COMPACTACION.md reportaba
14
- * "147 skills, 40 comandos, 31 hooks". El hook `verificar-release.js` valida
15
- * versión pero no contadores. Este módulo cierra ese hueco.
16
- *
17
- * Cobertura intencional:
18
- * - Patrón resumen "X agentes, Y skills, Z comandos, W reglas, V hooks"
19
- * (separadores `,` o `+` o `y`) — valida los 5 números a la vez.
20
- * - Patrón "X agentes + Y habilidades + Z comandos + W reglas + V hooks"
21
- * común en tablas de perfiles `completo`.
22
- * - Patrón "Total de agentes: X" / "Total de skills: X" en MAPEO.
23
- *
24
- * Cobertura intencionalmente excluida (falsos positivos clásicos):
25
- * - "los 17 skills de Anthropic" (skills externos)
26
- * - "8 lenguajes × 5 reglas" (multiplicación)
27
- * - perfiles parciales "2 hooks", "6 hooks" (no son totales)
28
- * - notas históricas "estimé 28 hooks visualmente y propagué el error"
29
- * - referencias a versiones pasadas en MAPEO ("136 skills tras release X")
30
- * - filas de tabla histórica con SemVer + fecha ("| 5.4.0 | 2026-04-10 | 57 agentes...")
31
- *
32
- * Estas se ignoran porque el patrón resumen es el único que tiene los 5
33
- * componentes alineados; las menciones aisladas de un solo número se dejan
34
- * para una auditoría aparte (no bloqueante).
35
- */
36
-
37
- const COMPONENTES = ['agentes', 'skills', 'comandos', 'hooks', 'reglas'];
38
-
39
- /**
40
- * Parsea la sección "## Resumen" de INVENTARIO.md y retorna un mapa con
41
- * los contadores reales del sistema.
42
- *
43
- * Espera tabla del tipo:
44
- * ## Resumen
45
- * | Componente | Cantidad |
46
- * |-----------|----------|
47
- * | Agentes SWL | 59 |
48
- * | Habilidades | 150 |
49
- * | Comandos | 41 |
50
- * | Reglas base | 20 |
51
- * | Reglas por lenguaje | 40 |
52
- * | Hooks | 36 |
53
- *
54
- * Retorna `{ agentes, skills, comandos, hooks, reglas }`.
55
- * `skills` y `habilidades` se mapean al mismo número.
56
- * `reglas` = base + por_lenguaje.
57
- */
58
- function parsearInventario(rawMd) {
59
- const lineas = rawMd.split(/\r?\n/);
60
- const resumen = {};
61
- let enResumen = false;
62
- for (const linea of lineas) {
63
- if (/^##\s+Resumen\s*$/.test(linea)) { enResumen = true; continue; }
64
- if (enResumen && /^##\s+/.test(linea)) break;
65
- if (!enResumen) continue;
66
- const m = linea.match(/^\|\s*([^|]+?)\s*\|\s*(\d+)\s*\|/);
67
- if (m) resumen[m[1].trim()] = parseInt(m[2], 10);
68
- }
69
-
70
- const reglasBase = resumen['Reglas base'] || 0;
71
- const reglasLeng = resumen['Reglas por lenguaje'] || 0;
72
-
73
- return {
74
- agentes: resumen['Agentes SWL'] || 0,
75
- skills: resumen['Habilidades'] || 0,
76
- comandos: resumen['Comandos'] || 0,
77
- hooks: resumen['Hooks'] || 0,
78
- reglas: reglasBase + reglasLeng,
79
- };
80
- }
81
-
82
- /**
83
- * Detecta el patrón resumen en una línea y retorna los números encontrados.
84
- * Soporta separadores `,`, `+` y `y`. Tolera el orden canónico
85
- * agentes → skills/habilidades → comandos → reglas → hooks.
86
- *
87
- * Ejemplos que matchean:
88
- * "59 agentes, 150 skills, 41 comandos, 60 reglas, 36 hooks"
89
- * "59 agentes + 150 habilidades + 41 comandos + 60 reglas + 36 hooks"
90
- * "59 agentes, 150 skills, 41 comandos, 60 reglas y 36 hooks"
91
- * "Frontmatter: 59 agentes + 150 skills + 41 comandos verificados"
92
- * (los 3 primeros se validan; los 2 últimos faltan → no full match,
93
- * se devuelve match parcial con flags)
94
- *
95
- * Retorna `null` si no encuentra al menos `agentes` y un segundo componente.
96
- */
97
- function extraerResumenDeLinea(linea) {
98
- // Patrón principal: 5 componentes en orden
99
- const re5 = /(\d{1,4})\s+agentes?[\s,+y]+\s*(\d{1,4})\s+(?:skills|habilidades)[\s,+y]+\s*(\d{1,4})\s+comandos[\s,+y]+\s*(\d{1,4})\s+reglas[\s,+y]+\s*(\d{1,4})\s+hooks/i;
100
- const m5 = linea.match(re5);
101
- if (m5) {
102
- return {
103
- tipo: 'resumen5',
104
- texto: m5[0],
105
- agentes: parseInt(m5[1], 10),
106
- skills: parseInt(m5[2], 10),
107
- comandos: parseInt(m5[3], 10),
108
- reglas: parseInt(m5[4], 10),
109
- hooks: parseInt(m5[5], 10),
110
- };
111
- }
112
-
113
- // Patrón parcial: 3 componentes (agentes, skills/habilidades, comandos)
114
- const re3 = /(\d{1,4})\s+agentes?[\s,+y]+\s*(\d{1,4})\s+(?:skills|habilidades)[\s,+y]+\s*(\d{1,4})\s+comandos/i;
115
- const m3 = linea.match(re3);
116
- if (m3) {
117
- return {
118
- tipo: 'resumen3',
119
- texto: m3[0],
120
- agentes: parseInt(m3[1], 10),
121
- skills: parseInt(m3[2], 10),
122
- comandos: parseInt(m3[3], 10),
123
- };
124
- }
125
-
126
- return null;
127
- }
128
-
129
- /**
130
- * Decide si una línea debe ignorarse por ser obviamente histórica
131
- * (fila de changelog/timeline con versión SemVer + fecha en formato tabla).
132
- */
133
- function esLineaHistorica(linea) {
134
- // Tabla con versión SemVer + fecha ISO: "| 5.4.0 | 2026-04-10 | ..."
135
- if (/^\s*\|\s*\d+\.\d+\.\d+\s*\|\s*\d{4}-\d{2}-\d{2}/.test(linea)) return true;
136
- // "Baseline:" suele ser nota histórica
137
- if (/\bBaseline\s*:/i.test(linea)) return true;
138
- // "antes de v|previa a|tras release N" sugiere historia
139
- if (/\b(?:antes de v|previa a|tras release|en v\d+\.\d+)\b/i.test(linea)) return true;
140
- // Línea entre comillas dobles (cita textual de un patrón documentado): "22 hooks"
141
- if (/"\s*\d+\s+(?:agentes|skills|habilidades|comandos|hooks|reglas)\s*"/i.test(linea)) return true;
142
- return false;
143
- }
144
-
145
- /**
146
- * Valida un archivo contra los contadores reales.
147
- * Solo flagea las líneas que contienen patrón resumen identificable.
148
- *
149
- * Retorna { ok, lineas_revisadas, discrepancias: [{linea, encontrado, esperado, tipo, contexto}] }
150
- */
151
- function validarArchivo(rawMd, contadoresReales) {
152
- const lineas = rawMd.split(/\r?\n/);
153
- const discrepancias = [];
154
- let lineasRevisadas = 0;
155
-
156
- for (let i = 0; i < lineas.length; i++) {
157
- if (esLineaHistorica(lineas[i])) continue;
158
- const r = extraerResumenDeLinea(lineas[i]);
159
- if (!r) continue;
160
- lineasRevisadas++;
161
-
162
- const campos = r.tipo === 'resumen5'
163
- ? COMPONENTES
164
- : ['agentes', 'skills', 'comandos'];
165
-
166
- for (const campo of campos) {
167
- if (r[campo] !== contadoresReales[campo]) {
168
- discrepancias.push({
169
- linea: i + 1,
170
- campo,
171
- encontrado: r[campo],
172
- esperado: contadoresReales[campo],
173
- tipo: r.tipo,
174
- contexto: lineas[i].trim().slice(0, 110),
175
- });
176
- }
177
- }
178
- }
179
-
180
- // Patrón nominal: "**Total de agentes**: N", "**Total de skills**: N"
181
- const reTotal = /\*?\*?Total\s+de\s+(agentes|skills|habilidades|comandos|hooks|reglas)\*?\*?\s*:\s*(\d{1,4})/gi;
182
- for (let i = 0; i < lineas.length; i++) {
183
- if (esLineaHistorica(lineas[i])) continue;
184
- let m;
185
- reTotal.lastIndex = 0;
186
- while ((m = reTotal.exec(lineas[i])) !== null) {
187
- const palabra = m[1].toLowerCase();
188
- const campo = palabra === 'habilidades' ? 'skills' : palabra;
189
- const num = parseInt(m[2], 10);
190
- lineasRevisadas++;
191
- if (num !== contadoresReales[campo]) {
192
- discrepancias.push({
193
- linea: i + 1,
194
- campo,
195
- encontrado: num,
196
- esperado: contadoresReales[campo],
197
- tipo: 'total_nominal',
198
- contexto: lineas[i].trim().slice(0, 110),
199
- });
200
- }
201
- }
202
- }
203
-
204
- return {
205
- ok: discrepancias.length === 0,
206
- lineas_revisadas: lineasRevisadas,
207
- discrepancias,
208
- };
209
- }
210
-
211
- module.exports = {
212
- COMPONENTES,
213
- parsearInventario,
214
- extraerResumenDeLinea,
215
- esLineaHistorica,
216
- validarArchivo,
217
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * contadores-inventario.js
5
+ *
6
+ * Funciones puras zero-deps para validar consistencia entre los contadores
7
+ * de inventario (agentes, skills, comandos, hooks, reglas) declarados en
8
+ * `INVENTARIO.md` (fuente de verdad regenerada por
9
+ * `scripts/generar-inventario.js`) y los contadores citados en los demás
10
+ * documentos canónicos del repositorio.
11
+ *
12
+ * Motivado por el drift recurrente detectado el 2026-04-29: MAPEO_SKILLS_AGENTES.md
13
+ * decía "153 skills" cuando la realidad eran 150; COMPACTACION.md reportaba
14
+ * "147 skills, 40 comandos, 31 hooks". El hook `verificar-release.js` valida
15
+ * versión pero no contadores. Este módulo cierra ese hueco.
16
+ *
17
+ * Cobertura intencional:
18
+ * - Patrón resumen "X agentes, Y skills, Z comandos, W reglas, V hooks"
19
+ * (separadores `,` o `+` o `y`) — valida los 5 números a la vez.
20
+ * - Patrón "X agentes + Y habilidades + Z comandos + W reglas + V hooks"
21
+ * común en tablas de perfiles `completo`.
22
+ * - Patrón "Total de agentes: X" / "Total de skills: X" en MAPEO.
23
+ *
24
+ * Cobertura intencionalmente excluida (falsos positivos clásicos):
25
+ * - "los 17 skills de Anthropic" (skills externos)
26
+ * - "8 lenguajes × 5 reglas" (multiplicación)
27
+ * - perfiles parciales "2 hooks", "6 hooks" (no son totales)
28
+ * - notas históricas "estimé 28 hooks visualmente y propagué el error"
29
+ * - referencias a versiones pasadas en MAPEO ("136 skills tras release X")
30
+ * - filas de tabla histórica con SemVer + fecha ("| 5.4.0 | 2026-04-10 | 57 agentes...")
31
+ *
32
+ * Estas se ignoran porque el patrón resumen es el único que tiene los 5
33
+ * componentes alineados; las menciones aisladas de un solo número se dejan
34
+ * para una auditoría aparte (no bloqueante).
35
+ */
36
+
37
+ const COMPONENTES = ['agentes', 'skills', 'comandos', 'hooks', 'reglas'];
38
+
39
+ /**
40
+ * Parsea la sección "## Resumen" de INVENTARIO.md y retorna un mapa con
41
+ * los contadores reales del sistema.
42
+ *
43
+ * Espera tabla del tipo:
44
+ * ## Resumen
45
+ * | Componente | Cantidad |
46
+ * |-----------|----------|
47
+ * | Agentes SWL | 59 |
48
+ * | Habilidades | 150 |
49
+ * | Comandos | 41 |
50
+ * | Reglas base | 20 |
51
+ * | Reglas por lenguaje | 40 |
52
+ * | Hooks | 36 |
53
+ *
54
+ * Retorna `{ agentes, skills, comandos, hooks, reglas }`.
55
+ * `skills` y `habilidades` se mapean al mismo número.
56
+ * `reglas` = base + por_lenguaje.
57
+ */
58
+ function parsearInventario(rawMd) {
59
+ const lineas = rawMd.split(/\r?\n/);
60
+ const resumen = {};
61
+ let enResumen = false;
62
+ for (const linea of lineas) {
63
+ if (/^##\s+Resumen\s*$/.test(linea)) { enResumen = true; continue; }
64
+ if (enResumen && /^##\s+/.test(linea)) break;
65
+ if (!enResumen) continue;
66
+ const m = linea.match(/^\|\s*([^|]+?)\s*\|\s*(\d+)\s*\|/);
67
+ if (m) resumen[m[1].trim()] = parseInt(m[2], 10);
68
+ }
69
+
70
+ const reglasBase = resumen['Reglas base'] || 0;
71
+ const reglasLeng = resumen['Reglas por lenguaje'] || 0;
72
+
73
+ return {
74
+ agentes: resumen['Agentes SWL'] || 0,
75
+ skills: resumen['Habilidades'] || 0,
76
+ comandos: resumen['Comandos'] || 0,
77
+ hooks: resumen['Hooks'] || 0,
78
+ reglas: reglasBase + reglasLeng,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Detecta el patrón resumen en una línea y retorna los números encontrados.
84
+ * Soporta separadores `,`, `+` y `y`. Tolera el orden canónico
85
+ * agentes → skills/habilidades → comandos → reglas → hooks.
86
+ *
87
+ * Ejemplos que matchean:
88
+ * "59 agentes, 150 skills, 41 comandos, 60 reglas, 36 hooks"
89
+ * "59 agentes + 150 habilidades + 41 comandos + 60 reglas + 36 hooks"
90
+ * "59 agentes, 150 skills, 41 comandos, 60 reglas y 36 hooks"
91
+ * "Frontmatter: 59 agentes + 150 skills + 41 comandos verificados"
92
+ * (los 3 primeros se validan; los 2 últimos faltan → no full match,
93
+ * se devuelve match parcial con flags)
94
+ *
95
+ * Retorna `null` si no encuentra al menos `agentes` y un segundo componente.
96
+ */
97
+ function extraerResumenDeLinea(linea) {
98
+ // Patrón principal: 5 componentes en orden
99
+ const re5 = /(\d{1,4})\s+agentes?[\s,+y]+\s*(\d{1,4})\s+(?:skills|habilidades)[\s,+y]+\s*(\d{1,4})\s+comandos[\s,+y]+\s*(\d{1,4})\s+reglas[\s,+y]+\s*(\d{1,4})\s+hooks/i;
100
+ const m5 = linea.match(re5);
101
+ if (m5) {
102
+ return {
103
+ tipo: 'resumen5',
104
+ texto: m5[0],
105
+ agentes: parseInt(m5[1], 10),
106
+ skills: parseInt(m5[2], 10),
107
+ comandos: parseInt(m5[3], 10),
108
+ reglas: parseInt(m5[4], 10),
109
+ hooks: parseInt(m5[5], 10),
110
+ };
111
+ }
112
+
113
+ // Patrón parcial: 3 componentes (agentes, skills/habilidades, comandos)
114
+ const re3 = /(\d{1,4})\s+agentes?[\s,+y]+\s*(\d{1,4})\s+(?:skills|habilidades)[\s,+y]+\s*(\d{1,4})\s+comandos/i;
115
+ const m3 = linea.match(re3);
116
+ if (m3) {
117
+ return {
118
+ tipo: 'resumen3',
119
+ texto: m3[0],
120
+ agentes: parseInt(m3[1], 10),
121
+ skills: parseInt(m3[2], 10),
122
+ comandos: parseInt(m3[3], 10),
123
+ };
124
+ }
125
+
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Decide si una línea debe ignorarse por ser obviamente histórica
131
+ * (fila de changelog/timeline con versión SemVer + fecha en formato tabla).
132
+ */
133
+ function esLineaHistorica(linea) {
134
+ // Tabla con versión SemVer + fecha ISO: "| 5.4.0 | 2026-04-10 | ..."
135
+ if (/^\s*\|\s*\d+\.\d+\.\d+\s*\|\s*\d{4}-\d{2}-\d{2}/.test(linea)) return true;
136
+ // "Baseline:" suele ser nota histórica
137
+ if (/\bBaseline\s*:/i.test(linea)) return true;
138
+ // "antes de v|previa a|tras release N" sugiere historia
139
+ if (/\b(?:antes de v|previa a|tras release|en v\d+\.\d+)\b/i.test(linea)) return true;
140
+ // Línea entre comillas dobles (cita textual de un patrón documentado): "22 hooks"
141
+ if (/"\s*\d+\s+(?:agentes|skills|habilidades|comandos|hooks|reglas)\s*"/i.test(linea)) return true;
142
+ return false;
143
+ }
144
+
145
+ /**
146
+ * Valida un archivo contra los contadores reales.
147
+ * Solo flagea las líneas que contienen patrón resumen identificable.
148
+ *
149
+ * Retorna { ok, lineas_revisadas, discrepancias: [{linea, encontrado, esperado, tipo, contexto}] }
150
+ */
151
+ function validarArchivo(rawMd, contadoresReales) {
152
+ const lineas = rawMd.split(/\r?\n/);
153
+ const discrepancias = [];
154
+ let lineasRevisadas = 0;
155
+
156
+ for (let i = 0; i < lineas.length; i++) {
157
+ if (esLineaHistorica(lineas[i])) continue;
158
+ const r = extraerResumenDeLinea(lineas[i]);
159
+ if (!r) continue;
160
+ lineasRevisadas++;
161
+
162
+ const campos = r.tipo === 'resumen5'
163
+ ? COMPONENTES
164
+ : ['agentes', 'skills', 'comandos'];
165
+
166
+ for (const campo of campos) {
167
+ if (r[campo] !== contadoresReales[campo]) {
168
+ discrepancias.push({
169
+ linea: i + 1,
170
+ campo,
171
+ encontrado: r[campo],
172
+ esperado: contadoresReales[campo],
173
+ tipo: r.tipo,
174
+ contexto: lineas[i].trim().slice(0, 110),
175
+ });
176
+ }
177
+ }
178
+ }
179
+
180
+ // Patrón nominal: "**Total de agentes**: N", "**Total de skills**: N"
181
+ const reTotal = /\*?\*?Total\s+de\s+(agentes|skills|habilidades|comandos|hooks|reglas)\*?\*?\s*:\s*(\d{1,4})/gi;
182
+ for (let i = 0; i < lineas.length; i++) {
183
+ if (esLineaHistorica(lineas[i])) continue;
184
+ let m;
185
+ reTotal.lastIndex = 0;
186
+ while ((m = reTotal.exec(lineas[i])) !== null) {
187
+ const palabra = m[1].toLowerCase();
188
+ const campo = palabra === 'habilidades' ? 'skills' : palabra;
189
+ const num = parseInt(m[2], 10);
190
+ lineasRevisadas++;
191
+ if (num !== contadoresReales[campo]) {
192
+ discrepancias.push({
193
+ linea: i + 1,
194
+ campo,
195
+ encontrado: num,
196
+ esperado: contadoresReales[campo],
197
+ tipo: 'total_nominal',
198
+ contexto: lineas[i].trim().slice(0, 110),
199
+ });
200
+ }
201
+ }
202
+ }
203
+
204
+ return {
205
+ ok: discrepancias.length === 0,
206
+ lineas_revisadas: lineasRevisadas,
207
+ discrepancias,
208
+ };
209
+ }
210
+
211
+ module.exports = {
212
+ COMPONENTES,
213
+ parsearInventario,
214
+ extraerResumenDeLinea,
215
+ esLineaHistorica,
216
+ validarArchivo,
217
+ };