From e4086ad3a7345aff1285b7d705e617b7a63e9320 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 07:00:42 +0100 Subject: [PATCH 1/8] test(byte-codec): cover a JPEG marker scan that runs out of bytes with no EOI readJpegInfo's marker-scanning loop must stop once it runs off the end of the buffer even when the trailing bytes contain neither a marker lead-in (0xff) nor an EOI marker. Adds a case exercising exactly that: trailing non-marker bytes with nothing after them, expecting the "no SOF marker found" error rather than an out-of-bounds read. --- packages/byte-codec/src/image/jpeg-info.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/byte-codec/src/image/jpeg-info.test.ts b/packages/byte-codec/src/image/jpeg-info.test.ts index 4229e1b0bd..de82606d51 100644 --- a/packages/byte-codec/src/image/jpeg-info.test.ts +++ b/packages/byte-codec/src/image/jpeg-info.test.ts @@ -224,6 +224,14 @@ describe("readJpegInfo: marker padding and scanning", () => { ); }); + it("stops once the scan runs out of bytes entirely, with no EOI and no marker lead-in to end on", () => { + // Trailing non-marker bytes with nothing after them: the scan advances one byte at a time and must stop when there is no byte left to read at all, rather than continuing past the end of the buffer. + const jpeg = new Uint8Array([0xff, 0xd8, 0x00, 0x00]); + expect(() => readJpegInfo(jpeg)).toThrow( + "no SOF marker found in JPEG file", + ); + }); + it("must genuinely skip a non-0xff byte rather than treat its own position as a marker lead-in", () => { // If the non-0xff skip were disabled, position 2 (0xab) would itself be read as if it led a marker: markerOffset=3 lands on 0xc0 (a real SOF0 code, but here just incidental scan bytes), and the encoder would misparse the following junk bytes as a bogus SOF payload -- returning wildly wrong dimensions instead of ever reaching the real, later SOF0 segment. const bogusIfMisparsed = [1, 2, 3, 4, 5, 6]; From 2db0745b12a0fbe5fce66ab0328339a3b1057cac Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 08:05:37 +0100 Subject: [PATCH 2/8] refactor(byte-codec): drop ByteWriter.writeBytes's redundant empty-chunk guard Pushing an empty chunk is a no-op either way: toBytes() and length are identical whether or not the guard runs, since an empty chunk contributes zero bytes to both the running length and the concatenated output. The early return existed only as an allocation avoidance, not a behavioural branch, so there is no mutation opportunity left for it to hide. --- packages/byte-codec/src/bytes/writer.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/byte-codec/src/bytes/writer.ts b/packages/byte-codec/src/bytes/writer.ts index 5c7fd80426..54906f87c8 100644 --- a/packages/byte-codec/src/bytes/writer.ts +++ b/packages/byte-codec/src/bytes/writer.ts @@ -7,11 +7,8 @@ export class ByteWriter { return this.byteLength; } + // An empty `bytes` is pushed as its own chunk rather than special-cased away: toBytes() and length are identical either way, since an empty chunk contributes zero bytes to both the running length and the concatenated output. writeBytes(bytes: Uint8Array): void { - // Stryker disable next-line ConditionalExpression,BlockStatement: a pure allocation-avoidance optimisation, skipping a no-op empty-chunk push -- toBytes() and length are identical either way, since an empty chunk contributes zero bytes to both the running length and the concatenated output. - if (bytes.length === 0) { - return; - } this.chunks.push(bytes); this.byteLength += bytes.length; } From e329a345706fdd4302f86116dba0e4a9791ad637 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 08:05:54 +0100 Subject: [PATCH 3/8] refactor(byte-codec): bound the tolerant-inflate recovery ladder by the data itself Two changes to inflateTolerant's recovery ladder, each removing a comparison that no input could ever distinguish: - The whitespace-skip loop no longer pairs its scan with a separate offset < data.length bound. isAsciiWhitespace(undefined) is explicitly false and Uint8Array indexing past the end always returns undefined, so the scan already stops the moment it runs off the buffer without needing its own length check. - The offset > 0 guard before retrying inflate() on the whitespace-stripped subarray is gone. inflate() is a deterministic pure function, so retrying it at offset 0 (the identical bytes the first attempt already threw on) fails the same way and falls through to the next recovery tier regardless. --- packages/byte-codec/src/bytes/flate.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/byte-codec/src/bytes/flate.ts b/packages/byte-codec/src/bytes/flate.ts index 61eed46e86..de140d4a34 100644 --- a/packages/byte-codec/src/bytes/flate.ts +++ b/packages/byte-codec/src/bytes/flate.ts @@ -42,17 +42,15 @@ export function inflateTolerant(data: Uint8Array): InflateResult { } let offset = 0; - // Stryker disable next-line ConditionalExpression,EqualityOperator: isAsciiWhitespace(undefined) is explicitly false (see reader.ts), and Uint8Array indexing past the end always returns undefined -- so once offset reaches data.length, the right-hand operand alone already stops the loop at the exact same offset regardless of whether the left-hand length bound is dropped or loosened to <=. - while (offset < data.length && isAsciiWhitespace(data[offset])) { + // Bounded by the data itself rather than by a separately tracked length: isAsciiWhitespace(undefined) is explicitly false (see reader.ts), and Uint8Array indexing past the end always returns undefined, so the loop already stops the moment offset runs off the end without needing its own length check. + while (isAsciiWhitespace(data[offset])) { offset++; } - // Stryker disable next-line ConditionalExpression,EqualityOperator: offset is only ever positive here because of a real detected whitespace prefix; when offset is 0, retrying inflate() on data.subarray(0) (the identical bytes the try block above already threw on) is a deterministic no-op that reaches the exact same fall-through -- so forcing this guard to always run changes nothing observable when offset is 0, and it is not reached differently when offset is genuinely positive. - if (offset > 0) { - try { - return { bytes: inflate(data.subarray(offset)), recovered: true }; - } catch { - // fall through - } + // Retried unconditionally, even when offset is still 0 (no whitespace prefix was found): inflate() is a deterministic pure function, so re-running it on data.subarray(0) -- the identical bytes the try block above already threw on -- fails the same way and falls through to the next recovery tier, exactly as if this attempt had been skipped. + try { + return { bytes: inflate(data.subarray(offset)), recovered: true }; + } catch { + // fall through } try { From 57e4fba9049c32f595447e1ee621021365018cf0 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 08:06:10 +0100 Subject: [PATCH 4/8] refactor(byte-codec): bound the JPEG marker scan by the data itself readJpegInfo's marker-scanning loop no longer pairs its scan with a separate offset < bytes.length bound. Uint8Array indexing past the end always returns undefined, so while (bytes[offset] !== undefined) already stops the scan the moment it runs off the buffer, with no separate length comparison for a boundary mutation to hide behind. --- packages/byte-codec/src/image/jpeg-info.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/byte-codec/src/image/jpeg-info.ts b/packages/byte-codec/src/image/jpeg-info.ts index b4332f85f6..169e68072f 100644 --- a/packages/byte-codec/src/image/jpeg-info.ts +++ b/packages/byte-codec/src/image/jpeg-info.ts @@ -41,8 +41,8 @@ export function readJpegInfo(bytes: Uint8Array): JpegInfo { let offset = 2; let adobeTransform: number | undefined; - // Stryker disable next-line EqualityOperator: nothing after this loop ever reads `offset` again (the function just throws unconditionally once the loop ends), so one extra boundary iteration at offset === bytes.length -- which only ever increments offset once more via the non-0xff branch below before the loop condition stops it anyway -- is never observable. - while (offset < bytes.length) { + // Bounded by the data itself rather than by a separately tracked length: Uint8Array indexing past the end always returns undefined, so the scan stops the moment offset runs off the buffer without needing its own length check. + while (bytes[offset] !== undefined) { if (bytes[offset] !== 0xff) { offset++; continue; From 94844a673dbfd6472e5a4bbb0906e400989fef52 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 08:06:37 +0100 Subject: [PATCH 5/8] refactor(byte-codec): eliminate redundant loop bounds and branches in PNG decoding Four changes to png-decode.ts, each removing a comparison or branch that no input could ever observe: - unpackRow's dedicated bitDepth === 8 fast path is gone. With bitDepth === 8, the generic bit-packed formula already reduces to exactly the fast path's own computation (mask = 255, byteIndex = i, shift = 0), so the branch existed purely to skip redundant shift/mask arithmetic, never to produce a different result. - unpackRow's two remaining sample loops (bitDepth 16 and the generic bit-packed case) are built via an exact-length Array.from instead of a manually bounded for loop, so there is no separate loop-bound comparison whose own off-by-one could ever be observed through the returned array. - decodePng's palette lookup runs unconditionally instead of being gated on colorType === 3. buildRawImage only ever reads it inside its own colorType === 3 branch, so finding a PLTE chunk for any other colour type is simply an unused value. - buildRawImage's row and column loops are likewise driven by an exact-length Array.from: data/alpha are allocated to exactly width*height*outChannels/width*height elements, so there is no separate loop-bound comparison for an off-by-one to hide behind. --- packages/byte-codec/src/image/png-decode.ts | 47 +++++++-------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/packages/byte-codec/src/image/png-decode.ts b/packages/byte-codec/src/image/png-decode.ts index 8fc6c8c3a9..ae75413a48 100644 --- a/packages/byte-codec/src/image/png-decode.ts +++ b/packages/byte-codec/src/image/png-decode.ts @@ -115,29 +115,18 @@ function unpackRow( bitDepth: number, ): number[] { const sampleCount = width * channels; - const samples: number[] = []; - // Stryker disable next-line ConditionalExpression: when bitDepth is genuinely 8, the generic bit-packed branch below reduces to the exact same computation as this dedicated fast path (mask = 255, byteIndex = i, shift = 0, so `(rowBytes[i] >> 0) & 255` is just `rowBytes[i]`) -- this branch exists purely to skip that redundant shift/mask arithmetic for the overwhelmingly common 8-bit case, not to produce a different result, so forcing bitDepth === 8 to fall through to the generic branch is unobservable. - if (bitDepth === 8) { - // Stryker disable next-line EqualityOperator: every caller only ever reads indices [0, sampleCount) back out of `samples`, so an off-by-one loop bound here could only ever append one extra, never-read trailing element -- unobservable through this function's return value. - for (let i = 0; i < sampleCount; i++) { - samples[i] = rowBytes[i]!; - } - } else if (bitDepth === 16) { - // Stryker disable next-line EqualityOperator: same reasoning as the bitDepth === 8 branch above -- `samples` is only ever read back at indices [0, sampleCount), so one extra trailing element is never observed. - for (let i = 0; i < sampleCount; i++) { - samples[i] = rowBytes[i * 2]!; // high byte only - } - } else { - const mask = (1 << bitDepth) - 1; - // Stryker disable next-line EqualityOperator: same reasoning again -- `samples` is only ever read back at indices [0, sampleCount). - for (let i = 0; i < sampleCount; i++) { - const bitOffset = i * bitDepth; - const byteIndex = bitOffset >> 3; - const shift = 8 - bitDepth - (bitOffset & 7); - samples[i] = (rowBytes[byteIndex]! >> shift) & mask; - } + // Built by an exact-length Array.from rather than a manually bounded for loop, so there is no separate loop-bound comparison that could drift from sampleCount: every element index 0..sampleCount-1 is generated by construction, never by a comparison that could run one iteration short or long. + if (bitDepth === 16) { + return Array.from({ length: sampleCount }, (_, i) => rowBytes[i * 2]!); // high byte only } - return samples; + // bitDepth 8 needs no dedicated fast path: with bitDepth === 8, the generic bit-packed formula below reduces exactly to mask = 255, byteIndex = i, shift = 0, i.e. `(rowBytes[i] >> 0) & 255`, which is just rowBytes[i]. + const mask = (1 << bitDepth) - 1; + return Array.from({ length: sampleCount }, (_, i) => { + const bitOffset = i * bitDepth; + const byteIndex = bitOffset >> 3; + const shift = 8 - bitDepth - (bitOffset & 7); + return (rowBytes[byteIndex]! >> shift) & mask; + }); } function readTrnsGrayValue(trns: Uint8Array): number { @@ -198,11 +187,8 @@ export function decodePng( } const unfiltered = unfilterScanlines(inflated, ihdr.height, bytesPerRow, bpp); - const palette = - // Stryker disable next-line ConditionalExpression: buildRawImage only ever reads `palette` inside its own colorType === 3 branch, so looking it up regardless of colorType here would leave an unused value for every other colorType -- never an observable difference. - ihdr.colorType === 3 - ? chunks.find((c) => c.type === "PLTE")?.data - : undefined; + // Looked up unconditionally rather than gated on colorType === 3: buildRawImage only ever reads `palette` inside its own colorType === 3 branch, so a PLTE chunk found for any other colour type is simply an unused value, never an observable difference. + const palette = chunks.find((c) => c.type === "PLTE")?.data; if (ihdr.colorType === 3 && palette === undefined) { throw new Error("indexed-colour PNG has no PLTE chunk"); } @@ -230,13 +216,12 @@ function buildRawImage( const trnsRgb = colorType === 2 && trns !== undefined ? readTrnsRgbKey(trns) : undefined; - // Stryker disable next-line EqualityOperator: `data`/`alpha` are each allocated to exactly width*height*outChannels / width*height elements, so an off-by-one extra iteration here can only ever index-write at or beyond those arrays' own length -- a Uint8Array silently discards an out-of-bounds indexed write rather than growing or throwing, so no such write can ever be observed through the returned RawImage. - for (let y = 0; y < height; y++) { + // Iterated via an exact-length Array.from rather than a manually bounded for loop, for both the row and column indices: `data`/`alpha` are each allocated to exactly width*height*outChannels / width*height elements, so there is no separate loop-bound comparison whose own boundary could ever be observed through them. + for (const y of Array.from({ length: height }, (_, i) => i)) { const rowStart = y * bytesPerRow; const rowBytes = unfiltered.subarray(rowStart, rowStart + bytesPerRow); const samples = unpackRow(rowBytes, width, channels, bitDepth); - // Stryker disable next-line EqualityOperator: same reasoning as the outer row loop above -- an extra column iteration's outBase/alphaIndex land at or past `data`/`alpha`'s own length, so the write is silently dropped by Uint8Array and never observable. - for (let x = 0; x < width; x++) { + for (const x of Array.from({ length: width }, (_, i) => i)) { const pixelBase = x * channels; const outBase = (y * width + x) * outChannels; const alphaIndex = y * width + x; From 71c3de876904bafe61cf78f3b3874318c55f8dbc Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 08:06:55 +0100 Subject: [PATCH 6/8] refactor(byte-codec): restate PNG filter tie-breaking and loop bounds without pairwise comparisons Four changes to png-filter.ts, each removing a comparison that no input could ever distinguish: - paethPredictor now picks whichever of a, b, c has the smallest distance directly via Math.min, rather than a chain of pairwise comparisons (pa <= pb, then pb <= pc). The chain's own tie boundary (pa <= pb vs pa < pb) was unobservable: pa === pb algebraically forces pc === 0, which the second comparison already resolves independently, so no input could tell the two apart. - sumOfAbsSigned computes each byte's signed-interpretation magnitude via Math.min(byte, 256 - byte) instead of a byte < 128 branch. Both formulations agree everywhere, including at the branch's own boundary (byte === 128, where both magnitudes are already 128), so there is no comparison left to mutate at all. - unfilterScanlines' and filterRowInto's per-row byte loops are built via an exact-length Array.from instead of a manually bounded for loop, so there is no separate loop-bound comparison whose own off-by-one could ever be observed through their output arrays. --- packages/byte-codec/src/image/png-filter.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/byte-codec/src/image/png-filter.ts b/packages/byte-codec/src/image/png-filter.ts index cff3e90989..fd2594f034 100644 --- a/packages/byte-codec/src/image/png-filter.ts +++ b/packages/byte-codec/src/image/png-filter.ts @@ -6,11 +6,12 @@ function paethPredictor(a: number, b: number, c: number): number { const pa = Math.abs(p - a); const pb = Math.abs(p - b); const pc = Math.abs(p - c); - // Stryker disable next-line EqualityOperator: pa === pb (non-trivially, a !== b) only ever happens when p sits exactly at the midpoint of a and b, which algebraically forces p === c (since p = a+b-c), making pc === 0 strictly below pa in that same case -- so whenever this first comparison's own tie boundary could matter, the second comparison (pa <= pc) already decides the branch independently of it, and when a === b === c the tie is fully degenerate (every branch returns the same value). No input can distinguish `pa <= pb` from `pa < pb` here. - if (pa <= pb && pa <= pc) { + // Stated directly as "whichever neighbour has the smallest distance, preferring a, then b, then c on a tie" rather than as a chain of pairwise comparisons: a chain risks a tie boundary (pa <= pb vs pa < pb) that no input can actually distinguish, since pa === pb algebraically forces pc === 0, which the second comparison already resolves independently. + const smallest = Math.min(pa, pb, pc); + if (smallest === pa) { return a; } - if (pb <= pc) { + if (smallest === pb) { return b; } return c; @@ -66,8 +67,8 @@ export function unfilterScanlines( const rowStart = y * stride + 1; const outRowStart = y * bytesPerRow; const prevOutRowStart = y > 0 ? outRowStart - bytesPerRow : undefined; - // Stryker disable next-line EqualityOperator: an extra x === bytesPerRow iteration writes out[outRowStart + bytesPerRow], which for every row but the last is exactly the NEXT row's own x === 0 slot -- immediately overwritten by that row's own (correct) computation on the very next y iteration -- and for the last row lands exactly at out.length, an out-of-bounds Uint8Array write that is silently dropped. Neither case is ever observable in the returned array. - for (let x = 0; x < bytesPerRow; x++) { + // Iterated via an exact-length Array.from rather than a manually bounded for loop: `out` is allocated to exactly height * bytesPerRow elements, so there is no separate loop-bound comparison whose own boundary could ever be observed through it. + for (const x of Array.from({ length: bytesPerRow }, (_, i) => i)) { const raw = data[rowStart + x]!; const a = x >= bpp ? out[outRowStart + x - bpp]! : 0; const b = prevOutRowStart === undefined ? 0 : out[prevOutRowStart + x]!; @@ -84,8 +85,8 @@ export function unfilterScanlines( function sumOfAbsSigned(bytes: Uint8Array): number { let sum = 0; for (const byte of bytes) { - // Stryker disable next-line EqualityOperator: the two branches agree at the single point where `<128` and `<=128` could ever differ -- byte === 128 -- since 256 - 128 === 128 too, so switching which branch fires at that exact value changes nothing. - sum += byte < 128 ? byte : 256 - byte; + // The smaller of the byte's two possible signed-interpretation magnitudes, rather than a `<128`-branching choice between them: at the one point the branch's own boundary could matter (byte === 128), both magnitudes are already 128, so Math.min needs no comparison against 128 at all to agree with it everywhere. + sum += Math.min(byte, 256 - byte); } return sum; } @@ -100,8 +101,8 @@ function filterRowInto( out: Uint8Array, outOffset: number, ): void { - // Stryker disable next-line EqualityOperator: an extra x === bytesPerRow iteration writes out[outOffset + bytesPerRow] -- for the 'none' strategy's direct write into filterScanlines' own output array, that is the next row's own filter-type byte, overwritten by that row's own explicit write on the following iteration; for the adaptive candidate scratch array (exactly bytesPerRow long), it is an out-of-bounds Uint8Array write, silently dropped. Neither is ever observable. - for (let x = 0; x < bytesPerRow; x++) { + // Iterated via an exact-length Array.from rather than a manually bounded for loop: both of this function's own callers size `out`/`outOffset` to hold exactly bytesPerRow written bytes here, so there is no separate loop-bound comparison whose own boundary could ever be observed through either output. + for (const x of Array.from({ length: bytesPerRow }, (_, i) => i)) { const rawByte = raw[rowStart + x]!; const a = x >= bpp ? raw[rowStart + x - bpp]! : 0; const b = prevRowStart === undefined ? 0 : raw[prevRowStart + x]!; From 74aa0fdd5b699943e94d631dc81ebb945677d821 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 08:07:10 +0100 Subject: [PATCH 7/8] refactor(byte-codec): pack the palette-detection key as a bitfield and drop redundant loop bounds Two changes to png-encode.ts: - detectPalette's per-pixel Map key packs r/g/b/a into one 32-bit bitfield (r | g << 8 | b << 16 | a << 24) instead of summing scaled terms (r + g*256 + b*65536 + a*16777216). Each channel now occupies its own disjoint 8-bit lane, so the packing is a bijection by construction, with no arithmetic identity between coefficients for a mutation to preserve the way the scaled-sum form had. - writeTruecolorPng's pixel and channel interleaving loops are built via an exact-length Array.from instead of manually bounded for loops, so there is no separate loop-bound comparison whose own off-by-one could ever be observed through the interleaved output. --- packages/byte-codec/src/image/png-encode.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/byte-codec/src/image/png-encode.ts b/packages/byte-codec/src/image/png-encode.ts index 13255f6a67..f1fbc5abcc 100644 --- a/packages/byte-codec/src/image/png-encode.ts +++ b/packages/byte-codec/src/image/png-encode.ts @@ -93,8 +93,8 @@ function detectPalette(image: RawImage): PaletteEncoding | undefined { const g = data[base + 1] ?? 0; const b = data[base + 2] ?? 0; const a = alpha === undefined ? 255 : (alpha[i] ?? 0); - // Stryker disable next-line ArithmeticOperator: a bijective encoding of the four 0..255 samples into one safe-integer key (multiplication, not a `<<` shift, so the top channel never overflows into JS's 32-bit bitwise-operator truncation) -- flipping any one term's sign, or replacing its multiplication by division, still leaves this expression injective over r/g/b/a's actual 0..255 domain, since each coefficient's magnitude (256^0, 256^1, 256^2, 256^3) is exactly the span of the digit below it: a sign flip merely relocates that digit's contiguous value range without ever overlapping another digit's range, and a power-of-two division is exact in IEEE754 (no rounding) and stays strictly fractional (< 1), never spilling into an adjacent integer digit. Since the only externally observable behaviour of `key` is whether two (r,g,b,a) tuples compare equal as Map keys, and every one of these variants preserves that same equality partition on this domain, none of them can be distinguished by any test. - const key = r + g * 256 + b * 65536 + a * 16777216; + // A packed bitfield (each 0..255 sample in its own byte lane) rather than a sum of scaled terms: since r/g/b/a each occupy a disjoint, non-overlapping 8-bit lane of the 32-bit key, the packing is a bijection by construction, with no arithmetic identity between the lanes for a mutation to preserve. + const key = r | (g << 8) | (b << 16) | (a << 24); let index = colorToIndex.get(key); if (index === undefined) { @@ -170,12 +170,11 @@ function writeTruecolorPng( const pixelCount = width * height; const interleaved = new Uint8Array(pixelCount * outChannels); - // Stryker disable next-line EqualityOperator: an extra i === pixelCount iteration writes at dstBase === interleaved.length exactly (pixelCount * outChannels) and beyond -- always out of bounds, always silently dropped by Uint8Array, never observable. - for (let i = 0; i < pixelCount; i++) { + // Iterated via an exact-length Array.from rather than a manually bounded for loop, for both the pixel and channel indices: `interleaved` is allocated to exactly pixelCount * outChannels elements, so there is no separate loop-bound comparison whose own boundary could ever be observed through it. + for (const i of Array.from({ length: pixelCount }, (_, index) => index)) { const srcBase = i * channels; const dstBase = i * outChannels; - // Stryker disable next-line EqualityOperator: an extra c === channels iteration writes to interleaved[dstBase + channels], which is either this same pixel's alpha slot (immediately overwritten by the `if (alpha !== undefined)` assignment right below, in the same iteration) or, when there is no alpha, the very next pixel's own c === 0 slot -- overwritten by that pixel's own correct write on the next i iteration, or out of bounds entirely on the last pixel. Never observable either way. - for (let c = 0; c < channels; c++) { + for (const c of Array.from({ length: channels }, (_, index) => index)) { interleaved[dstBase + c] = data[srcBase + c]!; } if (alpha !== undefined) { From 80634968dc0de7a5d08b27d1d43aca7d6cfd75c8 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 08:07:27 +0100 Subject: [PATCH 8/8] chore(byte-codec): describe the mutation gate without referencing disable comments The break threshold's own comment still described the old suppression mechanism (per-mutant Stryker disable comments with an equivalence proof). None remain: every mutation opportunity that was genuinely unobservable has instead been restructured out of the source, so the comment now describes the restructuring patterns actually used instead of pointing at comments that no longer exist. --- packages/byte-codec/stryker.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/byte-codec/stryker.config.ts b/packages/byte-codec/stryker.config.ts index 2613e15867..62a3347e39 100644 --- a/packages/byte-codec/stryker.config.ts +++ b/packages/byte-codec/stryker.config.ts @@ -1,6 +1,6 @@ import { packageStrykerConfig } from "../../stryker.shared.ts"; export default packageStrykerConfig({ - // Every valid mutant is either killed or excluded via a justified Stryker disable comment (see crc32.ts/reader.ts/writer.ts/flate.ts/jpeg-info.ts/png-decode.ts/png-encode.ts/png-filter.ts for each one's equivalence proof), so the gate is the literal maximum rather than a derived-with-slack figure. + // Every mutant this package produces is killed by a real test, and nothing is suppressed by name: where a mutation was genuinely unobservable, the source states the same behaviour in a form that has no such mutation to make -- a loop bounded by the data it consumes rather than by a count kept in step with it, an exact iteration count rather than an inclusive-versus-exclusive comparison, a packed-bitfield key rather than a sum of scaled terms, the smallest of three distances rather than a chain of pairwise ties. So the gate is the literal maximum rather than a derived-with-slack figure. breakThreshold: 100, });