@saulwade/swl-ses 1.9.0 → 2.0.0

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 (108) hide show
  1. package/CLAUDE.md +8 -8
  2. package/README.md +12 -12
  3. package/agentes/accesibilidad-wcag-swl.md +3 -3
  4. package/agentes/auto-evolucion-swl.md +908 -908
  5. package/agentes/disenador-ui-swl.md +6 -5
  6. package/agentes/frontend-angular-swl.md +2 -2
  7. package/agentes/frontend-css-swl.md +2 -2
  8. package/agentes/frontend-react-swl.md +4 -4
  9. package/agentes/frontend-swl.md +6 -6
  10. package/agentes/investigador-ux-swl.md +5 -5
  11. package/agentes/orquestador-swl.md +7 -7
  12. package/agentes/perfilador-usuario-swl.md +308 -308
  13. package/agentes/producto-prd-swl.md +1 -1
  14. package/agentes/red-team-swl.md +218 -218
  15. package/agentes/tdd-qa-swl.md +17 -1
  16. package/comandos/swl/actualizar.md +1 -1
  17. package/comandos/swl/aprender.md +2 -2
  18. package/comandos/swl/aprobar-plan.md +152 -0
  19. package/comandos/swl/ayuda.md +3 -3
  20. package/comandos/swl/discutir-fase.md +20 -2
  21. package/comandos/swl/ejecutar-fase.md +53 -6
  22. package/comandos/swl/evolucionar.md +1 -1
  23. package/comandos/swl/inbox.md +1 -1
  24. package/comandos/swl/instalar.md +1 -1
  25. package/comandos/swl/nemesis.md +1 -1
  26. package/comandos/swl/planear-fase.md +17 -1
  27. package/comandos/swl/plugins.md +1 -1
  28. package/comandos/swl/release.md +1 -1
  29. package/comandos/swl/status.md +279 -0
  30. package/comandos/swl/verificar.md +26 -1
  31. package/habilidades/ai-runtime-security/SKILL.md +1 -1
  32. package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
  33. package/habilidades/benchmark-memoria/SKILL.md +1 -1
  34. package/habilidades/calidad-contract-testing/SKILL.md +165 -0
  35. package/habilidades/changelog-generator/SKILL.md +9 -2
  36. package/habilidades/changelog-generator/scripts/parse-commits.js +11 -1
  37. package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
  38. package/habilidades/drift-detection/SKILL.md +179 -179
  39. package/habilidades/ejecutar-fase/SKILL.md +64 -14
  40. package/habilidades/estructura-proyecto-claude/SKILL.md +17 -14
  41. package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +34 -23
  42. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +70 -53
  43. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -77
  44. package/habilidades/extractor-de-aprendizajes/SKILL.md +9 -5
  45. package/habilidades/harness-claude-code/SKILL.md +10 -7
  46. package/{reglas/harness-claude-code.md → habilidades/harness-claude-code/recursos/disciplina-harness-regla.md} +2 -2
  47. package/habilidades/instalar-sistema/SKILL.md +3 -3
  48. package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +1 -1
  49. package/habilidades/perfil-usuario/SKILL.md +200 -200
  50. package/habilidades/planear-fase/SKILL.md +25 -4
  51. package/habilidades/proceso-ddia-fundamentos/SKILL.md +1 -1
  52. package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
  53. package/habilidades/proceso-debate-adversarial/SKILL.md +2 -2
  54. package/habilidades/protocolo-revision-swl/SKILL.md +1 -1
  55. package/habilidades/seguridad-skills-ia/SKILL.md +1 -1
  56. package/habilidades/swl-claudemd/SKILL.md +50 -210
  57. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -0
  58. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -0
  59. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -0
  60. package/habilidades/swl-dashboard/SKILL.md +9 -9
  61. package/habilidades/swl-revisar-impacto/SKILL.md +1 -1
  62. package/habilidades/tdd-workflow/SKILL.md +45 -5
  63. package/habilidades/validacion-ci-sistema/SKILL.md +3 -3
  64. package/hooks/calidad-pre-commit.js +340 -3
  65. package/hooks/ciclo-evolucion-subagente.js +26 -0
  66. package/hooks/ciclo-evolucion.js +26 -0
  67. package/hooks/extraccion-aprendizajes.js +13 -0
  68. package/hooks/lib/ciclo-evolucion.js +47 -0
  69. package/hooks/{auto-evolucion.js → lib/etapa-auto-evolucion.js} +701 -700
  70. package/hooks/{metricas-evolucion.js → lib/etapa-metricas.js} +388 -376
  71. package/hooks/{actualizar-perfil-usuario.js → lib/etapa-perfil-usuario.js} +376 -364
  72. package/hooks/lib/evolution-tracker.js +24 -3
  73. package/hooks/spec-gate.js +211 -0
  74. package/hooks/tdd-gate.js +241 -0
  75. package/hooks/validar-intent-spec.js +30 -10
  76. package/llms.txt +6 -6
  77. package/manifiestos/hooks-config.json +26 -17
  78. package/manifiestos/modulos.json +17 -14
  79. package/manifiestos/skills-lock.json +63 -56
  80. package/package.json +2 -2
  81. package/plugin.json +6 -10
  82. package/reglas/accesibilidad.md +10 -0
  83. package/reglas/api-diseno.md +9 -0
  84. package/reglas/auditorias-documentales-estructurales.md +7 -0
  85. package/reglas/cloud-infra.md +8 -0
  86. package/reglas/fragmentos-compartidos.md +5 -0
  87. package/reglas/gobernanza.md +4 -4
  88. package/reglas/hooks.md +6 -0
  89. package/reglas/intent-engineering.md +4 -0
  90. package/reglas/markitdown.md +8 -0
  91. package/reglas/memoria-consolidada.md +1 -1
  92. package/reglas/patrones.md +6 -0
  93. package/reglas/registro-componentes-nuevos.md +10 -1
  94. package/reglas/seguridad-agentes.md +1 -1
  95. package/reglas/skills-estandar.md +6 -0
  96. package/reglas/testing.md +7 -0
  97. package/reglas/tests-cleanup.md +4 -0
  98. package/reglas/usar-sistema-swl.md +1 -1
  99. package/scripts/lib/gitignore-manifest.js +29 -1
  100. package/scripts/lib/plan-lock.js +275 -0
  101. package/scripts/migrar-fase-dominio.js +0 -1
  102. package/scripts/verificar-trazabilidad.js +292 -0
  103. package/agentes/ux-disenador-swl.md +0 -503
  104. package/comandos/swl/dashboard.md +0 -146
  105. package/comandos/swl/evolucion-estado.md +0 -191
  106. package/comandos/swl/metricas.md +0 -376
  107. package/comandos/swl/salud.md +0 -481
  108. package/reglas/verificar-citas-temporales.md +0 -139
package/reglas/testing.md CHANGED
@@ -1,3 +1,10 @@
1
+ ---
2
+ paths:
3
+ - "**/*.tsx"
4
+ - "**/*.jsx"
5
+ - "**/vitest.config.{js,ts,mjs}"
6
+ - "**/playwright.config.{js,ts}"
7
+ ---
1
8
  # Regla: Testing — Next.js
2
9
 
3
10
  Probar Next.js con App Router requiere estrategia porque existen tres tipos de
@@ -1,3 +1,7 @@
1
+ ---
2
+ paths:
3
+ - "**/tests/**"
4
+ ---
1
5
  # Regla: Cleanup obligatorio de directorios temporales en tests
2
6
 
3
7
  Esta regla es **OBLIGATORIA** y aplica a todo test Node.js del sistema SWL
@@ -78,7 +78,7 @@ de ejecutar.
78
78
  | Capturar aprendizaje recurrente | `/swl:aprender` → APRENDIZAJES.md → posible promoción a regla/skill |
79
79
  | Release con bump de versión | `/swl:release` (sincronización de ubicaciones de versión) |
80
80
  | Documentación viva post-feature | `documentador-swl` |
81
- | Diagnóstico del sistema | `/swl:salud` |
81
+ | Diagnóstico del sistema | `/swl:status salud` |
82
82
 
83
83
  ### Para tareas de búsqueda y contexto
84
84
 
@@ -79,7 +79,13 @@ const ENTRADAS_BASE = [
79
79
  // artefactos del proyecto usuario.
80
80
  ".planning/evolution/",
81
81
  ".planning/auto-evolution/",
82
- ".planning/locks/",
82
+ // locks/: contenido runtime ignorado (singleton-guard {pid,ts},
83
+ // fase-activa.json) EXCEPTO los plan-locks de G1 (*PLAN.md.lock), que son
84
+ // evidencia de aprobación versionable (audit trail SDD — aprobar-plan.md).
85
+ // Patrón `dir/*` + negación: git no re-incluye archivos si el DIRECTORIO
86
+ // está ignorado, por eso se ignora el contenido y no el dir.
87
+ ".planning/locks/*",
88
+ "!.planning/locks/*PLAN.md.lock",
83
89
  ".planning/user-profile/",
84
90
 
85
91
  // Instintos modificados automáticamente por hooks (degradacion-instintos.js)
@@ -87,6 +93,28 @@ const ENTRADAS_BASE = [
87
93
 
88
94
  // Base de datos de uso
89
95
  "usage.db",
96
+
97
+ // ── Runtime adicional: derivados regenerables, caches y telemetría ──
98
+ // Cobertura completada en Fase 09 tras auditar qué genera SWL en proyectos
99
+ // destino vs lo que ENTRADAS_BASE propagaba. Todo esto es output de runtime,
100
+ // regenerable, no editado por el equipo → no se commitea.
101
+ ".planning/feature-list.json", // derivar-feature-list.js (derivado de HOJA-RUTA.md)
102
+ ".planning/loops/", // loop-telemetry.js (/swl:verificar, /swl:status loops)
103
+ ".planning/archive/", // rotar-audit-auto.js (audit/logs rotados y comprimidos)
104
+ ".planning/analysis/", // outputs de análisis
105
+ ".planning/graph.json", // code-review-graph (cache del grafo)
106
+ ".planning/graph-cache.json", // code-review-graph (cache incremental)
107
+ ".planning/mcp-snapshot.json", // /swl:status (smoke test MCP) / mcp-status
108
+ ".planning/skill-index.json", // /swl:skill-search (índice FTS de skills)
109
+ ".planning/inventario-aviso-state.json", // throttle del aviso de inventario
110
+ ".planning/*.lock", // locks sueltos (ej. STATE.md.lock; el dir locks/ ya está arriba)
111
+
112
+ // Estado runtime de /swl:verificar --until-converge. Vive DENTRO de
113
+ // .planning/fases/, que SÍ contiene plantillas commiteadas
114
+ // (0N-CONTEXTO/PLAN/RESUMEN/VERIFICACION.md). Glob específico para ignorar
115
+ // SOLO el estado de convergencia, nunca esas plantillas.
116
+ ".planning/fases/*-converge-state.json",
117
+ ".planning/fases/*-converge-run-*.json",
90
118
  ];
91
119
 
92
120
  // Entradas adicionales por runtime (se agregan cuando install usa ese target)
@@ -0,0 +1,275 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * plan-lock.js — Firma y verificación de integridad de PLAN.md (Gate G1).
5
+ *
6
+ * Materializa el gate G1 del enforcement SDD (Fase 09, ADR de la Revisión
7
+ * Evolutiva 03): un PLAN aprobado vía `/swl:aprobar-plan` queda firmado con
8
+ * un SHA256 escrito en `.planning/locks/<basename>.lock`. `ejecutar-fase`
9
+ * recomputa ese hash al arrancar; un plan mutado tras la aprobación detiene
10
+ * la ejecución. Cierra el hueco "el plan puede mutar tras aprobación" que
11
+ * dejaba SDD en ~60%.
12
+ *
13
+ * Cláusula de gracia D-05 (compatibilidad legacy): un PLAN con
14
+ * `estado: aprobado` en su frontmatter pero SIN `.lock` correspondiente
15
+ * (los planes creados antes de la Fase 09) se considera `modo: "legacy"` y
16
+ * se ejecuta con advertencia, sin bloqueo. Solo los planes firmados vía
17
+ * `/swl:aprobar-plan` (post-Fase 09) exigen coincidencia de hash.
18
+ *
19
+ * MODELO DE AMENAZA (gate G1): el lock detecta MUTACIÓN ACCIDENTAL o no
20
+ * aprobada del PLAN tras su firma, y planes alterados por un agente que
21
+ * confabula. NO es un control criptográfico contra un adversario con acceso
22
+ * de escritura al repo — quien pueda commitear puede recalcular el SHA256 y
23
+ * re-firmar. La evidencia real de aprobación es `aprobadoPor:` + el commit del
24
+ * lock por `/swl:aprobar-plan` en el audit trail de git. El SHA256 solo detecta
25
+ * drift entre firma y verificación. (Verificación Fase 09, hallazgo S-6.)
26
+ *
27
+ * API:
28
+ * firmarPlan(planPath, opciones?) -> { ok, sha256, lockPath, archivo }
29
+ * verificarPlan(planPath) -> { ok, modo, motivo, ... }
30
+ * resolverLockPath(planPath) -> string (ruta del .lock esperado)
31
+ *
32
+ * Zero-dependencies. Compatible Windows (hash sobre buffer crudo). Node 18+.
33
+ *
34
+ * Origen: Fase 09 — enforcement SDD/TDD vNext H1, Slice 1.
35
+ */
36
+
37
+ const fs = require('fs');
38
+ const path = require('path');
39
+ const crypto = require('crypto');
40
+
41
+ // Escritura atómica obligatoria (regla CLAUDE.md). Fallback defensivo idéntico
42
+ // al de scripts/generar-skills-lock.js para no acoplar a un layout fijo.
43
+ let atomicWriteSync;
44
+ try {
45
+ ({ atomicWriteSync } = require(path.join(__dirname, '..', '..', 'hooks', 'lib', 'atomic-write.js')));
46
+ } catch {
47
+ atomicWriteSync = (p, c, e) => {
48
+ const dir = path.dirname(p);
49
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
50
+ fs.writeFileSync(p, c, e);
51
+ };
52
+ }
53
+
54
+ const LOCK_VERSION = 1;
55
+
56
+ // Basename válido de un PLAN de fase: `PLAN.md` o `0N-PLAN.md`. Evita que
57
+ // `firmarPlan` produzca locks de archivos arbitrarios (falsearía el audit
58
+ // trail de aprobación). Verificación Fase 09, hallazgo S-2.
59
+ const PLAN_BASENAME_RE = /^(\d{2}-)?PLAN\.md$/;
60
+
61
+ /**
62
+ * SHA256 de un buffer. Idéntico a scripts/generar-skills-lock.js:42-44.
63
+ * Se hashea el buffer crudo (no string utf8) para que el hash sea byte-exacto
64
+ * y detecte incluso cambios de fin de línea introducidos por un editor.
65
+ *
66
+ * @param {Buffer|string} data
67
+ * @returns {string} hex digest
68
+ */
69
+ function sha256(data) {
70
+ return crypto.createHash('sha256').update(data).digest('hex');
71
+ }
72
+
73
+ /**
74
+ * Extrae el campo `estado:` del frontmatter YAML del PLAN.
75
+ * Reutiliza el patrón CRLF-safe de scripts/generar-skills-lock.js:46-55.
76
+ *
77
+ * @param {string} contenido
78
+ * @returns {string|null} valor de `estado` o null si no hay frontmatter/campo
79
+ */
80
+ function leerEstado(contenido) {
81
+ const m = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
82
+ if (!m) return null;
83
+ for (const linea of m[1].split(/\r?\n/)) {
84
+ const kv = linea.match(/^estado:\s*(.*)$/);
85
+ if (kv) return kv[1].trim();
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Resuelve el directorio `.planning/locks/` ascendiendo desde el PLAN hasta
92
+ * encontrar el directorio `.planning` ancestro. Robusto ante planes en
93
+ * `.planning/fases/0N-PLAN.md` o `.planning/PLAN.md`.
94
+ *
95
+ * Hardening S-1 (verificación Fase 09):
96
+ * - Resuelve symlinks con `fs.realpathSync` antes de ascender, para que un
97
+ * symlink plantado (`temp/.planning` → `/etc/.planning`) no permita escapar
98
+ * del árbol real del proyecto.
99
+ * - SIN fallback ciego: si no hay `.planning` ancestro, retorna `null` (antes
100
+ * escribía `locks/` al lado del PLAN, en ubicación arbitraria). El caller
101
+ * trata `null` como error explícito.
102
+ *
103
+ * @param {string} planPath
104
+ * @returns {string|null} ruta absoluta del directorio de locks, o null si no
105
+ * hay `.planning` ancestro.
106
+ */
107
+ function resolverLocksDir(planPath) {
108
+ let dir;
109
+ try {
110
+ dir = path.dirname(fs.realpathSync(path.resolve(planPath)));
111
+ } catch {
112
+ dir = path.dirname(path.resolve(planPath));
113
+ }
114
+ while (dir !== path.dirname(dir)) {
115
+ if (path.basename(dir) === '.planning') {
116
+ return path.join(dir, 'locks');
117
+ }
118
+ dir = path.dirname(dir);
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Ruta del `.lock` esperado para un PLAN dado.
125
+ *
126
+ * @param {string} planPath
127
+ * @returns {string|null} ruta absoluta del archivo `.lock`, o null si el PLAN
128
+ * no tiene un `.planning` ancestro.
129
+ */
130
+ function resolverLockPath(planPath) {
131
+ const dir = resolverLocksDir(planPath);
132
+ return dir ? path.join(dir, `${path.basename(planPath)}.lock`) : null;
133
+ }
134
+
135
+ /**
136
+ * Firma un PLAN: computa su SHA256 y escribe el `.lock` correspondiente.
137
+ * Idempotente — re-firmar sobreescribe el lock (caso re-aprobación).
138
+ *
139
+ * @param {string} planPath
140
+ * @param {object} [opciones]
141
+ * @param {string} [opciones.firmadoPor='swl:aprobar-plan']
142
+ * @param {string} [opciones.firmadoEn] ISO timestamp; default new Date()
143
+ * @returns {{ok: boolean, sha256?: string, lockPath?: string, archivo?: string, motivo?: string}}
144
+ */
145
+ function firmarPlan(planPath, opciones = {}) {
146
+ if (!fs.existsSync(planPath)) {
147
+ return { ok: false, motivo: `PLAN no existe: ${planPath}` };
148
+ }
149
+ const archivo = path.basename(planPath);
150
+ if (!PLAN_BASENAME_RE.test(archivo)) {
151
+ return {
152
+ ok: false,
153
+ motivo: `basename inválido para firma: "${archivo}". Solo se firman PLAN.md o 0N-PLAN.md de fases.`,
154
+ };
155
+ }
156
+ const lockPath = resolverLockPath(planPath);
157
+ if (!lockPath) {
158
+ return {
159
+ ok: false,
160
+ motivo: `el PLAN no tiene un directorio .planning ancestro: ${planPath}`,
161
+ };
162
+ }
163
+ const buffer = fs.readFileSync(planPath);
164
+ const hash = sha256(buffer);
165
+
166
+ const lock = {
167
+ version: LOCK_VERSION,
168
+ archivo,
169
+ sha256: hash,
170
+ firmadoEn: opciones.firmadoEn || new Date().toISOString(),
171
+ firmadoPor: opciones.firmadoPor || 'swl:aprobar-plan',
172
+ };
173
+
174
+ atomicWriteSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`, 'utf8');
175
+ return { ok: true, sha256: hash, lockPath, archivo };
176
+ }
177
+
178
+ /**
179
+ * Verifica la integridad de un PLAN contra su `.lock`.
180
+ *
181
+ * Modos de retorno:
182
+ * - `firmado` : existe lock y el hash coincide -> ok: true
183
+ * - `legacy` : NO existe lock pero estado:aprobado (gracia D-05) -> ok: true
184
+ * - `mutado` : existe lock y el hash NO coincide -> ok: false
185
+ * - `lock-corrupto`: el lock existe pero no es JSON válido -> ok: false
186
+ * - `sin-firmar` : NO existe lock y estado != aprobado -> ok: false
187
+ * - `sin-planning`: el PLAN no tiene `.planning` ancestro -> ok: false
188
+ * - `no-existe` : el PLAN no existe en disco -> ok: false
189
+ *
190
+ * @param {string} planPath
191
+ * @returns {{ok: boolean, modo: string, motivo?: string, hashEsperado?: string, hashActual?: string, lockPath?: string}}
192
+ */
193
+ function verificarPlan(planPath) {
194
+ if (!fs.existsSync(planPath)) {
195
+ return { ok: false, modo: 'no-existe', motivo: `PLAN no existe: ${planPath}` };
196
+ }
197
+ const lockPath = resolverLockPath(planPath);
198
+ if (!lockPath) {
199
+ return {
200
+ ok: false,
201
+ modo: 'sin-planning',
202
+ motivo: `el PLAN no tiene un directorio .planning ancestro: ${planPath}`,
203
+ };
204
+ }
205
+ const buffer = fs.readFileSync(planPath);
206
+ const hashActual = sha256(buffer);
207
+
208
+ if (!fs.existsSync(lockPath)) {
209
+ // Cláusula de gracia D-05: plan aprobado antes de que existiera el lock.
210
+ const estado = leerEstado(buffer.toString('utf8'));
211
+ if (estado === 'aprobado') {
212
+ return {
213
+ ok: true,
214
+ modo: 'legacy',
215
+ motivo: 'plan aprobado pre-Fase09 (sin lock); ejecutar con advertencia',
216
+ hashActual,
217
+ lockPath,
218
+ };
219
+ }
220
+ return {
221
+ ok: false,
222
+ modo: 'sin-firmar',
223
+ motivo: `plan sin aprobar y sin lock (estado: ${estado || 'ausente'})`,
224
+ hashActual,
225
+ lockPath,
226
+ };
227
+ }
228
+
229
+ let lock;
230
+ try {
231
+ lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
232
+ } catch (err) {
233
+ return {
234
+ ok: false,
235
+ modo: 'lock-corrupto',
236
+ motivo: `lock malformado en ${lockPath}: ${err.message}`,
237
+ hashActual,
238
+ lockPath,
239
+ };
240
+ }
241
+
242
+ const hashEsperado = lock && typeof lock.sha256 === 'string' ? lock.sha256 : null;
243
+ if (!hashEsperado) {
244
+ return {
245
+ ok: false,
246
+ modo: 'lock-corrupto',
247
+ motivo: `lock sin campo sha256 en ${lockPath}`,
248
+ hashActual,
249
+ lockPath,
250
+ };
251
+ }
252
+
253
+ if (hashEsperado === hashActual) {
254
+ return { ok: true, modo: 'firmado', hashEsperado, hashActual, lockPath };
255
+ }
256
+
257
+ return {
258
+ ok: false,
259
+ modo: 'mutado',
260
+ motivo: 'plan mutado tras aprobación (el hash no coincide con el lock)',
261
+ hashEsperado,
262
+ hashActual,
263
+ lockPath,
264
+ };
265
+ }
266
+
267
+ module.exports = {
268
+ firmarPlan,
269
+ verificarPlan,
270
+ resolverLockPath,
271
+ resolverLocksDir,
272
+ sha256,
273
+ leerEstado,
274
+ LOCK_VERSION,
275
+ };
@@ -80,7 +80,6 @@ const MAPEO = {
80
80
  'revisor-typescript-swl': ['verify', 'quality'],
81
81
  'sre-swl': ['release', 'infra'],
82
82
  'tdd-qa-swl': ['verify', 'quality'],
83
- 'ux-disenador-swl': ['plan', 'ux'],
84
83
  };
85
84
 
86
85
  function migrarAgente(nombre) {
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * verificar-trazabilidad.js — Valida la cadena REQ→T→commit→test de una fase (G4).
6
+ *
7
+ * Uso:
8
+ * node scripts/verificar-trazabilidad.js --fase=10 [--solo-plan] [--json]
9
+ *
10
+ * Fuentes:
11
+ * - REQ-NN: `.planning/fases/NN-CONTEXTO.md` § Criterios de aceptación
12
+ * (definiciones `**REQ-NN**:` o `REQ-NN:`; los marcados RETIRADO
13
+ * se excluyen del universo exigible)
14
+ * - T-NN: `.planning/fases/NN-PLAN.md` — campo `**Verifica REQ**: ...`
15
+ * por tarea `### T-NN:`
16
+ * - Commits: `git log` — footer `Refs: REQ-NN[, REQ-MM]`
17
+ * - Tests: archivos de test versionados (git ls-files + clasificador de
18
+ * calidad-pre-commit) con marker `verifica: REQ-NN`
19
+ *
20
+ * Exit codes:
21
+ * 0 — cadena completa, o CONTEXTO legacy sin REQ-IDs (gracia, con nota)
22
+ * 1 — huérfanos detectados (REQ sin tarea / sin commit / sin test)
23
+ * 2 — error de invocación o parseo (fase inexistente, args inválidos)
24
+ *
25
+ * `--solo-plan`: valida únicamente la matriz REQ×T (lo usa /swl:aprobar-plan,
26
+ * donde commits y tests aún no existen).
27
+ *
28
+ * Zero-deps. Espejo estructural de verificar-release.js (gates + exit codes).
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const { execFileSync } = require('child_process');
34
+
35
+ const LIMITE_COMMITS = 300;
36
+
37
+ // ─── Parseo de fuentes ──────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Extrae definiciones REQ-NN del CONTEXTO.
41
+ * Método de verificación por REQ (patrón requirements engineering):
42
+ * - default `test` — exige test con marker `verifica: REQ-NN`
43
+ * - `(verificación: inspección)` en el criterio — satisfecho por prosa/docs/grep;
44
+ * exige tarea y commit, pero NO test automatizado.
45
+ * @returns {{activos: string[], retirados: string[], metodo: Object<string,string>}}
46
+ */
47
+ function extraerReqs(contextoTexto) {
48
+ const activos = new Set();
49
+ const retirados = new Set();
50
+ const metodo = {};
51
+ // Definición: línea con `**REQ-NN**:` o `REQ-NN:` (con bullet opcional).
52
+ // El criterio puede continuar en líneas siguientes indentadas — se capturan
53
+ // hasta la próxima definición para detectar la anotación de método.
54
+ const re = /^\s*(?:[-*]\s*)?\*{0,2}(REQ-\d{1,3})\*{0,2}\s*:([\s\S]*?)(?=^\s*(?:[-*]\s*)?\*{0,2}REQ-\d|^#|\n\n|$(?![\s\S]))/gm;
55
+ let m;
56
+ while ((m = re.exec(contextoTexto)) !== null) {
57
+ const cuerpo = m[2];
58
+ if (/RETIRADO/i.test(cuerpo)) {
59
+ retirados.add(m[1]);
60
+ continue;
61
+ }
62
+ activos.add(m[1]);
63
+ metodo[m[1]] = /verificaci[oó]n:\s*inspecci[oó]n/i.test(cuerpo) ? 'inspeccion' : 'test';
64
+ }
65
+ return { activos: [...activos], retirados: [...retirados], metodo };
66
+ }
67
+
68
+ /** Extrae mapa tarea→REQs del PLAN. @returns {Map<string, string[]>} */
69
+ function extraerMatrizPlan(planTexto) {
70
+ const tareas = new Map();
71
+ let tareaActual = null;
72
+ for (const linea of planTexto.split('\n')) {
73
+ const t = linea.match(/^#{2,4}\s*(T-\d{1,3})\s*:/);
74
+ if (t) {
75
+ tareaActual = t[1];
76
+ if (!tareas.has(tareaActual)) tareas.set(tareaActual, []);
77
+ continue;
78
+ }
79
+ if (tareaActual && /\*\*Verifica REQ\*\*\s*:/i.test(linea)) {
80
+ const reqs = linea.match(/REQ-\d{1,3}/g) || [];
81
+ tareas.get(tareaActual).push(...reqs);
82
+ }
83
+ }
84
+ return tareas;
85
+ }
86
+
87
+ /** Commits con footer `Refs: REQ-NN`. @returns {Map<string, string[]>} REQ → hashes cortos */
88
+ function extraerCommitsConRefs(cwd) {
89
+ const porReq = new Map();
90
+ let salida;
91
+ try {
92
+ salida = execFileSync(
93
+ 'git',
94
+ ['log', `-${LIMITE_COMMITS}`, '--format=%h%x00%B%x01'],
95
+ { cwd, encoding: 'utf8', timeout: 10000 }
96
+ );
97
+ } catch (_) {
98
+ return porReq; // sin repo git o sin commits — el reporte lo refleja como gap
99
+ }
100
+ for (const bloque of salida.split('\x01')) {
101
+ const [hash, cuerpo] = bloque.split('\x00');
102
+ if (!hash || !cuerpo) continue;
103
+ const refs = cuerpo.match(/^Refs:\s*(.+)$/m);
104
+ if (!refs) continue;
105
+ for (const req of refs[1].match(/REQ-\d{1,3}/g) || []) {
106
+ if (!porReq.has(req)) porReq.set(req, []);
107
+ porReq.get(req).push(hash.trim());
108
+ }
109
+ }
110
+ return porReq;
111
+ }
112
+
113
+ /** Tests con marker `verifica: REQ-NN`. @returns {Map<string, string[]>} REQ → archivos */
114
+ function extraerTestsConMarker(cwd) {
115
+ const porReq = new Map();
116
+ let esArchivoTest;
117
+ try {
118
+ ({ esArchivoTest } = require(path.join(__dirname, '..', 'hooks', 'calidad-pre-commit.js')));
119
+ } catch (_) {
120
+ esArchivoTest = (r) => /\.(test|spec)\.[jt]sx?$/.test(r) || /test_.*\.py$/.test(r);
121
+ }
122
+
123
+ let archivos = [];
124
+ try {
125
+ archivos = execFileSync('git', ['ls-files'], { cwd, encoding: 'utf8', timeout: 10000 })
126
+ .split('\n')
127
+ .map((l) => l.trim())
128
+ .filter(Boolean)
129
+ .filter(esArchivoTest);
130
+ } catch (_) {
131
+ return porReq;
132
+ }
133
+
134
+ for (const archivo of archivos) {
135
+ let contenido;
136
+ try {
137
+ contenido = fs.readFileSync(path.join(cwd, archivo), 'utf-8');
138
+ } catch (_) {
139
+ continue;
140
+ }
141
+ for (const m of contenido.matchAll(/verifica:\s*(REQ-\d{1,3})/g)) {
142
+ const req = m[1];
143
+ if (!porReq.has(req)) porReq.set(req, []);
144
+ if (!porReq.get(req).includes(archivo)) porReq.get(req).push(archivo);
145
+ }
146
+ }
147
+ return porReq;
148
+ }
149
+
150
+ // ─── Validación ─────────────────────────────────────────────────────────────
151
+
152
+ function validarFase({ cwd, fase, soloPlan }) {
153
+ const nn = String(Number(fase)).padStart(2, '0');
154
+ const contextoPath = path.join(cwd, '.planning', 'fases', `${nn}-CONTEXTO.md`);
155
+ if (!fs.existsSync(contextoPath)) {
156
+ return { error: `No existe ${path.relative(cwd, contextoPath)}`, exit: 2 };
157
+ }
158
+
159
+ const { activos, retirados, metodo } = extraerReqs(fs.readFileSync(contextoPath, 'utf-8'));
160
+ if (activos.length === 0) {
161
+ return {
162
+ fase: nn,
163
+ legacy: true,
164
+ nota: 'CONTEXTO sin REQ-IDs — trazabilidad no exigible (gracia legacy, fases pre-10)',
165
+ exit: 0,
166
+ };
167
+ }
168
+
169
+ const planPath = path.join(cwd, '.planning', 'fases', `${nn}-PLAN.md`);
170
+ const matrizTareas = fs.existsSync(planPath)
171
+ ? extraerMatrizPlan(fs.readFileSync(planPath, 'utf-8'))
172
+ : new Map();
173
+
174
+ const reqATareas = new Map(activos.map((r) => [r, []]));
175
+ const tareasSinReq = [];
176
+ for (const [tarea, reqs] of matrizTareas) {
177
+ if (reqs.length === 0) tareasSinReq.push(tarea);
178
+ for (const req of reqs) {
179
+ if (reqATareas.has(req)) reqATareas.get(req).push(tarea);
180
+ }
181
+ }
182
+
183
+ const commits = soloPlan ? new Map() : extraerCommitsConRefs(cwd);
184
+ const tests = soloPlan ? new Map() : extraerTestsConMarker(cwd);
185
+
186
+ const validaciones = {
187
+ req_sin_tarea: activos.filter((r) => (reqATareas.get(r) || []).length === 0),
188
+ req_sin_commit: soloPlan ? [] : activos.filter((r) => !(commits.get(r) || []).length),
189
+ req_sin_test: soloPlan
190
+ ? []
191
+ : activos.filter((r) => metodo[r] === 'test' && !(tests.get(r) || []).length),
192
+ tarea_sin_req: tareasSinReq, // informativo, no rompe
193
+ };
194
+
195
+ const trazabilidad = {};
196
+ for (const req of activos) {
197
+ trazabilidad[req] = {
198
+ metodo: metodo[req],
199
+ tareas: reqATareas.get(req) || [],
200
+ commits: commits.get(req) || [],
201
+ tests: tests.get(req) || [],
202
+ };
203
+ }
204
+
205
+ const huerfanos =
206
+ validaciones.req_sin_tarea.length +
207
+ validaciones.req_sin_commit.length +
208
+ validaciones.req_sin_test.length;
209
+
210
+ return {
211
+ fase: nn,
212
+ legacy: false,
213
+ soloPlan: Boolean(soloPlan),
214
+ reqs: activos,
215
+ retirados,
216
+ trazabilidad,
217
+ validaciones,
218
+ exit: huerfanos > 0 ? 1 : 0,
219
+ };
220
+ }
221
+
222
+ // ─── Reporte ────────────────────────────────────────────────────────────────
223
+
224
+ function imprimirReporte(r) {
225
+ if (r.error) {
226
+ console.error(`ERROR: ${r.error}`);
227
+ return;
228
+ }
229
+ if (r.legacy) {
230
+ console.log(`Fase ${r.fase}: ${r.nota}. Exit 0.`);
231
+ return;
232
+ }
233
+ console.log(`Trazabilidad REQ→T→commit→test — Fase ${r.fase}${r.soloPlan ? ' (--solo-plan: matriz REQ×T)' : ''}`);
234
+ console.log('');
235
+ for (const req of r.reqs) {
236
+ const t = r.trazabilidad[req];
237
+ const partes = [`tareas: ${t.tareas.join(', ') || '—'}`];
238
+ if (!r.soloPlan) {
239
+ partes.push(`commits: ${t.commits.join(', ') || '—'}`);
240
+ partes.push(t.metodo === 'inspeccion' ? 'verificación: inspección' : `tests: ${t.tests.join(', ') || '—'}`);
241
+ }
242
+ console.log(` ${req} → ${partes.join(' | ')}`);
243
+ }
244
+ if (r.retirados.length) console.log(` Retirados (no exigibles): ${r.retirados.join(', ')}`);
245
+ console.log('');
246
+
247
+ const v = r.validaciones;
248
+ const gaps = [];
249
+ if (v.req_sin_tarea.length) gaps.push(`REQ sin tarea: ${v.req_sin_tarea.join(', ')}`);
250
+ if (v.req_sin_commit.length) gaps.push(`REQ sin commit (footer Refs:): ${v.req_sin_commit.join(', ')}`);
251
+ if (v.req_sin_test.length) gaps.push(`REQ sin test (marker verifica:): ${v.req_sin_test.join(', ')}`);
252
+
253
+ if (gaps.length) {
254
+ console.log('HUÉRFANOS DETECTADOS:');
255
+ for (const g of gaps) console.log(` ✗ ${g}`);
256
+ } else {
257
+ console.log('✓ Cadena de trazabilidad completa: 0 huérfanos.');
258
+ }
259
+ if (v.tarea_sin_req.length) {
260
+ console.log(` (info) Tareas sin REQ declarado: ${v.tarea_sin_req.join(', ')} — válido para infraestructura/refactor`);
261
+ }
262
+ }
263
+
264
+ // ─── CLI ────────────────────────────────────────────────────────────────────
265
+
266
+ function main() {
267
+ const args = process.argv.slice(2);
268
+ const faseArg = args.find((a) => a.startsWith('--fase='));
269
+ if (!faseArg || !/^\d+$/.test(faseArg.split('=')[1] || '')) {
270
+ console.error('Uso: node scripts/verificar-trazabilidad.js --fase=N [--solo-plan] [--json]');
271
+ process.exit(2);
272
+ }
273
+
274
+ const resultado = validarFase({
275
+ cwd: process.cwd(),
276
+ fase: faseArg.split('=')[1],
277
+ soloPlan: args.includes('--solo-plan'),
278
+ });
279
+
280
+ if (args.includes('--json')) {
281
+ console.log(JSON.stringify(resultado, null, 2));
282
+ } else {
283
+ imprimirReporte(resultado);
284
+ }
285
+ process.exit(resultado.exit);
286
+ }
287
+
288
+ if (require.main === module) {
289
+ main();
290
+ }
291
+
292
+ module.exports = { extraerReqs, extraerMatrizPlan, extraerCommitsConRefs, extraerTestsConMarker, validarFase };