Commit graph

428 commits

Author SHA1 Message Date
monkey-w1n5t0n
b6be081cf0 docs: record the operator's L25 and telemetry decisions
L25: keep the firmware loss-history buffer. With §7.3's "make it real, browser
only, behind a feature flag", the telemetry decision set is now complete and
plan 5e has no judgement calls left in it — only work.

Also corrects the ALIGNMENT entry, which described four live fragments: the
fabricated gradient-health UI (L1) was already deleted in Phase 1, so three
remain. Fragment inventory re-verified against the tree rather than copied
forward from the audit.
2026-07-21 17:05:30 +02:00
monkey-w1n5t0n
1f38f1a7fb fix(engines): restore MEMLCelium's feedback tap (L7)
feedback_/fbzm1_/fb_smooth_alpha_ were read every process() call but nothing
ever wrote feedback_, so the path was silently inert. Git history shows this
was a *muted* feature, not dead code: `feedback = v0 * feedbackGain` with
feedbackGain 0.1f (MEMLCeliumAudioApp.hpp @ d095688), then "0; //0.1f" —
zeroed with the value kept in the comment — then the write was dropped
entirely when the engine was ported to nisps/engines/ at the 2026-04-29
rewrite (8d0d47b). Operator decision: port it back, live at 0.1f.

The tap is voice 0's own enveloped output, post-envelope and pre voice-1/mix/
shape — NOT the final output. This differs from PAFSynth's feedback shape;
where the sibling and the reference disagreed, the reference won.

Not schema-exposed: the reference hardcoded it, and MEMLCelium has no voice
spaces to vary it by. If feedback depth becomes a research axis it wants to be
a runtime parameter — noted, not built.

Verification, and a gap worth recording: parity-check.sh PASSES (max delta
2.38e-7) but that is NOT evidence for this change — its harness only exercises
PAFSynth and ChannelStrip, and the impulse baseline runs all-params-0.5, under
which voice 0's sequencer never triggers, so v0_env stays 0 and the new write
is 0 either way. That baseline is blind to this path, not a check on it. So it
was cross-checked directly instead: sequencer params forced to trigger early,
4800 samples native vs WASM through the nisps_engine_* C ABI —

  native vs WASM max abs diff  1.19e-7   (tolerance 1e-5)
  all 4800 samples nonzero, finite, max amplitude ~0.99 (tanh-bounded)
  same run with gain forced to 0 diverges by up to 1.49 — not a no-op

No fixture regenerated. nisps.wasm ships here because it is a tracked artifact
under the Phase 0 freshness gate.

Follow-up: the parity harness covers 2 of 8 engines. "Firmware and WASM share
the same engines" is asserted repo-wide but tested narrowly.
2026-07-21 17:04:35 +02:00
monkey-w1n5t0n
c19d84619e feat(firmware): bump memllib to current upstream and dissolve the fork
The §7.5 decision was "rebase our commits onto upstream, then vendor". On
inspection there was no rebase to do, and the inventory that recommended it
(7a30da9) was wrong on two counts — both corrected in this commit.

All three fork commits touch only `examples/`. That directory is not in the
sketch symlink forest (firmware/MEMLNaut-NISPS/src/ links exactly audio,
hardware, interface, synth, utils, PicoDefs.hpp), so it is never compiled.
They existed to let the RL code build against nisps/core *while it was being
ported*, and that port is done — nisps/ml/{jolt,ou_noise,feedback,geo_push}.hpp
cite the upstream sources directly. Two of the 31 commits I flagged as
"work we are missing", `9fcd459 jolts` and `d0d8a72 noise`, are precisely the
ones already absorbed.

So the question was never rebase-or-not but which snapshot to vendor. Current
upstream, because the staleness was already costing us:

  - upstream has DisplayDriver::NavigateToView; the pin does not. The SelfTest
    variant called it and had been failing to compile (worked around in
    b953681). It was written against a newer memllib, not against nothing.
  - e291192 "l r input swap" is a hardware bug fix: the physical L/R input
    sockets are wired to opposite codec ADC channels. Every mode on the old
    pin sees its stereo input backwards.

Verified by building all three variants with arduino-cli:

  SLPWorkshop  145348 flash (+320)  87388 RAM (+4)
  PAFSynth     145300 flash (+312) 107060 RAM (+4)
  SelfTest     141840 flash (+320)  12028 RAM (+4)

Exactly one compile error: the .ino used kSampleRate in a constexpr, and
upstream 1997699 made it a runtime `extern size_t` so a mode can pick its own
rate. constexpr -> const; it is a once-per-second diagnostic print. The uniform
+316 is the AudioDriver/DisplayDriver deltas — the bulky new upstream code
(GrainDelayI16, ReverbI16, ModFXI16, CCSelectView, RLView, VUMeterView,
PSRAMManager) is header-only and unreferenced, so the linker drops all of it.

.gitmodules points at upstream again: with the pin on an upstream commit, the
fork holds nothing the firmware compiles. Phase 0 pointed it at the fork only
because b37fc53 existed on no remote. The fork's feat/nisps-core-swap branch
stays pushed; nothing is destroyed.

Refs: plan §5, §7.5; ALIGNMENT Q4.
2026-07-21 17:04:10 +02:00
monkey-w1n5t0n
7a30da91a8 docs(recon): memllib usage inventory — the §7.5 prerequisite, with a wrinkle
Plan §5 names an inventory of the load-bearing memllib surface as the
prerequisite for the vendor-vs-fork-pin decision. Built it from the firmware
LINKER MAP rather than grep — the same method that would have caught daisysp
(S8) mechanically instead of by reading.

Result: all 24 compiled translation units contribute kept sections to the image;
no discarded-only TU exists. memllib is not a daisysp situation. "The
load-bearing subset" is ~1.8 MB across 84 files — essentially all of memllib
minus examples/. There is no small subset to lift; synth/ alone is 1.3 MB and
all three of its TUs link.

The wrinkle, which was not visible when §7.5 was decided: src/memllib is not a
third-party dependency, it is the LAB's shared library
(MusicallyEmbodiedML/memllib), and our fork is 3 commits ahead but 31 commits
BEHIND — +2034/-153 across exactly the files we link, including "new staticmlp",
"jolts", "noise", "verb". Vendoring as-is freezes that gap permanently.

Recommendation recorded: rebase the three small NISPS commits onto upstream main
FIRST, then vendor. Same self-contained end state, but it does not silently
discard work that sounds directly relevant, and it is the only moment that merge
is cheap — after vendoring there is no upstream history to merge against.

Flagged for an operator call before the PlatformIO cut, which this gates.
2026-07-21 14:12:55 +02:00
monkey-w1n5t0n
b953681157 fix(firmware): restore the SelfTest build — it called a DisplayDriver method that does not exist
Found by actually building the firmware, which nothing in this repo does: CI
skips it, run-all-tests.sh skips it, and lint skips it (S9). The SelfTest
variant did not compile:

  glue/selftest.hpp:650: error: 'class DisplayDriver' has no member named
  'NavigateToView'

Pre-existing, not caused by this audit's work: no commit in this series touched
selftest.hpp, and `NavigateToView` appears nowhere in memllib at the pinned
commit. DisplayDriver's actual surface is AddView / InsertViewAfter /
ChangeView(delta) over a carousel.

The call was redundant as well as wrong. DisplayDriver::Setup() leaves
currentViewIndex_ at 0 and the selftest adds exactly one view, so that view is
already the one on screen. Deleting the call restores the build with no
behavioural change on device; a comment records why, since the intent
("navigate to my view") reads as necessary.

Worth noting against Phase 1: SineOsc was KEPT there specifically because "the
firmware selftest uses it". That was still the right call — deleting it would
have added a second breakage to a variant that already had one — but the
justification was weaker than it looked, because the selftest had not compiled
for some time.

Verified by building three variants end to end with arduino-cli, which also
constitutes the first real check of this audit's firmware-side deletions
(daisysp, input_router.hpp, the perf macros, the peripherals helper extraction):

  SLPWorkshop  flash 145028 B   RAM  87384 B (16%)
  PAFSynth     flash 144988 B   RAM 107056 B (20%)
  SelfTest     flash 141520 B   RAM  12024 B  (2%)

Those numbers are also the first per-variant size baseline this project has had
(plan §6.5f wants exactly this as a gate).

Incidentally confirms A6 live: `scripts/build-firmware.sh` rewrote the committed
.ino to select each variant, and the working tree had to be restored after every
build. That is what the PlatformIO migration deletes.
2026-07-21 14:10:05 +02:00
monkey-w1n5t0n
fae519092f docs: sync MAP with Phase 3; burn down plan §4 and hand S26 to the operator
MAP.md: codegen's generated/ now owns ParamSchema and the per-mode MLP aliases;
new shared headers (dsp/ratio_seq.hpp, dsp/seq_clock.hpp, core/event_queue.hpp
with a note on why it is deliberately not RingBuffer); backends/base-backend.ts;
the seed script and codegen/templates bullets removed; the synth-midi-cc.json
path corrected to its sources/ subdir.

Plan §4 burned down. S26 is NOT done — it is now an operator decision with the
per-field inventory that makes it cheap, recorded in the plan: default_spread is
already wired (the audit's "zero consumers" was a quarter wrong);
default_learning_rate/default_max_iterations are unread but every schema carries
exactly the values already hardcoded, so wiring them is numerically a no-op
today; input_channels is codegen-time-validated and carries real meaning for
sound_analysis_midi; and per-param curve is a trap — it is descriptive of
squaring the engines already do internally, so wiring it would double-apply on
35 params.
2026-07-21 14:03:16 +02:00
monkey-w1n5t0n
7ce4598a0b chore(schemas): retire the clobbering seed script; fix the direction-of-truth text
Phase 3 (L39, ST13, ST9, ST10).

- L39: deleted codegen/seed-midi-devices.ts. It regenerated — and therefore
  silently CLOBBERED — the hand-tuned device schemas it had originally created.
  Its purpose is served and git retains it. Confirmed nothing invokes it (the
  root package.json that once might have was deleted in Phase 1).
- ST13: moved the provenance artifact out of the repo root. AUDIT CORRECTION:
  the finding's proposed destination, schemas/midi_devices/synth-midi-cc.sources.json,
  would have BROKEN codegen and CI — generate-midi-devices.ts treats every *.json
  directly under that directory as a device template and ajv-validates it, and
  the sources file has no device_id/midi/params, so the generator would exit
  non-zero. It went to schemas/midi_devices/sources/ instead.
- ST9: schemas/schema.json's own description stated the direction of truth
  BACKWARDS. Rewritten: schemas are canonical, codegen emits C++ and TypeScript.
  (The audit cited params_notes.md; the real path is schemas/modes/params_notes.md.)
- ST10: codegen/README.md rewritten against current reality — it described
  retired playground paths and a phantom temp dir.

Descriptions are never emitted, so this is behaviour-inert; proven by
regenerating all four generated directories byte-identically.
2026-07-21 14:03:07 +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
96737a3d42 refactor(engines): extract the shared sequencer machinery
Phase 3 (L8). Pure statement-for-statement relocation into
dsp/ratio_seq.hpp, dsp/seq_clock.hpp and core/event_queue.hpp.

AUDIT CORRECTION: L8 says "breakor and elysiamorf duplicate ratio_seq". That is
false — ElysiamorfEngine has no ratio_seq at all; it triggers continuously via
FM operators. The real duplicate pair is BreakOrEngine and MEMLCeliumEngine,
whose copies are byte-for-byte identical. Elysiamorf did share the clock and
event-queue machinery, so it uses those. memlcelium now includes the shared
ratio_seq too, which is what actually closes this finding.

Deliberately NOT folded into core/ring_buffer.hpp: RingBuffer is an
atomics-based cross-core SPSC channel (its header says so), whereas the engines'
event queue is produced and drained on one thread. Reusing it would have meant
paying for atomics to serve a single-threaded FIFO. The distinction is now
recorded in MAP.md so the next audit does not read them as duplicates.

Bit-exactness: verified the MIDI-clock tick and bar-phasor tick preserve the
original operation order with no floating-point re-association, and that
EventQueue keeps the original `% N` indexing rather than adopting RingBuffer's
bitmask. The golden suite (nisps_golden_tests) and the native<->WASM parity blob
both pass unchanged — they are the check, and they were not re-baselined.
2026-07-21 14:02:23 +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
a1a158b8d0 docs: burn down plan §3, record Phase 2's two deliberate deviations
Plan §3 marked burned down, noting where execution departed from the written
plan and why: L35 deleted rather than relocated (zero consumers), and S35's
describe ABI widening forcing two out-of-scope buffer fixes. Also records the
package.json test-glob bug found en route — new unit tests were silently not
being run.

ALIGNMENT defect 5's trailing sentence updated: the behaviour bugs it listed as
pending are fixed.
2026-07-21 13:23:11 +02:00
monkey-w1n5t0n
8c44a51220 fix(gates): close the no-heap lint's false negatives and parity's silent FAIL
Phase 2 (S30, L52, L51). These are the gates that are supposed to protect the
core's headline constraints, so the fix is demonstrated rather than asserted.

- S30: lint-cpp.sh's heap audit hardcoded dsp/engines/ml/modes, so nisps/core/
  and nisps/pipeline/ — the P4 control-rate hot path — were NEVER scanned. Its
  comment handling also post-filtered with `grep -v ' *//'`, which misses
  trailing comments. Coverage is now exclusion-based (everything under nisps/
  except wasm/, tests/, build/), comments are STRIPPED before matching using the
  same perl strip audit_float_suffix already used, and the pattern set is
  extended. dynamic_storage.hpp remains deliberately allowlisted — it is a real,
  documented heap user that #errors on RP2350.

  PROOF (run by me, not just reported): planted `new float[4]` in
  nisps/pipeline/input_chain.hpp plus a std::malloc in the trailing-comment form
  the old filter skipped.
      old lint: "[lint-cpp] clean", exit 0     <- the false negative, live
      new lint: FAIL, both lines named, exit 1
  Plants removed; lint clean and exit 0 again; tree verified unmodified.

- L52: parity-check.sh's FAIL branch was unreachable — under `set -e` the script
  died at the diff command before it could print anything. A mismatch therefore
  failed silently. The diff now runs inside the `if` condition, where set -e does
  not apply. Verified by forcing NISPS_PARITY_TOL=0: old script exited silently,
  new one prints FAIL with the exit code.

- L51: parity_check.cpp and parity_wasm.mjs headers documented 4 stages / blob
  v1 against a real 7-stage / v5 implementation. Comment-only rewrite; parity
  behaviour unchanged (re-ran the gate: PASS, 1273 floats within 1e-5).

Note for future work: the lint now permanently scans nisps/core/ and
nisps/pipeline/, so changes there are enforced that were not before.
2026-07-21 13:23:11 +02:00
monkey-w1n5t0n
1a78ed9597 fix(vcv): close the audio-thread race and remove JSON from process()
Phase 2 (L34, L35). Both findings' line citations were accurate this time.

- L34 data race: process() (Rack's audio thread) called
  imlShadow.get_example_features()/get_example_labels() directly on the WORKER
  thread's private engine — which the file's own THREADING INVARIANT comment
  says only the worker may touch — while workerLoop() concurrently
  clear/refills those same std::vector<std::vector<float>> members via
  load_examples(), train_(), randomise_weights and clear_dataset. Unsynchronised
  reader/writer on a non-atomic vector: undefined behaviour.
  Fix extends the staged handoff the file ALREADY uses for pendingWeights
  rather than adding a second mutex: the worker deep-copies features/labels
  into pendingFeatures/pendingLabels at the same instant it copies
  pendingWeights, immediately before weightsPending.store(true), and the flag is
  now released only after the whole batch is consumed — closing an early-release
  window the old code had. process() no longer references imlShadow at all
  (verified: the only surviving mention is a comment).
- L35: process() ran full jansson serialize on every weight-swap OSC push and
  full json_loads + dataFromJson on incoming OSC state — heap-heavy tree work
  on the audio thread. The plan said to move it to the worker. It is DELETED
  instead: reading the actual transport shows both directions talk to nobody —
  osc-client.ts only ever sends {params|input|feedback}, and bridge.ts has no
  state/weights case and explicitly drops other addresses. Relocating
  heap-heavy work to serve a confirmed-zero consumer is complexity without a
  requirement; removing the cause is the smaller coherent design.
  dataToJson/dataFromJson are UNTOUCHED — they remain the live consumers for
  Rack patch save/load and the .nisps preset menu, both off the audio thread.

Neither is empirically reproduced: a real race needs a live Rack engine under
TSan, which is not available here. Justified by reading, and verified by
`cd vcv && make -j4` (clean) plus the host suite including
test_vcv_iml_parity.cpp, which pins iml.hpp bit-exactly against the core MLP —
iml.hpp was not modified, and parity holds.

Known remaining, pre-existing and out of scope: process() still takes a brief
lock_guard on feedbackMutex to copy a small staged struct, and several config
fields (slewMs, oscPort, output/input range flags) are written by the UI thread
without atomics.
2026-07-21 13:23:11 +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
bf3d088ff1 docs: sync MAP/ALIGNMENT/AGENT-REFERENCE with the Phase 1 sweep
The three top-level orienting docs are each falsified by several of the eight
preceding commits, so they land here as one sync rather than being split
across commits that would each leave them half-true. Same phase, same push.

MAP.md — removed fixed_buffer.hpp, voice_space.hpp, the src/daisysp entry and
its symlink from the sketch-tree list, input_router.hpp/wire_inputs (inputs are
wired by bind_peripherals now), test_fixed_buffer.cpp, SplitStage/ReadoutStrip/
InputMini, BackendAdvanced.tsx, and feedback/rng.ts; corrected the primitives
count 12 -> 7; replaced the "engine LIFTED from playground/src" provenance;
rewrote the perf-attribute convention and deleted the NISPS_AUDIO_FUNC gotcha
(both macros and gotcha are gone).

docs/AGENT-REFERENCE.md — the memory-section-attribute instruction now names
only the macros that exist; input_router.hpp dropped from the firmware tree;
symlink list and the submodule-init note no longer mention daisysp.

ALIGNMENT.md — defect 5 (dead mass and registry sprawl) rewritten: the deletion
half is done, so the entry now scopes to what actually remains, which is the
registry/dual-truth half (Phase 3) plus the stale specs (docs pass). Per the
ALIGNMENT convention this is a rewrite-to-current, not a checkbox.
2026-07-21 12:49:39 +02:00
monkey-w1n5t0n
ffac04090c refactor(firmware): delete vendored daisysp and the input_router layer
Phase 1 group 8 (S8, ST2, L14).

- S8: the vendored src/daisysp tree (96 files, 41 .cpp compiled into every
  firmware build) and its sketch-tree symlink. Zero consumers — nisps replaced
  daisysp's PitchShifter with a custom granular implementation. Corrected
  firmware/README.md and setup-firmware-toolchain.sh, which called it a live
  submodule. The attribution comment in nisps/dsp/pitch_shift.hpp stays: it is
  an honest provenance note about a port, not a dependency.
  Noted while verifying: src/memllib/examples/KassiaAudioApp includes
  ../../daisysp/..., but examples/ is not symlinked into the sketch tree and is
  never compiled by the firmware build, so the deletion stands.
- ST2: input_router.hpp was a zero-logic speculative layer with one consumer;
  the .ino now calls bind_peripherals directly.
- L14: peripherals.hpp — deleted kAnalogInputCount, PeripheralBindings and the
  unused spread local, and extracted the duplicated commit-and-train block into
  one helper. The audit's suggested commit_and_train(mode, feedback) signature
  could NOT be behaviour-identical: repositioning() implies placing() (both
  live in ExploreState::Placing), so dispatching on feedback state inside the
  helper would silently reroute a TogB2 press mid-reposition from commit_place
  to commit_reposition, which differs (no snapshot restore, clears
  reposition_). Implemented as commit_and_train(mode, feedback, bool
  reposition) with the branch decided at the call site, preserving behaviour.

Firmware is not compiled by any gate yet (that arrives with the PlatformIO
migration, plan §5/S9), so this group is verified by reading and grep only.
Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:49:25 +02:00
monkey-w1n5t0n
53da84c425 chore(vcv): delete the dead test rig
Phase 1 group 7 (L33). vcv/test/smoke_test.cpp included a header that no longer
exists (a retired nisps-core path), asserted the pre-P6 2x12 module shape, and
ran in no gate; a compiled smoke_test binary was tracked alongside it. Removed
the directory plus Makefile.dist, updated BUILDING.md's two references, and
deleted the unreachable reply-to-sender branch in osc_server.hpp.

Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:49:25 +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
ea588e79cd refactor(nisps): delete dead engine/mode mass
Phase 1 group 3 (L3, L9, L4, L5, L6, ST1).

- L3: nisps/modes/voice_space.hpp — entirely dead, no includes anywhere.
- L4: deleted SawOsc and SquareOsc. KEPT SineOsc — a verifier caught that the
  original reviewer's grep missed its live consumer (the firmware selftest);
  re-confirmed here before touching the file.
- L9: removed the no-op VoiceSpace enum/table/setter boilerplate from the five
  engines with no real voice spaces; kept it on PAFSynth, VerbFX and
  ChannelStrip, which have real ones. Every engine member was checked against
  nisps/wasm/bindings.cpp, firmware/ and tests/ for callers first.
- L5: ModeBase::input_dirty_ was write-only state — deleted the flag rather
  than making it gate inference, which would have been a behaviour change.
- L6: VerbFXEngine's delay_to_verb_ (computed 12x per block, never read),
  enable_reverb_, enable_delay_to_reverb_ and the unused set_enable_* setters.
- ST1: rewrote the four engine header comment blocks that described
  implementations which do not exist.

L7 (MEMLCeliumEngine's inert feedback path) is deliberately NOT done — tracing
git history showed feedbackGain went 0.1f (live) -> "0; //0.1f" (explicitly
muted, value preserved) -> dropped entirely in the port. That is a muted
feature, not dead weight, and deleting it would silently lose it. Left intact
pending an operator decision; see the phase report.

Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:48:50 +02:00
monkey-w1n5t0n
e37f16739e refactor(nisps): delete dead core/ML mass; keep the legacy feedback modes
Phase 1 group 2 (L27, L26, L28, S21, L13, ST6, S20).

- L27: fixed_buffer.hpp + its test + the CMake entry — no consumers.
- L26: dislike_multiplier_ and its doubling/halving bookkeeping — upstream
  InterfaceRL residue that drove nothing. The audit pointed at the wrong test
  file for the surviving reference; the actual assert was in
  test_mlp_geo_dislike.cpp:211, removed here.
- L28: added copy_weights_to(std::span<float>) to FixedStorage and
  DynamicStorage and switched feedback.hpp's take_snapshot/push_undo/nudge to
  it. Drops the permanent whole-net flat_ scratch buffer from FixedStorage and
  the per-gesture double copy. Behaviour-identical: same source values, same
  write order, same RNG draw order in nudge().
- S21 + L13: deleted NISPS_AUDIO_MEM / NISPS_APP_SRAM / NISPS_AUDIO_FUNC —
  zero use sites outside perf.hpp and comments — and rewrote midi_io.hpp's one
  misshapen NISPS_AUDIO_FUNC use as a plain `inline void`. perf.hpp now
  documents only the inlining/hotness macros that actually exist, and
  audio_driver.hpp no longer claims an SRAM discipline the code never had.
- ST6: feedback.hpp's header now describes the four current modes and the
  Geometric default, dropping the retracted "geometric push NOT ported" claim.

S20 — OPERATOR DECISION (§7.1): the four legacy feedback behaviours
(RandomiseOutputs, RandomiseMlp, AvoidStyle::Diffuse, the RandomiseMlp branch of
on_drag) are KEPT, not deleted. They are wanted as building blocks for
experimenting with how different instruments feel under different behaviours.
Each is now marked at its definition as deliberately-retained research reserve
so future audits stop flagging it as dead code.

L25 (the 16 KB firmware loss-history buffer) is NOT done here — see the phase
report; it turned out to be coupled into the shared mlp.hpp, and its fate
belongs with the browser telemetry build (§7.3 / plan §6.5e).

Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:48:27 +02:00
monkey-w1n5t0n
abb569b287 chore: delete retired-playground artefacts and root relics
Phase 1 of the 2026-07 simplification audit, group 1. All verified dead by
independent grep across scripts/, .github/workflows/, codegen/, manifold/,
firmware/, vcv/, nisps/ and README.md before removal.

- S23 / L55: playground/ — only dist/ was tracked (1.2 MB of bundled JS,
  sourcemaps and a second copy of nisps.wasm). The app itself is preserved on
  branch archive/playground-solidjs and tag playground-solidjs-final.
- S29: the root Playwright rig — playwright.config.js, package.json,
  package-lock.json and tests/e2e/. It served ./playground, a page that no
  longer exists anywhere in the tree, and was invoked by nothing. NOTE: the
  live suite is manifold/tests/e2e/ with manifold/playwright.config.ts, which
  is untouched; every CI Playwright step runs with working-directory: manifold.
- L48: NISPS_CORE_EXTRACTION_PLAN.md + NISPS_CORE_TASKS.md (818 lines of
  extraction relics; docs/specs/plans/ is the sanctioned home for plans).
- L36: data/ — two CSVs from a retired era, plus a tracked LibreOffice lock file.
- L32: .claude/worktrees/agent-ae87fe47/ only. The audit's wording invites
  deleting .claude/worktrees/ wholesale; that would have been destructive —
  the directory holds 12 LIVE registered git worktrees (332 MB), four of them
  on branches with unpushed commits. Verified agent-ae87fe47 is NOT registered
  before removing it; every live worktree is left intact. Cleaning up the rest
  is a separate operator decision.
- ST10 fallout: dropped the dead `port-solidjs` branch trigger from ci.yml and
  the dead `feat/manifold-mission` / `feat/vcv-dist` triggers from
  vcv-plugin.yml — those branches are stale and the SolidJS target is retired.

tests/cpp/ is untouched. Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:47:58 +02:00
monkey-w1n5t0n
aa60fbb466 fix(build): resolve a bare EMCC command name in build-wasm.sh
Third failure uncovered by the restored pipeline. With checkout and the C++
build fixed, CI reached the WASM step and died on `[build-wasm] emcc not found
at emcc`: ci.yml passed `EMCC: emcc`, but the script's existence check
(`[[ ! -x "$EMCC" && ! -f "$EMCC" ]]`) only understands paths, so it looked for
a file literally named "emcc" in the working directory. This has been broken
for as long as the override existed; it was invisible because CI never got past
checkout to run it.

Resolve a bare command name through `command -v` before the check, and drop the
now-redundant EMCC override in ci.yml — setup-emsdk already puts emcc on PATH,
which is what the script's own default looks for.

Verified by invoking it the way CI does: `EMCC=emcc bash scripts/build-wasm.sh`
now builds. Incidentally confirms the new freshness gate is sound — the local
toolchain is the same pinned emcc 3.1.69, and the rebuild reproduced the
committed artifact byte-for-byte.

run-all-tests.sh ALL GREEN.
2026-07-21 12:14:21 +02:00
monkey-w1n5t0n
3552ee46f6 fix(core): order RingBuffer::buf_ before the atomics to unblock the CI compiler
First real signal from the restored pipeline: with checkout fixed, the cpp-tests
job got far enough to fail at `-Werror=stringop-overflow` in ring_buffer.hpp on
GitHub's GCC 13, a failure that had been invisible behind the broken submodule
checkout for a month. Reproduced locally against gcc 13.4.0 (local default is
gcc 14, which does not fire).

The warning is a false positive: GCC anchors the destination object to the
member at offset 0 and reports `buf_[head & kMask]` as writing past
`head_._M_i` (size 8) at offset [16, 268] — offsets that are precisely
buf_[0..63] of a 64-entry, 4-byte ControlEvent array. Adding an explicit
`__builtin_unreachable()` bound hint does not help, because the index range was
never what GCC got wrong. Declaring buf_ first anchors the analysis correctly.

Not a suppression, and behaviour-preserving: RingBuffer has exactly one
production user (ModeBase::events_) and is never serialized, copied, or sent
over a wire, so member order is unobservable. Reasoning recorded at the
declaration so nobody "tidies" the order back.

Verified: full ctest suite green under gcc 13.4.0 (the CI compiler) as well as
gcc 14.2, and scripts/run-all-tests.sh ALL GREEN (parity 1273 floats within
1e-5, lint, 33 Playwright specs).
2026-07-21 12:08:52 +02:00
monkey-w1n5t0n
8c249ea8af ci: restore verification — reachable submodule pin, codegen + WASM freshness gates
Phase 0 of the 2026-07 simplification audit (plan §1). CI has been 100% red on
main since 2026-07-13 and every "gates green" claim since rested on local runs.

- S7 / critic gap 2: push memllib `feat/nisps-core-swap` (3 commits incl. the
  pin b37fc53) to monkey-w1n5t0n/memllib and repoint .gitmodules at the fork.
  Those commits existed on exactly one disk; `git ls-remote` now resolves the
  pin, so `submodules: recursive` checkout and fresh clones work again. Drops
  the compensating unreachable-pin error paragraph in build-firmware-arch.sh.
- S24 / S31: the manifold-tests job regenerates from schemas/, runs the codegen
  golden test, and fails on a dirty diff — the "schema changes ship with both
  generated outputs" rule is now enforced rather than assumed.
- S32: a WASM freshness gate runs the parity harness against the *committed*
  manifold/public/nisps.{js,wasm} before the CI rebuild overwrites it. That
  artifact is what the webhook ships to production, so a stale commit now fails
  loudly instead of shipping.
- critic gap 3 / operator decision §7.4: the VPS webhook
  (~/.config/webhooks/meml-deploy.sh, not in this repo) waits for the `CI`
  workflow to conclude success on the pushed SHA before building. Fail-closed;
  MEML_SKIP_CI_GATE=1 for an emergency hand-deploy. Verified the gate query
  returns `failure` for fa37047, i.e. it would have blocked that deploy.
- S31: corrected run-all-tests.sh's false "single command CI invokes" header.

Docs moved with the code: ALIGNMENT defect 1 deleted (resolved) and the rest
renumbered; MAP.md's unreachable-pin warning replaced with the fork pin and a
pointer to the §7.5 vendoring decision; ONBOARDING documents the deploy gate
and the tracked-WASM-ships-to-prod hazard; plan §1 marked burned down.

Gates: scripts/run-all-tests.sh ALL GREEN (ctest 4/4, parity 1273 floats within
1e-5, lint, typecheck, 33 Playwright specs).
2026-07-21 11:57:32 +02:00
monkey-w1n5t0n
fd0aee2354 docs(audit): simplification audit 2026-07 — recon findings, phased plan, ALIGNMENT rewrite
66-agent adversarially-verified audit vs the five-bullet one-core vision.
113 findings: CI red since 2026-07-13 (memllib pin on no remote), ungated
deploys, unshared mode layer, no curated/advanced split, dead-mass inventory.
Recon: docs/specs/recon/simplification-audit-2026-07.md (immutable).
Plan: docs/specs/plans/simplification-plan.md (proposal; phases gated on
operator adoption, §7 decisions). ALIGNMENT rewritten; MAP flatly-false
lines fixed (phantom MEMLCelium-upstream entry, exploration.ts, daisysp
non-submodule, pre-P5 sentence, perf-attr claims); MAIN registry updated.
2026-07-21 01:24:35 +02:00
monkey-w1n5t0n
fa3704765c docs(plan): P6 burned down; execution wrap-up — all phases landed, hardware chokepoints remain operator items 2026-07-18 13:03:21 +02:00
monkey-w1n5t0n
f028f1256d feat(vcv): merge P6 — module reunified onto core MLP (closes vcv delta #5) 2026-07-18 13:02:28 +02:00
monkey-w1n5t0n
1b69254de2 feat(vcv): reunify module onto core MLP — thin iml.hpp adapter (P6)
Replace the vendored runtime MLP in vcv/src/iml.hpp (DetRng + 3D-weight-store
MLP + Dataset + IML) with a THIN, Rack-free adapter over the shared core:
nisps::ml::MLPCore<nisps::ml::DynamicStorage> (8->[16,24,16]->16, the P2 dynamic
case), nisps::Rng, and the core MLP's own FIFO dataset. Behaviour changes from
the vendored approximation to core-exact firmware/WASM semantics.

- MEMLNaut.cpp: staged/pending weight buffers and patch JSON now use the core's
  flat [weights..][biases..] vector (nisps::IML<float>::Weights); patch version
  bumped to 3. Double-buffer / single-writer threading discipline unchanged.
- New ctest tests/cpp/test_vcv_iml_parity.cpp: seeded train/infer/move_weights
  session through the adapter is memcmp-equal to a bare MLPCore<DynamicStorage>.
- Docs: vcv-module.md delta #5 marked CLOSED (2026-07-18); MAP.md vcv/ updated.

Closes vcv-module.md delta #5.
2026-07-18 13:01:28 +02:00
monkey-w1n5t0n
e1713ae41f docs(plan): P5 burned down 2026-07-18 12:47:15 +02:00
monkey-w1n5t0n
161ce155e4 feat(manifold): merge P5 — MF_MODES from schema truth; per-mode engine dims real 2026-07-18 12:46:24 +02:00
monkey-w1n5t0n
28e394ddf3 docs+ci: P5 doc sync; codegen golden wired into run-all-tests stage 5 2026-07-18 12:45:45 +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
bd35432797 docs(plan): P4 burned down — pipelines/curves in core, browser firmware-exact
MAP: nisps/pipeline entry + manifold engine listing; ALIGNMENT: curve-maths
unification note; plan gate evidence incl. the proven-inherent f32 momentum
drift and parity stage 7.
2026-07-18 12:23:14 +02:00
monkey-w1n5t0n
a7c1f29aa6 feat(manifold): merge P4 — pipelines + curves run the WASM core; TS ports deleted 2026-07-18 12:22:21 +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
1672fe3474 feat(pipeline)!: P4 core — input/output chains + curve catalog in nisps/
- nisps/pipeline/input_chain.hpp: faithful f32 port of the manifold input
  pipeline (invert→deadzone→circular clamp→momentum-modulated zoom→centred
  power→EMA→momentum update). Caller-supplied dt, internal accumulated
  clock (no wall clock — deterministic; matches the P1 fixtures' clock
  contract). Fixed-capacity velocity ring; serialisable state.
- nisps/pipeline/output_chain.hpp: curve→EMA→slew→freeze(+per-output mask)
  chain, capacity-templated (browser cap 4096; firmware would use NOut).
- nisps/core/math.hpp: + centered_power(x, exponent) (both chains use it).
- bindings: nisps_pipeline_create/destroy, nisps_input_set_config(15-float
  wire layout)/process/reset, nisps_output_set_config/set_freeze_mask/
  process/reset, pipeline state save/load, nisps_curve_apply(+batch)
  (ids 0-6 = Curve enum, 7 = centred power).
- parity v5 Stage 7: rational (transcendental-free) traces through both
  chains (2 configs each) + full curve catalog — 1273 floats PASS, the
  pipeline floats bit-identical native↔WASM.
- ctest test_pipeline.cpp: deadzone remap, circular clamp, zoom+freeze,
  sticky anchor, frame-rate-independent EMA, momentum zoom-out/recovery,
  state round-trip, slew, freeze gate/mask, reseed-on-length-change,
  centred-power endpoints.

Part of one-core-engine-refactor P4; the manifold TS switch follows.
2026-07-18 11:52:53 +02:00
monkey-w1n5t0n
1f4513802f docs: P3 software burned down — ADR §8 ALIGNMENT updates, MAP feedback/replay entries
- ALIGNMENT: retract the 2026-06-18 'geometric push not ported' accepted
  divergence (it IS ported); record the two deliberate divergences
  (useRandom via nisps::Rng; synchronous per-press SGD vs upstream's
  shuffled TrainBatch — behavioural parity with firmware, 1e-5 native↔WASM)
- MAP: nisps/ml gains replay/geo_push/warm_start; feedback is storage-
  policied with Geometric default + Diffuse legacy
- AGENT-REFERENCE: Jolt/OU/dislike TS-math limitation closed
- plan: P3 software marked landed; chokepoint A pending hardware + bug
  10c3e55c (explore wiring linker-GC'd out of PAFSynth ELF, pre-existing)
2026-07-14 04:56:18 +02:00
monkey-w1n5t0n
a36b58dce8 feat(manifold): merge P3 TS switch — geometric dislike + jolt/OU on the C++ core 2026-07-14 04:55:25 +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
9490e20a7a feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):

- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
  store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
  deterministic index tie-break + fixed accumulation order, proportional
  decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
  *0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
  deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
  toward computed targets (negative lr = cold-start train-away); solo/
  focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
  move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
  press+optimise into one synchronous call; on_up in geometric Avoid
  feeds the positive centroid; dislike-multiplier bookkeeping. Storage
  gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
  Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
  positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
  nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
  nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
  over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
  f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
  tie-break, push direction/taper/mask/clamp, cold-start inertness +
  train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
  Diffuse per the ADR's deliberate-break note.

Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
monkey-w1n5t0n
7e957457cd docs: P2 complete — burn down plan, close ALIGNMENT defect #3 (fixed WASM arity)
- plan P2 marked landed (hardware timing spot-check deferred to the
  chokepoint-B session on physical MEMLNaut)
- ALIGNMENT: defect #3 resolved by the runtime-shaped browser MLP
- MAP/AGENT-REFERENCE/nisps-wasm-README: describe the honoured-dims
  create + reshape ABI; note per-mode dims become schema-real at P5
- fresh wasm artifacts from the gate run
2026-07-14 03:56:04 +02:00
monkey-w1n5t0n
7e4dc26f6e feat(manifold): merge P2.3 — runtime reshape wiring + reset-on-reshape modal 2026-07-14 03:55:07 +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
8a19e5b52c refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:

- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
  classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
  kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
  compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
  at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
  (new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
  a lint check that fails if the #error guard disappears.

Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
  init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
  layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
  RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}

Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00