@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.
- package/CLAUDE.md +32 -61
- package/README.md +20 -3
- package/agentes/datos-swl.md +1 -1
- package/agentes/frontend-angular-swl.md +7 -7
- package/agentes/frontend-css-swl.md +4 -4
- package/agentes/frontend-react-swl.md +7 -7
- package/agentes/frontend-swl.md +9 -9
- package/agentes/frontend-tailwind-swl.md +4 -4
- package/agentes/rendimiento-swl.md +2 -2
- package/bin/swl-ses.js +49 -7
- package/comandos/swl/brainstorm.md +1 -0
- package/comandos/swl/compactar.md +1 -1
- package/comandos/swl/discutir-fase.md +15 -1
- package/comandos/swl/mapear-codebase.md +1 -1
- package/comandos/swl/nemesis.md +29 -0
- package/comandos/swl/planear-fase.md +2 -2
- package/comandos/swl/verificar.md +4 -4
- package/habilidades/aprendizaje-continuo/SKILL.md +7 -1
- package/habilidades/diseno-herramientas-agente/SKILL.md +1 -0
- package/habilidades/doc-sync/SKILL.md +441 -1
- package/habilidades/doubt-driven-review/SKILL.md +177 -171
- package/habilidades/feynman-auditor-swl/SKILL.md +129 -123
- package/habilidades/infra-github-actions/SKILL.md +172 -166
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/nemesis-evaluacion-json/SKILL.md +5 -0
- package/habilidades/nemesis-redistribuir/SKILL.md +5 -0
- package/habilidades/node-experto/SKILL.md +197 -3
- package/habilidades/prevencion-racionalizacion/SKILL.md +1 -0
- package/habilidades/privacy-memoria/SKILL.md +1 -0
- package/habilidades/sre-patrones/SKILL.md +1 -1
- package/habilidades/state-inconsistency-auditor-swl/SKILL.md +172 -166
- package/habilidades/tdd-workflow/SKILL.md +178 -3
- package/habilidades/verificacion-evidencia/SKILL.md +1 -0
- package/habilidades/web-fetcher-routing/SKILL.md +81 -75
- package/habilidades/workflow-claude-code/SKILL.md +2 -2
- package/hooks/extraccion-aprendizajes.js +11 -0
- package/manifiestos/modulos.json +2 -1
- package/manifiestos/skills-lock.json +1142 -1114
- package/package.json +7 -4
- package/plugin.json +4 -2
- package/reglas/auditorias-documentales-estructurales.md +205 -0
- package/schemas/agent-frontmatter.schema.json +1 -1
- package/scripts/desinstalar.js +105 -24
- package/scripts/generar-inventario.js +450 -420
- package/scripts/instalador.js +55 -4
- package/scripts/lib/parsear-opciones.js +3 -0
- package/scripts/lib/ui.js +148 -22
- package/scripts/tui/componentes/selector-multi.js +189 -0
- package/scripts/tui/componentes/selector-unico.js +158 -0
- package/scripts/tui/ejecutores.js +375 -0
- package/scripts/tui/index.js +162 -0
- package/scripts/tui/lib/colores.js +129 -0
- package/scripts/tui/lib/render.js +264 -0
- package/scripts/tui/lib/teclas.js +113 -0
- package/scripts/tui/pantallas/inspect.js +173 -0
- package/scripts/tui/pantallas/install-wizard.js +334 -0
- package/scripts/tui/pantallas/menu-principal.js +52 -0
- package/scripts/tui/pantallas/progreso.js +274 -0
- package/scripts/tui/pantallas/resumen.js +132 -0
- package/scripts/tui/pantallas/uninstall-wizard.js +208 -0
- package/scripts/tui/pantallas/update-wizard.js +232 -0
- package/scripts/tui/pantallas/welcome.js +187 -0
- package/scripts/verificar-docs-vs-codigo.js +654 -0
- 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
|
-
|
|
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
|
);
|