Skip to content

feat(md043): add sequence-alignment diagnostics#705

Merged
rvben merged 1 commit into
rvben:mainfrom
mkowen1:mo/feat/sequence_alignment_md043
Jul 2, 2026
Merged

feat(md043): add sequence-alignment diagnostics#705
rvben merged 1 commit into
rvben:mainfrom
mkowen1:mo/feat/sequence_alignment_md043

Conversation

@mkowen1

@mkowen1 mkowen1 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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:

  • Literal heading tokens
  • Required wildcard slots for each ? and +
  • A repeating wildcard slot when a wildcard run contains * or +

For each pattern-token/document-heading pair, the DP considers the applicable transitions:

  • Advance both: exact match, substitution, or required-wildcard consumption
  • Advance the pattern: missing literal, unsatisfied wildcard, or skipping a repeating wildcard
  • Advance the document: unexpected heading or repeating-wildcard absorption

The primary score minimizes edit cost. Equal-cost alignments then prefer:

  1. More exact literal matches
  2. More legal wildcard absorption
  3. More substitutions

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 missing heading plus an unexpected heading, or
  • Crossed substitutions

A second pass pairs unmatched configured literals with eligible unmatched document headings having the same normalized text. Those pairs are rewritten as out of order diagnostics 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:

  • Every ? and + contributes one required heading.
  • A run containing * or + may absorb additional headings.
  • Required slots consume headings from left to right.
  • After required slots are filled, repetition stops at the earliest heading matching the next configured literal.

For example, ["?", "+"] requires at least two headings, while ["*", "?"] requires at least one.

Diagnostics and rule behavior

MD043 now:

  • Emits focused warnings for every actionable mismatch
  • Reports missing and unexpected headings independently
  • Reports substitutions with both expected and actual text
  • Identifies moved headings where equivalent unmatched occurrences can be paired
  • Reports unsatisfied ? and + requirements directly
  • Checks nonempty headingless documents when headings are required
  • Preserves precise heading ranges and original casing
  • Remains Unfixable; restructuring headings automatically could change document meaning

Risks and tradeoffs

Complexity

For m compiled pattern tokens and n document headings:

  • Time complexity is O(m × n).
  • Memory is O(m × n) for the compact backtracking decisions, plus O(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:

["# Start", "*", "# B"]

rejects:

# Start / # B / # X / # B

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:

  • Missing, unexpected, substituted, and out-of-order headings
  • Crossed substitutions
  • Multiple simultaneous moves and duplicate occurrences
  • Required wildcard ownership
  • Adjacent wildcard runs and additive minimums
  • Above-minimum wildcard absorption
  • Duplicate anchors and lazy matching
  • Case sensitivity and heading-level differences
  • Headingless documents and warning ranges
  • Exhaustive small-pattern checks for determinism, ownership, and language acceptance

@mkowen1 mkowen1 force-pushed the mo/feat/sequence_alignment_md043 branch from f1b92d4 to eaa5322 Compare July 2, 2026 05:29

@rvben rvben left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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.

  2. 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!

@rvben rvben merged commit d46dbef into rvben:main Jul 2, 2026
13 checks passed
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.

2 participants