diff --git a/.claude/skills/run-virtuoso/driver.mjs b/.claude/skills/run-virtuoso/driver.mjs index 8a6d06a..5956f6d 100644 --- a/.claude/skills/run-virtuoso/driver.mjs +++ b/.claude/skills/run-virtuoso/driver.mjs @@ -26,7 +26,7 @@ import { fileURLToPath } from "node:url"; const HOST = process.env.SLOPSMITH_HOST || "http://127.0.0.1:8765"; const SHOT_DIR = process.env.SHOT_DIR || resolve(dirname(fileURLToPath(import.meta.url)), "../../../.virtuoso-shots"); -const RENDERERS = ["highway_3d", "builtin_2d", "tab_2d", "notation_2d"]; +const RENDERERS = ["highway_3d", "highway_2d", "builtin_2d", "tab_2d", "notation_2d"]; async function ensureHost() { const r = await fetch(`${HOST}/api/plugins/virtuoso/status`).catch(() => null); @@ -64,7 +64,7 @@ async function gotoVirtuoso(page) { await page.waitForSelector("#virtuoso-root", { state: "attached", timeout: 10_000 }); // The bootstrap script in screen.html runs bind() on DOMContentLoaded; give // it a moment to wire up everything (pathway dropdown, view-switcher). - await page.waitForSelector(".virtuoso-view-btn", { timeout: 5_000 }); + await page.waitForSelector("#virtuoso-view-select", { timeout: 5_000 }); } async function generate(page) { @@ -93,9 +93,16 @@ async function generate(page) { } async function switchRenderer(page, kind) { - const btn = await page.$(`.virtuoso-view-btn[data-renderer="${kind}"]`); - if (!btn) throw new Error(`Renderer button not found: ${kind}`); - await btn.click(); + // View selector is a dropdown (v0.1.13): set the value + dispatch change, + // the same event path a user pick takes (change → onViewSwitch). + const ok = await page.evaluate((k) => { + const sel = document.querySelector("#virtuoso-view-select"); + if (!sel || ![...sel.options].some((o) => o.value === k)) return false; + sel.value = k; + sel.dispatchEvent(new Event("change", { bubbles: true })); + return true; + }, kind); + if (!ok) throw new Error(`Renderer option not found: ${kind}`); await page.waitForTimeout(600); // attachRenderer is async; let it settle } @@ -111,6 +118,13 @@ async function withBrowser(fn) { try { const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const page = await ctx.newPage(); + // Seed the L1 instrument store BEFORE page scripts run: the host-settings + // sync (v0.1.11) treats an empty localStorage as a fresh install and ADOPTS + // host config — which persists whatever instrument the PREVIOUS suite's panel + // drives wrote through (cross-suite contamination; the panel flipped to bass + // mid-suite). With the store seeded, the local-wins boot path holds the + // deterministic 6-string default AND heals the host config for later suites. + await page.addInitScript(() => { try { localStorage.setItem("virtuoso.instrument", JSON.stringify({ stringSetup: "guitar_6_standard", customOpenMidis: "" })); } catch (_) {} }); page.on("pageerror", (e) => console.error("[page error]", e.message)); page.on("console", (m) => { const t = m.type(); diff --git a/.claude/skills/run-virtuoso/package.json b/.claude/skills/run-virtuoso/package.json index 9835633..3d85b26 100644 --- a/.claude/skills/run-virtuoso/package.json +++ b/.claude/skills/run-virtuoso/package.json @@ -23,8 +23,9 @@ "smoke:contained": "node smoke-contained-verifier.mjs", "smoke:level-gate": "node smoke-level-gate-async.mjs", "smoke:host-surface": "node smoke-host-surface.mjs", + "smoke:coach": "node smoke-coach-rx.mjs", "test": "node run-all.mjs", - "test:seq": "node smoke-renderers.mjs && node smoke-generators.mjs && node smoke-highway-settings.mjs && node smoke-strings.mjs && node smoke-meter-subdiv.mjs && node smoke-over-barline.mjs && node smoke-herta.mjs && node smoke-audioctx.mjs && node smoke-connect.mjs && node smoke-variation.mjs && node smoke-session-sync.mjs && node smoke-progress.mjs && node smoke-core-purity.mjs && node smoke-gems.mjs && node smoke-scoring-e2e.mjs && node smoke-backing-engine.mjs && node smoke-contained-verifier.mjs && node smoke-level-gate-async.mjs && node smoke-host-surface.mjs" + "test:seq": "node smoke-renderers.mjs && node smoke-generators.mjs && node smoke-highway-settings.mjs && node smoke-strings.mjs && node smoke-meter-subdiv.mjs && node smoke-over-barline.mjs && node smoke-herta.mjs && node smoke-audioctx.mjs && node smoke-connect.mjs && node smoke-variation.mjs && node smoke-session-sync.mjs && node smoke-progress.mjs && node smoke-core-purity.mjs && node smoke-gems.mjs && node smoke-scoring-e2e.mjs && node smoke-backing-engine.mjs && node smoke-contained-verifier.mjs && node smoke-level-gate-async.mjs && node smoke-host-surface.mjs && node smoke-coach-rx.mjs" }, "keywords": [], "author": "", diff --git a/.claude/skills/run-virtuoso/smoke-audioctx.mjs b/.claude/skills/run-virtuoso/smoke-audioctx.mjs index 0265b0d..b19ba69 100644 --- a/.claude/skills/run-virtuoso/smoke-audioctx.mjs +++ b/.claude/skills/run-virtuoso/smoke-audioctx.mjs @@ -43,6 +43,13 @@ try { const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); await ctx.addInitScript(() => { globalThis.__SS_HARNESS__ = true; }); // sink rows read __ss_debug const page = await ctx.newPage(); + // Seed the L1 instrument store BEFORE page scripts run: the host-settings + // sync (v0.1.11) treats an empty localStorage as a fresh install and ADOPTS + // host config — which persists whatever instrument the PREVIOUS suite's panel + // drives wrote through (cross-suite contamination; the panel flipped to bass + // mid-suite). With the store seeded, the local-wins boot path holds the + // deterministic 6-string default AND heals the host config for later suites. + await page.addInitScript(() => { try { localStorage.setItem("virtuoso.instrument", JSON.stringify({ stringSetup: "guitar_6_standard", customOpenMidis: "" })); } catch (_) {} }); page.on("pageerror", e => console.error("[page error]", e.message)); await page.goto(`${HOST}/`, { waitUntil: "domcontentloaded" }); await page.waitForSelector("#plugin-virtuoso", { state: "attached", timeout: 20000 }); diff --git a/.claude/skills/run-virtuoso/smoke-backing-engine.mjs b/.claude/skills/run-virtuoso/smoke-backing-engine.mjs index 581eeb4..3a37dfc 100644 --- a/.claude/skills/run-virtuoso/smoke-backing-engine.mjs +++ b/.claude/skills/run-virtuoso/smoke-backing-engine.mjs @@ -46,6 +46,13 @@ try { try { new PerformanceObserver((l) => l.getEntries().forEach((e) => window.__longTasks.push(Math.round(e.duration)))).observe({ entryTypes: ["longtask"] }); } catch {} }); const page = await ctx.newPage(); + // Seed the L1 instrument store BEFORE page scripts run: the host-settings + // sync (v0.1.11) treats an empty localStorage as a fresh install and ADOPTS + // host config — which persists whatever instrument the PREVIOUS suite's panel + // drives wrote through (cross-suite contamination; the panel flipped to bass + // mid-suite). With the store seeded, the local-wins boot path holds the + // deterministic 6-string default AND heals the host config for later suites. + await page.addInitScript(() => { try { localStorage.setItem("virtuoso.instrument", JSON.stringify({ stringSetup: "guitar_6_standard", customOpenMidis: "" })); } catch (_) {} }); const errs = []; page.on("pageerror", (e) => errs.push(e.message)); await page.goto(`${HOST}/`, { waitUntil: "domcontentloaded" }); await page.waitForSelector("#plugin-virtuoso", { state: "attached", timeout: 20000 }); @@ -57,7 +64,7 @@ try { catch (e) { if (i >= 2) throw e; await page.waitForTimeout(1500); } } await page.waitForSelector("#virtuoso-root", { state: "attached" }); - await page.waitForSelector(".virtuoso-view-btn"); + await page.waitForSelector("#virtuoso-view-select"); await page.waitForFunction(() => window.Virtuoso && typeof window.Virtuoso.generateExercise === "function" && globalThis.__ss_debug); await page.waitForTimeout(600); // let the screen's boot settle (first-load race) diff --git a/.claude/skills/run-virtuoso/smoke-coach-rx.mjs b/.claude/skills/run-virtuoso/smoke-coach-rx.mjs new file mode 100644 index 0000000..dc0e988 --- /dev/null +++ b/.claude/skills/run-virtuoso/smoke-coach-rx.mjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node +// Regression guard for the prescriptive COACH LOOP (PR 1.3; npm run smoke:coach, +// in npm test). The results modal turns the already-computed diagnostic data +// (heat.fgMiss / transMiss, leanMs, nearMiss, the bass felt verdict) into ONE +// prescriptive next-drill. This suite unit-tests the PURE fault→drill mapping via +// the window.__virtuosoCoach.coachRxFor hook — it takes a fault FIXTURE directly, +// so it is immune to the "smoke mocks the verifier" blind spot (no real detector +// needed). It also asserts every prescribed pathway id is a real #virtuoso-pathway +//