Series seed

The error from day one

Two features, and most of the day spent on things that had been broken for weeks.

5 min read

Two things I wanted on the 27th. Type @ and link to a post I’ve already written. Drop an image onto a card and have it replace that card instead of landing next to it.

Both are small. Neither took the day.

The @ part went in cleanly. Type @ or [[ and a list of your posts appears, filtered as you type, and picking one inserts a link. It opens on both prefixes because they’re different tools: [[ allows spaces, since post titles have them and the closing brackets tell it where the query ends, while @ stops at the first space and needs whitespace in front of it so an email address never triggers it.

The wikilink node already existed. It had existed since the first morning, with a parser, a serializer, an input rule and round-trip tests. All I needed was a menu in front of it.

So before building the menu I checked the other end. Write a post with [[notes/something]] in it, build the site, look at the anchor that comes out.

The build failed.

Cannot read properties of undefined (reading 'data')

I’ve quoted that error in this series before. It’s in part two , from the morning of the 19th, where I wrote it down as D6, worked around it inside the editor’s own pipeline and moved on to the next thing.

I had assumed it was a quirk in how I was calling the library. It wasn’t. @portaljs/remark-wiki-link@1.2.0 dereferences the return value of mdast-util-from-markdown v2’s exit(), and in v2 that function returns nothing. So the plugin cannot work with the parser this site installs. Not “works with a workaround”. Cannot work.

Any [[wikilink]] in any post on this site failed the build. Had done the entire time. 1.2.0 is the latest published version, so there was no upgrade to take.

And here’s the part that made me put my coffee down. The editor’s schema has had a [[ ]] input rule since day one. Typing [[my-note]] in my own CMS produced a post that broke npm run build. The only reason it had never bitten me is that nothing in src/content used the syntax, so nothing ever triggered it.

I’d built a feature into the editor that quietly created broken posts, and shipped a renderer that couldn’t render them, and neither half ever noticed the other, because I never used the thing they disagreed about.

The replacement is about sixty lines that turn [[target|alias]] into a plain mdast link. Being a plain link has a side benefit: the MDX component map already aliases a to Link.astro, so wikilinks now get the site’s internal-link tooltips without asking for them.

The image drop, and a bug with two layers

The image half went the same way.

The targeting itself was straightforward once measured. Dropping onto a card used posAtCoords, which over an atom node returns the boundary before it, so the new card landed ahead of the one you dropped on. The node views already stamp data-card-pos onto their DOM, so the fix is to hit-test that instead.

Then, while checking that an assertion was even reachable, I wrote a throwaway test against the existing behaviour: drop an image, assert the settings panel opens on the Alt field. That already worked. Or it was supposed to.

It failed on both browsers.

Dropping an image had never opened the alt-text field. Every image I’d uploaded through the editor went in with no alt text and no prompt to add any, which is a quietly awful thing for a site that has an accessibility check in its own test suite.

It had two layers, stacked.

The first: the insert passed the position it asked to insert at, but insertContentAt inside a text block splits that block, so the card lands somewhere else, and the lookup for [data-card-pos="…"] found nothing.

The second only appeared once the first was fixed. With the right position, the panel opened and the field still didn’t hold focus. Tiptap’s focus() schedules requestAnimationFrame(() => view.focus()) whenever the selection isn’t a text selection, which is always true straight after setNodeSelection. The field was being focused two microtasks in, and the editor took it back a frame later. Measured: activeElement sitting on div.tiptap.ProseMirror at 0, 100, 300, 800 and 2000ms. Waiting longer never helps, because the two things are on different clocks.

The test that proved nothing

One more, because it’s the one I’d have missed.

With both layers fixed and the spec green, I deleted the position fix to check the test would catch it.

It stayed green.

The drop in the spec was dispatched without coordinates, so posAtCoords returned null and the code fell back to the current selection, which happened to be a block boundary, where no split occurs and the old code lands in exactly the right place. The bug could not occur in the situation the test had set up. The fix could have been deleted entirely and nothing would have gone red.

Giving the drop real coordinates fixed that, and then reverting the fix failed the spec properly.

I’d written that test. I’d reviewed it. It passed, and it was worth nothing, and the only reason I know is that I broke the code on purpose to see what happened.

Where it lands

Two features. Most of a day on three things that had been broken for weeks: wikilinks that never rendered, images that never asked for alt text, and a test that pinned nothing.

[GAP: did that feel like progress, or like a tax? I have the commits but not the mood, and I’d rather ask than guess.]

What I’ll say is that none of the three were found by thinking. The wikilink one came from checking the far end of a feature before building the near end. The alt one came from testing something that already worked, on the assumption that it did. The empty test came from deliberately breaking working code.

All three are the same move, really. Assume the thing you believe is wrong, then go and find out cheaply.

Nine days, twenty one commits, and an editor I’m writing this sentence in.

seed maturity Seed — this note will change as the strip does