From 638549161f0a4291ef366523b1efe976b614df8b Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 29 Oct 2026 19:13:11 +0100 Subject: [PATCH] feat(ci): verify Expansion Drop solutions; README quick start, disclaimer link; Rust reference lockfiles - verify-references --expansion overlays expansion/solution on each reference and runs the cumulative tests plus the drop test; refuses a solution that touches src/engine/ - CI job verify-expansions - README: workspace-template + git init step, tool list, repo disclaimer link - Cargo.lock for every Rust reference (staged with the reference, pins serde for rustc 1.85.1) Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/ci.yml | 13 +++ README.md | 17 ++- course/01-handles/reference/rust/Cargo.lock | 7 ++ .../02-action-queue/reference/rust/Cargo.lock | 7 ++ .../03-death-step/reference/rust/Cargo.lock | 7 ++ .../04-interceptors/reference/rust/Cargo.lock | 7 ++ course/05-assemble/reference/rust/Cargo.lock | 107 ++++++++++++++++++ course/06-parser/reference/rust/Cargo.lock | 107 ++++++++++++++++++ .../reference/rust/Cargo.lock | 107 ++++++++++++++++++ .../08-push-chains/reference/rust/Cargo.lock | 107 ++++++++++++++++++ .../reference/rust/Cargo.lock | 107 ++++++++++++++++++ course/10-spellgrid/reference/rust/Cargo.lock | 107 ++++++++++++++++++ .../expansion/solution/cards/test-drop.json | 1 + .../solution/rust/src/content/drop.rs | 1 + .../01-m01/expansion/solution/words.json | 1 + tools/test/verify-references.test.mjs | 36 ++++++ tools/verify-references.mjs | 66 ++++++++++- 17 files changed, 796 insertions(+), 9 deletions(-) create mode 100644 course/01-handles/reference/rust/Cargo.lock create mode 100644 course/02-action-queue/reference/rust/Cargo.lock create mode 100644 course/03-death-step/reference/rust/Cargo.lock create mode 100644 course/04-interceptors/reference/rust/Cargo.lock create mode 100644 course/05-assemble/reference/rust/Cargo.lock create mode 100644 course/06-parser/reference/rust/Cargo.lock create mode 100644 course/07-rule-resolution/reference/rust/Cargo.lock create mode 100644 course/08-push-chains/reference/rust/Cargo.lock create mode 100644 course/09-turn-pipeline/reference/rust/Cargo.lock create mode 100644 course/10-spellgrid/reference/rust/Cargo.lock create mode 100644 tools/test/fixtures/course/01-m01/expansion/solution/cards/test-drop.json create mode 100644 tools/test/fixtures/course/01-m01/expansion/solution/rust/src/content/drop.rs create mode 100644 tools/test/fixtures/course/01-m01/expansion/solution/words.json diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9837587..00d019a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -30,6 +30,19 @@ jobs: - uses: dtolnay/rust-toolchain@1.85.1 - run: node tools/verify-references.mjs + verify-expansions: + name: Verify Expansion Drop solutions (all modules, Rust and TS) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: dtolnay/rust-toolchain@1.85.1 + # Each module's reference + expansion/solution, cumulative tests + the drop's test. + - run: node tools/verify-references.mjs --expansion + coupling-audit: name: Coupling audit on references runs-on: ubuntu-latest diff --git a/README.md b/README.md index 2be9d3a..a38f79f 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,25 @@ Builder path exercises exist in **Rust** and **TypeScript**. Both tracks have id ## Quick start +From the course repository: + ```sh -node --version # Node 24 for the course tooling +node --version # Node 22 or later for the course tooling (CI uses 24) npm run test:tools # tooling tests npm run check # test names agree across Rust, TS and scenarios -node tools/start-module.mjs 01 path/to/your/workspace +``` + +Create your workspace from the template for your track (Module 00, "Before Module 1: choose your track", has the details). Copy the whole directory with `cp -r` so the hidden `.course.json` comes along: + +```sh +cp -r workspace-template/rust ~/my-engine # or workspace-template/ts +cd ~/my-engine && git init && git add -A && git commit -m "start from template" && cd - +node tools/start-module.mjs 01 ~/my-engine ``` `start-module` copies a module's tests for your track (from your workspace's `.course.json`) and refuses to start a module if you have not yet taken the previous one. Module 00 uses the `naive-engine/` project and is not started this way. -Other tools: `tools/coupling-audit.mjs ` (engine code must not name specific cards or nouns), `tools/expansion-check.mjs ` (an Expansion Drop must change content only), `tools/verify-references.mjs [NN]` (reference solutions pass the tests). +Other tools: `tools/coupling-audit.mjs ` (engine code must not name specific cards or nouns), `tools/expansion-check.mjs ` (an Expansion Drop must change content only), `tools/verify-references.mjs [NN]` (reference solutions pass the tests; `--expansion` also checks every Expansion Drop solution), `tools/play-spellgrid.mjs [l1|l2|l3]` (play the Module 10 capstone, SpellGrid, on the command line with the reference engine). ## Licence @@ -40,4 +49,4 @@ MIT, for both prose and code. See [LICENSE](LICENSE). The course is and stays fr ## Disclaimer -Not affiliated with Blizzard or Hempuli. See the [disclaimer](/disclaimer/). +Not affiliated with Blizzard or Hempuli. See the [disclaimer](site/src/content/docs/disclaimer.md) (also the Disclaimer page of the course website). diff --git a/course/01-handles/reference/rust/Cargo.lock b/course/01-handles/reference/rust/Cargo.lock new file mode 100644 index 0000000..e9d1789 --- /dev/null +++ b/course/01-handles/reference/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" diff --git a/course/02-action-queue/reference/rust/Cargo.lock b/course/02-action-queue/reference/rust/Cargo.lock new file mode 100644 index 0000000..e9d1789 --- /dev/null +++ b/course/02-action-queue/reference/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" diff --git a/course/03-death-step/reference/rust/Cargo.lock b/course/03-death-step/reference/rust/Cargo.lock new file mode 100644 index 0000000..e9d1789 --- /dev/null +++ b/course/03-death-step/reference/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" diff --git a/course/04-interceptors/reference/rust/Cargo.lock b/course/04-interceptors/reference/rust/Cargo.lock new file mode 100644 index 0000000..e9d1789 --- /dev/null +++ b/course/04-interceptors/reference/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" diff --git a/course/05-assemble/reference/rust/Cargo.lock b/course/05-assemble/reference/rust/Cargo.lock new file mode 100644 index 0000000..6b7cc4c --- /dev/null +++ b/course/05-assemble/reference/rust/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/course/06-parser/reference/rust/Cargo.lock b/course/06-parser/reference/rust/Cargo.lock new file mode 100644 index 0000000..6b7cc4c --- /dev/null +++ b/course/06-parser/reference/rust/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/course/07-rule-resolution/reference/rust/Cargo.lock b/course/07-rule-resolution/reference/rust/Cargo.lock new file mode 100644 index 0000000..6b7cc4c --- /dev/null +++ b/course/07-rule-resolution/reference/rust/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/course/08-push-chains/reference/rust/Cargo.lock b/course/08-push-chains/reference/rust/Cargo.lock new file mode 100644 index 0000000..6b7cc4c --- /dev/null +++ b/course/08-push-chains/reference/rust/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/course/09-turn-pipeline/reference/rust/Cargo.lock b/course/09-turn-pipeline/reference/rust/Cargo.lock new file mode 100644 index 0000000..6b7cc4c --- /dev/null +++ b/course/09-turn-pipeline/reference/rust/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/course/10-spellgrid/reference/rust/Cargo.lock b/course/10-spellgrid/reference/rust/Cargo.lock new file mode 100644 index 0000000..6b7cc4c --- /dev/null +++ b/course/10-spellgrid/reference/rust/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "engine" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tools/test/fixtures/course/01-m01/expansion/solution/cards/test-drop.json b/tools/test/fixtures/course/01-m01/expansion/solution/cards/test-drop.json new file mode 100644 index 0000000..5a4f9dc --- /dev/null +++ b/tools/test/fixtures/course/01-m01/expansion/solution/cards/test-drop.json @@ -0,0 +1 @@ +{"name":"Test Drop"} diff --git a/tools/test/fixtures/course/01-m01/expansion/solution/rust/src/content/drop.rs b/tools/test/fixtures/course/01-m01/expansion/solution/rust/src/content/drop.rs new file mode 100644 index 0000000..671ee22 --- /dev/null +++ b/tools/test/fixtures/course/01-m01/expansion/solution/rust/src/content/drop.rs @@ -0,0 +1 @@ +// drop diff --git a/tools/test/fixtures/course/01-m01/expansion/solution/words.json b/tools/test/fixtures/course/01-m01/expansion/solution/words.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/tools/test/fixtures/course/01-m01/expansion/solution/words.json @@ -0,0 +1 @@ +[] diff --git a/tools/test/verify-references.test.mjs b/tools/test/verify-references.test.mjs index 8edf7ab..4710433 100644 --- a/tools/test/verify-references.test.mjs +++ b/tools/test/verify-references.test.mjs @@ -93,3 +93,39 @@ test("verify_stage_copies_module_cards_up_to_the_module", async () => { assert.ok(existsSync(join(later, "cards", "module-fixture.json")), lang); } }); + +test("verify_expansion_overlays_solution_and_drop_test", async () => { + // --expansion: the reference, the cumulative tests, then the module's drop solution + // (code over the root, cards/ into cards/, words.json into src/content/) and drop test. + process.env.COURSE_ROOT = root; + const { planVerification, stage, overlayExpansion, hasExpansion } = await import("../verify-references.mjs"); + const { existsSync } = await import("node:fs"); + const plan = planVerification(moduleDirs(), 1)[0]; + assert.ok(hasExpansion(plan, "rust")); + assert.ok(!hasExpansion(planVerification(moduleDirs(), 2)[1], "rust")); + const dir = overlayExpansion(plan, "rust", stage(plan, "rust")); + for (const f of ["Cargo.toml", "tests/m01.rs", "tests/m01x.rs", "src/content/drop.rs", "src/content/words.json", "cards/test-drop.json"]) { + assert.ok(existsSync(join(dir, f)), f); + } +}); + +test("verify_expansion_rejects_solution_that_changes_engine", async () => { + const { overlayExpansion } = await import("../verify-references.mjs"); + const { mkdirSync, mkdtempSync, writeFileSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const mod = mkdtempSync(join(tmpdir(), "sdc-exp-")); + mkdirSync(join(mod, "expansion/solution/ts/src/engine"), { recursive: true }); + writeFileSync(join(mod, "expansion/solution/ts/src/engine/core.ts"), ""); + assert.throws(() => overlayExpansion({ num: "09", path: mod }, "ts", mkdtempSync(join(tmpdir(), "sdc-st-"))), /changes the engine/); +}); + +test("verify_expansion_cli_runs_only_modules_with_a_solution", () => { + const r = run("verify-references.mjs", ["02", "--expansion"], { + COURSE_ROOT: root, + VERIFY_CMD_RUST: "test -f tests/m01x.rs && test -f src/content/drop.rs", + VERIFY_CMD_TS: "true", + }); + assert.equal(r.code, 0, r.out); + assert.match(r.out, /module 01-m01 \[rust\] expansion: ok/); + assert.match(r.out, /module 02-m02 \[rust\] expansion: skipped/); +}); diff --git a/tools/verify-references.mjs b/tools/verify-references.mjs index 5ca8a30..3fd6f93 100644 --- a/tools/verify-references.mjs +++ b/tools/verify-references.mjs @@ -4,10 +4,16 @@ // from Module 5 on (Modules 1-4 keep their cards in code), plus the cards/ of each module up to // the one verified. Each tested module's levels/ (the puzzle levels its tests load, from // Module 10) is staged as /levels. -import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +// +// node tools/verify-references.mjs [NN] [--expansion] +// +// --expansion checks each module's Expansion Drop solution instead: the reference is staged as +// above, then the module's expansion/ is overlaid (see overlayExpansion) and the cumulative tests +// run together with the drop's own test. Modules without an expansion/solution are skipped. +import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; -import { basename, join } from "node:path"; +import { basename, join, relative } from "node:path"; import { cardDirs, moduleDirs, testFiles, isMain } from "./lib.mjs"; // A staged reference has no node_modules, so TS installs the reference's own @@ -32,6 +38,43 @@ export function planVerification(mods, upTo) { .filter((p) => p.langs.length > 0); } +/** Files under `dir`, as paths relative to it ("/"-separated). */ +function files(dir, root = dir, out = []) { + for (const e of readdirSync(dir)) { + const p = join(dir, e); + statSync(p).isDirectory() ? files(p, root, out) : out.push(relative(root, p).split("\\").join("/")); + } + return out; +} + +/** Does this module's Expansion Drop have a solution to check for `lang`? */ +export function hasExpansion(plan, lang) { + const exp = join(plan.path, "expansion"); + return existsSync(join(exp, "solution")) && testFiles(exp, lang).length > 0; +} + +/** + * Overlay a module's Expansion Drop solution onto a staged reference, as a learner applies it: + * expansion/solution// over the reference root, solution/cards/ into cards/, + * solution/words.json and solution/seams.json into src/content/, and the drop's test into tests/. + * Throws if the solution touches src/engine/ (a drop changes content only). + */ +export function overlayExpansion(plan, lang, dir) { + const sol = join(plan.path, "expansion", "solution"); + const code = join(sol, lang); + if (existsSync(code)) { + const engine = files(code).filter((f) => f.startsWith("src/engine/")); + if (engine.length) throw new Error(`${plan.num} expansion solution changes the engine: ${engine.join(", ")}`); + cpSync(code, dir, { recursive: true }); + } + if (existsSync(join(sol, "cards"))) cpSync(join(sol, "cards"), join(dir, "cards"), { recursive: true }); + for (const f of ["words.json", "seams.json"]) { + if (existsSync(join(sol, f))) cpSync(join(sol, f), join(dir, "src", "content", f)); + } + for (const f of testFiles(join(plan.path, "expansion"), lang)) cpSync(f, join(dir, "tests", basename(f))); + return dir; +} + export function stage(plan, lang) { const dir = mkdtempSync(join(tmpdir(), `verify-${plan.num}-${lang}-`)); cpSync(join(plan.path, "reference", lang), dir, { recursive: true }); @@ -45,18 +88,31 @@ export function stage(plan, lang) { } if (isMain(import.meta.url)) { + const args = process.argv.slice(2); + const expansion = args.includes("--expansion"); + const num = args.find((a) => a !== "--expansion"); const all = moduleDirs(); - const upTo = process.argv[2] ? Number(process.argv[2]) : Math.max(0, ...all.map((m) => Number(m.num))); + const upTo = num ? Number(num) : Math.max(0, ...all.map((m) => Number(m.num))); const plans = planVerification(all, upTo); if (!plans.length) console.log("verify-references: no modules with reference/ up to " + upTo); let failed = false; for (const p of plans) { for (const lang of p.langs) { + if (expansion && !hasExpansion(p, lang)) { + console.log(`module ${p.num}-${p.slug} [${lang}] expansion: skipped (no expansion solution)`); + continue; + } const dir = stage(p, lang); - const r = spawnSync(commandFor(lang), { cwd: dir, shell: true, encoding: "utf8" }); + let r; + try { + if (expansion) overlayExpansion(p, lang, dir); + r = spawnSync(commandFor(lang), { cwd: dir, shell: true, encoding: "utf8" }); + } catch (e) { + r = { status: 1, stdout: "", stderr: String(e.message) }; + } const ok = r.status === 0; failed ||= !ok; - console.log(`module ${p.num}-${p.slug} [${lang}]: ${ok ? "ok" : "FAIL"}`); + console.log(`module ${p.num}-${p.slug} [${lang}]${expansion ? " expansion" : ""}: ${ok ? "ok" : "FAIL"}`); if (!ok) console.log((r.stdout || "") + (r.stderr || "")); rmSync(dir, { recursive: true, force: true }); }