# Magicka VM — Phase 0/1 > The deliverable is a Rust engine whose tests make a fake universe fail. This repository implements the Phase 0/1 specification in `plan.md`: an **adversarial testing framework first**, then a **reference runtime** that passes it, then a **runtime under test** that matches the reference. No spell content, templates, or cosmetic runes — the value is in the tests that refuse to let the universe collapse into a single score, resource, effect axis, executor, rune, hidden formula, or decorative domain. ## Compliance model No gate may pass from configuration, naming, shared implementation, smoke-scale runs, regenerated expectations, proxy metrics, a default profile, or a locally-runnable binary. A gate passes only from persisted, independently reproducible, full-scale adversarial evidence enforced at merge. Every acceptance obligation has all four of: a **measured artifact**, a **provenance chain** to the run that produced it, a **merge-blocking enforcement point**, and a **failure condition if the artifact or provenance is absent**. - The merge-blocking enforcement point is `.github/workflows/merge-gates.yml`, whose `merge-gates` job runs `MAGICKA_PROFILE=merge` (full gates) and must be a **required status check** on the protected branch / merge queue. It is not a local binary, and the fast profile is advisory only — it can never stand in for acceptance. - `compliance_report.json` enumerates every obligation, its artifact, its floor, the actual measured value, and whether the artifact is present. A missing required report fails acceptance. - The merge floors (50k worlds, 250k programs, 1,000,000 executions, 10 perturbations/exec, 100% reference/runtime comparison over base **and** perturbations, 500 mutants, 10,000 replay cases) cannot be lowered by environment overrides: a lowering override is recorded as a provenance failure and the floor is kept. Every gate is built to be *able to fail*, and a negative-control test proves it does: | Gate | How it is made unbypassable | Negative control proving it can fail | |------|-----------------------------|--------------------------------------| | runtime_equivalence | Compares two **independent implementations** (the reference engine vs. `runtime_under_test::native`, which never calls the reference engine) | `buggy_runtime_is_rejected` — a runtime with one dropped causal edge is caught | | compression_resistance | Attacks operate on the **real serialized trace** (causal influence, info-flow, access, temporal, deltas), not a hash proxy; info loss is genuine unexplained variance | `single_factor_corpus_is_rejected` — a rank-1 universe is rejected | | mutation_survivor | Each mutant must fail the **named gate** it targets, not merely differ from the reference | `reference_passes_every_named_gate` + `no_mutant_survives_its_named_gate` | | replay | Expectations are **loaded from a committed file**, not regenerated in the same run | `corrupted_expectation_is_detected` | | domain_participation | Decorative/redundant domains are flagged directly | `decorative_domain_is_rejected` | | merge scale floor | Env overrides may only **raise** merge counts; a lowering attempt is recorded and the floor kept; executions actually performed are counted | `merge_floor_cannot_be_lowered_by_override`, `merge_profile_at_smoke_scale_is_rejected` | | 100% comparison | Reference vs. runtime-under-test compared for **every** execution — base and all perturbations, never base only | `runtime_equivalence` gate fails unless `equiv_total == base + perturbations` | | provenance | A Merkle root over per-execution records, plus independent engine identities, binds reported numbers to executed work | `merkle_root_binds_to_leaves` | ## Workspace layout Built in the mandatory order from the spec: | # | Crate | Role | |---|-------|------| | 1 | `world_model` | 8 independent domains, world snapshot, perturbation axes, deltas, deterministic primitives (ids, stable hash, RNG) | | – | `rune_ir` | Rune token / program model (no stream is ever rejected) | | 2 | `trace_model` | Execution trace + all graphs, behavior fingerprint, replay record, fault log, trace metrics | | 3 | `generators` | Worlds, programs, executors, contracts, perturbations; rejects flat cases | | 4 | `collapse_analysis` | The 11 compression attacks over real trace structure + collapse gates | | 5 | `semantic_mutation` | Structurally generated mutant runtimes; proves every one fails its named gate | | 6 | `replay_corpus` | Permanent, bit-exact replay cases persisted to `corpus/replay_corpus.tsv` | | 7 | `reference_runtime` | The executable spec engine (`Runtime` trait, `resolve`) | | 8 | `runtime_under_test` | An **independent** interpreter (`native`) proven equivalent to the reference | | – | `ci_reports` | Orchestrator + `ci` binary; emits 8 gate reports + a provenance report | The runtime under test does not call the reference engine. It re-derives the canonical behavior from the spec in a different code organization, so 100% agreement is *evidence* the spec is implemented correctly rather than a tautology. (`native_matches_reference_bit_for_bit` checks this over a 2000-seed sweep.) ## The engine in one paragraph A world is 8 domains, each with 4 observed + 2 hidden integer lanes, a dense 8×8 coupling matrix, partial observability, and pending scheduled effects. A rune program is interpreted under ≥3 executors; each opcode reads several domains, mixes them through a nonlinear avalanche keyed by per-domain constants, the world coupling, and the executor's salt, then writes back — recording causal/read/write/information-flow/temporal edges as it goes. Scheduled effects and coupling diffusion propagate changes 3 turns into the future. ## Running CI ```bash cargo test # unit tests + negative controls MAGICKA_PROFILE=fast cargo run --release -p ci_reports --bin ci # advisory PR slice MAGICKA_PROFILE=merge cargo run --release -p ci_reports --bin ci # acceptance (full gates) ``` Reports are written to the output dir (8 gate reports + `provenance_report.json` + `compliance_report.json` + `ci_summary.md`). The binary exits non-zero if any gate fails or any required artifact is absent. ### Profiles `MAGICKA_PROFILE` (or `MAGICKA_SCALE`) selects the run profile. | Profile | executions | replay | mutants | role | |---------|-----------|--------|---------|------| | `fast` (default) | 600 | 10,000 (committed) | 520 | **advisory only — never acceptance** | | `tiny` | 120 | 10,000 | 520 | smoke | | `merge` (`MAGICKA_SCALE=full`) | 1,000,000 | 10,000 | 600 | **acceptance — hard floors** | The fast/tiny profiles print `ADVISORY … NOT a merge-blocking acceptance run` and are labelled non-acceptance in `compliance_report.json`. Acceptance comes only from the merge profile, run by the merge-gates workflow. The merge floors cannot be lowered by environment overrides (a lowering override is recorded as a provenance failure and the floor kept). ### Merge-blocking enforcement (required check) `.github/workflows/merge-gates.yml` defines the enforcement point. Configure branch protection / the merge queue to **require** the `merge-gates` job. That job runs the full merge profile, verifies the committed corpus has ≥10,000 cases, and fails if any required artifact is missing. The full run executes ~1M base executions × (1 base + 10 perturbations) with 100% reference/runtime comparison; it completes in minutes on a CI runner. ### Replay corpus The replay corpus is committed at `crates/replay_corpus/corpus/replay_corpus.tsv` (10,000 cases). Replay loads those expectations and re-executes the reference, so any engine change that alters a hash makes the committed file and the fresh run disagree and CI fails. Regenerate it only as a deliberate, reviewed migration: ```bash cargo run --release -p replay_corpus --bin freeze -- 10000 ``` ## Determinism Everything is seed-derived and integer-only (SplitMix64 RNG, FNV-1a content hashing, wrapping/guarded arithmetic). No floating point enters a canonical hash, so replay is bit-exact across machines and runs. No external crates. ## The web game (plan2.md) A browser game is built **around** the existing runtime — it is a playable window into the Rust universe, never a second simulation. The browser sends only *intent*; the server is the sole authority; every rune program executes through the **independent** interpreter (`runtime_under_test::native_resolve`) against the shared world. The game deliberately does **not** call the reference engine — the interpreter it uses is the one the runtime-equivalence gate proves correct (with a negative control proving that gate can fail). Same constraints as the rest of the repo: pure `std`, no external crates (the WebSocket server hand-rolls SHA-1, base64, and RFC 6455 framing; JSON is hand-rolled with a total parser). > Audit note: the hand-rolled SHA-1 / base64 / RFC-6455 framing and JSON parser > are checked against published test vectors (RFC 6455 §1.3 accept key, SHA-1 > "abc", base64 length cases) and a fuzz gate, but they are bespoke > cryptographic/parsing code and carry audit risk relative to a reviewed > library. They exist to honor the repo's no-external-crates rule; a future > hardening pass could swap in vetted implementations behind the same interface. ``` Rust runtime → game_runtime (authority) → protocol (WS messages) → server → browser ``` | Crate | Role | |-------|------| | `protocol` | Versioned, hashable, **total-decode** client/server messages + JSON value/parser. A malformed packet yields `Err`, never a panic. | | `game_runtime` | Authoritative match state. Resolves turns through the **independent interpreter** (`runtime_under_test`, not the reference engine), filters visibility/knowledge, records + regenerates replays. A match is a pure function of `(seed, roster, ordered inputs)`. | | `web_assets` | The embedded browser client (HTML/CSS/JS): arena, rune editor, domain/knowledge panels, replay viewer. | | `web_client` | Static-asset HTTP delivery (keeps raw assets separate from framing). | | `server` | `std::net` HTTP + WebSocket server: turn timer, action collection, disconnect handling, panic-proof dispatch. | | `web_tests` | A dependency-free WebSocket test client + the Phase H gates. | ### Running it ```bash cargo run --release -p server --bin magicka-server # serve on 127.0.0.1:8080 # then open http://127.0.0.1:8080 in a browser MAGICKA_ADDR=0.0.0.0:9000 MAGICKA_TURN_MS=8000 cargo run --release -p server --bin magicka-server ``` Join is immediate (1 player + a training dummy). A duel shares a match by id: two browsers that `JoinMatch` the same `match_id` take slots 1 and 2. ### Web CI gates (Phase H) These gates are **merge-blocking**: they run inside the merge-required job in `.github/workflows/merge-gates.yml` (and as fast PR feedback in `web-gates.yml`). They are the Rust suite in `crates/web_tests`, run with `cargo test -p web_tests`: | Gate | Test | Minimum | Status | |------|------|---------|--------| | Replay determinism | `determinism.rs` | 1,000 simulated matches, **0 hash mismatches** | merge-blocking | | Protocol fuzz | `fuzz.rs` | 10,000 fuzz cases, **0 panics** (+ a live server survives a malformed-packet burst) | merge-blocking | | End-to-end matches | `e2e.rs` | **100** full matches over real sockets; recorded replay reproduces every live per-turn hash | merge-blocking | | Hidden-state leaks | `visibility.rs` | **0 leaks** — no client-bound frame carries a hidden key; redaction counts every withheld value | merge-blocking | | Disconnect / timer edges | `resilience.rs` | mid-match disconnect does not corrupt the match; wrong-turn / late submits are rejected deterministically | merge-blocking | | Rendered-browser E2E | `e2e/specs/play.spec.js` | a real browser joins, casts, and replays a match | **external-blocked (advisory only)** | Scope honesty — two distinct things, not conflated: - The "100 E2E matches" merge-blocking gate drives the full HTTP→WebSocket→protocol→runtime path **headlessly over real sockets**. This is protocol-level coverage. It is **not** rendered-browser coverage and is not claimed as such. - Rendered-browser coverage is **blocked on CI infrastructure**: this CI has no real browser, so the Playwright suite under `crates/web_tests/e2e/` cannot be merge-blocking yet. It runs **advisory-only** (`continue-on-error`) in the `rendered-browser-e2e` job and uploads its report as an artifact. Until a CI runner with a browser exists, rendered-browser E2E is treated as **unsatisfied**, not green. Run it locally with: ```bash cd crates/web_tests/e2e && npm install && npx playwright install chromium && npm test ``` ### Acceptance criteria mapping (plan2.md) | Criterion | Where it holds | |-----------|----------------| | A player can join a browser match | `server` join + `web_assets` client; `e2e.rs::single_match_full_playthrough` | | A turn timer runs | `server` timer thread; client header countdown | | Inspect / move / attack / cast | `Action` in `protocol`; `game_runtime::apply_action` | | Rune programs execute only on the server | `game_runtime` is the only caller of the interpreter (`runtime_under_test::native_resolve`); client never imports `EngineConfig` (asserted in `web_assets`) | | Results return as filtered observations | `VisibleWorldSnapshot`; `visibility.rs` | | Replay can reproduce the match | `game_runtime::replay`; `determinism.rs`, `e2e.rs` | | Browser cannot alter hidden truth | intent-only protocol; `visibility.rs` leak gate | | CI proves protocol, replay, visibility, authority | merge-blocking gates in `merge-gates.yml` (+ `web-gates.yml`); rendered-browser E2E remains external-blocked |