@saulwade/swl-ses 1.3.4 → 1.3.5

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 (102) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/swl-mcp-server.js +187 -187
  4. package/bin/swl-ses.js +4 -62
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/adoptar-proyecto.md +207 -207
  7. package/comandos/swl/contribuir.md +233 -233
  8. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  9. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  10. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  11. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  12. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  13. package/habilidades/eval-framework/SKILL.md +212 -212
  14. package/habilidades/extractor-de-aprendizajes/SKILL.md +321 -321
  15. package/habilidades/harness-claude-code/SKILL.md +299 -299
  16. package/habilidades/infra-github-actions/SKILL.md +166 -166
  17. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  18. package/habilidades/manejo-errores/.evolved.json +8 -8
  19. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  20. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  21. package/habilidades/patrones-python/SKILL.md +229 -229
  22. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  23. package/habilidades/planear-fase/SKILL.md +319 -319
  24. package/habilidades/release-semver/.evolved.json +8 -8
  25. package/habilidades/swl-claudemd/SKILL.md +220 -220
  26. package/habilidades/testing-python/SKILL.md +340 -340
  27. package/hooks/claudemd-bloat-detector.js +161 -161
  28. package/hooks/extraccion-aprendizajes.js +19 -12
  29. package/hooks/lib/agent-routing.js +107 -107
  30. package/hooks/lib/auto-consolidator.js +335 -335
  31. package/hooks/lib/error-classifier.js +308 -308
  32. package/hooks/lib/merkle-audit.js +96 -96
  33. package/hooks/lib/provenance-tracker.js +191 -191
  34. package/hooks/lib/rate-limit-tracker.js +253 -253
  35. package/hooks/lib/resource-quota.js +122 -122
  36. package/hooks/lib/retry-jitter.js +165 -165
  37. package/hooks/lib/skill-auditor.js +588 -588
  38. package/hooks/lib/sync-status.js +228 -228
  39. package/hooks/lib/taint-tracker.js +107 -107
  40. package/hooks/lib/text-similarity.js +241 -241
  41. package/hooks/lib/toon-compressor.js +245 -245
  42. package/hooks/registro-turnos.js +209 -209
  43. package/hooks/sugerir-regenerar-inventario.js +170 -170
  44. package/hooks/validar-formato-post-subagente.js +140 -140
  45. package/hooks/validar-memoria-hook.js +218 -218
  46. package/instintos/prompt-appendices.yaml +57 -57
  47. package/manifiestos/agent-output-schemas.json +57 -57
  48. package/manifiestos/skills-lock.json +1093 -1093
  49. package/package.json +1 -1
  50. package/plantillas/auditor-veto-template.md +105 -105
  51. package/plantillas/github-workflows/README.md +47 -47
  52. package/plantillas/github-workflows/release-please.yml +44 -44
  53. package/plantillas/github-workflows/swl-ci.yml +107 -107
  54. package/plantillas/github-workflows/swl-security.yml +51 -51
  55. package/plugin.json +1 -1
  56. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  57. package/reglas/arreglar-al-detectar.md +147 -147
  58. package/reglas/fragmentos-compartidos.md +152 -152
  59. package/reglas/harness-claude-code.md +213 -213
  60. package/reglas/usar-context7.md +226 -226
  61. package/schemas/diary-entry.schema.json +80 -80
  62. package/scripts/benchmark-memoria.js +167 -167
  63. package/scripts/configurar-branch-protection.js +418 -418
  64. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  65. package/scripts/doctor.js +77 -3
  66. package/scripts/field-report.js +199 -199
  67. package/scripts/generar-checklists-consolidados.js +273 -273
  68. package/scripts/generar-inventario.js +420 -420
  69. package/scripts/generar-matriz-lenguajes.js +271 -271
  70. package/scripts/instalador.js +38 -1
  71. package/scripts/lib/artefactos-python.js +43 -43
  72. package/scripts/lib/benchmark-metrics.js +160 -160
  73. package/scripts/lib/budget-enforcer.js +252 -252
  74. package/scripts/lib/configurar-ci.js +380 -380
  75. package/scripts/lib/contadores-inventario.js +217 -217
  76. package/scripts/lib/detectar-stack-detallado.js +307 -307
  77. package/scripts/lib/diary-entry.js +234 -234
  78. package/scripts/lib/eval-metrics-store.js +218 -218
  79. package/scripts/lib/eval-quality.js +171 -171
  80. package/scripts/lib/eval-schemas.js +144 -144
  81. package/scripts/lib/eval-self-correct.js +106 -106
  82. package/scripts/lib/eval-validator.js +185 -185
  83. package/scripts/lib/jaccard-similarity.js +98 -98
  84. package/scripts/lib/longmemeval-runner.js +125 -125
  85. package/scripts/lib/npm-version.js +261 -261
  86. package/scripts/lib/paquetes-conocidos.js +50 -50
  87. package/scripts/lib/parsear-opciones.js +136 -0
  88. package/scripts/lib/prompt-builder.js +264 -264
  89. package/scripts/lib/rrf-fusion.js +175 -175
  90. package/scripts/lib/scoring-instintos.js +277 -277
  91. package/scripts/lib/semantic-search.js +252 -252
  92. package/scripts/lib/transformadores/claude.js +200 -200
  93. package/scripts/limpiar-artefactos-python.js +131 -131
  94. package/scripts/mcp-server/README.md +128 -128
  95. package/scripts/mcp-server/handlers.js +206 -206
  96. package/scripts/migrar-csv-a-array.js +168 -168
  97. package/scripts/migrar-fase-dominio.js +201 -201
  98. package/scripts/publicar.js +511 -511
  99. package/scripts/run-eval.js +141 -141
  100. package/scripts/validar-manifest.js +195 -195
  101. package/scripts/validar-userland-vacio.js +110 -110
  102. package/scripts/verificar-release.js +5 -1
@@ -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
+ }