test(mutants): the cursor rule's row clause and the end-of-line clause get a mutant each

The catalogue goes 45 to 47 and the runner stays `killed N of N`, exit 0.

`P8` is PM's own survivor from `44ad845`: the cursor rule one row too lenient
(`height - 1` -> `height - 2`), which survived 51 tests because no arm in
`CURSOR_CASES` stopped a row early. `P13` removes the clause this round added,
so the round leaves a mutant behind and not only a test. Both are judged by
`tests/test_asset_viewable.py`, which is why the per-mutant suite added in the
previous round was worth having.

CHANGELOG under 0.10.1, no new version and no tag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-19 22:57:50 +02:00
commit ce4ff70a21
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
2 changed files with 66 additions and 0 deletions

View file

@ -393,6 +393,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Two docstrings this round was sent to correct are rewritten: the test no
longer claims every pixel is decoded, and `_bmp_rle8_rows` no longer
frames the delta argument as read off the corpus, which it never was.
- **An end-of-line escape at column 0 states no skip, and neither does a delta
out of its row (0.10.1).** The entry above says a delta escape and an
end-of-line escape both leave pixels every decoder agrees on. Measured by PM
and reproduced here: that is true of the delta and false of the end-of-line.
Four end-of-line escapes and an end-of-bitmap carried an 8x4 frame with 32 of
32 pixels never decoded, and Pillow refuses those same bytes.
- The class is wider than the one construction, and this round measured it
rather than patching it: over every opcode sequence of length 1 to 4 on a
4x3 frame — **22 620 streams**, swept in the suite — this package carried
**703** streams the independent decoder refuses and drew **1 492** more
differently. PM's recommendation on its own (refuse a stream that painted
nothing) leaves **512** and **1 171** of those, so it would have narrowed
the class for the third round running.
- `_bmp_rle8_rows` refuses an end-of-line escape at column 0 (it closes no
row, so the row it passes over is one the stream never wrote) and a delta
whose horizontal offset would leave the row (the format puts that offset
inside the line; this reader keeps the cursor past the row end and a flat
decoder rolls it into the next row). Both with `asset_samples_invalid`.
After: **0** carried-here-refused-there and **32** drawn differently.
- **What is not closed is stated.** All 32 residual streams are a run or
absolute block that OVERRUNS its row. Refusing those gives 0 and 0 — and
costs **15 of the 25** real RLE8 files, which would drop 15 real figures
and move a pinned bundle's bytes.
- **Cost measured on the corpus first:** over **11 441** files scanned across
the four raw standard deliveries and the K2 reference corpus, the only
**25** BMPs on this machine use an end-of-line at column 0 in **0 of 25**
and a delta in **0 of 25**, and **25 of 25** still decode to Pillow's
pixels exactly (**3 117 220** pixels compared) after the change.
- `CURSOR_CASES` goes 8 arms to 12: one for the cursor rule's ROW clause
(PM's `P8`, `height - 1``height - 2`, which survived 51 tests) and four
for the end-of-line class. `P8` and `P13` join the mutant runner.
- **The published `--accounting` contract names every key the gate reads
(0.10.1).** The JSON sketch in `tools/okf_accounting_gate.py` is what a
consumer implements the door from, and it did not name `conversions`, which
`asset_holds`' conversion route depends on, nor `normalised_soft_hyphen`,
`unaccounted` or `double_booked`, which the door had written for a round
longer. A door built from the contract writes a ledger the gate reads as
"nothing was converted", and every converted image comes out
claimed-and-not-found — 19 of 50 on R761. Two tests hold the sketch against
both sides: what the gate LOOKS UP (measured with a ledger that records its
own lookups, not by grep) and what the door SERIALISES.
- **A bundle built without the door now says why a converted image cannot be
proved (0.10.1).** Without `--accounting` there is no ledger, so `asset_holds`
falls back to its first route and a converted picture is counted
claimed-and-not-found. The fallback was honest and silent; the count was
printed and its cause was not. `_tally` names the missing ledger when, and
only when, something was claimed and not found, and `asset_holds` says it in
its own docstring. The gate always passes the flag, so no row moves.
- **The published `tbx:` count is one number, guarded without the delivery
(0.10.1).** `assert sum(tbx.values()) == 568` sat behind a `skipif` on a file
only one machine has, so on a fresh clone the sentence five files publish was

View file

@ -366,6 +366,24 @@ MUTANTS: tuple[Mutant, ...] = (
" if False:",
suite="tests/test_asset_viewable.py",
),
# PM's P8 from `44ad845`: the cursor rule was held by no arm that could
# see its ROW clause, so a rule one row too lenient survived 51 tests.
Mutant(
"P8 the cursor rule is one row too lenient (height - 1 -> height - 2)",
"src/llm_ingestion_okf/assets.py",
" if y < height - 1 or (y == height - 1 and x < width):",
" if y < height - 2 or (y == height - 1 and x < width):",
suite="tests/test_asset_viewable.py",
),
# And the clause added beside it, so the round that wrote it leaves a
# mutant behind rather than only a test.
Mutant(
"P13 an end-of-line escape may claim a row it never started",
"src/llm_ingestion_okf/assets.py",
" if value == 0:\n if x == 0:",
" if value == 0:\n if False:",
suite="tests/test_asset_viewable.py",
),
# PM's three survivors from `43331fc`, held until now by ordinary tests
# and not by what runs AS the gate.
Mutant(