@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.
- package/CLAUDE.md +14 -2
- package/README.md +65 -18
- package/agentes/_intent-spec.md +73 -73
- package/agentes/_propose-step.md +90 -90
- package/bin/swl-ses.js +10 -0
- package/comandos/swl/brainstorm.md +1 -0
- package/comandos/swl/briefing.md +119 -119
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/deuda-codigo.md +97 -97
- package/comandos/swl/mcp-status.md +1 -0
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/agent-deep-links/SKILL.md +148 -148
- package/habilidades/backend-async-postgres-testing/SKILL.md +216 -216
- package/habilidades/backend-error-design/SKILL.md +221 -221
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/calidad-anti-patrones-universales/SKILL.md +105 -1
- package/habilidades/calidad-contract-testing/SKILL.md +165 -165
- package/habilidades/calidad-mutation-testing/SKILL.md +25 -1
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
- package/habilidades/ci-cd-pipelines/SKILL.md +5 -1
- package/habilidades/css-moderno/SKILL.md +7 -1
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
- package/habilidades/extractor-de-aprendizajes/SKILL.md +5 -1
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/SKILL.md +3 -2
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
- package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
- package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
- package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
- package/habilidades/proceso-dynamic-workflows/SKILL.md +60 -0
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
- package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
- package/habilidades/proceso-modular-split/SKILL.md +256 -256
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
- package/hooks/calidad-pre-commit.js +159 -10
- package/hooks/ciclo-evolucion-subagente.js +26 -26
- package/hooks/ciclo-evolucion.js +26 -26
- package/hooks/contexto-subagente.js +68 -68
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/ciclo-evolucion.js +47 -47
- package/hooks/lib/deep-links.js +185 -185
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/notificacion-formato.js +45 -11
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/raiz-proyecto.js +35 -4
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/notificacion-telegram.js +5 -11
- package/hooks/session-briefing.js +12 -4
- package/instintos/autonomia.yaml +27 -27
- package/instintos/prompt-appendices.yaml +57 -57
- package/llms.txt +1 -1
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/canonical-hashes.json +662 -0
- package/manifiestos/harness-ir.json +47536 -0
- package/manifiestos/hooks-config.json +469 -469
- package/manifiestos/invariantes-criticos.json +30 -30
- package/manifiestos/policy-bundle.json +2065 -0
- package/manifiestos/policy-corpus-w2.json +3926 -0
- package/manifiestos/runtime-adapters-core3.json +208 -0
- package/manifiestos/runtime-conformance.json +139 -0
- package/manifiestos/skills-lock.json +43 -43
- package/package.json +2 -2
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +2 -2
- package/reglas/accesibilidad.md +10 -10
- package/reglas/auditorias-documentales-estructurales.md +7 -7
- package/reglas/cloud-infra.md +8 -8
- package/reglas/consultar-vault-primero.md +195 -195
- package/reglas/git-workflow.md +1 -0
- package/reglas/hooks.md +6 -6
- package/reglas/intent-engineering.md +218 -218
- package/reglas/markitdown.md +8 -8
- package/reglas/monitor-ci.md +12 -0
- package/reglas/patrones.md +6 -6
- package/reglas/testing.md +7 -7
- package/reglas/tests-cleanup.md +224 -224
- package/schemas/agent-message.schema.json +73 -73
- package/schemas/agent-output-implementacion.schema.json +114 -114
- package/schemas/agent-output-planificacion.schema.json +150 -150
- package/schemas/agent-output-review.schema.json +98 -98
- package/schemas/diary-entry.schema.json +112 -112
- package/schemas/gate-state.schema.json +76 -0
- package/schemas/harness-ir.schema.json +369 -0
- package/schemas/hook-profiles.schema.json +54 -54
- package/schemas/hooks-config.schema.json +89 -89
- package/schemas/legacy-gates.schema.json +45 -0
- package/schemas/modulos.schema.json +38 -38
- package/schemas/perfiles.schema.json +36 -36
- package/schemas/plugin.schema.json +77 -77
- package/schemas/policy-bundle.schema.json +140 -0
- package/schemas/policy-enforcement.schema.json +117 -0
- package/schemas/policy-operation.schema.json +261 -0
- package/schemas/runtime-adapter.schema.json +176 -0
- package/schemas/runtime-build-attestation.schema.json +100 -0
- package/schemas/runtime-conformance.schema.json +239 -0
- package/schemas/runtime-diagnostic.schema.json +395 -0
- package/schemas/skill-evals.schema.json +119 -119
- package/schemas/skill-frontmatter.schema.json +245 -245
- package/schemas/w4-certification-request.schema.json +72 -0
- package/schemas/w4-certification-verdict.schema.json +224 -0
- package/schemas/w4-corpus.schema.json +172 -0
- package/schemas/w4-mutation-report.schema.json +116 -0
- package/schemas/w4-replay-result.schema.json +164 -0
- package/schemas/w4-scoring-report.schema.json +89 -0
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/bootstrap-instintos.js +3 -0
- package/scripts/cli/aprobar-plan.js +73 -73
- package/scripts/cli/briefing.js +23 -23
- package/scripts/cli/ciclo-evolucion.js +26 -26
- package/scripts/cli/derivar-feature-list.js +25 -25
- package/scripts/cli/detectar-host.js +27 -27
- package/scripts/cli/diary-entry.js +69 -69
- package/scripts/cli/execution-state.js +18 -18
- package/scripts/cli/gateway-notify.js +41 -41
- package/scripts/cli/liberar-fase.js +42 -42
- package/scripts/cli/mark-evolved.js +56 -56
- package/scripts/cli/metricas-dora.js +26 -26
- package/scripts/cli/near-duplicate.js +55 -55
- package/scripts/cli/notificaciones.js +123 -123
- package/scripts/cli/propose-step.js +29 -29
- package/scripts/cli/schedule-parse.js +19 -19
- package/scripts/cli/sugerir-modelo.js +20 -20
- package/scripts/cli/verificar-plan.js +36 -36
- package/scripts/cli/verificar-trazabilidad.js +35 -35
- package/scripts/comandos/install-asistido.js +8 -7
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/doctor.js +61 -36
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-claims-runtime.js +1342 -0
- package/scripts/generar-harness-ir.js +257 -0
- package/scripts/generar-inventario.js +52 -54
- package/scripts/generar-policy-bundle.js +202 -0
- package/scripts/instalador.js +26 -7
- package/scripts/lib/approval-receipts.js +190 -0
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/certificacion-loop-state.js +421 -0
- package/scripts/lib/ci-reader.js +193 -193
- package/scripts/lib/ciclo-autonomo/yaml-instintos.js +56 -0
- package/scripts/lib/clasificar-directorio.js +92 -0
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-host-swl.js +175 -175
- package/scripts/lib/detectar-runtime.js +29 -20
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
- package/scripts/lib/detector-reglas-duplicadas.js +220 -220
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/evidence-verifier.js +192 -0
- package/scripts/lib/evidencia-release.js +322 -322
- package/scripts/lib/frontmatter-canonico.js +509 -0
- package/scripts/lib/gate-engine.js +871 -0
- package/scripts/lib/gate-hooks-requires.js +249 -249
- package/scripts/lib/gate-licencias.js +212 -212
- package/scripts/lib/git-config-preflight.js +48 -0
- package/scripts/lib/git-metricas.js +257 -257
- package/scripts/lib/harness-ir.js +778 -0
- package/scripts/lib/harness-source-snapshot.js +309 -0
- package/scripts/lib/integrity-ledger.js +1147 -0
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/legacy-gate-migration.js +324 -0
- package/scripts/lib/limpiar-basura-global.js +45 -2
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/metricas-dora.js +204 -204
- package/scripts/lib/notificaciones-telegram.js +1 -0
- package/scripts/lib/npm-version.js +1 -0
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/plan-lock.js +61 -13
- package/scripts/lib/policy-broker.js +338 -0
- package/scripts/lib/policy-bundle.js +342 -0
- package/scripts/lib/policy-context-provider.js +310 -0
- package/scripts/lib/policy-contract.js +479 -0
- package/scripts/lib/policy-verifier-utils.js +65 -0
- package/scripts/lib/pr-analyzer.js +399 -399
- package/scripts/lib/principal-verifier.js +178 -0
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/resolver-plan-fase.js +37 -37
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/runtime-adapter-contract.js +267 -0
- package/scripts/lib/runtime-artifact-verifier.js +426 -0
- package/scripts/lib/runtime-build-attestation.js +127 -0
- package/scripts/lib/runtime-bundle-installer.js +586 -0
- package/scripts/lib/runtime-compiler.js +327 -0
- package/scripts/lib/runtime-conformance.js +202 -0
- package/scripts/lib/runtime-doctor-core3.js +567 -0
- package/scripts/lib/runtime-doctor-input.js +59 -0
- package/scripts/lib/runtime-operation-adapter.js +267 -0
- package/scripts/lib/schema-version.js +164 -164
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/signed-envelope.js +545 -0
- package/scripts/lib/single-use-store.js +359 -0
- package/scripts/lib/skills-externas.js +31 -0
- package/scripts/lib/transformadores/codex.js +15 -8
- package/scripts/lib/transformadores/gemini.js +79 -5
- package/scripts/lib/w4-attestation-adapter.js +158 -0
- package/scripts/lib/w4-canario.js +337 -0
- package/scripts/lib/w4-claims.js +182 -0
- package/scripts/lib/w4-corpus-generador.js +542 -0
- package/scripts/lib/w4-gate-c5.js +115 -0
- package/scripts/lib/w4-harness-bajo-prueba.js +155 -0
- package/scripts/lib/w4-matriz-combos.js +55 -0
- package/scripts/lib/w4-motor-mutacion.js +1348 -0
- package/scripts/lib/w4-motor-replay.js +735 -0
- package/scripts/lib/w4-pin-origen.js +54 -0
- package/scripts/lib/w4-publicar-request.js +132 -0
- package/scripts/lib/w4-revocacion.js +62 -0
- package/scripts/lib/w4-runtimes-core3.js +38 -0
- package/scripts/lib/w4-scorer-certificacion.js +692 -0
- package/scripts/lib/w4-superficie-candidato.js +49 -0
- package/scripts/lib/w4-veredicto.js +452 -0
- package/scripts/lib/w4-verificar-veredicto.js +302 -0
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +200 -200
- package/scripts/migrar-gates-legacy.js +108 -0
- package/scripts/publicar-certification-request.js +115 -0
- package/scripts/runtime-doctor.js +107 -0
- package/scripts/tui/componentes/selector-multi.js +189 -189
- package/scripts/tui/componentes/selector-unico.js +158 -158
- package/scripts/tui/ejecutores.js +375 -375
- package/scripts/tui/lib/colores.js +129 -129
- package/scripts/tui/lib/render.js +264 -264
- package/scripts/tui/lib/teclas.js +113 -113
- package/scripts/tui/pantallas/install-wizard.js +12 -7
- package/scripts/tui/pantallas/menu-principal.js +52 -52
- package/scripts/tui/pantallas/progreso.js +274 -274
- package/scripts/tui/pantallas/resumen.js +132 -132
- package/scripts/validar-userland-vacio.js +110 -110
- package/scripts/verificar-aislamiento-swl-eval.js +87 -0
- package/scripts/verificar-empaquetado-downstream.js +375 -0
- package/scripts/verificar-loop-constructor.js +215 -0
- package/scripts/verificar-trazabilidad.js +13 -6
- 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
|
+
};
|