chore(build): P0 plumbing — WASM build/parity retarget to manifold/public

- build-wasm.sh emits to manifold/public/ (transitional copy to
  playground/public/ until P1 retires the playground)
- parity-check.sh + parity_wasm.mjs read the manifold artifact
- fix stale MLP<2,...> arity in AGENT-REFERENCE.md + nisps/wasm/README.md
- gitignore .claude/worktrees/
- plan one-core-engine-refactor.md: P0 marked landed

Gate: run-all-tests green; parity PASS from manifold artifact (2.4e-7);
manifold builds against freshly-built nisps.wasm.
This commit is contained in:
monkey-w1n5t0n 2026-07-13 23:14:23 +02:00
parent c936bf75c9
commit 29dc88be3a
10 changed files with 43 additions and 28 deletions

3
.gitignore vendored
View file

@ -50,3 +50,6 @@ node_modules/
bun.lock
playwright-report/
test-results/
# Agent worktrees
.claude/worktrees/

4
MAP.md
View file

@ -40,7 +40,7 @@ MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 cod
- `playground/src/input/pipeline.ts`, `playground/src/output/pipeline.ts`, `playground/src/output/curves.ts` — pure-fn pipelines (deadzone→zoom→curve→smoothing→momentum, then global curve→smoothing→slew→freeze).
- `playground/src/features/` — additional feature modules (heatmap sampling, snapshot stack, A/B compare, region pin, param pin, trail, weight health, etc.).
- `playground/src/debug/probe.ts` — synchronous `window.__nisps` debug probe for Playwright.
- `playground/public/nisps.{wasm,js}`, `c15.wasm`, `c15-glue.js`compiled WASM artifacts (built by `scripts/build-wasm.sh`).
- `playground/public/nisps.{wasm,js}`, `c15.wasm`, `c15-glue.js`WASM artifacts. `nisps.*` are a transitional copy from `scripts/build-wasm.sh` (canonical output is `manifold/public/`; copy dies with playground at P1).
- `playground/tests/e2e/` — Playwright specs (`ml-engine`, `modes`, `persistence`, `ui-interactions`) + `helpers.ts`.
- `playground/playwright.config.ts` — Vite preview server setup.
@ -113,7 +113,7 @@ the "BUILD DELTAS" block at the top of `docs/specs/vcv-module.md`). `src/MEMLNau
### `scripts/` — build + verify entry points
- `build-firmware.sh`, `flash-firmware.sh`, `build-and-flash-firmware.sh`, `firmware-common.sh` — Arduino-CLI wrapper for RP2350 target with C++20 flag.
- `build-wasm.sh` — Emscripten compile producing `playground/public/nisps.{wasm,js}`.
- `build-wasm.sh` — Emscripten compile producing `manifold/public/nisps.{wasm,js}` (+ transitional copy to `playground/public/` until P1).
- `build-cpp-tests.sh` — CMake configure + build + ctest (Ninja).
- `parity-check.sh` — runs native + WASM and diffs binary outputs.
- `lint-cpp.sh``.f` literal warn + heap/`Arduino.h` violation fail.

View file

@ -124,9 +124,9 @@ Two WASM instances at runtime:
1. **Main thread** (`playground/src/ml/wasm-iml.ts`): ML inference + sync training + RL primitives. Update store after each call. Async training via disposable Web Worker (`wasm-worker.ts`).
2. **AudioWorklet** (`playground/src/audio/worklet/nisps-processor.ts`): runs engine `process_block` per audio block. Loads `nisps.wasm` directly via `WebAssembly.compile` (no Emscripten glue in worklet). Bytes posted from main thread.
C API is in `nisps/wasm/bindings.cpp`. Build: `bash scripts/build-wasm.sh` (~94KB output to `playground/public/`).
C API is in `nisps/wasm/bindings.cpp`. Build: `bash scripts/build-wasm.sh` (~94KB output to `manifold/public/`, transitional copy to `playground/public/` until P1).
The WASM target is fixed at `MLP<2, 10, 14, 18, 126>`. Modes with smaller `output_size` use the first N outputs only.
The WASM target is fixed at `MLP<32u, 10u, 14u, 18u, 126u>` (`nisps_ml_create` ignores requested dims — see `docs/specs/plans/one-core-engine-refactor.md` P2). Modes with smaller `input_size`/`output_size` use a slice.
### Known limitations

View file

@ -74,13 +74,13 @@ skin) still applies inside manifold; this plan adds the vertical one.
Each phase ends green on its test gate and is independently landable. File phases as **ergo** tasks
(one per phase, sub-tasks per bullet); do not start a phase before its predecessor's gate is green.
### P0 — Plumbing hygiene (hours, no behaviour change)
### P0 — Plumbing hygiene (hours, no behaviour change) — ✅ landed 2026-07-13
- `scripts/build-wasm.sh` emits to `manifold/public/` (keep `playground/public/` copy only until P1 lands).
- `scripts/parity-check.sh` reads the manifold artifact.
- Fix stale doc: `MAP.md` §WASM still says `MLP<2,…>`; code is `MLP<32u,10u,14u,18u,126u>`.
- Delete `ml-debug.log`, `graphify-out/` from tree root; gitignore `.claude/worktrees/`.
- **Gate:** `run-all-tests.sh` green; manifold builds against a freshly-built (not copied) `nisps.wasm`.
- `scripts/build-wasm.sh` emits to `manifold/public/` (keep `playground/public/` copy only until P1 lands).
- `scripts/parity-check.sh` (and `tests/cpp/parity_wasm.mjs`) read the manifold artifact.
- ✅ Fix stale doc: the `MLP<2,…>` line was in `docs/AGENT-REFERENCE.md` + `nisps/wasm/README.md` (MAP.md was already correct); both now say `MLP<32u,10u,14u,18u,126u>`.
- `ml-debug.log`/`graphify-out/` were already absent from the tree; `.claude/worktrees/` gitignored.
- **Gate met:** `run-all-tests.sh` green (Playwright leg via the BUILD-PLAN non-snap-node VPS runner); parity PASS reading `manifold/public/` (max delta 2.4e-7); manifold typecheck+build green against the freshly-built artifact.
### P1 — Retire playground, single TS home (≈1 day)

Binary file not shown.

View file

@ -1,11 +1,13 @@
# nisps/wasm
Emscripten target that exposes `nisps/ml` (MLP) and `nisps/engines` (audio
engines) to the SolidJS playground via a flat C ABI.
engines) to the browser apps via a flat C ABI.
This directory is a leaf — it does not export headers for inclusion by
other C++ code. The only artifact is `bindings.cpp` plus the build script
that turns it into `playground/public/nisps.{wasm,js}`.
that turns it into `manifold/public/nisps.{wasm,js}` (with a transitional
copy to `playground/public/` until P1 of
`docs/specs/plans/one-core-engine-refactor.md` retires the playground).
## Building
@ -18,11 +20,11 @@ Requires `emcc` (Emscripten). The script defaults to
Output:
- `playground/public/nisps.wasm` — the compiled module.
- `playground/public/nisps.js` — Emscripten glue (factory function
- `manifold/public/nisps.wasm` — the compiled module.
- `manifold/public/nisps.js` — Emscripten glue (factory function
`createNispsModule`, MODULARIZE=1).
Both files are committed (so the playground works from a fresh clone
Both files are committed (so the browser apps work from a fresh clone
without a C++ toolchain). Re-run `build-wasm.sh` after changes to
`nisps/{core,ml,engines,wasm}`.
@ -32,9 +34,9 @@ The MLP class template is parametrised on `(input_size, hidden1, hidden2,
hidden3, output_size)`. WASM cannot recompile templates at runtime, so
this build instantiates exactly ONE configuration:
nisps::ml::MLP<2, 10, 14, 18, 126>
nisps::ml::MLP<32, 10, 14, 18, 126>
That serves the playground use case (2-D joystick → up to 126 synth
That serves the browser use case (up to 32 input axes → up to 126 synth
parameters). `nisps_ml_create()` accepts caller-supplied dimensions for
forward compatibility but currently ignores them — see comment at the top
of `bindings.cpp`. The schemas in `schemas/modes/*.json` use up to

Binary file not shown.

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# scripts/build-wasm.sh — compile nisps/wasm/bindings.cpp via Emscripten and
# write the result to playground/public/.
# write the result to manifold/public/.
#
# Requires emcc. Defaults to /usr/lib/emscripten/emcc; override via the EMCC
# env var. Sample invocation:
@ -8,14 +8,17 @@
# EMCC=$(which emcc) scripts/build-wasm.sh
#
# Output:
# playground/public/nisps.js — Emscripten glue, MODULARIZE factory
# playground/public/nisps.wasm — the compiled module
# manifold/public/nisps.js — Emscripten glue, MODULARIZE factory
# manifold/public/nisps.wasm — the compiled module
#
# A copy is also written to playground/public/ until P1 of
# docs/specs/plans/one-core-engine-refactor.md retires the playground.
set -euo pipefail
EMCC="${EMCC:-$(command -v emcc || echo /usr/lib/emscripten/emcc)}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="$ROOT/playground/public"
OUT="$ROOT/manifold/public"
SRC="$ROOT/nisps/wasm/bindings.cpp"
if [[ ! -x "$EMCC" && ! -f "$EMCC" ]]; then
@ -82,3 +85,10 @@ set -x
echo "[build-wasm] wrote $OUT/nisps.js + $OUT/nisps.wasm"
ls -lh "$OUT/nisps.js" "$OUT/nisps.wasm"
# Transitional copy for the playground; removed when P1 deletes playground/.
PLAYGROUND_OUT="$ROOT/playground/public"
if [[ -d "$PLAYGROUND_OUT" ]]; then
command cp -f "$OUT/nisps.js" "$OUT/nisps.wasm" "$PLAYGROUND_OUT/"
echo "[build-wasm] copied artifacts to $PLAYGROUND_OUT (transitional, until P1)"
fi

View file

@ -4,7 +4,7 @@
#
# Prerequisites:
# 1. scripts/build-cpp-tests.sh has run (need nisps_parity_check binary).
# 2. scripts/build-wasm.sh has run (need playground/public/nisps.{js,wasm}).
# 2. scripts/build-wasm.sh has run (need manifold/public/nisps.{js,wasm}).
#
# This script can run either step on demand if the artifacts are missing.
# Skip auto-build with NISPS_PARITY_NO_BUILD=1.
@ -21,8 +21,8 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TESTS_DIR="$ROOT/tests/cpp"
BUILD_DIR="${NISPS_BUILD_DIR:-$ROOT/nisps/build}"
NATIVE_BIN="$BUILD_DIR/nisps_parity_check"
WASM_GLUE="$ROOT/playground/public/nisps.js"
WASM_MOD="$ROOT/playground/public/nisps.wasm"
WASM_GLUE="$ROOT/manifold/public/nisps.js"
WASM_MOD="$ROOT/manifold/public/nisps.wasm"
NATIVE_OUT="$TESTS_DIR/parity_native.bin"
WASM_OUT="$TESTS_DIR/parity_wasm.bin"
TOL="${NISPS_PARITY_TOL:-1e-5}"

View file

@ -4,7 +4,7 @@
* parity_check.cpp against the WASM build of nisps and writes a binary blob
* with identical layout. The shell wrapper compares the two blobs.
*
* The WASM module is loaded from playground/public/nisps.{js,wasm}
* The WASM module is loaded from manifold/public/nisps.{js,wasm}
* scripts/build-wasm.sh must have run first.
*
* Output blob format matches parity_check.cpp:
@ -48,7 +48,7 @@ const SYNTH_FRAMES = 128;
const PROBE_IDX = [0, 5, 19, 31, 73, 137, 251, 491, 999, 1583, 2401, 3289];
async function loadWasm() {
const wasmGluePath = resolve(repoRoot, 'playground', 'public', 'nisps.js');
const wasmGluePath = resolve(repoRoot, 'manifold', 'public', 'nisps.js');
try {
await access(wasmGluePath, fsConstants.R_OK);
} catch {
@ -59,7 +59,7 @@ async function loadWasm() {
// The Emscripten glue is generated with MODULARIZE=1, which writes
// var createNispsModule = (() => ...)();
// if (typeof exports==='object' && typeof module==='object') module.exports = ...;
// It lives in playground/public/, which is a sub-package with
// It lives in manifold/public/, which is a sub-package with
// "type":"module" in its parent package.json — so neither `require()` nor
// `import()` can extract the factory cleanly. We work around this by
// reading the file as text and evaluating it inside a thin shim that
@ -77,7 +77,7 @@ async function loadWasm() {
console.error('[parity_wasm] could not locate createNispsModule in glue');
process.exit(2);
}
const wasmBinaryPath = resolve(repoRoot, 'playground', 'public', 'nisps.wasm');
const wasmBinaryPath = resolve(repoRoot, 'manifold', 'public', 'nisps.wasm');
const wasmBinary = await readFile(wasmBinaryPath);
const Module = await factory({ wasmBinary });
return Module;