@klhapp/skillmux 1.5.1 → 1.6.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.
- package/CHANGELOG.md +17 -0
- package/docs/calibration.md +91 -4
- package/docs/cli.md +22 -1
- package/package.json +1 -1
- package/src/adapters.ts +410 -49
- package/src/calibrate.ts +526 -22
- package/src/cli.ts +75 -0
- package/src/router-core.ts +87 -13
package/src/adapters.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
applyCalibrationRun,
|
|
5
|
+
assertCalibrationFeasibility,
|
|
6
|
+
computeCorpusFingerprint,
|
|
7
|
+
createInitialCalibrationRun,
|
|
8
|
+
finalizeCalibrationRun,
|
|
9
|
+
getCalibrationObservations,
|
|
10
|
+
getCalibrationRun,
|
|
11
|
+
insertCalibrationRun,
|
|
12
|
+
listCalibrationRuns,
|
|
13
|
+
loadDecisionCasesFromFile,
|
|
14
|
+
openCalibrateDb,
|
|
15
|
+
runCalibration,
|
|
16
|
+
saveCalibrationObservation,
|
|
17
|
+
summarizeDatasetProvenance,
|
|
18
|
+
type CalibrationResult,
|
|
19
|
+
type QueryObservation,
|
|
20
|
+
} from "./calibrate";
|
|
4
21
|
import { createClients } from "./clients";
|
|
5
22
|
import { embeddingFingerprint, expandHome, loadConfig, rerankerFingerprint, resolveConfigPath } from "./config";
|
|
6
23
|
import { openIndex } from "./db";
|
|
@@ -19,8 +36,33 @@ import {
|
|
|
19
36
|
type SetConfigResult,
|
|
20
37
|
} from "./config-service";
|
|
21
38
|
import type { ResolvedTarget } from "./context";
|
|
22
|
-
import {
|
|
23
|
-
|
|
39
|
+
import {
|
|
40
|
+
classifyInferenceError,
|
|
41
|
+
configure,
|
|
42
|
+
retrieveAndRerankSnapshot,
|
|
43
|
+
syncVaultIfNeeded,
|
|
44
|
+
type StageTiming,
|
|
45
|
+
} from "./router-core";
|
|
46
|
+
import type { Clients, Config } from "./types";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Maximum retry attempts for transient reranker availability errors during calibration.
|
|
50
|
+
*
|
|
51
|
+
* Single-capacity remote reranker endpoints can suffer isolated blips (e.g. transient
|
|
52
|
+
* connection reset or momentary 503) during long multi-case calibration runs.
|
|
53
|
+
* A small bounded budget of 2 retries (3 attempts total) allows calibration to ride out
|
|
54
|
+
* transient blips without stalling on hard failures.
|
|
55
|
+
*/
|
|
56
|
+
export const CALIBRATION_RERANK_MAX_RETRIES = 2;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Base backoff delay (ms) between reranker retries during calibration.
|
|
60
|
+
*
|
|
61
|
+
* A conservative nonzero delay gives recovering remote endpoints time to settle.
|
|
62
|
+
* Because reranker admission is strictly FIFO serialized, waiting workers remain
|
|
63
|
+
* queued rather than creating synchronized retry storms.
|
|
64
|
+
*/
|
|
65
|
+
export const CALIBRATION_RERANK_RETRY_BACKOFF_MS = 250;
|
|
24
66
|
|
|
25
67
|
export interface Capabilities {
|
|
26
68
|
config_read: boolean;
|
|
@@ -34,6 +76,44 @@ export interface Capabilities {
|
|
|
34
76
|
export interface TargetAdapterOptions {
|
|
35
77
|
configPath?: string;
|
|
36
78
|
allowInsecure?: boolean;
|
|
79
|
+
clients?: Clients;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Aggregate timing data collected during a single calibrateRun invocation.
|
|
84
|
+
*
|
|
85
|
+
* All durations are in milliseconds and are non-negative.
|
|
86
|
+
*
|
|
87
|
+
* Cumulative fields (cumulative_embedding_ms, cumulative_lexical_ms,
|
|
88
|
+
* cumulative_vector_ms, cumulative_reranker_ms, cumulative_checkpoint_ms)
|
|
89
|
+
* represent total worker time summed across all concurrent query retrievals
|
|
90
|
+
* or checkpoint writes. Because queries run concurrently, the sum of these
|
|
91
|
+
* cumulative fields may exceed wall_ms — they measure how much worker time
|
|
92
|
+
* each stage consumed, not how much wall-clock time it contributed.
|
|
93
|
+
*/
|
|
94
|
+
export interface CalibrationTimingSummary {
|
|
95
|
+
/** Total number of dataset cases. */
|
|
96
|
+
cases_total: number;
|
|
97
|
+
/** Cases actually retrieved in this invocation (not reused from a prior run). */
|
|
98
|
+
cases_executed: number;
|
|
99
|
+
/** Cases loaded from a prior interrupted run (resume observations). */
|
|
100
|
+
cases_reused: number;
|
|
101
|
+
/** Wall-clock duration of the full calibrateRun operation (ms). */
|
|
102
|
+
wall_ms: number;
|
|
103
|
+
/** Duration of the one-time vault synchronization before retrieval (ms). */
|
|
104
|
+
vault_sync_ms: number;
|
|
105
|
+
/** Cumulative worker time spent in embedding across all queries (ms). */
|
|
106
|
+
cumulative_embedding_ms: number;
|
|
107
|
+
/** Cumulative worker time spent in lexical search across all queries (ms). */
|
|
108
|
+
cumulative_lexical_ms: number;
|
|
109
|
+
/** Cumulative worker time spent in vector search across all queries (ms). */
|
|
110
|
+
cumulative_vector_ms: number;
|
|
111
|
+
/** Cumulative worker time spent in reranking across all queries (ms). */
|
|
112
|
+
cumulative_reranker_ms: number;
|
|
113
|
+
/** Cumulative worker time spent writing observation checkpoints (ms). */
|
|
114
|
+
cumulative_checkpoint_ms: number;
|
|
115
|
+
/** Duration of threshold selection and test-split certification (ms). */
|
|
116
|
+
policy_evaluation_ms: number;
|
|
37
117
|
}
|
|
38
118
|
|
|
39
119
|
export interface TargetAdapter {
|
|
@@ -50,6 +130,16 @@ export interface TargetAdapter {
|
|
|
50
130
|
minRetrievalRecallAtK?: number;
|
|
51
131
|
minDeliveredShortlistRecallAtK?: number;
|
|
52
132
|
minAutoMatchCount?: number;
|
|
133
|
+
tuneAutoMatchPrecisionBuffer?: number;
|
|
134
|
+
tuneAutoMatchCountBuffer?: number;
|
|
135
|
+
tuneDeliveredShortlistRecallBuffer?: number;
|
|
136
|
+
concurrency?: number;
|
|
137
|
+
resumeRunId?: string;
|
|
138
|
+
onProgress?: (completed: number, total: number) => void;
|
|
139
|
+
/** Opt-in timing collection. When true, onTimingSummary is called after a non-throwing result. */
|
|
140
|
+
timing?: boolean;
|
|
141
|
+
/** Called after calibration completes or fails-gates (not called on throws). */
|
|
142
|
+
onTimingSummary?: (summary: CalibrationTimingSummary) => void;
|
|
53
143
|
}): Promise<{ run_id?: string; result?: CalibrationResult }>;
|
|
54
144
|
calibrateList(): Promise<any[]>;
|
|
55
145
|
calibrateShow(runId: string): Promise<any>;
|
|
@@ -68,9 +158,11 @@ export function isLoopbackHost(hostname: string): boolean {
|
|
|
68
158
|
|
|
69
159
|
export class LocalAdapter implements TargetAdapter {
|
|
70
160
|
private configPath: string;
|
|
161
|
+
private clients?: Clients;
|
|
71
162
|
|
|
72
163
|
constructor(opts?: TargetAdapterOptions) {
|
|
73
164
|
this.configPath = resolveConfigPath(opts?.configPath);
|
|
165
|
+
this.clients = opts?.clients;
|
|
74
166
|
}
|
|
75
167
|
|
|
76
168
|
async getCapabilities(): Promise<Capabilities> {
|
|
@@ -119,6 +211,7 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
119
211
|
if (caps.persistence === "externally_managed") {
|
|
120
212
|
throw new CliError("Configuration is externally managed and cannot be modified", 4);
|
|
121
213
|
}
|
|
214
|
+
validateDottedKey(key);
|
|
122
215
|
return setDottedKey(key, rawValStr, {
|
|
123
216
|
configPath: this.configPath,
|
|
124
217
|
dryRun: opts?.dryRun,
|
|
@@ -136,10 +229,74 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
136
229
|
minRetrievalRecallAtK?: number;
|
|
137
230
|
minDeliveredShortlistRecallAtK?: number;
|
|
138
231
|
minAutoMatchCount?: number;
|
|
232
|
+
tuneAutoMatchPrecisionBuffer?: number;
|
|
233
|
+
tuneAutoMatchCountBuffer?: number;
|
|
234
|
+
tuneDeliveredShortlistRecallBuffer?: number;
|
|
235
|
+
concurrency?: number;
|
|
236
|
+
resumeRunId?: string;
|
|
237
|
+
onProgress?: (completed: number, total: number) => void;
|
|
238
|
+
timing?: boolean;
|
|
239
|
+
onTimingSummary?: (summary: CalibrationTimingSummary) => void;
|
|
139
240
|
}): Promise<{ run_id?: string; result?: CalibrationResult }> {
|
|
241
|
+
const collectTiming = opts?.timing === true;
|
|
242
|
+
const wallStart = collectTiming ? performance.now() : 0;
|
|
243
|
+
|
|
140
244
|
const config = await loadConfig(this.configPath);
|
|
245
|
+
const baseClients = this.clients ?? createClients(config);
|
|
246
|
+
|
|
247
|
+
// Bounded admission at the calibration reranker boundary: serialize rerank calls
|
|
248
|
+
// so concurrent case retrieval (embedding, lexical, vector) does not overwhelm
|
|
249
|
+
// a single-capacity remote reranker endpoint.
|
|
250
|
+
//
|
|
251
|
+
// If an admitted request encounters a transient reranker_unavailable error,
|
|
252
|
+
// retry with conservative backoff up to CALIBRATION_RERANK_MAX_RETRIES.
|
|
253
|
+
// Non-availability errors (protocol, timeouts) and permanent failures fail closed.
|
|
254
|
+
let rerankQueue = Promise.resolve() as Promise<any>;
|
|
255
|
+
const serializedRerank: typeof baseClients.rerank = baseClients.rerank
|
|
256
|
+
? (query, docs) => {
|
|
257
|
+
const run = async () => {
|
|
258
|
+
let attempt = 0;
|
|
259
|
+
while (true) {
|
|
260
|
+
try {
|
|
261
|
+
return await baseClients.rerank!(query, docs);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
attempt++;
|
|
264
|
+
const degradationReason = classifyInferenceError("reranker", err);
|
|
265
|
+
if (
|
|
266
|
+
degradationReason === "reranker_unavailable" &&
|
|
267
|
+
attempt <= CALIBRATION_RERANK_MAX_RETRIES
|
|
268
|
+
) {
|
|
269
|
+
await Bun.sleep(CALIBRATION_RERANK_RETRY_BACKOFF_MS * attempt);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const next = rerankQueue.then(run, run);
|
|
277
|
+
rerankQueue = next.catch(() => {});
|
|
278
|
+
return next;
|
|
279
|
+
}
|
|
280
|
+
: undefined;
|
|
281
|
+
|
|
282
|
+
const clients: Clients = {
|
|
283
|
+
...baseClients,
|
|
284
|
+
rerank: serializedRerank,
|
|
285
|
+
};
|
|
286
|
+
configure({ config, clients });
|
|
287
|
+
|
|
288
|
+
// Measure vault synchronization
|
|
289
|
+
let vault_sync_ms = 0;
|
|
290
|
+
if (collectTiming) {
|
|
291
|
+
const t0 = performance.now();
|
|
292
|
+
await syncVaultIfNeeded();
|
|
293
|
+
vault_sync_ms = Math.max(0, performance.now() - t0);
|
|
294
|
+
} else {
|
|
295
|
+
await syncVaultIfNeeded();
|
|
296
|
+
}
|
|
297
|
+
|
|
141
298
|
const candidateLimit =
|
|
142
|
-
config.output?.ambiguous_candidate_limit ?? config.thresholds
|
|
299
|
+
config.output?.ambiguous_candidate_limit ?? config.thresholds?.candidate_limit ?? 5;
|
|
143
300
|
const datasetFile = opts?.datasetPath ?? join(expandHome(config.state_dir), "queries.json");
|
|
144
301
|
const indexDb = openIndex(expandHome(config.state_dir));
|
|
145
302
|
let indexedSkills: Array<{ skill_id: string; content_sha256: string }>;
|
|
@@ -156,71 +313,266 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
156
313
|
datasetFile,
|
|
157
314
|
indexedSkills.map((skill) => skill.skill_id),
|
|
158
315
|
);
|
|
159
|
-
|
|
160
|
-
configure({ config, clients });
|
|
161
|
-
const result = await runCalibration({
|
|
162
|
-
cases,
|
|
163
|
-
getRankedCandidates: async (query: string) => {
|
|
164
|
-
const result = await retrieveAndRerank({ query, forceLexical: false });
|
|
165
|
-
if (result.retrieval !== "reranked") {
|
|
166
|
-
throw new Error(
|
|
167
|
-
"Calibration requires successful hybrid retrieval and reranking for every query.",
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
return result.candidates.map((candidate) => ({
|
|
171
|
-
skill_id: candidate.skill_id,
|
|
172
|
-
score: candidate.score ?? 0,
|
|
173
|
-
}));
|
|
174
|
-
},
|
|
175
|
-
reranker: clients.rerank,
|
|
176
|
-
candidateLimit,
|
|
177
|
-
minAutoMatchPrecision: opts?.minAutoMatchPrecision,
|
|
178
|
-
minRetrievalRecallAtK: opts?.minRetrievalRecallAtK,
|
|
179
|
-
minDeliveredShortlistRecallAtK: opts?.minDeliveredShortlistRecallAtK,
|
|
180
|
-
minAutoMatchCount: opts?.minAutoMatchCount,
|
|
181
|
-
});
|
|
316
|
+
|
|
182
317
|
const fingerprint = rerankerFingerprint(config);
|
|
183
318
|
if (!fingerprint) {
|
|
184
319
|
throw new Error("A configured remote reranker is required to record calibration.");
|
|
185
320
|
}
|
|
186
321
|
const datasetText = await Bun.file(datasetFile).text();
|
|
187
|
-
const
|
|
322
|
+
const datasetHash = createHash("sha256").update(datasetText).digest("hex");
|
|
323
|
+
const embedFp = embeddingFingerprint(config);
|
|
324
|
+
const recallSettings = {
|
|
325
|
+
k_lexical: config.recall.k_lexical,
|
|
326
|
+
k_vector: config.recall.k_vector,
|
|
327
|
+
k_rerank: config.recall.k_rerank ?? Math.min(10, config.recall.k_lexical + config.recall.k_vector),
|
|
328
|
+
};
|
|
329
|
+
const minAutoMatchPrecision = opts?.minAutoMatchPrecision ?? 0.75;
|
|
330
|
+
const minAutoMatchCount = opts?.minAutoMatchCount ?? 15;
|
|
331
|
+
const minDeliveredShortlistRecallAtK =
|
|
332
|
+
opts?.minDeliveredShortlistRecallAtK ??
|
|
333
|
+
opts?.minRetrievalRecallAtK ??
|
|
334
|
+
0.95;
|
|
335
|
+
const minShortlistRecallAt5 = opts?.minRetrievalRecallAtK ?? 0.95;
|
|
336
|
+
const tuneAutoMatchPrecisionBuffer = opts?.tuneAutoMatchPrecisionBuffer ?? 0.03;
|
|
337
|
+
const tuneAutoMatchCountBuffer = opts?.tuneAutoMatchCountBuffer ?? 3;
|
|
338
|
+
const tuneDeliveredShortlistRecallBuffer = opts?.tuneDeliveredShortlistRecallBuffer ?? 0.02;
|
|
339
|
+
const concurrency = opts?.concurrency ?? 4;
|
|
340
|
+
|
|
341
|
+
assertCalibrationFeasibility(cases, {
|
|
342
|
+
minAutoMatchPrecision,
|
|
343
|
+
minAutoMatchCount,
|
|
344
|
+
minDeliveredShortlistRecallAtK,
|
|
345
|
+
tuneAutoMatchPrecisionBuffer,
|
|
346
|
+
tuneAutoMatchCountBuffer,
|
|
347
|
+
tuneDeliveredShortlistRecallBuffer,
|
|
348
|
+
});
|
|
349
|
+
|
|
188
350
|
const db = openCalibrateDb(expandHome(config.state_dir));
|
|
351
|
+
let runId: string;
|
|
352
|
+
let initialObservations: Map<number, QueryObservation> | undefined = undefined;
|
|
353
|
+
|
|
354
|
+
// Cumulative timing accumulators (only used when collectTiming=true)
|
|
355
|
+
let cumulative_embedding_ms = 0;
|
|
356
|
+
let cumulative_lexical_ms = 0;
|
|
357
|
+
let cumulative_vector_ms = 0;
|
|
358
|
+
let cumulative_reranker_ms = 0;
|
|
359
|
+
let cumulative_checkpoint_ms = 0;
|
|
360
|
+
|
|
189
361
|
try {
|
|
190
|
-
|
|
362
|
+
if (opts?.resumeRunId) {
|
|
363
|
+
runId = opts.resumeRunId;
|
|
364
|
+
const existingRun = getCalibrationRun(db, runId);
|
|
365
|
+
if (!existingRun) {
|
|
366
|
+
throw new Error(`Calibration run "${runId}" not found`);
|
|
367
|
+
}
|
|
368
|
+
if (existingRun.status !== "running") {
|
|
369
|
+
throw new Error(`Cannot resume completed calibration run "${runId}"`);
|
|
370
|
+
}
|
|
371
|
+
if (existingRun.dataset_hash !== datasetHash) {
|
|
372
|
+
throw new Error("Dataset hash mismatch: cannot resume run with a different dataset");
|
|
373
|
+
}
|
|
374
|
+
if (existingRun.corpus_fingerprint !== corpusFingerprint) {
|
|
375
|
+
throw new Error("Corpus fingerprint mismatch: cannot resume run with modified vault skills");
|
|
376
|
+
}
|
|
377
|
+
if (existingRun.embedding_fingerprint !== embedFp) {
|
|
378
|
+
throw new Error("Embedding fingerprint mismatch: cannot resume run with different embedding configuration");
|
|
379
|
+
}
|
|
380
|
+
if (existingRun.reranker_fingerprint !== fingerprint) {
|
|
381
|
+
throw new Error("Reranker fingerprint mismatch: cannot resume run with different reranker configuration");
|
|
382
|
+
}
|
|
383
|
+
if (existingRun.candidate_limit !== candidateLimit) {
|
|
384
|
+
throw new Error("Candidate limit mismatch: cannot resume run with different candidate limit");
|
|
385
|
+
}
|
|
386
|
+
if (
|
|
387
|
+
existingRun.recall_settings &&
|
|
388
|
+
JSON.stringify(existingRun.recall_settings) !== JSON.stringify(recallSettings)
|
|
389
|
+
) {
|
|
390
|
+
throw new Error("Recall settings mismatch: cannot resume run with different recall parameters");
|
|
391
|
+
}
|
|
392
|
+
if (existingRun.min_auto_match_precision !== minAutoMatchPrecision) {
|
|
393
|
+
throw new Error("Certification gate mismatch: min_auto_match_precision differs from original run");
|
|
394
|
+
}
|
|
395
|
+
if ((existingRun.min_auto_match_count ?? 1) !== minAutoMatchCount) {
|
|
396
|
+
throw new Error("Certification gate mismatch: min_auto_match_count differs from original run");
|
|
397
|
+
}
|
|
398
|
+
if (
|
|
399
|
+
(existingRun.min_delivered_shortlist_recall_at_k ?? existingRun.min_shortlist_recall_at_5) !==
|
|
400
|
+
minDeliveredShortlistRecallAtK
|
|
401
|
+
) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
"Certification gate mismatch: min_delivered_shortlist_recall_at_k differs from original run",
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (existingRun.min_shortlist_recall_at_5 !== minShortlistRecallAt5) {
|
|
407
|
+
throw new Error("Certification gate mismatch: min_retrieval_recall_at_k differs from original run");
|
|
408
|
+
}
|
|
409
|
+
if (
|
|
410
|
+
existingRun.tune_auto_match_precision_buffer !== undefined &&
|
|
411
|
+
existingRun.tune_auto_match_precision_buffer !== tuneAutoMatchPrecisionBuffer
|
|
412
|
+
) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
"Certification gate mismatch: tune_auto_match_precision_buffer differs from original run",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (
|
|
418
|
+
existingRun.tune_auto_match_count_buffer !== undefined &&
|
|
419
|
+
existingRun.tune_auto_match_count_buffer !== tuneAutoMatchCountBuffer
|
|
420
|
+
) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
"Certification gate mismatch: tune_auto_match_count_buffer differs from original run",
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (
|
|
426
|
+
existingRun.tune_delivered_shortlist_recall_buffer !== undefined &&
|
|
427
|
+
existingRun.tune_delivered_shortlist_recall_buffer !== tuneDeliveredShortlistRecallBuffer
|
|
428
|
+
) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
"Certification gate mismatch: tune_delivered_shortlist_recall_buffer differs from original run",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
initialObservations = getCalibrationObservations(db, runId);
|
|
435
|
+
if (initialObservations.size === 0 && existingRun.observations && existingRun.observations.length > 0) {
|
|
436
|
+
existingRun.observations.forEach((obs, idx) => initialObservations!.set(idx, obs));
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
runId = `run_${crypto.randomUUID()}`;
|
|
440
|
+
createInitialCalibrationRun(db, {
|
|
441
|
+
run_id: runId,
|
|
442
|
+
created_at: new Date().toISOString(),
|
|
443
|
+
status: "running",
|
|
444
|
+
reranker_fingerprint: fingerprint,
|
|
445
|
+
embedding_fingerprint: embedFp,
|
|
446
|
+
corpus_fingerprint: corpusFingerprint,
|
|
447
|
+
dataset_hash: datasetHash,
|
|
448
|
+
dataset_provenance: summarizeDatasetProvenance(cases),
|
|
449
|
+
recall_settings: recallSettings,
|
|
450
|
+
candidate_limit: candidateLimit,
|
|
451
|
+
min_auto_match_precision: minAutoMatchPrecision,
|
|
452
|
+
min_auto_match_count: minAutoMatchCount,
|
|
453
|
+
min_delivered_shortlist_recall_at_k: minDeliveredShortlistRecallAtK,
|
|
454
|
+
min_shortlist_recall_at_5: minShortlistRecallAt5,
|
|
455
|
+
tune_auto_match_precision_buffer: tuneAutoMatchPrecisionBuffer,
|
|
456
|
+
tune_auto_match_count_buffer: tuneAutoMatchCountBuffer,
|
|
457
|
+
tune_delivered_shortlist_recall_buffer: tuneDeliveredShortlistRecallBuffer,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const casesReused = initialObservations?.size ?? 0;
|
|
462
|
+
|
|
463
|
+
const onProgress = opts?.onProgress ?? ((completed, total) => {
|
|
464
|
+
process.stderr.write(`Calibration observations: ${completed}/${total}\n`);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const retrievalTiming = collectTiming
|
|
468
|
+
? {
|
|
469
|
+
onTiming: (timing: StageTiming) => {
|
|
470
|
+
cumulative_embedding_ms += timing.embedding_ms;
|
|
471
|
+
cumulative_lexical_ms += timing.lexical_ms;
|
|
472
|
+
cumulative_vector_ms += timing.vector_ms;
|
|
473
|
+
cumulative_reranker_ms += timing.reranker_ms;
|
|
474
|
+
},
|
|
475
|
+
}
|
|
476
|
+
: undefined;
|
|
477
|
+
|
|
478
|
+
const getRankedCandidates = async (query: string) => {
|
|
479
|
+
const res = await retrieveAndRerankSnapshot(
|
|
480
|
+
{ query, forceLexical: false },
|
|
481
|
+
retrievalTiming,
|
|
482
|
+
);
|
|
483
|
+
if (res.retrieval !== "reranked") {
|
|
484
|
+
throw new Error(
|
|
485
|
+
"Calibration requires successful hybrid retrieval and reranking for every query.",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
return res.candidates.map((candidate) => ({
|
|
489
|
+
skill_id: candidate.skill_id,
|
|
490
|
+
score: candidate.score ?? 0,
|
|
491
|
+
}));
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
let policyEvaluationStart = 0;
|
|
495
|
+
|
|
496
|
+
const result = await runCalibration({
|
|
497
|
+
cases,
|
|
498
|
+
getRankedCandidates,
|
|
499
|
+
reranker: clients.rerank,
|
|
500
|
+
candidateLimit,
|
|
501
|
+
minAutoMatchPrecision,
|
|
502
|
+
minRetrievalRecallAtK: minShortlistRecallAt5,
|
|
503
|
+
minDeliveredShortlistRecallAtK,
|
|
504
|
+
minAutoMatchCount,
|
|
505
|
+
tuneAutoMatchPrecisionBuffer,
|
|
506
|
+
tuneAutoMatchCountBuffer,
|
|
507
|
+
tuneDeliveredShortlistRecallBuffer,
|
|
508
|
+
concurrency,
|
|
509
|
+
initialObservations,
|
|
510
|
+
onProgress,
|
|
511
|
+
onObservation: collectTiming
|
|
512
|
+
? (obs, caseIdx) => {
|
|
513
|
+
const t0 = performance.now();
|
|
514
|
+
saveCalibrationObservation(db, runId, caseIdx, obs);
|
|
515
|
+
const t1 = performance.now();
|
|
516
|
+
cumulative_checkpoint_ms += Math.max(0, t1 - t0);
|
|
517
|
+
}
|
|
518
|
+
: (obs, caseIdx) => {
|
|
519
|
+
saveCalibrationObservation(db, runId, caseIdx, obs);
|
|
520
|
+
},
|
|
521
|
+
onObservationsReady: collectTiming
|
|
522
|
+
? () => {
|
|
523
|
+
policyEvaluationStart = performance.now();
|
|
524
|
+
}
|
|
525
|
+
: undefined,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// policy_evaluation_ms: threshold selection + certification duration
|
|
529
|
+
const policyEvalEnd = collectTiming ? performance.now() : 0;
|
|
530
|
+
|
|
531
|
+
finalizeCalibrationRun(db, {
|
|
191
532
|
run_id: runId,
|
|
192
|
-
created_at: new Date().toISOString(),
|
|
193
533
|
status: result.status,
|
|
194
|
-
reranker_fingerprint: fingerprint,
|
|
195
|
-
embedding_fingerprint: embeddingFingerprint(config),
|
|
196
|
-
corpus_fingerprint: corpusFingerprint,
|
|
197
|
-
dataset_hash: createHash("sha256").update(datasetText).digest("hex"),
|
|
198
|
-
dataset_provenance: summarizeDatasetProvenance(cases),
|
|
199
|
-
recall_settings: {
|
|
200
|
-
k_lexical: config.recall.k_lexical,
|
|
201
|
-
k_vector: config.recall.k_vector,
|
|
202
|
-
k_rerank: config.recall.k_rerank ?? Math.min(10, config.recall.k_lexical + config.recall.k_vector),
|
|
203
|
-
},
|
|
204
|
-
candidate_limit: candidateLimit,
|
|
205
|
-
min_auto_match_precision: opts?.minAutoMatchPrecision ?? 0.99,
|
|
206
|
-
min_auto_match_count: opts?.minAutoMatchCount ?? 30,
|
|
207
|
-
min_delivered_shortlist_recall_at_k:
|
|
208
|
-
opts?.minDeliveredShortlistRecallAtK ??
|
|
209
|
-
opts?.minRetrievalRecallAtK ??
|
|
210
|
-
0.95,
|
|
211
|
-
min_shortlist_recall_at_5: opts?.minRetrievalRecallAtK ?? 0.95,
|
|
212
534
|
failed_reason: result.failed_reason,
|
|
213
535
|
selected_thresholds: result.selected_thresholds,
|
|
214
536
|
tune_metrics: result.tune_metrics,
|
|
215
537
|
test_metrics: result.test_metrics,
|
|
538
|
+
tune_gate_slack: result.tune_gate_slack,
|
|
216
539
|
observations: result.observations,
|
|
217
540
|
});
|
|
541
|
+
|
|
542
|
+
// Emit timing summary after a non-throwing result (completed or failed-gates).
|
|
543
|
+
// Never called when calibrateRun throws.
|
|
544
|
+
if (collectTiming && opts?.onTimingSummary) {
|
|
545
|
+
const wall_ms = Math.max(0, performance.now() - wallStart);
|
|
546
|
+
// policy_evaluation_ms covers threshold selection + test-split certification,
|
|
547
|
+
// the internal work runCalibration does after all observations are collected.
|
|
548
|
+
const policy_evaluation_ms = Math.max(0, policyEvalEnd - policyEvaluationStart);
|
|
549
|
+
|
|
550
|
+
const cases_reused = casesReused;
|
|
551
|
+
const cases_total = cases.length;
|
|
552
|
+
const cases_executed = cases_total - cases_reused;
|
|
553
|
+
|
|
554
|
+
opts.onTimingSummary({
|
|
555
|
+
cases_total,
|
|
556
|
+
cases_executed,
|
|
557
|
+
cases_reused,
|
|
558
|
+
wall_ms,
|
|
559
|
+
vault_sync_ms,
|
|
560
|
+
cumulative_embedding_ms,
|
|
561
|
+
cumulative_lexical_ms,
|
|
562
|
+
cumulative_vector_ms,
|
|
563
|
+
cumulative_reranker_ms,
|
|
564
|
+
cumulative_checkpoint_ms,
|
|
565
|
+
policy_evaluation_ms,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return { run_id: runId, result };
|
|
218
570
|
} finally {
|
|
219
571
|
db.close();
|
|
220
572
|
}
|
|
221
|
-
return { run_id: runId, result };
|
|
222
573
|
}
|
|
223
574
|
|
|
575
|
+
|
|
224
576
|
async calibrateList(): Promise<any[]> {
|
|
225
577
|
const config = await loadConfig(this.configPath);
|
|
226
578
|
const db = openCalibrateDb(expandHome(config.state_dir));
|
|
@@ -429,11 +781,20 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
429
781
|
minRetrievalRecallAtK?: number;
|
|
430
782
|
minDeliveredShortlistRecallAtK?: number;
|
|
431
783
|
minAutoMatchCount?: number;
|
|
784
|
+
tuneAutoMatchPrecisionBuffer?: number;
|
|
785
|
+
tuneAutoMatchCountBuffer?: number;
|
|
786
|
+
tuneDeliveredShortlistRecallBuffer?: number;
|
|
787
|
+
concurrency?: number;
|
|
788
|
+
resumeRunId?: string;
|
|
789
|
+
onProgress?: (completed: number, total: number) => void;
|
|
790
|
+
timing?: boolean;
|
|
791
|
+
onTimingSummary?: (summary: CalibrationTimingSummary) => void;
|
|
432
792
|
}): Promise<{ run_id?: string; result?: CalibrationResult }> {
|
|
433
793
|
void opts;
|
|
434
794
|
throw this.remoteCalibrationNotImplemented();
|
|
435
795
|
}
|
|
436
796
|
|
|
797
|
+
|
|
437
798
|
async calibrateList(): Promise<any[]> {
|
|
438
799
|
throw this.remoteCalibrationNotImplemented();
|
|
439
800
|
}
|