@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,296 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const require_errors = require('./errors.cjs');
3
+ require('./runner.cjs');
4
+ let node_fs = require("node:fs");
5
+ let _aigne_afs = require("@aigne/afs");
6
+ let zod = require("zod");
7
+ let node_crypto = require("node:crypto");
8
+ let node_path = require("node:path");
9
+ node_path = require_rolldown_runtime.__toESM(node_path);
10
+
11
+ //#region src/patcher.ts
12
+ /**
13
+ * Phase 3 — patchCatalog. Merges a results JSON into the ai-device catalog's
14
+ * `entry.afs` subtree. Maintainer tool: directly reads/writes catalog.json
15
+ * (a git-tracked dev artifact, not runtime data) — see decisions.md #24.
16
+ *
17
+ * Invariants (plan.md L459-468):
18
+ * 1. Models missing from results keep their existing `entry.afs` (deep-equal).
19
+ * 2. Only `r.scores` is copied (no other result fields leak into catalog).
20
+ * 3. The written `entry.afs` block has alphabetically sorted keys (clean diff).
21
+ * 4. Atomic write: tmp → rename, no half-written catalog on failure.
22
+ * 5. Prototype-pollution: __proto__/constructor/prototype rejected anywhere.
23
+ * 6. Path traversal: resultsPath must resolve inside resultsRoot.
24
+ * 7. Cache reload via `refreshRegistry` — failures propagate so the caller
25
+ * knows the catalog landed but routing still sees the previous snapshot.
26
+ * 8. Vault credentials sanitisation: api_key / Authorization / Bearer / etc.
27
+ * are rejected as field names anywhere in `results.<model>` payloads.
28
+ *
29
+ * Other catalog entries are written unchanged (key order preserved) so the
30
+ * git diff is limited to the `afs` block of patched models.
31
+ *
32
+ * Phase 5: weight tables are no longer inlined — the bench provider passes
33
+ * `loadPolicyWeights` (which the wiring in `bench.ts` resolves through
34
+ * `/dev/ai/score-weights/<name>`), so this module remains independent of
35
+ * ai-device internals.
36
+ */
37
+ const log = (0, _aigne_afs.makeNsLog)("provider:llm-bench");
38
+ const RESERVED_KEYS = new Set([
39
+ "__proto__",
40
+ "constructor",
41
+ "prototype",
42
+ "__defineGetter__",
43
+ "__defineSetter__",
44
+ "__lookupGetter__",
45
+ "__lookupSetter__"
46
+ ]);
47
+ const SENSITIVE_KEY_PATTERNS = [
48
+ /^api[_-]?key$/i,
49
+ /^authorization$/i,
50
+ /^bearer$/i,
51
+ /^service[_-]?account$/i,
52
+ /^private[_-]?key$/i,
53
+ /^client[_-]?secret$/i,
54
+ /^secret$/i,
55
+ /^credentials?$/i
56
+ ];
57
+ const ALLOWED_RESULT_ENTRY_FIELDS = new Set([
58
+ "n_samples",
59
+ "scores",
60
+ "domains",
61
+ "stddev",
62
+ "per_prompt",
63
+ "errors"
64
+ ]);
65
+ const MAX_RESULT_ENTRIES = 500;
66
+ const MAX_N_SAMPLES = 1e3;
67
+ const ScoresSchema = zod.z.object({
68
+ exploration: zod.z.number().min(0).max(1).optional(),
69
+ tool_reliability: zod.z.number().min(0).max(1).optional(),
70
+ self_stop: zod.z.number().min(0).max(1).optional(),
71
+ parallelism: zod.z.number().min(0).max(1).optional(),
72
+ efficiency: zod.z.number().min(0).max(1).optional(),
73
+ instruction_following: zod.z.number().min(0).max(1).optional()
74
+ }).strict();
75
+ const DomainsSchema = zod.z.object({
76
+ chinese: zod.z.number().min(0).max(1).optional(),
77
+ english: zod.z.number().min(0).max(1).optional(),
78
+ coding: zod.z.number().min(0).max(1).optional()
79
+ }).strict();
80
+ const ResultEntrySchema = zod.z.object({
81
+ n_samples: zod.z.number().int().min(1).max(MAX_N_SAMPLES),
82
+ scores: ScoresSchema,
83
+ domains: DomainsSchema.optional(),
84
+ stddev: zod.z.record(zod.z.string(), zod.z.number()).optional(),
85
+ per_prompt: zod.z.record(zod.z.string().regex(/^[A-Za-z0-9._-]+$/), ScoresSchema).optional(),
86
+ errors: zod.z.array(zod.z.unknown()).optional()
87
+ }).strict();
88
+ const JudgeIdSchema = zod.z.string().regex(/^[A-Za-z0-9._-]+$/);
89
+ const JudgeModelSchema = zod.z.union([JudgeIdSchema, zod.z.array(JudgeIdSchema).min(1)]).nullable().optional();
90
+ const ResultsRootSchema = zod.z.object({
91
+ version: zod.z.string().min(1),
92
+ judge_model: JudgeModelSchema,
93
+ config: zod.z.unknown().optional(),
94
+ results: zod.z.record(zod.z.string().regex(/^[A-Za-z0-9._-]+$/), ResultEntrySchema),
95
+ outliers: zod.z.unknown().optional()
96
+ }).strict();
97
+ /**
98
+ * Validate raw JSON shape + reject reserved/sensitive keys anywhere.
99
+ * Throws `ValidationError` on any rejection.
100
+ *
101
+ * Layers (in order):
102
+ * 1. Deep-walk: reject `__proto__` / `constructor` / `prototype` and any
103
+ * key matching a credential pattern (`api_key`, `Authorization`, …).
104
+ * 2. Resource-exhaustion guard: results entries ≤ MAX_RESULT_ENTRIES.
105
+ * 3. Per-entry whitelist: only `n_samples`/`scores`/`domains`/`stddev`.
106
+ * 4. Zod schema parse (z.strict() also catches unknown scores dimensions).
107
+ */
108
+ function validateResultsShape(raw) {
109
+ rejectDangerousKeysDeep(raw);
110
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
111
+ const r = raw.results;
112
+ if (r && typeof r === "object" && !Array.isArray(r)) {
113
+ const keys = Object.keys(r);
114
+ if (keys.length > MAX_RESULT_ENTRIES) throw new require_errors.ValidationError(`results contains ${keys.length} entries; max ${MAX_RESULT_ENTRIES}`);
115
+ for (const k of keys) {
116
+ const entry = r[k];
117
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
118
+ for (const f of Object.keys(entry)) if (!ALLOWED_RESULT_ENTRY_FIELDS.has(f)) throw new require_errors.ValidationError(`unknown field "${f}" in results.${k}`);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ const parsed = ResultsRootSchema.safeParse(raw);
124
+ if (!parsed.success) throw new require_errors.ValidationError(`results JSON is invalid: ${parsed.error.message}`);
125
+ return {
126
+ version: parsed.data.version,
127
+ judge_model: parsed.data.judge_model ?? null,
128
+ results: parsed.data.results
129
+ };
130
+ }
131
+ function rejectDangerousKeysDeep(value, p = "$") {
132
+ if (value === null || typeof value !== "object") return;
133
+ if (Array.isArray(value)) {
134
+ for (let i = 0; i < value.length; i++) rejectDangerousKeysDeep(value[i], `${p}[${i}]`);
135
+ return;
136
+ }
137
+ for (const k of Object.keys(value)) {
138
+ if (RESERVED_KEYS.has(k)) throw new require_errors.ValidationError(`reserved key "${k}" not allowed at ${p}`);
139
+ for (const re of SENSITIVE_KEY_PATTERNS) if (re.test(k)) throw new require_errors.ValidationError(`sensitive field "${k}" not allowed at ${p}`);
140
+ rejectDangerousKeysDeep(value[k], `${p}.${k}`);
141
+ }
142
+ }
143
+ /**
144
+ * Resolve `p` against cwd if relative, then ensure it lives inside `resultsRoot`.
145
+ * Defends against `..` segments and absolute paths that escape the root.
146
+ * Returns the absolute path on success; throws `ValidationError` otherwise.
147
+ *
148
+ * Note: this is purely lexical (no `fs.realpath`) — a maintainer-created
149
+ * symlink inside `resultsRoot` pointing outside is not caught here. The threat
150
+ * model is a maintainer tool, so the lexical check is intentionally cheap; if
151
+ * a symlink-based vector materialises in production, swap to `fs.realpath`.
152
+ */
153
+ function assertWithinResultsDir(p, resultsRoot) {
154
+ const absRoot = node_path.resolve(resultsRoot);
155
+ const abs = node_path.resolve(p);
156
+ const rel = node_path.relative(absRoot, abs);
157
+ if (rel === "" || rel.startsWith("..") || node_path.isAbsolute(rel)) throw new require_errors.ValidationError(`results path outside ${resultsRoot}: ${p}`);
158
+ return abs;
159
+ }
160
+ /**
161
+ * Return a deeply-key-sorted clone of an `afs` block. Plain objects get keys
162
+ * in alphabetical order; arrays preserve order; primitives pass through.
163
+ * Does not mutate input.
164
+ */
165
+ function sortAfsBlock(value) {
166
+ if (value === null || typeof value !== "object") return value;
167
+ if (Array.isArray(value)) return value.map((v) => sortAfsBlock(v));
168
+ const sorted = {};
169
+ for (const k of Object.keys(value).sort()) sorted[k] = sortAfsBlock(value[k]);
170
+ return sorted;
171
+ }
172
+ const BIOME_LINE_WIDTH = 100;
173
+ const BIOME_INDENT = " ";
174
+ function stringifyBiomeJSON(value, currentIndent = "", startColumn = 0) {
175
+ if (value === null) return "null";
176
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
177
+ if (typeof value === "string") return JSON.stringify(value);
178
+ if (Array.isArray(value)) {
179
+ if (value.length === 0) return "[]";
180
+ if (value.every((v) => v === null || typeof v !== "object")) {
181
+ const inline = `[${value.map((v) => stringifyBiomeJSON(v)).join(", ")}]`;
182
+ if (startColumn + inline.length <= BIOME_LINE_WIDTH) return inline;
183
+ }
184
+ const inner$1 = currentIndent + BIOME_INDENT;
185
+ return `[\n${value.map((v) => `${inner$1}${stringifyBiomeJSON(v, inner$1, inner$1.length)}`).join(",\n")}\n${currentIndent}]`;
186
+ }
187
+ if (typeof value !== "object") return JSON.stringify(value);
188
+ const entries = Object.entries(value);
189
+ if (entries.length === 0) return "{}";
190
+ const inner = currentIndent + BIOME_INDENT;
191
+ return `{\n${entries.map(([k, v]) => {
192
+ const keyPart = `${inner}${JSON.stringify(k)}: `;
193
+ return `${keyPart}${stringifyBiomeJSON(v, inner, keyPart.length)}`;
194
+ }).join(",\n")}\n${currentIndent}}`;
195
+ }
196
+ /** Write `obj` as biome-compatible pretty-printed JSON to `target` via tmp+rename. */
197
+ async function atomicWriteJSON(target, obj) {
198
+ const dir = node_path.dirname(target);
199
+ await node_fs.promises.mkdir(dir, { recursive: true });
200
+ const tmp = node_path.join(dir, `.tmp-${(0, node_crypto.randomBytes)(6).toString("hex")}.json`);
201
+ const payload = `${stringifyBiomeJSON(obj)}\n`;
202
+ await node_fs.promises.writeFile(tmp, payload, "utf8");
203
+ try {
204
+ await node_fs.promises.rename(tmp, target);
205
+ } catch (err) {
206
+ await node_fs.promises.rm(tmp, { force: true });
207
+ throw err;
208
+ }
209
+ }
210
+ function meanOf(xs) {
211
+ if (xs.length === 0) return 0;
212
+ let s = 0;
213
+ for (const x of xs) s += x;
214
+ return s / xs.length;
215
+ }
216
+ function getNested(obj, p) {
217
+ const parts = p.split(".");
218
+ let cur = obj;
219
+ for (const k of parts) {
220
+ if (cur === null || typeof cur !== "object") return void 0;
221
+ cur = cur[k];
222
+ }
223
+ return cur;
224
+ }
225
+ function computeWeightedScore(entry, weights) {
226
+ let total = 0;
227
+ for (const [pathKey, w] of Object.entries(weights)) {
228
+ const v = getNested(entry, pathKey);
229
+ if (typeof v === "number" && Number.isFinite(v)) total += v * w;
230
+ }
231
+ return total;
232
+ }
233
+ async function patchCatalog(input) {
234
+ const safeResultsPath = assertWithinResultsDir(input.resultsPath, input.resultsRoot);
235
+ let raw;
236
+ try {
237
+ raw = await node_fs.promises.readFile(safeResultsPath, "utf8");
238
+ } catch (err) {
239
+ throw new require_errors.ValidationError(`failed to read results: ${err instanceof Error ? err.message : String(err)}`);
240
+ }
241
+ let parsedRaw;
242
+ try {
243
+ parsedRaw = JSON.parse(raw);
244
+ } catch (err) {
245
+ throw new require_errors.ValidationError(`results is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
246
+ }
247
+ const validated = validateResultsShape(parsedRaw);
248
+ const catalogText = await node_fs.promises.readFile(input.catalogPath, "utf8");
249
+ const catalog = JSON.parse(catalogText);
250
+ if (!Array.isArray(catalog)) throw new require_errors.ValidationError("catalog.json must be an array of entries");
251
+ const summary = {
252
+ changes: [],
253
+ skipped: []
254
+ };
255
+ if (catalog.length === 0) return summary;
256
+ const weightsTable = await input.loadPolicyWeights();
257
+ if (!weightsTable || typeof weightsTable !== "object" || Array.isArray(weightsTable)) throw new require_errors.ValidationError("loadPolicyWeights must return a plain object of { policy: weights }");
258
+ for (const [policy, weights] of Object.entries(weightsTable)) if (!weights || typeof weights !== "object" || Array.isArray(weights)) throw new require_errors.ValidationError(`weights for policy "${policy}" must be a plain object`);
259
+ for (const entry of catalog) {
260
+ const r = validated.results[entry.model];
261
+ if (!r) {
262
+ summary.skipped.push(entry.model);
263
+ continue;
264
+ }
265
+ const before = entry.afs;
266
+ const policyScores = {};
267
+ for (const [name, w] of Object.entries(weightsTable)) policyScores[name] = computeWeightedScore(r, w);
268
+ entry.afs = sortAfsBlock({
269
+ status: "benchmarked",
270
+ version: validated.version,
271
+ last_seen_in_run: validated.version,
272
+ judge: validated.judge_model,
273
+ samples: r.n_samples,
274
+ scores: { ...r.scores },
275
+ overall: meanOf(Object.values(r.scores)),
276
+ policyScores,
277
+ ...r.domains ? { domains: { ...r.domains } } : {}
278
+ });
279
+ summary.changes.push({
280
+ model: entry.model,
281
+ before,
282
+ after: entry.afs
283
+ });
284
+ }
285
+ await atomicWriteJSON(input.catalogPath, catalog);
286
+ if (input.refreshRegistry) await input.refreshRegistry();
287
+ 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.`);
288
+ return summary;
289
+ }
290
+
291
+ //#endregion
292
+ exports.assertWithinResultsDir = assertWithinResultsDir;
293
+ exports.atomicWriteJSON = atomicWriteJSON;
294
+ exports.patchCatalog = patchCatalog;
295
+ exports.sortAfsBlock = sortAfsBlock;
296
+ exports.validateResultsShape = validateResultsShape;
@@ -0,0 +1,119 @@
1
+ //#region src/patcher.d.ts
2
+ /**
3
+ * Phase 3 — patchCatalog. Merges a results JSON into the ai-device catalog's
4
+ * `entry.afs` subtree. Maintainer tool: directly reads/writes catalog.json
5
+ * (a git-tracked dev artifact, not runtime data) — see decisions.md #24.
6
+ *
7
+ * Invariants (plan.md L459-468):
8
+ * 1. Models missing from results keep their existing `entry.afs` (deep-equal).
9
+ * 2. Only `r.scores` is copied (no other result fields leak into catalog).
10
+ * 3. The written `entry.afs` block has alphabetically sorted keys (clean diff).
11
+ * 4. Atomic write: tmp → rename, no half-written catalog on failure.
12
+ * 5. Prototype-pollution: __proto__/constructor/prototype rejected anywhere.
13
+ * 6. Path traversal: resultsPath must resolve inside resultsRoot.
14
+ * 7. Cache reload via `refreshRegistry` — failures propagate so the caller
15
+ * knows the catalog landed but routing still sees the previous snapshot.
16
+ * 8. Vault credentials sanitisation: api_key / Authorization / Bearer / etc.
17
+ * are rejected as field names anywhere in `results.<model>` payloads.
18
+ *
19
+ * Other catalog entries are written unchanged (key order preserved) so the
20
+ * git diff is limited to the `afs` block of patched models.
21
+ *
22
+ * Phase 5: weight tables are no longer inlined — the bench provider passes
23
+ * `loadPolicyWeights` (which the wiring in `bench.ts` resolves through
24
+ * `/dev/ai/score-weights/<name>`), so this module remains independent of
25
+ * ai-device internals.
26
+ */
27
+ interface AFSScores {
28
+ status: "benchmarked" | "untested" | "deprecated_score";
29
+ version: string;
30
+ last_seen_in_run: string;
31
+ /**
32
+ * Judge model id(s) used to score `instruction_following`. String for
33
+ * single-judge runs (back-compat), array for multi-judge ensemble runs
34
+ * (Phase 0 of Run 4 — plan.md L1052), `null` when no judging happened.
35
+ */
36
+ judge: string | string[] | null;
37
+ samples: number;
38
+ scores: Record<string, number>;
39
+ overall: number;
40
+ policyScores: Record<string, number>;
41
+ domains?: Record<string, number>;
42
+ }
43
+ interface CatalogEntry {
44
+ model: string;
45
+ afs?: AFSScores;
46
+ [k: string]: unknown;
47
+ }
48
+ interface PatchSummary {
49
+ changes: Array<{
50
+ model: string;
51
+ before: AFSScores | undefined;
52
+ after: AFSScores;
53
+ }>;
54
+ skipped: string[];
55
+ }
56
+ type PolicyWeightsTable = Record<string, Record<string, number>>;
57
+ interface PatchCatalogInput {
58
+ resultsPath: string;
59
+ catalogPath: string;
60
+ resultsRoot: string;
61
+ /**
62
+ * Resolve every policy → its weight map. Bench wires this through AFS
63
+ * (`/dev/ai/score-weights/<name>`), keeping this module free of ai-device
64
+ * imports. Returning an empty object means no `policyScores` are produced.
65
+ */
66
+ loadPolicyWeights: () => Promise<PolicyWeightsTable>;
67
+ /**
68
+ * Triggered after the catalog has been atomically written so the device's
69
+ * registry cache picks the new scores up immediately. Failures propagate —
70
+ * the catalog already landed, but the caller should know routing still
71
+ * sees the previous snapshot until the next refresh tick.
72
+ */
73
+ refreshRegistry?: () => Promise<void>;
74
+ }
75
+ interface ValidatedResults {
76
+ version: string;
77
+ judge_model: string | string[] | null;
78
+ results: Record<string, ResultEntry>;
79
+ }
80
+ interface ResultEntry {
81
+ n_samples: number;
82
+ scores: Record<string, number>;
83
+ domains?: Record<string, number>;
84
+ }
85
+ /**
86
+ * Validate raw JSON shape + reject reserved/sensitive keys anywhere.
87
+ * Throws `ValidationError` on any rejection.
88
+ *
89
+ * Layers (in order):
90
+ * 1. Deep-walk: reject `__proto__` / `constructor` / `prototype` and any
91
+ * key matching a credential pattern (`api_key`, `Authorization`, …).
92
+ * 2. Resource-exhaustion guard: results entries ≤ MAX_RESULT_ENTRIES.
93
+ * 3. Per-entry whitelist: only `n_samples`/`scores`/`domains`/`stddev`.
94
+ * 4. Zod schema parse (z.strict() also catches unknown scores dimensions).
95
+ */
96
+ declare function validateResultsShape(raw: unknown): ValidatedResults;
97
+ /**
98
+ * Resolve `p` against cwd if relative, then ensure it lives inside `resultsRoot`.
99
+ * Defends against `..` segments and absolute paths that escape the root.
100
+ * Returns the absolute path on success; throws `ValidationError` otherwise.
101
+ *
102
+ * Note: this is purely lexical (no `fs.realpath`) — a maintainer-created
103
+ * symlink inside `resultsRoot` pointing outside is not caught here. The threat
104
+ * model is a maintainer tool, so the lexical check is intentionally cheap; if
105
+ * a symlink-based vector materialises in production, swap to `fs.realpath`.
106
+ */
107
+ declare function assertWithinResultsDir(p: string, resultsRoot: string): string;
108
+ /**
109
+ * Return a deeply-key-sorted clone of an `afs` block. Plain objects get keys
110
+ * in alphabetical order; arrays preserve order; primitives pass through.
111
+ * Does not mutate input.
112
+ */
113
+ declare function sortAfsBlock<T>(value: T): T;
114
+ /** Write `obj` as biome-compatible pretty-printed JSON to `target` via tmp+rename. */
115
+ declare function atomicWriteJSON(target: string, obj: unknown): Promise<void>;
116
+ declare function patchCatalog(input: PatchCatalogInput): Promise<PatchSummary>;
117
+ //#endregion
118
+ export { AFSScores, CatalogEntry, PatchCatalogInput, PatchSummary, PolicyWeightsTable, assertWithinResultsDir, atomicWriteJSON, patchCatalog, sortAfsBlock, validateResultsShape };
119
+ //# sourceMappingURL=patcher.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patcher.d.cts","names":[],"sources":["../src/patcher.ts"],"mappings":";;AAqCA;;;;;;;;;;;;;;;;;;;;;;AAiBA;;UAjBiB,SAAA;EACf,MAAA;EACA,OAAA;EACA,gBAAA;EAgBM;;;;AAIR;EAdE,KAAA;EACA,OAAA;EACA,MAAA,EAAQ,MAAA;EACR,OAAA;EACA,YAAA,EAAc,MAAA;EACd,OAAA,GAAU,MAAA;AAAA;AAAA,UAGK,YAAA;EACf,KAAA;EACA,GAAA,GAAM,SAAA;EAAA,CACL,CAAA;AAAA;AAAA,UAGc,YAAA;EACf,OAAA,EAAS,KAAA;IAAQ,KAAA;IAAe,MAAA,EAAQ,SAAA;IAAuB,KAAA,EAAO,SAAA;EAAA;EACtE,OAAA;AAAA;AAAA,KAGU,kBAAA,GAAqB,MAAA,SAAe,MAAA;AAAA,UAE/B,iBAAA;EACf,WAAA;EACA,WAAA;EACA,WAAA;EAMiC;;;;;EAAjC,iBAAA,QAAyB,OAAA,CAAQ,kBAAA;EAPjC;;;;;;EAcA,eAAA,SAAwB,OAAA;AAAA;AAAA,UAuGT,gBAAA;EACf,OAAA;EACA,WAAA;EACA,OAAA,EAAS,MAAA,SAAe,WAAA;AAAA;AAAA,UAGhB,WAAA;EACR,SAAA;EACA,MAAA,EAAQ,MAAA;EACR,OAAA,GAAU,MAAA;AAAA;;;AALX;;;;;;;;;iBAmBe,oBAAA,CAAqB,GAAA,YAAe,gBAAA;;AAApD;;;;;AAqEA;;;;iBAAgB,sBAAA,CAAuB,CAAA,UAAW,WAAA;AAiBlD;;;;;AAAA,iBAAgB,YAAA,GAAA,CAAgB,KAAA,EAAO,CAAA,GAAI,CAAA;;iBAoDrB,eAAA,CAAgB,MAAA,UAAgB,GAAA,YAAe,OAAA;AAAA,iBA4C/C,YAAA,CAAa,KAAA,EAAO,iBAAA,GAAoB,OAAA,CAAQ,YAAA"}
@@ -0,0 +1,119 @@
1
+ //#region src/patcher.d.ts
2
+ /**
3
+ * Phase 3 — patchCatalog. Merges a results JSON into the ai-device catalog's
4
+ * `entry.afs` subtree. Maintainer tool: directly reads/writes catalog.json
5
+ * (a git-tracked dev artifact, not runtime data) — see decisions.md #24.
6
+ *
7
+ * Invariants (plan.md L459-468):
8
+ * 1. Models missing from results keep their existing `entry.afs` (deep-equal).
9
+ * 2. Only `r.scores` is copied (no other result fields leak into catalog).
10
+ * 3. The written `entry.afs` block has alphabetically sorted keys (clean diff).
11
+ * 4. Atomic write: tmp → rename, no half-written catalog on failure.
12
+ * 5. Prototype-pollution: __proto__/constructor/prototype rejected anywhere.
13
+ * 6. Path traversal: resultsPath must resolve inside resultsRoot.
14
+ * 7. Cache reload via `refreshRegistry` — failures propagate so the caller
15
+ * knows the catalog landed but routing still sees the previous snapshot.
16
+ * 8. Vault credentials sanitisation: api_key / Authorization / Bearer / etc.
17
+ * are rejected as field names anywhere in `results.<model>` payloads.
18
+ *
19
+ * Other catalog entries are written unchanged (key order preserved) so the
20
+ * git diff is limited to the `afs` block of patched models.
21
+ *
22
+ * Phase 5: weight tables are no longer inlined — the bench provider passes
23
+ * `loadPolicyWeights` (which the wiring in `bench.ts` resolves through
24
+ * `/dev/ai/score-weights/<name>`), so this module remains independent of
25
+ * ai-device internals.
26
+ */
27
+ interface AFSScores {
28
+ status: "benchmarked" | "untested" | "deprecated_score";
29
+ version: string;
30
+ last_seen_in_run: string;
31
+ /**
32
+ * Judge model id(s) used to score `instruction_following`. String for
33
+ * single-judge runs (back-compat), array for multi-judge ensemble runs
34
+ * (Phase 0 of Run 4 — plan.md L1052), `null` when no judging happened.
35
+ */
36
+ judge: string | string[] | null;
37
+ samples: number;
38
+ scores: Record<string, number>;
39
+ overall: number;
40
+ policyScores: Record<string, number>;
41
+ domains?: Record<string, number>;
42
+ }
43
+ interface CatalogEntry {
44
+ model: string;
45
+ afs?: AFSScores;
46
+ [k: string]: unknown;
47
+ }
48
+ interface PatchSummary {
49
+ changes: Array<{
50
+ model: string;
51
+ before: AFSScores | undefined;
52
+ after: AFSScores;
53
+ }>;
54
+ skipped: string[];
55
+ }
56
+ type PolicyWeightsTable = Record<string, Record<string, number>>;
57
+ interface PatchCatalogInput {
58
+ resultsPath: string;
59
+ catalogPath: string;
60
+ resultsRoot: string;
61
+ /**
62
+ * Resolve every policy → its weight map. Bench wires this through AFS
63
+ * (`/dev/ai/score-weights/<name>`), keeping this module free of ai-device
64
+ * imports. Returning an empty object means no `policyScores` are produced.
65
+ */
66
+ loadPolicyWeights: () => Promise<PolicyWeightsTable>;
67
+ /**
68
+ * Triggered after the catalog has been atomically written so the device's
69
+ * registry cache picks the new scores up immediately. Failures propagate —
70
+ * the catalog already landed, but the caller should know routing still
71
+ * sees the previous snapshot until the next refresh tick.
72
+ */
73
+ refreshRegistry?: () => Promise<void>;
74
+ }
75
+ interface ValidatedResults {
76
+ version: string;
77
+ judge_model: string | string[] | null;
78
+ results: Record<string, ResultEntry>;
79
+ }
80
+ interface ResultEntry {
81
+ n_samples: number;
82
+ scores: Record<string, number>;
83
+ domains?: Record<string, number>;
84
+ }
85
+ /**
86
+ * Validate raw JSON shape + reject reserved/sensitive keys anywhere.
87
+ * Throws `ValidationError` on any rejection.
88
+ *
89
+ * Layers (in order):
90
+ * 1. Deep-walk: reject `__proto__` / `constructor` / `prototype` and any
91
+ * key matching a credential pattern (`api_key`, `Authorization`, …).
92
+ * 2. Resource-exhaustion guard: results entries ≤ MAX_RESULT_ENTRIES.
93
+ * 3. Per-entry whitelist: only `n_samples`/`scores`/`domains`/`stddev`.
94
+ * 4. Zod schema parse (z.strict() also catches unknown scores dimensions).
95
+ */
96
+ declare function validateResultsShape(raw: unknown): ValidatedResults;
97
+ /**
98
+ * Resolve `p` against cwd if relative, then ensure it lives inside `resultsRoot`.
99
+ * Defends against `..` segments and absolute paths that escape the root.
100
+ * Returns the absolute path on success; throws `ValidationError` otherwise.
101
+ *
102
+ * Note: this is purely lexical (no `fs.realpath`) — a maintainer-created
103
+ * symlink inside `resultsRoot` pointing outside is not caught here. The threat
104
+ * model is a maintainer tool, so the lexical check is intentionally cheap; if
105
+ * a symlink-based vector materialises in production, swap to `fs.realpath`.
106
+ */
107
+ declare function assertWithinResultsDir(p: string, resultsRoot: string): string;
108
+ /**
109
+ * Return a deeply-key-sorted clone of an `afs` block. Plain objects get keys
110
+ * in alphabetical order; arrays preserve order; primitives pass through.
111
+ * Does not mutate input.
112
+ */
113
+ declare function sortAfsBlock<T>(value: T): T;
114
+ /** Write `obj` as biome-compatible pretty-printed JSON to `target` via tmp+rename. */
115
+ declare function atomicWriteJSON(target: string, obj: unknown): Promise<void>;
116
+ declare function patchCatalog(input: PatchCatalogInput): Promise<PatchSummary>;
117
+ //#endregion
118
+ export { AFSScores, CatalogEntry, PatchCatalogInput, PatchSummary, PolicyWeightsTable, assertWithinResultsDir, atomicWriteJSON, patchCatalog, sortAfsBlock, validateResultsShape };
119
+ //# sourceMappingURL=patcher.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patcher.d.mts","names":[],"sources":["../src/patcher.ts"],"mappings":";;AAqCA;;;;;;;;;;;;;;;;;;;;;;AAiBA;;UAjBiB,SAAA;EACf,MAAA;EACA,OAAA;EACA,gBAAA;EAgBM;;;;AAIR;EAdE,KAAA;EACA,OAAA;EACA,MAAA,EAAQ,MAAA;EACR,OAAA;EACA,YAAA,EAAc,MAAA;EACd,OAAA,GAAU,MAAA;AAAA;AAAA,UAGK,YAAA;EACf,KAAA;EACA,GAAA,GAAM,SAAA;EAAA,CACL,CAAA;AAAA;AAAA,UAGc,YAAA;EACf,OAAA,EAAS,KAAA;IAAQ,KAAA;IAAe,MAAA,EAAQ,SAAA;IAAuB,KAAA,EAAO,SAAA;EAAA;EACtE,OAAA;AAAA;AAAA,KAGU,kBAAA,GAAqB,MAAA,SAAe,MAAA;AAAA,UAE/B,iBAAA;EACf,WAAA;EACA,WAAA;EACA,WAAA;EAMiC;;;;;EAAjC,iBAAA,QAAyB,OAAA,CAAQ,kBAAA;EAPjC;;;;;;EAcA,eAAA,SAAwB,OAAA;AAAA;AAAA,UAuGT,gBAAA;EACf,OAAA;EACA,WAAA;EACA,OAAA,EAAS,MAAA,SAAe,WAAA;AAAA;AAAA,UAGhB,WAAA;EACR,SAAA;EACA,MAAA,EAAQ,MAAA;EACR,OAAA,GAAU,MAAA;AAAA;;;AALX;;;;;;;;;iBAmBe,oBAAA,CAAqB,GAAA,YAAe,gBAAA;;AAApD;;;;;AAqEA;;;;iBAAgB,sBAAA,CAAuB,CAAA,UAAW,WAAA;AAiBlD;;;;;AAAA,iBAAgB,YAAA,GAAA,CAAgB,KAAA,EAAO,CAAA,GAAI,CAAA;;iBAoDrB,eAAA,CAAgB,MAAA,UAAgB,GAAA,YAAe,OAAA;AAAA,iBA4C/C,YAAA,CAAa,KAAA,EAAO,iBAAA,GAAoB,OAAA,CAAQ,YAAA"}