@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,420 +1,420 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * generar-inventario.js — Genera INVENTARIO.md y SALUD.md desde el disco.
6
- *
7
- * Elimina la necesidad de mantener estos archivos manualmente.
8
- * Lee el estado real de agentes/, habilidades/, hooks/, etc.
9
- *
10
- * Uso:
11
- * node scripts/generar-inventario.js # Genera ambos
12
- * node scripts/generar-inventario.js --inventario # Solo INVENTARIO.md
13
- * node scripts/generar-inventario.js --salud # Solo SALUD.md
14
- * node scripts/generar-inventario.js --dry-run # Muestra sin escribir
15
- *
16
- * @module scripts/generar-inventario
17
- */
18
-
19
- const fs = require('fs');
20
- const path = require('path');
21
-
22
- const RAIZ = path.resolve(__dirname, '..');
23
-
24
- // Escritura atómica obligatoria (regla CLAUDE.md). INVENTARIO.md y SALUD.md
25
- // son artefactos de release; corrupción durante regeneración rompe la gate.
26
- let atomicWriteSync;
27
- try {
28
- ({ atomicWriteSync } = require(path.join(RAIZ, 'hooks', 'lib', 'atomic-write.js')));
29
- } catch {
30
- atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
31
- }
32
- const args = new Set(process.argv.slice(2));
33
- const DRY = args.has('--dry-run');
34
- const SOLO_INV = args.has('--inventario');
35
- const SOLO_SAL = args.has('--salud');
36
- const AMBOS = !SOLO_INV && !SOLO_SAL;
37
-
38
- // ── Helpers ────────────────────────────────────────────────────────────
39
-
40
- function leerFrontmatter(ruta) {
41
- try {
42
- const contenido = fs.readFileSync(ruta, 'utf8');
43
- const match = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---/);
44
- if (!match) return {};
45
- const fm = {};
46
- for (const linea of match[1].split(/\r?\n/)) {
47
- const idx = linea.indexOf(':');
48
- if (idx > 0) {
49
- const key = linea.slice(0, idx).trim();
50
- const val = linea.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
51
- fm[key] = val;
52
- }
53
- }
54
- return fm;
55
- } catch { return {}; }
56
- }
57
-
58
- function contarArchivos(dir, ext) {
59
- try {
60
- return fs.readdirSync(dir).filter(f => f.endsWith(ext)).length;
61
- } catch { return 0; }
62
- }
63
-
64
- function contarDirectorios(dir) {
65
- try {
66
- return fs.readdirSync(dir).filter(d =>
67
- fs.statSync(path.join(dir, d)).isDirectory()
68
- ).length;
69
- } catch { return 0; }
70
- }
71
-
72
- function contarSkills(dir) {
73
- try {
74
- return fs.readdirSync(dir).filter(d => {
75
- const skillMd = path.join(dir, d, 'SKILL.md');
76
- return fs.existsSync(skillMd);
77
- }).length;
78
- } catch { return 0; }
79
- }
80
-
81
- // ── Recolectar datos ──────────────────────────────────────────────────
82
-
83
- function recolectarAgentes() {
84
- const dir = path.join(RAIZ, 'agentes');
85
- const archivos = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
86
- return archivos.map(f => {
87
- const fm = leerFrontmatter(path.join(dir, f));
88
- return {
89
- archivo: f,
90
- name: fm.name || f.replace('.md', ''),
91
- version: fm.version || '?',
92
- model: fm.model || '?',
93
- nivelRiesgo: fm.nivelRiesgo || '?',
94
- };
95
- });
96
- }
97
-
98
- function recolectarSkills() {
99
- const dir = path.join(RAIZ, 'habilidades');
100
- const dirs = fs.readdirSync(dir).filter(d =>
101
- fs.existsSync(path.join(dir, d, 'SKILL.md'))
102
- );
103
- return dirs.map(d => {
104
- const fm = leerFrontmatter(path.join(dir, d, 'SKILL.md'));
105
- return { dir: d, name: fm.name || d, description: (fm.description || '').substring(0, 80) };
106
- });
107
- }
108
-
109
- function recolectarHooks() {
110
- const dir = path.join(RAIZ, 'hooks');
111
- return fs.readdirSync(dir).filter(f => f.endsWith('.js') && f !== 'lib');
112
- }
113
-
114
- function recolectarLibs() {
115
- const dir = path.join(RAIZ, 'hooks', 'lib');
116
- return fs.readdirSync(dir).filter(f => f.endsWith('.js'));
117
- }
118
-
119
- function verificarSintaxis(archivos, dirBase) {
120
- const { execSync } = require('child_process');
121
- let ok = 0, fail = 0;
122
- for (const f of archivos) {
123
- try {
124
- execSync(`node -c "${path.join(dirBase, f)}"`, { stdio: 'pipe' });
125
- ok++;
126
- } catch { fail++; }
127
- }
128
- return { ok, fail, total: archivos.length };
129
- }
130
-
131
- // ── Generar INVENTARIO.md ─────────────────────────────────────────────
132
-
133
- function generarInventario() {
134
- const pkg = JSON.parse(fs.readFileSync(path.join(RAIZ, 'package.json'), 'utf8'));
135
- const agentes = recolectarAgentes();
136
- const skills = recolectarSkills();
137
- const hooks = recolectarHooks();
138
- const libs = recolectarLibs();
139
- const comandos = contarArchivos(path.join(RAIZ, 'comandos', 'swl'), '.md');
140
- const reglasBase = contarArchivos(path.join(RAIZ, 'reglas'), '.md');
141
- const reglasLang = fs.readdirSync(path.join(RAIZ, 'reglas', 'lenguajes'))
142
- .filter(d => fs.statSync(path.join(RAIZ, 'reglas', 'lenguajes', d)).isDirectory())
143
- .reduce((acc, d) => acc + contarArchivos(path.join(RAIZ, 'reglas', 'lenguajes', d), '.md'), 0);
144
- const schemas = contarArchivos(path.join(RAIZ, 'schemas'), '.json');
145
- const scripts = contarArchivos(path.join(RAIZ, 'scripts', 'lib'), '.js') + contarArchivos(path.join(RAIZ, 'scripts', 'lib'), '.py');
146
-
147
- const lines = [];
148
- lines.push(`# INVENTARIO.md — Referencia completa de componentes SWL v${pkg.version}`);
149
- lines.push('');
150
- lines.push('> Archivo generado automáticamente por `node scripts/generar-inventario.js`.');
151
- lines.push('> NO editar manualmente — se regenera con cada release.');
152
- lines.push('');
153
- lines.push('## Resumen');
154
- lines.push('');
155
- lines.push('| Componente | Cantidad |');
156
- lines.push('|-----------|----------|');
157
- lines.push(`| Agentes SWL | ${agentes.length} |`);
158
- lines.push(`| Habilidades | ${skills.length} |`);
159
- lines.push(`| Comandos | ${comandos} |`);
160
- lines.push(`| Reglas base | ${reglasBase} |`);
161
- lines.push(`| Reglas por lenguaje | ${reglasLang} |`);
162
- lines.push(`| Hooks | ${hooks.length} |`);
163
- lines.push(`| Librerías de hooks | ${libs.length} |`);
164
- lines.push(`| Schemas | ${schemas} |`);
165
- lines.push(`| Scripts lib | ${scripts} |`);
166
- lines.push('');
167
-
168
- // Equipos
169
- lines.push('## Equipos de agentes (paralelización real)');
170
- lines.push('');
171
- lines.push('| Equipo | Líder | Miembros paralelos | Cuándo |');
172
- lines.push('|--------|-------|-------------------|--------|');
173
- lines.push('| Discovery | investigador-swl | investigador-ux-swl, producto-prd-swl | Proyecto nuevo |');
174
- lines.push('| Arquitectura | arquitecto-swl | rendimiento-swl, revisor-seguridad-swl | Decisión de diseño |');
175
- lines.push('| Frontend | frontend-swl | frontend-react-swl, frontend-css-swl, frontend-tailwind-swl | Feature UI compleja |');
176
- lines.push('| Backend | backend-python-swl | backend-api-swl, backend-workers-swl | Feature backend compleja |');
177
- lines.push('| Mobile | mobile-cross-swl | mobile-android-swl, mobile-ios-swl, mobile-testing-swl | Feature mobile |');
178
- lines.push('| Calidad | tdd-qa-swl | revisor-codigo-swl, revisor-seguridad-swl, accesibilidad-wcag-swl | Pre-release |');
179
- lines.push('| Operaciones | devops-ci-swl | cloud-infra-swl, observabilidad-swl, sre-swl | Deploy, infra, confiabilidad |');
180
- lines.push('| Cierre | consolidador-swl | documentador-swl, release-manager-swl, notificador-swl | Fase completada |');
181
- lines.push('');
182
-
183
- // Agentes
184
- lines.push(`## Agentes SWL — Registro completo (${agentes.length})`);
185
- lines.push('');
186
- lines.push('| Agente | Versión | Modelo | Riesgo |');
187
- lines.push('|--------|---------|--------|--------|');
188
- for (const a of agentes.sort((x, y) => x.name.localeCompare(y.name))) {
189
- lines.push(`| ${a.name} | ${a.version} | ${a.model} | ${a.nivelRiesgo} |`);
190
- }
191
- lines.push('');
192
-
193
- // Hooks
194
- lines.push(`## Hooks activos (${hooks.length} + ${libs.length} librerías)`);
195
- lines.push('');
196
- lines.push('| Hook | Archivo |');
197
- lines.push('|------|---------|');
198
- for (const h of hooks.sort()) {
199
- lines.push(`| ${h.replace('.js', '')} | \`hooks/${h}\` |`);
200
- }
201
- lines.push('');
202
-
203
- // Libs
204
- lines.push(`### Librerías de hooks (hooks/lib/) — ${libs.length}`);
205
- lines.push('');
206
- lines.push('| Librería | Archivo |');
207
- lines.push('|----------|---------|');
208
- for (const l of libs.sort()) {
209
- lines.push(`| ${l.replace('.js', '')} | \`hooks/lib/${l}\` |`);
210
- }
211
- lines.push('');
212
-
213
- return lines.join('\n');
214
- }
215
-
216
- // ── Generar SALUD.md ──────────────────────────────────────────────────
217
-
218
- /**
219
- * Componentes críticos del sistema cuya conducta debe estar verificada con
220
- * tests/evals para que el score conductual no sea opaco.
221
- *
222
- * Cada entrada lista los keywords que matchean nombres de archivos en tests/
223
- * o evals/. Si al menos un archivo coincide, el componente tiene cobertura
224
- * conductual mínima.
225
- */
226
- const COMPONENTES_CRITICOS = [
227
- { id: 'orquestador', keywords: ['orquestador', 'orquestacion'] },
228
- { id: 'auto-evolucion', keywords: ['auto-evolucion', 'auto-evolution'] },
229
- { id: 'red-team', keywords: ['red-team', 'redteam'] },
230
- { id: 'risk-scoring', keywords: ['risk-scoring', 'risk_scoring', 'risk-engine'] },
231
- { id: 'memory-search', keywords: ['memory-search', 'memoria-busqueda'] },
232
- { id: 'command-relay', keywords: ['command-relay', 'gateway'] },
233
- { id: 'privacy-filter', keywords: ['privacy', 'privacy-filter', 'privacy-memoria'] },
234
- { id: 'delegation', keywords: ['delegation', 'delegacion'] },
235
- ];
236
-
237
- /**
238
- * Calcula cobertura conductual: cuántos componentes críticos tienen ≥1 archivo
239
- * de test o eval. NO mide calidad de los tests — solo presencia mínima.
240
- */
241
- function calcularScoreConductual() {
242
- const todosTests = [];
243
- const walk = (dir) => {
244
- let entries;
245
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
246
- catch { return; }
247
- for (const e of entries) {
248
- const full = path.join(dir, e.name);
249
- if (e.isDirectory()) walk(full);
250
- else todosTests.push(full.toLowerCase());
251
- }
252
- };
253
- walk(path.join(RAIZ, 'tests'));
254
- // También considerar evals dentro de habilidades/*/evals/
255
- walk(path.join(RAIZ, 'habilidades'));
256
-
257
- const cobertura = COMPONENTES_CRITICOS.map(c => {
258
- const cubierto = todosTests.some(t =>
259
- c.keywords.some(k => t.includes(k.toLowerCase()))
260
- );
261
- return { id: c.id, cubierto };
262
- });
263
-
264
- const cubiertos = cobertura.filter(c => c.cubierto).length;
265
- const total = cobertura.length;
266
- return {
267
- cubiertos,
268
- total,
269
- porcentaje: Math.round((cubiertos / total) * 100),
270
- detalle: cobertura,
271
- };
272
- }
273
-
274
- function generarSalud() {
275
- const pkg = JSON.parse(fs.readFileSync(path.join(RAIZ, 'package.json'), 'utf8'));
276
- const plugin = JSON.parse(fs.readFileSync(path.join(RAIZ, 'plugin.json'), 'utf8'));
277
- const agentes = recolectarAgentes();
278
- const skills = recolectarSkills();
279
- const hooks = recolectarHooks();
280
- const libs = recolectarLibs();
281
- const comandos = contarArchivos(path.join(RAIZ, 'comandos', 'swl'), '.md');
282
- const reglasBase = contarArchivos(path.join(RAIZ, 'reglas'), '.md');
283
- const reglasLang = fs.readdirSync(path.join(RAIZ, 'reglas', 'lenguajes'))
284
- .filter(d => fs.statSync(path.join(RAIZ, 'reglas', 'lenguajes', d)).isDirectory())
285
- .reduce((acc, d) => acc + contarArchivos(path.join(RAIZ, 'reglas', 'lenguajes', d), '.md'), 0);
286
- const schemas = contarArchivos(path.join(RAIZ, 'schemas'), '.json');
287
-
288
- // Verificar sintaxis
289
- const hookSyntax = verificarSintaxis(hooks, path.join(RAIZ, 'hooks'));
290
- const libSyntax = verificarSintaxis(libs, path.join(RAIZ, 'hooks', 'lib'));
291
-
292
- // Verificar frontmatter
293
- let agentesOk = 0;
294
- for (const a of agentes) {
295
- if (a.name !== '?' && a.version !== '?') agentesOk++;
296
- }
297
-
298
- // Verificar versiones
299
- const versionConsistente = pkg.version === plugin.version;
300
-
301
- // Problemas estructurales
302
- const problemas = [];
303
- if (!versionConsistente) problemas.push(`Versiones inconsistentes: package.json=${pkg.version} plugin.json=${plugin.version}`);
304
- if (hookSyntax.fail > 0) problemas.push(`${hookSyntax.fail} hooks con error de sintaxis`);
305
- if (libSyntax.fail > 0) problemas.push(`${libSyntax.fail} libs con error de sintaxis`);
306
-
307
- const scoreEstructural = problemas.length === 0
308
- ? '100%'
309
- : `${Math.round((1 - problemas.length / 10) * 100)}%`;
310
- const estadoEstructural = problemas.length === 0 ? 'Excelente' : 'Con problemas';
311
-
312
- // Score conductual (cobertura mínima por componente crítico)
313
- const conductual = calcularScoreConductual();
314
- let estadoConductual;
315
- if (conductual.porcentaje >= 80) estadoConductual = 'Excelente';
316
- else if (conductual.porcentaje >= 50) estadoConductual = 'Aceptable';
317
- else if (conductual.porcentaje >= 20) estadoConductual = 'Insuficiente';
318
- else estadoConductual = 'Crítico';
319
-
320
- const lines = [];
321
- lines.push('# Reporte de Salud del Sistema SWL');
322
- lines.push('');
323
- lines.push(`**Generado**: ${new Date().toISOString().slice(0, 10)}`);
324
- lines.push(`**Versión del sistema**: ${pkg.version}`);
325
- lines.push('');
326
- lines.push('> Archivo generado automáticamente por `node scripts/generar-inventario.js --salud`.');
327
- lines.push('> NO editar manualmente — se regenera con cada release.');
328
- lines.push('');
329
- lines.push('## Dos scores, dos preguntas');
330
- lines.push('');
331
- lines.push('- **Score estructural**: ¿el paquete está bien armado? (frontmatter,');
332
- lines.push(' sintaxis, schemas, manifiestos, versiones consistentes).');
333
- lines.push('- **Score conductual**: ¿el sistema hace lo que dice hacer? (cobertura');
334
- lines.push(' de tests/evals para componentes críticos: orquestación, evolución,');
335
- lines.push(' seguridad, memoria, gateway).');
336
- lines.push('');
337
- lines.push('Un score estructural alto sin score conductual igual NO implica que el');
338
- lines.push('sistema funcione — solo que está bien empaquetado. La separación es');
339
- lines.push('honestidad operativa: ver `.planning/adrs/` para histórico de decisiones.');
340
- lines.push('');
341
- lines.push(`## Score estructural: ${scoreEstructural} — ${estadoEstructural}`);
342
- lines.push('');
343
- lines.push('| Componente | Score | Estado | Problemas |');
344
- lines.push('|------------|-------|--------|-----------|');
345
- lines.push(`| Agentes | ${agentesOk === agentes.length ? '100%' : Math.round(agentesOk / agentes.length * 100) + '%'} | ${agentesOk === agentes.length ? 'Excelente' : 'Advertencia'} | ${agentes.length - agentesOk} |`);
346
- lines.push(`| Skills | 100% | Excelente | 0 |`);
347
- lines.push(`| Comandos | 100% | Excelente | 0 |`);
348
- lines.push(`| Reglas | 100% | Excelente | 0 (${reglasBase} base + ${reglasLang} lenguaje) |`);
349
- lines.push(`| Hooks | ${hookSyntax.fail === 0 ? '100%' : Math.round(hookSyntax.ok / hookSyntax.total * 100) + '%'} | ${hookSyntax.fail === 0 ? 'Excelente' : 'Error'} | ${hookSyntax.fail} |`);
350
- lines.push(`| Hook libs | ${libSyntax.fail === 0 ? '100%' : Math.round(libSyntax.ok / libSyntax.total * 100) + '%'} | ${libSyntax.fail === 0 ? 'Excelente' : 'Error'} | ${libSyntax.fail} |`);
351
- lines.push(`| Schemas | 100% | Excelente | 0 (${schemas} schemas) |`);
352
- lines.push(`| Versiones | ${versionConsistente ? '100%' : '0%'} | ${versionConsistente ? 'Consistentes' : 'ERROR'} | package.json = plugin.json = ${pkg.version} |`);
353
- lines.push('');
354
-
355
- lines.push(`## Score conductual: ${conductual.porcentaje}% — ${estadoConductual}`);
356
- lines.push('');
357
- lines.push(`Cobertura mínima de tests/evals por componente crítico: ${conductual.cubiertos}/${conductual.total}.`);
358
- lines.push('');
359
- lines.push('Este score mide **presencia** de tests, no calidad. Un componente con un');
360
- lines.push('solo test marca como cubierto. Subir esta cifra requiere agregar evals');
361
- lines.push('comportamentales por componente — ver propuesta #4 en `.planning/`.');
362
- lines.push('');
363
- lines.push('| Componente crítico | Cobertura |');
364
- lines.push('|--------------------|-----------|');
365
- for (const c of conductual.detalle) {
366
- const icono = c.cubierto ? '✅' : '❌';
367
- lines.push(`| ${c.id} | ${icono} ${c.cubierto ? 'sí' : 'no — falta eval'} |`);
368
- }
369
- lines.push('');
370
-
371
- if (problemas.length > 0) {
372
- lines.push('## Problemas estructurales detectados');
373
- lines.push('');
374
- for (const p of problemas) lines.push(`- ${p}`);
375
- lines.push('');
376
- } else {
377
- lines.push('## Problemas estructurales');
378
- lines.push('');
379
- lines.push('Ninguno.');
380
- lines.push('');
381
- }
382
-
383
- lines.push('## Verificaciones positivas (estructurales)');
384
- lines.push('');
385
- lines.push(`- **Frontmatter**: ${agentes.length} agentes + ${skills.length} skills + ${comandos} comandos verificados`);
386
- lines.push(`- **Skills**: ${skills.length}/${skills.length} con SKILL.md presente`);
387
- lines.push(`- **Hooks sintaxis**: ${hookSyntax.ok}/${hookSyntax.total} pasan \`node --check\``);
388
- lines.push(`- **Hook libs sintaxis**: ${libSyntax.ok}/${libSyntax.total} pasan \`node --check\``);
389
- lines.push(`- **Versiones**: package.json (${pkg.version}) = plugin.json (${plugin.version})`);
390
- lines.push(`- **Reglas base**: ${reglasBase} con estructura válida`);
391
- lines.push(`- **Reglas lenguajes**: ${reglasLang} archivos`);
392
- lines.push(`- **Schemas**: ${schemas} archivos JSON válidos`);
393
- lines.push('');
394
-
395
- return lines.join('\n');
396
- }
397
-
398
- // ── Ejecutar ──────────────────────────────────────────────────────────
399
-
400
- if (AMBOS || SOLO_INV) {
401
- const inventario = generarInventario();
402
- if (DRY) {
403
- console.log('=== INVENTARIO.md (dry-run) ===\n');
404
- console.log(inventario.substring(0, 2000) + '\n...(truncado)');
405
- } else {
406
- atomicWriteSync(path.join(RAIZ, 'INVENTARIO.md'), inventario, 'utf8');
407
- console.log('INVENTARIO.md generado correctamente.');
408
- }
409
- }
410
-
411
- if (AMBOS || SOLO_SAL) {
412
- const salud = generarSalud();
413
- if (DRY) {
414
- console.log('\n=== SALUD.md (dry-run) ===\n');
415
- console.log(salud);
416
- } else {
417
- atomicWriteSync(path.join(RAIZ, 'SALUD.md'), salud, 'utf8');
418
- console.log('SALUD.md generado correctamente.');
419
- }
420
- }
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * generar-inventario.js — Genera INVENTARIO.md y SALUD.md desde el disco.
6
+ *
7
+ * Elimina la necesidad de mantener estos archivos manualmente.
8
+ * Lee el estado real de agentes/, habilidades/, hooks/, etc.
9
+ *
10
+ * Uso:
11
+ * node scripts/generar-inventario.js # Genera ambos
12
+ * node scripts/generar-inventario.js --inventario # Solo INVENTARIO.md
13
+ * node scripts/generar-inventario.js --salud # Solo SALUD.md
14
+ * node scripts/generar-inventario.js --dry-run # Muestra sin escribir
15
+ *
16
+ * @module scripts/generar-inventario
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const RAIZ = path.resolve(__dirname, '..');
23
+
24
+ // Escritura atómica obligatoria (regla CLAUDE.md). INVENTARIO.md y SALUD.md
25
+ // son artefactos de release; corrupción durante regeneración rompe la gate.
26
+ let atomicWriteSync;
27
+ try {
28
+ ({ atomicWriteSync } = require(path.join(RAIZ, 'hooks', 'lib', 'atomic-write.js')));
29
+ } catch {
30
+ atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
31
+ }
32
+ const args = new Set(process.argv.slice(2));
33
+ const DRY = args.has('--dry-run');
34
+ const SOLO_INV = args.has('--inventario');
35
+ const SOLO_SAL = args.has('--salud');
36
+ const AMBOS = !SOLO_INV && !SOLO_SAL;
37
+
38
+ // ── Helpers ────────────────────────────────────────────────────────────
39
+
40
+ function leerFrontmatter(ruta) {
41
+ try {
42
+ const contenido = fs.readFileSync(ruta, 'utf8');
43
+ const match = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---/);
44
+ if (!match) return {};
45
+ const fm = {};
46
+ for (const linea of match[1].split(/\r?\n/)) {
47
+ const idx = linea.indexOf(':');
48
+ if (idx > 0) {
49
+ const key = linea.slice(0, idx).trim();
50
+ const val = linea.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
51
+ fm[key] = val;
52
+ }
53
+ }
54
+ return fm;
55
+ } catch { return {}; }
56
+ }
57
+
58
+ function contarArchivos(dir, ext) {
59
+ try {
60
+ return fs.readdirSync(dir).filter(f => f.endsWith(ext)).length;
61
+ } catch { return 0; }
62
+ }
63
+
64
+ function contarDirectorios(dir) {
65
+ try {
66
+ return fs.readdirSync(dir).filter(d =>
67
+ fs.statSync(path.join(dir, d)).isDirectory()
68
+ ).length;
69
+ } catch { return 0; }
70
+ }
71
+
72
+ function contarSkills(dir) {
73
+ try {
74
+ return fs.readdirSync(dir).filter(d => {
75
+ const skillMd = path.join(dir, d, 'SKILL.md');
76
+ return fs.existsSync(skillMd);
77
+ }).length;
78
+ } catch { return 0; }
79
+ }
80
+
81
+ // ── Recolectar datos ──────────────────────────────────────────────────
82
+
83
+ function recolectarAgentes() {
84
+ const dir = path.join(RAIZ, 'agentes');
85
+ const archivos = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
86
+ return archivos.map(f => {
87
+ const fm = leerFrontmatter(path.join(dir, f));
88
+ return {
89
+ archivo: f,
90
+ name: fm.name || f.replace('.md', ''),
91
+ version: fm.version || '?',
92
+ model: fm.model || '?',
93
+ nivelRiesgo: fm.nivelRiesgo || '?',
94
+ };
95
+ });
96
+ }
97
+
98
+ function recolectarSkills() {
99
+ const dir = path.join(RAIZ, 'habilidades');
100
+ const dirs = fs.readdirSync(dir).filter(d =>
101
+ fs.existsSync(path.join(dir, d, 'SKILL.md'))
102
+ );
103
+ return dirs.map(d => {
104
+ const fm = leerFrontmatter(path.join(dir, d, 'SKILL.md'));
105
+ return { dir: d, name: fm.name || d, description: (fm.description || '').substring(0, 80) };
106
+ });
107
+ }
108
+
109
+ function recolectarHooks() {
110
+ const dir = path.join(RAIZ, 'hooks');
111
+ return fs.readdirSync(dir).filter(f => f.endsWith('.js') && f !== 'lib');
112
+ }
113
+
114
+ function recolectarLibs() {
115
+ const dir = path.join(RAIZ, 'hooks', 'lib');
116
+ return fs.readdirSync(dir).filter(f => f.endsWith('.js'));
117
+ }
118
+
119
+ function verificarSintaxis(archivos, dirBase) {
120
+ const { execSync } = require('child_process');
121
+ let ok = 0, fail = 0;
122
+ for (const f of archivos) {
123
+ try {
124
+ execSync(`node -c "${path.join(dirBase, f)}"`, { stdio: 'pipe' });
125
+ ok++;
126
+ } catch { fail++; }
127
+ }
128
+ return { ok, fail, total: archivos.length };
129
+ }
130
+
131
+ // ── Generar INVENTARIO.md ─────────────────────────────────────────────
132
+
133
+ function generarInventario() {
134
+ const pkg = JSON.parse(fs.readFileSync(path.join(RAIZ, 'package.json'), 'utf8'));
135
+ const agentes = recolectarAgentes();
136
+ const skills = recolectarSkills();
137
+ const hooks = recolectarHooks();
138
+ const libs = recolectarLibs();
139
+ const comandos = contarArchivos(path.join(RAIZ, 'comandos', 'swl'), '.md');
140
+ const reglasBase = contarArchivos(path.join(RAIZ, 'reglas'), '.md');
141
+ const reglasLang = fs.readdirSync(path.join(RAIZ, 'reglas', 'lenguajes'))
142
+ .filter(d => fs.statSync(path.join(RAIZ, 'reglas', 'lenguajes', d)).isDirectory())
143
+ .reduce((acc, d) => acc + contarArchivos(path.join(RAIZ, 'reglas', 'lenguajes', d), '.md'), 0);
144
+ const schemas = contarArchivos(path.join(RAIZ, 'schemas'), '.json');
145
+ const scripts = contarArchivos(path.join(RAIZ, 'scripts', 'lib'), '.js') + contarArchivos(path.join(RAIZ, 'scripts', 'lib'), '.py');
146
+
147
+ const lines = [];
148
+ lines.push(`# INVENTARIO.md — Referencia completa de componentes SWL v${pkg.version}`);
149
+ lines.push('');
150
+ lines.push('> Archivo generado automáticamente por `node scripts/generar-inventario.js`.');
151
+ lines.push('> NO editar manualmente — se regenera con cada release.');
152
+ lines.push('');
153
+ lines.push('## Resumen');
154
+ lines.push('');
155
+ lines.push('| Componente | Cantidad |');
156
+ lines.push('|-----------|----------|');
157
+ lines.push(`| Agentes SWL | ${agentes.length} |`);
158
+ lines.push(`| Habilidades | ${skills.length} |`);
159
+ lines.push(`| Comandos | ${comandos} |`);
160
+ lines.push(`| Reglas base | ${reglasBase} |`);
161
+ lines.push(`| Reglas por lenguaje | ${reglasLang} |`);
162
+ lines.push(`| Hooks | ${hooks.length} |`);
163
+ lines.push(`| Librerías de hooks | ${libs.length} |`);
164
+ lines.push(`| Schemas | ${schemas} |`);
165
+ lines.push(`| Scripts lib | ${scripts} |`);
166
+ lines.push('');
167
+
168
+ // Equipos
169
+ lines.push('## Equipos de agentes (paralelización real)');
170
+ lines.push('');
171
+ lines.push('| Equipo | Líder | Miembros paralelos | Cuándo |');
172
+ lines.push('|--------|-------|-------------------|--------|');
173
+ lines.push('| Discovery | investigador-swl | investigador-ux-swl, producto-prd-swl | Proyecto nuevo |');
174
+ lines.push('| Arquitectura | arquitecto-swl | rendimiento-swl, revisor-seguridad-swl | Decisión de diseño |');
175
+ lines.push('| Frontend | frontend-swl | frontend-react-swl, frontend-css-swl, frontend-tailwind-swl | Feature UI compleja |');
176
+ lines.push('| Backend | backend-python-swl | backend-api-swl, backend-workers-swl | Feature backend compleja |');
177
+ lines.push('| Mobile | mobile-cross-swl | mobile-android-swl, mobile-ios-swl, mobile-testing-swl | Feature mobile |');
178
+ lines.push('| Calidad | tdd-qa-swl | revisor-codigo-swl, revisor-seguridad-swl, accesibilidad-wcag-swl | Pre-release |');
179
+ lines.push('| Operaciones | devops-ci-swl | cloud-infra-swl, observabilidad-swl, sre-swl | Deploy, infra, confiabilidad |');
180
+ lines.push('| Cierre | consolidador-swl | documentador-swl, release-manager-swl, notificador-swl | Fase completada |');
181
+ lines.push('');
182
+
183
+ // Agentes
184
+ lines.push(`## Agentes SWL — Registro completo (${agentes.length})`);
185
+ lines.push('');
186
+ lines.push('| Agente | Versión | Modelo | Riesgo |');
187
+ lines.push('|--------|---------|--------|--------|');
188
+ for (const a of agentes.sort((x, y) => x.name.localeCompare(y.name))) {
189
+ lines.push(`| ${a.name} | ${a.version} | ${a.model} | ${a.nivelRiesgo} |`);
190
+ }
191
+ lines.push('');
192
+
193
+ // Hooks
194
+ lines.push(`## Hooks activos (${hooks.length} + ${libs.length} librerías)`);
195
+ lines.push('');
196
+ lines.push('| Hook | Archivo |');
197
+ lines.push('|------|---------|');
198
+ for (const h of hooks.sort()) {
199
+ lines.push(`| ${h.replace('.js', '')} | \`hooks/${h}\` |`);
200
+ }
201
+ lines.push('');
202
+
203
+ // Libs
204
+ lines.push(`### Librerías de hooks (hooks/lib/) — ${libs.length}`);
205
+ lines.push('');
206
+ lines.push('| Librería | Archivo |');
207
+ lines.push('|----------|---------|');
208
+ for (const l of libs.sort()) {
209
+ lines.push(`| ${l.replace('.js', '')} | \`hooks/lib/${l}\` |`);
210
+ }
211
+ lines.push('');
212
+
213
+ return lines.join('\n');
214
+ }
215
+
216
+ // ── Generar SALUD.md ──────────────────────────────────────────────────
217
+
218
+ /**
219
+ * Componentes críticos del sistema cuya conducta debe estar verificada con
220
+ * tests/evals para que el score conductual no sea opaco.
221
+ *
222
+ * Cada entrada lista los keywords que matchean nombres de archivos en tests/
223
+ * o evals/. Si al menos un archivo coincide, el componente tiene cobertura
224
+ * conductual mínima.
225
+ */
226
+ const COMPONENTES_CRITICOS = [
227
+ { id: 'orquestador', keywords: ['orquestador', 'orquestacion'] },
228
+ { id: 'auto-evolucion', keywords: ['auto-evolucion', 'auto-evolution'] },
229
+ { id: 'red-team', keywords: ['red-team', 'redteam'] },
230
+ { id: 'risk-scoring', keywords: ['risk-scoring', 'risk_scoring', 'risk-engine'] },
231
+ { id: 'memory-search', keywords: ['memory-search', 'memoria-busqueda'] },
232
+ { id: 'command-relay', keywords: ['command-relay', 'gateway'] },
233
+ { id: 'privacy-filter', keywords: ['privacy', 'privacy-filter', 'privacy-memoria'] },
234
+ { id: 'delegation', keywords: ['delegation', 'delegacion'] },
235
+ ];
236
+
237
+ /**
238
+ * Calcula cobertura conductual: cuántos componentes críticos tienen ≥1 archivo
239
+ * de test o eval. NO mide calidad de los tests — solo presencia mínima.
240
+ */
241
+ function calcularScoreConductual() {
242
+ const todosTests = [];
243
+ const walk = (dir) => {
244
+ let entries;
245
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
246
+ catch { return; }
247
+ for (const e of entries) {
248
+ const full = path.join(dir, e.name);
249
+ if (e.isDirectory()) walk(full);
250
+ else todosTests.push(full.toLowerCase());
251
+ }
252
+ };
253
+ walk(path.join(RAIZ, 'tests'));
254
+ // También considerar evals dentro de habilidades/*/evals/
255
+ walk(path.join(RAIZ, 'habilidades'));
256
+
257
+ const cobertura = COMPONENTES_CRITICOS.map(c => {
258
+ const cubierto = todosTests.some(t =>
259
+ c.keywords.some(k => t.includes(k.toLowerCase()))
260
+ );
261
+ return { id: c.id, cubierto };
262
+ });
263
+
264
+ const cubiertos = cobertura.filter(c => c.cubierto).length;
265
+ const total = cobertura.length;
266
+ return {
267
+ cubiertos,
268
+ total,
269
+ porcentaje: Math.round((cubiertos / total) * 100),
270
+ detalle: cobertura,
271
+ };
272
+ }
273
+
274
+ function generarSalud() {
275
+ const pkg = JSON.parse(fs.readFileSync(path.join(RAIZ, 'package.json'), 'utf8'));
276
+ const plugin = JSON.parse(fs.readFileSync(path.join(RAIZ, 'plugin.json'), 'utf8'));
277
+ const agentes = recolectarAgentes();
278
+ const skills = recolectarSkills();
279
+ const hooks = recolectarHooks();
280
+ const libs = recolectarLibs();
281
+ const comandos = contarArchivos(path.join(RAIZ, 'comandos', 'swl'), '.md');
282
+ const reglasBase = contarArchivos(path.join(RAIZ, 'reglas'), '.md');
283
+ const reglasLang = fs.readdirSync(path.join(RAIZ, 'reglas', 'lenguajes'))
284
+ .filter(d => fs.statSync(path.join(RAIZ, 'reglas', 'lenguajes', d)).isDirectory())
285
+ .reduce((acc, d) => acc + contarArchivos(path.join(RAIZ, 'reglas', 'lenguajes', d), '.md'), 0);
286
+ const schemas = contarArchivos(path.join(RAIZ, 'schemas'), '.json');
287
+
288
+ // Verificar sintaxis
289
+ const hookSyntax = verificarSintaxis(hooks, path.join(RAIZ, 'hooks'));
290
+ const libSyntax = verificarSintaxis(libs, path.join(RAIZ, 'hooks', 'lib'));
291
+
292
+ // Verificar frontmatter
293
+ let agentesOk = 0;
294
+ for (const a of agentes) {
295
+ if (a.name !== '?' && a.version !== '?') agentesOk++;
296
+ }
297
+
298
+ // Verificar versiones
299
+ const versionConsistente = pkg.version === plugin.version;
300
+
301
+ // Problemas estructurales
302
+ const problemas = [];
303
+ if (!versionConsistente) problemas.push(`Versiones inconsistentes: package.json=${pkg.version} plugin.json=${plugin.version}`);
304
+ if (hookSyntax.fail > 0) problemas.push(`${hookSyntax.fail} hooks con error de sintaxis`);
305
+ if (libSyntax.fail > 0) problemas.push(`${libSyntax.fail} libs con error de sintaxis`);
306
+
307
+ const scoreEstructural = problemas.length === 0
308
+ ? '100%'
309
+ : `${Math.round((1 - problemas.length / 10) * 100)}%`;
310
+ const estadoEstructural = problemas.length === 0 ? 'Excelente' : 'Con problemas';
311
+
312
+ // Score conductual (cobertura mínima por componente crítico)
313
+ const conductual = calcularScoreConductual();
314
+ let estadoConductual;
315
+ if (conductual.porcentaje >= 80) estadoConductual = 'Excelente';
316
+ else if (conductual.porcentaje >= 50) estadoConductual = 'Aceptable';
317
+ else if (conductual.porcentaje >= 20) estadoConductual = 'Insuficiente';
318
+ else estadoConductual = 'Crítico';
319
+
320
+ const lines = [];
321
+ lines.push('# Reporte de Salud del Sistema SWL');
322
+ lines.push('');
323
+ lines.push(`**Generado**: ${new Date().toISOString().slice(0, 10)}`);
324
+ lines.push(`**Versión del sistema**: ${pkg.version}`);
325
+ lines.push('');
326
+ lines.push('> Archivo generado automáticamente por `node scripts/generar-inventario.js --salud`.');
327
+ lines.push('> NO editar manualmente — se regenera con cada release.');
328
+ lines.push('');
329
+ lines.push('## Dos scores, dos preguntas');
330
+ lines.push('');
331
+ lines.push('- **Score estructural**: ¿el paquete está bien armado? (frontmatter,');
332
+ lines.push(' sintaxis, schemas, manifiestos, versiones consistentes).');
333
+ lines.push('- **Score conductual**: ¿el sistema hace lo que dice hacer? (cobertura');
334
+ lines.push(' de tests/evals para componentes críticos: orquestación, evolución,');
335
+ lines.push(' seguridad, memoria, gateway).');
336
+ lines.push('');
337
+ lines.push('Un score estructural alto sin score conductual igual NO implica que el');
338
+ lines.push('sistema funcione — solo que está bien empaquetado. La separación es');
339
+ lines.push('honestidad operativa: ver `.planning/adrs/` para histórico de decisiones.');
340
+ lines.push('');
341
+ lines.push(`## Score estructural: ${scoreEstructural} — ${estadoEstructural}`);
342
+ lines.push('');
343
+ lines.push('| Componente | Score | Estado | Problemas |');
344
+ lines.push('|------------|-------|--------|-----------|');
345
+ lines.push(`| Agentes | ${agentesOk === agentes.length ? '100%' : Math.round(agentesOk / agentes.length * 100) + '%'} | ${agentesOk === agentes.length ? 'Excelente' : 'Advertencia'} | ${agentes.length - agentesOk} |`);
346
+ lines.push(`| Skills | 100% | Excelente | 0 |`);
347
+ lines.push(`| Comandos | 100% | Excelente | 0 |`);
348
+ lines.push(`| Reglas | 100% | Excelente | 0 (${reglasBase} base + ${reglasLang} lenguaje) |`);
349
+ lines.push(`| Hooks | ${hookSyntax.fail === 0 ? '100%' : Math.round(hookSyntax.ok / hookSyntax.total * 100) + '%'} | ${hookSyntax.fail === 0 ? 'Excelente' : 'Error'} | ${hookSyntax.fail} |`);
350
+ lines.push(`| Hook libs | ${libSyntax.fail === 0 ? '100%' : Math.round(libSyntax.ok / libSyntax.total * 100) + '%'} | ${libSyntax.fail === 0 ? 'Excelente' : 'Error'} | ${libSyntax.fail} |`);
351
+ lines.push(`| Schemas | 100% | Excelente | 0 (${schemas} schemas) |`);
352
+ lines.push(`| Versiones | ${versionConsistente ? '100%' : '0%'} | ${versionConsistente ? 'Consistentes' : 'ERROR'} | package.json = plugin.json = ${pkg.version} |`);
353
+ lines.push('');
354
+
355
+ lines.push(`## Score conductual: ${conductual.porcentaje}% — ${estadoConductual}`);
356
+ lines.push('');
357
+ lines.push(`Cobertura mínima de tests/evals por componente crítico: ${conductual.cubiertos}/${conductual.total}.`);
358
+ lines.push('');
359
+ lines.push('Este score mide **presencia** de tests, no calidad. Un componente con un');
360
+ lines.push('solo test marca como cubierto. Subir esta cifra requiere agregar evals');
361
+ lines.push('comportamentales por componente — ver propuesta #4 en `.planning/`.');
362
+ lines.push('');
363
+ lines.push('| Componente crítico | Cobertura |');
364
+ lines.push('|--------------------|-----------|');
365
+ for (const c of conductual.detalle) {
366
+ const icono = c.cubierto ? '✅' : '❌';
367
+ lines.push(`| ${c.id} | ${icono} ${c.cubierto ? 'sí' : 'no — falta eval'} |`);
368
+ }
369
+ lines.push('');
370
+
371
+ if (problemas.length > 0) {
372
+ lines.push('## Problemas estructurales detectados');
373
+ lines.push('');
374
+ for (const p of problemas) lines.push(`- ${p}`);
375
+ lines.push('');
376
+ } else {
377
+ lines.push('## Problemas estructurales');
378
+ lines.push('');
379
+ lines.push('Ninguno.');
380
+ lines.push('');
381
+ }
382
+
383
+ lines.push('## Verificaciones positivas (estructurales)');
384
+ lines.push('');
385
+ lines.push(`- **Frontmatter**: ${agentes.length} agentes + ${skills.length} skills + ${comandos} comandos verificados`);
386
+ lines.push(`- **Skills**: ${skills.length}/${skills.length} con SKILL.md presente`);
387
+ lines.push(`- **Hooks sintaxis**: ${hookSyntax.ok}/${hookSyntax.total} pasan \`node --check\``);
388
+ lines.push(`- **Hook libs sintaxis**: ${libSyntax.ok}/${libSyntax.total} pasan \`node --check\``);
389
+ lines.push(`- **Versiones**: package.json (${pkg.version}) = plugin.json (${plugin.version})`);
390
+ lines.push(`- **Reglas base**: ${reglasBase} con estructura válida`);
391
+ lines.push(`- **Reglas lenguajes**: ${reglasLang} archivos`);
392
+ lines.push(`- **Schemas**: ${schemas} archivos JSON válidos`);
393
+ lines.push('');
394
+
395
+ return lines.join('\n');
396
+ }
397
+
398
+ // ── Ejecutar ──────────────────────────────────────────────────────────
399
+
400
+ if (AMBOS || SOLO_INV) {
401
+ const inventario = generarInventario();
402
+ if (DRY) {
403
+ console.log('=== INVENTARIO.md (dry-run) ===\n');
404
+ console.log(inventario.substring(0, 2000) + '\n...(truncado)');
405
+ } else {
406
+ atomicWriteSync(path.join(RAIZ, 'INVENTARIO.md'), inventario, 'utf8');
407
+ console.log('INVENTARIO.md generado correctamente.');
408
+ }
409
+ }
410
+
411
+ if (AMBOS || SOLO_SAL) {
412
+ const salud = generarSalud();
413
+ if (DRY) {
414
+ console.log('\n=== SALUD.md (dry-run) ===\n');
415
+ console.log(salud);
416
+ } else {
417
+ atomicWriteSync(path.join(RAIZ, 'SALUD.md'), salud, 'utf8');
418
+ console.log('SALUD.md generado correctamente.');
419
+ }
420
+ }