What changed, and why
Issue #135. The per-turn swing breakdown — the little equation (+24 ✒ sharp → +29%) that shows how a base swing became the final percent — only rendered when an amplifier had actually moved the base. So it appeared on stake or chain turns and vanished everywhere else: a plain in-period sound turn (sound is ×1.0, nothing to show) and, more importantly, most losses. Craft and momentum are gains-only, so on a miss the base usually equals the final and the equation disappeared with it.
That second case buried the game's clearest lesson: a sharp dispatch buys no protection on a miss. Players saw the math only on some turns and asked why. The fix is one removed condition on each of the two surfaces that render the equation, so it shows on every resolved turn. On an unamplified turn it collapses to a single clean number, (+22 → +22%). Nothing else moves.
The Thread: the gate, dropped
In the Thread (the conversation rail), the equation lives inside the ripple line. The v-if calls showEquation(message), and each amplifier between base and final is a hover-hinted glyph emitted by equationTokens.
The ripple line: the equation span and its per-token glyphs.
components/MessageHistory.vue · 214 lines
⋯ 59 lines hidden (lines 1–59)
⋯ 145 lines hidden (lines 70–214)
showEquation was three conditions. The last one — baseProgressChange !== progressChange — was the gate. Dropping it leaves only the guard that both numbers exist, since you can't print base → final without a base. The comment is rewritten to match.
The gate is gone; the both-defined guard stays.
components/MessageHistory.vue · 214 lines
⋯ 137 lines hidden (lines 1–137)
⋯ 68 lines hidden (lines 147–214)
The Spine: the same one-line gate
The Spine's node-detail strip carries its own copy of the equation, so the two surfaces move together. Same edit: drop the base !== final half of the v-if, keep the !== undefined guard. The Spine's equation has no inline tokens — the ⚡/⚑ amplifiers render as separate badges beside it — so it always read as a bare (base → final) anyway.
The Spine equation, now ungated.
components/TimelineLedger.vue · 210 lines
⋯ 75 lines hidden (lines 1–75)
⋯ 124 lines hidden (lines 87–210)
What stays intact: base → final still reconciles
The issue's one caution: always-showing must still reconcile base → final. That contract lives in equationTokens, and this PR does not touch it. Every amplifier the server applies has a matching token — anachronism, causal chain, craft, momentum, stake — and the gains-only ones (craft at line 172, momentum at line 177) are gated on progressChange > 0, so they stay absent on a loss. No amplifier flips the swing's sign, so a loss reads cleanly: (-10 → -10%) with nothing between.
Untouched: the tokens that explain the gap between base and final.
components/MessageHistory.vue · 214 lines
⋯ 159 lines hidden (lines 1–159)
⋯ 32 lines hidden (lines 183–214)
The tests that pin it
Three tests fix the new behavior in place — each one fails on the old gated code, where the equation was hidden when base equals final. In the Thread, a plain in-period gain collapses to (+22 → +22%) with no amplifier glyphs, and a losing turn with a sharp dispatch shows the breakdown but no craft token (the lesson made literal).
The plain-gain and the sharp-but-lost cases.
tests/unit/components/MessageHistory.spec.ts · 215 lines
⋯ 143 lines hidden (lines 1–143)
⋯ 29 lines hidden (lines 187–215)
On the Spine, a node whose base equals its final now renders the equation too.
tests/unit/components/TimelineLedger.spec.ts · 91 lines
⋯ 52 lines hidden (lines 1–52)
⋯ 30 lines hidden (lines 62–91)
One stale comment in the e2e fixtures claimed the equation only appears when a fixture asks for it. The change made that false, so it now says the equation always renders and a fixture only changes its contents.