@saulwade/swl-ses 2.1.0 → 2.2.1
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 +199 -196
- package/README.md +597 -579
- package/agentes/arquitecto-swl.md +0 -5
- package/agentes/backend-python-swl.md +0 -5
- package/agentes/implementador-swl.md +0 -5
- package/agentes/nemesis-auditor-swl.md +0 -5
- package/agentes/orquestador-swl.md +0 -5
- package/agentes/planificador-swl.md +0 -5
- package/agentes/revisor-codigo-swl.md +0 -5
- package/bin/swl-ses.js +63 -0
- package/comandos/swl/adoptar-proyecto.md +12 -14
- package/comandos/swl/aprender.md +30 -47
- package/comandos/swl/aprobar-plan.md +23 -35
- package/comandos/swl/autoresearch.md +12 -14
- package/comandos/swl/briefing.md +5 -8
- package/comandos/swl/checkpoint.md +10 -15
- package/comandos/swl/claudemd.md +12 -12
- package/comandos/swl/configurar-ci.md +20 -19
- package/comandos/swl/cron.md +10 -12
- package/comandos/swl/ejecutar-fase.md +10 -8
- package/comandos/swl/evolucionar.md +6 -11
- package/comandos/swl/inbox.md +10 -10
- package/comandos/swl/modelo.md +7 -9
- package/comandos/swl/notificaciones.md +19 -116
- package/comandos/swl/nuevo-proyecto.md +9 -14
- package/comandos/swl/release.md +19 -5
- package/comandos/swl/revisar-impacto.md +0 -5
- package/comandos/swl/status.md +333 -348
- package/comandos/swl/verificar.md +817 -813
- package/habilidades/agent-browser/SKILL.md +0 -5
- package/habilidades/angular-moderno/SKILL.md +0 -5
- package/habilidades/api-rest-diseno/SKILL.md +0 -5
- package/habilidades/aprendizaje-continuo/SKILL.md +0 -5
- package/habilidades/auth-patrones/SKILL.md +0 -5
- package/habilidades/build-errors-nextjs/SKILL.md +0 -5
- package/habilidades/changelog-generator/SKILL.md +174 -179
- package/habilidades/checklist-seguridad/SKILL.md +0 -5
- package/habilidades/contenedores-docker/SKILL.md +0 -5
- package/habilidades/datos-etl/SKILL.md +0 -5
- package/habilidades/doc-sync/SKILL.md +0 -5
- package/habilidades/extractor-de-aprendizajes/SKILL.md +0 -5
- package/habilidades/fastapi-experto/SKILL.md +0 -5
- package/habilidades/frontend-avanzado/SKILL.md +0 -5
- package/habilidades/iam-secretos/SKILL.md +0 -5
- package/habilidades/manejo-errores/SKILL.md +0 -5
- package/habilidades/mapear-codebase/SKILL.md +0 -5
- package/habilidades/meta-skills-estandar/SKILL.md +0 -5
- package/habilidades/monitoring-alertas/SKILL.md +0 -5
- package/habilidades/nextjs-experto/SKILL.md +0 -5
- package/habilidades/nextjs-testing/SKILL.md +0 -5
- package/habilidades/node-experto/SKILL.md +0 -5
- package/habilidades/orquestacion-async/SKILL.md +0 -5
- package/habilidades/patrones-python/SKILL.md +227 -232
- package/habilidades/planear-fase/SKILL.md +336 -341
- package/habilidades/postgresql-experto/SKILL.md +0 -5
- package/habilidades/prevencion-sobreingenieria/SKILL.md +0 -5
- package/habilidades/protocolo-revision-swl/SKILL.md +0 -5
- package/habilidades/react-experto/SKILL.md +0 -5
- package/habilidades/release-semver/SKILL.md +0 -5
- package/habilidades/swl-claudemd/SKILL.md +10 -11
- package/habilidades/tdd-workflow/SKILL.md +710 -715
- package/habilidades/testing-python/SKILL.md +335 -340
- package/habilidades/verificar-trabajo/SKILL.md +0 -5
- package/hooks/lib/evolution-tracker.js +191 -35
- package/hooks/lib/propose-step.js +1 -0
- package/llms.txt +1 -1
- package/manifiestos/canonical-hashes.json +656 -0
- package/manifiestos/modulos.json +3 -0
- package/manifiestos/skills-lock.json +71 -71
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/auditar-claudemd.js +38 -0
- package/scripts/cli/aprobar-plan.js +73 -0
- package/scripts/cli/briefing.js +23 -0
- package/scripts/cli/ciclo-evolucion.js +26 -0
- package/scripts/cli/configurar-ci.js +40 -0
- package/scripts/cli/derivar-feature-list.js +25 -0
- package/scripts/cli/detectar-host.js +27 -0
- package/scripts/cli/diary-entry.js +69 -0
- package/scripts/cli/execution-state.js +18 -0
- package/scripts/cli/gateway-notify.js +41 -0
- package/scripts/cli/liberar-fase.js +42 -0
- package/scripts/cli/loop-telemetry.js +125 -0
- package/scripts/cli/mark-evolved.js +56 -0
- package/scripts/cli/metricas-dora.js +26 -0
- package/scripts/cli/near-duplicate.js +55 -0
- package/scripts/cli/notificaciones.js +123 -0
- package/scripts/cli/propose-step.js +29 -0
- package/scripts/cli/schedule-parse.js +19 -0
- package/scripts/cli/sugerir-modelo.js +20 -0
- package/scripts/cli/verificar-plan.js +36 -0
- package/scripts/cli/verificar-trazabilidad.js +35 -0
- package/scripts/derivar-feature-list.js +1 -0
- package/scripts/generar-canonical-hashes.js +147 -0
- package/scripts/instalador.js +126 -53
- package/scripts/lib/audit-evolved.js +71 -0
- package/scripts/lib/auditar-invocaciones-comandos.js +104 -0
- package/scripts/lib/canonical-hash.js +94 -0
- package/scripts/lib/evolved-fuente.js +138 -0
- package/scripts/lib/resolver-plan-fase.js +37 -0
- package/scripts/remediar-evolved-instaladas.js +239 -0
- package/scripts/validar.js +27 -0
- package/scripts/verificar-evolucion.js +36 -0
- package/scripts/verificar-release.js +33 -0
- package/scripts/verificar-trazabilidad.js +1 -1
- package/agentes/.evolved.json +0 -9
- package/comandos/swl/.evolved.json +0 -23
- package/habilidades/auth-patrones/.evolved.json +0 -9
- package/habilidades/extractor-de-aprendizajes/.evolved.json +0 -9
- package/habilidades/instalar-sistema/.evolved.json +0 -9
- package/habilidades/manejo-errores/.evolved.json +0 -9
- package/habilidades/node-experto/.evolved.json +0 -9
- package/habilidades/release-semver/.evolved.json +0 -9
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* canonical-hash — Hash determinista del "cuerpo canónico" de un componente SWL.
|
|
5
|
+
*
|
|
6
|
+
* El discriminador A/B de la Fase 16 necesita distinguir:
|
|
7
|
+
* - Población A: evolución genuina del usuario (cuerpo modificado tras recibir
|
|
8
|
+
* el componente del paquete) → debe MERGE, jamás overwrite.
|
|
9
|
+
* - Población B: shipped-evolved de fábrica (frontmatter `evolved:*` espurio
|
|
10
|
+
* puesto por el repo madre, cuerpo idéntico al canónico) → actualizable.
|
|
11
|
+
*
|
|
12
|
+
* La señal es el cuerpo SIN los campos `evolved-*`: si el cuerpo instalado
|
|
13
|
+
* hashea igual al cuerpo canónico de la versión `evolved-from`, el usuario NO
|
|
14
|
+
* lo tocó (es B). Si difiere, hubo mutación real del usuario (es A).
|
|
15
|
+
*
|
|
16
|
+
* El hash ignora los campos `evolved-*` del frontmatter (que el shipping mete y
|
|
17
|
+
* quita) y normaliza CRLF→LF para ser estable cross-OS (el usuario corre
|
|
18
|
+
* Windows; el CI/manifiesto se genera en cualquier plataforma).
|
|
19
|
+
*
|
|
20
|
+
* Zero-deps: solo `crypto` nativo. Patrón de hashing precedente en
|
|
21
|
+
* `hooks/lib/merkle-audit.js` y `scripts/lib/plan-lock.js`.
|
|
22
|
+
*
|
|
23
|
+
* @module scripts/lib/canonical-hash
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const { createHash } = require('crypto');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normaliza el contenido a su "cuerpo canónico":
|
|
30
|
+
* - Elimina toda línea de frontmatter `evolved*:` (`evolved:`, `evolved-from:`,
|
|
31
|
+
* `evolved-at:`, `evolved-by:`, `evolved-rounds:`, `evolved-score:`,
|
|
32
|
+
* `evolved-note:`, `evolved-origin:`).
|
|
33
|
+
* - Normaliza line endings CRLF/CR → LF.
|
|
34
|
+
* - Recorta whitespace final (un solo `\n` al cierre) para que diferencias de
|
|
35
|
+
* EOF no cambien el hash.
|
|
36
|
+
*
|
|
37
|
+
* El filtro `^evolved[-\w]*:` se aplica EXCLUSIVAMENTE al bloque frontmatter.
|
|
38
|
+
* Aplicarlo a todo el archivo (bug detectado en auditoría nemesis Fase 16)
|
|
39
|
+
* borraría líneas legítimas del body que empiecen con `evolved-*` (p. ej.
|
|
40
|
+
* `aprender.md`/`evolucionar.md` documentan esos campos en su cuerpo), lo que
|
|
41
|
+
* causaba un FALSO POSITIVO en el discriminador A/B: el cuerpo del usuario
|
|
42
|
+
* hasheaba igual al baseline → se clasificaba como shipped → overwrite de
|
|
43
|
+
* evolución del usuario (violación del invariante merge-no-overwrite).
|
|
44
|
+
*
|
|
45
|
+
* Normaliza CRLF/CR → LF (estable cross-OS) antes de hashear.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} content
|
|
48
|
+
* @returns {string} cuerpo canónico normalizado
|
|
49
|
+
*/
|
|
50
|
+
function canonicalBody(content) {
|
|
51
|
+
const norm = String(content).replace(/\r\n|\r/g, '\n');
|
|
52
|
+
// Separar frontmatter (entre los primeros dos `---`) del body.
|
|
53
|
+
const m = norm.match(/^(---\n[\s\S]*?\n---\n?)([\s\S]*)$/);
|
|
54
|
+
if (!m) {
|
|
55
|
+
// Sin frontmatter: el contenido completo es body, no se filtra nada.
|
|
56
|
+
return norm.replace(/\s+$/, '') + '\n';
|
|
57
|
+
}
|
|
58
|
+
const fmFiltrado = m[1]
|
|
59
|
+
.split('\n')
|
|
60
|
+
.filter((line) => !/^evolved[-\w]*:/i.test(line)) // flag i: simétrico con la detección
|
|
61
|
+
.join('\n');
|
|
62
|
+
return (fmFiltrado + m[2]).replace(/\s+$/, '') + '\n';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* SHA256 hex (64 chars) del cuerpo canónico de un componente.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} content - Contenido completo del archivo `.md`.
|
|
69
|
+
* @returns {string} hash hex de 64 caracteres
|
|
70
|
+
*/
|
|
71
|
+
function canonicalHash(content) {
|
|
72
|
+
return createHash('sha256').update(canonicalBody(content), 'utf8').digest('hex');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Decide si el contenido local corresponde al cuerpo canónico shipped de una
|
|
77
|
+
* versión dada (= NO fue tocado por el usuario = población B).
|
|
78
|
+
*
|
|
79
|
+
* @param {object} manifiesto - `{ version: { rutaRel: sha256 } }`
|
|
80
|
+
* @param {string} versionPrev - Versión base (típicamente `evolved-from`).
|
|
81
|
+
* @param {string} rutaRel - Ruta relativa del componente (ej. `agentes/X-swl.md`).
|
|
82
|
+
* @param {string} contenidoLocal- Contenido completo del archivo instalado.
|
|
83
|
+
* @returns {boolean} true si el hash local coincide con el baseline de esa versión.
|
|
84
|
+
*/
|
|
85
|
+
function hashCoincide(manifiesto, versionPrev, rutaRel, contenidoLocal) {
|
|
86
|
+
if (!manifiesto || typeof manifiesto !== 'object') return false;
|
|
87
|
+
const porVersion = manifiesto[versionPrev];
|
|
88
|
+
if (!porVersion || typeof porVersion !== 'object') return false;
|
|
89
|
+
const esperado = porVersion[rutaRel];
|
|
90
|
+
if (!esperado) return false;
|
|
91
|
+
return canonicalHash(contenidoLocal) === esperado;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { canonicalBody, canonicalHash, hashCoincide };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* evolved-fuente — Higiene de marcadores de evolución en el repo madre (Fase 16).
|
|
5
|
+
*
|
|
6
|
+
* Invariante del sistema: `evolved: true` (frontmatter o sidecar `.evolved.json`)
|
|
7
|
+
* significa "este componente fue modificado por un USUARIO tras recibirlo del
|
|
8
|
+
* paquete". En el repo madre (fuente) NO debe existir ningún marcador evolved —
|
|
9
|
+
* los cambios del mantenedor se rastrean por git + bump de `version`. Un marcador
|
|
10
|
+
* evolved en el fuente es "shipped-evolved" espurio: confunde al discriminador
|
|
11
|
+
* A/B del instalador y congela el componente en las máquinas de los usuarios.
|
|
12
|
+
*
|
|
13
|
+
* Esta lib:
|
|
14
|
+
* - `listarOfensores(raiz)`: detecta marcadores evolved en el fuente (gate inverso).
|
|
15
|
+
* - `limpiar(raiz, opts)`: elimina los marcadores (frontmatter evolved-* y
|
|
16
|
+
* sidecars), preservando el body intacto.
|
|
17
|
+
*
|
|
18
|
+
* Dominios cubiertos: agentes/, habilidades/, comandos/, reglas/.
|
|
19
|
+
* Zero-deps. CRLF-safe.
|
|
20
|
+
*
|
|
21
|
+
* @module scripts/lib/evolved-fuente
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
let atomicWriteSync;
|
|
28
|
+
try {
|
|
29
|
+
({ atomicWriteSync } = require('../../hooks/lib/atomic-write'));
|
|
30
|
+
} catch {
|
|
31
|
+
atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DOMINIOS = ['agentes', 'habilidades', 'comandos', 'reglas'];
|
|
35
|
+
|
|
36
|
+
function _walk(dir, pred, out) {
|
|
37
|
+
if (!fs.existsSync(dir)) return;
|
|
38
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
39
|
+
const p = path.join(dir, entry.name);
|
|
40
|
+
if (entry.isDirectory()) _walk(p, pred, out);
|
|
41
|
+
else if (pred(entry.name)) out.push(p);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _rel(raiz, abs) {
|
|
46
|
+
return path.relative(raiz, abs).replace(/\\/g, '/');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** ¿El frontmatter (primer bloque ---) declara evolved: true|yes? */
|
|
50
|
+
function _frontmatterTieneEvolved(content) {
|
|
51
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
52
|
+
if (!m) return false;
|
|
53
|
+
return /^evolved:\s*(true|yes)\b/mi.test(m[1]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Lista los marcadores evolved presentes en el fuente.
|
|
58
|
+
* @param {string} [raiz=process.cwd()]
|
|
59
|
+
* @returns {{ frontmatter: string[], sidecars: string[] }}
|
|
60
|
+
*/
|
|
61
|
+
function listarOfensores(raiz) {
|
|
62
|
+
raiz = raiz || process.cwd();
|
|
63
|
+
const frontmatter = [];
|
|
64
|
+
const sidecars = [];
|
|
65
|
+
|
|
66
|
+
for (const d of DOMINIOS) {
|
|
67
|
+
const base = path.join(raiz, d);
|
|
68
|
+
|
|
69
|
+
const mds = [];
|
|
70
|
+
_walk(base, (name) => name.endsWith('.md'), mds);
|
|
71
|
+
for (const abs of mds) {
|
|
72
|
+
try {
|
|
73
|
+
if (_frontmatterTieneEvolved(fs.readFileSync(abs, 'utf8'))) frontmatter.push(_rel(raiz, abs));
|
|
74
|
+
} catch { /* ilegible: lo ignora el gate, lo verá el linter de IO */ }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const sc = [];
|
|
78
|
+
_walk(base, (name) => name === '.evolved.json', sc);
|
|
79
|
+
for (const abs of sc) sidecars.push(_rel(raiz, abs));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
frontmatter: frontmatter.sort(),
|
|
84
|
+
sidecars: sidecars.sort(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Elimina las líneas `evolved-*` SOLO del bloque frontmatter, preservando el
|
|
90
|
+
* body (que puede tener líneas `evolved-*` legítimas como documentación, p. ej.
|
|
91
|
+
* aprender.md / evolucionar.md).
|
|
92
|
+
* @param {string} content
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
function limpiarFrontmatter(content) {
|
|
96
|
+
const m = content.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
|
|
97
|
+
if (!m) return content;
|
|
98
|
+
const eol = m[1].includes('\r\n') ? '\r\n' : '\n';
|
|
99
|
+
const fmLimpio = m[2]
|
|
100
|
+
.split(/\r?\n/)
|
|
101
|
+
// flag `i`: simétrico con la detección case-insensitive de
|
|
102
|
+
// _frontmatterTieneEvolved. Sin él, `Evolved: true` se detectaría como
|
|
103
|
+
// ofensor pero no se limpiaría → --fix inútil (nemesis O4).
|
|
104
|
+
.filter((line) => !/^evolved[-\w]*:/i.test(line))
|
|
105
|
+
.join(eol);
|
|
106
|
+
return m[1] + fmLimpio + m[3] + content.slice(m[0].length);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Limpia todos los marcadores evolved del fuente.
|
|
111
|
+
* @param {string} [raiz=process.cwd()]
|
|
112
|
+
* @param {{ dryRun?: boolean }} [opts]
|
|
113
|
+
* @returns {Array<{ tipo: string, archivo: string }>} acciones realizadas/propuestas
|
|
114
|
+
*/
|
|
115
|
+
function limpiar(raiz, { dryRun = false } = {}) {
|
|
116
|
+
raiz = raiz || process.cwd();
|
|
117
|
+
const { frontmatter, sidecars } = listarOfensores(raiz);
|
|
118
|
+
const acciones = [];
|
|
119
|
+
|
|
120
|
+
for (const rel of frontmatter) {
|
|
121
|
+
const abs = path.join(raiz, rel);
|
|
122
|
+
const orig = fs.readFileSync(abs, 'utf8');
|
|
123
|
+
const limpio = limpiarFrontmatter(orig);
|
|
124
|
+
if (limpio !== orig) {
|
|
125
|
+
acciones.push({ tipo: 'strip-frontmatter', archivo: rel });
|
|
126
|
+
if (!dryRun) atomicWriteSync(abs, limpio, 'utf8');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const rel of sidecars) {
|
|
131
|
+
acciones.push({ tipo: 'rm-sidecar', archivo: rel });
|
|
132
|
+
if (!dryRun) fs.unlinkSync(path.join(raiz, rel));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return acciones;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { listarOfensores, limpiar, limpiarFrontmatter, DOMINIOS };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* resolver-plan-fase.js
|
|
5
|
+
*
|
|
6
|
+
* Resuelve la ruta del PLAN.md de una fase a partir de las opciones del CLI.
|
|
7
|
+
* Compartido por los subcomandos `aprobar-plan` y `verificar-plan`.
|
|
8
|
+
*
|
|
9
|
+
* Acepta `--plan=<ruta>` explícito o `--fase=N` (deriva
|
|
10
|
+
* `.planning/fases/0N-PLAN.md`, con fallback sin cero a la izquierda).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{plan?:string, fase?:(string|number|boolean)}} [opciones]
|
|
18
|
+
* @returns {string|null} ruta al PLAN, o null si no se pudo derivar.
|
|
19
|
+
*/
|
|
20
|
+
function resolverPlanPath(opciones = {}) {
|
|
21
|
+
if (opciones && opciones.plan && typeof opciones.plan === 'string') {
|
|
22
|
+
return opciones.plan;
|
|
23
|
+
}
|
|
24
|
+
const fase = opciones ? opciones.fase : undefined;
|
|
25
|
+
if (fase == null || fase === true) return null;
|
|
26
|
+
const n = String(fase).padStart(2, '0');
|
|
27
|
+
const candidatos = [
|
|
28
|
+
path.join('.planning', 'fases', `${n}-PLAN.md`),
|
|
29
|
+
path.join('.planning', 'fases', `${fase}-PLAN.md`),
|
|
30
|
+
];
|
|
31
|
+
for (const c of candidatos) {
|
|
32
|
+
if (fs.existsSync(c)) return c;
|
|
33
|
+
}
|
|
34
|
+
return candidatos[0];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { resolverPlanPath };
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* remediar-evolved-instaladas — Descongela copias instaladas con marcadores
|
|
6
|
+
* evolved espurios (Fase 16, T-25, REQ-16-14).
|
|
7
|
+
*
|
|
8
|
+
* Las instalaciones previas a Fase 16 quedaron con componentes `evolved:true`
|
|
9
|
+
* shippeados de fábrica que el instalador preservaba para siempre (bug). Este
|
|
10
|
+
* sub-comando los reclasifica en la máquina del usuario:
|
|
11
|
+
*
|
|
12
|
+
* - B (shipped intacto): el cuerpo canónico instalado coincide con el baseline
|
|
13
|
+
* de su `evolved-from` (manifiesto actual o `git show v<from>:<ruta>`).
|
|
14
|
+
* → el usuario NO lo tocó → actualizar al canónico actual (con backup + auditoría).
|
|
15
|
+
* - A (evolución del usuario): el cuerpo difiere del baseline, o no hay baseline
|
|
16
|
+
* verificable. → PRESERVAR (jamás overwrite). Invariante.
|
|
17
|
+
*
|
|
18
|
+
* SEGURO por defecto: `--dry-run` (solo reporta). Requiere `--apply` explícito.
|
|
19
|
+
* Ante CUALQUIER duda (sin baseline, sin fuente, error) clasifica A (preserva).
|
|
20
|
+
*
|
|
21
|
+
* Uso:
|
|
22
|
+
* node scripts/remediar-evolved-instaladas.js --target=~/.claude (dry-run)
|
|
23
|
+
* node scripts/remediar-evolved-instaladas.js --target=~/.claude --apply (aplica)
|
|
24
|
+
*
|
|
25
|
+
* Zero-deps salvo git (para baseline histórico). Layout runtime: claude.
|
|
26
|
+
*
|
|
27
|
+
* @module scripts/remediar-evolved-instaladas
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { spawnSync } = require('child_process');
|
|
34
|
+
|
|
35
|
+
const { canonicalHash } = require('./lib/canonical-hash');
|
|
36
|
+
const { auditarEscritura } = require('./lib/audit-evolved');
|
|
37
|
+
const { mergeEvolved } = require('../hooks/lib/evolution-tracker');
|
|
38
|
+
|
|
39
|
+
const RAIZ_PKG = path.resolve(__dirname, '..');
|
|
40
|
+
|
|
41
|
+
// Mapeo dir-runtime (claude) → dir-fuente (clave del manifiesto / git).
|
|
42
|
+
const MAP_DIRS = [
|
|
43
|
+
{ runtime: 'agents', fuente: 'agentes', skill: false },
|
|
44
|
+
{ runtime: 'commands/swl', fuente: 'comandos/swl', skill: false },
|
|
45
|
+
{ runtime: 'rules', fuente: 'reglas', skill: false },
|
|
46
|
+
{ runtime: 'skills', fuente: 'habilidades', skill: true },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function expandirTarget(t) {
|
|
50
|
+
if (!t) return path.join(os.homedir(), '.claude');
|
|
51
|
+
if (t === '~' || t.startsWith('~/') || t.startsWith('~\\')) return path.join(os.homedir(), t.slice(1).replace(/^[/\\]/, ''));
|
|
52
|
+
return path.resolve(t);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function leerEvolved(content) {
|
|
56
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
57
|
+
if (!m) return { evolved: false };
|
|
58
|
+
const fm = m[1];
|
|
59
|
+
if (!/^evolved:\s*(true|yes)\b/mi.test(fm)) return { evolved: false };
|
|
60
|
+
const from = (/^evolved-from:\s*["']?([^"'\n\r]+)/mi.exec(fm) || [])[1];
|
|
61
|
+
// CRÍTICO (nemesis): extraer evolved-origin — sin esto, la defensa
|
|
62
|
+
// 'evolved-origin: user fuerza A' era letra muerta aquí (ev.origin undefined).
|
|
63
|
+
const origin = (/^evolved-origin:\s*["']?([^"'\n\r]+)/mi.exec(fm) || [])[1];
|
|
64
|
+
return { evolved: true, from: from ? from.trim() : '', origin: origin ? origin.trim() : '' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Baseline canónico de un componente en una versión: manifiesto actual o git. */
|
|
68
|
+
function baselineCanonico(manifiesto, from, sourceRel) {
|
|
69
|
+
if (manifiesto && manifiesto[from] && manifiesto[from][sourceRel]) return manifiesto[from][sourceRel];
|
|
70
|
+
if (!from) return null;
|
|
71
|
+
const r = spawnSync('git', ['show', `v${from}:${sourceRel}`], { cwd: RAIZ_PKG, encoding: 'utf8' });
|
|
72
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
73
|
+
return canonicalHash(r.stdout);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Conjunto de hashes canónicos que el archivo `sourceRel` tuvo a lo largo de
|
|
78
|
+
* TODA la historia del repo madre (siguiendo renames). Si el cuerpo instalado
|
|
79
|
+
* coincide con cualquiera de ellos, el componente es contenido shipped del repo
|
|
80
|
+
* (ya incorporado en algún momento → B), no una evolución inventada por el
|
|
81
|
+
* usuario (Punto 1: "analizar si ya lo incorporas al proyecto madre").
|
|
82
|
+
* @param {string} sourceRel
|
|
83
|
+
* @param {number} [maxCommits=120]
|
|
84
|
+
* @returns {Set<string>}
|
|
85
|
+
*/
|
|
86
|
+
function hashesHistoricos(sourceRel, maxCommits = 120) {
|
|
87
|
+
const set = new Set();
|
|
88
|
+
const GIT_OPTS = { cwd: RAIZ_PKG, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 };
|
|
89
|
+
const log = spawnSync('git', ['log', '--all', '--follow', '--pretty=%H', '--', sourceRel], GIT_OPTS);
|
|
90
|
+
if (log.status !== 0 || !log.stdout) return set;
|
|
91
|
+
const commits = log.stdout.trim().split('\n').filter(Boolean).slice(0, maxCommits);
|
|
92
|
+
for (const c of commits) {
|
|
93
|
+
// status !== 0 es esperado en commits pre-rename (la ruta nueva no existía):
|
|
94
|
+
// se saltan, no son error. Solo se agregan blobs realmente presentes.
|
|
95
|
+
const show = spawnSync('git', ['show', `${c}:${sourceRel}`], GIT_OPTS);
|
|
96
|
+
if (show.status === 0 && show.stdout) set.add(canonicalHash(show.stdout));
|
|
97
|
+
}
|
|
98
|
+
return set;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function cargarManifiesto() {
|
|
102
|
+
try { return JSON.parse(fs.readFileSync(path.join(RAIZ_PKG, 'manifiestos', 'canonical-hashes.json'), 'utf8')); }
|
|
103
|
+
catch { return {}; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Lista componentes evolved instalados en el target, con su mapeo a fuente. */
|
|
107
|
+
function escanear(target) {
|
|
108
|
+
const items = [];
|
|
109
|
+
for (const { runtime, fuente, skill } of MAP_DIRS) {
|
|
110
|
+
const dir = path.join(target, runtime);
|
|
111
|
+
if (!fs.existsSync(dir)) continue;
|
|
112
|
+
if (skill) {
|
|
113
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
114
|
+
if (!e.isDirectory()) continue;
|
|
115
|
+
const skillMd = path.join(dir, e.name, 'SKILL.md');
|
|
116
|
+
if (fs.existsSync(skillMd)) items.push({ archivo: skillMd, sourceRel: `${fuente}/${e.name}/SKILL.md`, esDir: true, dir: path.join(dir, e.name) });
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
120
|
+
if (e.isFile() && e.name.endsWith('.md')) items.push({ archivo: path.join(dir, e.name), sourceRel: `${fuente}/${e.name}`, esDir: false });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function backupSimple(target, archivo, ts) {
|
|
128
|
+
const rel = path.relative(target, archivo);
|
|
129
|
+
const dest = path.join(target, '.swl-evolved-backups', ts, rel);
|
|
130
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
131
|
+
fs.copyFileSync(archivo, dest);
|
|
132
|
+
return dest;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function remediar({ target, apply = false } = {}) {
|
|
136
|
+
target = expandirTarget(target);
|
|
137
|
+
const manifiesto = cargarManifiesto();
|
|
138
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
139
|
+
const reconcileDir = path.join(target, '.planning', 'evolution', 'reconcile');
|
|
140
|
+
const resumen = { target, A: [], B: [], errores: [], reconcileDir };
|
|
141
|
+
|
|
142
|
+
for (const item of escanear(target)) {
|
|
143
|
+
let content;
|
|
144
|
+
try { content = fs.readFileSync(item.archivo, 'utf8'); } catch (e) { resumen.errores.push({ archivo: item.sourceRel, error: e.message }); continue; }
|
|
145
|
+
const ev = leerEvolved(content);
|
|
146
|
+
if (!ev.evolved) continue;
|
|
147
|
+
|
|
148
|
+
const fuenteAbs = path.join(RAIZ_PKG, item.sourceRel);
|
|
149
|
+
const instaladoHash = canonicalHash(content);
|
|
150
|
+
|
|
151
|
+
// Clasificación B (shipped → ya incorporado en el repo madre):
|
|
152
|
+
// (1) marca explícita evolved-origin: user → A definitivo (defensa en profundidad).
|
|
153
|
+
// (2) coincide con baseline del manifiesto / git show v<from>.
|
|
154
|
+
// (3) coincide con CUALQUIER estado histórico del archivo en el repo (Punto 1).
|
|
155
|
+
let esB = false;
|
|
156
|
+
let evidencia = '';
|
|
157
|
+
if ((ev.origin || '').toLowerCase() === 'user') {
|
|
158
|
+
evidencia = 'evolved-origin: user (marca explícita)';
|
|
159
|
+
} else {
|
|
160
|
+
const baseline = baselineCanonico(manifiesto, ev.from, item.sourceRel);
|
|
161
|
+
if (baseline && instaladoHash === baseline) { esB = true; evidencia = `baseline v${ev.from} coincide`; }
|
|
162
|
+
else if (hashesHistoricos(item.sourceRel).has(instaladoHash)) { esB = true; evidencia = 'coincide con un estado histórico del repo madre'; }
|
|
163
|
+
else evidencia = baseline ? 'cuerpo difiere de todo estado del repo (edición del usuario)' : 'sin baseline ni coincidencia histórica';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!esB) {
|
|
167
|
+
// A — evolución del usuario. PRESERVAR + diff centralizado para revisión upstream.
|
|
168
|
+
let diffRel = null;
|
|
169
|
+
if (apply && fs.existsSync(fuenteAbs)) {
|
|
170
|
+
const m = mergeEvolved(item.archivo, fuenteAbs, 'actual', { diffDir: reconcileDir });
|
|
171
|
+
if (m.merged && m.diffPath) diffRel = path.relative(target, m.diffPath).replace(/\\/g, '/');
|
|
172
|
+
}
|
|
173
|
+
resumen.A.push({ archivo: item.sourceRel, from: ev.from, razon: evidencia, diff: diffRel });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// B — shipped intacto. Actualizar al canónico actual del paquete.
|
|
178
|
+
if (!fs.existsSync(fuenteAbs)) { resumen.errores.push({ archivo: item.sourceRel, error: 'fuente actual no existe' }); continue; }
|
|
179
|
+
if (apply) {
|
|
180
|
+
try {
|
|
181
|
+
const hashFuente = canonicalHash(fs.readFileSync(fuenteAbs, 'utf8')); // antes del copy
|
|
182
|
+
const backupPath = backupSimple(target, item.archivo, ts); // backup PRECEDE al overwrite
|
|
183
|
+
fs.copyFileSync(fuenteAbs, item.archivo); // fuente limpio (sin evolved-*) → descongelado
|
|
184
|
+
auditarEscritura({ archivo: item.sourceRel, clasificacion: 'B', accion: 'overwrite', hashAntes: instaladoHash, hashDespues: hashFuente, backupPath, evidencia: `remediación: ${evidencia}` });
|
|
185
|
+
} catch (e) { resumen.errores.push({ archivo: item.sourceRel, error: e.message }); continue; }
|
|
186
|
+
}
|
|
187
|
+
resumen.B.push({ archivo: item.sourceRel, from: ev.from, razon: evidencia });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (apply) escribirIndiceReconcile(resumen);
|
|
191
|
+
return resumen;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Escribe un índice consolidado de reconciliación (superficie de revisión upstream). */
|
|
195
|
+
function escribirIndiceReconcile(resumen) {
|
|
196
|
+
if (resumen.A.length === 0) return;
|
|
197
|
+
try {
|
|
198
|
+
fs.mkdirSync(resumen.reconcileDir, { recursive: true });
|
|
199
|
+
const lineas = [
|
|
200
|
+
'# Reconciliación de evoluciones del usuario (revisión upstream)',
|
|
201
|
+
'',
|
|
202
|
+
'Estos componentes difieren de todo estado conocido del repo madre — son',
|
|
203
|
+
'ediciones locales genuinas. Revisa cada diff y decide si incorporarlo al',
|
|
204
|
+
'repo con `/swl:aprender` o `/swl:autoresearch`, o descartarlo.',
|
|
205
|
+
'',
|
|
206
|
+
'| Componente | evolved-from | Diff | Motivo |',
|
|
207
|
+
'|---|---|---|---|',
|
|
208
|
+
...resumen.A.map(a => `| ${a.archivo} | v${a.from || '?'} | ${a.diff || '—'} | ${a.razon} |`),
|
|
209
|
+
'',
|
|
210
|
+
];
|
|
211
|
+
fs.writeFileSync(path.join(resumen.reconcileDir, 'INDEX.md'), lineas.join('\n'), 'utf8');
|
|
212
|
+
} catch { /* best-effort */ }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function imprimir(r, apply) {
|
|
216
|
+
process.stdout.write(`\n=== Remediación evolved instaladas — target: ${r.target} ===\n`);
|
|
217
|
+
process.stdout.write(`Modo: ${apply ? 'APPLY (escribe)' : 'DRY-RUN (solo reporta)'}\n\n`);
|
|
218
|
+
process.stdout.write(`B (shipped intacto → ${apply ? 'actualizado' : 'se actualizaría'}): ${r.B.length}\n`);
|
|
219
|
+
for (const b of r.B) process.stdout.write(` ⇪ ${b.archivo} (from v${b.from})\n`);
|
|
220
|
+
process.stdout.write(`\nA (evolución del usuario → PRESERVADO): ${r.A.length}\n`);
|
|
221
|
+
for (const a of r.A) process.stdout.write(` ★ ${a.archivo} (from v${a.from || '?'}) — ${a.razon}\n`);
|
|
222
|
+
if (r.errores.length) {
|
|
223
|
+
process.stdout.write(`\nErrores: ${r.errores.length}\n`);
|
|
224
|
+
for (const e of r.errores) process.stdout.write(` ! ${e.archivo}: ${e.error}\n`);
|
|
225
|
+
}
|
|
226
|
+
if (apply && r.A.length) process.stdout.write(`\nDiffs de reconciliación (revisión upstream): ${path.relative(r.target, r.reconcileDir).replace(/\\/g, '/')}/INDEX.md\n`);
|
|
227
|
+
if (!apply && r.B.length) process.stdout.write(`\n→ Para aplicar: agregar --apply (backups en <target>/.swl-evolved-backups/, diffs centralizados en .planning/evolution/reconcile/).\n`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (require.main === module) {
|
|
231
|
+
const args = process.argv.slice(2);
|
|
232
|
+
const targetArg = (args.find(a => a.startsWith('--target=')) || '').slice('--target='.length) || undefined;
|
|
233
|
+
const apply = args.includes('--apply');
|
|
234
|
+
const r = remediar({ target: targetArg, apply });
|
|
235
|
+
imprimir(r, apply);
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = { remediar, escanear, leerEvolved, baselineCanonico, expandirTarget };
|
package/scripts/validar.js
CHANGED
|
@@ -88,6 +88,33 @@ verificar(habilidades.length >= 25, `Habilidades: ${habilidades.length} (mínimo
|
|
|
88
88
|
const comandos = fs.readdirSync(path.join(RAIZ, 'comandos', 'swl')).filter(f => f.endsWith('.md'));
|
|
89
89
|
verificar(comandos.length >= 9, `Comandos: ${comandos.length} (mínimo 9)`);
|
|
90
90
|
|
|
91
|
+
// 7b. Gate cross-scope: comandos user-facing sin invocaciones relativas al
|
|
92
|
+
// proyecto (node scripts/ | node hooks/ | require('./scripts|hooks/...')).
|
|
93
|
+
// Esas rutas no existen downstream; deben usar el subcomando del CLI.
|
|
94
|
+
// Ver docs/invocacion-cli-cross-scope.md.
|
|
95
|
+
const { auditarInvocacionesComandos } = require('./lib/auditar-invocaciones-comandos');
|
|
96
|
+
const auditInvoc = auditarInvocacionesComandos(path.join(RAIZ, 'comandos', 'swl'));
|
|
97
|
+
verificar(
|
|
98
|
+
auditInvoc.ok,
|
|
99
|
+
auditInvoc.ok
|
|
100
|
+
? `Cross-scope: ${auditInvoc.escaneados} comandos sin rutas relativas al proyecto`
|
|
101
|
+
: `Cross-scope: ${auditInvoc.violaciones.length} invocación(es) relativa(s) — ${auditInvoc.violaciones.map(v => `${v.comando}.md:${v.linea}`).join(', ')}`
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// 7c. Gate inverso de evolución (Fase 16, REQ-16-03): el FUENTE no debe portar
|
|
105
|
+
// marcadores evolved (frontmatter evolved:true ni sidecar .evolved.json). Un
|
|
106
|
+
// marcador en el repo madre es shipped-evolved espurio que congela el componente
|
|
107
|
+
// downstream. Ver scripts/lib/evolved-fuente.js y verificar-evolucion.js --gate-inverso.
|
|
108
|
+
const { listarOfensores } = require('./lib/evolved-fuente');
|
|
109
|
+
const ofensoresEvolved = listarOfensores(RAIZ);
|
|
110
|
+
const totalOfensores = ofensoresEvolved.frontmatter.length + ofensoresEvolved.sidecars.length;
|
|
111
|
+
verificar(
|
|
112
|
+
totalOfensores === 0,
|
|
113
|
+
totalOfensores === 0
|
|
114
|
+
? 'Gate inverso evolved: fuente sin marcadores evolved espurios'
|
|
115
|
+
: `Gate inverso evolved: ${totalOfensores} componente(s) del fuente con evolved (corregir: node scripts/verificar-evolucion.js --gate-inverso --fix)`
|
|
116
|
+
);
|
|
117
|
+
|
|
91
118
|
// 8. Reglas
|
|
92
119
|
const reglas = fs.readdirSync(path.join(RAIZ, 'reglas')).filter(f => f.endsWith('.md'));
|
|
93
120
|
verificar(reglas.length >= 7, `Reglas: ${reglas.length} (mínimo 7)`);
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
const fs = require('fs');
|
|
33
33
|
const path = require('path');
|
|
34
34
|
const { execSync } = require('child_process');
|
|
35
|
+
const { listarOfensores, limpiar } = require('./lib/evolved-fuente');
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Detecta si `dir` es la raíz del paquete swl-ses (repo madre).
|
|
@@ -291,9 +292,43 @@ function imprimirResultado(r) {
|
|
|
291
292
|
process.stdout.write('[' + marca + '] ' + r.archivo + ': ' + mensaje + '\n');
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Gate inverso (Fase 16, REQ-16-03): ningún componente del FUENTE (repo madre)
|
|
297
|
+
* puede portar marcador evolved (frontmatter `evolved:true` o sidecar
|
|
298
|
+
* `.evolved.json`). Un marcador en el fuente es shipped-evolved espurio que
|
|
299
|
+
* congela el componente downstream. Corre SIEMPRE en el repo madre — es el
|
|
300
|
+
* complemento exacto del bypass `if (raizPaquete)` de verificarArchivo (que
|
|
301
|
+
* omite los checks evolved-de-usuario; este gate verifica lo contrario).
|
|
302
|
+
*
|
|
303
|
+
* @param {{ fix?: boolean, raiz?: string }} [opts]
|
|
304
|
+
* @returns {number} 0 si limpio, 1 si hay ofensores (sin --fix)
|
|
305
|
+
*/
|
|
306
|
+
function gateInverso({ fix = false, raiz = process.cwd() } = {}) {
|
|
307
|
+
if (fix) {
|
|
308
|
+
const acciones = limpiar(raiz);
|
|
309
|
+
for (const a of acciones) process.stdout.write(` [fix] ${a.tipo}: ${a.archivo}\n`);
|
|
310
|
+
process.stdout.write(`[gate-inverso] limpieza aplicada: ${acciones.length} acción(es)\n`);
|
|
311
|
+
}
|
|
312
|
+
const { frontmatter, sidecars } = listarOfensores(raiz);
|
|
313
|
+
const total = frontmatter.length + sidecars.length;
|
|
314
|
+
if (total === 0) {
|
|
315
|
+
process.stdout.write('[gate-inverso] OK — ningún componente del fuente porta marcador evolved.\n');
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
process.stdout.write(`[gate-inverso] FALLA — ${total} componente(s) del fuente con marcador evolved (deben limpiarse):\n`);
|
|
319
|
+
for (const f of frontmatter) process.stdout.write(` - frontmatter evolved: ${f}\n`);
|
|
320
|
+
for (const s of sidecars) process.stdout.write(` - sidecar .evolved.json: ${s}\n`);
|
|
321
|
+
process.stdout.write(' → corregir: node scripts/verificar-evolucion.js --gate-inverso --fix\n');
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
294
325
|
function main() {
|
|
295
326
|
const args = process.argv.slice(2);
|
|
296
327
|
|
|
328
|
+
if (args.includes('--gate-inverso')) {
|
|
329
|
+
process.exit(gateInverso({ fix: args.includes('--fix') }));
|
|
330
|
+
}
|
|
331
|
+
|
|
297
332
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
298
333
|
process.stdout.write(
|
|
299
334
|
'Uso:\n' +
|
|
@@ -351,5 +386,6 @@ module.exports = {
|
|
|
351
386
|
leerCampo,
|
|
352
387
|
obtenerVersionEnHEAD,
|
|
353
388
|
esRaizDelPaquete,
|
|
389
|
+
gateInverso,
|
|
354
390
|
main,
|
|
355
391
|
};
|
|
@@ -317,6 +317,18 @@ function main() {
|
|
|
317
317
|
// calibración + ADR posterior (mismo patrón que G0/G2/G3).
|
|
318
318
|
const gateLicencias = ejecutarGateLicencias();
|
|
319
319
|
|
|
320
|
+
// Gate de evolución (Fase 16, REQ-16-13): el manifiesto canonical-hashes debe
|
|
321
|
+
// estar al día y el fuente NO debe portar marcadores evolved espurios. Ambos
|
|
322
|
+
// son blocking — un release con manifiesto stale o fuente evolved congelaría
|
|
323
|
+
// componentes downstream.
|
|
324
|
+
const gateEvolved = ejecutarGateEvolved();
|
|
325
|
+
if (!gateEvolved.ok) {
|
|
326
|
+
fallasObligatorias++;
|
|
327
|
+
}
|
|
328
|
+
console.log(gateEvolved.ok
|
|
329
|
+
? ' [OK] Gate evolución: manifiesto al día + fuente sin evolved espurio'
|
|
330
|
+
: ` [FALLA] Gate evolución: ${gateEvolved.problemas.join('; ')}`);
|
|
331
|
+
|
|
320
332
|
if (jsonOut) {
|
|
321
333
|
process.stdout.write(JSON.stringify({
|
|
322
334
|
version,
|
|
@@ -330,6 +342,7 @@ function main() {
|
|
|
330
342
|
description_gate: gateDescription,
|
|
331
343
|
aiisms_gate: aiismsGate,
|
|
332
344
|
gate_licencias: gateLicencias,
|
|
345
|
+
gate_evolved: gateEvolved,
|
|
333
346
|
resultados: resultados.map(({ entrada, resultado }) => ({
|
|
334
347
|
archivo: entrada.archivo,
|
|
335
348
|
obligatorio: entrada.obligatorio,
|
|
@@ -887,6 +900,25 @@ function ejecutarGateDescription(contadoresReales) {
|
|
|
887
900
|
* para tests; en producción usa el CWD del proceso).
|
|
888
901
|
* @returns {{disponible: boolean, hallazgos?: Array, resumen?: object, error?: string}}
|
|
889
902
|
*/
|
|
903
|
+
/**
|
|
904
|
+
* Gate de evolución (Fase 16): manifiesto canonical-hashes al día + fuente sin
|
|
905
|
+
* marcadores evolved espurios. Reusa los scripts existentes vía subproceso.
|
|
906
|
+
* @returns {{ ok: boolean, problemas: string[] }}
|
|
907
|
+
*/
|
|
908
|
+
function ejecutarGateEvolved() {
|
|
909
|
+
const { spawnSync } = require('child_process');
|
|
910
|
+
const problemas = [];
|
|
911
|
+
const correr = (args) => spawnSync(process.execPath, args, { cwd: CWD, encoding: 'utf8' }).status;
|
|
912
|
+
|
|
913
|
+
if (correr([path.join(CWD, 'scripts', 'generar-canonical-hashes.js'), '--check']) !== 0) {
|
|
914
|
+
problemas.push('manifiesto canonical-hashes desactualizado (node scripts/generar-canonical-hashes.js)');
|
|
915
|
+
}
|
|
916
|
+
if (correr([path.join(CWD, 'scripts', 'verificar-evolucion.js'), '--gate-inverso']) !== 0) {
|
|
917
|
+
problemas.push('fuente con marcadores evolved espurios (node scripts/verificar-evolucion.js --gate-inverso --fix)');
|
|
918
|
+
}
|
|
919
|
+
return { ok: problemas.length === 0, problemas };
|
|
920
|
+
}
|
|
921
|
+
|
|
890
922
|
function ejecutarGateLicencias(baseDir = CWD) {
|
|
891
923
|
try {
|
|
892
924
|
const { hallazgos, resumen } = evaluarLicencias(baseDir);
|
|
@@ -905,4 +937,5 @@ module.exports = {
|
|
|
905
937
|
extraerCifrasDescription,
|
|
906
938
|
ejecutarGateDescription,
|
|
907
939
|
ejecutarGateLicencias,
|
|
940
|
+
ejecutarGateEvolved,
|
|
908
941
|
};
|
|
@@ -295,4 +295,4 @@ if (require.main === module) {
|
|
|
295
295
|
main();
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
module.exports = { extraerReqs, extraerMatrizPlan, extraerCommitsConRefs, extraerTestsConMarker, validarFase };
|
|
298
|
+
module.exports = { extraerReqs, extraerMatrizPlan, extraerCommitsConRefs, extraerTestsConMarker, validarFase, main };
|