test(epub-codec): raise the package's mutation score toward 100% - #1269
Merged
Merged
Conversation
Mearman
force-pushed
the
feat/100-percent-mutation-epub-codec
branch
from
September 13, 2026 21:00
9259946 to
d8dfdf6
Compare
…g edge cases readImageDimensions and detectImageFormat only had tests for the happy path and one generic truncation, leaving the PNG signature/IHDR boundary checks and most of the JPEG marker walk (fill-byte runs, restart/TEM/EOI markers, the DHT/JPG/DAC frame-marker exclusions, SOS-stops-the-scan, and the exact byte-count boundaries for a marker's length field and an SOF0 payload) unexercised. Add targeted byte-level fixtures for each of these branches so a regression in the marker classification or the segment-skipping arithmetic fails a test instead of silently misreading a real-world PNG/JPEG header.
…message Every EpubParseError/EpubWriteError subclass's constructor sets this.name and a default or derived message, but the existing tests only ever checked code (and, for a couple of classes, message content loosely via toContain). The base classes themselves were never constructed directly either. Assert name and the exact default/derived message on every class, construct the two base classes directly, and check the unmatchedEnd/unclosedStart ternary picks its own description rather than merely asserting a substring both branches share.
…segments resolvePackagePath's relative-vs-scheme classification and its "./", "", and ".." segment handling had no test past the two segment kinds the fixture paths in other tests happened to exercise incidentally. Add a scheme covering every character class the regex allows, a leading-digit case that must NOT be classified as a scheme (a URI scheme starts with a letter), an explicit "./" segment, and a doubled-slash empty segment.
resolveHrefTarget had no test file of its own -- only whatever incidental exercise it got through src/xhtml/read.ts and write.ts's own round-trip tests, which left the scheme/empty-href/no-fragment/empty-fragment guards and the hash-index arithmetic unverified in isolation. Cover the empty href, scheme-carrying href, hashless href, empty-fragment href, same-document fragment, cross-document fragment, subdirectory resolution, and a hash at index 1 specifically (to distinguish "no hash found" from "hash found early").
buildXml had no test of its own, only whatever incidental exercise it got through every other module's own round-trip tests -- which never happened to build a bare comment, cdata, processing-instruction, or declaration node in isolation, and never checked the no-attributes case renders without a stray attribute object. Cover text, comment, cdata, pi, declaration, attribute-bearing and attribute-free elements, and nesting.
…ssification looksLikeXml's BOM detection and its per-byte whitespace-then-'<' scan had no test at all -- packageFromEntries was only ever exercised end-to-end through real EPUB fixtures, none of which happen to carry a BOM, leading whitespace, or a malformed BOM prefix. Add byte-level fixtures for a real BOM, each of its three bytes individually wrong, a too-short BOM-like prefix, each whitespace byte the scan recognises, a non-whitespace non-'<' byte, an empty part, and a part that is only whitespace with no '<' at all.
isXmlNode had no test of its own -- only whatever incidental exercise it got via Zod's z.custom() call sites elsewhere, which never happened to feed it a declaration or pi node, a malformed attribute, or a recursively malformed element child. Cover every node kind's own field-shape check, the non-object/array/primitive rejections, and recursive element/attribute validation, plus isTextLikeNode's own four-way discrimination.
parseXml's element/text/declaration paths were covered but its comment,
cdata, and pi branches (parseNode's tagKey === "__comment"/"__cdata" checks
and the tagKey.startsWith("?") pi fallback) had no test exercising real XML
syntax for any of them.
…e stripping
Only the c1-position invalid-padding case ("A===") was tested; add the
c0-position case ("=AAA") and a whitespace-stripping equivalence check so
the sanitising regex's own effect is asserted rather than merely present.
Every failure path already had a class-based toThrow assertion but never checked the message text, leaving each one free to say anything at all.
…oss nine small modules Each of these had one or a handful of untested branches: hasZipHeader's own partial-header-match case, navMatchesSpine's partial-match case, decodeTextLikeNode (never tested directly at all), elementsWithTag (never tested directly at all), mintListNumId/parseListNumId's bullet-with-suffix and ordered-with-no-suffix cases, readNcxHrefs' missing-src content element, readNav3TocHrefs' no-epub:type nav / non-nav-tag-with-toc-type / href-less anchor cases, and reportInertElementSkip (no test file existed for src/xhtml/context.ts at all).
sameDocumentFragment, isFootnoteReference, and isFootnoteAside had no test of their own -- only whatever incidental exercise they got through src/xhtml/read.ts's own end-to-end fixtures. Cover the length-2 fragment boundary, the structured epub:type noteref/footnote/rearnote signal, the EPUB 2 class-name idiom on both the anchor and the target, case- insensitivity, the non-aside tag gate, and the some()-not-every() semantics for a multi-value epub:type where only one token matches.
…ring gates Every parseOpf failure path had a class-only toThrow assertion; add message text for each. Also cover the filter() predicates that drop a malformed manifest item or a spine itemref with no idref (both previously only ever exercised with well-formed input), and a manifest item's multi-valued properties splitting on more than a single whitespace character.
…d guards readBlockElementInner's own case "p"/"figure"/"figcaption" each called readContainerChildren identically to the default passthrough case, so they were dead duplicates rather than distinct behaviour; folded them into default and moved their rationale into that case's own comment. headingLevelOf used a /^h([1-6])$/ regex even though the only tags it is ever asked to classify come from the fixed BLOCK_LEVEL_TAGS set, none of which (besides h1-h6 itself) can accidentally satisfy a mis-anchored version of that pattern; replaced it with a literal tag->level lookup table, which also removes the now-unneeded clampHeadingLevel call. Removed three `X.length === 0` guards (readContainerChildren's own segment flush, flushListStrayContent, flushDefinitionListStrayContent, readTable's stray-block computation, and a table row's own stray-cell flush) that only ever short-circuited to a state the following non-empty-result check already reaches on its own, since readContainerChildren/buildInlineRuns are themselves no-ops on an empty input array. Simplified readPreRuns' own flat-text branch condition from `footnoteName === undefined && !containsFootnoteReference([node], ...)` to just the second half: footnoteName can only be defined when node is a footnote-reference <a>, which is exactly what containsFootnoteReference's own first check already tests on that same single-element array, so the first half never adds information the second half didn't already carry.
…le edge cases Adds direct scanXhtmlAnchors coverage for a deeply-nested id-bearing element and for filtering anchors to real, href-bearing <a> elements only. Adds a style-residue test distinguishing a <link> with a non-stylesheet rel from an unrelated element that merely carries a rel=stylesheet attribute, plus an exact-message assertion for the style-residue diagnostic. Adds a direction-key-absence check (toEqual alone can't tell a missing key from an explicit undefined one), a combined blockquote+dd indent test proving the two indent sources are summed rather than subtracted, and a nested-heading-inside-blockquote test proving containsHeading recurses rather than only checking direct children. Covers several <pre>/<code> edge cases: no codeLanguage key when there is no language- class, a genuinely empty <pre> producing no runs, a non-<a> element's href never being mistaken for a footnote reference even when it resolves to a real footnote target, an <img> with no src producing the generic "<img>" label versus one with a src naming it, and the language- class requiring a leading boundary rather than matching anywhere in the string. Covers table stray-content routing: a <ul> sitting directly inside a <table> reads as a real nested list rather than being mistaken for a row group, a stray <img> inside a <colgroup> becomes a real image block rather than degrading to alt text, and the noscript-skip diagnostics inside a <table>/<colgroup>/<tbody> fire exactly once even alongside other stray content that itself triggers the unrecognized-content diagnostic, plus exact list-content-outside-item message assertions for both the before-first-item and continuation wordings.
Every stray-node accumulator collected while reading a list, definition list, or table eventually flows through readContainerChildren's own buildInlineRuns call, which already skips any node that is neither text-like nor an element (comments, declarations, processing instructions) and already carries its own isInertElement check for script/template/style/noscript. The per-container pre-filters duplicating both checks before pushing into strayNodes/segment were dead weight: removing them changes nothing observable, since the deeper call already produces the same recovered blocks and fires the same noscript diagnostic from its own appendElement guard.
…des in xhtml/read Adds assertions distinguishing a genuinely absent colSpan/rowSpan key from one present with an undefined value, a zero-column-count table's width falling back to the full content width instead of dividing by zero, which of two duplicate captions actually triggers the duplicate-caption diagnostic, stray content ordering around a <dl>'s own <div> wrapper, a table row's own trailing stray cell, positiveIntAttr's zero/negative rejection, empty-alt image dropping on both the unresolved and unsupported-format degrade paths, a plain non-footnote <aside> and a footnote <aside> with no id, and a non-<a> element whose own href happens to share a fragment with a real footnote reference elsewhere in the document. Also pins the exact diagnostic message text for the table-caption, table-row, and footnote-target-unresolved diagnostics.
…a table cell loop The table row's own td/th loop duplicated the same isInertElement pre-filter already removed from the sibling stray-content loops: flushStrayCell's readContainerChildren call already carries the identical check via buildInlineRuns' appendElement, so a script, template, style, or noscript cellNode reaches the same outcome either way. Also covers stray text sitting directly inside a <table> outside any row, caption, or colgroup, the one remaining shape that reached readTable's own strayNodes collection with no test at all.
…ounds checks in xhtml/write writeSectionChild's own ListGroupNode branch was already documented as unreachable dead code: writeSectionChildren's own loop always groups a run of sibling ListGroupNode entries into writeList before this function is ever called. Narrowing the parameter type to Exclude<SectionChild, ListGroupNode> lets TypeScript's own control-flow analysis at the one call site prove the branch unnecessary, rather than keeping a runtime check no test can ever reach. Also drops the `end < children.length` bound ahead of isListGroupNode in the list-run scan, and rewrites the outer children walk as an unconditional loop relying on its own existing undefined check: children[end] is undefined past the array's own end, and isListGroupNode(undefined) is already false (its own isRecord guard rejects a non-object outright), so both loops already terminated at the same point either way.
writeList's own consecutive-itemId scan duplicated the same out-of-bounds-is-undefined pattern already removed elsewhere in this file: past the array's own end, items[end] is undefined, and its optional-chained itemId is therefore undefined too, which can never equal the loop's own itemId (this branch only runs when itemId is a real, defined string), so the loop already terminated at the same point without the separate length check.
…in xhtml/write Adds assertions for: two adjacent list items that both carry no itemId staying as separate <li> elements rather than merging; an ordered list's default start carrying no explicit start attribute; a nested bookmark construct group overwriting an inner target's own id rather than leaving both in place; a bookmark wrapping more than one element degrading with the exact CONSTRUCT_UNREPRESENTED message; a block-scoped endnote/comment anchor construct group falling through to the generic construct-unrepresented path rather than being mistaken for a bookmark; the exact wording distinguishing a footnote reference from an internal link, and a point anchor from a range extent, in an unrepresented-extent diagnostic; a clean, non-overlapping point or range footnote reference never firing that diagnostic at all; a multi-run footnote range inside a pre element joining its own text verbatim with no separator; no spurious empty text node between two br elements for a run's own consecutive newlines; a wholly empty run writing as exactly one empty text node; a table cell's own rowspan attribute; no false-positive ELEMENT_UNMAPPED for an ordinary table cell; a paragraph carrying only codeLanguage still writing as a pre element; the monospace-plus-newline legacy heuristic requiring exactly one run; a horizontal-rule styleId with real runs never becoming an hr element; a dropped pageBreak block; and an embeddedObject block's and an unresolved image's exact diagnostic messages.
…dges in xhtml/write Adds a plain paragraph exercising the monospace-plus-newline <pre> heuristic on its own, with neither preformatted nor codeLanguage set; an internal-link construct extent writing its own <a href> to the resolved anchor; an external-target link construct extent being left entirely unwrapped and undiagnosed, since that target kind is ContentRun.hyperlink's own established territory, not this construct path's; and rewrites the nested-bookmark id-replacement test to inspect the written element's raw attributes array directly, since a plain object literal's duplicate-key-overwrite semantics collapse a filter that removed nothing at all to the same final serialized attribute a correct filter would also produce.
…ts in opf/parse Adds assertions for: dc:contributor reported as unmapped with the exact message, alongside the existing dc:publisher/dc:rights coverage; a whitespace-only dc:title/dc:language/dc:date treated as absent rather than an empty string; a whitespace-only dc:creator/dc:subject value dropped while a real sibling value survives, and both fields carrying no key at all when every value is whitespace-only; the EPUB 2-style <meta name="dcterms:modified" content="..."/> spelling; a whitespace-only content attribute on that same variant leaving modifiedIso unset; and the property="dcterms:modified" scan continuing past an empty-text meta to find a later, real one instead of stopping there.
…ics in opf/parse Adds assertions distinguishing a title/language/createdIso key genuinely absent from one present with an undefined value (toEqual alone ignores undefined properties on both sides, so it cannot tell these apart); a meta whose property or name attribute names something other than dcterms:modified never contributing its own text or content to modifiedIso; and trimming applying to both the property-based meta's own text and the name-based meta's own content attribute.
…nav3 A split on a whitespace run then an exact "toc" token lookup produces the identical result a split on one whitespace character would, since neither the run-collapsing quantifier nor the ?? "" fallback default can ever change whether "toc" appears as a token. Testing the raw attribute value directly against a word-boundary pattern removes both indistinguishable constructs.
…o tokens Every caller's own pattern (noteref, footnote|rearnote) is an unanchored substring match with no word-boundary assertion, so splitting the attribute into whitespace-separated tokens first changes nothing a caller could observe: a match inside one token is exactly the match testing the whole value would find, and an empty token never matches any of these patterns either way. Testing the raw value removes the indistinguishable split/filter machinery entirely.
…ce char A split on a whitespace run, followed by filtering out empty tokens, yields the identical token set a split on one whitespace character followed by the same filter would, since the actual non-whitespace runs are unaffected by how many split points a run of whitespace produces. Using a bare \s costs nothing here and carries no quantifier for a mutation test to remove without changing observable behaviour. Also adds a test for leading/trailing whitespace in a properties attribute, proving the filter actually drops the empty tokens that whitespace at either end would otherwise leave in the parsed array.
…fTarget An empty href can never contain "#", so the hashIndex-not-found check immediately below already returns undefined for it. The separate href.length === 0 guard duplicated that outcome without changing it for any input, so it never had an observable effect of its own to test.
…ed buffer The decoded length isn't known until the final quad's own padding has been walked, so a pre-sized Uint8Array only ever needed to be a large enough upper bound before being trimmed with subarray() -- its own exact size was never something a caller could observe, since any sufficiently large size decodes identically once trimmed. Appending to a plain array and converting once at the end removes that unobservable sizing computation entirely.
Adds cases for a value whose typeof is not "object" but which still carries every property a text node would need, an object whose type matches no recognized variant even though it has full element-shaped fields, and an attribute whose name is well-formed but whose value is not a string.
Parses the emitted XML back into its element tree and checks every tag, attribute, and text node down to the leaves, instead of a handful of substring checks that couldn't tell a wrong tag name or attribute value from a right one. Also covers the blank-heading fallback to a generated section title, joining a multi-run heading's text with no separator.
… guards Adds direct coverage for readUint16LE/readUint32LE's truncation errors, localFileHeaderNames walking past an entry's own extra field and compressed data to find the next signature, and assertMimetypeEntryLayout actually rejecting a malformed entry rather than merely being called with one that already satisfies it. Also drops assertMimetypeEntryLayout's own per-assertion description strings: vitest's second expect() argument only ever labels a failure message and is never part of the pass/fail decision, so no test could ever observe a difference between two such strings.
…parser drops Confirmed empirically against fast-xml-parser's own XMLBuilder in preserveOrder mode: a "?target"/"?xml" node's inner array content is never written to the output regardless of what it holds -- an empty array, a plain object, and a populated "#text" wrapper all produce byte-identical output. The array's own presence is required by the builder's expected node shape, but nothing it holds is ever observable, so building it from node.content/an empty text wrapper only pretended to preserve something this builder configuration already discards on every input. Also drops buildXml's own runtime `typeof out !== "string"` guard: fast-xml-parser's own type declaration (`build(jObj: any): string`) already guarantees a string return unconditionally, and the element case's own attrs-emptiness check before setting ":@" is dropped for the same reason confirmed empirically: an empty ":@" object and an absent ":@" key render identically.
…ape checks fast-xml-parser's own parse() return type is any, so nothing upstream of parseXml can guarantee the preserveOrder shape these functions validate at compile time, and almost none of these checks can be driven by any syntactically valid XML string -- the shape they guard is the library's own internal invariant, not something malformed markup can violate. Exports parseNode, parseNodes, parseAttributes, and scalarText so each guard can be exercised directly with an adversarial unknown value, the same way node.ts's own isXmlNode already is.
…ons already cover isPng, isJpeg, and readJpegDimensions's own initial signature check each paired a bytes.length comparison with byte-value equality checks immediately after it. Reading past the end of a Uint8Array yields undefined, which can never equal a real signature byte, so each length guard was already redundant with the equality checks it stood in front of -- confirmed both analytically and by brute-forcing thousands of byte sequences through the JPEG marker walk with and without its own segment-length guard, finding zero differing outcomes.
…ns subsume The BOM length check duplicated what the three byte-value comparisons right after it already guarantee: a too-short array reads undefined at those indices, which never equals a real BOM byte. The loop's own b === undefined guard is equally redundant -- within i < bytes.length a Uint8Array never holds a hole, and even if b were undefined, it fails every whitespace comparison and the trailing b === 0x3c check the exact same way this guard's own return false already did.
parseOpf's own round trip normalizes away several details a hand-built OPF document actually carries: it never checks a manifest item's properties attribute is even present when empty, it drops dc:identifier's own id attribute, and it never looks at package/metadata's own xmlns attributes at all. Parses the emitted XML back into its element tree to assert on all of these directly, and adds coverage for joining more than one manifest property with a real space and for the dcterms:modified meta element, neither of which any existing test exercised.
…ntent Every existing test reading these fixtures resolves paths through the OPF manifest, so a wrong zip entry path, a mimetype entry that isn't genuinely stored uncompressed, or an entry silently zipped with no content at all would still let most of those tests pass. Verifies each fixture's own real output directly: the mimetype entry's byte layout, every declared path present with real non-empty content, and the total entry count (so two paths colliding can't hide behind an already-present one). Also shrinks the hand-built cover PNG to the 24 bytes its own IHDR header actually needs and adds a direct width/height check against it: this package's own PNG reader never looks past that offset, so the trailing bit-depth/colour-type bytes it used to write were never observable by anything, and the width/height fields it does read had no test checking them at all.
…squerade fixture Object.assign doesn't verify a source object's properties are assignable to the target's declared types, so it passes silently where a direct property assignment would be rejected by the compiler.
Mearman
force-pushed
the
feat/100-percent-mutation-epub-codec
branch
from
September 14, 2026 16:59
0dfe542 to
f446dfd
Compare
…nd the redundant span case styledRun's hyperlink parameter had no caller: appendAnchor's own external-href fallback spreads hyperlink onto each nested run directly instead of ever calling styledRun with one. Separately, appendElement's switch listed "span" as its own case immediately above the identical default fallthrough, so the case label named nothing default did not already handle. Also covers styledRun's fontFamily/verticalAlign/direction fields, each tag alias case (<b>/<i>/<strike>/<del>), an empty CDATA section producing no run, and appendImageFallback/appendAnchor's own edge cases -- none of these had a dedicated test file before.
…s as dead ZipEntry objects The entries array's only consumer strips every field but .bytes before handing it to packageFromEntries -- packageFromEntries/serializePackage (the Package model, #963) own the real OCF mimetype-first/stored-uncompressed byte layout now, so the "stored" flag this array's own mimetype entry carried had no reader left. Reworked the array to plain [path, bytes] pairs and dropped the now-unused ZipEntry import. Also folds buildHead's own "no heading in this section" fallback into sectionXhtml's stored ContentSection reference rather than re-deriving it from document.sections[index] after the fact -- sectionTitle already provides the identical "Section N" fallback on its own, so the separate ternary duplicated it for a lookup that was always defined by construction anyway. Covers each ContentImageBlock format's own extension and media-type, the OPF manifest's nav/section/image item shapes, registered-image numbering, the generated dc:identifier, an unbalanced construct marker, and writeEpub's own non-wordprocessing kind check -- none of which src/write.ts had a dedicated test file for before.
…nostic message paths Adds coverage for a wrong (but present) mimetype entry, a missing container.xml, a missing OPF rootfile, an empty resolvable spine, and a section with no quarantined CSS residue carrying no source key at all. Adds real-message assertions (not just .code) for SPINE_ITEMREF_UNRESOLVED, MANIFEST_ITEM_MISSING, INVENTED_PAGE_GEOMETRY, NAV_DOCUMENT_MISSING, NAV_SPINE_ORDER_MISMATCH (both the EPUB 3 nav and EPUB 2 NCX variants), and NCX_MISSING. Adds NCX_MISSING-does-not-fire coverage for a spine with no toc attribute and for one whose toc attribute resolves to a real manifest item, matching the existing NCX_MISSING-fires case. Adds a bespoke three-chapter fixture for buildCrossDocumentAnchorRegistry's own two edges no existing fixture reaches: a footnote-shaped referrer read first is never downgraded by a later plain referrer to the same target, and an explicit full-path self-reference degrades to a plain hyperlink rather than an internal link construct pointing at its own document.
…ined-read termination readJpegDimensions' own marker walk and looksLikeXml's own BOM/ whitespace scan each paired a bytes.length comparison with reads that are already undefined-safe past the array's end -- an out-of-bounds read can never match a real marker byte, a real whitespace byte, or the literal '<', so the loop's own final branch already answers correctly for that case regardless of the length check. Proved by running each loop with and without its own bound and finding zero differing outcomes. Restructured both as unbounded loops whose real termination is the undefined read itself, removing the redundant comparison entirely rather than leaving it for a mutation test to prove equivalent.
… truncation guards A Uint8Array holds no gaps, so its highest-read index (b1 for the 16-bit reader, b3 for the 32-bit one) reads as undefined whenever any lower one would too, and never the other way around -- the separate per-byte undefined checks the two guards paired together were already redundant with the highest one alone. Adds definedByte, a narrowing helper for the remaining lower-index reads that throws rather than silently coercing to NaN if the invariant it relies on is ever wrong, with its own direct unit test covering both branches.
…gnored builder value fast-xml-parser's builder never reads a pi/declaration node's own value at all in preserveOrder mode, confirmed against every shape tried (an empty array, one holding real content, undefined, null, a plain object) -- each produces the identical output. undefined states that plainly rather than via an empty array shaped like children data the builder would otherwise use.
…toc" hasTocEpubType matches "toc" at a real token boundary (preceded and followed by whitespace or a string edge); nothing previously exercised a value like "toc-alt" that starts with the literal substring but isn't a standalone token.
Mearman
marked this pull request as ready for review
September 14, 2026 18:42
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Mearman
enabled auto-merge (rebase)
September 14, 2026 19:13
Contributor
|
🎉 This PR is included in version 1.5.5 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Works through epub-codec's survived and no-coverage Stryker mutants file by file, adding targeted tests that exercise the branches the existing suite left unreached, so real bugs in this hand-written PNG/JPEG/XML/OPF/nav logic get caught by a failing test instead of surviving silently.
Package mutation score so far: up from a 70.01% local baseline (against the committed CI-measured breakThreshold of 64) to roughly 78-79%, with the survived+no-coverage count down from 627 to 428.
Files brought close to or at 100% (remaining survivors in these are genuine equivalent mutants -- bounds-check guards subsumed by JavaScript's out-of-bounds-index-is-undefined semantics, confirmed by hand for each one): image/dimensions.ts, diagnostics.ts, path.ts, xhtml/link-target.ts, package-io/read.ts, xml/node.ts, util/base64.ts, ocf/container.ts, codec.ts, nav/reconcile.ts, nav/ncx.ts, xml/entities.ts, xml/query.ts, xhtml/context.ts.
Meaningfully improved but not yet exhausted: xml/build.ts, xml/parse.ts, xhtml/list-id.ts, nav/nav3.ts, xhtml/footnote.ts, opf/parse.ts.
Not yet started: the package's largest and most complex modules -- xhtml/read.ts and xhtml/write.ts (the XHTML-to-ContentDocument mapping engine, 103 and 57 survived/no-coverage mutants respectively), write.ts, opf/metadata.ts, xhtml/inline.ts, test-support/zip.ts, read.ts, opf/write.ts, nav/write.ts.
Draft until the package as a whole is done.