@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,1348 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Motor de mutation testing zero-deps para los módulos críticos de swl-ses
5
+ * (ADR-0050 § "mutation score de al menos 90%, con 100% de mutantes críticos
6
+ * eliminados"; F32/T-07, REQ-32-07).
7
+ *
8
+ * Construido desde cero porque el repo es zero-deps en `scripts/lib/` y no
9
+ * tiene ninguna librería de mutation testing instalada (ni Stryker ni un
10
+ * parser de AST como @babel/parser/acorn) — no se puede agregar ninguna
11
+ * dependencia npm nueva (Whitelist F32). Por eso los 5 operadores de
12
+ * mutación (ver `OPERADORES`) son mutaciones de TEXTO sobre una versión
13
+ * "enmascarada" del código fuente (strings/comentarios/regex-literales
14
+ * reemplazados por relleno neutro, misma longitud), nunca un AST completo.
15
+ * Es una técnica de mutation testing legítima y usada en la práctica
16
+ * (herramientas tempranas de mutation testing empezaron así antes de mover
17
+ * a AST completo) — documentado honestamente como tal, no un atajo oculto.
18
+ *
19
+ * DECLARACIÓN DE EFECTOS (a diferencia de T-01/T-02/T-04/T-05, que son puros
20
+ * por contrato): `ejecutarMutationTesting` SÍ tiene efectos — crea
21
+ * directorios temporales reales en `os.tmpdir()`, escribe archivos ahí y
22
+ * spawnea procesos `node` reales. Es necesario por la naturaleza de la
23
+ * tarea (no se puede saber si una suite mata un mutante sin ejecutarla).
24
+ * `generarMutantes`, `clasificarCritico`, `enmascarar` y
25
+ * `verificarTasaSyntaxError` sí son puras.
26
+ *
27
+ * CRÍTICO — invariante de aislamiento: el motor NUNCA escribe un mutante de
28
+ * vuelta a los 20 módulos ORIGINALES de `scripts/lib/`. Cada mutante se
29
+ * escribe en una COPIA efímera bajo `os.tmpdir()` (creada y borrada por
30
+ * tarea), nunca en el árbol real del repo — los 20 módulos críticos están
31
+ * fuera de la whitelist de escritura de F32 (son de F29/F30/F31, ya
32
+ * cerrados).
33
+ *
34
+ * DESVIACIÓN DOCUMENTADA respecto al memo de diseño aprobado (§4b/§4c): el
35
+ * memo sugería `child_process.spawnSync` con un pool de workers "por
36
+ * módulo" (uno por cada uno de los 20 módulos, min(20, cpus)). La medición
37
+ * real de baseline (hecha ANTES de implementar, sobre los 20 módulos reales)
38
+ * mostró que 4 módulos por sí solos tardan 26-87s por corrida de su suite
39
+ * (`gate-engine` ~28s con ~330 mutantes, `runtime-compiler` ~86s,
40
+ * `runtime-bundle-installer` ~44s, `policy-bundle`/`harness-source-snapshot`
41
+ * ~10-14s compartiendo `tests/scripts/policy-bundle.test.js`) — paralelizar
42
+ * sólo "por módulo" habría dejado esos módulos corriendo SECUENCIALMENTE
43
+ * sus propios cientos de mutantes (decenas de minutos a varias horas cada
44
+ * uno, inviable en una sesión). Este módulo paraleliza a nivel de TAREA
45
+ * individual (módulo, mutante) con `child_process.spawn` asíncrono (no
46
+ * bloqueante, a diferencia de `spawnSync`) sobre un pool de concurrencia
47
+ * acotada — cada tarea usa su propia copia efímera aislada, creada y
48
+ * borrada sólo para esa tarea. El comando exacto que se spawnea
49
+ * (`node --test --test-reporter=tap --test-reporter-destination=stdout
50
+ * <testFiles...>`, `cwd` = copia efímera) es EXACTAMENTE el especificado en
51
+ * el memo — sólo cambia el mecanismo de concurrencia (async spawn + pool de
52
+ * tareas en vez de spawnSync + pool de módulos), no el comando ni la
53
+ * semántica de clasificación muerto/sobrevivió/crítico.
54
+ *
55
+ * CORRECCIONES POST-VERIFICACIÓN (misma sesión T-07, ver detalle junto a
56
+ * cada función): (1) `matarArbolDeProceso` era exclusivamente Windows
57
+ * (`taskkill`) y fallaba silenciosamente en la CI real (ubuntu-latest) —
58
+ * ahora branchea por `process.platform` vía `decidirComandoMatarArbol`
59
+ * (pura, testeable en cualquier SO); (2) `prepararPlantillaGit` podía dejar
60
+ * un directorio huérfano en `os.tmpdir()` si `clonarGitLocal` fallaba antes
61
+ * de que el `finally` del caller tuviera una referencia que limpiar; (3)
62
+ * `clasificarCritico`/`nombreFuncionContenedora` no reconocían funciones
63
+ * declaradas como propiedad-flecha de objeto (`nombre: (v) => ...`) ni
64
+ * prefijos críticos en inglés (`validate`/`verify`/`is[A-Z]`/`can[A-Z]`),
65
+ * pese a ser la convención de nombrado real de los 20 módulos críticos —
66
+ * subestimaba el conteo de mutantes críticos; (4) `correrLineaBase` validaba
67
+ * la línea base con la UNIÓN de `directoriosExtra` de todo el batch en un
68
+ * único árbol compartido, en vez de aislar cada módulo con EXACTAMENTE los
69
+ * mismos directorios que usan después sus tareas de mutante reales
70
+ * (`directoriosParaModulo`) — con los 20 módulos juntos esa unión incluía
71
+ * `DIRECTORIOS_EXTRA_PAQUETE_COMPLETO` (declarado sólo por los módulos #1/#2)
72
+ * y enmascaraba que 7 módulos más nunca declararon el suyo propio: sus
73
+ * tareas de mutante reales fallaban por MODULE_NOT_FOUND/ENOENT en CADA
74
+ * mutante, contadas falsamente como "muerto" por SYNTAX_ERROR (100% de
75
+ * score que medía entorno roto, no detección real). Corregido: baseline
76
+ * aislado por módulo + `directoriosExtra` correcto declarado para cada uno
77
+ * de los 20 (ver DEUDA-TECNICA.md#DT-T07-MUTANTES-CRITICOS-SOBREVIVIENTES).
78
+ */
79
+
80
+ const fs = require('node:fs');
81
+ const os = require('node:os');
82
+ const path = require('node:path');
83
+ const { spawn, execFileSync } = require('node:child_process');
84
+
85
+ const SCHEMA_VERSION = '1.0.0';
86
+ const UMBRAL_SYNTAX_ERROR = 0.01;
87
+ const DIRECTORIOS_POR_DEFECTO = Object.freeze(['scripts', 'schemas', 'tests/lib', 'tests/scripts']);
88
+
89
+ // (HALLAZGO real durante T-07, corrida completa 2026-07-17). Sin timeout, dos
90
+ // mutantes de módulos con tests concurrentes (fork()/spawn() internos, p.ej.
91
+ // integrity-ledger.test.js) COLGARON el proceso hijo indefinidamente (~17min
92
+ // de CPU activa medidos antes de detectarlo y matar el árbol de procesos) —
93
+ // bloqueando esos 2 de 12 "slots" de concurrencia para siempre y estancando
94
+ // la corrida real al 99.5%. Una mutación puede razonablemente convertir una
95
+ // guarda de terminación en un loop infinito o un deadlock de espera — es una
96
+ // forma VÁLIDA de "matar" el mutante (cambió el comportamiento observable
97
+ // tan drásticamente que ni siquiera termina), no un defecto del motor.
98
+ // 150s da margen holgado sobre el baseline medido más lento de los 20
99
+ // módulos reales (~86s, runtime-compiler.test.js).
100
+ const TIMEOUT_MS_POR_DEFECTO = 150000;
101
+
102
+ // (HALLAZGO real durante T-07, opción A de arreglar-al-detectar.md — fix
103
+ // acotado, reversible, sólo toca este módulo). `tests/scripts/policy-bundle.test.js`
104
+ // -- único testFile de los módulos #1/#2 de MODULOS_CRITICOS -- NO es un test
105
+ // acotado a esos dos módulos: es un archivo de tests de PAQUETE COMPLETO.
106
+ // Hace require('../../package.json'/'package-lock.json'/'plugin.json'), lee
107
+ // '.planning/loops/tdd-f30-policy-gates.json' (ya anticipado por el memo de
108
+ // diseño §1), corre `git diff <baseCommit> -- <paths>` contra RAIZ (sólo
109
+ // lectura: los únicos git() con efectos de escritura del archivo apuntan a
110
+ // fixtures temporales propias del test, nunca a RAIZ), compila el harness
111
+ // real contra `agentes/`/`habilidades/`/`comandos/swl/`/`reglas/`, y
112
+ // verifica contenido de 10 documentos raíz (AGENTS.md, CLAUDE.md,
113
+ // COMANDOS.md, INSTALACION.md, llms.txt, MANUAL_USO.md, INVENTARIO.md,
114
+ // README.md, SALUD.md, CHANGELOG.md) — verificado exhaustivamente vía grep
115
+ // de `path.join(RAIZ, ...)` en el archivo, no por prueba y error incremental.
116
+ // Sin estos elementos la línea base fallaba con MUTATION_BASELINE_FAILED por
117
+ // ENOENT/"not a git repository" — no por una guarda real de seguridad.
118
+ //
119
+ // Esta lista NUNCA se agrega a DIRECTORIOS_POR_DEFECTO (penalizaría a los
120
+ // otros 18 módulos, cuyos testFiles no la necesitan): sólo los módulos #1/#2
121
+ // de MODULOS_CRITICOS declaran `directoriosExtra` con esta lista.
122
+ //
123
+ // `.git` requiere un CHECKOUT real (no sólo `git clone --no-checkout`): el
124
+ // snapshot del harness lee `.git/index` directamente
125
+ // (`harness-source-snapshot.js#resolverIndiceGit`, vía `git rev-parse
126
+ // --git-path index` + `fs.realpathSync`) y ese archivo sólo existe tras un
127
+ // checkout. `--no-checkout` mide ~120ms pero dejaría `.git/index` ausente
128
+ // (ENOENT); un checkout completo mide ~3.6s (dominado por escribir ~1700
129
+ // archivos pequeños en NTFS, no por el clon en sí — `--shared` evita
130
+ // recopiar el historial vía alternates). Ese costo se paga sólo en las 222
131
+ // tareas de los módulos #1/#2 (62+160 mutantes), nunca en las otras 2000+.
132
+ const DIRECTORIOS_EXTRA_PAQUETE_COMPLETO = Object.freeze([
133
+ 'manifiestos', 'gateway', '.github', 'hooks', '.planning/loops',
134
+ 'agentes', 'habilidades', 'comandos', 'reglas',
135
+ 'package.json', 'package-lock.json', 'plugin.json',
136
+ 'AGENTS.md', 'CLAUDE.md', 'COMANDOS.md', 'INSTALACION.md', 'llms.txt',
137
+ 'MANUAL_USO.md', 'INVENTARIO.md', 'README.md', 'SALUD.md', 'CHANGELOG.md',
138
+ '.git',
139
+ ]);
140
+
141
+ function crearError(codigo, mensaje) {
142
+ const error = new Error(mensaje);
143
+ error.codigo = codigo;
144
+ return error;
145
+ }
146
+
147
+ function fallar(codigo, mensaje) {
148
+ throw crearError(codigo, mensaje);
149
+ }
150
+
151
+ function esObjetoPlano(valor) {
152
+ return Boolean(valor) && typeof valor === 'object' && !Array.isArray(valor)
153
+ && Object.getPrototypeOf(valor) === Object.prototype;
154
+ }
155
+
156
+ function congelarProfundo(valor) {
157
+ if (valor && typeof valor === 'object' && !Object.isFrozen(valor)) {
158
+ Object.values(valor).forEach(congelarProfundo);
159
+ Object.freeze(valor);
160
+ }
161
+ return valor;
162
+ }
163
+
164
+ // =============================================================================
165
+ // MODULOS_CRITICOS — los 13 módulos del kernel W2 (F29/F30) + los 7 de F31,
166
+ // con su(s) testFile(s) real(es) verificado(s) con Glob/Grep contra el árbol
167
+ // (32-CONTEXTO.md/memo de diseño de T-07 § 0). Array LITERAL hardcodeado —
168
+ // NUNCA descubierto con fs.readdirSync: el orden de un listado de directorio
169
+ // no está garantizado estable entre filesystems/plataformas, y esto rompería
170
+ // el determinismo del reporte (§ ensamblarReporte).
171
+ // =============================================================================
172
+ // (HALLAZGO real durante T-07, opción A de arreglar-al-detectar.md). El test
173
+ // "T-01 usa módulos locales y conserva su aislamiento durante Core-3" de
174
+ // tests/scripts/policy-bundle.test.js corre `git -c core.autocrlf=false diff
175
+ // <baseCommit> -- gateway .github ...` esperando diff vacío. Esa aserción
176
+ // depende de una OPTIMIZACIÓN DE STAT-CACHE de git: si el mtime de un
177
+ // archivo coincide con el cacheado en el índice, git confía en el OID
178
+ // cacheado SIN re-hashear el contenido — enmascarando que el blob commiteado
179
+ // está en LF puro mientras el archivo real en disco está en CRLF (git
180
+ // show <commit>:gateway/index.js = 9770 bytes sin CRLF; el archivo real =
181
+ // 10090 bytes con CRLF). Raíz verificada de forma determinante: un simple
182
+ // `touch gateway/index.js` en el repo REAL (sin tocar contenido) hace que
183
+ // ESTA MISMA aserción falle ahí también — es una fragilidad preexistente de
184
+ // la aserción, no un defecto introducido por este motor. CUALQUIER copia
185
+ // fresca (checkout de `.git`, clon, `cpSync`) invalida el stat-cache y
186
+ // revela la discrepancia LF/CRLF que ya existía. Como este chequeo de rutas
187
+ // congeladas es ortogonal a la lógica real de harness-source-snapshot.js/
188
+ // policy-bundle.js (los módulos que este motor sí mutation-testea), se
189
+ // excluye ese test puntual vía `--test-skip-pattern` al spawnear
190
+ // policy-bundle.test.js para estos 2 módulos — nunca se modifica el archivo
191
+ // de test (fuera del boundary de escritura de F32) ni se enmascara un
192
+ // hallazgo real de las guardas de seguridad que sí ejercitan estos módulos.
193
+ const TEST_SKIP_PATTERN_GIT_STATCACHE = 'T-01 usa m.dulos locales y conserva su aislamiento durante Core-3';
194
+
195
+ // integrity-ledger.test.js incluye 3 tests que forkean procesos reales
196
+ // (childProcess.fork/spawn) para ejercitar concurrencia de append y reclaim de
197
+ // lock. Bajo el harness de mutation testing son NO DETERMINISTAS: además de
198
+ // colgar bajo carga (timeout — ya tratado como kill válido, ver
199
+ // TIMEOUT_MS_POR_DEFECTO), producen fallos-assertion FLAKY por carreras entre los
200
+ // forks reales, independientes de la mutación bajo prueba. Un fallo-flaky así,
201
+ // coincidiendo con un mutante EQUIVALENTE anotado, dispararía EN FALSO el
202
+ // invariante fail-closed (MUTATION_EQUIVALENTE_MATADO) y volvería el Gate no
203
+ // reproducible. Se excluyen del scoring de mutación (siguen corriendo en la suite
204
+ // normal de CI para su cobertura). El descubrimiento determinista de T-07 confirmó
205
+ // 0 mutantes críticos MATABLES cubiertos SÓLO por estos tests: excluirlos no pierde
206
+ // señal (246/249 críticos muertos, 3 sobrevivientes todos equivalentes anotados).
207
+ // Ver ADR-0055 § Determinismo del Gate para integrity-ledger.
208
+ const TEST_SKIP_PATTERN_INTEGRITY_LEDGER = 'dos procesos reales|worker que falla antes|dos reclaimers reales';
209
+
210
+ const MODULOS_CRITICOS = Object.freeze([
211
+ Object.freeze({
212
+ modulo: 'scripts/lib/harness-source-snapshot.js', testFiles: Object.freeze(['tests/scripts/policy-bundle.test.js']),
213
+ directoriosExtra: DIRECTORIOS_EXTRA_PAQUETE_COMPLETO, testSkipPattern: TEST_SKIP_PATTERN_GIT_STATCACHE,
214
+ }),
215
+ Object.freeze({
216
+ modulo: 'scripts/lib/policy-bundle.js', testFiles: Object.freeze(['tests/scripts/policy-bundle.test.js']),
217
+ directoriosExtra: DIRECTORIOS_EXTRA_PAQUETE_COMPLETO, testSkipPattern: TEST_SKIP_PATTERN_GIT_STATCACHE,
218
+ }),
219
+ Object.freeze({ modulo: 'scripts/lib/signed-envelope.js', testFiles: Object.freeze(['tests/lib/signed-envelope.test.js']) }),
220
+ Object.freeze({ modulo: 'scripts/lib/policy-contract.js', testFiles: Object.freeze(['tests/lib/policy-contract.test.js']) }),
221
+ Object.freeze({ modulo: 'scripts/lib/principal-verifier.js', testFiles: Object.freeze(['tests/lib/principal-verifier.test.js']) }),
222
+ // (HALLAZGO real de fix de granularidad, corrida diagnóstica 2026-07-17,
223
+ // ver DEUDA-TECNICA.md#DT-T07-MUTANTES-CRITICOS-SOBREVIVIENTES § granularidad):
224
+ // `policy-context-provider.test.js` compila el policy bundle real en cada
225
+ // test (`compilarPolicyBundle` -> `capturarHarnessSourceSnapshot`), que
226
+ // requiere `.git` (`resolverRaizGit`/`ejecutarGit`) Y el ÁRBOL de trabajo
227
+ // real (`leerFuente`/`validarSegmentos` leen `agentes/`, `habilidades/`,
228
+ // `comandos/`, `reglas/` DIRECTAMENTE del filesystem, no vía git plumbing).
229
+ // CORRECCIÓN DE UNA FALSA DECLARACIÓN PROPIA (segunda iteración de este
230
+ // mismo fix): `['.git']` a secas pasaba en un diagnóstico AISLADO (sin
231
+ // `plantillaGit` compartida) porque, sin plantilla, `copiarGit` cae a
232
+ // `clonarGitLocal` — un checkout COMPLETO real que de paso trae todo el
233
+ // árbol de trabajo. En la corrida real (`ejecutarMutationTesting`, que SÍ
234
+ // usa una `plantillaGit` compartida) `copiarGit` sólo copia el subdirectorio
235
+ // `.git` de la plantilla, NO su árbol de trabajo — un módulo que declare
236
+ // sólo `.git` queda con `.git` pero SIN `agentes/`, dando ENOENT real
237
+ // ("lstat ... agentes") en la corrida de punta a punta. Por eso, igual que
238
+ // los módulos #1/#2, se declara `DIRECTORIOS_EXTRA_PAQUETE_COMPLETO`
239
+ // completo (ya incluye `.git` + el árbol de trabajo real).
240
+ Object.freeze({
241
+ modulo: 'scripts/lib/policy-context-provider.js', testFiles: Object.freeze(['tests/lib/policy-context-provider.test.js']),
242
+ directoriosExtra: DIRECTORIOS_EXTRA_PAQUETE_COMPLETO,
243
+ }),
244
+ Object.freeze({ modulo: 'scripts/lib/approval-receipts.js', testFiles: Object.freeze(['tests/lib/approval-receipts.test.js']) }),
245
+ Object.freeze({ modulo: 'scripts/lib/evidence-verifier.js', testFiles: Object.freeze(['tests/lib/evidence-verifier.test.js']) }),
246
+ Object.freeze({ modulo: 'scripts/lib/integrity-ledger.js', testFiles: Object.freeze(['tests/lib/integrity-ledger.test.js']), testSkipPattern: TEST_SKIP_PATTERN_INTEGRITY_LEDGER }),
247
+ Object.freeze({ modulo: 'scripts/lib/single-use-store.js', testFiles: Object.freeze(['tests/lib/single-use-store.test.js']) }),
248
+ Object.freeze({ modulo: 'scripts/lib/gate-engine.js', testFiles: Object.freeze(['tests/lib/gate-engine.test.js']) }),
249
+ // (HALLAZGO real, misma corrida diagnóstica): `policy-broker.test.js`
250
+ // combina el mismo `compilarPolicyBundle` (requiere el árbol completo +
251
+ // `.git`, ver nota arriba junto a policy-context-provider.js) CON
252
+ // `loadPolicyCorpus()` que lee `manifiestos/policy-corpus-w2.json`
253
+ // directamente. `DIRECTORIOS_EXTRA_PAQUETE_COMPLETO` ya incluye
254
+ // `manifiestos`, así que cubre ambos requisitos con una sola declaración.
255
+ Object.freeze({
256
+ modulo: 'scripts/lib/policy-broker.js', testFiles: Object.freeze(['tests/lib/policy-broker.test.js']),
257
+ directoriosExtra: DIRECTORIOS_EXTRA_PAQUETE_COMPLETO,
258
+ }),
259
+ // (HALLAZGO real, misma corrida diagnóstica, tres iteraciones): ambos
260
+ // testFiles importan `tests/_helpers/sandbox.js` (patrón obligatorio de
261
+ // cleanup de `~/.claude/rules/tests-cleanup.md`) — directorio HERMANO de
262
+ // `tests/lib`, no incluido por DIRECTORIOS_POR_DEFECTO.
263
+ // `legacy-gate-migration.js` ADEMÁS requiere `hooks/lib/atomic-write.js`
264
+ // directamente (mismo patrón que `runtime-compiler.js`, ver abajo). Y
265
+ // `gate-cli-integration.test.js` spawnea el CLI real
266
+ // (`CLI = path.join(__dirname, '..', '..', 'bin', 'swl-ses.js')`) como
267
+ // proceso hijo — requiere `bin`, y `bin/swl-ses.js` a su vez hace
268
+ // `require('../package.json')` para resolver metadata del paquete
269
+ // (sólo LECTURA de la copia efímera — nunca se escribe, ver Whitelist F32).
270
+ // Los cuatro hallazgos verificados con `correrLineaBase` aislado en
271
+ // iteraciones sucesivas.
272
+ Object.freeze({
273
+ modulo: 'scripts/lib/legacy-gate-migration.js',
274
+ testFiles: Object.freeze(['tests/scripts/migrar-gates-legacy.test.js', 'tests/scripts/gate-cli-integration.test.js']),
275
+ directoriosExtra: Object.freeze(['tests/_helpers', 'hooks', 'bin', 'package.json']),
276
+ }),
277
+ // (HALLAZGO real, misma corrida diagnóstica): `runtime-adapter-contract.test.js`
278
+ // lee `manifiestos/runtime-adapters-core3.json` y `manifiestos/runtime-conformance.json`
279
+ // directamente (contrato Core-3 real, no un fixture). Verificado con
280
+ // `correrLineaBase` aislado: ENOENT en ambos archivos sin esta declaración.
281
+ Object.freeze({
282
+ modulo: 'scripts/lib/runtime-adapter-contract.js', testFiles: Object.freeze(['tests/lib/runtime-adapter-contract.test.js']),
283
+ directoriosExtra: Object.freeze(['manifiestos']),
284
+ }),
285
+ Object.freeze({ modulo: 'scripts/lib/runtime-artifact-verifier.js', testFiles: Object.freeze(['tests/lib/runtime-artifact-verifier.test.js']) }),
286
+ // (HALLAZGO real, diagnóstico original del orquestador, confirmado y
287
+ // ampliado por `correrLineaBase` aislado en varias iteraciones):
288
+ // `runtime-compiler.js` depende transitivamente de `hooks/lib/atomic-write.js`
289
+ // vía `scripts/lib/toml-merge.js` (MODULE_NOT_FOUND sin `hooks`) Y su
290
+ // testFile compila el harness IR real (`compilarHarnessIR` ->
291
+ // `capturarHarnessSourceSnapshot`), que requiere el árbol completo + `.git`
292
+ // (mismo patrón que policy-context-provider.js, ver nota arriba).
293
+ // `DIRECTORIOS_EXTRA_PAQUETE_COMPLETO` ya incluye `hooks`, así que cubre
294
+ // ambos requisitos con una sola declaración.
295
+ Object.freeze({
296
+ modulo: 'scripts/lib/runtime-compiler.js', testFiles: Object.freeze(['tests/lib/runtime-compiler.test.js']),
297
+ directoriosExtra: DIRECTORIOS_EXTRA_PAQUETE_COMPLETO,
298
+ }),
299
+ Object.freeze({ modulo: 'scripts/lib/runtime-operation-adapter.js', testFiles: Object.freeze(['tests/lib/runtime-operation-adapter.test.js']) }),
300
+ Object.freeze({ modulo: 'scripts/lib/runtime-build-attestation.js', testFiles: Object.freeze(['tests/lib/runtime-build-attestation.test.js']) }),
301
+ // (HALLAZGO real, misma corrida diagnóstica): `runtime-doctor-core3.test.js`
302
+ // lee `manifiestos/runtime-adapters-core3.json` directamente. Verificado
303
+ // con `correrLineaBase` aislado: ENOENT sin esta declaración.
304
+ Object.freeze({
305
+ modulo: 'scripts/lib/runtime-doctor-core3.js', testFiles: Object.freeze(['tests/lib/runtime-doctor-core3.test.js']),
306
+ directoriosExtra: Object.freeze(['manifiestos']),
307
+ }),
308
+ // (HALLAZGO real, misma corrida diagnóstica, varias iteraciones): igual
309
+ // que legacy-gate-migration.js, importa `tests/_helpers/sandbox.js`
310
+ // (directorio HERMANO de `tests/lib`, no cubierto por
311
+ // DIRECTORIOS_EXTRA_PAQUETE_COMPLETO). Y, como runtime-compiler.js/
312
+ // policy-context-provider.js, su testFile ejercita rutas que compilan el
313
+ // harness real y requieren el árbol completo + `.git` — se agrega
314
+ // `tests/_helpers` al paquete completo (no basta con `.git` solo, ver nota
315
+ // arriba junto a policy-context-provider.js).
316
+ Object.freeze({
317
+ modulo: 'scripts/lib/runtime-bundle-installer.js', testFiles: Object.freeze(['tests/lib/runtime-bundle-installer.test.js']),
318
+ directoriosExtra: Object.freeze([...DIRECTORIOS_EXTRA_PAQUETE_COMPLETO, 'tests/_helpers']),
319
+ }),
320
+ ]);
321
+
322
+ // Metadata descriptiva de los 5 operadores — sólo para el reporte/documentación,
323
+ // la lógica real vive en generarMutantes().
324
+ const OPERADORES = Object.freeze([
325
+ Object.freeze({ id: 'M1', nombre: 'Negar igualdad estricta', patron: '=== <-> !==' }),
326
+ Object.freeze({ id: 'M2', nombre: 'Negar operador lógico', patron: '&& <-> ||' }),
327
+ Object.freeze({ id: 'M3', nombre: 'Invertir literal booleano', patron: 'true <-> false' }),
328
+ Object.freeze({ id: 'M4', nombre: 'Relajar umbral numérico', patron: '[<>]=? entero (genera +1 y -1)' }),
329
+ Object.freeze({
330
+ id: 'M5', nombre: 'Neutralizar guard de seguridad',
331
+ patron: 'fallar(...)/throw ...; standalone -> comentada; if (cond) fallar/throw; -> if (false) ...',
332
+ }),
333
+ ]);
334
+
335
+ // =============================================================================
336
+ // enmascarar — reemplaza el contenido de strings, comentarios y literales
337
+ // regex por relleno neutro ('#'), preservando longitud y saltos de línea
338
+ // reales. Los operadores M1-M5 sólo escanean el texto YA enmascarado, para
339
+ // nunca mutar dentro de un string/comentario/regex.
340
+ // =============================================================================
341
+
342
+ const PALABRAS_INICIO_REGEX = new Set([
343
+ 'return', 'typeof', 'case', 'in', 'of', 'new', 'instanceof', 'throw', 'yield', 'await',
344
+ ]);
345
+ const CHARS_INICIO_REGEX = new Set(['{', '(', ',', '=', ':', '[', '!', '&', '|', '?', '}', ';', '\n']);
346
+
347
+ function esInicioDeRegex(colaCode) {
348
+ const recortado = colaCode.replace(/\s+$/u, '');
349
+ if (recortado.length === 0) return true;
350
+ const ultimo = recortado[recortado.length - 1];
351
+ if (CHARS_INICIO_REGEX.has(ultimo)) return true;
352
+ const coincidencia = recortado.match(/[A-Za-z_$][A-Za-z0-9_$]*$/u);
353
+ return Boolean(coincidencia && PALABRAS_INICIO_REGEX.has(coincidencia[0]));
354
+ }
355
+
356
+ /** Busca, desde `inicio` (justo tras el '/' de apertura), un '/' de cierre no
357
+ * escapado y fuera de una clase de caracteres `[...]`, antes del siguiente
358
+ * '\n'. Devuelve -1 si no hay cierre en la misma línea (un regex literal de
359
+ * JS nunca puede contener un salto de línea real sin escapar) — esto acota
360
+ * el riesgo de falso-positivo descrito en el memo de diseño: si no hay
361
+ * cierre en la línea, NO es un regex, se trata el '/' como código normal. */
362
+ function buscarCierreRegexEnLinea(codigoFuente, inicio) {
363
+ let dentroDeClase = false;
364
+ for (let j = inicio; j < codigoFuente.length; j += 1) {
365
+ const c = codigoFuente[j];
366
+ if (c === '\n') return -1;
367
+ if (c === '\\') { j += 1; continue; }
368
+ if (c === '[') dentroDeClase = true;
369
+ else if (c === ']') dentroDeClase = false;
370
+ else if (c === '/' && !dentroDeClase) return j;
371
+ }
372
+ return -1;
373
+ }
374
+
375
+ function enmascararDelimitado(codigoFuente, salida, inicio, delimitador) {
376
+ let i = inicio;
377
+ const n = codigoFuente.length;
378
+ while (i < n) {
379
+ const d = codigoFuente[i];
380
+ if (d === '\n') { salida[i] = '\n'; i += 1; continue; }
381
+ if (d === '\\' && i + 1 < n) { salida[i] = '#'; salida[i + 1] = '#'; i += 2; continue; }
382
+ salida[i] = '#';
383
+ i += 1;
384
+ if (d === delimitador) break;
385
+ }
386
+ return i;
387
+ }
388
+
389
+ function enmascararComentarioLinea(codigoFuente, salida, inicio) {
390
+ let i = inicio;
391
+ salida[i] = '#'; salida[i + 1] = '#'; i += 2;
392
+ const n = codigoFuente.length;
393
+ while (i < n && codigoFuente[i] !== '\n') { salida[i] = '#'; i += 1; }
394
+ return i;
395
+ }
396
+
397
+ function enmascararComentarioBloque(codigoFuente, salida, inicio) {
398
+ let i = inicio;
399
+ salida[i] = '#'; salida[i + 1] = '#'; i += 2;
400
+ const n = codigoFuente.length;
401
+ while (i < n) {
402
+ if (codigoFuente[i] === '\n') { salida[i] = '\n'; i += 1; continue; }
403
+ if (codigoFuente[i] === '*' && codigoFuente[i + 1] === '/') {
404
+ salida[i] = '#'; salida[i + 1] = '#'; i += 2; break;
405
+ }
406
+ salida[i] = '#'; i += 1;
407
+ }
408
+ return i;
409
+ }
410
+
411
+ function enmascararRegexLiteral(codigoFuente, salida, inicio, fin) {
412
+ let j = inicio;
413
+ for (; j <= fin; j += 1) salida[j] = '#';
414
+ j += 1;
415
+ while (j < codigoFuente.length && /[a-z]/iu.test(codigoFuente[j])) { salida[j] = '#'; j += 1; }
416
+ return j;
417
+ }
418
+
419
+ /** Procesa un único carácter de posición `i` del enmascarado (string,
420
+ * comentario de línea/bloque, regex-literal o código normal) y devuelve el
421
+ * nuevo índice y la nueva "cola de código" para decidir si el siguiente '/'
422
+ * puede ser inicio de regex. Extraído de `enmascarar` (hallazgo MENOR de
423
+ * verificación T-07: la función excedía el límite de 30 líneas de
424
+ * estilo-codigo.md). Puro. */
425
+ function procesarCaracter(codigoFuente, salida, i, colaCode) {
426
+ const c = codigoFuente[i];
427
+ if (c === "'" || c === '"' || c === '`') {
428
+ salida[i] = '#';
429
+ return { i: enmascararDelimitado(codigoFuente, salida, i + 1, c), colaCode: '' };
430
+ }
431
+ if (c === '/' && codigoFuente[i + 1] === '/') {
432
+ return { i: enmascararComentarioLinea(codigoFuente, salida, i), colaCode: '' };
433
+ }
434
+ if (c === '/' && codigoFuente[i + 1] === '*') {
435
+ return { i: enmascararComentarioBloque(codigoFuente, salida, i), colaCode: '' };
436
+ }
437
+ if (c === '/' && esInicioDeRegex(colaCode)) {
438
+ const fin = buscarCierreRegexEnLinea(codigoFuente, i + 1);
439
+ if (fin !== -1) {
440
+ return { i: enmascararRegexLiteral(codigoFuente, salida, i, fin), colaCode: '' };
441
+ }
442
+ }
443
+ salida[i] = c;
444
+ return { i: i + 1, colaCode: c === '\n' ? '' : (colaCode + c).slice(-20) };
445
+ }
446
+
447
+ /** Enmascara strings/comentarios/regex-literales de un código fuente JS,
448
+ * preservando longitud exacta y saltos de línea reales. Puro. */
449
+ function enmascarar(codigoFuente) {
450
+ const n = codigoFuente.length;
451
+ const salida = new Array(n);
452
+ let colaCode = '';
453
+ let i = 0;
454
+ while (i < n) {
455
+ ({ i, colaCode } = procesarCaracter(codigoFuente, salida, i, colaCode));
456
+ }
457
+ return salida.join('');
458
+ }
459
+
460
+ // =============================================================================
461
+ // generarMutantes — escanea el texto ENMASCARADO línea por línea y produce
462
+ // mutantes cuyos offsets (inicioAbs/finAbs) apuntan al código fuente ORIGINAL.
463
+ // =============================================================================
464
+
465
+ function calcularOffsetsDeLineas(lineas) {
466
+ const offsets = new Array(lineas.length);
467
+ let acumulado = 0;
468
+ for (let i = 0; i < lineas.length; i += 1) {
469
+ offsets[i] = acumulado;
470
+ acumulado += lineas[i].length + 1;
471
+ }
472
+ return offsets;
473
+ }
474
+
475
+ function construirMutante(ctx, operador, inicioRel, finRel, original, mutado) {
476
+ return {
477
+ modulo: ctx.rutaRelativa, linea: ctx.numeroLinea, operador, original, mutado,
478
+ inicioAbs: ctx.offsetLinea + inicioRel, finAbs: ctx.offsetLinea + finRel,
479
+ };
480
+ }
481
+
482
+ function agregarM1(mutantes, ctx) {
483
+ for (const m of ctx.lineaMask.matchAll(/===|!==/gu)) {
484
+ const mutado = m[0] === '===' ? '!==' : '===';
485
+ mutantes.push(construirMutante(ctx, 'M1', m.index, m.index + m[0].length, m[0], mutado));
486
+ }
487
+ }
488
+
489
+ function agregarM2(mutantes, ctx) {
490
+ for (const m of ctx.lineaMask.matchAll(/&&|\|\|/gu)) {
491
+ const mutado = m[0] === '&&' ? '||' : '&&';
492
+ mutantes.push(construirMutante(ctx, 'M2', m.index, m.index + m[0].length, m[0], mutado));
493
+ }
494
+ }
495
+
496
+ function agregarM3(mutantes, ctx) {
497
+ for (const m of ctx.lineaMask.matchAll(/\btrue\b|\bfalse\b/gu)) {
498
+ const mutado = m[0] === 'true' ? 'false' : 'true';
499
+ mutantes.push(construirMutante(ctx, 'M3', m.index, m.index + m[0].length, m[0], mutado));
500
+ }
501
+ }
502
+
503
+ function agregarM4(mutantes, ctx) {
504
+ for (const m of ctx.lineaMask.matchAll(/[<>]=?\s*([0-9]+)/gu)) {
505
+ const digitos = m[1];
506
+ const inicio = m.index + (m[0].length - digitos.length);
507
+ const fin = inicio + digitos.length;
508
+ const valor = Number.parseInt(digitos, 10);
509
+ for (const delta of [1, -1]) {
510
+ mutantes.push(construirMutante(ctx, 'M4', inicio, fin, digitos, String(valor + delta)));
511
+ }
512
+ }
513
+ }
514
+
515
+ const RE_STANDALONE_GUARD = /^(fallar\([\s\S]*\);|throw\s[\s\S]*;)$/u;
516
+ const RE_IF_GUARD = /^(\s*if\s*\()([\s\S]*?)(\)\s*(?:fallar\(|throw\s)[\s\S]*;\s*)$/u;
517
+
518
+ function detectarGuardEnLinea(lineaMask) {
519
+ const m = lineaMask.match(RE_IF_GUARD);
520
+ if (!m) return null;
521
+ const inicioCondicion = m[1].length;
522
+ const finCondicion = inicioCondicion + m[2].length;
523
+ return { inicioCondicion, finCondicion };
524
+ }
525
+
526
+ function agregarM5(mutantes, ctx) {
527
+ const trimMask = ctx.lineaMask.trim();
528
+ if (RE_STANDALONE_GUARD.test(trimMask)) {
529
+ const indent = ctx.lineaOriginal.match(/^\s*/u)[0];
530
+ const contenido = ctx.lineaOriginal.slice(indent.length);
531
+ const mutado = `${indent}// mutación: ${contenido}`;
532
+ mutantes.push(construirMutante(ctx, 'M5', 0, ctx.lineaOriginal.length, ctx.lineaOriginal, mutado));
533
+ return;
534
+ }
535
+ const guard = detectarGuardEnLinea(ctx.lineaMask);
536
+ if (!guard) return;
537
+ const original = ctx.lineaOriginal.slice(guard.inicioCondicion, guard.finCondicion);
538
+ mutantes.push(construirMutante(ctx, 'M5', guard.inicioCondicion, guard.finCondicion, original, 'false'));
539
+ }
540
+
541
+ function agregarMutantesDeLinea(mutantes, ctx) {
542
+ agregarM1(mutantes, ctx);
543
+ agregarM2(mutantes, ctx);
544
+ agregarM3(mutantes, ctx);
545
+ agregarM4(mutantes, ctx);
546
+ agregarM5(mutantes, ctx);
547
+ }
548
+
549
+ /** Genera todos los mutantes M1-M5 de un módulo. Puro: sólo texto en
550
+ * memoria, ningún I/O ni reloj. El orden de recorrido (línea ascendente,
551
+ * M1->M2->M3->M4->M5 dentro de cada línea) es determinista — memo §8. */
552
+ function generarMutantes(codigoFuente, rutaRelativa) {
553
+ const enmascarado = enmascarar(codigoFuente);
554
+ const lineasOriginal = codigoFuente.split('\n');
555
+ const lineasMask = enmascarado.split('\n');
556
+ const offsets = calcularOffsetsDeLineas(lineasOriginal);
557
+ const mutantes = [];
558
+ for (let i = 0; i < lineasOriginal.length; i += 1) {
559
+ agregarMutantesDeLinea(mutantes, {
560
+ lineaOriginal: lineasOriginal[i], lineaMask: lineasMask[i],
561
+ offsetLinea: offsets[i], numeroLinea: i + 1, rutaRelativa,
562
+ });
563
+ }
564
+ return mutantes;
565
+ }
566
+
567
+ function aplicarMutante(codigoFuente, mutante) {
568
+ return codigoFuente.slice(0, mutante.inicioAbs) + mutante.mutado + codigoFuente.slice(mutante.finAbs);
569
+ }
570
+
571
+ // =============================================================================
572
+ // clasificarCritico — REQ-32-07 "100% de mutantes críticos eliminados"; regla
573
+ // exacta de §3 del memo de diseño. Deliberadamente conservadora hacia marcar
574
+ // de más como crítico, nunca hacia excluir.
575
+ // =============================================================================
576
+
577
+ const RE_DECLARACION_FUNCION = /^function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/u;
578
+ // (HALLAZGO real de verificación T-07, MAYOR): los 20 módulos críticos
579
+ // declaran funciones de capability-token ("no falsificable vía WeakMap+owner")
580
+ // como propiedades de objeto con arrow function -- `isGateReceipt: (value) =>
581
+ // ...` (gate-engine.js:850-851, single-use-store.js:340-341,
582
+ // integrity-ledger.js:1043-1044, policy-broker.js:334) -- que
583
+ // RE_DECLARACION_FUNCION nunca detectaba (sólo matchea `function nombre(`).
584
+ const RE_DECLARACION_PROPIEDAD_ARROW = /^\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*:\s*(?:async\s+)?\([^()]*\)\s*=>/u;
585
+ // (HALLAZGO real de verificación T-07, MAYOR, mismo defecto de clase): un
586
+ // grep amplio de los 20 módulos críticos (todos F29/F30/F31, nombrados en
587
+ // inglés) confirmó que RE_NOMBRE_FUNCION_CRITICA sólo reconocía prefijos en
588
+ // ESPAÑOL -- "validar"/"verificar"/"puede[A-Z]"/"es[A-Z]" nunca matchean
589
+ // "validate"/"verify"/"can[A-Z]"/"is[A-Z]" (decenas de funciones validate*/
590
+ // verify* en policy-contract.js, principal-verifier.js,
591
+ // policy-context-provider.js, approval-receipts.js, evidence-verifier.js,
592
+ // single-use-store.js, gate-engine.js, legacy-gate-migration.js,
593
+ // runtime-build-attestation.js, runtime-doctor-core3.js; canPersistInvalidation
594
+ // en gate-engine.js; isDataObject/isDataArray/isRealDirectory en
595
+ // runtime-doctor-core3.js, isVerifiedOperation/isKernelDependencies en
596
+ // policy-contract.js, fail() en legacy-gate-migration.js/
597
+ // runtime-build-attestation.js) -- se agregan los equivalentes ingleses
598
+ // paralelos a los ya existentes en español (misma exigencia de mayúscula
599
+ // tras el prefijo para is[A-Z]/can[A-Z], igual que es[A-Z]/puede[A-Z], para
600
+ // no matchear substrings como "issueGrant"/"canonicalHash").
601
+ const RE_NOMBRE_FUNCION_CRITICA = /^(validar|validate|verificar|verify|fallar|fail|es[A-Z]|is[A-Z]|puede[A-Z]|can[A-Z]|comparar|coincide)/u;
602
+ const IDENTIFICADORES_INTEGRIDAD = Object.freeze(['digest', 'Hash', 'signature', 'Digest', 'SHA256', 'firma']);
603
+ const IDENTIFICADORES_UMBRAL = Object.freeze(['MAX_', 'Length', 'byteLength', 'size', 'sequence', 'expiresAt', 'issuedAt']);
604
+
605
+ function nombreFuncionContenedora(lineasMask, numeroLinea) {
606
+ for (let i = numeroLinea - 1; i >= 0; i -= 1) {
607
+ const linea = lineasMask[i];
608
+ const declaracion = linea.match(RE_DECLARACION_FUNCION) || linea.match(RE_DECLARACION_PROPIEDAD_ARROW);
609
+ if (declaracion) return declaracion[1];
610
+ }
611
+ return null;
612
+ }
613
+
614
+ /** Clasifica un mutante como crítico ("cambia una decisión de seguridad")
615
+ * según §3 del memo. `codigoFuenteEnmascarado` es el texto YA enmascarado
616
+ * del módulo completo (identificadores y nombres de función siguen
617
+ * visibles en el enmascarado — sólo strings/comentarios/regex se ocultan).
618
+ * Puro. */
619
+ function clasificarCritico(mutante, codigoFuenteEnmascarado) {
620
+ if (mutante.operador === 'M5') return true;
621
+
622
+ const lineasMask = codigoFuenteEnmascarado.split('\n');
623
+ const nombreFuncion = nombreFuncionContenedora(lineasMask, mutante.linea);
624
+ if (nombreFuncion && RE_NOMBRE_FUNCION_CRITICA.test(nombreFuncion)) return true;
625
+
626
+ const lineaMask = lineasMask[mutante.linea - 1] || '';
627
+ if ((mutante.operador === 'M1' || mutante.operador === 'M2')
628
+ && IDENTIFICADORES_INTEGRIDAD.some((id) => lineaMask.includes(id))) {
629
+ return true;
630
+ }
631
+ if (mutante.operador === 'M4' && IDENTIFICADORES_UMBRAL.some((id) => lineaMask.includes(id))) {
632
+ return true;
633
+ }
634
+ return false;
635
+ }
636
+
637
+ // =============================================================================
638
+ // Ejecución real: copia efímera + spawn asíncrono + clasificación
639
+ // muerto/sobrevivió. Única parte del módulo con efectos.
640
+ // =============================================================================
641
+
642
+ /** Clona `.git` local con checkout completo (`--shared` referencia los
643
+ * objects reales del repo vía alternates — sin copiar el historial) en
644
+ * `destino`, que debe estar vacío en este punto. Requiere el checkout real
645
+ * (no `--no-checkout`): `harness-source-snapshot.js` lee `.git/index`
646
+ * directamente, y ese archivo sólo existe tras un checkout — ver comentario
647
+ * de DIRECTORIOS_EXTRA_PAQUETE_COMPLETO. */
648
+ function clonarGitLocal(raizRepo, destino) {
649
+ execFileSync('git', ['clone', '--local', '--shared', raizRepo, destino], { stdio: 'pipe' });
650
+ }
651
+
652
+ /** Crea UNA plantilla de `.git` (clon+checkout completo) reutilizable por
653
+ * TODAS las tareas que la necesiten (memo T-07 § hallazgo policy-bundle.test.js).
654
+ * Medido: clonar de nuevo por tarea cuesta ~3.6s (dominado por escribir
655
+ * ~1700 archivos en NTFS) — con 222 tareas eso paralizó la corrida real bajo
656
+ * concurrencia (contención de I/O/antivirus). La plantilla post-checkout
657
+ * pesa sólo ~260KB (--shared no copia objects) y copiarla por tarea cuesta
658
+ * ~30ms — la mutación real sólo toca `scripts/lib/<modulo>.js`, nunca
659
+ * `.git`, así que compartir la plantilla entre tareas es seguro: cada tarea
660
+ * la copia (nunca la enlaza — un `.git/index` enlazado se RECHAZA
661
+ * explícitamente por `harness-source-snapshot.js#resolverIndiceGit`). */
662
+ function prepararPlantillaGit(raizRepo) {
663
+ const plantilla = fs.mkdtempSync(path.join(os.tmpdir(), 'swl-w4-git-plantilla-'));
664
+ // (HALLAZGO real de verificación T-07, MAYOR): si clonarGitLocal lanza
665
+ // (git ausente, fallo de disco/permisos), el directorio ya creado por
666
+ // mkdtempSync quedaba huérfano -- nada lo referenciaba para limpiarlo
667
+ // (el finally del caller sólo limpia `plantillaGit`, que nunca llegaba a
668
+ // asignarse). Se limpia aquí mismo antes de relanzar.
669
+ try {
670
+ clonarGitLocal(raizRepo, plantilla);
671
+ } catch (error) {
672
+ fs.rmSync(plantilla, { recursive: true, force: true });
673
+ throw error;
674
+ }
675
+ return plantilla;
676
+ }
677
+
678
+ /** Combina DIRECTORIOS_POR_DEFECTO (o el override del caller) con los
679
+ * `directoriosExtra` propios de un módulo (memo T-07 § hallazgo
680
+ * policy-bundle.test.js) — nunca penaliza a los módulos que no los
681
+ * necesitan. */
682
+ function directoriosParaModulo(directoriosBase, entrada) {
683
+ if (!entrada || !Array.isArray(entrada.directoriosExtra) || entrada.directoriosExtra.length === 0) {
684
+ return directoriosBase;
685
+ }
686
+ return [...new Set([...directoriosBase, ...entrada.directoriosExtra])];
687
+ }
688
+
689
+ function copiarGit(raizRepo, destino, plantillaGit) {
690
+ if (plantillaGit) {
691
+ fs.cpSync(path.join(plantillaGit, '.git'), path.join(destino, '.git'), { recursive: true });
692
+ return;
693
+ }
694
+ if (fs.existsSync(path.join(raizRepo, '.git'))) clonarGitLocal(raizRepo, destino);
695
+ }
696
+
697
+ function copiarArboles(raizRepo, destino, directorios, plantillaGit) {
698
+ if (directorios.includes('.git')) copiarGit(raizRepo, destino, plantillaGit);
699
+ for (const dir of directorios) {
700
+ if (dir === '.git') continue;
701
+ const origen = path.join(raizRepo, dir);
702
+ if (!fs.existsSync(origen)) continue;
703
+ fs.cpSync(origen, path.join(destino, dir), { recursive: true });
704
+ }
705
+ }
706
+
707
+ /** Decide QUÉ comando mataría el árbol completo del proceso `pid` para
708
+ * `platform` (parámetro explícito, nunca lee `process.platform`
709
+ * internamente — así ambas ramas son testeables desde cualquier SO). Puro.
710
+ *
711
+ * (HALLAZGO real de verificación T-07, CRÍTICO): la versión previa usaba
712
+ * EXCLUSIVAMENTE `taskkill` (Windows). La CI real del proyecto corre en
713
+ * `ubuntu-latest` (.github/workflows/ci.yml, paso "Ejecutar tests
714
+ * unitarios"): en Linux/macOS `execFileSync('taskkill', ...)` lanza ENOENT
715
+ * y se tragaba en el catch vacío de `matarArbolDeProceso` SIN matar al
716
+ * proceso colgado — reproducido experimentalmente: un proceso Node padre
717
+ * con stdio en modo pipe se queda esperando indefinidamente a un hijo
718
+ * colgado que nunca murió. En POSIX no hay 'kill de árbol' nativo sin
719
+ * recorrer /proc (fuera de zero-deps): la alternativa portable es spawnear
720
+ * el hijo como líder de su propio grupo de procesos (`detached: true` en
721
+ * `spawnTestsAsync`) y matar el GRUPO completo con `process.kill(-pid,
722
+ * 'SIGKILL')` — alcanza también a los nietos (forks internos de módulos
723
+ * como integrity-ledger.test.js). */
724
+ function decidirComandoMatarArbol(pid, platform) {
725
+ if (platform === 'win32') {
726
+ return { ejecutable: 'taskkill', argv: ['/pid', String(pid), '/t', '/f'] };
727
+ }
728
+ return { grupoPosix: true, pid };
729
+ }
730
+
731
+ /** Best-effort: mata el árbol completo del proceso (no sólo el hijo directo
732
+ * — módulos como integrity-ledger.test.js forkean workers propios). Puede
733
+ * fallar si el proceso ya terminó entre el timeout y el intento de matarlo;
734
+ * eso no es un error real, sólo una carrera benigna. */
735
+ function matarArbolDeProceso(pid) {
736
+ const comando = decidirComandoMatarArbol(pid, process.platform);
737
+ try {
738
+ if (comando.grupoPosix) {
739
+ process.kill(-comando.pid, 'SIGKILL');
740
+ } else {
741
+ execFileSync(comando.ejecutable, comando.argv, { stdio: 'pipe' });
742
+ }
743
+ } catch {
744
+ // Ignorado a propósito — ver docstring.
745
+ }
746
+ }
747
+
748
+ function spawnTestsAsync(cwd, testFiles, testSkipPattern, timeoutMs) {
749
+ return new Promise((resolve) => {
750
+ const argv = ['--test', '--test-reporter=tap', '--test-reporter-destination=stdout'];
751
+ if (testSkipPattern) argv.push(`--test-skip-pattern=${testSkipPattern}`);
752
+ argv.push(...testFiles);
753
+ // detached=true en POSIX: el hijo se vuelve líder de su propio grupo de
754
+ // procesos, condición necesaria para poder matar el GRUPO completo con
755
+ // `process.kill(-pid, ...)` en decidirComandoMatarArbol — en win32 no
756
+ // aplica (taskkill /t recorre el árbol por sí solo).
757
+ const proceso = spawn(
758
+ process.execPath, argv, { cwd, shell: false, detached: process.platform !== 'win32' }
759
+ );
760
+ let stdout = '';
761
+ let stderr = '';
762
+ let resuelto = false;
763
+ const limite = Number.isInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs : TIMEOUT_MS_POR_DEFECTO;
764
+ const temporizador = setTimeout(() => {
765
+ if (resuelto) return;
766
+ resuelto = true;
767
+ matarArbolDeProceso(proceso.pid);
768
+ resolve({ exitCode: null, stdout, stderr, agotoTiempo: true });
769
+ }, limite);
770
+ proceso.stdout.on('data', (dato) => { stdout += dato; });
771
+ proceso.stderr.on('data', (dato) => { stderr += dato; });
772
+ proceso.on('close', (exitCode) => {
773
+ if (resuelto) return;
774
+ resuelto = true;
775
+ clearTimeout(temporizador);
776
+ resolve({ exitCode, stdout, stderr, agotoTiempo: false });
777
+ });
778
+ });
779
+ }
780
+
781
+ const RE_NOT_OK = /^\s*not ok \d+ - (.+)$/mu;
782
+
783
+ function construirEntradaMutante(mutante, critico, estado, testQueLoMato, detailCode) {
784
+ return {
785
+ modulo: mutante.modulo, linea: mutante.linea, operador: mutante.operador,
786
+ original: mutante.original, mutado: mutante.mutado, inicioAbs: mutante.inicioAbs,
787
+ critico, estado, testQueLoMato, detailCode,
788
+ };
789
+ }
790
+
791
+ /** Clasifica el resultado de correr los tests contra un mutante: timeout ->
792
+ * muerto por TIMEOUT (el mutante convirtió una terminación en un colgado —
793
+ * ver TIMEOUT_MS_POR_DEFECTO); exit 0 -> sobrevivió; exit != 0 con "not ok"
794
+ * en el TAP -> muerto (por ese test); exit != 0 sin "not ok" -> muerto por
795
+ * SYNTAX_ERROR (el proceso no llegó a emitir TAP — típicamente el módulo
796
+ * mutado no cargó). Ver memo §5: contar SYNTAX_ERROR como muerto es la
797
+ * opción conservadora y difícil de gamear; TIMEOUT sigue el mismo criterio. */
798
+ function clasificarResultado(mutante, critico, resultado) {
799
+ if (resultado.agotoTiempo) {
800
+ return construirEntradaMutante(mutante, critico, 'muerto', null, 'TIMEOUT');
801
+ }
802
+ if (resultado.exitCode === 0) {
803
+ return construirEntradaMutante(mutante, critico, 'sobrevivio', null, null);
804
+ }
805
+ const coincidencia = resultado.stdout.match(RE_NOT_OK);
806
+ if (!coincidencia) {
807
+ return construirEntradaMutante(mutante, critico, 'muerto', null, 'SYNTAX_ERROR');
808
+ }
809
+ return construirEntradaMutante(mutante, critico, 'muerto', coincidencia[1].trim(), null);
810
+ }
811
+
812
+ /** Salvaguarda anti-gaming (memo §5): si más del 1% de los mutantes de un
813
+ * módulo terminan en SYNTAX_ERROR, es señal de un enmascarado roto (los 5
814
+ * operadores son sintaxis-preservantes por construcción), no de guardas
815
+ * reales — lanza en vez de reportar un score que mentiría. Puro. */
816
+ function verificarTasaSyntaxError(resultadosModulo, nombreModulo) {
817
+ const total = resultadosModulo.length;
818
+ if (total === 0) return;
819
+ const sintacticos = resultadosModulo.filter((r) => r.detailCode === 'SYNTAX_ERROR').length;
820
+ if (sintacticos / total > UMBRAL_SYNTAX_ERROR) {
821
+ fallar(
822
+ 'MUTATION_ENGINE_INVALID',
823
+ `demasiados mutantes con SYNTAX_ERROR en ${nombreModulo}: ${sintacticos}/${total} `
824
+ + `(umbral ${UMBRAL_SYNTAX_ERROR * 100}%) — señal de enmascarado roto, no de guardas reales`
825
+ );
826
+ }
827
+ }
828
+
829
+ async function ejecutarUnaTarea(tarea, raizRepo, plantillaGit) {
830
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swl-w4-mutante-'));
831
+ try {
832
+ copiarArboles(raizRepo, tmp, tarea.directoriosACopiar, plantillaGit);
833
+ const contenidoMutado = aplicarMutante(tarea.codigoFuente, tarea.mutante);
834
+ fs.writeFileSync(path.join(tmp, tarea.mutante.modulo), contenidoMutado, 'utf8');
835
+ const resultado = await spawnTestsAsync(tmp, tarea.testFiles, tarea.testSkipPattern, tarea.timeoutMs);
836
+ return clasificarResultado(tarea.mutante, tarea.critico, resultado);
837
+ } finally {
838
+ fs.rmSync(tmp, { recursive: true, force: true });
839
+ }
840
+ }
841
+
842
+ async function ejecutarTareasConcurrentes(tareas, concurrencia, raizRepo, plantillaGit, onProgreso) {
843
+ const resultados = new Array(tareas.length);
844
+ let siguiente = 0;
845
+ let completadas = 0;
846
+
847
+ async function trabajador() {
848
+ for (;;) {
849
+ const indice = siguiente;
850
+ siguiente += 1;
851
+ if (indice >= tareas.length) return;
852
+ resultados[indice] = await ejecutarUnaTarea(tareas[indice], raizRepo, plantillaGit);
853
+ completadas += 1;
854
+ if (onProgreso) onProgreso({ completadas, total: tareas.length, ultimo: resultados[indice] });
855
+ }
856
+ }
857
+
858
+ const nTrabajadores = Math.max(1, Math.min(concurrencia, tareas.length || 1));
859
+ await Promise.all(Array.from({ length: nTrabajadores }, trabajador));
860
+ return resultados;
861
+ }
862
+
863
+ // =============================================================================
864
+ // Ensamblado del reporte — determinista (memo §8 punto 4): orden fijo por
865
+ // (índice de módulo en `modulos`, línea ascendente, orden de operador
866
+ // M1..M5), aplicado ANTES de congelar, sin importar el orden real en que
867
+ // terminaron las tareas concurrentes.
868
+ // =============================================================================
869
+
870
+ function calcularScore(muertos, total) {
871
+ if (total === 0) return 100;
872
+ return Math.round((muertos / total) * 10000) / 100;
873
+ }
874
+
875
+ const ORDEN_OPERADOR = Object.freeze({ M1: 0, M2: 1, M3: 2, M4: 3, M5: 4 });
876
+
877
+ function ordenarMutantes(resultados, modulos) {
878
+ const indice = new Map(modulos.map((m, i) => [m.modulo, i]));
879
+ return [...resultados].sort((a, b) => {
880
+ const ia = indice.get(a.modulo);
881
+ const ib = indice.get(b.modulo);
882
+ if (ia !== ib) return ia - ib;
883
+ if (a.linea !== b.linea) return a.linea - b.linea;
884
+ return ORDEN_OPERADOR[a.operador] - ORDEN_OPERADOR[b.operador];
885
+ });
886
+ }
887
+
888
+ // ADR-0055: mutantes verificados como EQUIVALENTES — inmatables por diseño fail-closed
889
+ // del kernel W2 (el operador mutado produce el MISMO código/mensaje observable por ambos
890
+ // caminos; una validación redundante posterior atrapa lo que la anterior dejaría pasar).
891
+ // Clave: modulo\0linea\0operador\0original\0mutado. Cada entrada pasa verificación
892
+ // adversarial (un escéptico intenta matarla) antes de entrar; append-only con razón.
893
+ // Se excluyen del DENOMINADOR de críticos SÓLO cuando sobreviven; un anotado que resulte
894
+ // MUERTO hace FALLAR el motor (verificarAnotacionEquivalentes) porque la anotación
895
+ // mentiría. La lista se puebla desde la verificación adversarial del cierre de DT-T07.
896
+ const MUTANTES_EQUIVALENTES = new Set([
897
+ "scripts/lib/policy-bundle.js 253 M4 0 1 10891", // policy-bundle L253 off10891: El regex M4 (/[<>]=?\s*[0-9]+/) solo matchea el '0' final de '.localeCompare(item) < 0'; NO el 'indice === 0' (no precedido de < o >), pese a que el…
898
+ "scripts/lib/signed-envelope.js 133 M5 error.codigo === 'IJSON_INVALID' false 5253", // signed-envelope L133 off5253: El guard controla, dentro del catch de canonicalizarIJson, si se relanza el error original (rama verdadera) o se envuelve en uno nuevo vía…
899
+ "scripts/lib/policy-contract.js 59 M2 || && 2186", // policy-contract L59 off2186: Nodo 2 (segundo `||`, aísla `Array.isArray(valor)`).
900
+ "scripts/lib/policy-contract.js 59 M2 || && 2215", // policy-contract L59 off2215: Nodo 2 (segundo `||`, aísla `Array.isArray(valor)`).
901
+ "scripts/lib/policy-contract.js 101 M3 false true 3798", // policy-contract L101 off3798: Default `permitirVacio = false` mutado a `true`.
902
+ "scripts/lib/policy-contract.js 102 M2 || && 3840", // policy-contract L102 off3840: Nodo que aísla `(!permitirVacio && valor.length === 0)` del resto de la cadena `||`.
903
+ "scripts/lib/policy-contract.js 118 M2 || && 4416", // policy-contract L118 off4416: Segundo nodo de L118 (aísla `item === '..'` de `!item || item === '.' || item === '..'`).
904
+ "scripts/lib/policy-contract.js 159 M2 || && 6100", // policy-contract L159 off6100: Nodo que aísla `!Array.isArray(effect.argv)` en la cadena de validarProcessEffect.
905
+ "scripts/lib/policy-contract.js 191 M4 0 -1 7567", // policy-contract L191 off7567: `index > 0` mutado a `index > -1`.
906
+ "scripts/lib/policy-contract.js 208 M5 fallar('CONTRACT_INVALID', 'effect desconocido');\r // mutación: fallar('CONTRACT_INVALID', 'effect desconocido');\r 8109", // policy-contract L208 off8109: `fallar('CONTRACT_INVALID', 'effect desconocido')` comentado.
907
+ "scripts/lib/principal-verifier.js 145 M2 || && 6213", // principal-verifier L145 off6213: El termino C (current.rootKeyDigest !== authority.rootKeyDigest) es estructuralmente inalcanzable como true al llegar a la linea 145:…
908
+ "scripts/lib/approval-receipts.js 34 M2 || && 1469", // approval-receipts L34 off1469: isScoped (línea 34) solo se usa para (a) elegir el arreglo de claves esperado por exactKeys() (línea 37) y (b) el guard de la línea 53.
909
+ "scripts/lib/evidence-verifier.js 73 M4 0 -1 3293", // evidence-verifier L73 off3293: canonical.some((value, index) => index > 0 && canonical[index - 1] >= value).
910
+ "scripts/lib/single-use-store.js 229 M2 || && 9863", // single-use-store L229 off9863: La línea 229 contiene DOS operadores || físicos distintos que colapsan a la misma clave de reporte (mismo módulo/línea/operador/original/mutado): (a)…
911
+ "scripts/lib/runtime-artifact-verifier.js 197 M2 || && 7781", // runtime-artifact-verifier L197 off7781: Mismo fundamento estructural: B (`includes(':')`) es SIEMPRE false en la línea 197 porque SAFE_RELATIVE_PATH (líneas 192-195, evaluado antes) rechaza…
912
+ "scripts/lib/runtime-artifact-verifier.js 197 M2 || && 7807", // runtime-artifact-verifier L197 off7807: Mismo fundamento estructural: B (`includes(':')`) es SIEMPRE false en la línea 197 porque SAFE_RELATIVE_PATH (líneas 192-195, evaluado antes) rechaza…
913
+ "scripts/lib/runtime-operation-adapter.js 84 M2 || && 3219", // runtime-operation-adapter L84 off3219: esObjetoPlano(valor) = A||B||C||D con A=!valor, B=typeof valor!=='object', C=Array.isArray(valor), D=util.types.isProxy(valor).
914
+ "scripts/lib/runtime-operation-adapter.js 84 M2 || && 3248", // runtime-operation-adapter L84 off3248: esObjetoPlano(valor) = A||B||C||D con A=!valor, B=typeof valor!=='object', C=Array.isArray(valor), D=util.types.isProxy(valor).
915
+ "scripts/lib/runtime-operation-adapter.js 85 M2 || && 3279", // runtime-operation-adapter L85 off3279: Unica ocurrencia de '||' en L85, entre el termino C (Array.isArray) y D (isProxy): original A||B||C||D se muta a A||B||(C&&D).
916
+ "scripts/lib/runtime-operation-adapter.js 91 M5 !esObjetoPlano(valor) false 3482", // runtime-operation-adapter L91 off3482: El guard `if (!esObjetoPlano(valor)) throw new TypeError('objeto invalido');` en validarClaves se desactiva por completo con la mutacion.
917
+ "scripts/lib/runtime-doctor-core3.js 78 M2 || && 2882", // runtime-doctor-core3 L78 off2882: La condición completa es `!value || A || B || C` con A = `typeof value !== 'object'` y B = `Array.isArray(value)`.
918
+ "scripts/lib/runtime-doctor-core3.js 105 M4 0 -1 4150", // runtime-doctor-core3 L105 off4150: El único valor donde `entry.length > 0` y `entry.length > -1` difieren es `entry.length === 0`, es decir `entry === ''` (el .length de un string…
919
+ "scripts/lib/integrity-ledger.js 165 M3 true false 6065", // integrity-ledger L165 off6065: Mutación sobre el parámetro por defecto 'privado = true' de validarPermisos(stat, codigo, esDirectorio=false, privado=true).
920
+ "scripts/lib/integrity-ledger.js 711 M5 indice === inicio false 26125", // integrity-ledger L711 off26125: dividirJsonl vive dentro del try de parsearLedger cuyo catch convierte toda excepcion a LEDGER_INVALID; indice===inicio produce un buffer de longitud…
921
+ "scripts/lib/gate-engine.js 572 M2 || && 25671", // gate-engine L572 off25671: validateActiveG1 compara approval.planDigest !== state.planDigest.
922
+ "scripts/lib/gate-engine.js 573 M2 || && 25722", // gate-engine L573 off25722: Mismo patrón que L572 pero con planBindingDigest/activeApprovalDigest: state.activeApprovalDigest=payload.approvalDigest se asigna en la MISMA…
923
+ "scripts/lib/gate-engine.js 574 M2 || && 25790", // gate-engine L574 off25790: Compara approval.approverDigest !== context.config.actorBindings.human_approver.
924
+ "scripts/lib/gate-engine.js 633 M1 === !== 28533", // gate-engine L633 off28533: Documentado en sesión previa (comentario ya existente en el archivo de test): la asignación task.g3EvidenceDigest=evidences[0].evidenceDigest escribe…
925
+ "scripts/lib/integrity-ledger.js 843 M3 false true 31149", // integrity-ledger L843 off31149: esAutoridad=false es un default DEAD CODE: ambos call-sites (L897 y L932) pasan el 2do argumento explicitamente, el default nunca se ejercita.
926
+ ]);
927
+
928
+ // La clave incluye inicioAbs (offset absoluto del operador en el fuente) para
929
+ // ser ÚNICA por mutante: una línea puede tener varios operadores iguales (p.ej.
930
+ // `!a || b || c` genera 3 mutantes M2 ||->&& con idéntico {linea,operador,
931
+ // original,mutado}); sin el offset, anotar uno como equivalente anotaría a los
932
+ // tres, confundiendo un matable con un equivalente. inicioAbs es estable porque
933
+ // los 20 módulos del kernel están congelados (whitelist F32). Ver ADR-0055.
934
+ function claveMutanteEquivalente(m) {
935
+ return [m.modulo, m.linea, m.operador, m.original, m.mutado, m.inicioAbs].join(' ');
936
+ }
937
+
938
+ function esEquivalenteSobreviviente(m) {
939
+ return m.estado === 'sobrevivio' && MUTANTES_EQUIVALENTES.has(claveMutanteEquivalente(m));
940
+ }
941
+
942
+ // Invariante fail-closed (ADR-0055 § Anti-gaming): un mutante anotado como equivalente
943
+ // que resulte MUERTO demuestra que la anotación es falsa (un equivalente genuino es
944
+ // inmatable). El motor falla en vez de excluirlo o inflar el score en silencio.
945
+ function verificarAnotacionEquivalentes(mutantes) {
946
+ // Un anotado equivalente que muere por RUIDO de infraestructura (TIMEOUT del
947
+ // motor bajo carga, o SYNTAX_ERROR de un enmascarado roto) NO invalida la
948
+ // anotación: ni el timeout ni el syntax-error son un test que distinga la
949
+ // lógica del mutante del original — un equivalente genuino, por definición,
950
+ // no altera la terminación ni la sintaxis, así que un timeout/syntax-error
951
+ // sobre él es ruido, no detección. Sólo una muerte por un TEST real
952
+ // (detailCode ausente) demuestra que la anotación es falsa. Ver ADR-0055
953
+ // § "Ruido de infraestructura: tests de workers concurrentes".
954
+ const anotadoMatadoPorTest = mutantes.find(
955
+ (m) => m.estado === 'muerto' && !m.detailCode && MUTANTES_EQUIVALENTES.has(claveMutanteEquivalente(m))
956
+ );
957
+ if (anotadoMatadoPorTest) {
958
+ fallar('MUTATION_EQUIVALENTE_MATADO',
959
+ `mutante anotado equivalente fue matado por un test (anotación falsa): ${claveMutanteEquivalente(anotadoMatadoPorTest)}`);
960
+ }
961
+ }
962
+
963
+ // Cota anti-crecimiento-silencioso del conjunto de equivalentes (endurecimiento
964
+ // tras la barrida de estabilización 2026-07-20; ADR-0055/0056 § Anti-gaming).
965
+ // El vector de gaming detectado: `esEquivalenteSobreviviente` excluye un mutante
966
+ // crítico SOBREVIVIENTE del denominador de críticos, subiendo el critical-score
967
+ // a 100%; `verificarAnotacionEquivalentes` sólo caza la dirección contraria
968
+ // (anotado que MUERE). Un maker podría mislabelar un crítico sobreviviente como
969
+ // equivalente para inflar el score. Bajo ADR-0056 (operador único, maker =
970
+ // revisor, misma superficie pineada) esto NO es prevenible por máquina — el
971
+ // maker que agrega el equivalente falso controla también cualquier control en el
972
+ // mismo tarball. Lo que SÍ se puede hacer es volverlo tamper-EVIDENTE: pinear el
973
+ // tamaño del conjunto para que ninguna alta/baja pase en silencio; toda mutación
974
+ // del conjunto exige bumpear esta cota explícitamente — un cambio visible en el
975
+ // diff, revisable por CODEOWNERS. Controles de buena fe restantes: la
976
+ // justificación por-entrada (comentarios de cada clave) y la verificación
977
+ // adversarial previa a anotar (ADR-0055, invariante 3). La garantía adversarial
978
+ // plena sólo se recupera con Ruta B (identidad independiente del juez).
979
+ const MUTANTES_EQUIVALENTES_ESPERADOS = 29;
980
+
981
+ // `tamano` es inyectable sólo para tests; el gate (ensamblarReporte) siempre lo
982
+ // llama sin argumentos, atándose al tamaño real del conjunto pineado.
983
+ function verificarCotaEquivalentes(tamano = MUTANTES_EQUIVALENTES.size) {
984
+ if (tamano !== MUTANTES_EQUIVALENTES_ESPERADOS) {
985
+ fallar('MUTATION_EQUIVALENTES_COTA',
986
+ `el conjunto de mutantes equivalentes cambió de tamaño (${tamano} vs ${MUTANTES_EQUIVALENTES_ESPERADOS} pineados): `
987
+ + 'toda alta/baja debe bumpear la cota explícitamente y justificarse (ADR-0055/0056 § Anti-gaming)');
988
+ }
989
+ }
990
+
991
+ function construirEntradaModulo(entrada, mutantes) {
992
+ const delModulo = mutantes.filter((m) => m.modulo === entrada.modulo);
993
+ const muertos = delModulo.filter((m) => m.estado === 'muerto').length;
994
+ const criticos = delModulo.filter((m) => m.critico && !esEquivalenteSobreviviente(m));
995
+ const criticosMuertos = criticos.filter((m) => m.estado === 'muerto').length;
996
+ return {
997
+ modulo: entrada.modulo, testFiles: [...entrada.testFiles],
998
+ totalMutantes: delModulo.length, muertos, sobrevivientes: delModulo.length - muertos,
999
+ score: calcularScore(muertos, delModulo.length),
1000
+ criticos: { total: criticos.length, muertos: criticosMuertos },
1001
+ };
1002
+ }
1003
+
1004
+ function construirGlobal(mutantes) {
1005
+ const total = mutantes.length;
1006
+ const muertos = mutantes.filter((m) => m.estado === 'muerto').length;
1007
+ const criticos = mutantes.filter((m) => m.critico && !esEquivalenteSobreviviente(m));
1008
+ const criticosMuertos = criticos.filter((m) => m.estado === 'muerto').length;
1009
+ return {
1010
+ totalMutantes: total, muertos, sobrevivientes: total - muertos,
1011
+ score: calcularScore(muertos, total),
1012
+ criticos: { total: criticos.length, muertos: criticosMuertos, score: calcularScore(criticosMuertos, criticos.length) },
1013
+ };
1014
+ }
1015
+
1016
+ function ensamblarReporte(modulos, resultados) {
1017
+ const mutantesOrdenados = ordenarMutantes(resultados, modulos);
1018
+ verificarAnotacionEquivalentes(mutantesOrdenados);
1019
+ verificarCotaEquivalentes();
1020
+ return {
1021
+ kind: 'swl-mutation-report',
1022
+ schemaVersion: SCHEMA_VERSION,
1023
+ generatedAt: new Date().toISOString(),
1024
+ global: construirGlobal(mutantesOrdenados),
1025
+ porModulo: modulos.map((entrada) => construirEntradaModulo(entrada, mutantesOrdenados)),
1026
+ mutantes: mutantesOrdenados,
1027
+ };
1028
+ }
1029
+
1030
+ // =============================================================================
1031
+ // ejecutarMutationTesting — orquestador público. `raizRepo`/`modulos`/
1032
+ // `directoriosACopiar` son inyectables EXCLUSIVAMENTE para que los tests
1033
+ // rápidos del propio motor usen un repo-fixture sintético en vez del kernel
1034
+ // real de 8,102 líneas; el uso de certificación real no pasa overrides.
1035
+ // =============================================================================
1036
+
1037
+ function leerPristinos(raizRepo, modulos) {
1038
+ const mapa = new Map();
1039
+ for (const entrada of modulos) {
1040
+ mapa.set(entrada.modulo, fs.readFileSync(path.join(raizRepo, entrada.modulo), 'utf8'));
1041
+ }
1042
+ return mapa;
1043
+ }
1044
+
1045
+ /** Corre, SIN mutar, los testFiles de cada módulo — cada módulo dentro de su
1046
+ * PROPIA copia efímera, construida con EXACTAMENTE los mismos
1047
+ * `directoriosParaModulo(directoriosACopiar, entrada)` que usarán después sus
1048
+ * tareas de mutante reales (`generarTareas`). Si algo falla aquí, es la
1049
+ * señal correcta y temprana de que a ESE módulo le falta declarar su propio
1050
+ * `directoriosExtra` — nunca un problema del entorno compartido (memo § Paso 2).
1051
+ *
1052
+ * CORRECCIÓN DE GRANULARIDAD (hallazgo real T-07, post-corrida completa
1053
+ * 2026-07-17, ver DEUDA-TECNICA.md#DT-T07-MUTANTES-CRITICOS-SOBREVIVIENTES):
1054
+ * la versión previa copiaba UNA sola vez la UNIÓN de `directoriosExtra` de
1055
+ * todos los módulos del batch (`directoriosUnionParaModulos`, eliminada) y
1056
+ * corría los testFiles de cada módulo DENTRO de esa copia compartida. Con
1057
+ * los 20 módulos críticos juntos, esa unión incluía siempre
1058
+ * `DIRECTORIOS_EXTRA_PAQUETE_COMPLETO` (declarado sólo por los módulos #1/#2)
1059
+ * — enmascarando que otros módulos (p.ej. `runtime-compiler.js`, que
1060
+ * requiere transitivamente `hooks/lib/atomic-write.js` vía
1061
+ * `scripts/lib/toml-merge.js`) nunca declararon su propio `directoriosExtra`.
1062
+ * El baseline "pasaba" para esos módulos SOLO por la presencia accidental de
1063
+ * directorios que otro módulo del batch necesitaba — pero sus tareas de
1064
+ * mutante reales (`generarTareas`, que sí usa `directoriosParaModulo` por
1065
+ * módulo, sin unión) fallaban después en CADA mutante por MODULE_NOT_FOUND,
1066
+ * contado falsamente como "muerto" por SYNTAX_ERROR (mide entorno roto, no
1067
+ * detección real de mutación). Correr el baseline con la copia AISLADA por
1068
+ * módulo (misma función `directoriosParaModulo` que usan las tareas reales)
1069
+ * expone el hueco de inmediato, antes de generar un solo mutante. */
1070
+ async function correrLineaBase(raizRepo, modulos, directoriosACopiar, timeoutMs, plantillaGit) {
1071
+ for (const entrada of modulos) {
1072
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swl-w4-mutacion-baseline-'));
1073
+ try {
1074
+ copiarArboles(raizRepo, tmp, directoriosParaModulo(directoriosACopiar, entrada), plantillaGit);
1075
+ const resultado = await spawnTestsAsync(tmp, entrada.testFiles, entrada.testSkipPattern, timeoutMs);
1076
+ if (resultado.exitCode !== 0 || resultado.agotoTiempo) {
1077
+ fallar(
1078
+ 'MUTATION_BASELINE_FAILED',
1079
+ `línea base falló para ${entrada.modulo} (${entrada.testFiles.join(', ')}): `
1080
+ + `exit=${resultado.exitCode} agotoTiempo=${resultado.agotoTiempo}\n${resultado.stdout}\n${resultado.stderr}`
1081
+ );
1082
+ }
1083
+ } finally {
1084
+ fs.rmSync(tmp, { recursive: true, force: true });
1085
+ }
1086
+ }
1087
+ }
1088
+
1089
+ function generarTareas(modulos, pristinos, directoriosBase, timeoutMs, filtroClaves) {
1090
+ const tareas = [];
1091
+ for (const entrada of modulos) {
1092
+ const codigoFuente = pristinos.get(entrada.modulo);
1093
+ const enmascarado = enmascarar(codigoFuente);
1094
+ const directoriosACopiar = directoriosParaModulo(directoriosBase, entrada);
1095
+ for (const mutante of generarMutantes(codigoFuente, entrada.modulo)) {
1096
+ // filtroClaves (opt-in, diagnóstico): cuando es un Set no vacío, sólo se
1097
+ // ejecutan los mutantes cuya clave está en él. Por defecto (null) el
1098
+ // comportamiento es idéntico — NO afecta el Gate. Ver probar-mutante (T-07).
1099
+ if (filtroClaves && !filtroClaves.has(claveMutanteEquivalente({ modulo: entrada.modulo, ...mutante }))) continue;
1100
+ tareas.push({
1101
+ mutante, codigoFuente, testFiles: entrada.testFiles, directoriosACopiar, timeoutMs,
1102
+ testSkipPattern: entrada.testSkipPattern,
1103
+ critico: clasificarCritico(mutante, enmascarado),
1104
+ });
1105
+ }
1106
+ }
1107
+ return tareas;
1108
+ }
1109
+
1110
+ /**
1111
+ * Ejecuta mutation testing completo sobre `modulos` (por defecto,
1112
+ * `MODULOS_CRITICOS`, los 20 módulos críticos de F29/F30/F31). Efectos:
1113
+ * crea/borra directorios temporales, escribe archivos ahí, spawnea procesos
1114
+ * `node --test` reales. NUNCA toca los módulos originales en disco — cada
1115
+ * mutante vive sólo en una copia efímera de `os.tmpdir()`.
1116
+ *
1117
+ * Cada entrada de `modulos` puede declarar, además de `modulo`/`testFiles`:
1118
+ * `directoriosExtra` (string[], directorios/archivos adicionales a copiar
1119
+ * SÓLO para las tareas de ese módulo — ver DIRECTORIOS_EXTRA_PAQUETE_COMPLETO)
1120
+ * y `testSkipPattern` (string, pasado como `--test-skip-pattern` al spawnear
1121
+ * ese módulo — ver TEST_SKIP_PATTERN_GIT_STATCACHE). Ambos son excepciones
1122
+ * puntuales descubiertas durante T-07, documentadas junto a su constante.
1123
+ *
1124
+ * @param {object} [opciones]
1125
+ * @param {string} [opciones.raizRepo] - raíz del repo a copiar (default: raíz real de swl-ses).
1126
+ * @param {Array<{modulo:string,testFiles:string[],directoriosExtra?:string[],testSkipPattern?:string}>} [opciones.modulos] - default: MODULOS_CRITICOS.
1127
+ * @param {string[]} [opciones.directoriosACopiar] - default: DIRECTORIOS_POR_DEFECTO.
1128
+ * @param {number} [opciones.concurrencia] - tareas (módulo,mutante) concurrentes (default: 1).
1129
+ * @param {number} [opciones.timeoutMs] - límite por corrida de tests antes de matar el proceso
1130
+ * y clasificar el mutante como muerto por TIMEOUT (default: TIMEOUT_MS_POR_DEFECTO, 150s —
1131
+ * un mutante puede convertir una guarda de terminación en un colgado indefinido; ver hallazgo
1132
+ * junto a esa constante).
1133
+ * @param {(progreso:{completadas:number,total:number}) => void} [opciones.onProgreso]
1134
+ * @returns {Promise<object>} MutationReport (ver schemas/w4-mutation-report.schema.json).
1135
+ */
1136
+ async function ejecutarMutationTesting(opciones = {}) {
1137
+ const raizRepo = opciones.raizRepo || path.resolve(__dirname, '..', '..');
1138
+ const modulos = opciones.modulos || MODULOS_CRITICOS;
1139
+ const directoriosACopiar = opciones.directoriosACopiar || DIRECTORIOS_POR_DEFECTO;
1140
+ const concurrencia = Number.isInteger(opciones.concurrencia) && opciones.concurrencia > 0 ? opciones.concurrencia : 1;
1141
+ const timeoutMs = Number.isInteger(opciones.timeoutMs) && opciones.timeoutMs > 0 ? opciones.timeoutMs : TIMEOUT_MS_POR_DEFECTO;
1142
+ const onProgreso = typeof opciones.onProgreso === 'function' ? opciones.onProgreso : null;
1143
+ const filtroClaves = opciones.filtroClaves instanceof Set && opciones.filtroClaves.size > 0 ? opciones.filtroClaves : null;
1144
+
1145
+ const pristinos = leerPristinos(raizRepo, modulos);
1146
+ // Calculado sobre `modulos` (no sobre `tareas`, que todavía no existe):
1147
+ // misma condición — algún módulo, con su propio directoriosParaModulo,
1148
+ // necesita '.git'. Se prepara ANTES de la línea base porque ésta ahora
1149
+ // corre aislada por módulo (ver correrLineaBase) y también necesita la
1150
+ // plantilla para los módulos que declaran '.git' en su directoriosExtra.
1151
+ const necesitaGit = modulos.some((entrada) => directoriosParaModulo(directoriosACopiar, entrada).includes('.git'));
1152
+ const plantillaGit = necesitaGit ? prepararPlantillaGit(raizRepo) : null;
1153
+ try {
1154
+ await correrLineaBase(raizRepo, modulos, directoriosACopiar, timeoutMs, plantillaGit);
1155
+ const tareas = generarTareas(modulos, pristinos, directoriosACopiar, timeoutMs, filtroClaves);
1156
+ const resultados = await ejecutarTareasConcurrentes(tareas, concurrencia, raizRepo, plantillaGit, onProgreso);
1157
+
1158
+ for (const entrada of modulos) {
1159
+ verificarTasaSyntaxError(resultados.filter((r) => r.modulo === entrada.modulo), entrada.modulo);
1160
+ }
1161
+
1162
+ return ensamblarReporte(modulos, resultados);
1163
+ } finally {
1164
+ if (plantillaGit) fs.rmSync(plantillaGit, { recursive: true, force: true });
1165
+ }
1166
+ }
1167
+
1168
+ // =============================================================================
1169
+ // validateMutationReport — validación sin efectos del MutationReport.
1170
+ // =============================================================================
1171
+
1172
+ const REPORT_KEYS = ['kind', 'schemaVersion', 'generatedAt', 'global', 'porModulo', 'mutantes'];
1173
+ const GLOBAL_KEYS = ['totalMutantes', 'muertos', 'sobrevivientes', 'score', 'criticos'];
1174
+ const GLOBAL_CRITICOS_KEYS = ['total', 'muertos', 'score'];
1175
+ const MODULO_ENTRY_KEYS = ['modulo', 'testFiles', 'totalMutantes', 'muertos', 'sobrevivientes', 'score', 'criticos'];
1176
+ const MODULO_CRITICOS_KEYS = ['total', 'muertos'];
1177
+ const MUTANTE_KEYS = ['modulo', 'linea', 'operador', 'original', 'mutado', 'inicioAbs', 'critico', 'estado', 'testQueLoMato', 'detailCode'];
1178
+ const OPERADORES_VALIDOS = new Set(['M1', 'M2', 'M3', 'M4', 'M5']);
1179
+ const ESTADOS_VALIDOS = new Set(['muerto', 'sobrevivio']);
1180
+ const DETAIL_CODES_VALIDOS = new Set(['SYNTAX_ERROR', 'TIMEOUT']);
1181
+
1182
+ function validarClaves(valor, esperadas, etiqueta) {
1183
+ if (!esObjetoPlano(valor)) fallar('MUTATION_REPORT_INVALID', `${etiqueta} inválido`);
1184
+ const actuales = Object.keys(valor).sort();
1185
+ const objetivo = [...esperadas].sort();
1186
+ if (JSON.stringify(actuales) !== JSON.stringify(objetivo)) {
1187
+ fallar('MUTATION_REPORT_INVALID', `${etiqueta} inválido`);
1188
+ }
1189
+ }
1190
+
1191
+ function validarScore(valor, etiqueta) {
1192
+ if (typeof valor !== 'number' || Number.isNaN(valor) || valor < 0 || valor > 100) {
1193
+ fallar('MUTATION_REPORT_INVALID', `${etiqueta} inválido`);
1194
+ }
1195
+ }
1196
+
1197
+ function validarEnteroNoNegativo(valor, etiqueta) {
1198
+ if (!Number.isInteger(valor) || valor < 0) fallar('MUTATION_REPORT_INVALID', `${etiqueta} inválido`);
1199
+ }
1200
+
1201
+ function validarCriticosGlobal(criticos) {
1202
+ validarClaves(criticos, GLOBAL_CRITICOS_KEYS, 'global.criticos');
1203
+ validarEnteroNoNegativo(criticos.total, 'global.criticos.total');
1204
+ validarEnteroNoNegativo(criticos.muertos, 'global.criticos.muertos');
1205
+ if (criticos.muertos > criticos.total) fallar('MUTATION_REPORT_INVALID', 'global.criticos inconsistente');
1206
+ validarScore(criticos.score, 'global.criticos.score');
1207
+ }
1208
+
1209
+ function validarGlobal(global) {
1210
+ validarClaves(global, GLOBAL_KEYS, 'global');
1211
+ validarEnteroNoNegativo(global.totalMutantes, 'global.totalMutantes');
1212
+ validarEnteroNoNegativo(global.muertos, 'global.muertos');
1213
+ validarEnteroNoNegativo(global.sobrevivientes, 'global.sobrevivientes');
1214
+ if (global.muertos + global.sobrevivientes !== global.totalMutantes) {
1215
+ fallar('MUTATION_REPORT_INVALID', 'global inconsistente');
1216
+ }
1217
+ validarScore(global.score, 'global.score');
1218
+ validarCriticosGlobal(global.criticos);
1219
+ }
1220
+
1221
+ function validarModuloEntry(entrada) {
1222
+ validarClaves(entrada, MODULO_ENTRY_KEYS, 'porModulo[]');
1223
+ if (typeof entrada.modulo !== 'string' || entrada.modulo.length === 0) {
1224
+ fallar('MUTATION_REPORT_INVALID', 'porModulo[].modulo inválido');
1225
+ }
1226
+ if (!Array.isArray(entrada.testFiles) || entrada.testFiles.length === 0
1227
+ || !entrada.testFiles.every((f) => typeof f === 'string' && f.length > 0)) {
1228
+ fallar('MUTATION_REPORT_INVALID', 'porModulo[].testFiles inválido');
1229
+ }
1230
+ validarEnteroNoNegativo(entrada.totalMutantes, 'porModulo[].totalMutantes');
1231
+ validarEnteroNoNegativo(entrada.muertos, 'porModulo[].muertos');
1232
+ validarEnteroNoNegativo(entrada.sobrevivientes, 'porModulo[].sobrevivientes');
1233
+ if (entrada.muertos + entrada.sobrevivientes !== entrada.totalMutantes) {
1234
+ fallar('MUTATION_REPORT_INVALID', 'porModulo[] inconsistente');
1235
+ }
1236
+ validarScore(entrada.score, 'porModulo[].score');
1237
+ validarClaves(entrada.criticos, MODULO_CRITICOS_KEYS, 'porModulo[].criticos');
1238
+ validarEnteroNoNegativo(entrada.criticos.total, 'porModulo[].criticos.total');
1239
+ validarEnteroNoNegativo(entrada.criticos.muertos, 'porModulo[].criticos.muertos');
1240
+ if (entrada.criticos.muertos > entrada.criticos.total) {
1241
+ fallar('MUTATION_REPORT_INVALID', 'porModulo[].criticos inconsistente');
1242
+ }
1243
+ }
1244
+
1245
+ function validarMutanteEntry(entrada) {
1246
+ validarClaves(entrada, MUTANTE_KEYS, 'mutantes[]');
1247
+ if (typeof entrada.modulo !== 'string' || entrada.modulo.length === 0) {
1248
+ fallar('MUTATION_REPORT_INVALID', 'mutantes[].modulo inválido');
1249
+ }
1250
+ if (!Number.isInteger(entrada.linea) || entrada.linea < 1) {
1251
+ fallar('MUTATION_REPORT_INVALID', 'mutantes[].linea inválido');
1252
+ }
1253
+ if (!OPERADORES_VALIDOS.has(entrada.operador)) fallar('MUTATION_REPORT_INVALID', 'mutantes[].operador inválido');
1254
+ if (typeof entrada.original !== 'string' || typeof entrada.mutado !== 'string') {
1255
+ fallar('MUTATION_REPORT_INVALID', 'mutantes[].original/mutado inválido');
1256
+ }
1257
+ if (!Number.isInteger(entrada.inicioAbs) || entrada.inicioAbs < 0) {
1258
+ fallar('MUTATION_REPORT_INVALID', 'mutantes[].inicioAbs inválido');
1259
+ }
1260
+ if (typeof entrada.critico !== 'boolean') fallar('MUTATION_REPORT_INVALID', 'mutantes[].critico inválido');
1261
+ if (!ESTADOS_VALIDOS.has(entrada.estado)) fallar('MUTATION_REPORT_INVALID', 'mutantes[].estado inválido');
1262
+ if (entrada.operador === 'M5' && entrada.critico !== true) {
1263
+ fallar('MUTATION_REPORT_INVALID', 'mutantes[] todo M5 debe ser crítico');
1264
+ }
1265
+ if (entrada.testQueLoMato !== null && typeof entrada.testQueLoMato !== 'string') {
1266
+ fallar('MUTATION_REPORT_INVALID', 'mutantes[].testQueLoMato inválido');
1267
+ }
1268
+ if (entrada.detailCode !== null && !DETAIL_CODES_VALIDOS.has(entrada.detailCode)) {
1269
+ fallar('MUTATION_REPORT_INVALID', 'mutantes[].detailCode inválido');
1270
+ }
1271
+ if (entrada.estado === 'sobrevivio' && (entrada.testQueLoMato !== null || entrada.detailCode !== null)) {
1272
+ fallar('MUTATION_REPORT_INVALID', 'mutantes[] sobreviviente inconsistente');
1273
+ }
1274
+ if (entrada.estado === 'muerto' && entrada.testQueLoMato === null && !DETAIL_CODES_VALIDOS.has(entrada.detailCode)) {
1275
+ fallar('MUTATION_REPORT_INVALID', 'mutantes[] muerto sin evidencia de causa');
1276
+ }
1277
+ }
1278
+
1279
+ function verificarConsistenciaAgregados(value) {
1280
+ const total = value.mutantes.length;
1281
+ const muertos = value.mutantes.filter((m) => m.estado === 'muerto').length;
1282
+ if (value.global.totalMutantes !== total || value.global.muertos !== muertos
1283
+ || value.global.score !== calcularScore(muertos, total)) {
1284
+ fallar('MUTATION_REPORT_INVALID', 'global no coincide con mutantes[]');
1285
+ }
1286
+ const criticos = value.mutantes.filter((m) => m.critico);
1287
+ const criticosMuertos = criticos.filter((m) => m.estado === 'muerto').length;
1288
+ if (value.global.criticos.total !== criticos.length || value.global.criticos.muertos !== criticosMuertos
1289
+ || value.global.criticos.score !== calcularScore(criticosMuertos, criticos.length)) {
1290
+ fallar('MUTATION_REPORT_INVALID', 'global.criticos no coincide con mutantes[]');
1291
+ }
1292
+ for (const entrada of value.porModulo) verificarConsistenciaModulo(entrada, value.mutantes);
1293
+ }
1294
+
1295
+ function verificarConsistenciaModulo(entrada, mutantes) {
1296
+ const delModulo = mutantes.filter((m) => m.modulo === entrada.modulo);
1297
+ const muertosModulo = delModulo.filter((m) => m.estado === 'muerto').length;
1298
+ if (entrada.totalMutantes !== delModulo.length || entrada.muertos !== muertosModulo
1299
+ || entrada.score !== calcularScore(muertosModulo, delModulo.length)) {
1300
+ fallar('MUTATION_REPORT_INVALID', `porModulo[${entrada.modulo}] no coincide con mutantes[]`);
1301
+ }
1302
+ const criticosModulo = delModulo.filter((m) => m.critico);
1303
+ const criticosMuertosModulo = criticosModulo.filter((m) => m.estado === 'muerto').length;
1304
+ if (entrada.criticos.total !== criticosModulo.length || entrada.criticos.muertos !== criticosMuertosModulo) {
1305
+ fallar('MUTATION_REPORT_INVALID', `porModulo[${entrada.modulo}].criticos no coincide con mutantes[]`);
1306
+ }
1307
+ }
1308
+
1309
+ /** Valida un `MutationReport` sin efectos: no lee disco, no hace red, no
1310
+ * consulta el reloj. Lanza MUTATION_REPORT_INVALID si el valor no cumple
1311
+ * `schemas/w4-mutation-report.schema.json`; retorna un valor deeply frozen
1312
+ * equivalente si es válido. */
1313
+ function validateMutationReport(value) {
1314
+ validarClaves(value, REPORT_KEYS, 'mutationReport');
1315
+ if (value.kind !== 'swl-mutation-report') fallar('MUTATION_REPORT_INVALID', 'kind inválido');
1316
+ if (value.schemaVersion !== SCHEMA_VERSION) fallar('MUTATION_REPORT_INVALID', 'schemaVersion inválida');
1317
+ if (typeof value.generatedAt !== 'string' || Number.isNaN(Date.parse(value.generatedAt))) {
1318
+ fallar('MUTATION_REPORT_INVALID', 'generatedAt inválido');
1319
+ }
1320
+ validarGlobal(value.global);
1321
+ if (!Array.isArray(value.porModulo) || value.porModulo.length === 0) {
1322
+ fallar('MUTATION_REPORT_INVALID', 'porModulo inválido');
1323
+ }
1324
+ value.porModulo.forEach(validarModuloEntry);
1325
+ if (!Array.isArray(value.mutantes)) fallar('MUTATION_REPORT_INVALID', 'mutantes inválido');
1326
+ value.mutantes.forEach(validarMutanteEntry);
1327
+
1328
+ verificarConsistenciaAgregados(value);
1329
+
1330
+ return congelarProfundo(JSON.parse(JSON.stringify(value)));
1331
+ }
1332
+
1333
+ module.exports = {
1334
+ ejecutarMutationTesting,
1335
+ generarMutantes,
1336
+ clasificarCritico,
1337
+ enmascarar,
1338
+ validateMutationReport,
1339
+ verificarTasaSyntaxError,
1340
+ MODULOS_CRITICOS,
1341
+ OPERADORES,
1342
+ // Export interno sólo para tests (y diagnóstico manual) del fix
1343
+ // cross-platform de matarArbolDeProceso, del fix de granularidad de
1344
+ // correrLineaBase (verificación T-07) y del filtro opt-in filtroClaves
1345
+ // (generarTareas + claveMutanteEquivalente). NO usar en código de
1346
+ // producción — la API estable es la lista de arriba.
1347
+ __internalParaTest: { decidirComandoMatarArbol, correrLineaBase, prepararPlantillaGit, generarTareas, claveMutanteEquivalente, verificarAnotacionEquivalentes, verificarCotaEquivalentes, esEquivalenteSobreviviente, MUTANTES_EQUIVALENTES_ESPERADOS },
1348
+ };