The mechanic, and why
History should bend by STEPPING STONES, not blasts. A message decays in power the further, in years, it lands from your nearest FOOTHOLD — and your footholds are the objective's anchor year plus every change you've already landed.
So chaining forward is the rewarded path: a far-back seed (Leonardo + gunpowder, aiming at the Moon by 1900) lands modestly but plants a foothold; the next move near it lands strong; the chain marches to the event, compounding. A lone deep-past blast (Caesar in 50 BC to change 1960) is diffuse by design — a sliver even on a crit. It's the opposite-pulling sibling of the anachronism amplifier, and it closes the roadmap's known-open balance hole (max-anachronism was EV-positive; the decay diffuses far, unbridged reaches toward zero).
The pure dial
The whole mechanic is one pure module — no model judgment, no calendar math, just signed years we already hold. nearestFootholdGap finds the smallest year-gap to any foothold; chainDecay maps that gap to a factor: full power within a bridge-free radius, then exponential decay to a floor that's never zero (diffuse, not blocked — rails, not walls). The constants are calibrated to the two anchor cases in the docstring: a ~400yr opening reach stays a worthwhile seed (~0.55); a ~2,000yr lone leap bottoms out at the floor.
nearestFootholdGap → chainDecay → chainStatus. A factor of 1 (near a foothold, or no foothold to measure) is a clean no-op.
utils/causal-chain.ts · 129 lines
⋯ 81 lines hidden (lines 1–81)
Applied after the amplifier
In the Timeline Engine the dials compose in order: the rolled band value, then the anachronism amplifier widens it, then the chain decay diffuses it. The footholds are assembled from the objective's anchor plus every dated change on the ledger — so landing a change plants a new foothold and the next nearby move is cheap. The result rides back on the ripple as causalChain for the UI.
amplify → decay → the footholds (objective anchor + ledger years). Ungrounded turns / anchorless objectives → chainStatus null → no-op.
server/utils/openai.ts · 517 lines
⋯ 218 lines hidden (lines 1–218)
⋯ 282 lines hidden (lines 236–517)
The model is now told NOT to shrink its own swing for distance — the engine does it once, deterministically — so the old prompt-only causal-distance guidance is retired (no double-counting).
Objectives gained a signed anchorYear — the chain's far foothold. The composer emits one; sweeping 'across the ages' goals omit it and the dial no-ops.
server/utils/objective-generator.ts · 163 lines
⋯ 4 lines hidden (lines 1–4)
⋯ 10 lines hidden (lines 18–27)
⋯ 132 lines hidden (lines 32–163)
Legible both ends
The dial is shown, not hidden — the same discipline as the ⚡ wager chip. Before the roll, a ⏳ chip in the compose dock reads the tier for the chosen contact (hidden at the hinge, where there's nothing to warn). After the roll, the chain step joins the resolved equation: +20 ⏳ diffuse → +2%.
The pre-send ⏳ chip; chainRead (lower in the script) maps the store's causalChainRead getter to a tier + hint.
pages/index.vue · 446 lines
⋯ 132 lines hidden (lines 1–132)
⋯ 305 lines hidden (lines 142–446)
The resolved equation gains the ⏳ step between the wager and the stake.
components/MessageHistory.vue · 188 lines
⋯ 143 lines hidden (lines 1–143)
⋯ 33 lines hidden (lines 156–188)
Verification
427 unit tests: the decay curve (bridge-free, exponential, floor, monotonic), the Leonardo-seed vs Caesar-blast calibration cases, chaining-compounds, the server decay applied end-to-end, and the in-store chip read. Clean typecheck, 12/12 Playwright e2e.
And the end-to-end eval (the deterministic dial composing with the real model's swings): a lone far blast diffuses to mean 3.0 vs a hinge contact's 22.0, and a planted foothold lifts the same move — stepping stones, not blasts.
The calibration cases pinned: a real-but-modest far seed, a near-dead lone blast, and the compounding that rewards a chain.