feat(manifold): P5.2/P5.3 — derive MF_MODES from schema truth + per-mode engine dims
Schema-backed modes in console/model.ts are now DERIVED from the codegen
schemas in src/modes/generated/ (source of truth): real param names/groups/
count, plus each mode's ml net shape (MFMode.ml) and schema engine_id. A thin
manifold OVERLAY supplies only label/glyph/ModeClass/input/ordering. New
browser-viable modes xiasri + slp_workshop get derived entries; schema-less
visualizer + c15 stay hand-written on DEFAULT_MODE_ML. Schema min/max/default/
label/curve surface as engine-unit metadata (schemaMin/... on MFParam) without
touching the 0..1 routing semantics.
Switching instrument mode reshapes the runtime-shaped WASM net to the mode's
schema ml config (ConsoleApp effect keyed on [engine, modeId]; no confirm
modal). Boot lands paf_synth dims (4->[10,10,14]->33) once WASM is ready. The
P2.3 axis-count reshape offer still reads the engine's live inputSize and does
not spuriously prompt on a mode switch.
Adds schema-modes.spec.ts (P5 gate): drives switches via a new window.__mf
debug seam and asserts describe() dims, getWeights count, output length/bounds,
and UI param count FROM the imported schemas; spot-checks trainAsync after a
switch. Updates reshape/probe-api/geo-dislike specs to assert from the boot
mode schema instead of the retired fixed 32/126 shape.
All gates green: typecheck, unit (9), build, e2e (33).
2026-07-18 12:45:06 +02:00
|
|
|
/**
|
|
|
|
|
* Schema-driven per-mode dims — the one-core-engine P5 gate.
|
|
|
|
|
*
|
|
|
|
|
* The generated mode schemas (`src/modes/generated/`) are the SOURCE OF TRUTH:
|
|
|
|
|
* `MF_MODES` derives its params from them (P5.2) and the engine reshapes to each
|
|
|
|
|
* mode's `ml` config on switch (P5.3). This spec imports the schemas DIRECTLY
|
|
|
|
|
* and, for a representative set of modes, drives a mode switch through the UI
|
|
|
|
|
* debug seam (`window.__mf`), then asserts against the schema — never hard-coded
|
|
|
|
|
* numbers — that:
|
|
|
|
|
*
|
2026-07-25 17:12:26 +02:00
|
|
|
* - `describe()` reports the schema's hidden/output dims and Manifold's
|
|
|
|
|
* default 2-input working shape;
|
feat(manifold): P5.2/P5.3 — derive MF_MODES from schema truth + per-mode engine dims
Schema-backed modes in console/model.ts are now DERIVED from the codegen
schemas in src/modes/generated/ (source of truth): real param names/groups/
count, plus each mode's ml net shape (MFMode.ml) and schema engine_id. A thin
manifold OVERLAY supplies only label/glyph/ModeClass/input/ordering. New
browser-viable modes xiasri + slp_workshop get derived entries; schema-less
visualizer + c15 stay hand-written on DEFAULT_MODE_ML. Schema min/max/default/
label/curve surface as engine-unit metadata (schemaMin/... on MFParam) without
touching the 0..1 routing semantics.
Switching instrument mode reshapes the runtime-shaped WASM net to the mode's
schema ml config (ConsoleApp effect keyed on [engine, modeId]; no confirm
modal). Boot lands paf_synth dims (4->[10,10,14]->33) once WASM is ready. The
P2.3 axis-count reshape offer still reads the engine's live inputSize and does
not spuriously prompt on a mode switch.
Adds schema-modes.spec.ts (P5 gate): drives switches via a new window.__mf
debug seam and asserts describe() dims, getWeights count, output length/bounds,
and UI param count FROM the imported schemas; spot-checks trainAsync after a
switch. Updates reshape/probe-api/geo-dislike specs to assert from the boot
mode schema instead of the retired fixed 32/126 shape.
All gates green: typecheck, unit (9), build, e2e (33).
2026-07-18 12:45:06 +02:00
|
|
|
* - `getWeights().length` equals the schema-implied weight count;
|
|
|
|
|
* - post-ML outputs have length == output_size and stay bounded in [0,1];
|
|
|
|
|
* - the rendered UI param count equals `schema.params.length`;
|
|
|
|
|
* - training still works after a mode switch (per-mode dims flow through the
|
|
|
|
|
* async training worker — one-core-engine P2.2/P2.3 buffer sizing).
|
|
|
|
|
*/
|
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
|
import type { Page } from '@playwright/test';
|
|
|
|
|
import { loadProbe, getOutputs, allWithin, weightCountFromMl } from './helpers';
|
|
|
|
|
import type { MfDebugHook } from '../../src/console/ConsoleApp';
|
|
|
|
|
import type { ModeSchema } from '../../src/modes/generated/types';
|
|
|
|
|
import {
|
|
|
|
|
PafSynthSchema,
|
|
|
|
|
ChannelStripSchema,
|
|
|
|
|
MemlceliumSchema,
|
|
|
|
|
XiasriSchema,
|
|
|
|
|
} from '../../src/modes/generated';
|
|
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
|
interface Window {
|
|
|
|
|
__mf?: MfDebugHook;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Representative modes: an xy synth (33 out), a joystick synth (24 out), a
|
|
|
|
|
// sequencer (56 out), and a browser-viable NEW mode (xiasri, 24 out) that never
|
|
|
|
|
// had a hand-written catalogue entry.
|
|
|
|
|
const CASES: ReadonlyArray<ModeSchema> = [
|
|
|
|
|
PafSynthSchema,
|
|
|
|
|
ChannelStripSchema,
|
|
|
|
|
MemlceliumSchema,
|
|
|
|
|
XiasriSchema,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Switch the instrument mode via the debug seam and wait until the switch has
|
|
|
|
|
* fully landed: the modeId, the rendered param count, and the reshaped net dims
|
|
|
|
|
* must all reflect the target schema before we assert.
|
|
|
|
|
*/
|
|
|
|
|
async function switchToMode(page: Page, schema: ModeSchema): Promise<void> {
|
2026-07-25 17:12:26 +02:00
|
|
|
const expectedInput = schema.ui.primary_input === 'audio_in' ? schema.ml.input_size : 2;
|
feat(manifold): P5.2/P5.3 — derive MF_MODES from schema truth + per-mode engine dims
Schema-backed modes in console/model.ts are now DERIVED from the codegen
schemas in src/modes/generated/ (source of truth): real param names/groups/
count, plus each mode's ml net shape (MFMode.ml) and schema engine_id. A thin
manifold OVERLAY supplies only label/glyph/ModeClass/input/ordering. New
browser-viable modes xiasri + slp_workshop get derived entries; schema-less
visualizer + c15 stay hand-written on DEFAULT_MODE_ML. Schema min/max/default/
label/curve surface as engine-unit metadata (schemaMin/... on MFParam) without
touching the 0..1 routing semantics.
Switching instrument mode reshapes the runtime-shaped WASM net to the mode's
schema ml config (ConsoleApp effect keyed on [engine, modeId]; no confirm
modal). Boot lands paf_synth dims (4->[10,10,14]->33) once WASM is ready. The
P2.3 axis-count reshape offer still reads the engine's live inputSize and does
not spuriously prompt on a mode switch.
Adds schema-modes.spec.ts (P5 gate): drives switches via a new window.__mf
debug seam and asserts describe() dims, getWeights count, output length/bounds,
and UI param count FROM the imported schemas; spot-checks trainAsync after a
switch. Updates reshape/probe-api/geo-dislike specs to assert from the boot
mode schema instead of the retired fixed 32/126 shape.
All gates green: typecheck, unit (9), build, e2e (33).
2026-07-18 12:45:06 +02:00
|
|
|
await page.evaluate((id) => window.__mf!.setMode(id), schema.mode_id);
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
(s) =>
|
|
|
|
|
window.__mf?.getModeId() === s.id &&
|
|
|
|
|
window.__mf?.paramCount() === s.params &&
|
|
|
|
|
window.__nisps?.describe().outputSize === s.out &&
|
|
|
|
|
window.__nisps?.describe().inputSize === s.in,
|
2026-07-25 17:12:26 +02:00
|
|
|
{ id: schema.mode_id, params: schema.params.length, out: schema.ml.output_size, in: expectedInput },
|
feat(manifold): P5.2/P5.3 — derive MF_MODES from schema truth + per-mode engine dims
Schema-backed modes in console/model.ts are now DERIVED from the codegen
schemas in src/modes/generated/ (source of truth): real param names/groups/
count, plus each mode's ml net shape (MFMode.ml) and schema engine_id. A thin
manifold OVERLAY supplies only label/glyph/ModeClass/input/ordering. New
browser-viable modes xiasri + slp_workshop get derived entries; schema-less
visualizer + c15 stay hand-written on DEFAULT_MODE_ML. Schema min/max/default/
label/curve surface as engine-unit metadata (schemaMin/... on MFParam) without
touching the 0..1 routing semantics.
Switching instrument mode reshapes the runtime-shaped WASM net to the mode's
schema ml config (ConsoleApp effect keyed on [engine, modeId]; no confirm
modal). Boot lands paf_synth dims (4->[10,10,14]->33) once WASM is ready. The
P2.3 axis-count reshape offer still reads the engine's live inputSize and does
not spuriously prompt on a mode switch.
Adds schema-modes.spec.ts (P5 gate): drives switches via a new window.__mf
debug seam and asserts describe() dims, getWeights count, output length/bounds,
and UI param count FROM the imported schemas; spot-checks trainAsync after a
switch. Updates reshape/probe-api/geo-dislike specs to assert from the boot
mode schema instead of the retired fixed 32/126 shape.
All gates green: typecheck, unit (9), build, e2e (33).
2026-07-18 12:45:06 +02:00
|
|
|
{ timeout: 10_000 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
|
|
|
await loadProbe(page);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe('schema-driven per-mode dims (P5 gate)', () => {
|
|
|
|
|
test('the debug seam exposes every catalogue mode id', async ({ page }) => {
|
|
|
|
|
const ids = await page.evaluate(() => window.__mf!.modeIds());
|
|
|
|
|
for (const schema of CASES) expect(ids).toContain(schema.mode_id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const schema of CASES) {
|
|
|
|
|
test(`${schema.mode_id}: engine + UI match the schema`, async ({ page }) => {
|
|
|
|
|
await switchToMode(page, schema);
|
|
|
|
|
|
2026-07-25 17:12:26 +02:00
|
|
|
// describe() reports schema hidden/output dims and the UI's effective
|
|
|
|
|
// input arity (2 for normal modes, schema-fixed for audio analysis).
|
feat(manifold): P5.2/P5.3 — derive MF_MODES from schema truth + per-mode engine dims
Schema-backed modes in console/model.ts are now DERIVED from the codegen
schemas in src/modes/generated/ (source of truth): real param names/groups/
count, plus each mode's ml net shape (MFMode.ml) and schema engine_id. A thin
manifold OVERLAY supplies only label/glyph/ModeClass/input/ordering. New
browser-viable modes xiasri + slp_workshop get derived entries; schema-less
visualizer + c15 stay hand-written on DEFAULT_MODE_ML. Schema min/max/default/
label/curve surface as engine-unit metadata (schemaMin/... on MFParam) without
touching the 0..1 routing semantics.
Switching instrument mode reshapes the runtime-shaped WASM net to the mode's
schema ml config (ConsoleApp effect keyed on [engine, modeId]; no confirm
modal). Boot lands paf_synth dims (4->[10,10,14]->33) once WASM is ready. The
P2.3 axis-count reshape offer still reads the engine's live inputSize and does
not spuriously prompt on a mode switch.
Adds schema-modes.spec.ts (P5 gate): drives switches via a new window.__mf
debug seam and asserts describe() dims, getWeights count, output length/bounds,
and UI param count FROM the imported schemas; spot-checks trainAsync after a
switch. Updates reshape/probe-api/geo-dislike specs to assert from the boot
mode schema instead of the retired fixed 32/126 shape.
All gates green: typecheck, unit (9), build, e2e (33).
2026-07-18 12:45:06 +02:00
|
|
|
const arch = await page.evaluate(() => window.__nisps!.describe());
|
2026-07-25 17:12:26 +02:00
|
|
|
expect(arch.inputSize).toBe(schema.ui.primary_input === 'audio_in' ? schema.ml.input_size : 2);
|
feat(manifold): P5.2/P5.3 — derive MF_MODES from schema truth + per-mode engine dims
Schema-backed modes in console/model.ts are now DERIVED from the codegen
schemas in src/modes/generated/ (source of truth): real param names/groups/
count, plus each mode's ml net shape (MFMode.ml) and schema engine_id. A thin
manifold OVERLAY supplies only label/glyph/ModeClass/input/ordering. New
browser-viable modes xiasri + slp_workshop get derived entries; schema-less
visualizer + c15 stay hand-written on DEFAULT_MODE_ML. Schema min/max/default/
label/curve surface as engine-unit metadata (schemaMin/... on MFParam) without
touching the 0..1 routing semantics.
Switching instrument mode reshapes the runtime-shaped WASM net to the mode's
schema ml config (ConsoleApp effect keyed on [engine, modeId]; no confirm
modal). Boot lands paf_synth dims (4->[10,10,14]->33) once WASM is ready. The
P2.3 axis-count reshape offer still reads the engine's live inputSize and does
not spuriously prompt on a mode switch.
Adds schema-modes.spec.ts (P5 gate): drives switches via a new window.__mf
debug seam and asserts describe() dims, getWeights count, output length/bounds,
and UI param count FROM the imported schemas; spot-checks trainAsync after a
switch. Updates reshape/probe-api/geo-dislike specs to assert from the boot
mode schema instead of the retired fixed 32/126 shape.
All gates green: typecheck, unit (9), build, e2e (33).
2026-07-18 12:45:06 +02:00
|
|
|
expect(arch.outputSize).toBe(schema.ml.output_size);
|
|
|
|
|
expect(arch.hidden).toEqual([...schema.ml.hidden_layers]);
|
|
|
|
|
|
|
|
|
|
// getWeights length equals the schema-implied weight count.
|
|
|
|
|
const weights = await page.evaluate(() => window.__nisps!.getWeights().length);
|
2026-07-25 17:12:26 +02:00
|
|
|
expect(weights).toBe(
|
|
|
|
|
weightCountFromMl({
|
|
|
|
|
...schema.ml,
|
|
|
|
|
input_size: schema.ui.primary_input === 'audio_in' ? schema.ml.input_size : 2,
|
|
|
|
|
}),
|
|
|
|
|
);
|
feat(manifold): P5.2/P5.3 — derive MF_MODES from schema truth + per-mode engine dims
Schema-backed modes in console/model.ts are now DERIVED from the codegen
schemas in src/modes/generated/ (source of truth): real param names/groups/
count, plus each mode's ml net shape (MFMode.ml) and schema engine_id. A thin
manifold OVERLAY supplies only label/glyph/ModeClass/input/ordering. New
browser-viable modes xiasri + slp_workshop get derived entries; schema-less
visualizer + c15 stay hand-written on DEFAULT_MODE_ML. Schema min/max/default/
label/curve surface as engine-unit metadata (schemaMin/... on MFParam) without
touching the 0..1 routing semantics.
Switching instrument mode reshapes the runtime-shaped WASM net to the mode's
schema ml config (ConsoleApp effect keyed on [engine, modeId]; no confirm
modal). Boot lands paf_synth dims (4->[10,10,14]->33) once WASM is ready. The
P2.3 axis-count reshape offer still reads the engine's live inputSize and does
not spuriously prompt on a mode switch.
Adds schema-modes.spec.ts (P5 gate): drives switches via a new window.__mf
debug seam and asserts describe() dims, getWeights count, output length/bounds,
and UI param count FROM the imported schemas; spot-checks trainAsync after a
switch. Updates reshape/probe-api/geo-dislike specs to assert from the boot
mode schema instead of the retired fixed 32/126 shape.
All gates green: typecheck, unit (9), build, e2e (33).
2026-07-18 12:45:06 +02:00
|
|
|
|
|
|
|
|
// Outputs have length == output_size and stay bounded.
|
|
|
|
|
await page.evaluate(() => window.__nisps!.setInputs(0.35, 0.65));
|
|
|
|
|
const outs = await getOutputs(page);
|
|
|
|
|
expect(outs).toHaveLength(schema.ml.output_size);
|
|
|
|
|
expect(allWithin(outs, 0, 1)).toBe(true);
|
|
|
|
|
|
|
|
|
|
// The rendered UI param count equals schema.params.length.
|
|
|
|
|
const paramCount = await page.evaluate(() => window.__mf!.paramCount());
|
|
|
|
|
expect(paramCount).toBe(schema.params.length);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 17:12:26 +02:00
|
|
|
test('normal modes can opt into four model inputs from the Inputs dock seam', async ({ page }) => {
|
|
|
|
|
await switchToMode(page, PafSynthSchema);
|
|
|
|
|
await page.getByTitle('Inputs').click();
|
|
|
|
|
await expect(page.getByTestId('model-input-size')).toBeVisible();
|
|
|
|
|
await page.getByTestId('model-input-size').getByRole('button', { name: '4 inputs' }).click();
|
|
|
|
|
await page.waitForFunction(() => window.__nisps?.describe().inputSize === 4);
|
|
|
|
|
expect(await page.evaluate(() => window.__mf!.getModelInputSize())).toBe(4);
|
|
|
|
|
|
|
|
|
|
await page.evaluate(() => window.__mf!.setModelInputSize(2));
|
|
|
|
|
await page.waitForFunction(() => window.__nisps?.describe().inputSize === 2);
|
|
|
|
|
});
|
|
|
|
|
|
feat(manifold): P5.2/P5.3 — derive MF_MODES from schema truth + per-mode engine dims
Schema-backed modes in console/model.ts are now DERIVED from the codegen
schemas in src/modes/generated/ (source of truth): real param names/groups/
count, plus each mode's ml net shape (MFMode.ml) and schema engine_id. A thin
manifold OVERLAY supplies only label/glyph/ModeClass/input/ordering. New
browser-viable modes xiasri + slp_workshop get derived entries; schema-less
visualizer + c15 stay hand-written on DEFAULT_MODE_ML. Schema min/max/default/
label/curve surface as engine-unit metadata (schemaMin/... on MFParam) without
touching the 0..1 routing semantics.
Switching instrument mode reshapes the runtime-shaped WASM net to the mode's
schema ml config (ConsoleApp effect keyed on [engine, modeId]; no confirm
modal). Boot lands paf_synth dims (4->[10,10,14]->33) once WASM is ready. The
P2.3 axis-count reshape offer still reads the engine's live inputSize and does
not spuriously prompt on a mode switch.
Adds schema-modes.spec.ts (P5 gate): drives switches via a new window.__mf
debug seam and asserts describe() dims, getWeights count, output length/bounds,
and UI param count FROM the imported schemas; spot-checks trainAsync after a
switch. Updates reshape/probe-api/geo-dislike specs to assert from the boot
mode schema instead of the retired fixed 32/126 shape.
All gates green: typecheck, unit (9), build, e2e (33).
2026-07-18 12:45:06 +02:00
|
|
|
test('training works after a mode switch (per-mode dims flow to the worker)', async ({ page }) => {
|
|
|
|
|
// Switch to a mode with distinct dims from the boot mode, then add a couple
|
|
|
|
|
// of contrasting examples at the mode's output arity and train. A finite,
|
|
|
|
|
// non-negative loss proves the async worker re-created its mirror net at the
|
|
|
|
|
// reshaped dims (buffer sizing did not assume a fixed 126).
|
|
|
|
|
await switchToMode(page, MemlceliumSchema);
|
|
|
|
|
const outSize = MemlceliumSchema.ml.output_size;
|
|
|
|
|
const loss = await page.evaluate(async (n) => {
|
|
|
|
|
const p = window.__nisps!;
|
|
|
|
|
p.addExample([0.1, 0.9], new Array(n).fill(0.1));
|
|
|
|
|
p.addExample([0.9, 0.1], new Array(n).fill(0.9));
|
|
|
|
|
return p.trainAsync();
|
|
|
|
|
}, outSize);
|
|
|
|
|
expect(typeof loss).toBe('number');
|
|
|
|
|
expect(Number.isFinite(loss)).toBe(true);
|
|
|
|
|
expect(loss).toBeGreaterThanOrEqual(0);
|
|
|
|
|
});
|
|
|
|
|
});
|