Commit graph

445 commits

Author SHA1 Message Date
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
ec3118004d fix(ml): re-base the geometric dislike on upstream e291192 — delete the taper
geo_push.hpp and replay.hpp cited memllib @ 0a541cc. upstream/main pins
e291192, where the same code had been deliberately redesigned — and because
InterfaceRL was not in the tree (fixed one commit ago), we carried the
superseded version for months. Three changes, all upstream's:

  kGeometricPushScale   0.5 -> 1.0    (InterfaceRL.hpp:409)
  kNegLRBase            0.5 -> 1.5    (InterfaceRL.hpp:410)
  /(1+len) taper        deleted       (InterfaceRL.tpp:724)

Upstream's own comment on the taper: "a 'no' should clearly move the mapping
away even from a sound already far from the liked region (the taper used to
kill exactly that case)". The direction is already a unit vector, so the
taper only ever shrank the push for exactly the sounds a user is most likely
to be rejecting.

Cold start is folded into the same path. Upstream's useRandom is
`!havePositives || len <= 1e-4`: with nothing liked yet there is no centroid
to push away from, so every dim goes in a random direction. Ours instead
kept the older 0a541cc fallback — train AWAY from the heard action at a
NEGATIVE lr — which was inert whenever the heard action equalled the net's
own output, i.e. in the common case. One path now, and a "no" moves the
mapping before any likes exist (ml_bench E1: 0 -> 2.3e-3). The
GeometricColdStart action is still reported so callers keep their "like a
few sounds first" prompt; only the training changed.

Measured (ml_bench, one dislike at a point):
  A4  0.0157 -> 0.0533 at-point displacement (3.4x), so end to end across
      this and the RMSProp fix: 5.3e-5 -> 5.3e-2, ~1000x. The gap to the
      legacy Diffuse design closes from ~4100x to ~4.2x.
  D1  effective_lr 4.7e-4 -> 1.5e-3; 10 presses now reach 0.34, 100 reach
      the full intended push.
  A5  compounding 0.87 -> 0.96 (a second press at the same spot is no
      longer noticeably weaker than the first).
  A7  damage_ratio essentially unchanged (0.87-1.57) — the collateral
      damage to protected positives scales with the push and is NOT
      addressed here; it is the negative-feedback design question.

NOT adopted, deliberately: upstream's per-tick batch retraining over all
live negatives, and its fixed kDislikeLifetimeMs=2500 in place of our
proportional decay. Both need something the core does not have — a per-tick
call site and a millisecond clock inside nisps/ml — so they change
FeedbackControllerCore's interface rather than its constants. Recorded in
ALIGNMENT's deferred-debt entry alongside the existing one-press-one-step
divergence, and filed as its own task.

test_mlp_geo_dislike.cpp: the taper test now pins its ABSENCE (equal
displacement near and far), the cold-start test pins movement where it used
to pin inertness, and a new test covers the random-direction branch.

ALIGNMENT defect 6b resolved. Gates: build-cpp-tests 139 tests / ctest 4/4,
parity-check PASS (WASM rebuilt), lint-cpp clean, firmware slpworkshop
SUCCESS.
2026-07-25 11:22:15 +02:00
monkey-w1n5t0n
1f0eecfe78 docs(firmware): vendor InterfaceRL as read-only reference
InterfaceRL.{hpp,cpp,tpp} + InterfaceRLFileFormat.hpp are the upstream
reference implementation of the entire NISPS feedback subsystem —
nisps/ml/{geo_push,replay,feedback,jolt,ou_noise}.hpp are all ports of it,
several still carrying `// upstream InterfaceRL.hpp:NNN` line references.
The Phase-4 vendoring dropped examples/ because nothing compiled it. That
was correct for the build and wrong for the codebase: with the source of
truth out of tree, upstream redesigned the geometric dislike (deleted the
/(1+len) taper, doubled kGeometricPushScale, tripled the negative-LR base,
moved to batch training over all negatives every tick) and we did not notice
for months.

Copied verbatim from memllib @ e291192 — the same commit the rest of the
vendored tree pins — into lib/memllib/reference/, which sits OUTSIDE src/
and is therefore never compiled: PlatformIO's LDF only recursively builds an
Arduino-format library's src/ folder. Verified: slpworkshop still builds
(RAM 28.8%, flash 2.1%).

reference/README.md states the two rules (never compiled, never edited — a
divergence from upstream is a recorded decision, not an edit here) and
VENDORED.md's "what was dropped" section now tells the truth.

Resolves ALIGNMENT defect 6c.
2026-07-25 11:14:35 +02:00
monkey-w1n5t0n
f57cddc278 fix(ml): port RMSProp — ported learning rates were landing in SGD
Upstream memlp (github.com/MusicallyEmbodiedML/memlp @ ea777502, the commit
upstream/main pins) applies gradients with RMSProp everywhere: Layer.h:239
ApplyAccumulatedGradients, the m_sq_grad_avg running average at Layer.h:601,
StaticMLP.h:268. nisps/ml/training.hpp shipped SGD only and filed the
difference as an optimiser-choice research question. It was not one.

RMSProp divides each step by the running gradient magnitude, so an upstream
lr is a NORMALISED step; under SGD the same number multiplies the raw
gradient. Every learning rate ported from upstream therefore landed in an
optimiser that reads it differently — most visibly feedback.hpp's
`geo_lr_ = 0.001f  // upstream InterfaceRL.hpp:312`, an RMSProp LR pasted
into a single SGD step.

rmsprop_step() ports Layer.h:239 exactly: clip at +/-10, sq = min(0.9*sq +
0.1*g^2, 1e6), adj = min(lr/(sqrt(sq)+1e-6), 1.0), w -= adj*g. The
adjusted-LR clamp stays one-sided as upstream's std::min is, so the negative
lr used by train_targets' "train away from this target" path behaves as it
does upstream. The per-weight squared-gradient average is new persistent
state and lives in the storage policies (FixedStorage arrays /
DynamicStorage arena) so nisps/ stays allocation-free and the firmware's
zero-heap contract holds. It is optimiser state, not model state: excluded
from weight_count()/get_weights()/set_weights(), matching upstream, and
cleared by MLPCore::reset_optimizer_state() (upstream ResetOptimizerState).
draw_weights() deliberately does NOT clear it — upstream's DrawWeights
doesn't either.

Measured with tests/cpp/ml_bench.cpp:
  D1  one geometric dislike moves the mapping 1.6e-2, up from 5.3e-5 (~295x),
      and repeated presses now CONVERGE on the intended 0.5 push (0.12 at 10,
      0.56 at 100) instead of creeping linearly forever.
  A4  geometric-vs-Diffuse gap narrows from ~4100x to ~14x in one press.
  U4  the upstream-LR positive path actually trains now (range_util 0.71 at
      100 ticks/gesture, was 0.016 — it was inert under SGD).
Not fixed by this, and now tracked as ALIGNMENT defect 6d: the dose
asymmetry. lurch_max is still ~1.08 against a [0,1] output range.

Golden vector stages 2 and 3 re-captured; stages 0 and 1 are pre-training
and did not move, which is the cross-check that only the update rule
changed. manifold/public/nisps.wasm rebuilt so parity-check compares like
with like — it FAILED at up to 5e-2 against the stale artifact and PASSES at
2.4e-7 against a fresh one. parity-check.sh only builds the WASM when it is
missing, never when it is stale; noted in MAP.md and filed separately.

ALIGNMENT defect 6 resolved (moved to Recently resolved); 6b's optimiser
cross-reference updated; new defect 6d for the positive-training dose.

Gates: build-cpp-tests 138 tests / ctest 4/4, parity-check PASS, lint-cpp
clean, manifold typecheck clean.
2026-07-25 11:11:23 +02:00
monkey-w1n5t0n
1603ea798e test(ml): behavioural benchmark + 20 invariants for the control mapping
NISPS is a controller, not a synth: the object of study is the mapping
f: control-space -> parameter-space and how a musician's gestures deform
it. Loss measures fit to points the user dictated, which is the one thing
they never experience. So this measures geometry and gesture-response.

  tests/cpp/ml_bench.cpp          61 scenarios, REPORTS never asserts (same
                                  discipline as engine_bench.cpp). Shape-
                                  agnostic via MLPCore<DynamicStorage>
                                  (--shape, default 2,16,16,16,8), seeded
                                  RNG throughout, branch points replayed
                                  from scratch rather than snapshotted.
  tests/cpp/test_ml_behaviour.cpp 20 asserting invariants, wired into
                                  nisps_core_tests.
  scripts/bench-ml.sh             native + WASM from one source; --compare,
                                  --sweep-shape, --smoke, --scenario, --seed.

Documents two contracts that fail SILENTLY (both now pinned by tests):
a thumbs-up must call BOTH mlp.add_example() and fb.store_positive(),
since dislike_geometric k-NNs the replay buffer and not the MLP dataset;
and placed_output() is valid only while state == Placing, after which an
empty span whose l2() is 0 scores a broken lifecycle as a perfect place.

ALIGNMENT defect 6 re-ranked (SGD-vs-RMSProp is not a research axis - it
silently invalidated every ported hyperparameter) and split into 6b (the
geometric dislike was ported from a superseded upstream design) and 6c
(InterfaceRL, the reference impl, is not in the tree).

Gates: build-cpp-tests (138 tests, ctest 4/4), parity-check PASS,
lint-cpp clean, bench-ml.sh --smoke runs end to end.
2026-07-25 11:02:24 +02:00
monkey-w1n5t0n
60584606a8 docs(specs): P5 architectural specs — 5a, 5b, 5c, 5d
Plan §6 says each P5 item is spec-first with its own session. These are the
specs; no implementation is authorised by them and none was written.

  plans/mode-layer-reunification.md   5a — ALIGNMENT defect 1, the largest
                                      architectural gap. Storage-policies the
                                      ModeBase orchestration the way P2 did
                                      MLPCore, rather than binding monolithic
                                      mode objects into WASM (which would
                                      contradict the locked two-instance RT
                                      architecture).
  plans/browser-mode-coverage-spec.md 5b — an audio-topology notion so Manifold
                                      stops cataloguing 4 modes that
                                      structurally cannot run in a browser.
  plans/curated-presets-spec.md       5c — ALIGNMENT defect 2, built on the
                                      operator's §7.6 definition: a curated
                                      preset is configuration only, network
                                      untrained.
  plans/hardware-editor-spec.md       5d — ALIGNMENT defect 3, applying
                                      useq-celium's existing discipline (C
                                      header as wire truth + TS mirror + parity
                                      test) to a MEMLNaut serial protocol.

2434 lines. I spot-checked their path citations mechanically against
git ls-files: of 170 backticked paths, every unresolved one is either a file
the spec proposes to create or a cross-reference to a sibling spec in this same
commit. None describes deleted code as live — which is the failure mode that
made half the existing corpus untrustworthy, and the reason the §8 pass earlier
today had so much to do.

The preset spec is the most valuable byproduct: en route it found five places
where existing docs still describe a deleted world, including
manifold-parity-features-spec.md §1.1 specifying a PipelineLayer over
engine/input-pipeline.ts and output-pipeline.ts, both deleted at one-core P4.

Also carries the doc sync for the telemetry and benchmark work in a77770f:
AGENT-REFERENCE gains the throughput and loss-history entries, and dock-spec
§1.3 records that its long-deferred diagnostics suite shipped PARTLY — the loss
curve and weight-health table are real, GradientFlow is not built and is not
planned as drawn (the core records no per-layer gradient magnitudes, and the
fabricated version was deleted in Phase 1).
2026-07-21 22:03:39 +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
68d4cc4017 build(firmware): migrate to PlatformIO and vendor memllib (plan §5)
One cut, no dual path. Closes ALIGNMENT defect 3 ("Arduino-CLI build
machinery is actively hostile") and vision bullet 4.

platformio.ini carries 16 [env:], one per variant, each passing
-DMEMLNAUT_MODE_TYPE; selftest passes -DNISPS_SELFTEST=1 instead. The env list
IS the registry now — the .ino comment-registry and the NISPS_ST_* token-paste
table are deleted rather than migrated. L12 noted that table was already
silently missing the currently-shipped SLPWorkshop variant, which is the whole
argument against having a second list.

Also deleted: the Python/sed machinery that rewrote the COMMITTED .ino on every
build, the sketch symlink forest, the global TFT_eSPI User_Setup.h mutation
(now -D flags — TFT_eSPI's own documented PlatformIO recipe), the UF2
boot-mount detection stack (upload_protocol=picotool talks to the bootloader
directly), and build-firmware-arch.sh entirely. Scripts 683 -> 435 lines.

memllib is vendored at lib/memllib/ from upstream e291192; no submodules
remain. VENDORED.md records provenance and the re-sync procedure.

S9: a firmware-build CI job compiles three representative envs against a cached
toolchain and reports per-variant flash/RAM. Firmware is in an automated gate
for the FIRST time. The old ci.yml comment justified excluding it as "low
verification value" — an assessment that did not survive contact, since the
SelfTest variant sat broken for an unknown period calling a DisplayDriver
method that did not exist at the pinned memllib commit, and nothing noticed
because nothing built it.

Verified: all 16 envs build from an empty cache, each within ~520 bytes of the
arduino-cli binary it replaces, flash and RAM. Measured as .text+.rodata /
.data+.bss+vector+uninitialized — NOT PlatformIO's console line, which
double-counts .data on this board. This does not prove the hardware boots; no
flash+smoke test was possible and that stays an operator chokepoint.

  slpworkshop 248232/145028   pafsynth 256880/149716   selftest 216228/17960
  (all 16 in the CI log format; none exceeds 2% of a 16 MB flash)

Two traps recorded so nobody rediscovers them: vendoring memllib's subdirs
without a src/ wrapper makes PlatformIO's library builder silently compile
NOTHING while still linking; and project build_flags land BEFORE the
framework's own -std=gnu++17 -Os, so build_unflags is required.

CORRECTION carried in this commit: the firmware sizes in c19d846's message and
the first version of the memllib recon doc were wrong — SLPWorkshop 145348,
PAFSynth 145300, SelfTest 141840. They came from building variants in sequence
through a SHARED incremental arduino-cli build directory, which reused stale
objects and under-reported by ~75 KB. Clean-cache rebuilds of the identical
commit give 216736/18492 for SelfTest. The real cost of the memllib upstream
bump is +216 bytes flash, not +316. Never measure firmware size through a
reused build dir.

HISTORY NOTE: this commit and the docs commit before it were rebuilt (force-push,
2026-07-21) so that each contains only what its message describes. The first
versions had the firmware deletions stranded in the docs commit by a shared-index
race between concurrent agents; content is byte-identical to the originals.

Gates: run-all-tests.sh ALL GREEN (nisps/ untouched by this change beyond
include paths); 16/16 pio envs build.
2026-07-21 20:17:58 +02:00
monkey-w1n5t0n
9ad1f78ddd docs: the specs disposition pass (plan §8)
Roughly 20k lines were deleted from this repo in the last week and much of the
corpus still described the pre-deletion world in the present tense. Executes
the §8 table: archive the retired, reclassify the executed, prune the stale.

  aimmersive-clone-spec      -> _archive/ with a deprecated-by note
  feedback-modes-port-spec   -> plans/, kind: plan, status: executed
  manifold-parity-features   -> plans/, kind: plan, status: active
  playground-2.0-rewrite     -> status: superseded
  engine-architecture        434 -> ~120 lines; seam + spine kept, rewritten
                             present-tense against the shipped engine/
  MAIN.md                    six contradicted claims fixed; registry resynced
  vcv-module.md              pruned to the current 8->16 contract and made the
                             single .nisps format spec
  vcv/NISPS-FORMAT.md        DELETED — documented a v1 format that no longer loads
  vcv/README.md, BUILDING.md rewritten to the real contract, menu, OSC table
  inputs/backends/dock trio  grounding sections marked historical, dead cites fixed

Two rows of the §8 table were themselves wrong, corrected here: the deleted
full-state sync lives in backends-spec.md §6.3, not vcv-module.md (which has no
§6.3), and codegen/README.md was already a MAP pointer with no port-solidjs
trigger left to remove.

Beyond the table — found by sweeping every backticked path in the changed docs
against `git ls-files`, which is how these should have been caught before:

  manifold/ONBOARDING.md documented a UI that Phase 1 deleted, as if current:
  SplitStage, ReadoutStrip, InputMini, BackendAdvanced, AltitudeNav, and a
  shot.spec.ts that does not exist. The whole stage table was keyed on a `focus`
  axis that no longer exists — selection is now sandwich > particles >
  composite. This matters more than the rest: CLAUDE.md tells every agent to
  read ONBOARDING.md first for Manifold work, so it was actively teaching a
  fiction. Rewritten against ConsoleApp.tsx.

  MAP.md claimed the input layer reduces axes to the engine arity with an
  "even/odd blend". input-layer.ts says the opposite in its own header: one
  dedicated slot per axis, 1:1, into a 32-input over-provisioned head, and
  mean-blending was removed deliberately because it diluted every source.

  AGENT-REFERENCE.md still promised TS emission "returns at P5" (landed),
  per-mode dims "become schema-real at P5" (landed at P5.3), and pointed at
  nisps::FixedBuffer (deleted).

Doc-right/code-suspect, filed rather than fixed: VCV computes derivedMean/Std/
Delta and cachedNovelty behind a live context-menu toggle that nothing reads;
vcv/plugin.json points at the MusicallyEmbodiedML org rather than this repo's
origin; and the module defaults to UDP 7001+id%64 while bridge.ts defaults to
9000, so out of the box they do not meet.

Firmware-build docs are deliberately untouched — the PlatformIO migration
lands next and rewrites all of them.
2026-07-21 20:17:58 +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
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