Three sweeping additions and one new file, reflecting how the project
has grown:
* Parent `README.md` rewritten. The architecture mermaid now shows
`crisis_agents` as a third sibling layer on top of the pure
protocol algorithms, alongside the CrisisNode TCP runtime and the
SimulatedNode in-process recorder. A fourth audience-shaped quick
start (🤖 "run the AI-agent coordination demo") joins the
protocol-pytest, simulation-CLI, and visualizer entries. The
repository-layout tree expands to enumerate `src/crisis_agents/`'s
modules. Test count corrected (~170).
* New `src/crisis_agents/README.md`. Comprehensive package
documentation:
- threat model + what's out of scope
- the two principles enforced by tests: no chokepoint, no clock
- mental-model mermaid (closed phase → boundary opens → async
loop → quorum vote → multi-signer proof)
- six-phase walkthrough matching the CLI output
- module-by-module reference table
- reuse map from `src/crisis/` (Message, LamportGraph,
find_mutations, ProofOfWorkWeight, etc.)
- build/run/test instructions including the `--live` Claude path
- quorum-threshold formula in LaTeX: ⌈2N/3⌉
- test taxonomy with the two sentinel files
(test_no_chokepoint, test_async_quiescence) highlighted
* `INSTALL.md` extended. New Section 4 covers running the
`crisis-agents demo`, both mocked-deterministic and `--live` with
real Claude sub-agents. Anthropic SDK shown as optional `[live]`
extras. Old sections renumbered (Section 5 → Section 6 for Swift,
6 → 7 for Troubleshooting). Two new troubleshooting entries for
live-mode failures.
* `CrisisViz/HANDOFF.md` gets a new Section 0. Brief notice that a
sibling Python sub-project (`crisis_agents`) now exists, what it
does, and — most importantly — that it doesn't share code with
CrisisViz: refactoring one cannot break the other. Cross-link to
the crisis_agents README so a future Swift-side agent has the
pointer without having to discover it via grep.
Source-of-truth corrections in the parent README:
- the "three audiences" framing becomes four
- the layout tree now lists `src/crisis_agents/`
- the architecture diagram explicitly marks the agent layer as
"decentralized, asynchronous" (the two principles the recent
refactors enforce)
CrisisViz code: still untouched by all this. Only its HANDOFF doc
gets a heads-up paragraph.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bundle.sh produces CrisisViz.app but stops there; the standard way
to hand a macOS app to someone on a different machine is a DMG with
the app and an Applications symlink so the user can drag-install.
This script fills the gap, using only macOS-built-in tools
(`hdiutil`, `codesign`, `shasum`) so a fresh checkout doesn't need
Homebrew packages.
What it does, end-to-end:
1. If CrisisViz.app is missing, calls bundle.sh --no-launch.
2. Re-codesigns the .app ad-hoc and clears the quarantine xattr.
3. Stages the .app + a symlink "Applications -> /Applications"
in a tmpdir.
4. hdiutil create -format UDZO (compressed, read-only).
5. Codesigns the DMG itself ad-hoc.
6. hdiutil verify the result; print size + SHA-256.
Ad-hoc signing is fine for personal / educational distribution —
the user gets a Gatekeeper warning on first open and right-clicks
"Open" once. Notarization would require a paid Apple Developer
account and is not in scope for this artifact.
`*.dmg` added to CrisisViz/.gitignore.
Verified end-to-end: produces a 31 MB DMG that mounts cleanly,
contains CrisisViz.app and the Applications symlink, and passes
`hdiutil verify`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The old HANDOFF documented animation bugs that were fixed weeks
ago and predates the serial-timeline migration. It referenced
"Ch07 sort", "Ch09 erasure coding", and "Ch10 shield appear" as
broken in ways that no longer apply, and it pointed at code paths
(`.id(engine.currentGlobal)`, the 4s `sceneDuration`) that have
since been replaced. A new agent reading the old file would chase
ghosts.
The rewrite is structured as an engineering log:
1. Current state (10 chapters migrated, testbed green, last
commit on master, what the bundle pipeline produces);
2. The pure-function timeline pattern (Beat list, WorldState,
state(at: t), ChXXScenes wrappers, the renderer's shape);
3. Pedagogy invariants the renderer MUST hold (strictly serial,
introduce-before-show, lane = lifeline, fixed detail slot,
courier track, cast colors via DataManager, beat tag,
scene indicator);
4. Hard-won rules from past sessions (restart the .app, testbed
doesn't verify smoothness, narration ≡ canvas, arrows must
be visible, layout from the full dataset);
5. Test-harness reference (five layers + when to update each);
6. Known open items (DA polish, per-scene vertex counts in
invariants, LaneRenderKit extraction, animation diff
analyzer, CrisisNode/gossip TCP tests);
7. How to resume (exact commands, debug ladder).
README and HANDOFF intentionally overlap — README is the lobby
for humans, HANDOFF is the engineering log for the next agent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Swift sub-project had no README of its own. Anyone navigating
into CrisisViz/ would see Package.swift, bundle.sh, and a stale
HANDOFF.md, with no orientation about the chapter / scene model
or why the renderer is organized the way it is.
The new README is shaped for someone reading on GitHub or iPad:
- LaTeX-rendered formula for the pure-function timeline pattern
(state(t) = fold of beats whose start <= t);
- Mermaid block diagram of SceneEngine -> ChXXTimeline -> Canvas
+ GlassNarration;
- per-chapter table with beat count, runtime at 1x, and concept,
plus the off-by-one naming gotcha (renderer Ch01..Ch10 vs.
timeline Ch00..Ch09);
- build / run / test / distribute one-liners;
- testbed output map (INVARIANTS / SOURCE_AUDIT / VIDEO_CLIPS /
MANIFEST / SANITY);
- cast convention (lane = lifeline, strictly serial, introduce-
before-show, one detail slot) and the architecture-pointer
table for the critical files.
GitHub renders the LaTeX and Mermaid natively, so this serves as
both repo doc and iPad-readable reference.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The closing chapter. Aaron splits ξ's body into 4 erasure-coded
shards (s1, s2, s3, s4) where any 2 reconstruct the body. Aaron's
vault visually morphs from "1 MB body grid" to "4 labeled shards
on a row". One shard each is then sent to Ben (s1), Carl (s2),
Dave (s3); s4 stays with Aaron.
Each non-Aaron cast gets a small vault next to their cast circle
that shows whichever shards they currently hold. Empty vaults read
"(empty)"; once shards arrive they appear as small filled chips
labeled s1/s2/s3.
Then Aaron goes offline (dimmed cast circle + red ⚠ OFFLINE label).
Carl asks Ben for s1; Ben sends; Carl now holds s1+s2 — k=2 reached.
A green halo pulses on Carl's vault as he runs the decoder, then a
"✓ ξ body reconstructed" caption pins under his vault and a big
banner reads "✓ ξ BODY RECONSTRUCTED — k=2 of 4 shards was enough
— Aaron NOT NEEDED".
Closes with a final-summary banner: "CRISIS COMPLETE · consensus +
DA + Byzantine resilience" and an outro inviting the viewer to
scrub.
`Ch08Timeline.swift` (new) — 19 beats over ~85s. Beat kinds:
`splitIntoShards`, `sendShard`, `stowShard`, `aaronOffline`,
`askForShard`, `shardArrives`, `reconstructBody`, `reconstructed`,
`finalSummary`.
`Ch09_DA_Design.swift` rewritten end-to-end to render from
Ch08Timeline. Aaron's vault adapts to "shards" mode after the
split beat. Per-cast vaults sit just to the right of each cast
circle (small, dashed, expand to fit chips). Reconstruction halo
pulses while running, then settles into a steady green border once
the body is back.
`SceneEngine` and `ImmersiveView` extended for chapter 8 — every
chapter (0–9) now has a timeline-backed renderer + beat-bound
narration.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs,
36/36 MP4 written.
All 10 chapters migrated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces Aaron's vault as a new visual element on the right side
of the canvas: a dashed coral box that holds ξ's body as a 4×6
grid of small filled rectangles labeled "ξ body · 1 MB" once Aaron
has sealed it. The vault is private storage — nobody else has it.
Beat structure:
Scene 0 (17.5s) — carry-forward {α..ε}, Aaron composes ξ with a
heavy blob, seals it, body materializes in vault.
Scene 1 (20s) — Aaron sends only the HASH (small envelope, no
body) to Ben and to Carl. Each recipient marks
ξ on their lane with a red "⚠ BODY MISSING" flag.
Scene 2 (21s) — Ben requests ξ's body; arrow flies to Aaron.
Aaron silent. Big red ✗ + "AARON SILENT —
REQUEST TIMED OUT" pulse. Carl tries; same
outcome.
Scene 3 (10s) — "⛔ DA PROBLEM — Aaron knows ξ's body. Nobody
else can use it." + "→ erasure coding (next
chapter) makes data un-loseable" follow-up.
`Ch07Timeline.swift` (new) — 14 beats over ~68.5s. New beat kinds:
`composeXi`, `sealXi`, `sendHashOnly`, `markBodyMissing`,
`askForBody`, `aaronSilent`, `stuckBadge`.
`Ch08_DA_Problem.swift` rewritten end-to-end to render from
Ch07Timeline. ξ vertex sits past ε on each lane that has it. Hash
flight envelope is intentionally smaller than the body-carrying
flights of earlier chapters — visual cue that this carries less.
The vault uses the right margin so the lanes can stay at full
width on the left.
`SceneEngine` and `ImmersiveView` extended for chapter 7.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs,
36/36 MP4 written.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The chapter walks the leader chain from Ch05 and adds each leader's
round-N ancestor closure to a single linear sequence at the bottom
of the canvas — the "total order snake". Dashed placeholder slots
#1 through #5 sit empty; as `appendToOrder` beats fire, blocks slide
up from below into their position with cubic-out easing and the
snake's spine arrows draw between consecutive slots.
Sequence built across the chapter:
Round 0: α (#1) → β (#2) → γ (#3) → δ (#4)
Round 1: ε (#5)
Round-N-ordered badges (green check + "ROUND 0 ORDERED" / "ROUND 1
ORDERED") fire as each round completes; final-convergence badge
("ALL VALIDATORS COMPUTE THE SAME TOTAL ORDER — CONVERGENCE")
fires at the top once the snake is full.
`Ch06Timeline.swift` (new) — 11 beats over ~51.5s. Beat kinds:
`appendToOrder`, `roundOrderedBadge`, `finalConvergence`.
`Ch07_Order.swift` rewritten to render from Ch06Timeline. Lanes
fade slightly when a message goes into the order so the focus
shifts to the snake. Slot connectors are arrowed.
`SceneEngine` and `ImmersiveView` extended for chapter 6.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs,
36/36 MP4 written.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-round leader picked by heaviest-vertex (all w=1 here, so the
tiebreaker fires) plus lexicographic-hash. Visualization:
candidates get yellow rings, weights show as 'w=1 · {hash}' subscript,
the tiebreaker pops a comparator strip showing 43f3 < 5ce9 < 7638 <
be1c, the winner gets a gold crown ring + "♛ LEADER · r0" label on
every cast lane.
Round 0 leader: α. Round 1 leader: ε.
The chapter punches the determinism property: every honest
validator runs the same arithmetic on the same DAG and crowns the
same leaders without exchanging any messages. Determinism badge
fires at scene end.
`Ch05Timeline.swift` (new) — 10 beats over ~47.5s. Beat kinds:
`showCandidates`, `showWeights`, `tiebreakerCompare`, `crownLeader`,
`determinismBadge`.
`Ch06_Leader.swift` rewritten end-to-end (108 → ~340 lines) with
candidate / leader rings on lane vertices, comparator strip, crown
markers in towers (♛ prefix on leader blocks).
`SceneEngine` and `ImmersiveView` extended for chapter 5.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs,
36/36 MP4 written.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Picks Aaron's recent vertex ε on his lane and Carl's ε on his (same
position because they hold the same accepted set after Ch02 healed).
Walks each ancestor cone one edge at a time — ε → γ, then γ → α —
with a yellow tracer animating along the edge. Settles each cone
with steady yellow rings on every vertex it contains. Reveals the
overlap by pulsing white on the shared vertices in BOTH cones.
Closes with the chapter's punch line: implicit vote complete, no
ballot ever sent.
`Ch04Timeline.swift` (new) — 15 beats over ~62.5s. Beat kinds:
- `pickLeaf(cast, mid)` — yellow halo on the chosen leaf
- `walkEdge(cast, from, to)` — yellow tracer along one edge,
parent permanently joins the cone
- `settleCone(cast, label)` — quiet beat
- `revealOverlap` — surfaces intersection of Aaron's and Carl's cones
- `voteComplete` — green banner
`Ch05_Voting.swift` rewritten end-to-end (473 → ~370 lines) to
render from Ch04Timeline. Each cast lane's vertices get layered
indicators: leaf halo (yellow, thicker), cone ring (yellow), then
white pulse for overlap members on Aaron's and Carl's lanes once
the reveal beat fires.
`SceneEngine` and `ImmersiveView` extended for chapter 4.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs,
36/36 MP4 written.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dave creates two messages with the same author + same parent set but
different bodies (ζ_a "send 50 BTC to Aaron", ζ_b "send 50 BTC to
Charlie"). Sends one to Aaron, the other to Ben. Aaron and Ben gossip;
the conflict is detected; Dave's vertices get banned with a red X;
honest 3 converge.
Beat structure:
Scene 0 (47.5s) — Dave thinks ("I'll send different things"), then
composes ζ_a (5s slo-mo with red-ring slot
chrome to flag this is a fork), seals, repeats
for ζ_b (same identity, different body). Sends
ζ_a only to Aaron, ζ_b only to Ben.
Scene 1 (32s) — Aaron forwards his copy of ζ_a to Ben → Ben already
has ζ_b → fork detected. Threshold bar appears
(f=1, n=4, 3f<n). Both Dave-vertices banned.
AARON·BEN·CARL CONVERGE badge.
`Ch09Timeline.swift` (new) — 18 beats over ~80s. Beat kinds
introduced: `forkCompose` / `forkSeal` / `forkSend` / `forkAccept`
(envelope acceptance into a recipient's view), `gossipExchange`
(Aaron forwarding to Ben that triggers detection), `forkDetected`,
`thresholdBar` (f<n/3 visualization), `banDave` (red X across
Dave's lane forks + dim them in towers).
`Ch10_Byzantine.swift` rewritten end-to-end (269 → ~525 lines) to
render from Ch09Timeline. Two red-ringed fork vertices appear on
Dave's lane past the carry-forward {α..ε}. Composing slot for forks
gets a red border to flag the deception. Towers track each player's
fork-acceptance: Aaron picks up ζ_a, Ben picks up ζ_b then ζ_a from
gossip, Carl never receives either, Dave's own view stays clean of
the lies he wrote. Once banned, the fork blocks dim and a red X
overlays them.
`SceneEngine` gets duration overrides for Ch09's 2 scenes.
`ImmersiveView.liveNarration` extends to chapter 9.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs,
36/36 MP4 written.
Known polish item: Dave's thought bubble lands at Carl's lane Y
because the bubble is offset −80pt from the cast circle. Lower-lane
casts need a "below-instead-of-above" placement variant. Tracked
for next pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ch03 walks through the five messages from Ch01/Ch02 and shows how
round numbers are derived — not declared — from accumulated PoW
weight. A horizontal thermometer at the top of the canvas fills
green as each highlighted vertex contributes its weight; a yellow
dashed line marks the round-closing threshold. When δ pushes the
total over the threshold, a yellow ring + "is_last" label appears
on δ on every cast member's lane simultaneously. Round 0 closes;
round 1 opens with ε at weight 1.
The chapter's pedagogical punch: nobody voted, nobody declared the
boundary. Every honest player who has the same five messages
computes the same total weight, sees δ push the same threshold, and
flags δ identically. The thermometer is a calculator, not a
ballot box.
Closing beats:
- "Each player keeps their own DAG. Full stop." overlay
- "Re-gossip is harmless" demonstrated: a duplicate α envelope
flies to Ben, arrives, gets a "✗ DUPLICATE — DROPPED" label,
and dissolves. No tower update, no thermometer change.
- "Weight is arithmetic. Arithmetic doesn't depend on who you ask."
`Ch03Timeline.swift` (new) — 17 beats over ~72s. New beat kinds
this chapter introduces: `introduceWeights` / `highlightVertex` /
`markIsLast` / `openNewRound` / `bookkeepingNote` /
`reGossipDuplicate`.
`Ch04_Rounds.swift` rewritten end-to-end (91 → 460 lines) to
render from Ch03Timeline. Lanes carry the carry-forward five
messages with weight + round labels; thermometer at top; bookkeeping
note overlay below; perception towers at the bottom (now also
showing round numbers per block).
`SceneEngine` and `ImmersiveView` extended for chapter 3.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs,
36/36 MP4 written.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`Ch02Timeline.swift` (new) holds 27 beats over ~115s at 1×, mapped to
Ch02's 4 scenes:
Scene 0 (14s) — carry-forward state from Ch01, then Dave's link
cracks (linkDegrade) and breaks (linkBroken).
Scene 1 (35s) — Aaron writes δ, sends successfully to Ben and
Carl, fails to send to Dave (envelope hits the
partition barrier and dissolves with a red ✗).
Scene 2 (22.5s) — Dave writes ε locally, references only γ
(he doesn't know about δ). His attempt to send
ε to Aaron also fails. Two stories now visible.
Scene 3 (44s) — Link restored, missing messages flood through,
all four towers reunite at {α, β, γ, δ, ε} —
but Dave's stack ORDER is different (ε before δ
in his tower) because that's what he locally
observed during the split.
The chapter introduces two new beat kinds: `linkDegrade`/`linkBroken`/
`linkRestored` (drives a red dashed barrier between Carl's and Dave's
lanes that thickens with intensity) and `flyFailed` (the envelope
animates partway, hits the barrier, and fades with a big red ✗ at the
impact point).
`Ch03_Partition.swift` rewritten end-to-end (343 → ~470 lines) to
render from Ch02Timeline. Five-block-tall perception towers at the
bottom show all messages α through ε. The asymmetric Dave-stack vs
honest-3-stack is permanently visible at a glance.
`SceneEngine` gets duration overrides for Ch02's 4 scenes.
`ImmersiveView.liveNarration` extends to chapter 2.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs,
36/36 MP4 written.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Each player gets a Tetris-style stack at the bottom of the canvas
showing the messages they've accepted, in the order they accepted
them. As `acceptIntoView` (and `seal`, for the author) beats fire,
new blocks slide up from below and settle into place at the top of
the tower with a cubic-out ease. The block's color comes from the
message's AUTHOR, not the tower owner — so a tower mixes colors,
which is exactly what a local DAG looks like in real Crisis.
The pedagogical payoff: stack ORDER differs between players because
Carl wrote γ before β reached him. Aaron and Ben end with stacks
[α, β, γ]; Carl ends with [α, γ, β]. The asymmetry that the chapter
spends 5 minutes establishing is now permanently visible at the
bottom of the screen, in one glance.
Implementation:
- `Ch01WorldState.viewOrder: [Ch01Cast: [String]]` — set type
can't carry order, so we track an array alongside `views`.
Populated when `seal` fires (author gains own message) and when
`acceptIntoView` fires (recipient gains incoming message).
- `Ch00WorldState.towerBlocks: [Ch01Cast: [Ch00TowerBlock]]` —
abstract `tx-N` blocks with author colors, populated during the
`logsDiverge` beat in a DIFFERENT order per cast (preview of
the same asymmetry that Ch01 will demonstrate concretely).
- Tower geometry is identical between chapters: 4 columns of
width 110, gap 24, base Y at canvas.height − 110, blocks 26pt
tall with 4pt gaps. Cast name + "VIEW" header at the top of
each column. Faint dashed rails frame the empty tower so the
viewer can see the silhouette before any block has landed.
- Active block in Ch01 fades + slides in over the active beat's
progress, so the viewer sees each new arrival actually land.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Same structure as Ch01: a flat list of micro-beats, each with its own
narration sentence, rendered as a pure function of timeline position.
Twelve beats over ~43.5 seconds at 1×, mapped onto Ch00's 3 scenes:
Scene 0 (16s) — title fades in, then Aaron / Ben / Carl / Dave
fade onto their lanes one at a time, then settle.
Scene 1 (14s) — "no boss" beat, then per-lane scribbles of
vertices to make "four logs, four stories" visible.
Scene 2 (13.5s) — convergence arrows from all four lanes toward a
single "ONE HISTORY" target, then a red pulse and
"⚠ ONE OF THESE WILL LIE" warning over Dave's
lane. End settle.
Replaces the old 2×2 portrait grid with the lane idiom — Ch00 now
ends in the same coordinate system Ch01 begins in (cast on lanes),
so the chapter transition is a soft focus shift, not a hard cut.
`Ch00Timeline.swift` (new) holds the beats + pure `state(at:)`.
`Ch01_Problem.swift` rewritten to render from it (324 → 280 lines,
no per-scene switch).
`SceneEngine` gets duration overrides for Ch00's 3 scenes.
`ImmersiveView.liveNarration` extended with a Ch00 case so the
narration overlay reads beat-bound text for chapters 0 and 1.
Bundled, harness 55/55 invariants, 0 audit errors, 281 PNGs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three concrete pollution issues found by sampling testbed frames:
1. The composing slot landed on top of the previously-accepted
vertex on Aaron's lane (Aaron's α at x=360 vs slot starting at
x=324). Even worse for Ben's composing — slot crowded Ben's
own cast circle.
2. The in-flight envelope drew at progress=0 directly on top of
the just-sealed accepted vertex on the sender's lane. For ~1s
of every flight you couldn't tell which was which.
3. The open-envelope card sat next to the recipient's lane and ran
into the adjacent lane (e.g. Ben's card extended y=157..297,
covering both Aaron's lane area and parts of Carl's).
Fixes:
- Composing and open-envelope share ONE fixed top-center slot
(`detailSlotRect`). They're mutually exclusive on the timeline,
so reusing the same slot is honest. A short colored dashed
connector runs from the slot to the in-focus cast member's
circle so the viewer knows who is writing/reading.
- In-flight envelopes are drawn on a "courier track" 36pt above
the lane axis. The track has a faint dashed path between sender
and recipient; the envelope glides along it; a small drop-line
from the envelope toward the lane keeps the spatial cue intact.
The accepted-on-sender's-lane vertex stays clean.
- Removed the bottom-left footer (was overlapping
GlassNarration in the live app). Replaced with a tiny beat-id
tag in the top-right corner so PNG sweeps can still be matched
to a specific beat for debugging.
Bundled, harness 55/55 invariants, 0 audit errors. Ready to
propagate the pattern to other chapters with these layout
constraints baked in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The viewer is now in full control of time, like a film editor at a jog
wheel. Two new controls in the bottom Glass bar:
- Speed slider, range −16…0…+16. Pull it left for reverse playback;
sit at 0 to freeze. Speed change captures localTime at the OLD
speed before applying the new one, so it doesn't retroactively
rescale time already elapsed.
- Chapter scrubber across the top of the bar. Drag to any point in
the current chapter's continuous timeline (Ch01 ≈ 326 seconds at
1×). Snaps to the resolved scene + offset under the playhead;
isPlaying / speed are preserved across a scrub.
SceneEngine internals:
- `localTime` is now clamped to [0, sceneDuration] so reverse play
halts cleanly at scene boundaries (the auto-advance task picks up
and crosses the boundary into the previous scene).
- `startAutoAdvance` schedules the right side of the boundary based
on the sign of speed. Crossing forward enters the next scene at
t=0; crossing backward enters the previous scene at t=duration.
- `setChapterPosition(_:)` resolves a chapter-global time into a
(scene, localTime) pair and re-anchors. Used by the scrubber.
- `currentChapterDuration` exposes the length the slider needs.
GlassControls rewrite:
- Two-row layout: scrubber on top, transport + speed + dots + nav
+ settings on bottom.
- The +/- discrete buttons are gone — speed is continuous.
- Speed label color-codes direction (green forward / orange reverse
/ grey frozen) and shows ❚❚ 0× when stopped.
- Wraps in a TimelineView so scrubber and label refresh at frame
rate while paused or playing.
Bundled, harness still 55/55 invariants, 0 audit errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the parallel-beats GossipScript with a strictly serial Ch01
timeline (~75 micro-beats over ~5.5 minutes at 1×). Pedagogical
principle: the learner's eye can only follow serial events, so even
though Crisis is parallel by design we serialize for teaching.
Every micro-event is its own beat with its own narration sentence:
think → select payload → select parents → grind PoW → seal → choose
recipient → fly (one at a time) → arrive → open → read body → read
parents → resolve each parent against the receiver's own local view →
verify hash → accept into view. The asymmetry beat (Carl writes γ
before β reaches him) is the centerpiece of the chapter.
Architecture:
- `Ch01Timeline.state(at: t)` is a pure function — replaying every
beat up to t produces the world state. This makes the chapter
scrub-able and reverse-play-able cleanly later.
- The 7 existing scenes become navigation labels — windows of the
same continuous timeline. Arrow keys still let you jump between
them. Scene durations are now per-scene overrides matching each
scene's window in the timeline.
- `Ch02_Graph.swift` rewritten end-to-end (1506 → ~470 lines): one
timeline-driven render path, no per-scene switch, no dense graph
rendering for scenes 4-6 (their content folds into the main
timeline as resolveParent / verifyHash / acceptIntoView beats).
- `ImmersiveView`'s narration overlay now wraps in its own
TimelineView so the displayed text updates at frame rate to
match the active beat. Ch01 reads from
`Ch01Scenes.narrationAt(scene, localTime)`; other chapters fall
back to the static per-scene SceneNarrations.
- `GossipScript.swift` deleted. Old helpers in Ch02_Graph
(renderStagedBeat, renderHashOneWayVignette,
renderLocalDAGDeterminismVignette, renderAncestorConeWalk,
drawCastBubble + view-bubble + composing box) are gone.
- SceneEngine + SceneVideoCapture duration overrides updated for
the 7 new scene windows.
Cast members fade onto the stage only when the timeline introduces
them — Ben isn't on screen during "Aaron writes α", Carl isn't on
screen until "Aaron decides to send α to Carl". No ghosted lanes.
Build clean, harness still 55/55 invariants, 0 audit errors,
281 PNGs, 36/36 MP4 written.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three problems landed together:
1. Scenes 0/1/2 ("Aaron speaks", "Ben joins", "Carl arrives") showed
a static cast vertex on each scene's lane and trusted the user to
imagine the writing and the message flying. The user wanted to
SEE Aaron compose α in slo-mo, see α physically fly across the
canvas to Ben, and see Ben compose β with α's hash inside.
2. Scene 3 (gossip dramatization) used a 3-up triangle layout that
ignored the lane lifelines — Aaron wasn't on Aaron's row, Ben
wasn't on Ben's row, etc. The 6/36→7/36 transition was a hard cut
into a totally different visual idiom.
3. The composing box was placed 100pt above the author. For the
triangle layout that was OK, but for ANY lane layout the upper
lane (Aaron, lane 0 ≈ y=116) put the box off-canvas (y≈-24).
Replace `renderStagedBeat` and `renderGossipDramatization` with one
`renderGossipBeats(scriptT:)` helper that:
- Anchors all three cast circles to their actual lane centers
(`castLaneY(0)` for Aaron, `castLaneY(1)` for Ben, etc.),
staircased on X so flight diagonals don't cross other circles.
Lifeline rule satisfied across scenes 0–3.
- Drives writing/flight/view-bubble updates from
`GossipScript.state(at: scriptT)`. Each scene maps its 0..N local
seconds onto a different time window of the script:
Scene 0 (8s) → script 0..6.5 : Aaron writes α + sends
Scene 1 (8s) → script 4..14 : α flies, Ben writes β
Scene 2 (8s) → script 9..22 : Carl writes γ before β arrives
Scene 3 (24s) → script 0..24 : full asymmetric replay
- Renders the composing slot in a fixed top-center box just under
the perspective panel with a colored connector to the active
author — guaranteed on-canvas regardless of which lane the
author sits on.
- `drawCastBubble` now takes an explicit `BubbleSide` so view
bubbles attach to the correct side of each cast circle without
depending on NSScreen geometry.
Drops dead helpers `renderStagedBeat`, `drawStagedPerspectivePanel`,
and `drawComposingBox` (replaced by the unified renderer +
`drawComposingSlot`). Net −73 lines; harness still 55/55 invariants,
0 audit errors, 281 PNGs clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The pause button was just a no-op for animation: localTime was always
(now − sceneStart) × speed, so anything inside the scene kept moving
even when isPlaying=false. The auto-advance task was the only thing
the toggle actually controlled. Worse: every fresh launch from the
Dock immediately started ticking, so the user opened the app and saw
the title scene already several seconds in.
Replace the "wall-clock anchor" with a two-state timer:
- accumulatedLocal: time already watched in this scene
- playSessionStart: wall-clock anchor for the live delta (nil when
paused)
localTime = accumulatedLocal + (now − sessionStart) × speed if playing
= accumulatedLocal if paused
Pause now folds the live delta into accumulatedLocal and clears the
session anchor. Resume re-anchors. Scene navigation (next/previous/
goTo) zeros the accumulator and only opens a fresh session if the
engine was already in the playing state. Speed changes capture the
current localTime at the OLD speed so they don't retroactively rescale
already-elapsed time. Auto-advance fires after the REMAINING duration
(not the full duration) so a pause halfway through Ch1.3 doesn't
restart its 24s clock when the user resumes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The perspective panel (AARON/BEN/CARL/COMMON KNOWLEDGE columns) was
hidden in the live app: it rendered at the bottom of the canvas, where
GlassNarration's overlay covers it. The MP4 testbed renders the canvas
in isolation so the panel showed there, creating the impression that
the testbed and the running app diverged. Move the panel to the top of
the canvas where the narration overlay can't reach it, and extend it
to scenes 0/1/2/3 (was scene 2 only) so the perspective build-up is
visible across the whole "Aaron speaks" arc.
The "knows" sets are now derived from currently-visible vertices and
their parent edges (was the full step-5 simulation snapshot, which let
the panel claim a cast member knew vertices that weren't on screen).
For scene 3 the sets come straight from GossipScript.state — α/β/γ
flip ✓ as the scripted receive beats complete.
Scenes 4-6 were "totally static, completely different" from the
slow-motion staged beats of 0-3. Replaced all three:
Scene 4 (hash one-way) — Aaron's α envelope slides in, payload lines
reveal serially, hash bubble appears, dashed reverse arrow with
red ✗ shows "PREIMAGE IMPOSSIBLE", then green ✓ forward verify,
then bridge to chapter 8 (data availability).
Scene 5 (local DAG / determinism) — two side-by-side panes, AARON's
and BEN's local DAGs, populate from the same scripted α/β/γ at
slightly different arrival times, then converge to identical
contents with an "=" sign and "SAME MESSAGES → SAME GRAPH —
BYTE-FOR-BYTE" caption.
Scene 6 (ancestor cone) — picks Aaron's leaf vertex, BFS-walks the
cone one level per beat (pacing computed from cone depth so the
full reveal fits the 8-second scene), cast colors on cast
ancestors, ★ GENESIS markers at the leaves of the cone.
Vertex-count line moves to top-right corner so the new top-center
panel band has room. Test harness still 55/55 invariants, 0 audit
errors, 281 PNGs clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Locks the four-validator cast (Aaron/Ben/Carl/Dave) into every chapter
and replaces the static-PNG-only testbed with a five-layer dynamic
verification harness so the curriculum's logical claims are checked
against the simulation data, not just eyeballed in screenshots.
- Lane = lifeline: DAGLayoutEngine no longer Y-jitters; every vertex
sits exactly on its player's lane. SourceAudit forbids regression.
- Cast color discipline across all 10 chapters: palette[i] -> castColor(for:),
node.name.suffix() lane labels -> cast names. Lane order = cast order.
- Ch02 partition rewrite: isolates the actual cast Dave (was hardcoded
honest-PIDs causing story/canvas mismatch).
- Ch07 total order masterclass: wave + cast-tracer + named callouts +
AARON=BEN=CARL convergence stamp.
- Ch10 byzantine rewrite on lane base; Dave's forks pulse, threshold
bar f<n/3, three-way ✓ banner.
- Narration: story-beat titles with [Technical: ...] suffix, scene
indicator (CH X.Y n/N) on every screen.
- Inter-chapter morph transition (asymmetric cross-fade).
- 80-step converged simulation (was 35-step, never converged).
- 55 NarrativeInvariants, SourceAudit (forbidden patterns),
SceneVideoCapture (36 MP4 clips), GossipScript.
Test harness: 55/55 invariants pass, 0/0 audit errors, 281 PNGs clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dm.sim was destructured for the guard but never used inside the body.
Replaced with a nil-check on the same property.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two chapters rewritten to match the redesign's pedagogical pacing:
Ch00 "Four friends, one ledger, no boss." (Sources/CrisisViz/Chapters/Ch01_Problem.swift):
- Scene 0 is now a slow cast intro. Aaron, Ben, Carl, then Dave fade in
one at a time at ~1.5 s intervals, each with their portrait, name,
personality cue, and (for Dave) a BYZ badge — so the viewer has met
the cast and knows who the trouble-maker is before any consensus
protocol shows up.
- Scene 1 reuses the four cast portraits but each character now writes
a DIFFERENT ordering of three transactions (tx-A/tx-B/tx-C). "No
shared truth yet" is concrete instead of abstract.
- Scene 2 dims that backdrop instead of cutting to a new view, so the
question ("HOW DO WE ALL AGREE?") emerges from the same scene rather
than replacing it. Subtitle calls out "ONE OF US (DAVE) MIGHT BE
LYING" so the Byzantine threat is foreshadowed up front.
Ch04 "Did you see what I saw?" (Sources/CrisisViz/Chapters/Ch05_Voting.swift):
- Replaces the old 3-scene "single static SVP highlight per scene" with
a 10-step narrated collapse spread across the chapter's 3 scenes
(3 + 3 + 4 steps, ~2.7 s per step). Each step adds ONE new visual
layer — vertices stay drawn, cones stack on top, the overlap pulses,
ancestor tags surface, badges appear, then the pair physically
migrates together and a CONSENSUS frame snaps around them.
- A persistent step legend on the bottom-left tells the viewer exactly
what the current step is doing and what's coming next, so the lesson
is "explicit unfolding" rather than "trust the highlight".
- Uses Aaron (coral) and Carl (amber) as the convergence pair so the
cast colors carry the meaning. Ancestor cones use the same colors at
reduced alpha; the overlap pulses white.
- pickPairVertex picks each character's heaviest vertex one round below
the frontier so the depth-2 ancestor cones have meaningful overlap.
Tasks #34 and #36 in the redesign task list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous chapter-by-chapter visualization felt disconnected — every
scene used a different idiom and the cast was anonymous "honest-0". This
commit lays the foundation for a coherent end-to-end story:
Cast (Sources/CrisisViz/Model/Cast.swift):
- Four named validators with stable color slots — Aaron (coral), Ben (teal),
Carl (amber), Dave (violet, Byzantine). The first three honest nodes claim
the lead slots; the byzantine node always plays Dave; surplus honest
nodes become muted "Peer-N" fallbacks.
- buildAssignment(nodes:) does the mapping once at load time and is cached
on DataManager so every render is a dictionary lookup.
DataManager:
- nodeColors / nodeNames now serve cast colors and cast display names so
every existing call site that reads them automatically picks up the new
naming with no chapter-by-chapter changes required.
- castRole(for:), castColor(for:), laneIndex(for:), castOrderedNodes() —
helpers chapters can opt into when they want lane order to follow the
cast (Aaron→Ben→Carl→Dave) rather than the simulation's raw node order.
Persistent sidebars (Sources/CrisisViz/Views/Sidebars.swift):
- CastSidebar (180pt left edge): one card per lead with color swatch,
display name, and personality cue. BYZ badge on Dave.
- LegendSidebar (200pt right edge): persistent encoding rules — color
= validator, stripe = round, border = vertex state, edge style =
parent link. The answer to "every view looks different".
- ImmersiveView wraps the SceneRouter in an HStack(CastSidebar, scene,
LegendSidebar) so chapters get a slightly narrower canvas without
needing to know the sidebars exist.
Story-beat titles (ChapterDefinitions + SceneNarrations):
- Chapter titles are now full sentences with a [Technical: ...] suffix
("Aaron speaks. Ben listens. The graph begins. [Technical: gossip
& DAG]"). Story-beat for the noob, technical handle for the engineer.
- Scene titles likewise rewritten as continuous narrative beats.
- All chapter narrations rewritten to address the cast by name and walk
the reader through what is happening rather than asserting it.
Decisions saved to memory at project_redesign_decisions.md so future
sessions know cast names, title format, and morph-not-cut policy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Testbed (Sources/CrisisViz/Testbed/SceneCapture.swift):
- Add convergenceFineOffsets (21 slices), textScaleLadder (6 scales),
canvasSizeMatrix (7 sizes), and four extended capture functions
(captureComparisonAtAllSizes/AllScales, captureConvergenceFineGrained,
captureInspectorAtAllScales) — testbed now produces 279 PNGs across
16 folders.
- pickConvergingPair(snap:) BFS helper picks two vertices whose depth-3
cones share >=2 ancestors, so comparison sweeps always show a real
convergence story instead of a degenerate pair.
- runSanityChecks() walks every PNG, flags tiny files (<8KB), and
distinguishes real mid-animation freezes (3+ byte-identical frames
surrounded by varying frames) from expected settled-tail plateaus
by extracting numeric _t<value>s suffixes.
- runWindowResizeUnitTests() returns a 12-case ResizeUnitReport
covering shrink-below-min, grow-past-screen (PREVENTS TILING),
exact boundaries, ultrawide, and headless harness — currently 12/12.
Window resize (Sources/CrisisViz/App/CrisisApp.swift):
- Mark CrisisAppDelegate @MainActor and conform to NSWindowDelegate.
- Static clampResize(proposed:visibleSize:) extracted as pure helper
so the testbed can unit-test the resize policy without an NSWindow.
- windowWillResize caps proposed sizes at the screen visibleFrame so
drags never push past the menu bar — that push is what arms macOS
15+/Tahoe automatic edge tiling and silently locks every handle
except the left edge.
- Opt out of auto-tiling explicitly via .fullScreenDisallowsTiling and
re-attach our delegate on didBecomeKey, because SwiftUI replaces it
on key changes.
- Initial window is 80% of visibleFrame, centered (not flush against
edges), so the very first drag cannot trip the tile gesture.
AppSettings (Sources/CrisisViz/Engine/AppSettings.swift):
- New @Observable @MainActor settings object injected via
.environment(settings) at App scope; survives the entire process
and feeds the inspector text-scale slider.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- bundle.sh assembles CrisisViz.app (Info.plist + AppIcon.icns + resources)
and ad-hoc codesigns it so it launches as a proper Foreground app.
- Tools/MakeAppIcon.swift renders the app icon programmatically (10 sizes,
16-1024 px) as a 3-round mini-DAG matching the live node palette.
- CrisisApp.swift forces .regular activation policy via NSApplicationDelegate
so the Dock tile and menu bar appear even when launched unbundled
via `swift run CrisisViz`.
- Ignore build artifacts (.build, AppIcon.iconset, CrisisViz.app) and the
user-local .claude/ auto-memory directory.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Keynote-style presentation of the Crisis consensus protocol — 36 scenes
across 10 chapters, single-TimelineView Canvas rendering at 60fps with
Liquid Glass chrome. Driven by the simulation JSON dump.
The signature feature is the click-to-inspect overlay on Ch02: tapping a
vertex opens a recursive hash-unwrapping animation that reveals the
selected message's payload, then its parent hashes, then their pre-images
(parent messages with their own payloads + grandparent hashes), staggered
through to genesis. Makes the abstract idea of "what does a vertex know?"
visceral.
Includes a time-scrubbing testbed harness (`swift run CrisisViz --testbed`)
that captures 180 scene PNGs + 18 inspector reveal PNGs at successive time
offsets, with a MANIFEST quality checklist for human-eye verification.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>