37 KiB
MAP
MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 codebase (nisps/) compiles to two targets: (1) Arduino/RP2350 firmware for the MEMLNaut hardware, (2) WASM in the Manifold React browser app that runs the same engines + ML through an AudioWorklet. (The former SolidJS playground was retired 2026-07-13 — branch archive/playground-solidjs, tag playground-solidjs-final; the browser-only C15 engine lives only there for now.) See CLAUDE.md for the long-form architecture narrative and ALIGNMENT.md for current strategic gaps.
Layout
nisps/ — platform-agnostic C++20 library (the only ML/DSP/engine code)
nisps/core/—perf.hpp(hot-path/inlining attrs),types.hpp,concepts.hpp(MLEngine,AudioEngine,Mode),ring_buffer.hpp(SPSC lock-free cross-core channel, replaces pico/util/queue),event_queue.hpp(single-threaded in-engine event FIFO — deliberately NOT RingBuffer, which is an atomics-based cross-thread channel),rng.hpp(xoshiro256+ deterministic),math.hpp(fast_sigmoid,Curveenum +apply_curve).nisps/ml/— the MLP core, written once against a storage policy (mlp.hppMLPCore<Storage>):storage.hpp(FixedStorage— template-sizedstd::array, zero heap;MLP<NIn,NH1,NH2,NH3,NOut>alias preserves the classic compile-time surface) anddynamic_storage.hpp(DynamicStorage— runtime dims, single arena alloc at construction;#errors on RP2350 builds, sole lint heap-allowlist entry). Fixed↔dynamic bit-parity enforced bytests/cpp/test_mlp_storage_parity.cpp. Files:mlp.hpp,activations.hpp,loss.hpp(MSE, no double-scaling),training.hpp(RMSProp + grad clipping —rmsprop_step()is a line-for-line port of upstream memlpLayer.h:239 ApplyAccumulatedGradients@ea777502; the per-weight running squared-gradient average is optimiser state held in the storage policies, NOT part ofweight_count()/get_weights(), andMLPCore::reset_optimizer_state()clears it.draw_weights()deliberately does not, matching upstreamDrawWeights),init.hpp(spread-aware uniform↔Xavier),rl.hpp(move_weightswith output pin mask + per-layer scaling + weight decay),jolt.hpp(Jolt— held continuous weight-morph over the flat weight buffer + post-release LR ramp; ported from upstream InterfaceRL),ou_noise.hpp(OUNoise<N>— Ornstein-Uhlenbeck exploration walk on the output vector; ported from upstream InterfaceRL),feedback.hpp(FeedbackControllerCore<FbStorage>— the "Down Action" state machine: Avoid (geometric push-away default / Diffuse legacy) / RandomiseOutputs / RandomiseMlp / ExploreAndPlace; geometric replay is advanced deterministically by caller-supplied elapsed time with configurable rate/lifetime; storage-policied like the MLP, own deterministic RNG, exposed vianisps_ml_feedback_*C API),replay.hpp(ReplayView— reward-tagged memory: dedup/deepen, k-NN positive centroid with deterministic tie-break, wall-clock age/lifetime + eviction),geo_push.hpp(push-away target computation, upstream InterfaceRL @e291192— re-based from0a541ccon 2026-07-25:kGeometricPushScale1.0,kNegLRBase1.5, and NO/(1+len)taper, so distance from the liked centroid does not shrink a "no". Cold start is the same push in a random direction, not a separate branch),warm_start.hpp(overlapping-weights copy for reshape),stats.hpp.generated/ml_defaults.hppis codegen output (do not edit):nisps::ml::generated::kMlTrainDefaults, the ONE learning-rate / max-iterations / min-error default shared by firmware, WASM and VCV (sourceschemas/ml_defaults.json);MLPCore::set_train_config()andnisps_ml_set_train_config()override it at runtime. It lives underml/rather thanmodes/generated/becausenisps/mlsits belownisps/modes— mlp.hpp must not include upward. Jolt + OU are inert by default and wired intoModeBase, so every mode exposesjolt_press/jolt_release,jolt_lr_scale, andset_explore_intensity.nisps/pipeline/— the control-rate input/output processing chains (P4):input_chain.hpp(InputChain— invert→deadzone→circular clamp→momentum-modulated zoom→centred power→EMA→momentum; caller-supplied dt, internal clock, fixed velocity ring, serialisable state) andoutput_chain.hpp(OutputChain<NMax>— curve→EMA→slew→freeze(+mask), capacity-templated). Behaviour contract = the retired manifold TS pipelines, pinned bymanifold/tests/fixtures/and parity stage 7.nisps/dsp/—biquad.hpp,delay.hpp,reverb.hpp,filter.hpp,env.hpp,osc.hpp,pitch_shift.hpp,dc_blocker.hpp, plus the sequencer primitives shared by the sequencer engines:ratio_seq.hppandseq_clock.hpp(bar phasor + MIDI clock + bpm). Lean primitives extracted from maximilian; daisysp PitchShifter replaced with custom granular impl.nisps/engines/— eight audio engines, each satisfyingAudioEngine:paf_synth.hpp,channel_strip.hpp,xiasri.hpp,verb_fx.hpp,memlcelium.hpp,breakor.hpp(sequencer, NoOp audio),elysiamorf.hpp(sequencer, NoOp audio),analysis.hpp(input-side spectral features). Plusbase.hpp(NoOpEngine, engine_id "thru").nisps/modes/— platform-agnostic modes binding{ML config, engine, voice space lambdas, abstract I/O channels}. Files:paf_synth.hpp,channel_strip.hpp,xiasri.hpp,verb_fx.hpp,memlcelium.hpp,slp_workshop.hpp(SLPWorkshopMode— the Synth Library Portland workshop build; reuses the MEMLCelium engine + MLP shape, foregrounds the Jolt + OU explore gestures),breakor.hpp,elysiamorf.hpp,sound_analysis_midi.hpp,external_synth_midi.hpp(ExternalSynthMIDIMode<const MidiDevice&, NOut>— joystick→MLP→MIDI CC for an external synth; compile-time device fromnisps/midi;consteval pick_cc_slotscurates which params fill the NOut slots; NoOpEngine,kRouteOutputsToEngine=false).base.hppprovides a CRTP scaffold eliminating the duplication that previously plagued firmware modes; it also ownsdriver_config()— the audio-driver setup (mic vs line, gain staging, sample rate) the platform glue reads at mode start. Defaults toengine().driver_config(); a mode overrides it with an optionalon_driver_config()hook only when its engine is not what consumes the audio input (sound_analysis_midi, whose analyser rather than its NoOp engine owns the mic).generated/contains codegen output (do not edit by hand): per-modek<Mode>SchemaParamSchema instances, the<Mode>MLPtype aliases built from the schema's own dims, andschema_types.hppwhich now owns theParamSchemastruct itself. Mode headers no longer hand-write either their schema aggregate or their net shape.nisps/wasm/bindings.cpp— flat C API exported to WASM (Emscripten target only).nisps/midi/generated/midi_devices.hpp— codegen output: no-heapconstexprexternal-MIDI-synth templates (nisps::midi::generated;MidiParam/MidiDevice+kMidiDevicesregistry). Source =schemas/midi_devices/; do not edit by hand.nisps/CMakeLists.txt+nisps/build/— host-target builds + ctest.
firmware/ — PlatformIO project + hardware glue
firmware/MEMLNaut-NISPS/platformio.ini— the variant registry: one[env:<alias>]per firmware variant (16 of them), each passing-DMEMLNAUT_MODE_TYPE=<alias>;selftestpasses-DNISPS_SELFTEST=1instead. There is no second list to keep in sync. Shared[env]base pins the platform wrapper + arduino-pico framework, sets-std=gnu++20 -O3(viabuild_unflags, because the framework appends its own-std=gnu++17 -OsAFTER project flags), reachesnisps/with-I${PROJECT_DIR}/../.., and carries the TFT_eSPI panel config as-Dflags. Build:pio run -e <alias>, orscripts/build-firmware.sh [--all].firmware/MEMLNaut-NISPS/src/main.cpp— entry point (wasMEMLNaut-NISPS.ino). Forks onNISPS_SELFTEST: normal modes run the engine/ML path; theSelfTestvariant delegates all four entry points toglue/selftest.hpp.firmware/MEMLNaut-NISPS/glue/— hardware bindings:audio_driver.hpp— bridges memllibAudioDrivercallback →Mode::process(stereosample_t), and brings the codec up on the active mode'sdriver_config()(setup_audio_driver) plus publishes its preferred sample rate before the system clock is derived from it (apply_mode_sample_rate). Mic vs line input is therefore a mode-level declaration, not a firmware constant.codec_config.hpp— pure, Arduino-free clamping of anisps::DriverConfigto SGTL5000-representable values + sample-rate resolution (unsupported/"don't care" → 48 kHz, becauseAudioDriver::GetSysClockSpeed()panic()s otherwise). Host-tested bytests/cpp/test_mode_driver_config.cpp.peripherals.hpp— joystick / pots / buttons →Mode::set_inputand ML primitives. Wires the sharedFeedbackControllerExploreAndPlace lifecycle (MomA1 = enter/exit explore, MomA2 = freeze/place, TogB2 = commit; MomB1/MomB2 = reroll/nudge while exploring or grab/drop reposition while idle) plus the adaptive-learning gestures: TogB1 = Jolt (held weight morph), RVX1 = exploration amount (OU output walk). Reposition relocates an existing positive example's output to a new input position (feedback.hppbegin_reposition/commit_reposition) — no scratchpad, no weight restore.midi_io.hpp— MIDI in → modenote_on/update_bpm/set_playing; drainsControlEventring → MIDI UART.mode_select.hpp— type aliases mapping firmware mode identifiers tonisps::modes::*ModeC++ types, selected by the-Dfrom platformio.ini. Includes the sixMEMLNautModeExtSynth*external-synth variants (one per device template innisps/midi, e.g.MEMLNautModeExtSynthSub37). TheNISPS_ST_*/NISPS_ST_CATtoken-paste table and theSelfTestRigtag type are GONE — selftest is now just an env with its own-D.selftest.hpp— standalone guided hardware self-test rig (SelfTestvariant; no engine/ML). Step-driven state machine on aSelfTestView: TFT prompts the operator through every control, auto-advances on detection, encoder-press skips. Ends with optional L/R/BOTH sine-sweep headphone check (core 1 block callback) + MIDI loopback-cable test. Lives firmware-side (touches TFT + raw pins) so it stays out of platform-agnosticnisps/.output_router.hpp— top-leveldrain_outputs()entry point. (Inputs are wired directly byperipherals.hpp'sbind_peripherals().)settings_view.hpp—wire_settings(mode): adds on-device settings views to the MEMLNaut display carousel (TFT + rotary encoder). Joystick Dual/Single toggle for the 4-input ("two 2-D joystick") modes — "Single" pins ML input channels 2,3 to neutral viaModeBase::set_input_pinned(no net rebuild). Registered in the.inoafteraddSystemInfoView().
firmware/MEMLNaut-NISPS/lib/memllib/— vendored memllib (was thesrc/memllibsubmodule): hardware abstraction (audio driver, TFT display, MIDI, peripherals), ~1.9 MB / 100 files,examples/dropped exceptreference/InterfaceRL.{hpp,cpp,tpp}+InterfaceRLFileFormat.hpp— upstream's reference implementation of the feedback subsystemnisps/ml/{geo_push,replay,feedback,jolt,ou_noise}.hppwere ported from, kept verbatim and NEVER compiled (reference/sits outsidesrc/, which is the only thing PlatformIO builds). It is there so upstream drift is adiff; losing it is how the e291192 geometric-dislike redesign went unnoticed for months.VENDORED.mdrecords the upstream commit and the re-sync procedure;LICENSEis MPL-2.0, copied verbatim. Sources must sit underlib/memllib/src/— PlatformIO's library builder falls back to a flat root-only scan without it and silently compiles nothing while still linking (see VENDORED.md).firmware/README.md— structure + build instructions.firmware/useq-celium/— standalone RP2040 firmware (PlatformIO, Arduino-Pico core) that turns a uSEQ module + CV expander into a USB→CV/gate converter driven by the manifoldcvgatebackend.shared/protocol.his the v2 wire-protocol single source of truth (mirrored bymanifold/src/backends/useq-protocol.ts);main/(USB serial → CV1–3 + GATE1–3, I2C → expander) andexpander/(I2C slave → CV4–11). Wire spec:docs/specs/useq-cv-protocol.md. Restored from the April-2026 "uSEQ-Celium" mode.
manifold/ — Vite + React + TS convertible-mode app (the sole browser app)
The Manifold "convertible" Console on the real engine, deployed at meml.lnfinitemonkeys.org/next (staging,
alongside the live vanilla a-immersive at /). Built 2026-06-27/28; see docs/specs/plans/BUILD-PLAN.md (resume
anchor + locked decisions) and the docs/specs/*-spec.md set.
manifold/src/engine/— the parity-tested TS engine (samenisps.wasm), made framework-neutral:wasm-iml.ts(rewired off Solid stores onto an injectedEngineSink),engine-host.ts+worklet/nisps-processor.ts(audio), thin WASM wrappers over the core pipelines + curve catalog (the TSinput-pipeline/output-pipeline/curvesimplementations died at P4),wasm-worker.ts,spine.ts(the reactive spine BELOW React —setInputderives processed→ml→routed eagerly off-render),engine-api.ts(EngineApifaçade incl. live architecture/weight/example metrics andfeedback.*wrappers over thenisps_ml_feedback_*C ABI),EngineProvider.tsx/useEngine.ts(React binding viauseSyncExternalStoreversion counter). nisps.js is loaded via fetch+indirect-eval (Emscripten MODULARIZE glue has no ES exports), base-aware viadocument.baseURIfor the/nextsub-path.manifold/src/primitives/— the 7 design primitives as typed React (Badge, Button, PillToggle, Slider, Switch, VirtualJoystick, XYPad). Five unused ones were deleted in the 2026-07 sweep (L22).manifold/src/console/— the convertible Console:ConsoleApp,CompositeStage(single-divider convertible with snap/magnetism/minimap-demotion),OutputStage/SandwichStage/ParticleStage/Manifold(canvas, rect↔circular + feedback markers; ParticleStage has interactive cursor-labelled heatmap sliders, an adjustable joystick, and double-click whole-screen follow-mouse input),Dock(top Mode selector + 5 vertically-centred drawers),Drawers(Learning/Inputs/Outputs/Settings/Help; Learning includes the live model-architecture inspector and Outputs owns the remaining scroll height),TrainingHealth(real per-iteration loss curve fromnisps_ml_loss_history+ per-layer weight health fromnisps_ml_get_layer_stats; rendered only at the Learning drawer'sexpandeddepth — that IS the advanced-surface flag),VerdictCluster(mode-aware),OutputEditor/DualRange/CurvePad,icons.tsx(monochrome currentColor SVG),model.ts(MF_MODEScatalogue — schema-backed modes DERIVED frommanifold/src/modes/generated/; carries per-modemlnet shape +engineId),output-mode.ts.manifold/src/modes/generated/— codegen output (*_schema.ts, do NOT hand-edit):ModeSchemaconsts (mode_id, engine_id, ml dims, params, voice_spaces, ui) — the SOURCE OF TRUTH forMF_MODES. Switching mode reshapes the WASM net to the mode'smldims (ConsoleApp P5.3; boot mode paf_synth → 4→[10,10,14]→33).manifold/src/dock/—OutputControlRow(editable names, corner delete, cycling off/fixed/live status, mute + solo/arm + min/max/curve, plus MIDI card fields),output-state.ts,OutputsBackendConfig.tsx(per-backend specialised Outputs panel — the sole per-backend editor; centered prepend/append add-card controls live above and below the rows).manifold/src/backends/—OutputBackendadapter +BackendManager(spine consumer);midi-backend.ts(WebMIDI),osc-backend.ts+osc-client.ts(OSC-over-WS),vcv-backend.ts(VCV-over-WS),cv-backend.ts(UseqCvBackend— uSEQ CV/gate over USB Web Serial, backend idcvgate) +useq-protocol.ts(v2 wire protocol, mirrorsfirmware/useq-celium/shared/protocol.h;useq-protocol.test.tsruns viabun test),particle-backend.ts,passthrough-backend.ts,presets.ts(named presets),manager.ts.manifold/src/midi-devices/— external-synth device templates.generated/is codegen output fromschemas/midi_devices/(MIDI_DEVICEScatalogue +MIDI_DEVICES_BY_ID, params by name+CC). The MIDI Outputs config (dock/OutputsBackendConfig.tsx) reads it for the device picker + param-select that fills the per-card MIDI fields.manifold/src/inputs/— modular INPUT layer feeding the ML head. The Inputs dock picks ONE exclusive mode (InputMode=internal|gamepad|midi; Internal/XY-pad is default).input-layer.tsowns a single rAF loop composing the active source's axes → one dedicated engine input slot per axis, 1:1, no blending → onesetInputs, plus anonReducedInputcallback the manifold tracks. The WASM net is over-provisioned to a 32-input head (MAX_AXES,nisps/wasm/bindings.cpp); unused slots are zero-padded and a zero input is inert, so idle sources cannot perturb the net. Mean-blending was removed deliberately — it diluted every source and biased the net toward idle sources' resting values. Active-axis edits follow the persistent I/O policy: keep-capacity permutes stable identities in place until more slots are required; exact-I/O reconstructs to the active count. Surviving weights and (under adapt policy) examples are identity-remapped; feedback scratch state resets. Sources:xy-pad-source(push-driven),gamepad-source(sticks→axes single/double; buttons emit press+release actions, bound inConsoleAppto verdicts — LB/RB=down/up, X/Y/B=randomise/nudge/undo, A-hold=reposition),midi-input-source(device picker + BATCH "MIDI Learn": every CC swept while armed becomes an axis, shown as read-only meters).useInputLayer.tsis the React binding;base-source.tsshared status/action plumbing;types.tsthe adapter contract.backends/base-backend.tsis its output-side counterpart (status + throttle + lastSent) used by the midi/osc/vcv transports.manifold/src/feedback/—controller.ts(Explore-and-place scratchpad + geometric-dislike + solo; owns the elapsed-time timer for geometric replay while every weight-affecting step stays in the shared C++ core).manifold/src/settings/—settings-store.ts(monochrome icons, input-map shape, I/O resize policy, corner radius, and the opt-in legacy Xavier/spread feature flag; Manifold randomisation is full-range uniform by default).manifold/src/serial/—memlnaut-serial.tsWeb Serial scaffold +EditorPanel.tsx(MEMLNaut Editor mode).manifold/src/engine/exploration.ts— Jolt press + OU explore gestures (Learning drawer): a thin timer-driver over the shared C++ core via thenisps_ml_jolt_*/nisps_ml_ou_*bindings (the interim TS math andjolt.ts/ou-explore.tswere deleted when P3 landed).manifold/src/engine/io-reshape.ts— the deep identity-migration module for I/O card edits: exact-vs-capacity reconstruction decisions, flat weight remapping, and example vector adaptation.manifold/src/debug/probe.ts—window.__nisps(?debug=1).manifold/tests/e2e/—smoke,probe-api(engine-contract port),spine(spine invariant + probe-survives-mode-switch),geo-dislike,reshape,schema-modes,training-health(the loss/layer-stats panel + its expanded-depth gating). E2E on the VPS runs via non-snap node (see BUILD-PLAN).manifold/tests/fixtures/— golden parity fixtures (gesture trace, curves, input/output pipelines) captured 2026-07-13 pre-P4, guarded bytests/pipeline-golden.test.ts(inbun run test).tests/loss-history.test.tsdrives thenisps_ml_loss_historyC ABI straight at the committed WASM — the training path parity-check never touches.manifold/osc-bridge/— Deno WS↔UDP-OSC bridge.
vcv/ — VCV Rack 2 plugin (MEMLNaut module, WIP)
Native C++ Rack module: ML CV-mapper with RL feedback + a browser bridge. 8 inputs × 16 outputs + per-output
LED rings, palette from the frontend tokens, WS↔OSC browser bridge (see docs/specs/vcv-module.md).
src/MEMLNaut.cpp (module, 8→[16,24,16]→16), src/iml.hpp (**thin adapter over nisps::ml::MLPCore<DynamicStorage>
- core
nisps::Rng** — P6 reunification 2026-07-18, closes vcv-module.md delta #5; behaviour is now core-exact, pinned bytests/cpp/test_vcv_iml_parity.cpp),src/osc_server.hpp(bridge, transport-only),src/plugin.{hpp,cpp},res/*.svg(panels),Makefile(needsRACK_DIR). Builds against the current../nisps/core via relative includes; nonisps-core.
schemas/ — JSON parameter contracts (firmware/browser source of truth)
schemas/schema.json— Draft 2020-12 meta-schema validating mode files.schemas/modes/<mode>.json(×9) — each mode's params, ranges, defaults, curves, voice spaces, ML config. (slp_workshop.jsonreusesengine_id: memlcelium.)params[].curveis the mode-wide DEFAULT response curve; avoice_spacesentry may be an object{name, curve_overrides}declaring only the slots where THAT voice space deviates (index i ==VoiceSpaceordinal i). Descriptive throughout: the curve is applied exactly once, inside the engine's voice space.schemas/modes/params_notes.md— provenance notes and judgement calls per mode, plus the exactsquare/sqrt/linearpredicate the drift check enforces.schemas/midi_device.schema.json— Draft 2020-12 meta-schema for external-MIDI-synth templates.schemas/midi_devices/<device>.json(×6) — CC-controllable external synths (Moog Sub 37 / Sub Phatty, Creamware Pro-12 ASB, Elektron Analog Keys, ASM Hydrasynth, Roland JD-800). Each param:{id, cc, label, min, max, default, group}. Canonical source for both firmware + browser device pickers. Verified-CC provenance + sources live inschemas/midi_devices/sources/synth-midi-cc.json(asources/subdir, because the generator ajv-validates every*.jsondirectly undermidi_devices/as a device template).
codegen/ — schema → C++/TS code
codegen/generate.ts— Bun script: validates schemas via ajv (incl. the P5 firmware-fit check: exactly 3 hidden layers, dims ≤4096), emits per-mode C++nisps/modes/generated/<mode>_schema.hpp(constexpr,nisps::modes::generated) AND TSmanifold/src/modes/generated/<mode>_schema.ts(+types.ts,index.ts). Idempotent; golden-tested inrun-all-tests.shstage 5. The TS output is the SOURCE OF TRUTH consumed byMF_MODES(manifold/src/console/model.ts).codegen/generate-midi-devices.ts— separate Bun script (isolated from the mode golden test): validatesschemas/midi_devices/via ajv, emitsnisps/midi/generated/midi_devices.hpp(no-heapconstexpr) andmanifold/src/midi-devices/generated/{types,devices,index}.ts. Idempotent.codegen/curve-audit.ts— readsnisps/engines/*.hppand derives, per voice space, which response curve the engine applies to each NN-output slot. Handles the four idioms a regex misses (const float v = p[n]; v*v, memlcelium's implicit-countersq()lambda, loop-generated indices,smooth_params_[n]) and RAISES on anything it cannot reduce rather than defaulting tolinear.codegen/tests/curve_drift_test.ts— the gate: schema-declared curves (JSON and generated TS) must equal whatcurve-audit.tsextracts, and schemavoice_spacesorder must equal the engine'skVoiceSpaceNames. Source-level by necessity — the curve is not recoverable from engine output. Run byrun-all-tests.shstage 5, CI, andbun run testincodegen/.codegen/lib.ts— helpers shared by both generators.codegen/tests/golden/— golden snapshot for paf_synth (C++ + TS).
tests/cpp/ — host C++ tests
- Per-component tests:
test_dsp_*.cpp,test_engine_*.cpp,test_mlp_*.cpp,test_mode_*.cpp,test_ring_buffer.cpp,test_rng.cpp,test_math.cpp. Helpers intest_helpers.hpp. - Verification:
ml_golden_vectors.cpp,engine_impulse.cpp(+engine_impulse_baseline.bin),parity_check.cpp+parity_wasm.mjs+parity_diff.mjs— native-vs-WASM bit-equivalence within 1e-5. - Behaviour:
test_ml_behaviour.cpp— 20 INVARIANTS of the interaction model (not tuning). Asserts what must hold however the knobs are set: a like is reachable and a re-like overwrites; dislike never yields non-finite weights/outputs in eitherAvoidStyle, and is safe at cold start; repeat dislikes insidekReplayDedupRadiusdeepen ONE negative while distant ones store separately; explore→reroll→undo→exit restores weights bit-exactly and over-undoing is safe;RandomiseOutputsleaves weights untouched (the outputs-vs-weights randomisation distinction); same seed + same gesture sequence ⇒ bit-identical weights (what makes any behavioural benchmark comparable); example-ring overflow keeps the newest; contradictory examples stay finite; the ExploreAndPlace accessor contract (placed_output()while Placing,committed_output()after commit — for both place and reposition); a FULLY-masked focus gate freezes every weight; switching mode mid-exploration never strands the net in a randomised scratchpad; and all of it holds at 1×1, 2×8, 1×33, 8×2 and 32×8 shapes. - Measurement (asserts nothing):
ml_bench.cpp— the BEHAVIOURAL benchmark. NISPS is a controller, so this measures the shape of the control→parameter mapping and how interaction journeys deform it, never loss alone. Shape-agnostic (--shape N_IN,H1,H2,H3,N_OUT, default2,16,16,16,8) viaMLPCore<DynamicStorage>, so "does a wider/deeper net change the UX?" becomes a number; sample points come from a deterministic Kronecker low-discrepancy sequence rather than a raster, which is what makes it work at any input arity. Field metrics: local gain p50/p95, cliff index, dead fraction, range utilisation, rail occupancy, effective dimensionality (participation ratio — trace²/‖C‖²_F, no eigendecomposition). Displacement metrics: at-point, rings, global, blast ratio, and collateral damage at the protected positives. 61 scenarios. Diagnostic (D1 — the geometric-dislike dose decomposed: intended push vs effective LR vs measured movement at 1/10/100/1000 presses). Atomic probes A1–A14: at/around/far-from an example; one dislike under BOTH candidate designs; twice at one point; adjacent-then-return across the dedup radius; near a protected positive; roll-a-patch-and-place; the full explore→audition→place→commit lifecycle; explore-then-cancel; reposition; like-then-dislike in place; dislike-then-repair; focus/solo mask leakage. Journeys J1–J11: positive-only retention curve, randomise-place-only, mixed, branch (one shared prefix, three divergent gestures, replayed from scratch per branch because that is exact under a deterministic RNG), explore-place-only, 120-gesture long session with drift + weight-norm checkpoints, dislike storm, revisit-after-wandering, two-region interference, sweep-and-teach along a continuous path, undo-heavy. Edge cases E1–E13: cold start, single example, contradictory, collinear, corners, capacity overflow, undo exhaustion, minimal shape, identical targets, rail targets, rapid like/dislike alternation, mode-switch mid-exploration, fully-masked dislike. Upstream comparison U1–U3: the older memllibinterfaceRL(a DDPG actor-critic, recoverable from this repo's own git history at blob755ff8b) differs structurally — the user HEARSactorTarget, a soft copy updatedtarget += alpha*(online-target)at alpha=0.005, and it trains a batch of 4 from replay only everyoptimiseDivisor=40gestures. The critic half is not reproducible here (MLPCore hastrain_targetsbut not the per-layer gradient extraction the policy-gradient step needs), but both OUTPUT-PATH ideas are: U1 sweeps the soft-target alpha (alpha=1 IS NISPS today, a free control), U2 sweeps the train-every-Nth divisor, U3 compares actor shapes, U4 sweeps the positive-path training dose. U4's numbers became comparable to upstream's on 2026-07-25, whennisps/ml/training.hppstopped being SGD-only and ported upstream's RMSProp — before that an upstream LR meant something different here than there, which is what made the geometric dislike inert (D1: 5.3e-5 per press before, 1.6e-2 after). Their shared metric is lurch — how far the mapping the musician is playing moves per single gesture, averaged over the whole field. Knobs:--spread(1 = Xavier, 0 = uniform with NO fan_in coupling — i.e. the post-removal behaviour, measurable before paying for the refactor),--geo-lr,--geo-iters. Driven byscripts/bench-ml.sh. Two contracts that fail SILENTLY and are pinned by tests: (1) a thumbs-up must go through BOTHmlp.add_exampleANDfb.store_positive—dislike_geometrick-NNs the replay buffer, not the MLP dataset, so a harness that only callsadd_examplemeasures the cold-start branch instead; (2)placed_output()is valid ONLY while state isPlacing— aftercommit_place()/commit_reposition()the vector moves tocommitted_output(), and reading the wrong one yields an empty span whosel2()is 0, i.e. a broken lifecycle scored as a perfect placement. - Measurement (asserts nothing):
engine_bench.cpp+bench_report.mjs— per-engine throughput (ns/sample, blocks/s, realtime factor) for theprocess()hot path. ONE source compiled twice (CMakenisps_engine_benchnatively, emcc for WASM) so the two targets are comparable without adding a single export tonisps/wasm/bindings.cpp. Engines are driven into a working state (transport running + event drain for the sequencers, periodicnote_onfor paf_synth, a noise+sine input bed for the fx/analysis engines) and every row prints its own working-state evidence, so a number produced by an idle engine is visible rather than plausible. Driven byscripts/bench-engines.sh.
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 producingmanifold/public/nisps.{wasm,js}.build-cpp-tests.sh— CMake configure + build + ctest (Ninja).parity-check.sh— runs native + WASM and diffs binary outputs. Gotcha: it only buildsmanifold/public/nisps.{js,wasm}when they are MISSING, never when they are stale, so after any change undernisps/you must runbuild-wasm.shyourself or you are diffing fresh native against an old WASM — which reports a parity FAILURE that is really a staleness failure (this is how the RMSProp port first "broke" parity).bench-ml.sh— the ML BEHAVIOUR benchmark on native + WASM from one source (same trick asbench-engines.sh).--shape,--scenario,--smoke,--seed,--compare, and--sweep-shape(runs the corpus across a ladder of architectures and arities — the knob-sensitivity instrument). Reports, never asserts: a cliff index is a description, not a pass/fail. Invariants live intests/cpp/test_ml_behaviour.cppinstead. Reports land innisps/build/bench-ml/.bench-engines.sh— engine throughput on native + WASM;--compare <report.json>prints per-engine Δ%. Reports, never asserts (a wall-clock threshold on shared hardware is meaningless or flaky — same call as the firmware size job). Reports land innisps/build/bench/.lint-cpp.sh—.fliteral warn + heap/Arduino.hviolation fail.run-all-tests.sh— master verification script.
.github/workflows/
ci.yml— GitHub Actions: cmake build + ctest + WASM build + parity check + lint + Playwright (cpp-tests + manifold-tests jobs). Firmware compile is documented as manual.
src/ — submodule + vendored trees
- There are no submodules.
src/memllibwas one until the Phase 4 PlatformIO migration; it is now vendored atfirmware/MEMLNaut-NISPS/lib/memllib/. Fresh clones need nogit submodulestep.
Top-level docs
CLAUDE.md— long-form architecture narrative.MAP.md— this file.ALIGNMENT.md— strategic gaps + open mission questions, dated, opinionated.README.md— short quickstart.AGENTS.md— canonical agent contract: architecture, build/test, scope, and Ergo workflow.
Entry points
- Firmware:
scripts/build-firmware.sh [VARIANT](interactive prompt if omitted),scripts/flash-firmware.sh,scripts/build-and-flash-firmware.sh. Target:rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3,-std=gnu++20. - Manifold dev:
cd manifold && bun install && bun run dev(Vite, COOP/COEP headers). - Manifold build:
cd manifold && bun run build. - WASM rebuild:
bash scripts/build-wasm.sh(needsemcc). - Host C++ tests:
bash scripts/build-cpp-tests.sh. - Parity check:
bash scripts/parity-check.sh. - Engine benchmark:
bash scripts/bench-engines.sh(add--compare nisps/build/bench/latest.jsonto diff against the previous run). - ML behaviour benchmark:
bash scripts/bench-ml.sh(--smokefor a fast run,--shape 2,16,16,16,8,--sweep-shapefor the architecture/arity sweep,--compareto diff). - All tests:
bash scripts/run-all-tests.sh(stage 6 is a bench smoke report; it does not gate). - Playwright:
cd manifold && node node_modules/.bin/playwright test(non-snap node runner on the VPS — BUILD-PLAN gotcha;bunx playwright testworks elsewhere). - Codegen:
cd codegen && bun run generate.ts(regeneratesnisps/modes/generated/+nisps/ml/generated/C++ andmanifold/src/modes/generated/TS).
Conventions
- Firmware mode selection is compile-time only — one
-DMEMLNAUT_MODE_TYPEper[env:]inplatformio.ini. nisps/follows Chris's RP2350 perf rules: no heap,static const floatfor non-trivial constants, strict.fsuffix.perf.hppnow carries onlyNISPS_HOT/NISPS_FORCE_INLINE; the three dead/misshapen SRAM-section macros were deleted in the 2026-07 sweep (S21/L13).- C++ identifiers:
PascalCasetypes,snake_casefunctions/variables,kPascalCaseconstexpr. JSON keyssnake_case. TS typesPascalCase, componentsPascalCase.tsx, moduleskebab-case.ts. Curveenum lives innisps/core/math.hpp(lowercase:linear/exp/log/square/sqrt/sigmoid/cubic, plus the parameterisedcentered_powerfree function); generated mode headers re-export viausing Curve = ::nisps::Curve;. Since P4 there is NO TS mirror — the browser samples the WASM catalog (nisps_curve_apply(+batch)).- Modes are TSX components composed of primitives; mode parameter contracts are JSON schemas with codegen → C++ and TS types (
MF_MODESderives params/ml-config from the generated schemas since P5; labels/ordering stay a manifold overlay). No declarative JSON UI. - WASM and firmware share the same C++; the browser MLP is runtime-shaped (
MLPCore<DynamicStorage>, since P2):nisps_ml_createhonours(input, output, hidden[3])with non-positive/null args defaulting to32→[10,14,18]→126;nisps_ml_reshapewarm-starts a new shape. Firmware keeps compile-timeMLP<...>(zero heap). Per-mode dims are schema-real on both targets since P5.3 (the browser reshapes on mode switch). - Cross-platform parity:
scripts/parity-check.shenforces native vs WASM agreement within 1e-5.
Gotchas
- Firmware needs PlatformIO:
nix-shell -p platformio-core. Useplatformio-core, NOTplatformio— the latter is nixpkgs' bubblewrap-wrapped FHS build and fails without a working user namespace. First build pulls ~1-2 GB into~/.platformio. firmware/MEMLNaut-NISPS/glue/mode_select.hpp#undefs Arduino macros (sq,min,max,abs,round) before pulling nisps headers — engines use those identifiers as method names.nisps_firmware::g_active_mode_bridgeisexterninglue/audio_driver.hppand defined insrc/main.cpp; combininginlinewith__not_in_flashproduces a comdat conflict at link time.pio run's own "Flash: NN%" console line double-counts.dataon this board (PlatformIO's generic size checker counts every PROGBITS+ALLOC section). Comparearm-none-eabi-size -A— flash =.text+.rodata— before believing a size regression.nisps_modes_testsbuilds against generated schemas undernisps/modes/generated/; if you add a new mode, regenerate viabun run codegen/generate.tsbefore building. It also compiles one firmware header (glue/codec_config.hpp), so the repo root is on its include path.nisps::DriverConfig's member defaults are load-bearing: they reproduce memllib's historical hardcoded codec setup, so a mode that declares nothing gets exactly the pre-wiring behaviour. Changing them changes the codec setup of every mode that expresses no opinion (pinned bytests/cpp/test_mode_driver_config.cpp).
Smells / strategic concerns
See ALIGNMENT.md.
Specs
- Root:
docs/specs/ - Entry:
MAIN.md - Layout: flat (with
plans/,recon/,_archive/subdirs) - Index: none (intentionally — generate when a consumer exists)
- Skill: invoke
/specsto review/maintain/add/navigate. - Conventions: Four-genre ontology —
kind: spec(timeless contract, wins by intent),kind: plan(status: active|executed|superseded, never authority for behaviour),kind: finding(dated, immutable, exempt from drift lint), ADRs indocs/adr/.
The corpus holds platform-level specs (engine architecture, I/O backends, feedback design), implementation specs (port specs, wire protocols), feature specs (e.g. slp-workshop-firmware.md), historical findings (dated research artifacts), and finite build plans. Before changing behaviour a spec covers, find it via /specs; the spec wins by intent — if it's wrong, update it in the same commit as the code.