@aigne/afs-llm-bench 1.12.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +26 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/bench.cjs +944 -0
- package/dist/bench.d.cts +89 -0
- package/dist/bench.d.cts.map +1 -0
- package/dist/bench.d.mts +89 -0
- package/dist/bench.d.mts.map +1 -0
- package/dist/bench.mjs +944 -0
- package/dist/bench.mjs.map +1 -0
- package/dist/errors.cjs +14 -0
- package/dist/errors.d.cts +13 -0
- package/dist/errors.d.cts.map +1 -0
- package/dist/errors.d.mts +13 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +14 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.cjs +38 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +10 -0
- package/dist/judge.cjs +189 -0
- package/dist/judge.d.cts +67 -0
- package/dist/judge.d.cts.map +1 -0
- package/dist/judge.d.mts +67 -0
- package/dist/judge.d.mts.map +1 -0
- package/dist/judge.mjs +185 -0
- package/dist/judge.mjs.map +1 -0
- package/dist/outlier.cjs +73 -0
- package/dist/outlier.d.cts +30 -0
- package/dist/outlier.d.cts.map +1 -0
- package/dist/outlier.d.mts +30 -0
- package/dist/outlier.d.mts.map +1 -0
- package/dist/outlier.mjs +73 -0
- package/dist/outlier.mjs.map +1 -0
- package/dist/patcher.cjs +296 -0
- package/dist/patcher.d.cts +119 -0
- package/dist/patcher.d.cts.map +1 -0
- package/dist/patcher.d.mts +119 -0
- package/dist/patcher.d.mts.map +1 -0
- package/dist/patcher.mjs +291 -0
- package/dist/patcher.mjs.map +1 -0
- package/dist/prompts.cjs +123 -0
- package/dist/prompts.d.cts +30 -0
- package/dist/prompts.d.cts.map +1 -0
- package/dist/prompts.d.mts +30 -0
- package/dist/prompts.d.mts.map +1 -0
- package/dist/prompts.mjs +121 -0
- package/dist/prompts.mjs.map +1 -0
- package/dist/reporter.cjs +322 -0
- package/dist/reporter.d.cts +27 -0
- package/dist/reporter.d.cts.map +1 -0
- package/dist/reporter.d.mts +27 -0
- package/dist/reporter.d.mts.map +1 -0
- package/dist/reporter.mjs +322 -0
- package/dist/reporter.mjs.map +1 -0
- package/dist/runner.cjs +710 -0
- package/dist/runner.d.cts +345 -0
- package/dist/runner.d.cts.map +1 -0
- package/dist/runner.d.mts +345 -0
- package/dist/runner.d.mts.map +1 -0
- package/dist/runner.mjs +697 -0
- package/dist/runner.mjs.map +1 -0
- package/dist/scoring.cjs +47 -0
- package/dist/scoring.d.cts +28 -0
- package/dist/scoring.d.cts.map +1 -0
- package/dist/scoring.d.mts +28 -0
- package/dist/scoring.d.mts.map +1 -0
- package/dist/scoring.mjs +46 -0
- package/dist/scoring.mjs.map +1 -0
- package/manifest.json +39 -0
- package/package.json +62 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.mjs","names":["parseYaml","dumpYaml"],"sources":["../src/prompts.ts"],"sourcesContent":["/**\n * Prompt loader for the LLM-AFS benchmark suite.\n *\n * Each prompt is a Markdown file with YAML frontmatter (`gray-matter`-parsed)\n * and a body sliced by H1 sections (`# Task`, `# System prompt`, etc.).\n *\n * Phase 0 only needs the load + parse path so that `list /bench/prompts` and\n * `read /bench/prompts/<id>` return real entries. Runner / patcher consume the\n * parsed shape in Phase 1.\n */\n\nimport { getPlatform } from \"@aigne/afs\";\nimport matter from \"gray-matter\";\nimport { dump as dumpYaml, load as parseYaml } from \"js-yaml\";\nimport { z } from \"zod\";\n\n// gray-matter@4 bundles `js-yaml.safeLoad`, which js-yaml@4 removed — calling\n// matter() with the default engine throws \"yaml.safeLoad is removed\". Supply an\n// explicit engine backed by js-yaml@4's load/dump so frontmatter parses.\nconst GRAY_MATTER_OPTIONS = {\n engines: {\n yaml: {\n parse: (str: string) => parseYaml(str) as object,\n stringify: (data: object) => dumpYaml(data),\n },\n },\n} as const;\n\nconst promptFrontmatterSchema = z.object({\n id: z.string().min(1, \"frontmatter.id is required\"),\n title: z.string().min(1, \"frontmatter.title is required\"),\n domain: z.enum([\"chinese\", \"english\", \"coding\"]).optional(),\n max_rounds: z.number().int().positive(\"max_rounds must be a positive integer\"),\n weight_dim: z.string().optional(),\n});\n\nexport type PromptFrontmatter = z.infer<typeof promptFrontmatterSchema>;\n\nexport interface Prompt {\n /** Stable id from frontmatter, used as the path segment under `/prompts/`. */\n id: string;\n /** Source filename relative to `promptsDir`. */\n filename: string;\n /** Validated frontmatter. */\n frontmatter: PromptFrontmatter;\n /** Raw markdown body (excludes frontmatter). */\n body: string;\n /** Body sliced by `# H1` headings — section name → section body. */\n sections: Record<string, string>;\n}\n\n/**\n * Slice markdown body into sections keyed by H1 heading text.\n *\n * Section headings are case-sensitive and trimmed. Content before the first H1\n * is dropped — by convention every benchmark prompt starts with `# Task`.\n */\nexport function sliceBodyByH1(body: string): Record<string, string> {\n const out: Record<string, string> = {};\n const lines = body.split(/\\r?\\n/);\n let currentName: string | undefined;\n let currentBuf: string[] = [];\n\n const flush = () => {\n if (currentName !== undefined) {\n out[currentName] = currentBuf.join(\"\\n\").trim();\n }\n };\n\n for (const line of lines) {\n const match = /^#\\s+(.+?)\\s*$/.exec(line);\n if (match && !line.startsWith(\"##\")) {\n flush();\n currentName = match[1]!.trim();\n currentBuf = [];\n } else if (currentName !== undefined) {\n currentBuf.push(line);\n }\n }\n flush();\n\n return out;\n}\n\n/**\n * Parse a single prompt file. Throws `BadPromptError` (regular Error subclass)\n * if frontmatter is missing required fields.\n */\nexport class BadPromptError extends Error {\n override readonly name = \"BadPromptError\";\n constructor(\n public readonly filename: string,\n message: string,\n ) {\n super(`[${filename}] ${message}`);\n }\n}\n\nexport function parsePrompt(filename: string, raw: string): Prompt {\n const parsed = matter(raw, GRAY_MATTER_OPTIONS);\n const fm = promptFrontmatterSchema.safeParse(parsed.data);\n if (!fm.success) {\n const issues = fm.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n throw new BadPromptError(filename, `invalid frontmatter — ${issues}`);\n }\n\n const body = parsed.content.replace(/^\\s+/, \"\");\n return {\n id: fm.data.id,\n filename,\n frontmatter: fm.data,\n body,\n sections: sliceBodyByH1(body),\n };\n}\n\n/**\n * Load every `*.md` file under `dir` and parse it into a Prompt. Missing\n * directory → empty array (Phase 0 must not crash on unconfigured promptsDir).\n *\n * Files that fail frontmatter validation are skipped with their error returned\n * in `errors` so callers can surface them (Phase 1 runner, conformance probe).\n */\nexport async function loadPromptsFromDir(\n dir: string,\n): Promise<{ prompts: Prompt[]; errors: BadPromptError[] }> {\n const platform = getPlatform();\n const fs = platform.fs;\n const path = platform.path;\n if (!fs) return { prompts: [], errors: [] };\n\n if (!(await fs.exists(dir))) return { prompts: [], errors: [] };\n const entries = await fs.readdir(dir);\n\n const prompts: Prompt[] = [];\n const errors: BadPromptError[] = [];\n for (const filename of entries.slice().sort()) {\n if (path.extname(filename).toLowerCase() !== \".md\") continue;\n const raw = await fs.readTextFile(path.join(dir, filename));\n try {\n prompts.push(parsePrompt(filename, raw));\n } catch (err) {\n if (err instanceof BadPromptError) errors.push(err);\n else throw err;\n }\n }\n\n return { prompts, errors };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAmBA,MAAM,sBAAsB,EAC1B,SAAS,EACP,MAAM;CACJ,QAAQ,QAAgBA,KAAU,IAAI;CACtC,YAAY,SAAiBC,KAAS,KAAK;CAC5C,EACF,EACF;AAED,MAAM,0BAA0B,EAAE,OAAO;CACvC,IAAI,EAAE,QAAQ,CAAC,IAAI,GAAG,6BAA6B;CACnD,OAAO,EAAE,QAAQ,CAAC,IAAI,GAAG,gCAAgC;CACzD,QAAQ,EAAE,KAAK;EAAC;EAAW;EAAW;EAAS,CAAC,CAAC,UAAU;CAC3D,YAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,wCAAwC;CAC9E,YAAY,EAAE,QAAQ,CAAC,UAAU;CAClC,CAAC;;;;;;;AAuBF,SAAgB,cAAc,MAAsC;CAClE,MAAM,MAA8B,EAAE;CACtC,MAAM,QAAQ,KAAK,MAAM,QAAQ;CACjC,IAAI;CACJ,IAAI,aAAuB,EAAE;CAE7B,MAAM,cAAc;AAClB,MAAI,gBAAgB,OAClB,KAAI,eAAe,WAAW,KAAK,KAAK,CAAC,MAAM;;AAInD,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,iBAAiB,KAAK,KAAK;AACzC,MAAI,SAAS,CAAC,KAAK,WAAW,KAAK,EAAE;AACnC,UAAO;AACP,iBAAc,MAAM,GAAI,MAAM;AAC9B,gBAAa,EAAE;aACN,gBAAgB,OACzB,YAAW,KAAK,KAAK;;AAGzB,QAAO;AAEP,QAAO;;;;;;AAOT,IAAa,iBAAb,cAAoC,MAAM;CACxC,AAAkB,OAAO;CACzB,YACE,AAAgB,UAChB,SACA;AACA,QAAM,IAAI,SAAS,IAAI,UAAU;EAHjB;;;AAOpB,SAAgB,YAAY,UAAkB,KAAqB;CACjE,MAAM,SAAS,OAAO,KAAK,oBAAoB;CAC/C,MAAM,KAAK,wBAAwB,UAAU,OAAO,KAAK;AACzD,KAAI,CAAC,GAAG,QAEN,OAAM,IAAI,eAAe,UAAU,yBADpB,GAAG,MAAM,OAAO,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,KAAK,GACpB;CAGvE,MAAM,OAAO,OAAO,QAAQ,QAAQ,QAAQ,GAAG;AAC/C,QAAO;EACL,IAAI,GAAG,KAAK;EACZ;EACA,aAAa,GAAG;EAChB;EACA,UAAU,cAAc,KAAK;EAC9B;;;;;;;;;AAUH,eAAsB,mBACpB,KAC0D;CAC1D,MAAM,WAAW,aAAa;CAC9B,MAAM,KAAK,SAAS;CACpB,MAAM,OAAO,SAAS;AACtB,KAAI,CAAC,GAAI,QAAO;EAAE,SAAS,EAAE;EAAE,QAAQ,EAAE;EAAE;AAE3C,KAAI,CAAE,MAAM,GAAG,OAAO,IAAI,CAAG,QAAO;EAAE,SAAS,EAAE;EAAE,QAAQ,EAAE;EAAE;CAC/D,MAAM,UAAU,MAAM,GAAG,QAAQ,IAAI;CAErC,MAAM,UAAoB,EAAE;CAC5B,MAAM,SAA2B,EAAE;AACnC,MAAK,MAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE;AAC7C,MAAI,KAAK,QAAQ,SAAS,CAAC,aAAa,KAAK,MAAO;EACpD,MAAM,MAAM,MAAM,GAAG,aAAa,KAAK,KAAK,KAAK,SAAS,CAAC;AAC3D,MAAI;AACF,WAAQ,KAAK,YAAY,UAAU,IAAI,CAAC;WACjC,KAAK;AACZ,OAAI,eAAe,eAAgB,QAAO,KAAK,IAAI;OAC9C,OAAM;;;AAIf,QAAO;EAAE;EAAS;EAAQ"}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_errors = require('./errors.cjs');
|
|
3
|
+
require('./runner.cjs');
|
|
4
|
+
const require_patcher = require('./patcher.cjs');
|
|
5
|
+
let node_fs = require("node:fs");
|
|
6
|
+
|
|
7
|
+
//#region src/reporter.ts
|
|
8
|
+
/**
|
|
9
|
+
* Phase 2 (Run 4) — HTML / Markdown report generator (plan.md L1063-1068).
|
|
10
|
+
*
|
|
11
|
+
* Reads a results JSON and produces either a markdown or HTML report:
|
|
12
|
+
* - model × dimension heatmap (emoji thresholds for markdown, color-coded
|
|
13
|
+
* CSS classes for HTML)
|
|
14
|
+
* - per-policy ranking (computed from the same weight tables the patcher
|
|
15
|
+
* uses, resolved through `/dev/ai/score-weights/<name>` by the bench wiring)
|
|
16
|
+
* - delta vs a `previous` results JSON when supplied
|
|
17
|
+
* - per-domain table when domains are present
|
|
18
|
+
*
|
|
19
|
+
* Security invariants (plan.md L1098-1101):
|
|
20
|
+
* 1. All user-controlled strings (model id, judge id, version, etc.) are
|
|
21
|
+
* HTML-escaped before insertion in HTML mode. Markdown mode also escapes
|
|
22
|
+
* `<>` so a model name like `<script>alert(1)<\/script>` cannot be smuggled
|
|
23
|
+
* into a downstream HTML viewer that renders the markdown.
|
|
24
|
+
* 2. Vault credentials never end up in the report — `validateResultsShape`
|
|
25
|
+
* rejects sensitive field names before we ever reach the renderer.
|
|
26
|
+
* 3. Default `includeRaw: false` — raw trace[] is never embedded unless the
|
|
27
|
+
* caller explicitly asks; even then we serialise via JSON.stringify with
|
|
28
|
+
* no key reordering so audit diffs stay clean.
|
|
29
|
+
* 4. `assertWithinResultsDir` keeps the path inside the results root, same
|
|
30
|
+
* as patcher.ts.
|
|
31
|
+
*
|
|
32
|
+
* Read-only: never writes files, never mutates input.
|
|
33
|
+
*/
|
|
34
|
+
const DIMENSIONS = [
|
|
35
|
+
"exploration",
|
|
36
|
+
"tool_reliability",
|
|
37
|
+
"self_stop",
|
|
38
|
+
"parallelism",
|
|
39
|
+
"efficiency",
|
|
40
|
+
"instruction_following"
|
|
41
|
+
];
|
|
42
|
+
const THRESHOLDS = {
|
|
43
|
+
good: .75,
|
|
44
|
+
ok: .5
|
|
45
|
+
};
|
|
46
|
+
async function generateReport(input) {
|
|
47
|
+
const format = input.format ?? "markdown";
|
|
48
|
+
if (format !== "markdown" && format !== "html") throw new require_errors.ValidationError(`unsupported format "${format}"; use "markdown" or "html"`);
|
|
49
|
+
const current = await loadResults(input.resultsPath, input.resultsRoot);
|
|
50
|
+
const previous = input.previousPath ? await loadResults(input.previousPath, input.resultsRoot) : void 0;
|
|
51
|
+
const weightsTable = await input.loadPolicyWeights();
|
|
52
|
+
if (!weightsTable || typeof weightsTable !== "object" || Array.isArray(weightsTable)) throw new require_errors.ValidationError("loadPolicyWeights must return a plain object of { policy: weights }");
|
|
53
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
54
|
+
const data = assembleReport(current, previous, weightsTable);
|
|
55
|
+
return {
|
|
56
|
+
content: format === "markdown" ? renderMarkdown(data, generatedAt, input.includeRaw === true ? current.raw : void 0) : renderHtml(data, generatedAt, input.includeRaw === true ? current.raw : void 0),
|
|
57
|
+
format,
|
|
58
|
+
generatedAt
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function loadResults(p, root) {
|
|
62
|
+
const safe = require_patcher.assertWithinResultsDir(p, root);
|
|
63
|
+
let text;
|
|
64
|
+
try {
|
|
65
|
+
text = await node_fs.promises.readFile(safe, "utf8");
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw new require_errors.ValidationError(`failed to read results: ${err instanceof Error ? err.message : String(err)}`);
|
|
68
|
+
}
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(text);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw new require_errors.ValidationError(`results is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
...require_patcher.validateResultsShape(parsed),
|
|
77
|
+
raw: parsed
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function assembleReport(current, previous, weightsTable) {
|
|
81
|
+
const judges = Array.isArray(current.judge_model) ? current.judge_model : current.judge_model ? [current.judge_model] : [];
|
|
82
|
+
const modelIds = Object.keys(current.results).sort();
|
|
83
|
+
const models = modelIds.map((m) => {
|
|
84
|
+
const r = current.results[m];
|
|
85
|
+
const scores = {};
|
|
86
|
+
for (const d of DIMENSIONS) scores[d] = r.scores[d];
|
|
87
|
+
return {
|
|
88
|
+
model: m,
|
|
89
|
+
scores,
|
|
90
|
+
overall: meanOf(Object.values(r.scores).filter(Number.isFinite))
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
const dims = models.some((m) => typeof m.scores.instruction_following === "number") ? DIMENSIONS : DIMENSIONS.filter((d) => d !== "instruction_following");
|
|
94
|
+
const policies = Object.entries(weightsTable).map(([policy, weights]) => {
|
|
95
|
+
return {
|
|
96
|
+
policy,
|
|
97
|
+
rows: modelIds.map((m) => ({
|
|
98
|
+
model: m,
|
|
99
|
+
score: computeWeightedScore(current.results[m], weights)
|
|
100
|
+
})).sort((a, b) => b.score - a.score)
|
|
101
|
+
};
|
|
102
|
+
}).sort((a, b) => a.policy.localeCompare(b.policy));
|
|
103
|
+
const domains = modelIds.map((m) => ({
|
|
104
|
+
model: m,
|
|
105
|
+
domains: current.results[m]?.domains ?? {}
|
|
106
|
+
})).filter((row) => Object.keys(row.domains).length > 0);
|
|
107
|
+
const delta = previous ? computeDelta(current, previous) : [];
|
|
108
|
+
return {
|
|
109
|
+
version: current.version,
|
|
110
|
+
judges,
|
|
111
|
+
dims,
|
|
112
|
+
models,
|
|
113
|
+
policies,
|
|
114
|
+
domains,
|
|
115
|
+
delta
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function meanOf(xs) {
|
|
119
|
+
if (xs.length === 0) return void 0;
|
|
120
|
+
let s = 0;
|
|
121
|
+
for (const x of xs) s += x;
|
|
122
|
+
return s / xs.length;
|
|
123
|
+
}
|
|
124
|
+
function computeDelta(current, previous) {
|
|
125
|
+
const rows = [];
|
|
126
|
+
const models = new Set([...Object.keys(current.results), ...Object.keys(previous.results)]);
|
|
127
|
+
for (const m of [...models].sort()) {
|
|
128
|
+
const cur = current.results[m]?.scores ?? {};
|
|
129
|
+
const prev = previous.results[m]?.scores ?? {};
|
|
130
|
+
for (const dim of DIMENSIONS) {
|
|
131
|
+
const c = cur[dim];
|
|
132
|
+
const p = prev[dim];
|
|
133
|
+
if (typeof c !== "number" && typeof p !== "number") continue;
|
|
134
|
+
if (c === p) continue;
|
|
135
|
+
const d = typeof c === "number" && typeof p === "number" ? c - p : void 0;
|
|
136
|
+
rows.push({
|
|
137
|
+
model: m,
|
|
138
|
+
dimension: dim,
|
|
139
|
+
prev: p,
|
|
140
|
+
curr: c,
|
|
141
|
+
delta: d
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return rows;
|
|
146
|
+
}
|
|
147
|
+
function computeWeightedScore(entry, weights) {
|
|
148
|
+
let total = 0;
|
|
149
|
+
for (const [key, w] of Object.entries(weights)) {
|
|
150
|
+
const v = getNested(entry, key);
|
|
151
|
+
if (typeof v === "number" && Number.isFinite(v)) total += v * w;
|
|
152
|
+
}
|
|
153
|
+
return total;
|
|
154
|
+
}
|
|
155
|
+
function getNested(obj, p) {
|
|
156
|
+
let cur = obj;
|
|
157
|
+
for (const k of p.split(".")) {
|
|
158
|
+
if (cur === null || typeof cur !== "object") return void 0;
|
|
159
|
+
cur = cur[k];
|
|
160
|
+
}
|
|
161
|
+
return cur;
|
|
162
|
+
}
|
|
163
|
+
function renderMarkdown(data, generatedAt, raw) {
|
|
164
|
+
const lines = [];
|
|
165
|
+
lines.push("# LLM AFS Benchmark Report");
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push(`**Generated:** ${escMd(generatedAt)}`);
|
|
168
|
+
lines.push(`**Run version:** ${escMd(data.version)}`);
|
|
169
|
+
if (data.judges.length > 0) lines.push(`**Judge(s):** ${data.judges.map(escMd).join(", ")}`);
|
|
170
|
+
lines.push(`**Models:** ${data.models.length}`);
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push("## Model × Dimension Heatmap");
|
|
173
|
+
lines.push("");
|
|
174
|
+
lines.push(`| model | ${data.dims.join(" | ")} | overall |`);
|
|
175
|
+
lines.push(`|---|${data.dims.map(() => "---").join("|")}|---|`);
|
|
176
|
+
for (const row of data.models) {
|
|
177
|
+
const cells = data.dims.map((d) => formatCellMd(row.scores[d]));
|
|
178
|
+
const overall = typeof row.overall === "number" ? row.overall.toFixed(2) : "-";
|
|
179
|
+
lines.push(`| ${escMd(row.model)} | ${cells.join(" | ")} | ${overall} |`);
|
|
180
|
+
}
|
|
181
|
+
lines.push("");
|
|
182
|
+
if (data.policies.length > 0) {
|
|
183
|
+
lines.push("## Per-Policy Ranking");
|
|
184
|
+
lines.push("");
|
|
185
|
+
for (const p of data.policies) {
|
|
186
|
+
lines.push(`### ${escMd(p.policy)}`);
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push("| rank | model | score |");
|
|
189
|
+
lines.push("|---|---|---|");
|
|
190
|
+
p.rows.forEach((r, i) => {
|
|
191
|
+
lines.push(`| ${i + 1} | ${escMd(r.model)} | ${r.score.toFixed(3)} |`);
|
|
192
|
+
});
|
|
193
|
+
lines.push("");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (data.domains.length > 0) {
|
|
197
|
+
const allDomains = collectDomainKeys(data.domains);
|
|
198
|
+
lines.push("## Domains");
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push(`| model | ${allDomains.join(" | ")} |`);
|
|
201
|
+
lines.push(`|---|${allDomains.map(() => "---").join("|")}|`);
|
|
202
|
+
for (const row of data.domains) {
|
|
203
|
+
const cells = allDomains.map((d) => formatCellMd(row.domains[d]));
|
|
204
|
+
lines.push(`| ${escMd(row.model)} | ${cells.join(" | ")} |`);
|
|
205
|
+
}
|
|
206
|
+
lines.push("");
|
|
207
|
+
}
|
|
208
|
+
if (data.delta.length > 0) {
|
|
209
|
+
lines.push("## Delta vs previous run");
|
|
210
|
+
lines.push("");
|
|
211
|
+
lines.push("| model | dimension | prev | curr | delta |");
|
|
212
|
+
lines.push("|---|---|---|---|---|");
|
|
213
|
+
for (const d of data.delta) lines.push(`| ${escMd(d.model)} | ${escMd(d.dimension)} | ${formatNum(d.prev)} | ${formatNum(d.curr)} | ${formatDelta(d.delta)} |`);
|
|
214
|
+
lines.push("");
|
|
215
|
+
}
|
|
216
|
+
if (raw !== void 0) {
|
|
217
|
+
lines.push("## Raw trace");
|
|
218
|
+
lines.push("");
|
|
219
|
+
lines.push("```json");
|
|
220
|
+
lines.push(JSON.stringify(raw, null, 2));
|
|
221
|
+
lines.push("```");
|
|
222
|
+
lines.push("");
|
|
223
|
+
}
|
|
224
|
+
return lines.join("\n");
|
|
225
|
+
}
|
|
226
|
+
function escMd(s) {
|
|
227
|
+
return String(s).replace(/[<>|]/g, (c) => c === "<" ? "<" : c === ">" ? ">" : "\\|");
|
|
228
|
+
}
|
|
229
|
+
function formatCellMd(v) {
|
|
230
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return "-";
|
|
231
|
+
return `${v >= THRESHOLDS.good ? "✅" : v >= THRESHOLDS.ok ? "⚠️" : "❌"} ${v.toFixed(2)}`;
|
|
232
|
+
}
|
|
233
|
+
function formatNum(v) {
|
|
234
|
+
return typeof v === "number" && Number.isFinite(v) ? v.toFixed(2) : "-";
|
|
235
|
+
}
|
|
236
|
+
function formatDelta(v) {
|
|
237
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return "-";
|
|
238
|
+
return `${v > 0 ? "+" : ""}${v.toFixed(2)}`;
|
|
239
|
+
}
|
|
240
|
+
function renderHtml(data, generatedAt, raw) {
|
|
241
|
+
const out = [];
|
|
242
|
+
out.push("<!doctype html>");
|
|
243
|
+
out.push("<html lang=\"en\">");
|
|
244
|
+
out.push("<head>");
|
|
245
|
+
out.push("<meta charset=\"utf-8\">");
|
|
246
|
+
out.push("<title>LLM AFS Benchmark Report</title>");
|
|
247
|
+
out.push("<style>");
|
|
248
|
+
out.push("body{font-family:system-ui,sans-serif;margin:2rem;color:#222}", "table{border-collapse:collapse;margin-bottom:1.5rem}", "th,td{border:1px solid #ccc;padding:.4rem .8rem;text-align:left}", "th{background:#f4f4f4}", ".good{background:#c6f6d5}.ok{background:#fef3c7}.bad{background:#fee2e2}", ".pos{color:#15803d}.neg{color:#b91c1c}", "h2{margin-top:2rem}");
|
|
249
|
+
out.push("</style>");
|
|
250
|
+
out.push("</head>");
|
|
251
|
+
out.push("<body>");
|
|
252
|
+
out.push("<h1>LLM AFS Benchmark Report</h1>");
|
|
253
|
+
out.push(`<p><strong>Generated:</strong> ${esc(generatedAt)}</p>`);
|
|
254
|
+
out.push(`<p><strong>Run version:</strong> ${esc(data.version)}</p>`);
|
|
255
|
+
if (data.judges.length > 0) out.push(`<p><strong>Judge(s):</strong> ${data.judges.map(esc).join(", ")}</p>`);
|
|
256
|
+
out.push(`<p><strong>Models:</strong> ${data.models.length}</p>`);
|
|
257
|
+
out.push("<h2>Model × Dimension Heatmap</h2>");
|
|
258
|
+
out.push("<table>");
|
|
259
|
+
out.push(`<thead><tr><th>model</th>${data.dims.map((d) => `<th>${esc(d)}</th>`).join("")}<th>overall</th></tr></thead>`);
|
|
260
|
+
out.push("<tbody>");
|
|
261
|
+
for (const row of data.models) {
|
|
262
|
+
const cells = data.dims.map((d) => htmlCell(row.scores[d])).join("");
|
|
263
|
+
const overall = typeof row.overall === "number" ? row.overall.toFixed(2) : "-";
|
|
264
|
+
out.push(`<tr><td>${esc(row.model)}</td>${cells}<td>${overall}</td></tr>`);
|
|
265
|
+
}
|
|
266
|
+
out.push("</tbody></table>");
|
|
267
|
+
if (data.policies.length > 0) {
|
|
268
|
+
out.push("<h2>Per-Policy Ranking</h2>");
|
|
269
|
+
for (const p of data.policies) {
|
|
270
|
+
out.push(`<h3>${esc(p.policy)}</h3>`);
|
|
271
|
+
out.push("<table>");
|
|
272
|
+
out.push("<thead><tr><th>rank</th><th>model</th><th>score</th></tr></thead><tbody>");
|
|
273
|
+
p.rows.forEach((r, i) => {
|
|
274
|
+
out.push(`<tr><td>${i + 1}</td><td>${esc(r.model)}</td><td>${r.score.toFixed(3)}</td></tr>`);
|
|
275
|
+
});
|
|
276
|
+
out.push("</tbody></table>");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (data.domains.length > 0) {
|
|
280
|
+
const allDomains = collectDomainKeys(data.domains);
|
|
281
|
+
out.push("<h2>Domains</h2>");
|
|
282
|
+
out.push("<table>");
|
|
283
|
+
out.push(`<thead><tr><th>model</th>${allDomains.map((d) => `<th>${esc(d)}</th>`).join("")}</tr></thead>`);
|
|
284
|
+
out.push("<tbody>");
|
|
285
|
+
for (const row of data.domains) {
|
|
286
|
+
const cells = allDomains.map((d) => htmlCell(row.domains[d])).join("");
|
|
287
|
+
out.push(`<tr><td>${esc(row.model)}</td>${cells}</tr>`);
|
|
288
|
+
}
|
|
289
|
+
out.push("</tbody></table>");
|
|
290
|
+
}
|
|
291
|
+
if (data.delta.length > 0) {
|
|
292
|
+
out.push("<h2>Delta vs previous run</h2>");
|
|
293
|
+
out.push("<table>");
|
|
294
|
+
out.push("<thead><tr><th>model</th><th>dimension</th><th>prev</th><th>curr</th><th>delta</th></tr></thead><tbody>");
|
|
295
|
+
for (const d of data.delta) {
|
|
296
|
+
const sign = d.delta === void 0 ? "" : d.delta > 0 ? "pos" : d.delta < 0 ? "neg" : "";
|
|
297
|
+
out.push(`<tr><td>${esc(d.model)}</td><td>${esc(d.dimension)}</td><td>${formatNum(d.prev)}</td><td>${formatNum(d.curr)}</td><td class="${sign}">${formatDelta(d.delta)}</td></tr>`);
|
|
298
|
+
}
|
|
299
|
+
out.push("</tbody></table>");
|
|
300
|
+
}
|
|
301
|
+
if (raw !== void 0) {
|
|
302
|
+
out.push("<h2>Raw trace</h2>");
|
|
303
|
+
out.push(`<pre>${esc(JSON.stringify(raw, null, 2))}</pre>`);
|
|
304
|
+
}
|
|
305
|
+
out.push("</body></html>");
|
|
306
|
+
return out.join("\n");
|
|
307
|
+
}
|
|
308
|
+
function htmlCell(v) {
|
|
309
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return "<td>-</td>";
|
|
310
|
+
return `<td class="${v >= THRESHOLDS.good ? "good" : v >= THRESHOLDS.ok ? "ok" : "bad"}">${v.toFixed(2)}</td>`;
|
|
311
|
+
}
|
|
312
|
+
function esc(s) {
|
|
313
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
314
|
+
}
|
|
315
|
+
function collectDomainKeys(rows) {
|
|
316
|
+
const set = /* @__PURE__ */ new Set();
|
|
317
|
+
for (const r of rows) for (const k of Object.keys(r.domains)) set.add(k);
|
|
318
|
+
return [...set].sort();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
//#endregion
|
|
322
|
+
exports.generateReport = generateReport;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PolicyWeightsTable } from "./patcher.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/reporter.d.ts
|
|
4
|
+
type ReportFormat = "markdown" | "html";
|
|
5
|
+
interface GenerateReportInput {
|
|
6
|
+
resultsPath: string;
|
|
7
|
+
resultsRoot: string;
|
|
8
|
+
format?: ReportFormat;
|
|
9
|
+
previousPath?: string;
|
|
10
|
+
includeRaw?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve every policy → its weight map. Bench wires this through AFS
|
|
13
|
+
* (`/dev/ai/score-weights/<name>`) so the reporter stays decoupled from
|
|
14
|
+
* ai-device internals — same pattern as patcher.ts. Returning an empty
|
|
15
|
+
* object means the report skips the per-policy ranking section.
|
|
16
|
+
*/
|
|
17
|
+
loadPolicyWeights: () => Promise<PolicyWeightsTable>;
|
|
18
|
+
}
|
|
19
|
+
interface GeneratedReport {
|
|
20
|
+
content: string;
|
|
21
|
+
format: ReportFormat;
|
|
22
|
+
generatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
declare function generateReport(input: GenerateReportInput): Promise<GeneratedReport>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { GenerateReportInput, GeneratedReport, ReportFormat, generateReport };
|
|
27
|
+
//# sourceMappingURL=reporter.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporter.d.cts","names":[],"sources":["../src/reporter.ts"],"mappings":";;;KAsCY,YAAA;AAAA,UAEK,mBAAA;EACf,WAAA;EACA,WAAA;EACA,MAAA,GAAS,YAAA;EACT,YAAA;EACA,UAAA;EAaA;;;AAqBF;;;EA3BE,iBAAA,QAAyB,OAAA,CAAQ,kBAAA;AAAA;AAAA,UAGlB,eAAA;EACf,OAAA;EACA,MAAA,EAAQ,YAAA;EACR,WAAA;AAAA;AAAA,iBAqBoB,cAAA,CAAe,KAAA,EAAO,mBAAA,GAAsB,OAAA,CAAQ,eAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PolicyWeightsTable } from "./patcher.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/reporter.d.ts
|
|
4
|
+
type ReportFormat = "markdown" | "html";
|
|
5
|
+
interface GenerateReportInput {
|
|
6
|
+
resultsPath: string;
|
|
7
|
+
resultsRoot: string;
|
|
8
|
+
format?: ReportFormat;
|
|
9
|
+
previousPath?: string;
|
|
10
|
+
includeRaw?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve every policy → its weight map. Bench wires this through AFS
|
|
13
|
+
* (`/dev/ai/score-weights/<name>`) so the reporter stays decoupled from
|
|
14
|
+
* ai-device internals — same pattern as patcher.ts. Returning an empty
|
|
15
|
+
* object means the report skips the per-policy ranking section.
|
|
16
|
+
*/
|
|
17
|
+
loadPolicyWeights: () => Promise<PolicyWeightsTable>;
|
|
18
|
+
}
|
|
19
|
+
interface GeneratedReport {
|
|
20
|
+
content: string;
|
|
21
|
+
format: ReportFormat;
|
|
22
|
+
generatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
declare function generateReport(input: GenerateReportInput): Promise<GeneratedReport>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { GenerateReportInput, GeneratedReport, ReportFormat, generateReport };
|
|
27
|
+
//# sourceMappingURL=reporter.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporter.d.mts","names":[],"sources":["../src/reporter.ts"],"mappings":";;;KAsCY,YAAA;AAAA,UAEK,mBAAA;EACf,WAAA;EACA,WAAA;EACA,MAAA,GAAS,YAAA;EACT,YAAA;EACA,UAAA;EAaA;;;AAqBF;;;EA3BE,iBAAA,QAAyB,OAAA,CAAQ,kBAAA;AAAA;AAAA,UAGlB,eAAA;EACf,OAAA;EACA,MAAA,EAAQ,YAAA;EACR,WAAA;AAAA;AAAA,iBAqBoB,cAAA,CAAe,KAAA,EAAO,mBAAA,GAAsB,OAAA,CAAQ,eAAA"}
|