What changed, and why
A saved run at /runs/:id used to show its timeline as a plain text list: headline, "era · figure", a bare signed %. The live end screen, meanwhile, renders the real Spine — a horizontal era-axis with valence dots, ⚡/⚑ badges, the roll, and the Δ (base → final) equation. So a finished run read more flatly in the archive than it did the moment it ended.
That gap was left over from #84, which moved EndGameScreen onto the shared Spine but left RunView on the old hand-rolled <ol>. #84 even anticipated the fix: a reusable read-only Spine the saved-run view could share.
The change is small and has two halves. First, the Spine learns to render from a passed list of events, not just the live store, so RunView can feed it a saved snapshot. Second, the saved snapshot is widened to carry the two fields the Spine renders that it wasn't yet storing — baseProgressChange (the Δ equation) and staked (the ⚑ badge). Live play is untouched, and older saved runs still load.
The Spine learns a second source
TimelineLedger (the Spine) sourced its events straight from the Pinia store. This adds an optional events prop and falls back to the store only when it's absent. In live play nothing is passed, so the computed resolves to exactly what it did before; the read-only run view passes a snapshot's ledger instead.
The new prop, and the one-line change to where events comes from.
components/TimelineLedger.vue · 209 lines
⋯ 105 lines hidden (lines 1–105)
⋯ 87 lines hidden (lines 123–209)
RunView drops the flat list for the Spine
With the prop in place, the retrospective view swaps its <ol> for the shared component, read-only, fed the snapshot's ledger. It mirrors EndGameScreen exactly: no separate section label (the Spine's own header carries it), the same mt-6 wrapper, the same readonly. The only difference from the end screen is the source — a saved snapshot instead of the live store.
The whole timeline section is now one line of Spine.
components/RunView.vue · 102 lines
⋯ 68 lines hidden (lines 1–68)
⋯ 27 lines hidden (lines 76–102)
The old block (a <v-for> over <li> rows, plus a local deltaClass helper) is gone, and so is the helper — the Spine owns all that rendering now.
The two fields the Spine renders, that the snapshot lacked
The Spine reads two fields off each event that the saved RunLedgerEntry didn't carry yet. The Δ equation only shows when the pre-amplifier swing differs from the final one, and the ⚑ badge only when the turn was the staked last stand:
baseProgressChange drives the swing equation; staked the ⚑ badge.
components/TimelineLedger.vue · 209 lines
⋯ 77 lines hidden (lines 1–77)
⋯ 123 lines hidden (lines 87–209)
So the contract gains both, optional. Optional is the point: a turn that didn't amplify has no equation to show, and most turns aren't staked — and an older saved run that predates these fields simply renders without them.
The two additions to the saved-run ledger contract.
utils/run-snapshot.ts · 113 lines
⋯ 66 lines hidden (lines 1–66)
⋯ 27 lines hidden (lines 87–113)
Plumbing the fields through the save and the boundary
Two consumers of the contract follow. The store's snapshot builder copies the fields off the live ledger when it persists a finished run:
baseProgressChange and staked join the persisted ledger entry.
stores/game.ts · 1382 lines
⋯ 1306 lines hidden (lines 1–1306)
⋯ 61 lines hidden (lines 1322–1382)
And the server's boundary validator — a saved run can be POSTed directly, so every field is coerced — bounds the two new ones. The pre-amplifier swing is signed-clamped like its sibling progressChange; staked is a strict-boolean flag, so only a real true survives.
The two new coercion branches sit beside craft/anachronism.
server/utils/run-snapshot.ts · 171 lines
⋯ 117 lines hidden (lines 1–117)
⋯ 30 lines hidden (lines 142–171)
Tests: both sources, and plumbing that can't silently rot
A new spec drives the Spine from both sources. The passed-events path proves a saved run renders the full graphic — nodes, the Δ equation, both badges — and that it ignores the live store entirely; the no-prop path proves live play is unchanged, down to the ▷ now present marker. (SymbolHint wraps each badge in a tooltip that needs a provider the bare mount lacks, so it's stubbed to a slot passthrough — the same idiom MessageHistory.spec uses.)
Two describes: from a passed prop, and the store fallback.
tests/unit/components/TimelineLedger.spec.ts · 81 lines
⋯ 20 lines hidden (lines 1–20)
⋯ 4 lines hidden (lines 31–34)
⋯ 21 lines hidden (lines 47–67)
⋯ 8 lines hidden (lines 74–81)
The render tests hand-build fixtures with both fields present, so on their own they wouldn't catch the plumbing breaking. Two more assertions close that: one proves the store builder actually copies the fields into the saved snapshot, the other that the boundary validator preserves and bounds them. Drop either plumbing line and one of these goes red.
The save path carries the two fields into the POST body.
tests/unit/stores/game-save-run.spec.ts · 109 lines
⋯ 80 lines hidden (lines 1–80)
⋯ 21 lines hidden (lines 89–109)
Preserve, signed-clamp, and the strict-boolean guard at the boundary.