Skip to content

Commit 29f1077

Browse files
author
Test
committed
chore(skill/mutation-analyzer): add --diff mode to avoid sed loops
When inspecting several escaped mutants from a module, the previous pattern was: for L in 3780 3792 ...; do sed -n "\${L},\$((L+10))p" log; done which trips the permission allow-list (sed is not whitelisted) and is fragile against log layout shifts. infection-stats.php now accepts --diff=L1[,L2,...] [--lines=N]: a single forward pass over infection.log emits each requested block prefixed by === L<n> ===. Already covered by the existing Bash(php8.4:*) allow rule — no settings change required. SKILL.md documents the new mode in both example sections and replaces the prior Read log offset=L<n> hint for bulk inspection.
1 parent b1921bc commit 29f1077

2 files changed

Lines changed: 96 additions & 4 deletions

File tree

.claude/skills/mutation-analyzer/SKILL.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,32 @@ php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --by-mutator
5151
# 3. Ficheros con más escapes (orienta orden de análisis)
5252
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --by-file --top=15
5353

54-
# 4. Lista detallada de mutants de un fichero (con la línea del log para abrirla con Read)
54+
# 4. Lista detallada de mutants de un fichero (con la línea del log para volcar el diff)
5555
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --filter=src/Execution/FlowExecutor.php
5656

5757
# 5. Lo mismo pero para timeouts
5858
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --filter=src/Execution/FlowExecutor.php --section=timeout
59+
60+
# 6. Diff completo de N mutants (acepta lista de L<n> o números pelados)
61+
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --diff=552,673,686 --lines=12
5962
```
6063

61-
`--filter` imprime una línea por mutant con `L<n>` (línea del log), índice, path, mutator, ID. Usa el `L<n>` como `offset` para leer el diff con `Read`:
64+
`--filter` imprime una línea por mutant con `L<n>` (línea del log), índice, path, mutator, ID. Para inspeccionar el diff:
6265

6366
```
6467
L552 43) /var/www/html1/src/Execution/FlowExecutor.php:286 Continue_ f4ef6cc1...
65-
Read reports/infection/infection.log offset=552 limit=15
68+
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --diff=552 --lines=15
6669
```
6770

71+
**Usa `--diff` en bulk para varios mutants a la vez** (ej. al procesar un módulo entero):
72+
73+
```bash
74+
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php \
75+
--diff=3780,3792,3805,3818,3831,3844 --lines=10
76+
```
77+
78+
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.
79+
6880
**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).
6981

7082
### 3. Diseñar el análisis: calidad primero
@@ -190,9 +202,12 @@ Cuando se quiera bajar al detalle de un módulo concreto:
190202
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --filter=src/Execution/FlowExecutor.php
191203
# Para timeouts:
192204
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --filter=src/Execution/FlowExecutor.php --section=timeout
205+
206+
# Volcar los diffs de N mutants en una sola invocación (acepta lista de L<n> o números pelados):
207+
php8.4 .claude/skills/mutation-analyzer/scripts/infection-stats.php --diff=552,673,686 --lines=12
193208
```
194209

195-
Y luego `Read reports/infection/infection.log offset=L<n> limit=15` para inspeccionar el diff exacto. **No** componer `grep | sed`/`awk` ad-hoc — rompe permisos y es frágil.
210+
**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.
196211

197212
**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`.
198213

.claude/skills/mutation-analyzer/scripts/infection-stats.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
* php8.4 infection-stats.php --section=timeout
2929
* List timed-out mutants (default --section=escaped).
3030
*
31+
* php8.4 infection-stats.php --diff=L1[,L2,...] [--lines=N]
32+
* Dump the diff block of each mutant whose log line is given as L<n>
33+
* (use the L<n> values from --filter). Default block size is 12 lines.
34+
* Avoids `sed -n "${L},$((L+10))p"` loops that trip the permission
35+
* allow-list and that are fragile across log layout changes.
36+
*
3137
* Flags can combine: `--by-file --top=10`.
3238
*
3339
* The reports directory defaults to reports/infection/ relative to the cwd
@@ -280,6 +286,72 @@ function commandFilter(array $opts): void
280286
printf("--- %d mutants ---%s", $count, PHP_EOL);
281287
}
282288

289+
function commandDiff(array $opts): void
290+
{
291+
$dir = reportsDir($opts);
292+
$path = "$dir/infection.log";
293+
if (!is_file($path)) {
294+
fail("infection.log not found at '$path'.");
295+
}
296+
297+
$raw = $opts['diff'];
298+
if ($raw === '' || $raw === '1') {
299+
fail('--diff requires one or more line offsets, e.g. --diff=3780,3792 (use the L<n> from --filter).');
300+
}
301+
$lines = isset($opts['lines']) ? max(1, (int) $opts['lines']) : 12;
302+
303+
// Parse offsets: tolerate "3780", "L3780", whitespace, and stray commas.
304+
$offsets = [];
305+
foreach (preg_split('/[,\s]+/', $raw) ?: [] as $token) {
306+
$token = ltrim($token, 'L');
307+
if ($token === '' || !ctype_digit($token)) {
308+
continue;
309+
}
310+
$offsets[] = (int) $token;
311+
}
312+
if ($offsets === []) {
313+
fail("--diff has no valid numeric offsets after parsing '$raw'.");
314+
}
315+
316+
// Sort once so multiple offsets read the file in a single forward pass.
317+
sort($offsets);
318+
$needed = [];
319+
foreach ($offsets as $start) {
320+
for ($i = $start; $i < $start + $lines; $i++) {
321+
$needed[$i] = true;
322+
}
323+
}
324+
325+
$buffer = [];
326+
$handle = fopen($path, 'rb');
327+
if ($handle === false) {
328+
fail("Cannot read '$path'.");
329+
}
330+
$lineNo = 0;
331+
while (($line = fgets($handle)) !== false) {
332+
$lineNo++;
333+
if (isset($needed[$lineNo])) {
334+
$buffer[$lineNo] = rtrim($line, "\r\n");
335+
}
336+
if ($lineNo > max($offsets) + $lines) {
337+
break;
338+
}
339+
}
340+
fclose($handle);
341+
342+
// Emit each requested block in original offset order with a marker so the
343+
// caller can demarcate them when several are queried at once.
344+
foreach ($offsets as $start) {
345+
printf("=== L%d ===%s", $start, PHP_EOL);
346+
for ($i = $start; $i < $start + $lines; $i++) {
347+
if (isset($buffer[$i])) {
348+
echo $buffer[$i] . PHP_EOL;
349+
}
350+
}
351+
echo PHP_EOL;
352+
}
353+
}
354+
283355
function help(): void
284356
{
285357
echo <<<HELP
@@ -290,6 +362,7 @@ function help(): void
290362
--by-file [--top=N] [--section=] escaped|timeout per file
291363
--by-mutator [--escaped-only] per-mutator.md compact list
292364
--filter=<needle> [--section=] mutants whose path contains the needle
365+
--diff=L1[,L2,...] [--lines=N] dump diff blocks at log offsets (default 12 lines)
293366
294367
Common options:
295368
--reports=<path> override reports dir (default: reports/infection/)
@@ -322,6 +395,10 @@ function help(): void
322395
commandFilter($opts);
323396
exit(0);
324397
}
398+
if (isset($opts['diff'])) {
399+
commandDiff($opts);
400+
exit(0);
401+
}
325402

326403
help();
327404
exit(1);

0 commit comments

Comments
 (0)