A reserving tutorial that makes you show your work
Reserving 101 is a nine-step walkthrough of P&C loss reserving fundamentals — the claim lifecycle, loss triangles, age-to-age factors, chain ladder, Bornhuetter-Ferguson, and the case reserve/IBNR/IBNER split. It’s built the way I wish more tutorials were: you can’t just click through it. Every non-intro step is a multiple-choice or worked-calculation question, and you don’t see the next step until you’ve answered the current one correctly.
One data shape, four step kinds
The whole tutorial is one array of ReservingStep objects in src/data/reserving-101-content.ts, each carrying a kind: intro, mcq, numeric, or summary. An MCQ step adds options/correctIndex/explanation; a numeric step adds correctValue/tolerance/placeholder; the summary step adds a glossary. Reserving101.astro has one render() function that switches on kind and builds the right markup and check logic for that step — content and behaviour live in the same object, so adding a tenth step is just adding an entry to the array, not touching the component.
Numeric answers are checked with a tolerance band, Math.abs(value - step.correctValue) <= step.tolerance, rather than exact equality. The age-to-age factor question wants 1.5; a learner who types 1.50 or 1.499 from rounding an intermediate step shouldn’t get marked wrong for it.
Gating without locking you out
Progress state is just two things: currentIndex and a completed: Set<string> of step IDs, persisted to localStorage as { currentIndex, completed: Array.from(completed) } (Sets don’t survive JSON.stringify, so it round-trips through an array). isGated(step) returns true for MCQ and numeric steps; findFirstIncomplete() walks the array for the first gated step not yet in completed.
That split is what lets the dot-navigation at the top do the right thing: clicking a dot for any step you’ve already completed jumps straight there — no need to re-answer something you got right five minutes ago — but the Continue button on your current step won’t move you past a question until it’s answered. You’re never trapped, but you can’t skip ahead either.
Trusted HTML by design, not by accident
Every step’s body is plain HTML written directly into the content file — worked examples, a loss triangle rendered as an actual <table>, the Bornhuetter-Ferguson formula — and it goes into the page via innerHTML with no sanitization step. That’s deliberate and safe because the content is author-controlled and ships in the same bundle as the code; there’s no path from a visitor’s input to that string, unlike the CSV validator, where every rendered value comes from a file someone else uploaded and has to be treated as hostile. Same site, two different trust boundaries, two different rendering strategies.
A scroll bug only mobile would show
Continue, Back, and the reset link all just re-run render() in place — the DOM changes but the scroll position doesn’t. On desktop that’s invisible because the whole card fits above the fold anyway. On mobile, with the sticky top bar wrapping onto two rows, a learner could tap Continue and land on a new question with its title scrolled off-screen and no visual cue that anything had changed.
The fix, scrollToTop(), computes the topbar’s height from getBoundingClientRect() at click time rather than hardcoding it, specifically because that height isn’t constant — it changes when the topbar wraps to two rows on a narrow viewport. A fixed offset would have been wrong on exactly the devices this was fixing it for.
Ideas for later
- More methods. Cape Cod, expected loss ratio, and frequency-severity are the natural next chapter, following the same MCQ/numeric pattern already in place.
- A real triangle to work, not just read. Every current example uses the same small fixed triangle; an exercise where the learner fills in an age-to-age factor from a triangle generated with different numbers per visit would test understanding rather than pattern-matching against the worked example above it.
- Shareable progress, or at least a “time to complete” stat at the summary step — right now finishing just clears back to a static congratulations message.