Last updated on

A Registry That Can't Drift


Part 4 of 4 in the series Architecture Review: Blog Surface Deepening Opportunities. Full series ↓

Last in this series on running Matt Pocock's improve-codebase-architecture skill (see the first post in the series) against this blog's codebase: a bug class that's easy to miss because, on the surface, the type system already looked like it was handling it.

Posts on this blog support a handful of custom content blocks beyond plain text: a syntax-highlighted code snippet, a terminal transcript, a Mermaid diagram. Registering one of these blocks turned out to require updating four separate places: the block's registration in the rich-text editor, the function that renders it on the live site, a second, independent renderer used by the admin's content-diff view, and the script that maps a Markdown code fence's language to the right block type.

Every one of those four places listed the same three block types (code snippet, terminal, Mermaid) as its own independent literal. Nothing tied them together. The library this blog's editor is built on types its block-rendering map as partial, meaning it's entirely legal, as far as the compiler is concerned, to register a block and simply forget to add a renderer for it anywhere. That's not hypothetical. It's exactly the kind of gap that produces a block which edits fine in the admin and renders as nothing at all on the live page, discovered only by opening the post.

Flowchart showing four files — postContentEditorFeatures.ts, jsxConverters.tsx, PostContentDiff.tsx, and markdownToPost.ts — each independently listing the same three block types (code snippet, terminal, Mermaid), highlighted in red to show the duplication that let them drift out of sync.

The fix: one file exporting the three block types as a single typed list, and a compile-time exhaustiveness check at each of the four sites, so that adding a fourth block type without updating everywhere fails the type check instead of failing silently the first time someone actually uses it. I proved this worked the only way that actually proves anything: temporarily added a fake fourth block type to just the shared list, watched all four sites fail to compile exactly as intended, and reverted.

Flowchart showing the fix: a single POST_CONTENT_BLOCK_TYPES list that all four files — postContentEditorFeatures.ts, jsxConverters.tsx, PostContentDiff.tsx, and markdownToPost.ts — derive from, so they can no longer list different block types.

The exhaustiveness check that wasn't

That would have been the whole story, except the same adversarial-review pass that's caught something real in every post in this series found one more layer under it. The new check verified that each of the four sites had an entry for every block name, but it never verified that the value attached to each name was the right one. Each block's own definition was typed as a generic "some block," which meant its slug field, the string used at runtime to match content to a renderer, had quietly widened to "any string." Two block definitions could have been swapped under each other's names, and the check I'd just finished proving worked would have stayed green the entire time. The compiler would confirm "yes, all three names are present" while having no opinion at all about whether one name pointed at the right renderer.

The actual fix was narrower than it sounds: pin each block definition's slug to its own literal string instead of the general string type, and tighten the exhaustiveness check to bind each name to a block carrying that exact slug, not just a block of the right general shape. I reproduced the reviewer's exact scenario, swapping one block's registration for another's, confirmed it now failed to compile, and reverted.

Four candidates, four real fixes, and in three of the four cases, the finding that actually mattered showed up on the second pass of review, not the first. That's less a knock on the skill that found these in the first place, and more the actual argument for running two passes at all: the first one is good at finding what's shallow. It takes a second, deliberately adversarial one to find what the first fix quietly assumed.

Comments

No comments yet. Be the first to comment.

Leave a comment