An emotional cartography — nodes joined by warm and cold threads

Scriptorium · Engine Note

A Chart of the Human Heart The sparse, event-sourced character & world state graph

MODELsparse directed graph UPDATEevent-sourced · deterministic AXESvalence · trust · power · bond TESTS9 / 9 passing SCALE1000+ scenes, linear PLACEMENTCP-SAT · OR-Tools PACINGL4D AI-Director FACTIONSinfluence-weighted SCALEact · chapter · scene · beat VOICEfirst · limited · omniscient · narrated RESUMEchapter-by-chapter · idempotent BILLINGno Anthropic API · subscription

A language model can write a luminous scene. What it cannot do is hold the whole evolving social world of a novel in its head across a thousand scenes — who has cooled toward whom, who quietly betrayed whom four chapters ago, which faction is slowly turning. So we don't ask it to. The model writes the prose and proposes what changed; a small deterministic graph remembers everything, ripples the consequences to the people off-stage, and hands the next scene exactly the slice it needs.

This note explains the mechanics end to end — the model, the math, the guarantees — and shows them on a real cast generated by the engine: the novella The Drowned Accounts.


01Why it exists

The frozen graph

Every scene used to be written with a running recap — "the story so far." That works at fourteen scenes and is fatal at a thousand: the recap grows without bound, and it can never carry the current state of the eighty characters not in this scene. You cannot prompt scene #847 by re-reading 846 scenes of prose.

Worse, the relationships didn't actually move. The engine applied a character's fortune, mood, and beliefs each scene — but a relationship's stance was set once at casting and never mutated. The prose could describe a friendship souring while the modeled state sat perfectly still.

The fix is one idea: replace the unbounded recap with an authoritative, queryable graph that is the engine's memory — and let consequences propagate through it to characters the model isn't currently looking at.

02The model

Nodes, directed edges, four axes

Three kinds of node — characters, factions, locations — joined by typed, directed edges. Directed is essential: A can love B more than B loves A. The relationship edge A → B carries four axes, because a single "affinity" scalar is the best-documented mistake in the field (it makes antagonists who share scenes look "close," and collapses every kind of bond into one number).

valence ∈ [−1, 1] — warmth. The standing value; stance (for/neutral/against) is derived from it, with hysteresis.
trust ∈ [0, 1] — separable from valence. You can love someone you would never lend money. The circumplex says warmth and trust dissociate; so do we.
power ∈ [−1, 1] — status asymmetry on the directed edge. +1 = A holds power over B.
bond — the categorical type: kin, lover, ally, rival, mentor, employer…

A faction carries a slow disposition and an inertia weight; a character's affiliation to it carries its own valence — so a guild member can privately turn against the guild long before the guild itself moves. And a belief may name a subject — "A believes B is loyal" — decoupled from the truth. That gap is the engine of dramatic irony.

Customs Wardens Harbor Court Merchant Guildfaction · inertia 0.85 Dock Clansfaction · inertia 0.85 +0.70 +0.65 -0.20 -0.35 +0.25 +0.55 +0.30 +0.30 Yarashe/herprotagonist Solenhe/himmirror Veldhe/himfoil Nessashe/herally warm valencecold valencecooling · thickness = power
The cast of The Drowned Accounts as the engine generated it. Arrows are directed edges, coloured by valence (gold warm · blue cold), dashed where the bond is set to cool. Chips are faction affiliations. Note Solen: a Dock-Clan native who now sits on the Merchant Guild council — two memberships, two different loyalties.

The same graph, exactly

from → tobondvalencetrustpowerstancetrajectory
yara → solensibling — she is the y+0.700.75-0.35forcooling
solen → yaraelder sibling and erst+0.650.55+0.45forcooling
yara → veldformer credentialing s-0.200.30-0.55neutralcooling
veld → yaraformer credentialing s-0.350.10+0.70againststatic
yara → nessapetitioner and eventua+0.250.50+0.20neutralwarming
nessa → yarapetitioner who becomes+0.550.65-0.10forwarming
solen → veldfellow councillor held+0.300.35-0.40neutralcooling
veld → solenfellow councillor held+0.300.45+0.45forcooling

03The mathematics

One deterministic step per scene

The model never overwrites a number. Following Crusader Kings' insight that a relationship is best represented as a list of remembered reasons, each scene proposes named modifiers. An edge's value is its baseline plus the sum of its active modifiers:

$$ e^{\,\text{axis}}_{A\to B}(t)\;=\;\operatorname{clamp}\!\Big(\, b_{A\to B}\;+\!\!\sum_{m\,\in\,\mathcal{M}_{A\to B},\;s_m\le t}\! a_m\cdot \delta_m(t)\,,\; \ell,\,1\Big) $$

where each modifier m has amount am, birth-scene sm, and a decay factor δm. This is where the model earns its realism with two clocks:

$$ \delta_m(t)=\begin{cases} 1 & \text{permanent (no half-life): }\textit{standing}\\[4pt] \left(\tfrac{1}{2}\right)^{\!(t-s_m)/h_m} & \text{decaying with half-life } h_m\text{ scenes: }\textit{mood} \end{cases} $$

A permanent modifier moves the durable standing — a betrayal scars. A decaying one is a passing mood that mean-reverts toward the standing value as it ages (dropped once it falls below a small cutoff). RimWorld learned the same lesson: "killed my mother" never decays, but the shock of a shouting match does.

+0.0 -0.5 -1.0 scene 0 scene 2 scene 4 scene 6 scene 8 scene 10 the scene standing (permanent)mood (decaying)felt = sum
Two clocks. A permanent −0.5 to valence (the floor drops and stays). A decaying −0.4 with half-life 2 (the dip the reader feels for a few scenes, then forgets).

Stance, with hysteresis

The legible band for / neutral / against is derived from valence — but with asymmetric enter/exit thresholds, so a relationship hovering near a boundary doesn't flicker scene to scene (the failure that makes every game's social sim feel mechanical):

$$ \text{stance}(v,\text{prev})=\begin{cases} \textsf{for} & \text{prev}=\textsf{for}\;\wedge\; v\ge \varepsilon_{\text{exit}}\\ \textsf{against} & \text{prev}=\textsf{against}\;\wedge\; v\le -\varepsilon_{\text{exit}}\\ \operatorname{band}(v) & \text{otherwise} \end{cases}\qquad \operatorname{band}(v)=\begin{cases}\textsf{for}& v\ge \varepsilon_{\text{enter}}\\ \textsf{against}& v\le -\varepsilon_{\text{enter}}\\ \textsf{neutral}&\text{else}\end{cases} $$

with enter margin εenter=0.20 and exit εexit=0.05. You cross a higher bar to enter a camp than to leave it.

ε enter (+0.20) ε exit (+0.05) neu neu for for neu for for for neu for for for neu for for for neu for naive→ hyst → valence wobbles across the enter line; naive stance flickers, hysteresis holds "for"
Hysteresis. Once "for," valence must fall all the way past the inner margin to leave — no buzzing at the threshold.

Propagation — the consequence the model can't see

When a scene moves an on-stage edge sharply, the change ripples, in one bounded pass, to the people who aren't in the room. An ally of A cools toward B when A does — balance theory's "the enemy of my friend":

$$ \Delta e_{X\to B} \;=\; a \cdot \gamma \cdot e_{X\to A}, \qquad \text{for off-stage } X \text{ with } |e_{X\to A}|\ge \theta $$

damped by γ<1, capped at one hop, dropped below a magnitude cutoff — guards against runaway on a cyclic signed graph. And one invariant keeps it honest:

On-stage wins. The model's deltas for the characters in the scene are authoritative and are never overwritten by propagation. Ripples only reach off-stage edges the scene didn't touch. So the graph fills in the consequences the model couldn't see — it never argues with the prose the model just wrote.
ON STAGE (scene 3) −0.50 "you stripped my credential" ripple −0.50·γ·(Nessa→Yara) = −0.15 (off-stage, never overwrites the page) Yara Veld Nessa a stranger indifferent → no ripple
Yara turns against Veld on the page (solid). Off-stage, her ally Nessa cools toward Veld too — damped by γ and by how much Nessa sides with Yara (dashed). The indifferent never move.

Group inertia

A faction is a slow Markov state. Its valence is an inertia-weighted moving average of its members' — so the group always lags the individuals, and a member can defect long before the institution shifts:

$$ v_G \,\leftarrow\, w\,v_G \,+\,(1-w)\,\frac{1}{|G|}\!\sum_{i\in G} v_{i\to G}, \qquad w=\text{inertia}\;(\approx 0.85\text{ for a guild}) $$

04Watch an edge move

Real dialogue · real delta · The Drowned Accounts, scene 3

Pre-production set the standing relationships. Then the dialogist wrote a scene and, alongside the prose, proposed the modifiers it earned. Here is the engine doing the whole loop on one real scene — the confrontation in which Factor Veld clears Yara's desk and strips her credential.

The Drowned Accounts · Scene 3 · "The Cleared Desk"
Factor Aldric Veld — Archivist Vask. Please do not get up.
Yara Vask — Factor Veld. I have an open audit under Reference Seven-Four. If this concerns Lot Eight-Thirty, I should inform you the consignment file was reassigned before I could complete the provenance verification.
Factor Aldric Veld — Clerk, the order, if you please.
Yara Vask — The council member clearance. The countersignature.
Yara Vask — Each of these twenty-three counts references the consignment I flagged. The evidence of the irregularity is the evidence of the crime. I want you to understand that I understand what you've done.
Factor Aldric Veld — The order addresses certifications issued under your authority, Archivist, not the provenance of any subsequent report. The Code is quite precise on that distinction. Your seal, please.
Yara Vask — Tevan.
The clerk's shoes were the wrong kind for the quayside — thin city leather that tocked on the cobblestones, announcing each step. Yara had noticed this before she noticed Veld, and later she was glad of it: one moment of the morning that did not require her full comprehension.…

From that scene the dialogist proposed exactly these modifiers — and the graph folded them. Note the separable axes: Yara's valence toward her brother barely moves (she still loves him) while her trust falls hard. And the customs-warden slight is a decaying mood, not a permanent scar:

Yara Vask → Factor Aldric Veld · trust -0.25 · permanent · standing
+0.30 → +0.05
“Veld reframed her own audit evidence as the evidence against her — the credential she served was a weapon he held and she did not”
Yara Vask → Factor Aldric Veld · valence -0.25 · permanent · standing
-0.20 → -0.45
“He walked her out through the public entrance with two Wardens in attendance, pleasant and unhurried; procedure rather than malice, which made it cold”
Yara Vask → Solen Vask · trust -0.45 · permanent · standing
+0.75 → +0.30
“His countersignature in the council clearance block is not silence — it is a signature; the unanswered calls were one thing, this is another”
Yara Vask → Solen Vask · valence -0.20 · decaying · mood · half-life 5
+0.70 → +0.50
“She watched him take one step toward her on the quay and stop and turn away; she still loves him and she will not name what this cost for some time”
Solen Vask → Yara Vask · valence -0.10 · decaying · mood · half-life 3
+0.65 → +0.55
“He came to the quay, saw what his name on that order had made possible, took one step toward her and could not finish it; the turning away is not cold”
The auditor sent the first draft back — the relationship shift was asserted, not yet dramatized — then passed the revision (coherence 0.89 · fit 0.88). A delta only counts when it is earned on the page.

05Storage & scale

Event-sourced, in SQLite

The materialised graph lives in the story's JSON for short work. At novel scale it is backed by an append-only modifier log plus a per-scene snapshot in SQLite — the event-sourcing pattern. Any past scene reconstructs in O(1) from its checkpoint; a chapter can be repaired without re-running the book; and the log is the inspector's "why" trail. No graph database is needed at tens-to-hundreds of nodes across thousands of scenes. The decay sum is bounded to a fixed look-back window, so the whole pass stays linear in the number of scenes, not quadratic.

ConcernMechanismCost
Bounded context for scene Nk-hop neighbourhood of the present castO(neighbourhood)
Off-stage consequencesone damped propagation passO(edges·hop)
Reconstruct any past scenenearest snapshot + fold logO(1) at per-scene cadence
Two-clock decay over a long bookfixed look-back windowO(1) per edge

06Placing the scenes — the arc-fitter

CP-SAT · OR-Tools · PESP-isomorphic

Each character has an independent fortune curve, and the beats say where several of them must be at the same narrative moment. Making independently-arced lives cross at the right beat is a constraint-scheduling problem — the same shape as a PESP timetable — so we hand it to a CP-SAT solver. Narrative time is discretised into slots; each scene gets a slot variable.

hard — the sequence is strictly increasing (so the placement survives downstream), and scenes sharing a beat stay within a small window.
soft — minimise each present character's fortune deviation from its beat target (a table lookup on that character's sampled arc), plus drift from the authored plan.
$$ \min_{x}\; W_{\text{fit}}\!\!\sum_{s,\,c}\big|\,F_c[x_s]-\tau_{b(s),c}\,\big| \;+\; W_{\text{anc}}\!\sum_{s}\big|\,x_s-\hat{x}_s\,\big| $$
$$ \text{s.t.}\quad x_{s+1}\ge x_s+1,\qquad |x_a-x_b|\le W\ \ \forall\,a,b\in \text{beat} $$
+1 +0 -1 t=0 t=0.25 t=0.5 t=0.75 t=1.0 beat b · targets a=0.0, b=0.0 scene placed here char a (rising) char b (falling)
Two independent arcs — one rising, one falling — that the beat requires to meet at fortune 0.0. The solver slides the scene along narrative time to the slot where both arcs pass through their targets: the crossing.
ARC_FIT(beats, characters, scenes):                  # PESP-isomorphic; OR-Tools CP-SAT
  n  <- max(21, |scenes|+1);   grid t_i = i / (n-1)
  F[c][i] <- fortune_c(t_i)                           # sample each independent arc

  var x_s in [0, n-1]                                 # the slot each scene is placed at

  hard:  x_{s+1} >= x_s + 1                           # strictly increasing narrative time
         |x_a - x_b| <= W      for a,b sharing a beat # beat-mates stay together
  soft:  SUM |F[c][x_s] - target_{beat(s),c}|         # present c with a target: hit the arc
         SUM |x_s - slot(authored t_s)|               # don't wander from the plan

  minimise   W_fit * fit  +  W_anchor * anchor
  solve (5s);  t_s <- grid(x_s);   return scenes sorted by t
  # infeasible hard constraints -> raise (bounce to beat-planner); soft misses are reported
Targets are soft — the solver always returns its best placement and reports the worst residual, so an over-constrained beat is surfaced, not fatal. Only the hard constraints can make the model infeasible, and then it raises so the director can bounce back to the beat-planner.

One solve per chapter, not one for the book

At novel scale the single global solve is the wrong unit: a hundred scenes pinned across a hundred slots is a model large enough to grind, and the beats of chapter sixteen have nothing to say about the placement of chapter two. So the fitter now runs per chapter — per act. For each chapter it places only that chapter's scenes, bounded to the chapter's own tBounds, so the characters present there have their fortune arcs cross at that chapter's beats. The intractable global solve dissolves into many small, independent, fast ones — each a handful of scenes inside a known window — and the per-act decomposition this section already gestures at becomes literal. A short story is simply the degenerate case: one implicit chapter spanning the whole curve, so it takes the single global solve exactly as before, unchanged.

07The whole machine

End-to-end pseudocode

The model is the brain; Python is the nervous system. Every stage below is a single-purpose Claude Code subagent on the user's subscription; everything in the placement and assemble phases is deterministic Python — no model, no API.

GENERATE_STORY(premise, length):

  # ── pre-production · one Claude Code subagent per stage, on the subscription ──
  setup      -> genre, form, working title
  journey    -> fortune shape (Vonnegut / Reagan) + control points
  summary    -> synopsis
  locations  -> places + the journey DAG
  cast       -> characters (multi-axis edges, beliefs, own arcs) + factions + tension
  beats      -> plot beats on the curve, each with fortuneTargets {char -> fortune}
  storyboard -> N scene titles + purpose
  blocking   -> who is present + where, per scene
  chapters   -> group scenes into chapters (novella / novel)

  # ── placement · deterministic, no model ──
  for each scene:  beatId <- nearest beat by t
  scenes <- ARC_FIT(beats, characters, scenes)      # CP-SAT: arcs intersect at beats

  # ── prose · fractal and parallel ──
  PIN_SEAMS(chapters)                                # boundary states agree (breadth-first)
  PLAN_SCENES(between pinned bounds)                 # parallel, per chapter
  for each scene in parallel (<= GEN_WORKERS):
     repeat up to REVISE_CAP+1 times:
        draft   <- DIALOGIST(scene, NEIGHBOURHOOD, pinned bounds)   # prose + deltas + edgeDeltas
        verdict <- AUDITOR(draft)                    # coherence and fit both >= 0.8 ?
        if verdict = pass: break
     commit draft
  SEAM_AUDIT(every boundary: left <-> right <-> up); repair the broken ones

  # ── assemble · deterministic ──
  for each scene in order:
     apply fortune / mood / belief deltas
     APPLY_GRAPH_STEP(scene)                         # edges, ripple, bands, inertia
     snapshot to the event store                     # novels only
  set each character's final fortune from its arc

08The algorithms

Each deterministic step, in full

The graph step — one transition per scene

APPLY_GRAPH_STEP(scene):                              # one deterministic transition per scene
  for each edgeDelta (from -> to, axis, amount, halfLife?):
     if no halfLife:  edge[axis] <- clamp(edge[axis] + amount)   # permanent = STANDING (slow clock)
     # decaying modifiers are NOT stored; they live in the log and surface via FELT()
  ripples <- PROPAGATE(scene)                         # consequences for the off-stage cast
  UPDATE_GROUP_INERTIA()                              # factions lag their members (Markov)
  DERIVE_BANDS()                                      # stance (hysteresis) + trajectory

Propagation — the off-stage ripple

PROPAGATE(scene):                                     # one bounded pass; on-stage always wins
  for each sharp permanent valence shift a on edge A -> B  (|a| >= THETA):
     for each off-stage X with |val(X -> A)| >= ALLY_MIN:
        ripple <- a * GAMMA * val(X -> A)             # balance theory, damped (GAMMA < 1)
        if |ripple| >= CUTOFF and X->B was not set this scene:
           val(X -> B) <- clamp(val(X -> B) + ripple)

The two clocks — what a character feels

FELT(edge, axis, t):                                  # the value a character currently FEELS
  standing  <- edge[axis]                             # permanent residue (slow clock)
  transient <- SUM  a * (1/2)^((t - s)/h)             # decaying modifiers (s,h) in the last W scenes
  return clamp(standing + transient)                  # mean-reverts to standing as moods fade

The neighbourhood — what replaces the recap

NEIGHBOURHOOD(present, t):                            # bounded ego-graph; replaces the recap
  for each character c in present:
     bonds   <- { to, bond, FELT(valence), FELT(trust), stance, recent reasons }
     groups  <- { faction, standing, FELT(valence), stance }
     beliefs <- c.beliefs   (each with its optional subject)
  factions <- the groups in play
  return the slice   # O(neighbourhood), not O(book) -> a 1000-scene prompt stays flat

09Rupture — the dislocation of expectation

Rare · planned · irreversible

Every mechanic so far smooths: mean-reversion, inertia, hysteresis, damped propagation, an auditor that rewards continuity. Wonderful for coherence — and the enemy of the rug-pull. A rupture is the deliberate exception: a single scene where a high-confidence belief is shattered, dislocating expectation and setting an irreversible outcome in motion (a character flees and the story restarts; innocence is lost). Valve's own words for why it works: "Low probability + High drama = Memorable." So it is rare — at most one per story, usually none — and planned at the beat/arc level, sitting where the fortune curve drops steeply.

0.0 0.5 1.0 conf. ⚡ the rupture scene planted certainty (0.9) shattered (0.05) — irreversible dislocation = 0.85
The engine models expectation as a belief's confidence. A rupture detonates it in one scene — and the magnitude of the dislocation is exactly the confidence destroyed.
$$ \text{dislocation}\;=\;c_{\text{before}} - c_{\text{after}}\quad(\text{the confidence destroyed; bigger}=\text{more brutal}) $$

At a rupture the usual rules invert: the belief flips in one move (bypassing the gradual linear-threshold), the holder's factions shock in one step (inertia is bypassed — their world is upended), a permanent bond breaks, and the auditor's bar flips — it now passes the scene only if the certainty is genuinely shattered and the outcome irreversible, and it reads as retroactively inevitable. A rupture that stays smooth, hedges, or feels arbitrary is a revise.

10Tension & pacing — the AI Director

After Valve's Left 4 Dead (Booth, GDC 2009)

A story, like a co-op horror game, lives or dies on its pacing. Valve's L4D "AI Director" estimates a per-moment emotional intensity and shapes it into a sawtooth — because "constant, unchanging combat is fatiguing; long periods of inactivity are boring; unpredictable peaks and valleys of intensity create a powerfully compelling experience." We measure the same thing per scene, from state we already model:

$$ \tau(s)\;=\;w_t\,\text{threat} + w_v\,\text{vol} + w_c\,\text{conflict} + w_k\,\text{stakes}\,,\qquad \tau_{\text{rupture}}=1 $$
threat — low protagonist fortune (adversity)
volatility — steepness of the arc here (things changing fast — up or down)
conflict — antagonism among the characters on stage
stakes — strong convictions in play + nearness to a planned rupture
0.0 0.5 1.0 tension peak threshold ⚡ rupture (scripted peak) Build-Up Sustain Fade Relax
The pacing the Director shapes: Build-Up → Sustain Peak → Peak Fade → Relax, then build again. A rupture is a scripted peak (like L4D's Tank) that overrides the smoothing — and must be followed by a release. Flat-high fatigues; flat-low bores.
PACE(scenes):                          # the AI-Director, per scene (after Booth / Valve, GDC 2009)
  for each scene s in narrative order:
     intensity(s) <- w_t*threat + w_v*volatility + w_c*conflict + w_k*stakes   # in [0,1]

  # the rhythm must be a SAWTOOTH, never a flat line:
  Build-Up      while intensity climbs toward the peak threshold
  Sustain Peak  hold the peak briefly (a scene or two) after it crests
  Peak Fade     ease off — but only relax at a NATURAL break in the action
  Relax         a genuine breather before the next build

  flag if:  BORING (flat-low run) | FATIGUING (flat-high run)
            peak-without-release | front-loaded climax
  # a rupture is a SCRIPTED peak (like L4D's Tank) — exempt from smoothing, but it MUST release after
The engine annotates every scene with its tension, labels each with its pacing phase, and reports the rhythm — flagging a flat stretch, an unrelenting climax, or a rupture with no breather after it. The auditor is handed each scene's target tension, so "gripping where it should grip, easing where it should rest" becomes a quantitative fit check, not a vibe.

11Factions — beyond the average, beyond the moment

Influence-weighted · history-bearing

A faction is a node like any other, but its stored valence is a summary of many members' affiliations. The naïve summary — the plain arithmetic mean — is wrong twice over: it treats a guild's founder and its newest initiate as equal votes, and it forgets everything the moment membership shifts. Two fixes, one per subsection.

Not the plain average — who steers the faction

A faction's stance is not a referendum. It is steered by its high-standing members — the leaders and founders whose word carries — and pulled by a vocal minority who feel intensely, while tepid moderates fall quiet. This is settled social science: DeGroot showed group opinion converges as an influence-weighted pool, not an equal one; Katz & Lazarsfeld's two-step flow of communication found that group opinion is mediated by a few opinion leaders; and Noelle-Neumann's spiral of silence explains why the intense are heard and the indifferent are not. So we replace the mean with a weighted pool:

$$ V_{\text{faction}} = w\,V_{\text{old}} + (1-w)\,\frac{\sum_i \omega_i\,v_i}{\sum_i \omega_i}\,,\qquad \omega_i = \rho(\text{standing}_i)\,\bigl(1 + \beta\,|v_i|\bigr)\,,\ \ \beta=0.5 $$

Here w is the faction's inertia — the slow-clock self-weight from §07, unchanged — and ρ is the role weight: how much standing this member's voice carries.

RoleExamplesWeight ρ
steererleader · founder · head · chief3.0
second tierrival · defector · officer · captain1.5
rank & filemember · initiate1.0
marginalexile · enemy · outcast0.5
unknown—1.0

The intensity term (1+β|v|) is the vocal-minority boost: a member who feels strongly — high |v|, for or against — is louder than one who shrugs. A passionate dissenter outweighs a tepid loyalist; that is the spiral of silence made arithmetic. The crucial safety property: when every member shares the same standing and the same intensity, all ωᵢ are equal and the pool degrades exactly to the plain mean. The new term can only matter when there is genuine asymmetry to honour — so it never destabilises the simple case, and the §07 inertia / lag behaviour is untouched.

-1.0 -0.5 +0.0 +0.5 +1.0 member affiliation valence v 3 × member v=+0.2 ω = 1.0·(1+0.5·0.2) = 1.1 each lead leader v=−0.8 ω = 3.0·(1+0.5·0.8) = 4.2 plain mean -0.05 barely negative — leader drowned out weighted -0.36 clearly against — the leader steers dislocation 0.31
A four-member faction: three rank-and-file at v=+0.2 and one leader at v=−0.8. The plain mean is −0.05 — barely negative, the leader drowned out three-to-one. The influence-weighted value is −0.36: ρ=3.0 for the leader, amplified by the intensity of a strong conviction, flips the whole faction against. The marker areas are the weights ωᵢ.

A faction remembers — the founding grievance

A scalar valence is Markovian: it depends only on who belongs now, with no memory. Real factions are not. They carry institutional memory — a founding wound, a standing grievance, a "chosen trauma," traditions and prior decisions that bias behaviour regardless of the present mood. A guild born from a massacre acts like it, even in a calm decade. We honour this honestly, and cheaply.

v0 — one permanent scalar For short stories we do not add a decaying memory array. The LLM writing the prose is the real non-Markovian engine. We add a single permanent string per faction, foundingGrievance — a non-decaying bias (unlike the slow/fast clocks of §07) fed into the dialogist's scene context as "permanent — colors this faction in EVERY scene, never decays." The faction's history shows on the page.
novel — the decaying memory[] The full Crusader-Kings-style memory[] — a list of labelled, half-life-weighted grievances / traditions / triumphs that actively pull the faction's valence — is a deliberately deferred extension, gated on meta.form == 'novel'. That is where a resurfacing grievance becomes a plot beat. It is intentionally not built yet (YAGNI for six-scene stories), not an oversight.
faction_context(f, scene):                   # what the dialogist is handed about a faction
  valence  <- influence_weighted_pool(f)     # §11A — steered, not averaged
  if f.foundingGrievance:                     # ONE permanent scalar string (v0)
     present "PERMANENT — colors this faction in EVERY scene, never decays:"
     present f.foundingGrievance              # the non-decaying floor under the moving valence
  # NOT built for short stories (YAGNI):
  #   memory[] — decaying, half-life-weighted grievances/traditions/triumphs (CK-style)
  #   gated on meta.form == 'novel', where a resurfacing grievance IS a plot beat
A permanent floor under the moving valence keeps the change cheap and the engine honest: a short story gains a faction's history with one string and zero new dynamics, while the expensive decaying machinery waits for the form that actually earns it.

12Scale — the book as a hierarchy of arcs

From a flat list to a nested form

Scriptorium began the way every prototype should: a single flat list of six scenes hung on one global fortune curve. That is the right shape for a short story and the wrong one for a novel. A book is not a longer short story; it is a nested thing, and so the engine now grows a hierarchy when the form warrants it — Story → structure[] (the acts) → chapters[] → scenes[] → beats[]. The short story keeps its flat list, untouched; the larger forms acquire the floors above it.

Because the structure is real, the scene budget is now derived from it rather than declared as a flat constant. The arithmetic falls straight out of the hierarchy: a novel is 18 chapters × 4 scenes = 72; a novella is 6 × 3 = 18; a short story stays a flat 6, with no chapters at all. The length of the book is a consequence of its skeleton, not a number chosen beside it.

The structure-planner — a skeleton before any scene

A new pre-production skill, the structure-planner, lays down that act-and-chapter skeleton before a single scene is written. Each chapter is born carrying its own contract: a goal, a value-shift — the emotional polarity it must travel from → to, the same begin-to-end logic as the global shape but local to the chapter — an exit hook to pull the reader across the chapter break, and a point-of-view character through whom the chapter is seen. The book knows what each of its chambers is for before it furnishes any of them.

Chapter-first — growing chapters, not slicing them

This reverses the order of construction. For form ∈ {novel, novella} a structure stage runs ahead of scene generation and grows the chapters from the planned outline — each chapter materialised with its own scene purposes — rather than writing one long flat scene-list and slicing it into chapters after the fact. Build up from intent; do not cut down from a finished slab. The path is gated behind the SCRIPTORIUM_CHAPTER_FIRST environment flag, default-on for those two forms; set it to 0 to fall back to the legacy slice path. A short story never enters the chapter-first path at all — it has nothing to grow.

Holding a long book together

Length introduces a problem the short story never had: continuity that must survive across dozens of chapters a model cannot hold in its head at once. Two additive structures carry it. A Story Bible — meta.bible — is the ledger of canonical facts that every later chapter must stay true to; and threads[] are the running storylines, each tracked across chapters so a promise made in chapter two is paid in chapter sixteen rather than quietly forgotten.

The decisive point is what does not change. The affective machinery of the earlier sections is not replaced by the hierarchy — it scales into it. The arc-fitter of §06, the tension and pacing of §10, and the rupture of §09 now operate across the chapter structure: each chapter is a tension sub-arc nested inside the global shape, with its own build and release, while the whole still rises and falls as one curve. And the event-sourced graph store of §05 persists the character and world state across the entire book — which is precisely what makes novel-length continuity tractable for a model that cannot hold the whole book in its context. The graph remembers so the model need not. The cast scales to match: the casting cap lifts from the 3–5 named characters of a short story to 15–40 in a novel.

Nothing in §§03–11 is thrown away to reach novel scale. The same edges, the same sawtooth, the same dislocation math run inside each chapter; the hierarchy only gives them a larger room to run in, and the graph store carries their consequences from the first chapter to the last.

13Voice — who tells the story

How it is told, apart from what happens

Everything to here has been a chart of what happens — the heart's motion, the faction's drift, the dislocation of a belief. Distinct from all of it is how the happening is told. The narrator layer gives each story a chosen viewpoint, and that choice colours every line of prose without altering a single fact beneath it. There are four: first, third-limited, omniscient, and fully-narrated.

viewpoint is the authoritative field. The older person field — third versus first — is now derived from it at seed time rather than set independently, so the two can never disagree: first → first, third-limited → third-limited, omniscient → third-omniscient, and fully-narrated → third-omniscient. One field is chosen; the other follows.

Four voices, four instructions

Each viewpoint injects its own prose-instruction block at generation time, so the difference is written into the page and not merely labelled on it.

first — keeps interiority bound to the point-of-view character; the telling lives inside one mind, in I / we.
third-limited — stays close over a single character's knowledge within a scene; the camera sits behind one pair of eyes and learns only what they learn.
omniscient — moves freely between minds, privy to more than any one character can know.
fully-narrated — gives a present narratorial voice that addresses the reader and shapes the telling, a teller who is felt on the page.

The interject gate

An interject toggle — meta.narrator.interject — lets the narrator step in with wit or inner monologue even in third person, so a limited or omniscient telling can earn a flash of voice without abandoning its stance. The gate governs only the other three viewpoints: fully-narrated always narrates, by definition, so the toggle leaves it alone and presses only where a narrator's intrusion is a choice rather than a given.

Voice and chart are orthogonal. The graph decides what is true and how it moves; the narrator layer decides through whose eyes, and in whose voice, that motion reaches the reader — and the two are tuned independently, so the same charted heart can be told four different ways.

14The unfinished book — resumable generation

Written in chapters, stopped at the seam, resumed where it broke off

A novel cannot — and should not — be written in a single pass. The context window will not hold eighty thousand words, and the cost of trying would be paid for nothing the moment the run faltered. So a long book is generated the way it is read: chapter by chapter, with the freedom to stop between any two and pick the work up later exactly where it was set down.

The mechanism is deliberately spartan: there is no new checkpoint artifact. The persisted StoryState on disk is the checkpoint. A scene is done by a single, legible criterion — it carries audited prose — and that fact is already written into the state the engine saves. Nothing about completion needs to be recorded twice.

Walking the chapters, flushing at each seam

Generation walks the chapters in order. After each chapter it flushes a checkpoint to disk, stamps meta.genProgress — chaptersDone / total, scenesDone / total, and the currentChapterId — emits a progress note, and is free to halt. The chapter boundary is the safe seam: the same seam the structure-planner of §12 furnishes, now doubling as the place a run can be interrupted without leaving the book in an incoherent half-state.

Idempotent restart — finish only the remainder

On restart the engine scans for the first scene without prose and continues there, skipping everything already written. This makes generation idempotent in the strict sense: running a finished book regenerates nothing, and running a half-finished one completes only what remains. A run stopped mid-chapter loses at most the single scene in flight — every chapter sealed before it is already on disk, audited and done.

$$ \text{resume-from} \;=\; \min\,\{\, s \;:\; \operatorname{prose}(s)=\varnothing \,\} \qquad\Longrightarrow\qquad \operatorname{Gen}\circ\operatorname{Gen} \;=\; \operatorname{Gen} $$

GET /genstatus surfaces that same progress to the dashboard, so a long run is legible while it is still being written. And this is not merely a recovery convenience — it is the serialization runway itself: write N chapters, pause, resume, repeat, is precisely how an 80–120k-word novel is actually produced. The pause is a first-class part of the method, not an exception to it.

Form-gated, like everything expensive in this engine. A short story still generates in a single uninterrupted pass — it has no chapters to seam, and no need of a runway — so the canonical 161-event sample stream is untouched. The resumable machinery only switches on for the forms that cannot be written any other way.

Scriptorium · engine note · generated for The Drowned Accounts. Hero image: Gemini 2.5 Flash Image (the one non-Anthropic call; all text runs on the Claude subscription). Design & validation: docs/DESIGN-character-world-graph.md. Implementation: service/scriptorium_svc/graph.py, graphstore.py. Math typeset with KaTeX.