diff --git a/docs/challenge-explainers.md b/docs/challenge-explainers.md index c3c55ee..95567ad 100644 --- a/docs/challenge-explainers.md +++ b/docs/challenge-explainers.md @@ -59,3 +59,27 @@ check the before/after story at phone width and desktop width; inspect a real screenshot rather than relying on a build; test keyboard replay and reduced motion; and keep the surrounding editorial to one observation, one invariant, and one learner decision. The first pilots are Cache Pressure and Lossy Link. + +## Recovery and time batch + +These mechanisms should not inherit the cache tile composition. Bricolage +states the consequence, Inter explains it in one sentence, and mono marks only +literal keys, LSNs, ticks, and results. Layout stays left-aligned in the narrow +problem pane. + +| Story | Surface and accent | Singular visual action | +| --- | --- | --- | +| Journal Recovery | Pale butter `#fff2c8`, ink `#282334`, verified mint `#a9e8cb`, torn orange `#ff784f` | A scan cursor reaches an incomplete third frame and stops; only the first two form the recovered prefix. | +| LSM Compactor | Lilac `#d9cbff`, ink `#282334`, selected violet `#7954ef`, intersecting orange `#ff784f` | Two selected Level-0 ranges cast one union over Level 1, pulling in both overlapping runs. | +| Stream Windows | Deep blue `#2141b4`, cyan `#53d9ff`, event orange `#ffad83`, final lime `#b7ff45` | A watermark reaches tick 10, emits and removes the left half-open window while the right remains live. | + +```text +journal: [verified 1][verified 2][torn 3] scan stops before 3 +LSM: A [10------30] B [25-------------50] + X [0----20] Y [40------60] +windows: [0,10) | [10,20) watermark = 10 +``` + +The axes are conceptual but use the exact published numbers. Do not interpolate +a fake performance result. In an editor split, keep critical labels legible +without relying on viewport breakpoints. diff --git a/src/lib/arena/statement.ts b/src/lib/arena/statement.ts index ce1c89f..2fd471f 100644 --- a/src/lib/arena/statement.ts +++ b/src/lib/arena/statement.ts @@ -74,6 +74,9 @@ const conceptFigureProps = { "systems.exchange": exchangeProps, "systems.cache-story": z.object({ mode: z.enum(["lru", "scan"]) }).strict(), "systems.lost-ack-story": z.object({}).strict(), + "systems.journal-story": z.object({}).strict(), + "systems.lsm-story": z.object({}).strict(), + "systems.window-story": z.object({}).strict(), } as const; const figureBlock = z.object({ @@ -84,6 +87,7 @@ const figureBlock = z.object({ "systems.snapshot", "systems.budget", "systems.frames", "systems.ranges", "systems.exchange", "systems.cache-story", "systems.lost-ack-story", + "systems.journal-story", "systems.lsm-story", "systems.window-story", ]), props: z.record(z.string(), z.unknown()).optional(), }).strict(), diff --git a/src/packs/systems/index.ts b/src/packs/systems/index.ts index 003a443..d659702 100644 --- a/src/packs/systems/index.ts +++ b/src/packs/systems/index.ts @@ -3,6 +3,7 @@ import { registerIllustration } from "../registry"; import { BudgetDiagram, ExchangeDiagram, FrameDiagram, RangeDiagram, SnapshotDiagram } from "./concept-illustrations"; import { FlowDiagram, StateDiagram, TimelineDiagram } from "./illustrations"; +import { JournalRecoveryStory, LsmOverlapStory, StreamWatermarkStory } from "./recovery-time-stories"; import { CacheStory, LostAckStory } from "./story-illustrations"; registerIllustration("systems.flow", FlowDiagram); @@ -15,3 +16,6 @@ registerIllustration("systems.ranges", RangeDiagram); registerIllustration("systems.exchange", ExchangeDiagram); registerIllustration("systems.cache-story", CacheStory); registerIllustration("systems.lost-ack-story", LostAckStory); +registerIllustration("systems.journal-story", JournalRecoveryStory); +registerIllustration("systems.lsm-story", LsmOverlapStory); +registerIllustration("systems.window-story", StreamWatermarkStory); diff --git a/src/packs/systems/recovery-time-stories.tsx b/src/packs/systems/recovery-time-stories.tsx new file mode 100644 index 0000000..275211a --- /dev/null +++ b/src/packs/systems/recovery-time-stories.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +import { Replay, useVisibleStory } from "./story-illustrations"; + +const JOURNAL_STEPS = [ + "LSN 1 and 2 are synced, complete frames with valid checksums.", + "A third write stops before its payload and checksum are complete.", + "Recovery scans left to right and finds the incomplete tail.", + "Apply LSN 1 and 2 only. Report a torn tail.", +] as const; + +export function JournalRecoveryStory() { + const { ref, stage, reducedMotion, replay } = useVisibleStory(3, 1100); + + return ( +
+
+
+

+ Recovery stops here. +

+ + {stage < 2 ? "write" : "read back"} + +
+ + + +
+ scan + 1 ✓2 ✓ + {stage >= 2 && 3 stops} +
+
+ {stage === 3 ? "recovered prefix: LSN 1–2" : "read complete frames only"} +
+
+
+

{JOURNAL_STEPS[stage]}

+ {stage === 3 && !reducedMotion && } +
+
+ ); +} + +const LSM_STEPS = [ + "Level 0 has two selected runs: A spans 10–30 and B spans 25–50.", + "Together they select the key range 10–50.", + "That range intersects both Level-1 runs, X and Y.", +] as const; + +function RunBar({ label, start, end, selected, color, animate }: { + label: string; + start: number; + end: number; + selected: boolean; + color: string; + animate: boolean; +}) { + return ( +
+ {label} +
+ ); +} + +export function LsmOverlapStory() { + const { ref, stage, reducedMotion, replay } = useVisibleStory(2, 1450); + + return ( +
+
+
+

+ One range pulls in more. +

+ + L0 → L1 + +
+ + + +
+

{LSM_STEPS[stage]}

+ {stage === 2 && !reducedMotion && } +
+
+ ); +} + +const WINDOW_STEPS = [ + "An event at tick 9 adds 4 to the first window.", + "Tick 10 starts the next window; its event adds 7 there.", + "Watermark 10 proves the first window cannot change again.", + "Emit [0,10) once and evict it. [10,20) stays live.", +] as const; + +export function StreamWatermarkStory() { + const { ref, stage, reducedMotion, replay } = useVisibleStory(3, 1100); + const finalized = stage === 3; + + return ( +
+
+
+

+ Tick 10 starts over. +

+ + {stage < 2 ? "event time" : "watermark 10"} + +
+ + +
+ = 2 ? "text-[#b7ff45]" : "text-white/65"}> + {finalized ? "emitted · state freed" : stage >= 2 ? "watermark 10 ✓" : "waiting for watermark"} + + {finalized ? "right window live" : "half-open windows"} +
+
+
+

{WINDOW_STEPS[stage]}

+ {stage === 3 && !reducedMotion && } +
+
+ ); +} diff --git a/src/packs/systems/story-illustrations.tsx b/src/packs/systems/story-illustrations.tsx index 4304712..bddc7a2 100644 --- a/src/packs/systems/story-illustrations.tsx +++ b/src/packs/systems/story-illustrations.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; -function useVisibleStory(lastStage: number, intervalMs: number) { +export function useVisibleStory(lastStage: number, intervalMs: number) { const ref = useRef(null); const [visible, setVisible] = useState(false); const [reducedMotion, setReducedMotion] = useState(false); @@ -47,7 +47,7 @@ function useVisibleStory(lastStage: number, intervalMs: number) { }; } -function Replay({ onClick }: { onClick: () => void }) { +export function Replay({ onClick }: { onClick: () => void }) { return (