Files
magicka-vm/README.md
T
linus-dandClaude Opus 4.8 9d9d5ce41c Add web game (plan2.md) on the independent runtime, merge-blocking gates
Builds the browser game around the existing Rust runtime: a window into the
universe, not a second simulation. Pure std, no external crates.

New crates:
- protocol: versioned, hashable client/server messages + hand-rolled JSON value
  and total parser (malformed packet -> Err, never panic).
- game_runtime: authoritative match state. Resolves turns through the
  INDEPENDENT interpreter (runtime_under_test::native_resolve), not the
  reference engine; filters visibility/knowledge; records and regenerates
  replays. A match is a pure function of (seed, roster, ordered inputs).
- web_assets/web_client: embedded browser client (arena, rune editor,
  knowledge panels, replay viewer) + static HTTP delivery.
- server: std::net HTTP + WebSocket (hand-rolled SHA-1/base64/RFC-6455 framing),
  turn timer, disconnect handling, panic-proof dispatch, poison-tolerant lock.
- web_tests: dependency-free WebSocket test client + Phase H gates.

Trust hardening per review:
- game_runtime no longer delegates to reference_runtime::execute; it runs the
  independent interpreter that the runtime-equivalence gate proves correct.
- Protocol/socket/replay/visibility/resilience gates are merge-blocking
  (added to the merge_group-required job in merge-gates.yml): 1k matches/0
  drift, 10k fuzz/0 panics, 100 headless socket E2E, 0 hidden-state leaks.
- Rendered-browser E2E is marked EXTERNAL-BLOCKED: Playwright runs advisory-only
  (continue-on-error, artifacts) until CI infrastructure with a browser exists;
  it is treated as unsatisfied, not green. The headless 100-match gate is
  labeled protocol-level coverage, not rendered-browser coverage.
- README documents the hand-rolled crypto/parser audit risk explicitly.

Fixes an integer-overflow panic in observed-volatility inference (i64 sum /
abs near i64::MIN) that could poison the server mutex.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:20:46 -07:00

227 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |