barts.work

July 2, 2026

Verify Green or It Didn't Happen

Bloom is a cozy idle-garden game — tap to earn Petals, buy plants, prestige into a New Season — and I built it solo, mostly in the hours after everyone else in the house was asleep. Which is exactly the problem this post is about: the person building it at 11pm and the person continuing at 11pm three days later are, for practical purposes, two different people. The second one doesn’t remember what the first one was thinking.

One gate, no negotiation

So I built the project around a rule instead of around memory: verify green or it didn’t happen.

The mechanism is almost insultingly simple. The plan lives on disk — an ordered task list, a progress file, a lessons file. Every session starts the same way: read the plan, pick exactly one task, write the failing test, make it pass, then face verify.sh — one gate, absolute: engine unit tests pass on the host (swift test, no simulator needed) and the app compiles for the iphonesimulator SDK. Green, commit, cross the task off. Red, and the task isn’t done no matter how done it feels. The rule I’d frame and put on a wall: never go green by cheating. No deleting, skipping, or weakening tests to pass. A clean red-blocked state is better than a fake-green commit — because a fake green doesn’t just lie to some abstract process, it lies to the stranger who sits down at this desk next.

The lessons file

What I didn’t expect was how much of the real work ended up living in tasks/lessons.md — a running list I appended to every time I got burned. It’s the most honest engineering journal I’ve ever kept. One entry: I burned most of an evening “fixing” app icons on a red build, regenerating assets over and over, when the actual problem was that this Mac was missing the iOS simulator runtime, so actool could never compile the asset catalog headlessly — the icons were never broken. Another: Color.blendMode(...) is ambiguous on newer SDKs because Color conforms to both View and ShapeStyle, and a blended tint overlay darkens everything behind its frame, including a moon it had no business touching.

Only crashes on the first launch

The one I keep coming back to: an async-startup refactor set started = true before the AVAudioEngine graph was actually assembled, so the first scenePhase → active after a fresh install started an empty graph and crashed — an Objective-C exception that Swift’s try? cannot catch, full stop, process gone. The lesson I wrote for myself afterwards: verify launch on a FRESH install, several times — my post-refactor check used a seeded save and one launch, and the race passed it. A single happy-path smoke test hid a crash that only fires on the very first launch. That’s not an exotic mistake. That’s a mistake I’ve watched entire teams make in every retro I’ve ever sat through.

”Implemented” means reachable

My favorite entry isn’t a bug at all — it’s a completeness audit. The seed-tree engine shipped with sixteen passing tests and no screen that let a player actually spend the currency it managed. Working code, unreachable feature. The fix was mechanical: grep every public func in the engine and check each action had a UI entry point wired through the view model. “Implemented” has to mean reachable, not compiles.

What the gate can’t cover

By the end, the gate was green over the whole thing — 191 Swift Testing cases plus 22 XCTest, engine and app — and I wrote the completion marker with a clean conscience. What the gate couldn’t cover stayed on a human checklist, on purpose: StoreKit sandbox purchases, Restore Purchases on a physical device, App Store Connect setup, a real icon. No amount of swift test substitutes for a person tapping “Buy” on a phone.

The thing I took from this as a QA lead, not just as the guy building a garden game at night: I didn’t need discipline to be a personality trait. I needed one gate I couldn’t negotiate with at 11pm, and a place to write down every time reality disagreed with my assumptions. That’s the whole job, really — mine included.