@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,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Subcomando `swl-ses verificar-plan --fase=N` — verificación read-only del lock
|
|
5
|
+
* de un PLAN (gate G1). Lo usa /swl:ejecutar-fase antes de implementar y
|
|
6
|
+
* /swl:aprobar-plan Paso 2 para detectar planes ya firmados/mutados/legacy.
|
|
7
|
+
*
|
|
8
|
+
* Reemplaza el `node -e "require('./scripts/lib/plan-lock')"` inline que se
|
|
9
|
+
* rompía downstream (ver docs/invocacion-cli-cross-scope.md).
|
|
10
|
+
*
|
|
11
|
+
* Exit 0 si ok (firmado o legacy); exit 1 si mutado/sin-firmar/corrupto.
|
|
12
|
+
*
|
|
13
|
+
* Uso: swl-ses verificar-plan --fase=N [--plan=<ruta>]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { verificarPlan } = require('../lib/plan-lock');
|
|
17
|
+
const { resolverPlanPath } = require('../lib/resolver-plan-fase');
|
|
18
|
+
|
|
19
|
+
function verificarPlanCli(opciones = {}) {
|
|
20
|
+
opciones = opciones || {};
|
|
21
|
+
const planPath = resolverPlanPath(opciones);
|
|
22
|
+
if (!planPath) {
|
|
23
|
+
console.error('Uso: swl-ses verificar-plan --fase=N [--plan=<ruta>]');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const r = verificarPlan(planPath);
|
|
27
|
+
console.log(JSON.stringify(r, null, 2));
|
|
28
|
+
process.exit(r.ok ? 0 : 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = verificarPlanCli;
|
|
32
|
+
|
|
33
|
+
if (require.main === module) {
|
|
34
|
+
const { parsearOpciones } = require('../lib/parsear-opciones');
|
|
35
|
+
verificarPlanCli(parsearOpciones(process.argv.slice(2)));
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper CLI para el subcomando `swl-ses verificar-trazabilidad`.
|
|
5
|
+
*
|
|
6
|
+
* Traduce las opciones parseadas del bin a las flags que espera el script
|
|
7
|
+
* standalone `scripts/verificar-trazabilidad.js` (que lee process.argv y hace
|
|
8
|
+
* su propio process.exit). Mismo patrón que scripts/cli/audit-claudemd.js.
|
|
9
|
+
*
|
|
10
|
+
* Reemplaza el `node scripts/verificar-trazabilidad.js` relativo al proyecto que
|
|
11
|
+
* /swl:aprobar-plan invocaba — roto downstream (ver
|
|
12
|
+
* docs/invocacion-cli-cross-scope.md).
|
|
13
|
+
*
|
|
14
|
+
* Uso: swl-ses verificar-trazabilidad --fase=N [--solo-plan] [--json]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { main } = require('../verificar-trazabilidad.js');
|
|
18
|
+
|
|
19
|
+
function verificarTrazabilidad(opciones = {}) {
|
|
20
|
+
opciones = opciones || {};
|
|
21
|
+
const args = [];
|
|
22
|
+
if (opciones.fase != null && opciones.fase !== true) args.push(`--fase=${opciones.fase}`);
|
|
23
|
+
if (opciones['solo-plan'] || opciones.solo_plan) args.push('--solo-plan');
|
|
24
|
+
if (opciones.json) args.push('--json');
|
|
25
|
+
|
|
26
|
+
const argvOriginal = process.argv;
|
|
27
|
+
process.argv = ['node', 'verificar-trazabilidad.js', ...args];
|
|
28
|
+
try {
|
|
29
|
+
main();
|
|
30
|
+
} finally {
|
|
31
|
+
process.argv = argvOriginal;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = verificarTrazabilidad;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* generar-canonical-hashes — Genera/actualiza `manifiestos/canonical-hashes.json`.
|
|
6
|
+
*
|
|
7
|
+
* Recorre los componentes versionables del fuente (agentes, skills, comandos,
|
|
8
|
+
* reglas) y registra el hash del cuerpo canónico (sin campos `evolved-*`,
|
|
9
|
+
* CRLF→LF) bajo la versión actual del paquete:
|
|
10
|
+
*
|
|
11
|
+
* { "<version>": { "<rutaRel>": "<sha256>", ... }, ... }
|
|
12
|
+
*
|
|
13
|
+
* El manifiesto es la baseline que el discriminador A/B (Fase 16) usa para
|
|
14
|
+
* decidir si una copia instalada `evolved:true` fue tocada por el usuario
|
|
15
|
+
* (población A → merge) o es shipped-evolved intacto (población B → actualizar).
|
|
16
|
+
*
|
|
17
|
+
* Modelo de amenaza (auditoría nemesis Fase 16, hallazgo BAJO): el manifiesto
|
|
18
|
+
* NO lleva firma criptográfica propia. No es un gap incremental: viaja DENTRO
|
|
19
|
+
* del paquete npm publicado, el mismo límite de confianza que `evolution-tracker.js`
|
|
20
|
+
* y todo el código. Quien pueda alterar este JSON ya puede alterar el código que
|
|
21
|
+
* lo consume — firmar el manifiesto sin firmar el paquete entero no añade defensa.
|
|
22
|
+
* La integridad la provee la cadena de publicación de npm (provenance/SBOM del
|
|
23
|
+
* release). NO se añade firma propia (sería seguridad teatral). Reabrir SOLO si
|
|
24
|
+
* el manifiesto se expone como archivo editable fuera del paquete (p. ej. P2P).
|
|
25
|
+
*
|
|
26
|
+
* Idempotente: dos corridas sin cambios producen el mismo archivo (keys
|
|
27
|
+
* ordenadas). Preserva entradas de versiones anteriores (histórico necesario
|
|
28
|
+
* para discriminar contra clientes que evolucionaron desde una versión vieja).
|
|
29
|
+
*
|
|
30
|
+
* Uso: node scripts/generar-canonical-hashes.js [--check]
|
|
31
|
+
* --check No escribe; exit 2 si el manifiesto está desactualizado (uso CI).
|
|
32
|
+
*
|
|
33
|
+
* @module scripts/generar-canonical-hashes
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
|
|
39
|
+
const { canonicalHash } = require('./lib/canonical-hash');
|
|
40
|
+
|
|
41
|
+
const RAIZ = path.resolve(__dirname, '..');
|
|
42
|
+
const MANIFIESTO = path.join(RAIZ, 'manifiestos', 'canonical-hashes.json');
|
|
43
|
+
|
|
44
|
+
let atomicWriteSync;
|
|
45
|
+
try {
|
|
46
|
+
({ atomicWriteSync } = require('../hooks/lib/atomic-write'));
|
|
47
|
+
} catch {
|
|
48
|
+
atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Recolecta los componentes versionables como rutas relativas a la raíz. */
|
|
52
|
+
function recolectarComponentes() {
|
|
53
|
+
const rutas = [];
|
|
54
|
+
|
|
55
|
+
const listarMd = (dirRel) => {
|
|
56
|
+
const dir = path.join(RAIZ, dirRel);
|
|
57
|
+
if (!fs.existsSync(dir)) return;
|
|
58
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
59
|
+
if (entry.endsWith('.md')) rutas.push(path.posix.join(dirRel, entry));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Agentes, comandos, reglas: archivos .md planos.
|
|
64
|
+
listarMd('agentes');
|
|
65
|
+
listarMd('comandos/swl');
|
|
66
|
+
listarMd('reglas');
|
|
67
|
+
|
|
68
|
+
// Habilidades: cada skill es un directorio con SKILL.md.
|
|
69
|
+
const habDir = path.join(RAIZ, 'habilidades');
|
|
70
|
+
if (fs.existsSync(habDir)) {
|
|
71
|
+
for (const entry of fs.readdirSync(habDir)) {
|
|
72
|
+
const skillMd = path.join(habDir, entry, 'SKILL.md');
|
|
73
|
+
if (fs.existsSync(skillMd)) rutas.push(path.posix.join('habilidades', entry, 'SKILL.md'));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return rutas.sort();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Construye el mapa { rutaRel: hash } de la versión actual del fuente. */
|
|
81
|
+
function hashesActuales() {
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const rel of recolectarComponentes()) {
|
|
84
|
+
const abs = path.join(RAIZ, rel);
|
|
85
|
+
out[rel] = canonicalHash(fs.readFileSync(abs, 'utf8'));
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Ordena recursivamente las keys de un objeto plano de 2 niveles. */
|
|
91
|
+
function ordenar(manifiesto) {
|
|
92
|
+
const out = {};
|
|
93
|
+
for (const version of Object.keys(manifiesto).sort()) {
|
|
94
|
+
const porVersion = manifiesto[version];
|
|
95
|
+
out[version] = {};
|
|
96
|
+
for (const k of Object.keys(porVersion).sort()) out[version][k] = porVersion[k];
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function cargarManifiesto() {
|
|
102
|
+
if (!fs.existsSync(MANIFIESTO)) return {};
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(fs.readFileSync(MANIFIESTO, 'utf8'));
|
|
105
|
+
} catch {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function generar({ check = false } = {}) {
|
|
111
|
+
const version = require(path.join(RAIZ, 'package.json')).version;
|
|
112
|
+
const manifiesto = cargarManifiesto();
|
|
113
|
+
manifiesto[version] = hashesActuales();
|
|
114
|
+
const ordenado = ordenar(manifiesto);
|
|
115
|
+
const serializado = JSON.stringify(ordenado, null, 2) + '\n';
|
|
116
|
+
|
|
117
|
+
const previo = fs.existsSync(MANIFIESTO) ? fs.readFileSync(MANIFIESTO, 'utf8') : '';
|
|
118
|
+
// Comparación EOL-tolerante: git puede normalizar el .json a CRLF en checkout
|
|
119
|
+
// (Windows). El contenido semántico es idéntico; comparar normalizado evita
|
|
120
|
+
// un falso "desactualizado" en el gate `--check` cross-OS.
|
|
121
|
+
const norm = (s) => s.replace(/\r\n/g, '\n');
|
|
122
|
+
const cambio = norm(previo) !== norm(serializado);
|
|
123
|
+
|
|
124
|
+
if (check) {
|
|
125
|
+
if (cambio) {
|
|
126
|
+
console.error(`[canonical-hashes] DESACTUALIZADO para v${version} — ejecuta: node scripts/generar-canonical-hashes.js`);
|
|
127
|
+
process.exitCode = 2;
|
|
128
|
+
} else {
|
|
129
|
+
console.log(`[canonical-hashes] OK — v${version} al día (${Object.keys(ordenado[version]).length} componentes)`);
|
|
130
|
+
}
|
|
131
|
+
return ordenado;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (cambio) {
|
|
135
|
+
atomicWriteSync(MANIFIESTO, serializado, 'utf8');
|
|
136
|
+
console.log(`[canonical-hashes] escrito v${version} (${Object.keys(ordenado[version]).length} componentes) → ${path.relative(RAIZ, MANIFIESTO)}`);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(`[canonical-hashes] sin cambios para v${version}`);
|
|
139
|
+
}
|
|
140
|
+
return ordenado;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (require.main === module) {
|
|
144
|
+
generar({ check: process.argv.includes('--check') });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { generar, hashesActuales, recolectarComponentes, MANIFIESTO };
|
package/scripts/instalador.js
CHANGED
|
@@ -36,6 +36,8 @@ const {
|
|
|
36
36
|
mergeEvolved,
|
|
37
37
|
scanEvolved,
|
|
38
38
|
} = require('../hooks/lib/evolution-tracker');
|
|
39
|
+
const { canonicalHash } = require('./lib/canonical-hash');
|
|
40
|
+
const { auditarEscritura } = require('./lib/audit-evolved');
|
|
39
41
|
const { detectarStack, filtrarReglasPorStack } = require('./lib/detectar-stack');
|
|
40
42
|
const { actualizarGitignore, entradasParaRuntime, limpiarTracked, leerManifest, escribirManifest } = require('./lib/gitignore-manifest');
|
|
41
43
|
|
|
@@ -575,24 +577,12 @@ async function install(opciones) {
|
|
|
575
577
|
}
|
|
576
578
|
console.log('');
|
|
577
579
|
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
const tieneSkillEvolucionado = evolved.some((e) => path.basename(e.path) === 'SKILL.md');
|
|
585
|
-
if (targetCopiaDirectorios && tieneSkillEvolucionado) {
|
|
586
|
-
console.log(
|
|
587
|
-
` ⚠ Aviso: en target "${target}" las habilidades se copian como directorio. Las ` +
|
|
588
|
-
`evoluciones de SKILL.md serán sobreescritas. Backup de seguridad creado en ` +
|
|
589
|
-
`.planning/backups/v${versionAnterior || 'previa'}/.`
|
|
590
|
-
);
|
|
591
|
-
console.log(
|
|
592
|
-
' (Ver DT-EVOL-DIR en .planning/DEUDA-TECNICA.md para el plan de cierre.)'
|
|
593
|
-
);
|
|
594
|
-
console.log('');
|
|
595
|
-
}
|
|
580
|
+
// Fase 16 (REQ-16-09, cierra DT-EVOL-DIR): el path de habilidades —incluido
|
|
581
|
+
// cursor/codex que copian el directorio completo— ahora pasa por el
|
|
582
|
+
// discriminador A/B a nivel de SKILL.md en instalarArchivo: la evolución
|
|
583
|
+
// del usuario (A) se preserva (merge + diff) y solo los recursos se
|
|
584
|
+
// actualizan; shipped-evolved (B) se actualiza con backup + auditoría. Ya
|
|
585
|
+
// NO hay sobreescritura silenciosa de SKILL.md evolucionado.
|
|
596
586
|
}
|
|
597
587
|
}
|
|
598
588
|
|
|
@@ -1166,47 +1156,121 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
|
|
|
1166
1156
|
return { instalado: false, protegido: true };
|
|
1167
1157
|
}
|
|
1168
1158
|
|
|
1169
|
-
// Evolución:
|
|
1159
|
+
// Evolución (Fase 16): decidir estrategia para componentes evolved SIN exigir
|
|
1160
|
+
// --force. El discriminador A/B garantiza el invariante: A (evolución del
|
|
1161
|
+
// usuario) → merge (preserve + diff), JAMÁS overwrite; B (shipped-evolved
|
|
1162
|
+
// intacto, hash baseline coincide) → overwrite con backup + auditoría.
|
|
1170
1163
|
const esComponenteEvolucionable = ['agentes', 'habilidades', 'comandos', 'reglas'].includes(archivo.tipo);
|
|
1171
|
-
|
|
1172
|
-
|
|
1164
|
+
let forzarEscrituraEvolved = false;
|
|
1165
|
+
if (esComponenteEvolucionable && fs.existsSync(destino)) {
|
|
1173
1166
|
const origenAbsoluto = path.resolve(RAIZ_PKG, archivo.origen);
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1167
|
+
// Las habilidades son DIRECTORIOS: la marca evolved vive en su SKILL.md.
|
|
1168
|
+
// Discriminar sobre el SKILL.md interno, no sobre el directorio (REQ-16-09,
|
|
1169
|
+
// cierra DT-EVOL-DIR — cursor/codex copiaban el dir completo y pisaban la
|
|
1170
|
+
// evolución del usuario).
|
|
1171
|
+
const esDirSkill = archivo.tipo === 'habilidades';
|
|
1172
|
+
const cmpDestino = esDirSkill ? path.join(destino, 'SKILL.md') : destino;
|
|
1173
|
+
const cmpOrigen = esDirSkill ? path.join(origenAbsoluto, 'SKILL.md') : origenAbsoluto;
|
|
1174
|
+
const versionBackup = opciones.versionAnterior || opciones.versionActual || 'previa';
|
|
1175
|
+
|
|
1176
|
+
// MEDIO (nemesis O6): dir de skill existe pero sin SKILL.md = instalación
|
|
1177
|
+
// corrupta. Diagnóstico explícito (cae al flujo normal, no rompe invariante).
|
|
1178
|
+
if (esDirSkill && !fs.existsSync(cmpDestino)) {
|
|
1179
|
+
console.log(` ! Skill ${nombreArchivo}: directorio existe pero SKILL.md faltante — instalación incompleta; usa --force para reparar`);
|
|
1178
1180
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1181
|
+
|
|
1182
|
+
if (fs.existsSync(cmpDestino)) {
|
|
1183
|
+
const strategy = decideUpdateStrategy(cmpDestino, cmpOrigen, opciones.versionActual || '', { manifestPath: opciones.manifestPath });
|
|
1184
|
+
const hashAntes = (() => { try { return canonicalHash(fs.readFileSync(cmpDestino, 'utf8')); } catch { return null; } })();
|
|
1185
|
+
|
|
1186
|
+
if (strategy.strategy === 'preserve') {
|
|
1187
|
+
console.log(` ★ Evolucionado: ${nombreArchivo} — preservado (${strategy.reason})`);
|
|
1188
|
+
return { instalado: false, evolucionado: true, destino };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (strategy.strategy === 'conflict') {
|
|
1192
|
+
// Población A — evolución del usuario. NUNCA overwrite: backup + merge (diff).
|
|
1193
|
+
const backup = crearBackup(process.cwd(), cmpDestino, versionBackup);
|
|
1194
|
+
if (!backup.respaldado) {
|
|
1195
|
+
console.log(` ⚠ Backup falló (${backup.error || 'desconocido'}) para ${nombreArchivo} — merge abortado, original preservado`);
|
|
1196
|
+
auditarEscritura({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'A', accion: 'preserve', hashAntes, evidencia: `backup falló: ${backup.error || 'desconocido'}` });
|
|
1197
|
+
return { instalado: false, evolucionado: true, destino, error: 'backup-failed' };
|
|
1194
1198
|
}
|
|
1199
|
+
const backupPath = backup.rutaBackup;
|
|
1200
|
+
console.log(` ⚠ Conflicto: ${nombreArchivo} — evolución del usuario (${strategy.reason})`);
|
|
1201
|
+
console.log(` ↩ Backup: ${path.relative(process.cwd(), backupPath)}`);
|
|
1202
|
+
if (opciones.estado) {
|
|
1203
|
+
registrarBackup(opciones.estado, { rutaOriginal: cmpDestino, rutaBackup: backupPath, version: versionBackup, evolucionado: true });
|
|
1204
|
+
}
|
|
1205
|
+
// Diff centralizado bajo el root del install (no adyacente al comando/skill).
|
|
1206
|
+
const reconcileDir = path.join(rutas.base || process.cwd(), '.planning', 'evolution', 'reconcile');
|
|
1207
|
+
const merge = mergeEvolved(cmpDestino, cmpOrigen, opciones.versionActual || '', { diffDir: reconcileDir });
|
|
1208
|
+
if (merge.merged && merge.diffPath) {
|
|
1209
|
+
console.log(` ⊕ Diff generado: ${path.relative(process.cwd(), merge.diffPath)} (${merge.diffsCount} mutaciones)`);
|
|
1210
|
+
console.log(` → Re-aplicar con /swl:autoresearch ${nombreArchivo} sobre v${opciones.versionActual}`);
|
|
1211
|
+
}
|
|
1212
|
+
// Habilidad: actualizar recursos del directorio PRESERVANDO el SKILL.md
|
|
1213
|
+
// evolucionado del usuario (los demás archivos sí se actualizan).
|
|
1214
|
+
let recursosActualizados = false;
|
|
1215
|
+
if (esDirSkill) {
|
|
1216
|
+
try {
|
|
1217
|
+
copiarDirectorio(origenAbsoluto, destino, { excepto: new Set(['SKILL.md']) });
|
|
1218
|
+
recursosActualizados = true;
|
|
1219
|
+
} catch (e) { console.log(` ⚠ No se pudieron actualizar recursos del skill: ${e.message}`); }
|
|
1220
|
+
}
|
|
1221
|
+
const aud = auditarEscritura({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'A', accion: 'merge', hashAntes, backupPath, evidencia: strategy.reason });
|
|
1222
|
+
if (!aud.registrado) console.log(` ⚠ Auditoría no registrada: ${aud.error}`);
|
|
1223
|
+
return { instalado: recursosActualizados, evolucionado: true, destino };
|
|
1195
1224
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1225
|
+
|
|
1226
|
+
if (strategy.strategy === 'overwrite' && strategy.origin === 'shipped') {
|
|
1227
|
+
// Población B — shipped-evolved intacto. Actualización segura con backup.
|
|
1228
|
+
let hashDespues = null;
|
|
1229
|
+
try { hashDespues = canonicalHash(fs.readFileSync(cmpOrigen, 'utf8')); } catch { /* best-effort */ }
|
|
1230
|
+
|
|
1231
|
+
if (esDirSkill) {
|
|
1232
|
+
// ALTO (nemesis O6): respaldar el DIRECTORIO COMPLETO (recursos incluidos)
|
|
1233
|
+
// antes de sobrescribir — el usuario pudo modificar recursos/ aunque no
|
|
1234
|
+
// tocara el SKILL.md. Overwrite controlado en-bloque (no fall-through).
|
|
1235
|
+
const backupDir = path.join(process.cwd(), '.planning', 'backups', `v${versionBackup}`, path.relative(process.cwd(), destino));
|
|
1236
|
+
let backupOk = false;
|
|
1237
|
+
try { copiarDirectorio(destino, backupDir); backupOk = true; } catch (e) { console.log(` ⚠ Backup de directorio falló (${e.message})`); }
|
|
1238
|
+
if (!backupOk) {
|
|
1239
|
+
auditarEscritura({ archivo: path.relative(process.cwd(), destino), clasificacion: 'B', accion: 'preserve', hashAntes, evidencia: 'backup de directorio falló' });
|
|
1240
|
+
return { instalado: false, evolucionado: true, destino, error: 'backup-failed' };
|
|
1241
|
+
}
|
|
1242
|
+
console.log(` ⇪ Actualizado (shipped-evolved): ${nombreArchivo} — ${strategy.reason}`);
|
|
1243
|
+
if (opciones.estado) registrarBackup(opciones.estado, { rutaOriginal: destino, rutaBackup: backupDir, version: versionBackup, evolucionado: true });
|
|
1244
|
+
const aud = auditarEscritura({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'B', accion: 'overwrite', hashAntes, hashDespues, backupPath: backupDir, evidencia: strategy.reason });
|
|
1245
|
+
if (!aud.registrado) console.log(` ⚠ Auditoría no registrada: ${aud.error}`);
|
|
1246
|
+
copiarDirectorio(origenAbsoluto, destino); // overwrite dir completo (recursos + SKILL.md)
|
|
1247
|
+
return { instalado: true, evolucionado: true, destino };
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Componente-archivo (agentes/comandos/reglas): backup del archivo + fall-through.
|
|
1251
|
+
const backup = crearBackup(process.cwd(), cmpDestino, versionBackup);
|
|
1252
|
+
// H1 (nemesis O5): si el backup falla, NO sobrescribir — preservar original.
|
|
1253
|
+
if (!backup.respaldado) {
|
|
1254
|
+
console.log(` ⚠ Backup falló (${backup.error || 'desconocido'}) para ${nombreArchivo} — overwrite abortado, original preservado`);
|
|
1255
|
+
auditarEscritura({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'B', accion: 'preserve', hashAntes, evidencia: `backup falló: ${backup.error || 'desconocido'}` });
|
|
1256
|
+
return { instalado: false, evolucionado: true, destino, error: 'backup-failed' };
|
|
1257
|
+
}
|
|
1258
|
+
const backupPath = backup.rutaBackup;
|
|
1259
|
+
console.log(` ⇪ Actualizado (shipped-evolved): ${nombreArchivo} — ${strategy.reason}`);
|
|
1260
|
+
if (opciones.estado) {
|
|
1261
|
+
registrarBackup(opciones.estado, { rutaOriginal: cmpDestino, rutaBackup: backupPath, version: versionBackup, evolucionado: true });
|
|
1262
|
+
}
|
|
1263
|
+
const aud = auditarEscritura({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'B', accion: 'overwrite', hashAntes, hashDespues, backupPath, evidencia: strategy.reason });
|
|
1264
|
+
if (!aud.registrado) console.log(` ⚠ Auditoría no registrada: ${aud.error}`);
|
|
1265
|
+
forzarEscrituraEvolved = true; // bypassa la verificación de colisión sin --force
|
|
1204
1266
|
}
|
|
1267
|
+
// strategy === 'overwrite' sin origin shipped = destino no evolucionado →
|
|
1268
|
+
// sigue al flujo normal de colisión/force más abajo.
|
|
1205
1269
|
}
|
|
1206
1270
|
}
|
|
1207
1271
|
|
|
1208
1272
|
// Verificar colisión
|
|
1209
|
-
if (fs.existsSync(destino) && !opciones.force) {
|
|
1273
|
+
if (fs.existsSync(destino) && !opciones.force && !forzarEscrituraEvolved) {
|
|
1210
1274
|
if (typeof opciones.onProgress === 'function') {
|
|
1211
1275
|
opciones.onProgress({
|
|
1212
1276
|
tipo: 'colision',
|
|
@@ -1219,8 +1283,9 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
|
|
|
1219
1283
|
return { instalado: false, colision: true };
|
|
1220
1284
|
}
|
|
1221
1285
|
|
|
1222
|
-
// Nivel 2: Backup antes de sobreescribir con --force
|
|
1223
|
-
|
|
1286
|
+
// Nivel 2: Backup antes de sobreescribir con --force (el caso B shipped-evolved
|
|
1287
|
+
// ya hizo su propio backup arriba; no duplicar).
|
|
1288
|
+
if (fs.existsSync(destino) && opciones.force && opciones.versionAnterior && !forzarEscrituraEvolved) {
|
|
1224
1289
|
const backup = crearBackup(process.cwd(), destino, opciones.versionAnterior);
|
|
1225
1290
|
if (backup.respaldado) {
|
|
1226
1291
|
if (typeof opciones.onProgress === 'function') {
|
|
@@ -1305,18 +1370,24 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
|
|
|
1305
1370
|
/**
|
|
1306
1371
|
* Copia un directorio recursivamente
|
|
1307
1372
|
*/
|
|
1308
|
-
function copiarDirectorio(origen, destino) {
|
|
1373
|
+
function copiarDirectorio(origen, destino, opts = {}) {
|
|
1309
1374
|
if (!fs.existsSync(destino)) {
|
|
1310
1375
|
fs.mkdirSync(destino, { recursive: true });
|
|
1311
1376
|
}
|
|
1312
1377
|
|
|
1378
|
+
// `excepto` (Fase 16): nombres de archivo del nivel superior a NO copiar.
|
|
1379
|
+
// Se usa para preservar el SKILL.md evolucionado del usuario (población A)
|
|
1380
|
+
// mientras se actualizan los demás recursos del directorio del skill.
|
|
1381
|
+
const excepto = opts.excepto instanceof Set ? opts.excepto : null;
|
|
1382
|
+
|
|
1313
1383
|
const entradas = fs.readdirSync(origen, { withFileTypes: true });
|
|
1314
1384
|
for (const entrada of entradas) {
|
|
1385
|
+
if (excepto && excepto.has(entrada.name)) continue;
|
|
1315
1386
|
const src = path.join(origen, entrada.name);
|
|
1316
1387
|
const dst = path.join(destino, entrada.name);
|
|
1317
1388
|
|
|
1318
1389
|
if (entrada.isDirectory()) {
|
|
1319
|
-
copiarDirectorio(src, dst);
|
|
1390
|
+
copiarDirectorio(src, dst); // exclusión solo aplica al nivel superior
|
|
1320
1391
|
} else {
|
|
1321
1392
|
fs.copyFileSync(src, dst);
|
|
1322
1393
|
}
|
|
@@ -1371,3 +1442,5 @@ function limpiarReglasSinStack(dirReglas, stackDetectado) {
|
|
|
1371
1442
|
}
|
|
1372
1443
|
|
|
1373
1444
|
module.exports = install;
|
|
1445
|
+
// Export para tests del wiring de evolución (Fase 16 T-19).
|
|
1446
|
+
module.exports.instalarArchivo = instalarArchivo;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* audit-evolved — Registro append-only de escrituras del instalador sobre
|
|
5
|
+
* componentes evolucionados (Fase 16, REQ-16-08).
|
|
6
|
+
*
|
|
7
|
+
* Toda operación que escriba sobre un componente `evolved` (overwrite de un
|
|
8
|
+
* shipped-evolved B, o merge de una evolución del usuario A) debe dejar rastro
|
|
9
|
+
* en `.planning/AUDITORIA.md` con: archivo, clasificación A/B, acción, hashes
|
|
10
|
+
* antes/después y ruta del backup. Cumple `reglas/gobernanza.md § Auditoría`
|
|
11
|
+
* (append-only) y `reglas/seguridad-agentes.md § no-degradación-silenciosa`.
|
|
12
|
+
*
|
|
13
|
+
* Zero-deps. AUDITORIA.md es append-only → fs.appendFileSync (no atomicWrite,
|
|
14
|
+
* que reescribiría todo el archivo).
|
|
15
|
+
*
|
|
16
|
+
* @module scripts/lib/audit-evolved
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const HEADER = '# AUDITORIA\n\n' +
|
|
23
|
+
'Registro append-only de operaciones de alto riesgo (no borrar entradas).\n';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Appenda una entrada de auditoría por escritura sobre un componente evolved.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} e
|
|
29
|
+
* @param {string} e.archivo - Ruta (relativa) del componente afectado.
|
|
30
|
+
* @param {'A'|'B'} e.clasificacion - A = evolución del usuario (merge); B = shipped (overwrite).
|
|
31
|
+
* @param {string} e.accion - 'overwrite' | 'merge' | 'preserve'.
|
|
32
|
+
* @param {string} [e.hashAntes] - Hash canónico previo.
|
|
33
|
+
* @param {string} [e.hashDespues] - Hash canónico posterior.
|
|
34
|
+
* @param {string} [e.backupPath] - Ruta del backup creado antes de escribir.
|
|
35
|
+
* @param {string} [e.evidencia] - Razón/criterio que clasificó A vs B.
|
|
36
|
+
* @param {string} [e.timestamp] - ISO; default `new Date().toISOString()`.
|
|
37
|
+
* @param {string} [e.cwd] - Raíz del proyecto; default process.cwd().
|
|
38
|
+
* @returns {{ registrado: boolean, ruta: string }}
|
|
39
|
+
*/
|
|
40
|
+
function auditarEscritura(e) {
|
|
41
|
+
const cwd = e.cwd || process.cwd();
|
|
42
|
+
const ruta = path.join(cwd, '.planning', 'AUDITORIA.md');
|
|
43
|
+
try {
|
|
44
|
+
fs.mkdirSync(path.dirname(ruta), { recursive: true });
|
|
45
|
+
if (!fs.existsSync(ruta)) fs.writeFileSync(ruta, HEADER, 'utf8');
|
|
46
|
+
|
|
47
|
+
const ts = e.timestamp || new Date().toISOString();
|
|
48
|
+
const corto = (h) => (h ? String(h).slice(0, 12) : 'n/a');
|
|
49
|
+
const lineas = [
|
|
50
|
+
'',
|
|
51
|
+
`## [${ts}] evolved-${e.accion}`,
|
|
52
|
+
'',
|
|
53
|
+
`**Agente**: instalador`,
|
|
54
|
+
`**Archivo**: ${e.archivo}`,
|
|
55
|
+
`**Clasificación**: ${e.clasificacion === 'A' ? 'A (evolución del usuario — merge)' : 'B (shipped-evolved — overwrite)'}`,
|
|
56
|
+
`**Acción**: ${e.accion}`,
|
|
57
|
+
`**Hash antes**: ${corto(e.hashAntes)}`,
|
|
58
|
+
`**Hash después**: ${corto(e.hashDespues)}`,
|
|
59
|
+
`**Backup**: ${e.backupPath ? path.relative(cwd, e.backupPath).replace(/\\/g, '/') : 'n/a'}`,
|
|
60
|
+
`**Evidencia**: ${e.evidencia || 'n/a'}`,
|
|
61
|
+
'',
|
|
62
|
+
];
|
|
63
|
+
fs.appendFileSync(ruta, lineas.join('\n'), 'utf8');
|
|
64
|
+
return { registrado: true, ruta };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
// No-fallback-silencioso: el caller debe ver el fallo de auditoría.
|
|
67
|
+
return { registrado: false, ruta, error: err.message };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { auditarEscritura };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auditar-invocaciones-comandos.js
|
|
5
|
+
*
|
|
6
|
+
* Gate anti-regresión de la clase de bug "invocación relativa al proyecto en
|
|
7
|
+
* comandos user-facing" (ver docs/invocacion-cli-cross-scope.md).
|
|
8
|
+
*
|
|
9
|
+
* Un comando que corre en el proyecto del usuario NUNCA debe invocar scripts SWL
|
|
10
|
+
* con ruta relativa al proyecto (`node scripts/lib/...`, `node hooks/...`,
|
|
11
|
+
* `require('./scripts/...')`): esas rutas no existen downstream. Debe usar el
|
|
12
|
+
* subcomando del CLI (`swl-ses <sub>` / `npx ... <sub>` / `node scripts/cli/<sub>.js`).
|
|
13
|
+
*
|
|
14
|
+
* Solo escanea bloques de código (``` ... ```) para no marcar menciones
|
|
15
|
+
* prohibitivas en prosa. Excluye `scripts/cli/` (forma repo-madre sancionada).
|
|
16
|
+
*
|
|
17
|
+
* Función pura + entrypoint CLI con exit 1 si hay violaciones.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
// Comandos que corren SOLO en el repo madre de swl-ses (el CWD ES el repo, así
|
|
24
|
+
// que `node scripts/...` relativo es correcto). NO son user-facing.
|
|
25
|
+
const SELF_DEV = new Set([
|
|
26
|
+
'release',
|
|
27
|
+
'contribuir',
|
|
28
|
+
'evaluar-skill',
|
|
29
|
+
'reflect-skills',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// Patrones de invocación relativa al proyecto que se rompen downstream.
|
|
33
|
+
// Se evalúan SOLO dentro de bloques de código.
|
|
34
|
+
const PATRONES = [
|
|
35
|
+
{ re: /\bnode\s+(?:\.\/)?scripts\/(?!cli\/)/, desc: 'node scripts/ (usa swl-ses <sub>)' },
|
|
36
|
+
{ re: /\bnode\s+(?:\.\/)?hooks\//, desc: 'node hooks/ (usa swl-ses <sub>)' },
|
|
37
|
+
{ re: /require\(\s*['"]\.\.?\/scripts\//, desc: "require('./scripts/ inline" },
|
|
38
|
+
{ re: /require\(\s*['"]\.\.?\/hooks\//, desc: "require('./hooks/ inline" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} [comandosDir] default: comandos/swl relativo al repo de este script.
|
|
43
|
+
* @returns {{ok:boolean, violaciones:Array<{comando:string,linea:number,texto:string,patron:string}>, escaneados:number}}
|
|
44
|
+
*/
|
|
45
|
+
function auditarInvocacionesComandos(comandosDir) {
|
|
46
|
+
const dir = comandosDir || path.join(__dirname, '..', '..', 'comandos', 'swl');
|
|
47
|
+
const violaciones = [];
|
|
48
|
+
let escaneados = 0;
|
|
49
|
+
|
|
50
|
+
let archivos;
|
|
51
|
+
try {
|
|
52
|
+
archivos = fs.readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('_'));
|
|
53
|
+
} catch {
|
|
54
|
+
return { ok: true, violaciones, escaneados: 0 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const archivo of archivos) {
|
|
58
|
+
const comando = archivo.replace(/\.md$/, '');
|
|
59
|
+
if (SELF_DEV.has(comando)) continue;
|
|
60
|
+
escaneados++;
|
|
61
|
+
|
|
62
|
+
const contenido = fs.readFileSync(path.join(dir, archivo), 'utf8');
|
|
63
|
+
const lineas = contenido.split('\n');
|
|
64
|
+
let dentroCodigo = false;
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
67
|
+
const linea = lineas[i];
|
|
68
|
+
if (/^\s*```/.test(linea)) {
|
|
69
|
+
dentroCodigo = !dentroCodigo;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!dentroCodigo) continue;
|
|
73
|
+
for (const { re, desc } of PATRONES) {
|
|
74
|
+
if (re.test(linea)) {
|
|
75
|
+
violaciones.push({
|
|
76
|
+
comando,
|
|
77
|
+
linea: i + 1,
|
|
78
|
+
texto: linea.trim(),
|
|
79
|
+
patron: desc,
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ok: violaciones.length === 0, violaciones, escaneados };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { auditarInvocacionesComandos, SELF_DEV };
|
|
91
|
+
|
|
92
|
+
if (require.main === module) {
|
|
93
|
+
const r = auditarInvocacionesComandos();
|
|
94
|
+
if (r.ok) {
|
|
95
|
+
console.log(`[OK] invocaciones-comandos — ${r.escaneados} comandos user-facing sin rutas relativas al proyecto`);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
console.error(`[FALLA] invocaciones-comandos — ${r.violaciones.length} invocación(es) relativa(s) al proyecto en comandos user-facing:`);
|
|
99
|
+
for (const v of r.violaciones) {
|
|
100
|
+
console.error(` ${v.comando}.md:${v.linea} [${v.patron}] ${v.texto}`);
|
|
101
|
+
}
|
|
102
|
+
console.error('\nUsa el patrón cross-scope (docs/invocacion-cli-cross-scope.md): swl-ses <sub> / npx ... <sub> / node scripts/cli/<sub>.js');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|