Aspirational vs mechanical: how I lost two months of Skaldborn (and got something better back)

· series: The Rebuild

In February of this year, I asked my wife Nicole to sit down at my computer and chat with one of the NPCs in Skaldborn.

I’d been building Skaldborn around a feature I called Skaldish. The idea is simple: you type whatever you want into the text box, in plain modern English, and the system translates your input into in-world fantasy dialogue before it reaches the NPC. You can be yourself. The NPC hears the medieval-fantasy version. Nobody has to write stilted thee-and-thou prose to roleplay; the world handles it.

Nicole sat down. She typed: Check out deez nuts.

Skaldish translated it to something close to Will thou assist me in searching for thyne nuts? — I no longer have the exact log, but the gist is preserved.

The NPC, helpful soul, offered to search the forest with her.

We laughed for about five minutes. I think I texted three friends. It was the moment I knew Skaldish was going to be one of the best things in the project.

In late April I deleted the system that produced this moment, along with about two months of working code, and started over.

This is the story of how that happened, and why I’d do it the same way again.

Two months in

I should set the scene. Skaldborn is a passion project. I have a day job and a family. The way the work fit into my life looked like 5am mornings and late nights, six and seven days a week, for two months. Roughly 120-hour months on top of everything else. Nicole noticed. I told myself the pace was fine because the work was working.

I was, I thought, doing it right. The architecture decisions were written down as ADRs. There were unit tests. There were component contracts that declared what each part of the system was supposed to expect from its neighbors. I’d been disciplined about clean code, layered design, separation of concerns — all the things you say in a code review. By February I had a system that compiled, ran, let you boot the game in a browser, walk around, talk to NPCs, and translate “deez nuts” into Old English.

I had what felt like a real game.

I was about to learn that I had a real game wrapped around a layer of writing that I’d never actually checked.

Death by a thousand cuts

The trouble started with combat.

I’d designed the fight system on paper for weeks before writing a line of code. I had the ADRs, the contracts, the tests. I started building. The fight loop went in. Tests passed. I felt great.

Then I’d boot the game and end up with a different character sprite each time. Which sort of made sense — I’d been iterating on the art and hadn’t cleaned out earlier sprite versions. Annoying, but cosmetic. I’d live with it.

Then I’d trigger a fight, and nothing would happen.

I’d go back to the logs. Claude would look at the error, dig in, and come back with: “Oh, found the root cause — argument was missing, enum was hardcoded.” We’d fix it. Tests still passing. I’d boot again.

A different sprite. Trigger the fight. Nothing.

Back to the logs. New error, different shape. Found the root cause — argument was missing. We’d fix that one. Tests still passing.

This kept happening. Three weeks of it. By the middle of March I was raging at my screen, questioning my life choices, wondering if starting Skaldborn had been a huge mistake, sleeping in shorter and shorter windows because the only way out of a bug seemed to be more hours.

I want to be specific about what kind of pain this was. It wasn’t a hard problem. Each individual bug was easy. Argument was missing — fix the call. Enum was hardcoded — replace it with a lookup. Each fix took ten minutes. Each fix didn’t actually fix anything, because the next fight would expose the next version of the same shape, somewhere else in the code, that nobody had thought to check.

It was death by a thousand cuts. And the worst part — the part I couldn’t admit to myself for two weeks — was that none of my unit tests, none of my contracts, none of my ADRs had caught any of it.

The thing my plans had been protecting me from was apparently nothing.

The phrase that explained everything

Somewhere in the middle of March, I started using a phrase to describe what I was looking at: aspirational vs mechanical.

Here’s what I meant.

An aspirational contract is one you wrote down. It says, “the combat system will receive a CombatCommand with these fields, and emit a CombatResolved event with these other fields.” It lives in a markdown file. It’s in version control. It looks like a contract.

A mechanical contract is one that, when violated, breaks the build. The compiler refuses to compile the violation. A validator script runs on every push and fails when the violation lands. A test, the kind that runs every commit, fails when the contract is broken. Mechanical means the only way to do the wrong thing is to also do something visible.

I had aspirational contracts everywhere in Skaldborn. Beautiful ones. Carefully reasoned. Full of words like “must” and “shall” and “the producer guarantees.” I’d worked on them for weeks before writing code.

I had mechanical contracts almost nowhere. The compiler couldn’t tell when the combat system received a CombatCommand without one of the required fields, because the type didn’t actually require the field — it was optional in the schema, defaulted to a sentinel, and the contract that said “this field is required” lived in a markdown file two directories away that the compiler had never been introduced to.

This is what was happening every time Claude debugged a fight failure. Argument was missing — meaning the contract said it was required, but the implementation accepted the call without it. Enum was hardcoded — meaning the contract said the dispatch was open to new variants, but the implementation switched over a closed set. Aspirational said one thing. Mechanical said another. The aspirational version is what I’d planned. The mechanical version is what was actually running.

The contracts I’d been so proud of were a kind of fan fiction. They described the system I wished I had. They didn’t describe the system I’d actually built.

Once I had the phrase, I couldn’t stop seeing it. Almost every part of the system I checked, the same shape was there. The architecture was real on paper and approximate in code. Everywhere.

The week the books came out

I knew I couldn’t scrap two months of work without a much better plan than the first one. So I went back to the bookshelf.

I’d had four books on architecture sitting on the shelf the whole time. Eric Evans’s Domain-Driven Design. Scott Wlaschin’s Domain Modeling Made Functional. Martin Fowler’s Refactoring. Chris Richardson’s Microservices Patterns. I’d read them. I’d cited them in conversations. I’d written ADRs that referenced them by name.

What I hadn’t done — and this is the part I was about to fix — was use them as the actual material the architecture was built against. They were on the shelf. They weren’t in the loop.

So I started pulling passages out, page by page, into my conversations with Claude. We’d be working on a piece of the design and I’d say, “OK, but Wlaschin’s chapter on making illegal states unrepresentable says this — does our shape match?” Or, “Fowler describes this exact smell — switch over a type code — and the remedy is Replace Conditional with Polymorphism. We have that smell in three places. What does it look like to do the remedy here?”

The change was immediate. With the books actively in the conversation — not as references, but as constraints — Claude would start drafting decisions that were structurally different from what we’d been doing. The closed enums became registries. The hardcoded switches became attribute-driven dispatch. The contracts started looking like the kind of thing the compiler could check, because they were grounded in a discipline (Wlaschin’s, mainly) that says the type system is the contract. If you can construct an invalid value, the contract is wrong.

One day, with the books in the loop, we did a clean design pass on combat — one iteration, with the type contracts written first and the implementation built against them — and the fight flow worked.

I’d been chasing combat bugs for three weeks before that. With the books actually shaping the design, the class of bug that had been killing me stopped showing up.

That was the proof. The architecture had to be rebuilt against the books, not adjacent to them. And the existing code, which had been built before that rule existed, was going to have to go.

A Saturday in late March

I made the call on a Saturday. I’d spent a week pulling passages, drafting new decision documents with the books as constraints, building out a plan that explicitly named what was wrong and what we were going to do about it. I went all in on the planning phase.

It wasn’t a dramatic moment. There wasn’t a dramatic moment available. I’d had hundreds of hours of conversation with Claude about this code, this design, these problems. Claude had been laying out the failure metrics for days. Pervasive isn’t a strong enough word for what we were looking at — almost every component had the aspirational/mechanical gap, and the gap was costing me real time, every day, in the form of bugs that should never have been possible.

We weren’t going to fix it incrementally. The shape of the thing was wrong. We were going to start over.

The decision was: gut the implementation, keep the contracts and the ADRs and the test suites that had survived the discipline pass, and rebuild against the new plan.

The honest version of that decision is: it wasn’t all lost. The plans were good. The plans had always been good. What was lost was the layer between the plans and the running code — the implementation that had been written before the rule “the implementation must satisfy the plan, mechanically” was in force.

The cut

I want to be careful not to dramatize the deletion itself, because for me it wasn’t dramatic. I’d done a massive amount of preparation. By the time I actually deleted, I’d already mentally said goodbye to that codebase weeks earlier; the delete commit was paperwork.

Also, git is magical. The pre-rebuild code is on an archive branch. Nothing was lost in any final sense. If I want to look at how I’d written the original combat system, I can look at it. I have not, since the day I deleted it, gone back to look. There’s nothing on the archive branch I want to copy.

The hardest thing to let go of, if I’m being honest, was nothing. I was just happy to be rid of the junk code. The thing I was attached to was the time — the two months I’d put in — not the code. The code, by the day I deleted it, was an apology for the planning I should have been doing before I wrote any of it.

I slept that night, because I always sleep. Exhaustion is a fantastic sleep aid.

A blocky figure on grass

The first four days of the rebuild produced nothing visible. I was rebuilding the substrate — the contracts that Claude could now actually use as constraints, the type designs that made illegal states unrepresentable, the new dispatch shape that didn’t have a switch in it.

On day five, I got the simulation running again, and I booted the game in a browser to see if the player could move.

There was a blocky figure on a green tile that we were calling “grass.” I pressed an arrow key. The figure walked.

I walked the figure to a tile we were calling “beach.” It walked.

That was the moment. After two months of fight bugs that wouldn’t quit, after the deletion, after a week of substrate work, I had a blocky figure walking on grass and then on the beach. It looked like a child’s drawing. It worked.

It worked because the contracts were actually being consumed by the code now. Because the bounded context boundaries had real, mechanically-enforced definitions. Because the dispatch was registry-based, so adding a new tile type was a registration step, not an engine edit. Because the type system was carrying the weight that aspiration had been carrying badly two months before.

I’m not going to pretend I cried. I didn’t. But I sat there and watched the blocky figure walk back and forth between grass and beach for a while, and it was one of the best moments of the project so far.

What changed

The version of Skaldborn I have now is structurally different from the one I deleted, in ways the screenshots don’t show.

The biggest single change is that the architectural rules are mechanical now. Scott Wlaschin, in Domain Modeling Made Functional, has a phrase I think about constantly: make illegal states unrepresentable. (The principle has its roots in Eric Evans’s Domain-Driven Design; Wlaschin gave it the phrasing that stuck.) The version of the system that broke under combat bugs had illegal states represented all the time, because nothing in the type system or the build pipeline objected to them. The new version refuses. There are validators that run on every push. There’s a determinism replay test that hashes the simulation’s output and asserts byte-for-byte equality across runs. There are nine separate gates, each enforcing one architectural rule that used to live in a markdown file and now lives in CI.

The cost of breaking a rule is no longer “discover it three weeks later in production.” It’s “the push fails.” That’s the difference between aspirational and mechanical, and it’s the only difference that matters.

The other change, which I want to be honest about, is in how the work itself is done.

Skaldborn is built in collaboration with Claude. This isn’t a side detail. Claude fills the architect role on the project — drafting decision documents against the books and stopping to ask before any move that isn’t supported by a citation. A second Claude, in a narrower reviewer role, checks the citation chain on every document. Scoped executors do the bounded code work against contracts the architect already wrote. I bring the problem and the source material; Claude does the drafting, retrieval, and bounded implementation. The accountability is mine. The books are non-negotiable on either side.

I don’t think this is the only way to build software, and I’m not selling a methodology. I’m telling you what’s true: the version of Skaldborn I have now exists because a human (me) and Claude found a working shape for the collaboration that puts a serious corpus of architecture books on the spine of every decision and refuses to let the discipline lapse.

The phrase aspirational vs mechanical applies recursively. The aspirational version of “we’ll consult the books” is reading the books and citing them in conversations. The mechanical version is having a system where every decision document has to cite a passage, a reviewer that checks the citation, and a discipline that fails closed when the citation doesn’t trace.

I built the aspirational version first, in February. It cost me two months. The mechanical version is what I have now. The two-month cost was the price of learning to tell the difference.

What I’d tell you, if you’re staring at a fork like this

A senior engineer asked me recently what I’d tell someone in 2027 looking at their own version of this — two thousand lines of working code, an extensibility test that’s probably going to fail, a deadline.

The answer is short:

Do what’s right, not what’s fast.

Fast and wrong always costs more in the long run. Any credible code publication will tell you this. I knew it before February. I knew it during February. I told myself the system was fine because the system worked, and I confused the system works with the system is right.

A system that works isn’t necessarily a system that’s going to keep working. A system that’s structurally right is. The way to tell the difference between them is to ask: if I had to add one thing tomorrow that I haven’t planned for, would the system bend, or break? The aspirational version of the answer is “bend.” The mechanical version is whatever the build pipeline tells you when you try.

The best code review question I know now is the one I should have been asking my own design from day one: what does Fowler / Wlaschin / Richardson / Evans say about this commit? If the answer is “I’m not sure,” go look. If the answer is “they don’t address this directly,” mark the decision as opinion, in writing, and move on with eyes open. If the answer is “they explicitly warn against this shape, here’s the chapter” — listen. They’ve watched a lot of people make the mistake you’re about to make. Their patterns are downstream of more pain than you’ll ever be.

I lost two months. I gained an architecture that will outlast the next decision I make. I gained a working shape for the human-and-AI collaboration that will keep working as the project grows. I gained a phrase — aspirational vs mechanical — that I will be using for the rest of my career.

I do not regret the deletion. You don’t learn lessons without going through the failure motions. The motions are the lesson.

If you’re looking at your own two-thousand-line fork right now, the only advice I have is the one I wish someone had said out loud to me in February:

Working code is necessary. It is not sufficient. Make sure your contracts are mechanical before you trust the running system to keep them.

The blocky figure on the beach is what this advice produces, on the other side of doing it right.


Footnotes and pointers

Companion posts

Books referenced (the four on the shelf)

  • Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (2003).
  • Scott Wlaschin, Domain Modeling Made Functional (2018).
  • Martin Fowler, Refactoring: Improving the Design of Existing Code (2nd ed., 2018).
  • Chris Richardson, Microservices Patterns (2018).