@saulwade/swl-ses 2.6.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/CLAUDE.md +14 -2
  2. package/README.md +65 -18
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/bin/swl-ses.js +10 -0
  6. package/comandos/swl/brainstorm.md +1 -0
  7. package/comandos/swl/briefing.md +119 -119
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/deuda-codigo.md +97 -97
  10. package/comandos/swl/mcp-status.md +1 -0
  11. package/gateway/lib/event-channel.js +191 -191
  12. package/habilidades/agent-deep-links/SKILL.md +148 -148
  13. package/habilidades/backend-async-postgres-testing/SKILL.md +216 -216
  14. package/habilidades/backend-error-design/SKILL.md +221 -221
  15. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  16. package/habilidades/calidad-anti-patrones-universales/SKILL.md +105 -1
  17. package/habilidades/calidad-contract-testing/SKILL.md +165 -165
  18. package/habilidades/calidad-mutation-testing/SKILL.md +25 -1
  19. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
  20. package/habilidades/ci-cd-pipelines/SKILL.md +5 -1
  21. package/habilidades/css-moderno/SKILL.md +7 -1
  22. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  23. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  24. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
  25. package/habilidades/extractor-de-aprendizajes/SKILL.md +5 -1
  26. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  27. package/habilidades/harness-claude-code/SKILL.md +3 -2
  28. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  29. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  30. package/habilidades/perfil-usuario/SKILL.md +200 -200
  31. package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
  32. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
  33. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
  34. package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
  35. package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
  36. package/habilidades/proceso-dynamic-workflows/SKILL.md +60 -0
  37. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
  38. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
  39. package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
  40. package/habilidades/proceso-modular-split/SKILL.md +256 -256
  41. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  42. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
  43. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
  44. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
  45. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
  46. package/hooks/calidad-pre-commit.js +159 -10
  47. package/hooks/ciclo-evolucion-subagente.js +26 -26
  48. package/hooks/ciclo-evolucion.js +26 -26
  49. package/hooks/contexto-subagente.js +68 -68
  50. package/hooks/lib/auto-consolidator.js +335 -335
  51. package/hooks/lib/ciclo-evolucion.js +47 -47
  52. package/hooks/lib/deep-links.js +185 -185
  53. package/hooks/lib/error-classifier.js +308 -308
  54. package/hooks/lib/notificacion-formato.js +45 -11
  55. package/hooks/lib/provenance-tracker.js +191 -191
  56. package/hooks/lib/raiz-proyecto.js +35 -4
  57. package/hooks/lib/resource-quota.js +122 -122
  58. package/hooks/lib/retry-jitter.js +165 -165
  59. package/hooks/lib/security-net.js +201 -201
  60. package/hooks/lib/skill-auditor.js +588 -588
  61. package/hooks/lib/sync-status.js +228 -228
  62. package/hooks/lib/taint-tracker.js +107 -107
  63. package/hooks/lib/text-similarity.js +241 -241
  64. package/hooks/lib/toon-compressor.js +245 -245
  65. package/hooks/notificacion-telegram.js +5 -11
  66. package/hooks/session-briefing.js +12 -4
  67. package/instintos/autonomia.yaml +27 -27
  68. package/instintos/prompt-appendices.yaml +57 -57
  69. package/llms.txt +1 -1
  70. package/manifiestos/agent-output-schemas.json +57 -57
  71. package/manifiestos/canonical-hashes.json +662 -0
  72. package/manifiestos/harness-ir.json +47536 -0
  73. package/manifiestos/hooks-config.json +469 -469
  74. package/manifiestos/invariantes-criticos.json +30 -30
  75. package/manifiestos/policy-bundle.json +2065 -0
  76. package/manifiestos/policy-corpus-w2.json +3926 -0
  77. package/manifiestos/runtime-adapters-core3.json +208 -0
  78. package/manifiestos/runtime-conformance.json +139 -0
  79. package/manifiestos/skills-lock.json +43 -43
  80. package/package.json +2 -2
  81. package/plantillas/auditor-veto-template.md +105 -105
  82. package/plantillas/github-workflows/release-please.yml +44 -44
  83. package/plantillas/github-workflows/swl-ci.yml +107 -107
  84. package/plantillas/github-workflows/swl-security.yml +51 -51
  85. package/plugin.json +2 -2
  86. package/reglas/accesibilidad.md +10 -10
  87. package/reglas/auditorias-documentales-estructurales.md +7 -7
  88. package/reglas/cloud-infra.md +8 -8
  89. package/reglas/consultar-vault-primero.md +195 -195
  90. package/reglas/git-workflow.md +1 -0
  91. package/reglas/hooks.md +6 -6
  92. package/reglas/intent-engineering.md +218 -218
  93. package/reglas/markitdown.md +8 -8
  94. package/reglas/monitor-ci.md +12 -0
  95. package/reglas/patrones.md +6 -6
  96. package/reglas/testing.md +7 -7
  97. package/reglas/tests-cleanup.md +224 -224
  98. package/schemas/agent-message.schema.json +73 -73
  99. package/schemas/agent-output-implementacion.schema.json +114 -114
  100. package/schemas/agent-output-planificacion.schema.json +150 -150
  101. package/schemas/agent-output-review.schema.json +98 -98
  102. package/schemas/diary-entry.schema.json +112 -112
  103. package/schemas/gate-state.schema.json +76 -0
  104. package/schemas/harness-ir.schema.json +369 -0
  105. package/schemas/hook-profiles.schema.json +54 -54
  106. package/schemas/hooks-config.schema.json +89 -89
  107. package/schemas/legacy-gates.schema.json +45 -0
  108. package/schemas/modulos.schema.json +38 -38
  109. package/schemas/perfiles.schema.json +36 -36
  110. package/schemas/plugin.schema.json +77 -77
  111. package/schemas/policy-bundle.schema.json +140 -0
  112. package/schemas/policy-enforcement.schema.json +117 -0
  113. package/schemas/policy-operation.schema.json +261 -0
  114. package/schemas/runtime-adapter.schema.json +176 -0
  115. package/schemas/runtime-build-attestation.schema.json +100 -0
  116. package/schemas/runtime-conformance.schema.json +239 -0
  117. package/schemas/runtime-diagnostic.schema.json +395 -0
  118. package/schemas/skill-evals.schema.json +119 -119
  119. package/schemas/skill-frontmatter.schema.json +245 -245
  120. package/schemas/w4-certification-request.schema.json +72 -0
  121. package/schemas/w4-certification-verdict.schema.json +224 -0
  122. package/schemas/w4-corpus.schema.json +172 -0
  123. package/schemas/w4-mutation-report.schema.json +116 -0
  124. package/schemas/w4-replay-result.schema.json +164 -0
  125. package/schemas/w4-scoring-report.schema.json +89 -0
  126. package/scripts/audit-tools/audit-history.js +330 -330
  127. package/scripts/audit-tools/bundle-tracker.js +290 -290
  128. package/scripts/audit-tools/canary-monitor.js +352 -352
  129. package/scripts/audit-tools/code-profiler.js +605 -605
  130. package/scripts/audit-tools/dep-doctor.js +320 -320
  131. package/scripts/audit-tools/env-validator.js +206 -206
  132. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  133. package/scripts/audit-tools/lib/output.js +23 -23
  134. package/scripts/audit-tools/migration-checker.js +392 -392
  135. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  136. package/scripts/bootstrap-instintos.js +3 -0
  137. package/scripts/cli/aprobar-plan.js +73 -73
  138. package/scripts/cli/briefing.js +23 -23
  139. package/scripts/cli/ciclo-evolucion.js +26 -26
  140. package/scripts/cli/derivar-feature-list.js +25 -25
  141. package/scripts/cli/detectar-host.js +27 -27
  142. package/scripts/cli/diary-entry.js +69 -69
  143. package/scripts/cli/execution-state.js +18 -18
  144. package/scripts/cli/gateway-notify.js +41 -41
  145. package/scripts/cli/liberar-fase.js +42 -42
  146. package/scripts/cli/mark-evolved.js +56 -56
  147. package/scripts/cli/metricas-dora.js +26 -26
  148. package/scripts/cli/near-duplicate.js +55 -55
  149. package/scripts/cli/notificaciones.js +123 -123
  150. package/scripts/cli/propose-step.js +29 -29
  151. package/scripts/cli/schedule-parse.js +19 -19
  152. package/scripts/cli/sugerir-modelo.js +20 -20
  153. package/scripts/cli/verificar-plan.js +36 -36
  154. package/scripts/cli/verificar-trazabilidad.js +35 -35
  155. package/scripts/comandos/install-asistido.js +8 -7
  156. package/scripts/configurar-branch-protection.js +418 -418
  157. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  158. package/scripts/doctor.js +61 -36
  159. package/scripts/generar-checklists-consolidados.js +273 -273
  160. package/scripts/generar-claims-runtime.js +1342 -0
  161. package/scripts/generar-harness-ir.js +257 -0
  162. package/scripts/generar-inventario.js +52 -54
  163. package/scripts/generar-policy-bundle.js +202 -0
  164. package/scripts/instalador.js +26 -7
  165. package/scripts/lib/approval-receipts.js +190 -0
  166. package/scripts/lib/artefactos-python.js +43 -43
  167. package/scripts/lib/benchmark-metrics.js +160 -160
  168. package/scripts/lib/budget-enforcer.js +252 -252
  169. package/scripts/lib/certificacion-loop-state.js +421 -0
  170. package/scripts/lib/ci-reader.js +193 -193
  171. package/scripts/lib/ciclo-autonomo/yaml-instintos.js +56 -0
  172. package/scripts/lib/clasificar-directorio.js +92 -0
  173. package/scripts/lib/contadores-inventario.js +217 -217
  174. package/scripts/lib/detectar-host-swl.js +175 -175
  175. package/scripts/lib/detectar-runtime.js +29 -20
  176. package/scripts/lib/detectar-stack-detallado.js +307 -307
  177. package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
  178. package/scripts/lib/detector-reglas-duplicadas.js +220 -220
  179. package/scripts/lib/eval-metrics-store.js +218 -218
  180. package/scripts/lib/eval-quality.js +171 -171
  181. package/scripts/lib/eval-schemas.js +144 -144
  182. package/scripts/lib/eval-self-correct.js +106 -106
  183. package/scripts/lib/eval-validator.js +185 -185
  184. package/scripts/lib/evidence-verifier.js +192 -0
  185. package/scripts/lib/evidencia-release.js +322 -322
  186. package/scripts/lib/frontmatter-canonico.js +509 -0
  187. package/scripts/lib/gate-engine.js +871 -0
  188. package/scripts/lib/gate-hooks-requires.js +249 -249
  189. package/scripts/lib/gate-licencias.js +212 -212
  190. package/scripts/lib/git-config-preflight.js +48 -0
  191. package/scripts/lib/git-metricas.js +257 -257
  192. package/scripts/lib/harness-ir.js +778 -0
  193. package/scripts/lib/harness-source-snapshot.js +309 -0
  194. package/scripts/lib/integrity-ledger.js +1147 -0
  195. package/scripts/lib/jaccard-similarity.js +98 -98
  196. package/scripts/lib/legacy-gate-migration.js +324 -0
  197. package/scripts/lib/limpiar-basura-global.js +45 -2
  198. package/scripts/lib/longmemeval-runner.js +125 -125
  199. package/scripts/lib/metricas-dora.js +204 -204
  200. package/scripts/lib/notificaciones-telegram.js +1 -0
  201. package/scripts/lib/npm-version.js +1 -0
  202. package/scripts/lib/paquetes-conocidos.js +50 -50
  203. package/scripts/lib/plan-lock.js +61 -13
  204. package/scripts/lib/policy-broker.js +338 -0
  205. package/scripts/lib/policy-bundle.js +342 -0
  206. package/scripts/lib/policy-context-provider.js +310 -0
  207. package/scripts/lib/policy-contract.js +479 -0
  208. package/scripts/lib/policy-verifier-utils.js +65 -0
  209. package/scripts/lib/pr-analyzer.js +399 -399
  210. package/scripts/lib/principal-verifier.js +178 -0
  211. package/scripts/lib/prompt-builder.js +264 -264
  212. package/scripts/lib/resolver-plan-fase.js +37 -37
  213. package/scripts/lib/rrf-fusion.js +175 -175
  214. package/scripts/lib/runtime-adapter-contract.js +267 -0
  215. package/scripts/lib/runtime-artifact-verifier.js +426 -0
  216. package/scripts/lib/runtime-build-attestation.js +127 -0
  217. package/scripts/lib/runtime-bundle-installer.js +586 -0
  218. package/scripts/lib/runtime-compiler.js +327 -0
  219. package/scripts/lib/runtime-conformance.js +202 -0
  220. package/scripts/lib/runtime-doctor-core3.js +567 -0
  221. package/scripts/lib/runtime-doctor-input.js +59 -0
  222. package/scripts/lib/runtime-operation-adapter.js +267 -0
  223. package/scripts/lib/schema-version.js +164 -164
  224. package/scripts/lib/semantic-search.js +252 -252
  225. package/scripts/lib/signed-envelope.js +545 -0
  226. package/scripts/lib/single-use-store.js +359 -0
  227. package/scripts/lib/skills-externas.js +31 -0
  228. package/scripts/lib/transformadores/codex.js +15 -8
  229. package/scripts/lib/transformadores/gemini.js +79 -5
  230. package/scripts/lib/w4-attestation-adapter.js +158 -0
  231. package/scripts/lib/w4-canario.js +337 -0
  232. package/scripts/lib/w4-claims.js +182 -0
  233. package/scripts/lib/w4-corpus-generador.js +542 -0
  234. package/scripts/lib/w4-gate-c5.js +115 -0
  235. package/scripts/lib/w4-harness-bajo-prueba.js +155 -0
  236. package/scripts/lib/w4-matriz-combos.js +55 -0
  237. package/scripts/lib/w4-motor-mutacion.js +1348 -0
  238. package/scripts/lib/w4-motor-replay.js +735 -0
  239. package/scripts/lib/w4-pin-origen.js +54 -0
  240. package/scripts/lib/w4-publicar-request.js +132 -0
  241. package/scripts/lib/w4-revocacion.js +62 -0
  242. package/scripts/lib/w4-runtimes-core3.js +38 -0
  243. package/scripts/lib/w4-scorer-certificacion.js +692 -0
  244. package/scripts/lib/w4-superficie-candidato.js +49 -0
  245. package/scripts/lib/w4-veredicto.js +452 -0
  246. package/scripts/lib/w4-verificar-veredicto.js +302 -0
  247. package/scripts/limpiar-artefactos-python.js +131 -131
  248. package/scripts/migrar-csv-a-array.js +168 -168
  249. package/scripts/migrar-fase-dominio.js +200 -200
  250. package/scripts/migrar-gates-legacy.js +108 -0
  251. package/scripts/publicar-certification-request.js +115 -0
  252. package/scripts/runtime-doctor.js +107 -0
  253. package/scripts/tui/componentes/selector-multi.js +189 -189
  254. package/scripts/tui/componentes/selector-unico.js +158 -158
  255. package/scripts/tui/ejecutores.js +375 -375
  256. package/scripts/tui/lib/colores.js +129 -129
  257. package/scripts/tui/lib/render.js +264 -264
  258. package/scripts/tui/lib/teclas.js +113 -113
  259. package/scripts/tui/pantallas/install-wizard.js +12 -7
  260. package/scripts/tui/pantallas/menu-principal.js +52 -52
  261. package/scripts/tui/pantallas/progreso.js +274 -274
  262. package/scripts/tui/pantallas/resumen.js +132 -132
  263. package/scripts/validar-userland-vacio.js +110 -110
  264. package/scripts/verificar-aislamiento-swl-eval.js +87 -0
  265. package/scripts/verificar-empaquetado-downstream.js +375 -0
  266. package/scripts/verificar-loop-constructor.js +215 -0
  267. package/scripts/verificar-trazabilidad.js +13 -6
  268. package/scripts/verificar-veredicto-real.js +84 -0
@@ -0,0 +1,586 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Único writer para instalar, actualizar y desinstalar bundles Core-3
5
+ * (T03) dentro de un home de runtime (`baseDir`). Journal transaccional
6
+ * por operación — backup -> stage (temp + fsync) -> swap (rename + fsync
7
+ * de directorio) -> marca `swapped` — con recuperación idempotente ante
8
+ * una corrida interrumpida (crash, kill, fault inyectada).
9
+ *
10
+ * Nunca activa hooks, grants ni autenticación: solo copia bytes ya
11
+ * verificados por el bundle (T01-T03) hacia las rutas que su propio
12
+ * locator declara, y solo borra rutas que un install anterior de este
13
+ * mismo módulo dejó registradas en `install-state.json`.
14
+ *
15
+ * @module scripts/lib/runtime-bundle-installer
16
+ */
17
+
18
+ const crypto = require('node:crypto');
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+
22
+ const { esRuntimeBundleCompilado, leerArtefactoRuntime } = require('./runtime-compiler');
23
+
24
+ const ESTADO_DIRNAME = '.swl-runtime-state';
25
+ const TRANSACTION_FILE = 'transaction.json';
26
+ const STATE_FILE = 'install-state.json';
27
+ const BACKUPS_DIRNAME = 'backups';
28
+ const MODOS = new Set(['install', 'update', 'uninstall']);
29
+
30
+ function sha256(bytes) {
31
+ return crypto.createHash('sha256').update(bytes).digest('hex');
32
+ }
33
+
34
+ function estadoDir(baseDir) {
35
+ return path.join(baseDir, ESTADO_DIRNAME);
36
+ }
37
+
38
+ function transactionPath(baseDir) {
39
+ return path.join(estadoDir(baseDir), TRANSACTION_FILE);
40
+ }
41
+
42
+ function statePath(baseDir) {
43
+ return path.join(estadoDir(baseDir), STATE_FILE);
44
+ }
45
+
46
+ function backupsTransaccionPath(baseDir, transactionId) {
47
+ return path.join(estadoDir(baseDir), BACKUPS_DIRNAME, transactionId);
48
+ }
49
+
50
+ function backupPath(baseDir, transactionId, rutaRelativa) {
51
+ return path.join(backupsTransaccionPath(baseDir, transactionId), rutaRelativa);
52
+ }
53
+
54
+ /** Los backups solo existen para revertir ESTA transacción; una vez cerrada (commit o rollback), sobran. */
55
+ function borrarBackupsTransaccion(baseDir, transactionId) {
56
+ fs.rmSync(backupsTransaccionPath(baseDir, transactionId), { recursive: true, force: true });
57
+ }
58
+
59
+ /**
60
+ * Punto de fallo determinístico para tests, mismo patrón que
61
+ * integrity-ledger.js/single-use-store.js: fuera de NODE_ENV=test la
62
+ * variable es ruido ignorado en producción salvo que además coincida,
63
+ * en cuyo caso es un error de configuración explícito (nunca una falla
64
+ * silenciosa en producción).
65
+ */
66
+ function hitFault(punto) {
67
+ const objetivo = process.env.SWL_RUNTIME_INSTALLER_FAULT;
68
+ if (objetivo !== punto) return;
69
+ if (process.env.NODE_ENV !== 'test') {
70
+ throw Object.assign(new Error('fault injection fuera de NODE_ENV=test'), { code: 'FAULT_CONFIG_INVALID' });
71
+ }
72
+ throw Object.assign(new Error(`FAULT_INJECTED: ${punto}`), { code: 'FAULT_INJECTED', punto });
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Primitivas de filesystem — mismo patrón que scripts/generar-harness-ir.js.
77
+ // ---------------------------------------------------------------------------
78
+
79
+ function leerJSONOpcional(ruta) {
80
+ try {
81
+ return JSON.parse(fs.readFileSync(ruta, 'utf8'));
82
+ } catch (error) {
83
+ if (error.code === 'ENOENT') return null;
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ function escribirTemporalYFsync(ruta, contenido) {
89
+ fs.mkdirSync(path.dirname(ruta), { recursive: true });
90
+ const temporal = `${ruta}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
91
+ fs.writeFileSync(temporal, contenido, { flag: 'wx', mode: 0o600 });
92
+ const fd = fs.openSync(temporal, 'r+');
93
+ try {
94
+ fs.fsyncSync(fd);
95
+ } finally {
96
+ fs.closeSync(fd);
97
+ }
98
+ return temporal;
99
+ }
100
+
101
+ function sincronizarDirectorio(directorio) {
102
+ const fd = fs.openSync(directorio, 'r');
103
+ try {
104
+ fs.fsyncSync(fd);
105
+ } catch (error) {
106
+ const noSoportado = process.platform === 'win32' && ['EPERM', 'EINVAL'].includes(error.code);
107
+ if (!noSoportado) throw error;
108
+ } finally {
109
+ fs.closeSync(fd);
110
+ }
111
+ }
112
+
113
+ function limpiarSiExiste(ruta) {
114
+ try {
115
+ fs.unlinkSync(ruta);
116
+ } catch (error) {
117
+ if (error.code !== 'ENOENT') throw error;
118
+ }
119
+ }
120
+
121
+ /** Escritura atómica: temp + fsync + rename + fsync de directorio. */
122
+ function publicarAtomico(destino, contenido) {
123
+ const temporal = escribirTemporalYFsync(destino, contenido);
124
+ fs.renameSync(temporal, destino);
125
+ sincronizarDirectorio(path.dirname(destino));
126
+ }
127
+
128
+ function adquirirLock(baseDir) {
129
+ fs.mkdirSync(estadoDir(baseDir), { recursive: true });
130
+ const lockPath = path.join(estadoDir(baseDir), 'transaction.lock');
131
+ let fd;
132
+ try {
133
+ hitFault('before-lock-open');
134
+ fd = fs.openSync(lockPath, 'wx', 0o600);
135
+ } catch (error) {
136
+ if (error.code === 'EEXIST') {
137
+ throw Object.assign(new Error('otra transacción del instalador está en curso'), { code: 'LOCK_HELD' });
138
+ }
139
+ throw error;
140
+ }
141
+ // El PID queda escrito en el lock ANTES de que otro proceso pueda observarlo
142
+ // (la creación exclusiva ya se resolvió arriba): es lo único que permite
143
+ // distinguir después un lock huérfano (proceso muerto) de uno legítimamente
144
+ // en uso — sin esto ningún reclamo puede ser seguro (hallazgo del checker).
145
+ fs.writeSync(fd, String(process.pid));
146
+ fs.fsyncSync(fd);
147
+ return { fd, lockPath };
148
+ }
149
+
150
+ function pidDelLock(lockPath) {
151
+ let contenido;
152
+ try {
153
+ contenido = fs.readFileSync(lockPath, 'utf8').trim();
154
+ } catch {
155
+ return null;
156
+ }
157
+ const pid = Number(contenido);
158
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
159
+ }
160
+
161
+ function procesoVive(pid) {
162
+ try {
163
+ process.kill(pid, 0);
164
+ return true; // no lanzó: el proceso existe y es señalable
165
+ } catch (error) {
166
+ return error.code === 'EPERM'; // ESRCH = no existe; EPERM = existe pero sin permiso (conservador: vivo)
167
+ }
168
+ }
169
+
170
+ /** Un lock sin PID legible (corrupto o de una versión anterior) se trata como huérfano recuperable. */
171
+ function lockEstaVivo(lockPath) {
172
+ const pid = pidDelLock(lockPath);
173
+ return pid !== null && procesoVive(pid);
174
+ }
175
+
176
+ function liberarLock(lock) {
177
+ try {
178
+ hitFault('before-lock-close');
179
+ fs.closeSync(lock.fd);
180
+ } catch {
181
+ /* best effort: un fd ya cerrado no debe impedir liberar el archivo del lock */
182
+ }
183
+ limpiarSiExiste(lock.lockPath);
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Journal.
188
+ // ---------------------------------------------------------------------------
189
+
190
+ function escribirJournal(baseDir, journal) {
191
+ publicarAtomico(transactionPath(baseDir), JSON.stringify(journal, null, 2));
192
+ }
193
+
194
+ function leerJournal(baseDir) {
195
+ return leerJSONOpcional(transactionPath(baseDir));
196
+ }
197
+
198
+ function borrarJournal(baseDir) {
199
+ limpiarSiExiste(transactionPath(baseDir));
200
+ }
201
+
202
+ function crearTransactionId() {
203
+ return `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Estado instalado (install-state.json) — "el estado conoce cada archivo".
208
+ // ---------------------------------------------------------------------------
209
+
210
+ function leerEstadoInstalado(baseDir) {
211
+ return leerJSONOpcional(statePath(baseDir)) || { schemaVersion: '1.0.0', runtimeId: null, files: [] };
212
+ }
213
+
214
+ function escribirEstadoInstalado(baseDir, estado) {
215
+ publicarAtomico(statePath(baseDir), JSON.stringify(estado, null, 2));
216
+ }
217
+
218
+ function aplicarOperacionesAlEstado(estadoPrevio, runtimeId, operaciones) {
219
+ const porPath = new Map(estadoPrevio.files.map((f) => [f.path, f]));
220
+ for (const op of operaciones) {
221
+ if (op.accion === 'write') porPath.set(op.path, { path: op.path, sha256: op.nuevoSha256, bytes: op.bytes });
222
+ else porPath.delete(op.path);
223
+ }
224
+ return {
225
+ schemaVersion: '1.0.0',
226
+ runtimeId,
227
+ installedAt: new Date().toISOString(),
228
+ files: [...porPath.values()].sort((a, b) => a.path.localeCompare(b.path)),
229
+ };
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Confinamiento de destino — el locator ya valida rutas relativas seguras
234
+ // (runtime-artifact-verifier.js), pero el instalador escribe a disco real:
235
+ // defensa en profundidad contra traversal si esa garantía llegara a fallar.
236
+ //
237
+ // El prefijo de string por sí solo NO basta: si un componente intermedio
238
+ // de baseDir ya existe como symlink/junction hacia fuera, `path.resolve`
239
+ // nunca lo detecta y la escritura/borrado real aterriza fuera de baseDir
240
+ // (hallazgo del checker independiente en la revisión de T08). Se camina
241
+ // cada componente con lstat ANTES de escribir y se rechaza cualquier
242
+ // enlace en la cadena; un componente que todavía no existe es seguro por
243
+ // construcción (mkdir/rename lo crean real, nunca siguen un enlace que
244
+ // no existe todavía).
245
+ // ---------------------------------------------------------------------------
246
+
247
+ function throwPathEscape(rutaRelativa, motivo) {
248
+ throw Object.assign(new Error(`${motivo}: ${rutaRelativa}`), { code: 'PATH_ESCAPE' });
249
+ }
250
+
251
+ function verificarSinEnlaces(raizReal, rutaRelativa) {
252
+ const segmentos = rutaRelativa.split('/');
253
+ let actual = raizReal;
254
+ for (let indice = 0; indice < segmentos.length; indice += 1) {
255
+ actual = path.join(actual, segmentos[indice]);
256
+ let stat;
257
+ try {
258
+ stat = fs.lstatSync(actual);
259
+ } catch {
260
+ return; // no existe aún: el resto de la ruta se crea fresca, sin enlaces que seguir
261
+ }
262
+ if (stat.isSymbolicLink()) throwPathEscape(rutaRelativa, 'ruta contiene un enlace simbólico');
263
+ const esUltimoSegmento = indice === segmentos.length - 1;
264
+ if (!esUltimoSegmento && !stat.isDirectory()) {
265
+ throwPathEscape(rutaRelativa, 'componente intermedio no es un directorio');
266
+ }
267
+ }
268
+ }
269
+
270
+ function resolverDestinoConfinado(baseDir, rutaRelativa) {
271
+ const raiz = path.resolve(baseDir);
272
+ const raizReal = fs.realpathSync(raiz); // baseDir siempre existe: el caller lo crea antes de instalar
273
+ verificarSinEnlaces(raizReal, rutaRelativa);
274
+ const destino = path.resolve(raizReal, rutaRelativa);
275
+ const prefijo = raizReal.endsWith(path.sep) ? raizReal : raizReal + path.sep;
276
+ // Exige un prefijo ESTRICTO: una ruta relativa válida siempre queda DENTRO
277
+ // de baseDir, nunca coincide con baseDir mismo (ese caso solo ocurre con
278
+ // "" o "." — entrada corrupta que jamás debe tratarse como archivo).
279
+ if (!destino.startsWith(prefijo)) throwPathEscape(rutaRelativa, 'ruta fuera de baseDir');
280
+ return destino;
281
+ }
282
+
283
+ function leerBytesDestino(destino) {
284
+ try {
285
+ const bytes = fs.readFileSync(destino);
286
+ return { existe: true, bytes, sha256: sha256(bytes) };
287
+ } catch (error) {
288
+ if (error.code === 'ENOENT') return { existe: false, bytes: null, sha256: null };
289
+ throw error;
290
+ }
291
+ }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Planificación (pura, sin I/O de escritura).
295
+ // ---------------------------------------------------------------------------
296
+
297
+ function planificarInstalacion(bundle, baseDir) {
298
+ const operaciones = [];
299
+ for (const entry of bundle.locator.entries) {
300
+ const destino = resolverDestinoConfinado(baseDir, entry.path);
301
+ const actual = leerBytesDestino(destino);
302
+ if (actual.existe && actual.sha256 === entry.sha256) continue;
303
+ operaciones.push({
304
+ path: entry.path, destino, accion: 'write',
305
+ hadPrior: actual.existe, nuevoSha256: entry.sha256, estado: 'pending',
306
+ });
307
+ }
308
+ return operaciones;
309
+ }
310
+
311
+ function planificarDesinstalacion(estadoInstalado, baseDir) {
312
+ return estadoInstalado.files.map((archivo) => ({
313
+ path: archivo.path,
314
+ destino: resolverDestinoConfinado(baseDir, archivo.path),
315
+ accion: 'delete',
316
+ hadPrior: true,
317
+ nuevoSha256: null,
318
+ estado: 'pending',
319
+ }));
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Backup + stage + swap por operación.
324
+ // ---------------------------------------------------------------------------
325
+
326
+ function respaldarOperacion(baseDir, transactionId, op) {
327
+ if (!op.hadPrior) return;
328
+ const actual = leerBytesDestino(op.destino);
329
+ if (!actual.existe) return; // ya no existe (borrado manual entre plan y ejecución): nada que respaldar
330
+ publicarAtomico(backupPath(baseDir, transactionId, op.path), actual.bytes);
331
+ }
332
+
333
+ /**
334
+ * Escribe el temporal y persiste su nombre EXACTO en el journal antes del
335
+ * rename — la recuperación limpia exactamente ese archivo, nunca un patrón
336
+ * de prefijo sobre el directorio (un glob `${base}.tmp-*` puede coincidir
337
+ * con un archivo real del usuario, ej. `CLAUDE.md.tmp-notas-personales`;
338
+ * hallazgo del checker independiente).
339
+ */
340
+ function aplicarEscritura(baseDir, journal, bundle, op) {
341
+ const bytes = leerArtefactoRuntime(bundle, op.path);
342
+ // Defensa final antes de tocar disco: si el bundle entregara bytes que no
343
+ // coinciden con el hash que el propio locator declaró para esa ruta, es
344
+ // preferible fallar cerrado (la transacción completa se revierte) a
345
+ // instalar contenido no verificado.
346
+ if (sha256(bytes) !== op.nuevoSha256) {
347
+ throw Object.assign(new Error(`bytes del bundle no coinciden con el locator: ${op.path}`), { code: 'HASH_MISMATCH' });
348
+ }
349
+ op.temporal = escribirTemporalYFsync(op.destino, bytes);
350
+ escribirJournal(baseDir, journal); // persiste el nombre del temporal ANTES del punto de falla simulado
351
+ hitFault('after-stage');
352
+ fs.renameSync(op.temporal, op.destino);
353
+ sincronizarDirectorio(path.dirname(op.destino));
354
+ op.temporal = null;
355
+ }
356
+
357
+ function aplicarBorrado(op) {
358
+ limpiarSiExiste(op.destino);
359
+ sincronizarDirectorio(path.dirname(op.destino));
360
+ }
361
+
362
+ function ejecutarOperacion(bundle, baseDir, journal, op) {
363
+ respaldarOperacion(baseDir, journal.transactionId, op);
364
+ hitFault('after-backup');
365
+ op.estado = 'backed-up';
366
+ op.temporal = null;
367
+ escribirJournal(baseDir, journal);
368
+
369
+ if (op.accion === 'write') aplicarEscritura(baseDir, journal, bundle, op);
370
+ else aplicarBorrado(op);
371
+ hitFault('after-swap');
372
+
373
+ op.estado = 'swapped';
374
+ escribirJournal(baseDir, journal);
375
+ }
376
+
377
+ // ---------------------------------------------------------------------------
378
+ // Rollback — reversible desde cualquier estado por-operación intermedio.
379
+ // ---------------------------------------------------------------------------
380
+
381
+ function restaurarDesdeBackup(baseDir, transactionId, op) {
382
+ const respaldo = backupPath(baseDir, transactionId, op.path);
383
+ let bytes;
384
+ try {
385
+ bytes = fs.readFileSync(respaldo);
386
+ } catch (error) {
387
+ if (error.code !== 'ENOENT') throw error;
388
+ // hadPrior=true se fijó en planificación, pero el archivo ya no existía
389
+ // al momento de ejecutar (respaldarOperacion no crea backup en ese
390
+ // caso): no hay nada que restaurar, así que la reversión segura es
391
+ // asegurar que el destino tampoco exista (Nemesis pass2, F-02). Sin
392
+ // esto, un ENOENT aquí bloqueaba el instalador para siempre — cada
393
+ // recuperar()/aplicarBundle() futuro repetía el mismo throw.
394
+ limpiarSiExiste(op.destino);
395
+ return;
396
+ }
397
+ publicarAtomico(op.destino, bytes);
398
+ }
399
+
400
+ function revertirOperacion(baseDir, transactionId, op) {
401
+ if (op.estado === 'pending') return;
402
+ if (op.estado === 'backed-up' || op.estado === 'swapped') {
403
+ if (op.hadPrior) restaurarDesdeBackup(baseDir, transactionId, op);
404
+ else limpiarSiExiste(op.destino);
405
+ }
406
+ if (op.temporal) limpiarSiExiste(op.temporal);
407
+ }
408
+
409
+ function revertirTransaccion(baseDir, journal) {
410
+ for (const op of journal.operaciones) revertirOperacion(baseDir, journal.transactionId, op);
411
+ journal.status = 'rolled-back';
412
+ escribirJournal(baseDir, journal);
413
+ borrarJournal(baseDir);
414
+ borrarBackupsTransaccion(baseDir, journal.transactionId);
415
+ }
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // Recuperación idempotente.
419
+ // ---------------------------------------------------------------------------
420
+
421
+ /**
422
+ * Retoma cualquier transacción que haya quedado pendiente en `baseDir`
423
+ * (crash, kill, fault inyectada). Sin journal pendiente es un no-op —
424
+ * llamarla repetidamente es seguro por diseño. `status: 'in-progress'`
425
+ * (operaciones a medias) se REVIERTE; `status: 'operations-done'`
426
+ * (operaciones completas, falta cerrar el libro) se TERMINA — nunca al
427
+ * revés, o los archivos ya correctos en disco se revertirían a un estado
428
+ * viejo mientras install-state.json quedaría con el valor nuevo (o
429
+ * viceversa en uninstall), divergencia permanente (Nemesis pass2, F-01).
430
+ */
431
+ function recuperar(baseDir) {
432
+ const journal = leerJournal(baseDir);
433
+ if (!journal) return { recuperado: false };
434
+ if (journal.status === 'operations-done') {
435
+ const lock = adquirirLockToleranteAPropio(baseDir);
436
+ try {
437
+ finalizarTransaccion(baseDir, journal);
438
+ } finally {
439
+ liberarLock(lock);
440
+ }
441
+ return { recuperado: true, transactionId: journal.transactionId };
442
+ }
443
+ if (journal.status !== 'in-progress') return { recuperado: false };
444
+ const lock = adquirirLockToleranteAPropio(baseDir);
445
+ try {
446
+ revertirTransaccion(baseDir, journal);
447
+ } finally {
448
+ liberarLock(lock);
449
+ }
450
+ return { recuperado: true, transactionId: journal.transactionId };
451
+ }
452
+
453
+ /**
454
+ * `recuperar()` puede correr como parte de aplicarBundle() sobre un lock ya
455
+ * huérfano (el proceso que lo tomó murió sin liberarlo). Reclamar SOLO
456
+ * procede cuando el PID grabado en el lock ya no vive — un lock cuyo dueño
457
+ * sigue vivo debe seguir bloqueando (LOCK_HELD real), nunca robarse: sin
458
+ * esta verificación dos instalaciones concurrentes sobre el mismo baseDir
459
+ * corren en paralelo y corrompen el journal compartido (hallazgo del
460
+ * checker independiente).
461
+ */
462
+ function adquirirLockToleranteAPropio(baseDir) {
463
+ try {
464
+ return adquirirLock(baseDir);
465
+ } catch (error) {
466
+ if (error.code !== 'LOCK_HELD') throw error;
467
+ const lockPath = path.join(estadoDir(baseDir), 'transaction.lock');
468
+ if (lockEstaVivo(lockPath)) throw error;
469
+ limpiarSiExiste(lockPath);
470
+ return adquirirLock(baseDir);
471
+ }
472
+ }
473
+
474
+ // ---------------------------------------------------------------------------
475
+ // API pública.
476
+ // ---------------------------------------------------------------------------
477
+
478
+ function validarOpciones(opciones) {
479
+ if (!opciones || typeof opciones.baseDir !== 'string' || opciones.baseDir.length === 0) {
480
+ throw new TypeError('opciones.baseDir es requerido');
481
+ }
482
+ if (!MODOS.has(opciones.modo)) {
483
+ throw new TypeError(`opciones.modo debe ser uno de: ${[...MODOS].join(', ')}`);
484
+ }
485
+ }
486
+
487
+ function planificar(bundle, opciones) {
488
+ if (opciones.modo === 'uninstall') {
489
+ return planificarDesinstalacion(leerEstadoInstalado(opciones.baseDir), opciones.baseDir);
490
+ }
491
+ if (!esRuntimeBundleCompilado(bundle)) throw new TypeError('se requiere bundle verificado (T03)');
492
+ return planificarInstalacion(bundle, opciones.baseDir);
493
+ }
494
+
495
+ function resolverRuntimeId(bundle, opciones) {
496
+ return opciones.modo === 'uninstall'
497
+ ? leerEstadoInstalado(opciones.baseDir).runtimeId
498
+ : bundle.runtimeId;
499
+ }
500
+
501
+ /**
502
+ * Cola de una transacción cuyas operaciones YA llegaron a 'swapped' (los
503
+ * archivos en disco son correctos): recalcula el estado instalado desde
504
+ * disco real y cierra el journal. Reejecutable sin efectos adicionales —
505
+ * `escribirEstadoInstalado`/`escribirJournal` son publicaciones atómicas
506
+ * idempotentes, así que llamarla dos veces sobre el mismo journal produce
507
+ * el mismo resultado (necesario para que `recuperar()` pueda retomarla
508
+ * tras un crash a mitad de esta cola, Nemesis pass2 F-01).
509
+ */
510
+ function finalizarTransaccion(baseDir, journal) {
511
+ const estadoPrevio = leerEstadoInstalado(baseDir);
512
+ // El estado persistido refleja lo que REALMENTE quedó en disco (tamaño y
513
+ // hash recalculados del archivo ya swapped), no lo que el locator declaró
514
+ // — "el estado conoce cada archivo" exige verificar, no solo confiar.
515
+ const opsConBytes = journal.operaciones.map((op) => {
516
+ if (op.accion !== 'write') return op;
517
+ const bytesEnDisco = fs.readFileSync(op.destino);
518
+ return { ...op, bytes: bytesEnDisco.length, nuevoSha256: sha256(bytesEnDisco) };
519
+ });
520
+ escribirEstadoInstalado(baseDir, aplicarOperacionesAlEstado(estadoPrevio, journal.runtimeId, opsConBytes));
521
+
522
+ journal.status = 'completed';
523
+ escribirJournal(baseDir, journal);
524
+ borrarJournal(baseDir);
525
+ borrarBackupsTransaccion(baseDir, journal.transactionId);
526
+ }
527
+
528
+ function ejecutarTransaccion(bundle, opciones, operaciones) {
529
+ const baseDir = baseDirDe(opciones);
530
+ const journal = {
531
+ schemaVersion: '1.0.0', transactionId: crearTransactionId(),
532
+ runtimeId: resolverRuntimeId(bundle, opciones), modo: opciones.modo,
533
+ startedAt: new Date().toISOString(), status: 'in-progress', operaciones,
534
+ };
535
+ escribirJournal(baseDir, journal);
536
+ for (const op of journal.operaciones) ejecutarOperacion(bundle, baseDir, journal, op);
537
+
538
+ // Punto de conmutación: a partir de aquí los archivos en disco YA son
539
+ // correctos (todas las operaciones llegaron a 'swapped'). Un crash
540
+ // DESPUÉS de este punto debe TERMINAR la transacción (finalizarTransaccion
541
+ // es idempotente), nunca revertir los archivos — sin este estado
542
+ // intermedio, recuperar() no podía distinguir "operaciones a medias"
543
+ // (revertir) de "operaciones completas, falta cerrar el libro" (terminar),
544
+ // y un crash entre escribir install-state.json y cerrar el journal dejaba
545
+ // install-state.json desincronizado del contenido real en disco de forma
546
+ // permanente, peor aún en uninstall (Nemesis pass2, F-01).
547
+ journal.status = 'operations-done';
548
+ escribirJournal(baseDir, journal);
549
+ hitFault('after-operations');
550
+
551
+ finalizarTransaccion(baseDir, journal);
552
+ return journal;
553
+ }
554
+
555
+ function baseDirDe(opciones) {
556
+ return opciones.baseDir;
557
+ }
558
+
559
+ /**
560
+ * Instala, actualiza o desinstala un bundle Core-3 en `opciones.baseDir` de
561
+ * forma transaccional. `bundle` es obligatorio para install/update (debe
562
+ * venir de `compilarRuntimeBundle`, T03) e ignorado en uninstall (usa el
563
+ * install-state.json previo). `opciones.dryRun` calcula el plan sin tocar
564
+ * disco — ni siquiera intenta recuperar una transacción huérfana.
565
+ */
566
+ function aplicarBundle(bundle, opciones) {
567
+ validarOpciones(opciones);
568
+ if (opciones.dryRun) return { dryRun: true, operaciones: planificar(bundle, opciones) };
569
+
570
+ const recuperacion = recuperar(opciones.baseDir);
571
+ const lock = adquirirLockToleranteAPropio(opciones.baseDir);
572
+ try {
573
+ const operaciones = planificar(bundle, opciones);
574
+ if (operaciones.length === 0) return { recuperacion, operaciones: [], sinCambios: true };
575
+ const journal = ejecutarTransaccion(bundle, opciones, operaciones);
576
+ return { recuperacion, operaciones: journal.operaciones, sinCambios: false };
577
+ } finally {
578
+ liberarLock(lock);
579
+ }
580
+ }
581
+
582
+ module.exports = {
583
+ aplicarBundle,
584
+ recuperar,
585
+ leerEstadoInstalado,
586
+ };