@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.
Files changed (73) hide show
  1. package/LICENSE.md +26 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
  4. package/dist/_virtual/rolldown_runtime.cjs +29 -0
  5. package/dist/bench.cjs +944 -0
  6. package/dist/bench.d.cts +89 -0
  7. package/dist/bench.d.cts.map +1 -0
  8. package/dist/bench.d.mts +89 -0
  9. package/dist/bench.d.mts.map +1 -0
  10. package/dist/bench.mjs +944 -0
  11. package/dist/bench.mjs.map +1 -0
  12. package/dist/errors.cjs +14 -0
  13. package/dist/errors.d.cts +13 -0
  14. package/dist/errors.d.cts.map +1 -0
  15. package/dist/errors.d.mts +13 -0
  16. package/dist/errors.d.mts.map +1 -0
  17. package/dist/errors.mjs +14 -0
  18. package/dist/errors.mjs.map +1 -0
  19. package/dist/index.cjs +38 -0
  20. package/dist/index.d.cts +10 -0
  21. package/dist/index.d.mts +10 -0
  22. package/dist/index.mjs +10 -0
  23. package/dist/judge.cjs +189 -0
  24. package/dist/judge.d.cts +67 -0
  25. package/dist/judge.d.cts.map +1 -0
  26. package/dist/judge.d.mts +67 -0
  27. package/dist/judge.d.mts.map +1 -0
  28. package/dist/judge.mjs +185 -0
  29. package/dist/judge.mjs.map +1 -0
  30. package/dist/outlier.cjs +73 -0
  31. package/dist/outlier.d.cts +30 -0
  32. package/dist/outlier.d.cts.map +1 -0
  33. package/dist/outlier.d.mts +30 -0
  34. package/dist/outlier.d.mts.map +1 -0
  35. package/dist/outlier.mjs +73 -0
  36. package/dist/outlier.mjs.map +1 -0
  37. package/dist/patcher.cjs +296 -0
  38. package/dist/patcher.d.cts +119 -0
  39. package/dist/patcher.d.cts.map +1 -0
  40. package/dist/patcher.d.mts +119 -0
  41. package/dist/patcher.d.mts.map +1 -0
  42. package/dist/patcher.mjs +291 -0
  43. package/dist/patcher.mjs.map +1 -0
  44. package/dist/prompts.cjs +123 -0
  45. package/dist/prompts.d.cts +30 -0
  46. package/dist/prompts.d.cts.map +1 -0
  47. package/dist/prompts.d.mts +30 -0
  48. package/dist/prompts.d.mts.map +1 -0
  49. package/dist/prompts.mjs +121 -0
  50. package/dist/prompts.mjs.map +1 -0
  51. package/dist/reporter.cjs +322 -0
  52. package/dist/reporter.d.cts +27 -0
  53. package/dist/reporter.d.cts.map +1 -0
  54. package/dist/reporter.d.mts +27 -0
  55. package/dist/reporter.d.mts.map +1 -0
  56. package/dist/reporter.mjs +322 -0
  57. package/dist/reporter.mjs.map +1 -0
  58. package/dist/runner.cjs +710 -0
  59. package/dist/runner.d.cts +345 -0
  60. package/dist/runner.d.cts.map +1 -0
  61. package/dist/runner.d.mts +345 -0
  62. package/dist/runner.d.mts.map +1 -0
  63. package/dist/runner.mjs +697 -0
  64. package/dist/runner.mjs.map +1 -0
  65. package/dist/scoring.cjs +47 -0
  66. package/dist/scoring.d.cts +28 -0
  67. package/dist/scoring.d.cts.map +1 -0
  68. package/dist/scoring.d.mts +28 -0
  69. package/dist/scoring.d.mts.map +1 -0
  70. package/dist/scoring.mjs +46 -0
  71. package/dist/scoring.mjs.map +1 -0
  72. package/manifest.json +39 -0
  73. 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 === "<" ? "&lt;" : c === ">" ? "&gt;" : "\\|");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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"}