fix: final-review findings (median pass tick, R11 secondary metrics, R12 chord duration, render version, dsp resample)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-30 05:55:04 +01:00
co-authored by Claude Sonnet 5.5
parent 2b7d5ffbeb
commit b631bd2c7e
16 changed files with 199 additions and 48 deletions
+25 -1
View File
@@ -2,7 +2,7 @@ import { test, expect } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildSong, loadSongDir } from "../../src/bench/dataset.ts";
import { buildSong, loadSongDir, RENDER_VERSION } from "../../src/bench/dataset.ts";
test("buildSong writes all artefacts; loadSongDir reads them; stems sum to mix; idempotent", async () => {
const root = await mkdtemp(join(tmpdir(), "stsheets-ds-"));
@@ -55,3 +55,27 @@ test("multi-track t1b: stems on disk sum to mix.wav", async () => {
await rm(root, { recursive: true, force: true });
}
}, 120_000);
test("a dir built under an older/missing RENDER_VERSION is rebuilt", async () => {
const root = await mkdtemp(join(tmpdir(), "stsheets-ds-"));
try {
const dir = await buildSong("t1a", 2, root);
const mp3 = join(dir, "mix.mp3");
const v = Bun.file(join(dir, "render-version"));
expect((await v.text()).trim()).toBe(String(RENDER_VERSION));
const m0 = (await Bun.file(mp3).stat()).mtimeMs;
await Bun.write(join(dir, "render-version"), String(RENDER_VERSION - 1)); // simulate an old build
await Bun.sleep(20);
await buildSong("t1a", 2, root);
const m1 = (await Bun.file(mp3).stat()).mtimeMs;
expect(m1).toBeGreaterThan(m0);
expect((await Bun.file(join(dir, "render-version")).text()).trim()).toBe(String(RENDER_VERSION));
// missing marker file also rebuilds
await rm(join(dir, "render-version"));
await Bun.sleep(20);
await buildSong("t1a", 2, root);
expect((await Bun.file(mp3).stat()).mtimeMs).toBeGreaterThan(m1);
} finally {
await rm(root, { recursive: true, force: true });
}
}, 180_000);
+29 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { renderReport, type BenchRun } from "../../src/bench/report.ts";
import { renderReport, resultsJson, type BenchRun } from "../../src/bench/report.ts";
import type { SongScore } from "../../src/eval/score.ts";
const mr = (f1: number | null, oct = 0) => ({ tp: 0, fp: 0, fn: 0, precision: f1, recall: f1, f1, octaveErrors: oct, matches: [] as [number, number][] });
@@ -13,6 +13,8 @@ function score(gf1: number, vocals: number | null, flags = { oracleStems: false,
rhythm: { factor: 1, beatOffset: 0, onBeat: 0.9, durationOk: null },
stringAgreement: null,
spuriousRate: 0.1,
gpPitch: { ...mr(gf1), tp: 0, fp: 0, fn: 1 },
barAgreement: { refBars: 8, outBars: 8, timeSigMatch: true },
};
}
const modes = [
@@ -44,6 +46,23 @@ describe("renderReport", () => {
expect(bad).toContain("FAIL");
expect(bad).not.toContain("Tier t1a: PASS");
});
test("per-mode tick uses the median rule with a k/n pass count", () => {
const gs = [0.9, 0.9, 0.9, 0.9, 0.3];
const run: BenchRun = { runId: "fx", date: "d", modes: [modes[0]!], results: gs.map((g, i) => ({ tier: "t1a" as const, seed: i, mode: "full", score: score(g, null), seconds: 1 })) };
const row = renderReport(run).split("\n").find((l) => l.startsWith("| full"))!;
expect(row).toContain("✓ 4/5");
expect(row).not.toContain("✗");
const bad = renderReport({ ...run, results: gs.map((g, i) => ({ tier: "t1a" as const, seed: i, mode: "full", score: score(g === 0.9 ? 0.3 : 0.9, null), seconds: 1 })) });
expect(bad.split("\n").find((l) => l.startsWith("| full"))).toContain("✗ 1/5");
});
test("secondary metric columns: GP-file F1, bar agreement, techniques n/a", () => {
const md = renderReport(mk(0.9));
const header = md.split("\n").find((l) => l.startsWith("| Mode"))!;
expect(header).toContain("GP-file guitar F1");
expect(header).toContain("Bar count match");
expect(header).toContain("Techniques");
expect(md).toContain("n/a (tier 1 has no technique marks)");
});
test("role with empty truth renders n/a with spurious count", () => {
const run = mk(0.9);
const s = run.results[0]!.score;
@@ -53,3 +72,12 @@ describe("renderReport", () => {
expect(md).not.toContain("0.00 (0.00");
});
});
test("results JSON omits per-note matches", () => {
const run = mk(0.9);
run.results[0]!.score.guitarMerged.matches = [[0, 1]];
const json = resultsJson(run);
expect(json).not.toContain('"matches"');
expect(JSON.parse(json).results).toHaveLength(3);
expect(JSON.stringify(run)).toContain('"matches"'); // in-memory run (and score.json) keep them
});