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>
The previous driver imposed a synchronous turn-counted clock that the
Crisis paper explicitly forbids — Crisis is supposed to work in
asynchronous P2P networks, with any synchronicity being virtual and
derived inside the consensus algorithm from the DAG structure, not
imposed externally by a coordinator. This commit removes the wall clock.
What changed in the engine:
- `Mothership.run_crisis_phase(num_turns, gossip_rounds_per_turn)`
is replaced by `run_until_quiescent(max_steps=200)`. The loop
interleaves three concerns on each iteration — emissions, gossip,
and alarm emissions — until none make progress. Termination is by
quiescence, not by a fixed turn count. `max_steps` is a safety
bound (loop-iteration cap), not an exposed clock.
- `Mothership.run_closed_phase(num_turns)` becomes
`run_closed_phase(max_steps=50)`. Same quiescence model — the
closed-phase conversation runs until no agent has more to say.
- Agents grew `pending_alarm_claims()`: each agent checks its own
graph for un-alarmed mutations and produces AlarmClaims directly.
The driver loop calls this every iteration, so alarms emit and
propagate in the same loop as regular emissions and gossip — no
separate "alarm phase."
- `Mothership.emit_alarms_from_detectors()` and the explicit
`run_gossip_round()` step are no longer needed by callers; both
are subsumed by the async loop. `run_gossip_round()` stays as a
helper but tests no longer call it externally.
What changed in the agent interface:
- `CrisisAgent.next_turn(turn, received_claims)` becomes
`try_emit()` — no arguments. Agents in an async network don't see
a global tick. They decide based on their own internal state.
- `CrisisAgent.observe(claim)` is the new optional callback the
closed-phase loop uses to feed context into agents that care
(overridden by LiveClaudeAgent to populate its prompt buffer).
- `pending_alarm_claims()` is idempotent: an internal
`_already_alarmed` set tracks claims this agent has emitted, so
the loop calls it every step without flooding the network with
duplicate alarms.
What changed in the dataclass schema:
- `AlarmClaim.detected_at_turn` -> `emitted_at_step`. The word
"turn" implies a global clock; "step" is a per-agent sequence
number used only for log ordering — local, not networked.
- `ClosedPhaseEntry.turn` and `CrisisPhaseEntry.turn` -> `step`.
Same rename, same reasoning.
- `Scenario.closed_phase_turns` and `Scenario.crisis_phase_turns`
are gone. The scenario no longer prescribes how many turns; it
just provides agents and lets the async loop run them out.
What changed in the CLI:
- Phase 3 reports "drove to quiescence in N step(s)" with a
breakdown of regular emissions / gossip transfers / alarm
emissions, instead of "ran N turns".
- `QuiescenceReport` (new dataclass) carries the run statistics
back from `run_until_quiescent`/`run_closed_phase` — steps taken,
emissions made, gossip transfers, alarm claims emitted, plus
whether termination was via quiescence or max-step cap.
New regression tests (`test_async_quiescence.py`):
- `test_run_until_quiescent_terminates`: the loop must exit.
- `test_two_runs_produce_identical_final_state`: determinism check —
if anything in the loop depended on real wall time, this would
fail.
- `test_max_steps_bound_caps_runtime`: setting max_steps=1 exits
immediately and `QuiescenceReport.reached_quiescence` reflects
reality.
- `test_no_turn_argument_exposed_to_agents`: introspects
`CrisisAgent.try_emit` signature; fails if anyone re-adds a
`turn` parameter.
- `test_no_turn_field_on_alarmclaim`: introspects the dataclass
fields; fails if `detected_at_turn` reappears.
- `test_alarms_propagate_through_async_loop_alone`: the loop alone
(no manual emit_alarms / run_gossip_round) ratifies an alarm.
- `test_quiescence_report_counts_match_logs`: sanity check that
the report's emission count equals the crisis log length.
Suite: 163 -> 170 tests, all green in 0.79s.
Behavioral end-state is identical to the previous (synchronous)
version: same fact-check scenario, same byzantine equivocation, same
proof JSON shape, same three signers, same quorum-met outcome. The
difference is structural: the protocol now matches the paper's async
shape, and a future port to actual TCP gossip + concurrent agents
needs no change to this engine.
CrisisViz: still untouched. The `crisis_data.json` pipeline that
drives the visualizer is orthogonal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous design routed every Crisis message through a `Mothership` that
also held every agent's LamportGraph, ran the byzantine scan from a
privileged vantage, and built proofs from its own view. That made the
mothership a chokepoint — exactly what a BFT layer is supposed to remove.
This commit redistributes responsibility along the lines you'd expect from
a real open protocol:
Each `CrisisAgent` now owns:
- its own `LamportGraph` (the agent's view of the network)
- `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis
Message built from the agent's OWN graph state, with chain link + cross
references + mined PoW nonce
- `receive(message)`: extends my graph if integrity holds; idempotent
- `gossip_to(peer) → int`: shares everything I have with peer until
quiescence (Algorithm 4 in the paper, in-process flavor)
- `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id
spacelike vertex pairs via the existing
`LamportGraph.find_mutations`, filtered by application-layer
`statement_id` so cross-detector AlarmClaims canonicalize
The `Mothership` shrinks to coordinator-only:
- bootstrap (register honest agents; trigger boundary open with a joiner)
- clock (call each agent's `next_turn()` per turn)
- first-hop routing (sender's emission → declared target subset)
- all-pairs gossip rounds between turns
- emit_alarms_from_detectors(): poll each agent for its LocalAlarms,
wrap any returned alarms into AlarmClaim payloads, broadcast them as
Crisis Messages over the gossip layer
Gone (regression-tested in `test_no_chokepoint.py`):
- `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()`
- `alarm.scan_for_mutations(mothership)`
- any path where the mothership reads an agent's internal state
New voting layer (`crisis_agents/vote.py`):
- `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`.
Wraps the accused process_id, statement_id, witness_digests, and
detection turn. Round-trips through JSON same as Claim.
- `quorum_for(n) = ceil(2n/3)`: classic BFT threshold.
- `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by
(accused, statement_id, witness_pair), counts unique signer
process_ids, ratifies groups meeting the threshold. Deterministic
ordering so two equal graphs produce equal `RatifiedAlarm` lists.
- `RatifiedAlarm`: the network-level consensus on byzantine behavior.
Multi-signer proofs (`crisis_agents/proof.py`):
- schema_version bumped 1 → 2.
- ProofDocument now embeds every signer's process_id_hex and the
quorum threshold that was met. Self-consistency check enforces
distinct signers, witness pairs, and signer count ≥ threshold.
Byzantine scenario rewrite:
- `MockByzantineAgent` now takes an `intro_claim` for its first turn (a
benign broadcast). The intro is technically necessary: the agent's two
contradictory variants both chain to the intro vertex, so they can
propagate through gossip — without it, the second variant would fail
the chain constraint in any graph already holding the first.
- `fact_check` scenario: closed phase still has 3 honest agents emitting
6 claims each into the closed log; Crisis phase grew to 2 turns (intro
+ equivocation) so the byzantine can establish its same-id anchor
before equivocating.
End-to-end CLI output reframed around six phases:
1. closed team (no Crisis)
2. boundary opens
3. emission + gossip
4. decentralized detection (each agent reports its own findings)
5. alarms emitted + gossiped + ratified by quorum
6. proof emission
Tests (51 fresh + 5 carried over for boundary):
- `test_mothership.py`: per-agent graph ownership, broadcast vs.
targeted delivery semantics, gossip propagation, regression guards
against the removed centralization attributes.
- `test_alarm.py`: every honest agent independently detects the same
mutation; the byzantine doesn't detect itself; witness pairs are
canonical across detectors.
- `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally
determinism, mothership convenience method matches direct tallying.
- `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON
round-trip; tampered-witness/below-quorum/duplicate-signer rejection.
- `test_no_chokepoint.py` (the centerpiece): after the full lifecycle,
every honest agent's ratified-alarm set is byte-identical. A single
byzantine accuser alone cannot ratify. Forbidden attributes don't
exist on Mothership.
Full suite: 163 tests, all green in 0.80s.
CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline
the visualizer consumes is produced by the orthogonal
`crisis.demo.Simulation`, which this commit doesn't touch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`crisis-agents demo --live` swaps the three honest MockAgents for
LiveClaudeAgent instances that issue one Anthropic Messages API call
per turn. The byzantine joiner stays mocked: making the byzantine
deterministic with an LLM would require multiple API calls per turn
(one per peer subset) for unreliable yields. Better demo legibility
to keep the equivocator scripted.
Prompt shape: the honest agent receives the reference doc, a list of
statements still to adjudicate, and the last 12 claims observed from
peers; it responds with a JSON array of {statement_id, verdict,
confidence, evidence} objects. The parser tolerates markdown fences
and per-item validation failures; malformed responses produce no
emissions rather than crashing the demo.
Default model: claude-haiku-4-5-20251001 — fast enough and cheap
enough for short-form structured-output adjudication. Override with
`--model <id>`.
Dependency: anthropic SDK as an optional install — `pip install -e
".[live]"`. Lazy-imported so the mocked path never needs it.
Tests: 6 new tests in test_live_agent.py using a fake Anthropic client
(no real API calls in CI):
- clean JSON response parsing
- markdown-fence tolerance
- malformed-response graceful degradation
- per-item validation skipping
- already-adjudicated statement filtering (the agent doesn't keep
re-asking about statements it has already answered)
- evidence-length truncation to Claim.EVIDENCE_MAX_LEN
Suite: 145 -> 150 tests, all green in 0.77s.
Manual test (not in CI; requires API credits):
pip install -e ".[live]"
export ANTHROPIC_API_KEY=...
crisis-agents demo --live
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A new sibling Python package, `crisis_agents`, that lifts the Crisis
protocol from "consensus between machines" to "consensus between AI
agents". Threat model: a team of sub-agents normally talks freely
with its orchestrator (the "mothership"); when the team's boundary
opens and an external agent of unknown trust joins, the mothership
activates the Crisis layer so byzantine equivocation is detectable.
Two-phase orchestration model:
Phase 1 — closed team, no Crisis: agents emit claims directly, the
mothership collects them flat.
Phase 2 — boundary opens: every subsequent claim is wrapped into a
Crisis Message with the agent's stable process_id and a PoW nonce,
delivered into per-agent LamportGraphs, and after each turn the
mothership scans for mutations via LamportGraph.find_mutations.
Phase 3 — proof: when an alarm fires, the mothership emits a
replayable JSON proof-of-malfeasance document with the contradictory
witnesses, their delivery sets, and DAG cross-references showing
which honest agents saw what.
Modules:
- claim.py Claim dataclass + JSON round-trip
- boundary.py membership tracker + open() trigger
- agent.py CrisisAgent abstract + MockAgent + MockByzantineAgent
(the latter equivocates by emitting two variants to
disjoint peer subsets at the same logical turn)
- mothership.py orchestrator driving both phases, building Crisis
Messages from Claims, per-agent LamportGraphs, log
- alarm.py scan_for_mutations: same-agent same-turn distinct
digests with non-identical delivery sets, verified
spacelike via LamportGraph.are_spacelike on the
honest-agent graphs
- proof.py build_proof + ProofDocument + JSON serializer +
verify_proof_self_consistent
- cli.py `crisis-agents demo` + `crisis-agents verify`
- scenarios/ fact_check: reference doc + 6 statements + scripted
honest/byzantine agents producing a deterministic
equivocation on statement s03
Tests: 50 new tests across test_claim, test_boundary, test_mothership,
test_alarm, test_proof, test_demo_fact_check. End-to-end test runs the
fact_check scenario, asserts exactly one alarm raised, proof is built,
re-serialized JSON passes self-consistency. Full suite (existing
crisis + new crisis_agents) green in 0.77s — 145 tests.
Out of scope (deliberately): visualization (separate CrisisViz upgrade
later), real TCP gossip (agents talk via in-process function calls in
the mothership), false-claim detection without equivocation (an
agent that consistently lies but never equivocates is out-voted, not
"caught"; catching it would require a ground-truth oracle).
Reuse from existing crisis package: Message, Vertex, LamportGraph,
LamportGraph.find_mutations, ProofOfWorkWeight, digest. The new code
is a thin adapter layer; the protocol substrate did the heavy lifting.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pre-existing tests covered crypto / graph / message / order / rounds /
weight, but left three high-value modules unverified:
- voting.py — 25 KB of BBA virtual leader election + safe voting
pattern (Algorithms 6 & 7), the heart of the protocol. Zero
tests. Now 14 tests covering the four public entry points
(`build_knowledge_graph`, `select_quorum`, `voting_set`,
`compute_safe_voting_pattern`, `compute_virtual_leader_election`)
plus `initial_vote`. Uses a small in-process Simulation to
produce realistic multi-round graphs.
- recorder.py — the bridge that turns simulation runs into the
JSON consumed by CrisisViz. Zero tests despite being the choke
point: if recorder silently drops fields, the viz lies. Now 11
tests covering EventRecorder bookkeeping (sequence, filtering),
SimulationRecording integration (STEP_BEGIN/END,
MESSAGE_CREATED/DELIVERED), capture_snapshot well-formedness,
and JSON-serializability of both snapshots and event data.
- test_simulation.py extended with three regression guards:
- test_byzantine_vertices_flagged_in_snapshots: ensures the
`is_byzantine_source` flag survives the recorder pipeline.
CrisisViz's Ch10 (byzantine) chapter relies on this to
colour Dave's lane red.
- test_recorder_deterministic_with_seed: same seed produces
identical event-stream length and type ordering. Tightens
the existing vertex-count determinism check.
- test_consensus_pipeline_progresses: a fast claim that rounds
advance past 0 and the SVP / voting code paths engage. The
stronger claim (full convergence + non-empty total order)
takes minutes in pure Python and belongs in a separate
long-running benchmark, not the unit-test suite — but the
weaker claim is sufficient to catch the dead-pipeline
failure mode that motivated regenerating crisis_data.json
on 2026-05-04.
Suite: 72 -> 100 tests, all green in ~0.75s.
Explicitly out of scope (separate engineering effort):
- gossip.py / node.py TCP integration tests — heavy harness;
- export_json.py — thin composition of tested layers;
- Swift XCTest — the CrisisViz testbed harness already covers
the curriculum-correctness layer.
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>
End-to-end install guide aimed at a developer who has just cloned
the repo onto an unconfigured Mac. Covers:
- prerequisites (macOS 26 Tahoe, Xcode 17+, Python 3.11, git);
- Python venv + editable install + pytest verification;
- regenerating crisis_data.json and copying it into the bundled
visualizer resources;
- three Swift build paths — `swift build`/`swift run` for the
dev binary, `bundle.sh` for the .app, `package-dmg.sh` for a
distributable DMG;
- testbed run + where the five report files land;
- troubleshooting for the common first-time failures
(Gatekeeper, missing data, missing toolchain, blank window).
This is the document we hand to someone who wants the visualizer
to run on a target Mac without first having to reverse-engineer
the project layout.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous 23-line README called the implementation a "Go PoC"
(it is Python) and was silent on installation, configuration,
running, regenerating the simulation, and — most importantly — the
architectural shape of the project. Anyone reading the old README
would not learn that there are two parallel implementations of "a
Crisis player" (the real distributed `CrisisNode` over TCP and the
in-process `SimulatedNode`), nor that only the latter feeds the
recording pipeline that drives CrisisViz.
This rewrite adds:
- a Mermaid flowchart of the five layers (paper -> pure
algorithms -> two runtimes -> recorder + JSON exporter ->
CrisisViz playback) that GitHub renders natively;
- an annotated repo layout;
- three audience-shaped quick-start blocks (pytest for the
protocol, `python -m crisis.demo` for the simulation, the
Swift bundler for the visualizer);
- pointers to the new INSTALL.md, CrisisViz/README.md, and
refreshed HANDOFF.md.
The Go/Python slip is corrected throughout.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The repo had no LICENSE file and pyproject.toml claimed CC-BY-4.0
— a license that isn't designed for code and conflicts with the
project's actual intent of letting people fork, learn from, and
redistribute the implementation. The paper itself remains CC-BY-4.0
as a separately licensed artifact; that fact is now stated in the
LICENSE file and in the parent README.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Short framing: this repo is a proof-of-concept and educational
artifact for Mirco Richter's Crisis paper, comprising a Go PoC, a
Python recorder, and CrisisViz — a native macOS/SwiftUI curriculum
visualizer that walks the protocol end to end across ten chapters
in serial slow motion. Speed slider goes -16x to +16x with reverse
playback; narration is beat-bound to whichever moment the playhead
sits on.
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>
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>