diff --git a/MAP.md b/MAP.md
index eeb4c6b..ef37aa6 100644
--- a/MAP.md
+++ b/MAP.md
@@ -84,7 +84,7 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set.
`backends/base-backend.ts` is 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; a thin
driver over the shared C++ core).
-- `manifold/src/settings/` — `settings-store.ts` (monochrome icons, input-map shape, corner radius).
+- `manifold/src/settings/` — `settings-store.ts` (monochrome icons, input-map shape, corner radius, and the opt-in legacy Xavier/spread feature flag; Manifold randomisation is full-range uniform by default).
- `manifold/src/serial/` — `memlnaut-serial.ts` Web 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 the `nisps_ml_jolt_*`/`nisps_ml_ou_*` bindings (the interim
diff --git a/docs/AGENT-REFERENCE.md b/docs/AGENT-REFERENCE.md
index 026a7d4..48a62a4 100644
--- a/docs/AGENT-REFERENCE.md
+++ b/docs/AGENT-REFERENCE.md
@@ -130,7 +130,7 @@ The browser MLP is runtime-shaped since P2 (`MLPCore`): `nisps_m
|-------|-------|---------|--------|
| `debug` | 1 | _(off)_ | Exposes the `window.__nisps` debug probe. |
-(The playground-era `tame`/`spread`/`preset` URL params died with the playground; `spread` survives as an engine concept — see below.)
+(The playground-era `tame`/`spread`/`preset` URL params died with the playground; `spread` survives as an opt-in engine concept — see below.)
### `spread` — sigmoid saturation control
@@ -140,6 +140,13 @@ The MLP uses ReLU hidden layers with a sigmoid output. With uniform [-1,1] weigh
- `spread=1` (centered): Xavier-scaled weights, RL noise cap 0.05, 10% weight decay per move. Outputs spread across [0,1] — better for fine-grained shaping.
- Intermediate values interpolate.
+Manifold deliberately defaults to `spread=0` at every browser boundary: initial construction,
+mode-switch reshape, direct re-roll, explore-and-place scratchpad re-roll, and forwarded VCV
+randomise. The old schema spread and the expanded Learning-drawer centred switch are available only
+after enabling Settings → Experimental features → **Xavier / spread randomisation**. This is a
+Manifold compatibility flag; the shared C++ core and generated mode schemas still expose spread for
+firmware, VCV, benchmarks, and explicit API callers.
+
## Verification chokepoints (user-confirmed)
- **A. Hardware**: each firmware mode flashes and produces correct audio on RP2350.
diff --git a/manifold/ONBOARDING.md b/manifold/ONBOARDING.md
index ca5139a..29e0107 100644
--- a/manifold/ONBOARDING.md
+++ b/manifold/ONBOARDING.md
@@ -251,7 +251,7 @@ a setting → `--r-*` tokens.
match. Debug: `window.__nisps.reshape(nIn)` / `.describe()`. See the `manifold-mixed-inputs` memory
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
- `ml` config (`MFMode.ml` — input/hidden/output + spread) via a `ConsoleApp` effect keyed on
+ `ml` config (`MFMode.ml` — input/hidden/output + legacy spread) via a `ConsoleApp` effect keyed on
`[engine, modeId]`. No confirm modal (switching instrument is deliberate); the axis-count
`ReshapeModal` above is for input-LAYOUT changes only. The effect depends on `engine`, so on boot
it fires once WASM is ready and lands the boot mode's dims (**paf_synth → 4→[10,10,14]→33**, weights
@@ -261,6 +261,10 @@ a setting → `--r-*` tokens.
Debug seam for tests: under `?debug=1` ConsoleApp installs `window.__mf`
(`setMode`/`getModeId`/`paramCount`/`modeIds`) — the UI-level analogue of `__nisps`, since no
in-UI instrument picker exists yet (`ctx.modes`/`setModeId` are plumbed but unrendered).
+ Manifold passes `spread=0` for boot, mode-switch reshapes, direct re-rolls, explore-and-place
+ scratchpad rolls, and VCV-forwarded randomise gestures by default. Settings → Experimental
+ features → **Xavier / spread randomisation** restores the schema spread and reveals the centred
+ regime switch in the expanded Learning drawer.
### Feedback — `src/feedback/`
- `controller.ts` — `FeedbackController`, framework-neutral, owned by ConsoleApp. **As of one-core-
@@ -291,7 +295,8 @@ a setting → `--r-*` tokens.
### Misc
- `src/serial/memlnaut-serial.ts` — **STUB** Web Serial scaffold for the MEMLNaut Editor mode (protocol TODO). `EditorPanel.tsx` is its UI.
-- `src/settings/settings-store.ts` — localStorage settings (`mf-settings`): icon style, input-map shape, corner radius.
+- `src/settings/settings-store.ts` — localStorage settings (`mf-settings`): icon style, input-map
+ shape, corner radius, and the opt-in legacy Xavier/spread feature flag.
- `src/midi-devices/` — codegen'd external-synth device templates.
- `src/debug/probe.ts` — `window.__nisps` synchronous probe (engine/audio/bus). Some playground
feature-store methods are present-but-inert (not ported yet) to keep the surface stable.
diff --git a/manifold/src/console/ConsoleApp.tsx b/manifold/src/console/ConsoleApp.tsx
index 37bd08c..cbbc480 100644
--- a/manifold/src/console/ConsoleApp.tsx
+++ b/manifold/src/console/ConsoleApp.tsx
@@ -109,6 +109,11 @@ export function ConsoleApp() {
const [addingExample, setAddingExample] = useState(false);
const [busy, setBusy] = useState(false);
const [spread, setSpread] = useState(false);
+ // Manifold's normal regime is a full-range uniform draw. The per-mode legacy
+ // spread remains available only when explicitly enabled in Settings.
+ const randomisationSpread = settings.xavierSpreadEnabled
+ ? (spread ? 1 : mode.ml.defaultSpread)
+ : 0;
const [active, setActive] = useState(null);
const [depth, setDepth] = useState('condensed');
// Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon.
@@ -172,7 +177,7 @@ export function ConsoleApp() {
const controllerRef = useRef(null);
if (engine && !controllerRef.current) {
controllerRef.current = new FeedbackController(engine, {
- spread: 0.6,
+ spread: randomisationSpread,
});
}
@@ -229,6 +234,12 @@ export function ConsoleApp() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [engine, feedbackMode]);
+ // Settings changes affect future randomise/explore gestures without
+ // destructively re-drawing the currently audible network.
+ useEffect(() => {
+ controllerRef.current?.setSpread(randomisationSpread);
+ }, [engine, randomisationSpread]);
+
// Push the selected solo-mode + the arm mask into the controller whenever they
// change (dock-spec §1.2). The controller RESPECTS the arm mask at the example
// level in BOTH modes and forwards it to engine.feedback.setFocus.
@@ -257,8 +268,8 @@ export function ConsoleApp() {
// modal stays for input-LAYOUT changes only (see the reshape-offer effect).
useEffect(() => {
if (!engine) return;
- const { inputSize, outputSize, hidden, defaultSpread } = mode.ml;
- engine.reshape({ inputSize, outputSize, hidden }, defaultSpread);
+ const { inputSize, outputSize, hidden } = mode.ml;
+ engine.reshape({ inputSize, outputSize, hidden }, randomisationSpread);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [engine, modeId]);
@@ -402,7 +413,7 @@ export function ConsoleApp() {
const forwardVcvFeedback = (op: 'up' | 'down' | 'rand' | 'clear') => {
backendManager?.forwardFeedback({
op,
- spread: spread ? 1 : 0.6,
+ spread: randomisationSpread,
input: [pos[0], pos[1]],
output: Array.from(engine?.getOutputs() ?? new Float32Array(0)),
});
@@ -508,7 +519,7 @@ export function ConsoleApp() {
c.reroll();
} else {
// Outside a scratchpad session a re-roll randomises the real net directly.
- engine?.randomise(spread ? 1 : 0.6);
+ engine?.randomise(randomisationSpread);
}
// VCV bridged mode: re-roll the module's net too.
forwardVcvFeedback('rand');
@@ -794,6 +805,7 @@ export function ConsoleApp() {
inputs,
spread,
setSpread,
+ xavierSpreadEnabled: settings.xavierSpreadEnabled,
noiseCap,
setNoiseCap,
// learning-behaviour
diff --git a/manifold/src/console/Drawers.tsx b/manifold/src/console/Drawers.tsx
index bfe8112..578983f 100644
--- a/manifold/src/console/Drawers.tsx
+++ b/manifold/src/console/Drawers.tsx
@@ -9,7 +9,7 @@
* MEMLNaut serial panel. The old separate "Synth" and
* "Particle/Visual" drawers are REMOVED — their config now
* lives here under the active Mode (TOP dock selector).
- * settings — Settings : icon style + input-map shape (settings-store)
+ * settings — Settings : icon style, input-map shape + feature flags
* help — Help : keymap + the loop explanation
*
* The TOP dock selector ("Mode") chooses the active OUTPUT backend/target; this
@@ -259,7 +259,9 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
training is paused and the joystick auditions a random scratchpad net; + commits a placed
anchor and restores the real net.
-
+ {ctx.xavierSpreadEnabled && (
+
+ )}
Training health
@@ -742,6 +744,20 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
buttons are intentionally exempt. Default 2px.
)}
+
+ Experimental features
+ set('xavierSpreadEnabled', v)}
+ label="Xavier / spread randomisation"
+ />
+ {depth === 'expanded' && (
+
+ Off by default: new networks and re-rolls use full-range uniform weights for broad,
+ strongly varied mappings. Enable this to restore the legacy centred regime and its
+ Learning-drawer switch.
+
+ )}
>
);
}
diff --git a/manifold/src/console/icons.tsx b/manifold/src/console/icons.tsx
index fba380c..f2bfd21 100644
--- a/manifold/src/console/icons.tsx
+++ b/manifold/src/console/icons.tsx
@@ -111,8 +111,8 @@ export function SettingsIcon({ size = 18, style }: IconProps) {
size,
style,
<>
+
-
>,
);
}
diff --git a/manifold/src/console/types.ts b/manifold/src/console/types.ts
index 2edddee..6c9b595 100644
--- a/manifold/src/console/types.ts
+++ b/manifold/src/console/types.ts
@@ -118,6 +118,8 @@ export interface ConsoleCtx {
spread: boolean;
setSpread: (v: boolean) => void;
+ /** Settings feature flag: expose and apply the legacy Xavier/spread regime. */
+ xavierSpreadEnabled: boolean;
noiseCap: number;
setNoiseCap: (v: number) => void;
diff --git a/manifold/src/engine/engine-api.ts b/manifold/src/engine/engine-api.ts
index 8809195..800d19b 100644
--- a/manifold/src/engine/engine-api.ts
+++ b/manifold/src/engine/engine-api.ts
@@ -109,7 +109,7 @@ export interface EngineApiOptions {
learningRate?: number;
/** Default max training iterations for train/trainAsync. */
maxIterations?: number;
- /** Default RL move speed / spread for thumbsDown. */
+ /** Default RL move speed / randomisation spread. Spread defaults to 0. */
noiseCap?: number;
spread?: number;
/**
@@ -141,7 +141,7 @@ export class EngineApi {
this.learningRate = opts.learningRate ?? ML_TRAIN_DEFAULTS.learningRate;
this.maxIterations = opts.maxIterations ?? ML_TRAIN_DEFAULTS.maxIterations;
this.noiseCap = opts.noiseCap ?? 0.3;
- this.spread_ = opts.spread ?? 0.6;
+ this.spread_ = opts.spread ?? 0;
// Persist the configured default on the underlying MLP too (S26) — makes
// the WASM engine's OWN training config match EngineApi's knobs, the same
// real runtime-configurability firmware/VCV get for free from
@@ -216,6 +216,7 @@ export class EngineApi {
const spine = new Spine();
const iml = await WasmIML.create({
seed: opts.seed,
+ initialSpread: opts.spread ?? 0,
storageKey: opts.storageKey,
maxExamples: opts.maxExamples,
sink: spine,
diff --git a/manifold/src/engine/wasm-iml.ts b/manifold/src/engine/wasm-iml.ts
index 153de86..dbed2e0 100644
--- a/manifold/src/engine/wasm-iml.ts
+++ b/manifold/src/engine/wasm-iml.ts
@@ -123,6 +123,11 @@ export interface WasmIMLOptions {
outputSize?: number;
hiddenLayers?: ReadonlyArray;
seed?: number;
+ /**
+ * Initial draw regime. Manifold defaults to 0 (full-range uniform); callers
+ * must opt into the legacy Xavier/spread behaviour explicitly.
+ */
+ initialSpread?: number;
/** localStorage key the loaded weights/dataset will be persisted under. */
storageKey?: string;
/**
@@ -231,6 +236,10 @@ export class WasmIML {
const seed = (opts.seed ?? (Date.now() >>> 0)) >>> 0;
this.mlHandle = this.module._nisps_ml_create(wantedIn, wantedOut, 0, 0, seed);
if (!this.mlHandle) throw new Error('[wasm-iml] nisps_ml_create returned null');
+ // The C ABI constructor retains the cross-platform core's historical
+ // initialisation. Manifold deliberately overrides it at its boundary so an
+ // unconfigured browser engine starts with genuinely broad randomisation.
+ this.module._nisps_ml_draw_weights(this.mlHandle, opts.initialSpread ?? 0);
this.module._nisps_ml_describe(this.mlHandle, this.describePtr);
const dims = new Int32Array(this.module.HEAP32.buffer, this.describePtr, 7);
@@ -354,7 +363,7 @@ export class WasmIML {
*/
reshape(
dims: { inputSize?: number; outputSize?: number; hidden?: readonly [number, number, number] },
- spread = 0.6,
+ spread = 0,
): boolean {
const wantIn = dims.inputSize ?? this.arch_.inputSize;
const wantOut = dims.outputSize ?? this.arch_.outputSize;
@@ -743,7 +752,7 @@ export class WasmIML {
// RL ops
// -------------------------------------------------------------------
- randomiseWeights(spread = 0.6): void {
+ randomiseWeights(spread = 0): void {
this.module._nisps_ml_draw_weights(this.mlHandle, spread);
this.sink.emit('ml.delta_update', { reason: 'randomise' });
this.scheduleSave_();
diff --git a/manifold/src/feedback/controller.ts b/manifold/src/feedback/controller.ts
index 68412e1..e0f8bcd 100644
--- a/manifold/src/feedback/controller.ts
+++ b/manifold/src/feedback/controller.ts
@@ -108,7 +108,7 @@ export interface FeedbackControllerState {
}
export interface FeedbackControllerOptions {
- /** Master spread for randomise / nudge (mirrors the engine spread knob). */
+ /** Master spread for randomise / nudge. Defaults to full-range uniform (0). */
spread?: number;
/** Nudge perturbation standard deviation (small bounded weight jitter). */
nudgeStddev?: number;
@@ -145,7 +145,7 @@ export class FeedbackController {
constructor(engine: ControllerEngine, opts: FeedbackControllerOptions = {}) {
this.engine = engine;
- this.spread = opts.spread ?? 0.6;
+ this.spread = opts.spread ?? 0;
this.nudgeStddev = opts.nudgeStddev ?? 0.05;
}
diff --git a/manifold/src/settings/settings-store.ts b/manifold/src/settings/settings-store.ts
index 1be16f3..20a51a9 100644
--- a/manifold/src/settings/settings-store.ts
+++ b/manifold/src/settings/settings-store.ts
@@ -8,6 +8,9 @@
* - inputMap: the 2D input-surface shape. 'follow-mode' (default) uses the
* active mode's declared input (joystick → circular, else rectangular);
* 'rectangular' / 'circular' are explicit global overrides.
+ * - xavierSpreadEnabled: compatibility feature flag for the old centred
+ * Xavier/spread randomisation regime. Off by default, so Manifold initial
+ * weights and re-rolls use the full uniform range.
*
* British spelling in copy. No React inside the store itself — the hook is a
* separate, additive binding so a headless consumer (debug probe / test) can
@@ -35,6 +38,11 @@ export interface Settings {
* verdict buttons are intentionally exempt (separate tokens).
*/
cornerRadius: number;
+ /**
+ * Restore the legacy Xavier/spread randomisation regime and expose its
+ * Learning-drawer control. Off means full-range uniform randomisation.
+ */
+ xavierSpreadEnabled: boolean;
}
export const DEFAULT_SETTINGS: Settings = {
@@ -42,6 +50,7 @@ export const DEFAULT_SETTINGS: Settings = {
unfocusedIconColour: 'off-white',
inputMap: 'follow-mode',
cornerRadius: 2,
+ xavierSpreadEnabled: false,
};
const STORAGE_KEY = 'mf-settings';
diff --git a/manifold/tests/e2e/probe-api.spec.ts b/manifold/tests/e2e/probe-api.spec.ts
index a70edf2..9356ad3 100644
--- a/manifold/tests/e2e/probe-api.spec.ts
+++ b/manifold/tests/e2e/probe-api.spec.ts
@@ -63,6 +63,33 @@ test.describe('ML engine — debug probe contract', () => {
expect(countChanged(before, after, 1e-3)).toBeGreaterThan(0);
});
+ test('randomise defaults to a broad full-range mapping', async ({ page }) => {
+ const distribution = await page.evaluate(() => {
+ const probe = window.__nisps!;
+ const values: number[] = [];
+ for (let draw = 0; draw < 48; ++draw) {
+ probe.randomise();
+ probe.setInputs(0.5, 0.5);
+ values.push(...probe.getOutputs());
+ }
+ values.sort((a, b) => a - b);
+ const percentile = (p: number) => values[Math.floor((values.length - 1) * p)]!;
+ const centralFraction =
+ values.filter((value) => value >= 0.35 && value <= 0.65).length / values.length;
+ return {
+ p05: percentile(0.05),
+ p95: percentile(0.95),
+ centralFraction,
+ };
+ });
+
+ // The former implicit spread=0.6 regime put ~99.8% of values inside this
+ // central band. Uniform spread=0 must visibly reach both sides of it.
+ expect(distribution.p05).toBeLessThan(0.3);
+ expect(distribution.p95).toBeGreaterThan(0.7);
+ expect(distribution.centralFraction).toBeLessThan(0.8);
+ });
+
test('setInputs runs inference and yields bounded outputs', async ({ page }) => {
await page.evaluate(() => window.__nisps!.setInputs(0.25, 0.75));
const outs = await getOutputs(page);
diff --git a/manifold/tests/e2e/randomisation-settings.spec.ts b/manifold/tests/e2e/randomisation-settings.spec.ts
new file mode 100644
index 0000000..fafc810
--- /dev/null
+++ b/manifold/tests/e2e/randomisation-settings.spec.ts
@@ -0,0 +1,22 @@
+import { test, expect } from '@playwright/test';
+import { loadProbe } from './helpers';
+
+test('legacy Xavier control is hidden until enabled in Settings', async ({ page }) => {
+ await loadProbe(page);
+
+ await page.getByTitle('Learning').click();
+ await page.getByTitle('Expand').click();
+ await expect(page.getByText('Xavier (centred) weight regime')).toHaveCount(0);
+
+ await page.getByTitle('Close').click();
+ await page.getByTitle('Settings').click();
+ const featureFlag = page.getByRole('switch', { name: 'Xavier / spread randomisation' });
+ await expect(featureFlag).toHaveAttribute('aria-checked', 'false');
+ await featureFlag.click();
+ await expect(featureFlag).toHaveAttribute('aria-checked', 'true');
+
+ await page.getByTitle('Close').click();
+ await page.getByTitle('Learning').click();
+ await page.getByTitle('Expand').click();
+ await expect(page.getByRole('switch', { name: 'Xavier (centred) weight regime' })).toBeVisible();
+});