2026-08-22·9 min read

The QA lab under the love story

field-notesperpeturatoolingclaude-code

A spec I wrote said to gate a beat on a flag.

The flag was real. It gets set at exactly one fork, and the only exit from that fork runs off down a branch that never comes back to the chapter the beat lives in. So the beat was finished, wired, indistinguishable from every other beat around it, and it would have rendered for zero players.

The build was green.

It was green because the check I had asks a global question: is this flag ever set anywhere? Yes. Once, in a place no reader of that chapter has ever been. The bug is not never set. The bug is never set on a path that reaches the reader, and those are different questions.

Reading the chapter does not catch it either. The chapter is fine. What is wrong is the shape of the graph three chapters upstream, and I cannot hold that in my head.

A story that is also a program

Perpetūra is interactive fiction, a literary romance in first person, five stories live and a second phase in progress. It is also a program: chapter nodes, choices as the edges between them, flags riding on the edges, and a running tally underneath that decides which variant of a chapter you get served. Nobody sees the tally.

So prose problems and program problems live in the same file and look identical on the page. A sentence can be beautiful and unreachable. Changing one branch can break four others without raising an error, because nothing crashes. It just quietly stops being true.

There are 42 entries in tools/ and five of them are named test- something. test-lean.js replays a set of picks through the accumulator to prove which lane they land in. test-flags.js does the same for choice visibility. Neither is a literary instrument. They exist because I write a love story on Monday and debug a state machine on Tuesday, and it is the same afternoon.

The department I built has editors on one side and engineers on the other. Job titles, because that is what they are. Here are six of them.

01 / The playtester

It runs in two halves. First a code audit: reachability, choice wiring, codex and music hookups, the version triple, then a prose pass for em-dashes, spelling, diction, the AI tells. Then it starts the local server and plays the game. Prologue, a choice that advances a chapter and records lean, the pause menu, save and resume with the tally intact, screenshots at 375px because most bugs are mobile bugs.

Neither half substitutes for the other. A story can pass every static check and render nothing on a phone, or look perfect on a phone and be structurally rotten in a lane nobody opened.

Its limit is the interesting part. It may not implement any structural check itself. An earlier version did, embedding a copy of the audit logic inline, and it drifted: it stopped seeing the later choice sets, missed flags entirely, and would have reported clean under a stale definition of clean. Now anything worth checking goes into the tool CI runs. One definition of clean, in one place, held by the thing that cannot be talked out of it. It is also required to state which sections it did not cover, and to refuse to call a story QA-clean without that caveat.

Which is the payoff. A green report from something that has to enumerate its own blind spots is worth reading. One that only tells you what it looked at is worth almost nothing, and I have written both.

02 / The front desk

Feedback goes from a form inside the game to a card on a board, with a footer recording which screen the player was on, which story and chapter, and any screenshot they attached. It reads the board, groups the duplicates, looks at the picture before it looks at the code, reproduces at mobile width, fixes, commits, comments the card with the commit hash, and moves it to Done.

The hard part is that a quiet board is ambiguous. The player sees “Sent” the instant the host accepts the form. The relay that turns a submission into a card runs afterwards, and if its token has expired it fails invisibly. Submissions pile up where nobody is looking, and silence looks exactly like nobody having anything to say.

The payoff is the paper trail. Every closed card carries the commit that closed it, so when I look at some odd piece of layout eight months later, the answer is a person who told me it was broken on their phone.

03 / The set builder

A new story is eighteen chapters built against a fixed set of invariants and a set of axes each story has to land differently: era, number of love interests, route mode, the shape of the central dilemma. It confirms each axis choice with me rather than inventing one, generates the outline, and stubs the story file: prologue through eighteen, correct keys, valid choices, parseable and reachable while still entirely placeholder.

Every invariant is cheap to build in and expensive to retrofit. All recurring characters introduced by chapter three, a personal artifact by chapter five, any living relative alive on every route. Discovering in chapter fourteen that one of those was never true is a rewrite of everything downstream.

It does not write prose. That is done by hand, chapter by chapter. But before it scaffolds anything it writes about 500 words of the new story’s register, reads them against a page of Hyunjae, a page of Reine, a page of Liuyan, and shows me the comparison. That is the only cheap moment to find out the voice is wrong. After it, eighteen chapters are standing on that voice.

04 / The continuity editor

Some of what Perpetūra promises its readers cannot be checked by anything that does not read for meaning. A choice has to be answerable from the variant the player actually saw, never a callback to a scene on a branch they did not take. A living relative has to read as alive on the routes where she never appears. Several more are checks on a promise I am not going to tell you about, because describing what the story is careful never to say is the same as saying it.

It is a multi-agent run: one reader per route across the invariant matrix, then a skeptic per finding that defaults to false. It is hard because the violation is never in one place. It is a sentence in chapter eleven of one lane sitting against a sentence in chapter three of another, and no amount of care reading either lane alone will surface it.

Its limit is that it reports and never edits prose. The fix for a continuity break is usually a design decision, and a good share of what comes back is not a break at all. I keep a record of the findings I have ruled a deliberate keep, so the next run reports them once as adjudicated instead of re-litigating them.

It has also failed in a way worth saying out loud: the list of drafted routes is hard-coded, and when it went stale, passing an unlisted story filtered the list to nothing and the run returned a clean audit having read no prose at all.

05 / The test audience

It is four reader personas who play the story blind. Not read it, play it. They choose at every fork, in character, and journal each chapter before they are allowed to see the next one. The orchestrator mirrors the real accumulator, so which route each persona ends up on is itself a finding. The impatient one reading on a phone and the careful one reading for whether a reveal was fairly built do not take the same branch, and a lane whose timing only works for readers who never go there is worth knowing.

Every other instrument in the repo reads the story already knowing what it is supposed to be. This is the only thing I have that can be wrong the way a stranger is wrong.

So a persona is served exactly what the screen shows: chapter label, badge, the default prose, the visible choice texts. No chapter keys, since a key names its own lane. No node ids, no route letters, no design docs. Afterwards there is a contamination screen, because a persona that has leaked reads like a persona that is confident.

That screen has already changed a verdict. On one run two personas used terms for the story’s mechanics that appeared nowhere in what they had been served, and both declared themselves certain about something the story means to deliver late. On their ledgers alone that files the reveal as landing far too early, a serious finding against the most carefully paced thing in the book. The two clean personas stopped at “suspects”, which is the delivery working as designed.

A leaked persona is not thrown away. Its reactions to the page are still reactions to the page, so the drop-off points and the moments that pulled it forward all stand. Only the predictions come out, because a contaminated ledger cannot tell the story made me think this from I already knew.

06 / The editorial board

It runs five lenses over a route: romance and audience, literary craft, route architecture and choice design, canon, and how the route sits against the ones already shipped. Some are argued in-session, some go out to a bench of models from other vendors, on a debate-then-verify pass. It exists because I am the worst available judge of whether a route reads as distinct. Every planted detail lands for me, because I put it there.

The limit is that it reads for meaning only, never re-checking anything mechanical or structural and never editing prose. Running it honestly is its own discipline: a model can pass the health check and contribute nothing, spending its whole budget on reasoning and returning zero characters of content, which the summary counts as a success. One got cut for that.

Effort is spent unevenly on purpose. The lens panel runs high, the debate a tier below it, the thirty-odd verifiers low, and the chair at the top, because the chair writes the one document I actually act on. Running every stage at the same tier cost 6.1 million tokens for a single review, which is the waste the split was built to end.

The rest of the staff

The drafter (07) takes a chapter through its done-sequence. The writers’ room (08) is the two-writer fan-out I have written about separately. The second reader (09) is the cheap craft pass between the expensive ones, and the re-reader (10) exists for when a style rule changes after the prose was written. The character analyst (11) tests whether a character’s dialogue survives having the attributions stripped out. The production editor (12) is the rollup, and its most useful behaviour is saying that a sibling report has never been run rather than inventing one. Then the acquisitions editor (13), the trailer cutter (14), the shipping desk (15), and the composer (16), who writes lyrics.

There is a file in the repo called docs/operations.md. It maps every service the game touches: where its config lives, how it fails, what to do when a release goes wrong. Near the bottom is a heading that reads “Known monitoring blind spots (accepted, not solved)”, under which sit three things that fail silently and that I have decided not to fix, with a manual check to run before releases instead.

I wrote a novel and then wrote myself an on-call runbook for it.