@saulwade/swl-ses 2.2.0 → 2.2.3
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-mcp-server.js +1 -1
- package/comandos/swl/adoptar-proyecto.md +253 -258
- package/comandos/swl/aprender.md +823 -828
- package/comandos/swl/claudemd.md +234 -239
- package/comandos/swl/ejecutar-fase.md +0 -5
- package/comandos/swl/nuevo-proyecto.md +200 -205
- package/comandos/swl/release.md +19 -5
- package/comandos/swl/revisar-impacto.md +0 -5
- 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 +0 -5
- 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/etapa-perfil-usuario.js +1 -1
- package/hooks/lib/evolution-tracker.js +191 -35
- package/hooks/resumen-sesion.js +4 -4
- package/llms.txt +1 -1
- package/manifiestos/canonical-hashes.json +1310 -0
- package/manifiestos/modulos.json +3 -0
- package/manifiestos/skills-lock.json +70 -70
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/doctor.js +13 -0
- package/scripts/generar-canonical-hashes.js +147 -0
- package/scripts/instalador.js +140 -54
- package/scripts/lib/audit-evolved.js +76 -0
- package/scripts/lib/canonical-hash.js +94 -0
- package/scripts/lib/evolved-fuente.js +138 -0
- package/scripts/lib/manifiestos.js +1 -1
- package/scripts/publicar.js +42 -5
- package/scripts/remediar-evolved-instaladas.js +242 -0
- package/scripts/validar.js +14 -0
- package/scripts/vendor/claude-usage/__pycache__/scanner.cpython-314.pyc +0 -0
- package/scripts/verificar-evolucion.js +36 -0
- package/scripts/verificar-release.js +33 -0
- 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
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
|
|
|
@@ -616,6 +606,7 @@ async function install(opciones) {
|
|
|
616
606
|
try {
|
|
617
607
|
const resultado = instalarArchivo(archivo, rutas, runtime, {
|
|
618
608
|
force,
|
|
609
|
+
esGlobal,
|
|
619
610
|
versionAnterior,
|
|
620
611
|
versionActual: VERSION,
|
|
621
612
|
estado,
|
|
@@ -693,7 +684,7 @@ async function install(opciones) {
|
|
|
693
684
|
// Instalar userland (prioridad sobre core)
|
|
694
685
|
for (const archivo of archivosUserland) {
|
|
695
686
|
try {
|
|
696
|
-
const resultado = instalarArchivo(archivo, rutas, runtime, { force: true });
|
|
687
|
+
const resultado = instalarArchivo(archivo, rutas, runtime, { force: true, esGlobal });
|
|
697
688
|
if (resultado.instalado) {
|
|
698
689
|
registrarArchivo(estado, {
|
|
699
690
|
origen: archivo.rutaRelativa,
|
|
@@ -1166,47 +1157,133 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
|
|
|
1166
1157
|
return { instalado: false, protegido: true };
|
|
1167
1158
|
}
|
|
1168
1159
|
|
|
1169
|
-
// Evolución:
|
|
1160
|
+
// Evolución (Fase 16): decidir estrategia para componentes evolved SIN exigir
|
|
1161
|
+
// --force. El discriminador A/B garantiza el invariante: A (evolución del
|
|
1162
|
+
// usuario) → merge (preserve + diff), JAMÁS overwrite; B (shipped-evolved
|
|
1163
|
+
// intacto, hash baseline coincide) → overwrite con backup + auditoría.
|
|
1170
1164
|
const esComponenteEvolucionable = ['agentes', 'habilidades', 'comandos', 'reglas'].includes(archivo.tipo);
|
|
1171
|
-
|
|
1172
|
-
|
|
1165
|
+
let forzarEscrituraEvolved = false;
|
|
1166
|
+
if (esComponenteEvolucionable && fs.existsSync(destino)) {
|
|
1173
1167
|
const origenAbsoluto = path.resolve(RAIZ_PKG, archivo.origen);
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1168
|
+
// Las habilidades son DIRECTORIOS: la marca evolved vive en su SKILL.md.
|
|
1169
|
+
// Discriminar sobre el SKILL.md interno, no sobre el directorio (REQ-16-09,
|
|
1170
|
+
// cierra DT-EVOL-DIR — cursor/codex copiaban el dir completo y pisaban la
|
|
1171
|
+
// evolución del usuario).
|
|
1172
|
+
const esDirSkill = archivo.tipo === 'habilidades';
|
|
1173
|
+
const cmpDestino = esDirSkill ? path.join(destino, 'SKILL.md') : destino;
|
|
1174
|
+
const cmpOrigen = esDirSkill ? path.join(origenAbsoluto, 'SKILL.md') : origenAbsoluto;
|
|
1175
|
+
const versionBackup = opciones.versionAnterior || opciones.versionActual || 'previa';
|
|
1176
|
+
|
|
1177
|
+
// C-3: los artefactos de gobernanza evolved (AUDITORIA.md + diffs de
|
|
1178
|
+
// reconciliación) se enraízan en la base del install. En scope GLOBAL
|
|
1179
|
+
// (~/.claude, ~/.codex) usan `.swl-evolved-backups/` para NO crear un dir
|
|
1180
|
+
// `.planning/` de proyecto dentro del dir de config global; en proyecto
|
|
1181
|
+
// usan `.planning/` (gobernanza.md).
|
|
1182
|
+
const govBase = rutas.base || process.cwd();
|
|
1183
|
+
const govSubdir = opciones.esGlobal ? '.swl-evolved-backups' : '.planning';
|
|
1184
|
+
const reconcileDir = govSubdir === '.planning'
|
|
1185
|
+
? path.join(govBase, '.planning', 'evolution', 'reconcile')
|
|
1186
|
+
: path.join(govBase, '.swl-evolved-backups', 'reconcile');
|
|
1187
|
+
const auditEvolved = (e) => auditarEscritura({ ...e, cwd: govBase, subdir: govSubdir });
|
|
1188
|
+
|
|
1189
|
+
// MEDIO (nemesis O6): dir de skill existe pero sin SKILL.md = instalación
|
|
1190
|
+
// corrupta. Diagnóstico explícito (cae al flujo normal, no rompe invariante).
|
|
1191
|
+
if (esDirSkill && !fs.existsSync(cmpDestino)) {
|
|
1192
|
+
console.log(` ! Skill ${nombreArchivo}: directorio existe pero SKILL.md faltante — instalación incompleta; usa --force para reparar`);
|
|
1178
1193
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
+
|
|
1195
|
+
if (fs.existsSync(cmpDestino)) {
|
|
1196
|
+
const strategy = decideUpdateStrategy(cmpDestino, cmpOrigen, opciones.versionActual || '', { manifestPath: opciones.manifestPath });
|
|
1197
|
+
const hashAntes = (() => { try { return canonicalHash(fs.readFileSync(cmpDestino, 'utf8')); } catch { return null; } })();
|
|
1198
|
+
|
|
1199
|
+
if (strategy.strategy === 'preserve') {
|
|
1200
|
+
console.log(` ★ Evolucionado: ${nombreArchivo} — preservado (${strategy.reason})`);
|
|
1201
|
+
return { instalado: false, evolucionado: true, destino };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (strategy.strategy === 'conflict') {
|
|
1205
|
+
// Población A — evolución del usuario. NUNCA overwrite: backup + merge (diff).
|
|
1206
|
+
const backup = crearBackup(process.cwd(), cmpDestino, versionBackup);
|
|
1207
|
+
if (!backup.respaldado) {
|
|
1208
|
+
console.log(` ⚠ Backup falló (${backup.error || 'desconocido'}) para ${nombreArchivo} — merge abortado, original preservado`);
|
|
1209
|
+
auditEvolved({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'A', accion: 'preserve', hashAntes, evidencia: `backup falló: ${backup.error || 'desconocido'}` });
|
|
1210
|
+
return { instalado: false, evolucionado: true, destino, error: 'backup-failed' };
|
|
1194
1211
|
}
|
|
1212
|
+
const backupPath = backup.rutaBackup;
|
|
1213
|
+
console.log(` ⚠ Conflicto: ${nombreArchivo} — evolución del usuario (${strategy.reason})`);
|
|
1214
|
+
console.log(` ↩ Backup: ${path.relative(process.cwd(), backupPath)}`);
|
|
1215
|
+
if (opciones.estado) {
|
|
1216
|
+
registrarBackup(opciones.estado, { rutaOriginal: cmpDestino, rutaBackup: backupPath, version: versionBackup, evolucionado: true });
|
|
1217
|
+
}
|
|
1218
|
+
// Diff centralizado bajo el root del install (reconcileDir calculado arriba,
|
|
1219
|
+
// scope-aware: .planning en proyecto, .swl-evolved-backups en global).
|
|
1220
|
+
const merge = mergeEvolved(cmpDestino, cmpOrigen, opciones.versionActual || '', { diffDir: reconcileDir });
|
|
1221
|
+
if (merge.merged && merge.diffPath) {
|
|
1222
|
+
console.log(` ⊕ Diff generado: ${path.relative(process.cwd(), merge.diffPath)} (${merge.diffsCount} mutaciones)`);
|
|
1223
|
+
console.log(` → Re-aplicar con /swl:autoresearch ${nombreArchivo} sobre v${opciones.versionActual}`);
|
|
1224
|
+
}
|
|
1225
|
+
// Habilidad: actualizar recursos del directorio PRESERVANDO el SKILL.md
|
|
1226
|
+
// evolucionado del usuario (los demás archivos sí se actualizan).
|
|
1227
|
+
let recursosActualizados = false;
|
|
1228
|
+
if (esDirSkill) {
|
|
1229
|
+
try {
|
|
1230
|
+
copiarDirectorio(origenAbsoluto, destino, { excepto: new Set(['SKILL.md']) });
|
|
1231
|
+
recursosActualizados = true;
|
|
1232
|
+
} catch (e) { console.log(` ⚠ No se pudieron actualizar recursos del skill: ${e.message}`); }
|
|
1233
|
+
}
|
|
1234
|
+
const aud = auditEvolved({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'A', accion: 'merge', hashAntes, backupPath, evidencia: strategy.reason });
|
|
1235
|
+
if (!aud.registrado) console.log(` ⚠ Auditoría no registrada: ${aud.error}`);
|
|
1236
|
+
return { instalado: recursosActualizados, evolucionado: true, destino };
|
|
1195
1237
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1238
|
+
|
|
1239
|
+
if (strategy.strategy === 'overwrite' && strategy.origin === 'shipped') {
|
|
1240
|
+
// Población B — shipped-evolved intacto. Actualización segura con backup.
|
|
1241
|
+
let hashDespues = null;
|
|
1242
|
+
try { hashDespues = canonicalHash(fs.readFileSync(cmpOrigen, 'utf8')); } catch { /* best-effort */ }
|
|
1243
|
+
|
|
1244
|
+
if (esDirSkill) {
|
|
1245
|
+
// ALTO (nemesis O6): respaldar el DIRECTORIO COMPLETO (recursos incluidos)
|
|
1246
|
+
// antes de sobrescribir — el usuario pudo modificar recursos/ aunque no
|
|
1247
|
+
// tocara el SKILL.md. Overwrite controlado en-bloque (no fall-through).
|
|
1248
|
+
const backupDir = path.join(process.cwd(), '.planning', 'backups', `v${versionBackup}`, path.relative(process.cwd(), destino));
|
|
1249
|
+
let backupOk = false;
|
|
1250
|
+
try { copiarDirectorio(destino, backupDir); backupOk = true; } catch (e) { console.log(` ⚠ Backup de directorio falló (${e.message})`); }
|
|
1251
|
+
if (!backupOk) {
|
|
1252
|
+
auditEvolved({ archivo: path.relative(process.cwd(), destino), clasificacion: 'B', accion: 'preserve', hashAntes, evidencia: 'backup de directorio falló' });
|
|
1253
|
+
return { instalado: false, evolucionado: true, destino, error: 'backup-failed' };
|
|
1254
|
+
}
|
|
1255
|
+
console.log(` ⇪ Actualizado (shipped-evolved): ${nombreArchivo} — ${strategy.reason}`);
|
|
1256
|
+
if (opciones.estado) registrarBackup(opciones.estado, { rutaOriginal: destino, rutaBackup: backupDir, version: versionBackup, evolucionado: true });
|
|
1257
|
+
const aud = auditEvolved({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'B', accion: 'overwrite', hashAntes, hashDespues, backupPath: backupDir, evidencia: strategy.reason });
|
|
1258
|
+
if (!aud.registrado) console.log(` ⚠ Auditoría no registrada: ${aud.error}`);
|
|
1259
|
+
copiarDirectorio(origenAbsoluto, destino); // overwrite dir completo (recursos + SKILL.md)
|
|
1260
|
+
return { instalado: true, evolucionado: true, destino };
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Componente-archivo (agentes/comandos/reglas): backup del archivo + fall-through.
|
|
1264
|
+
const backup = crearBackup(process.cwd(), cmpDestino, versionBackup);
|
|
1265
|
+
// H1 (nemesis O5): si el backup falla, NO sobrescribir — preservar original.
|
|
1266
|
+
if (!backup.respaldado) {
|
|
1267
|
+
console.log(` ⚠ Backup falló (${backup.error || 'desconocido'}) para ${nombreArchivo} — overwrite abortado, original preservado`);
|
|
1268
|
+
auditEvolved({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'B', accion: 'preserve', hashAntes, evidencia: `backup falló: ${backup.error || 'desconocido'}` });
|
|
1269
|
+
return { instalado: false, evolucionado: true, destino, error: 'backup-failed' };
|
|
1270
|
+
}
|
|
1271
|
+
const backupPath = backup.rutaBackup;
|
|
1272
|
+
console.log(` ⇪ Actualizado (shipped-evolved): ${nombreArchivo} — ${strategy.reason}`);
|
|
1273
|
+
if (opciones.estado) {
|
|
1274
|
+
registrarBackup(opciones.estado, { rutaOriginal: cmpDestino, rutaBackup: backupPath, version: versionBackup, evolucionado: true });
|
|
1275
|
+
}
|
|
1276
|
+
const aud = auditEvolved({ archivo: path.relative(process.cwd(), cmpDestino), clasificacion: 'B', accion: 'overwrite', hashAntes, hashDespues, backupPath, evidencia: strategy.reason });
|
|
1277
|
+
if (!aud.registrado) console.log(` ⚠ Auditoría no registrada: ${aud.error}`);
|
|
1278
|
+
forzarEscrituraEvolved = true; // bypassa la verificación de colisión sin --force
|
|
1204
1279
|
}
|
|
1280
|
+
// strategy === 'overwrite' sin origin shipped = destino no evolucionado →
|
|
1281
|
+
// sigue al flujo normal de colisión/force más abajo.
|
|
1205
1282
|
}
|
|
1206
1283
|
}
|
|
1207
1284
|
|
|
1208
1285
|
// Verificar colisión
|
|
1209
|
-
if (fs.existsSync(destino) && !opciones.force) {
|
|
1286
|
+
if (fs.existsSync(destino) && !opciones.force && !forzarEscrituraEvolved) {
|
|
1210
1287
|
if (typeof opciones.onProgress === 'function') {
|
|
1211
1288
|
opciones.onProgress({
|
|
1212
1289
|
tipo: 'colision',
|
|
@@ -1219,8 +1296,9 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
|
|
|
1219
1296
|
return { instalado: false, colision: true };
|
|
1220
1297
|
}
|
|
1221
1298
|
|
|
1222
|
-
// Nivel 2: Backup antes de sobreescribir con --force
|
|
1223
|
-
|
|
1299
|
+
// Nivel 2: Backup antes de sobreescribir con --force (el caso B shipped-evolved
|
|
1300
|
+
// ya hizo su propio backup arriba; no duplicar).
|
|
1301
|
+
if (fs.existsSync(destino) && opciones.force && opciones.versionAnterior && !forzarEscrituraEvolved) {
|
|
1224
1302
|
const backup = crearBackup(process.cwd(), destino, opciones.versionAnterior);
|
|
1225
1303
|
if (backup.respaldado) {
|
|
1226
1304
|
if (typeof opciones.onProgress === 'function') {
|
|
@@ -1305,18 +1383,24 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
|
|
|
1305
1383
|
/**
|
|
1306
1384
|
* Copia un directorio recursivamente
|
|
1307
1385
|
*/
|
|
1308
|
-
function copiarDirectorio(origen, destino) {
|
|
1386
|
+
function copiarDirectorio(origen, destino, opts = {}) {
|
|
1309
1387
|
if (!fs.existsSync(destino)) {
|
|
1310
1388
|
fs.mkdirSync(destino, { recursive: true });
|
|
1311
1389
|
}
|
|
1312
1390
|
|
|
1391
|
+
// `excepto` (Fase 16): nombres de archivo del nivel superior a NO copiar.
|
|
1392
|
+
// Se usa para preservar el SKILL.md evolucionado del usuario (población A)
|
|
1393
|
+
// mientras se actualizan los demás recursos del directorio del skill.
|
|
1394
|
+
const excepto = opts.excepto instanceof Set ? opts.excepto : null;
|
|
1395
|
+
|
|
1313
1396
|
const entradas = fs.readdirSync(origen, { withFileTypes: true });
|
|
1314
1397
|
for (const entrada of entradas) {
|
|
1398
|
+
if (excepto && excepto.has(entrada.name)) continue;
|
|
1315
1399
|
const src = path.join(origen, entrada.name);
|
|
1316
1400
|
const dst = path.join(destino, entrada.name);
|
|
1317
1401
|
|
|
1318
1402
|
if (entrada.isDirectory()) {
|
|
1319
|
-
copiarDirectorio(src, dst);
|
|
1403
|
+
copiarDirectorio(src, dst); // exclusión solo aplica al nivel superior
|
|
1320
1404
|
} else {
|
|
1321
1405
|
fs.copyFileSync(src, dst);
|
|
1322
1406
|
}
|
|
@@ -1371,3 +1455,5 @@ function limpiarReglasSinStack(dirReglas, stackDetectado) {
|
|
|
1371
1455
|
}
|
|
1372
1456
|
|
|
1373
1457
|
module.exports = install;
|
|
1458
|
+
// Export para tests del wiring de evolución (Fase 16 T-19).
|
|
1459
|
+
module.exports.instalarArchivo = instalarArchivo;
|
|
@@ -0,0 +1,76 @@
|
|
|
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 de la operación; default process.cwd().
|
|
38
|
+
* @param {string} [e.subdir] - Subdir bajo `cwd` donde vive AUDITORIA.md.
|
|
39
|
+
* Default `.planning` (scope proyecto, gobernanza.md). En scope global/runtime
|
|
40
|
+
* (~/.claude, ~/.codex) usar `.swl-evolved-backups` para NO crear un dir
|
|
41
|
+
* `.planning/` de proyecto dentro del dir de config global (C-3 Fase 16.1).
|
|
42
|
+
* @returns {{ registrado: boolean, ruta: string }}
|
|
43
|
+
*/
|
|
44
|
+
function auditarEscritura(e) {
|
|
45
|
+
const cwd = e.cwd || process.cwd();
|
|
46
|
+
const subdir = e.subdir || '.planning';
|
|
47
|
+
const ruta = path.join(cwd, subdir, 'AUDITORIA.md');
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(path.dirname(ruta), { recursive: true });
|
|
50
|
+
if (!fs.existsSync(ruta)) fs.writeFileSync(ruta, HEADER, 'utf8');
|
|
51
|
+
|
|
52
|
+
const ts = e.timestamp || new Date().toISOString();
|
|
53
|
+
const corto = (h) => (h ? String(h).slice(0, 12) : 'n/a');
|
|
54
|
+
const lineas = [
|
|
55
|
+
'',
|
|
56
|
+
`## [${ts}] evolved-${e.accion}`,
|
|
57
|
+
'',
|
|
58
|
+
`**Agente**: instalador`,
|
|
59
|
+
`**Archivo**: ${e.archivo}`,
|
|
60
|
+
`**Clasificación**: ${e.clasificacion === 'A' ? 'A (evolución del usuario — merge)' : 'B (shipped-evolved — overwrite)'}`,
|
|
61
|
+
`**Acción**: ${e.accion}`,
|
|
62
|
+
`**Hash antes**: ${corto(e.hashAntes)}`,
|
|
63
|
+
`**Hash después**: ${corto(e.hashDespues)}`,
|
|
64
|
+
`**Backup**: ${e.backupPath ? path.relative(cwd, e.backupPath).replace(/\\/g, '/') : 'n/a'}`,
|
|
65
|
+
`**Evidencia**: ${e.evidencia || 'n/a'}`,
|
|
66
|
+
'',
|
|
67
|
+
];
|
|
68
|
+
fs.appendFileSync(ruta, lineas.join('\n'), 'utf8');
|
|
69
|
+
return { registrado: true, ruta };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// No-fallback-silencioso: el caller debe ver el fallo de auditoría.
|
|
72
|
+
return { registrado: false, ruta, error: err.message };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { auditarEscritura };
|
|
@@ -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 };
|
|
@@ -93,7 +93,7 @@ function resolverPerfil(nombrePerfil, opciones = {}) {
|
|
|
93
93
|
|
|
94
94
|
// Verificar soporte de target
|
|
95
95
|
if (opciones.target && modulo.targets && !modulo.targets.includes(opciones.target)) {
|
|
96
|
-
warnings.push(`Módulo "${nombreModulo}" no
|
|
96
|
+
warnings.push(`Módulo "${nombreModulo}" no declarado para target "${opciones.target}" (omitido; declarado para: ${modulo.targets.join(', ')})`);
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
99
|
|