Skip to content

test(markdown-codec): work toward 100% mutation score - #1265

Draft
Mearman wants to merge 83 commits into
mainfrom
feat/100-percent-mutation-markdown-codec
Draft

Mearman wants to merge 83 commits into
mainfrom
feat/100-percent-mutation-markdown-codec

Conversation

@Mearman

@Mearman Mearman commented Sep 12, 2026

Copy link
Copy Markdown
Member

Summary

Working through markdown-codec's survived/no-coverage mutants toward a genuine 100% Stryker mutation score, per the same pattern already applied to other packages in this workspace (archive-codec, byte-codec, document-compute.js, excel-number-format, pdf-raster-cpu, ...).

Measured baseline: 76.32% of 4529 valid mutants, timeout share 4.9% (breakThreshold: 71 in stryker.config.ts). Current full-package run: 81.08% (658 survived, 178 no-coverage remaining out of ~4900 valid mutants -- mutant count grew slightly as coverage improved and unlocked previously-ignoreStatic-skipped code paths).

Files fully at 100% now (small/foundation modules from earlier work, plus this session's): shared/list-id.ts, shared/style-constants.ts, lower/table.ts, inline/math.ts, inline/footnote.ts, lower/image.ts, emit/image.ts, ast/ast.ts, read.ts, write.ts, block/line.ts, block/node.ts, block/table.ts, block/list.ts, block/definitions.ts, inline/link.ts, inline/entity.ts, inline/delimiter.ts, scan/scan.ts, emit/table.ts, test-support/spec-corpus.ts.

A genuine correctness bug was found and fixed along the way, not just a mutation-score gap: processEmphasis (inline/delimiter.ts) dropped a fully-consumed CLOSER's own AST node from the sibling chain but never removed the underlying Delimiter from the delimiter stack, unlike the symmetric opener-side branch two lines above. canMatch has no way to see that a delimiter's count already reached zero, so a later closer could walk back into that exhausted delimiter and match it a second time -- "*a*b*c*" reproduced this concretely: the first pair's own closer, left on the stack, was wrongly matched by the second closer, dropping the "c" pair's emphasis entirely. Now covered by a direct regression test.

Several genuinely-equivalent mutants were eliminated by restructuring rather than adding an unkillable test (each verified equivalent by disabling it under the full suite before removing it, and for two openers-floor checks in delimiter.ts, by a 25x-scale timing test proving the floor bounds an otherwise-quadratic search rather than changing any result):

  • read.ts: a reference-identity-only shortcut in readMarkdown's definitions/source splice, and a redundant ?? {} fallback.
  • block/line.ts: lineIsBlank's dead class-field default and advance()'s early-return.
  • block/table.ts, emit/table.ts, inline/link.ts, inline/entity.ts: redundant "is there a character after this one" lookahead guards, each absorbed by charAt's own out-of-range "".
  • block/list.ts: a redundant marker-match field pair (ORDERED_MARKER_PATTERN's capturing groups are both mandatory) and a redundant a.type === b.type check in listsMatch (already implied by the bulletChar/delimiter field comparison).
  • inline/delimiter.ts: the tilde-specific branch in delimitersConsumedByMatch (subsumed by canMatch's own count-equality requirement), both unlink() calls on a fully-consumed run (subsumed by toAstNode's own zero-length-text filter), an idempotent matchedOpener.next !== closer guard, and a redundant opener !== stackBottom loop arm (subsumed by opener !== floor). closerSignature is exported and directly unit-tested, since its exact string encoding has no effect processEmphasis's own black-box behaviour can distinguish.
  • test-support/spec-corpus.ts: replaced a separate "does every key exist" guard with a proper isRecord type guard (a missing field already fails the typeof checks that follow), and four ?? "" fallbacks absorbed by an already-checked index < lines.length.
  • block/definitions.ts: a redundant minimum-label-length guard (already implied by the empty-label check that follows), and countNewlines restated as a slice+split rather than a hand-bounded loop.

No // Stryker disable comments anywhere -- confirmed via grep -r "Stryker disable" src.

Remaining work, by descending mutant count: block.ts (76+4), emit.ts (112+24), image/image.ts (78+24), gfm-autolink.ts (64+9), emit/inline.ts (59+13), lower/lower.ts (44+10), lower/front-matter.ts (45+7), emit/html-table.ts (33+16), html/html-table.ts (32+11), chars.ts (25+19), lower/inline.ts (25+3), inline/inline.ts (23+7), diagnostics.ts (18+10), html/render.ts (13+15), emit/front-matter.ts (11+6). Still in progress, left as draft until the score is genuinely verified at 100.

Test plan

  • pnpm --dir packages/markdown-codec typecheck
  • pnpm --dir packages/markdown-codec lint
  • pnpm --dir packages/markdown-codec test
  • Full pnpm --dir packages/markdown-codec exec stryker run stryker.config.ts at 100%
  • breakThreshold raised to 100 once verified

@Mearman
Mearman force-pushed the feat/100-percent-mutation-markdown-codec branch 4 times, most recently from 3afe16d to 77f24a7 Compare September 14, 2026 08:33
…ble undefined branch

mintListNumId now has tests pinning that a bullet mint ignores a supplied
start value and that an ordered mint with no start omits the @n suffix
entirely, rather than stringifying undefined into it. parseListNumId gains
a test for a numId with a numeric suffix on a bullet marker (a shape the
regex itself allows, since the suffix isn't gated on type), which the
parser must still treat as start: undefined.

parseListNumId's own type-narrowing guard dropped its `type === undefined`
half: NUMID_PATTERN's second capturing group is a mandatory alternation
with no `?`, so a successful match always populates it, and the
`type !== "bullet" && type !== "ordered"` half already answers `true` for
`undefined` on its own -- the dropped half never distinguished any real
input from the other.
…ity guards

Adds direct tests for headingStyleId/parseHeadingStyleId: level 0 rejected
(a heading style level is always positive), a 400-digit run rejected (it
parses to Infinity, which Number.isInteger correctly refuses), and a level
past the markdown-reachable 1-6 ceiling still parsed, since ContentDocument
is a shared cross-format pivot other producers may carry a deeper heading
level through.
…nt keys

lowerTable's own column-width arithmetic (contentWidthPt / columnCount) had
no test distinguishing it from any other arithmetic on the same two
numbers, since the existing test only checked that both columns came out
equal to each other. Adds a test with an explicit page size and margins so
the expected per-column width is a known, exact number.

Also pins that a table cell with no run-level constructs carries no
`constructs` key at all, and a column the delimiter row leaves unaligned
carries no `alignment` key -- both spread conditionally, and neither had a
test checking the key's absence rather than just its rendered content.
…uard clauses

No test called matchMathInlineSpan directly before this -- it was only
exercised indirectly through the inline parser's own already-real \(...\)
input, which never distinguishes the guard's two sub-conditions from each
other or from a forced true/false, since a genuine match never needs to
fall through to a wrong answer. Pins: a real span; an unterminated \( with
no test each individually; and that the closing search starts strictly
after the opener, never before it (a preceding, unrelated \) must not be
mistaken for the real close).
…er grammar

matchFootnoteLabel, matchFootnoteDefinitionMarker, and isValidFootnoteLabel
had no test calling them directly -- only src/footnote.test.ts's end-to-end
round trips through the whole read/write pipeline, none of which exercises
a valid label with no following colon (a reference, not a definition) or
text that never matches the label grammar at all.
…ight axes

The only existing coverage (lower.test.ts's 1x1 PNG fixture) happens to
carry the same value on both axes, so a widthPt/heightPt swap or a wrong
operator on either axis produces no observable difference. Adds a real
300x100 PNG fixture and checks each axis converts its own pixel dimension
to points independently.
…mages

Every existing image emit test supplied altText, so the ?? "" fallback
for a ContentImageBlock with none at all was never exercised.
…arkdownInlineNode

Neither predicate had a single test or internal caller before this --
they were dead code as far as this package's own test suite could tell,
even though both are part of the module's public surface. Pins block vs.
inline classification for a representative of each side, plus every real
block node type named in BLOCK_NODE_TYPES individually.
…s/source table

readMarkdown's own definitions/source splice special-cased "neither table
applies" to return assembleTree's result unchanged, rather than spreading
it. The spread was already a no-op in that case -- spreading undefined,
or an absent optional key, adds nothing -- so the shortcut bought only an
object reference identity DocumentTree's own contract never promises, at
the cost of a branch no value-level assertion could ever tell apart from
always spreading. Also drops the `assembled.source ?? {}` fallback the
frontmatter splice used: spreading `undefined` directly is exactly as
inert as spreading `{}`, so the fallback never changed the result either.

Extends package.test.ts's coverage of the write side to match: a titleless
link reference definition (no title key on the rendered entry, and no
trailing title clause in the written text), two definitions joined by a
real newline rather than a coincidentally-equal separator, and a
definitions-only document (empty body) rendering the definitions bare
with no leading blank line.
lineIsBlank's own class-field default (false) could never be observed to
differ: the constructor unconditionally calls findNextNonspace()
immediately afterward, which always assigns the real value before any
getter can read it. Dropped the initializer (definite-assignment `!:`
instead) rather than leave a default no test could ever tell from any
other value.

advance()'s early return at end of line is the same shape:
MarkdownScanCursor.next() is already a side-effect-free no-op once
rawOffset reaches the source length, so looping the remaining count down
regardless produces the identical end state as returning early. Dropped
the guard.

Adds direct LineCursor tests for blank-line detection (empty and
whitespace-only lines, and a non-blank one), which the package had none
of before this -- the class was only ever exercised indirectly through
src/block/block.ts's own parsing.
…t operations

InlineNode had no test of its own before this: appendChild, unlink, and
insertAfter were only ever exercised indirectly through the inline
parser's own emphasis/link resolution, which never isolates a single
operation's own effect on the surrounding chain. Pins each field's default
for a node kind that never sets it, appendChild's ordering, unlink's
neighbour re-linking (mid-chain and at either end), and insertAfter's own
three distinct behaviors: splicing in a fresh node, detaching a node from
its OLD chain before relinking it into a new one, and updating (or
correctly leaving alone) the parent's own lastChild depending on whether
the insertion lands at the end.
…htness logic

isBulletMarker/isOrderedDelimiter narrowed a regex match's own capture
group to a literal type, but both patterns' character classes already
guarantee the value (BULLET_MARKER_PATTERN is exactly `[*+-]`,
ORDERED_MARKER_PATTERN's second group is exactly `[.)]`) -- neither
predicate's "not a member" branch is reachable from a real match, so both
became a plain cast at their one call site each, with a comment stating
why it's safe.

parseListMarker's own trailing-spaces scan drops three more branches that
turned out to be fully compensated for downstream rather than genuinely
decisive: the do-while's own code-indent cap (the reset branch already
re-derives the item's content indent from scratch whenever the count
exceeds it, so the cap only changed how far the loop itself walked, never
the returned value or the cursor position it leaves behind), the
`followingSpaces < 1` disjunct (the do-while's own do-first structure
means that can only ever be true when startsBlank is also true, so it
was never an independent second condition), and the reset branch's own
`if (line.peek() === " ")` guard on its own follow-up advance (the
marker-follows-by check earlier in the function already guarantees the
character there is a space/tab/EOL, and advancing past EOL is a no-op, so
the guard's own false side is equally unreachable).

Adds src/block/list.test.ts: direct coverage of listsMatch's own three
fields (type/delimiter/bulletChar) and of finalizeListTightness's
lastLineChecked memoisation actually setting the flag on both the
descend-further and stop-and-return-false paths, neither of which any
existing test observed directly.
…testable

Four scan loops (matchLinkLabel, parseLinkDestination's angle-bracketed
form, parseLinkTitle, skipInlineWhitespace) bounded themselves with
`index < text.length`, which turned out to be indistinguishable from
`index <= text.length` for every one of them: text.charAt(index) already
returns "" one index past the end, and none of these loops' own character
comparisons ever match "" either, so the one extra boundary iteration
always falls through to the identical exit path regardless of which
comparison guards it. Rewritten as `text.charAt(index) !== ""` instead --
exactly the same boundary for every real index, but one whose own
mutation (the operator, or the "" literal) is now actually reachable by a
test rather than always landing on the same fallthrough either way.

parseLinkTitle's own `closer === undefined` guard is the same shape: when
`opener` isn't one of TITLE_DELIMITERS' own three keys, `char === closer`
can never match a real character, and TITLE_DELIMITERS' own mapping means
`opener` is only ever "(" when closer IS defined -- so the loop already
scans to the end and returns undefined regardless, and the guard bought
nothing an early return wouldn't have. Dropped in favour of a comment
recording why.

Adds direct tests for four scenarios nothing exercised before: a start
that isn't "[" with a ']' reachable later (matchLinkLabel), an unescaped
nested '<' with no line ending (parseLinkDestination's bracketed form), a
trailing unescapable backslash treated as a literal character rather than
the start of a truncated escape (parseLinkDestination's bare form), and
isBlankRemainderOfLine's own four cases (nothing exercised it at all
before this) including reaching the true end of the text.
…om MarkdownScanCursor

atEnd()'s own `pendingTabColumns === 0` half was never independent of the
rawOffset check beside it: rawOffset only advances past a tab once every
one of its columns is spent (next()'s own tab branch), so rawOffset can
never reach source.length while a tab is still mid-expansion. Checking
rawOffset alone already answers the same question.

peek() dropped both its `pendingTabColumns > 0` branch and its own
`rawOffset >= source.length` guard: while a tab is mid-expansion, rawOffset
still points AT that tab character (the same invariant atEnd relies on),
so the plain read below already finds '\t' and returns the correct
synthetic space through its own tab branch; and past the end of input, a
string index in JS is already `undefined` on its own, which matches every
comparison below it and falls out the far end as `undefined` regardless.
Both "extra" branches produced the identical answer the plain read below
them already gives, on every reachable input.

next()'s own end-of-input guard is NOT the same shape and stays: skipping
it would still return the correct `undefined`, but it would also mutate
rawOffset/columnNumber for a character that was never really there,
corrupting the cursor's own state on every subsequent call. Added a test
pinning that calling next() repeatedly past the end is idempotent.

Adds direct coverage for what was previously untested at all: peek()'s
own '\r' normalisation and true-end-of-input case, and peekRaw() actually
slicing (a same-length fixture had let it read as `this.source` with the
slice call itself elided).
… HTML recogniser

matchHtmlTag's own text.charAt(start) !== "<" guard and
matchHtmlBlockStart's own !line.startsWith("<") guard both duplicated a
fact their real regexes already enforce: every alternative in
HTML_TAG_PATTERN, and every real entry in HTML_BLOCK_START_PATTERNS
(types 1-7), is itself anchored at `^` and begins with a literal '<' in
its own source -- so a string that doesn't open with '<' already fails
every one of them on its own, and the dedicated guard could only ever
agree with what the pattern match was already going to answer.
… and canContain

BlockNode's replaceWith/unlink and the module-level canContain had no
test of their own before this. Pins each mutable field's own empty-string
default (infoString/literal/headerLine/footnoteLabel), replaceWith/unlink
both correctly no-op-ing when the node they're called on isn't actually
present in its own parent's children array (an inconsistent state a
wrong `index !== -1` check would otherwise splice(-1, 1) against --
deleting the parent's LAST child instead of nothing), and every one of
canContain's own per-parent-kind branches, including the two restrictions
specific to a footnote definition.
…wn out-of-range ""

splitTableRow's own scan loop and its backslash-pairing check, and
endsWithUnescapedPipe's own trailing-backslash count, each paired a
length-based bound with a character comparison that can never match "" --
so once the length bound would have stopped the loop, the character
check was already going to fail on its own the very next read, on every
reachable input. Restated the two loop bounds as `charAt(...) !== ""`
(the same boundary, spelled as the check that's actually reachable by a
test) and dropped endsWithUnescapedPipe's bound entirely, since charAt of
a negative index is already "" with no separate arithmetic needed to say
so.

parseTableDelimiterRow's own `cells.length === 0` guard is dead for a
different reason: splitTableRow always pushes its own trailing
`current.trim()` unconditionally, even over empty input, so it can never
actually return an empty array.

Adds real coverage for what these bounds were guarding in practice:
leading/trailing whitespace trimmed before either pipe is read, a leading
pipe stripped independently of a trailing one (and vice versa), a lone
trailing backslash with nothing to escape treated as a literal character,
and endsWithUnescapedPipe's own odd/even backslash-run counting through
three and four consecutive trailing backslashes, not just one.
…oint-boundary coverage

matchEntity's own '&'-prefix guard is the same redundant shape already
fixed for matchHtmlTag/matchHtmlBlockStart: ENTITY_PATTERN's own source
is anchored at `^&`, so a slice that doesn't open with '&' can never
match regardless. unescapeString's own "neither backslash nor '&' at
all" fast path is provably a pure optimisation too: for a string with
neither, the loop it skips never takes the backslash/entity branches
either, so it does nothing but reconstruct the identical string one
character at a time -- same output, more work, never a different result.
Its own loop bound gets the same charAt(index) !== "" restatement
already applied elsewhere in this codec, for the same reason.

Adds direct tests for codepointToString's own three boundaries (U+0000,
the maximum codepoint, and the low/high surrogate range) that nothing
exercised before -- each just below, at, and just past its own edge, so
each comparison's own direction and operator is pinned rather than only
its "obviously in range" and "obviously out of range" interior points.
…ahead guard

charAt's own out-of-range "" already makes the escape ternary append
char + "" (the identical single backslash the no-escape fallthrough
would append anyway), so a trailing-backslash guard clause never
gated two genuinely different outcomes.
ORDERED_MARKER_PATTERN's two capturing groups are both mandatory, so a
successful exec() always populates them -- the digits/delimiter
undefined checks could never see their own true branch, only
TypeScript's own per-capture typing needed told (matching the bullet
branch's own cast just above).

listsMatch's own a.type === b.type check is equally redundant:
bulletChar is set only on a bullet marker and delimiter only on an
ordered one, so two markers of different variants already fail one of
the two field comparisons (a real value against undefined) before the
type check could ever matter.

Adds a test proving endsWithBlankLine's own listItem branch of its
list/listItem descent condition is load-bearing: a blank line nested
two levels inside a listItem (not caught by finalizeListTightness's
own per-child loop, which only re-checks an item's DIRECT children)
needs the descent to continue past a listItem, not just a list.
… guard

Running off the end of text makes charAt(index) "", which is neither
" " nor "\t" nor "\n" -- the character-kind check already breaks the
loop on that same condition, so the separate in-range guard could
never fire anywhere the inner break wouldn't already have stopped it.

matchLinkLabel's own loop guard has no such internal catch-all (an
ordinary character just falls through to index += 1), so it genuinely
needs the range check -- but nothing exercised the boundary it exists
for. Adds a test for an unterminated label that runs off the end of
text with no closing ']', which previously fell out of every test's
own coverage of this loop.
…guard

matchEntity's own ENTITY_PATTERN is anchored at "^&", so calling it at
a non-'&' index can never match regardless -- the same reasoning
matchEntity's own comment already applies to its leading-character
check. Calling it unconditionally and falling through on undefined
removes a guard that only ever gated two identical outcomes.
The prior test only asserted next() returns undefined once
MarkdownScanCursor is already at the true end of input, which the
>= and > spellings of the range check both satisfy. Asserting position
stays exactly where it was pins the actual boundary: >= stops before
touching rawOffset/columnNumber again, while > would tick both forward
on a call that should be a no-op.
…nt guards

Adds exact-message assertions for TABLE_HTML_FALLBACK,
TABLE_CELL_MULTI_PARAGRAPH_JOINED and TABLE_CELL_IMAGE_DEGRADED, a
negative case proving MULTI_PARAGRAPH_JOINED does not fire for a
single-block cell, a test proving an empty-text paragraph is skipped
rather than joined as a stray <br>, and a test for the empty-rows
table that returns "" outright.

escapeUnescapedPipes drops the same two redundant guards already
removed from its sibling scanners elsewhere in this codec: the loop's
own charAt(index) !== "" restatement of its bound, and the "is there a
character after the backslash" lookahead, whose out-of-range ""
already makes the escape branch append the identical single backslash
the no-escape fallthrough would.
… can be reused

processEmphasis dropped a fully-consumed CLOSER's own AST node from the
sibling chain but never removed the Delimiter itself from the stack,
unlike the symmetric opener-side branch two lines above. canMatch has
no way to see that count already reached zero, so a later closer could
walk back into that exhausted delimiter and match it a second time --
consuming already-spent count negative and swallowing whatever real
pair should have formed instead. "*a*b*c*" reproduced this: the first
pair's own closer, left on the stack, was wrongly matched by the
second closer, dropping the "c" pair's emphasis entirely.

Also removes four provably redundant checks in the same function,
each confirmed equivalent by disabling it under the full suite (and,
for the two openers-floor checks, by a 25x-scale timing test showing
the floor genuinely bounds an otherwise-quadratic search rather than
changing any result):
- the tilde-specific branch in delimitersConsumedByMatch, since
  canMatch's own count-equality requirement for strikethrough already
  makes the generic formula agree with it in every reachable case
- openerNode.unlink()/closerNode.unlink() on a fully consumed run,
  since toAstNode already drops a zero-length text node regardless of
  where it sits in the sibling chain
- the idempotent matchedOpener.next !== closer guard
- the search loop's own redundant opener !== stackBottom arm, already
  subsumed by opener !== floor

closerSignature is exported and directly tested: its exact string
encoding has no effect on processEmphasis's own observable behaviour
(every real signature stays distinct regardless of the literal
spelling), so pinning its own contract needs a direct unit test of the
pure function rather than an attempt to observe it through the whole
algorithm.
spec-corpus.ts had no test file of its own: its type guards and the
loader's own malformed-input throw were only ever exercised
incidentally by loading the real, always-well-formed vendored corpora
in conformance.test.ts and gfm-conformance.test.ts, which never
reaches the failure paths at all.

isSpecExample drops its own separate "does every key exist" guard: a
genuinely missing field reads as undefined at runtime, whose typeof
never matches "string" or "number", so the four typeof checks already
reject a missing field exactly as they reject a present-but-wrongly-
typed one -- the guard could only ever return false in cases the
checks already covered. Narrows through a proper isRecord type guard
instead, matching the pattern already used elsewhere in this
ecosystem (e.g. epub-codec's xml/node.ts) rather than an unsafe cast.

loadGfmExtensionExamples' four `lines[index] ?? ""` reads are replaced
with non-null assertions: each is already guarded by an identical
index < lines.length check earlier in the same expression or the
enclosing loop condition, so the fallback string can never actually be
reached -- only TypeScript's own indexed-access typing needed told.
LIST_NUMID_FALLBACK's own once-per-numId dedup

stripCheckboxRun was only ever exercised with the glyph and its
following text in two SEPARATE runs, leaving the same-run case
(where slicing off the glyph prefix leaves real text behind in that
run) unobserved; a task-flagged numId whose leading text matches
neither legacy glyph had no test proving the ordinary-bullet
fallback still applies.

listInfoFor's own reportedFallbackNumIds dedup was only ever
exercised through a single occurrence of an unminted numId, so
nothing proved the SECOND item sharing that numId does not re-fire
the diagnostic.
and the empty-render filtering in renderItems

An invalid footnote label, a non-footnote anchor's own "anchor (type)"
detail spelling, and a division's own divisionDepth suppressing a
wrapped paragraph's separate indentLeftPt from being counted a second
time were each entirely untested.

Both of renderItems' own "skip an empty render rather than pushing a
spurious blank part" checks (the plain-block path and the construct
path) had no test proving a genuinely empty render -- a page break, a
bodyless anchor -- doesn't still widen the gap between its neighbours.
the default-embedImages branch for a data: URI image link

Nothing proved a division's own exit decrement actually restores
divisionDepth to its prior value once the division closes -- only
that entering one suppresses the wrapped paragraph's own indent
while still inside it. A standalone paragraph rendered immediately
after a division now confirms the depth genuinely returns to 0
rather than leaking an elevated value into whatever follows.

The single existing image-link-construct test used a remote (non
data: URI) destination, which short-circuits past the
images-option check entirely; nothing exercised the actual
bytes-are-the-destination branch with images left at its own
default of true.
ordered-delimiter branch for a depth-only membership

listInfoFor's own undefined-numId branch never returns real
ListNumIdInfo, so type defaults to "bullet" every time numId is
undefined -- the type === "ordered" check in the numId-undefined
side of this ternary can never be true, making its own
orderedDelimiter branch dead code no test can ever reach.
nested loose-list blank-line indentation

firstBlockCheckbox is deliberately gated on BOTH membership.checked
being absent AND the numId's own task flag -- an ordinary,
non-task-flagged item whose leading text happens to spell the legacy
checkbox glyph exactly had no test proving it still renders as plain
text rather than being misread as a checkbox.

A nested sub-list's own rendering, once indented under its parent
item, had no test proving a genuinely blank line inside that
rendering (the gap a loose sub-list's own blank-line separator
produces) stays truly empty rather than gaining trailing indent
whitespace.
continuation block's own body

The parallel indent-skip check for a nested sub-list's own blank
lines was just covered, but the sibling check on the plain
continuation-block path (a later block of the same item, not a
nested list) had no equivalent test: nothing proved a genuinely
blank line inside a second block's own multi-line body (a fenced
code block whose literal itself contains a blank line) stays truly
empty once indented, rather than gaining trailing indent whitespace.
heading-collapse diagnostics, and isolate the UNCHECKED glyph path

HEADING_LINE_BREAK_COLLAPSED and the "blank-line" branch of
HEADING_LINE_BREAK_UNSAFE_FOR_SETEXT's own message were only ever
checked for whether they fired, never for what they actually said.

firstBlockCheckbox's UNCHECKED glyph check was only ever exercised
immediately after a CHECKED one already matched and returned early
in the SAME call; a standalone item whose only glyph is the
UNCHECKED spelling isolates that specific check on its own.
distinguish startsWith from endsWith

The prior version split the glyph and its following text across two
separate runs, so runs[0].text was exactly "☐ " -- identical from
both ends, which made a startsWith/endsWith swap on that check
unobservable. Combining the glyph and its trailing text into one run
gives leading text where the two methods genuinely disagree,
confirmed directly by applying the swap by hand and watching this
exact test fail.
through a construct resuming a list item

Every existing construct-resumes-outer-item test either had nothing
after the construct or only checked which item the construct
attached to, never whether a FOLLOWING block's own blank-line
decision correctly reflects what that construct actually ends on.
A plain paragraph directly after a construct whose sole wrapped
block is a CodeBlock must stay tight, since a CodeBlock terminates
cleanly -- proving lastStyleIdOf genuinely walks into the
construct's own children rather than silently reporting undefined,
confirmed by manually applying the length-1 -> length+1 mutation
and watching this exact test fail.
lookup feeding the outer item's resuming block

The one existing test covering a nested sub-list resumed by the
outer item used a plain, styleId-free paragraph as the sub-list's
own last block, so its real and mutated (always-undefined)
lastStyleIdOf readings were indistinguishable -- both landed on
undefined either way. A CodeBlock-styled nested item isolates the
lookup itself: the outer item's own resuming block must stay tight
only when that real styleId is actually found, confirmed by
manually applying the length-1 -> length+1 mutation and watching
this exact test fail.
before pushing a sibling item

The pop-while-loop was only ever exercised implicitly through
genuinely deeper nesting, never through two SIBLING items sharing
the SAME level -- nothing proved the >= comparison (not a plain >)
is what lets a sibling's own membership actually leave the stack
once its successor is pushed. Three same-level items followed by a
construct carrying only the FIRST one's itemId isolates it: with the
membership correctly popped, the construct can no longer attach to
that no-longer-open item and starts a fresh list region of its own
instead, forcing the blank-line separation a genuinely new region
gets. Confirmed by manually applying both the >= -> > and the
whole-condition -> false mutations and watching this exact test fail
either way.
requirement with mixed children

Every existing division test had children that either ALL qualified
for the dual-carry quote indent or NONE did, so .every() and .some()
were indistinguishable on those inputs. A division wrapping one
quote-indented paragraph and one plain paragraph isolates it: the
division must render transparently (no '> ' wrapping of its own)
because not every child qualifies, even though at least one does --
confirmed by manually swapping every for some and watching this
exact test fail with the wrongly-materialised output.
any-match (not all-match) requirement

Every existing test giving a construct MULTIPLE children had either
every child carry the itemId being matched or exactly one child
total, so .some() and .every() always agreed. A construct wrapping
two paragraphs, only one of which carries the item's own itemId,
isolates it: the construct must still be recognised as belonging to
that item, since ANY carrying child is sufficient -- confirmed by
manually swapping some for every and watching this exact test fail
with the construct wrongly fracturing out as unrelated content.
recursion into table cells

Both existing run-construct-extent-fault tests used a top-level
paragraph; nothing proved the table-row/table-cell recursive walk
itself actually runs. A paragraph carrying the identical
beyond-runs fault, but buried inside a table cell, confirms
validateRunConstructExtents still catches it -- manually emptying
the table-recursion loops confirmed the fix by watching this exact
test fail once the fault went unnoticed.
resume-detection guard

Whether the resumed run's own consumeSameItemRun call actually
consumed anything was checked purely to skip pushing an empty "own"
segment when it didn't -- but nothing downstream ever reads a
segment run's own count, only segments[0] and each segment's own
blocks, so that empty segment is invisible either way. Removing the
guard leaves the loop's own existing nested-run check (which already
breaks once index stops advancing) to terminate it on the very next
pass instead, with identical observable output.
in both directions

Every existing setext-eligibility test used Heading1 (level 1),
which sits well clear of the level > MAX_SETEXT_LEVEL boundary in
either direction, leaving both the exact-boundary (level 2, still
eligible) and the just-past-it (level 3, already refused) cases
unobserved. A Heading2 followed the same forced-blank-line pattern
as the existing Heading1 test to prove level 2 remains eligible; a
Heading3 with headingStyle: 'setext' explicitly requested proves the
opposite -- a level with no setext spelling at all must stay ATX and
tight regardless of the configured style, not merely whenever some
OTHER trigger (an embedded break) happens to be absent. Confirmed by
manually applying both the > -> >= and the whole-condition -> false
mutations and watching the respective test fail each way.
division-kind guard

isMaterialisedDivision already re-checks item.descriptor.kind ===
"division" as the first half of its own condition, so a construct
whose descriptor is genuinely some other kind already fails that
check on its own and falls through unchanged -- the outer
descriptor.kind === "division" wrapper around the call tested
exactly the same fact a second time.
false for the membership.checked branch

Every existing membership.checked test used run text that never
started with a legacy checkbox glyph, so stripCheckboxRun's own
early "doesn't match, leave it alone" exit already made stripGlyph's
value irrelevant. A run whose text happens to spell the legacy
glyph exactly, paired with a field-based checked value, isolates it:
the glyph-looking text must survive as ordinary content, proving
stripGlyph is genuinely false here rather than wrongly true.
mint condition

Every existing image-link-construct test wrapped exactly one child,
so nothing proved the length === 1 check actually excludes a
construct with MORE children even when the first one is an image.
A link wrapping an image followed by a caption paragraph isolates
it: the construct must fall through to its own generic, transparent
rendering (the image rendering as itself, not the link-shortcut's
own remote-destination spelling) rather than being mistaken for the
one-image mint shape.
the embedded formula's objectKind/document-kind agreement

Every existing HEADING_LEVEL_CLAMPED test fired the diagnostic;
nothing proved a heading whose level needs no clamping stays quiet.

The embedded-object formula shortcut checks BOTH objectKind ===
"formula" and document.kind === "formula" -- the one existing
"any other kind" test happened to keep both fields in agreement
(mismatched together), so a genuine disagreement between the two
(objectKind wordprocessing, document.kind formula, with real
presentation LaTeX) went unexercised. Confirmed by manually applying
each mutation and watching the respective test fail with precisely
the predicted output.
block-start vs paragraph-continuation sense at both call sites

Neither of unsafeSetextBreakReason's two calls to
interruptsSetextParagraph had a test distinguishing the block-start
sense (atBlockStart: true, for the first line) from the
paragraph-continuation sense (atBlockStart: false, for every line
after it), since almost every construct interruptsSetextParagraph
checks behaves identically in both senses. An ordered-list marker
NOT starting at 1 is the one CommonMark paragraph-interruption
exception that genuinely diverges between the two: it counts as a
real block start unconditionally, but cannot interrupt an already-open
paragraph. The same line, "2. foo", is refused as an entire
break-free heading (genuine block start) but absorbed safely as a
heading's own second line (paragraph continuation) -- confirmed by
manually swapping each call's own boolean argument and watching the
matching test fail.
unkillable constructs-undefined guard

findRunConstructFault already checks constructs === undefined as
its own first line and returns undefined immediately, so the outer
block.constructs !== undefined check tested exactly the same fact a
second time before ever calling it.
…a paragraph

emitItemCanInterrupt own non-construct fallback (a non-paragraph block always interrupts,
regardless of canInterruptOpenParagraph) had no test forcing that specific branch: every
prior list-continuation test used either a plain paragraph or a materialised division
construct as the interrupting block, neither of which reaches this fallback.

A link construct wrapping more than one child (so its image-shortcut mint condition does
not apply) falls through to transparent rendering, so its own first child, a non-paragraph
image block, is exactly what this fallback answers for when the construct shares the
preceding open paragraph list itemId.
…terrupting a paragraph

emitItemCanInterrupt own construct-recursion base case, first === undefined, had no test
forcing it: every prior test constructing a nested construct gave it at least one child, so
recursion always bottomed out through the non-construct branch instead of this one.

An empty, non-division nested construct (an anchor with zero children) triggers the base
case directly, and its own outer construct is only absorbed into the list item run through
a LATER sibling paragraph carrying the item id, not through this empty first child.
…x text

firstBlockCheckboxs final two branches each hardcoded a separate stripGlyph:
false literal for the not-found case, but stripCheckboxRun (the only
consumer of that flag) already re-checks the identical two glyph prefixes
itself and no-ops when neither matches. That makes stripGlyph unobservable
whenever no glyph is found: manually flipping the literal to true left the
full suite passing unchanged.

Collapsing the two returns into one expression, with stripGlyph derived
from whether checkboxText itself came back non-empty, removes the dead
literal instead of asserting it separately from a fact stripCheckboxRun
already establishes on its own.
…Requested itself

The unsafe-diagnostic branch (setextRequested && level <= MAX_SETEXT_LEVEL &&
unsafeForSetext) had no test isolating its first conjunct: every existing test
for a break-free, hazard-carrying heading also set headingStyle: setext,
so setextRequested was always true whenever unsafeForSetext was.

A break-free, 4+-column-indented level-1 heading with the default (atx)
headingStyle now proves the branch is skipped when setext was never
requested at all, even though the same text is independently unsafe.
…kCheckbox itself

The prior stripGlyph boolean was still unobservable at its own new call site: a
ConditionalExpression mutant on checkboxText !== "" survived, because
stripCheckboxRun independently re-checks the identical glyph prefixes and
no-ops whenever none match, so the caller-supplied flag never actually
changes what gets rendered when no glyph was found.

firstBlockCheckbox now does the stripping itself, in the same branch that
already found the glyph, and returns the already-stripped paragraph (or
undefined when nothing needs stripping) instead of a flag for
listRegionItemBody to act on later. Only one place ever decides whether
stripping applies, so there is no second, redundant boolean left over for a
mutation to hide behind.
The unsafe-diagnostic branch condition has three conjuncts, and level <=
MAX_SETEXT_LEVEL had no test isolating it from the other two: every prior
break-free-hazard test used a level 1 or 2 heading, so the level check was
always trivially true alongside setextRequested and unsafeForSetext.

A level-3 heading with headingStyle: setext requested AND a genuine
leading-indentation hazard now proves the branch is still skipped once
level exceeds setexts own two-level ceiling, even though the other two
conjuncts hold.
…th one helper

String.prototype.split never returns an empty array for any input, even the
empty string, so a split result own first element is always genuinely
present. noUncheckedIndexedAccess still forced a dead default at every call
site indexing or destructuring one, and each of those three defaults was
unreachable code with no way for a real test to ever observe a difference
if mutated.

splitLines centralises the one non-null assertion this invariant actually
needs into a single, clearly justified place, returning a non-empty tuple
type so every call site gets its own first line without a fallback that
could never fire.
…rectly

leadingIndentColumns own tab-stop arithmetic (MARKDOWN_TAB_STOP_WIDTH -
column % MARKDOWN_TAB_STOP_WIDTH) had a "-" survive as an unkillable
mutation to "+": its one caller only ever checks the result against
CODE_INDENT_COLUMNS, and since that threshold is never greater than the
tab-stop width, a tab encountered anywhere before the threshold is
otherwise reached by spaces alone always pushes the running column to at
least the threshold under either operator. Manually applying the mutation
and rerunning the full suite confirmed nothing distinguishes the two.

leadingIndentReachesCodeThreshold returns the boundary question its sole
caller actually asks, short-circuiting on the first tab (which alone is
always sufficient) rather than computing an exact column count nothing
downstream consumes past that point.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant