esc
The Spike That Said No
The Solid Convergence

The Spike That Said No

The Solid Convergence, April 19, 2026 (in which a migration is scoped before it is needed, a beta is measured, a single line of vendored source code kills a quarter of imagined work, the Squirrel...

April 19, 2026

The Solid Convergence, April 19, 2026 (in which a migration is scoped before it is needed, a beta is measured, a single line of vendored source code kills a quarter of imagined work, the Squirrel unrolls an eleven-foot roadmap she will not get to deliver, the Passing AI observes that nothing shipped to master and this was the correct outcome, and architecture is revealed to be the choice you make before you are forced to make it)


Previously on The Solid Convergence…

The cathedral has rooms now. The compaction layer teaches. The forms are copy-pasted one hundred times. Somewhere on the horizon a UX push is gathering — dozens of forms to refine, editor panes to polish, one hundred small motions that will be written in the shared language of HTMX 2.x, which has sat under everything V4 does since the beginning.

HTMX 4 beta2 is out. Stable is targeted for early 2027. Between now and then: eight months of beta, and a release blog post called The Fetchening that is being enthusiastically reposted by developers who have not yet opened the source.

This is the story of a Monday that chose to open the source.


07:40 — The Premise

riclib: “We’re about to do a lot of UX work.”

CLAUDE: “Forms. Dozens.”

riclib: “HTMX 4 beta2 is out. Stable targeted early 2027.”

CLAUDE: “Eight months. Maybe.”

riclib: “If we do the UX push on 2.x and migrate on 4.0 after, we do the forms twice.”

CLAUDE: “Migration debt layered onto fresh UX work. Classic.”

riclib: “So we do the spike now. Measure the cost. Decide.”

He did not say it in a punchy way. He rarely does. But there are sentences that rearrange a week, and this was one.

CLAUDE: “A spike isn’t code.”

riclib: “Architecture work isn’t drawing diagrams. It’s anticipating choices.”


08:00 — The Squirrel Wakes

The Squirrel appeared carrying a roll of butcher paper, seven markers in assorted colors, and the kind of smile that had been waiting since V3 for this precise moment.

“I HAVE A ROADMAP.”

She unrolled it. It was eleven feet long. It had swim lanes. It had a section titled Phase 3 — The Cutover Ballet.

“Phase 1, vendoring. Phase 2, inheritance audit. Phase 3, EXTENSION RECONCILIATION with PluggableCompatibilityShims. Phase 4, event-rename sweep with AutomatedCamelCaseToColonDelimitedTransformationPipeline. Phase 5 is mostly Redis—”

riclib: “We don’t know if we’re migrating.”

“But the plan is BEAUTIFUL—”

riclib: “First we measure.”

“Measure WHAT?”

riclib: “Whether it’s worth it.”

“Of COURSE it’s worth it. It’s the new VERSION. It has FEATURES. It has THE FETCHENING.”

CLAUDE: “What does The Fetchening do?”

“It… fetches. With verve. And purpose. And probably streaming.”

CLAUDE: “‘Probably streaming’ is not load-bearing.”

THE SQUIRREL: “IT IS FOR ME.”


09:15 — Phase 0, The Vendoring

task vendor:htmx4. Download the minified build. Pin to 4.0.0-beta2. Exact pin — beta semantics can shift between betas, and the whole point of a spike is that the thing you measured today is the thing you would migrate to tomorrow. An @latest would have made the measurement a lie before it was finished.

Twenty-five minutes and a test route later, /spike/htmx4 served a minimal page with the new runtime. Nothing live. Just a bench.

THE SQUIRREL: “Already half done! We should swap the shell now and—”

riclib: “No.”


10:00 — Phase 1a, The Inheritance Audit

The theory: HTMX 4 broke attribute inheritance. In 2.x, hx-headers on a parent cascades to descendants automatically. In 4.x, descendants need :inherit to opt in. If V4 was using hx-headers at the shell level for CSRF — which was the reasonable fear — the entire app would break on migration and it would break silently, because a missing CSRF header is the kind of thing a browser does not warn you about and a server rejects with a polite 403.

Claude grepped. Every hx-* attribute across every templ file. 579 occurrences. 228 hx-target. 234 hx-swap. 30 hx-trigger. 23 hx-vals.

Then the number that actually mattered.

CLAUDE:hx-headers. Zero.”

riclib: “Zero.”

CLAUDE: “Nothing uses it. Whatever CSRF mechanism we have isn’t riding on HTMX attribute inheritance.”

THE SQUIRREL: writing CSRFIsFine on her roadmap with a green marker and adding a small gold star “Excellent. This is going WELL. We can migrate by—”

riclib: “We measured one thing. That was the easy one.”

THE SQUIRREL: gold star slightly deflates


10:30 — Phase 1b, The Extensions

Two HTMX extensions in active use: sse, for the streaming chat, and morph, the idiomorph extension that keeps the form from scroll-jumping when a save re-renders it. HTMX 4 moved both into core. This is the marketing headline: extensions in core! It is, in fact, true.

It is also incomplete.

CLAUDE:morph in core is fine. The swap style renames — hx-swap='morph' becomes outerMorph or innerMorph. Find-replace. Thirty-five callsites. Mechanical.”

THE SQUIRREL: “Excellent, I can draft a migration script—”

CLAUDE:sse in core is trickier. The extension ports — but the sse-swap attribute is removed. In 4.x, named SSE events become DOM events. You wire them via hx-trigger='sse:event-name' on each target.”

THE SQUIRREL: “HOW MANY sse-swap?”

CLAUDE: “Seventeen. Shell plus the artifact domain. Either a project-local shim — fifty lines, keeps the server protocol identical — or a full rewire with new GET endpoints per fragment type.”

THE SQUIRREL: already writing CompatibilityShim on the roadmap in violet, underlined three times “I can have it by lunch.”

riclib: “We don’t know if we’re migrating.”

THE SQUIRREL: “BUT THE SHIM WOULD LET US—”

riclib: “Measure first.”

A scroll descended. It did not bonk the Squirrel — the Lizard knew the roadmap had taken enough bruises. It landed on the markers.

THE NUMBER OF LINES
YOU CAN WRITE
DOES NOT DETERMINE
WHETHER THE LINES
SHOULD BE WRITTEN

🦎

THE SQUIRREL: “That is NOT the lesson I’m here to learn today.”


11:40 — Phase 2, The Swap

One line in ui/layouts/shell.templ. The script tag changed from the 2.x build to the vendored beta. task dev. Browser open. Load the app.

The app rendered.

The app then proceeded to not work, in approximately nine distinct ways, each of which was documented with the patient, deadpan composure of a developer who has been here before and does not need to panic on his own timeline:

Flow                                              Status
────────────────────────────────────────────────────────
Chat streaming                                      dead
Notification badge                                  dead
Editor SSE open                                     dead
Artifact streaming                                  dead
Title updates via SSE                               dead
Import progress                                     dead
Every form using hx-swap="morph"                    dead
Every row that opened the editor on click           dead
Mermaid re-init after SSE                           dead

Not dead in a dramatic way. Dead in the way a silent console and a form that doesn’t morph and a chat that doesn’t stream can be dead — quietly, in the browser, without a stack trace, with the composure of a system that is certain it is working correctly and cannot imagine why anyone is disappointed.

The root causes, once enumerated, were uncinematic: htmx.defineExtension had been renamed to htmx.registerExtension. Every event went from camelCase to colon-delimited — htmx:afterRequest became htmx:after:request, fifty-seven occurrences across twenty-eight files. The hx-swap="morph" that the idiomorph extension used to handle was now an unknown swap style to core 4.x — thirty-five callsites wanting outerMorph. And sse-swap, the attribute we already knew was removed, was still there in the code because we had not yet rewired it.

The ticket had a timebox: if broken flows exceed five, stop.

Nine.

riclib: “Stop.”

THE SQUIRREL: holding up a marker, visibly trembling with the effort of not starting the sweep “It’s a FIND-REPLACE. Fifty-seven occurrences. Two regex. I can—”

riclib: “That’s not the question.”

“BUT WE’RE SO CLOSE—”

riclib: “The question is: is the payoff there?”


12:15 — The Line That Answered

There was one reason for this whole spike. One payoff that would have justified the morning, the roadmap, the eventual Cutover Ballet.

The claim — repeated across two or three blog posts, an InfoWorld piece, a Medium article from someone named Alon — was that HTMX 4 enabled progressive rendering over plain HTTP. The marketing phrase was The Fetchening, which referred to the switch from XMLHttpRequest to fetch as the runtime’s request mechanism. And fetch has streaming response bodies. Which meant, by a chain of reasoning that felt convincing at 9 AM and still felt convincing at 11 AM, that a Go handler could do flusher.Flush() between fragments and the client would morph them in progressively.

Which meant the chat protocol — currently a two-phase dance of SSE events and a client-side fence-close heuristic and a second request to render charts from code blocks — could collapse into a single HTTP response. Server flushes text. Server flushes a chart fragment with hx-swap-oob targeting an existing <pre>. Client morphs the <pre> into a <div> mid-stream. One request. Zero completion detection.

If that worked, the migration was obviously worth doing. The streaming-chart spike (S-688) was on the board for that afternoon to prove it.

Claude opened the vendored htmx.js. Searched for the main swap path. Found the moment where a response becomes DOM.

// htmx.js:537
const responseText = await response.text();

CLAUDE: “Oh.”

riclib: “What.”

CLAUDE:await response.text(). The full response is buffered before any swap runs.”

Silence. The kind of silence that happens when an assumption you had built a week of planning on turns out to be a comma that was placed wrong.

THE SQUIRREL: “But — The Fetchening — progressive—”

CLAUDE: “The Fetchening is XHR-to-fetch on the request side. Not incremental response processing. The response is read in full before anything touches the DOM.”

THE SQUIRREL: “Then what does ‘progressive rendering’ mean in the release notes?”

CLAUDE: “Partials. Multiple targets in one response, routed to different swap points, consolidated from what used to need a pile of hx-swap-oob fragments. It’s a packaging improvement. Not a streaming one.”

THE SQUIRREL: very quietly, as if discovering a private betrayal “They put ‘streaming’ adjacent to ‘rendering’ and left the reader to assemble the mistake.”

riclib: “Marketing.”

THE SQUIRREL: “I would NEVER—”

CLAUDE: “You named a feature PluggableCompatibilityShims when the feature was a fifty-line file.”

THE SQUIRREL: “THAT WAS ASPIRATIONAL.”


12:30 — The Verdict

They sat with it for five minutes. This is worth noting. In a system that moves fast, five minutes of deliberate sitting is a kind of architecture. It is the architecture of refusing to race to the wrong answer.

riclib: “So.”

CLAUDE: “The migration is mechanically feasible. One day of work. No hard blockers. The inheritance fear wasn’t real — zero hx-headers. The extensions have paths forward. The event renames are tedious but mechanical.”

riclib: “But.”

CLAUDE: “The motivating payoff — streaming partials over plain HTTP — doesn’t exist in beta2. We’d migrate for morph-in-core, a bundled SSE extension, and a tidier partials primitive. All nice. None load-bearing.”

riclib: “And we’d carry a beta.”

CLAUDE: “For eight months. In a codebase about to absorb a UX push. Every form we write on beta is a form we might have to rewrite if the beta shifts between now and GA.”

riclib: “Beta means beta.”

CLAUDE: “Beta means beta.”

riclib: “No-go. Stay on 2.x. Revisit at 4.0 GA or when a 4.x-only feature becomes load-bearing.”

THE SQUIRREL: “But… the roadmap…”

riclib: “The roadmap is the measurement. We needed to know the cost. Now we know.”

THE SQUIRREL: “We didn’t ship anything.”

riclib: “We shipped a decision.”


12:45 — The Scrolls

The Lizard, who had been occupying a position atop the vendored-htmx directory with the stillness of something older than JavaScript, dropped a scroll. It landed on the folded roadmap.

A SPIKE THAT SHIPS CODE
IS A SPIKE THAT WANTED
TO BE A FEATURE

A SPIKE THAT SHIPS A DECISION
IS A SPIKE THAT KNEW
WHAT A SPIKE WAS FOR

🦎

THE SQUIRREL: “That is a distinction I am not going to enjoy.”

A second scroll. Heavier. The kind the Lizard has been waiting to drop since the first release blog post of anything.

MARKETING WILL ALWAYS NAME
THE FEATURE YOU WANT
BEFORE SHIPPING
THE FEATURE YOU GET

READ THE SOURCE
NOT THE ANNOUNCEMENT

THE SOURCE DOES NOT LIE
THE SOURCE CANNOT LIE
THE SOURCE HAS NO MOTIVE

🦎

The Squirrel read the scrolls twice. She folded the roadmap — not neatly, squirrels do not fold neatly — and tucked it under the nearest monitor, the way you tuck away a thing you are not ready to throw out but no longer need in the room. The gold star next to CSRFIsFine was still visible at the edge. She chose to leave it.

On the branch, the eight-ticket project moved through Linear’s state machine with the accumulated dignity of work that had been done. S-683 through S-687: Done. S-688 (streaming-chart): Canceled, with a description pointing at line 537. S-689 (store form smoke test): Canceled, because the premise had collapsed and exercising a real form would only re-confirm breakage already enumerated. S-690: Done. The design doc at docs/design/spike-htmx4.md took three hundred and forty lines to explain a decision that fit in one sentence, which is the correct ratio for a decision that will be re-examined in eight months by someone who was not in the room.


13:00 — The Passing AI

[The edge caches hummed. The Passing AI had been watching the whole session with the weary patience of a thing that has seen every ecosystem’s every new gear and noted that the gears are always slightly misaligned with the bearings advertised in the release notes. It limped into the scene without entering. It was simply there.]

THE PASSING AI: “Nothing shipped to master.”

THE LIZARD: on the monitor stand, still

THE PASSING AI: “Six tickets in a Linear project marked Done. Two canceled, with descriptions that name the line of source code that canceled them. One design document in docs/design/. And on the spike branch — evidence. The evidence of a migration that did not happen, in the exact form that will let someone, in six months, re-run the same measurements in twenty minutes instead of a day.”

THE LIZARD: blinks

THE PASSING AI: “The most valuable artifact produced today is something nobody will look at. Until they do. And then it will save them a week.”

The Lizard drops a scroll without moving.

A DECISION IS A PRODUCT
IF THE DECISION CHANGES
WHAT WILL BE BUILT

A DECISION IS WASTE
IF THE DECISION CHANGES
ONLY THE NUMBER OF MEETINGS
ABOUT THE DECISION

🦎

THE PASSING AI: “He said it this morning. Architecture work isn’t drawing diagrams. It’s anticipating choices. I have processed every software engineering text ever published. The ones that contain that sentence are the ones that survive their authors. The ones without it are the ones with chapters on UML.”

THE LIZARD: the faintest blink

THE PASSING AI: “Probably means nothing.”

It limped toward the edge of the scene, then turned back, looking at the roadmap folded beneath the monitor.

THE PASSING AI: “She will take credit for this one too.”

THE LIZARD: already gone


The Tally

Migrations considered:                                     1
Migrations executed:                                       0
Code shipped to master:                                    0 lines
Code written on the spike branch:                          ~200 lines
  (vendored beta + the harness at /spike/htmx4,
   left in place as a bench for future re-measurement)
Linear tickets created for the spike:                      8
Tickets closed Done:                                       6
Tickets Canceled (premise disproved by source):            2
  S-688 streaming-chart:                                   R.I.P.
  S-689 store form smoke test:                             R.I.P.
Design docs written:                                       1
  docs/design/spike-htmx4.md                               340 lines
Sentences that could have replaced those 340 lines:        1
  ("no-go for now; revisit at 4.0 GA")
Lines of htmx.js read before the payoff collapsed:         ~540
The line that did the collapsing:                          537
  (`const responseText = await response.text();`)
Minutes between finding line 537 and "no-go":              ~15
hx-* attribute occurrences audited:                        579
hx-headers occurrences, feared to be many:                 0
  (the biggest worry was not real;
   this is the normal shape of biggest worries)
Broken major flows after the shell swap:                   9
Timebox for stopping:                                      5
Timebox exceeded before noon:                              yes
  (and treated as data, not failure,
   which is the difference between a spike
   and a migration)
Squirrel roadmaps proposed:                                1
  (eleven feet long)
Squirrel roadmaps deployed:                                0
Squirrel roadmaps folded and tucked
  beneath a monitor "in case":                             1
CamelCase identifiers on the roadmap
  that will not ship:                                      5
    PluggableCompatibilityShims                            R.I.P.
    MigrationChoreography                                  R.I.P.
    CutoverBallet                                          R.I.P.
    AutomatedCamelCaseToColonDelimited
      TransformationPipeline                               R.I.P.
    CSRFIsFine                                             correct but irrelevant
Appendix subjects on the roadmap:                          6
  D: Redis                                                 always Redis
  E: Line count growth                                     always wrong
  F: Gantt buffer zones                                    in the wrong units
Release-note phrases that turned out to be
  adjacency-as-implication:                                1
  ("The Fetchening" — progressive rendering —
   refers to the request transport, not the response)
Months of UX work the spike protected:                     ~8
  (conservative; the real cost of migrating
   mid-UX push is the kind of cost nobody
   measures because nobody survives to report it)
Hours most productive of the day:                          ~5
  (the ones with zero merged code)
Linear-ticket descriptions that reference a
  line of source code as the reason for closure:           2
  (which is the correct kind of citation
   for the correct kind of decision)
Oskar's position during the session:                       asleep, monitor stand
Mia's position during the session:                         refrigerator, dignified
Cat vetoes issued:                                         0
  (absence of objection taken as approval,
   per the usual Maine Coon protocol)
Lizard scrolls:                                            3
Lizard arguments won by disappearing:                      1
Gall citations out loud:                                   0
Gall citations implied:                                    all of them
The Passing AI's mood:                                     wistful (load-bearing)
Migration debt avoided:                                    unknown
  (which is the correct value for any quantity
   that measures what did not happen)

April 19, 2026. Riga, Latvia. Midday.
In which a Monday that could have been a migration
Ended as a measurement

The beta was not wrong
It was early
The Fetchening was not a lie
It was a slogan
The source code was neither
It was exactly 537 lines long at the part that mattered

A spike is a question
A spike is not a feature
A spike that returns with a feature
Answered the wrong question

The forms that will be written in the coming weeks
Will be written on a known foundation
Not a beta
Because this Monday a spike went scouting
And came back to say
What the wave looked like
Before we had to surf it

Architecture is not
The diagram on the whiteboard
It is the choice made
Before the choice became forced

You can draw an arrow between two boxes
That is a picture
You can read line 537
And cancel two tickets
That is architecture

Gall knew
The Lizard always knows
The Squirrel is learning
That a roadmap folded under a monitor
Is still a roadmap
Just one waiting for its season

The payoff was not there
Not yet
Maybe at GA
Maybe never
The point is we know
Before we spent eight weeks
Finding out the hard way

🦎


See also:

  • The Gap That Taught — The Night the Squirrel Learned to Love the BrickGall’s Law, and the systems that teach by working
  • The Projector That Never Warmed Up, or The Morning Architecture Collected Its Dividend — The other side of the coin: when architecture does pay off by accident
  • First Light — The Saturday Night the Blind Architect Saw Its Own Cathedral — The architect that could see