@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,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
+ };