| name | mutation-analyzer |
| description | Analiza informes de Infection (mutation testing) para el proyecto GitHooks. Clasifica los mutants escaped, distingue bugs latentes reales de mutants equivalentes/cosméticos, y produce un plan de refuerzo de tests priorizado. Usa esta skill cuando el usuario pida "analizar Infection", "revisar mutants escaped", "interpretar mutation testing", "qué mutants han sobrevivido", "MSI bajo", o cuando ejecute Infection y haya mutants escaped que revisar.
|
Mutation Analyzer para GitHooks
Esta skill interpreta el output de Infection y convierte una lista bruta de mutants escaped en un plan accionable para endurecer los tests.
Infection se ejecuta manualmente de forma esporádica (no en CI por coste). Cuando deja mutants escaped, esta skill sistematiza su análisis para que el trabajo resultante sea coherente y priorizado.
Flujo de decisión
1. ¿Qué analizar?
| Señal | Acción |
|---|
Usuario invoca tras vendor/bin/infection con mutants escaped | Análisis completo del log |
| MSI bajo (<90 %) y pregunta qué hacer | Análisis completo del log |
| Usuario apunta un fichero concreto | Análisis focalizado de ese fichero |
| Usuario pide sólo el catálogo de mutators | Referencia directa a references/mutator-catalog.md |
2. Localizar los artefactos
Infection escribe en reports/infection/. Usa sólo los tres ficheros de texto; ignora el HTML.
| Fichero | Tamaño típico | Uso |
|---|
infection-summary.log | <1 KB | Primer vistazo: volumen total (Escaped / Killed / Timeouts / MSI) |
per-mutator.md | 10-30 KB | Segundo vistazo: desglose por tipo de mutator — identifica patrones repetidos antes de bajar al detalle |
infection.log | 50-300 KB | Fuente primaria: diffs completos de cada mutant. Navegable con Grep y Read con offset |
mutation-report.html | NUNCA leer | 5-10 MB de HTML con CSS/JS embebidos. Destinado a navegador humano; inútil y contraproducente para el análisis en contexto |
Flujo de lectura:
Usar siempre el script scripts/infection-stats.php para los conteos y el perfilado por fichero/mutator. Está pensado para reemplazar pipelines grep | awk / grep | sed ad-hoc que rompen permisos y son frágiles. Sólo recurrir a Grep + Read cuando se quiera leer el diff completo de un mutant concreto.
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --summary
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --by-mutator --escaped-only
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --by-file --top=15
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --filter=src/Execution/FlowExecutor.php
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --filter=src/Execution/FlowExecutor.php --section=timeout
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --diff=552,673,686 --lines=12
--filter imprime una línea por mutant con L<n> (línea del log), índice, path, mutator, ID. Para inspeccionar el diff:
L552 43) /var/www/html1/src/Execution/FlowExecutor.php:286 Continue_ f4ef6cc1...
→ php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --diff=552 --lines=15
Usa --diff en bulk para varios mutants a la vez (ej. al procesar un módulo entero):
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php \
--diff=3780,3792,3805,3818,3831,3844 --lines=10
Una sola invocación devuelve los N bloques separados por === L<n> ===. Mucho más eficiente que Read individual por mutant y evita los for L in …; do sed -n …; done que disparan prompts de permiso y son frágiles ante cambios de layout del log.
Nunca usar Read sobre mutation-report.html — romperá el límite de mensaje y no añade información sobre los .log/.md. Si el usuario lo adjunta, recordárselo y pedir que aporte el infection.log o que re-ejecute Infection acotado (ver abajo).
3. Diseñar el análisis: calidad primero
Regla general: priorizar análisis centralizado. El orquestador principal tiene más contexto acumulado (puede correlacionar módulos, ver patrones cross-fichero, y re-leer el mismo test desde ángulos distintos). Los subagentes trabajan en silos con presupuesto cognitivo acotado y tienden a sesgo defensivo.
Criterio de delegación (nuevo):
| Situación | Estrategia |
|---|
| Cualquier volumen, ventana de contexto holgada | Análisis centralizado: leer log + código + test iterativamente hasta clasificar con evidencia. Preferido por defecto. |
| Volumen grande y ventana comprometida (ej. >150 mutants densos con código de contexto obligatorio) | Análisis centralizado por módulo (no paralelo): procesa un módulo a la vez, consolida y pasa al siguiente. |
| Volumen extremo o restricción dura de contexto | Delegación con piloto (ver abajo). Último recurso. |
El volumen no es el criterio; la complejidad y el presupuesto de contexto disponibles sí. Con Opus 1M de contexto, 228 mutants se procesan centralizadamente sin problema.
3.b Delegación (sólo si no queda alternativa)
Si se delega, no lanzar todos los lotes a la vez:
Fase piloto obligatoria. Lanzar un único lote representativo primero (p.ej. un módulo con mutants heterogéneos: Hooks, Jobs o Execution). Revisar el output contra el código y tests reales:
- ¿Clasificó algo como EQUIVALENTE sin citar línea de test que lo cubre?
- ¿Aplicó una pista como veredicto sin verificar?
- ¿Hay mutants agrupados por razón genérica ("rama inalcanzable") que no encaje con el código real?
Si hay ≥2 errores en el piloto, no lanzar el resto: reajustar pistas, endurecer el prompt, o caer a análisis centralizado.
Diseño del prompt de lote — pasar tareas de verificación, no veredictos:
-
❌ "CpuDetector tiene ramas Windows/Darwin inalcanzables en Linux CI — clasifica como equivalentes"
-
✅ "CpuDetector tiene ramas guardadas por PHP_OS_FAMILY. Antes de clasificar como EQUIVALENTE, verifica si existe un stub (WindowsCpuDetectorStub, DarwinCpuDetectorStub) en tests/ y si lo usa algún test. Cita el método de test que cubre la rama o marca AMBIGUO."
-
❌ "Dashboard concentra cosméticos ANSI — sé estricto agrupándolos"
-
✅ "Dashboard tiene mezcla de lógica real (queued/clear/render) y decoración (marcos, padding, timers). Para cada mutant en líneas 188-236, verifica si algún test TTY (dashboard_handler_*_tty_*) hace assert sobre el output exacto. Si lo hace → MEDIA/ALTA; si sólo verifica presencia de substring → COSMÉTICO."
La pista orienta qué verificar, no cómo clasificar.
Post-validación obligatoria por lote. Tras recibir la respuesta del subagente, antes de consolidar:
- Muestrear 3 clasificaciones al azar por lote (una ALTA, una MEDIA, una EQUIV/COSM).
- Re-inspeccionar el código + test referenciado.
- Si alguna clasificación no resiste la inspección, re-clasificar manualmente y marcar el lote como no fiable: re-procesar el resto del lote centralizadamente.
Lanza lotes en paralelo sólo después de pasar el piloto.
Formato del log de Infection
Cada mutant escapado sigue este patrón:
1) /abs/path/To/File.php:42 [M] MutatorName [ID] hash
@@ @@
$context line
- $original line
+ $mutant line
$context line
Estructura:
1) → índice dentro de la sección
[M] MutatorName → tipo de mutación (ver catálogo)
[ID] hash → identificador único (útil si quieres re-ejecutar sólo ese)
- Diff unified con ± indicando la transformación
Las secciones del log son:
Escaped mutants: (línea ~4, el grueso del trabajo)
Timed Out mutants: (mutants que no terminaron — trátalos aparte si los hay)
Un Grep con patrón ^\d+\) /abs/path/src/Modulo/ filtra los mutants de un módulo concreto con su línea.
Taxonomía y patrones: dónde viven
La taxonomía detallada (4 categorías) y la tabla de patrones por mutator están en el system prompt del subagente infection-mutant-classifier — no se duplican aquí. El catálogo canónico con ejemplos de código sigue en references/mutator-catalog.md y el subagente lo consulta bajo demanda para mutators ambiguos.
Recordatorio corto de las 5 categorías (para orquestar y consolidar):
- ALTA — Real escape: bug latente, crear/ampliar test.
- MEDIA — Cobertura débil: test existe, endurecer assert.
- EQUIVALENTE — Mismo output / rama inalcanzable: descartar con evidencia (cita el test que lo cubre, o la rama SO inalcanzable).
- COSMÉTICO — Decoración ANSI, contadores de reporting: descartar.
- AMBIGUO — No hay evidencia suficiente para decidir tras verificar. El orquestador (no el subagente) re-inspecciona y resuelve. Mejor ambiguo que mal clasificado.
Contratos locales vs. cruzados (importante)
Infection detecta contratos locales — los que viven dentro de un método o componente. No detecta contratos cruzados, donde el componente A produce un valor que B debe propagar a Y.
Ejemplo real (BUG-001, fix b9f5ff8): FlowMemoryHandler::enrichSingle() setea correctamente memoryThresholdState = FAILED en el JobResult. MemoryThresholdEvaluator::evaluate() calcula correctamente el state. Cada uno con cobertura local y mutantes muertos. Pero el contrato cruzado "cuando state == FAILED, success debe propagar a false" no estaba codificado en ningún test, y ningún mutante en aislamiento podía revelarlo: mutar withMemoryThreshold para no tocar success es legítimo porque ese método no es responsable de success — la responsabilidad vive en el coordinador (FlowMemoryHandler), pero el coordinador tampoco lo verificaba. El bug vivió bajo MSI 97 % y >90 % cobertura durante toda la v3.3 RC.
Implicación para la clasificación: ante un mutante que parece EQUIVALENTE genuino (el componente no toca un campo X y el mutante mantiene esa no-acción), preguntarse:
¿Este componente colabora con otro? ¿Hay test del coordinador que verifique el contrato agregado (X visible al consumidor final)?
Si la respuesta es "no", el mutante no es EQUIVALENTE — es un síntoma de agujero macro. Promover a una sub-categoría:
- CRUZADO — Mutante localmente equivalente que oculta un contrato cruzado no verificado. Acción: test al nivel del coordinador (no del componente mutado), o test integration que verifique el efecto end-to-end.
CRUZADO no infla las métricas de Infection (el mutante seguirá vivo o lo mata el test del coordinador). Pero codifica el contrato real. Es el caso donde "la métrica engaña": Infection no puede señalarlo, sólo puede señalar el síntoma.
Flujo de análisis
Paso 1 — Resumen y perfil (los tres ficheros de texto)
Ejecutar el script scripts/infection-stats.php para los tres conteos básicos. Sustituye lo que antes eran 3 invocaciones de Grep/Read con post-proceso manual:
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --summary
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --by-mutator --escaped-only
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --by-file --top=15
Cuando se quiera bajar al detalle de un módulo concreto:
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --filter=src/Execution/FlowExecutor.php
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --filter=src/Execution/FlowExecutor.php --section=timeout
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --diff=552,673,686 --lines=12
Nunca componer for L in …; do sed -n "${L},$((L+10))p" …; done ni grep | awk ad-hoc: dispara prompts de permiso para sed / awk y es frágil ante cambios de layout. Usa --diff (incluso para un solo mutant) o Read reports/infection/infection.log offset=L<n> limit=15 para casos puntuales.
Nunca abrir mutation-report.html: es HTML de 5-10 MB con CSS/JS embebidos; rompe el límite de mensaje y no aporta sobre los .log/.md.
Paso 2 — Análisis (centralizado por defecto)
Ruta preferente — centralizado por módulo. Procesa un módulo a la vez:
Grep los mutants del módulo sobre infection.log (ya lo hiciste en Paso 1).
Read el log con offset/limit del módulo.
- Para cada fichero con mutants:
Read el código fuente con contexto (±10 líneas por mutante).
Glob y Read el test directo (tests/Unit/<subruta>/<Class>Test.php).
- Clasificar cada mutant con evidencia citada: qué test lo cubre (MEDIA), qué línea/método lo cubre por rama (EQUIV), qué hace decorativo el cambio (COSM).
- Antes de cerrar como EQUIVALENTE: preguntarse si el componente colabora con otro y si el contrato agregado está verificado en algún test del coordinador. Si no → CRUZADO (test al nivel del coordinador, no del componente mutado).
- Acumular clasificaciones y pasar al siguiente módulo.
Esta es la ruta preferente aunque el log tenga 200+ mutants. Mantener un módulo en contexto a la vez evita silos y permite correlaciones cross-módulo (ej. patrones Windows/Darwin compartidos entre CpuDetector y Platform).
Ruta alternativa — delegación. Sólo si el presupuesto de contexto no alcanza (log muy denso, código fuente voluminoso, muchos tests). En ese caso:
- Piloto: diseñar 1 lote representativo. Invocar
Agent(subagent_type=infection-mutant-classifier) con prompt de verificación (no de veredicto).
- Validar: ¿cita evidencia (línea de test) para EQUIV/COSM? ¿hay AMBIGUOS razonables o fuerza categoría? ¿aplica pistas como tareas?
- Si el piloto es sólido → lanzar el resto de lotes en paralelo.
- Si el piloto tiene errores → caer a ruta centralizada por módulo.
Criterio de agrupación para lotes (si se delega):
- Ficheros del mismo sub-árbol de
src/ en el mismo lote.
- Evitar lotes >60 mutants (truncado).
- Evitar lotes <15 (overhead).
Post-validación obligatoria de cada lote delegado (ver Paso 2 del apartado 3.b): 3 muestras aleatorias re-inspeccionadas antes de consolidar.
El subagente devuelve tablas Markdown. El orquestador:
- Resuelve los AMBIGUOS (re-inspección manual).
- Revisa cada EQUIV/COSM sin evidencia citada (promover a AMBIGUO).
- Consolida en el informe final del Paso 3.
Paso 3 — Salida
Producir un informe en Markdown estructurado así:
# Informe Infection — <fecha>
## Resumen
- Total / Killed / Escaped / Timeouts
- MSI cubierto
- Contadores por categoría (ALTA / MEDIA / EQUIV / COSM). La suma debe cuadrar con Escaped.
## Prioridad ALTA (bugs latentes reales)
Tabla por fichero:línea / Mutator / Problema / Acción
## Prioridad MEDIA (cobertura débil)
Tabla similar
## Contratos cruzados a verificar (CRUZADO)
Mutantes localmente equivalentes que ocultan un contrato no codificado entre componentes. Tabla: fichero:línea / contrato cruzado sospechoso / coordinador donde añadir el test / cómo verificarlo (test unit del coordinador o test integration end-to-end).
## No accionable (equivalentes / cosméticos)
Lista resumida agrupada por tipo, con evidencia (test que lo cubre o razón de rama inalcanzable)
## Candidatos a fixes de código (OBLIGATORIO si aplica)
Mutants ALTA cuya mutación sugiere que el código actual puede esconder un bug real (no sólo cobertura débil). Tabla: fichero:línea / sospecha / verificación sugerida. PR aparte del refuerzo de tests.
## Plan de acción priorizado por ROI
1. Tests nuevos (clases sin test directo): cuantificar "mata N mutants".
2. Refuerzos de asserts masivos (patrones repetidos): cuantificar "mata N mutants".
3. Refuerzos puntuales.
4. Supresiones en `infection.json5` (cosméticos densos, ramas inalcanzables).
Guardar como Infection.md en la raíz del proyecto si el usuario lo pide, o mantener en pantalla si sólo quiere revisar.
Validación final antes de reportar:
- La suma de clasificaciones debe igualar el total de escaped. Si faltan mutants, son AMBIGUOS y van explícitamente en una sub-sección.
- Toda EQUIV/COSM debe llevar evidencia (línea de test que lo cubre, o razón concreta de equivalencia/decoración). Sin evidencia → AMBIGUO.
- La sección "Candidatos a fixes de código" no se omite: si no hay candidatos, decirlo explícitamente ("No se detectan bugs latentes — todos los ALTA son huecos de test").
Paso 4 — Puente con php-test-creator
Cuando pases a implementar:
- Para clases sin test directo → crear fichero nuevo siguiendo
php-test-creator (paso 0 del flujo de decisión de esa skill).
- Para cobertura débil → aplicar los principios de asserts fuertes y cobertura por operador de
php-test-creator.
- Para bugs de código detectados → commit aparte, no mezclar con los de tests.
Orden de implementación sugerido (máximo ROI):
- Tests nuevos de clases sin cobertura directa (cada uno mata 6–12 mutants de golpe).
- Refuerzos masivos sobre patrones repetidos (p.ej. los 12
LogicalOr de parsers con un test por parser).
- Refuerzos puntuales en el resto.
Ejecución de Infection
Configurado en infection.json.dist (si existe) o vía opciones CLI:
php7.4 vendor/bin/infection --threads=4 --min-msi=85 --min-covered-msi=85 \
--logger-html=reports/infection/mutation-report.html \
--log-verbosity=default 2> reports/infection/infection-summary.log
El log principal (reports/infection/infection.log) se genera automáticamente si el .dist lo tiene configurado como text logger. Verifica la configuración antes de ejecutar.
Si el log que vas a analizar es viejo, re-ejecutar Infection antes: los mutants pueden haber sido matados por commits posteriores.
Anti-patrones a evitar
- No clasificar "real" por defecto. Muchos mutants son equivalentes genuinos; forzar test para todos infla la suite sin beneficio.
- No perseguir mutantes cosméticos. Strings ANSI, paddings y anchos de marco no necesitan test — documentar y descartar.
- No delegar por defecto. La delegación a subagentes es un recurso para cuando la ventana de contexto no da más de sí. Con 1M de contexto, 200+ mutants se procesan centralizadamente sin problema y con mejor calidad. Preferir siempre ruta centralizada.
- No clasificar sin evidencia. EQUIVALENTE/COSMÉTICO requiere cita: fichero de test + método que cubre, o razón concreta de decoración/inalcanzabilidad. Sin evidencia → AMBIGUO (que el orquestador resuelve con re-inspección).
- No convertir pistas en veredictos. Una pista del orquestador ("X tiene ramas Windows inalcanzables") no es un veredicto — es una tarea de verificación. El clasificador debe buscar el stub o test de esa rama antes de aplicar la pista.
- Nunca hacer
Read sobre mutation-report.html. Es HTML con CSS/JS embebido de 5-10 MB pensado para navegador humano; en contexto LLM rompe el límite de mensaje sin aportar nada sobre los tres ficheros de texto. Si el usuario lo menciona o adjunta, recordarle que la fuente correcta es infection.log + infection-summary.log + per-mutator.md.
- No mezclar fixes de código con tests en el mismo commit. Los bugs latentes detectados merecen PR propio.
- No "tapar mutantes" con tests del nivel equivocado. Si para matar un mutante escribes un test que asserta el detalle de implementación que el mutante toca (en vez del contrato observable), el mutante muere pero el contrato sigue sin verificarse. Y si el código actual está mal, congelas el comportamiento incorrecto: el siguiente que toque el componente no podrá arreglarlo sin "romper tu test". Pregunta de control antes de escribir el test: si el contrato cambiase mañana, ¿este test fallaría por la razón correcta?. Si la respuesta es no, estás midiendo la implementación, no el contrato.
- No asumir que MSI alto = contratos completos. MSI cubre contratos LOCALES. Contratos cruzados entre componentes (productor/consumidor) no aparecen en Infection — el mutante en el productor que "no toca el campo X" es legítimamente equivalente cuando el productor no es responsable de X. Para esos contratos hay que añadir tests al coordinador o tests integration end-to-end. Ver "Contratos locales vs. cruzados" arriba.
Checklist de verificación
Antes de reportar: