Devlog 3: Cutting the editor in half
Pulling the editing core out of the app took one commit and three days. Making it releasable took the next three weeks and thirteen burnt tags. This covers 16 July to 9 August, SDK 0.2.13 through 0.2.34.
Six weeks of nothing, then this. The gap between 24 June and 16 July is me using the editor instead of working on it, which is the correct order of operations and also how I ended up wanting something else.
The idea: the editing core is the valuable part, and it’s welded to a Win32 application that only runs on my operating system. Text handling, selections, undo, Markdown projection, an 80-variant edit planner. None of that is Windows-specific. All of it is trapped.
The thing blocking extraction wasn’t the UI
The obvious plan is to delete the platform-specific parts and ship what’s left. That plan dies on the first thing you try to delete.
My buffer layer owned its own persistence. Edits went into a rope and into SQLite in the same call. Which is a completely reasonable design for the app from devlog 1, where the whole premise is that every keystroke is durable, and completely wrong for an embedded editor where the host owns storage and may not have a filesystem.
So the first move wasn’t extraction. It was making the engine storage-neutral: it owns buffers, selections, edit planning, undo, and revisions, and it owns nothing else. No SQLite. No filesystem. No threads. No window.
The desktop app didn’t lose durability. It stopped being the thing that has durability and became the thing that projects engine output into SQLite. Same guarantee, moved one layer out. An embedded engine simply has no such host, and works, because nothing inside ever assumed one existed.
That inversion is the whole trick. Everything else fell out of it.
It landed as one commit on 17 July, feat(embedding): add shared editor engine and desktop shell: 500 files touched, +34,540 and -9,135 lines, six milestones closed, three days for milestones 0 through 10.5, zero durability lost.
Not a commit I’d defend as good practice. It’s one architectural move that either applies everywhere or nowhere, and splitting it would have produced a series of non-compiling states.
Then CI met three operating systems
The engine came out cleanly. What followed was twelve consecutive commits on 17 and 18 July whose messages are all some variation of “stabilize hosted gates.”
Selecting a wasm-capable clang on macOS. Probing which LLVM toolchains a runner actually has. Preserving generated line endings, twice, for different files. Isolating snapshot registry assertions. Pinning a runner for pixel baselines. Normalizing Markdown golden line endings. Isolating Windows performance scheduling.
Almost none of that is about my code. It’s about the fact that a test suite which passes on one machine encodes a hundred assumptions about that machine, and every one of them is a separate discovery on somebody else’s.
Line endings alone cost multiple commits. Generated files, golden tests, and a C header, each failing differently on a checkout that normalized them differently.
Thirteen tags, twelve failures
Here’s the part I’d want to read in somebody else’s devlog, so: the SDK release pipeline took thirteen tags to produce one working release, and a pushed tag is consumed forever whether or not it published anything.
Each tag is immutable. Each failure had a genuinely different cause, which is why it took thirteen.
| Tag | Failure | What it actually was |
|---|---|---|
| 0.2.22 | Public-sync exclusions | The generated public checkout was missing files the gates needed |
| 0.2.23 | Missing test support | Packed consumers need their support code to survive the filter |
| 0.2.24 | Browser p99 gate | Exact packed artifact missed its performance budget |
| 0.2.25 | Browser timing | Wall-clock guesses instead of renderer-ready signals |
| 0.2.26 | Large-paste geometry | A real product bug in wrapped-line measurement |
| 0.2.27 | Renderer assertion | Treated as noise at first. It wasn’t. |
| 0.2.28 | Test timeout | Packed Chromium and Electron need real process time |
| 0.2.29-30 | Performance variance | Consumed two immutable tags to p99 flakiness |
| 0.2.31 | Stale pointer coordinates | Projected coordinates not recomputed after scroll |
| 0.2.32 | Missing public C consumer | Sync filter again, different file |
| 0.2.33 | Invalid CycloneDX | Attestation needs bomFormat, specVersion, serialNumber |
| 0.2.34 | Registry activation | Published a release. Registries were never configured. |
Every one of these is now a line in the release playbook. That document is mostly a list of things that went wrong once.
The through-line: a green run in the private checkout proves nothing about the public one. The public repo is generated by a sync script that intentionally excludes tests, development docs, and hooks. Three separate failures were “the sync filter dropped a file the release gates need.”
The rehearsal step exists now. You generate the public checkout and run the full dry run from it before tagging anything. Obvious in retrospect. Cost about five tags to learn.
Two that were actually interesting
0.2.33: the SBOM was invalid. GitHub’s attestation action requires CycloneDX with bomFormat, specVersion, and a serial number. Mine had the first two. Fixed with a deterministic RFC 9562 UUIDv8 serial, deterministic being the operative word because the bundle has to hash identically on a rebuild.
0.2.26: large-paste geometry. A browser perf gate failed on wrapped-line measurement that only manifested with a large paste into heavily wrapped content. That one was a real product bug the release process caught, which is the argument for having release gates at all.
What the engine bought
Four surfaces off one codebase: the native Win32 app, a browser Web Component compiled to WASM, a Python module, and a C ABI. Same edit planner, same undo, same Markdown projection.
The thing that makes it real rather than aspirational is the shared behavior corpus. Same inputs, same resulting text, same selections, same revisions, same deltas, run against every binding. When Python and the browser disagree about a block toggle, that’s a failing test today, not a support ticket in eighteen months.
Cost: 1.2 MB of WASM. Real, and I’m not going to spin it. What it buys is that the browser runs the identical planner, which is the only reason the corpus means anything.
Next: discovering that publishing a package and making it installable are different problems.