memlnaut-nisps/playground/src/features/snapshots.ts
Claude (stream-10) 54821fffd0 playground/stream-10: wire phase 1-4 features into modes (meml-5wg)
Builds the SolidJS-side feature parity with the legacy a-immersive
playground. Every Phase 1–4 feature called out in
.local/recon/04-playground.md now flows through the runtime → stores →
primitives pipeline.

Phase 1 (compound axes / zoom / drawer / trail):
  - control-routing.ts: resolves Boldness/Memory/Precision into
    input/output/exploration store writes via createEffect.
  - SettingsDrawer (new): tabbed sections for Input / Training /
    Exploration / Output / Per-param overrides / Advanced. Per-param
    ParamEditor list wires through modeStore.setOverride.
  - JoyMap navigator added to ModeShell; tap-to-return + long-press
    region pin handlers wired into the runtime.
  - Anchor mode toggle (auto/sticky/center) exposed via drawer.

Phase 2 (pinning / history / A/B):
  - features/snapshots.ts: bridge sessionStore push/pop to actual
    Float32Array weights. Auto-snapshot before train/randomize/thumbs-down.
  - Undo button + long-press snapshot list popup wired in ModeShell.
  - A/B capture/toggle/accept/revert in SettingsDrawer.
  - features/overrides.ts buildPinMask() composes override `pinned`
    flags + sessionStore param pins; passed to mlStore.moveWeights.
  - Region pinning long-press → addRegionPin + auto-snapshot tag
    "pinned baseline".

Phase 3 (refinement / exploration):
  - features/heatmap-sampler.ts: 16x16 grid via mlStore.inferBatch,
    three colour modes, throttled 5/sec, refreshed on ml.trained /
    ml.delta_update bus events.
  - exploration-store.ts (new): RL noise level, floor/cap/growth/decay,
    spread, learning rate, weight decay, auto-explore, pressure.
  - Auto-explore timer in mode-runtime drives moveWeights at user
    interval, zoom-scaled intensity.
  - Pressure feedback: window pointerdown/up timestamps mapped to
    explorationStore.setPressure; feeds growNoise/decayNoise.

Phase 4 (output / persistence / polish):
  - features/weight-health.ts: histogram, status, per-layer L2 deltas,
    vanishing/exploding/converged classification.
  - features/session-preset.ts: full state capture/restore +
    base64url URL sharing; main.tsx applies on load.
  - SettingsDrawer Advanced tab renders WeightHealth, GradientFlow,
    LayerStats, Heatmap, session preset save/load/share.

Override application:
  - features/overrides.ts applies per-param mute/freeze/curve/range
    between MLP outputs and the engine. Mode runtime exposes
    `paramOutputs` (length = schema.params) which all modes now bind to
    OutputDisplay so ranges and mutes are visible.
  - Freeze flags push into outputStore.freezeMask reactively.

Mic input:
  - features/mic-input.ts: getUserMedia + AnalyserNode-derived
    {energy, brightness, pitch, aperiodicity}. Mode runtime feeds
    them into channels 2..(input_size-1) when active. ModeShell shows
    a Mic toggle for audio_in modes.

Debug probe (probe.ts):
  - Synchronous bypass for snapshot, A/B, pins, overrides, axes,
    spread, output freeze, heatmap, weight health, session presets,
    URL params, bus emit/on. Untracked reads/writes throughout to
    avoid SolidJS reactivity surprises in tests.

Modes:
  - All 8 firmware mode TSX files now use the default SettingsDrawer
    (no per-mode SliderBank scaffolding). They bind OutputDisplay to
    runtime.paramOutputs (post-override) instead of processedOutputs.

Build status:
  - bun run typecheck: clean
  - bun run build: clean
  - dev server smoke: index, /modes, mode-runtime, SettingsDrawer,
    features/* all serve.

Note: bd close meml-5wg failed because Dolt server unreachable from
this worktree. Issue should be closed manually by orchestrator.
2026-04-29 17:33:01 +03:00

123 lines
3.6 KiB
TypeScript

/**
* Snapshot helpers — bridge sessionStore.pushSnapshot/popSnapshot to the
* actual MLP weights via mlStore. Auto-snapshot before train/randomize/
* thumbs-down.
*/
import { mlStore } from '../stores/ml-store';
import { sessionStore } from '../stores/session-store';
import { explorationStore } from '../stores/exploration-store';
import { inputStore } from '../stores/input-store';
/** Capture current weights + noise level + zoom level. */
export function autoSnapshot(tag: string): void {
if (!mlStore.iml) return;
const w = mlStore.getWeights();
if (w.length === 0) return;
sessionStore.pushSnapshot(tag, {
noiseLevel: explorationStore.state.noiseLevel,
zoomLevel: inputStore.config.zoom,
weights: w,
});
}
/**
* Pop the most recent snapshot and restore weights. Returns true on success.
*/
export function undoLastSnapshot(): boolean {
const snap = sessionStore.popSnapshot();
if (!snap || !snap.weights) return false;
if (!mlStore.iml) return false;
try {
mlStore.setWeights(snap.weights);
explorationStore.setNoiseLevel(snap.noiseLevel);
return true;
} catch {
return false;
}
}
/** Restore a specific snapshot by id. */
export function restoreSnapshotById(id: string): boolean {
const snap = sessionStore.jumpToSnapshot(id);
if (!snap || !snap.weights) return false;
if (!mlStore.iml) return false;
try {
mlStore.setWeights(snap.weights);
explorationStore.setNoiseLevel(snap.noiseLevel);
return true;
} catch {
return false;
}
}
/**
* A/B compare helpers — capture the current weights as A, the next state
* becomes B. Toggle swaps in O(1).
*/
export function captureA(): void {
if (!mlStore.iml) return;
const w = mlStore.getWeights();
if (w.length === 0) return;
sessionStore.captureA({
id: `a-${Date.now().toString(36)}`,
tag: 'A',
timestamp: Date.now(),
noiseLevel: explorationStore.state.noiseLevel,
zoomLevel: inputStore.config.zoom,
weights: w,
});
}
export function toggleAB(): 'A' | 'B' {
if (!mlStore.iml) return sessionStore.state.ab.live;
// Capture the current live state on first toggle.
const cur = mlStore.getWeights();
const live = sessionStore.state.ab.live;
if (live === 'B' && !sessionStore.state.ab.b) {
// First toggle from B → A: capture the current state as B.
sessionStore.captureB({
id: `b-${Date.now().toString(36)}`,
tag: 'B',
timestamp: Date.now(),
noiseLevel: explorationStore.state.noiseLevel,
zoomLevel: inputStore.config.zoom,
weights: cur,
});
} else if (live === 'A') {
// Going back from A → B: update B with the current edits before flipping.
sessionStore.captureB({
id: `b-${Date.now().toString(36)}`,
tag: 'B',
timestamp: Date.now(),
noiseLevel: explorationStore.state.noiseLevel,
zoomLevel: inputStore.config.zoom,
weights: cur,
});
}
const next = sessionStore.toggleAB();
// Apply the side we just switched to.
const target = next === 'A' ? sessionStore.state.ab.a : sessionStore.state.ab.b;
if (target?.weights) {
try { mlStore.setWeights(target.weights); } catch { /* ignore */ }
explorationStore.setNoiseLevel(target.noiseLevel);
}
return next;
}
export function acceptB(): void {
sessionStore.acceptB();
}
export function revertToA(): void {
if (!mlStore.iml) {
sessionStore.revertToA();
return;
}
const a = sessionStore.state.ab.a;
if (a?.weights) {
try { mlStore.setWeights(a.weights); } catch { /* ignore */ }
explorationStore.setNoiseLevel(a.noiseLevel);
}
sessionStore.revertToA();
}