diff --git a/docs/challenge-explainers.md b/docs/challenge-explainers.md index 95567ad..4f156d8 100644 --- a/docs/challenge-explainers.md +++ b/docs/challenge-explainers.md @@ -83,3 +83,21 @@ 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. + +## Work and resources batch + +These four diagrams share ink typography but not a layout metaphor. Each one +answers a single question with the exact numbers in its public example. A +labelled final frame must remain useful without motion or color perception. + +| Story | Surface | Single visual transition | +| --- | --- | --- | +| Chunk Store | Cool cyan paper with a dark frame boundary | A two-byte selection lights up `CD` in block 1; block 0 remains untouched. | +| Deadline Scheduler | Ink paper with coral worker lane | C arrives at tick 1 while A continues until tick 6; only then is another dispatch possible. | +| Durable Queue | Mint field with orange leased state | Nack returns A behind B without releasing resident capacity; B is next. | +| Fair Gate | Butter field with cobalt token cells | Spend three of four, reject cost three, refill two at tick 3, then admit. | + +The typography remains Bricolage for the question and JetBrains Mono for +bytes, ticks, and tokens. Motion is a finite reveal, never a loop. The queue +example chooses `Nack` rather than expiry because it matches the published +worked example; the caption explains that expiry follows the same tail rule. diff --git a/src/lib/arena/statement.ts b/src/lib/arena/statement.ts index 2fd471f..113f6ac 100644 --- a/src/lib/arena/statement.ts +++ b/src/lib/arena/statement.ts @@ -77,6 +77,10 @@ const conceptFigureProps = { "systems.journal-story": z.object({}).strict(), "systems.lsm-story": z.object({}).strict(), "systems.window-story": z.object({}).strict(), + "systems.chunk-story": z.object({}).strict(), + "systems.scheduler-story": z.object({}).strict(), + "systems.queue-story": z.object({}).strict(), + "systems.gate-story": z.object({}).strict(), } as const; const figureBlock = z.object({ @@ -88,6 +92,7 @@ const figureBlock = z.object({ "systems.ranges", "systems.exchange", "systems.cache-story", "systems.lost-ack-story", "systems.journal-story", "systems.lsm-story", "systems.window-story", + "systems.chunk-story", "systems.scheduler-story", "systems.queue-story", "systems.gate-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 d659702..85904fc 100644 --- a/src/packs/systems/index.ts +++ b/src/packs/systems/index.ts @@ -5,6 +5,7 @@ import { BudgetDiagram, ExchangeDiagram, FrameDiagram, RangeDiagram, SnapshotDia import { FlowDiagram, StateDiagram, TimelineDiagram } from "./illustrations"; import { JournalRecoveryStory, LsmOverlapStory, StreamWatermarkStory } from "./recovery-time-stories"; import { CacheStory, LostAckStory } from "./story-illustrations"; +import { ChunkReadStory, SchedulerStory, QueueLeaseStory, TokenGateStory } from "./work-stories"; registerIllustration("systems.flow", FlowDiagram); registerIllustration("systems.state", StateDiagram); @@ -19,3 +20,7 @@ registerIllustration("systems.lost-ack-story", LostAckStory); registerIllustration("systems.journal-story", JournalRecoveryStory); registerIllustration("systems.lsm-story", LsmOverlapStory); registerIllustration("systems.window-story", StreamWatermarkStory); +registerIllustration("systems.chunk-story", ChunkReadStory); +registerIllustration("systems.scheduler-story", SchedulerStory); +registerIllustration("systems.queue-story", QueueLeaseStory); +registerIllustration("systems.gate-story", TokenGateStory); diff --git a/src/packs/systems/work-stories.tsx b/src/packs/systems/work-stories.tsx new file mode 100644 index 0000000..7089022 --- /dev/null +++ b/src/packs/systems/work-stories.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +import { Replay, useVisibleStory } from "./story-illustrations"; + +function StoryFooter({ copy, finished, reducedMotion, replay }: { + copy: string; + finished: boolean; + reducedMotion: boolean; + replay: () => void; +}) { + return ( +
+

{copy}

+ {finished && !reducedMotion && } +
+ ); +} + +const CHUNK_STEPS = [ + "ReadAt(5, 2) asks for bytes C and D.", + "The index points to block 1. Block 0 is not read.", + "Verify and decode block 1, then return CD.", +] as const; + +export function ChunkReadStory() { + const { ref, stage, reducedMotion, replay } = useVisibleStory(2, 1250); + return ( +
+
+
+

Read only what you need.

+ ReadAt(5, 2) +
+ +
+ logical bytes 0–7{stage === 2 ? "result: CD" : "one indexed frame"} +
+
+ +
+ ); +} + +const SCHEDULER_STEPS = [ + "A takes the only worker at tick 0.", + "C arrives at tick 1, but cannot interrupt A.", + "A finishes at tick 6. The next choice happens now.", +] as const; + +export function SchedulerStory() { + const { ref, stage, reducedMotion, replay } = useVisibleStory(2, 1400); + return ( +
+
+
+

Arrival isn’t interruption.

+ 1 worker +
+ + +
+ +
+ ); +} + +const QUEUE_STEPS = [ + "A and B are ready; both occupy capacity.", + "Lease A. It is still resident while B waits.", + "Nack A. Its old token is stale; B is now next.", +] as const; + +export function QueueLeaseStory() { + const { ref, stage, reducedMotion, replay } = useVisibleStory(2, 1350); + return ( +
+
+
+

Returned means last.

+ FIFO by availability +
+ +
+ resident: A + B{stage === 2 ? "old A token: stale" : "capacity held"} +
+
+ +
+ ); +} + +const GATE_STEPS = [ + "At tick 0, the burst budget contains four tokens.", + "Cost 3 is allowed; one token remains.", + "Another cost 3 is rejected. Earliest retry is tick 3.", + "At tick 3, two tokens have accrued. Three are available.", + "The retry spends all three. Zero remain.", +] as const; + +export function TokenGateStory() { + const { ref, stage, reducedMotion, replay } = useVisibleStory(4, 1150); + const tokens = [4, 1, 1, 3, 0][stage]; + return ( +
+
+
+

Wait for the exact tick.

+ 2 tokens / 3 ticks +
+ + +
+ ); +} diff --git a/tests/e2e/challenge-explainers.spec.ts b/tests/e2e/challenge-explainers.spec.ts index dd4c69f..699583e 100644 --- a/tests/e2e/challenge-explainers.spec.ts +++ b/tests/e2e/challenge-explainers.spec.ts @@ -48,6 +48,33 @@ test.describe("challenge explainers", () => { await expect(page.getByText("count 1 · sum 7")).toBeVisible(); }); + test("chunk read decodes only the indexed block", async ({ page }) => { + await page.goto("/challenges/chunk-store-easy"); + await expect(page.getByRole("img", { name: /Only block 1 is verified and decoded/ })).toBeVisible(); + await expect(page.getByText("CRC verified · CD copied")).toBeVisible(); + await expect(page.getByText("result: CD")).toBeVisible(); + }); + + test("non-preemptive worker makes its next choice at tick six", async ({ page }) => { + await page.goto("/challenges/deadline-scheduler-easy"); + await expect(page.getByRole("img", { name: /C arrives at tick 1 and waits/ })).toBeVisible(); + await expect(page.getByText("next dispatch @ 6")).toBeVisible(); + }); + + test("nack returns A behind B and keeps capacity occupied", async ({ page }) => { + await page.goto("/challenges/durable-queue-easy"); + await expect(page.getByRole("img", { name: /ready order B then A/ })).toBeVisible(); + await expect(page.getByText("resident: A + B")).toBeVisible(); + await expect(page.getByText("old A token: stale")).toBeVisible(); + }); + + test("the token gate admits the retry at the exact refill tick", async ({ page }) => { + await page.goto("/challenges/fair-gate-easy"); + await expect(page.getByRole("img", { name: /earliest RetryAt tick three/ })).toBeVisible(); + await expect(page.getByText("3 spent · 0 left")).toBeVisible(); + await expect(page.getByText("ALLOWED", { exact: true })).toBeVisible(); + }); + test("the finite story settles and can be replayed", async ({ page }) => { await page.emulateMedia({ reducedMotion: "no-preference" }); await page.goto("/challenges/cache-pressure-easy");