memlnaut-nisps/codegen
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
..
tests refactor(ml): one runtime-configurable training default (S26) 2026-07-21 17:20:10 +02:00
.gitignore Add codegen tool: schemas -> C++ headers + TS modules 2026-04-29 15:29:08 +03:00
generate-midi-devices.ts refactor(codegen): codegen owns mode identity, per-mode schemas and net dims 2026-07-21 14:02:23 +02:00
generate.ts refactor(ml): one runtime-configurable training default (S26) 2026-07-21 17:20:10 +02:00
lib.ts refactor(codegen): codegen owns mode identity, per-mode schemas and net dims 2026-07-21 14:02:23 +02:00
package.json Add codegen tool: schemas -> C++ headers + TS modules 2026-04-29 15:29:08 +03:00
README.md chore(schemas): retire the clobbering seed script; fix the direction-of-truth text 2026-07-21 14:03:07 +02:00
tsconfig.json Add codegen tool: schemas -> C++ headers + TS modules 2026-04-29 15:29:08 +03:00

codegen — schemas → C++ + TypeScript

Bun scripts that turn the JSON schemas under schemas/ into generated code for both targets. The per-file inventory lives in MAP.mdcodegen/) — this README only covers how to run it.

  • generate.ts — validates schemas/modes/*.json against schemas/schema.json, emits nisps/modes/generated/<mode>_schema.hpp (+ schema_types.hpp) and manifold/src/modes/generated/<mode>_schema.ts (+ types.ts, index.ts).
  • generate-midi-devices.ts — validates schemas/midi_devices/*.json against schemas/midi_device.schema.json, emits nisps/midi/generated/midi_devices.hpp and manifold/src/midi-devices/generated/{types,devices,index}.ts.

Both validate with ajv (Draft 2020-12), exit non-zero on failure, and are idempotent (re-running with unchanged schemas is byte-identical).

Run

cd codegen
bun install
bun run generate.ts               # mode schemas
bun run generate-midi-devices.ts  # MIDI device templates

Schema or codegen changes ship with the regenerated C++ and TypeScript in the same commit — CI re-runs both generators and fails on any diff.

Golden test

tests/golden_test.ts re-runs generate.ts against the live output dirs, diffs paf_synth_schema.{hpp,ts} against the snapshots in tests/golden/, then re-runs to prove idempotency:

bun run test

After an intentional emission change, refresh the goldens:

bun run generate.ts
cp ../nisps/modes/generated/paf_synth_schema.hpp tests/golden/
cp ../manifold/src/modes/generated/paf_synth_schema.ts tests/golden/

Adding a mode / device

Drop the new JSON into schemas/modes/ (or schemas/midi_devices/), run the matching generator, and commit the JSON plus the regenerated outputs together.