@aigne/afs-llm-bench 1.12.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +26 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/bench.cjs +944 -0
- package/dist/bench.d.cts +89 -0
- package/dist/bench.d.cts.map +1 -0
- package/dist/bench.d.mts +89 -0
- package/dist/bench.d.mts.map +1 -0
- package/dist/bench.mjs +944 -0
- package/dist/bench.mjs.map +1 -0
- package/dist/errors.cjs +14 -0
- package/dist/errors.d.cts +13 -0
- package/dist/errors.d.cts.map +1 -0
- package/dist/errors.d.mts +13 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +14 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.cjs +38 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +10 -0
- package/dist/judge.cjs +189 -0
- package/dist/judge.d.cts +67 -0
- package/dist/judge.d.cts.map +1 -0
- package/dist/judge.d.mts +67 -0
- package/dist/judge.d.mts.map +1 -0
- package/dist/judge.mjs +185 -0
- package/dist/judge.mjs.map +1 -0
- package/dist/outlier.cjs +73 -0
- package/dist/outlier.d.cts +30 -0
- package/dist/outlier.d.cts.map +1 -0
- package/dist/outlier.d.mts +30 -0
- package/dist/outlier.d.mts.map +1 -0
- package/dist/outlier.mjs +73 -0
- package/dist/outlier.mjs.map +1 -0
- package/dist/patcher.cjs +296 -0
- package/dist/patcher.d.cts +119 -0
- package/dist/patcher.d.cts.map +1 -0
- package/dist/patcher.d.mts +119 -0
- package/dist/patcher.d.mts.map +1 -0
- package/dist/patcher.mjs +291 -0
- package/dist/patcher.mjs.map +1 -0
- package/dist/prompts.cjs +123 -0
- package/dist/prompts.d.cts +30 -0
- package/dist/prompts.d.cts.map +1 -0
- package/dist/prompts.d.mts +30 -0
- package/dist/prompts.d.mts.map +1 -0
- package/dist/prompts.mjs +121 -0
- package/dist/prompts.mjs.map +1 -0
- package/dist/reporter.cjs +322 -0
- package/dist/reporter.d.cts +27 -0
- package/dist/reporter.d.cts.map +1 -0
- package/dist/reporter.d.mts +27 -0
- package/dist/reporter.d.mts.map +1 -0
- package/dist/reporter.mjs +322 -0
- package/dist/reporter.mjs.map +1 -0
- package/dist/runner.cjs +710 -0
- package/dist/runner.d.cts +345 -0
- package/dist/runner.d.cts.map +1 -0
- package/dist/runner.d.mts +345 -0
- package/dist/runner.d.mts.map +1 -0
- package/dist/runner.mjs +697 -0
- package/dist/runner.mjs.map +1 -0
- package/dist/scoring.cjs +47 -0
- package/dist/scoring.d.cts +28 -0
- package/dist/scoring.d.cts.map +1 -0
- package/dist/scoring.d.mts +28 -0
- package/dist/scoring.d.mts.map +1 -0
- package/dist/scoring.mjs +46 -0
- package/dist/scoring.mjs.map +1 -0
- package/manifest.json +39 -0
- package/package.json +62 -0
package/dist/patcher.cjs
ADDED
|
@@ -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"}
|