@saulwade/swl-ses 1.8.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 (135) hide show
  1. package/CLAUDE.md +8 -8
  2. package/README.md +13 -13
  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 +96 -8
  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/revisor-codigo-swl.md +34 -10
  16. package/agentes/revisor-seguridad-swl.md +7 -0
  17. package/agentes/tdd-qa-swl.md +39 -2
  18. package/comandos/swl/actualizar.md +1 -1
  19. package/comandos/swl/aprender.md +2 -2
  20. package/comandos/swl/aprobar-plan.md +152 -0
  21. package/comandos/swl/autoresearch.md +102 -6
  22. package/comandos/swl/ayuda.md +3 -3
  23. package/comandos/swl/discutir-fase.md +20 -2
  24. package/comandos/swl/ejecutar-fase.md +53 -6
  25. package/comandos/swl/evolucionar.md +1 -1
  26. package/comandos/swl/inbox.md +1 -1
  27. package/comandos/swl/instalar.md +1 -1
  28. package/comandos/swl/nemesis.md +42 -1
  29. package/comandos/swl/planear-fase.md +25 -1
  30. package/comandos/swl/plugins.md +1 -1
  31. package/comandos/swl/predecir.md +139 -0
  32. package/comandos/swl/release.md +1 -1
  33. package/comandos/swl/status.md +279 -0
  34. package/comandos/swl/verificar.md +75 -7
  35. package/habilidades/ai-runtime-security/SKILL.md +1 -1
  36. package/habilidades/angular-moderno/SKILL.md +44 -1
  37. package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
  38. package/habilidades/autoresearch/SKILL.md +15 -1
  39. package/habilidades/benchmark-memoria/SKILL.md +1 -1
  40. package/habilidades/calidad-contract-testing/SKILL.md +165 -0
  41. package/habilidades/calidad-mutation-testing/SKILL.md +170 -0
  42. package/habilidades/changelog-generator/SKILL.md +9 -2
  43. package/habilidades/changelog-generator/scripts/parse-commits.js +12 -1
  44. package/habilidades/checklist-seguridad/SKILL.md +29 -1
  45. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -0
  46. package/habilidades/css-moderno/SKILL.md +3 -1
  47. package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
  48. package/habilidades/drift-detection/SKILL.md +179 -179
  49. package/habilidades/ejecutar-fase/SKILL.md +64 -14
  50. package/habilidades/estructura-proyecto-claude/SKILL.md +17 -14
  51. package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +34 -23
  52. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +70 -53
  53. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -77
  54. package/habilidades/extractor-de-aprendizajes/SKILL.md +9 -5
  55. package/habilidades/fastapi-experto/SKILL.md +56 -5
  56. package/habilidades/harness-claude-code/SKILL.md +10 -7
  57. package/{reglas/harness-claude-code.md → habilidades/harness-claude-code/recursos/disciplina-harness-regla.md} +2 -2
  58. package/habilidades/instalar-sistema/SKILL.md +3 -3
  59. package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +1 -1
  60. package/habilidades/patrones-python/SKILL.md +8 -5
  61. package/habilidades/perfil-usuario/SKILL.md +200 -200
  62. package/habilidades/planear-fase/SKILL.md +25 -4
  63. package/habilidades/proceso-ddia-fundamentos/SKILL.md +1 -1
  64. package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
  65. package/habilidades/proceso-debate-adversarial/SKILL.md +164 -0
  66. package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -0
  67. package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -0
  68. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -0
  69. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -0
  70. package/habilidades/protocolo-revision-swl/SKILL.md +1 -1
  71. package/habilidades/seguridad-skills-ia/SKILL.md +1 -1
  72. package/habilidades/swl-claudemd/SKILL.md +50 -210
  73. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -0
  74. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -0
  75. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -0
  76. package/habilidades/swl-dashboard/SKILL.md +9 -9
  77. package/habilidades/swl-revisar-impacto/SKILL.md +1 -1
  78. package/habilidades/tdd-workflow/SKILL.md +58 -5
  79. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -0
  80. package/habilidades/validacion-ci-sistema/SKILL.md +3 -3
  81. package/hooks/calidad-pre-commit.js +340 -3
  82. package/hooks/ciclo-evolucion-subagente.js +26 -0
  83. package/hooks/ciclo-evolucion.js +26 -0
  84. package/hooks/contexto-iteracion.js +144 -0
  85. package/hooks/extraccion-aprendizajes.js +13 -0
  86. package/hooks/lib/ciclo-evolucion.js +47 -0
  87. package/hooks/{auto-evolucion.js → lib/etapa-auto-evolucion.js} +701 -700
  88. package/hooks/{metricas-evolucion.js → lib/etapa-metricas.js} +388 -376
  89. package/hooks/{actualizar-perfil-usuario.js → lib/etapa-perfil-usuario.js} +376 -364
  90. package/hooks/lib/evolution-tracker.js +24 -3
  91. package/hooks/lib/loop-telemetry.js +321 -0
  92. package/hooks/notificacion-telegram.js +11 -3
  93. package/hooks/spec-gate.js +211 -0
  94. package/hooks/tdd-gate.js +241 -0
  95. package/hooks/validar-intent-spec.js +30 -10
  96. package/llms.txt +29 -0
  97. package/manifiestos/hooks-config.json +36 -18
  98. package/manifiestos/modulos.json +23 -14
  99. package/manifiestos/skills-lock.json +100 -72
  100. package/package.json +4 -3
  101. package/plugin.json +9 -10
  102. package/reglas/accesibilidad.md +10 -0
  103. package/reglas/api-diseno.md +9 -0
  104. package/reglas/arquitectura.evolved.json +7 -0
  105. package/reglas/arquitectura.md +65 -0
  106. package/reglas/auditorias-documentales-estructurales.md +7 -0
  107. package/reglas/cloud-infra.md +8 -0
  108. package/reglas/fragmentos-compartidos.md +5 -0
  109. package/reglas/gobernanza.md +4 -4
  110. package/reglas/hooks.md +6 -0
  111. package/reglas/intent-engineering.md +4 -0
  112. package/reglas/markitdown.md +8 -0
  113. package/reglas/memoria-consolidada.md +1 -1
  114. package/reglas/patrones.md +6 -0
  115. package/reglas/registro-componentes-nuevos.md +10 -1
  116. package/reglas/seguridad-agentes.md +1 -1
  117. package/reglas/seguridad.evolved.json +7 -0
  118. package/reglas/seguridad.md +144 -0
  119. package/reglas/skills-estandar.md +6 -0
  120. package/reglas/testing.md +7 -0
  121. package/reglas/tests-cleanup.md +4 -0
  122. package/reglas/usar-sistema-swl.md +1 -1
  123. package/scripts/generar-inventario.js +64 -1
  124. package/scripts/instalador.js +32 -2
  125. package/scripts/lib/gitignore-manifest.js +29 -1
  126. package/scripts/lib/plan-lock.js +275 -0
  127. package/scripts/migrar-fase-dominio.js +0 -1
  128. package/scripts/smoke-test.js +24 -2
  129. package/scripts/verificar-trazabilidad.js +292 -0
  130. package/agentes/ux-disenador-swl.md +0 -503
  131. package/comandos/swl/dashboard.md +0 -146
  132. package/comandos/swl/evolucion-estado.md +0 -191
  133. package/comandos/swl/metricas.md +0 -342
  134. package/comandos/swl/salud.md +0 -481
  135. package/reglas/verificar-citas-temporales.md +0 -139
@@ -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) {
@@ -161,7 +161,8 @@ function verificarArchivosEnDestino(destino, archivosEsperados) {
161
161
 
162
162
  function faseInstall(tmp, home) {
163
163
  log("\n[FASE 1/3] install --profile completo --global --all-langs");
164
- // --all-langs evita el filtrado por stack detectado (tmp está vacío, no detectaría lenguajes).
164
+ // --all-langs evita el filtrado por stack; en scope GLOBAL las reglas por-lenguaje
165
+ // se excluyen SIEMPRE (viven project-scoped) para no saturar el contexto de subagentes.
165
166
  const r = correrCli(
166
167
  [
167
168
  "install",
@@ -199,9 +200,14 @@ function faseInstall(tmp, home) {
199
200
  // Verificar que todos los archivos esperados se copiaron
200
201
  const destino = path.join(home, ".claude");
201
202
  const esperados = archivosEsperadosEnPerfil("completo");
203
+ // Install --global: las reglas por-lenguaje NO se instalan en scope global
204
+ // (se instalan project-scoped, filtradas por stack). Excluirlas de lo esperado.
205
+ const esperadosGlobal = new Set(
206
+ [...esperados].filter((rel) => !rel.startsWith("reglas/lenguajes/")),
207
+ );
202
208
  const { faltantes, totalEsperados } = verificarArchivosEnDestino(
203
209
  destino,
204
- esperados,
210
+ esperadosGlobal,
205
211
  );
206
212
 
207
213
  if (faltantes.length > 0) {
@@ -216,6 +222,22 @@ function faseInstall(tmp, home) {
216
222
  log(
217
223
  ` [OK] ${totalEsperados}/${totalEsperados} archivos presentes en el destino`,
218
224
  );
225
+
226
+ // Verificación del fix anti-thrashing (Opción B): ninguna regla por-lenguaje
227
+ // debe existir en ~/.claude/rules/{LANG}/ tras un install global — saturarían
228
+ // la ventana de todo subagente que herede ~/.claude/rules/.
229
+ const reglasGlobalDir = path.join(destino, "rules");
230
+ const LANGS_REGLAS = ["csharp", "go", "java", "kotlin", "nextjs", "php", "rust", "swift"];
231
+ const langsPresentes = LANGS_REGLAS.filter((l) =>
232
+ fs.existsSync(path.join(reglasGlobalDir, l)),
233
+ );
234
+ if (langsPresentes.length > 0) {
235
+ log(
236
+ ` [FALLA] reglas por-lenguaje presentes en scope global (deberían excluirse): ${langsPresentes.join(", ")}`,
237
+ );
238
+ return { ok: false };
239
+ }
240
+ log(" [OK] reglas por-lenguaje correctamente excluidas del scope global");
219
241
  return { ok: true };
220
242
  }
221
243
 
@@ -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 };