Commit graph

19 commits

Author SHA1 Message Date
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