@saulwade/swl-ses 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +19 -2
- package/README.md +561 -561
- package/agentes/arquitecto-swl.md +33 -1
- package/agentes/nemesis-auditor-swl.md +59 -19
- package/bin/swl-mcp-server.js +214 -214
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/nemesis.md +230 -56
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/feynman-auditor-swl/SKILL.md +123 -123
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/SKILL.md +225 -1
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/nemesis-evaluacion-json/SKILL.md +266 -0
- package/habilidades/nemesis-redistribuir/SKILL.md +341 -0
- package/habilidades/node-experto/SKILL.md +105 -4
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/protocolo-revision-swl/SKILL.md +350 -276
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -166
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/tdd-workflow/SKILL.md +150 -4
- package/habilidades/testing-python/SKILL.md +340 -340
- package/habilidades/verificar-trabajo/SKILL.md +8 -3
- package/habilidades/web-fetcher-routing/SKILL.md +75 -75
- package/hooks/check-update.js +31 -3
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/modulos.json +1324 -1321
- package/manifiestos/skills-lock.json +1114 -1114
- package/package.json +2 -2
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +353 -351
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/registro-componentes-nuevos.md +192 -0
- package/reglas/usar-context7.md +226 -226
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/actualizar.js +110 -1
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/derivar-feature-list.js +489 -489
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/doctor.js +58 -4
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/mcp_config.py +127 -0
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/lib/transformadores/codex.js +375 -375
- package/scripts/lib/transformadores/cursor.js +359 -359
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-orchestrator.py +8 -18
- package/scripts/mcp-pool-manager.py +12 -23
- package/scripts/mcp-server/README.md +170 -170
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- package/scripts/mcp-server/telemetry.js +78 -78
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-userland-vacio.js +110 -110
|
@@ -1,489 +1,489 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* derivar-feature-list.js — Sub-fase 5 (Opción B desde análisis cc-sdd)
|
|
6
|
-
*
|
|
7
|
-
* Genera `.planning/feature-list.json` como **state machine machine-readable**
|
|
8
|
-
* derivado del Markdown libre de `.planning/HOJA-RUTA.md` y los PLAN.md/
|
|
9
|
-
* RESUMEN.md de cada fase en `.planning/fases/`.
|
|
10
|
-
*
|
|
11
|
-
* El HOJA-RUTA.md es la fuente de verdad human-friendly; este script produce
|
|
12
|
-
* una proyección JSON consumible por:
|
|
13
|
-
* - `/swl:dashboard` (vista de progreso de fases)
|
|
14
|
-
* - `/swl:metricas` (cálculo de velocity, lead time, completitud)
|
|
15
|
-
* - Hooks de observabilidad
|
|
16
|
-
* - Auditoría externa programática
|
|
17
|
-
*
|
|
18
|
-
* NO reemplaza HOJA-RUTA.md. Si entran en conflicto, HOJA-RUTA.md gana —
|
|
19
|
-
* este JSON es una derivación cache, regenerable.
|
|
20
|
-
*
|
|
21
|
-
* Inspirado en `feature_list.json` de harness-sdd-main, adaptado al modelo
|
|
22
|
-
* por-fase de swl-ses (no por-feature).
|
|
23
|
-
*
|
|
24
|
-
* Uso:
|
|
25
|
-
* node scripts/derivar-feature-list.js
|
|
26
|
-
* node scripts/derivar-feature-list.js --out custom/path.json
|
|
27
|
-
* node scripts/derivar-feature-list.js --check # exit 1 si JSON está stale
|
|
28
|
-
* node scripts/derivar-feature-list.js --verbose
|
|
29
|
-
*
|
|
30
|
-
* Exit codes:
|
|
31
|
-
* 0 OK / generado / sincronizado
|
|
32
|
-
* 1 HOJA-RUTA.md ausente o .planning/ no inicializado
|
|
33
|
-
* 2 --check detectó drift entre HOJA-RUTA.md y feature-list.json
|
|
34
|
-
* 3 Error de parsing
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
const fs = require('fs');
|
|
38
|
-
const path = require('path');
|
|
39
|
-
|
|
40
|
-
const CWD = process.cwd();
|
|
41
|
-
const PLANNING_DIR = path.join(CWD, '.planning');
|
|
42
|
-
const HOJA_RUTA = path.join(PLANNING_DIR, 'HOJA-RUTA.md');
|
|
43
|
-
const FASES_DIR = path.join(PLANNING_DIR, 'fases');
|
|
44
|
-
const DEFAULT_OUT = path.join(PLANNING_DIR, 'feature-list.json');
|
|
45
|
-
|
|
46
|
-
const ESTADOS_VALIDOS = new Set([
|
|
47
|
-
'pendiente',
|
|
48
|
-
'en_progreso',
|
|
49
|
-
'spec_listo',
|
|
50
|
-
'completado',
|
|
51
|
-
'bloqueado',
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
// ─── Parsing del HOJA-RUTA.md ────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Extrae fases del HOJA-RUTA.md.
|
|
58
|
-
*
|
|
59
|
-
* Reconoce dos formatos:
|
|
60
|
-
*
|
|
61
|
-
* 1. Tabla resumen al inicio:
|
|
62
|
-
* | Fase | Nombre | Objetivo | Duración estimada | Estado |
|
|
63
|
-
* | 1 | Bootstrap | ... | 2 semanas | Completado |
|
|
64
|
-
*
|
|
65
|
-
* 2. Secciones detalladas:
|
|
66
|
-
* ## Fase N: Nombre
|
|
67
|
-
* **Objetivo**: ...
|
|
68
|
-
* **Estado**: Pendiente | En progreso | Completado
|
|
69
|
-
* **Fecha inicio**: YYYY-MM-DD
|
|
70
|
-
* **Fecha fin real**: YYYY-MM-DD
|
|
71
|
-
*
|
|
72
|
-
* Si solo existe la tabla, se usa esa. Si existen ambos, las secciones
|
|
73
|
-
* detalladas ganan (más metadata).
|
|
74
|
-
*/
|
|
75
|
-
function parsearHojaRuta(contenido) {
|
|
76
|
-
const fases = new Map(); // numero → fase
|
|
77
|
-
|
|
78
|
-
// Parsear tabla resumen
|
|
79
|
-
parsearTablaResumen(contenido, fases);
|
|
80
|
-
|
|
81
|
-
// Parsear secciones detalladas (sobreescriben con más detalle)
|
|
82
|
-
parsearSecciones(contenido, fases);
|
|
83
|
-
|
|
84
|
-
return Array.from(fases.values()).sort((a, b) => a.numero - b.numero);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function parsearTablaResumen(contenido, fases) {
|
|
88
|
-
// Detecta tabla cuyo encabezado contiene "Fase" o "Phase" + "Nombre" + "Estado"
|
|
89
|
-
const lineas = contenido.split('\n');
|
|
90
|
-
let enTabla = false;
|
|
91
|
-
let idxFase = -1;
|
|
92
|
-
let idxNombre = -1;
|
|
93
|
-
let idxObjetivo = -1;
|
|
94
|
-
let idxEstado = -1;
|
|
95
|
-
|
|
96
|
-
for (const linea of lineas) {
|
|
97
|
-
const trim = linea.trim();
|
|
98
|
-
if (!trim.startsWith('|')) {
|
|
99
|
-
if (enTabla && trim === '') enTabla = false;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const celdas = trim.split('|').map(c => c.trim()).filter(c => c !== '');
|
|
104
|
-
|
|
105
|
-
// Detectar header
|
|
106
|
-
if (idxFase === -1) {
|
|
107
|
-
const lowered = celdas.map(c => c.toLowerCase());
|
|
108
|
-
const fa = lowered.findIndex(c => c === 'fase' || c === 'phase' || c === '#');
|
|
109
|
-
const no = lowered.findIndex(c => c === 'nombre' || c === 'name');
|
|
110
|
-
const ob = lowered.findIndex(c => c === 'objetivo' || c === 'objective');
|
|
111
|
-
const es = lowered.findIndex(c => c === 'estado' || c === 'status');
|
|
112
|
-
if (fa !== -1 && no !== -1 && es !== -1) {
|
|
113
|
-
idxFase = fa;
|
|
114
|
-
idxNombre = no;
|
|
115
|
-
idxObjetivo = ob;
|
|
116
|
-
idxEstado = es;
|
|
117
|
-
enTabla = true;
|
|
118
|
-
}
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Línea separadora (|---|---|)
|
|
123
|
-
if (celdas.every(c => /^-+$/.test(c.replace(/:/g, '')))) continue;
|
|
124
|
-
|
|
125
|
-
if (!enTabla) continue;
|
|
126
|
-
|
|
127
|
-
const numStr = celdas[idxFase];
|
|
128
|
-
const numero = parseInt(numStr, 10);
|
|
129
|
-
if (!Number.isFinite(numero)) continue;
|
|
130
|
-
|
|
131
|
-
fases.set(numero, {
|
|
132
|
-
numero,
|
|
133
|
-
nombre: celdas[idxNombre] || `Fase ${numero}`,
|
|
134
|
-
objetivo: idxObjetivo !== -1 ? (celdas[idxObjetivo] || null) : null,
|
|
135
|
-
estado: normalizarEstado(celdas[idxEstado]),
|
|
136
|
-
estado_raw: celdas[idxEstado],
|
|
137
|
-
fecha_inicio: null,
|
|
138
|
-
fecha_fin_estimada: null,
|
|
139
|
-
fecha_fin_real: null,
|
|
140
|
-
entregables: [],
|
|
141
|
-
criterios_verificacion: [],
|
|
142
|
-
fuente: 'tabla-resumen',
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function parsearSecciones(contenido, fases) {
|
|
148
|
-
// ## Fase N: Nombre o ## Fase N — Nombre
|
|
149
|
-
const reFase = /^##\s+Fase\s+(\d+)\s*[:\-—–]\s*(.+?)$/gm;
|
|
150
|
-
const matches = [...contenido.matchAll(reFase)];
|
|
151
|
-
|
|
152
|
-
for (let i = 0; i < matches.length; i++) {
|
|
153
|
-
const m = matches[i];
|
|
154
|
-
const numero = parseInt(m[1], 10);
|
|
155
|
-
const nombre = m[2].trim();
|
|
156
|
-
const inicio = m.index + m[0].length;
|
|
157
|
-
const fin = i + 1 < matches.length ? matches[i + 1].index : contenido.length;
|
|
158
|
-
const seccion = contenido.slice(inicio, fin);
|
|
159
|
-
|
|
160
|
-
const existente = fases.get(numero) || {
|
|
161
|
-
numero,
|
|
162
|
-
nombre,
|
|
163
|
-
objetivo: null,
|
|
164
|
-
estado: 'pendiente',
|
|
165
|
-
estado_raw: 'Pendiente',
|
|
166
|
-
fecha_inicio: null,
|
|
167
|
-
fecha_fin_estimada: null,
|
|
168
|
-
fecha_fin_real: null,
|
|
169
|
-
entregables: [],
|
|
170
|
-
criterios_verificacion: [],
|
|
171
|
-
fuente: 'seccion-detallada',
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
existente.nombre = nombre || existente.nombre;
|
|
175
|
-
existente.fuente = 'seccion-detallada';
|
|
176
|
-
|
|
177
|
-
const objetivo = extraerCampo(seccion, 'Objetivo');
|
|
178
|
-
if (objetivo) existente.objetivo = objetivo;
|
|
179
|
-
|
|
180
|
-
const estadoRaw = extraerCampo(seccion, 'Estado');
|
|
181
|
-
if (estadoRaw) {
|
|
182
|
-
existente.estado = normalizarEstado(estadoRaw);
|
|
183
|
-
existente.estado_raw = estadoRaw;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const fIni = extraerCampo(seccion, 'Fecha inicio');
|
|
187
|
-
if (fIni && esFecha(fIni)) existente.fecha_inicio = fIni;
|
|
188
|
-
|
|
189
|
-
const fFinEst = extraerCampo(seccion, 'Fecha fin estimada');
|
|
190
|
-
if (fFinEst && esFecha(fFinEst)) existente.fecha_fin_estimada = fFinEst;
|
|
191
|
-
|
|
192
|
-
const fFinReal = extraerCampo(seccion, 'Fecha fin real');
|
|
193
|
-
if (fFinReal && esFecha(fFinReal)) existente.fecha_fin_real = fFinReal;
|
|
194
|
-
|
|
195
|
-
// Entregables (subtabla)
|
|
196
|
-
existente.entregables = parsearTablaEntregables(seccion);
|
|
197
|
-
|
|
198
|
-
// Criterios de verificación (checklist)
|
|
199
|
-
existente.criterios_verificacion = parsearChecklistCriterios(seccion);
|
|
200
|
-
|
|
201
|
-
fases.set(numero, existente);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function extraerCampo(seccion, etiqueta) {
|
|
206
|
-
const re = new RegExp(`^\\s*[\\*\\-]?\\s*\\*\\*${etiqueta}\\*\\*\\s*:\\s*(.+?)$`, 'mi');
|
|
207
|
-
const m = seccion.match(re);
|
|
208
|
-
if (!m) return null;
|
|
209
|
-
const valor = m[1].trim();
|
|
210
|
-
if (valor === '—' || valor === '-' || valor === '' || valor.toLowerCase().startsWith('[')) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
return valor;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function parsearTablaEntregables(seccion) {
|
|
217
|
-
const lineas = seccion.split('\n');
|
|
218
|
-
const entregables = [];
|
|
219
|
-
let enTabla = false;
|
|
220
|
-
let idxNum = -1, idxNombre = -1, idxDesc = -1, idxReqs = -1, idxEstado = -1;
|
|
221
|
-
|
|
222
|
-
for (const linea of lineas) {
|
|
223
|
-
const trim = linea.trim();
|
|
224
|
-
if (!trim.startsWith('|')) {
|
|
225
|
-
if (enTabla && trim === '') {
|
|
226
|
-
enTabla = false;
|
|
227
|
-
idxNum = -1;
|
|
228
|
-
}
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const celdas = trim.split('|').map(c => c.trim()).filter(c => c !== '');
|
|
232
|
-
|
|
233
|
-
if (idxNum === -1) {
|
|
234
|
-
const low = celdas.map(c => c.toLowerCase());
|
|
235
|
-
const n = low.findIndex(c => c === '#' || c === 'id');
|
|
236
|
-
const no = low.findIndex(c => c === 'entregable' || c === 'nombre');
|
|
237
|
-
const de = low.findIndex(c => c === 'descripción' || c === 'descripcion');
|
|
238
|
-
const re = low.findIndex(c => c.includes('requisitos') || c.includes('cubiertos'));
|
|
239
|
-
const es = low.findIndex(c => c === 'estado');
|
|
240
|
-
if (n !== -1 && no !== -1 && es !== -1) {
|
|
241
|
-
idxNum = n; idxNombre = no; idxDesc = de; idxReqs = re; idxEstado = es;
|
|
242
|
-
enTabla = true;
|
|
243
|
-
}
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (celdas.every(c => /^-+$/.test(c.replace(/:/g, '')))) continue;
|
|
248
|
-
if (!enTabla) continue;
|
|
249
|
-
|
|
250
|
-
const numEntregable = celdas[idxNum];
|
|
251
|
-
if (!numEntregable) continue;
|
|
252
|
-
|
|
253
|
-
entregables.push({
|
|
254
|
-
id: numEntregable,
|
|
255
|
-
nombre: celdas[idxNombre] || null,
|
|
256
|
-
descripcion: idxDesc !== -1 ? (celdas[idxDesc] || null) : null,
|
|
257
|
-
requisitos: idxReqs !== -1
|
|
258
|
-
? (celdas[idxReqs] || '').split(',').map(s => s.trim()).filter(Boolean)
|
|
259
|
-
: [],
|
|
260
|
-
estado: normalizarEstado(celdas[idxEstado]),
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return entregables;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function parsearChecklistCriterios(seccion) {
|
|
268
|
-
// Captura solo el bloque "Criterios de verificación" si existe
|
|
269
|
-
const idx = seccion.toLowerCase().indexOf('criterios de verificación');
|
|
270
|
-
if (idx === -1) return [];
|
|
271
|
-
const subseccion = seccion.slice(idx);
|
|
272
|
-
// Cortar en la siguiente sub-sección o el final
|
|
273
|
-
const finIdx = subseccion.search(/\n###?\s+/m);
|
|
274
|
-
const recorte = finIdx === -1 ? subseccion : subseccion.slice(0, finIdx);
|
|
275
|
-
|
|
276
|
-
const criterios = [];
|
|
277
|
-
const re = /^\s*-\s*\[(x| )\]\s+(.+?)$/gim;
|
|
278
|
-
let m;
|
|
279
|
-
while ((m = re.exec(recorte)) !== null) {
|
|
280
|
-
criterios.push({
|
|
281
|
-
cumplido: m[1].toLowerCase() === 'x',
|
|
282
|
-
descripcion: m[2].trim(),
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
return criterios;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function normalizarEstado(estadoRaw) {
|
|
289
|
-
if (!estadoRaw) return 'pendiente';
|
|
290
|
-
const lower = estadoRaw.toLowerCase().trim();
|
|
291
|
-
|
|
292
|
-
if (lower.includes('complet')) return 'completado';
|
|
293
|
-
if (lower.includes('done') || lower.includes('hecho')) return 'completado';
|
|
294
|
-
if (lower.includes('progreso') || lower.includes('progress') || lower.includes('curso')) {
|
|
295
|
-
return 'en_progreso';
|
|
296
|
-
}
|
|
297
|
-
if (lower.includes('spec') && lower.includes('listo')) return 'spec_listo';
|
|
298
|
-
if (lower.includes('bloque') || lower.includes('block')) return 'bloqueado';
|
|
299
|
-
if (lower.includes('pendiente') || lower.includes('pending')) return 'pendiente';
|
|
300
|
-
|
|
301
|
-
return 'pendiente'; // default conservador
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function esFecha(s) {
|
|
305
|
-
return /^\d{4}-\d{2}-\d{2}/.test(s);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// ─── Enriquecimiento desde .planning/fases/ ──────────────────────────────────
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Para cada fase, intenta enriquecer con info de:
|
|
312
|
-
* .planning/fases/0N-PLAN.md → existe + estado
|
|
313
|
-
* .planning/fases/0N-RESUMEN.md → completada
|
|
314
|
-
* .planning/fases/0N-CONTEXTO.md → discutida
|
|
315
|
-
*/
|
|
316
|
-
function enriquecerDesdeFases(fases, opciones = {}) {
|
|
317
|
-
// CWD dinámico — recalcula al llamar, no al require, para que los tests con
|
|
318
|
-
// process.chdir() funcionen sin requerir reload del módulo.
|
|
319
|
-
const cwd = opciones.cwd || process.cwd();
|
|
320
|
-
const fasesDir = path.join(cwd, '.planning', 'fases');
|
|
321
|
-
if (!fs.existsSync(fasesDir)) return fases;
|
|
322
|
-
|
|
323
|
-
const archivos = fs.readdirSync(fasesDir);
|
|
324
|
-
|
|
325
|
-
for (const fase of fases) {
|
|
326
|
-
const num = fase.numero.toString().padStart(2, '0');
|
|
327
|
-
|
|
328
|
-
fase.artefactos = {
|
|
329
|
-
contexto_md: archivos.includes(`${num}-CONTEXTO.md`)
|
|
330
|
-
? `.planning/fases/${num}-CONTEXTO.md` : null,
|
|
331
|
-
plan_md: archivos.includes(`${num}-PLAN.md`)
|
|
332
|
-
? `.planning/fases/${num}-PLAN.md` : null,
|
|
333
|
-
resumen_md: archivos.includes(`${num}-RESUMEN.md`)
|
|
334
|
-
? `.planning/fases/${num}-RESUMEN.md` : null,
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
// Si tiene PLAN.md, intentar leer su frontmatter `estado:` para refinar
|
|
338
|
-
if (fase.artefactos.plan_md) {
|
|
339
|
-
const planPath = path.join(cwd, fase.artefactos.plan_md);
|
|
340
|
-
try {
|
|
341
|
-
const planContent = fs.readFileSync(planPath, 'utf-8');
|
|
342
|
-
const frontMatch = planContent.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
343
|
-
if (frontMatch) {
|
|
344
|
-
const fm = frontMatch[1];
|
|
345
|
-
const estadoPlan = (fm.match(/^estado:\s*(.+)$/m) || [])[1];
|
|
346
|
-
if (estadoPlan) {
|
|
347
|
-
const estadoPlanLow = estadoPlan.trim().toLowerCase();
|
|
348
|
-
fase.plan_estado = estadoPlanLow;
|
|
349
|
-
// Si el PLAN está aprobado pero el roadmap dice "pendiente",
|
|
350
|
-
// refinar a "en_progreso" porque ya pasó la planeación.
|
|
351
|
-
if (estadoPlanLow === 'aprobado' && fase.estado === 'pendiente') {
|
|
352
|
-
fase.estado = 'en_progreso';
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
} catch { /* no bloquear si el PLAN tiene formato no esperado */ }
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Si tiene RESUMEN.md, asumir completada (a menos que HOJA-RUTA diga otra cosa explícita)
|
|
360
|
-
if (fase.artefactos.resumen_md && fase.estado !== 'bloqueado') {
|
|
361
|
-
fase.estado = 'completado';
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return fases;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// ─── Generación del JSON canónico ────────────────────────────────────────────
|
|
369
|
-
|
|
370
|
-
function generarFeatureList(fases) {
|
|
371
|
-
const ahora = new Date().toISOString();
|
|
372
|
-
const totales = {
|
|
373
|
-
fases: fases.length,
|
|
374
|
-
completadas: fases.filter(f => f.estado === 'completado').length,
|
|
375
|
-
en_progreso: fases.filter(f => f.estado === 'en_progreso').length,
|
|
376
|
-
pendientes: fases.filter(f => f.estado === 'pendiente').length,
|
|
377
|
-
bloqueadas: fases.filter(f => f.estado === 'bloqueado').length,
|
|
378
|
-
spec_listas: fases.filter(f => f.estado === 'spec_listo').length,
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
schema: 'swl-feature-list/v1',
|
|
383
|
-
generado_en: ahora,
|
|
384
|
-
fuente: '.planning/HOJA-RUTA.md',
|
|
385
|
-
nota: 'Derivado automáticamente. HOJA-RUTA.md es la fuente de verdad. Regenerar con `node scripts/derivar-feature-list.js`.',
|
|
386
|
-
totales,
|
|
387
|
-
fases,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ─── Comparación para --check ────────────────────────────────────────────────
|
|
392
|
-
|
|
393
|
-
function compararConDisco(generado, rutaOut) {
|
|
394
|
-
if (!fs.existsSync(rutaOut)) {
|
|
395
|
-
return { iguales: false, razon: `${rutaOut} no existe` };
|
|
396
|
-
}
|
|
397
|
-
let actual;
|
|
398
|
-
try {
|
|
399
|
-
actual = JSON.parse(fs.readFileSync(rutaOut, 'utf-8'));
|
|
400
|
-
} catch (err) {
|
|
401
|
-
return { iguales: false, razon: `JSON existente está corrupto: ${err.message}` };
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Comparar ignorando `generado_en` (timestamp cambia siempre)
|
|
405
|
-
const a = { ...generado, generado_en: '_' };
|
|
406
|
-
const b = { ...actual, generado_en: '_' };
|
|
407
|
-
|
|
408
|
-
const aStr = JSON.stringify(a);
|
|
409
|
-
const bStr = JSON.stringify(b);
|
|
410
|
-
|
|
411
|
-
if (aStr === bStr) return { iguales: true };
|
|
412
|
-
return {
|
|
413
|
-
iguales: false,
|
|
414
|
-
razon: 'Contenido difiere. Regenerar con `node scripts/derivar-feature-list.js`.',
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
419
|
-
|
|
420
|
-
function parsearArgs(argv) {
|
|
421
|
-
const args = { out: DEFAULT_OUT, check: false, verbose: false };
|
|
422
|
-
for (let i = 2; i < argv.length; i++) {
|
|
423
|
-
const a = argv[i];
|
|
424
|
-
if (a === '--check' || a === '-c') args.check = true;
|
|
425
|
-
else if (a === '--verbose' || a === '-v') args.verbose = true;
|
|
426
|
-
else if (a === '--out' || a === '-o') args.out = argv[++i];
|
|
427
|
-
else if (a === '--help' || a === '-h') {
|
|
428
|
-
console.log('Uso: derivar-feature-list.js [--out <path>] [--check] [--verbose]');
|
|
429
|
-
process.exit(0);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return args;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function main() {
|
|
436
|
-
const args = parsearArgs(process.argv);
|
|
437
|
-
|
|
438
|
-
if (!fs.existsSync(HOJA_RUTA)) {
|
|
439
|
-
console.error('[derivar-feature-list] .planning/HOJA-RUTA.md no encontrado.');
|
|
440
|
-
console.error(' Inicializa con `/swl:nuevo-proyecto` o `/swl:adoptar-proyecto` primero.');
|
|
441
|
-
process.exit(1);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const contenido = fs.readFileSync(HOJA_RUTA, 'utf-8');
|
|
445
|
-
let fases;
|
|
446
|
-
try {
|
|
447
|
-
fases = parsearHojaRuta(contenido);
|
|
448
|
-
fases = enriquecerDesdeFases(fases);
|
|
449
|
-
} catch (err) {
|
|
450
|
-
console.error(`[derivar-feature-list] Error de parsing: ${err.message}`);
|
|
451
|
-
process.exit(3);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (fases.length === 0 && args.verbose) {
|
|
455
|
-
console.log('[derivar-feature-list] No se detectaron fases en HOJA-RUTA.md.');
|
|
456
|
-
console.log(' El JSON se genera vacío (válido para proyectos recién inicializados).');
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const generado = generarFeatureList(fases);
|
|
460
|
-
|
|
461
|
-
if (args.check) {
|
|
462
|
-
const cmp = compararConDisco(generado, args.out);
|
|
463
|
-
if (cmp.iguales) {
|
|
464
|
-
if (args.verbose) console.log(`[derivar-feature-list] OK — ${args.out} está sincronizado.`);
|
|
465
|
-
process.exit(0);
|
|
466
|
-
}
|
|
467
|
-
console.error(`[derivar-feature-list] DRIFT: ${cmp.razon}`);
|
|
468
|
-
process.exit(2);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
fs.writeFileSync(args.out, JSON.stringify(generado, null, 2), 'utf-8');
|
|
472
|
-
if (args.verbose) {
|
|
473
|
-
console.log(`[derivar-feature-list] Generado ${args.out}`);
|
|
474
|
-
console.log(` ${generado.totales.fases} fases | ${generado.totales.completadas} completadas | ${generado.totales.en_progreso} en progreso | ${generado.totales.pendientes} pendientes`);
|
|
475
|
-
} else {
|
|
476
|
-
console.log(`[derivar-feature-list] ${args.out} actualizado (${generado.totales.fases} fases)`);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (require.main === module) {
|
|
481
|
-
main();
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
module.exports = {
|
|
485
|
-
parsearHojaRuta,
|
|
486
|
-
enriquecerDesdeFases,
|
|
487
|
-
generarFeatureList,
|
|
488
|
-
normalizarEstado,
|
|
489
|
-
};
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* derivar-feature-list.js — Sub-fase 5 (Opción B desde análisis cc-sdd)
|
|
6
|
+
*
|
|
7
|
+
* Genera `.planning/feature-list.json` como **state machine machine-readable**
|
|
8
|
+
* derivado del Markdown libre de `.planning/HOJA-RUTA.md` y los PLAN.md/
|
|
9
|
+
* RESUMEN.md de cada fase en `.planning/fases/`.
|
|
10
|
+
*
|
|
11
|
+
* El HOJA-RUTA.md es la fuente de verdad human-friendly; este script produce
|
|
12
|
+
* una proyección JSON consumible por:
|
|
13
|
+
* - `/swl:dashboard` (vista de progreso de fases)
|
|
14
|
+
* - `/swl:metricas` (cálculo de velocity, lead time, completitud)
|
|
15
|
+
* - Hooks de observabilidad
|
|
16
|
+
* - Auditoría externa programática
|
|
17
|
+
*
|
|
18
|
+
* NO reemplaza HOJA-RUTA.md. Si entran en conflicto, HOJA-RUTA.md gana —
|
|
19
|
+
* este JSON es una derivación cache, regenerable.
|
|
20
|
+
*
|
|
21
|
+
* Inspirado en `feature_list.json` de harness-sdd-main, adaptado al modelo
|
|
22
|
+
* por-fase de swl-ses (no por-feature).
|
|
23
|
+
*
|
|
24
|
+
* Uso:
|
|
25
|
+
* node scripts/derivar-feature-list.js
|
|
26
|
+
* node scripts/derivar-feature-list.js --out custom/path.json
|
|
27
|
+
* node scripts/derivar-feature-list.js --check # exit 1 si JSON está stale
|
|
28
|
+
* node scripts/derivar-feature-list.js --verbose
|
|
29
|
+
*
|
|
30
|
+
* Exit codes:
|
|
31
|
+
* 0 OK / generado / sincronizado
|
|
32
|
+
* 1 HOJA-RUTA.md ausente o .planning/ no inicializado
|
|
33
|
+
* 2 --check detectó drift entre HOJA-RUTA.md y feature-list.json
|
|
34
|
+
* 3 Error de parsing
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
|
|
40
|
+
const CWD = process.cwd();
|
|
41
|
+
const PLANNING_DIR = path.join(CWD, '.planning');
|
|
42
|
+
const HOJA_RUTA = path.join(PLANNING_DIR, 'HOJA-RUTA.md');
|
|
43
|
+
const FASES_DIR = path.join(PLANNING_DIR, 'fases');
|
|
44
|
+
const DEFAULT_OUT = path.join(PLANNING_DIR, 'feature-list.json');
|
|
45
|
+
|
|
46
|
+
const ESTADOS_VALIDOS = new Set([
|
|
47
|
+
'pendiente',
|
|
48
|
+
'en_progreso',
|
|
49
|
+
'spec_listo',
|
|
50
|
+
'completado',
|
|
51
|
+
'bloqueado',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// ─── Parsing del HOJA-RUTA.md ────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extrae fases del HOJA-RUTA.md.
|
|
58
|
+
*
|
|
59
|
+
* Reconoce dos formatos:
|
|
60
|
+
*
|
|
61
|
+
* 1. Tabla resumen al inicio:
|
|
62
|
+
* | Fase | Nombre | Objetivo | Duración estimada | Estado |
|
|
63
|
+
* | 1 | Bootstrap | ... | 2 semanas | Completado |
|
|
64
|
+
*
|
|
65
|
+
* 2. Secciones detalladas:
|
|
66
|
+
* ## Fase N: Nombre
|
|
67
|
+
* **Objetivo**: ...
|
|
68
|
+
* **Estado**: Pendiente | En progreso | Completado
|
|
69
|
+
* **Fecha inicio**: YYYY-MM-DD
|
|
70
|
+
* **Fecha fin real**: YYYY-MM-DD
|
|
71
|
+
*
|
|
72
|
+
* Si solo existe la tabla, se usa esa. Si existen ambos, las secciones
|
|
73
|
+
* detalladas ganan (más metadata).
|
|
74
|
+
*/
|
|
75
|
+
function parsearHojaRuta(contenido) {
|
|
76
|
+
const fases = new Map(); // numero → fase
|
|
77
|
+
|
|
78
|
+
// Parsear tabla resumen
|
|
79
|
+
parsearTablaResumen(contenido, fases);
|
|
80
|
+
|
|
81
|
+
// Parsear secciones detalladas (sobreescriben con más detalle)
|
|
82
|
+
parsearSecciones(contenido, fases);
|
|
83
|
+
|
|
84
|
+
return Array.from(fases.values()).sort((a, b) => a.numero - b.numero);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parsearTablaResumen(contenido, fases) {
|
|
88
|
+
// Detecta tabla cuyo encabezado contiene "Fase" o "Phase" + "Nombre" + "Estado"
|
|
89
|
+
const lineas = contenido.split('\n');
|
|
90
|
+
let enTabla = false;
|
|
91
|
+
let idxFase = -1;
|
|
92
|
+
let idxNombre = -1;
|
|
93
|
+
let idxObjetivo = -1;
|
|
94
|
+
let idxEstado = -1;
|
|
95
|
+
|
|
96
|
+
for (const linea of lineas) {
|
|
97
|
+
const trim = linea.trim();
|
|
98
|
+
if (!trim.startsWith('|')) {
|
|
99
|
+
if (enTabla && trim === '') enTabla = false;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const celdas = trim.split('|').map(c => c.trim()).filter(c => c !== '');
|
|
104
|
+
|
|
105
|
+
// Detectar header
|
|
106
|
+
if (idxFase === -1) {
|
|
107
|
+
const lowered = celdas.map(c => c.toLowerCase());
|
|
108
|
+
const fa = lowered.findIndex(c => c === 'fase' || c === 'phase' || c === '#');
|
|
109
|
+
const no = lowered.findIndex(c => c === 'nombre' || c === 'name');
|
|
110
|
+
const ob = lowered.findIndex(c => c === 'objetivo' || c === 'objective');
|
|
111
|
+
const es = lowered.findIndex(c => c === 'estado' || c === 'status');
|
|
112
|
+
if (fa !== -1 && no !== -1 && es !== -1) {
|
|
113
|
+
idxFase = fa;
|
|
114
|
+
idxNombre = no;
|
|
115
|
+
idxObjetivo = ob;
|
|
116
|
+
idxEstado = es;
|
|
117
|
+
enTabla = true;
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Línea separadora (|---|---|)
|
|
123
|
+
if (celdas.every(c => /^-+$/.test(c.replace(/:/g, '')))) continue;
|
|
124
|
+
|
|
125
|
+
if (!enTabla) continue;
|
|
126
|
+
|
|
127
|
+
const numStr = celdas[idxFase];
|
|
128
|
+
const numero = parseInt(numStr, 10);
|
|
129
|
+
if (!Number.isFinite(numero)) continue;
|
|
130
|
+
|
|
131
|
+
fases.set(numero, {
|
|
132
|
+
numero,
|
|
133
|
+
nombre: celdas[idxNombre] || `Fase ${numero}`,
|
|
134
|
+
objetivo: idxObjetivo !== -1 ? (celdas[idxObjetivo] || null) : null,
|
|
135
|
+
estado: normalizarEstado(celdas[idxEstado]),
|
|
136
|
+
estado_raw: celdas[idxEstado],
|
|
137
|
+
fecha_inicio: null,
|
|
138
|
+
fecha_fin_estimada: null,
|
|
139
|
+
fecha_fin_real: null,
|
|
140
|
+
entregables: [],
|
|
141
|
+
criterios_verificacion: [],
|
|
142
|
+
fuente: 'tabla-resumen',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parsearSecciones(contenido, fases) {
|
|
148
|
+
// ## Fase N: Nombre o ## Fase N — Nombre
|
|
149
|
+
const reFase = /^##\s+Fase\s+(\d+)\s*[:\-—–]\s*(.+?)$/gm;
|
|
150
|
+
const matches = [...contenido.matchAll(reFase)];
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < matches.length; i++) {
|
|
153
|
+
const m = matches[i];
|
|
154
|
+
const numero = parseInt(m[1], 10);
|
|
155
|
+
const nombre = m[2].trim();
|
|
156
|
+
const inicio = m.index + m[0].length;
|
|
157
|
+
const fin = i + 1 < matches.length ? matches[i + 1].index : contenido.length;
|
|
158
|
+
const seccion = contenido.slice(inicio, fin);
|
|
159
|
+
|
|
160
|
+
const existente = fases.get(numero) || {
|
|
161
|
+
numero,
|
|
162
|
+
nombre,
|
|
163
|
+
objetivo: null,
|
|
164
|
+
estado: 'pendiente',
|
|
165
|
+
estado_raw: 'Pendiente',
|
|
166
|
+
fecha_inicio: null,
|
|
167
|
+
fecha_fin_estimada: null,
|
|
168
|
+
fecha_fin_real: null,
|
|
169
|
+
entregables: [],
|
|
170
|
+
criterios_verificacion: [],
|
|
171
|
+
fuente: 'seccion-detallada',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
existente.nombre = nombre || existente.nombre;
|
|
175
|
+
existente.fuente = 'seccion-detallada';
|
|
176
|
+
|
|
177
|
+
const objetivo = extraerCampo(seccion, 'Objetivo');
|
|
178
|
+
if (objetivo) existente.objetivo = objetivo;
|
|
179
|
+
|
|
180
|
+
const estadoRaw = extraerCampo(seccion, 'Estado');
|
|
181
|
+
if (estadoRaw) {
|
|
182
|
+
existente.estado = normalizarEstado(estadoRaw);
|
|
183
|
+
existente.estado_raw = estadoRaw;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const fIni = extraerCampo(seccion, 'Fecha inicio');
|
|
187
|
+
if (fIni && esFecha(fIni)) existente.fecha_inicio = fIni;
|
|
188
|
+
|
|
189
|
+
const fFinEst = extraerCampo(seccion, 'Fecha fin estimada');
|
|
190
|
+
if (fFinEst && esFecha(fFinEst)) existente.fecha_fin_estimada = fFinEst;
|
|
191
|
+
|
|
192
|
+
const fFinReal = extraerCampo(seccion, 'Fecha fin real');
|
|
193
|
+
if (fFinReal && esFecha(fFinReal)) existente.fecha_fin_real = fFinReal;
|
|
194
|
+
|
|
195
|
+
// Entregables (subtabla)
|
|
196
|
+
existente.entregables = parsearTablaEntregables(seccion);
|
|
197
|
+
|
|
198
|
+
// Criterios de verificación (checklist)
|
|
199
|
+
existente.criterios_verificacion = parsearChecklistCriterios(seccion);
|
|
200
|
+
|
|
201
|
+
fases.set(numero, existente);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function extraerCampo(seccion, etiqueta) {
|
|
206
|
+
const re = new RegExp(`^\\s*[\\*\\-]?\\s*\\*\\*${etiqueta}\\*\\*\\s*:\\s*(.+?)$`, 'mi');
|
|
207
|
+
const m = seccion.match(re);
|
|
208
|
+
if (!m) return null;
|
|
209
|
+
const valor = m[1].trim();
|
|
210
|
+
if (valor === '—' || valor === '-' || valor === '' || valor.toLowerCase().startsWith('[')) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return valor;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parsearTablaEntregables(seccion) {
|
|
217
|
+
const lineas = seccion.split('\n');
|
|
218
|
+
const entregables = [];
|
|
219
|
+
let enTabla = false;
|
|
220
|
+
let idxNum = -1, idxNombre = -1, idxDesc = -1, idxReqs = -1, idxEstado = -1;
|
|
221
|
+
|
|
222
|
+
for (const linea of lineas) {
|
|
223
|
+
const trim = linea.trim();
|
|
224
|
+
if (!trim.startsWith('|')) {
|
|
225
|
+
if (enTabla && trim === '') {
|
|
226
|
+
enTabla = false;
|
|
227
|
+
idxNum = -1;
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const celdas = trim.split('|').map(c => c.trim()).filter(c => c !== '');
|
|
232
|
+
|
|
233
|
+
if (idxNum === -1) {
|
|
234
|
+
const low = celdas.map(c => c.toLowerCase());
|
|
235
|
+
const n = low.findIndex(c => c === '#' || c === 'id');
|
|
236
|
+
const no = low.findIndex(c => c === 'entregable' || c === 'nombre');
|
|
237
|
+
const de = low.findIndex(c => c === 'descripción' || c === 'descripcion');
|
|
238
|
+
const re = low.findIndex(c => c.includes('requisitos') || c.includes('cubiertos'));
|
|
239
|
+
const es = low.findIndex(c => c === 'estado');
|
|
240
|
+
if (n !== -1 && no !== -1 && es !== -1) {
|
|
241
|
+
idxNum = n; idxNombre = no; idxDesc = de; idxReqs = re; idxEstado = es;
|
|
242
|
+
enTabla = true;
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (celdas.every(c => /^-+$/.test(c.replace(/:/g, '')))) continue;
|
|
248
|
+
if (!enTabla) continue;
|
|
249
|
+
|
|
250
|
+
const numEntregable = celdas[idxNum];
|
|
251
|
+
if (!numEntregable) continue;
|
|
252
|
+
|
|
253
|
+
entregables.push({
|
|
254
|
+
id: numEntregable,
|
|
255
|
+
nombre: celdas[idxNombre] || null,
|
|
256
|
+
descripcion: idxDesc !== -1 ? (celdas[idxDesc] || null) : null,
|
|
257
|
+
requisitos: idxReqs !== -1
|
|
258
|
+
? (celdas[idxReqs] || '').split(',').map(s => s.trim()).filter(Boolean)
|
|
259
|
+
: [],
|
|
260
|
+
estado: normalizarEstado(celdas[idxEstado]),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return entregables;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parsearChecklistCriterios(seccion) {
|
|
268
|
+
// Captura solo el bloque "Criterios de verificación" si existe
|
|
269
|
+
const idx = seccion.toLowerCase().indexOf('criterios de verificación');
|
|
270
|
+
if (idx === -1) return [];
|
|
271
|
+
const subseccion = seccion.slice(idx);
|
|
272
|
+
// Cortar en la siguiente sub-sección o el final
|
|
273
|
+
const finIdx = subseccion.search(/\n###?\s+/m);
|
|
274
|
+
const recorte = finIdx === -1 ? subseccion : subseccion.slice(0, finIdx);
|
|
275
|
+
|
|
276
|
+
const criterios = [];
|
|
277
|
+
const re = /^\s*-\s*\[(x| )\]\s+(.+?)$/gim;
|
|
278
|
+
let m;
|
|
279
|
+
while ((m = re.exec(recorte)) !== null) {
|
|
280
|
+
criterios.push({
|
|
281
|
+
cumplido: m[1].toLowerCase() === 'x',
|
|
282
|
+
descripcion: m[2].trim(),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return criterios;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizarEstado(estadoRaw) {
|
|
289
|
+
if (!estadoRaw) return 'pendiente';
|
|
290
|
+
const lower = estadoRaw.toLowerCase().trim();
|
|
291
|
+
|
|
292
|
+
if (lower.includes('complet')) return 'completado';
|
|
293
|
+
if (lower.includes('done') || lower.includes('hecho')) return 'completado';
|
|
294
|
+
if (lower.includes('progreso') || lower.includes('progress') || lower.includes('curso')) {
|
|
295
|
+
return 'en_progreso';
|
|
296
|
+
}
|
|
297
|
+
if (lower.includes('spec') && lower.includes('listo')) return 'spec_listo';
|
|
298
|
+
if (lower.includes('bloque') || lower.includes('block')) return 'bloqueado';
|
|
299
|
+
if (lower.includes('pendiente') || lower.includes('pending')) return 'pendiente';
|
|
300
|
+
|
|
301
|
+
return 'pendiente'; // default conservador
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function esFecha(s) {
|
|
305
|
+
return /^\d{4}-\d{2}-\d{2}/.test(s);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ─── Enriquecimiento desde .planning/fases/ ──────────────────────────────────
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Para cada fase, intenta enriquecer con info de:
|
|
312
|
+
* .planning/fases/0N-PLAN.md → existe + estado
|
|
313
|
+
* .planning/fases/0N-RESUMEN.md → completada
|
|
314
|
+
* .planning/fases/0N-CONTEXTO.md → discutida
|
|
315
|
+
*/
|
|
316
|
+
function enriquecerDesdeFases(fases, opciones = {}) {
|
|
317
|
+
// CWD dinámico — recalcula al llamar, no al require, para que los tests con
|
|
318
|
+
// process.chdir() funcionen sin requerir reload del módulo.
|
|
319
|
+
const cwd = opciones.cwd || process.cwd();
|
|
320
|
+
const fasesDir = path.join(cwd, '.planning', 'fases');
|
|
321
|
+
if (!fs.existsSync(fasesDir)) return fases;
|
|
322
|
+
|
|
323
|
+
const archivos = fs.readdirSync(fasesDir);
|
|
324
|
+
|
|
325
|
+
for (const fase of fases) {
|
|
326
|
+
const num = fase.numero.toString().padStart(2, '0');
|
|
327
|
+
|
|
328
|
+
fase.artefactos = {
|
|
329
|
+
contexto_md: archivos.includes(`${num}-CONTEXTO.md`)
|
|
330
|
+
? `.planning/fases/${num}-CONTEXTO.md` : null,
|
|
331
|
+
plan_md: archivos.includes(`${num}-PLAN.md`)
|
|
332
|
+
? `.planning/fases/${num}-PLAN.md` : null,
|
|
333
|
+
resumen_md: archivos.includes(`${num}-RESUMEN.md`)
|
|
334
|
+
? `.planning/fases/${num}-RESUMEN.md` : null,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Si tiene PLAN.md, intentar leer su frontmatter `estado:` para refinar
|
|
338
|
+
if (fase.artefactos.plan_md) {
|
|
339
|
+
const planPath = path.join(cwd, fase.artefactos.plan_md);
|
|
340
|
+
try {
|
|
341
|
+
const planContent = fs.readFileSync(planPath, 'utf-8');
|
|
342
|
+
const frontMatch = planContent.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
343
|
+
if (frontMatch) {
|
|
344
|
+
const fm = frontMatch[1];
|
|
345
|
+
const estadoPlan = (fm.match(/^estado:\s*(.+)$/m) || [])[1];
|
|
346
|
+
if (estadoPlan) {
|
|
347
|
+
const estadoPlanLow = estadoPlan.trim().toLowerCase();
|
|
348
|
+
fase.plan_estado = estadoPlanLow;
|
|
349
|
+
// Si el PLAN está aprobado pero el roadmap dice "pendiente",
|
|
350
|
+
// refinar a "en_progreso" porque ya pasó la planeación.
|
|
351
|
+
if (estadoPlanLow === 'aprobado' && fase.estado === 'pendiente') {
|
|
352
|
+
fase.estado = 'en_progreso';
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch { /* no bloquear si el PLAN tiene formato no esperado */ }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Si tiene RESUMEN.md, asumir completada (a menos que HOJA-RUTA diga otra cosa explícita)
|
|
360
|
+
if (fase.artefactos.resumen_md && fase.estado !== 'bloqueado') {
|
|
361
|
+
fase.estado = 'completado';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return fases;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ─── Generación del JSON canónico ────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
function generarFeatureList(fases) {
|
|
371
|
+
const ahora = new Date().toISOString();
|
|
372
|
+
const totales = {
|
|
373
|
+
fases: fases.length,
|
|
374
|
+
completadas: fases.filter(f => f.estado === 'completado').length,
|
|
375
|
+
en_progreso: fases.filter(f => f.estado === 'en_progreso').length,
|
|
376
|
+
pendientes: fases.filter(f => f.estado === 'pendiente').length,
|
|
377
|
+
bloqueadas: fases.filter(f => f.estado === 'bloqueado').length,
|
|
378
|
+
spec_listas: fases.filter(f => f.estado === 'spec_listo').length,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
schema: 'swl-feature-list/v1',
|
|
383
|
+
generado_en: ahora,
|
|
384
|
+
fuente: '.planning/HOJA-RUTA.md',
|
|
385
|
+
nota: 'Derivado automáticamente. HOJA-RUTA.md es la fuente de verdad. Regenerar con `node scripts/derivar-feature-list.js`.',
|
|
386
|
+
totales,
|
|
387
|
+
fases,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── Comparación para --check ────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
function compararConDisco(generado, rutaOut) {
|
|
394
|
+
if (!fs.existsSync(rutaOut)) {
|
|
395
|
+
return { iguales: false, razon: `${rutaOut} no existe` };
|
|
396
|
+
}
|
|
397
|
+
let actual;
|
|
398
|
+
try {
|
|
399
|
+
actual = JSON.parse(fs.readFileSync(rutaOut, 'utf-8'));
|
|
400
|
+
} catch (err) {
|
|
401
|
+
return { iguales: false, razon: `JSON existente está corrupto: ${err.message}` };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Comparar ignorando `generado_en` (timestamp cambia siempre)
|
|
405
|
+
const a = { ...generado, generado_en: '_' };
|
|
406
|
+
const b = { ...actual, generado_en: '_' };
|
|
407
|
+
|
|
408
|
+
const aStr = JSON.stringify(a);
|
|
409
|
+
const bStr = JSON.stringify(b);
|
|
410
|
+
|
|
411
|
+
if (aStr === bStr) return { iguales: true };
|
|
412
|
+
return {
|
|
413
|
+
iguales: false,
|
|
414
|
+
razon: 'Contenido difiere. Regenerar con `node scripts/derivar-feature-list.js`.',
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
function parsearArgs(argv) {
|
|
421
|
+
const args = { out: DEFAULT_OUT, check: false, verbose: false };
|
|
422
|
+
for (let i = 2; i < argv.length; i++) {
|
|
423
|
+
const a = argv[i];
|
|
424
|
+
if (a === '--check' || a === '-c') args.check = true;
|
|
425
|
+
else if (a === '--verbose' || a === '-v') args.verbose = true;
|
|
426
|
+
else if (a === '--out' || a === '-o') args.out = argv[++i];
|
|
427
|
+
else if (a === '--help' || a === '-h') {
|
|
428
|
+
console.log('Uso: derivar-feature-list.js [--out <path>] [--check] [--verbose]');
|
|
429
|
+
process.exit(0);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return args;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function main() {
|
|
436
|
+
const args = parsearArgs(process.argv);
|
|
437
|
+
|
|
438
|
+
if (!fs.existsSync(HOJA_RUTA)) {
|
|
439
|
+
console.error('[derivar-feature-list] .planning/HOJA-RUTA.md no encontrado.');
|
|
440
|
+
console.error(' Inicializa con `/swl:nuevo-proyecto` o `/swl:adoptar-proyecto` primero.');
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const contenido = fs.readFileSync(HOJA_RUTA, 'utf-8');
|
|
445
|
+
let fases;
|
|
446
|
+
try {
|
|
447
|
+
fases = parsearHojaRuta(contenido);
|
|
448
|
+
fases = enriquecerDesdeFases(fases);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.error(`[derivar-feature-list] Error de parsing: ${err.message}`);
|
|
451
|
+
process.exit(3);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (fases.length === 0 && args.verbose) {
|
|
455
|
+
console.log('[derivar-feature-list] No se detectaron fases en HOJA-RUTA.md.');
|
|
456
|
+
console.log(' El JSON se genera vacío (válido para proyectos recién inicializados).');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const generado = generarFeatureList(fases);
|
|
460
|
+
|
|
461
|
+
if (args.check) {
|
|
462
|
+
const cmp = compararConDisco(generado, args.out);
|
|
463
|
+
if (cmp.iguales) {
|
|
464
|
+
if (args.verbose) console.log(`[derivar-feature-list] OK — ${args.out} está sincronizado.`);
|
|
465
|
+
process.exit(0);
|
|
466
|
+
}
|
|
467
|
+
console.error(`[derivar-feature-list] DRIFT: ${cmp.razon}`);
|
|
468
|
+
process.exit(2);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
fs.writeFileSync(args.out, JSON.stringify(generado, null, 2), 'utf-8');
|
|
472
|
+
if (args.verbose) {
|
|
473
|
+
console.log(`[derivar-feature-list] Generado ${args.out}`);
|
|
474
|
+
console.log(` ${generado.totales.fases} fases | ${generado.totales.completadas} completadas | ${generado.totales.en_progreso} en progreso | ${generado.totales.pendientes} pendientes`);
|
|
475
|
+
} else {
|
|
476
|
+
console.log(`[derivar-feature-list] ${args.out} actualizado (${generado.totales.fases} fases)`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (require.main === module) {
|
|
481
|
+
main();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
module.exports = {
|
|
485
|
+
parsearHojaRuta,
|
|
486
|
+
enriquecerDesdeFases,
|
|
487
|
+
generarFeatureList,
|
|
488
|
+
normalizarEstado,
|
|
489
|
+
};
|