Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/challenge-explainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,18 @@ 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.

## SSD ladder batch

The ranked ladder keeps its original blue/orange identity but each rung asks
one harder question. The examples remain teaching examples, not grades.

| Rung | Surface | Consequential action |
| --- | --- | --- |
| Compaction | Peach field, ink slot array, cyan filled holes | Three values outside the five-slot prefix move directly into its three holes. Slot 3 stays put. |
| Victim Selection | Cobalt field, one-page lime marker | Block 3 migrates one live page, then erases eight pages; block 1 would migrate two. |
| Wear Levelling | Lilac field, two contrasted wear columns | On the separate worked snapshot, block 3 has 17 erases; choosing block 1 costs one extra migration without worsening that tail. |

The previous Compaction end frame showed `[4,7,2,9,5]` and claimed three
moves. That order requires moving the resident 7 as a fourth move. The correct
three-move end frame is `[4,2,9,7,5]`.
34 changes: 11 additions & 23 deletions prisma/seed/challenges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,6 @@ import { COMPACTION_STARTERS, VICTIM_STARTERS } from "./starters";

const ALL_LANGUAGES = ["GO", "PYTHON", "C", "CPP", "JAVA", "RUST"];

/** A small hand-made instance, used to illustrate rather than to grade. */
const EXAMPLE_SLOTS = [4, -1, -1, 7, -1, 2, -1, -1, 9, -1, -1, 5];

/** A device mid-life: some blocks mostly dead, one already empty. */
const EXAMPLE_BLOCKS = [
{ index: 0, valid: 7, invalid: 1, free: 0, eraseCount: 3, isOverProvision: false },
{ index: 1, valid: 2, invalid: 6, free: 0, eraseCount: 4, isOverProvision: false },
{ index: 2, valid: 8, invalid: 0, free: 0, eraseCount: 2, isOverProvision: false },
{ index: 3, valid: 1, invalid: 7, free: 0, eraseCount: 5, isOverProvision: false },
{ index: 4, valid: 4, invalid: 3, free: 1, eraseCount: 3, isOverProvision: false },
{ index: 5, valid: 0, invalid: 0, free: 8, eraseCount: 6, isOverProvision: true },
];

const UNEVEN_WEAR = [3, 4, 2, 5, 3, 17, 4, 3, 2, 4, 3, 5];

export const CHALLENGES = [
// ------------------------------------------------------------ rung 1
{
Expand Down Expand Up @@ -82,16 +67,17 @@ export const CHALLENGES = [
{
kind: "figure",
label: "FIG 1",
illustration: { id: "ssd.slotStrip", props: { slots: EXAMPLE_SLOTS, prefix: 5 } },
presentation: "immersive" as const,
illustration: { id: "ssd.compaction-story", props: {} },
caption:
"Five live values scattered across twelve slots. The line marks where they need to end up: slots 0 to 4.",
"Five live values scattered across twelve slots. Three direct moves fill the empty positions in the target prefix, slots 0 to 4.",
},
{
kind: "figure",
label: "FIG 2",
illustration: {
id: "ssd.slotStrip",
props: { slots: [4, 7, 2, 9, 5, -1, -1, -1, -1, -1, -1, -1], prefix: 5, highlight: [1, 2, 4] },
props: { slots: [4, 2, 9, 7, 5, -1, -1, -1, -1, -1, -1, -1], prefix: 5, highlight: [1, 2, 4] },
},
caption:
"Move the three values outside the prefix into its three holes. Each value moves once; all free slots end up together at the back.",
Expand Down Expand Up @@ -201,11 +187,12 @@ export const CHALLENGES = [
kind: "figure",
label: "FIG 1",
illustration: {
id: "ssd.blockGrid",
props: { blocks: EXAMPLE_BLOCKS, pagesPerBlock: 8 },
id: "ssd.victim-story",
props: {},
},
presentation: "immersive" as const,
caption:
"Six blocks of eight pages. Block 3 is almost entirely dead; block 2 is entirely live. Block 5 is over-provision space, held back for migrations.",
"Among six blocks of eight pages, block 3 needs only one live-page migration before erase. The held-back reserve receives that page.",
},
{
kind: "callout",
Expand Down Expand Up @@ -346,9 +333,10 @@ export const CHALLENGES = [
{
kind: "figure",
label: "FIG 1",
illustration: { id: "ssd.wearHistogram", props: { eraseCounts: UNEVEN_WEAR } },
illustration: { id: "ssd.wear-story", props: {} },
presentation: "immersive" as const,
caption:
"This policy optimises only for write amplification. Block 5 has 17 erases; the next highest has 5. One block is wearing more than three times as fast as the others.",
"A wear-aware choice may migrate one more live page to avoid erasing the most-worn block again. Whether that trade is worthwhile depends on future writes.",
},
{
kind: "callout",
Expand Down
135 changes: 135 additions & 0 deletions src/packs/ssd/illustrations/ladder-stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"use client";

import { cn } from "@/lib/utils";
import { Replay, useVisibleStory } from "@/packs/story-motion";

function Footer({ text, final, still, replay }: { text: string; final: boolean; still: boolean; replay: () => void }) {
return (
<div className="mt-4 flex min-h-10 items-start justify-between gap-3 border-t border-current/25 pt-3">
<p className="text-[13px] leading-snug font-medium">{text}</p>
{final && !still && <Replay onClick={replay} />}
</div>
);
}

export const COMPACTION_FRAMES = [
[4, -1, -1, 7, -1, 2, -1, -1, 9, -1, -1, 5],
[4, 2, -1, 7, -1, -1, -1, -1, 9, -1, -1, 5],
[4, 2, 9, 7, -1, -1, -1, -1, -1, -1, -1, 5],
[4, 2, 9, 7, 5, -1, -1, -1, -1, -1, -1, -1],
] as const;
const COMPACTION_STEPS = [
"Five live values; three sit beyond the required prefix.",
"Move slot 5 into the first hole, slot 1.",
"Move slot 8 into the next hole, slot 2.",
"Move slot 11 into slot 4. Slot 3 never moves.",
] as const;

export function CompactionStory() {
const { ref, stage, reducedMotion, replay } = useVisibleStory(3, 1200);
const moves = ["find the holes", "5 → 1", "8 → 2", "11 → 4"][stage];
return (
<div ref={ref} className="overflow-hidden rounded-[18px] bg-[#ffd5ba] p-5 text-[#282334]">
<div role="img" aria-label="Five live values occupy slots 0, 3, 5, 8, and 11. The target prefix is slots 0 through 4. In three moves, slot 5 goes to hole 1, slot 8 to hole 2, and slot 11 to hole 4. Slot 3 remains in place. The final array is 4, 2, 9, 7, 5 followed by seven empty slots.">
<div className="flex items-start justify-between gap-2">
<h3 className="font-[family-name:var(--font-editorial)] text-[25px] leading-[1.03] font-extrabold tracking-[-.04em]">Three moves. No more.</h3>
<span className="shrink-0 rounded-full bg-[#282334] px-2.5 py-1 font-mono text-[10px] text-white">{stage} / 3</span>
</div>
<div className="mt-7 rounded-[12px] bg-[#282334] p-3 text-white" aria-hidden="true">
<div className="mb-2 flex justify-between font-mono text-[10px] font-bold"><span>PACK INTO 0–4</span><span>{moves}</span></div>
<div className="grid grid-cols-6 gap-1">
{COMPACTION_FRAMES[stage].map((value, index) => (
<div key={index} className={cn("flex h-11 flex-col items-center justify-center rounded-[5px] font-mono text-[14px] font-bold", value === -1 ? "border border-white/25 text-white/55" : index < 5 ? "bg-[#53d9ff] text-[#282334]" : "bg-[#ff784f] text-[#282334]")}>
<span className="text-[8px] leading-none opacity-70">{index}</span>
<span>{value === -1 ? "·" : value}</span>
</div>
))}
</div>
<div className="mt-3 flex justify-between font-mono text-[10px] font-bold"><span>target: 5 live</span><span>{stage === 3 ? "free suffix: 7" : "holes in prefix: 3"}</span></div>
</div>
</div>
<Footer text={COMPACTION_STEPS[stage]} final={stage === 3} still={reducedMotion} replay={replay} />
</div>
);
}

const VICTIM_STEPS = [
"Block 1 has two live pages; block 3 has only one.",
"Choose block 3. Copy its one live page into reserve.",
"Erase the whole block. Seven net free pages are gained.",
] as const;

function PageRow({ label, valid, active, erased }: { label: string; valid: number; active: boolean; erased: boolean }) {
return (
<div className={cn("rounded-[9px] p-2.5", active ? "bg-[#b7ff45] text-[#282334]" : "bg-white/15 text-white")}>
<div className="mb-2 flex justify-between font-mono text-[10px] font-bold"><span>BLOCK {label}</span><span>{erased ? "8 FREE" : valid + " LIVE"}</span></div>
<div className="grid grid-cols-8 gap-0.5">
{Array.from({ length: 8 }, (_, i) => (
<span key={i} className={cn("h-9 rounded-[3px]", erased ? "border border-[#282334]/30 bg-white/60" : i < valid ? active ? "bg-[#282334]" : "bg-[#53d9ff]" : active ? "bg-[#282334]/15" : "bg-white/20")} />
))}
</div>
<div className="mt-2 font-mono text-[9px] font-semibold">{erased ? "one copied → eight erased" : (8 - valid) + " dead · " + valid + " to copy"}</div>
</div>
);
}

export function VictimStory() {
const { ref, stage, reducedMotion, replay } = useVisibleStory(2, 1500);
return (
<div ref={ref} className="overflow-hidden rounded-[18px] bg-[#2141b4] p-5 text-white">
<div role="img" aria-label="Each block has eight pages. Block 1 holds two live and six dead pages, while block 3 holds one live and seven dead. Selecting block 3 migrates its one live page into reserve, erases all eight pages, and yields seven net free pages. Block 1 would need two migrations.">
<div className="flex items-start justify-between gap-2">
<h3 className="font-[family-name:var(--font-editorial)] text-[25px] leading-[1.03] font-extrabold tracking-[-.04em] text-white">One copy. Eight free.</h3>
<span className="shrink-0 rounded-full bg-[#53d9ff] px-2.5 py-1 font-mono text-[10px] font-bold text-[#282334]">8 pages / block</span>
</div>
<div className="mt-7 space-y-2" aria-hidden="true">
<PageRow label="1" valid={2} active={false} erased={false} />
<PageRow label="3" valid={1} active={stage >= 1} erased={stage === 2} />
</div>
<div className="mt-3 flex justify-between gap-2 font-mono text-[10px] font-bold">
<span>{stage === 0 ? "compare migration cost" : "select block 3"}</span><span>{stage === 2 ? "+7 net free" : "reserve holds the copy"}</span>
</div>
</div>
<Footer text={VICTIM_STEPS[stage]} final={stage === 2} still={reducedMotion} replay={replay} />
</div>
);
}

const WEAR_STEPS = [
"This worked snapshot offers two mostly dead blocks.",
"Greedy picks block 3: one migration, but 17 prior erases.",
"Block 1 costs one extra migration and avoids the worn tail.",
] as const;

export function WearChoiceStory() {
const { ref, stage, reducedMotion, replay } = useVisibleStory(2, 1450);
return (
<div ref={ref} className="overflow-hidden rounded-[18px] bg-[#d9cbff] p-5 text-[#282334]">
<div role="img" aria-label="In the worked snapshot, block 3 has one live page, seven dead pages, and 17 prior erases. Block 1 has two live pages, six dead pages, and four prior erases. Greedy would choose block 3, copying one page but further wearing the worst block. Choosing block 1 copies one additional page but leaves block 3's erase count unchanged. This is a wear-aware trade-off, not a guaranteed global optimum.">
<div className="flex items-start justify-between gap-2">
<h3 className="font-[family-name:var(--font-editorial)] text-[25px] leading-[1.03] font-extrabold tracking-[-.04em]">Cheap can wear out.</h3>
<span className="shrink-0 rounded-full bg-[#282334] px-2.5 py-1 font-mono text-[10px] text-white">one decision</span>
</div>
<div className="mt-7 grid grid-cols-2 gap-2" aria-hidden="true">
{[
{ block: "3", live: 1, dead: 7, erase: 17, chosen: false },
{ block: "1", live: 2, dead: 6, erase: 4, chosen: true },
].map((candidate) => (
<div key={candidate.block} className={cn("min-w-0 rounded-[11px] p-3", candidate.chosen && stage === 2 ? "bg-[#b7ff45]" : "bg-white/75")}>
<div className="font-mono text-[10px] font-bold">BLOCK {candidate.block}</div>
<div className="mt-3 font-[family-name:var(--font-editorial)] text-[32px] leading-none font-extrabold">{candidate.erase}</div>
<div className="mt-1 font-mono text-[9px] font-semibold">prior erases</div>
<div className="mt-3 h-2 rounded-full bg-[#282334]/15"><div className={cn("h-full rounded-full", candidate.chosen ? "bg-[#2141b4]" : "bg-[#ff784f]")} style={{ width: `${candidate.erase / 17 * 100}%` }} /></div>
<div className="mt-3 font-mono text-[10px] font-bold">{candidate.live} live · {candidate.dead} dead</div>
</div>
))}
</div>
<div className="mt-3 flex justify-between gap-2 font-mono text-[10px] font-bold">
<span>{stage === 0 ? "compare both costs" : stage === 1 ? "greedy → block 3" : "wear-aware → block 1"}</span>
<span>{stage === 2 ? "+1 migration" : "tail: 17 erases"}</span>
</div>
</div>
<Footer text={WEAR_STEPS[stage]} final={stage === 2} still={reducedMotion} replay={replay} />
</div>
);
}
4 changes: 4 additions & 0 deletions src/packs/ssd/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { registerIllustration, registerInterface } from "../registry";
import { BlockGrid } from "./illustrations/block-grid";
import { EfficiencyDial, WearHistogram, WriteAmpBar } from "./illustrations/metrics";
import { SlotStrip } from "./illustrations/slot-strip";
import { CompactionStory, VictimStory, WearChoiceStory } from "./illustrations/ladder-stories";
import { SSD_INTERFACES } from "./interfaces";

export const PACK_ID = "ssd";
Expand All @@ -21,6 +22,9 @@ registerIllustration("ssd.blockGrid", BlockGrid);
registerIllustration("ssd.writeAmpBar", WriteAmpBar);
registerIllustration("ssd.wearHistogram", WearHistogram);
registerIllustration("ssd.efficiencyDial", EfficiencyDial);
registerIllustration("ssd.compaction-story", CompactionStory);
registerIllustration("ssd.victim-story", VictimStory);
registerIllustration("ssd.wear-story", WearChoiceStory);

for (const [task, snippets] of Object.entries(SSD_INTERFACES)) {
for (const language of LANGUAGE_IDS) {
Expand Down
59 changes: 59 additions & 0 deletions src/packs/story-motion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";

import { useEffect, useRef, useState } from "react";

/** A finite, viewport-triggered story with an accessible reduced-motion end state. */
export function useVisibleStory(lastStage: number, intervalMs: number) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
const [reducedMotion, setReducedMotion] = useState(false);
const [stage, setStage] = useState(0);

useEffect(() => {
const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReducedMotion(preference.matches);
update();
preference.addEventListener("change", update);
return () => preference.removeEventListener("change", update);
}, []);

useEffect(() => {
const element = ref.current;
if (!element) return;
if (typeof IntersectionObserver === "undefined") {
const frame = window.requestAnimationFrame(() => setVisible(true));
return () => window.cancelAnimationFrame(frame);
}
const observer = new IntersectionObserver(
([entry]) => setVisible(Boolean(entry?.isIntersecting)),
{ threshold: 0.35 },
);
observer.observe(element);
return () => observer.disconnect();
}, []);

useEffect(() => {
if (!visible || reducedMotion || stage >= lastStage) return;
const timer = window.setTimeout(() => setStage((current) => current + 1), intervalMs);
return () => window.clearTimeout(timer);
}, [visible, reducedMotion, stage, lastStage, intervalMs]);

return {
ref,
stage: reducedMotion ? lastStage : stage,
reducedMotion,
replay: () => setStage(0),
};
}

export function Replay({ onClick }: { onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
className="shrink-0 rounded-full border border-current px-3 py-1 font-mono text-[11px] font-semibold transition-colors hover:bg-white/15 focus-visible:outline-2 focus-visible:outline-offset-2"
>
Replay
</button>
);
}
2 changes: 1 addition & 1 deletion src/packs/systems/recovery-time-stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { cn } from "@/lib/utils";

import { Replay, useVisibleStory } from "./story-illustrations";
import { Replay, useVisibleStory } from "../story-motion";

const JOURNAL_STEPS = [
"LSN 1 and 2 are synced, complete frames with valid checksums.",
Expand Down
58 changes: 1 addition & 57 deletions src/packs/systems/story-illustrations.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,7 @@
"use client";

import { useEffect, useRef, useState } from "react";

import { cn } from "@/lib/utils";

export function useVisibleStory(lastStage: number, intervalMs: number) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
const [reducedMotion, setReducedMotion] = useState(false);
const [stage, setStage] = useState(0);

useEffect(() => {
const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReducedMotion(preference.matches);
update();
preference.addEventListener("change", update);
return () => preference.removeEventListener("change", update);
}, []);

useEffect(() => {
const element = ref.current;
if (!element) return;
if (typeof IntersectionObserver === "undefined") {
const frame = window.requestAnimationFrame(() => setVisible(true));
return () => window.cancelAnimationFrame(frame);
}
const observer = new IntersectionObserver(
([entry]) => setVisible(Boolean(entry?.isIntersecting)),
{ threshold: 0.35 },
);
observer.observe(element);
return () => observer.disconnect();
}, []);

useEffect(() => {
if (!visible || reducedMotion || stage >= lastStage) return;
const timer = window.setTimeout(() => setStage((current) => current + 1), intervalMs);
return () => window.clearTimeout(timer);
}, [visible, reducedMotion, stage, lastStage, intervalMs]);

return {
ref,
stage: reducedMotion ? lastStage : stage,
reducedMotion,
replay: () => setStage(0),
};
}

export function Replay({ onClick }: { onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
className="shrink-0 rounded-full border border-current px-3 py-1 font-mono text-[11px] font-semibold transition-colors hover:bg-white/15 focus-visible:outline-2 focus-visible:outline-offset-2"
>
Replay
</button>
);
}
import { Replay, useVisibleStory } from "../story-motion";

const EASY_STEPS = [
"A is oldest. B is newest. The 66-byte budget is full.",
Expand Down
2 changes: 1 addition & 1 deletion src/packs/systems/work-stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { cn } from "@/lib/utils";

import { Replay, useVisibleStory } from "./story-illustrations";
import { Replay, useVisibleStory } from "../story-motion";

function StoryFooter({ copy, finished, reducedMotion, replay }: {
copy: string;
Expand Down
Loading
Loading