From b36f4605ccd999f58b4b67d1653f7d5ff46e6989 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Wed, 11 Nov 2026 09:45:01 +0100 Subject: [PATCH] docs: spec and plan for the course v2 extensions Co-Authored-By: Claude Fable 5.2 --- .gitignore | 2 + .../plans/2026-11-11-course-v2-extensions.md | 225 ++++++++++++++++++ .../2026-11-11-course-v2-extensions-design.md | 114 +++++++++ 3 files changed, 341 insertions(+) create mode 100644 docs/superpowers/plans/2026-11-11-course-v2-extensions.md create mode 100644 docs/superpowers/specs/2026-11-11-course-v2-extensions-design.md diff --git a/.gitignore b/.gitignore index e62fc66..7f0a11f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ dist/ .DS_Store *.log deploy/.env +.claude/ +.superpowers/ diff --git a/docs/superpowers/plans/2026-11-11-course-v2-extensions.md b/docs/superpowers/plans/2026-11-11-course-v2-extensions.md new file mode 100644 index 0000000..abf6309 --- /dev/null +++ b/docs/superpowers/plans/2026-11-11-course-v2-extensions.md @@ -0,0 +1,225 @@ +# Course v2 Extensions Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Close the course's admitted gaps, add two modules and one seam, and add the tools, levels and site page described in the spec. + +**Architecture:** Engine changes are made in the cumulative Rust and TypeScript references, earliest module first, so later tasks copy finished engines. New tools reuse `tools/play-spellgrid.mjs`'s staging. New modules 11 and 12 are full modules; the old Module 11 becomes 13. + +**Tech Stack:** Rust (cargo, serde), TypeScript (vitest, tsx), Node 22+ tooling (`node --test`), Astro/Starlight site. + +**Spec:** `docs/superpowers/specs/2026-11-11-course-v2-extensions-design.md`. Every task implements the spec sections it names; the spec's text is the requirement and this plan adds order, files and steps. + +## Global Constraints + +Spec §1, all eight points, bind every task. In short: both tracks, every cumulative reference copy; test names identical across Rust, TS and scenarios; engine names no card or noun; the full verification list of §1.4 passes before reporting; lessons follow §1.5 (fetched sources, exact quotes of at most 125 characters, course choices labelled, counts and quoted code match the files); `harness.md`, `catalog.json`, `README.md` and tool tests travel with the code; commit on `extend/course-v2`, never push, commit trailer `Co-Authored-By: Claude Opus 5.5 `; no subagents. + +## Review Focus + +1. A change made in one track or one module copy and missed in another: the differential fuzzer and `verify-references` must both run on the final tree of every engine task. +2. A random level or script that crashes a CLI (out-of-range coordinates, a card not in hand, a chooser asked with no options): both tracks must print the same error line and carry on; Task 1 tests this. +3. An existing learner test that silently changes meaning: Armor absorption (Task 3) and the nesting guard (Task 4) must not alter any existing test's expected log; if one would, stop and report. +4. Undo after a turn that destroyed objects and created others: ids must not be reused or renumbered; Task 5's test asserts ids. +5. A lesson that quotes a source nobody fetched, or a test count that no longer matches: reviewers check every new quotation has a URL and every count against the test files. + +--- + +### Task 1: Rename Module 11 to 13; differential fuzzer; fix what it finds + +Implements spec §4 (first paragraph, rename only) and §2.1. + +**Files:** +- Move: `course/11-break-your-engine/` → `course/13-break-your-engine/` (title becomes "Module 13"); update every link and mention in `course/**/lesson.md`, `README.md`, `mentor/SKILL.md`. +- Create: `tools/diff-tracks.mjs`, `tools/test/diff-tracks.test.mjs` +- Modify: `tools/start-module.mjs` (clear message for a module with no tests), `package.json` (script `diff:tracks`), `README.md` +- Modify as needed: references and tests of any module where a divergence is fixed + +**Interfaces:** +- Produces: `node tools/diff-tracks.mjs [--seeds N] [--seed S] [--module NN] [--dir D] [--keep]`, exit 0 on agreement. Exports `parseArgs(argv)`, `generateCase(seed, vocab) -> { level, script }` (pure, deterministic) and `rng(seed)` for tests. Later tasks run it as verification. + +- [ ] **Step 1:** `git mv` the module, fix links and the H1, run `node tools/check-links.mjs` (relative links only matter) and `npm run test:site`. +- [ ] **Step 2:** Write `tools/test/diff-tracks.test.mjs`: `generateCase` is deterministic for a seed and differs across seeds; every generated level validates against `course/schema/level.schema.json`; every script line matches the CLI grammar (`w|a|s|d`, `c [x y]`, a chooser answer); `parseArgs` defaults. Run: fails (no module). +- [ ] **Step 3:** Implement the tool per §2.1. Compare the two CLIs' output formats first; where the Rust and TS CLIs print the same state differently, fix the CLI (both must print identical text for identical state) and add a CLI test. +- [ ] **Step 4:** Run `node tools/diff-tracks.mjs --seeds 500`. For each distinct divergence: minimise by hand to a scenario, decide the right behaviour (spec §2.1), write the regression test in both tracks and `scenarios.md` in the module that owns the behaviour, fix every affected reference copy. Repeat until 500 seeds agree. Record each divergence and its ruling in the report. +- [ ] **Step 5:** Full verification (§1.4). Commit. + +### Task 2: Log tree + +Implements spec §3.1. + +**Files:** +- Modify: `engine/event.*`, `engine/queue.*`, `engine/world.*`, `src/harness.*` in the references of Modules 02–10, both tracks; `cli.*` in Module 10 +- Modify: `course/02-action-queue/tests/*`, `scenarios.md`, `lesson.md`; `course/03-death-step/tests/*`, `scenarios.md`, `lesson.md`; `course/harness.md`; workspace templates' harness stubs if they list functions + +**Interfaces:** +- Produces: harness `logTree(): string` (TS) / `log_tree(&self) -> String` (Rust), on `Game` from Module 2 and on `SpellGrid` in Module 10. Format per §3.1; event text exactly as `scenarios.md` writes events. Tasks 10 and 11 consume it. + +- [ ] **Step 1:** Write `test_02_log_tree_shows_nesting` (Echo, Armorsmith, Reactor; `dealDamage(armorsmith, 1)`) asserting the exact string: `Damaged` at depth 0; Echo's `Triggered` at 0 with its hero `Damaged` at 1 and Reactor's `Triggered` at 1 with `StatsChanged` at 2; Armorsmith's `Triggered` at 0 with `ArmorGained` at 1. Write `test_03_log_tree_death_phase` (the chained Golem/Ghoul scenario): `Destroyed` and `DeathPhase` at 0, each Deathrattle's `Triggered` at 0, its effects at 1. Add both to scenarios. Run: fail. +- [ ] **Step 2:** Add depth to engine events and `logTree` to the harness, Module 2 first, then propagate to every later copy. +- [ ] **Step 3:** Module 10 CLIs print `Log:` and the tree of the last command's Sequences (both tracks, identical text); update the CLI tests. +- [ ] **Step 4:** Update Lesson 2 (Spec table, test list and count, Stress it question 3), Lesson 3 (test list and count), `harness.md`. +- [ ] **Step 5:** Full verification including `diff-tracks --seeds 200`. Commit. + +### Task 3: Replacement effects in Module 4 + +Implements spec §3.2. + +**Files:** +- Modify: `engine/aura.*`, `engine/queue.*`, `engine/card.*`, `engine/event.*`, `src/harness.*`, content, in the references of Modules 04–10, both tracks +- Modify: `course/schema/card.schema.json`, `course/catalog.json`, `course/cards/` (the redirect card, from Module 5's data format on), `course/04-interceptors/tests/*`, `scenarios.md`, `lesson.md`, `course/02-action-queue/lesson.md`, `course/harness.md`, `tools/test/schema.test.mjs` if it enumerates modifier types + +**Interfaces:** +- Produces: aura data `{ type: "damageRedirect", from: "friendlyHero", to: "self" }`; `GameEvent` `ArmorLost{player, amount}`; engine list `DAMAGE_STEPS = [redirect, armor]` (TS `readonly` array / Rust `const`). Task 9 (combat) and Task 10 (mutant) consume these names. + +- [ ] **Step 1:** Fetch the Bolf Ramshield page; settle the card per §3.2 and record the quotation. +- [ ] **Step 2:** Write the four tests of §3.2 with exact logs, and their scenarios. Run: fail. +- [ ] **Step 3:** Implement in Module 4 (card as code there, as that module's cards are), then propagate; from Module 5 the card is a JSON file and the interpreter needs no new effect. +- [ ] **Step 4:** Confirm no existing test's expected log changed. If one must, stop and report (Review Focus 3). +- [ ] **Step 5:** Lesson 4: new section "A second interceptor: replacement" after Name it, test list and count, further reading; Lesson 2: the "How the real game differs" sentence about Armor and Stress it question 4. +- [ ] **Step 6:** Full verification including `diff-tracks`. Commit. + +### Task 4: Robustness in Module 5 + +Implements spec §3.3. + +**Files:** +- Modify: `engine/queue.*`, `engine/assemble.*`, `engine/effect.*`, `engine/card.*`, `src/harness.*` in the references of Modules 05–10, both tracks +- Modify: `course/05-assemble/tests/*`, `scenarios.md`, `lesson.md`; Stress it answers in `course/02-action-queue/lesson.md`, `course/03-death-step/lesson.md`; `course/harness.md` +- Create: a test in `tools/test/` that every shipped card is well-formed (reimplement the rule in the tool, in JS, against the JSON) + +**Interfaces:** +- Produces: engine `wellFormed` / `well_formed`; error text containing `Death Phase limit`; `DEATH_PHASE_LIMIT = 64`. Nothing later depends on these names except lessons. + +- [ ] **Step 1:** Write `test_05_trigger_does_not_nest_in_itself`, `test_05_death_phase_limit`, `test_05_assemble_rejects_ill_formed_text`, `test_05_multi_summon_order` (cards registered inline, as `test_05_own_aura_misses_own_battlecry` does) and scenarios. For the Bomb test, work the expected `Triggered` sequence out by hand in the scenario text before running anything. Run: fail (the first two crash or hang: use a timeout). +- [ ] **Step 2:** Implement the four changes in Module 5, propagate. +- [ ] **Step 3:** Confirm Module 10's Graft level and every existing Assemble test still offer the options their tests expect. +- [ ] **Step 4:** Lessons 2, 3, 5 per §3.3; test counts. +- [ ] **Step 5:** Full verification including `diff-tracks`. Commit. + +### Task 5: Undo and `stackAt` + +Implements spec §3.4, the `stackAt` half of §2.3, and one leftover. + +**Files:** +- Modify: `engine/puzzle/grid.*`, `engine/puzzle/level.*`, `src/harness.*` in the references of Modules 08–10 (`stackAt`) and 09–10 (`undo`), both tracks +- Modify: `course/08-push-chains/tests/*`, `scenarios.md`, `lesson.md`; `course/09-turn-pipeline/tests/*`, `scenarios.md`, `lesson.md`; `course/harness.md` +- Fix: `course/08-push-chains/lesson.md` quotes `try_move` / `tryMove` with an inline comment the reference lacks: make the quote match the reference exactly + +**Interfaces:** +- Produces: `stackAt(x, y): string[]` / `stack_at(&self, x, y) -> Vec` (kinds on the tile, oldest first) on `Baba` from Module 8 and on `SpellGrid`; `undo(): boolean` / `undo(&mut self) -> bool` on `Baba` from Module 9; SpellGrid's `undo` errors with `not supported`. Task 7's solver consumes `stackAt`. + +- [ ] **Step 1:** Write `test_08_stack_is_age_ordered` and `test_09_undo_restores_everything` (§3.4) and `test_09_undo_walks_back_and_stops` (two steps, three undos, third returns false). Scenarios. Run: fail. +- [ ] **Step 2:** Implement; propagate. +- [ ] **Step 3:** Lessons 8 and 9 (Spec tables, test lists and counts; Lesson 9's Stress it question 3 now compares the learner's undo with the reference's snapshot contents), `harness.md`, the Lesson 8 quote fix. +- [ ] **Step 4:** Full verification including `diff-tracks`. Commit. + +### Task 6: Summoned minions enter the grid + +Implements spec §3.5. + +**Files:** +- Modify: `engine/spellgrid.*`, `engine/event.*`, `engine/queue.*` (the summoner on the internal event), `src/harness.*`, `src/content/words.json` in the Module 10 reference, both tracks +- Create: `course/10-spellgrid/cards/clay-golem.json`, `clay-shard.json`; catalog entries; words `GOLEM`, `SHARD` +- Modify: `course/10-spellgrid/tests/*`, `scenarios.md`, `lesson.md`, `course/harness.md` + +**Interfaces:** +- Produces: cards `Clay Golem` (noun `GOLEM`) and `Clay Shard` (noun `SHARD`) available to Task 7's levels. + +- [ ] **Step 1:** Write `test_10_summoned_minion_enters_grid` and `test_10_summon_without_noun_stays_off_grid` with scenarios. Run: fail. +- [ ] **Step 2:** Implement the seam; the order inside "a Sequence ends" is: write spawned text, place summoned minions, remove the dead, reparse, sync rule auras. +- [ ] **Step 3:** Lesson 10: three seams become four everywhere they are counted (Spec, Build diagram and hints, Name it, "How the real game differs", Stress it question 3), card table, test list. +- [ ] **Step 4:** Full verification including `diff-tracks` (the generator now sees the new cards). Commit. + +### Task 7: Solver upgrade, level proofs, seven new levels + +Implements spec §2.3 and §3.6. + +**Files:** +- Modify: `tools/solver/solve.ts` (age-aware key via `stackAt`), `tools/solve-spellgrid.mjs` if needed +- Create: `tools/verify-levels.mjs`, `tools/test/verify-levels.test.mjs`, `course/10-spellgrid/levels/proofs.json`, `course/10-spellgrid/levels/l4-….json` … `l10-….json` +- Modify: `course/10-spellgrid/tests/*`, `scenarios.md`, `lesson.md`, `course/schema/level.schema.json` only if a level needs nothing new (it should not), `package.json` (script `verify:levels`), `README.md`, both CLIs' tests if they enumerate levels, `tools/test/play-spellgrid.test.mjs` (it lists level names) + +**Interfaces:** +- Produces: `node tools/verify-levels.mjs` exit 0 when every proof in `proofs.json` holds; `proofs.json` entries `{ "level": "l4-…", "shortest": N, "neverRule": "X IS Y" }` or `"naiveUnsolvable": true`. Task 10 adds it to CI; Task 11 lists the levels. + +- [ ] **Step 1:** Solver key uses `stackAt`; re-run on `l1`–`l3` and confirm the same results as recorded in Lesson 10's Stress it question 4 (10 inputs; exhausted without the rule; 4; 6; naive exhausted). Update the numbers in Lessons 10 and 13 if position counts changed. +- [ ] **Step 2:** Write `verify-levels` and its test (arg parsing; a fixture proof that must fail is reported as failing). Add proofs for `l1`–`l3`. +- [ ] **Step 3:** Design `l4`–`l10`, one per idea in §3.6. For each: write the level, run the solver with `--all` and read every solution, run it with the idea forbidden, and iterate on the layout until the proof holds and no cook exists. Keep each level's intended solution and proof in the report. +- [ ] **Step 4:** `test_10_level_N_solution` for N = 4…10 in both tracks, scripted from the solver's shortest solution, with one assertion mid-script that shows the idea happening; scenarios; Lesson 10's level list (ideas, no solutions) and test count. +- [ ] **Step 5:** Full verification, `node tools/verify-levels.mjs`, `diff-tracks`. Commit. + +### Task 8: Module 11, Conditions + +Implements spec §4 (module shape) and §4.1. + +**Files:** +- Create: `course/11-conditions/` with `lesson.md`, `scenarios.md`, `tests/{rust,ts}/test_11.*`, `reference/{rust,ts}/` (copied from Module 10's final reference, then changed), `expansion/` (`README.md`, `tests/{rust,ts}/test_11x.*`, `solution/` with `words.json` and the content file that registers `NEAR`) +- Modify: `course/harness.md`, `course/catalog.json` (words `ON`, `NEAR` with a `condition` role), `tools/coupling-audit.mjs` only if its word-role handling needs the new role, `tools/lib.mjs`/`verify-references.mjs` only if a module after 10 needs staging changes (levels and SpellGrid cards must still be staged for 11+) + +**Interfaces:** +- Consumes: Module 10's final reference (Tasks 1–7). +- Produces: word role `"condition"`; content registry of condition functions `(level, objectId, nouns) -> bool`; rule text format `BABA ON LAVA IS MELT`; `Rule.conditions`. Task 9 copies this reference; Task 10 adds a mutant "conditions evaluated at parse time instead of at use" against it. + +- [ ] **Step 1:** Fetch the Baba Is You Wiki pages for `ON`, `NEAR` and Order of Operations (the course's `docs/research/baba-is-you.md` lists how earlier fetches were done); append what was fetched, with quotations, to that research file. +- [ ] **Step 2:** Create the module skeleton and reference copy; make `verify-references 11` pass with zero new tests. +- [ ] **Step 3:** Write the tests of §4.1 (at least the seven listed) and scenarios. Run: fail. +- [ ] **Step 4:** Implement: lexer role, parser, rule conditions, evaluation at use in `hasProperty`, conversion, hazards' `sharing`-based queries and the win check, resolution pairing, rule text. +- [ ] **Step 5:** Expansion Drop `NEAR`: test, README scenario, solution; `verify-references --expansion` passes and the solution touches no engine file. +- [ ] **Step 6:** Write the lesson (§4.1 theme; all beats; three hint tiers; "Decisions in the reference"; "How the real game differs"; a Stress it section with at least three questions, one of which asks which existing mutant-style change the new tests catch). +- [ ] **Step 7:** `harness.md`, mentor/README mentions of the module list, full verification, `diff-tracks --module 11`. Commit. + +### Task 9: Module 12, Combat + +Implements spec §4 (module shape) and §4.2. + +**Files:** +- Create: `course/12-combat/` with the full module shape; reference copied from Module 11's final reference +- Modify: `course/harness.md`, `course/catalog.json`, `course/cards/` or `course/12-combat/cards/` for any new card (follow how Module 10 scopes its cards so earlier modules never see them), `course/01-handles/lesson.md` and `course/02-action-queue/lesson.md` ("defender triggers first" is no longer unmodelled: point forward) + +**Interfaces:** +- Consumes: `DAMAGE_STEPS`, `ArmorLost` (Task 3); Module 11's reference. +- Produces: harness `attack(attacker, defender)`, `canAttack(attacker, defender): string | null`, `endTurn(player)` on `Game`; a per-queue-entry priority in `react`. Task 10's mutant list may add "defender priority removed". + +- [ ] **Step 1:** Fetch the Advanced rulebook's combat and end-of-turn passages and the Drop card's page; append to `docs/research/hearthstone.md`. +- [ ] **Step 2:** Skeleton and reference copy; `verify-references 12` green with no new tests. +- [ ] **Step 3:** Write the tests of §4.2 (at least the six listed) and scenarios. Run: fail. +- [ ] **Step 4:** Implement: per-target damage amounts, the attack action and its legality, queue priority, `endTurn` and the `endOfTurn` event. +- [ ] **Step 5:** Expansion Drop: one card file, one test, README; content only. +- [ ] **Step 6:** Lesson (§4.2 theme; all beats; hints; decisions; differences; Stress it with at least three questions). +- [ ] **Step 7:** `harness.md`, earlier lessons' forward pointers, full verification, `diff-tracks --module 12`. Commit. + +### Task 10: Mutation check, scenario runner, designer capstone, CI, closing docs + +Implements spec §2.2, §2.4, §2.5, §5.2 and the Module 13 updates. + +**Files:** +- Create: `tools/mutation-check.mjs`, `tools/mutants.json`, `tools/test/mutation-check.test.mjs`, `tools/run-scenario.mjs`, `tools/scenario/run.ts`, `tools/test/run-scenario.test.mjs`, `course/10-spellgrid/designer-capstone.md`, `course/10-spellgrid/designer-example/` (cards, word, `scenario.json`, `output.txt`) +- Modify: `.gitea/workflows/ci.yml`, `package.json` scripts (`mutation:check`, `run:scenario`), `README.md` (tools; Designer path), `mentor/SKILL.md` (module list, designer capstone), `course/13-break-your-engine/lesson.md`, `course/10-spellgrid/lesson.md` (link to the capstone) + +**Interfaces:** +- Consumes: `logTree` (Task 2), `verify-levels` (Task 7), `diff-tracks` (Task 1), final references (Task 9). + +- [ ] **Step 1:** `mutation-check`: tests first (a fixture mutant that must be killed; a stale `find` is an error; a surviving mutant exits 1). Implement. Fill `mutants.json` per §2.2 and run it; a mutant that survives gets a test in the owning module (both tracks, scenario) in this task. +- [ ] **Step 2:** `run-scenario`: tests first (schema error is readable; merged words appear in the rules; output has the tree). Implement. +- [ ] **Step 3:** Designer capstone document and worked example; `output.txt` is the tool's real output, regenerated in a tools test so it cannot go stale. +- [ ] **Step 4:** CI jobs per §2.5; run each job's commands locally and paste the results in the report. +- [ ] **Step 5:** Module 13: the mutation table matches `mutants.json` and names the tool; "Two engines, one specification" reports what `diff-tracks` found in Task 1 and how to run it; "State, undo and replay" and "Solving your own levels" match Tasks 5 and 7; the course outline in README and mentor lists Modules 11–13. +- [ ] **Step 6:** Full verification, `mutation-check`, `verify-levels`, `diff-tracks`. Commit. + +### Task 11: Playable SpellGrid on the site + +Implements spec §5.1. + +**Files:** +- Modify: `site/scripts/sync-content.mjs` (generate the game sources), `site/scripts/sync-content.test.mjs`, `site/package.json` if a script changes, `site/.gitignore` (generated directory) +- Create: `site/src/game/content.browser.ts` (static imports of cards, words, seams, levels, and the reference's hazards and movement), `site/src/game/play.ts` (DOM glue), `site/src/pages/play.astro` or a Starlight page embedding it, `site/scripts/game.test.mjs` +- Modify: `site/astro.config.mjs` (sidebar entry "Play SpellGrid"), `course/10-spellgrid/lesson.md`, `course/11-conditions/lesson.md`, `course/12-combat/lesson.md` (link), `site/README.md` + +**Interfaces:** +- Consumes: the Module 12 TypeScript reference's `src/engine` and `src/harness.ts` unmodified (if the harness imports `./content` in a way that cannot be redirected by the bundler, the sync step may rewrite that one import line, and nothing else); `logTree`; all levels. + +- [ ] **Step 1:** `site/scripts/game.test.mjs`: the generated game module, loaded under Node, plays `l3` with its scripted inputs to `won()`. Run: fail. +- [ ] **Step 2:** Sync step and browser content module; test passes. +- [ ] **Step 3:** The page per §5.1. Build the site, serve `dist` locally, and exercise the page in a headless browser if one is available (load, play `l3` to the win, open the log tree, Undo, switch level, at 375 px and 1280 px, light and dark); if none is available, say so in the report and describe what was checked instead. +- [ ] **Step 4:** Links from the lessons; `site/README.md`. +- [ ] **Step 5:** `npm run test:site`, site build, full verification. Commit. diff --git a/docs/superpowers/specs/2026-11-11-course-v2-extensions-design.md b/docs/superpowers/specs/2026-11-11-course-v2-extensions-design.md new file mode 100644 index 0000000..d41aaf4 --- /dev/null +++ b/docs/superpowers/specs/2026-11-11-course-v2-extensions-design.md @@ -0,0 +1,114 @@ +# Course v2 extensions: design + +Date: 2026-11-11. Branch: `extend/course-v2` (from `revise/edge-cases-and-sources`). + +## 0. Intent + +The first revision found three reference-engine bugs and six unpinned ordering decisions by testing the tests. This round closes the gaps the course itself admits (two references never compared; interceptor kinds named but not built; an engine one card can crash; Assemble accepting meaningless text), extends the subject by two modules (conditions, combat) and one seam, and makes the course easier to use (log tree, undo, more levels, a designer deliverable, CI for the new checks, a playable site). + +Anti-goals: no renumbering of Modules 00–10; no change that makes an existing learner workspace at Modules 1–10 fail tests it used to pass, except where §4 says a module's contract grows; no real-game claim without a fetched source. + +## 1. Conventions (binding on every task) + +1. **Two tracks, one behaviour.** Every engine change is made in the Rust and the TypeScript reference of every module that has the code. Module references are cumulative full copies: a change introduced "from Module N" goes into the references of N and every later module that has a `reference/`, both tracks. Keep the two tracks' `words.json` / `seams.json` byte-identical. +2. **Tests.** Named `test_NN_…` (Expansion Drop: `test_NNx_…`), the same names in `tests/rust/*.rs`, `tests/ts/*.test.ts` and as `### test_…` headings in `scenarios.md` (drop: `expansion/README.md`), written in the style of the neighbouring tests and scenarios. TDD: each new test is shown failing before the change (at least one track) and passing after. +3. **Zero-Coupling Law.** Engine code (`src/engine/`) never names a card or a noun. `tools/coupling-audit.mjs` must print `ok` for every reference. +4. **Verification, all of it, before a task is reported done:** `node tools/verify-references.mjs`, `node tools/verify-references.mjs --expansion`, `npm run check`, `npm run test:tools`, `npm run test:site`, and `for d in course/*/reference/*/; do node tools/coupling-audit.mjs "$d"; done`. From Task 1 on also `node tools/diff-tracks.mjs --seeds 200`. +5. **Lessons.** Plain, short sentences in the style of the existing `lesson.md` files. Beats in order: Observe, Predict, Spec, Build, Name it, Expansion Drop, Stress it, Further reading. Every claim about Hearthstone or Baba Is You carries a link to a page that was fetched in this task, with any quotation copied exactly and at most 125 characters; anything the course decides for itself is labelled as the course's choice, in a "How the real game differs" box where one fits. If a source cannot be fetched, say so in the text rather than quoting from memory. Counts of tests in a lesson ("Make the six tests pass") and code quoted from a reference must match the files. +6. **Docs that travel with code.** A change to a harness contract updates `course/harness.md`; a new card updates `course/catalog.json` (and passes `tools/test/schema.test.mjs`); a new tool is listed in `README.md` and has a test in `tools/test/`. +7. **Git.** Commit on `extend/course-v2`; do not push; every commit message ends with `Co-Authored-By: Claude Opus 5.5 `. +8. **No subagents, no scope creep.** Build what the task says. Problems found outside the task go in the report. + +## 2. Tools + +### 2.1 `tools/diff-tracks.mjs` (differential fuzzer) + +Stages the Rust and TypeScript references of one module (default: the highest-numbered module whose reference has a SpellGrid CLI) with cards and levels, as `play-spellgrid` does. For each seed it generates a random SpellGrid level (size 4–8 square-ish; text drawn from the staged `words.json` plus `IS`, `AND`, `NOT`, biased so that whole sentences occur; objects of the nouns; `cards` and `hand` drawn from the staged `cards/` that have a `noun` or are spells) and a random script of 5–25 CLI commands (moves, casts with and without targets, plays, chooser answers). It pipes the script to both CLIs and compares stdout. Output: per seed nothing on agreement; on the first divergence the seed, the level JSON path, the script and a unified diff of the two outputs. Flags: `--seeds N` (default 100), `--seed S` (one seed), `--module NN`, `--dir`, `--keep`. Exit 1 on any divergence. The generator is a seeded PRNG in the tool; the same seed always gives the same case. The Rust binary is built once per run. + +A level or script that makes *both* CLIs print the same error is agreement. A crash (panic, uncaught exception, timeout of 10 s) on one side only is a divergence; on both sides it is reported separately as `both crashed` and exits 1. + +Divergences found are fixed in this round: decide the right behaviour from the lessons and `harness.md`, fix the wrong track (or both), add a regression test in both tracks. If the right behaviour is undetermined, pick the simpler one, document it in the owning lesson's "Decisions in the reference", and say so in the report. The known `register_assembled` difference is in scope. + +### 2.2 `tools/mutation-check.mjs` + +Reads `tools/mutants.json`: a list of `{ name, file, find, replace, expect: [test names] }` against the TypeScript reference of the highest module with a reference. For each, stages the reference with all cumulative tests, applies the textual replacement (fail if `find` does not occur exactly once), runs vitest, and asserts that at least the `expect` tests fail and the run is otherwise sane. Restores the file. Exit 1 if any mutant survives or any `find` is stale. Initial list: the nine rows of Module 13's table (end-of-Phase order, four turn mutants, hazards reversed, DEFEAT before HOT/MELT, live auras, plus one new mutant per feature added in this round that has an order: redirect-before-armor (§3.2), conditions evaluated at use (§4.1)). + +### 2.3 `tools/verify-levels.mjs` and the solver + +`course/10-spellgrid/levels/proofs.json`: one entry per level, `{ level, shortest, neverRule?, naiveUnsolvable? }`. The tool runs `tools/solver/solve.ts` and checks: a solution exists with exactly `shortest` inputs and none shorter; with `neverRule`, the search exhausts with no solution (within `--max 60`); with `naiveUnsolvable`, likewise under `--naive`. The solver's position key becomes age-aware: the harness gains `stackAt(x, y): string[]` (kinds on the tile, oldest first), and the key uses it instead of `kindsAt`. + +### 2.4 `tools/run-scenario.mjs` (designer path) + +`node tools/run-scenario.mjs scenario.json`. A scenario is data: `{ cards?: CardDef[], words?: WordDef[], seams?: {triggerWords?, multiplierWords?}, level: LevelDef, inputs: string[] }` where inputs are CLI command lines. The tool stages the TypeScript SpellGrid reference, adds the cards to `cards/`, merges the words and seams into content's JSON files, and prints after each input the board, rules, minions, hand and the Part I log as an indented tree (§3.1). It validates cards against `course/schema/card.schema.json` first and reports schema errors readably. No engine file is touched: what a designer can express is exactly what an Expansion Drop can. + +### 2.5 CI + +`.gitea/workflows/ci.yml` gains jobs: `diff-tracks` (`--seeds 300`), `mutation-check`, `verify-levels`. The existing jobs are unchanged. (No runner is available to this work; the workflow is validated by running each job's commands locally.) + +## 3. Engine changes inside existing modules + +### 3.1 Log tree (from Module 2) + +Engine events carry the nesting depth at which they were recorded (0 for the outermost action's own events; +1 inside each resolving trigger). `GameEvent` as the tests see it is unchanged. Harness: `logTree(): string` / `log_tree(&self) -> String`, one event per line, two spaces of indent per level, each line the event in the form used by `scenarios.md` (`Triggered{source: 3, trigger: "deathrattle"}`), ending with a newline. New test `test_02_log_tree_shows_nesting` on the Echo / Armorsmith / Reactor scenario; Module 3's Death Phase events are at depth 0 and their Deathrattles' effects at depth 1 (`test_03_log_tree_death_phase`). Both SpellGrid CLIs print the tree of the Sequence(s) the last command ran, under a `Log:` line. Lesson 2's Stress it question 3 now points at the harness function and asks the learner to build theirs before reading the reference's. + +### 3.2 Replacement effects (from Module 4) + +Module 4 gains a second interceptor. One engine step, "apply a Damage to one target", consults modifiers before the world changes, in a fixed order written as a list (like `END_OF_PHASE`): `DAMAGE_STEPS = [redirect, armor]`. + +- **Redirect** is aura data: `{type: "damageRedirect", from: "friendlyHero", to: "self"}` (schema and both tracks' card types). While in the aura snapshot, damage that would be dealt to the aura owner's hero is dealt to the aura's minion instead. A redirect applies at most once to one Damage (the rewritten Damage is not offered to redirects again: MTG 614.5's guard); with several, the oldest minion's (order of play) wins. The `Damaged` event names the minion that took it. +- **Armor** is a rule of heroes: damage to a hero lowers Armor first, then Health. One `Damaged{target: hero, amount}` event with the full amount (taking damage "still counts"), preceded by `ArmorLost{player, amount}` (new `GameEvent`) when Armor absorbed any. + +Card: the implementer fetches `https://hearthstone.wiki.gg/wiki/Bolf_Ramshield` and uses the card's exact current text and stats if it is a hero-damage redirect; otherwise a fictional `Test Bodyguard` 1/9 "Whenever your hero would take damage, this minion takes it instead." Tests (Module 4 main suite): `test_04_armor_absorbs_before_health`, `test_04_redirect_replaces_hero_damage` (hero untouched, minion damaged, a hero-damage trigger does not fire, the minion's own damage trigger does), `test_04_redirect_applies_once` (two redirectors: the older takes all of it), `test_04_redirect_before_armor` (hero with Armor and a redirector: Armor untouched). Lesson 4: a new section after Name it, "A second interceptor: replacement", with the rules, the list, the tests and MTG 614 as the name; Module 2's "How the real game differs" and Stress it question 4 updated to point here. The redirect aura obeys the Aura Update schedule like any aura. + +### 3.3 Robustness (from Module 5, where a test can write the card) + +- **Nesting guard.** A trigger of a minion is not queued for an event raised while that same (minion, trigger) is resolving. `test_05_trigger_does_not_nest_in_itself`: `Test Bomb` 1/50 "Whenever a minion takes damage, deal 1 damage to all minions." registered in the test; two Bombs; `dealDamage(bomb1, 1)`; the Sequence ends; exact `Triggered` count asserted and explained in the scenario. +- **Death Phase cap.** More than 64 Death Phases in one Sequence is an error: TypeScript throws `Error` and Rust panics, both with a message containing `Death Phase limit`. `test_05_death_phase_limit`: `Test Ember` 1/1 "Deathrattle: Summon a Test Ember. Deal 1 damage to all minions." +- **Well-formed Assemble.** `wellFormed(text, type)` in the engine: for a minion, no effect clause before the first trigger and every trigger followed by at least one effect clause before the next trigger, aura, keyword or the end. `assemble` (harness and effect) fails with `AssembleError` for an ill-formed result, and the `assemble` effect's `"donor"` and `"half"` options exclude combinations that would be ill-formed (so do `"base"` options that would have no donor left). `test_05_assemble_rejects_ill_formed_text` covers Loot Hoarder with Baron Rivendare's top and bottom halves and checks an offer list. Every card in `course/cards` and `course/10-spellgrid/cards` must itself be well-formed (a tools test). +- **Summon count.** A `summon` effect with `count > 1` places its tokens left to right in summon order (position recomputed per token). `test_05_multi_summon_order`. + +Lessons 2, 3 and 5: the Stress it answers that say "the reference doesn't have it" now say where it is and since when. + +### 3.4 Undo (from Module 9) + +`Baba.undo(): boolean` / `undo(&mut self) -> bool`: restores the level to the state before the last `step` or `move` (false and no change if there is none; repeated calls walk back). The engine snapshots, per the lesson's list: every object's id, kind and position in age order, the id counter, and `won`; rules are recompiled, not saved. `place` clears the history. `test_09_undo_restores_everything`: a turn that moves, converts and destroys; after `undo`, every `posOf`, `kindsAt`, `stackAt`, `rules()`, `won()` equals the values before, ids included; then the same step again gives the same result as the first time. SpellGrid does not undo (its harness function throws / panics with a message containing `not supported`): Part I has state the snapshot does not cover; Module 13 says so. `stackAt` (§2.3) is added to the Baba harness in the same change (from Module 8, where `kindsAt` appears). + +### 3.5 Summoned minions enter the grid (Module 10: a fourth seam) + +When a Sequence ends, for each `Summoned` event whose minion has no grid object and whose card has a `noun`: place an object of that noun, mapped to the minion, on the tile of the event's source if the source is (or, having died in this Sequence, was) on the grid, before dead minions' objects are removed and before the reparse. A minion summoned by a spell, or whose card has no noun, stays off the grid as before. Engine events therefore record the summoner (`Summoned.by`, internal; the harness `GameEvent` is unchanged). New SpellGrid cards: `Clay Golem` 2/2 noun `GOLEM` "Deathrattle: Summon a Clay Shard."; `Clay Shard` 1/1 noun `SHARD`. Tests: `test_10_summoned_minion_enters_grid` (the shard stands on the golem's tile, is named by `SHARD IS …` rules at once, has the newest id), `test_10_summon_without_noun_stays_off_grid` (Harvest Golem). Lesson 10: the seam diagram and list become four; Stress it question 3 becomes a pointer to the built seam and keeps the open questions. + +### 3.6 More levels + +Seven new levels `l4`–`l10` in `course/10-spellgrid/levels/`, each built around one named idea from the course, each with `test_10_level_N_solution` in both tracks and a `proofs.json` entry with a `neverRule` (or `naiveUnsolvable`) that the solver confirms. Ideas, one each: `NOT` broken to enable a doubler; `X IS X` lock removed by `X IS NOT X`; hazard order observable through Deathrattles (Module 10 Stress it 1); a summoned shard used as a `PUSH` block or key (§3.5); Graft with a top-half swap; `PULL`-free push chain into `SINK`; a level where the obvious Whirlwind kills the player unless a rule is rewritten first. Each level 7×7 or smaller where possible, shortest solution at most 14 inputs, solvable by the solver in under 60 s. The lesson lists them with a one-line statement of the idea and no solution. + +## 4. New modules + +Module 11 (`course/11-break-your-engine`) is renamed to **13** first, with every link and mention updated. New modules get the full module shape: `lesson.md`, `scenarios.md`, `tests/{rust,ts}`, `reference/{rust,ts}` (a copy of the previous module's final reference plus the module's change, with README and `.course.json`), `expansion/` (README, tests, solution), and a line in `course/harness.md`. `start-module`, `verify-references`, `sync-check`, the site sync and the mentor skill must work with them; `start-module 13` fails with a message saying Module 13 has no tests to start. + +### 4.1 Module 11: Conditions (Part II) + +`NOUN ON NOUN IS …`: the rule applies to an object only while the condition holds for *that object*. Grammar: `subjects [ON noun {AND noun}] IS complements`; the condition belongs to every subject of the sentence. Rules carry `conditions: [{word, nouns}]`. What a condition word *means* is content: a function `(level, object, nouns) -> bool` registered by word, as hazards and responses are; the engine knows only that a word with role `condition` may stand between subjects and `IS`. `ON`: some other object named by each listed noun shares the object's tile (float level ignored unless the fetched wiki page says otherwise; record what it says). + +Evaluation is **at use**: `hasProperty`, conversion, hazards and the win check evaluate conditions against the board as it is when they ask. Resolution (`NOT`, identity) pairs a negation only with rules whose conditions are identical; otherwise negation is settled per object at use, among the rules whose conditions hold for it. `rules()` / `activeRules()` print `BABA ON LAVA IS MELT`. + +Tests (`test_11_…`), at least: condition gates a property; it follows the object as it moves within one turn (true after movement, checked by hazards); `AND` in the condition requires both; a conditional negation beats an unconditional rule only where it holds; a conditional conversion; conditions and stacked text; `ON` with a group noun. Expansion Drop: `NEAR` (the eight neighbours and the same tile, or as the fetched page defines it) as one word and one content function. Lesson theme: the third time a modifier is read. Baron: at a scheduled snapshot. `NOT`: at compile time. A condition: every time someone asks; what that costs (no table lookup, order-of-evaluation inside a turn becomes visible) and what the real game does about it (quote the Order of Operations page on conditions reparsing between stages). Name it: *guards*, or rules as predicates. + +### 4.2 Module 12: Combat (Part I) + +`attack(attacker, defender)` is one Sequence: legality, then both deal their Attack to each other simultaneously (one Damage action with two targets and per-target amounts), then triggers, then the usual end of Phase. Rules, each from the fetched Advanced rulebook or labelled as the course's: a minion with 0 Attack cannot attack; with an enemy `taunt` minion in play only taunt minions may be attacked (error otherwise; `canAttack(attacker, defender): string | null` gives the reason); heroes can be attacked and deal no damage back; **the defender's damage triggers queue before the attacker's regardless of order of play** (the exception Modules 1 and 2 list as unmodelled). `endTurn(player)` is one Sequence that raises the `endOfTurn` event for that player's minions, making Drakkari Enchanter's existing data live. No mana, no attack-once-per-turn, no summoning sickness: labelled simplifications. + +Tests (`test_12_…`), at least: simultaneous combat damage and both die in one Death Creation Step; defender triggers first with an older attacker; taunt restricts targets; attacking a hero with Armor (uses §3.2); a redirect does not apply to combat damage aimed at a minion; end of turn trigger fires once, twice with Drakkari, and Drakkari's own timing obeys the Aura Update schedule. A fictional `Test Bell` "At the end of your turn, deal 1 damage to the enemy hero." if no course card has an end-of-turn trigger. Expansion Drop: one real Hearthstone minion, text and stats fetched, expressible in the existing vocabulary, whose test needs combat (a `taunt` minion with a damage trigger is the shape to look for). Lesson theme: an exception to an ordering rule is itself data (a priority on the queue entry), tying back to Liquid Fire's `priority` field cited in Module 2. Name it: *priority within a queue*. + +## 5. Site and designer path + +### 5.1 Playable SpellGrid + +A page `/play/` on the course site: level picker (all levels), the grid drawn as text tiles, keyboard (arrows/WASD) and buttons for moves, hand cards as buttons (click a card, then a tile if it needs one), chooser prompts as buttons, the rules in force, minions with Health, the Part I log of the last input as a collapsible tree (§3.1), Undo by replaying inputs, Restart. It runs the TypeScript SpellGrid reference unmodified in the browser: the site's sync step copies the reference's `src/engine` and `src/harness.ts` into a generated directory and supplies a browser `content` module that provides cards, words, seams, hazards and movement via static imports instead of `node:fs`. No rules are reimplemented in the page. Works without JavaScript frameworks beyond what Astro/Starlight already ships; usable on a phone; honours light and dark themes. Each of Lessons 10–12 links to it. `npm run test:site` gains a test that the generated game module solves `l3` with its scripted inputs under Node. + +### 5.2 Designer capstone + +`course/10-spellgrid/designer-capstone.md`, linked from Lesson 10 and the README's Designer path: design three cards and one word within the existing vocabulary, write them as data, write five predictions as scenarios for `tools/run-scenario.mjs`, run them, and explain each surprise in terms of a named pattern. It includes one complete worked example (cards, word, scenario file under `course/10-spellgrid/designer-example/`, the tool's actual output) and a rubric. + +## 6. Left to the builder + +File-internal structure, helper names, exact wording of lessons within §1.5, the layouts of new levels, and any choice the lessons and this document leave open, provided it is recorded where the course records such choices ("Decisions in the reference", "How the real game differs").