Commit graph

39 commits

Author SHA1 Message Date
54aae1a4dd Update all documentation for the crisis_agents layer + async refactor
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>
2026-05-14 22:13:00 +02:00
0976239ebd crisis_agents: drop the wall-clock, drive asynchronously to quiescence
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>
2026-05-14 22:06:56 +02:00
a1064660d5 Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum
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>
2026-05-14 21:55:49 +02:00
6aa2c54b68 Add LiveClaudeAgent — back honest agents with real Claude API calls
`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>
2026-05-14 16:38:25 +02:00
b8684297fa Add crisis_agents — Crisis as a coordination layer for AI agent teams
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>
2026-05-14 16:38:11 +02:00
7f830a36ef Advance Python test coverage — voting, recorder, simulation extensions
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>
2026-05-14 15:52:30 +02:00
7c16d16a05 Add CrisisViz/package-dmg.sh — distributable DMG installer
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>
2026-05-14 15:52:14 +02:00
8ada0af0bc Refresh CrisisViz/HANDOFF.md — current state for the next coding agent
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>
2026-05-14 15:52:03 +02:00
5c6d220b38 Add CrisisViz/README.md — Swift-side human guide
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>
2026-05-14 15:51:50 +02:00
948b8864e5 Add INSTALL.md — clone-to-running on a fresh macOS box
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>
2026-05-14 15:51:39 +02:00
15a00f8438 Rewrite parent README — five-layer architecture + audience-shaped quick start
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>
2026-05-14 15:51:31 +02:00
2d7466fe93 Add MIT LICENSE; align pyproject.toml accordingly
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>
2026-05-14 15:51:19 +02:00
fb9bc9c726 Add README — Crisis PoC + educational visualizer
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>
2026-05-08 16:28:02 +02:00
5b4fe4ebaa Ch08 erasure shards: migrate to serial timeline + per-cast vaults
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>
2026-05-07 22:30:29 +02:00
32f01e3466 Ch07 DA problem: migrate to serial timeline + Aaron's vault
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>
2026-05-07 22:24:46 +02:00
6345ae94f5 Ch06 total order: migrate to serial timeline + ordering snake
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>
2026-05-07 11:49:34 +02:00
598bee5b81 Ch05 leader election: migrate to serial timeline
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>
2026-05-07 11:43:37 +02:00
6222755f92 Ch04 voting: migrate to serial timeline + ancestor cone walks
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>
2026-05-07 11:37:18 +02:00
c9689ec7e5 Ch09 Byzantine: migrate to serial timeline + fork detection
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>
2026-05-07 11:17:58 +02:00
b33a2cf4a5 Ch03 rounds: migrate to serial timeline + weight thermometer
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>
2026-05-07 11:10:56 +02:00
08f6e6ff8b Ch02 partition: migrate to serial timeline + perception towers
`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>
2026-05-07 10:56:05 +02:00
35470aadf0 Ch00 + Ch01: perception towers at the bottom
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>
2026-05-07 10:45:52 +02:00
7ac133f0d0 Ch00: migrate to serial timeline pattern
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>
2026-05-07 00:17:57 +02:00
51fe02eadb Ch01 UX audit: clean up the screen pollution
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>
2026-05-07 00:07:46 +02:00
715039bce2 Master-of-time controls: signed speed slider + chapter scrubber
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>
2026-05-06 22:50:11 +02:00
7291645524 Ch01: serial slo-mo timeline + beat-bound narration
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>
2026-05-06 22:45:38 +02:00
2d353b2208 Ch01: unify scenes 0-3 onto lane-anchored gossip dramatization
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>
2026-05-06 20:58:57 +02:00
463d77f072 SceneEngine: real pause + launch paused
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>
2026-05-06 20:46:36 +02:00
da3cd1d338 Ch01: top-anchored perspective panel + slow-motion scenes 4-6
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>
2026-05-06 19:42:32 +02:00
8975d65223 Masterclass redesign: cast lifelines, narrative invariants, video harness
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>
2026-05-06 19:11:36 +02:00
f9a438e066 Silence unused-sim warning in Ch05
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>
2026-05-04 11:23:48 +02:00
35d1b54248 Ch00 cast intro; Ch04 ten-step convergence collapse
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>
2026-05-04 11:22:47 +02:00
06ef8234d5 Foundation redesign: persistent cast, sidebars, story-beat titles
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>
2026-05-04 11:18:11 +02:00
dd185e70ae Extend testbed 10x; fix window resize regressions; @MainActor delegate
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>
2026-05-04 00:07:29 +02:00
93674001cd Add macOS .app bundle with native Dock icon and activation policy
- 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>
2026-04-30 20:21:18 +02:00
c269811f0f Add CrisisViz: native macOS 26 SwiftUI visualizer with vertex inspection
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>
2026-04-30 20:06:58 +02:00
1491422527 Add JSON export pipeline + event recorder for visualization
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>
2026-04-30 20:06:21 +02:00
37e9f26204 Fix round advancement and end-to-end consensus convergence
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>
2026-04-23 15:13:41 +02:00
1df4790fb4 Initial implementation of the Crisis protocol (Richter, 2019)
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>
2026-04-23 13:20:30 +02:00