@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.
- package/CLAUDE.md +8 -8
- package/README.md +13 -13
- 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 +96 -8
- 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/revisor-codigo-swl.md +34 -10
- package/agentes/revisor-seguridad-swl.md +7 -0
- package/agentes/tdd-qa-swl.md +39 -2
- 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/autoresearch.md +102 -6
- 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 +42 -1
- package/comandos/swl/planear-fase.md +25 -1
- package/comandos/swl/plugins.md +1 -1
- package/comandos/swl/predecir.md +139 -0
- package/comandos/swl/release.md +1 -1
- package/comandos/swl/status.md +279 -0
- package/comandos/swl/verificar.md +75 -7
- package/habilidades/ai-runtime-security/SKILL.md +1 -1
- package/habilidades/angular-moderno/SKILL.md +44 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
- package/habilidades/autoresearch/SKILL.md +15 -1
- package/habilidades/benchmark-memoria/SKILL.md +1 -1
- package/habilidades/calidad-contract-testing/SKILL.md +165 -0
- package/habilidades/calidad-mutation-testing/SKILL.md +170 -0
- package/habilidades/changelog-generator/SKILL.md +9 -2
- package/habilidades/changelog-generator/scripts/parse-commits.js +12 -1
- package/habilidades/checklist-seguridad/SKILL.md +29 -1
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -0
- package/habilidades/css-moderno/SKILL.md +3 -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/fastapi-experto/SKILL.md +56 -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/patrones-python/SKILL.md +8 -5
- 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 +164 -0
- package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -0
- package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -0
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -0
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -0
- 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 +58 -5
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -0
- 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/contexto-iteracion.js +144 -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/lib/loop-telemetry.js +321 -0
- package/hooks/notificacion-telegram.js +11 -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 +29 -0
- package/manifiestos/hooks-config.json +36 -18
- package/manifiestos/modulos.json +23 -14
- package/manifiestos/skills-lock.json +100 -72
- package/package.json +4 -3
- package/plugin.json +9 -10
- package/reglas/accesibilidad.md +10 -0
- package/reglas/api-diseno.md +9 -0
- package/reglas/arquitectura.evolved.json +7 -0
- package/reglas/arquitectura.md +65 -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/seguridad.evolved.json +7 -0
- package/reglas/seguridad.md +144 -0
- 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/generar-inventario.js +64 -1
- package/scripts/instalador.js +32 -2
- 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/smoke-test.js +24 -2
- 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 -342
- package/comandos/swl/salud.md +0 -481
- 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
|
+
};
|
package/scripts/smoke-test.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 };
|