feat(md043): add sequence-alignment diagnostics#705
Conversation
f1b92d4 to
eaa5322
Compare
rvben
left a comment
There was a problem hiding this comment.
Thanks for this - it's a substantial improvement to MD043's diagnostics, and the quality of the work shows. The exhaustive differential tests against an independent reference implementation (with the determinism and ownership invariants) are exactly what makes a DP rewrite like this trustworthy. I extended the same harness to pattern lengths 5 and 6 during review (~512k sampled pattern/document combinations, covering interactions like two separately anchored wildcard runs) and the alignment matched the reference everywhere. Nice work.
Two notes from verification, neither blocking:
-
The PR description says the change affects diagnostics but not acceptance. That holds for the lazy-anchor case, but adjacent wildcard runs do change acceptance:
["*", "?"]against two headings and["*", "?", "# End"]with extra headings before the anchor were rejected before and are accepted now. I think the new run semantics are the right call (the old behavior, where?ended flexible matching mid-run, was more accident than design), and the updated docs describe them accurately. I'll call both this and the headingless-document change out in the changelog so users aren't surprised. -
I'll push a small cleanup commit after merging: renaming the
test_structure_mismatch_message_*tests that still reference the removed helper, clarifying "position" in the unsatisfied-wildcard message to "pattern position" (it currently reads ambiguously against the document-position wording in the unexpected-heading message), and moving the "former behavior" note from the rule docs into the changelog. Nothing that needs another round from you.
This will go out in the next patch release.
Thanks again @mkowen1!
Why
MD043 previously stopped at the first divergence between the configured and actual heading sequences, then repeated that same diagnostic across the document. This made a single missing or moved heading look like many independent failures and forced users to fix and rerun the rule repeatedly.
This change treats heading validation as a sequence-alignment problem. By finding a minimum-edit alignment, MD043 can report all actionable differences at once - missing, unexpected, substituted, unsatisfied wildcard, and out-of-order headings - with each warning attached to the most relevant location.
What changed
Sequence alignment
MD043 now uses a Wagner–Fischer-style dynamic-programming algorithm, the same family used for Levenshtein edit distance.
The configured pattern is first compiled into:
?and+*or+For each pattern-token/document-heading pair, the DP considers the applicable transitions:
The primary score minimizes edit cost. Equal-cost alignments then prefer:
A final decision priority makes fully tied alignments deterministic.
The scores are calculated with two rolling rows. A compact decision matrix is retained for backtracking, reducing the storage constant while preserving the selected alignment.
Reorder classification
Plain edit distance represents a moved heading as either:
A second pass pairs unmatched configured literals with eligible unmatched document headings having the same normalized text. Those pairs are rewritten as
out of orderdiagnostics describing the heading’s configured literal neighbors.This pass exists to improve the diagnostic, not to change validity: the DP remains responsible for the minimum-edit alignment, while the second pass classifies equivalent unmatched occurrences as moves.
Headings consumed by required
?or+slots remain owned by those slots and cannot be reused as reorder candidates. Repeating-wildcard consumption is eligible because it does not satisfy a mandatory slot.Wildcard semantics
Adjacent wildcards form one run:
?and+contributes one required heading.*or+may absorb additional headings.For example,
["?", "+"]requires at least two headings, while["*", "?"]requires at least one.Diagnostics and rule behavior
MD043 now:
?and+requirements directlyUnfixable; restructuring headings automatically could change document meaningRisks and tradeoffs
Complexity
For
mcompiled pattern tokens andndocument headings:O(m × n).O(m × n)for the compact backtracking decisions, plusO(n)for two rolling score rows.MD043 configurations are normally small, but this is still a deliberate increase from the previous linear scan. The full decision matrix is required to recover the chosen alignment and produce individual diagnostics.
Lazy repeating wildcards
Repeating wildcards retain MD043’s existing first-anchor semantics. The previous matcher committed to the first heading matching the following literal and did not backtrack. The alignment DP preserves this behavior by preventing the repeating wildcard from consuming that anchor. This PR changes the resulting diagnostics, not whether the document is accepted.
For example, the pattern:
rejects:
The wildcard stops at the first
# B; it does not backtrack to use the later occurrence as the anchor.This is less permissive than regex-style backtracking, but it provides deterministic and explainable behavior consistent with the rule’s previous wildcard matching. The DP finds the minimum edit sequence within these deliberately lazy wildcard semantics.
Required
?and+slots are not anchor-blocked: they may consume a heading whose text matches the following literal because they must satisfy a mandatory heading requirement.Reordering is a diagnostic post-pass
Reordering is inferred after alignment rather than modeled as a Damerau–Levenshtein transposition. Adjacent transpositions do not naturally represent headings moved across several positions or across wildcard regions, and would introduce additional ambiguity around warning ownership.
The post-pass keeps the core alignment conventional while allowing moved headings—including crossed substitutions and duplicate occurrences—to receive useful diagnostics.
Diagnostic output changes
Warning counts, locations, and messages intentionally change. Consumers relying on exact MD043 output may need to update snapshots, although rule-finding changes are non-breaking under rumdl’s compatibility policy.
Testing
Coverage includes: