The river and the template
I’ve written before about Perpetūra’s stack: static HTML, CSS, vanilla JavaScript, no build step, no framework. That’s a decision I keep re-making, not a limitation I put up with. A framework buys faster scaffolding and pays for it with failures that live inside someone else’s abstraction; plain files fail in ways I can find with a text editor and a stack trace. For a studio of one, findable beats fast. This note is about what sits on top: the engine, and the story that broke it.
A graph that remembers you
Strip the prose away and each story is a directed graph of chapter nodes. The runtime is three small modules: a router (which also owns versioning and the service worker), a state module that owns saves, and a reader that renders whatever node you’re standing on. Each story’s chapters live in one plain JavaScript file, and a registry declares its flowchart of nodes and edges.
Navigation is deliberately unclever. Nodes advance by explicit next links.
Choices set flags, and flags gate variant beats inside later nodes.
Alongside the flags runs a lean accumulator: most choices nudge a counter
toward one person or another, and the engine uses that lean for soft routing
and for colouring, the small tonal shifts in a shared scene, without ever
hard-branching on it. Hard commitment happens once, at a route lock, after
which the reader belongs to a path.
The template is rigid on purpose
The three named stories share a fixed skeleton: a prologue, eighteen chapters, an epilogue, three acts. Most chapters are shared trunk with lean-coloured closing variants; five chapter slots are unique per route; the lock lands mid-story, at chapter thirteen. That rigidity is a feature. When every story has the same skeleton, the tooling can assume a shape, and “every route is reachable” has a checkable meaning.
One story refused, and I let it win
Her life moves across countries and across years, and a structure built around a single place and a three-act ceiling kept amputating her. So that story uses what I call a river model instead. Each route is its own continuous branch, a river with its own current. Commitment isn’t a single mid-story lock; it happens per exit, at the points where she could settle into a life, and a reader who never takes an exit meets one late forced choice at the climax. Rivers vary in length, the longest earning its length, and the story carries far more endings than its siblings.
The engine needed only additive work to hold all that. The state module already tracked flags; recording path history covered the rest; and the flowchart treats exit ramps as ordinary edges, so the reachability tooling didn’t change at all. That’s the quiet argument for boring architecture. The weird story cost me an outline document, not a rewrite.
Measuring the narrator
Register drift is the most reproducible failure I’ve found in AI-assisted revision. The drafts here are human, but every chapter still passes through refinement, and if each chapter gets calibrated against its neighbour, the voice moves wave by wave: every adjacent pair reads consistent, and twelve chapters later the narrator is somebody else.
The fix was to stop comparing neighbours. Every chapter is profiled against the story’s trunk, the human-set baseline voice of its approved opening, on mechanical proxies: contractions per thousand words, mean sentence length, paragraph blocking, dialogue share. Each story derives its own band from its own trunk, which matters — a period narrator legitimately sits near zero contractions where a modern one shouldn’t, and “correcting” one toward the other is the drift bug running in reverse. A chapter has to land inside its band to count as done, and overshooting is the same defect as undershooting, since the next pass calibrates to whatever you leave behind. The band is numeric because my ear is precisely the instrument this failure mode fools.
Strictly downstream
One ordering rule governs all of the above. The outline is mine, the first draft is mine, and every craft and story decision is made by a person before the material enters the automated side. The machine checks, stress-tests and refines work that already exists; it doesn’t originate anything. The work is authored, not generated, and the whole system exists to keep that sentence true.
Spoilers are a build problem
Unfinished stories ship inside the production codebase behind a flag the player-facing code respects everywhere, so committing scaffolding is safe rather than reckless. One finished story goes further, with an unlock gate and a sealed display name; I won’t tell you what’s sealed or why, because the mechanism is the point. The codex is tiered the same way. None of it relies on me remembering to be careful; it’s enforced by code that doesn’t know how to make the mistake.
Pushed is not shipped
The version triple (a version file, an app constant, the service worker’s cache key) has come up in these notes before. Let the three disagree and either the update banner cries wolf, or the service worker keeps serving the stale build while insisting it’s current. So after every release I load the live URL and look, because between the CDN, the cache and the service worker there are three layers happy to show me yesterday. A green deploy log is a claim, not evidence.