@saulwade/swl-ses 1.5.2 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CLAUDE.md +32 -61
  2. package/README.md +20 -3
  3. package/agentes/datos-swl.md +1 -1
  4. package/agentes/frontend-angular-swl.md +7 -7
  5. package/agentes/frontend-css-swl.md +4 -4
  6. package/agentes/frontend-react-swl.md +7 -7
  7. package/agentes/frontend-swl.md +9 -9
  8. package/agentes/frontend-tailwind-swl.md +4 -4
  9. package/agentes/rendimiento-swl.md +2 -2
  10. package/bin/swl-ses.js +49 -7
  11. package/comandos/swl/brainstorm.md +1 -0
  12. package/comandos/swl/compactar.md +1 -1
  13. package/comandos/swl/discutir-fase.md +15 -1
  14. package/comandos/swl/mapear-codebase.md +1 -1
  15. package/comandos/swl/nemesis.md +29 -0
  16. package/comandos/swl/planear-fase.md +2 -2
  17. package/comandos/swl/verificar.md +4 -4
  18. package/habilidades/aprendizaje-continuo/SKILL.md +7 -1
  19. package/habilidades/diseno-herramientas-agente/SKILL.md +1 -0
  20. package/habilidades/doc-sync/SKILL.md +441 -1
  21. package/habilidades/doubt-driven-review/SKILL.md +177 -171
  22. package/habilidades/feynman-auditor-swl/SKILL.md +129 -123
  23. package/habilidades/infra-github-actions/SKILL.md +172 -166
  24. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  25. package/habilidades/nemesis-evaluacion-json/SKILL.md +5 -0
  26. package/habilidades/nemesis-redistribuir/SKILL.md +5 -0
  27. package/habilidades/node-experto/SKILL.md +197 -3
  28. package/habilidades/prevencion-racionalizacion/SKILL.md +1 -0
  29. package/habilidades/privacy-memoria/SKILL.md +1 -0
  30. package/habilidades/sre-patrones/SKILL.md +1 -1
  31. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +172 -166
  32. package/habilidades/tdd-workflow/SKILL.md +178 -3
  33. package/habilidades/verificacion-evidencia/SKILL.md +1 -0
  34. package/habilidades/web-fetcher-routing/SKILL.md +81 -75
  35. package/habilidades/workflow-claude-code/SKILL.md +2 -2
  36. package/hooks/extraccion-aprendizajes.js +11 -0
  37. package/manifiestos/modulos.json +2 -1
  38. package/manifiestos/skills-lock.json +1142 -1114
  39. package/package.json +7 -4
  40. package/plugin.json +4 -2
  41. package/reglas/auditorias-documentales-estructurales.md +205 -0
  42. package/schemas/agent-frontmatter.schema.json +1 -1
  43. package/scripts/desinstalar.js +105 -24
  44. package/scripts/generar-inventario.js +450 -420
  45. package/scripts/instalador.js +55 -4
  46. package/scripts/lib/parsear-opciones.js +3 -0
  47. package/scripts/lib/ui.js +148 -22
  48. package/scripts/tui/componentes/selector-multi.js +189 -0
  49. package/scripts/tui/componentes/selector-unico.js +158 -0
  50. package/scripts/tui/ejecutores.js +375 -0
  51. package/scripts/tui/index.js +162 -0
  52. package/scripts/tui/lib/colores.js +129 -0
  53. package/scripts/tui/lib/render.js +264 -0
  54. package/scripts/tui/lib/teclas.js +113 -0
  55. package/scripts/tui/pantallas/inspect.js +173 -0
  56. package/scripts/tui/pantallas/install-wizard.js +334 -0
  57. package/scripts/tui/pantallas/menu-principal.js +52 -0
  58. package/scripts/tui/pantallas/progreso.js +274 -0
  59. package/scripts/tui/pantallas/resumen.js +132 -0
  60. package/scripts/tui/pantallas/uninstall-wizard.js +208 -0
  61. package/scripts/tui/pantallas/update-wizard.js +232 -0
  62. package/scripts/tui/pantallas/welcome.js +187 -0
  63. package/scripts/verificar-docs-vs-codigo.js +654 -0
  64. package/scripts/verificar-evolucion.js +19 -3
@@ -0,0 +1,654 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * scripts/verificar-docs-vs-codigo.js
6
+ *
7
+ * Verificador automatizado de drift entre documentación y estado real del
8
+ * proyecto. Pensado para correr en CI tras un release o cuando se modifican
9
+ * componentes del sistema.
10
+ *
11
+ * Checks implementados:
12
+ *
13
+ * 1. CONTADORES: INVENTARIO.md vs disco
14
+ * Verifica que los contadores (agentes, skills, comandos, hooks, reglas)
15
+ * reportados en INVENTARIO.md, README.md, AGENTS.md, MANUAL_USO.md,
16
+ * package.json#description y plugin.json#description coincidan con lo
17
+ * que existe en disco.
18
+ *
19
+ * 2. COMANDOS: comandos/swl/*.md vs COMANDOS.md y MANUAL_USO.md
20
+ * Cada comando con archivo en comandos/swl/ debe:
21
+ * (a) Aparecer en COMANDOS.md y MANUAL_USO.md (presencia).
22
+ * (b) Tener al menos PROFUNDIDAD_MINIMA menciones en MANUAL_USO si
23
+ * no es comando trivial (FAIL, no WARN — endurecido v1.6.1).
24
+ * Rationale: el verificador previo aceptaba 1 mención como
25
+ * "documentado", lo que genera drift estructural invisible. Ver
26
+ * reglas/auditorias-documentales-estructurales.md.
27
+ *
28
+ * 3. FLAGS críticos: parser vs docs
29
+ * Si parsear-opciones.js declara una flag como booleana, debe aparecer
30
+ * en bin/swl-ses.js --help y en COMANDOS.md tabla de opciones.
31
+ *
32
+ * 4. VERSIONES: package.json vs encabezados de docs
33
+ * Las primeras líneas de CLAUDE.md, README.md, AGENTS.md, COMANDOS.md,
34
+ * MANUAL_USO.md, INSTALACION.md deben mencionar la versión de
35
+ * package.json.
36
+ *
37
+ * 5. TUI: scripts/tui/pantallas/*.js vs docs
38
+ * Cada pantalla del TUI (welcome, menu-principal, install-wizard,
39
+ * update-wizard, uninstall-wizard, inspect, progreso, resumen) debe
40
+ * tener al menos una mención en MANUAL_USO.md.
41
+ *
42
+ * 6. VARIABLES DE ENTORNO: SWL_* en código vs docs/variables-entorno.md
43
+ * Toda variable SWL_* que aparezca en hooks/ o scripts/ debe estar
44
+ * documentada en docs/variables-entorno.md (catálogo formal único).
45
+ * FAIL si hay huérfanas. Cierra el gap detectado en sesión 2026-05-16
46
+ * donde 22 de 58 variables existían en código pero no en docs.
47
+ *
48
+ * 7. REGLAS: reglas/*.md en disco vs manifiestos/modulos.json
49
+ * Toda regla base (excluyendo fragmentos `_*.md`) debe estar
50
+ * registrada en algún módulo de manifiestos/modulos.json. Sin esto
51
+ * el instalador no la propaga al destino del usuario.
52
+ *
53
+ * 8. ADRs vs CHANGELOG: ADRs aceptados con referencia en CHANGELOG.md
54
+ * WARN si un ADR en estado "Aceptado" no aparece referenciado en
55
+ * CHANGELOG.md. Subjetivo (no todo ADR cambia comportamiento
56
+ * observable), por eso reporta WARN sin bloquear.
57
+ *
58
+ * Exit codes:
59
+ * 0 — todos los checks pasan
60
+ * 1 — hay drift en al menos un check obligatorio
61
+ * 2 — error de invocación
62
+ *
63
+ * Uso:
64
+ * node scripts/verificar-docs-vs-codigo.js [--check N] [--quiet]
65
+ *
66
+ * --check N Ejecuta solo el check N (1-8). Sin flag: todos.
67
+ * --quiet Suprime el output de checks que pasan; solo reporta fallas.
68
+ */
69
+
70
+ const fs = require('fs');
71
+ const path = require('path');
72
+
73
+ const RAIZ = path.resolve(__dirname, '..');
74
+
75
+ // ── helpers ───────────────────────────────────────────────────────────────────
76
+
77
+ function leerArchivo(rel) {
78
+ try {
79
+ return fs.readFileSync(path.join(RAIZ, rel), 'utf-8');
80
+ } catch (_) {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ function contarEntradasDirectorio(dir, opts = {}) {
86
+ const dirAbs = path.join(RAIZ, dir);
87
+ if (!fs.existsSync(dirAbs)) return 0;
88
+ const entradas = fs.readdirSync(dirAbs, { withFileTypes: true });
89
+ if (opts.dirsOnly) return entradas.filter(e => e.isDirectory()).length;
90
+ if (opts.filesOnly) {
91
+ return entradas.filter(e => e.isFile() && (!opts.ext || e.name.endsWith(opts.ext))).length;
92
+ }
93
+ return entradas.length;
94
+ }
95
+
96
+ function contarHooksEjecutables() {
97
+ const dirAbs = path.join(RAIZ, 'hooks');
98
+ if (!fs.existsSync(dirAbs)) return 0;
99
+ return fs.readdirSync(dirAbs)
100
+ .filter(n => n.endsWith('.js') && !n.startsWith('_'))
101
+ .length;
102
+ }
103
+
104
+ function contarReglasMd() {
105
+ const dirAbs = path.join(RAIZ, 'reglas');
106
+ if (!fs.existsSync(dirAbs)) return 0;
107
+ // Cuenta archivos .md en raíz de reglas/ + subdirectorios por lenguaje
108
+ let total = 0;
109
+ function recurse(d) {
110
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
111
+ if (e.isDirectory() && !e.name.startsWith('_')) recurse(path.join(d, e.name));
112
+ else if (e.isFile() && e.name.endsWith('.md') && !e.name.startsWith('_')) total++;
113
+ }
114
+ }
115
+ recurse(dirAbs);
116
+ return total;
117
+ }
118
+
119
+ // ── reporting ─────────────────────────────────────────────────────────────────
120
+
121
+ const COLOR_OK = '\x1b[32m';
122
+ const COLOR_FAIL = '\x1b[31m';
123
+ const COLOR_WARN = '\x1b[33m';
124
+ const COLOR_DIM = '\x1b[2m';
125
+ const COLOR_RESET = '\x1b[0m';
126
+ const tty = !!process.stdout.isTTY;
127
+ const c = (color, txt) => (tty ? `${color}${txt}${COLOR_RESET}` : txt);
128
+
129
+ const opts = (() => {
130
+ const args = process.argv.slice(2);
131
+ const checkIdx = args.indexOf('--check');
132
+ return {
133
+ only: checkIdx >= 0 ? parseInt(args[checkIdx + 1], 10) : null,
134
+ quiet: args.includes('--quiet'),
135
+ };
136
+ })();
137
+
138
+ const resultados = [];
139
+
140
+ function reportar(check, etiqueta, ok, detalle) {
141
+ resultados.push({ check, etiqueta, ok, detalle });
142
+ if (ok && opts.quiet) return;
143
+ const mark = ok ? c(COLOR_OK, '[OK]') : c(COLOR_FAIL, '[FAIL]');
144
+ console.log(` ${mark} check #${check} — ${etiqueta}`);
145
+ if (detalle && !opts.quiet) {
146
+ for (const linea of String(detalle).split('\n')) {
147
+ console.log(` ${c(COLOR_DIM, linea)}`);
148
+ }
149
+ } else if (detalle && !ok) {
150
+ for (const linea of String(detalle).split('\n')) {
151
+ console.log(` ${linea}`);
152
+ }
153
+ }
154
+ }
155
+
156
+ // ── Check 1: contadores ──────────────────────────────────────────────────────
157
+
158
+ function check1Contadores() {
159
+ if (opts.only && opts.only !== 1) return;
160
+ console.log(c(COLOR_DIM, '\n[1] Contadores: INVENTARIO.md / README.md / docs vs disco'));
161
+
162
+ const real = {
163
+ agentes: contarEntradasDirectorio('agentes', { filesOnly: true, ext: '.md' })
164
+ - (fs.existsSync(path.join(RAIZ, 'agentes', 'README.md')) ? 1 : 0),
165
+ skills: contarEntradasDirectorio('habilidades', { dirsOnly: true }),
166
+ comandos: contarEntradasDirectorio('comandos/swl', { filesOnly: true, ext: '.md' }),
167
+ reglas: contarReglasMd(),
168
+ hooks: contarHooksEjecutables(),
169
+ };
170
+
171
+ // Corregir agentes: descartar archivos `_*.md` (fragmentos compartidos)
172
+ const dirAgentes = path.join(RAIZ, 'agentes');
173
+ real.agentes = fs.readdirSync(dirAgentes)
174
+ .filter(n => n.endsWith('.md') && !n.startsWith('_') && n !== 'README.md')
175
+ .length;
176
+
177
+ // Cargar lo que dicen los manifiestos canónicos. INVENTARIO.md usa una
178
+ // tabla del tipo `| Agentes SWL | 60 |` así que extraemos por fila.
179
+ const inventario = leerArchivo('INVENTARIO.md') || '';
180
+ const extraerCelda = (regex) => {
181
+ const m = inventario.match(regex);
182
+ return m ? Number(m[1]) : null;
183
+ };
184
+ const declarado = {
185
+ agentes: extraerCelda(/\|\s*Agentes[^|]*\|\s*(\d+)\s*\|/i),
186
+ skills: extraerCelda(/\|\s*Habilidades[^|]*\|\s*(\d+)\s*\|/i)
187
+ || extraerCelda(/\|\s*Skills[^|]*\|\s*(\d+)\s*\|/i),
188
+ comandos: extraerCelda(/\|\s*Comandos[^|]*\|\s*(\d+)\s*\|/i),
189
+ reglas: (() => {
190
+ // INVENTARIO.md separa "Reglas base" + "Reglas por lenguaje".
191
+ const base = extraerCelda(/\|\s*Reglas base[^|]*\|\s*(\d+)\s*\|/i) || 0;
192
+ const lang = extraerCelda(/\|\s*Reglas por lenguaje[^|]*\|\s*(\d+)\s*\|/i) || 0;
193
+ const total = extraerCelda(/\|\s*Reglas\s*\|\s*(\d+)\s*\|/i);
194
+ if (total) return total;
195
+ if (base + lang > 0) return base + lang;
196
+ return null;
197
+ })(),
198
+ hooks: extraerCelda(/\|\s*Hooks\s*\|\s*(\d+)\s*\|/i),
199
+ };
200
+
201
+ for (const cat of Object.keys(real)) {
202
+ if (declarado[cat] == null) {
203
+ reportar(1, `INVENTARIO.md no declara ${cat}`, false,
204
+ `Real en disco: ${real[cat]}; INVENTARIO.md no incluye un patrón "(\\d+) ${cat}"`);
205
+ } else if (Number(declarado[cat]) === real[cat]) {
206
+ reportar(1, `INVENTARIO.md ${cat}: ${real[cat]}`, true);
207
+ } else {
208
+ reportar(1, `INVENTARIO.md ${cat} drift`, false,
209
+ `Real en disco: ${real[cat]}; INVENTARIO.md declara: ${declarado[cat]}`);
210
+ }
211
+ }
212
+ }
213
+
214
+ // ── Check 2: comandos/swl/*.md vs docs ──────────────────────────────────────
215
+
216
+ function check2Comandos() {
217
+ if (opts.only && opts.only !== 2) return;
218
+ console.log(c(COLOR_DIM, '\n[2] Comandos /swl:* en disco vs COMANDOS.md y MANUAL_USO.md'));
219
+
220
+ const dirAbs = path.join(RAIZ, 'comandos/swl');
221
+ if (!fs.existsSync(dirAbs)) {
222
+ reportar(2, 'comandos/swl/ no existe', false, 'Directorio faltante');
223
+ return;
224
+ }
225
+
226
+ const archivos = fs.readdirSync(dirAbs)
227
+ .filter(n => n.endsWith('.md') && !n.startsWith('_') && n !== 'README.md');
228
+
229
+ const comandosMd = leerArchivo('COMANDOS.md') || '';
230
+ const manualUsoMd = leerArchivo('MANUAL_USO.md') || '';
231
+
232
+ // Profundidad mínima de menciones en MANUAL_USO.md para comandos no triviales.
233
+ // Origen: reglas/auditorias-documentales-estructurales.md. Sesión 2026-05-16
234
+ // detectó que el check anterior aceptaba 1 sola mención como "documentado",
235
+ // ocultando comandos como /swl:exportar-vault con solo 2 menciones reales.
236
+ const PROFUNDIDAD_MINIMA = 4;
237
+ // Comandos operacionales simples cuya documentación profunda no aplica
238
+ // (son utilities de un solo flag o estado).
239
+ const COMANDOS_TRIVIALES = new Set([
240
+ 'ayuda', 'salud', 'modelo', 'contexto', 'dashboard',
241
+ 'sesiones', 'instintos', 'metricas', 'mcp-status',
242
+ ]);
243
+
244
+ let presenciaFails = 0;
245
+ let profundidadFails = 0;
246
+ let documentados = 0;
247
+ const ausentes = [];
248
+ const infraDocumentados = [];
249
+
250
+ for (const archivo of archivos) {
251
+ const nombre = archivo.replace(/\.md$/, '');
252
+ const refSlash = `/swl:${nombre}`;
253
+ const enComandos = comandosMd.includes(refSlash);
254
+ const enManual = manualUsoMd.includes(refSlash);
255
+
256
+ if (!enComandos || !enManual) {
257
+ presenciaFails++;
258
+ const faltas = [];
259
+ if (!enComandos) faltas.push('COMANDOS.md');
260
+ if (!enManual) faltas.push('MANUAL_USO.md');
261
+ ausentes.push(`${refSlash} (falta en ${faltas.join(' + ')})`);
262
+ continue;
263
+ }
264
+
265
+ // Gate de profundidad: comandos no triviales requieren >= PROFUNDIDAD_MINIMA
266
+ // menciones en MANUAL_USO.md.
267
+ if (!COMANDOS_TRIVIALES.has(nombre)) {
268
+ const menciones = (manualUsoMd.match(new RegExp(escapeRegex(refSlash), 'g')) || []).length;
269
+ if (menciones < PROFUNDIDAD_MINIMA) {
270
+ profundidadFails++;
271
+ infraDocumentados.push(`${refSlash} — solo ${menciones} mención${menciones === 1 ? '' : 'es'} en MANUAL_USO (mínimo ${PROFUNDIDAD_MINIMA})`);
272
+ continue;
273
+ }
274
+ }
275
+ documentados++;
276
+ }
277
+
278
+ const totalFails = presenciaFails + profundidadFails;
279
+ if (totalFails === 0) {
280
+ reportar(2, `${archivos.length} comandos /swl:* documentados con profundidad >= ${PROFUNDIDAD_MINIMA} (o triviales)`, true);
281
+ } else {
282
+ if (presenciaFails > 0) {
283
+ reportar(2, `${presenciaFails} comandos sin mención en docs`, false,
284
+ ausentes.join('\n'));
285
+ }
286
+ if (profundidadFails > 0) {
287
+ reportar(2, `${profundidadFails} comandos infra-documentados (< ${PROFUNDIDAD_MINIMA} menciones en MANUAL_USO)`, false,
288
+ infraDocumentados.join('\n') +
289
+ '\n(Documentar con sección dedicada: Uso / ¿Qué hace? / Cuándo usarlo / Ejemplo / Troubleshooting)');
290
+ }
291
+ }
292
+ }
293
+
294
+ function escapeRegex(s) {
295
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
296
+ }
297
+
298
+ // ── Check 3: flags booleanas vs docs ────────────────────────────────────────
299
+
300
+ function check3Flags() {
301
+ if (opts.only && opts.only !== 3) return;
302
+ console.log(c(COLOR_DIM, '\n[3] Flags booleanas del parser vs bin --help y COMANDOS.md'));
303
+
304
+ const parser = leerArchivo('scripts/lib/parsear-opciones.js') || '';
305
+ const binHelp = leerArchivo('bin/swl-ses.js') || '';
306
+ const comandosMd = leerArchivo('COMANDOS.md') || '';
307
+
308
+ // Extraer las booleanas del parser (busca el array BOOLEANAS)
309
+ const matchBool = parser.match(/const BOOLEANAS\s*=\s*\[([\s\S]*?)\];/);
310
+ if (!matchBool) {
311
+ reportar(3, 'parsear-opciones.js no exporta BOOLEANAS', false,
312
+ 'No se encontró el array BOOLEANAS en el parser');
313
+ return;
314
+ }
315
+
316
+ const booleanas = (matchBool[1].match(/['"]([^'"]+)['"]/g) || [])
317
+ .map(s => s.replace(/['"]/g, ''))
318
+ .filter(b => !['simular', 'forzar'].includes(b)); // alias en español ya cubiertos
319
+
320
+ // Flags críticas que SÍ deben estar documentadas (subset)
321
+ const criticas = ['tui', 'no-tui', 'with-mcp', 'all-langs', 'force', 'dry-run', 'global'];
322
+
323
+ let fallas = 0;
324
+ for (const flag of criticas) {
325
+ if (!booleanas.includes(flag)) {
326
+ // Si la flag no está en el parser tampoco; eso es señalable pero no fail aquí
327
+ continue;
328
+ }
329
+ const enBinHelp = binHelp.includes(`--${flag}`);
330
+ const enComandosMd = comandosMd.includes(`--${flag}`);
331
+ if (!enBinHelp || !enComandosMd) {
332
+ fallas++;
333
+ const faltas = [];
334
+ if (!enBinHelp) faltas.push('bin/swl-ses.js (--help)');
335
+ if (!enComandosMd) faltas.push('COMANDOS.md');
336
+ reportar(3, `Flag --${flag} sin documentar en ${faltas.join(' + ')}`, false,
337
+ `Parser la declara como booleana pero falta en docs`);
338
+ }
339
+ }
340
+ if (fallas === 0) {
341
+ reportar(3, `${criticas.length} flags críticas documentadas en bin --help y COMANDOS.md`, true);
342
+ }
343
+ }
344
+
345
+ // ── Check 4: versiones ──────────────────────────────────────────────────────
346
+
347
+ function check4Versiones() {
348
+ if (opts.only && opts.only !== 4) return;
349
+ console.log(c(COLOR_DIM, '\n[4] Versión de package.json vs encabezados de docs'));
350
+
351
+ let version;
352
+ try {
353
+ version = require(path.join(RAIZ, 'package.json')).version;
354
+ } catch (_) {
355
+ reportar(4, 'package.json no parsea', false);
356
+ return;
357
+ }
358
+
359
+ const docs = ['CLAUDE.md', 'README.md', 'AGENTS.md', 'COMANDOS.md', 'MANUAL_USO.md', 'INSTALACION.md'];
360
+ let fallas = 0;
361
+ for (const doc of docs) {
362
+ const contenido = leerArchivo(doc);
363
+ if (!contenido) {
364
+ fallas++;
365
+ reportar(4, `${doc} no se puede leer`, false);
366
+ continue;
367
+ }
368
+ const primeraLinea = contenido.split('\n')[0] || '';
369
+ if (!primeraLinea.includes(version)) {
370
+ fallas++;
371
+ reportar(4, `${doc} encabezado sin v${version}`, false,
372
+ `Primera línea: "${primeraLinea.slice(0, 100)}"`);
373
+ }
374
+ }
375
+ if (fallas === 0) {
376
+ reportar(4, `Encabezados de ${docs.length} docs consistentes con v${version}`, true);
377
+ }
378
+ }
379
+
380
+ // ── Check 5: pantallas TUI ───────────────────────────────────────────────────
381
+
382
+ function check5Tui() {
383
+ if (opts.only && opts.only !== 5) return;
384
+ console.log(c(COLOR_DIM, '\n[5] Pantallas TUI vs menciones en MANUAL_USO.md'));
385
+
386
+ const dirAbs = path.join(RAIZ, 'scripts/tui/pantallas');
387
+ if (!fs.existsSync(dirAbs)) {
388
+ // Si no hay TUI (pre-v1.6.0), saltar el check sin fallar
389
+ reportar(5, 'scripts/tui/pantallas/ no existe (TUI no instalado)', true);
390
+ return;
391
+ }
392
+
393
+ const pantallas = fs.readdirSync(dirAbs)
394
+ .filter(n => n.endsWith('.js'))
395
+ .map(n => n.replace(/\.js$/, ''));
396
+
397
+ const manualUsoMd = (leerArchivo('MANUAL_USO.md') || '').toLowerCase();
398
+
399
+ // Mapeo de nombre de archivo a término que debe aparecer en docs
400
+ const terminos = {
401
+ 'welcome': ['welcome', 'bienvenida'],
402
+ 'menu-principal': ['menú principal', 'menu principal'],
403
+ 'install-wizard': ['wizard install', 'install wizard', 'wizard de instalación', 'instalador tui'],
404
+ 'update-wizard': ['wizard update', 'update wizard', 'wizard de actualización'],
405
+ 'uninstall-wizard': ['uninstall', 'desinstal'],
406
+ 'inspect': ['inspect'],
407
+ 'progreso': ['progreso', 'barra de progreso'],
408
+ 'resumen': ['resumen', 'pantalla resumen'],
409
+ };
410
+
411
+ let fallas = 0;
412
+ for (const pantalla of pantallas) {
413
+ const expectedTerms = terminos[pantalla];
414
+ if (!expectedTerms) continue; // archivo no es una pantalla documentable
415
+ const algunoPresente = expectedTerms.some(t => manualUsoMd.includes(t.toLowerCase()));
416
+ if (!algunoPresente) {
417
+ fallas++;
418
+ reportar(5, `Pantalla "${pantalla}" sin mención en MANUAL_USO.md`, false,
419
+ `Esperaba al menos uno de: ${expectedTerms.join(', ')}`);
420
+ }
421
+ }
422
+ if (fallas === 0) {
423
+ const documentables = Object.keys(terminos).length;
424
+ reportar(5, `${documentables} pantallas TUI mencionadas en MANUAL_USO.md`, true);
425
+ }
426
+ }
427
+
428
+ // ── Check 6: catálogo SWL_* completo ─────────────────────────────────────────
429
+
430
+ function check6Variables() {
431
+ if (opts.only && opts.only !== 6) return;
432
+ console.log(c(COLOR_DIM, '\n[6] Variables SWL_* en código vs docs/variables-entorno.md'));
433
+
434
+ // Extraer variables del código
435
+ // v1.6.1: scope extendido a agentes/, comandos/, bin/ además de hooks/ y scripts/
436
+ // Origen: cierra Cabo A3 detectado en sondeo cross-dimensional post-v1.6.1.
437
+ const codeVars = new Set();
438
+ const escanear = (dir) => {
439
+ const dirAbs = path.join(RAIZ, dir);
440
+ if (!fs.existsSync(dirAbs)) return;
441
+ const recurse = (d) => {
442
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
443
+ const p = path.join(d, entry.name);
444
+ if (entry.isDirectory()) {
445
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
446
+ recurse(p);
447
+ } else if (entry.isFile()) {
448
+ const ext = path.extname(entry.name);
449
+ // .md incluido para agentes/ y comandos/ que pueden documentar variables
450
+ // como parte de su contrato operativo (ej: SWL_AUDIT_AGENTES en
451
+ // agentes/auto-evolucion-swl.md).
452
+ if (['.js', '.cjs', '.mjs', '.py', '.sh', '.md'].includes(ext)) {
453
+ try {
454
+ const contenido = fs.readFileSync(p, 'utf-8');
455
+ const matches = contenido.match(/\bSWL_[A-Z][A-Z0-9_]*\b/g);
456
+ if (matches) for (const m of matches) codeVars.add(m);
457
+ } catch (_) {}
458
+ }
459
+ }
460
+ }
461
+ };
462
+ recurse(dirAbs);
463
+ };
464
+ escanear('hooks');
465
+ escanear('scripts');
466
+ escanear('agentes');
467
+ escanear('comandos');
468
+ escanear('bin');
469
+
470
+ // Extraer variables del catálogo formal
471
+ const catalogo = leerArchivo('docs/variables-entorno.md') || '';
472
+ const manualUso = leerArchivo('MANUAL_USO.md') || '';
473
+ const docVars = new Set();
474
+ for (const m of catalogo.match(/\bSWL_[A-Z][A-Z0-9_]*\b/g) || []) docVars.add(m);
475
+ for (const m of manualUso.match(/\bSWL_[A-Z][A-Z0-9_]*\b/g) || []) docVars.add(m);
476
+
477
+ // Detectar huérfanas
478
+ const huerfanas = [...codeVars].filter(v => !docVars.has(v)).sort();
479
+
480
+ if (huerfanas.length === 0) {
481
+ reportar(6, `${codeVars.size} variables SWL_* del código documentadas en docs/variables-entorno.md o MANUAL_USO.md`, true);
482
+ } else {
483
+ reportar(6, `${huerfanas.length} variables SWL_* huérfanas (en código pero no documentadas)`, false,
484
+ huerfanas.map(v => `- ${v}`).join('\n') +
485
+ '\nAgregar entrada en docs/variables-entorno.md con: nombre, default, archivo que la consume.');
486
+ }
487
+ }
488
+
489
+ // ── Check 7: reglas en disco vs manifiestos/modulos.json ────────────────────
490
+
491
+ function check7Reglas() {
492
+ if (opts.only && opts.only !== 7) return;
493
+ console.log(c(COLOR_DIM, '\n[7] Reglas en disco vs manifiestos/modulos.json'));
494
+
495
+ const dirAbs = path.join(RAIZ, 'reglas');
496
+ if (!fs.existsSync(dirAbs)) {
497
+ reportar(7, 'reglas/ no existe', true);
498
+ return;
499
+ }
500
+
501
+ // Escanear reglas en disco (excluyendo subdirectorios de lenguajes y fragmentos _*)
502
+ const enDisco = new Set();
503
+ for (const entry of fs.readdirSync(dirAbs, { withFileTypes: true })) {
504
+ if (entry.isFile() && entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
505
+ enDisco.add(`reglas/${entry.name}`);
506
+ }
507
+ }
508
+
509
+ // Leer manifiestos/modulos.json
510
+ let manifiesto;
511
+ try {
512
+ manifiesto = require(path.join(RAIZ, 'manifiestos/modulos.json'));
513
+ } catch (err) {
514
+ reportar(7, `No se pudo leer manifiestos/modulos.json: ${err.message}`, false);
515
+ return;
516
+ }
517
+
518
+ // Extraer todas las reglas referenciadas en cualquier módulo
519
+ const enManifiesto = new Set();
520
+ const recurseFiles = (obj) => {
521
+ if (!obj) return;
522
+ if (Array.isArray(obj)) {
523
+ for (const item of obj) {
524
+ if (typeof item === 'string' && item.startsWith('reglas/')) {
525
+ enManifiesto.add(item);
526
+ } else if (typeof item === 'object') {
527
+ recurseFiles(item);
528
+ }
529
+ }
530
+ } else if (typeof obj === 'object') {
531
+ for (const v of Object.values(obj)) recurseFiles(v);
532
+ }
533
+ };
534
+ recurseFiles(manifiesto);
535
+
536
+ // Detectar reglas en disco sin entrada en manifiesto
537
+ const huerfanas = [...enDisco].filter(r => !enManifiesto.has(r)).sort();
538
+
539
+ if (huerfanas.length === 0) {
540
+ reportar(7, `${enDisco.size} reglas base en disco registradas en manifiestos/modulos.json`, true);
541
+ } else {
542
+ reportar(7, `${huerfanas.length} reglas en disco sin registrar en manifiestos/modulos.json`, false,
543
+ huerfanas.map(r => `- ${r}`).join('\n') +
544
+ '\nAgregar al módulo apropiado de manifiestos/modulos.json.');
545
+ }
546
+ }
547
+
548
+ // ── Check 8: ADRs aceptados vs referencia en CHANGELOG.md ───────────────────
549
+
550
+ function check8ADRsChangelog() {
551
+ if (opts.only && opts.only !== 8) return;
552
+ console.log(c(COLOR_DIM, '\n[8] ADRs aceptados vs referencia en CHANGELOG.md'));
553
+
554
+ const dirAbs = path.join(RAIZ, '.planning/adrs');
555
+ if (!fs.existsSync(dirAbs)) {
556
+ reportar(8, '.planning/adrs/ no existe', true);
557
+ return;
558
+ }
559
+
560
+ // Escanear ADRs por número (NNNN-titulo.md), excluyendo README
561
+ const adrsAceptados = [];
562
+ for (const entry of fs.readdirSync(dirAbs, { withFileTypes: true })) {
563
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
564
+ if (entry.name === 'README.md') continue;
565
+ const match = entry.name.match(/^(\d{4})-/);
566
+ if (!match) continue;
567
+
568
+ // Leer frontmatter/estado para confirmar "Aceptado"
569
+ try {
570
+ const contenido = fs.readFileSync(path.join(dirAbs, entry.name), 'utf-8');
571
+ const estadoMatch = contenido.match(/\*\*Estado\*\*:\s*(\w+)/i) ||
572
+ contenido.match(/^estado:\s*(\w+)/im);
573
+ const estado = estadoMatch ? estadoMatch[1].toLowerCase() : 'desconocido';
574
+ if (estado === 'aceptado') {
575
+ adrsAceptados.push({ numero: match[1], archivo: entry.name });
576
+ }
577
+ } catch (_) { /* ignorar lectura fallida */ }
578
+ }
579
+
580
+ // Leer CHANGELOG y buscar referencias ADR-NNNN
581
+ const changelog = leerArchivo('CHANGELOG.md') || '';
582
+ const referencias = new Set();
583
+ for (const m of changelog.match(/ADR-(\d{4})/g) || []) {
584
+ referencias.add(m.replace('ADR-', ''));
585
+ }
586
+
587
+ // ADRs aceptados sin referencia en CHANGELOG
588
+ const sinReferencia = adrsAceptados.filter(adr => !referencias.has(adr.numero));
589
+
590
+ if (sinReferencia.length === 0) {
591
+ reportar(8, `${adrsAceptados.length} ADRs aceptados referenciados en CHANGELOG.md`, true);
592
+ } else {
593
+ // WARN no falla — el criterio "cambia comportamiento observable" es subjetivo
594
+ const mark = tty ? `${COLOR_WARN}[WARN]${COLOR_RESET}` : '[WARN]';
595
+ console.log(` ${mark} check #8 — ${sinReferencia.length} ADRs aceptados sin referencia en CHANGELOG`);
596
+ if (!opts.quiet) {
597
+ for (const adr of sinReferencia) {
598
+ console.log(` ${c(COLOR_DIM, '- ADR-' + adr.numero + ' (' + adr.archivo + ')')}`);
599
+ }
600
+ console.log(` ${c(COLOR_DIM, '(WARN: no todos los ADRs cambian comportamiento observable. Documentar en CHANGELOG solo los que sí)')}`);
601
+ }
602
+ resultados.push({ check: 8, etiqueta: `${adrsAceptados.length - sinReferencia.length}/${adrsAceptados.length} ADRs referenciados`, ok: true, detalle: null });
603
+ }
604
+ }
605
+
606
+ // ── main ──────────────────────────────────────────────────────────────────────
607
+
608
+ function main() {
609
+ console.log(c(COLOR_DIM, 'Verificador docs-vs-código — swl-ses\n' + '═'.repeat(60)));
610
+
611
+ check1Contadores();
612
+ check2Comandos();
613
+ check3Flags();
614
+ check4Versiones();
615
+ check5Tui();
616
+ check6Variables();
617
+ check7Reglas();
618
+ check8ADRsChangelog();
619
+
620
+ const fails = resultados.filter(r => !r.ok).length;
621
+ const total = resultados.length;
622
+
623
+ console.log('\n' + '─'.repeat(60));
624
+ if (fails === 0) {
625
+ console.log(c(COLOR_OK, `✓ ${total} checks OK — docs alineadas con código`));
626
+ process.exit(0);
627
+ } else {
628
+ console.log(c(COLOR_FAIL, `✗ ${fails}/${total} checks con drift`));
629
+ console.log(c(COLOR_DIM, 'Revisa los hallazgos arriba. Las menciones marcadas FAIL deben'));
630
+ console.log(c(COLOR_DIM, 'actualizarse antes del próximo merge.'));
631
+ process.exit(1);
632
+ }
633
+ }
634
+
635
+ if (require.main === module) {
636
+ try {
637
+ main();
638
+ } catch (err) {
639
+ console.error(c(COLOR_FAIL, `Error de ejecución: ${err.message}`));
640
+ if (process.env.SWL_DEBUG) console.error(err.stack);
641
+ process.exit(2);
642
+ }
643
+ }
644
+
645
+ module.exports = {
646
+ check1Contadores,
647
+ check2Comandos,
648
+ check3Flags,
649
+ check4Versiones,
650
+ check5Tui,
651
+ check6Variables,
652
+ check7Reglas,
653
+ check8ADRsChangelog,
654
+ };
@@ -159,18 +159,34 @@ function verificarArchivo(filePath) {
159
159
 
160
160
  const versionActual = leerCampo(fm, 'version');
161
161
  const ev = leerMetadatosEvolucion(filePath, fm);
162
+ // v1.6.1 (Cabo A4): aceptar `deprecated: true` o `evolvable: false` como
163
+ // justificación legítima para ausencia de `version`. Origen: 12 skills
164
+ // detectados sin version, 1 deprecated (control-profundidad) legítimo.
165
+ const esDeprecated = /^deprecated:\s*true\s*$/m.test(fm);
166
+ const esEvolvableFalse = /^evolvable:\s*false/m.test(fm);
167
+ const excepcionLegitima = esDeprecated; // evolvable:false aún espera version
168
+ // si tiene historia; solo deprecated
169
+ // exime completamente
162
170
 
163
171
  resultado.info.version = versionActual;
164
172
  resultado.info.evolved = ev.evolved;
165
173
  resultado.info.fuenteEvolved = ev.fuente;
174
+ resultado.info.excepcion = excepcionLegitima
175
+ ? (esDeprecated ? 'deprecated' : 'evolvable-false')
176
+ : null;
166
177
 
167
- // CHECK 1: version presente
168
- if (!versionActual) {
178
+ // CHECK 1: version presente (exime deprecated)
179
+ if (!versionActual && !excepcionLegitima) {
169
180
  resultado.problemas.push('campo `version` ausente del frontmatter');
181
+ } else if (!versionActual && esDeprecated) {
182
+ resultado.info.notas = resultado.info.notas || [];
183
+ resultado.info.notas.push('version ausente — aceptado porque deprecated:true');
170
184
  }
171
185
 
172
186
  // CHECK 2: evolved = true (en frontmatter o en .evolved.json)
173
- if (ev.evolved !== 'true') {
187
+ // v1.6.1 (Cabo A4): exento si deprecated:true — no tiene sentido exigir
188
+ // metadatos de evolución para componentes que se van a eliminar.
189
+ if (ev.evolved !== 'true' && !esDeprecated) {
174
190
  resultado.problemas.push(
175
191
  '`evolved` no encontrado (ni en frontmatter ni en .evolved.json del directorio)'
176
192
  );