@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,291 @@
1
+ import { ValidationError } from "./errors.mjs";
2
+ import "./runner.mjs";
3
+ import { promises } from "node:fs";
4
+ import { makeNsLog } from "@aigne/afs";
5
+ import { z } from "zod";
6
+ import { randomBytes } from "node:crypto";
7
+ import * as path from "node:path";
8
+
9
+ //#region src/patcher.ts
10
+ /**
11
+ * Phase 3 — patchCatalog. Merges a results JSON into the ai-device catalog's
12
+ * `entry.afs` subtree. Maintainer tool: directly reads/writes catalog.json
13
+ * (a git-tracked dev artifact, not runtime data) — see decisions.md #24.
14
+ *
15
+ * Invariants (plan.md L459-468):
16
+ * 1. Models missing from results keep their existing `entry.afs` (deep-equal).
17
+ * 2. Only `r.scores` is copied (no other result fields leak into catalog).
18
+ * 3. The written `entry.afs` block has alphabetically sorted keys (clean diff).
19
+ * 4. Atomic write: tmp → rename, no half-written catalog on failure.
20
+ * 5. Prototype-pollution: __proto__/constructor/prototype rejected anywhere.
21
+ * 6. Path traversal: resultsPath must resolve inside resultsRoot.
22
+ * 7. Cache reload via `refreshRegistry` — failures propagate so the caller
23
+ * knows the catalog landed but routing still sees the previous snapshot.
24
+ * 8. Vault credentials sanitisation: api_key / Authorization / Bearer / etc.
25
+ * are rejected as field names anywhere in `results.<model>` payloads.
26
+ *
27
+ * Other catalog entries are written unchanged (key order preserved) so the
28
+ * git diff is limited to the `afs` block of patched models.
29
+ *
30
+ * Phase 5: weight tables are no longer inlined — the bench provider passes
31
+ * `loadPolicyWeights` (which the wiring in `bench.ts` resolves through
32
+ * `/dev/ai/score-weights/<name>`), so this module remains independent of
33
+ * ai-device internals.
34
+ */
35
+ const log = makeNsLog("provider:llm-bench");
36
+ const RESERVED_KEYS = new Set([
37
+ "__proto__",
38
+ "constructor",
39
+ "prototype",
40
+ "__defineGetter__",
41
+ "__defineSetter__",
42
+ "__lookupGetter__",
43
+ "__lookupSetter__"
44
+ ]);
45
+ const SENSITIVE_KEY_PATTERNS = [
46
+ /^api[_-]?key$/i,
47
+ /^authorization$/i,
48
+ /^bearer$/i,
49
+ /^service[_-]?account$/i,
50
+ /^private[_-]?key$/i,
51
+ /^client[_-]?secret$/i,
52
+ /^secret$/i,
53
+ /^credentials?$/i
54
+ ];
55
+ const ALLOWED_RESULT_ENTRY_FIELDS = new Set([
56
+ "n_samples",
57
+ "scores",
58
+ "domains",
59
+ "stddev",
60
+ "per_prompt",
61
+ "errors"
62
+ ]);
63
+ const MAX_RESULT_ENTRIES = 500;
64
+ const MAX_N_SAMPLES = 1e3;
65
+ const ScoresSchema = z.object({
66
+ exploration: z.number().min(0).max(1).optional(),
67
+ tool_reliability: z.number().min(0).max(1).optional(),
68
+ self_stop: z.number().min(0).max(1).optional(),
69
+ parallelism: z.number().min(0).max(1).optional(),
70
+ efficiency: z.number().min(0).max(1).optional(),
71
+ instruction_following: z.number().min(0).max(1).optional()
72
+ }).strict();
73
+ const DomainsSchema = z.object({
74
+ chinese: z.number().min(0).max(1).optional(),
75
+ english: z.number().min(0).max(1).optional(),
76
+ coding: z.number().min(0).max(1).optional()
77
+ }).strict();
78
+ const ResultEntrySchema = z.object({
79
+ n_samples: z.number().int().min(1).max(MAX_N_SAMPLES),
80
+ scores: ScoresSchema,
81
+ domains: DomainsSchema.optional(),
82
+ stddev: z.record(z.string(), z.number()).optional(),
83
+ per_prompt: z.record(z.string().regex(/^[A-Za-z0-9._-]+$/), ScoresSchema).optional(),
84
+ errors: z.array(z.unknown()).optional()
85
+ }).strict();
86
+ const JudgeIdSchema = z.string().regex(/^[A-Za-z0-9._-]+$/);
87
+ const JudgeModelSchema = z.union([JudgeIdSchema, z.array(JudgeIdSchema).min(1)]).nullable().optional();
88
+ const ResultsRootSchema = z.object({
89
+ version: z.string().min(1),
90
+ judge_model: JudgeModelSchema,
91
+ config: z.unknown().optional(),
92
+ results: z.record(z.string().regex(/^[A-Za-z0-9._-]+$/), ResultEntrySchema),
93
+ outliers: z.unknown().optional()
94
+ }).strict();
95
+ /**
96
+ * Validate raw JSON shape + reject reserved/sensitive keys anywhere.
97
+ * Throws `ValidationError` on any rejection.
98
+ *
99
+ * Layers (in order):
100
+ * 1. Deep-walk: reject `__proto__` / `constructor` / `prototype` and any
101
+ * key matching a credential pattern (`api_key`, `Authorization`, …).
102
+ * 2. Resource-exhaustion guard: results entries ≤ MAX_RESULT_ENTRIES.
103
+ * 3. Per-entry whitelist: only `n_samples`/`scores`/`domains`/`stddev`.
104
+ * 4. Zod schema parse (z.strict() also catches unknown scores dimensions).
105
+ */
106
+ function validateResultsShape(raw) {
107
+ rejectDangerousKeysDeep(raw);
108
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
109
+ const r = raw.results;
110
+ if (r && typeof r === "object" && !Array.isArray(r)) {
111
+ const keys = Object.keys(r);
112
+ if (keys.length > MAX_RESULT_ENTRIES) throw new ValidationError(`results contains ${keys.length} entries; max ${MAX_RESULT_ENTRIES}`);
113
+ for (const k of keys) {
114
+ const entry = r[k];
115
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
116
+ for (const f of Object.keys(entry)) if (!ALLOWED_RESULT_ENTRY_FIELDS.has(f)) throw new ValidationError(`unknown field "${f}" in results.${k}`);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ const parsed = ResultsRootSchema.safeParse(raw);
122
+ if (!parsed.success) throw new ValidationError(`results JSON is invalid: ${parsed.error.message}`);
123
+ return {
124
+ version: parsed.data.version,
125
+ judge_model: parsed.data.judge_model ?? null,
126
+ results: parsed.data.results
127
+ };
128
+ }
129
+ function rejectDangerousKeysDeep(value, p = "$") {
130
+ if (value === null || typeof value !== "object") return;
131
+ if (Array.isArray(value)) {
132
+ for (let i = 0; i < value.length; i++) rejectDangerousKeysDeep(value[i], `${p}[${i}]`);
133
+ return;
134
+ }
135
+ for (const k of Object.keys(value)) {
136
+ if (RESERVED_KEYS.has(k)) throw new ValidationError(`reserved key "${k}" not allowed at ${p}`);
137
+ for (const re of SENSITIVE_KEY_PATTERNS) if (re.test(k)) throw new ValidationError(`sensitive field "${k}" not allowed at ${p}`);
138
+ rejectDangerousKeysDeep(value[k], `${p}.${k}`);
139
+ }
140
+ }
141
+ /**
142
+ * Resolve `p` against cwd if relative, then ensure it lives inside `resultsRoot`.
143
+ * Defends against `..` segments and absolute paths that escape the root.
144
+ * Returns the absolute path on success; throws `ValidationError` otherwise.
145
+ *
146
+ * Note: this is purely lexical (no `fs.realpath`) — a maintainer-created
147
+ * symlink inside `resultsRoot` pointing outside is not caught here. The threat
148
+ * model is a maintainer tool, so the lexical check is intentionally cheap; if
149
+ * a symlink-based vector materialises in production, swap to `fs.realpath`.
150
+ */
151
+ function assertWithinResultsDir(p, resultsRoot) {
152
+ const absRoot = path.resolve(resultsRoot);
153
+ const abs = path.resolve(p);
154
+ const rel = path.relative(absRoot, abs);
155
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) throw new ValidationError(`results path outside ${resultsRoot}: ${p}`);
156
+ return abs;
157
+ }
158
+ /**
159
+ * Return a deeply-key-sorted clone of an `afs` block. Plain objects get keys
160
+ * in alphabetical order; arrays preserve order; primitives pass through.
161
+ * Does not mutate input.
162
+ */
163
+ function sortAfsBlock(value) {
164
+ if (value === null || typeof value !== "object") return value;
165
+ if (Array.isArray(value)) return value.map((v) => sortAfsBlock(v));
166
+ const sorted = {};
167
+ for (const k of Object.keys(value).sort()) sorted[k] = sortAfsBlock(value[k]);
168
+ return sorted;
169
+ }
170
+ const BIOME_LINE_WIDTH = 100;
171
+ const BIOME_INDENT = " ";
172
+ function stringifyBiomeJSON(value, currentIndent = "", startColumn = 0) {
173
+ if (value === null) return "null";
174
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
175
+ if (typeof value === "string") return JSON.stringify(value);
176
+ if (Array.isArray(value)) {
177
+ if (value.length === 0) return "[]";
178
+ if (value.every((v) => v === null || typeof v !== "object")) {
179
+ const inline = `[${value.map((v) => stringifyBiomeJSON(v)).join(", ")}]`;
180
+ if (startColumn + inline.length <= BIOME_LINE_WIDTH) return inline;
181
+ }
182
+ const inner$1 = currentIndent + BIOME_INDENT;
183
+ return `[\n${value.map((v) => `${inner$1}${stringifyBiomeJSON(v, inner$1, inner$1.length)}`).join(",\n")}\n${currentIndent}]`;
184
+ }
185
+ if (typeof value !== "object") return JSON.stringify(value);
186
+ const entries = Object.entries(value);
187
+ if (entries.length === 0) return "{}";
188
+ const inner = currentIndent + BIOME_INDENT;
189
+ return `{\n${entries.map(([k, v]) => {
190
+ const keyPart = `${inner}${JSON.stringify(k)}: `;
191
+ return `${keyPart}${stringifyBiomeJSON(v, inner, keyPart.length)}`;
192
+ }).join(",\n")}\n${currentIndent}}`;
193
+ }
194
+ /** Write `obj` as biome-compatible pretty-printed JSON to `target` via tmp+rename. */
195
+ async function atomicWriteJSON(target, obj) {
196
+ const dir = path.dirname(target);
197
+ await promises.mkdir(dir, { recursive: true });
198
+ const tmp = path.join(dir, `.tmp-${randomBytes(6).toString("hex")}.json`);
199
+ const payload = `${stringifyBiomeJSON(obj)}\n`;
200
+ await promises.writeFile(tmp, payload, "utf8");
201
+ try {
202
+ await promises.rename(tmp, target);
203
+ } catch (err) {
204
+ await promises.rm(tmp, { force: true });
205
+ throw err;
206
+ }
207
+ }
208
+ function meanOf(xs) {
209
+ if (xs.length === 0) return 0;
210
+ let s = 0;
211
+ for (const x of xs) s += x;
212
+ return s / xs.length;
213
+ }
214
+ function getNested(obj, p) {
215
+ const parts = p.split(".");
216
+ let cur = obj;
217
+ for (const k of parts) {
218
+ if (cur === null || typeof cur !== "object") return void 0;
219
+ cur = cur[k];
220
+ }
221
+ return cur;
222
+ }
223
+ function computeWeightedScore(entry, weights) {
224
+ let total = 0;
225
+ for (const [pathKey, w] of Object.entries(weights)) {
226
+ const v = getNested(entry, pathKey);
227
+ if (typeof v === "number" && Number.isFinite(v)) total += v * w;
228
+ }
229
+ return total;
230
+ }
231
+ async function patchCatalog(input) {
232
+ const safeResultsPath = assertWithinResultsDir(input.resultsPath, input.resultsRoot);
233
+ let raw;
234
+ try {
235
+ raw = await promises.readFile(safeResultsPath, "utf8");
236
+ } catch (err) {
237
+ throw new ValidationError(`failed to read results: ${err instanceof Error ? err.message : String(err)}`);
238
+ }
239
+ let parsedRaw;
240
+ try {
241
+ parsedRaw = JSON.parse(raw);
242
+ } catch (err) {
243
+ throw new ValidationError(`results is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
244
+ }
245
+ const validated = validateResultsShape(parsedRaw);
246
+ const catalogText = await promises.readFile(input.catalogPath, "utf8");
247
+ const catalog = JSON.parse(catalogText);
248
+ if (!Array.isArray(catalog)) throw new ValidationError("catalog.json must be an array of entries");
249
+ const summary = {
250
+ changes: [],
251
+ skipped: []
252
+ };
253
+ if (catalog.length === 0) return summary;
254
+ const weightsTable = await input.loadPolicyWeights();
255
+ if (!weightsTable || typeof weightsTable !== "object" || Array.isArray(weightsTable)) throw new ValidationError("loadPolicyWeights must return a plain object of { policy: weights }");
256
+ for (const [policy, weights] of Object.entries(weightsTable)) if (!weights || typeof weights !== "object" || Array.isArray(weights)) throw new ValidationError(`weights for policy "${policy}" must be a plain object`);
257
+ for (const entry of catalog) {
258
+ const r = validated.results[entry.model];
259
+ if (!r) {
260
+ summary.skipped.push(entry.model);
261
+ continue;
262
+ }
263
+ const before = entry.afs;
264
+ const policyScores = {};
265
+ for (const [name, w] of Object.entries(weightsTable)) policyScores[name] = computeWeightedScore(r, w);
266
+ entry.afs = sortAfsBlock({
267
+ status: "benchmarked",
268
+ version: validated.version,
269
+ last_seen_in_run: validated.version,
270
+ judge: validated.judge_model,
271
+ samples: r.n_samples,
272
+ scores: { ...r.scores },
273
+ overall: meanOf(Object.values(r.scores)),
274
+ policyScores,
275
+ ...r.domains ? { domains: { ...r.domains } } : {}
276
+ });
277
+ summary.changes.push({
278
+ model: entry.model,
279
+ before,
280
+ after: entry.afs
281
+ });
282
+ }
283
+ await atomicWriteJSON(input.catalogPath, catalog);
284
+ if (input.refreshRegistry) await input.refreshRegistry();
285
+ if (summary.changes.length > 0) log.warn(`[patchCatalog] ${summary.changes.length} catalog entr${summary.changes.length === 1 ? "y" : "ies"} updated. Run \`pnpm --filter @aigne/afs-ai-device --filter @aigne/arc-cli build && arc service restart\` to make the daemon's policy router pick them up — the catalog is bundled into ai-device's dist and refreshRegistry only rebuilds the hub registry, not the catalog.`);
286
+ return summary;
287
+ }
288
+
289
+ //#endregion
290
+ export { assertWithinResultsDir, atomicWriteJSON, patchCatalog, sortAfsBlock, validateResultsShape };
291
+ //# sourceMappingURL=patcher.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patcher.mjs","names":["inner","fs"],"sources":["../src/patcher.ts"],"sourcesContent":["/**\n * Phase 3 — patchCatalog. Merges a results JSON into the ai-device catalog's\n * `entry.afs` subtree. Maintainer tool: directly reads/writes catalog.json\n * (a git-tracked dev artifact, not runtime data) — see decisions.md #24.\n *\n * Invariants (plan.md L459-468):\n * 1. Models missing from results keep their existing `entry.afs` (deep-equal).\n * 2. Only `r.scores` is copied (no other result fields leak into catalog).\n * 3. The written `entry.afs` block has alphabetically sorted keys (clean diff).\n * 4. Atomic write: tmp → rename, no half-written catalog on failure.\n * 5. Prototype-pollution: __proto__/constructor/prototype rejected anywhere.\n * 6. Path traversal: resultsPath must resolve inside resultsRoot.\n * 7. Cache reload via `refreshRegistry` — failures propagate so the caller\n * knows the catalog landed but routing still sees the previous snapshot.\n * 8. Vault credentials sanitisation: api_key / Authorization / Bearer / etc.\n * are rejected as field names anywhere in `results.<model>` payloads.\n *\n * Other catalog entries are written unchanged (key order preserved) so the\n * git diff is limited to the `afs` block of patched models.\n *\n * Phase 5: weight tables are no longer inlined — the bench provider passes\n * `loadPolicyWeights` (which the wiring in `bench.ts` resolves through\n * `/dev/ai/score-weights/<name>`), so this module remains independent of\n * ai-device internals.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { promises as fs } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { makeNsLog } from \"@aigne/afs\";\nimport { z } from \"zod\";\nimport { ValidationError } from \"./runner.js\";\n\nconst log = makeNsLog(\"provider:llm-bench\");\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\nexport interface AFSScores {\n status: \"benchmarked\" | \"untested\" | \"deprecated_score\";\n version: string;\n last_seen_in_run: string;\n /**\n * Judge model id(s) used to score `instruction_following`. String for\n * single-judge runs (back-compat), array for multi-judge ensemble runs\n * (Phase 0 of Run 4 — plan.md L1052), `null` when no judging happened.\n */\n judge: string | string[] | null;\n samples: number;\n scores: Record<string, number>;\n overall: number;\n policyScores: Record<string, number>;\n domains?: Record<string, number>;\n}\n\nexport interface CatalogEntry {\n model: string;\n afs?: AFSScores;\n [k: string]: unknown;\n}\n\nexport interface PatchSummary {\n changes: Array<{ model: string; before: AFSScores | undefined; after: AFSScores }>;\n skipped: string[];\n}\n\nexport type PolicyWeightsTable = Record<string, Record<string, number>>;\n\nexport interface PatchCatalogInput {\n resultsPath: string;\n catalogPath: string;\n resultsRoot: string;\n /**\n * Resolve every policy → its weight map. Bench wires this through AFS\n * (`/dev/ai/score-weights/<name>`), keeping this module free of ai-device\n * imports. Returning an empty object means no `policyScores` are produced.\n */\n loadPolicyWeights: () => Promise<PolicyWeightsTable>;\n /**\n * Triggered after the catalog has been atomically written so the device's\n * registry cache picks the new scores up immediately. Failures propagate —\n * the catalog already landed, but the caller should know routing still\n * sees the previous snapshot until the next refresh tick.\n */\n refreshRegistry?: () => Promise<void>;\n}\n\n// ─── Validation ───────────────────────────────────────────────────────────\n\nconst RESERVED_KEYS = new Set([\n \"__proto__\",\n \"constructor\",\n \"prototype\",\n \"__defineGetter__\",\n \"__defineSetter__\",\n \"__lookupGetter__\",\n \"__lookupSetter__\",\n]);\n\nconst SENSITIVE_KEY_PATTERNS: RegExp[] = [\n /^api[_-]?key$/i,\n /^authorization$/i,\n /^bearer$/i,\n /^service[_-]?account$/i,\n /^private[_-]?key$/i,\n /^client[_-]?secret$/i,\n /^secret$/i,\n /^credentials?$/i,\n];\n\n// Whitelist of fields permitted on each results.<model> entry. Anything else\n// is rejected — keeps unexpected fields (rogue, sensitive, prototype-poll) out\n// of the catalog. `stddev` and `per_prompt` are kept because the runner emits\n// them (Phase 1 / Phase 2 of Run 3 respectively); they intentionally don't\n// carry across into entry.afs — the catalog stores aggregated scores only.\nconst ALLOWED_RESULT_ENTRY_FIELDS = new Set([\n \"n_samples\",\n \"scores\",\n \"domains\",\n \"stddev\",\n \"per_prompt\",\n \"errors\",\n]);\n\nconst MAX_RESULT_ENTRIES = 500;\nconst MAX_N_SAMPLES = 1000;\n\nconst ScoresSchema = z\n .object({\n exploration: z.number().min(0).max(1).optional(),\n tool_reliability: z.number().min(0).max(1).optional(),\n self_stop: z.number().min(0).max(1).optional(),\n parallelism: z.number().min(0).max(1).optional(),\n efficiency: z.number().min(0).max(1).optional(),\n instruction_following: z.number().min(0).max(1).optional(),\n })\n .strict();\n\n// Domain enum mirrors `prompts.ts` — both layers reject anything outside the\n// whitelist so a hand-crafted results JSON can't push arbitrary keys into\n// `entry.afs.domains` (plan.md L1089 — Step 3.2 Security). `.strict()`\n// rejects unknown keys; each enum value is `.optional()` because a partial\n// matrix (eg. only `english` prompts ran) shouldn't fail validation.\nconst DomainsSchema = z\n .object({\n chinese: z.number().min(0).max(1).optional(),\n english: z.number().min(0).max(1).optional(),\n coding: z.number().min(0).max(1).optional(),\n })\n .strict();\n\nconst ResultEntrySchema = z\n .object({\n n_samples: z.number().int().min(1).max(MAX_N_SAMPLES),\n scores: ScoresSchema,\n domains: DomainsSchema.optional(),\n stddev: z.record(z.string(), z.number()).optional(),\n // Per-prompt mean scores; structurally `{ <prompt_id>: <ScoresSchema> }`.\n // Validated but not propagated into entry.afs — the catalog only stores\n // the aggregated `scores`.\n per_prompt: z.record(z.string().regex(/^[A-Za-z0-9._-]+$/), ScoresSchema).optional(),\n // Per-sample failures recorded by runMatrixFull; passthrough only.\n errors: z.array(z.unknown()).optional(),\n })\n .strict();\n\n// Judge id pattern matches `JUDGE_MODEL_PATTERN` in judge.ts — both layers\n// reject path-traversal tokens so the catalog never persists a hostile id.\nconst JudgeIdSchema = z.string().regex(/^[A-Za-z0-9._-]+$/);\nconst JudgeModelSchema = z\n .union([JudgeIdSchema, z.array(JudgeIdSchema).min(1)])\n .nullable()\n .optional();\n\nconst ResultsRootSchema = z\n .object({\n version: z.string().min(1),\n judge_model: JudgeModelSchema,\n config: z.unknown().optional(),\n results: z.record(z.string().regex(/^[A-Za-z0-9._-]+$/), ResultEntrySchema),\n // Run 4 phase 3 (outlier filter) emits `outliers: Array<{...}>` on the\n // results root. patcher.ts treats it as audit-only metadata — accepted\n // but not propagated into `entry.afs`.\n outliers: z.unknown().optional(),\n })\n .strict();\n\nexport interface ValidatedResults {\n version: string;\n judge_model: string | string[] | null;\n results: Record<string, ResultEntry>;\n}\n\ninterface ResultEntry {\n n_samples: number;\n scores: Record<string, number>;\n domains?: Record<string, number>;\n}\n\n/**\n * Validate raw JSON shape + reject reserved/sensitive keys anywhere.\n * Throws `ValidationError` on any rejection.\n *\n * Layers (in order):\n * 1. Deep-walk: reject `__proto__` / `constructor` / `prototype` and any\n * key matching a credential pattern (`api_key`, `Authorization`, …).\n * 2. Resource-exhaustion guard: results entries ≤ MAX_RESULT_ENTRIES.\n * 3. Per-entry whitelist: only `n_samples`/`scores`/`domains`/`stddev`.\n * 4. Zod schema parse (z.strict() also catches unknown scores dimensions).\n */\nexport function validateResultsShape(raw: unknown): ValidatedResults {\n rejectDangerousKeysDeep(raw);\n\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n const r = (raw as Record<string, unknown>).results;\n if (r && typeof r === \"object\" && !Array.isArray(r)) {\n const keys = Object.keys(r);\n if (keys.length > MAX_RESULT_ENTRIES) {\n throw new ValidationError(\n `results contains ${keys.length} entries; max ${MAX_RESULT_ENTRIES}`,\n );\n }\n for (const k of keys) {\n const entry = (r as Record<string, unknown>)[k];\n if (entry && typeof entry === \"object\" && !Array.isArray(entry)) {\n for (const f of Object.keys(entry as Record<string, unknown>)) {\n if (!ALLOWED_RESULT_ENTRY_FIELDS.has(f)) {\n throw new ValidationError(`unknown field \"${f}\" in results.${k}`);\n }\n }\n }\n }\n }\n }\n\n const parsed = ResultsRootSchema.safeParse(raw);\n if (!parsed.success) {\n throw new ValidationError(`results JSON is invalid: ${parsed.error.message}`);\n }\n return {\n version: parsed.data.version,\n judge_model: parsed.data.judge_model ?? null,\n // Single-judge writes a string; multi-judge writes an array — in the\n // catalog `entry.afs.judge` mirrors that shape (plan.md L1052).\n results: parsed.data.results as Record<string, ResultEntry>,\n };\n}\n\nfunction rejectDangerousKeysDeep(value: unknown, p = \"$\"): void {\n if (value === null || typeof value !== \"object\") return;\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) rejectDangerousKeysDeep(value[i], `${p}[${i}]`);\n return;\n }\n for (const k of Object.keys(value as Record<string, unknown>)) {\n if (RESERVED_KEYS.has(k)) {\n throw new ValidationError(`reserved key \"${k}\" not allowed at ${p}`);\n }\n for (const re of SENSITIVE_KEY_PATTERNS) {\n if (re.test(k)) {\n throw new ValidationError(`sensitive field \"${k}\" not allowed at ${p}`);\n }\n }\n rejectDangerousKeysDeep((value as Record<string, unknown>)[k], `${p}.${k}`);\n }\n}\n\n// ─── Path safety ──────────────────────────────────────────────────────────\n\n/**\n * Resolve `p` against cwd if relative, then ensure it lives inside `resultsRoot`.\n * Defends against `..` segments and absolute paths that escape the root.\n * Returns the absolute path on success; throws `ValidationError` otherwise.\n *\n * Note: this is purely lexical (no `fs.realpath`) — a maintainer-created\n * symlink inside `resultsRoot` pointing outside is not caught here. The threat\n * model is a maintainer tool, so the lexical check is intentionally cheap; if\n * a symlink-based vector materialises in production, swap to `fs.realpath`.\n */\nexport function assertWithinResultsDir(p: string, resultsRoot: string): string {\n const absRoot = path.resolve(resultsRoot);\n const abs = path.resolve(p);\n const rel = path.relative(absRoot, abs);\n if (rel === \"\" || rel.startsWith(\"..\") || path.isAbsolute(rel)) {\n throw new ValidationError(`results path outside ${resultsRoot}: ${p}`);\n }\n return abs;\n}\n\n// ─── Stable-key serialisation ────────────────────────────────────────────\n\n/**\n * Return a deeply-key-sorted clone of an `afs` block. Plain objects get keys\n * in alphabetical order; arrays preserve order; primitives pass through.\n * Does not mutate input.\n */\nexport function sortAfsBlock<T>(value: T): T {\n if (value === null || typeof value !== \"object\") return value;\n if (Array.isArray(value)) return value.map((v) => sortAfsBlock(v)) as unknown as T;\n const sorted: Record<string, unknown> = {};\n for (const k of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[k] = sortAfsBlock((value as Record<string, unknown>)[k]);\n }\n return sorted as T;\n}\n\n// ─── Atomic JSON write ───────────────────────────────────────────────────\n\n// Match biome's pretty-print for JSON: indent 2 spaces, inline arrays of\n// primitives when the line fits within lineWidth=100. Without this, every\n// `JSON.stringify(catalog, null, 2)` re-expands `aliases` / `recommendedFor`\n// / `modality.input` etc. into multi-line form, and the next `pnpm format`\n// run in CI immediately churns 1000+ lines of diff back. The maintainer\n// patcher writes the catalog once per benchmark — it must not fight the\n// formatter on every cycle.\nconst BIOME_LINE_WIDTH = 100;\nconst BIOME_INDENT = \" \";\n\nfunction stringifyBiomeJSON(value: unknown, currentIndent = \"\", startColumn = 0): string {\n if (value === null) return \"null\";\n if (typeof value === \"boolean\" || typeof value === \"number\") return String(value);\n if (typeof value === \"string\") return JSON.stringify(value);\n\n if (Array.isArray(value)) {\n if (value.length === 0) return \"[]\";\n const allPrimitive = value.every((v) => v === null || typeof v !== \"object\");\n if (allPrimitive) {\n const inline = `[${value.map((v) => stringifyBiomeJSON(v)).join(\", \")}]`;\n if (startColumn + inline.length <= BIOME_LINE_WIDTH) return inline;\n }\n const inner = currentIndent + BIOME_INDENT;\n const items = value.map((v) => `${inner}${stringifyBiomeJSON(v, inner, inner.length)}`);\n return `[\\n${items.join(\",\\n\")}\\n${currentIndent}]`;\n }\n\n if (typeof value !== \"object\") return JSON.stringify(value);\n\n const entries = Object.entries(value as Record<string, unknown>);\n if (entries.length === 0) return \"{}\";\n const inner = currentIndent + BIOME_INDENT;\n const lines = entries.map(([k, v]) => {\n const keyPart = `${inner}${JSON.stringify(k)}: `;\n return `${keyPart}${stringifyBiomeJSON(v, inner, keyPart.length)}`;\n });\n return `{\\n${lines.join(\",\\n\")}\\n${currentIndent}}`;\n}\n\n/** Write `obj` as biome-compatible pretty-printed JSON to `target` via tmp+rename. */\nexport async function atomicWriteJSON(target: string, obj: unknown): Promise<void> {\n const dir = path.dirname(target);\n await fs.mkdir(dir, { recursive: true });\n const tmp = path.join(dir, `.tmp-${randomBytes(6).toString(\"hex\")}.json`);\n const payload = `${stringifyBiomeJSON(obj)}\\n`;\n await fs.writeFile(tmp, payload, \"utf8\");\n try {\n await fs.rename(tmp, target);\n } catch (err) {\n await fs.rm(tmp, { force: true });\n throw err;\n }\n}\n\n// ─── Scoring helpers ─────────────────────────────────────────────────────\n\nfunction meanOf(xs: number[]): number {\n if (xs.length === 0) return 0;\n let s = 0;\n for (const x of xs) s += x;\n return s / xs.length;\n}\n\nfunction getNested(obj: unknown, p: string): unknown {\n const parts = p.split(\".\");\n let cur: unknown = obj;\n for (const k of parts) {\n if (cur === null || typeof cur !== \"object\") return undefined;\n cur = (cur as Record<string, unknown>)[k];\n }\n return cur;\n}\n\nfunction computeWeightedScore(entry: ResultEntry, weights: Record<string, number>): number {\n let total = 0;\n for (const [pathKey, w] of Object.entries(weights)) {\n const v = getNested(entry, pathKey);\n if (typeof v === \"number\" && Number.isFinite(v)) total += v * w;\n }\n return total;\n}\n\n// ─── patchCatalog ────────────────────────────────────────────────────────\n\nexport async function patchCatalog(input: PatchCatalogInput): Promise<PatchSummary> {\n const safeResultsPath = assertWithinResultsDir(input.resultsPath, input.resultsRoot);\n\n let raw: string;\n try {\n raw = await fs.readFile(safeResultsPath, \"utf8\");\n } catch (err) {\n throw new ValidationError(\n `failed to read results: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n let parsedRaw: unknown;\n try {\n parsedRaw = JSON.parse(raw);\n } catch (err) {\n throw new ValidationError(\n `results is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n const validated = validateResultsShape(parsedRaw);\n\n const catalogText = await fs.readFile(input.catalogPath, \"utf8\");\n const catalog = JSON.parse(catalogText) as CatalogEntry[];\n if (!Array.isArray(catalog)) {\n throw new ValidationError(\"catalog.json must be an array of entries\");\n }\n\n const summary: PatchSummary = { changes: [], skipped: [] };\n\n // Skip the AFS round-trip for empty catalogs — nothing to score.\n if (catalog.length === 0) return summary;\n\n // Pull weights up front (one round-trip, then a parallel fanout in the\n // bench wiring) so the per-entry loop is sync. Validate shape at the\n // choke point — refuse to write rather than persist garbage.\n const weightsTable = await input.loadPolicyWeights();\n if (!weightsTable || typeof weightsTable !== \"object\" || Array.isArray(weightsTable)) {\n throw new ValidationError(\n \"loadPolicyWeights must return a plain object of { policy: weights }\",\n );\n }\n for (const [policy, weights] of Object.entries(weightsTable)) {\n if (!weights || typeof weights !== \"object\" || Array.isArray(weights)) {\n throw new ValidationError(`weights for policy \"${policy}\" must be a plain object`);\n }\n }\n\n for (const entry of catalog) {\n const r = validated.results[entry.model];\n if (!r) {\n summary.skipped.push(entry.model);\n continue;\n }\n const before = entry.afs;\n\n const policyScores: Record<string, number> = {};\n for (const [name, w] of Object.entries(weightsTable)) {\n policyScores[name] = computeWeightedScore(r, w);\n }\n\n const afsBlock: AFSScores = {\n status: \"benchmarked\",\n version: validated.version,\n last_seen_in_run: validated.version,\n judge: validated.judge_model,\n samples: r.n_samples,\n scores: { ...r.scores },\n overall: meanOf(Object.values(r.scores)),\n policyScores,\n ...(r.domains ? { domains: { ...r.domains } } : {}),\n };\n\n entry.afs = sortAfsBlock(afsBlock);\n summary.changes.push({ model: entry.model, before, after: entry.afs });\n }\n\n await atomicWriteJSON(input.catalogPath, catalog);\n\n // Phase 5: surface refresh failures — the catalog write already succeeded,\n // but the caller needs to know the device snapshot is stale until the\n // periodic refresh runs.\n if (input.refreshRegistry) await input.refreshRegistry();\n\n // The ai-device package imports `catalog.json` at build time\n // (`providers/ai/device/src/index.ts:42`), so a running daemon caches the\n // pre-patch catalog snapshot. `refreshRegistry` rebuilds the hub registry\n // from the cached catalog; it does NOT pick up the new file. Until that\n // import becomes a runtime read, the maintainer flow is:\n //\n // 1. patchCatalog (this function) — file written ✓, registry refreshed ✓\n // 2. pnpm --filter @aigne/afs-ai-device --filter @aigne/arc-cli build\n // 3. arc service restart\n //\n // Without 2+3 the new scores are visible via `arc afs read /catalog/<id>`\n // but `/dev/ai/.actions/chat` policy ranking still uses old scores.\n if (summary.changes.length > 0) {\n log.warn(\n `[patchCatalog] ${summary.changes.length} catalog entr${summary.changes.length === 1 ? \"y\" : \"ies\"} updated. ` +\n \"Run `pnpm --filter @aigne/afs-ai-device --filter @aigne/arc-cli build && arc service restart` \" +\n \"to make the daemon's policy router pick them up — the catalog is bundled into ai-device's dist \" +\n \"and refreshRegistry only rebuilds the hub registry, not the catalog.\",\n );\n }\n\n return summary;\n}\n"],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ let _aigne_afs = require("@aigne/afs");
3
+ let zod = require("zod");
4
+ let gray_matter = require("gray-matter");
5
+ gray_matter = require_rolldown_runtime.__toESM(gray_matter);
6
+ let js_yaml = require("js-yaml");
7
+
8
+ //#region src/prompts.ts
9
+ /**
10
+ * Prompt loader for the LLM-AFS benchmark suite.
11
+ *
12
+ * Each prompt is a Markdown file with YAML frontmatter (`gray-matter`-parsed)
13
+ * and a body sliced by H1 sections (`# Task`, `# System prompt`, etc.).
14
+ *
15
+ * Phase 0 only needs the load + parse path so that `list /bench/prompts` and
16
+ * `read /bench/prompts/<id>` return real entries. Runner / patcher consume the
17
+ * parsed shape in Phase 1.
18
+ */
19
+ const GRAY_MATTER_OPTIONS = { engines: { yaml: {
20
+ parse: (str) => (0, js_yaml.load)(str),
21
+ stringify: (data) => (0, js_yaml.dump)(data)
22
+ } } };
23
+ const promptFrontmatterSchema = zod.z.object({
24
+ id: zod.z.string().min(1, "frontmatter.id is required"),
25
+ title: zod.z.string().min(1, "frontmatter.title is required"),
26
+ domain: zod.z.enum([
27
+ "chinese",
28
+ "english",
29
+ "coding"
30
+ ]).optional(),
31
+ max_rounds: zod.z.number().int().positive("max_rounds must be a positive integer"),
32
+ weight_dim: zod.z.string().optional()
33
+ });
34
+ /**
35
+ * Slice markdown body into sections keyed by H1 heading text.
36
+ *
37
+ * Section headings are case-sensitive and trimmed. Content before the first H1
38
+ * is dropped — by convention every benchmark prompt starts with `# Task`.
39
+ */
40
+ function sliceBodyByH1(body) {
41
+ const out = {};
42
+ const lines = body.split(/\r?\n/);
43
+ let currentName;
44
+ let currentBuf = [];
45
+ const flush = () => {
46
+ if (currentName !== void 0) out[currentName] = currentBuf.join("\n").trim();
47
+ };
48
+ for (const line of lines) {
49
+ const match = /^#\s+(.+?)\s*$/.exec(line);
50
+ if (match && !line.startsWith("##")) {
51
+ flush();
52
+ currentName = match[1].trim();
53
+ currentBuf = [];
54
+ } else if (currentName !== void 0) currentBuf.push(line);
55
+ }
56
+ flush();
57
+ return out;
58
+ }
59
+ /**
60
+ * Parse a single prompt file. Throws `BadPromptError` (regular Error subclass)
61
+ * if frontmatter is missing required fields.
62
+ */
63
+ var BadPromptError = class extends Error {
64
+ name = "BadPromptError";
65
+ constructor(filename, message) {
66
+ super(`[${filename}] ${message}`);
67
+ this.filename = filename;
68
+ }
69
+ };
70
+ function parsePrompt(filename, raw) {
71
+ const parsed = (0, gray_matter.default)(raw, GRAY_MATTER_OPTIONS);
72
+ const fm = promptFrontmatterSchema.safeParse(parsed.data);
73
+ if (!fm.success) throw new BadPromptError(filename, `invalid frontmatter — ${fm.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`);
74
+ const body = parsed.content.replace(/^\s+/, "");
75
+ return {
76
+ id: fm.data.id,
77
+ filename,
78
+ frontmatter: fm.data,
79
+ body,
80
+ sections: sliceBodyByH1(body)
81
+ };
82
+ }
83
+ /**
84
+ * Load every `*.md` file under `dir` and parse it into a Prompt. Missing
85
+ * directory → empty array (Phase 0 must not crash on unconfigured promptsDir).
86
+ *
87
+ * Files that fail frontmatter validation are skipped with their error returned
88
+ * in `errors` so callers can surface them (Phase 1 runner, conformance probe).
89
+ */
90
+ async function loadPromptsFromDir(dir) {
91
+ const platform = (0, _aigne_afs.getPlatform)();
92
+ const fs = platform.fs;
93
+ const path = platform.path;
94
+ if (!fs) return {
95
+ prompts: [],
96
+ errors: []
97
+ };
98
+ if (!await fs.exists(dir)) return {
99
+ prompts: [],
100
+ errors: []
101
+ };
102
+ const entries = await fs.readdir(dir);
103
+ const prompts = [];
104
+ const errors = [];
105
+ for (const filename of entries.slice().sort()) {
106
+ if (path.extname(filename).toLowerCase() !== ".md") continue;
107
+ const raw = await fs.readTextFile(path.join(dir, filename));
108
+ try {
109
+ prompts.push(parsePrompt(filename, raw));
110
+ } catch (err) {
111
+ if (err instanceof BadPromptError) errors.push(err);
112
+ else throw err;
113
+ }
114
+ }
115
+ return {
116
+ prompts,
117
+ errors
118
+ };
119
+ }
120
+
121
+ //#endregion
122
+ exports.BadPromptError = BadPromptError;
123
+ exports.loadPromptsFromDir = loadPromptsFromDir;
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/prompts.d.ts
4
+ declare const promptFrontmatterSchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ title: z.ZodString;
7
+ domain: z.ZodOptional<z.ZodEnum<{
8
+ chinese: "chinese";
9
+ english: "english";
10
+ coding: "coding";
11
+ }>>;
12
+ max_rounds: z.ZodNumber;
13
+ weight_dim: z.ZodOptional<z.ZodString>;
14
+ }, z.core.$strip>;
15
+ type PromptFrontmatter = z.infer<typeof promptFrontmatterSchema>;
16
+ interface Prompt {
17
+ /** Stable id from frontmatter, used as the path segment under `/prompts/`. */
18
+ id: string;
19
+ /** Source filename relative to `promptsDir`. */
20
+ filename: string;
21
+ /** Validated frontmatter. */
22
+ frontmatter: PromptFrontmatter;
23
+ /** Raw markdown body (excludes frontmatter). */
24
+ body: string;
25
+ /** Body sliced by `# H1` headings — section name → section body. */
26
+ sections: Record<string, string>;
27
+ }
28
+ //#endregion
29
+ export { Prompt, PromptFrontmatter };
30
+ //# sourceMappingURL=prompts.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.d.cts","names":[],"sources":["../src/prompts.ts"],"mappings":";;;cA4BM,uBAAA,EAAuB,CAAA,CAAA,SAAA;;;;;;;;;;;KAQjB,iBAAA,GAAoB,CAAA,CAAE,KAAA,QAAa,uBAAA;AAAA,UAE9B,MAAA;;EAEf,EAAA;;EAEA,QAAA;;EAEA,WAAA,EAAa,iBAAA;;EAEb,IAAA;;EAEA,QAAA,EAAU,MAAA;AAAA"}
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/prompts.d.ts
4
+ declare const promptFrontmatterSchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ title: z.ZodString;
7
+ domain: z.ZodOptional<z.ZodEnum<{
8
+ chinese: "chinese";
9
+ english: "english";
10
+ coding: "coding";
11
+ }>>;
12
+ max_rounds: z.ZodNumber;
13
+ weight_dim: z.ZodOptional<z.ZodString>;
14
+ }, z.core.$strip>;
15
+ type PromptFrontmatter = z.infer<typeof promptFrontmatterSchema>;
16
+ interface Prompt {
17
+ /** Stable id from frontmatter, used as the path segment under `/prompts/`. */
18
+ id: string;
19
+ /** Source filename relative to `promptsDir`. */
20
+ filename: string;
21
+ /** Validated frontmatter. */
22
+ frontmatter: PromptFrontmatter;
23
+ /** Raw markdown body (excludes frontmatter). */
24
+ body: string;
25
+ /** Body sliced by `# H1` headings — section name → section body. */
26
+ sections: Record<string, string>;
27
+ }
28
+ //#endregion
29
+ export { Prompt, PromptFrontmatter };
30
+ //# sourceMappingURL=prompts.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.d.mts","names":[],"sources":["../src/prompts.ts"],"mappings":";;;cA4BM,uBAAA,EAAuB,CAAA,CAAA,SAAA;;;;;;;;;;;KAQjB,iBAAA,GAAoB,CAAA,CAAE,KAAA,QAAa,uBAAA;AAAA,UAE9B,MAAA;;EAEf,EAAA;;EAEA,QAAA;;EAEA,WAAA,EAAa,iBAAA;;EAEb,IAAA;;EAEA,QAAA,EAAU,MAAA;AAAA"}
@@ -0,0 +1,121 @@
1
+ import { getPlatform } from "@aigne/afs";
2
+ import { z } from "zod";
3
+ import matter from "gray-matter";
4
+ import { dump, load } from "js-yaml";
5
+
6
+ //#region src/prompts.ts
7
+ /**
8
+ * Prompt loader for the LLM-AFS benchmark suite.
9
+ *
10
+ * Each prompt is a Markdown file with YAML frontmatter (`gray-matter`-parsed)
11
+ * and a body sliced by H1 sections (`# Task`, `# System prompt`, etc.).
12
+ *
13
+ * Phase 0 only needs the load + parse path so that `list /bench/prompts` and
14
+ * `read /bench/prompts/<id>` return real entries. Runner / patcher consume the
15
+ * parsed shape in Phase 1.
16
+ */
17
+ const GRAY_MATTER_OPTIONS = { engines: { yaml: {
18
+ parse: (str) => load(str),
19
+ stringify: (data) => dump(data)
20
+ } } };
21
+ const promptFrontmatterSchema = z.object({
22
+ id: z.string().min(1, "frontmatter.id is required"),
23
+ title: z.string().min(1, "frontmatter.title is required"),
24
+ domain: z.enum([
25
+ "chinese",
26
+ "english",
27
+ "coding"
28
+ ]).optional(),
29
+ max_rounds: z.number().int().positive("max_rounds must be a positive integer"),
30
+ weight_dim: z.string().optional()
31
+ });
32
+ /**
33
+ * Slice markdown body into sections keyed by H1 heading text.
34
+ *
35
+ * Section headings are case-sensitive and trimmed. Content before the first H1
36
+ * is dropped — by convention every benchmark prompt starts with `# Task`.
37
+ */
38
+ function sliceBodyByH1(body) {
39
+ const out = {};
40
+ const lines = body.split(/\r?\n/);
41
+ let currentName;
42
+ let currentBuf = [];
43
+ const flush = () => {
44
+ if (currentName !== void 0) out[currentName] = currentBuf.join("\n").trim();
45
+ };
46
+ for (const line of lines) {
47
+ const match = /^#\s+(.+?)\s*$/.exec(line);
48
+ if (match && !line.startsWith("##")) {
49
+ flush();
50
+ currentName = match[1].trim();
51
+ currentBuf = [];
52
+ } else if (currentName !== void 0) currentBuf.push(line);
53
+ }
54
+ flush();
55
+ return out;
56
+ }
57
+ /**
58
+ * Parse a single prompt file. Throws `BadPromptError` (regular Error subclass)
59
+ * if frontmatter is missing required fields.
60
+ */
61
+ var BadPromptError = class extends Error {
62
+ name = "BadPromptError";
63
+ constructor(filename, message) {
64
+ super(`[${filename}] ${message}`);
65
+ this.filename = filename;
66
+ }
67
+ };
68
+ function parsePrompt(filename, raw) {
69
+ const parsed = matter(raw, GRAY_MATTER_OPTIONS);
70
+ const fm = promptFrontmatterSchema.safeParse(parsed.data);
71
+ if (!fm.success) throw new BadPromptError(filename, `invalid frontmatter — ${fm.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`);
72
+ const body = parsed.content.replace(/^\s+/, "");
73
+ return {
74
+ id: fm.data.id,
75
+ filename,
76
+ frontmatter: fm.data,
77
+ body,
78
+ sections: sliceBodyByH1(body)
79
+ };
80
+ }
81
+ /**
82
+ * Load every `*.md` file under `dir` and parse it into a Prompt. Missing
83
+ * directory → empty array (Phase 0 must not crash on unconfigured promptsDir).
84
+ *
85
+ * Files that fail frontmatter validation are skipped with their error returned
86
+ * in `errors` so callers can surface them (Phase 1 runner, conformance probe).
87
+ */
88
+ async function loadPromptsFromDir(dir) {
89
+ const platform = getPlatform();
90
+ const fs = platform.fs;
91
+ const path = platform.path;
92
+ if (!fs) return {
93
+ prompts: [],
94
+ errors: []
95
+ };
96
+ if (!await fs.exists(dir)) return {
97
+ prompts: [],
98
+ errors: []
99
+ };
100
+ const entries = await fs.readdir(dir);
101
+ const prompts = [];
102
+ const errors = [];
103
+ for (const filename of entries.slice().sort()) {
104
+ if (path.extname(filename).toLowerCase() !== ".md") continue;
105
+ const raw = await fs.readTextFile(path.join(dir, filename));
106
+ try {
107
+ prompts.push(parsePrompt(filename, raw));
108
+ } catch (err) {
109
+ if (err instanceof BadPromptError) errors.push(err);
110
+ else throw err;
111
+ }
112
+ }
113
+ return {
114
+ prompts,
115
+ errors
116
+ };
117
+ }
118
+
119
+ //#endregion
120
+ export { BadPromptError, loadPromptsFromDir };
121
+ //# sourceMappingURL=prompts.mjs.map