@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.
- package/CLAUDE.md +8 -8
- package/README.md +12 -12
- package/agentes/accesibilidad-wcag-swl.md +3 -3
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/disenador-ui-swl.md +6 -5
- package/agentes/frontend-angular-swl.md +2 -2
- package/agentes/frontend-css-swl.md +2 -2
- package/agentes/frontend-react-swl.md +4 -4
- package/agentes/frontend-swl.md +6 -6
- package/agentes/investigador-ux-swl.md +5 -5
- package/agentes/orquestador-swl.md +7 -7
- package/agentes/perfilador-usuario-swl.md +308 -308
- package/agentes/producto-prd-swl.md +1 -1
- package/agentes/red-team-swl.md +218 -218
- package/agentes/tdd-qa-swl.md +17 -1
- package/comandos/swl/actualizar.md +1 -1
- package/comandos/swl/aprender.md +2 -2
- package/comandos/swl/aprobar-plan.md +152 -0
- package/comandos/swl/ayuda.md +3 -3
- package/comandos/swl/discutir-fase.md +20 -2
- package/comandos/swl/ejecutar-fase.md +53 -6
- package/comandos/swl/evolucionar.md +1 -1
- package/comandos/swl/inbox.md +1 -1
- package/comandos/swl/instalar.md +1 -1
- package/comandos/swl/nemesis.md +1 -1
- package/comandos/swl/planear-fase.md +17 -1
- package/comandos/swl/plugins.md +1 -1
- package/comandos/swl/release.md +1 -1
- package/comandos/swl/status.md +279 -0
- package/comandos/swl/verificar.md +26 -1
- package/habilidades/ai-runtime-security/SKILL.md +1 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
- package/habilidades/benchmark-memoria/SKILL.md +1 -1
- package/habilidades/calidad-contract-testing/SKILL.md +165 -0
- package/habilidades/changelog-generator/SKILL.md +9 -2
- package/habilidades/changelog-generator/scripts/parse-commits.js +11 -1
- package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
- package/habilidades/drift-detection/SKILL.md +179 -179
- package/habilidades/ejecutar-fase/SKILL.md +64 -14
- package/habilidades/estructura-proyecto-claude/SKILL.md +17 -14
- package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +34 -23
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +70 -53
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -77
- package/habilidades/extractor-de-aprendizajes/SKILL.md +9 -5
- package/habilidades/harness-claude-code/SKILL.md +10 -7
- package/{reglas/harness-claude-code.md → habilidades/harness-claude-code/recursos/disciplina-harness-regla.md} +2 -2
- package/habilidades/instalar-sistema/SKILL.md +3 -3
- package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +1 -1
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/planear-fase/SKILL.md +25 -4
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +1 -1
- package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
- package/habilidades/proceso-debate-adversarial/SKILL.md +2 -2
- package/habilidades/protocolo-revision-swl/SKILL.md +1 -1
- package/habilidades/seguridad-skills-ia/SKILL.md +1 -1
- package/habilidades/swl-claudemd/SKILL.md +50 -210
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -0
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -0
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -0
- package/habilidades/swl-dashboard/SKILL.md +9 -9
- package/habilidades/swl-revisar-impacto/SKILL.md +1 -1
- package/habilidades/tdd-workflow/SKILL.md +45 -5
- package/habilidades/validacion-ci-sistema/SKILL.md +3 -3
- package/hooks/calidad-pre-commit.js +340 -3
- package/hooks/ciclo-evolucion-subagente.js +26 -0
- package/hooks/ciclo-evolucion.js +26 -0
- package/hooks/extraccion-aprendizajes.js +13 -0
- package/hooks/lib/ciclo-evolucion.js +47 -0
- package/hooks/{auto-evolucion.js → lib/etapa-auto-evolucion.js} +701 -700
- package/hooks/{metricas-evolucion.js → lib/etapa-metricas.js} +388 -376
- package/hooks/{actualizar-perfil-usuario.js → lib/etapa-perfil-usuario.js} +376 -364
- package/hooks/lib/evolution-tracker.js +24 -3
- package/hooks/spec-gate.js +211 -0
- package/hooks/tdd-gate.js +241 -0
- package/hooks/validar-intent-spec.js +30 -10
- package/llms.txt +6 -6
- package/manifiestos/hooks-config.json +26 -17
- package/manifiestos/modulos.json +17 -14
- package/manifiestos/skills-lock.json +63 -56
- package/package.json +2 -2
- package/plugin.json +6 -10
- package/reglas/accesibilidad.md +10 -0
- package/reglas/api-diseno.md +9 -0
- package/reglas/auditorias-documentales-estructurales.md +7 -0
- package/reglas/cloud-infra.md +8 -0
- package/reglas/fragmentos-compartidos.md +5 -0
- package/reglas/gobernanza.md +4 -4
- package/reglas/hooks.md +6 -0
- package/reglas/intent-engineering.md +4 -0
- package/reglas/markitdown.md +8 -0
- package/reglas/memoria-consolidada.md +1 -1
- package/reglas/patrones.md +6 -0
- package/reglas/registro-componentes-nuevos.md +10 -1
- package/reglas/seguridad-agentes.md +1 -1
- package/reglas/skills-estandar.md +6 -0
- package/reglas/testing.md +7 -0
- package/reglas/tests-cleanup.md +4 -0
- package/reglas/usar-sistema-swl.md +1 -1
- package/scripts/lib/gitignore-manifest.js +29 -1
- package/scripts/lib/plan-lock.js +275 -0
- package/scripts/migrar-fase-dominio.js +0 -1
- package/scripts/verificar-trazabilidad.js +292 -0
- package/agentes/ux-disenador-swl.md +0 -503
- package/comandos/swl/dashboard.md +0 -146
- package/comandos/swl/evolucion-estado.md +0 -191
- package/comandos/swl/metricas.md +0 -376
- package/comandos/swl/salud.md +0 -481
- package/reglas/verificar-citas-temporales.md +0 -139
package/reglas/testing.md
CHANGED
package/reglas/tests-cleanup.md
CHANGED
|
@@ -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
|
-
|
|
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
|
+
};
|
|
@@ -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 };
|