Download the PHP package rasuvaeff/mutation-history without Composer
On this page you can find all versions of the php package rasuvaeff/mutation-history. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download rasuvaeff/mutation-history
More information about rasuvaeff/mutation-history
Files in rasuvaeff/mutation-history
Package mutation-history
Short Description Cross-run history, diff and regression-ratchet gating on top of Infection PHP mutation testing logs
License BSD-3-Clause
Homepage https://github.com/rasuvaeff/mutation-history
Informations about the package mutation-history
rasuvaeff/mutation-history
Cross-run history for Infection mutation
testing. Infection is one-shot: run it, read the MSI, the log is gone. This
package parses Infection's per-mutant JSON log into
quality-ledger, giving you an
MSI trend across builds and a diff between any two runs that tells apart a
mutant that just started escaping (a real regression) from one that has
always escaped (background debt an unrelated PR did not create).
Using an AI coding assistant? llms.txt contains a compact API reference you can share with the model.
Why not just --min-msi?
infection --min-msi=85 blocks any PR that drops the aggregate below a fixed
number — including one that touches nothing near the debt. --git-diff-lines
only compares the current diff, not history. Neither remembers "this exact
mutant has been escaping for 40 runs" versus "this one just started escaping
today." mutation-history adds exactly that memory on top, via
quality-ledger's
RatchetGate, which fails only on a genuinely new regression.
Infection has no killedBy/coveredByTests field in its log, so per-test
killing power (which test killed a mutant, which test never kills anything)
is not something this package can give you from the log alone — it is future
work behind a processOutput-parsing extractor, not part of the core.
Requirements
- PHP 8.3–8.5
rasuvaeff/quality-ledger- Infection configured to write its JSON log:
"logs": { "json": "build/infection-log.json" }ininfection.json5(there is no--logger-jsonCLI flag — it is config-only)
Installation
Usage
The block above is executed on every build (composer docs, via
doc-exec) — the // => comments are
assertions, not decoration, so this sample cannot drift from the code.
In a real CI job the log comes from disk and the run identity from the environment, and a failed gate is what ends the job:
bin/mutation-history
The package ships a CLI — vendor/bin/mutation-history — which is how a CI
job normally consumes it. It is a thin wrapper over the API above: plain
$argv parsing, no console framework, no configuration file.
| Command | Does | Exit code |
|---|---|---|
digest --scope=<s> --run=<id> --log=<path> |
Parses one Infection JSON log and appends it as a run | 0; 1 on a missing option, an unreadable log or a malformed value |
diff --scope=<s> --base=<run> --head=<run> |
Prints NEW/OLD/FIXED lines plus a summary line |
1 when the ratchet gate fails (a mutant became bad), else 0 |
trend --scope=<s> |
Prints run<TAB>value for each recorded run |
0 |
badge --scope=<s> |
Renders the metric's latest value as an SVG badge — to --out, or to stdout |
0; 1 on an unwritable --out |
| Option | Applies to | Default | Notes |
|---|---|---|---|
--storage=<dir> |
all | <cwd>/build/mutation-history |
Where the ledger file lives |
--msi=<number> |
digest |
not recorded | Must be numeric — a typo is an error, not a 0.0 written into append-only history |
--bad-status=<status> |
diff |
escaped |
An empty value falls back to the default rather than classifying nothing as bad |
--metric=<name> |
trend |
msi |
|
--window=<n> |
trend |
all runs | Non-negative integer; 0 yields no points |
--metric=<name> |
badge |
msi |
|
--label=<text> |
badge |
the metric name | Left half of the badge |
--out=<path> |
badge |
stdout | The SVG is self-contained: no shields.io, no network |
Every malformed option value is refused with a message on stderr and exit
1. That is deliberate for a gating tool: the failure mode to avoid is not a
crash, it is a silent pass.
The ledger is a plain file: keep build/mutation-history/ between runs (a
cache, an artifact, a committed directory — the package does not care) and
diff has history to compare against.
badge renders quality-ledger's BadgeSvg, so the file is the whole
artifact — publish it wherever the README that shows it can reach:
A scope with no runs yet renders n/a in red and still exits 0: the first
job of a repository is exactly that state, and a badge step that broke the
build there would be useless.
In GitHub Actions
This repository gates itself with the recipe below — see
.github/workflows/build.yml and
.github/scripts/mutation-gate.sh.
Two consequences of caching worth knowing before you copy this:
- The ratchet only tightens on the default branch. A pull request reads the base branch's history but writes to its own isolated cache, so it compares against the last run on the base branch and cannot poison it.
- The history is as durable as the cache. GitHub evicts entries after
seven days of no reads, and the first run afterwards becomes a new
baseline. If the history must survive that, write it somewhere else —
--storagetakes any directory, and the ledger is one JSON file per scope.
Mutant identity includes originalFilePath exactly as Infection reports it,
which is absolute. A ledger filled at /home/runner/work/... shares no
ids with one filled at /app, so keep one storage per environment (or run
the gate only in CI, as here) rather than mixing a local run into the CI
history.
InfectionLogParser
Turns the JSON log into a Rasuvaeff\QualityLedger\RunReport. Every mutant
across killed, killedByStaticAnalysis, escaped, timeouted, errored,
uncovered and ignored becomes a Datum carrying meta: ['file', 'line', 'mutator', 'diff'] — killedByStaticAnalysis under the status killed, the
rest under a status of the same name (timeouted → timeout, errored →
error). Any of them is a valid --bad-status. syntaxErrors entries are dropped (tooling artifacts, not
mutants).
MutantId
sha256(len(file) + ":" + file + ":" + line + ":" + len(mutatorName) + ":" + mutatorName + ":" + normalizedDiff). The lengths are what make the encoding
injective: a plain separator would have to be a byte no field can contain,
and a JSON log can carry any byte in either name. normalizedDiff strips hunk-header line numbers (already carried separately)
and collapses whitespace, so a cosmetic reformat of the surrounding code does
not mint a new id — the actual +/- mutation content is never touched.
Security
The library reads no environment: you supply the JSON log's bytes and the run
identity. The log is treated as untrusted input — json_decode runs with
JSON_THROW_ON_ERROR, and every field of every entry is checked before use,
so a truncated or foreign-version log raises an InvalidArgumentException
naming the offending field instead of a TypeError from somewhere inside.
The CLI does read getcwd() (for the default --storage) and the path given
to --log. It loads that log whole: file_get_contents() plus
json_decode() on a large Infection log needs several times the file's size
in memory, so a job gating a package with tens of thousands of mutants should
budget a memory_limit well above the log's size. There is no streaming mode
and no size cap.
Examples
See examples/.
Development
No PHP/Composer on the host — run in Docker via the composer:2 image.
License
BSD-3-Clause