Chapter five · Days 28–38 · interleaved with the build
Building with AI is fast. Debugging with AI is where the craft actually lives - because the model can’t see your screen, can’t run your app, and will confidently invent causes. Your job is to be its eyes, and to know when it’s guessing.
Three truths before the stories. First: the model cannot observe. It only knows what you paste - so the quality of a debugging session is exactly the quality of your evidence gathering. Full crash logs, reproduction steps, and the actual file beat “it crashes sometimes” by an order of magnitude. Second: its first hypothesis is a guess wearing confidence. Treat first answers as leads, not verdicts. Third: rubber-ducking is real. Two of these three bugs were solved by the act of writing the reproduction steps carefully - before the AI even answered.
Symptom. The firing list crashed on launch - but only on my partner’s phone, never in the simulator, never on my device. Fatal error: unexpectedly found nil deep in a SwiftData fetch. Four evenings, the longest bug of the project.
The AI’s first theory: a migration issue. Two evenings of versioning ceremony that fixed nothing. Its confidence never wavered; its correctness did.
I finally compared devices properly: her library had a firing with zero entries - a kiln opened in disappointment and logged empty. My test data never did that.
A predicate chained force-unwraps through entries.first!. Empty array → nil → boom. Real users do things your fixtures don’t.
One prompt with the full crash log + the reproduction (“firing with no entries”): replaced the chain with compactMap and added an empty-state view. 20 minutes.
Lesson: the bug wasn’t in the code, it was in my test data’s imagination. Now every data-shaped feature ships with a “hostile fixture” - empty, enormous, emoji-named, and mid-sync states - generated by the AI, of course.
Symptom. After enabling sync, some glazes appeared twice on iPad. Then three times. Deleting one deleted them all - then they came back. No crash, no error, just quietly multiplying pottery. The scariest class of bug: silent, intermittent, and touching user data.
“Dedupe on appearance,” suggested the AI, and I nearly shipped it - a patch that would have hidden the corruption while it grew. The 40-minute rule fired: stop, ask what assumption is wrong.
Assumption found: SwiftData + CloudKit does not enforce uniqueness. Two devices creating “Shino #4” offline, then syncing, create two records. Correctly, by design.
I’d treated CloudKit like a server with constraints. It’s a merge log. My mental model was the bug - the code was doing exactly what the framework promises.
Stable UUIDs assigned at creation (not derived from content), plus a merge-on-import rule. The AI wrote it well - once the prompt contained the sentence “CloudKit is a merge log, not a database.”
Lesson: the most valuable sentence in a debugging prompt is often your corrected mental model. When the framework “misbehaves,” paste the framework’s actual contract into the chat and ask the AI to argue against your assumption. It will, gladly.
Symptom. On iPad, in Stage Manager, entering a glaze name and tapping “save” would… dismiss the keyboard and eat the tap. Second tap worked. Only in Stage Manager. Only on the 11-inch. Only sometimes. The kind of bug that makes you gaslight yourself.
Focus-state juggling - @FocusState gymnastics suggested across three prompts. Made it worse: now the keyboard flickered.
Screen-recording in slow motion showed the truth: the first tap was consumed by keyboard dismissal - the save button never received it. Not a focus bug. An event-routing fact of iPadOS.
One sentence prompt (“iPadOS eats the tap that dismisses the keyboard; make the toolbar save button use .scrollDismissesKeyboard(.never) semantics and resign first responder explicitly”) → fixed in one diff.
Added to the ledger template: “record a slow-mo video before prompting.” Seeing beats describing, for you and for the model you’re briefing.
Lesson: for UI heisenbugs, your best debugging tool is the screen recorder, and your best prompt is a precise description of a single frame. “At 00:04 the tap lands while the keyboard is animating” is worth a thousand “it feels laggy.”
By the end of the project, every debugging prompt followed the same skeleton. It’s in the Playbook as a copy-paste block, but here it is in situ:
❯ Bug report. Read everything before proposing anything. EXPECTED: [one sentence] ACTUAL: [one sentence, no adjectives] REPRO: [numbered steps, the smallest case that fails] EVIDENCE: [full crash log / slow-mo observation / exact file] CONTEXT: [device, OS, what changed in the last 3 commits] Rules: list your top 3 hypotheses ranked by likelihood, state what evidence would distinguish them, and do not write a fix until I confirm which hypothesis we're testing.
The AI is a brilliant mechanic on the phone. It cannot hear the engine. You are the noise it hears.
The debugging triad: (1) Evidence before hypotheses - full logs, minimal repro, slow-mo video. (2) Force the AI to rank three hypotheses and identify distinguishing evidence before any fix. (3) When stuck 40 minutes, stop patching and interrogate the architecture - and when a framework seems broken, paste its contract into the chat and ask to be proven wrong.