Files
functional-programming-snake/scripts/mutate-allow.txt
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

19 lines
4.0 KiB
Plaintext

# 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