Four items from one workflow, committed together because their build and CI
wiring genuinely interleaves — nisps/CMakeLists.txt, run-all-tests.sh and
ci.yml each carry hunks from two of them, and the stage renumbering (1/5 ->
1/6) touches every line. Splitting would produce commits that do not build,
which is worse than a commit that does four things and says so.
S26 part 2 — the curve declaration now matches reality. params[].curve stays
the mode-wide DEFAULT; a voice_spaces entry may now be {name, curve_overrides}
declaring only the slots where THAT voice space deviates. The 6 modes with one
voice space are byte-identical. The values were derived MECHANICALLY by a new
codegen/curve-audit.ts that models the four idioms a p[N]*p[N] regex misses
(alias form, memlcelium's implicit-counter sq() lambda, loop-generated indices,
smooth_params_), inlines helpers, and RAISES rather than guessing when it
cannot reduce an expression. A drift gate cross-checks 1179 (voice space x
param) slots against engine source on every run and was proved to fail loudly
on three drift classes. Application stays in the engine: nisps/engines,
nisps/pipeline and nisps/core are untouched, generated output is pure insertion
(755 insertions, 0 deletions), and the rebuilt nisps.wasm was byte-identical.
S4 / 7.2 — firmware reads the active mode's driver config at mode start, and
mic/line is real. My brief assumed the engine owns this; the code disagreed and
the code was right. sound_analysis_midi's EngineT is NoOpEngine — the mic lives
on a separately-composed AnalysisEngine member — so engine-level wiring would
have compiled, passed every gate, and left the one mic mode on line input.
Hence a mode-level seam defaulting to engine().driver_config(). Separately,
DriverConfig's defaults (line_level 0, output_volume 1.0) had drifted from
memllib's actual 3/0.8 because nothing had ever read them; wiring them as-is
would have made every silent mode louder and its line input maximally
insensitive — a behaviour change disguised as plumbing. Now pinned by a test.
Also: GetSysClockSpeed() panic()s on unsupported sample rates and runs on the
first line of setup(), so sample_rate needed a fallback ahead of clock setup.
CI's firmware env list gains soundanalysismidi — it is the only mic variant and
nothing else compiles that path.
Plan 5e — telemetry is real. A loss_history C-API entry across the full 5-layer
chain lets the browser read the per-iteration loss the core already records.
The audit named one fabrication site; there were two — wasm-iml.ts's
synchronous train() published lossHistory: [loss] as well. A third, ctx.loss,
was not merely dead but actively synthetic (fallbacks of prev * 0.82 and a
literal 0.5, rendered by nothing) and is deleted. The firmware buffer stays
untouched, per the L25 call. EngineApi.lossHistory() reads spine state rather
than the MLP handle, because trainAsync() fits on the worker's mirror net and
the handle would give a subtly-wrong second answer.
Plan 5f — engine throughput is measurable. One source compiled twice (CMake
natively, emcc for WASM) so the targets compare directly and no WASM export is
added. Sequencers are driven into a working state, and every row prints its own
working-state evidence so a number produced by an idle engine is visible rather
than plausible. Reports, never asserts: a wall-clock threshold on shared
hardware is meaningless or flaky, same call as the firmware size job.
ALIGNMENT: the telemetry defect is deleted (built, not deferred); the
performance defect is rewritten to what is actually left — these are HOST
numbers, and nothing measures the RP2350 at 150 MHz, which is the target the
mission's constraint is about. Q4 (memllib ownership) and Q5 (legacy feedback
modes) are closed.
Corrections to my own earlier claims, both found by agents contradicting the
brief: manifold/ONBOARDING.md was NOT "now accurate" — its primitives list
still named five deleted primitives and cited a seededGradient() that does not
exist. And the parity harness misses the sequencer engines because it runs 128
frames while their sequencers evaluate every 400-500 samples, NOT because
all-params-0.5 fails to trigger them (it does trigger: 0.5 maps to ratio 2,
firing three times per bar). The fix is a longer window, not different params.
Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS, lint clean, curve
drift 1179 slots ok, 39 e2e (was 33). Firmware: 5 envs built including the mic
variant.
26 KiB
Manifold UI — Agent Onboarding
Purpose of this doc: everything an agent needs to make a tweak or fix to the Manifold front-end without re-grepping the tree. Read this + the relevant section's source files; you should not need to read the 40KB design specs for routine work (they're pointers at the end).
Voice: ground-truth map. If something here contradicts the code, the code wins — fix this doc in the same commit.
1. What Manifold is
A convertible-mode React front-end on the real, parity-tested NISPS ML+audio engine (same
nisps/ C++ core that the firmware and the playground/ use, compiled to WASM). It lets you drive
a neural net's mapping from a small input space (XY pad / gamepad / MIDI) to many outputs, shape
that mapping with interactive ML feedback ("explore & place" / dislike), and route the outputs to
several backends (built-in synth, MIDI, OSC, particles, VCV, MEMLNaut hardware).
- Stack: Vite + React 18 + TypeScript (strict). No CSS-in-JS — design-token CSS variables.
- Lives at:
manifold/(besideplayground/). Deploys to https://meml.lnfinitemonkeys.org/next/. - Synth is labelled "Powerful Synth Engine" / "Built-in Synth" — NEVER "C15". British spelling.
2. Run / build / deploy / test
cd manifold
bun install
bun run dev # Vite dev server, port 5273 (COOP/COEP headers set)
bun run typecheck # tsc --noEmit ← run before every commit
bun run build # tsc --noEmit && vite build → dist/
bun run preview # serve dist/ on :4273 (honours COOP/COEP — needed by WASM/worklet)
bun run test:e2e # Playwright smoke (needs `bun run build` first; runs against preview)
- Typecheck is the cheap gate. It catches most regressions; run it after any edit.
- E2E on the VPS needs a non-snap node runner (bun is snap-confined and hides libs from
Chromium):
PLAYWRIGHT_BROWSERS_PATH=/home/w1n5t0n/snap/bun-js/87/.cache/ms-playwright node node_modules/.bin/playwright test. Preview via bun is fine. The smoke spec (tests/e2e/smoke.spec.ts) asserts: engine WASM loads, spine invariant (setInputs → outputs change), feedback runs, console renders, no "C15" in the bundle, no console errors. - Deploy is automatic on push to GitHub
main, but gated on CI → webhook → waits for theCIworkflow to concludesuccesson that exact SHA → buildsmanifold/→ rsyncs to the live/next/subdir. A red or missing CI run aborts the deploy (fail-closed, 20 min timeout);MEML_SKIP_CI_GATE=1bypasses it for an emergency hand-deploy. The gate lives VPS-side in~/.config/webhooks/meml-deploy.sh(not in this repo) — added 2026-07-21 per the simplification audit. See themanifold-deploy-pipelinememory for the full chain and gotchas (thecp index.html a-immersive.html403 workaround; git-ignoredbun.lock). manifold/public/nisps.{js,wasm}are tracked artifacts that ship to production — the webhook builds onlymanifold/, so vite copies whatever is committed. CI's WASM freshness gate runs the parity harness against the committed artifact before rebuilding it, so a stale commit fails loudly. Rebuild withscripts/build-wasm.shand commit it whenevernisps/changes.?debug=1installswindow.__nisps(synchronous probe for Playwright/console — seesrc/debug/probe.ts).base: './'invite.config.tskeeps asset URLs relative so onedist/mounts at both/and/next/. WASM URLs must resolve viadocument.baseURI, never hardcoded (see the assetUrl gotcha in §6).
3. The big picture — three layers
┌─────────────────────────────────────────────────────────────────┐
│ UI layer (React) src/console/ src/dock/ src/primitives/
│ ConsoleApp = spine of the UI: holds state, picks a Stage, │
│ renders the Dock. "Convertible" = swappable Stages. │
└───────────────┬─────────────────────────────────────────────────-┘
│ reads engine.version (useSyncExternalStore), reads buffers imperatively
│ writes via engine.setInput / setParam / feedback.*
┌───────────────▼─────────────────────────────────────────────────┐
│ Engine layer (NO React) src/engine/ src/inputs/ src/feedback/ src/backends/
│ Spine = reactive store below React. Per-frame ML inference is │
│ eager + synchronous, OFF the render cycle. React only watches │
│ a monotonic version counter. │
└───────────────┬─────────────────────────────────────────────────┘
│ C ABI
┌───────────────▼─────────────────────────────────────────────────┐
│ WASM (nisps.wasm) — built from the C++ nisps/ core │
│ Loaded TWICE: main thread (ML inference + training + feedback) │
│ and AudioWorklet (audio DSP, separate instance). │
└──────────────────────────────────────────────────────────────────┘
Golden rule of the reactivity model: components subscribe to engine.version() via
useEngineVersion() and then read live Float32Arrays imperatively (engine.getOutputs(),
engine.routedOutput()). They do not get outputs as React state — that was a deliberate perf
decision. Buffers are reused frame-to-frame; never assume a fresh array.
4. UI layer (where most tweaks happen)
Entry & composition
src/App.tsx→ mounts<EngineProvider>(async WASM load, showsLoading) →<ConsoleApp focus="composite" />. Installs the?debug=1probe once the engine is live.src/console/ConsoleApp.tsx(~1040 lines — the UI spine). Holds nearly all UI state:focus,modeId,params(MFParam[]),pos(2D input), feedback state (feedbackMode/soloMode/exploring/picking/anchorCount), output config (outputMode/midiOutputId/oscUrl/vcvUrl), and UI flags (sandwich,split,stripPinned,snapshots,markers,health,rev). Builds the flatConsoleCtxpassed to the Dock + drawers.
The Stages (one renders at a time)
There is no focus axis any more. The focus/altitude system (AltitudeNav, SplitStage,
ReadoutStrip, InputMini, CompactAxis) was deleted in the 2026-07 simplification audit —
Manifold ships a single "composite" altitude. Selection is now a plain three-way in ConsoleApp:
sandwich wins, else outputMode==='particles', else CompositeStage.
| Stage | File | Renders when | What it is |
|---|---|---|---|
| CompositeStage | CompositeStage.tsx |
default / hero | Draggable split-ratio; magnet-snaps to 0.14/0.33/0.5/0.66/0.86; collapses a side to a corner minimap at extremes. |
| SandwichStage | SandwichStage.tsx |
sandwich===true (wins over the others) |
Three-pane layout: Manifold input surface left, 3D parameter-landscape centre (input → MLP heatmap grid → outputs, drag to orbit), compact OutputStage right. |
| ParticleStage | ParticleStage.tsx |
outputMode==='particles' |
Flow-field visualiser (flow-field.ts, 400-particle Canvas2D port) + macro-axis bar + corner joystick. |
Manifold.tsx and OutputStage.tsx are no longer top-level stages — they are panes composed by
CompositeStage/SandwichStage. Manifold.tsx is the full-bleed 2D input surface (canvas trail + pins
- feedback markers; pointer →
onMove; double-click the input mark → follow-mouse mode, a windowpointermovelistener mapping the whole viewport onto this surface's space, Esc or a second double-click exits).OutputStage.tsxis the output columns; drag a bar to set value, and it takes acompactprop for the narrow pane.
- Output modes (the TOP dock selector, NOT the same axis as
focus):src/console/output-mode.tsdefinesOUTPUT_MODES= particles (default) / midi / osc / synth / editor, each mapping to aBackendId.DEFAULT_OUTPUT_MODE='particles'. src/console/output-mode.ts,types.ts,model.tsare the shared vocabulary — read these first when touching anything cross-cutting:types.ts:Focus,OutputMode,DrawerKey,DrawerDepth,FeedbackModeUI,SoloMode,Pin,FeedbackMarker,Snapshot, andConsoleCtx(the flat context handed to the dock).model.ts: the instrument catalogueMF_MODES,MFParam,ParamStatus(off|fixed|live),ParamGroup, plusshapeValues()(applies min/max/curve to raw engine outputs) andmodeEngineId(). (seededGradient()is GONE — it was the fabricated gradient-health source, deleted in the 2026-07 sweep, S16.) Schema-backed modes are DERIVED from the codegen schemas insrc/modes/generated/(one-core-engine P5.2) — real param names/groups/count, plus each mode'smlnet shape (MFMode.ml) and schemaengine_id(MFMode.engineId) come from schema truth. A thin manifold OVERLAY (SCHEMA_MODESin model.ts) supplies only label/glyph/ModeClass/input/ordering. Two schema-less manifold-only modes (visualizer,c15placeholder) stay hand-written onDEFAULT_MODE_ML. Do NOT hand-editsrc/modes/generated/— it is codegen output (bun run codegen/generate.ts).
The Dock (right-edge rail) — src/console/Dock.tsx + Drawers.tsx
- 48px right rail: TOP = mode selector (the 5 output modes, popover); MIDDLE = 5 drawer icons, vertically centred macOS-dock style; BOTTOM = sandwich toggle.
- Five drawers (
DRAWERSinDrawers.tsx, each has.render(ctx, depth)— condensed 360px panel vs expanded 80vw×80vh modal):- learn — feedback mode (explore-and-place / geometric-dislike) + solo mode + per-output arm.
At
expandeddepth ONLY it also rendersTrainingHealth.tsx: the real per-iteration loss curve (EngineApi.lossHistory()←nisps_ml_loss_history←MLPCore::loss_history) plus the per-layer weight-health table (getLayerStats).depth === 'expanded'is Manifold's advanced-surface flag — there is no separate feature-flag mechanism, so put advanced surface there rather than inventing one. The panel renders "no training run yet" when the core has no history; it never synthesises a curve. - inputs — enable/configure input sources (XY pad / MIDI / gamepad).
- route (label "Outputs") — per-output control matrix + per-backend config.
- settings — icon style (monochrome/colour), input-map shape (xy/joystick/rect/circular), corner radius.
- help — keymap pills + loop explanation.
- learn — feedback mode (explore-and-place / geometric-dislike) + solo mode + per-output arm.
At
src/dock/holds the output-routing internals used by theroutedrawer:output-state.ts— the per-output control model:OutputControl(state/muted/armed/min/max/ curve/fixedValue + backend specsMidiCcSpec/OscSpec/VcvSpec),toOutputControl(),buildArmMask()(solo focus).OutputControlRow.tsx— one output row: name · M(mute) · S(solo/arm) · off|fixed|live · dual-range · curve pad · live value. Writes eagerly to the sharedMFParamstore viaonChange.OutputsBackendConfig.tsx— preset bar (save/restore/rename/delete) + per-backend config (MIDI CC#/channel, OSC path/range, VCV polarity).
Primitives — src/primitives/ (barrel: index.ts)
Seven: Button, Slider, PillToggle, Badge, Switch, XYPad, VirtualJoystick. Dumb,
reusable, no engine knowledge. Side-effect import of styles/primitives.css styles the range
inputs. Panel/StatusLine/ControlAxis/CurvePlot/Sparkline were deleted in the 2026-07
sweep (L22, zero consumers) — don't cite them.
Other shared UI files
shared-ui.tsx—MiniMeters(read-only output bars).AltitudeNav/CompactAxiswere deleted with the focus system.icons.tsx— monochrome inline-SVG icons (mode icons + drawer icons +GLYPH_FALLBACKfor when monochrome is off).VerdictCluster.tsx— floating bottom-centre feedback UI (perturb ▽ / undo ↺ / commit △ + A/B); labels adapt to feedback mode.CurvePad.tsx— square canvas curve editor (vertical drag reshapes [0,1]; ~0.43 ≈ linear). Used in OutputEditor + OutputControlRow.OutputEditor.tsx— inline min/max/curve popup for a single output (hover/click on a bar).
Styling — src/styles/
CSS-variable design tokens, no CSS-in-JS. tokens.css @imports tokens/{base,colors,fonts, typography,spacing,effects}.css. All vars on :root, consumed via var(--name). Dark theme: deep
black bg, warm-orange --accent (#ff6a00), cyan --accent-2 (#00ccff). Corner radius is driven by
a setting → --r-* tokens.
5. Engine / inputs / feedback / backends (touch when behaviour, not chrome, changes)
Engine — src/engine/ (no React except the two binding files)
spine.ts— the reactive store.setInput(x,y)/setInputs(arr)drive raw input synchronously throughWasmIML.processInput(WASM input chain) →processInto()→WasmIML.processOutput(WASM output chain) → backend, all off the render cycle, reusing buffers. Bumps a monotonicversion.reprocess()re-ticks the last input after a weight change (stores the full N-D vector so extra axes survive). Pipeline config lives ininputConfig_/outputConfig_and is pushed C-side viasetInputConfig/setOutputConfig(state itself lives in the WASM pipeline handle since P4).engine-api.ts—EngineApi, the framework-neutral facade everything in the UI talks to:setInput/setInputs,getOutputs/routedOutput, training (addExample/train/trainAsync/evalLoss), weights (getWeights/setWeights/process/randomise), telemetry (lossHistory/getLayerStats),subscribe/version/on, plus nested.feedbackand.audiofacades.lossHistory()reads SPINE STATE, not the MLP handle — an async train runs on the worker's mirror net, so the main handle's own history is empty for those runs; both paths publish to the spine.EngineProvider.tsx/useEngine.ts— the only React coupling.useEngine()returns the API (null until WASM ready);useEngineVersion()=useSyncExternalStore(subscribe, version).engine-host.ts— main-thread audio wiring: AudioContext (user-gesture gated), fetchnisps.wasm, register + feed the worklet.wasm-iml.ts(~750 lines) — the ML interface tonisps.wasm: one MLP handle, dataset, heap buffers, feedback C-ABI bindings (nisps_ml_feedback_*), lazy training worker.wasm-worker.ts— off-thread training worker; returns weights, final loss, and the real per-iteration loss curve read off its own mirror handle.worklet/nisps-processor.ts— the AudioWorklet's separate WASM instance (rawWebAssembly.instantiate, no Emscripten glue; 128-sample blocks).- Input/output pipelines + curves live in the C++/WASM core (one-core-engine P4). The input chain
(invert → deadzone → circular clamp → momentum-modulated zoom → centred power → EMA → momentum) and
output chain (global curve → per-output EMA → slew → freeze/mask) are
nisps/pipeline/*, exposed vianisps_input_*/nisps_output_*and driven by thinWasmIMLwrappers (processInput,processOutput,setInputConfig,setOutputConfig,setOutputFreezeMask,reset*). State lives C++-side per pipeline handle. First 2 axes get the full pad pipeline; axes 2+ feed raw to the spine. The old TSinput-pipeline.ts/output-pipeline.ts/curves.tsare deleted; config TYPES arepipeline-types.ts, the curve NAME↔id contract iscurve-catalog.ts, and the curve MATHS is sampled from the core viaEngineApi.curveApply/curveApplyBatch. dataset.ts— JS-side example store + sample-weight modes (uniform/recency/spatial/combined).sink.ts—EngineSinkframework boundary.types.ts— C-ABI surface types.exploration.ts—ExplorationControlleradapter for the Jolt press + OU explore gestures (Learning drawer). As of one-core-engine P3 the maths lives in the shared C++/WASM core — this class is a thin driver that owns only the control-rate timers and callsengine.explore.*(joltPress/joltStep/joltRelease/joltActive→nisps_ml_jolt_*;setExploreIntensity/exploreApply→nisps_ml_explore_*). The held ~200 Hz driver callsjoltStep()+process(); the OU walk is the spine's inert-by-defaultsetOutputMorphhook, nowexploreApply(routed)(copy into the WASM heap → advance+add in the core → copy back). The interimjolt.ts/ou-explore.tsTS ports were deleted with the swap.
Inputs — src/inputs/
input-layer.ts— composition hub. One rAF loop polls sources, pulls all axes into a vector, forwards N→engine.MAX_AXES = 32(WASM net over-provisioned to 32 inputs). Dedicated dimensions, NO mean-blending — each active axis drives its own engine slot 1:1; unused slots zero-padded (inert). Changing axis count requires a net reset (UI confirm modal).base-source.ts+ sources:xy-pad-source.ts(push, 2 axes),gamepad-source.ts(single=2 / double=4 axes, deadzone 0.08),midi-input-source.ts(Web MIDI, batch CC-learn, multi-port).useInputLayer.ts— React binding; manages exclusive input mode + gamepad stick mode + MIDI device/learn map; exposespushPad,sources,channelLayout, etc.- Reshape (P2.3, live): the net is now runtime-shaped. It boots at the default
over-provisioned 32-input head (zero-padding preserved), and
EngineApi.reshape({ inputSize, … })→WasmIML.reshapeswaps in a new net at the requested arity, warm-started from the overlapping weights (nisps_ml_reshape; C-side dataset + feedback state RESET). When the active axis layout CHANGES to a count ≠ the net's arity,ConsoleAppoffers the swap behindReshapeModal.tsx(reset-on-reshape confirm; declining keeps the zero-padded head). Never offered on load. The spine tolerates the arity change (buffers resize, version bumps); the training worker (wasm-worker.ts) carries the current dims in its train message and re-creates its mirror net to match. Debug:window.__nisps.reshape(nIn)/.describe(). See themanifold-mixed-inputsmemory for the locked design (adaptive slider viz when >2 dims is still pending). - Per-mode net dims (P5.3): switching INSTRUMENT mode reshapes the net to that mode's schema
mlconfig (MFMode.ml— input/hidden/output + spread) via aConsoleAppeffect keyed on[engine, modeId]. No confirm modal (switching instrument is deliberate); the axis-countReshapeModalabove is for input-LAYOUT changes only. The effect depends onengine, so on boot it fires once WASM is ready and lands the boot mode's dims (paf_synth → 4→[10,10,14]→33, weights 809 — NOT the 32→126 default). The reshape-offer effect reads the engine's CURRENTinputSizelive, so a mode switch that changes arity doesn't spuriously prompt (its baseline tracks axis COUNT, unchanged by a pure dim change). Non-schema modes restoreDEFAULT_MODE_ML(32→126). Debug seam for tests: under?debug=1ConsoleApp installswindow.__mf(setMode/getModeId/paramCount/modeIds) — the UI-level analogue of__nisps, since no in-UI instrument picker exists yet (ctx.modes/setModeIdare plumbed but unrendered).
Feedback — src/feedback/
controller.ts—FeedbackController, framework-neutral, owned by ConsoleApp. As of one-core- engine P3 it holds NO algorithm approximation — a thin driver over the shared C++ core. Two modes: geometric-dislike (default, Mode 1, "Push away") —dislike()callsengine.feedback. dislikeGeometric(heardVec)(the k-NN centroid push-away innisps/ml/geo_push.hpp; returns FeedbackAction 14=push / 15=cold-start) thenprocess();like()runs the core'sthumbsUp(auto-stores the positive centroid in Avoid+Geometric) +addExample+train. explore-and- place (Mode 2, positive-only) drives the core's snapshot/scratchpad/undo lifecycle; caller accumulates anchors and trains on finalise with warm-start. Solo/arm via per-output mask.- The heard-vector rule: the geometric dislike trains AWAY from the HEARD (post-pipeline, routed)
output — pass
engine.routedOutput(), never the raw MLP output, or the cold-start MSE derivative is zero (inert). TheEngineApi.feedback.thumbsDownfacade + the Mode-1dislike()call site honour this. - Cold-start prompt: a dislike with zero positives returns action 15 → ConsoleApp shows a one-time "Like a few sounds first…" banner (dismissed on the next like or the dismiss button; rl-feedback §7).
- The interim
rng.ts(SeededRng) and theC++ GAPapproximation markers are gone — the seeded RNG, geometric push, jolt, and OU all run in the core now.
Backends — src/backends/
manager.ts—BackendManager, the single consumer of the engine spine for output: subscribes, readsroutedOutput()each version bump, callsactive.send(routed). Gates audio (mutes synth when a non-synth backend is active).- Registered:
midi(midi-backend.ts, CC out, ~20Hz throttle),osc(osc-client.ts→ WebSocket to the Deno bridge inosc-bridge/bridge.ts),synth/cvgate(passthrough-backend.ts),particles(particle-backend.ts, no-op — the visualiser is a separateflow-field.tsconsumer),vcv(vcv-backend.ts, bidirectional).mapping.ts,presets.ts,useBackendManager.tssupport config.backend.tsdefines theOutputBackendinterface +OutputMapping.
Misc
src/serial/memlnaut-serial.ts— STUB Web Serial scaffold for the MEMLNaut Editor mode (protocol TODO).EditorPanel.tsxis its UI.src/settings/settings-store.ts— localStorage settings (mf-settings): icon style, input-map shape, corner radius.src/midi-devices/— codegen'd external-synth device templates.src/debug/probe.ts—window.__nispssynchronous probe (engine/audio/bus). Some playground feature-store methods are present-but-inert (not ported yet) to keep the surface stable.
6. Gotchas & non-obvious rules
- Outputs are read imperatively, not via React state. Subscribe to
version, then callgetOutputs()/routedOutput(). Buffers are reused — copy if you need to retain. - Asset URLs must resolve against
document.baseURI, notlocation.origin.base: './'+ the/next/subpath mean a hardcoded/nisps.wasm404s. TheassetUrl()helper (inengine-host.ts,wasm-iml.ts,wasm-worker.ts) handles this — use it. nisps.jsis non-module Emscripten glue (no ES exports). Workers/worklet fetch + indirect-eval to install the globalcreateNispsModule; the worklet uses rawWebAssembly.instantiate.- WASM MLP is runtime-shaped (since one-core-engine P2).
nisps_ml_create(in, out, hidden[])honours its dims (non-positive/null → the default32→[10,14,18]→126head, so pre-P2 callers are bit-identical), andnisps_ml_reshapeswaps in a warm-started net at new dims. Weights = 3148 at the default shape; reshaping only the input arity shifts the first layer (e.g. →4 inputs = 2868). The firmware MLP stays compile-time templated — only the WASM/browser build is dynamic. - Curves + input/output pipelines are C++/WASM only (one-core-engine P4). No TS curve/pipeline
maths remains; the browser samples
nisps/core/math.hpp+nisps/pipeline/*via the WASM. The golden test (tests/pipeline-golden.test.ts) drives the WASM chains against the frozen fixtures. NOTE:exp/log/sigmoid/cubicdeliberately changed to the firmware-exact maths at P4 (seetests/fixtures/README.md);linear/square/sqrt/centered_powerare unchanged. The 3 momentum input configs carry a wide (1e-2) tolerance — proven-inherent f32 drift, documented in the test header. - COOP/COEP headers are mandatory for the WASM/worklet path — set in
vite.config.tsfor dev+preview, and at nginx server scope in prod (inherited by/next/). - Never emit "C15" in code or bundle — the smoke test fails on it. Synth is "Powerful Synth Engine"/"Built-in Synth".
- Two
MFParamwriters (stage controls AND dock rows) share one store — edits in either re-render both.
7. Deeper references (only when this doc isn't enough)
All in docs/specs/ (at the repo root, not under manifold/), with subdirectories:
plans/BUILD-PLAN.md— locked decisions + the 12-step build sequence + spec pointers (the resume anchor).engine-architecture.md— full engine/spine/WASM design.dock-spec.md— dock + drawers spec.inputs-spec.md— mixed-input design.backends-spec.md— backends.docs/adr/rl-feedback-design.md+plans/feedback-modes-port-spec.md(executed) +recon/findings-feedback-behaviour.md— feedback modes (Mode 1/Mode 2)._archive/aimmersive-clone-spec.md/recon/playground-2026.md— the a-immersive feature parity target (archived reference).src/backends/README.md— backend wiring notes.
Memories (auto-loaded): manifold-build (status + locked decisions), manifold-mixed-inputs
(N-D input design), manifold-deploy-pipeline (the auto-deploy chain), single-double-joystick-toggle.
Keep this in sync with the code — update it in the same commit as any change to UI structure, the engine spine, the dock/drawers, or the build/deploy commands.