pdf-codec is now gated at a break threshold of 76, set in #1270 from the first complete mutation run the package has ever had. This issue records what that run found still alive, so the next PR can pick files off it and raise the threshold as it goes.
The run was local, over the whole mutate scope, on the branch that became #1270. Measured 2026-09-20. Every valid mutant got a result: 15,743 valid, 12,148 killed, 478 timed out, 2,413 survived, 701 with no coverage, for a score of 80.20. CompileError and Ignored mutants are excluded from the denominator as Stryker does. It is local rather than from a CI shard because the mutation workflow could not give the branch a slot, which is #1294.
So the distance to 100 is 3,114 mutants across 71 files. It is concentrated: the twenty files below hold 2,137 of them, just over two thirds.
| file |
survived |
no coverage |
file score |
packages/pdf-codec/src/interpret.ts |
139 |
57 |
65.4 |
packages/pdf-codec/src/gsub-table.ts |
130 |
65 |
69.5 |
packages/pdf-codec/src/images-read.ts |
123 |
52 |
54.7 |
packages/pdf-codec/src/read.ts |
147 |
21 |
65.1 |
packages/pdf-codec/src/image/jpeg2000-t2.ts |
92 |
40 |
69.7 |
packages/pdf-codec/src/write.ts |
93 |
30 |
85.8 |
packages/pdf-codec/src/image/jpeg2000.ts |
87 |
33 |
65.6 |
packages/pdf-codec/src/builtin-encoding.ts |
96 |
17 |
67.2 |
packages/pdf-codec/src/cff-bounds.ts |
86 |
7 |
83.5 |
packages/pdf-codec/src/cff.ts |
83 |
10 |
61.7 |
packages/pdf-codec/src/xref.ts |
64 |
25 |
65.1 |
packages/pdf-codec/src/filters.ts |
64 |
24 |
73.7 |
packages/pdf-codec/src/gpos-table.ts |
58 |
26 |
70.5 |
packages/pdf-codec/src/encrypt.ts |
57 |
22 |
74.6 |
packages/pdf-codec/src/image/png-decode.ts |
40 |
39 |
64.9 |
packages/pdf-codec/src/glyf.ts |
34 |
37 |
53.6 |
packages/pdf-codec/src/image/jbig2.ts |
34 |
34 |
66.5 |
packages/pdf-codec/src/image/jpeg2000-t1.ts |
62 |
2 |
85.2 |
packages/pdf-codec/src/content-read.ts |
45 |
9 |
45.5 |
packages/pdf-codec/src/sfnt-subset.ts |
35 |
18 |
73.6 |
Reproduce the report with pnpm --filter pdf-codec _test:mutation, which writes packages/pdf-codec/reports/mutation/mutation.json and an HTML report beside it. Scope a single file with --mutate src/<file>.ts --force while working on it; a full run took about three and a half hours at concurrency 1 on a loaded machine, a scoped one is minutes. Keep concurrency low, this package's suite is contention-sensitive.
On method, because it is what made the first 80 points work. A survivor is killed by making the test distinguish the mutation, and an equivalent mutant is killed by restructuring the code so the mutant cannot exist. Nothing is suppressed: there are no // Stryker disable comments in this package and the campaign that got it here added none, skipped no tests, and weakened no assertions.
The recurring shapes worth knowing before starting a file:
A guard that duplicates a bounds check further down is dead, and deleting it is the fix rather than writing a test that cannot reach it. A loop bound over a fixed-length typed array is an equivalent mutant because an off-by-one writes past the end and the typed array silently drops it, so no assertion can see the difference; iterate the array's own length, or drive the count from an arithmetic value a mutation actually changes. A lookup table or constant built once at module scope and read by exactly one function produces static mutants, which this config ignores rather than kills; moving it inside the function turns them into ordinary killable ones.
There is also a trap specific to this package, found the hard way in cff-bounds.ts. A charstring that draws nothing reports undefined from a successful walk and a failed one alike, so a test for a failure path that never draws anything first cannot distinguish correct behaviour from an inverted return: both assert the same thing. Any "refuses X" test needs to draw something before hitting the failure and then assert the earlier result is lost.
Some survivors here are genuinely equivalent and should be recorded as such rather than chased. The way to establish that is to apply the mutation literally and run the whole package suite, not to reason about it. takeWidth's leading-width shift and flex1's dx/dy accumulators in cff-bounds.ts were both confirmed that way already.
Raise breakThreshold in packages/pdf-codec/stryker.config.ts as files land, by the derivation rule on PackageStrykerOptions.breakThreshold rather than by picking a number: floor the measured score and subtract the run's own timeout share, rounded up, with a floor of one point.
One thing to fix before trusting any of this in CI: no mutation run for pdf-codec has ever completed on a hosted runner, so the 76 threshold has never been confirmed there. Until #1294 is resolved and a shard actually runs, main's post-merge run is the only thing that can validate it, and it has been cancelled every time so far.
pdf-codec is now gated at a break threshold of 76, set in #1270 from the first complete mutation run the package has ever had. This issue records what that run found still alive, so the next PR can pick files off it and raise the threshold as it goes.
The run was local, over the whole mutate scope, on the branch that became #1270. Measured 2026-09-20. Every valid mutant got a result: 15,743 valid, 12,148 killed, 478 timed out, 2,413 survived, 701 with no coverage, for a score of 80.20. CompileError and Ignored mutants are excluded from the denominator as Stryker does. It is local rather than from a CI shard because the mutation workflow could not give the branch a slot, which is #1294.
So the distance to 100 is 3,114 mutants across 71 files. It is concentrated: the twenty files below hold 2,137 of them, just over two thirds.
packages/pdf-codec/src/interpret.tspackages/pdf-codec/src/gsub-table.tspackages/pdf-codec/src/images-read.tspackages/pdf-codec/src/read.tspackages/pdf-codec/src/image/jpeg2000-t2.tspackages/pdf-codec/src/write.tspackages/pdf-codec/src/image/jpeg2000.tspackages/pdf-codec/src/builtin-encoding.tspackages/pdf-codec/src/cff-bounds.tspackages/pdf-codec/src/cff.tspackages/pdf-codec/src/xref.tspackages/pdf-codec/src/filters.tspackages/pdf-codec/src/gpos-table.tspackages/pdf-codec/src/encrypt.tspackages/pdf-codec/src/image/png-decode.tspackages/pdf-codec/src/glyf.tspackages/pdf-codec/src/image/jbig2.tspackages/pdf-codec/src/image/jpeg2000-t1.tspackages/pdf-codec/src/content-read.tspackages/pdf-codec/src/sfnt-subset.tsReproduce the report with
pnpm --filter pdf-codec _test:mutation, which writespackages/pdf-codec/reports/mutation/mutation.jsonand an HTML report beside it. Scope a single file with--mutate src/<file>.ts --forcewhile working on it; a full run took about three and a half hours at concurrency 1 on a loaded machine, a scoped one is minutes. Keep concurrency low, this package's suite is contention-sensitive.On method, because it is what made the first 80 points work. A survivor is killed by making the test distinguish the mutation, and an equivalent mutant is killed by restructuring the code so the mutant cannot exist. Nothing is suppressed: there are no
// Stryker disablecomments in this package and the campaign that got it here added none, skipped no tests, and weakened no assertions.The recurring shapes worth knowing before starting a file:
A guard that duplicates a bounds check further down is dead, and deleting it is the fix rather than writing a test that cannot reach it. A loop bound over a fixed-length typed array is an equivalent mutant because an off-by-one writes past the end and the typed array silently drops it, so no assertion can see the difference; iterate the array's own length, or drive the count from an arithmetic value a mutation actually changes. A lookup table or constant built once at module scope and read by exactly one function produces static mutants, which this config ignores rather than kills; moving it inside the function turns them into ordinary killable ones.
There is also a trap specific to this package, found the hard way in
cff-bounds.ts. A charstring that draws nothing reportsundefinedfrom a successful walk and a failed one alike, so a test for a failure path that never draws anything first cannot distinguish correct behaviour from an inverted return: both assert the same thing. Any "refuses X" test needs to draw something before hitting the failure and then assert the earlier result is lost.Some survivors here are genuinely equivalent and should be recorded as such rather than chased. The way to establish that is to apply the mutation literally and run the whole package suite, not to reason about it.
takeWidth's leading-width shift andflex1'sdx/dyaccumulators incff-bounds.tswere both confirmed that way already.Raise
breakThresholdinpackages/pdf-codec/stryker.config.tsas files land, by the derivation rule onPackageStrykerOptions.breakThresholdrather than by picking a number: floor the measured score and subtract the run's own timeout share, rounded up, with a floor of one point.One thing to fix before trusting any of this in CI: no mutation run for pdf-codec has ever completed on a hosted runner, so the 76 threshold has never been confirmed there. Until #1294 is resolved and a shard actually runs, main's post-merge run is the only thing that can validate it, and it has been cancelled every time so far.