# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project overview

A collection of interactive browser demos showcasing **Rust compiled to WebAssembly**, grouped by category. A Cargo workspace with one crate per demo; each compiles to its own `.wasm` (`pkg/<demo>/`), so a page downloads only its own code and special build flags (SIMD, threads) stay local. Rendering backends: Canvas2D, WebGL, WebGPU, plus Web Audio (AudioWorklet).

## Build & test

```bash
python3 scripts/build.py [demo…]  # wasm-pack each crate → pkg/<demo>/index.js + index_bg.wasm
cargo test --workspace -- --test-threads=1   # all crates (single-threaded required)
python3 scripts/serve.py 8080     # Dev server (no-cache + COOP/COEP) → http://localhost:8080/www/
```

`pathtracer-mt` (threaded variant) needs nightly + `rust-src`; `build.py` skips it with a hint when they're missing. Never run rustup commands that name a missing toolchain (`rustup component list --toolchain X`, `cargo +X`): rustup auto-installs it. Check with `rustup toolchain list`, and hand toolchain changes to the user.

To keep the server running on this machine: `pm2 start ecosystem.config.cjs` (app `webtt`, port 8289 with `--mcp mcp.config.json`; the python interpreter is resolved to an absolute path at start, since PATH at boot may lack pyenv; `pm2 restart webtt` after changing the MCP config — rebuilt wasm only needs a refresh). See `docs/INSTALL.md` §3.4.

`cargo run` does not work: demo crates are `cdylib`-only. Setup and troubleshooting are in `docs/INSTALL.md`. Behind a proxy, `wasm-pack build` hangs while it downloads `wasm-bindgen`/`wasm-opt`. Pre-install both tools on PATH. The `wasm-bindgen` CLI version must match `Cargo.lock`.

**Tests must run single-threaded**: the stateful crates (see the table below) use `static Mutex<Option<…>>` global state. Always use `-- --test-threads=1`. Extra test modes: `cargo test -p pathtracer --features threads` (rayon path), `MNIST_DIR=… cargo test -p digits --release -- --ignored` (accuracy on the MNIST test set).

## Commit policy (this project only)

**Auto-commit and auto-push are enabled.** After each meaningful, verified change (tests pass, build succeeds), commit and push to `origin` immediately without asking.

- Conventional commit messages: `feat(<module>):`, `fix(<module>):`, `docs:`, `style(www):`, `refactor(<module>):`
- No `Co-Authored-By` trailers
- Stage only the files touched by the change (`git add <paths>`), never `git add -A` over unrelated work
- Run GitNexus `detect-changes` before each commit (see below)
- After committing, `git push` to `origin` (Gitee, `main` tracks `origin/main`). Never force-push; if the push is rejected, `git pull --rebase` then push, and stop to ask on conflicts

## Architecture

### Crates

`crates/<category>/<demo>/` — categories: `graphics`, `simulation`, `algorithms`, `media`, `tools`, `games`. The crate name equals the page directory `www/<demo>/` (`video-filter` reuses `image-filter`). `crates/common` holds shared helpers (e.g. `xorshift32`); don't duplicate them.

| Pattern | Crates | Characteristics |
|---|---|---|
| **Stateless** | `mandelbrot`, `mandelbulb`, `terrain`, `webgpu`, `benchmark`, `image-filter`, `spectrum`, `qrcode`, `compress`, `digits`, `markdown`, `jpeg`, `sudoku` | Pure functions, no global state (`digits` lazily parses its embedded weights) |
| **Stateful** | `data-table`, `game-of-life`, `particles`, `pathfind`, `fluid`, `sha256`, `falling-sand`, `gomoku`, `rasterizer`, `pathtracer`, `simd`, `synth`, `chip8`, `seam-carving`, `wfc`, `cloth`, `delaunay` | `static NAME: Mutex<Option<Struct>>`, init + mutate pattern |

All exports: `#[wasm_bindgen] pub fn` with `wasm-bindgen`-compatible types — except `synth`, which exports plain `#[no_mangle] extern "C"` functions and has no imports, because it is instantiated inside an AudioWorklet (no ES-module glue there). No `serde` — JSON built manually in `data-table`'s `get_page()`. Dependencies: `wasm-bindgen` (workspace) + `common`; `pathtracer` adds optional `rayon` + `wasm-bindgen-rayon` (feature `threads`).

Per-crate build settings live in `[package.metadata.webtt]` (documented in `scripts/build.py`): `rustflags`, `toolchain`, `wasm-pack-args`, `raw = true` (plain `cargo build` → `pkg/<demo>/index.wasm`, used by `synth`), and `[[…variants]]` for extra builds of the same crate (`pathtracer-mt`: nightly, `+atomics,+bulk-memory`, `-Z build-std`, `needs-rust-src`; current nightlies need the linker flags spelled out — `--shared-memory --import-memory` plus exports of `__wasm_init_tls`, `__tls_size`, `__tls_align`, `__tls_base`, `__heap_base` — else the memory isn't shared and posting it to the rayon workers fails with "#<Memory> could not be cloned"). Builds with their own toolchain/rustflags use `target/<name>/`.

- SIMD (`simd`): enable `simd128` per function with `#[target_feature(enable = "simd128")]`, not crate-wide rustflags — crate-wide, LLVM auto-vectorizes the scalar baseline too. SIMD exports are `cfg(target_arch = "wasm32")`; native tests cover the scalar kernels, the page checks both agree.
- `digits`: `src/weights.bin` (int8 MLP) is generated by `examples/train.rs` from MNIST and committed; the dataset is not (see `docs/INSTALL.md` §3.3).
- `mcp` (MCP console) is the one demo that needs a server: the MCP client runs in WASM (hand-written JSON, async exports, browser state in `thread_local!` + `RefCell` — never hold a borrow across `.await`), and `scripts/mcp_bridge.py` (enabled by `serve.py --mcp mcp.config.json`) only moves JSON-RPC to stdio / HTTP servers and wraps CLI tools as a virtual MCP server. The page listens on one merged event stream (`/api/mcp/events`, each event tagged `"server"` by the bridge's per-server hub, never taken from the message): browsers allow 6 HTTP/1.1 connections per host, so a stream per server blocked every request after the fifth server. Every `/api/mcp` request must pass loopback + Host + Origin + token-cookie checks (a request with proxy forwarding headers is never local), or — with a `remote` config section (`scripts/mcp_remote.py`) — be for a listed domain with a session from `POST /api/mcp/login` (password in `WEBTT_MCP_PASSWORD`; restricted shells need `"remote": true`). With `remote` on, localhost is not trusted either (a proxy rewriting Host to localhost would look local) unless `"trust_localhost": true`; serve.py sends `X-Frame-Options: DENY` and a 60 s socket timeout; CLI tools run without a shell. The restricted shell (`scripts/mcp_shell.py`, config kind `shell`) is the only way the page names a program: shlex-split, no shell, argv[0] must be the very file an `allow` entry resolves to, and each command is confirmed on `/dev/tty` (fails closed without a terminal; prompt text escapes control and bidi characters). Demo stdio servers: `scripts/mcp_demo_server.py` and its twins in Rust (`crates/tools/mcp/examples/demo_server.rs`, hence `mcp` is `cdylib` + `rlib`), Go (`examples/mcp-servers/go/`) and Zig (`examples/mcp-servers/zig/`, Zig 0.17 `std.Io` API), all std-only; keep their answers identical — `DemoTwinChecks` compares each built binary with the Python one (build commands in `docs/INSTALL.md` §3.2). Tests: `python3 -m unittest scripts/test_mcp_bridge.py`; `mcp.config.json` is gitignored.

### Frontend

```
www/index.html          ← Navigation home (static, no WASM)
www/style.css           ← Shared styles: base, nav, controls, components used by 2+ pages
www/explain.css         ← 原理说明 panel (every page)
www/home.css            ← Home page card grid
www/<demo>/index.html + index.js [+ style.css for page-only rules]
```

Conventions — page structure:
- No bundler/framework — vanilla JS + ES modules; `index.js` imports `../../pkg/<demo>/index.js` and calls `await init()` once
- `index.html` links `../style.css`, then `../explain.css`, then its own `style.css` (only if the page has page-specific rules)
- CSS placement rule: a rule used by exactly one page goes in `www/<demo>/style.css`; used by 2+ pages goes in `www/style.css`
- Top nav is generated by `scripts/gen_nav.py` (tests: `scripts/test_gen_nav.py`) and is one 44px bar: home, the current category and its demos (below 900px just the current page name), and a 全部示例 `<details>` panel with every demo by category; the current page is marked `aria-current="page"`. It also gives every `<body>` the `has-nav` class. `www/nav.js` only adds Escape-to-close; the panel works without script. `backdrop-filter` sits on `.top-nav::before`, not the nav itself, else the nav becomes the containing block of the fixed panel and its backdrop
- Each page ends with a `<details class="explain">` panel generated by `python3 scripts/gen_explain.py <demo>` from `scripts/explain/<demo>.json`
- Performance timing via `performance.now()` → `#perf-info`; escape user strings before `innerHTML`

Conventions — mobile / H5:
- Hover styles inside `@media (hover: hover)`; touch-only sizing inside `@media (pointer: coarse)` (16px form controls, 44px targets); pages use `viewport-fit=cover` + `env(safe-area-inset-*)`
- Fixed-resolution canvases shrink on narrow screens: map pointer coords with `canvas.width / rect.width`
- Canvas pages that drag must set `touch-action: none` or `preventDefault()` in non-passive touch handlers; support pinch where the desktop uses the wheel
- Heavy per-frame renders (mandelbulb, terrain) draw a half-resolution preview while interacting and a full frame once input pauses
- Don't assume `OffscreenCanvas` (iOS < 16.4): fall back to `document.createElement('canvas')`
- Dev server: `python3 scripts/serve.py` (Cache-Control: no-cache, avoids stale `pkg/` modules after a rebuild; COOP/COEP make pages cross-origin isolated for `SharedArrayBuffer`). All resources must stay same-origin, or COEP blocks them

Conventions — workers, threads, audio:
- rayon (`pathtracer-mt`) blocks the calling thread, which browsers forbid on the main thread: the wasm runs in a module Worker (`www/pathtracer/worker.js`) that calls `initThreadPool(n)`; the page offers threads only if `crossOriginIsolated` and `pkg/pathtracer-mt/` exists, else loads the single-threaded build in the same worker
- Feature-detect before loading a module that needs a newer wasm feature (SIMD: `WebAssembly.validate` of a tiny probe module), since an unsupported module fails to compile at all
- AudioWorklet (`synth`): the main thread fetches the bare `.wasm` bytes and passes them via `processorOptions`; the processor instantiates them and reads samples straight from wasm memory each 128-frame quantum

### Rendering backends

| Backend | Demos | JS API used |
|---|---|---|
| Canvas2D | mandelbrot, image-filter, game-of-life, particles, video-filter, pathfind, fluid, spectrum, terrain, rasterizer, pathtracer (from a Worker), qrcode, falling-sand, gomoku, digits, chip8, seam-carving, wfc, cloth, delaunay, jpeg | `ImageData` + `putImageData()`, or Canvas paths |
| WebGL | mandelbulb | WebGL2 texture upload + shader program |
| WebGPU | webgpu | WGSL compute shader + render pipeline |
| DOM | data-table, benchmark, sha256, simd, compress, markdown, sudoku | Virtual scrolling / result tables via `innerHTML` (escape user strings; `markdown` output is escaped and URL-allowlisted by the renderer) |
| Web Audio | synth (AudioWorklet), spectrum (AnalyserNode), chip8 (beep oscillator) | `AudioWorkletNode` / `AnalyserNode` / `OscillatorNode` |

## Adding a new demo

1. `crates/<category>/<name>/` with `Cargo.toml` (copy a sibling's) and `src/lib.rs` — stateless or stateful pattern
2. The category glob in the root `Cargo.toml` picks it up (add the glob for a new category)
3. `python3 scripts/new_demo.py <category> <name> --name … --icon … --title … --subtitle … --help-html … --card-desc … --tag … [--body body.html] [--css]` creates `www/<name>/index.html` (+ `style.css`), adds the demo to `CATEGORIES` in `scripts/gen_nav.py`, adds its home card and regenerates every page's nav (tests: `python3 -m unittest scripts/test_new_demo.py`)
4. Write `www/<name>/index.js` (and page-only CSS in `www/<name>/style.css`; shared rules in `www/style.css`)
5. Features exported with `JsError` can't be unit-tested natively (constructing one panics off-wasm): validate in an inner function returning `Result<_, String>` and test that
6. `scripts/explain/<name>.json` + placeholder `<details class="explain"></details>` → `python3 scripts/gen_explain.py <name>`
7. `cargo test -p <name> -- --test-threads=1 && python3 scripts/build.py <name>`

<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **webtt** (430 symbols, 845 relationships, 36 execution flows).

> Index stale? Run `node .gitnexus/run.cjs analyze --index-only` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? Bootstrap with `npx`, `bunx`, or `pnpm dlx` — e.g. `bunx gitnexus@latest analyze` (npm 11 npx crash; #1939).

## Always Do

- **MUST run impact before editing.** Use `impact({target: "symbolName", direction: "upstream"})` or `node .gitnexus/run.cjs impact "symbolName" --direction upstream --repo .`; report callers, processes, and risk. Never substitute grep for graph analysis.
- **MUST analyze graph changes before committing.** Use `detect_changes({scope: "all"})` (MCP) or `node .gitnexus/run.cjs detect-changes --scope all --repo .` (CLI fallback). `partial: true` or `truncated: true` is not a clean check — a zero means unseen, not unaffected; re-run it. For regression review: `detect_changes({scope: "compare", base_ref: "main"})` or `node .gitnexus/run.cjs detect-changes --scope compare --base-ref "main" --repo .`.
- MUST warn on HIGH/CRITICAL `risk` pre-edit; never use `riskSharedAxes` to waive a HIGH/CRITICAL `risk` warning. Compare File/symbol: MCP File omits axes; Graph-RAG expands File.
- **MUST treat `risk: UNKNOWN` as unresolved, not as low.** An empty caller set is not evidence the symbol is unused — it can also mean the callers are not resolvable by the index (plain-object property access, dynamic dispatch, cross-language calls). `impact` pairs `UNKNOWN` with a `riskNote` saying so. Confirm with a text search before treating the symbol as safe to change or delete; do not proceed on the strength of a zero.
- **MUST use `query({search_query: "concept"})` for concepts/flows, `context({name: "symbolName"})` for a named symbol, or `impact` for blast radius, on read-only callers, dependencies, imports, or execution flow.** Graph first; text search only for empty/`UNKNOWN`/literals.
- For security review, `explain({target: "fileOrSymbol"})` lists taint findings (source→sink flows; needs `analyze --pdg`).

## Never Do

- NEVER edit a function, class, or method before MCP/CLI impact analysis.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis, and never read `UNKNOWN` as an all-clear — it means the walk could not answer, which is the one verdict that requires confirming by other means.
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
- NEVER commit before MCP/CLI graph change analysis.

## Resources

| Resource | Use for |
| --- | --- |
| `gitnexus://repo/webtt/context` | Codebase overview, check index freshness |
| `gitnexus://repo/webtt/clusters` | All functional areas |
| `gitnexus://repo/webtt/processes` | All execution flows |
| `gitnexus://repo/webtt/process/{name}` | Step-by-step execution trace |

## CLI

| Task | Read this skill file |
| --- | --- |
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` |

<!-- gitnexus:end -->
