Commit graph

58 commits

Author SHA1 Message Date
monkey-w1n5t0n
5ed84cbf00 fix(manifold): make particle outputs interactive 2026-07-25 15:30:35 +02:00
monkey-w1n5t0n
a7b6230b8b fix(manifold): preserve output card names on delete 2026-07-25 15:28:26 +02:00
monkey-w1n5t0n
4db0f498eb feat(manifold): show live model architecture in docks 2026-07-25 15:24:50 +02:00
monkey-w1n5t0n
491820ac79 fix(manifold): preserve slider overrides until input moves 2026-07-25 15:20:11 +02:00
monkey-w1n5t0n
0010097d01 feat(manifold): edit I/O as identity-aware cards 2026-07-25 15:16:37 +02:00
monkey-w1n5t0n
3879d57b4c fix(manifold): clamp temporary output slider overrides 2026-07-25 15:16:01 +02:00
monkey-w1n5t0n
6e71ad7d35 fix(manifold): broaden randomisation by default 2026-07-25 15:07:35 +02:00
monkey-w1n5t0n
0bd65917c4 fix(manifold): share output range slider 2026-07-25 15:05:32 +02:00
monkey-w1n5t0n
766493fcdc fix(manifold): show active output count in drawer 2026-07-25 14:45:04 +02:00
monkey-w1n5t0n
f75f683f40 fix(manifold): sync output sliders with backend count 2026-07-25 13:55:19 +02:00
monkey-w1n5t0n
a77770f95d feat: curve truth, DriverConfig, real telemetry, engine benchmark
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.
2026-07-21 22:02:23 +02:00
monkey-w1n5t0n
b16f26e6ab refactor(ml): one runtime-configurable training default (S26)
The operator's call: "there should be one default learning rate and one
default max iterations and they should both be configurable at runtime."

There were SIX copies, not the four the audit described, and they did not
agree:

  nisps/ml/mlp.hpp        no-arg train() hardcoding 1.f / 1000u / 0.001f —
                          and firmware's ONLY training path calls exactly
                          this, so firmware had no runtime knob at all
  wasm-iml.ts             train() and trainAsync() TS default params (x2)
  engine-api.ts           learningRate ?? 1.0, with no maxIterations knob
  vcv/src/iml.hpp         200 / 0.1 / 0.00001 — silently divergent
  external_synth_midi.hpp its own kDefaultLearningRate/kDefaultMaxIterations
  schemas/modes/*.json    x9, identical, read by nobody at runtime

Now: schemas/ml_defaults.json is the single declaration (validated against a
sibling meta-schema, matching the midi_device.schema.json convention), codegen
emits it to C++ and TS in the same run, and MLPCore carries a TrainConfig whose
default member initialisers read the generated constant.
set_train_config()/nisps_ml_set_train_config() make it runtime-overridable on
every target; the explicit-argument train() overload is untouched. min_error
joins the tuple — it was duplicated identically and belongs with the other two.

The per-mode ml block loses default_learning_rate/default_max_iterations.
default_spread stays (genuinely wired on both targets) and input_channels stays
(codegen-time validated, real information for sound_analysis_midi).

VCV BEHAVIOUR CHANGE, deliberate: MEMLNaut.cpp constructs IML positionally and
relies on those defaults, so the module moves to 1000/1.0/0.001 — 5x the max
iterations, 10x the learning rate, and a 100x looser early-stop threshold. The
old values were never justified anywhere; they arrived with fbc68eb alongside
an unrelated module rewrite and no tuning rationale. Firmware and WASM have
shipped 1.0/1000 all along. It is now runtime-settable if this turns out worse.

The generated header lands in nisps/ml/generated/, not nisps/modes/generated/
where the rest of codegen output lives: training hyperparameters are an ML
fact, and nisps/ml sits below nisps/modes, so emitting them there would make
mlp.hpp include upward. The agent that built this flagged the directory-crossing
rather than hiding it; this is the fix. CI's generated-freshness gate learns the
new directory.

Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS (max delta 2.38e-7),
lint clean, manifold typecheck + 17 unit + 33 e2e (which exercise train() and
trainAsync() through a real browser).
2026-07-21 17:20:10 +02:00
monkey-w1n5t0n
ae2657a01b refactor(manifold): one applyCurve, one GROUP_COLOR — and make the knob honest
Phase 3 (L38, ST4).

L38 is a REAL, VISIBLE behaviour change, and it is a bug fix rather than a wash.
There were two divergent applyCurve implementations: model.ts's
(`e = 0.25 + c*1.75`, linear at c≈0.43) drove the on-screen bars, while
mapping.ts's spec-anchored version (0.5 = exactly linear, symmetric in
log-exponent space) drove `mapOutput` — i.e. the value actually sent to MIDI/OSC/
VCV. So the display disagreed with the signal at every curve setting except the
shared endpoint. mapping.ts's survives; model.ts's is deleted and re-exported.
Displayed values now move visibly, and they now match what is transmitted.

That unification exposed a third copy the finding did not mention: CurvePad.tsx
inlined the same deleted formula to draw the curve-knob preview and its numeric
readout. Left alone it would have matched NEITHER implementation — the knob
would have drawn one response while the bars and the output used another.
CurvePad now calls the same applyCurve, so knob, bars and transmitted value are
one thing. The readout shows the actual exponent.

ST4: GROUP_COLOR was duplicated across four files (Phase 1's ReadoutStrip
deletion removed a fifth). All four now import the single map in model.ts —
including OutputStage.tsx's OUT_GROUP_COLOR, which was outside the sweep's scope
and would have been the last survivor.

NOT done, deliberately: ST4 also invited replacing the fixed map with a
hash-to-palette function, because its keys (formant/pitch/amp/filter/fx/mod) are
JSX-era placeholders that do not match the real schema group strings
(verb/sequencer/general/filterbank/delays/eq/midi/...), so most groups currently
fall through to the accent colour. That is a visual redesign, not a
deduplication, and it should be one deliberate change now that every consumer
reads one map.

Gates: typecheck clean, 17/17 unit tests, 33 e2e specs.
2026-07-21 14:02:48 +02:00
monkey-w1n5t0n
87daac4f0d refactor(manifold): extract BaseBackend mirroring the input layer's BaseSource
Phase 3 (L17). The midi/osc/vcv transports each carried their own copy of the
status plumbing, throttle gate and lastSent management; the inputs layer already
had this factored out as BaseSource, so BaseBackend mirrors its shape rather
than inventing a second convention.

AUDIT CORRECTION: L17 says "all four" backends duplicate lastSent. Only three
do. The uSEQ CV backend has a genuinely different frame-level dead-zone (an
Int32Array over 14 channels plus gate bits) and no per-output lastSent, so it
takes the status half only — forcing it into the shared per-output path would
have needed a special case, and a base class with a per-subclass escape hatch is
worse than three backends sharing one. Also: BaseSource itself has no throttle
or lastSent (it is status + action plumbing only), so the mirror is partial by
nature; the throttle/lastSent half is the duplication the backends actually had.

Three internal representation changes, none observable: the MIDI backend's
lastSent moves Int16Array -> Float32Array (stored values are integers -1..127,
exact in f32, so every dead-zone comparison is bit-identical); the CV backend
now inherits a setContext that also allocates a per-output buffer it never reads
(cold path, no effect); and onStatusChange's unsubscribe returns void rather
than Set.delete's boolean (the declared type was already `() => void` and no
caller used it).

Phase 2's fix to vcv-backend.ts — tracking and dead-zoning the full N-dim input
vector rather than 2 — is preserved.

Gates: typecheck clean, 17/17 unit tests.
2026-07-21 14:02:48 +02:00
monkey-w1n5t0n
f5b571412f refactor(codegen): codegen owns mode identity, per-mode schemas and net dims
Phase 3 (S1, S5, S6, S25, L11, L37, ST11, ST12). Behaviour-preserving by
construction: the diff on the generated directories is PURELY ADDITIVE (217
insertions, 0 deletions), so no emitted constant changed value. This moves where
truth lives; it does not change what truth says.

- S5: nine mode headers each carried a mechanically identical 12-field
  positional ParamSchema aggregate. codegen now emits one
  `inline constexpr ParamSchema k<Mode>Schema` per mode, and the struct itself
  moved into generated/schema_types.hpp. Each param_schema() is a one-line
  return.
- S6 + S25: every mode hand-typed its net shape a second time as MLP template
  args, duplicating the schema's own dims. codegen emits a `<Mode>MLP` alias
  built from the already-emitted constants (not re-literalled), and all nine
  modes use it. NMaxExamples still defaults from kDefaultMaxExamples (Phase 2).
- S1: model.ts hand-imported all nine schemas by name and hand-paired each with
  its overlay — so the SET of modes was hand-maintained and could silently drift
  from codegen. codegen now emits ALL_MODE_SCHEMAS; SCHEMA_MODE_OVERLAYS is
  purely display truth (label/glyph/css/order), which stays hand-curated.
- L37: deleted the hand-written modeEngineId switch, which duplicated
  schema.engine_id and silently defaulted unknown modes to 'thru'. Routes on
  MFMode.engineId with exactly one documented exception: sound_analysis_midi
  declares engine_id 'thru' because its ModeBase audio slot really is
  NoOpEngine, while it separately drives the real AnalysisEngine.
- L11: ExternalSynthMIDIMode's shape was literal in two places; now named once
  in an ext_synth_defaults namespace with an ExtSynthMIDIMLP alias. Full folding
  into the JSON pipeline is NOT done and the reason is recorded in-file: it is a
  template family over an externally-supplied Device and variable NOut, with no
  single (device, NOut) schema to author.
- ST12: extracted codegen/lib.ts for the helpers both generators duplicated, and
  corrected the comment that claimed they had to be separate. Proof the
  extraction was behaviour-free: regenerating the MIDI-device outputs produces a
  byte-identical tree.
- ST11: deleted codegen/templates/ — dead "reference" files no generator reads,
  already drifted from the real emitters.

Gates: run-all-tests.sh ALL GREEN; codegen idempotent (re-running both
generators yields no further diff), which is what CI's dirty-diff gate checks.
2026-07-21 14:02:23 +02:00
monkey-w1n5t0n
75e0e58067 fix(manifold): full-width input vector, dropped switches, MIDI churn, re-subscribe churn
Phase 2 (S10, L19, L18, L24).

- S10: EngineApi.inputVector() returned a freshly allocated [lastRawX, lastRawY]
  on every spine tick, so VCV bridged mode silently truncated gamepad/MIDI input
  to 2-D while the spine already held the full N-dim raw vector. It now returns
  spine.lastRawInputs (ArrayLike<number>, documented as a live reused buffer —
  copy, don't retain; VcvBackend already copies), and VcvBackend tracks and
  dead-zones the full length. Audit correction: "32-input head" is not a
  constant — 32 is DEFAULT_MODE_ML.inputSize, the over-provisioned default
  before any mode is chosen; real per-mode widths come from the schemas.
- L19: BackendManager.setActive silently dropped a switch requested while
  another was in flight. Now stores the latest requested id and re-runs it in
  the finally block (latest-caller-wins).
- L18: MIDI CC messages triggered a React state update plus a snapshot
  allocation each. notifyBindings now fires only when the binding LIST changes.
- L24: two ConsoleApp global-listener effects had no dependency array and so
  re-subscribed on every render, including every pointer frame. Both now read
  through a single ref assigned in the render body, matching the existing
  onMoveRef pattern. Audit correction: its suggested `[inputs]` dep would not
  have worked — useInputLayer returns a fresh object literal each call, so that
  dep changes every render too.

Regression tests: input-vector-truncation.test.ts, backend-manager-switch.test.ts
(a fake backend whose start() is held open, to make the in-flight switch real),
midi-notify-churn.test.ts (fail-before confirmed: 51 notifications vs 1).
L24 has no test — this repo has no DOM render harness to count re-subscriptions
against a mounted component; verified by reading and reference-stability tracing.

ALSO: manifold/package.json's test script named its test files explicitly
("bun test src tests/pipeline-golden.test.ts"), so the three new files were not
run by `bun run test` or CI — regression tests that never execute. Now a glob.
Deliberately `tests/*.test.ts` rather than `tests`: bun's discovery matches
*.spec.ts too, which would drag the Playwright e2e specs into the unit run
(verified — it fails). Unit tests go 9 -> 17.

Gates: run-all-tests.sh ALL GREEN.
2026-07-21 13:22:38 +02:00
monkey-w1n5t0n
dbe0f5d8ba fix(ml): one named example capacity; train() and trainAsync() no longer diverge
Phase 2, S35. Two real defects from one root cause, both confirmed by trace
rather than taken from the audit:

1. Divergence. WasmIML built its TS Dataset mirror with a cap of 100 while
   every addExample() ALSO pushed into the C++ FIFO ring, capped at 128. Since
   train() reads the C++ ring and trainAsync() reads the TS mirror, past 100
   examples the two trained on different datasets — silently.
2. Latent OOB read. nisps_ml_train sizes its sample-weight span by the C++
   side's example_count() (up to 128), but wasm-iml.ts allocates that heap
   buffer from the TS dataset's size (<=100). Once the ring exceeds the mirror,
   the span reads past the end of the caller's allocation.

Fix: name the capacity ONCE as nisps::ml::kDefaultMaxExamples = 128, used by
FixedStorage's default template arg, DynamicStorage's default ctor arg, and the
MLP<> alias (which is the only real FixedStorage instantiation path and carried
its own independent 128 literal — the last copy of this dual truth). Expose it
through nisps_ml_describe and have the TS side read it instead of hardcoding.
Dataset's constructor default is removed entirely: a default was what invited
this bug class, and the sole call site now always supplies the describe() value.

ABI NOTE: this extends nisps_ml_describe from a 6-int to a 7-int descriptor.
nisps_ml_describe always writes 7 ints regardless of the caller's buffer, so
every call site had to grow in the same change or it would overflow the WASM
heap by 4 bytes per call. All five sites updated: three in wasm-iml.ts (init
defaults, init per-instance, reshape re-describe — the finding said there were
two), one in wasm-worker.ts, one in tests/cpp/parity_wasm.mjs. The parity
harness's expected-dims check now also pins the new max_examples slot.

Regression test: tests/cpp/test_mlp_storage_defaults.cpp — pins the two storage
policies to one constant, and drives MLPCore<DynamicStorage> exactly as
bindings.cpp does past the old TS cap, asserting it saturates at 128 and not at
100. Fail-before/pass-after confirmed by temporarily setting the constant to
100: 2 failures, named. Reverted: green.

Audit correction: the cited dataset.ts:81 is the FIFO eviction check; the
hardcoded default was at dataset.ts:45.

Gates: run-all-tests.sh ALL GREEN, parity PASS.
2026-07-21 13:22:38 +02:00
monkey-w1n5t0n
232d51039d refactor(manifold): delete the OSC bridge twin and dead protocol legs
Phase 1 group 6 (S11, L16).

- S11: deleted osc-bridge/bridge.mjs. It was not compiled output but a separate
  hand-written Node port of bridge.ts (node:dgram + ws vs Deno.listenDatagram).
  The completeness critic settled which twin survives:
  .github/workflows/osc-bridge.yml deno-compiles ONLY bridge.ts into the
  released cross-platform binaries, so bridge.ts plus those binaries are the
  distribution and the .mjs had no consumer in any workflow.
- L16: dead protocol legs left over from the retired playground —
  sendState/sendWeights in osc-client.ts, the legacy bare-array branch in the
  surviving bridge, the unreceivable /nisps/state path, and the unused
  module-output listeners.

Note for the docs phase: docs/specs/backends-spec.md still calls bridge.mjs
"already compiled" (doubly false now), and vcv/ still pushes /nisps/state via
OscServer::sendState with no manifold-side counterpart — flagged, not touched.

Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:49:25 +02:00
monkey-w1n5t0n
9b686eb312 refactor(manifold): delete the dead console UI stratum
Phase 1 group 5 (S15, S16, S18, S19, L22, L23, L20, L21, L1 delete-half).

- S15: the four-way focus/altitude system. setFocus was never called anywhere,
  so only the 'composite' branch was reachable. Deleted SplitStage,
  ReadoutStrip, InputMini, AltitudeNav, CompactAxis, the UI Focus type/prop,
  the focus branches, stripPinned and the vacuous keyboard gates. MiniMeters
  kept; engine.feedback.setFocus (a different, live thing) untouched.
- S16 + L1: the decorative stratum that rendered real-looking controls driving
  nothing — A/B machinery, the fake seed, seededGradient + weightsRevision,
  snapshots, master volume, bpm, and the learningRate/decay/tame/spreadLevel
  sliders with their Drawers rows. Each was confirmed self-referential first.
  NOTE a real behaviour change falls out of dropping `snapshots`: Undo outside
  an active explore-and-place session used to pop a UI-only snapshot that
  restored noiseCap/seed. It is now simply inactive unless a genuine
  core-backed scratchpad undo exists. Geometric-dislike mode never had a real
  undo primitive in the core, so only the fake path is gone.
- S18: BackendAdvanced.tsx and its Drawers block. It self-described as a
  duplicate of the inline OutputsBackendConfig editor and BOTH rendered in the
  same expanded drawer. OutputsBackendConfig already covers every backend.
- S19: pruned ConsoleCtx to the fields Dock/Drawers/OutputsBackendConfig
  actually read; deleted the Axes type + axes/setAxis and the
  preset/setPreset/offsetActive chain (permanently 'Sculpt'/false).
  KEPT ctx.modes and ctx.setModeId despite having no reader today — the
  Phase 5 instrument picker (§7.6, adopted) is built on exactly that plumbing.
- L22: the 5 dead primitives (Panel, StatusLine, ControlAxis, CurvePlot,
  Sparkline) and their barrel exports, plus the now-dead .mf-axis-input CSS.
- L23: OutputControl/toOutputControl, ModeIconComponent and the BACKENDS
  catalogue; Drawers now reads modeDesc.label/description from OUTPUT_MODES,
  the surviving single catalogue.
- L20: the solo-mode selector's two unimplemented options no longer pretend to
  be selectable.
- L21: FeedbackController vestiges — seed/undoDepth options, maxUndo, and six
  ControllerEngine members nothing called (the finding named three; the other
  three are used on the real EngineApi by debug/probe.ts, a different
  interface, so removing them from ControllerEngine is safe).

Gates: run-all-tests.sh ALL GREEN (typecheck, 33 Playwright specs).
2026-07-21 12:49:25 +02:00
monkey-w1n5t0n
c98d25c255 refactor(wasm): delete 12 dead C-API entries and the weights-publish channel
Phase 1 group 4 (S33, S34, L54).

- S33: removed 12 dead entries across the full 5-layer registration chain
  (bindings.cpp KEEPALIVE -> EXPORTED_FUNCTIONS -> the NispsModule declaration
  table -> the WasmIML wrapper -> the EngineApi facade): nisps_ml_reset,
  example_count, move_weights, feedback_learning_paused, feedback_drag,
  jolt_lr_scale, jolt_tick_lr_ramp, pipeline_state_size/save_state/load_state,
  feedback_placing and feedback_state. Each was grepped against manifold/src,
  manifold/tests, the e2e specs, manifold/tests/wasm-load.ts and
  tests/cpp/parity_wasm.mjs — the parity gate builds its own API via cwrap and
  is a real consumer, so it counts.
  KEPT deliberately: EXPORTED_RUNTIME's heap views + ccall/cwrap (the parity
  harness and wasm-load.ts depend on them), and nisps_ml_feedback_static_output,
  whose C export IS called directly by parity_wasm.mjs even though no TS
  wrapper reaches it. Also dropped parity_wasm.mjs's moveWeights cwrap, which
  was declared but never invoked.
- S34: deleted the publishWeights_ channel — EngineSink.setWeights,
  Spine.setWeights/weights()/liveWeights and every call site. It ran a C->heap
  copy plus a fresh Float32Array allocation at up to 200 Hz into a field
  nothing read. getWeights survives for persistence and the debug probe.
- L54: worklet loader — deleted the unused imports object, the 'c' branch,
  exMap and the duplicate second loop, and replaced the silent `() => 0` stub
  with one that throws, so a missing import fails loudly instead of returning
  plausible zeros into the audio path.

manifold/public/nisps.{js,wasm} rebuilt with the trimmed export list (emcc
3.1.69, the CI-pinned version) and committed — the freshness gate added in
Phase 0 requires it, and the webhook ships this artifact to production.

Gates: run-all-tests.sh ALL GREEN, parity 1273 floats within 1e-5.
2026-07-21 12:48:50 +02:00
monkey-w1n5t0n
6c499e6826 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
monkey-w1n5t0n
c45b91a5f9 feat(codegen): P5 — TS emission targets manifold/src/modes/generated/
- generate.ts re-activates the TS emitters against manifold (types.ts,
  per-mode <id>_schema.ts, index.ts); 9 schemas emitted
- firmware-fit check: exactly 3 hidden layers (fixed 4-layer topology) and
  every dim within (0, 4096] (the browser kMaxDim)
- golden test restores the TS case against the manifold path (byte-
  identical to the retained P1-era snapshot)
2026-07-18 12:28:16 +02:00
monkey-w1n5t0n
846a0c373a feat(manifold)!: route input/output pipelines + curves through the WASM core (P4.3/P4.4)
One-core-engine P4.3/P4.4: the input/output pipeline processing and the curve
catalog now live in the C++/WASM core (nisps/pipeline/*, nisps/core/math.hpp).
The TS ports are deleted and the browser drives the WASM chains.

Engine:
- WasmIML owns a nisps_pipeline_create handle + bridge buffers and exposes
  setInputConfig (TS InputConfig → 15-float wire), processInput, resetInput,
  setOutputConfig (Infinity slew → 0), setOutputFreezeMask, processOutput
  (in place), resetOutput, curveApply, curveApplyBatch (chunked). Handle +
  buffers created in init_, freed in dispose, output-sized buffers realloc'd
  on reshape.
- Spine routes setInputs through iml.processInput/processOutput (state lives
  C++-side); config source-of-truth stays TS-side and is pushed on attach /
  setInputConfig / setOutputConfig. Preserves ?debug=1 fixed-dt determinism
  (same dt fed to the WASM calls). EngineApi gains setInputConfig/
  setOutputConfig/curveApply/curveApplyBatch.
- New types-only modules: pipeline-types.ts (InputConfig/OutputConfig +
  defaults + wire int mappers) and curve-catalog.ts (CurveName + name→id).
  types.ts declares the pipeline/curve C ABI. engine barrel updated.
- DELETED src/engine/{input-pipeline,output-pipeline,curves}.ts.

Tests (P4.4 gate — recorded-gesture regression):
- pipeline-golden.test.ts now loads the built WASM (indirect-eval shim,
  tests/wasm-load.ts) and drives the frozen gesture/output fixtures through the
  C++ chains, honouring the per-event dt clock contract. Tolerance 1e-5
  (non-momentum drift <5e-7). The 3 momentum configs carry 1e-2: proven-inherent
  f32 drift (a byte-faithful f32 port of the exact original algorithm matches
  the WASM to <6e-8 while both diverge from the f64 capture by ~7-9e-3), NOT a
  core bug.
- curves-golden.json: linear/square/sqrt/centered_power kept as the original
  f64 captures (C++ matches within <3e-8); exp/log/sigmoid/cubic RE-BASELINED
  from the WASM (deliberate switch to firmware-exact k=1 exp/log, slope-6
  sigmoid, true cubic x^3). Provenance recorded in-file.
- _generate.ts rebuilt as the WASM curve re-baseline tool; pipeline-golden-lib
  trimmed to pure data builders.

Docs: fixtures/README.md + manifold/ONBOARDING.md updated.

Gates: typecheck, bun test (9), vite build, playwright e2e (27) all green.
2026-07-18 12:21:35 +02:00
monkey-w1n5t0n
7fd0dda3a2 feat(manifold): geometric dislike + jolt/OU via shared core (one-core-engine P3)
TS half of P3.1/P3.2/P3.3: wire the new WASM exports and delete the TS
approximations now that geometric-dislike, jolt, OU, and the seeded RNG live
in the C++/WASM core.

- types.ts/wasm-iml.ts: bind + wrap dislike_geometric, store_positive,
  positive/negative_count, set_avoid_style, jolt_press/step/release/active/
  lr_scale/tick_lr_ramp, explore_intensity/get/apply. Weight-mutating wrappers
  republish weights (version bump); explore_apply reuses feedbackBuf.
- engine-api: extend .feedback (dislikeGeometric/storePositive/counts/
  setAvoidStyle) + new .explore facade. thumbsDown now passes the HEARD
  (routed) vector, not the raw MLP output (raw == net output => inert cold-start).
- feedback/controller.ts: delete dislikes[] + applyDislikeBias + both C++ GAP
  blocks + the SeededRng field; dislike() -> core dislikeGeometric (returns
  action, 15 => cold-start prompt); like() feeds the centroid via the core
  thumbsUp; getState() exposes positive/negative counts. Delete feedback/rng.ts.
- engine/exploration.ts: execute the P3 SWAP POINT -> drive engine.explore.*;
  delete engine/jolt.ts + ou-explore.ts. UI surface unchanged.
- ConsoleApp: one-time cold-start banner (British spelling), routed heard vector
  at the dislike call site.
- App.tsx/spine.ts: under ?debug=1 pin a fixed seed + fixed per-tick dt so the
  probe/e2e are deterministic (production keeps time-seeded, real-time dt).
- tests: new geo-dislike.spec.ts; probe gains dislikeGeometric/storePositive/
  feedbackCounts/setAvoidStyle; the thumbsDown probe test now drives a real
  distinct-heard-vector dislike (bare thumbsDown on the net's own output is
  correctly inert under the geometric core). 27 e2e + 9 unit green.

Docs: manifold/ONBOARDING.md engine+feedback sections synced.
2026-07-14 04:54:27 +02:00
monkey-w1n5t0n
3af92b625a feat(manifold): runtime-shaped net reshape with confirm modal (P2.3)
Wire the runtime-shaped WASM MLP (one-core-engine P2) through the manifold:

- WasmIML.reshape(dims, spread): calls nisps_ml_reshape, re-describes the
  instance, reallocates every dim-dependent heap buffer, refreshes weightCount,
  clears the TS Dataset mirror (C-side resets), drops the stale training worker,
  and pushes the new shape through the sink.
- EngineApi.reshape exposes it and re-ticks the spine so outputs/audio reflect
  the new net. Spine already tolerates the arity change (buffers resize, version
  bumps); documented.
- Training worker protocol carries the current hidden dims; the worker
  ensureNet()s its mirror net to match after a reshape.
- ConsoleApp offers the reshape behind ReshapeModal on an active-layout CHANGE
  (never on load; default 32-input over-provisioned head + zero-padding
  preserved when declined). British copy, reset-on-reshape.
- Drawers: delete the stale even/odd blending note; honest dedicated-dimensions
  line + net-arity chip.
- Probe: __nisps.reshape(nIn) / .describe(); e2e reshape.spec (default 32/126,
  reshape to 4, describe reports 4, bounded outputs, weight count 3148→2868,
  spine still propagates). All 25 e2e pass (20 existing + 5 new).
- ONBOARDING: refresh the reshape status + stale hardwired-arity gotcha.
2026-07-14 03:54:12 +02:00
monkey-w1n5t0n
b6819fd26f feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:

- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
  non-positive/null fall back to the historical 32→[10,14,18]→126, so
  every pre-P2 caller (manifold, worker, parity harness) stays
  bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
  new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
  copied; rest keeps spread init); feedback controller re-created (state
  resets — reset-on-reshape modal is the front-end contract). Failure
  leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
  FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
  tests source-identical via the old alias; DynamicFeedbackStorage (one
  arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
  batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
  created instance, worker carries a shape-contract note for P2.3.

Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
monkey-w1n5t0n
9056ac3f5e feat(manifold): port Jolt + OU explore gestures as UI shells
Bring the two playground-only exploration UIs into manifold ahead of the
playground's retirement (one-core-engine refactor §P1):

- Jolt: press-and-hold continuous weight-morph, release to freeze.
- Explore: Ornstein-Uhlenbeck exploration intensity on the output vector.

Interim TS maths ported verbatim from the retired playground modules
(engine/jolt.ts, engine/ou-explore.ts). The ExplorationController
(engine/exploration.ts) is the single P3 swap boundary: it drives Jolt via the
existing EngineApi get/set-weights + process route, and OU via a new inert-by-
default output-morph hook on the spine. In §P3 only that module changes to call
nisps_ml_jolt_press/release + nisps_ml_explore_intensity.

UI lands in the Learning drawer (Jolt hold-button + Explore slider), monochrome-
consistent, British copy. Gates green: typecheck, build, Playwright smoke.
2026-07-13 23:27:40 +02:00
monkey-w1n5t0n
fa80a305d9 Merge remote-tracking branch 'origin/main' 2026-07-13 23:17:08 +03:00
monkey-w1n5t0n
45f3ca5cae docs: restructure design docs into docs/specs (adr/plans/recon), update path references 2026-07-13 23:15:46 +03:00
monkey-w1n5t0n
c7056e20e8 feat(manifold): follow-mouse knob (dbl-click mark) + 1/2 verdict keys
- Double-click the input mark to enter follow-mouse mode: a window-level
  pointermove listener maps the whole viewport onto the surface's [0,1]²
  space so the knob tracks the cursor across the entire UI. Esc or a second
  double-click exits; a badge + hidden cursor signal the active state. Local
  pan/long-press and auto-drift are suppressed while following.
- Keys 1 = thumbs-down/explore (perturb), 2 = thumbs-up (commit), on the
  window keydown handler so they fire even in follow-mouse mode. Drawers
  learn/inputs lose their 1/2 shortcuts (3-5 keep route/settings/help).
- Sync help keymap (Drawers) + ONBOARDING stages table.
2026-06-29 00:45:45 +02:00
monkey-w1n5t0n
d2b0427ec6 feat(manifold): default to geometric push + add Clear examples button
Geometric-dislike ('Push away', Mode 1) is now the default feedback mode
instead of explore-and-place, which works poorly. Add a Clear button to
the Learning drawer that forgets all recorded examples and wipes the
on-map visuals (feedback markers + placed-anchor pins) via the existing
ctx.onClear, now also resetting pins.
2026-06-29 00:36:32 +02:00
monkey-w1n5t0n
031c7f97ff fix(manifold): keep input knob inside the circular area for mouse + gamepad
The on-screen circular input disc let both the mouse and the gamepad drive
the knob outside the visible circle.

- Gamepad: each stick axis was clamped to [0,1] independently, so a full
  diagonal push reached the square corner. Clamp the stick *vector* to the
  unit disc (radially symmetric) before mapping to [0,1]; covers both sticks.
- Mouse: the circular variant clamped in normalised [0,1]^2 but the canvas
  drew the knob across the full non-square panel against the inscribed circle,
  so the disc rendered as an ellipse that spilled past the rim. Map both the
  pointer and every drawn position (knob/pins/markers/trail/flash) through the
  inscribed-circle geometry, and disc-clamp the auto-drift.
2026-06-29 00:14:49 +02:00
monkey-w1n5t0n
94b91f1cc0 fix(manifold): correct SandwichStage 3D depth sort so tilt occludes right
The painter's-algorithm sort was descending, but z2 (depth) increases
toward the camera in both the top-down and tilted-from-above views, so
nearest was painted first (underneath). This showed the last layer on
top in the default top-down view and made the stack occlude inside-out
when tilted back. Sort ascending: farthest first, nearest last.
2026-06-28 23:06:33 +02:00
monkey-w1n5t0n
4656568d4f feat(manifold,firmware): restore uSEQ CV/gate as an Outputs backend
Restores the April-2026 "uSEQ-Celium" functionality (browser → uSEQ
hardware + CV expander over USB Web Serial) as a first-class Manifold
Outputs backend, and re-vendors the RP2040 firmware into the repo.

- protocol v2 (uSEQ-CV): firmware/useq-celium/shared/protocol.h is the
  single source of truth, mirrored by manifold/src/backends/useq-protocol.ts.
  26-byte OUTPUT frame, 11×u16 CV (12-bit) + 3-gate bitfield + XOR; fixed
  topology; host-agnostic so the MEMLNaut RP2350 can emit identical bytes.
  Spec in docs/useq-celium/protocol.md.
- firmware/useq-celium/{main,expander}: PlatformIO RP2040 firmware rewritten
  to v2 from the real April pin maps (expander I2C addr 0x10).
- UseqCvBackend (id cvgate): Web Serial connect/identify/disconnect, 100 Hz
  stream, per-channel dead-zone, gate thresholding; modeled on midi-backend.
  Per-output CvSpec (channel + gateThreshold) on MFParam; config UI in
  OutputsBackendConfig + BackendAdvanced; new "CV / uSEQ" top-dock mode.
- bun-test for the protocol frame layout; MAP.md updated.
2026-06-28 22:30:54 +02:00
monkey-w1n5t0n
b7c46828f3 fix(manifold): keep the output hover-editor inside the viewport
The OutputEditor popover was placed with a fixed top + an index-based
left/right heuristic and never measured the viewport, so it clipped the
screen edges when the window wasn't full-screen. It now measures its
anchored rect on layout and translates itself back inside the viewport
(8px gutter), re-checking on resize.
2026-06-28 22:21:18 +02:00
monkey-w1n5t0n
30fc44ba1e fix(manifold): slider fill tracks value; hide noise-cap rings
- mf-slider-input fill used calc(var(--mf-pct)*100%) but three consumers
  (OutputControlRow held slider, OutputEditor, shared-ui Field) never set
  --mf-pct, so the coloured track-fill stayed empty while only the native
  thumb moved. Set --mf-pct inline on all three; harden the base class
  (appearance:none + transparent bg + --mf-pct fallback + moz-range-progress).
- hide the two concentric noise-cap exploration rings around the input dot.
2026-06-28 22:09:58 +02:00
monkey-w1n5t0n
cb28facc65 feat(manifold): verdict-cluster + dock polish
- thumbs-up → green; down/explore → orange (dice icon in explore mode,
  thumb-down in dislike mode); undo button shrunk
- add nudge | randomise two-segment pill under the verdict cluster
- dock drawers closed by default; collapse to two depths (condensed
  side-panel / expanded ~80% centred modal) with a left-edge expand tab
- Mode button → 'M' (white border, orange letter)
- smoke test asserts rail button title (drawers now closed by default)
2026-06-28 21:41:02 +02:00
monkey-w1n5t0n
453047612a feat(manifold): parameter-sandwich centre-stage toggle
Fuse the /learn parameter-landscape sandbox into the main console:
- New SandwichStage: ports the 3D landscape renderer to React, driven by the
  REAL EngineApi (inferBatch samples each output channel over the 2D input grid;
  getOutputs feeds the live probe). Re-samples on engine version bumps; orbit
  camera (top-down = flat 2D, tilt to reveal the stack), centred + grows to fill.
- Dock: new bottom toggle (SandwichIcon) → ctx.sandwich.
- ConsoleApp: when on, the sandwich replaces the centre stage in a 3-zone flex —
  shrunken Manifold (left) · sandwich (centre, fills) · compact OutputStage
  (right) — reusing pos/markers/verdicts. Opening it auto-closes the drawer so
  the output zone is visible. Layers = the mode's params (capped 8).

typecheck + build green; verified headless (toggle on/off, sample, tilt).
2026-06-28 21:18:50 +02:00
monkey-w1n5t0n
9e59eb04ce feat(manifold): MIDI + game controller inputs; widen ML net to N-D
Wire the modular input layer into the Console and reshape the browser
engine so input axes are genuine independent dimensions.

Inputs (manifold/src/inputs/):
- gamepad-source: emit press+release edges with standard-mapping labels
  (enables hold-and-move); single/double-stick already present.
- midi-input-source: single-device selection + batch "MIDI Learn"
  (every CC swept while armed becomes an axis); notes stay discrete.
- input-layer: compose() forwards each axis 1:1 (no mean-blend);
  add onReducedInput so the manifold tracks gamepad/MIDI position.
- types: InputAction.phase, InputMode.

Console (manifold/src/console/):
- ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down /
  X randomise / Y nudge / B undo / A-hold reposition); mirror composed
  position onto the manifold.
- Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI
  device picker + batch-learn flow, learned-control meters).

Engine (nisps/wasm, manifold/src/engine):
- DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each
  active axis gets a dedicated slot, unused slots held at 0 (inert).
  Rebuilt nisps.wasm (playground + manifold).
- spine/engine-api: setInputs writes the full N-D vector (was dropping
  arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the
  whole vector via spine.reprocess().

Tests:
- parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs.
- CMakeLists: build parity binary with -ffp-contract=off so native
  matches FMA-free WASM (training amplified the gap past 1e-5).

Inputs dock is still an exclusive picker; mixing toggles, reshape modal,
and the >2-D slider view (inputs-spec.md) are groundwork-laid but not
yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:48 +02:00
monkey-w1n5t0n
115fd501b5 style(manifold): on-theme heatmap-strip colours (brand orange→cyan ramp)
Replace the a-immersive original's clashing 20-colour palette (hot magenta,
lime, pink) with a ramp interpolated between Manifold's two brand accents —
--accent (#ff6a00) → --accent-2 (#00ccff) — softened to the app's semantic-
token saturation register so the strip reads as native chrome.
2026-06-28 20:46:48 +02:00
monkey-w1n5t0n
60220e9176 fix(manifold): particle top bar = a-immersive heatmap strip, not macro axes
ParticleStage showed the Boldness/Memory/Precision macro sliders across the
top, but the a-immersive design this mode clones uses a thin per-output
heatmap strip (one colored bar per visual output, width = live value). The
deployed a-immersive never used the macro-axis surface.

- flow-field.ts: export VISUAL_PARAM_NAMES/COLORS + N_VISUAL_OUTPUTS (verbatim
  from a-app.js:46/51)
- ParticleStage: replace macro-slider bar with the 22px heatmap strip; bar
  widths updated imperatively in the existing rAF loop; hover tooltip with
  live value; drop unused axes props
- ConsoleApp: update call site
2026-06-28 20:34:34 +02:00
monkey-w1n5t0n
b3c0e71bb7 feat(manifold): MIDI device-template picker in Outputs config
Adds a device picker to the MIDI backend config: choose an external synth
(Moog Sub 37, Hydrasynth, etc.), see its parameters grouped and BY NAME with
their CC numbers, tick which ones to control, and Apply — fills the per-output
CC table (CC#/channel/name) and sets the CC count from the template. Clamps to
the engine output count. Self-contained (local state + setParam/setMidiCcCount);
the result lives in the shared params store, so the existing named-preset bar
saves/restores it. Sourced from manifold/src/midi-devices (codegen).
2026-06-28 20:06:15 +02:00
monkey-w1n5t0n
9a9b66c5ee feat(midi-devices): canonical external-synth CC templates + dual codegen
Add a durable, committed source of truth for external MIDI synth control:
- synth-midi-cc.json: verified CC maps + provenance/sources (8 devices researched)
- schemas/midi_device.schema.json + schemas/midi_devices/*.json: 6 CC-controllable
  device templates (Moog Sub 37/Sub Phatty, Creamware Pro-12 ASB, Elektron Analog
  Keys, ASM Hydrasynth, Roland JD-800), params keyed {id, cc, label, min, max,
  default, group}.
- codegen/generate-midi-devices.ts (isolated from the mode golden test) emits both
  nisps/midi/generated/midi_devices.hpp (no-heap constexpr, firmware+WASM) and
  manifold/src/midi-devices/generated/ (typed catalogue for the browser).
- codegen/seed-midi-devices.ts: reproducible seed from the research artifact.

Lets a performer pick a device and address its parameters by name (not CC number)
on both the firmware and the Manifold browser engine.
2026-06-28 20:03:17 +02:00
monkey-w1n5t0n
0a17b3e76e feat(animations+firmware): /next/animations showcase (videos + interactive demos), in-app Help link, Arch firmware build script
- assets/media/showcase/index.html: standalone showcase on Manifold tokens —
  the 4 Manim explainers + 2 live interactive demos (knob->number, XY->two sliders).
- Help drawer links to it (animations/).
- scripts/build-firmware-arch.sh: Omarchy/Arch firmware build (pacman deps +
  arduino-cli + delegates to setup-firmware-toolchain.sh; clear error on the
  unreachable memllib pin).
2026-06-28 04:42:27 +02:00
monkey-w1n5t0n
45e59e1a23 merge(vcv-bridge): browser drives+trains VCV module over WS-OSC (/nisps/input + /nisps/feedback)
# Conflicts:
#	manifold/src/backends/manager.ts
2026-06-28 04:33:58 +02:00
monkey-w1n5t0n
1ab269415e merge(inputs): modular XY/MIDI/gamepad input layer + Inputs dock panel 2026-06-28 04:32:44 +02:00
monkey-w1n5t0n
cba7de6a4b merge(particle): integrate flow-field as default Particle Mode + ParticleBackend
# Conflicts:
#	manifold/src/console/flow-field.ts
2026-06-28 04:32:44 +02:00
monkey-w1n5t0n
7d36d3d18d feat(osc-bridge): add /nisps/input (multi-float) + /nisps/feedback (JSON) verbs; client sendInput/sendFeedback; VcvBackend uses them; document VCV runtime 2026-06-28 04:28:45 +02:00
monkey-w1n5t0n
964e37551f feat(dock): wire VCV bridge URL/connect + per-output polarity; forward verdict loop to /nisps/feedback in VCV mode 2026-06-28 04:25:39 +02:00
monkey-w1n5t0n
433a4989e1 feat(inputs): wire input layer into console + flesh out INPUTS drawer
Route onMove through the XY-pad source's pushPad; instantiate useInputLayer
per engine and expose it on ConsoleCtx. The Inputs drawer now picks the
source(s) (XY pad / MIDI / gamepad toggles), configures each (gamepad
single/double stick; MIDI learn-map with per-binding clear), and shows the
composed channel layout + the blend-to-2 reshape notice referencing the
multi-WASM TODO (inputs-spec).
2026-06-28 04:24:48 +02:00