On this page

Why the diff is always correct, but not always pretty

Wymark’s diff makes one promise – it is always correct, and it is always on – and deliberately does not make a second: that it tells the most flattering story about how your document evolved.

What you can rely on

The diff describes exactly how your file differs from your baseline, and accept and reject do exactly what they show. Accepting a hunk acknowledges precisely those lines; rejecting a hunk restores precisely the baseline’s bytes for them. There is no heuristic between your decision and the file.

Under the hood it is a line-based comparison, the same family as git diff, with word-level highlighting layered on top as presentation. Inside code blocks the diff stays line by line; in prose, replaced lines get word-level highlights so you can see what actually changed.

Here is a small edit shown exactly as Wymark’s Source view renders it – a word changed, a line added, read-only and live (this is the editor itself, not a screenshot):

Accept or reject the changes — the formatted result updates below.
Source
# Release roadmap

This page tracks the launch.

We ship the **beta** to customers on Thursday.

Steps before launch:

- Run the _entire_ test suite
- Email the team

> Note: the changelog must be `current and signed`.
Formatted

Release roadmap

This page tracks the launch.

We ship the beta to customers on Thursday.

Steps before launch:

  • Run the entire test suite
  • Email the team

Note: the changelog must be current and signed.

The Formatted view reviews the same changes with cards in the margin; try a review to see both views side by side.

What it costs

Three things will look coarser than you might want, and none of them is a bug:

  • A paragraph moved and edited reads as delete + insert. Cut a paragraph, paste it elsewhere and change a word on the way, and the diff shows a removal and an addition rather than one “moved” annotation. Only a byte-identical, unedited move earns the “Moved up” or “Moved down” card with its linked accept and reject; anything fuzzier is not called a move.
  • Adjacent changes can lump. Edits on neighbouring lines merge into one hunk, so a single accept or reject sometimes covers slightly more than you would like to decide at once.
  • Hard-wrapped paragraphs review line by line. If your source wraps paragraphs across physical lines, one prose edit can surface as several line hunks.

Two kinds of change are deliberately not shown, because they mean nothing: a paragraph re-wrapped at a different column, or the same markup spelled another way, renders identically and never reaches your review queue. And adding or resolving a comment edits the file, but the diff you review shows prose changes, not marker bookkeeping.

If you have a document where the diff told the story badly, we genuinely want to see it – real cases are how the engine grows. The constraint it will grow under is the one above: correct first, pretty when provable.

Why the engine declines to guess

Diff engines that produce prettier output – “this paragraph moved”, “these two merged”, “this was edited and relocated” – do it with similarity heuristics: fuzzy matching that pairs up blocks which look alike. Heuristics are wrong on real documents some fraction of the time. In a code review tool, a mispaired diff is a cosmetic annoyance. In Wymark, accept and reject act on the file. A diff that misdescribes a change is a button that does something other than what it says.

So the engine is built on a deliberately boring foundation, and what you see is derived from byte-level truth, not from a model’s guess about your intent. Each of the three costs above is the engine declining to guess. When we have experimented with cleverer pairing, the failure mode was always the same: a confident-looking diff that was subtly wrong about which text corresponds to which – exactly the failure an accept/reject tool cannot afford.

Why "always on" raises the bar

A mode you enter (like track changes) can afford occasional weirdness – you opted in, you are watching for it. Wymark’s diff runs on every keystroke and every external write, on the editing path of the application. That raises the bar twice over:

  • It has to be fast – the engine runs in a few milliseconds on every keystroke, so it never gets to “think hard” about a prettier pairing.
  • It has to be trustworthy by default – you did not opt into a review session; the diff appeared because the file changed. A false story, even rarely, would teach you to distrust the one surface that is supposed to be load-bearing.
Where the cleverness actually goes

The simplicity is in the pairing, not in the experience. Two refinements are worth naming:

  • Render-equivalent changes are suppressed. If a change produces byte-different markdown that renders identically – a reflowed paragraph, an equivalent way of spelling the same markup – it is not shown as a diff. The oracle is a real markdown parser comparing rendered structure, not a regex guessing at “whitespace only”. Your review queue contains things that mean something.
  • Comment markers are stripped from the prose diff. Adding or resolving a comment edits the file, but comment activity is not writing – so the diff you review shows prose changes, not marker bookkeeping.

Both refinements share the correctness property: they only ever remove noise that is provably meaningless, never reinterpret a change.

A review tool’s diff is an instrument, not an illustration. We would rather show you a structurally coarse truth than a polished guess, because every hunk has two buttons on it and those buttons edit your document. If a future heuristic can be made provably safe – exact-match move detection is the model: promoted only when the evidence is unambiguous – it earns its way in. Anything less stays out, no matter how nice the demo looks.

Why one dialect, and why it is not a preference

Markdown parsing is dialect-sensitive: | a | b | is a table with tables on and a paragraph with them off; - [ ] x is a task item or a bullet that begins with [ ]. Every part of Wymark – the diff engine, the render-equivalence check that hides cosmetic changes, the outline, the exporters, the comment scanner – is answering a question about the same document, so they must all see the same tree. The dialect is therefore one constant, and none of it is a setting: a file that meant two different things under two preferences would be a file whose diff could not be trusted.

The inline-maths setting is the apparent exception and is not one. It gates what the formatted view renders and lets you type, never how the file parses; the diff and the exports read $…$ as maths regardless.

Why footnote definitions stay where you wrote them

Most renderers collect every footnote definition and print them at the end. Wymark’s parser is pinned to leave definitions at their source position, because every other part of the app maps parsed nodes back to bytes in the file: a definition detached from its position, or dropped because nothing references it, would be a stretch of the file the engine could not account for. The formatted view therefore shows a definition where it sits, and lets you move it with ⌥↑ and ⌥↓ like any block.


# Why the diff is always correct, but not always pretty

Wymark's diff makes one promise -- **it is always correct, and it is always on** -- and deliberately does not make a second: that it tells the most flattering story about how your document evolved.

## What you can rely on

The diff describes exactly how your file differs from your baseline, and accept and reject do exactly what they show. Accepting a hunk acknowledges precisely those lines; rejecting a hunk restores precisely the baseline's bytes for them. There is no heuristic between your decision and the file.

Under the hood it is a line-based comparison, the same family as `git diff`, with word-level highlighting layered on top as presentation. Inside code blocks the diff stays line by line; in prose, replaced lines get word-level highlights so you can see what actually changed.

Here is a small edit shown exactly as Wymark's Source view renders it -- a word changed, a line added, read-only and live (this is the editor itself, not a screenshot):

[a release checklist edit](demo:release-checklist)

The Formatted view reviews the same changes with cards in the margin; [try a review](/docs/concepts/try-a-review) to see both views side by side.

## What it costs

Three things will look coarser than you might want, and none of them is a bug:

- **A paragraph moved *and* edited reads as delete + insert.** Cut a paragraph, paste it elsewhere and change a word on the way, and the diff shows a removal and an addition rather than one "moved" annotation. Only a byte-identical, unedited move earns the "Moved up" or "Moved down" card with its linked accept and reject; anything fuzzier is not called a move.
- **Adjacent changes can lump.** Edits on neighbouring lines merge into one hunk, so a single accept or reject sometimes covers slightly more than you would like to decide at once.
- **Hard-wrapped paragraphs review line by line.** If your source wraps paragraphs across physical lines, one prose edit can surface as several line hunks.

Two kinds of change are deliberately *not* shown, because they mean nothing: a paragraph re-wrapped at a different column, or the same markup spelled another way, renders identically and never reaches your review queue. And adding or resolving a [comment](/docs/concepts/comments) edits the file, but the diff you review shows prose changes, not marker bookkeeping.

If you have a document where the diff told the story badly, we genuinely want to see it -- real cases are how the engine grows. The constraint it will grow under is the one above: correct first, pretty when provable.

<details>
<summary>Why the engine declines to guess</summary>

Diff engines that produce *prettier* output -- "this paragraph moved", "these two merged", "this was edited *and* relocated" -- do it with similarity heuristics: fuzzy matching that pairs up blocks which look alike. Heuristics are wrong on real documents some fraction of the time. In a code review tool, a mispaired diff is a cosmetic annoyance. In Wymark, accept and reject *act on the file*. A diff that misdescribes a change is a button that does something other than what it says.

So the engine is built on a deliberately boring foundation, and what you see is derived from byte-level truth, not from a model's guess about your intent. Each of the three costs above is the engine declining to guess. When we have experimented with cleverer pairing, the failure mode was always the same: a confident-looking diff that was subtly wrong about *which* text corresponds to which -- exactly the failure an accept/reject tool cannot afford.

</details>

<details>
<summary>Why "always on" raises the bar</summary>

A mode you enter (like track changes) can afford occasional weirdness -- you opted in, you are watching for it. Wymark's diff runs on every keystroke and every external write, on the editing path of the application. That raises the bar twice over:

- It has to be **fast** -- the engine runs in a few milliseconds on every keystroke, so it never gets to "think hard" about a prettier pairing.
- It has to be **trustworthy by default** -- you did not opt into a review session; the diff appeared because the file changed. A false story, even rarely, would teach you to distrust the one surface that is supposed to be load-bearing.

</details>

<details>
<summary>Where the cleverness actually goes</summary>

The simplicity is in the pairing, not in the experience. Two refinements are worth naming:

- **Render-equivalent changes are suppressed.** If a change produces byte-different markdown that renders identically -- a reflowed paragraph, an equivalent way of spelling the same markup -- it is not shown as a diff. The oracle is a real markdown parser comparing rendered structure, not a regex guessing at "whitespace only". Your review queue contains things that *mean* something.
- **Comment markers are stripped from the prose diff.** Adding or resolving a comment edits the file, but comment activity is not writing -- so the diff you review shows prose changes, not marker bookkeeping.

Both refinements share the correctness property: they only ever *remove* noise that is provably meaningless, never reinterpret a change.

A review tool's diff is an instrument, not an illustration. We would rather show you a structurally coarse truth than a polished guess, because every hunk has two buttons on it and those buttons edit your document. If a future heuristic can be made provably safe -- exact-match move detection is the model: promoted only when the evidence is unambiguous -- it earns its way in. Anything less stays out, no matter how nice the demo looks.

</details>

<details>
<summary>Why one dialect, and why it is not a preference</summary>

Markdown parsing is dialect-sensitive: `| a | b |` is a table with tables on and a paragraph with them off; `- [ ] x` is a task item or a bullet that begins with `[ ]`. Every part of Wymark -- the diff engine, the render-equivalence check that hides cosmetic changes, the outline, the exporters, the comment scanner -- is answering a question about the same document, so they must all see the same tree. The dialect is therefore one constant, and none of it is a setting: a file that meant two different things under two preferences would be a file whose diff could not be trusted.

The inline-maths setting is the apparent exception and is not one. It gates what the formatted view *renders and lets you type*, never how the file parses; the diff and the exports read `$…$` as maths regardless.

</details>

<details>
<summary>Why footnote definitions stay where you wrote them</summary>

Most renderers collect every footnote definition and print them at the end. Wymark's parser is pinned to leave definitions at their source position, because every other part of the app maps parsed nodes back to bytes in the file: a definition detached from its position, or dropped because nothing references it, would be a stretch of the file the engine could not account for. The formatted view therefore shows a definition where it sits, and lets you move it with `⌥↑` and `⌥↓` like any block.

</details>