← All posts

The day the note format changed

Thirteen device builds, five backend deploys, and a bug class that kept coming back until markdown became the one canonical body every note is written from.

Today doesn't have one headline feature. It has a through-line, and it's the most expensive lesson this project has taught me so far: two representations of one thing will drift. Every note bug reported today — and there were several, reported independently, looking like unrelated problems — turned out to be the same root cause wearing different clothes.

How it actually broke

A note's content lived in two places at once: blocks, which the editor reads and writes, and extracted_text, which the list preview, search, the embedder, and the assistant all read instead. Nothing forced those two to agree. When the assistant edited a note, it wrote the new extracted_text and left blocks untouched — so the list preview showed the new content while the editor still showed the old, stale version. Worse: that stale editor copy was exactly what would get saved back the next time you opened and closed the note, silently undoing the assistant's edit the moment you looked at it.

Then a second bug compounded the first: a redo path had an "unchanged" guard that only compared the text field, so a note that had already drifted this way could never repair itself through normal use. It would just keep re-diverging.

The fix that actually holds

The instinct is to patch the specific drift — make the assistant write both fields, add a stronger unchanged check. I did those too, in escalating order. But the fix that removes the whole class of bug rather than one instance of it: markdown becomes the one canonical, authored column. blocks (what the editor renders) and extracted_text (what list, search, the embedder, and the assistant read) are now both recomputed from that single markdown body by a pure function, on every save. There's exactly one writer. Nothing can disagree with itself, because there's only one thing to agree with.

This only applies to notes. Vault documents stay blocks-canonical on purpose — that editor has a color picker and a font panel, and markdown has no way to carry a run's color or a paragraph's exact spacing. Forcing every document into one format regardless of what it actually needs would just move the drift somewhere else. The migration itself is lazy: a note with no markdown yet reads exactly as it always did, and the first save gives it a canonical body. A dry-run tool names, in advance, every note that would lose something in the conversion — run colors, sizes, paragraph alignment, an image's display size — so nothing gets silently flattened.

Twenty thousand documents, seven bugs nobody would have written by hand

Before trusting the new parser with real notes, I ran a seeded property test across the whole markdown vocabulary — twenty thousand generated documents, checked clean. It found seven parser bugs no one would have caught by hand-testing: a checklist item written as - [x] done parsing as an ordinary bullet, meaning checklists could never actually survive a save; and an escaped \* being miscounted as half of a **, which lost a run's italics differently on every single save depending on what surrounded it. Neither of those would show up in a quick manual test. They only show up when you throw enough generated inputs at the thing to hit the corners nobody thinks to check.

Two fixes that shipped and did nothing

Not everything today was a clean win. Two separate fixes — an arming-offset fix and a double-tap fix, both for the same underlying interaction — compiled, passed their tests, and changed nothing at all on a real device. The cause: @State written inside a gesture closure is invisible to that gesture's own later callbacks, because those closures belong to the body pass that originally attached the gesture recognizer, not to whatever redraws afterward. I was told about the same symptom three times before the actual cause was found. It's in the house rules now, in writing, specifically so it doesn't cost a fourth report.

What else the day turned up

Eight builder agents worked across the day in their own isolated worktrees, and between them surfaced a handful of things that had nothing to do with notes at all: an image cache that wrote the previous account's photo into a just-cleared cache slot, because cancellation was cooperative and nothing actually checked the flag; a 12MB PDF that had been silently killing the upload socket since attachments first existed; a tool-visibility check that was stricter than the actual access gate behind it; and a stale comment in the build config claiming the Mac test bundle didn't compile — false the day it was written, and sitting there long enough to send three different people looking for their Mac tests in the wrong place.

Thirteen builds, five deploys, about a hundred and thirty commits, and one sentence that covers most of it: if the same fact lives in two places, eventually those two places will disagree, and the fix is never "remember to keep them in sync." It's "give the fact exactly one home."

← Newer: The cut, and everything it changed Older: One machine, and the app under a stopwatch →