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>
This commit is contained in:
Lucas WintherandClaude Fable 5.2 committed 2026-11-11 12:05:29 +01:00
1 parent 876f5aabb1
commit edd57f0148
253 files changed
+21753 -503

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { pure, map, flatMap, get, put, modify, run } from '../src/state.js'
import * as Random from '../src/random.js'
// Small initial states for the "behaves like" checks: two States behave alike when they give the same pair from each.
const starts = [0, 1, 7, 42]
const sameOnAll = (st1, st2) => starts.forEach((s) => assert.deepStrictEqual(run(st1, s), run(st2, s), `from state ${s}`))
// A counter step: gives the current number times 10 and adds 1 to the state. Built inside the tests, so that a stub
// that throws fails each test on its own instead of the whole file at load time.
const step = () => flatMap(get, (n) => map(put(n + 1), () => n * 10))
test('m04 State pure gives its value and leaves the state alone', () =>
assert.deepStrictEqual(run(pure('x'), 99), ['x', 99]))
test('m04 State map applies f to the value and keeps the state from the underlying step', () =>
assert.deepStrictEqual(run(map(step(), (v) => v + 1), 4), [41, 5]))
test('m04 State flatMap threads the state through both steps', () =>
assert.deepStrictEqual(run(flatMap(step(), (a) => map(step(), (b) => [a, b])), 4), [[40, 50], 6]))
test('m04 State get gives the state as the value and leaves it alone', () =>
assert.deepStrictEqual(run(get, 'board'), ['board', 'board']))
test('m04 State put replaces the state and gives undefined', () =>
assert.deepStrictEqual(run(put(5), 99), [undefined, 5]))
test('m04 State modify applies f to the state and gives undefined', () =>
assert.deepStrictEqual(run(modify((s) => s * 2), 21), [undefined, 42]))
test('m04 State law: left identity, flatMap(pure(a), f) behaves like f(a)', () => {
const f = (a) => map(modify((s) => s * 2), () => a + 1)
sameOnAll(flatMap(pure(3), f), f(3))
})
test('m04 State law: right identity, flatMap(m, pure) behaves like m', () =>
sameOnAll(flatMap(step(), pure), step()))
test('m04 State law: associativity, the grouping of flatMap does not matter', () => {
const f = (a) => map(modify((s) => s * 2), () => a + 1)
const g = (b) => flatMap(get, (s) => pure([b, s]))
sameOnAll(flatMap(flatMap(step(), f), g), flatMap(step(), (a) => flatMap(f(a), g)))
})
test('m04 State a counter hands out fresh labels in order', () => {
const fresh = flatMap(get, (n) => map(put(n + 1), () => `L${n}`))
const three = flatMap(fresh, (a) => flatMap(fresh, (b) => map(fresh, (c) => [a, b, c])))
assert.deepStrictEqual(run(three, 0), [['L0', 'L1', 'L2'], 3])
assert.deepStrictEqual(run(three, 7), [['L7', 'L8', 'L9'], 10])
})
test('m04 State Random is State with the seed as S: the dice give the same values and seeds', () => {
// One draw written only with State's get and put: read the seed, compute, store the next seed.
const draw = flatMap(get, (seed) => {
const [v, next] = Random.run(Random.nextUint32, seed)
return map(put(next), () => v)
})
const d6State = map(draw, (v) => 1 + (v % 6))
const twoDiceState = flatMap(d6State, (a) => map(d6State, (b) => a + b))
const d6Random = Random.map(Random.nextUint32, (v) => 1 + (v % 6))
const twoDiceRandom = Random.flatMap(d6Random, (a) => Random.map(d6Random, (b) => a + b))
;[0, 1, 42, 4294967295].forEach((seed) => {
assert.deepStrictEqual(run(draw, seed), Random.run(Random.nextUint32, seed), `draw from ${seed}`)
assert.deepStrictEqual(run(d6State, seed), Random.run(d6Random, seed), `d6 from ${seed}`)
assert.deepStrictEqual(run(twoDiceState, seed), Random.run(twoDiceRandom, seed), `twoDice from ${seed}`)
})
})