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:
1 parent
876f5aabb1
commit
edd57f0148
253 files changed
+21753
-503
No files matched your search
@@ -0,0 +1,18 @@
|
||||
# Known equivalent mutants for scripts/mutate.mjs: mutants that survive because they cannot change behaviour on any
|
||||
# state the program can reach, not because a test is missing. One per line:
|
||||
# <file>: <original line> => <mutated line> # <why it is equivalent>
|
||||
# Copy the "allow-list line" the script prints for a survivor and replace <reason>. Only add a line after you have
|
||||
# convinced yourself the mutant is equivalent; otherwise it is a test gap, and the fix is a test.
|
||||
pong/rules.js: (nx === PLAYER_X && vel.dx < 0 && hits(player, ny)) || => (nx === PLAYER_X && vel.dx <= 0 && hits(player, ny)) || # vel.dx is always -1 or 1 (initialPong sets 1, a bounce negates it, serve sets -1 or 1), so < 0 and <= 0 agree
|
||||
pong/rules.js: (nx === PLAYER_X && vel.dx < 0 && hits(player, ny)) || => (nx === PLAYER_X && vel.dx < 1 && hits(player, ny)) || # vel.dx is always -1 or 1, so < 0 and < 1 agree
|
||||
pong/rules.js: (nx === CPU_X && vel.dx > 0 && hits(cpu, ny)) => (nx === CPU_X && vel.dx >= 0 && hits(cpu, ny)) # vel.dx is always -1 or 1, so > 0 and >= 0 agree
|
||||
pong/view.js: text(320, 200, state.score.player > state.score.cpu ? 'You Win!' : 'CPU Wins', 32), => text(320, 200, state.score.player >= state.score.cpu ? 'You Win!' : 'CPU Wins', 32), # only shown when status is Over; a match ends the moment one side reaches WIN and only one point is scored per frame, so the scores are never equal there
|
||||
check.js: .map((s, i) => (i > 0 && !sameHead(s.snake, states[i - 1].snake) ? i : -1)) => .map((s, i) => (i > 0 && !sameHead(s.snake, states[i - 1].snake) ? i : -2)) # -1 is only a "no move here" marker that the next line filters out with i >= 0; -2 is filtered out the same way
|
||||
check.js: .filter((i) => i >= 0) => .filter((i) => i > 0) # the indices kept come from i > 0 on the line above, so 0 never occurs and >= 0 and > 0 agree
|
||||
check.js: .filter((i) => i >= 0) => .filter((i) => i >= 1) # the indices kept come from i > 0 on the line above (integers >= 1, or the marker -1), so >= 0 and >= 1 agree
|
||||
safeio.js: return isDone(r.state) ? [r.state, toArray(outs)] : go(r.state, budget * 2, outs) => return isDone(r.state) ? [r.state, toArray(outs)] : go(r.state, budget * 3, outs) # the budget only decides how many steps run per chunk; the chunks' outputs are joined in order, so any growth factor >= 2 gives the same result (it only changes the stack depth bound)
|
||||
safeio.js: return go(start, 1, leaf([])) => return go(start, 2, leaf([])) # the first budget only decides the chunk sizes; the outputs are joined in order and runFor stops at isDone, so the result is the same
|
||||
life/rules.js: const neighbours = (p) => offsets.map(([dx, dy]) => ({ x: p.x + dx, y: p.y + dy })).filter(onBoard) => const neighbours = (p) => offsets.map(([dx, dy]) => ({ x: p.x - dx, y: p.y + dy })).filter(onBoard) # the eight offsets are symmetric (with (dx, dy) the list also holds (-dx, dy)), so negating dx gives the same set of neighbours, only in another order, and only their count is used
|
||||
life/rules.js: const neighbours = (p) => offsets.map(([dx, dy]) => ({ x: p.x + dx, y: p.y + dy })).filter(onBoard) => const neighbours = (p) => offsets.map(([dx, dy]) => ({ x: p.x + dx, y: p.y - dy })).filter(onBoard) # the eight offsets are symmetric (with (dx, dy) the list also holds (dx, -dy)), so negating dy gives the same set of neighbours, only in another order, and only their count is used
|
||||
life/rules.js: : [...cells.filter((c) => index(c) < index(p)), { x: p.x, y: p.y }, ...cells.filter((c) => index(c) > index(p))] => : [...cells.filter((c) => index(c) <= index(p)), { x: p.x, y: p.y }, ...cells.filter((c) => index(c) > index(p))] # this branch is only reached when no cell has p's index (the line above removes p if it is present), so < and <= agree
|
||||
life/rules.js: : [...cells.filter((c) => index(c) < index(p)), { x: p.x, y: p.y }, ...cells.filter((c) => index(c) > index(p))] => : [...cells.filter((c) => index(c) < index(p)), { x: p.x, y: p.y }, ...cells.filter((c) => index(c) >= index(p))] # this branch is only reached when no cell has p's index (the line above removes p if it is present), so > and >= agree
|
||||
Reference in new issue
Block a user