@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.
Files changed (113) hide show
  1. package/CLAUDE.md +199 -196
  2. package/README.md +597 -579
  3. package/agentes/arquitecto-swl.md +0 -5
  4. package/agentes/backend-python-swl.md +0 -5
  5. package/agentes/implementador-swl.md +0 -5
  6. package/agentes/nemesis-auditor-swl.md +0 -5
  7. package/agentes/orquestador-swl.md +0 -5
  8. package/agentes/planificador-swl.md +0 -5
  9. package/agentes/revisor-codigo-swl.md +0 -5
  10. package/bin/swl-ses.js +63 -0
  11. package/comandos/swl/adoptar-proyecto.md +12 -14
  12. package/comandos/swl/aprender.md +30 -47
  13. package/comandos/swl/aprobar-plan.md +23 -35
  14. package/comandos/swl/autoresearch.md +12 -14
  15. package/comandos/swl/briefing.md +5 -8
  16. package/comandos/swl/checkpoint.md +10 -15
  17. package/comandos/swl/claudemd.md +12 -12
  18. package/comandos/swl/configurar-ci.md +20 -19
  19. package/comandos/swl/cron.md +10 -12
  20. package/comandos/swl/ejecutar-fase.md +10 -8
  21. package/comandos/swl/evolucionar.md +6 -11
  22. package/comandos/swl/inbox.md +10 -10
  23. package/comandos/swl/modelo.md +7 -9
  24. package/comandos/swl/notificaciones.md +19 -116
  25. package/comandos/swl/nuevo-proyecto.md +9 -14
  26. package/comandos/swl/release.md +19 -5
  27. package/comandos/swl/revisar-impacto.md +0 -5
  28. package/comandos/swl/status.md +333 -348
  29. package/comandos/swl/verificar.md +817 -813
  30. package/habilidades/agent-browser/SKILL.md +0 -5
  31. package/habilidades/angular-moderno/SKILL.md +0 -5
  32. package/habilidades/api-rest-diseno/SKILL.md +0 -5
  33. package/habilidades/aprendizaje-continuo/SKILL.md +0 -5
  34. package/habilidades/auth-patrones/SKILL.md +0 -5
  35. package/habilidades/build-errors-nextjs/SKILL.md +0 -5
  36. package/habilidades/changelog-generator/SKILL.md +174 -179
  37. package/habilidades/checklist-seguridad/SKILL.md +0 -5
  38. package/habilidades/contenedores-docker/SKILL.md +0 -5
  39. package/habilidades/datos-etl/SKILL.md +0 -5
  40. package/habilidades/doc-sync/SKILL.md +0 -5
  41. package/habilidades/extractor-de-aprendizajes/SKILL.md +0 -5
  42. package/habilidades/fastapi-experto/SKILL.md +0 -5
  43. package/habilidades/frontend-avanzado/SKILL.md +0 -5
  44. package/habilidades/iam-secretos/SKILL.md +0 -5
  45. package/habilidades/manejo-errores/SKILL.md +0 -5
  46. package/habilidades/mapear-codebase/SKILL.md +0 -5
  47. package/habilidades/meta-skills-estandar/SKILL.md +0 -5
  48. package/habilidades/monitoring-alertas/SKILL.md +0 -5
  49. package/habilidades/nextjs-experto/SKILL.md +0 -5
  50. package/habilidades/nextjs-testing/SKILL.md +0 -5
  51. package/habilidades/node-experto/SKILL.md +0 -5
  52. package/habilidades/orquestacion-async/SKILL.md +0 -5
  53. package/habilidades/patrones-python/SKILL.md +227 -232
  54. package/habilidades/planear-fase/SKILL.md +336 -341
  55. package/habilidades/postgresql-experto/SKILL.md +0 -5
  56. package/habilidades/prevencion-sobreingenieria/SKILL.md +0 -5
  57. package/habilidades/protocolo-revision-swl/SKILL.md +0 -5
  58. package/habilidades/react-experto/SKILL.md +0 -5
  59. package/habilidades/release-semver/SKILL.md +0 -5
  60. package/habilidades/swl-claudemd/SKILL.md +10 -11
  61. package/habilidades/tdd-workflow/SKILL.md +710 -715
  62. package/habilidades/testing-python/SKILL.md +335 -340
  63. package/habilidades/verificar-trabajo/SKILL.md +0 -5
  64. package/hooks/lib/evolution-tracker.js +191 -35
  65. package/hooks/lib/propose-step.js +1 -0
  66. package/llms.txt +1 -1
  67. package/manifiestos/canonical-hashes.json +656 -0
  68. package/manifiestos/modulos.json +3 -0
  69. package/manifiestos/skills-lock.json +71 -71
  70. package/package.json +1 -1
  71. package/plugin.json +1 -1
  72. package/scripts/auditar-claudemd.js +38 -0
  73. package/scripts/cli/aprobar-plan.js +73 -0
  74. package/scripts/cli/briefing.js +23 -0
  75. package/scripts/cli/ciclo-evolucion.js +26 -0
  76. package/scripts/cli/configurar-ci.js +40 -0
  77. package/scripts/cli/derivar-feature-list.js +25 -0
  78. package/scripts/cli/detectar-host.js +27 -0
  79. package/scripts/cli/diary-entry.js +69 -0
  80. package/scripts/cli/execution-state.js +18 -0
  81. package/scripts/cli/gateway-notify.js +41 -0
  82. package/scripts/cli/liberar-fase.js +42 -0
  83. package/scripts/cli/loop-telemetry.js +125 -0
  84. package/scripts/cli/mark-evolved.js +56 -0
  85. package/scripts/cli/metricas-dora.js +26 -0
  86. package/scripts/cli/near-duplicate.js +55 -0
  87. package/scripts/cli/notificaciones.js +123 -0
  88. package/scripts/cli/propose-step.js +29 -0
  89. package/scripts/cli/schedule-parse.js +19 -0
  90. package/scripts/cli/sugerir-modelo.js +20 -0
  91. package/scripts/cli/verificar-plan.js +36 -0
  92. package/scripts/cli/verificar-trazabilidad.js +35 -0
  93. package/scripts/derivar-feature-list.js +1 -0
  94. package/scripts/generar-canonical-hashes.js +147 -0
  95. package/scripts/instalador.js +126 -53
  96. package/scripts/lib/audit-evolved.js +71 -0
  97. package/scripts/lib/auditar-invocaciones-comandos.js +104 -0
  98. package/scripts/lib/canonical-hash.js +94 -0
  99. package/scripts/lib/evolved-fuente.js +138 -0
  100. package/scripts/lib/resolver-plan-fase.js +37 -0
  101. package/scripts/remediar-evolved-instaladas.js +239 -0
  102. package/scripts/validar.js +27 -0
  103. package/scripts/verificar-evolucion.js +36 -0
  104. package/scripts/verificar-release.js +33 -0
  105. package/scripts/verificar-trazabilidad.js +1 -1
  106. package/agentes/.evolved.json +0 -9
  107. package/comandos/swl/.evolved.json +0 -23
  108. package/habilidades/auth-patrones/.evolved.json +0 -9
  109. package/habilidades/extractor-de-aprendizajes/.evolved.json +0 -9
  110. package/habilidades/instalar-sistema/.evolved.json +0 -9
  111. package/habilidades/manejo-errores/.evolved.json +0 -9
  112. package/habilidades/node-experto/.evolved.json +0 -9
  113. 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 };
@@ -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 };
@@ -1,9 +0,0 @@
1
- {
2
- "release-manager-swl.md": {
3
- "evolved": true,
4
- "evolvedFrom": "5.10.4",
5
- "evolvedAt": "2026-04-20",
6
- "evolvedBy": "aprender",
7
- "evolvedNote": "gap recurrente en bumps: package-lock nested + .planning/"
8
- }
9
- }