mirror of
https://github.com/Nutlope/hallmark.git
synced 2026-08-14 12:35:33 +02:00
Arms: Anthropic baseline, GLM on Together, Kimi on OpenRouter (DeepSeek and Qwen staged, disabled). Six briefs incl. metric-temptation, component-scope, and vision-probe probes. Floor-only vs full skill packs to measure prompt bloat. run-matrix orchestrates gen, shots, score, judge, report; the repair loop feeds sloplint FAILs back to the model.
292 lines
12 KiB
JavaScript
292 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
// report.mjs: assemble eval/index.html, a static self-contained gallery of
|
|
// every eval run: one row per brief, one card per arm-pack cell, plus a
|
|
// per-arm summary table at the top compared against fable-baseline.
|
|
//
|
|
// Reads: briefs.json, models.json, runs/*/*/{run,score,judge}.json, _shots/.
|
|
// Writes: eval/index.html (dark neutral shell, system-ui, no external assets).
|
|
//
|
|
// Usage: node eval/report.mjs
|
|
|
|
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import path from "node:path";
|
|
|
|
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
const RUNS = path.join(ROOT, "runs");
|
|
const OUT = path.join(ROOT, "index.html");
|
|
|
|
const AXES = ["philosophy", "hierarchy", "execution", "specificity", "restraint", "variety"];
|
|
const BASELINE_ARM = "fable-baseline";
|
|
|
|
function readJson(p, fallback = null) {
|
|
try {
|
|
return JSON.parse(readFileSync(p, "utf8"));
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s ?? "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
}
|
|
|
|
function splitCell(cell, armIds) {
|
|
// Run dirs are <armId>-<pack>; arm ids contain hyphens, so match the
|
|
// longest known arm id prefix. Falls back to the whole name.
|
|
const sorted = [...armIds].sort((a, b) => b.length - a.length);
|
|
for (const id of sorted) {
|
|
if (cell === id) return { armId: id, pack: "default" };
|
|
if (cell.startsWith(id + "-")) return { armId: id, pack: cell.slice(id.length + 1) };
|
|
}
|
|
return { armId: cell, pack: "default" };
|
|
}
|
|
|
|
function fmtDuration(sec) {
|
|
if (sec == null) return null;
|
|
const s = Math.round(sec);
|
|
return s >= 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`;
|
|
}
|
|
|
|
function fmtTok(run) {
|
|
const p = run?.promptTok;
|
|
const c = run?.completionTok;
|
|
if (p == null && c == null) return null;
|
|
const total = (p ?? 0) + (c ?? 0);
|
|
return total >= 1000 ? `${(total / 1000).toFixed(total >= 10000 ? 0 : 1)}k` : String(total);
|
|
}
|
|
|
|
function mean(nums) {
|
|
const v = nums.filter((n) => typeof n === "number" && Number.isFinite(n));
|
|
return v.length ? v.reduce((a, b) => a + b, 0) / v.length : null;
|
|
}
|
|
|
|
function judgeAxesMean(judge) {
|
|
if (!judge?.axes) return null;
|
|
return mean(AXES.map((a) => judge.axes[a]));
|
|
}
|
|
|
|
function collect() {
|
|
const briefs = readJson(path.join(ROOT, "briefs.json"), []);
|
|
const models = readJson(path.join(ROOT, "models.json"), { arms: [] });
|
|
const armIds = models.arms.map((a) => a.id);
|
|
const armOrder = new Map(armIds.map((id, i) => [id, i]));
|
|
|
|
const cells = []; // flat list of run cells
|
|
if (existsSync(RUNS)) {
|
|
for (const briefId of readdirSync(RUNS).sort()) {
|
|
const briefDir = path.join(RUNS, briefId);
|
|
if (!statSync(briefDir).isDirectory()) continue;
|
|
for (const cell of readdirSync(briefDir).sort()) {
|
|
const runDir = path.join(briefDir, cell);
|
|
if (!statSync(runDir).isDirectory()) continue;
|
|
const { armId, pack } = splitCell(cell, armIds);
|
|
const base = `${briefId}-${cell}`;
|
|
cells.push({
|
|
briefId,
|
|
cell,
|
|
armId,
|
|
pack,
|
|
href: `./runs/${briefId}/${cell}/index.html`,
|
|
hero: existsSync(path.join(ROOT, "_shots", `${base}-hero.png`))
|
|
? `./_shots/${base}-hero.png`
|
|
: null,
|
|
run: readJson(path.join(runDir, "run.json")),
|
|
score: readJson(path.join(runDir, "score.json")),
|
|
judge: readJson(path.join(runDir, "judge.json")),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
cells.sort((a, b) => {
|
|
const ao = armOrder.get(a.armId) ?? 99;
|
|
const bo = armOrder.get(b.armId) ?? 99;
|
|
return ao - bo || a.pack.localeCompare(b.pack);
|
|
});
|
|
return { briefs, models, cells };
|
|
}
|
|
|
|
function summarize(cells, briefCount) {
|
|
// One summary row per arm-pack column.
|
|
const byCol = new Map();
|
|
for (const c of cells) {
|
|
const key = `${c.armId} |