@ljwei-stak/dsh-model-router 0.21.0 → 0.22.0

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.
@@ -0,0 +1,720 @@
1
+ // .dsh-plugin/shared/routing-evidence.mjs
2
+ // Pure, dependency-free routing evidence utilities.
3
+ // User-controlled strings are used as Map keys or JSON-serialized values, never
4
+ // as dynamic property names. Nothing reads the filesystem, network, environment
5
+ // or evaluates source. Hashed features minimize plaintext exposure; they do not
6
+ // certify anonymity. Empirical outputs require independent held-out evaluation.
7
+
8
+ const SCHEMA_VERSION = 1;
9
+ const FEATURE_DIMS = 4096;
10
+ const TOKEN_CAP = 512;
11
+ const MAX_TEXT_CHARS = 20000;
12
+ const MAX_STRING = 200;
13
+ const MAX_EFFORT = 64;
14
+ const MAX_RESOURCE_GROUPS = 10000;
15
+ const MAX_QUALITY_SAMPLES = 10000;
16
+ const MAX_INPUT_TOKENS = 5e6;
17
+ const MAX_OUTPUT_TOKENS = 2e6;
18
+ const MAX_INPUT_CHARS = 5e6;
19
+ const MAX_DURATION_MS = 86400000;
20
+ const SIM_THRESHOLD = 0.2;
21
+ const MAX_NEIGHBORS = 32;
22
+ const SHRINK_PRIOR = 2;
23
+ const NORM_TOLERANCE = 0.05;
24
+ const DEFAULT_MAX_AGE_MS = 15552000000;
25
+ const SUBJECTIVE_SOURCES = [
26
+ 'rating', 'ratings', 'self-eval', 'self-evaluation', 'self-eval-score',
27
+ 'model-judge', 'model-judgement', 'model-preference', 'manual-opinion',
28
+ 'human-opinion', 'opinion', 'subjective', 'thumbs', 'preference'
29
+ ];
30
+ const ALLOWED_SAMPLE_KEYS = new Set([
31
+ 'id', 'groupId', 'provider', 'model', 'reasoningEffort', 'channel', 'type',
32
+ 'quality', 'timestamp', 'synthetic', 'split', 'provenance', 'features'
33
+ ]);
34
+
35
+ function hasOwn(o, k) { return Object.prototype.hasOwnProperty.call(o, k); }
36
+ function isObj(o) { return o !== null && typeof o === 'object' && !Array.isArray(o); }
37
+ function boundedString(s, max = MAX_STRING) { return typeof s === 'string' && s.length <= max && !/[\u0000-\u001f\u007f]/.test(s); }
38
+ function nonEmpty(s) { return boundedString(s) && s.trim().length > 0; }
39
+ function effortOk(s) { return boundedString(s, MAX_EFFORT); }
40
+ function finite(n) { return typeof n === 'number' && Number.isFinite(n); }
41
+ function cmp(a, b) { return a - b; }
42
+ function compareText(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
43
+
44
+ function normalizeChannel(c) {
45
+ if (!nonEmpty(c)) return null;
46
+ return c === 'api' ? 'harness-llm' : c;
47
+ }
48
+ function routeKey(p, m, e, c) { return JSON.stringify([p, m, e, c]); }
49
+ function actionKey(rk, type, groupId) { return JSON.stringify([rk, type, groupId]); }
50
+ // Content identity for cache invalidation only, never an authenticity signature.
51
+ function contentRevision(value) {
52
+ const text = JSON.stringify(value);
53
+ let hash = 2166136261 >>> 0;
54
+ for (let i = 0; i < text.length; i++) hash = Math.imul(hash ^ text.charCodeAt(i), 16777619) >>> 0;
55
+ return `evidence-v1-${hash.toString(16).padStart(8, '0')}`;
56
+ }
57
+
58
+ function isWordCode(c) {
59
+ return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95;
60
+ }
61
+ function isCjkCode(c) {
62
+ return (c >= 0x3400 && c <= 0x4dbf) || (c >= 0x4e00 && c <= 0x9fff) || (c >= 0xf900 && c <= 0xfaff);
63
+ }
64
+ function hashIndex(tok) {
65
+ let h = 2166136261 >>> 0;
66
+ for (let i = 0; i < tok.length; i++) {
67
+ h ^= tok.charCodeAt(i);
68
+ h = Math.imul(h, 16777619) >>> 0;
69
+ }
70
+ return h % FEATURE_DIMS;
71
+ }
72
+
73
+ // Sparse L2-normalized hashed lexical features: ASCII words + CJK bigrams.
74
+ function buildFeatures(text) {
75
+ const s = text.length > MAX_TEXT_CHARS ? text.slice(0, MAX_TEXT_CHARS) : text;
76
+ const idxCounts = new Map();
77
+ const seen = new Set();
78
+ const addToken = (tok) => {
79
+ if (!seen.has(tok)) {
80
+ if (seen.size >= TOKEN_CAP) return;
81
+ seen.add(tok);
82
+ }
83
+ const h = hashIndex(tok);
84
+ idxCounts.set(h, (idxCounts.get(h) || 0) + 1);
85
+ };
86
+ let word = '';
87
+ let cjk = '';
88
+ const flushWord = () => { if (word) { addToken(word.toLowerCase()); word = ''; } };
89
+ const flushCjk = () => {
90
+ if (cjk) {
91
+ if (cjk.length === 1) addToken(cjk);
92
+ else for (let i = 0; i + 1 < cjk.length; i++) addToken(cjk.slice(i, i + 2));
93
+ cjk = '';
94
+ }
95
+ };
96
+ for (let i = 0; i < s.length; i++) {
97
+ const c = s.charCodeAt(i);
98
+ if (isWordCode(c)) { flushCjk(); word += s[i]; }
99
+ else if (isCjkCode(c)) { flushWord(); cjk += s[i]; }
100
+ else { flushWord(); flushCjk(); }
101
+ if (idxCounts.size >= FEATURE_DIMS) break;
102
+ }
103
+ flushWord();
104
+ flushCjk();
105
+ let sum = 0;
106
+ for (const c of idxCounts.values()) sum += c * c;
107
+ if (sum <= 0) return [];
108
+ const norm = Math.sqrt(sum);
109
+ const feats = [];
110
+ for (const [idx, c] of idxCounts) feats.push([idx, c / norm]);
111
+ feats.sort((a, b) => a[0] - b[0]);
112
+ return feats;
113
+ }
114
+
115
+ function cosine(a, b) {
116
+ let na = 0;
117
+ let nb = 0;
118
+ for (let i = 0; i < a.length; i++) na += a[i][1] * a[i][1];
119
+ for (let j = 0; j < b.length; j++) nb += b[j][1] * b[j][1];
120
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
121
+ if (denom === 0) return 0;
122
+ let dot = 0;
123
+ let x = 0;
124
+ let y = 0;
125
+ while (x < a.length && y < b.length) {
126
+ const ai = a[x][0];
127
+ const bi = b[y][0];
128
+ if (ai === bi) { dot += a[x][1] * b[y][1]; x++; y++; }
129
+ else if (ai < bi) x++;
130
+ else y++;
131
+ }
132
+ return dot / denom;
133
+ }
134
+
135
+ function quantileSorted(arr, q) {
136
+ const n = arr.length;
137
+ if (n === 0) return null;
138
+ if (n === 1) return arr[0];
139
+ const pos = (n - 1) * q;
140
+ const lo = Math.floor(pos);
141
+ const hi = Math.ceil(pos);
142
+ const f = pos - lo;
143
+ return lo === hi ? arr[lo] : arr[lo] + (arr[hi] - arr[lo]) * f;
144
+ }
145
+
146
+ function isSubjectiveSource(source) {
147
+ const s = source.toLowerCase().replace(/[\s_]+/g, '-');
148
+ return SUBJECTIVE_SOURCES.some((k) => s.includes(k));
149
+ }
150
+
151
+ function checkWindow(ts, now, maxAgeMs) {
152
+ return finite(ts) && ts >= 0 && ts <= now && ts >= now - maxAgeMs;
153
+ }
154
+
155
+ function checkEvidenceConfig(now, minSamples, allowSynthetic, maxAgeMs, label) {
156
+ if (!finite(now) || now < 0 || now > 8.64e15) throw new Error(`routing-evidence: ${label} now must be a valid finite timestamp`);
157
+ if (!Number.isInteger(minSamples) || minSamples < 1 || minSamples > 1000) {
158
+ throw new Error(`routing-evidence: ${label} minSamples must be an integer in 1..1000`);
159
+ }
160
+ if (typeof allowSynthetic !== 'boolean') {
161
+ throw new Error(`routing-evidence: ${label} allowSynthetic must be a boolean`);
162
+ }
163
+ if (!finite(maxAgeMs) || maxAgeMs <= 0) {
164
+ throw new Error(`routing-evidence: ${label} maxAgeMs must be a positive finite number`);
165
+ }
166
+ }
167
+
168
+ function validateFeatures(features) {
169
+ if (!Array.isArray(features) || features.length === 0 || features.length > FEATURE_DIMS) return false;
170
+ let sum = 0;
171
+ let prev = -1;
172
+ for (const pair of features) {
173
+ if (!Array.isArray(pair) || pair.length !== 2) return false;
174
+ const idx = pair[0];
175
+ const w = pair[1];
176
+ if (!Number.isInteger(idx) || idx < 0 || idx >= FEATURE_DIMS) return false;
177
+ if (typeof w !== 'number' || !Number.isFinite(w) || w <= 0) return false;
178
+ if (idx <= prev) return false;
179
+ prev = idx;
180
+ sum += w * w;
181
+ }
182
+ const norm = Math.sqrt(sum);
183
+ if (!(norm > 0)) return false;
184
+ return Math.abs(norm - 1) <= NORM_TOLERANCE;
185
+ }
186
+
187
+ function validateSample(s, now, maxAgeMs, allowSynthetic) {
188
+ if (!isObj(s)) return false;
189
+ for (const k of Object.keys(s)) if (!ALLOWED_SAMPLE_KEYS.has(k)) return false;
190
+ if (!nonEmpty(s.id) || !nonEmpty(s.groupId)) return false;
191
+ if (!nonEmpty(s.provider) || !nonEmpty(s.model)) return false;
192
+ if (!effortOk(s.reasoningEffort)) return false;
193
+ if (!nonEmpty(s.channel) || normalizeChannel(s.channel) !== s.channel) return false;
194
+ if (!nonEmpty(s.type)) return false;
195
+ if (typeof s.quality !== 'number' || !Number.isFinite(s.quality) || s.quality < 0 || s.quality > 1) return false;
196
+ if (!checkWindow(s.timestamp, now, maxAgeMs)) return false;
197
+ if (s.split !== 'train') return false;
198
+ if (s.synthetic === true && !allowSynthetic) return false;
199
+ if (s.synthetic !== true && s.synthetic !== false) return false;
200
+ const p = s.provenance;
201
+ if (!isObj(p) || p.kind !== 'objective') return false;
202
+ if (!nonEmpty(p.source) || !nonEmpty(p.reference)) return false;
203
+ if (isSubjectiveSource(p.source)) return false;
204
+ return validateFeatures(s.features);
205
+ }
206
+
207
+ function validateGroup(g) {
208
+ if (!isObj(g)) return false;
209
+ const id = g.identity;
210
+ if (!isObj(id)) return false;
211
+ if (!nonEmpty(id.provider) || !nonEmpty(id.model)) return false;
212
+ if (!effortOk(id.reasoningEffort)) return false;
213
+ if (!nonEmpty(id.channel) || normalizeChannel(id.channel) !== id.channel) return false;
214
+ if (!nonEmpty(id.type)) return false;
215
+ if (!boundedString(id.purpose)) return false;
216
+ if (!Number.isInteger(g.sampleCount) || g.sampleCount < 1 || g.sampleCount > MAX_QUALITY_SAMPLES) return false;
217
+ for (const k of ['inputTokenRatio', 'outputTokens']) {
218
+ if (typeof g[k] !== 'number' || !Number.isFinite(g[k]) || g[k] < 0) return false;
219
+ }
220
+ for (const k of ['durationMs', 'promptCharRatio']) {
221
+ const v = g[k];
222
+ if (v !== null && (typeof v !== 'number' || !Number.isFinite(v) || v < 0)) return false;
223
+ }
224
+ const p90 = g.p90;
225
+ if (!isObj(p90)) return false;
226
+ for (const k of ['inputTokenRatio', 'outputTokens']) {
227
+ if (typeof p90[k] !== 'number' || !Number.isFinite(p90[k]) || p90[k] < 0) return false;
228
+ }
229
+ for (const k of ['durationMs', 'promptCharRatio']) {
230
+ const v = p90[k];
231
+ if (v !== null && (typeof v !== 'number' || !Number.isFinite(v) || v < 0)) return false;
232
+ }
233
+ for (const k of ['inputTokenRatio', 'outputTokens', 'durationMs', 'promptCharRatio']) {
234
+ if ((g[k] === null) !== (p90[k] === null)) return false;
235
+ if (g[k] !== null && p90[k] < g[k]) return false;
236
+ }
237
+ if (g.inputTokenRatio > MAX_INPUT_TOKENS || p90.inputTokenRatio > MAX_INPUT_TOKENS ||
238
+ g.outputTokens > MAX_OUTPUT_TOKENS || p90.outputTokens > MAX_OUTPUT_TOKENS ||
239
+ (g.durationMs !== null && p90.durationMs > MAX_DURATION_MS) ||
240
+ (g.promptCharRatio !== null && p90.promptCharRatio > MAX_INPUT_CHARS)) return false;
241
+ return true;
242
+ }
243
+
244
+ // Structural validation only. Identity and provenance fields are treated as
245
+ // caller assertions, NOT authenticated facts. This never verifies that a
246
+ // sample's provenance is truthful; it only verifies shape, bounds, and
247
+ // that subjective provenance kinds are excluded.
248
+ function validateArtifactObject(artifact, opts = {}) {
249
+ const now = hasOwn(opts, 'now') ? opts.now : Date.now();
250
+ const allowSynthetic = hasOwn(opts, 'allowSynthetic') ? opts.allowSynthetic : false;
251
+ const maxAgeMs = hasOwn(opts, 'maxAgeMs') ? opts.maxAgeMs : DEFAULT_MAX_AGE_MS;
252
+ checkEvidenceConfig(now, 8, allowSynthetic, maxAgeMs, 'artifact');
253
+ if (!isObj(artifact)) throw new Error('routing-evidence: artifact must be an object');
254
+ if (artifact.schemaVersion !== SCHEMA_VERSION) throw new Error('routing-evidence: unsupported schemaVersion');
255
+ if (!nonEmpty(artifact.revision)) throw new Error('routing-evidence: revision must be a non-empty string');
256
+ if (!finite(artifact.generatedAt) || artifact.generatedAt < 0 || artifact.generatedAt > now) {
257
+ throw new Error('routing-evidence: generatedAt must be finite and not in the future');
258
+ }
259
+ if (!Number.isInteger(artifact.minSamples) || artifact.minSamples < 1 || artifact.minSamples > 1000) {
260
+ throw new Error('routing-evidence: minSamples must be an integer in 1..1000');
261
+ }
262
+ if (!Array.isArray(artifact.resourceGroups) || artifact.resourceGroups.length > MAX_RESOURCE_GROUPS) {
263
+ throw new Error('routing-evidence: resourceGroups must be an array within bounds');
264
+ }
265
+ const seenGroups = new Set();
266
+ for (const g of artifact.resourceGroups) {
267
+ if (!validateGroup(g)) throw new Error('routing-evidence: malformed resourceGroup');
268
+ const id = g.identity;
269
+ const key = JSON.stringify([id.provider, id.model, id.reasoningEffort, id.channel, id.type, id.purpose]);
270
+ if (seenGroups.has(key)) throw new Error('routing-evidence: duplicate resourceGroup');
271
+ seenGroups.add(key);
272
+ }
273
+ if (!Array.isArray(artifact.qualitySamples) || artifact.qualitySamples.length > MAX_QUALITY_SAMPLES) {
274
+ throw new Error('routing-evidence: qualitySamples must be an array within bounds');
275
+ }
276
+ const seenIds = new Set();
277
+ const seenActions = new Set();
278
+ for (const s of artifact.qualitySamples) {
279
+ if (!validateSample(s, now, maxAgeMs, allowSynthetic)) {
280
+ throw new Error('routing-evidence: malformed qualitySample');
281
+ }
282
+ if (seenIds.has(s.id)) throw new Error('routing-evidence: duplicate qualitySample id');
283
+ seenIds.add(s.id);
284
+ const ak = actionKey(routeKey(s.provider, s.model, s.reasoningEffort, s.channel), s.type, s.groupId);
285
+ if (seenActions.has(ak)) throw new Error('routing-evidence: duplicate qualitySample action+groupId');
286
+ seenActions.add(ak);
287
+ }
288
+ const stats = artifact.stats;
289
+ if (!isObj(stats)) throw new Error('routing-evidence: stats must be an object');
290
+ for (const k of ['accepted', 'rejected']) {
291
+ if (!Number.isInteger(stats[k]) || stats[k] < 0) {
292
+ throw new Error(`routing-evidence: stats.${k} must be a non-negative integer`);
293
+ }
294
+ }
295
+ // Return a safe projection, rather than retaining unchecked nested metadata.
296
+ return {
297
+ schemaVersion: SCHEMA_VERSION, revision: artifact.revision,
298
+ generatedAt: artifact.generatedAt, minSamples: artifact.minSamples,
299
+ resourceGroups: artifact.resourceGroups.map(g => ({
300
+ identity: { provider: g.identity.provider, model: g.identity.model,
301
+ reasoningEffort: g.identity.reasoningEffort, channel: g.identity.channel,
302
+ type: g.identity.type, purpose: g.identity.purpose },
303
+ sampleCount: g.sampleCount, inputTokenRatio: g.inputTokenRatio,
304
+ outputTokens: g.outputTokens, durationMs: g.durationMs, promptCharRatio: g.promptCharRatio,
305
+ p90: { inputTokenRatio: g.p90.inputTokenRatio, outputTokens: g.p90.outputTokens,
306
+ durationMs: g.p90.durationMs, promptCharRatio: g.p90.promptCharRatio }
307
+ })),
308
+ qualitySamples: artifact.qualitySamples.map(s => ({
309
+ id: s.id, groupId: s.groupId, provider: s.provider, model: s.model,
310
+ reasoningEffort: s.reasoningEffort, channel: s.channel, type: s.type,
311
+ quality: s.quality, timestamp: s.timestamp, synthetic: s.synthetic, split: 'train',
312
+ provenance: { kind: 'objective', source: s.provenance.source, reference: s.provenance.reference },
313
+ features: s.features.map(pair => [...pair])
314
+ })),
315
+ stats: { accepted: stats.accepted, rejected: stats.rejected }
316
+ };
317
+ }
318
+
319
+ function makeStats() {
320
+ return {
321
+ accepted: 0, rejected: 0,
322
+ resourceAccepted: 0, qualityAccepted: 0,
323
+ resourceRejected: 0, qualityRejected: 0,
324
+ reasons: {}
325
+ };
326
+ }
327
+
328
+ function makeRejecter(stats, scope) {
329
+ return (reason) => {
330
+ stats.rejected++;
331
+ if (scope === 'resource') stats.resourceRejected++;
332
+ else stats.qualityRejected++;
333
+ stats.reasons[reason] = (stats.reasons[reason] || 0) + 1;
334
+ };
335
+ }
336
+
337
+ function resolveEffort(pkg) {
338
+ if (pkg.reasoningEffortStatus === 'requested' &&
339
+ typeof pkg.requestedReasoningEffort === 'string' &&
340
+ pkg.requestedReasoningEffort !== '' &&
341
+ effortOk(pkg.requestedReasoningEffort)) {
342
+ return pkg.requestedReasoningEffort;
343
+ }
344
+ return '';
345
+ }
346
+
347
+ function actualModelOk(pkg, provider, model, channel) {
348
+ const isApi = channel === 'harness-llm';
349
+ const accept = new Set([model, `${provider}/${model}`]);
350
+ if (!isApi && typeof pkg.requestedCliModel === 'string' && pkg.requestedCliModel !== '') {
351
+ accept.add(pkg.requestedCliModel);
352
+ }
353
+ const am = pkg.actualModel;
354
+ if (am === undefined || am === null) return isApi;
355
+ if (typeof am !== 'string' || am === '' || !accept.has(am)) return false;
356
+ return true;
357
+ }
358
+
359
+ function ingestRuns(runs, evidenceContext) {
360
+ const reject = makeRejecter(evidenceContext.stats, 'resource');
361
+ const now = evidenceContext.now;
362
+ const maxAgeMs = evidenceContext.maxAgeMs;
363
+ const seenPackages = new Set();
364
+ let totalPackages = 0;
365
+ if (!Array.isArray(runs)) return;
366
+ for (const run of runs) {
367
+ if (!isObj(run)) { reject('runNotObject'); continue; }
368
+ if (!nonEmpty(run.id)) { reject('runBadId'); continue; }
369
+ if (typeof run.task !== 'string') { reject('runBadTask'); continue; }
370
+ if (!finite(run.createdAt) || !finite(run.finishedAt) || run.createdAt < 0 ||
371
+ run.createdAt > run.finishedAt) { reject('runBadTimestamps'); continue; }
372
+ if (!checkWindow(run.finishedAt, now, maxAgeMs)) { reject('runOutOfWindow'); continue; }
373
+ if (!Array.isArray(run.packages)) { reject('runPackagesNotArray'); continue; }
374
+ for (const pkg of run.packages) {
375
+ if (totalPackages >= MAX_QUALITY_SAMPLES) { reject('tooManyResources'); return; }
376
+ if (!isObj(pkg)) { reject('packageNotObject'); continue; }
377
+ if (pkg.ran !== true || pkg.ok !== true) { reject('packageNotSuccessful'); continue; }
378
+ if (pkg.usageIncomplete === true) { reject('packageIncompleteUsage'); continue; }
379
+ if (Array.isArray(pkg.qualityAttempts) && pkg.qualityAttempts.length > 1) { reject('packageMultipleQualityAttempts'); continue; }
380
+ if (!nonEmpty(pkg.id)) { reject('packageBadId'); continue; }
381
+ const packageKey = JSON.stringify([run.id, pkg.id, pkg.finishedAt]);
382
+ if (seenPackages.has(packageKey)) { reject('duplicateResource'); continue; }
383
+ const channel = normalizeChannel(pkg.channel);
384
+ if (!channel) { reject('packageBadChannel'); continue; }
385
+ const provider = nonEmpty(pkg.provider) ? pkg.provider : null;
386
+ const model = nonEmpty(pkg.model) ? pkg.model : null;
387
+ if (!provider || !model) { reject('packageBadIdentity'); continue; }
388
+ if (!actualModelOk(pkg, provider, model, channel)) { reject('packageBadActualModel'); continue; }
389
+ const effort = resolveEffort(pkg);
390
+ const type = nonEmpty(pkg.type) ? pkg.type : 'general';
391
+ const purpose = nonEmpty(pkg.purpose) ? pkg.purpose : '';
392
+ const usage = pkg.usage;
393
+ if (!isObj(usage)) { reject('packageBadUsage'); continue; }
394
+ const inputTokens = usage.inputTokens;
395
+ const outputTokens = usage.outputTokens;
396
+ if (!finite(inputTokens) || inputTokens < 0 || inputTokens > MAX_INPUT_TOKENS ||
397
+ !finite(outputTokens) || outputTokens < 0 || outputTokens > MAX_OUTPUT_TOKENS) {
398
+ reject('packageBadTokens'); continue;
399
+ }
400
+ let invalidCache = false;
401
+ let promptTokens = inputTokens;
402
+ for (const ck of ['cacheReadTokens', 'cacheWriteTokens']) {
403
+ if (hasOwn(usage, ck) && usage[ck] !== undefined) {
404
+ const v = usage[ck];
405
+ if (!finite(v) || v < 0 || v > MAX_INPUT_TOKENS) { invalidCache = true; break; }
406
+ promptTokens += v;
407
+ }
408
+ }
409
+ if (invalidCache || !finite(promptTokens) || promptTokens > MAX_INPUT_TOKENS) { reject('packageBadCacheCounters'); continue; }
410
+ if (!Number.isInteger(pkg.inputChars) || pkg.inputChars <= 0 || pkg.inputChars > MAX_INPUT_CHARS) {
411
+ reject('packageBadInputChars'); continue;
412
+ }
413
+ let durationMs = null;
414
+ if (hasOwn(pkg, 'durationMs') && pkg.durationMs !== undefined && pkg.durationMs !== null) {
415
+ if (!finite(pkg.durationMs) || pkg.durationMs < 0 || pkg.durationMs > MAX_DURATION_MS) {
416
+ reject('packageBadDuration'); continue;
417
+ }
418
+ durationMs = pkg.durationMs;
419
+ }
420
+ if (!checkWindow(pkg.finishedAt, now, maxAgeMs) || pkg.finishedAt < run.createdAt ||
421
+ pkg.finishedAt > run.finishedAt) { reject('packageOutOfWindow'); continue; }
422
+ const rk = routeKey(provider, model, effort, channel);
423
+ const gk = JSON.stringify([rk, type, purpose]);
424
+ let group = evidenceContext.resourceMap.get(gk);
425
+ if (!group) {
426
+ if (evidenceContext.resourceMap.size >= MAX_RESOURCE_GROUPS) { reject('tooManyGroups'); continue; }
427
+ group = {
428
+ identity: { provider, model, reasoningEffort: effort, channel, type, purpose },
429
+ samples: { ir: [], ot: [], dm: [], pr: [] }
430
+ };
431
+ evidenceContext.resourceMap.set(gk, group);
432
+ }
433
+ // Counters represent disjoint uncached/read/write input categories. Learn
434
+ // total prompt size; future cache hits and unreported retries are unknown.
435
+ group.samples.ir.push(promptTokens / pkg.inputChars);
436
+ group.samples.ot.push(outputTokens);
437
+ if (durationMs !== null) group.samples.dm.push(durationMs);
438
+ if (run.task.length > 0) group.samples.pr.push(pkg.inputChars / run.task.length);
439
+ evidenceContext.stats.resourceAccepted++;
440
+ evidenceContext.stats.accepted++;
441
+ totalPackages++;
442
+ seenPackages.add(packageKey);
443
+ }
444
+ }
445
+ }
446
+
447
+ function ingestObservations(observations, evidenceContext) {
448
+ const reject = makeRejecter(evidenceContext.stats, 'quality');
449
+ if (!Array.isArray(observations)) return;
450
+ // A group appearing in any held-out split must never become a train neighbor.
451
+ const heldOutGroups = new Set(observations.filter(obs => isObj(obs) && nonEmpty(obs.groupId) &&
452
+ ['test', 'validation', 'calibration'].includes(obs.split)).map(obs => obs.groupId));
453
+ evidenceContext.heldOutGroups = heldOutGroups;
454
+ for (const obs of observations) {
455
+ if (!isObj(obs)) { reject('observationNotObject'); continue; }
456
+ if (!nonEmpty(obs.id)) { reject('observationBadId'); continue; }
457
+ if (evidenceContext.idSet.has(obs.id)) { reject('duplicateId'); continue; }
458
+ if (!nonEmpty(obs.groupId)) { reject('observationBadGroupId'); continue; }
459
+ if (heldOutGroups.has(obs.groupId)) { reject('heldOutGroup'); continue; }
460
+ if (typeof obs.text !== 'string' || obs.text.length === 0) { reject('observationBadText'); continue; }
461
+ const provider = nonEmpty(obs.provider) ? obs.provider : null;
462
+ const model = nonEmpty(obs.model) ? obs.model : null;
463
+ if (!provider || !model) { reject('observationBadIdentity'); continue; }
464
+ if (!effortOk(obs.reasoningEffort)) { reject('observationBadEffort'); continue; }
465
+ const channel = normalizeChannel(obs.channel);
466
+ if (!channel) { reject('observationBadChannel'); continue; }
467
+ if (!nonEmpty(obs.type)) { reject('observationBadType'); continue; }
468
+ if (typeof obs.quality !== 'number' || !Number.isFinite(obs.quality) || obs.quality < 0 || obs.quality > 1) {
469
+ reject('observationBadQuality'); continue;
470
+ }
471
+ if (obs.split !== 'train') { reject('observationBadSplit'); continue; }
472
+ if (!checkWindow(obs.timestamp, evidenceContext.now, evidenceContext.maxAgeMs)) { reject('observationBadTimestamp'); continue; }
473
+ const prov = obs.provenance;
474
+ if (!isObj(prov) || prov.kind !== 'objective') { reject('observationBadProvenance'); continue; }
475
+ if (!nonEmpty(prov.source) || !nonEmpty(prov.reference)) { reject('observationBadProvenance'); continue; }
476
+ if (isSubjectiveSource(prov.source)) { reject('observationSubjectiveSource'); continue; }
477
+ let synthetic = false;
478
+ if (obs.synthetic === true) {
479
+ if (!evidenceContext.allowSynthetic) { reject('syntheticDisabled'); continue; }
480
+ synthetic = true;
481
+ } else if (obs.synthetic !== undefined && obs.synthetic !== false) {
482
+ reject('observationBadSynthetic'); continue;
483
+ }
484
+ const rk = routeKey(provider, model, obs.reasoningEffort, channel);
485
+ const ak = actionKey(rk, obs.type, obs.groupId);
486
+ if (evidenceContext.actionSet.has(ak)) { reject('duplicateAction'); continue; }
487
+ if (evidenceContext.samples.length >= MAX_QUALITY_SAMPLES) { reject('tooManySamples'); continue; }
488
+ const features = buildFeatures(obs.text);
489
+ if (features.length === 0) { reject('observationEmptyFeatures'); continue; }
490
+ evidenceContext.samples.push({
491
+ id: obs.id,
492
+ groupId: obs.groupId,
493
+ provider, model,
494
+ reasoningEffort: obs.reasoningEffort,
495
+ channel,
496
+ type: obs.type,
497
+ quality: obs.quality,
498
+ timestamp: obs.timestamp,
499
+ synthetic,
500
+ split: 'train',
501
+ provenance: { kind: 'objective', source: prov.source, reference: prov.reference },
502
+ features
503
+ });
504
+ evidenceContext.idSet.add(obs.id);
505
+ evidenceContext.actionSet.add(ak);
506
+ evidenceContext.stats.qualityAccepted++;
507
+ evidenceContext.stats.accepted++;
508
+ }
509
+ }
510
+
511
+ function finalizeResources(resourceMap, minSamples) {
512
+ const resourceGroups = [];
513
+ for (const g of resourceMap.values()) {
514
+ if (resourceGroups.length >= MAX_RESOURCE_GROUPS) break;
515
+ const ir = g.samples.ir.slice().sort(cmp);
516
+ const ot = g.samples.ot.slice().sort(cmp);
517
+ const dm = g.samples.dm.slice().sort(cmp);
518
+ const pr = g.samples.pr.slice().sort(cmp);
519
+ const hasDur = dm.length >= minSamples;
520
+ const hasPr = pr.length >= minSamples;
521
+ resourceGroups.push({
522
+ identity: g.identity,
523
+ sampleCount: ir.length,
524
+ inputTokenRatio: quantileSorted(ir, 0.5),
525
+ outputTokens: quantileSorted(ot, 0.5),
526
+ durationMs: hasDur ? quantileSorted(dm, 0.5) : null,
527
+ promptCharRatio: hasPr ? quantileSorted(pr, 0.5) : null,
528
+ p90: {
529
+ inputTokenRatio: quantileSorted(ir, 0.9),
530
+ outputTokens: quantileSorted(ot, 0.9),
531
+ durationMs: hasDur ? quantileSorted(dm, 0.9) : null,
532
+ promptCharRatio: hasPr ? quantileSorted(pr, 0.9) : null
533
+ }
534
+ });
535
+ }
536
+ return resourceGroups;
537
+ }
538
+
539
+ export function buildRoutingEvidence({
540
+ runs = [],
541
+ observations = [],
542
+ qualityEvidence = null,
543
+ now = Date.now(),
544
+ minSamples = 8,
545
+ allowSynthetic = false,
546
+ maxAgeMs = DEFAULT_MAX_AGE_MS
547
+ } = {}) {
548
+ checkEvidenceConfig(now, minSamples, allowSynthetic, maxAgeMs, 'build');
549
+ const stats = makeStats();
550
+ const resourceMap = new Map();
551
+ const evidenceContext = { now, maxAgeMs, allowSynthetic, stats, resourceMap };
552
+ ingestRuns(runs, evidenceContext);
553
+ const samples = [];
554
+ const idSet = new Set();
555
+ const actionSet = new Set();
556
+ const qctx = { now, maxAgeMs, allowSynthetic, stats, samples, idSet, actionSet };
557
+ ingestObservations(observations, qctx);
558
+ if (qualityEvidence !== null && qualityEvidence !== undefined) {
559
+ // Strictly validate the whole artifact (including resourceGroups) before
560
+ // importing only its qualitySamples; resourceGroups are ignored because
561
+ // live runs rebuild resource evidence.
562
+ const art = validateArtifactObject(qualityEvidence, { now, allowSynthetic, maxAgeMs });
563
+ const reject = makeRejecter(stats, 'quality');
564
+ for (const s of art.qualitySamples) {
565
+ if (qctx.heldOutGroups?.has(s.groupId)) { reject('heldOutGroup'); continue; }
566
+ if (idSet.has(s.id)) { reject('duplicateId'); continue; }
567
+ const ak = actionKey(routeKey(s.provider, s.model, s.reasoningEffort, s.channel), s.type, s.groupId);
568
+ if (actionSet.has(ak)) { reject('duplicateAction'); continue; }
569
+ if (samples.length >= MAX_QUALITY_SAMPLES) { reject('tooManySamples'); continue; }
570
+ samples.push(s);
571
+ idSet.add(s.id);
572
+ actionSet.add(ak);
573
+ stats.qualityAccepted++;
574
+ stats.accepted++;
575
+ }
576
+ }
577
+ const resourceGroups = finalizeResources(resourceMap, minSamples);
578
+ resourceGroups.sort((a, b) => compareText(JSON.stringify(a.identity), JSON.stringify(b.identity)));
579
+ samples.sort((a, b) => compareText(JSON.stringify([a.provider, a.model, a.reasoningEffort,
580
+ a.channel, a.type, a.groupId, a.id]), JSON.stringify([b.provider, b.model,
581
+ b.reasoningEffort, b.channel, b.type, b.groupId, b.id])));
582
+ return {
583
+ schemaVersion: SCHEMA_VERSION,
584
+ revision: contentRevision({ minSamples, resourceGroups, qualitySamples: samples }),
585
+ generatedAt: now,
586
+ minSamples,
587
+ resourceGroups,
588
+ qualitySamples: samples,
589
+ stats
590
+ };
591
+ }
592
+
593
+ export function predictResources(evidence, {
594
+ provider, model, reasoningEffort = '', channel = 'harness-llm',
595
+ type = 'general', purpose = '', inputChars
596
+ } = {}) {
597
+ if (!isObj(evidence) || evidence.schemaVersion !== SCHEMA_VERSION || !Array.isArray(evidence.resourceGroups)) return null;
598
+ if (!nonEmpty(provider) || !nonEmpty(model)) return null;
599
+ if (!effortOk(reasoningEffort)) return null;
600
+ const ch = normalizeChannel(channel);
601
+ if (!ch) return null;
602
+ const ty = nonEmpty(type) ? type : 'general';
603
+ const pu = typeof purpose === 'string' ? purpose : '';
604
+ const minSamples = evidence.minSamples;
605
+ if (!Number.isInteger(minSamples) || minSamples < 1 || minSamples > 1000) return null;
606
+ void inputChars;
607
+ for (const g of evidence.resourceGroups) {
608
+ if (!validateGroup(g)) continue;
609
+ const id = g.identity;
610
+ if (id.provider === provider && id.model === model && id.reasoningEffort === reasoningEffort &&
611
+ id.channel === ch && id.type === ty && id.purpose === pu) {
612
+ if (!Number.isInteger(g.sampleCount) || g.sampleCount < minSamples) return null;
613
+ return {
614
+ inputTokenRatio: g.inputTokenRatio,
615
+ outputTokens: g.outputTokens,
616
+ durationMs: g.durationMs,
617
+ promptCharRatio: g.promptCharRatio,
618
+ p90: {
619
+ inputTokenRatio: g.p90.inputTokenRatio,
620
+ outputTokens: g.p90.outputTokens,
621
+ durationMs: g.p90.durationMs,
622
+ promptCharRatio: g.p90.promptCharRatio
623
+ },
624
+ sampleCount: g.sampleCount,
625
+ source: 'recorded-resource-evidence'
626
+ };
627
+ }
628
+ }
629
+ return null;
630
+ }
631
+
632
+ export function predictQuality(evidence, {
633
+ text, type = 'general', provider, model, reasoningEffort = '', channel = 'harness-llm'
634
+ } = {}) {
635
+ if (!isObj(evidence) || evidence.schemaVersion !== SCHEMA_VERSION || !Array.isArray(evidence.qualitySamples)) return null;
636
+ if (typeof text !== 'string' || text.length === 0) return null;
637
+ if (!nonEmpty(provider) || !nonEmpty(model)) return null;
638
+ if (!effortOk(reasoningEffort)) return null;
639
+ const ch = normalizeChannel(channel);
640
+ if (!ch) return null;
641
+ const ty = nonEmpty(type) ? type : 'general';
642
+ const minSamples = evidence.minSamples;
643
+ if (!Number.isInteger(minSamples) || minSamples < 1 || minSamples > 1000) return null;
644
+ const qfeat = buildFeatures(text);
645
+ if (qfeat.length === 0) return null;
646
+ const matched = [];
647
+ let sumAll = 0;
648
+ let nAll = 0;
649
+ for (const s of evidence.qualitySamples) {
650
+ if (!isObj(s) || !finite(s.quality) || s.quality < 0 || s.quality > 1 ||
651
+ !validateFeatures(s.features) || s.split !== 'train') continue;
652
+ if (s.provider !== provider || s.model !== model || s.reasoningEffort !== reasoningEffort ||
653
+ s.channel !== ch || s.type !== ty) continue;
654
+ sumAll += s.quality;
655
+ nAll++;
656
+ const sim = cosine(qfeat, s.features);
657
+ if (sim >= SIM_THRESHOLD) matched.push({ s, sim });
658
+ }
659
+ if (matched.length === 0) return null;
660
+ matched.sort((a, b) => b.sim - a.sim);
661
+ const seenGroup = new Set();
662
+ const neighbors = [];
663
+ for (const m of matched) {
664
+ if (seenGroup.has(m.s.groupId)) continue;
665
+ seenGroup.add(m.s.groupId);
666
+ neighbors.push(m);
667
+ if (neighbors.length >= MAX_NEIGHBORS) break;
668
+ }
669
+ if (neighbors.length < minSamples) return null;
670
+ const routeTypeMean = nAll > 0 ? sumAll / nAll :
671
+ neighbors.reduce((a, b) => a + b.s.quality, 0) / neighbors.length;
672
+ let wsum = 0;
673
+ let wq = 0;
674
+ for (const n of neighbors) { wsum += n.sim; wq += n.sim * n.s.quality; }
675
+ if (!(wsum > 0)) return null;
676
+ // Shrink toward the exact route+type mean with a fixed prior of 2 samples.
677
+ const quality = (wq + SHRINK_PRIOR * routeTypeMean) / (wsum + SHRINK_PRIOR);
678
+ const meanSim = wsum / neighbors.length;
679
+ const wmean = wq / wsum;
680
+ let varSum = 0;
681
+ for (const n of neighbors) {
682
+ const d = n.s.quality - wmean;
683
+ varSum += n.sim * d * d;
684
+ }
685
+ const spread = Math.sqrt(varSum / wsum);
686
+ const clamped = Math.min(1, Math.max(0, quality));
687
+ return {
688
+ quality: clamped,
689
+ sampleCount: neighbors.length,
690
+ similarity: Math.min(1, Math.max(0, meanSim)),
691
+ source: 'recorded-query-evidence',
692
+ uncertainty: {
693
+ kind: 'empirical-neighbor-spread',
694
+ neighborCount: neighbors.length,
695
+ similarityThreshold: SIM_THRESHOLD,
696
+ spread,
697
+ calibrated: false,
698
+ note: 'Empirical neighbor spread; not a calibrated probability.'
699
+ }
700
+ };
701
+ }
702
+
703
+ export function parseRoutingEvidenceJson(text, {
704
+ now = Date.now(),
705
+ allowSynthetic = false,
706
+ maxAgeMs = DEFAULT_MAX_AGE_MS
707
+ } = {}) {
708
+ if (typeof text !== 'string') {
709
+ throw new Error('routing-evidence: parse expects a JSON string');
710
+ }
711
+ let data;
712
+ try {
713
+ data = JSON.parse(text);
714
+ } catch (err) {
715
+ throw new Error(`routing-evidence: invalid JSON (${err.message})`);
716
+ }
717
+ // Validation is structural only. The artifact's identity and provenance
718
+ // claims are caller assertions and are NEVER treated as authenticated here.
719
+ return validateArtifactObject(data, { now, allowSynthetic, maxAgeMs });
720
+ }