Files
functional-programming-snake/docs/superpowers/notes/2026-10-29-build-log.md
T
Lucas WintherandClaude Fable 5.2 edd57f0148 feat(course): audit fixes and twelve extensions (Modules 7b, 12, 13)
Audit pass:
- Frame rule counts frames since the last move, so every step lasts
  exactly movesEvery(score) frames, also right after a speed-up
- clampTime + MAX_GAP_MS: a hidden tab no longer replays the whole gap
- Pong Restart keeps held keys
- Fact corrections (mulberry32 output coverage, Haskell IO, uniform)
- New fixture cases from a mutation analysis

Extensions (all three tracks, shared fixtures, course text):
- Turn queue (Exercise 10.6), persisted high score (10.7),
  replay viewer (10.8), play() end-to-end helper (9.5)
- Optional State (4.7) and optional Module 7b, stack-safe IO
- Module 12, property-based testing with generators, shrinking,
  invariants and a model-based oracle
- Module 13, Conway's Life with a Click event
- JS type checking via jsconfig, JSDoc and types.d.ts
- Maintainer tools: scripts/mutate.mjs, scripts/typecheck-js.sh,
  Playwright end-to-end suite (e2e/, scripts/e2e.sh)

Docs: docs/contract.md is the canonical contract; specs and build log
under docs/superpowers.

Verified: JS 728, Scala 689, F# 691 unit tests; stubs and progression
M01-M13 and M07b; 176 end-to-end tests on Chromium and WebKit.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 12:05:29 +01:00

13 KiB
Raw Blame History

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 <track>/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.
  4. Scala fixture helper is scala/test/Fixtures.test.scala — scala-cli scoping — none.
  5. Typed fixture decoders added with the Scala/F# ports — none.
  6. js/test.sh; JS runs node --test "test/*.test.js" — Node 24 rejects dir arg — none.
  7. Small adjacent tasks batched into one implementer + one review — overhead — larger review surface.
  8. 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.
  9. flatMap not stack-safe for ~100k left-nested binds; Module 7 discusses trampolining — game uses right-nested sequence — students' long flatMap loops overflow.
  10. With a fully stubbed tree, m09 tests fail at import — students reach m09 with io done — noisier output if skipping ahead.
  11. Pre-port JS polish: pongKey accepts W/S/R too (contract tweak), explicit held-key mapping, shared classicMain test helper, stronger no-mutation test.
  12. Added scripts/check-progression.sh <track>: for N=1..11, solutions for modules ≤N + stubs for the rest must pass M01..MN — caught a real "test helper imports src" bug.
  13. Scala/F# stream input type is Event (Time/KeyDown/KeyUp), not Input — name clash with the Input module.
  14. F# idioms: bind (flatMap), choose (filterMap), PascalCase record fields; IO.randomSeed (), App.main (), pongMain () are unit functions so stubs fail per test.
  15. Module 2 guiding questions may hint at the design (e.g. "why two directions?") — intended scaffolding — design exercise slightly easier.
  16. 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 <track>: M01–M11 fail against stubs in all tracks.
  • scripts/check-progression.sh <track>: 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.