MeridiansMeridians

Branch refinement — Meridians World Search (MWS): Monte Carlo tree search over a recorded world

Source path: knowledge-base/specs/branch-refinement/README.md

# Branch refinement — Meridians World Search (MWS): Monte Carlo tree search over a recorded world

Status: **rough spec (direction).** Not an implementation contract; nothing here ships until scheduled.
Manual refinement (Futures, regenerate, edit, delete) is the shipped surface and stays the override.

Meridians World Search (MWS) is the name of this search: a Monte Carlo tree search over a world's meridians (its fate pathways) that develops the world at the forks where outcomes are most uncertain and most costly and prices each pathway. It proposes and prices; it never chooses.

The idea in one line: **a branch tree is a bonsai, and MCTS is how a bonsai is grown.** Nothing is cut by
decree; branches that move the world get light and are deepened, branches that move nothing are starved
and wither. Today a Domain is authored once — a v1 with no selection pressure, every branch shipped at
fitness 1 — and refined by hand when someone notices. The future is an **opt-out, autonomous refinement
loop** that keeps searching and re-weighting the tree the world already has, so the visual novel a reader
meets is the survivor of a search, not the first draft. We call the loop **MWS**: well-travelled
paths through a world's possibility, found by visiting, valued by consequence, kept by an honest ledger.

MWS is what makes Meridians a **causal simulator** rather than a story generator, and it is the
first priority among the specs ([specs map](../README.md#priorities-2026-09-21)). Given a situation — a
world we made, or one built from a person's own circumstances — it spends its effort on the
high-uncertainty, high-consequence forks and returns the pathways with their costs, so a person rehearsing
inside the world can see the game being played (incentives, information asymmetry, timing, hidden
constraints), try a route, see what it caused, and retry. It exposes the board; it never plays it. Under
the VNMMO the same search runs more automated, as a shared tree with a value per seat (§12) — a stretch
goal on a separate roadmap, built on this loop.

This is not a new generator. [Fate exploration](../fate-exploration/README.md) already fans sisters at a
fork, already scores them with free deterministic derivations (fatefulness, orthogonality), and already
names an upper-confidence rule for choosing where to fan next (§5 step 1). This spec **completes that rule
into the full four-step MCTS loop** — select, expand, simulate, backpropagate — and makes it the standing
process a Domain runs between readings, rather than a cycle an author launches. MCTS is used for
**refinement and continued exploration of a tree that exists**, not for generating a story from nothing.

Authority is unchanged:

```text
Domain defines. Branch preserves. Scenario resolves. Episode embodies. World inherits and lives.
```

Read first: [fate exploration](../fate-exploration/README.md) (§4 scoring, §5 policy, §6 promotion),
[visual-novel branching](../visual-novel/branching.md) (the Reading, fan selection, branch-and-bottleneck),
[training worlds](../training-worlds/README.md) (the canonical route and the Rasch reading the tree serves),
[latent traits](../latent-traits/README.md) (the person's record, and the target band it hands the flag plan),
[the pentad](../pentad/README.md) (the currency every value below is stated in).

The subsystems it reads: `src/types/domain/branch.ts` (`Branch` — `parentBranchId`, `forkEntryId`,
`entryIds`: the tree is already a tree), `src/lib/core/graph/branch-tree.ts` (`resolveBranchTreeParents`,
`layoutBranchTree`), `src/types/domain/futures.ts` (`Future`, `FutureRun`, `priorLogit` — the prior),
`src/lib/core/forces/thread-market.ts` (`outcomeProbabilityDeltas`, `normalizedEntropy`, closure tests),
`src/lib/core/forces/composition.ts` (`openMassSeries`, `consequenceShareSeries` — the value),
`src/lib/core/forces/record-health.ts` (`recordHealth` — the existing "what should the author look at"
seam), `src/types/domain/settings.ts` (`AutoConfig`, `autoExecute` — the attributable mandate for
unattended runs).

---

## 1. Why MCTS and not the alternatives

The tree we hold has three properties that pick the algorithm:

- **It is deep and the payoff is late.** A fork at arc 2 matters because of what it does at arc 6. Beam
  search prunes hard and early and kills exactly that branch. Genetic search explodes breadth without depth.
- **Every branch has a real, free value.** Fatefulness and consequence share are derivations of the record,
  not a model's opinion — so a tree-search *value function* exists without training anything.
- **We are never done.** A Domain keeps being read; the search must be **anytime** — well shaped at every
  budget, better with more.

MCTS with an upper-confidence bound (UCT) has all three: it converges to the best subtree given enough
visits, it deepens only what has earned depth, and it can stop at any moment and leave a good tree. Its
prior-guided form (a policy prior times an exploration term, as in AlphaZero's PUCT) maps onto what we
already store: `priorLogit` is the world's belief in a future; visits are what readers and scouts actually
did with it.

---

## 2. The loop, in Meridians terms

One **refinement step** costs one scout-depth continuation (cents, per fate-exploration §3). A **run** is a
budget of steps. Each step:

1. **Select.** Walk from the canon root down the branch tree, at each fork taking the child that maximises

   ```text
   score(child) = Q(child) + c · P(child) · √N(fork) / (1 + N(child))
   ```

   where `Q` is the child's mean value (§3), `P` its prior (§4), `N` visit counts (§5), and `c` the
   exploration constant. Stop at a fork that is *expandable*: fewer than `k` accepted sisters, or a sister
   whose consequence has never been measured to depth `d`.

2. **Expand.** Fan one new sister at that fork with mandated divergence (fate-exploration §5 step 2: opposed
   `Future` variables, siblings passed as `considered`). Delta-gate it: empty or duplicate deltas mean it is
   not a candidate, not a low-scoring one.

3. **Simulate.** Roll the sister forward at scout depth for `d` arcs (default 2 — enough for `opens` to
   fire and for downstream threads to move). No prose, no assets; `WorldExpansion` deltas only. This is the
   rollout, and its whole cost is the scout continuations.

4. **Backpropagate.** Compute the rollout's value (§3) and add it to every node on the selected path; bump
   their visit counts. A fork whose new sister moved the world raises the value of every ancestor that led
   to it — that is how a deep payoff finds its way back to an early fork.

Repeat until budget, floor, or manual stop (fate-exploration §5 step 5). The tree after a run is the same
canonical `Branch[]` — with more sisters where the world was alive, none where it was not, and a visit and
value ledger over all of it.

**Simulation is scouting, not production.** A rollout never becomes a reader-facing branch by itself. It
earns production through the existing promotion ladder (fate-exploration §6), which now reads `Q` and `N`
instead of a single fatefulness snapshot.

---

## 3. The value function — bits, never opinions

`Q` must be deterministic, free, derived from state, and stated in the pentad's currency. Four terms, all
already computed:

- **Consequence** — belief movement downstream of the fork attributable to the sister's scene(s):
  `consequenceShareSeries` summed over the rollout window. This is fatefulness with a horizon.
- **Divergence of futures** — how far the sister's rolled-forward open mass (`openMassSeries` /
  `H_open` over threads) has moved from its siblings'. A fork whose children converge back within `d` arcs
  is not a fork; it is a detour. Jensen–Shannon divergence between the children's belief trajectories is
  the measure; direction disagreement on a shared thread is its strongest form.
- **Structural change** — `systemDeltas` / `ownershipDeltas` in the rollout, weighted as fate-exploration
  §4 already weights them.

- **Reach** — how far the sister's Flags compound: the Flag→Flag dependency graph the Pulse already reads
  (`closureGraph`, `closureReach` in `core/forces/closure-graph.ts`) gives each collapse a downstream count
  and a landing — whether its consequence stayed in its own thread or touched World / System state. A
  rollout that opens a Flag other Flags come to rely on is worth more than one that settles a question
  nothing else needs.

Will enters only through Causality: the search prices futures over the existing Flag tree, and the visual-novel turning-point rule places a reader decision where the ring hands the reader the Causality input. The constitution is a prior on which hops are cheap, never a chooser; MWS can price a future without deciding it. In the Readings pipeline, that prior is a System 1 score over generated candidates, not a field emitted by candidate generation.

One value, in nats, per rollout. **Where this value points is the whole of MWS's search allocation.**
Point it at plausibility and the search grows the safe, predictable tree every generator already writes.
Nothing above rewards plausibility: the terms pay for belief that *moved* and consequence that *compounded*,
so an impossible premise with a rigorous record out-values a probable one whose forks rejoin. The wire
(§4) decides which branches may exist; the value decides which get light. **The value is a readout, never
a training target** (fate-exploration §4, second hard rule). It selects what to explore and what to promote; it is never fed back into a prompt as
"write a high-value arc", and it is never tuned to make the numbers rise.

For a **training world**, one more term is admissible and only there: **item information**
(training-worlds §5a) — how much a fork pitched inside the hour's **target band** would tell and teach. The
band is a property of the flag plan: latent-traits steering writes it there (a difficulty range on a named
axis, ±1 logit wide) from the person's or cohort's reading ([latent traits §4](../latent-traits/README.md#4-steering--the-reading-generates-the-next-hour)),
and the search reads only the band. A fork nobody would find hard or easy is worth less to a course than
one pitched at its cohort. This is a property of the fork, not a judgement of a person (§7).

---

## 4. The prior — the world's own belief, then the author's

`P(child)` is the softmax over sisters of `priorLogit` — the world's belief in that future at the moment it
was read (`futures.ts`). That is the same quantity the shipped Futures panel already shows. Two overrides,
both attributable:

- **The canonical route.** In a training world the author's good route (training-worlds §3) carries a
  boosted prior so the search deepens around it first — the bonsai's copper wire. The prior is calibrated
  against players later (training-worlds §5a); the search does not need to wait for that.
- **A pin.** An author may pin a branch (prior → 1 at its fork) or fence one off (prior → 0). Both are
  typed actions on the record and both are visible in the ledger. This is what "manual refinement stays
  the override" means concretely: the author steers the prior, the search does the visiting.

Never a model asked "which future is more likely?" The prior is what the record believed.

---

## 5. Visits — scouts and readers count alike

`N(child)` counts two things and keeps them distinguishable:

- **Scout visits** — rollouts that passed through this branch.
- **Reader visits** — commitments at this fork by an actual reader or seat (visual-novel §3), attributable
  Will from outside the simulation.

A reader visit is worth more than a scout visit to the *search* (it is evidence the fork is where people
actually stand), but it never writes to `Q` directly — a reader choosing a branch is a fact about the
reader's Will, not about the world's consequence. Readers steer *where the search looks*; the record decides
*what it finds*.

Storage: visits and values are a **derived ledger over the branch tree**, keyed by `Branch.id`, recomputed
from the record and the rollout deltas. No stored `visits` field on `Branch`; nothing here adds a second
source of truth or a stored position register.

---

## 6. Pruning — starved, never cut

Selection is the pruning. A branch with low `Q` and a fading exploration bonus simply stops being visited:
no new sisters, no deeper rollouts, no promotion. Retention follows fate-exploration §5: keep the top `m`
per fork plus every branch any reader has visited; nothing referenced by a save, route, or produced asset is
ever deleted. A withered branch is still a parallel world someone might return to; it has just lost the sun.

The bonsai reading, stated once: **the wire is the prior, the light is the value, the visits are growth.**
An author who wants a shape wires it; the search grows toward light within that wire.

---

## 7. Boundaries — what the search may and may never do

- **Exploration proposes; it never commits.** Unchanged from fate-exploration. A rollout stages what a
  future would cost the world; taking it belongs to a reader, a seat, a Director's `autoExecute` mandate, or
  an agent through MCP. The search never selects the active branch, never resolves a Scenario, never
  writes a Position.
- **Opt-out, not silent.** A Domain runs refinement by default between readings within its budget; the
  owner can pause or fence it per Domain, per branch, or per fork. Every run leaves a ledger entry: budget
  spent, forks expanded, values before and after. `recordHealth`'s "what the author should look at" is
  where the run reports — it proposes; the author looks.
- **The player is never the thing selected against.** In a training world the value is a property of
  forks and futures — consequence, divergence, item information — never of a person. No term in `Q` reads
  a player's θ, response, or history; the one thing the person's record may hand the search is the flag
  plan's target band, and that hand-off is latent-traits steering, not the search. The search shapes the
  world the player meets; it does not shape a
  verdict about them (latent-model §2, training-worlds §5a).
- **No second canonical state.** Branches, scenes, deltas stay the record. The search's ledger is derived.
  Rollout deltas that never earn promotion are retained as candidates exactly as fate-exploration retains
  them today.
- **Reader-facing surfaces stay deterministic projections.** The reader's fork shows accepted sisters and
  their rarity (visual-novel §2); it never shows `Q`, `N`, or the exploration bonus.

---

## 8. Why this serves the visual novel and the training world

- **The fork is real because it survived.** A sister that reaches a reader has out-valued alternatives that
  were actually rolled out — the fork carries divergence of futures by construction, not by prompt.
- **The tree has the right shape.** Branch-and-bottleneck (visual-novel §6) falls out: the search deepens
  where consequence persists and lets detours rejoin, because rejoining scores zero divergence.
- **Continuations are the unit of production, not new worlds.** A fresh Domain is a table with no history:
  nothing staked, nothing carried, nothing compounding — which is why generated prose on its own reads as
  worthless to the people it is written for. A continuation inherits the record, so a Flag from the last
  hand still binds this one. The search is cheap on continuations (the tree already exists; only the
  frontier is rolled out) and expensive on new worlds, and that is the right way round: fewer worlds, kept
  open longer, with deeper chains.
- **Analysis first; generation deals.** The scarce thing in this loop is the book — the record, the belief
  market, the Flags, the dependency chains, the ledger of what readers staked and how it settled. Prose is
  the cheapest input the system has and is treated that way: dealt on demand at the frontier, never the
  artefact the world is proud of. Every reader-facing surface the loop touches should show what a fork
  *did*, not how well it was written.
- **Hours compound.** For the concierge journey, the refinement loop is what turns "AI generates a new hour
  every week" into "the hour was searched": every played hour's forks feed visits back, the next hour's tree
  is grown from a better-valued prior, and the author's wire stays in force. A training world that survives
  is one whose forks moved belief and pitched at its cohort — the world is selected, the person is measured.
- **Manual refinement becomes steering.** The author's time moves from fixing v1 branches to wiring the tree:
  pin, fence, boost the route, and read the ledger.

---

## 9. The honest ledger

| Piece | Status | Where it lives |
|---|---|---|
| Branch tree with fork pointers | shipped | `Branch`, `branch-tree.ts` |
| Fan of sisters with mandated divergence | shipped (Futures) | `futures.ts`, batch runner |
| Fatefulness and orthogonality score | build (fate-exploration §11) | new pure derivation in `core/forces` |
| Consequence share, open mass | shipped | `composition.ts` |
| Value function `Q` (§3) | build | pure, over rollout deltas |
| Prior `P` from `priorLogit`; pin / fence actions | prior shipped; pins build | `futures.ts`; reducer + `ACTION_META` |
| Visit / value ledger (§5) | build | derived projection keyed by `Branch.id` |
| UCT select → expand → simulate → backprop (§2) | build | Tier-2 operation over the scout lane |
| Budget bound, stop rules | shipped shape | `AutoConfig`, fate-exploration §2 |
| Run report into record health | build | `record-health.ts` |
| Opt-out per Domain / branch / fork | build | settings + typed deltas |
| Training-world item-information term | build, gated on §5a | `rasch.ts` (unbuilt) |
| Per-branch Iterate sessions (`revisions[branchId]`), rename, `read-iterate`, headless `iterate-turn` (§10) | shipped (2026-09-18) | `revision.ts`, `ops/handlers/iterate.ts`, `query-catalog.ts` |
| Branch-keyed operation lock (sister tips concurrent, one tip ordered) | shipped | `ops/client.ts` `operationTargetLockKey` |
| One live Scenario game per branch tip | shipped | `ops/handlers/game.ts` |
| Windowed / as-of Iterate context; Flags + reach as Iterate cues (§10) | build | `iterate-executor.ts` `buildIterateContext` |
| Driver-side turn memory across headless turns (§10) | build | `iterate-turn` `history` |

---

## 10. Refinement inside the loop — Iterate as the per-node refiner

MCTS as written above moves *structure*: it fans sisters, rolls deltas forward, and values consequence.
A visual novel also needs the *material* on a surviving branch to be good — scene structure, plan, prose,
questions, script — and that is Iterate's job. The full loop a driver runs is therefore

```text
explore (fan sisters at a fork)  →  refine (Iterate the frontier scene's cells)  →
generate (prose / script for the rung it earned)  →  refine (Iterate against the grader)  →  explore …
```

Iterate is the **refine** step, and it must be addressable per node, which is why an Iterate session is
a property of a **branch**, not of a Domain: `revisions[branchId]`, one session per tip, sister tips
running concurrently, exactly as Scenario holds one live game per tip. A driver — the shipped UI, an agent
over MCP, or a future MWS operation — works the loop with four moves and nothing else:

| Move | Surface | Jurisdiction |
|---|---|---|
| read the frontier | Tier-0 `read-iterate`, `read-branches`, force/flag reads | resolved view of one `(domainId, branchId)` |
| act on it | Tier-2 `iterate-turn { branchId, turn, goal? }` | writes only into that branch's session shelf |
| settle it | `MERGE_REVISION` / `DISCARD_REVISION` / `RENAME_REVISION` through dispatch | that branch only; siblings untouched |
| score it | deterministic derivations (§3) over the branch's resolved record | readout, never a prompt |

The same techniques plug in as different *explore* policies over the same refine step — Reading
probability (a Reading's priced fan), Scenario probability (seats resolving at a table), Flag probability
(which Fate questions collapse) — and MCTS settings (`c`, `d`, `k`, budget) are per-Domain constants; none
of them changes the refine contract. Two boundaries carry over unchanged: **refine never chooses** (an
Iterate turn improves the material of the branch it was pointed at; selecting which branch is a
participant's or the driver's attributable act), and **the score never enters the prompt** (§3).

### Scalability and context cues — what the audit found (2026-09-18)

Grounded against `ops/client.ts`, `iterate-executor.ts`, `mcp/catalog.ts`, `ops/handlers/game.ts`:

- **Concurrency is already branch-shaped.** Effectful operations take a branch-keyed in-process lock:
  sister tips run concurrently, one tip stays ordered whatever the trigger (cron, MCP, HTTP). Iterate
  now inherits that. Cross-process ordering (two hosts on one record) still rests on run-id dedup.
- **Iterate context grows with the timeline.** `buildIterateContext` lists every entry in the branch's
  resolved sequence with its version cells. Linear in scenes; fine at tens, not at hundreds. Needed
  before deep trees: a window (frontier ± *n*, as-of cursor) plus cheap read tools over the rest
  (`search_prose`, `read_flags`, `read_thread`) so grounding costs less than opening every scene.
- **Cues are thread-level, not flag-level.** The Iterate brief carries open Threads and downstream
  branch digests but not the branch's Flags, their reach, or which Fate question the frontier scene is
  meant to move. Those are exactly the cues a refine step under MWS needs; the Pulse already
  derives them (`closure-graph.ts`). Add them to the brief as a `<causality>` block, readout only.
- **Headless turns are memoryless.** `iterate-turn` runs each turn with empty history; the revision plan
  and summary are the only carried state. That is deliberate (the plan is the memory, and it is
  inspectable) but a driver running many passes should be able to pass a bounded prior-report digest.
- **Sessions are visible, sessions-of-sessions are not.** `read-iterate` shows every open session with
  `+N −N` and plan state. There is no ledger yet relating sessions to the visit/value ledger of §5 — when
  that ledger is built, a merged session should stamp the branch's node so the search can see that a
  tip was refined, not only that it was rolled out.
- **Scenario and Iterate must not share a tip blindly.** A live game and a live Iterate session on the
  same branch both write that branch; the branch lock orders them but does not reason about them. The
  driver should not refine a tip while its Scenario is mid-resolution.

---

## 11. Risks, and the test that would falsify this

- **Goodhart through the generator.** If the value leaks into prompts, the scout learns to write
  world-shattering nonsense. Held by §3's readout rule; detectable as `Q` rising while reader-observed
  consequence (visual-novel §10 telemetry) does not.
- **Convergence to one strand.** Too small a `c` and the tree deepens one route and starves the fork the
  reader wanted. Held by keeping `c` per Domain, reader visits in the exploration term, and the retention
  floor; detectable as sister count per reachable fork falling below `k`.
- **Rollout depth too shallow to see consequence.** `d = 2` may miss payoffs that fire at arc 4. Test on
  the six recorded exports: does `Q` at depth 2 rank forks the same way the full record does at depth 6?
  If not, the depth is wrong, not the idea.
- **Cost drift.** A run is bounded by the same budget lane as exploration; the falsifier is a run whose
  spend per unit of `Q` gained rises across cycles — the tree is saturated and should stop.

The falsifying experiment for the whole spec, cheap and read-only: take one recorded Domain with a deep
branch tree, replay MCTS over its *existing* branches (no new generation — visits and values only, rollouts
replaced by what the record already holds downstream), and check whether the forks it would deepen are the
ones readers actually diverged at. If the search and the readers disagree about where the world is alive,
the value function is wrong and nothing above should be built on it.

---

## 12. Open decisions

- Rollout depth `d` and the exploration constant `c`: per Domain constants, or derived from the Domain's
  settling rate (volume decay, volatility)?
- Whether reader visits should carry a decaying weight so an old cohort does not pin the search forever.
- **Stake.** The reader's counterpart to a scout visit: before a resolving scene, a reader commits a
  distribution over a Flag's outcomes; the record scores it in nats when the Flag collapses. It is the one
  move that gives a reader something of their own at risk on a table where the prose cost nothing, and it
  yields a second reader signal beside the Rasch item (a log score over one's own stakes — calibration,
  where the item measures seeing the right move; latent-traits §2 keeps both on the person's record).
  Constraints if built: the stake is belief, never money; the house edge is
  zero by construction — a reader's expected score rises with calibration and the world takes no cut; it
  scores a reading of the world, never a person; it steers visits (§5) and never writes `Q`.
- **Toward the shared tree — the pocket dimension's search.** Several seats committing at one fork make
  the value a vector (one entry per seat) and selection per seat (multi-agent / max-n MCTS); many timelines held live at once is tree
  parallelisation with virtual loss, and the visit distribution over branches is the posterior over which
  timeline the world is in — the quantity a Reading already prices; with generation in the loop, nodes
  group by decision sequence rather than exact state (open-loop search), which is what makes rejoin and
  bottleneck cheap. Boundary unchanged and harder: the search prices seats' joint futures, never chooses
  for any seat, and one seat's visits never write another seat's value. This is the form MWS takes
  under a populated World ([world roadmap](../world/README.md#what-the-gates-are-for--three-forms-one-substrate));
  it is not scheduled before a bounded multi-seat run has been played. Before seats, the same search
  grows the **shared multiverse**: timeline-dated VN releases cut from earned tips, seeds founding new
  Domains, and the Settled Layer a universe's surviving branches agree on ([world tree](../world-tree/README.md)).
- How a fenced branch reads to a reader who already committed to it — invisible in new readings, preserved
  in saves is the default.
- Whether the run schedule is "between readings" (idle) or "after each reading" (event), and who pays when a
  Hosted tenant has many readers and no idle.
- The exact shape of the ledger entry and where it surfaces beyond record health.
Open on GitHub

Raw Markdown source