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>
The simulation now optionally records structured events (message creation,
delivery, round computation, voting, leader election) via EventRecorder and
exports a complete simulation dump to JSON via the new export_json module.
crisis_data.json captures a 10-step run that the SwiftUI visualizer consumes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five bugs prevented the full consensus pipeline from producing results:
1. k-reachability with k=0 required weight > 0, but some vertices had
weight 0. Fixed: k <= 0 degenerates to simple past-containment check.
2. SVP incorrectly included the current round (v.round). The paper's
Algorithm 6 only includes rounds strictly < v.round. With the current
round in SVP, the voting set contained only the vertex itself (peers
are spacelike), making agreement impossible.
3. Stage delta (δ) was computed as the SVP index, but the paper defines
δ = d_{svp}(s, t) as distance from s=max(svp). Fixed: δ=0 at the
newest round (initial proposal), increasing toward older rounds.
4. The voting set was recomputed per-round, but Algorithm 7 line 6
computes it ONCE for s=max(svp). Fixed: single voting set S shared
across all stages.
5. Demo parameters (difficulty=2, pow_zeros=0) made thresholds
unreachable. Calibrated: difficulty=1, pow_zeros=2 gives weights
that cross both the is_last (3*d) and SVP (6*d) thresholds.
Result: 3 honest nodes now converge on identical total order. Leaders
are elected via the full BA* pipeline (initial proposal → presorting →
gradecast → BBA binary agreement). Byzantine nodes cannot prevent
convergence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Complete Python PoC of "Probabilistically Self Organizing Total Order
in Unstructured P2P Networks". Implements all 10 algorithms from the paper:
message generation, integrity checks, Lamport graphs, virtual synchronous
rounds, safe voting patterns, virtual leader election (BA*), longest chain
rule, total order via Kahn's algorithm, and push/pull gossip.
Includes simulation harness, full node binary, and 72 passing tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>