Inspiration

I write vertical drama. A season is thirty episodes, and it never gets written once. Dialogue changes, a scene moves, an arc gets rewritten, over weeks.

So I kept asking Claude and Gemini to find my continuity errors. And they did, sometimes. But not the same ones twice. I would get three problems in one run and a different three in the next, and I could never tell whether the ones that stopped appearing had been fixed or had just stopped being noticed.

That is not the model being bad. Thirty episodes in one context window and weigh every detail equally is not a fair thing to ask of anything.

What I actually wanted was the thing I already have when I write code. I change something, tests tell me what I broke. Scripts have nothing like that.

What it does

The model is a witness. The database is the judge.

Each section of the script goes to Gemini on its own. Reading section eighteen, it has not seen section one, so it cannot quietly reconcile a contradiction or decide someone is lying or the lighting was different. It does not know there is a contradiction. It writes down what the page says, with the exact line it came from, and stops.

Those facts go into ClickHouse, and seven checks run as SQL over the stored evidence.

Two of them find things that are provably wrong. A character has one eye colour in the bible and another in section eighteen. Story time moves backwards. Two recorded values that cannot both be true.

The other five just measure. A line repeated four times, one location carrying eleven scenes, five sections ending on the same beat. I do not call those errors. A repeated line might be a motif, an unanswered question might be the whole point. The system counts, the writer decides. Keeping those two things apart mattered more than I expected it to.

Then there is the part I care about most. Index a script, keep it as the approved cut, paste an edited draft, and every fact the revision changes gets compared against the whole indexed history. Not by re-reading it, by joining two tables.

APPROVED green — "Her green eyes reflect the pale morning light." REVISION blue — "Her blue eyes reflect the pale morning light."

You cannot ask an LLM what yesterday's edit broke. It does not remember yesterday.

How I built it

Gemini 2.5 Flash on Vertex AI does extraction, one section per call. Google ADK builds the agent, with the official mcp-clickhouse MCP server mounted as a tool. ClickHouse Cloud holds the facts and runs the checks. Cloud Run hosts it.

Three decisions do most of the work.

The blindness is the point, not a limitation. It is the only reason the model cannot smooth something over.

The model never invents a character id. It reports the name it saw, Maya or MAYA REYES or Ms. Reyes, and my code decides those are the same person. Identity is not a judgement I wanted a model making.

The model never does arithmetic. It records "three weeks later" as a cue and a number, and SQL builds the calendar from those cues using segments and running sums. The UI shows every step, so you can trace a finding back to the words on the page.

Challenges I ran into

My first timeline check asked Gemini to add elapsed days to the previous section's running total. Three weeks later, add twenty one. It worked until a flashback, and then every section after it was wrong.

I spent a while trying to fix the prompt before realising the prompt was not the problem. Addition is a decision. I had written a whole architecture around not letting the model decide things, then handed it arithmetic without noticing.

The second one was plot threads. In a mystery script every suspicious sentence raises a question, so my first version returned thirteen unresolved threads from one season. Most were single lines. Requiring a thread to actually appear in two different sections cut it to the handful that were real.

The third was smaller but annoying. Indexing thirty sections took almost four minutes because I was doing them one at a time. Running them in parallel broke name resolution, because concurrent calls could not see each other's characters. The fix was to index the first section alone, since a title page or character bible registers the cast, then run everything else concurrently against a populated registry.

Accomplishments I'm proud of

An eye colour contradiction found across a character bible and two sections, with the exact line quoted from each. Not the model saying something seems inconsistent, but four recorded statements and the pages they came from.

A timeline where every step is auditable. Day one, plus twenty one, plus seven, then a flashback resets it and time moves backwards, and each of those numbers traces back to the phrase on the page that produced it.

And a regression that catches a one word edit against the whole indexed history in milliseconds.

What I learned

The interesting part was never prompting the model better. It was working out what the model is not allowed to do, and then noticing twice that I had broken my own rule without meaning to.

The other thing I learned is that being honest about the limits made the system better rather than weaker. Once I stopped pretending the structure checks were errors and started calling them measurements, they became more useful, not less.

What's next for ContinuityOS

Event level date conflicts. Two sections stating different dates for the same event, resolved the same way the timeline already is.

And a relationship graph, for contradictions that live between characters rather than inside one. "I have no sister" in one section and a sister in another needs an entity graph, not an attribute table. Same for a father who died five years ago and turns up in a later scene. Both are extractable, neither is built.

There is also a limit I would rather state than have someone find. Extraction is still probabilistic. If Gemini misreads a line the fact is wrong, and I have watched the fact count vary between runs on the same script. That is an extraction error rather than a continuity decision, but it is real. What the architecture actually guarantees is narrower: once a fact is stored, no model changes whether a rule flags it.

Built With

Share this project:

Updates

Submission history