# Build log — Pure Snake course (2026-10-28 → 2026-10-29) Record of how the course was built, the decisions made on the owner's behalf, and what is still open. Spec: `docs/superpowers/specs/2026-10-28-pure-snake-course-design.md`. Plan: `docs/superpowers/plans/2026-10-28-pure-snake-course.md` (its "contract" section is the canonical API; changes below override it). ## Owner decisions (from the conversation) - Audience: CS bachelor's/master's students working alone at home, solo or with an AI as Socratic mentor (MENTOR.md). - Everything purely functional, including UI and input: hand-built `IO` type and event streams; no "before"/stateful starter version. - Tooling: plain JS ES modules, zero deps, `node --test`, canvas. Plus full Scala 3 and F# tracks with their own runnable test suites (shared JSON fixtures). - Size unconstrained; reference solutions shipped in `/solutions/`. - 2026-10-29: **no Scala bridging** in the text — bridges start from general programming knowledge (`> **From everyday programming:**`); FP basics (map/filter/reduce, Option, pattern matching) are taught in Modules 1–2. The Scala *track* remains as a language option. - Remote: `origin` = https://gitea.lucaswinther.info/lucasw89/functional-programming-snake (main pushed 2026-10-29). ## Rulings made during execution (decision — why — cost if wrong) 1. `scripts/serve.mjs` owned by Task 1 — duplicate ownership in plan — none. 2. `scripts/smoke.mjs` created in Task 8 (headless Chrome via CDP, zero deps) — no task owned it — rework smoke approach. 3. Subagent-driven execution without asking — owner's CLAUDE.md: no review stops after design approval — higher token spend. 4/16/17/19. Some implementers ran in parallel on disjoint files (F# track vs course text; Task 17 vs F# review) — wall-clock — commit races only. 5. Scala fixture helper is `scala/test/Fixtures.test.scala` — scala-cli scoping — none. 6. Typed fixture decoders added with the Scala/F# ports — none. 7. `js/test.sh`; JS runs `node --test "test/*.test.js"` — Node 24 rejects dir arg — none. 8. Small adjacent tasks batched into one implementer + one review — overhead — larger review surface. 9. `flatMap(Pure(a), f)` evaluates `f(a)` eagerly at construction; explained in Module 7 — unobservable for pure f — a later laziness lesson would need a Suspend case. 10. `flatMap` not stack-safe for ~100k left-nested binds; Module 7 discusses trampolining — game uses right-nested `sequence` — students' long flatMap loops overflow. 11. With a fully stubbed tree, m09 tests fail at import — students reach m09 with io done — noisier output if skipping ahead. 12. Pre-port JS polish: `pongKey` accepts W/S/R too (contract tweak), explicit held-key mapping, shared `classicMain` test helper, stronger no-mutation test. 13. Added `scripts/check-progression.sh `: for N=1..11, solutions for modules ≤N + stubs for the rest must pass M01..MN — caught a real "test helper imports src" bug. 14. Scala/F# stream input type is `Event` (Time/KeyDown/KeyUp), not `Input` — name clash with the Input module. 15. F# idioms: `bind` (flatMap), `choose` (filterMap), PascalCase record fields; `IO.randomSeed ()`, `App.main ()`, `pongMain ()` are unit functions so stubs fail per test. 18. Module 2 guiding questions may hint at the design (e.g. "why two directions?") — intended scaffolding — design exercise slightly easier. 20. F# mulberry32 routes uint32 `*`/`+` through uint64 helpers because Fable doesn't wrap uint32 arithmetic; Module 4 warns students — no automated Fable-vs-.NET drift check. ## Verification status (last full run 2026-10-29, before the docs-only Scala-bridge rewrite) - `scripts/verify.sh js|scala|fsharp`: 383 / 361 / 362 tests pass against solutions (JS re-run after the rewrite: 383 pass). - `scripts/check-stubs.sh `: M01–M11 fail against stubs in all tracks. - `scripts/check-progression.sh `: all checkpoints pass in all tracks. - `scripts/smoke.mjs`: all 6 pages (snake + pong × 3 tracks) paint with no console errors when built from solutions. - `node scripts/check-links.mjs`: 103 external links + all relative links OK. - Not done: a human playing the games end to end. ## Open follow-ups (judged OK to leave by the final review) - cats-effect / ZIO / fs2 still mentioned as "In production" comparisons in Modules 7–9 (owner may want them cut after the no-Scala-bridging change). - No automated guard that the Fable (F# browser) build reproduces the RNG fixtures. - Module 11 hint 11.4 gives away the stream `merge` shape; some near-solution hint skeletons are thin. - check-links.mjs ignores reference-style/HTML links and bare `(url)`; no 429 retry; soft-404s pass. - Purity audits exclude the whole runtime file; JS comment stripper cuts at `//` inside strings; F# `printf` ban misses `eprintfn`/`Console`. - Two Pong unknown-key fixture cases are vacuous in Scala/F# (unrepresentable by the types); m10 progression checkpoint adds no files for Scala/F#. - `interval` with a backwards `Time` re-emits; `history` is O(n²); `deepFreeze(obj, ancestors=[])` breaks if passed directly to `.map`. ## Environment notes - scala-cli installed at `~/.local/bin/scala-cli` (downloads its own JVM 17); dotnet at `~/.dotnet/dotnet`; Fable 5.24.0 via `fsharp/.config/dotnet-tools.json`; headless Chrome at `/usr/bin/google-chrome`. - First `scala/build-web.sh` / `fsharp/build-web.sh` run needs network (Scala.js linker / Fable restore). `scripts/serve.mjs` binds 127.0.0.1:8000; run from the repo root. --- # Audit and extension pass (2026-11-11) Owner request: audit the course, fact-check, add sources, knowledge and exercises, test edge cases, extend the project; no questions asked. Decisions below were made on the owner's behalf. ## Behaviour changes (all three tracks, fixtures regenerated) 1. **Frame rule (Module 10).** `frame` now counts frames since the last move and resets on each step (`n >= movesEvery(score)` → `tick` with `frame: 0`). The old rule (`totalFrames % movesEvery == 0`) made the snake take two steps as little as 25 ms apart at every speed-up. The old rule lives on as "Puzzle 3" in Module 10 and as a mutant in the M12 tests. Cost if wrong: `m10-update.json` frame cases were renamed/replaced. 2. **`clampTime(s, maxGap)` + `MAX_GAP_MS = 250` (Module 10, Exercise 10.5; also used by `pongMain`).** A hidden tab stops `requestAnimationFrame`; on return `interval` caught up on the whole gap at once and the snake died unseen. Fixed in pure stream code, not in the browser glue. Module 8's "the game should catch up" text now says catching up has a limit. 3. **Pong `Restart` keeps `held`.** The text claimed a key held across a restart "behaves sensibly"; the code cleared `held`. ## New content - **Module 12 (bonus), property-based testing**: `check.js` / `Check.scala` / `Check.fs` (`elementOf`, `listOf`, `forAll`, `removals`, `minimize`, `violations`, `moveGaps`), fixture `m12-violations.json`, tests that run the library against three mutants (double-turn, no walls, old frame rule). Scripts, README, reference and setup tables know M12. - New fixture cases from a mutation analysis of the JS solutions (212 killed, 14 survivors, 6 of them real gaps): Pong first paddle row, ball entering rows 0/49 and columns 0/79, `eating-the-last-free-cell-wins`, JS `deepFreeze` with `null`. - Challenge questions in Modules 3, 4, 7, 8, 10; new pitfalls in method.md; primary sources added (Wadler, Moggi, Peyton Jones, Kiselyov & Ishii, Steele/Lea/Flood, Lemire, Czaplicki, Elliott, Claessen & Hughes, Zeller, Kleppmann). ## Fact corrections - Module 4: mulberry32's output step is not a bijection; measured over the full period it emits 1 893 145 848 distinct values (44 %). The modulo-bias remark was reworded accordingly. Haskell `uniformR` → `uniform`. - Module 7: Haskell's `IO` has no `Suspend` case (laziness does that job); only cats-effect/ZIO do. - Module 10: "for a game this is invisible" (about uneven steps) was false; removed. ## Verification (2026-11-11) - `verify.sh`: JS 446, Scala 423, F# 425 tests pass (F# has one extra stack-safety test for `forAll`). - `check-stubs.sh` and `check-progression.sh`: M01–M12 ok in all tracks. - `smoke.mjs`: all 6 pages paint with no console errors, built from solutions. - `check-links.mjs`: all links OK after replacing one dead Moggi URL. - Not done: a human playing the games; the tab-switch fix is covered by stream tests only. ## Open follow-ups (added) - Changes are uncommitted in the working tree. - `Check.fs` is not in `fsharp/web/Web.fsproj` (not needed in the browser). - A turn queue (honouring Up-then-Left within one step) is still only a question in Module 3. - Resolved from the earlier list: backwards `Time` is now harmless behind `clampTime`. --- # Extension pass (2026-11-11, later the same day) Owner approved eleven extensions; spec: `docs/superpowers/specs/2026-11-11-course-extensions.md` (corrected in place where the build differed). Specs and verification by the orchestrating session; code and text by Opus 5.5 builders: a JS reference builder per wave, then one Scala and one F# porter. ## What was added 1. **Turn queue** (Exercise 10.6): dormant `queued` field in `GameState`; `steer`; every game-state fixture gained `"queued": null`. From Module 10 on, Up-then-Down with Up pending is ignored (Module 3's `turn` alone would replace it). 2. **Persisted high score** (Exercise 10.7): `LoadBest`/`SaveBest` ops, `Platform.loadBest/saveBest` (`localStorage` key `pure-snake-best`), a `Session {game, best, improved}` around the game; `GameState`, `update`, `view` untouched. 3. **Replay viewer** (Exercise 10.8): `replay.*`, pages `replay.html`, sample `fixtures/replay-sample.json` (seed 1, 226 actions, 19 apples), loaded with `fetch` in all tracks. 4. **Stack-safe IO** (optional Module 7b): `safeio.*`, tag `M07b`; it answers only Module 7's two ops and throws for others. 5. **`play(main, seed, events)`** (Exercise 9.5) and golden fixture `m10-golden.json`. 6. **`State`** (optional Exercise 4.7): `state.*`, tests under M04. A student who skips it must copy the solution file to keep M04 green. 7. **Module 12**: `genSnake`, given `genState`, `advanceSpec` as an oracle; invariant `queued-not-reversed`; six mutants in the tests. 8. **`scripts/mutate.mjs`** with `scripts/mutate-allow.txt` (13 reasoned equivalent mutants; exit 0). 9. **JS type checking**: `js/jsconfig.json`, `types.d.ts`, JSDoc; `scripts/typecheck-js.sh` (needs network for `npx`; exits 2 without it). 10. **Module 13, Life with the mouse**: `life/`, `Click` event, `clicks` source, `Platform.onClick`; clicks toggle cells whether running or paused; canvas 640×504. 11. **`scripts/play.mjs`**: scripted browser session over all pages of all tracks. ## Decisions made on the owner's behalf - Two earlier tests on `main` were loosened (they accept the program with or without `LoadBest`) so a student between Modules 9 and 10 stays green; the exact sequence is pinned by a 10.7 test. - Click support reaches students in Module 13; M09–M12 pass with a runtime that does not register clicks. - `./test.sh MNN` in Scala and F# now selects exactly that module (M07 no longer pulls in M07b). - `smoke.mjs` picks the tab it opened (Chrome sometimes lists an extension page first). - One builder ran `git checkout` on `fixtures/m00-sanity.json` after its script reformatted it; the file had no pending changes. ## Verification (2026-11-11, final) - `verify.sh`: JS 728, Scala 689, F# 691 tests pass. Counts differ because a few tests are track-specific (JS browser-glue tests, JS-only constructors, one F# stack test). - `check-stubs.sh`, `check-progression.sh`: M01–M13 and M07b ok in all tracks. - `typecheck-js.sh`: stubs and solutions clean (TypeScript 7.0.2; also checked with 5.9.3). - `mutate.mjs`: 404 mutants, 391 killed, 13 allow-listed, exit 0. - `smoke.mjs`: 12 pages paint with no errors. `play.mjs`: 66 checks pass on all tracks. - `check-links.mjs`: all resolve; two okmij.org PDFs timed out once and returned 200 on retry. ## Not verified / open - No person has played any of it. `play.mjs` cannot see overlay text, the speed-up, the turn queue at real speed, Pong scoring, replay autoplay timing, a real hidden desktop tab, or blocked storage. - VS Code's own display of the JS type errors was not checked, only `tsc`. - TypeScript version is not pinned (`TS_PACKAGE` can pin it). - Scala lacks the JS test "play without a best answers LoadBest with 0". - Everything is uncommitted in the working tree. ## Playwright end-to-end suite (2026-11-11, owner's suggestion; spec Item 12) - `e2e/` (own `package.json`, `@playwright/test` 1.65.0 pinned) and `scripts/e2e.sh`; `scripts/play.mjs` deleted, `smoke.mjs` kept. The course tracks stay dependency-free. - Technique: fixed seed (stubbed `crypto.getRandomValues`), fake clock driving `requestAnimationFrame`, recorded `fillRect`/`fillText` calls; every frame a page draws is compared with the pure JS model for the same inputs, for all three tracks on Chromium and WebKit. - Result: 176 tests pass in about 20 s. No frame of any track differs from the model. - Found and fixed: the three tracks read an unusual stored best differently (`"1e2"`, `"7.0"`, `"2147483648"`). Ruling: only plain decimal digits with a value of at most 2147483647 count; anything else reads as 0. Applied to `js/solutions/browser.js`, `scala/web/BrowserPlatform.scala`, `fsharp/web/BrowserPlatform.fs`. - Still not covered: real animation timing and real hidden-tab throttling (the clamp is tested with a fake 10 s frame), rendered pixels and fonts, zoom, touch, key auto-repeat, Firefox (no build installed), and a person playing.