🐛 fix 8 adversarial-review findings (zero/vibes/OMNI)

A 30-agent adversarial review (5 dimensions × find→refute×2) confirmed 8 defects;
all fixed and live-verified:

BLOCKERS
- #1 unwired OMNI trim couldn't be silenced: the offset-floor only seeded a dest
  while offset>0.0015, so reset/mute/lower stopped emitting the key and the last
  floored value stuck in the synth (Synth.update merges). Now ANY tweaked cableless
  non-note dest is seeded 0 so the processing loop writes its real value, incl. 0.
  (verified: crush norm 0.5 on drag → 0 on reset)
- #2 leaving zero with an OMNI/offset-only build (no cables) was silently discarded
  — exitZeroToMood gated on routesArr.length===0. New zeroHasBuild() also counts
  picked orbs + any non-neutral tweak, so the keep/old/weave card always shows.
  (verified: offset-only build → 'you shaped this instrument by hand. keep it?')
- #3 vibe scale whitelist used truthiness (SCALES[q.scale]) → 'constructor'/'__proto__'
  passed → NaN into a Web Audio AudioParam → threw ~40×/s (remotely triggerable via
  public #vibe= links). Now Array.isArray(SCALES[...]) + quantize only on note dests;
  mxQuantize fallback hardened too. (verified: fx quantize dropped, note scale→minor_pent)

SHOULD-FIX
- #4 finishZeroExit keep/weave clobbered the kept mix (applyMood resets MOOD_KEYS):
  re-lay the build's tweaks after applyMood; weave merges build tweaks over prezero.
- #5 crush knob was fully dead (never in dests): OMNI.render ensureDest()s every
  voice/FX key → arc lives, floor works, right-click opens the menu. (verified)
- #6 OMNI drags in zero weren't persisted: knob pointerup/dblclick call zeroUI.persist()
  when zeroMode → survives reload. (verified: offset 0.467 in gs_zerobuild)
- #7 reloaded vibes showed OFF: persist activeVibes + per-route _vibes/_base to
  gs_zerovibes, restoreVibeState() on enterZero(true). (verified: vibe restores ◉)
- #8 GODSONIQ chip lit ~3s late: update() re-lights engine/♪ chips each frame.

Console clean, syntax clean, 21 knobs, field healthy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-11 01:10:30 +10:00
parent fc1bb307bc
commit fa948823a7

View File

@ -1450,7 +1450,7 @@
return x;
}
function mxQuantize(x01, scale, root, octaves) {
const steps = SCALES[scale] || SCALES.chromatic;
const steps = Array.isArray(SCALES[scale]) ? SCALES[scale] : SCALES.chromatic; // never an inherited prototype name → no NaN
const n = steps.length * (octaves || 3);
const idx = Math.floor(clamp(x01, 0, 1) * (n - 1) + 0.5);
const oct = Math.floor(idx / steps.length), deg = idx % steps.length;
@ -2490,10 +2490,9 @@
}
for (const k in destVals) destVals[k] = clamp(destVals[k], 0, 1);
for (const k in notes) destVals[k] = notes[k];
for (const [k, n] of dests) { // a hand-set offset floor speaks even with no cable —
if (n.isNote || (k in destVals)) continue; // so an OMNI/trim knob gives a voice a level from nothing
const tk = tweaks[k];
if (tk && !(tk.mute >= 0.5) && (tk.offset || 0) > 0.0015) destVals[k] = 0;
for (const [k, n] of dests) { // any hand-tweaked cableless voice: emit its ACTUAL value —
if (n.isNote || (k in destVals)) continue; // an offset floor gives it a level from nothing, and a
if (tweaks[k]) destVals[k] = 0; // reset/mute/lower writes 0 too (else the last value sticks in the synth)
}
for (const k in destVals) {
const tk = tweaks[k];
@ -2647,10 +2646,11 @@
if (restore) { // reload — bring the zero build's cables back (C2)
let build = null; try { build = JSON.parse(localStorage.getItem("gs_zerobuild") || "null"); } catch (e) {}
loadPatch(build && build.routes ? build : { routes: [], tweaks: {}, seqs: {} });
zeroUI.restoreVibeState(); // re-tag vibe cables so they show ON and stay removable (#7)
} else { // fresh — empty the field and forget any old build
loadPatch({ routes: [], tweaks: {}, seqs: {} });
zeroUI.nodes = {};
try { localStorage.removeItem("gs_zeronodes"); localStorage.removeItem("gs_zerobuild"); } catch (e) {}
zeroUI.nodes = {}; activeVibes.clear();
try { localStorage.removeItem("gs_zeronodes"); localStorage.removeItem("gs_zerobuild"); localStorage.removeItem("gs_zerovibes"); } catch (e) {}
}
zeroMode = true;
try { localStorage.setItem("gs_mood", "0 zero"); } catch (e) {}
@ -2660,18 +2660,28 @@
}
function exitZeroToMood(name) { // leave zero back into the full world, then a mood
if (!zeroMode) { applyMood(name); return; }
if (routesArr.length === 0) { finishZeroExit("old", name); return; } // nothing built — quiet restore (C1)
if (!zeroHasBuild()) { finishZeroExit("old", name); return; } // truly nothing built — quiet restore (C1)
zeroExitChoice(name); // you built something — ask what to keep with it
}
function zeroHasBuild() { // a zero build can be cables, picked orbs, OR OMNI/trim shapes (no cables)
if (routesArr.length) return true;
if (zeroUI.nodes && Object.keys(zeroUI.nodes).length) return true;
for (const k in tweaks) { const t = tweaks[k];
if (t && ((t.offset && Math.abs(t.offset) > 0.0015) || (t.gain != null && Math.abs(t.gain - 1) > 0.01) || t.mute >= 0.5 || t.freeze >= 0.5)) return true; }
return false;
}
function finishZeroExit(mode, name) { // mode: keep | old | weave (C1)
const build = serializePatch(); // the zero-built patch, captured before we touch routes
const build = serializePatch(); // the zero-built patch (routes + tweaks + seqs), captured before we touch anything
zeroMode = false; zeroUI.close();
try { localStorage.removeItem("gs_zeronodes"); localStorage.removeItem("gs_zerobuild"); } catch (e) {}
try { localStorage.removeItem("gs_zeronodes"); localStorage.removeItem("gs_zerobuild"); localStorage.removeItem("gs_zerovibes"); } catch (e) {}
if (mode === "old" || mode === "weave") {
try { const pz = localStorage.getItem("gs_prezero"); if (pz) loadPatch(JSON.parse(pz)); } catch (e) {}
}
if (mode === "weave") for (const r of (build.routes || [])) registerRoute(r); // graft build atop the old world
applyMood(name); // keep: build stays as-is and the mood just rides it
if (mode === "weave") for (const r of (build.routes || [])) registerRoute(r); // graft build's cables atop the old world
applyMood(name); // the mood rides on top: its groove + tempo (+ its own mutes/gains)…
if (mode === "keep" || mode === "weave") { // …but the kept build's own mix wins — applyMood resets MOOD_KEYS, so re-lay it
for (const k in (build.tweaks || {})) tweaks[k] = Object.assign({ mute: 0, gain: 1, offset: 0 }, build.tweaks[k]);
}
layoutDirty = true;
}
function zeroExitChoice(name) { // compact three-way card (same language as the door card)
@ -2686,7 +2696,8 @@
const h = document.createElement("div"); h.textContent = "leaving zero";
Object.assign(h.style, { fontWeight: "800", fontSize: "clamp(20px,3.5vw,28px)", letterSpacing: "2px", marginBottom: "6px" });
const sub = document.createElement("div");
sub.textContent = "you built " + n + " cable" + (n === 1 ? "" : "s") + " here. keep it?";
sub.textContent = n ? ("you built " + n + " cable" + (n === 1 ? "" : "s") + " here. keep it?")
: "you shaped this instrument by hand. keep it?"; // OMNI/trim-only builds have no cables
Object.assign(sub.style, { color: "rgba(165,185,218,0.75)", fontStyle: "italic", marginBottom: "24px", fontSize: "14px" });
card.appendChild(h); card.appendChild(sub);
const row = document.createElement("div");
@ -2903,11 +2914,21 @@
},
open() { this.build(); this.restoreNodes(); this.root.classList.add("on"); this.renderStage(); this.refreshSockets(); },
close() { if (this.root) { this.root.classList.remove("on"); this.closeDrawer(); this.disarm(); } },
persist() { // zero build survives a reload: picks + cables (C2)
persist() { // zero build survives a reload: picks + cables + vibe ownership (C2)
try {
const nd = {}; for (const gk in this.nodes) nd[gk] = [...this.nodes[gk].members];
localStorage.setItem("gs_zeronodes", JSON.stringify(nd));
localStorage.setItem("gs_zerobuild", JSON.stringify(serializePatch()));
const own = {}; for (const r of routesArr) if (r._vibes && r._vibes.size) own[r.source + "|" + r.dest] = { v: [...r._vibes], b: !!r._base };
localStorage.setItem("gs_zerovibes", JSON.stringify({ active: [...activeVibes], own })); // so reloaded vibes stay live + removable (Z3/#7)
} catch (e) {}
},
restoreVibeState() { // re-tag routes so a reloaded vibe shows ON and its off-toggle works
try {
const zv = JSON.parse(localStorage.getItem("gs_zerovibes") || "null");
if (!zv) return;
activeVibes.clear(); for (const nm of (zv.active || [])) activeVibes.add(nm);
for (const id in (zv.own || {})) { const rt = routeIndex.get(id); if (rt) { rt._vibes = new Set(zv.own[id].v); rt._base = !!zv.own[id].b; } }
} catch (e) {}
},
restoreNodes() { // rebuild the picked orbs from storage (reload path)
@ -3166,10 +3187,13 @@
const route = { source: r.source, dest: r.dest,
amount: (typeof r.amount === "number" && isFinite(r.amount)) ? clamp(r.amount, -4, 4) : 0.5,
curve: VIBE_CURVES.has(r.curve) ? r.curve : "lin" };
if (r.quantize && typeof r.quantize === "object") {
// quantize only ever belongs on a NOTE dest, and the scale must be an own
// array entry of SCALES — never an inherited name ("constructor" etc.), which
// would smuggle a NaN into a Web Audio param and throw every audio tick.
if (r.quantize && typeof r.quantize === "object" && /note/.test(r.dest.split(".").pop())) {
const q = r.quantize;
route.quantize = {
scale: (typeof q.scale === "string" && SCALES[q.scale]) ? q.scale : "minor_pent",
scale: (typeof q.scale === "string" && Array.isArray(SCALES[q.scale])) ? q.scale : "minor_pent",
root: (typeof q.root === "number" && isFinite(q.root)) ? clamp(Math.round(q.root), 0, 127) : 60,
octaves: (typeof q.octaves === "number" && isFinite(q.octaves)) ? clamp(Math.round(q.octaves), 1, 4) : 2,
};
@ -3419,14 +3443,16 @@
["🎛 GODSONIQ", () => Synth.sampling(), () => { const ok = Synth.godsoniq(3);
eventTicker.unshift({ text: ok ? "🎛 GODSONIQ — sampling 3s of the world… every pluck now replays it" : "🎛 press ♪ first, then GODSONIQ", tAdded: now(), src: "sys" }); }],
];
this._chips = []; // remembered so update() can re-light them (GODSONIQ lands ~3s async) (#8)
for (const [label, isOn, act] of engines) {
const c = document.createElement("div"); c.className = "ochip" + (isOn() ? " on" : ""); c.textContent = label;
c.onclick = () => { try { act(); } catch (e) {} setTimeout(() => this.renderHead(), 40); };
h.appendChild(c);
h.appendChild(c); this._chips.push([c, isOn]);
}
const sp = document.createElement("div"); sp.className = "ospacer"; h.appendChild(sp);
const snd = document.createElement("div"); snd.className = "ochip" + (Synth.playing && Synth.playing() ? " on" : ""); snd.textContent = "♪";
snd.title = "start / stop the synth"; snd.onclick = () => { Synth.toggle(); setTimeout(() => this.renderHead(), 60); }; h.appendChild(snd);
this._chips.push([snd, () => Synth.playing && Synth.playing()]);
const bpm = document.createElement("div"); bpm.className = "ochip"; bpm.id = "omnibpm"; bpm.style.cursor = "default"; bpm.textContent = Math.round(godtime.bpm) + " bpm"; h.appendChild(bpm);
const lock = document.createElement("div"); lock.className = "ochip" + (godtime.locked ? " on" : ""); lock.textContent = godtime.locked ? "🔒" : "🔓";
lock.title = "lock the godtime tempo"; lock.onclick = () => { godtime.locked = !godtime.locked; this.renderHead(); }; h.appendChild(lock);
@ -3434,7 +3460,10 @@
},
render() {
const b = this.body; b.innerHTML = ""; this.knobs = [];
for (const mod of this.MODULES) b.appendChild(this.moduleEl(mod, false));
for (const mod of this.MODULES) {
for (const key of mod.keys) if (!/note/.test(key.split(".").pop())) ensureDest(key); // register voices/FX (e.g. crush) so their arcs live + floors work
b.appendChild(this.moduleEl(mod, false));
}
const em = this.moduleEl(this.ECHO, true); // earth echo — knobs hidden until it's on
em.classList.add("oecho"); if (!earthEchoOn) em.classList.add("off");
b.appendChild(em);
@ -3473,9 +3502,9 @@
let dragging = false, sy = 0, so = 0;
wrap.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; dragging = true; sy = e.clientY; so = (tw(key).offset || 0); try { wrap.setPointerCapture(e.pointerId); } catch (x) {} e.preventDefault(); });
wrap.addEventListener("pointermove", (e) => { if (!dragging) return; setParamLocal(key, "offset", clamp(so + (sy - e.clientY) / 150, -1, 1)); });
const end = (e) => { dragging = false; try { wrap.releasePointerCapture(e.pointerId); } catch (x) {} };
const end = (e) => { dragging = false; try { wrap.releasePointerCapture(e.pointerId); } catch (x) {} if (zeroMode) zeroUI.persist(); }; // a knob shape is part of the zero build
wrap.addEventListener("pointerup", end); wrap.addEventListener("pointercancel", end);
wrap.ondblclick = () => setParamLocal(key, "offset", 0);
wrap.ondblclick = () => { setParamLocal(key, "offset", 0); if (zeroMode) zeroUI.persist(); };
}
wrap.oncontextmenu = (e) => { e.preventDefault(); const n = dests.get(key); if (n) openCtxMenu(n, e.clientX, e.clientY); };
this.knobs.push({ key, val, dot, isNote });
@ -3484,6 +3513,7 @@
update() {
if (!this._open) return;
const bpmEl = document.getElementById("omnibpm"); if (bpmEl) bpmEl.textContent = Math.round(godtime.bpm) + " bpm";
if (this._chips) for (const [c, isOn] of this._chips) c.classList.toggle("on", !!isOn()); // GODSONIQ/♪ light when their async state lands (#8)
for (const k of this.knobs) {
const n = dests.get(k.key);
const frac = n ? clamp(n.normDisp != null ? n.normDisp : (n.norm || 0), 0, 1) : 0;