Which Paragraph Said That

, about 18 minutes

A world model derived from prose instead of typed into a form, where every fact can name the sentence and the paragraph that established it. What per-fact provenance costs, what it makes possible, and when a form is the right answer instead.

Quillsmith, our long-form writing system, keeps a structured model of the world an author is writing about: characters, places, relationships, timelines, the state a character is in at a given point in the story. The author never fills any of it in. They write prose, and the system reads it and derives the model.

That choice has a consequence that is easy to state and expensive to implement. Because every fact was read out of something, every fact can name what it was read out of, down to the verbatim sentence that supports it and the paragraph that sentence lives in.

The schema turned out to be the easy part. Four other things were worth the trouble of getting right. The paragraph anchor is recomputed by matching the quote against the text rather than reported by the model, which is why it survives the text moving. The derived rows split three ways by how they were produced, so an undo rebuilds the free ones for nothing and merely reports the expensive ones as stale. A paragraph deletion can find the facts whose only support it was and draft them back out of the world model. And for months we paid a model two or three times to extract the same references from the same unchanged text, because the storage layer was idempotent and hid it perfectly.


The model nobody types in

An author writes a chapter. Somewhere in it a character’s sister is named for the first time, a city gets a location, someone dies. A writing assistant that intends to stay consistent over three hundred thousand words has to know those things, and the obvious way to know them is to ask the author to enter them in a character sheet, a relationship picker, a timeline editor. We do not do that. The author is writing a novel, and every form between them and the next paragraph is a tax on the only activity that matters.

So extraction produces the model. The chapter pipeline reads the prose, pulls out the references it asserts, resolves each one against what already exists, and then either updates an entity or creates one.

It did not start in that order. The pipeline originally discovered entities first, in three separate model passes, and extracted their references last, as a by-product of entities that had already been committed. The revision turned it around. References are now extracted from the chapter text first, and entities are created from the references that mention them, with the quoted prose as the generation context (0502.21API_REV_references-first-extraction.md).

That reversal is what makes provenance natural rather than retrofitted. If entities come first, a fact is something the entity has. If references come first, a fact is something a piece of text said, and the text is right there.

What that buys is a question a form-filled model cannot be asked. Not “is Kira’s sister Elena”, which a form answers, but “which paragraph established that”, which only a derived model can answer. The author asks it constantly, because their own draft is the thing they are trying to remember.


One table, and one that stayed separate

The first version of this store was a fact table with an entity, an attribute, a value, a supporting quote and a confidence. Relationships lived in their own table and mentions in a third. The primitive that everything downstream wanted did not exist. To answer which facts reference entity X, you scanned all of them (0414.12API_DES_unified-reference-registry.md).

The consolidation rested on a single observation. A relationship is a fact, and a mention is a fact, and what separates them is not what they are but how they link. So the three tables became one row type. The semantic category (physical, temporal, spatial, relational, state) is kept orthogonal to the structural kind, and an array column holds the entity identifiers the row points at, GIN-indexed so the reverse query is an indexed array containment rather than a scan.

One thing was deliberately not merged, and the reasoning has held up since April 2026. Structural links (this character is the protagonist of that chapter, this plan covers those chapters) stayed in their own table. They are bidirectional pairs carrying a role, they are timeless, and when two of them conflict the higher confidence wins. Derived facts are entity-owned, can carry story-time scope (“age 32, in the early chapters”), and when two conflict the newer source scope replaces the older one wholesale. Those are different conflict rules on different shapes. One table holding both could have implemented neither rule properly, so there are two registries, complementary, and impact analysis walks both.

There is a related decision I would defend more cautiously. A reference whose target does not exist yet keeps the unresolved name in a string array on the row, and the coherence surface synthesises a “missing entity” finding from those arrays at query time rather than persisting one. Create the entity later and the finding disappears on its own, because nothing was ever written down to clean up (0430.19API_FIX_description-reference-provenance.md). It is elegant and it works. Its cost is that the finding has no durable identifier, so the row gets a deterministic one computed from the reference and the name. I would use synthetic findings that cannot be acted on individually once, and watch them carefully.

The store was called a fact store for three months, and the rename to references cost six weeks to finish, with a commit on 13 June still cleaning up names the one on 30 April had missed (0719.06API_REV_fact_store_to_reference_system.md). That is the honest price of renaming a substrate. What makes it worth recording is where the word went afterwards.

“Fact” came back, not as a table but as a distinction within the reference rows. A row whose relationship type is a real key asserts a fact. A row whose type is the sentinel “mention” records only that an entity was named somewhere, and asserts nothing. The substrate is references uniformly. A fact is a reading of certain references, and the word earns its keep because there is now something it contrasts with. The whole conflict machinery, which exists to stop a chapter’s implication silently overwriting what an entity’s own description says, is built on telling those two apart.


What the anchor actually is

A derived fact carries two pieces of evidence. One is a SourceQuote, exact text copied out of whatever the fact was read from. The other, for facts read out of story prose, is the identifier of the paragraph that text came from.

The obvious way to get the paragraph is to ask the model for it. We do not. The model returns the quote, and the paragraph identifier is computed afterwards by a deterministic pass that searches the chapter’s paragraphs for it, matching exactly and then retrying in a way that tolerates prose having been re-wrapped. A quote that spans two paragraphs, or that is not present in any of them, gets no anchor at all rather than a guessed one.

Three things fall out of that, and together they are the argument for it.

The model cannot fabricate a location. It can fabricate a quote, and the anchor pass is the check, because a quote that does not appear in the text anchors to nothing, which is visible. Anchoring is not a second thing the model must get right on top of the extraction.

The anchor is re-derivable. Because it is a function of (quote, paragraph set), any later pass can recompute it against the current paragraphs. That is how the ledger repair path finds rows whose quote no longer exists in any active paragraph and routes them into the purge funnel described further down.

And the matcher contains no language. It is substring comparison and Unicode whitespace collapse, with no sentence-boundary regex, no word splitting on spaces, no capitalisation heuristic. That is a house rule for us, since content processing may not assume English, and it is also why the same pass works on the German fixture we test extraction against.

The mistake in this area is worth naming because it cost schema rather than time. The original design also specified character offsets, a start and an end, so a fact could point at a span rather than a paragraph. The full version, with a separate anchor table and a rebase on every paragraph edit, was deferred as a later phase.

The columns shipped. No producer has ever written them.

They are carried in the domain model, in the archive format, in the merge path that copies a row, and in the API response, and every one of those values is null. The minimal approach turned out to be the whole approach. Relocating a quote does the job offsets were supposed to do, and does it better, because an offset is invalidated by an edit anywhere earlier in the paragraph while a quote survives until the quoted words themselves change.


What provenance costs

Per-fact provenance is not a column. It is a tax on every producer, every write path, every delete path and every read surface, and it fell due in all of them.

It hurts most in output tokens. Every extracted reference carries its supporting quote verbatim, and on the run sampled for our token-economy review on 21–22 August 2026, reference extraction was the top cost line in the whole extraction complex. Roughly two thirds of that output was the pipeline paying output rates to echo text it had just sent as input (0827.13API_REV_extraction_token_economy.md), and output is priced at six times input on the model we pin for this work. Provenance is the expensive half of the payload. The compression queued for it bounds the quote to a prefix. That works because attribution is a substring search, and a prefix attributes as well as a full sentence, which makes it a provenance optimisation rather than a prompt one.

Then there are the write paths. The store was meant to be the single funnel: validate, replace only your own scope, write a change-log entry, invalidate the cache. A review of the whole system in July found five producers writing reference rows and three of them bypassing the funnel entirely. Each had its own clear-by-source logic, its own source string chosen from scattered literals, and no cache invalidation, so a thirty-minute sliding cache could serve a stale list indefinitely to the very consumers that read it most during a writing session (0717.10API_REV_reference_system.md).

The same review found the scope mapping falling back silently. A user-triggered refresh landed in the derivation scope, so refreshing an entity destroyed its deterministic rows and wrote its AI-extracted rows under the wrong label, where the next automatic pass would not replace them and the next derivation would silently wipe them. Both are fixed. The source is an enum now, the mapping is exhaustive, and the store is the funnel. What I took from it is that advisory provenance is worse than none, because a mislabelled row is trusted as much as a correct one.

The empty case is the one everybody forgets. Every store call was guarded by “if there are references to write”. Content edited down to zero facts therefore kept its old ones forever, and kept the entity permanently marked stale, because the rows that would have been replaced were never visited. Replace semantics require calling the store with the empty list. The derivation service now does, with a comment saying why, because it looks like a redundant call until you know.

Deletes and undo have their own rules. A reference row is deliberately exempt from the version ledger. It writes no history row, because it is a distillation of content that the next extraction pass re-derives, and ledgering it would add churn for what is functionally a cache. That exemption is enforced by a test that fails the suite if the interceptor’s type map and the write-class registry ever disagree, rather than by a comment nobody re-reads. Deletion is a tombstone carrying the identifier of the teardown that set it, so a restore knows which rows to revive and which superseded tombstones to destroy.

One hazard there is sharp enough to have been carried verbatim through several plans as a standing note. The row carries the directive that produced it, and that field looks like creation provenance. It is not. When a later pass re-establishes a fact that already exists, it re-stamps the existing row with the new directive, so deleting by that field while undoing a directive would destroy pre-existing facts that were merely refreshed by it. Nothing in the system selects or deletes by it, and the reason is written down in more than one place.


Free to rebuild, expensive to rebuild

If I took one idea from this system to another one unchanged, it would be this.

One row type, three provenances, three rebuild costs Derived facts all live in one table, but they arrive from two different sources and carry one of three provenance scopes. An entity's own structured content yields Derivation rows, computed deterministically with no model call, which a rollback re-runs for nothing. The same entity's prose description yields AiExtraction rows, which cost a model call and are therefore refreshed only on request. Story paragraphs yield ProseExtraction rows, each carrying the verbatim quote that supports it and the identifier of the paragraph that quote was relocated to. The anchor is recomputed by matching the quote against the paragraph text, not reported by the model. Each scope is replaced independently, so one producer never destroys another's rows. Entity content what the world model says Story paragraphs what the author wrote Derivation deterministic, no model call AiExtraction one model call per entity ProseExtraction quote + the paragraph it was relocated to cost to rebuild after an undo free, re-run on the spot rollback does this itself paid, opt-in refresh reported as stale, never bought appended, never replaced purged when its paragraph dies one table · one row per fact · each scope replaced independently

One row type, three provenance scopes. Each is replaced independently, so a producer never destroys another’s rows — and each has a different price to rebuild, which is what makes the rollback behaviour possible.

Facts arrive three ways, and the row records which. Derivation rows are computed deterministically from the entity’s own structured sections, with no model involved and a confidence of 1.0. AiExtraction rows are what a model pulled out of the entity’s written description. ProseExtraction rows are what the story itself asserted, and they are the ones that carry a paragraph anchor. Each scope is replaced independently. Re-deriving an entity never touches what the prose established, and a new chapter’s prose facts never wipe an older chapter’s.

Splitting on how we know it, rather than on what the fact says, is what lets undo behave sensibly. When a rollback restores text, the derived layer above that text is suddenly describing prose that no longer exists. Four options were on the table: leave it wrong, re-derive everything, buy a full refresh, or split. We split.

A rollback re-runs the deterministic pass itself, against the restored text, for nothing. Then it reports which entities still have an AI-paid layer that is stale, in a staleReferenceEntityIds field on the response, so a client can offer a refresh that the user decides to pay for. A rollback stays a zero-dollar operation. That is a property worth protecting, because an undo that costs money is an undo people hesitate over.

Keeping it took one non-obvious exclusion. We audited the free re-derivation pass arm by arm to confirm no arm could spend, and one could: structural link extraction resolves ambiguous names through the name resolver, which is allowed to make a confirming model call. So link re-extraction is not on the rollback path at all. Links got the same staleness spine as references instead, and are flagged in the status read, which is a pure read.

The first cut of that spine was wrong in three ways at once. It compared the stamp against either endpoint, so links between entities with unequal edit counts were born stale. It stamped a constant for entity types with no real version sequence. And a re-scan never restamped, so a flag once raised could never clear. The fix was for the row to record which sequence its stamp came from. A version number without the identity of the sequence it counts is not a version number.

We checked it live on 27 August 2026, on a freshly imported project. A thirteen-member rename cascade rolled back with no force flag and zero failed actions. The eight cascade-touched entities were reported as stale, and no others. Their deterministic rows were re-derived at the post-restore version, and the status read showed eleven stale links project-wide and six on the most-affected character, cross-checked in SQL (0827.12API_FIX_rollback_derived_staleness.md).


A fact can die with its paragraph

An author deletes a paragraph. Somewhere in it was the only sentence that ever established that a character carries a particular scar, and that fact is now sitting in the character’s description, asserted by nothing.

Because every prose fact knows its paragraph, this is answerable. On a paragraph deletion the system loads every fact that paragraph supported, and asks of each one whether the same (category, relationship) is still supported by anything live. That can be another prose fact whose paragraph is still active, or a row from either of the two entity-sourced scopes. Facts that still have support lose only their paragraph-scoped row. Facts whose sole support was the deleted paragraph go to a purge, where one model call drafts the character’s description with that fact, and only that fact, removed, applied through the normal persistence funnel.

The behaviour I like best is the decline. If the fact is so interwoven with the rest of the description that it cannot be cleanly removed, the model returns unchanged content and the system falls back to raising a coherence finding for a human. It does not force an edit it cannot make cleanly. And all the purge edits for one deleted paragraph share a single operation identifier, so restoring the paragraph reverts them as a unit.

None of this is available to a model built from forms. A form-filled fact has no support to lose. It sits there being true until somebody remembers to go and change it, which in a three-hundred-thousand-word draft is never.


Paying three times for the same extraction

In July a note in an unrelated migration document mentioned that world creation appeared to inherit a double reference extraction from the chat route. We checked whether that recurred elsewhere, and found something worse. It was systemic (0713.00API_REV_reference-extraction-redundancy.md).

AI reference extraction, one model call per created entity with a 20,000-token budget, was running three times for every entity created by the chat and world-creation flow, and twice on the chapter flow. Three independent layers each ran it: the commit pipeline’s post-processing hook after each creation step, a dedicated plan step late in the run, and a finalize handler appended to chat flows. All three called the same service, on the same unchanged content.

How it survived unnoticed is the part worth keeping. The storage layer is idempotent by source scope (storing a second time removes the first set and re-adds it), so the duplicate passes produced identical data. Nothing was corrupt. Nothing looked wrong in the database. The idempotency was enforced after the expensive call had already been made, so the system was paying two extra model calls per entity to arrive at a state it was already in. The extraction prompt reads only the entity’s own title, content and subtype, so the later passes could not even find anything new; their output was a pure function of content that had not changed since the first pass. And this was on the interactive path, so it was also latency the user sat through.

The cheap fix was a version-keyed guard that skips the call when rows already exist for the entity’s current version. We did not take it. The house rule is to remove redundancy rather than guard against it, and the structural fix turned out not to be riskier, because an investigation confirmed nothing read the extracted rows mid-plan, which was the only real objection.

So the late plan step became the sole owner of the paid pass. The cheap deterministic pass stayed eager at commit time, where later steps rely on it, and the finalize handler was deleted outright (0713.15API_DES_post-entity-processing-consolidation.md). The plan definition carries the comment “Phase-2 sole owner” on that step today. The flows that have no such step, chat and the MCP surface, got the symmetric completion. They run the paid pass themselves, gated on an ambient scope that is set when a plan already owns it. Entities created through chat had otherwise been left with no AI-extracted references at all, which made them invisible to the context scoring that decides what goes into a writing prompt.

The general lesson is not about extraction. Idempotent storage in front of an expensive producer hides duplicate production perfectly. If the expensive step is a model call, a network request or anything else you are billed for, the guard has to sit in front of the producer, or the structure has to make the second call impossible. Correct data is not evidence that you paid once.


Where this stops applying

Derivation is expensive and lossy compared with a form, and there are conditions under which the form is straightforwardly correct.

When the facts are few, closed, and already known to the user, type them in. A dozen fields the user can name should be a dozen fields. Deriving them costs a model call per pass, resolves ambiguously, and produces a confidence below 1.0 for something the user could have stated with certainty in ten seconds.

When a wrong value is a real loss, use the form. Everything here is evidence, not guarantee. A quote is strong evidence, and it is still a model’s reading of a sentence. If the field is a payment amount, a dosage, or a legal date, “the paragraph says so” is not the standard you need, and the correct design records what someone entered and asserts it.

When the prose is not the authority, the architecture points the wrong way. It makes the text the record and the model its distillation. That is right for a novel, where the manuscript genuinely is the truth and everything else is a reading of it. In a domain where the structured record is the authority and documents are generated from it, deriving one from the other inverts the ownership and every edit becomes a merge conflict.

And when there is no text at all there is nothing to anchor to. That is obvious and still worth saying, because derived-model architectures get proposed for systems whose input is a handful of form posts.

The last precondition is not about the domain at all. The registry is worth very little unless “what entity is this name?” has exactly one answer, from one code path. Ours did not, for a while. A review found four separate name-resolution implementations feeding the same store. One place had a proper scoring chain, another a naive lowercase substring match over a dynamic list, a third exact normalised equality, a fourth an alias-register lookup. A reference graph built by four resolvers that disagree is a graph whose edges mean four different things.

Single name resolution is now a standing architecture rule for us, with one sanctioned exception for callers holding entities the register cannot see yet, and that exception uses the same scorer. If you are considering this pattern, build the resolver first. It is the load-bearing piece, and it is the one that looks optional.


What it looks like now

One table of derived facts. Each row names the entity it is about, a semantic category, a relationship key, the exact quote that supports it, a confidence, an array of entities it points at, the provenance scope it was produced under and, for facts the story asserted, the paragraph that quote was relocated to. Three provenance scopes, each replaced independently. A reverse query that is an indexed array containment. A deterministic anchor pass with no language and no model call in it. A rollback that costs nothing and reports what it could not restore for free. A paragraph deletion that can find the facts it orphaned and draft them back out of the world.

None of that is what took longest. What took longest was getting every writer to go through one door, every deleter to clean up after itself, and every reader to know which rows it was looking at.


How we apply this for clients

The pattern generalises past novels to any system where the durable artefact is text and the structured view of it is derived: case files, research corpora, technical documentation, incident histories. What we would bring to it:

  • The provenance audit. Can each derived row name what produced it, from which text, at which version? Systems that have a source column usually find it is advisory, written by some producers, defaulted by others, trusted by every reader. That is a one-day question with an answer that predicts everything downstream.
  • The single write funnel. One store API, one replace scope per producer, cache invalidation inside the door rather than at each call site, and a guard test that fails the build when something writes around it.
  • The rebuild-cost split. Classify derived data by what it costs to reproduce, not by what it says. It is the classification that lets undo, retention and refresh policies be written at all.
  • Resolution before graph. One answer to “which entity is this name”, before anything starts storing edges that depend on the answer.

If you are about to derive a structured model from a body of text, the first question is not which model or which schema. It is whether each derived fact will be able to name the sentence it came from, because retrofitting that later means re-deriving everything you have.


We build and maintain software systems, and develop the Quillsmith applications. This paper describes production architecture rather than a reference design; the behaviour described is from the shipping system and its dated design record.

All papers