@klhapp/skillmux 1.5.2 → 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 +7 -0
- package/docs/calibration.md +37 -1
- package/docs/cli.md +13 -1
- package/package.json +1 -1
- package/src/adapters.ts +117 -3
- package/src/calibrate.ts +193 -12
- package/src/cli.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.6.0](https://github.com/klhq/skillmux/compare/v1.5.2...v1.6.0) (2026-08-18)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* **calibrate:** add tune safety buffers ([#120](https://github.com/klhq/skillmux/issues/120)) ([66d501c](https://github.com/klhq/skillmux/commit/66d501c4ac16956e8e887622e5178caef743bec7))
|
|
14
|
+
|
|
8
15
|
## [1.5.2](https://github.com/klhq/skillmux/compare/v1.5.1...v1.5.2) (2026-08-17)
|
|
9
16
|
|
|
10
17
|
|
package/docs/calibration.md
CHANGED
|
@@ -89,6 +89,42 @@ The operator owns the labels: supply or review the cases, start the run,
|
|
|
89
89
|
inspect its evidence, and explicitly apply an acceptable result. A successful
|
|
90
90
|
run never changes live thresholds by itself.
|
|
91
91
|
|
|
92
|
+
## Certification gates and preflight feasibility
|
|
93
|
+
|
|
94
|
+
Calibration certifies threshold policies against statistical confidence gates before allowing them to be applied:
|
|
95
|
+
|
|
96
|
+
| Flag | Default | Description |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `--min-auto-match-precision` | `0.75` | Minimum 95% Wilson score lower confidence bound on auto-match precision |
|
|
99
|
+
| `--min-auto-match-count` | `15` | Minimum number of auto-matches required in evaluation |
|
|
100
|
+
| `--min-retrieval-recall-at-k` | `0.95` | Minimum top-k retrieval recall on matchable queries |
|
|
101
|
+
| `--min-delivered-shortlist-recall-at-k` | `0.95` | Minimum delivered shortlist recall on matchable queries |
|
|
102
|
+
| `--tune-auto-match-precision-buffer` | `0.03` | Tune-only selection buffer for Wilson auto-match precision lower bound |
|
|
103
|
+
| `--tune-auto-match-count-buffer` | `3` | Tune-only selection buffer for minimum auto-match count |
|
|
104
|
+
| `--tune-delivered-shortlist-recall-buffer` | `0.02` | Tune-only selection buffer for delivered shortlist recall |
|
|
105
|
+
|
|
106
|
+
### Tune selection buffers vs production gates
|
|
107
|
+
|
|
108
|
+
Tune selection buffers ensure that threshold optimization selects policies with sufficient headroom beyond production gates. A candidate policy during tune search must satisfy the production gates plus their respective tune selection buffers. Test-split certification evaluates selected policies against the original production gates without selection buffers.
|
|
109
|
+
|
|
110
|
+
### Wilson lower confidence bound and evidence size
|
|
111
|
+
|
|
112
|
+
`min-auto-match-precision` is evaluated not as raw sample accuracy, but as a **95% Wilson score lower confidence bound** ($z \approx 1.960$). This accounts for statistical uncertainty in small datasets.
|
|
113
|
+
|
|
114
|
+
Because the Wilson lower bound penalizes small sample sizes:
|
|
115
|
+
- A gate of **0.75** lower bound requires at least **15** flawless (15/15) auto-matches ($\text{Wilson}(15, 15) \approx 0.7961$).
|
|
116
|
+
- A 20-case tune matched split can achieve at most $\text{Wilson}(20, 20) \approx 0.8389$.
|
|
117
|
+
- A gate of **0.99** lower bound is statistically impossible on small datasets; it requires at least **381** flawless auto-matches ($\text{Wilson}(381, 381) \approx 0.9900$).
|
|
118
|
+
|
|
119
|
+
### Preflight feasibility check
|
|
120
|
+
|
|
121
|
+
To avoid running expensive remote embeddings and rerankings on gates that can never pass, Skillmux executes a **preflight feasibility calculation** immediately after loading the dataset and before creating a running calibration record:
|
|
122
|
+
|
|
123
|
+
$$\text{effective\_trials} = \max(N_{\text{tune\_matched}}, \text{minAutoMatchCount})$$
|
|
124
|
+
$$\text{max\_attainable\_precision} = \text{WilsonLowerBound}(N_{\text{tune\_matched}}, \text{effective\_trials})$$
|
|
125
|
+
|
|
126
|
+
If $\text{max\_attainable\_precision} < \text{minAutoMatchPrecision}$, calibration fails immediately with an actionable error indicating the requested precision, requested count, available tune matched cases, and maximum attainable lower bound.
|
|
127
|
+
|
|
92
128
|
## Reading a run
|
|
93
129
|
|
|
94
130
|
A `run_id` identifies one immutable calibration attempt and its evidence.
|
|
@@ -156,7 +192,7 @@ Provenance: the small synthetic corpus and labelled decision cases in
|
|
|
156
192
|
[`tests/router-core.spec.test.ts`](../tests/router-core.spec.test.ts), with the
|
|
157
193
|
wire contract captured by
|
|
158
194
|
[`tests/fixtures/reranker/jina-v1-request.json`](../tests/fixtures/reranker/jina-v1-request.json).
|
|
159
|
-
That fixture is below the default
|
|
195
|
+
That fixture is below the default 15-auto-match certification minimum, so the
|
|
160
196
|
values are a smoke-test/reference profile, not a completed calibration run.
|
|
161
197
|
Run the lifecycle above against the deployment's real corpus before enabling
|
|
162
198
|
automatic matches in production.
|
package/docs/cli.md
CHANGED
|
@@ -298,9 +298,21 @@ certification gates, run evidence, reference values, and the complete operator
|
|
|
298
298
|
lifecycle.
|
|
299
299
|
|
|
300
300
|
```sh
|
|
301
|
-
# Run calibration on a dataset with
|
|
301
|
+
# Run calibration on a dataset with default certification gates (min precision 0.75, min count 15)
|
|
302
302
|
skillmux calibrate run --dataset ./eval/queries.json
|
|
303
303
|
|
|
304
|
+
# Specify explicit certification gates and tune selection buffers
|
|
305
|
+
# Note: --min-auto-match-precision is interpreted as a 95% Wilson lower confidence bound.
|
|
306
|
+
# Calibration preflight validates that the requested gate is mathematically attainable on the tune split.
|
|
307
|
+
skillmux calibrate run --dataset ./eval/queries.json \
|
|
308
|
+
--min-auto-match-precision 0.75 \
|
|
309
|
+
--min-auto-match-count 15 \
|
|
310
|
+
--min-retrieval-recall-at-k 0.95 \
|
|
311
|
+
--min-delivered-shortlist-recall-at-k 0.95 \
|
|
312
|
+
--tune-auto-match-precision-buffer 0.03 \
|
|
313
|
+
--tune-auto-match-count-buffer 3 \
|
|
314
|
+
--tune-delivered-shortlist-recall-buffer 0.02
|
|
315
|
+
|
|
304
316
|
# Set the bounded worker count
|
|
305
317
|
skillmux calibrate run --dataset ./eval/queries.json --concurrency 6
|
|
306
318
|
|
package/package.json
CHANGED
package/src/adapters.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
applyCalibrationRun,
|
|
5
|
+
assertCalibrationFeasibility,
|
|
5
6
|
computeCorpusFingerprint,
|
|
6
7
|
createInitialCalibrationRun,
|
|
7
8
|
finalizeCalibrationRun,
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
} from "./config-service";
|
|
37
38
|
import type { ResolvedTarget } from "./context";
|
|
38
39
|
import {
|
|
40
|
+
classifyInferenceError,
|
|
39
41
|
configure,
|
|
40
42
|
retrieveAndRerankSnapshot,
|
|
41
43
|
syncVaultIfNeeded,
|
|
@@ -43,6 +45,25 @@ import {
|
|
|
43
45
|
} from "./router-core";
|
|
44
46
|
import type { Clients, Config } from "./types";
|
|
45
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;
|
|
66
|
+
|
|
46
67
|
export interface Capabilities {
|
|
47
68
|
config_read: boolean;
|
|
48
69
|
config_write: boolean;
|
|
@@ -109,6 +130,9 @@ export interface TargetAdapter {
|
|
|
109
130
|
minRetrievalRecallAtK?: number;
|
|
110
131
|
minDeliveredShortlistRecallAtK?: number;
|
|
111
132
|
minAutoMatchCount?: number;
|
|
133
|
+
tuneAutoMatchPrecisionBuffer?: number;
|
|
134
|
+
tuneAutoMatchCountBuffer?: number;
|
|
135
|
+
tuneDeliveredShortlistRecallBuffer?: number;
|
|
112
136
|
concurrency?: number;
|
|
113
137
|
resumeRunId?: string;
|
|
114
138
|
onProgress?: (completed: number, total: number) => void;
|
|
@@ -205,6 +229,9 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
205
229
|
minRetrievalRecallAtK?: number;
|
|
206
230
|
minDeliveredShortlistRecallAtK?: number;
|
|
207
231
|
minAutoMatchCount?: number;
|
|
232
|
+
tuneAutoMatchPrecisionBuffer?: number;
|
|
233
|
+
tuneAutoMatchCountBuffer?: number;
|
|
234
|
+
tuneDeliveredShortlistRecallBuffer?: number;
|
|
208
235
|
concurrency?: number;
|
|
209
236
|
resumeRunId?: string;
|
|
210
237
|
onProgress?: (completed: number, total: number) => void;
|
|
@@ -215,7 +242,47 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
215
242
|
const wallStart = collectTiming ? performance.now() : 0;
|
|
216
243
|
|
|
217
244
|
const config = await loadConfig(this.configPath);
|
|
218
|
-
const
|
|
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
|
+
};
|
|
219
286
|
configure({ config, clients });
|
|
220
287
|
|
|
221
288
|
// Measure vault synchronization
|
|
@@ -246,6 +313,7 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
246
313
|
datasetFile,
|
|
247
314
|
indexedSkills.map((skill) => skill.skill_id),
|
|
248
315
|
);
|
|
316
|
+
|
|
249
317
|
const fingerprint = rerankerFingerprint(config);
|
|
250
318
|
if (!fingerprint) {
|
|
251
319
|
throw new Error("A configured remote reranker is required to record calibration.");
|
|
@@ -258,15 +326,27 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
258
326
|
k_vector: config.recall.k_vector,
|
|
259
327
|
k_rerank: config.recall.k_rerank ?? Math.min(10, config.recall.k_lexical + config.recall.k_vector),
|
|
260
328
|
};
|
|
261
|
-
const minAutoMatchPrecision = opts?.minAutoMatchPrecision ?? 0.
|
|
262
|
-
const minAutoMatchCount = opts?.minAutoMatchCount ??
|
|
329
|
+
const minAutoMatchPrecision = opts?.minAutoMatchPrecision ?? 0.75;
|
|
330
|
+
const minAutoMatchCount = opts?.minAutoMatchCount ?? 15;
|
|
263
331
|
const minDeliveredShortlistRecallAtK =
|
|
264
332
|
opts?.minDeliveredShortlistRecallAtK ??
|
|
265
333
|
opts?.minRetrievalRecallAtK ??
|
|
266
334
|
0.95;
|
|
267
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;
|
|
268
339
|
const concurrency = opts?.concurrency ?? 4;
|
|
269
340
|
|
|
341
|
+
assertCalibrationFeasibility(cases, {
|
|
342
|
+
minAutoMatchPrecision,
|
|
343
|
+
minAutoMatchCount,
|
|
344
|
+
minDeliveredShortlistRecallAtK,
|
|
345
|
+
tuneAutoMatchPrecisionBuffer,
|
|
346
|
+
tuneAutoMatchCountBuffer,
|
|
347
|
+
tuneDeliveredShortlistRecallBuffer,
|
|
348
|
+
});
|
|
349
|
+
|
|
270
350
|
const db = openCalibrateDb(expandHome(config.state_dir));
|
|
271
351
|
let runId: string;
|
|
272
352
|
let initialObservations: Map<number, QueryObservation> | undefined = undefined;
|
|
@@ -326,6 +406,30 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
326
406
|
if (existingRun.min_shortlist_recall_at_5 !== minShortlistRecallAt5) {
|
|
327
407
|
throw new Error("Certification gate mismatch: min_retrieval_recall_at_k differs from original run");
|
|
328
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
|
+
}
|
|
329
433
|
|
|
330
434
|
initialObservations = getCalibrationObservations(db, runId);
|
|
331
435
|
if (initialObservations.size === 0 && existingRun.observations && existingRun.observations.length > 0) {
|
|
@@ -348,6 +452,9 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
348
452
|
min_auto_match_count: minAutoMatchCount,
|
|
349
453
|
min_delivered_shortlist_recall_at_k: minDeliveredShortlistRecallAtK,
|
|
350
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,
|
|
351
458
|
});
|
|
352
459
|
}
|
|
353
460
|
|
|
@@ -395,6 +502,9 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
395
502
|
minRetrievalRecallAtK: minShortlistRecallAt5,
|
|
396
503
|
minDeliveredShortlistRecallAtK,
|
|
397
504
|
minAutoMatchCount,
|
|
505
|
+
tuneAutoMatchPrecisionBuffer,
|
|
506
|
+
tuneAutoMatchCountBuffer,
|
|
507
|
+
tuneDeliveredShortlistRecallBuffer,
|
|
398
508
|
concurrency,
|
|
399
509
|
initialObservations,
|
|
400
510
|
onProgress,
|
|
@@ -425,6 +535,7 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
425
535
|
selected_thresholds: result.selected_thresholds,
|
|
426
536
|
tune_metrics: result.tune_metrics,
|
|
427
537
|
test_metrics: result.test_metrics,
|
|
538
|
+
tune_gate_slack: result.tune_gate_slack,
|
|
428
539
|
observations: result.observations,
|
|
429
540
|
});
|
|
430
541
|
|
|
@@ -670,6 +781,9 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
670
781
|
minRetrievalRecallAtK?: number;
|
|
671
782
|
minDeliveredShortlistRecallAtK?: number;
|
|
672
783
|
minAutoMatchCount?: number;
|
|
784
|
+
tuneAutoMatchPrecisionBuffer?: number;
|
|
785
|
+
tuneAutoMatchCountBuffer?: number;
|
|
786
|
+
tuneDeliveredShortlistRecallBuffer?: number;
|
|
673
787
|
concurrency?: number;
|
|
674
788
|
resumeRunId?: string;
|
|
675
789
|
onProgress?: (completed: number, total: number) => void;
|
package/src/calibrate.ts
CHANGED
|
@@ -330,6 +330,12 @@ export type CalibrationFailureReason =
|
|
|
330
330
|
| "insufficient_sample"
|
|
331
331
|
| "test_certification_failed";
|
|
332
332
|
|
|
333
|
+
export interface TuneGateSlack {
|
|
334
|
+
auto_match_precision_lower_bound_slack: number;
|
|
335
|
+
auto_match_count_slack: number;
|
|
336
|
+
delivered_shortlist_recall_slack: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
333
339
|
export interface CalibrationResult {
|
|
334
340
|
status: CalibrationStatus;
|
|
335
341
|
failed_reason?: CalibrationFailureReason;
|
|
@@ -337,6 +343,7 @@ export interface CalibrationResult {
|
|
|
337
343
|
selected_thresholds?: SelectedThresholds;
|
|
338
344
|
tune_metrics?: CalibrationMetrics;
|
|
339
345
|
test_metrics?: CalibrationTestMetrics;
|
|
346
|
+
tune_gate_slack?: TuneGateSlack;
|
|
340
347
|
}
|
|
341
348
|
|
|
342
349
|
export interface RunCalibrationOptions {
|
|
@@ -357,6 +364,12 @@ export interface RunCalibrationOptions {
|
|
|
357
364
|
minDeliveredShortlistRecallAtK?: number;
|
|
358
365
|
/** Default: 30 */
|
|
359
366
|
minAutoMatchCount?: number;
|
|
367
|
+
/** Tune-only selection buffer for Wilson auto-match precision lower bound. Default: 0.03 */
|
|
368
|
+
tuneAutoMatchPrecisionBuffer?: number;
|
|
369
|
+
/** Tune-only selection buffer for minimum auto-match count. Default: 3 */
|
|
370
|
+
tuneAutoMatchCountBuffer?: number;
|
|
371
|
+
/** Tune-only selection buffer for delivered shortlist recall. Default: 0.02 */
|
|
372
|
+
tuneDeliveredShortlistRecallBuffer?: number;
|
|
360
373
|
candidateLimit: number;
|
|
361
374
|
/** Default: 4 */
|
|
362
375
|
concurrency?: number;
|
|
@@ -465,6 +478,97 @@ export function wilsonLowerBound(successes: number, total: number): number {
|
|
|
465
478
|
return Math.max(0, (centre - adjustment) / denominator);
|
|
466
479
|
}
|
|
467
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Computes the maximum attainable 95% Wilson lower bound on auto-match precision
|
|
483
|
+
* for the tune split subject to minAutoMatchCount and the number of matched tune cases.
|
|
484
|
+
* Under perfect ranking/classification (zero false positives), at most all true matched
|
|
485
|
+
* tune cases can be auto-matched.
|
|
486
|
+
*/
|
|
487
|
+
export function computeMaxAttainablePrecisionLowerBound(
|
|
488
|
+
cases: DecisionCase[],
|
|
489
|
+
minAutoMatchCount: number,
|
|
490
|
+
): { tuneMatchedCount: number; maxAttainablePrecision: number } {
|
|
491
|
+
const tuneMatchedCount = cases.filter(
|
|
492
|
+
(c) => c.split === "tune" && c.expected_outcome === "matched",
|
|
493
|
+
).length;
|
|
494
|
+
const effectiveTrials = Math.max(tuneMatchedCount, minAutoMatchCount);
|
|
495
|
+
const maxAttainablePrecision = wilsonLowerBound(tuneMatchedCount, effectiveTrials);
|
|
496
|
+
return { tuneMatchedCount, maxAttainablePrecision };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Asserts that the requested auto-match precision and count gates are mathematically
|
|
501
|
+
* attainable on the given dataset's tune split before inference begins.
|
|
502
|
+
*/
|
|
503
|
+
export function assertCalibrationFeasibility(
|
|
504
|
+
cases: DecisionCase[],
|
|
505
|
+
gates: {
|
|
506
|
+
minAutoMatchPrecision: number;
|
|
507
|
+
minAutoMatchCount: number;
|
|
508
|
+
minDeliveredShortlistRecallAtK?: number;
|
|
509
|
+
tuneAutoMatchPrecisionBuffer?: number;
|
|
510
|
+
tuneAutoMatchCountBuffer?: number;
|
|
511
|
+
tuneDeliveredShortlistRecallBuffer?: number;
|
|
512
|
+
},
|
|
513
|
+
): void {
|
|
514
|
+
const precisionBuffer = gates.tuneAutoMatchPrecisionBuffer ?? 0.03;
|
|
515
|
+
const countBuffer = gates.tuneAutoMatchCountBuffer ?? 3;
|
|
516
|
+
const recallBuffer = gates.tuneDeliveredShortlistRecallBuffer ?? 0.02;
|
|
517
|
+
|
|
518
|
+
if (precisionBuffer < 0 || !Number.isFinite(precisionBuffer)) {
|
|
519
|
+
throw new Error("tune_auto_match_precision_buffer must be a non-negative number");
|
|
520
|
+
}
|
|
521
|
+
if (countBuffer < 0 || !Number.isInteger(countBuffer)) {
|
|
522
|
+
throw new Error("tune_auto_match_count_buffer must be a non-negative integer");
|
|
523
|
+
}
|
|
524
|
+
if (recallBuffer < 0 || !Number.isFinite(recallBuffer)) {
|
|
525
|
+
throw new Error("tune_delivered_shortlist_recall_buffer must be a non-negative number");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const effectivePrecision = gates.minAutoMatchPrecision + precisionBuffer;
|
|
529
|
+
if (effectivePrecision > 1.0) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`Requested calibration gates with tune buffers are mathematically impossible: ` +
|
|
532
|
+
`effective min_auto_match_precision (${effectivePrecision.toFixed(2)}) exceeds 1.0 ` +
|
|
533
|
+
`(min_auto_match_precision=${gates.minAutoMatchPrecision}, tune_buffer=${precisionBuffer}). ` +
|
|
534
|
+
`Lower --min-auto-match-precision or --tune-auto-match-precision-buffer.`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const baseDeliveredRecall = gates.minDeliveredShortlistRecallAtK ?? 0.95;
|
|
539
|
+
const effectiveDeliveredRecall = baseDeliveredRecall + recallBuffer;
|
|
540
|
+
if (effectiveDeliveredRecall > 1.0) {
|
|
541
|
+
throw new Error(
|
|
542
|
+
`Requested calibration gates with tune buffers are mathematically impossible: ` +
|
|
543
|
+
`effective min_delivered_shortlist_recall_at_k (${effectiveDeliveredRecall.toFixed(2)}) exceeds 1.0 ` +
|
|
544
|
+
`(min_delivered_shortlist_recall_at_k=${baseDeliveredRecall}, tune_buffer=${recallBuffer}). ` +
|
|
545
|
+
`Lower --min-delivered-shortlist-recall-at-k or --tune-delivered-shortlist-recall-buffer.`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const effectiveCount = gates.minAutoMatchCount + countBuffer;
|
|
550
|
+
const { tuneMatchedCount, maxAttainablePrecision } =
|
|
551
|
+
computeMaxAttainablePrecisionLowerBound(cases, effectiveCount);
|
|
552
|
+
if (effectiveCount > tuneMatchedCount) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Requested calibration gates are mathematically unattainable on this dataset: ` +
|
|
555
|
+
`effective min_auto_match_count (${effectiveCount}) exceeds the number of matched cases in the ` +
|
|
556
|
+
`tune split (${tuneMatchedCount}) (min_auto_match_count=${gates.minAutoMatchCount}, tune_buffer=${countBuffer}). ` +
|
|
557
|
+
`Lower --min-auto-match-count or --tune-auto-match-count-buffer, or supply a dataset with more matched tune cases.`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (maxAttainablePrecision < effectivePrecision) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`Requested calibration gates are mathematically unattainable on this dataset: ` +
|
|
563
|
+
`min_auto_match_precision=${gates.minAutoMatchPrecision} requires more evidence than the ` +
|
|
564
|
+
`tune split provides (${tuneMatchedCount} matched cases, min_auto_match_count=${gates.minAutoMatchCount}, effective min_auto_match_count=${effectiveCount}). ` +
|
|
565
|
+
`The maximum attainable 95% Wilson lower bound under perfect classification is ` +
|
|
566
|
+
`${maxAttainablePrecision.toFixed(4)}. ` +
|
|
567
|
+
`Lower --min-auto-match-precision or supply a larger dataset.`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
468
572
|
function computeTestMetrics(
|
|
469
573
|
observations: QueryObservation[],
|
|
470
574
|
thresholds: SelectedThresholds,
|
|
@@ -799,10 +903,13 @@ export async function runCalibration(opts: RunCalibrationOptions): Promise<Calib
|
|
|
799
903
|
getCandidates,
|
|
800
904
|
getRankedCandidates,
|
|
801
905
|
reranker,
|
|
802
|
-
minAutoMatchPrecision = 0.
|
|
906
|
+
minAutoMatchPrecision = 0.75,
|
|
803
907
|
minRetrievalRecallAtK = 0.95,
|
|
804
908
|
minDeliveredShortlistRecallAtK = minRetrievalRecallAtK,
|
|
805
|
-
minAutoMatchCount =
|
|
909
|
+
minAutoMatchCount = 15,
|
|
910
|
+
tuneAutoMatchPrecisionBuffer = 0.03,
|
|
911
|
+
tuneAutoMatchCountBuffer = 3,
|
|
912
|
+
tuneDeliveredShortlistRecallBuffer = 0.02,
|
|
806
913
|
candidateLimit,
|
|
807
914
|
concurrency = 4,
|
|
808
915
|
initialObservations,
|
|
@@ -904,9 +1011,16 @@ export async function runCalibration(opts: RunCalibrationOptions): Promise<Calib
|
|
|
904
1011
|
minDeliveredShortlistRecallAtK,
|
|
905
1012
|
minAutoMatchCount,
|
|
906
1013
|
};
|
|
1014
|
+
const tuneGates = {
|
|
1015
|
+
minAutoMatchPrecision: minAutoMatchPrecision + tuneAutoMatchPrecisionBuffer,
|
|
1016
|
+
minRetrievalRecallAtK,
|
|
1017
|
+
minDeliveredShortlistRecallAtK:
|
|
1018
|
+
minDeliveredShortlistRecallAtK + tuneDeliveredShortlistRecallBuffer,
|
|
1019
|
+
minAutoMatchCount: minAutoMatchCount + tuneAutoMatchCountBuffer,
|
|
1020
|
+
};
|
|
907
1021
|
const selection = selectThresholds(
|
|
908
1022
|
tuneObs,
|
|
909
|
-
|
|
1023
|
+
tuneGates,
|
|
910
1024
|
candidateLimit,
|
|
911
1025
|
);
|
|
912
1026
|
|
|
@@ -919,8 +1033,16 @@ export async function runCalibration(opts: RunCalibrationOptions): Promise<Calib
|
|
|
919
1033
|
}
|
|
920
1034
|
const selected = selection.selected;
|
|
921
1035
|
|
|
922
|
-
// --- Step 3: Report tune metrics ---
|
|
1036
|
+
// --- Step 3: Report tune metrics & explicit tune gate slack ---
|
|
923
1037
|
const tune_metrics = computeMetrics(tuneObs, selected, candidateLimit);
|
|
1038
|
+
const tune_gate_slack: TuneGateSlack = {
|
|
1039
|
+
auto_match_precision_lower_bound_slack:
|
|
1040
|
+
tune_metrics.auto_match_precision_lower_bound - minAutoMatchPrecision,
|
|
1041
|
+
auto_match_count_slack:
|
|
1042
|
+
tune_metrics.auto_match_count - minAutoMatchCount,
|
|
1043
|
+
delivered_shortlist_recall_slack:
|
|
1044
|
+
tune_metrics.delivered_shortlist_recall_at_k - minDeliveredShortlistRecallAtK,
|
|
1045
|
+
};
|
|
924
1046
|
|
|
925
1047
|
// --- Step 4: Evaluate untouched test split ---
|
|
926
1048
|
const test_metrics = computeTestMetrics(testObs, selected, candidateLimit);
|
|
@@ -932,10 +1054,18 @@ export async function runCalibration(opts: RunCalibrationOptions): Promise<Calib
|
|
|
932
1054
|
selected_thresholds: selected,
|
|
933
1055
|
tune_metrics,
|
|
934
1056
|
test_metrics,
|
|
1057
|
+
tune_gate_slack,
|
|
935
1058
|
};
|
|
936
1059
|
}
|
|
937
1060
|
|
|
938
|
-
return {
|
|
1061
|
+
return {
|
|
1062
|
+
status: "completed",
|
|
1063
|
+
observations,
|
|
1064
|
+
selected_thresholds: selected,
|
|
1065
|
+
tune_metrics,
|
|
1066
|
+
test_metrics,
|
|
1067
|
+
tune_gate_slack,
|
|
1068
|
+
};
|
|
939
1069
|
}
|
|
940
1070
|
|
|
941
1071
|
// ---------------------------------------------------------------------------
|
|
@@ -966,6 +1096,10 @@ export interface CalibrationRunRecord {
|
|
|
966
1096
|
min_auto_match_count?: number;
|
|
967
1097
|
min_delivered_shortlist_recall_at_k?: number;
|
|
968
1098
|
min_shortlist_recall_at_5: number;
|
|
1099
|
+
tune_auto_match_precision_buffer?: number;
|
|
1100
|
+
tune_auto_match_count_buffer?: number;
|
|
1101
|
+
tune_delivered_shortlist_recall_buffer?: number;
|
|
1102
|
+
tune_gate_slack?: TuneGateSlack;
|
|
969
1103
|
failed_reason?: CalibrationFailureReason;
|
|
970
1104
|
selected_thresholds?: SelectedThresholds;
|
|
971
1105
|
tune_metrics?: CalibrationMetrics;
|
|
@@ -990,6 +1124,9 @@ export interface CalibrationRunSummary {
|
|
|
990
1124
|
min_auto_match_count: number;
|
|
991
1125
|
min_delivered_shortlist_recall_at_k: number;
|
|
992
1126
|
min_shortlist_recall_at_5: number;
|
|
1127
|
+
tune_auto_match_precision_buffer: number;
|
|
1128
|
+
tune_auto_match_count_buffer: number;
|
|
1129
|
+
tune_delivered_shortlist_recall_buffer: number;
|
|
993
1130
|
failed_reason?: CalibrationFailureReason;
|
|
994
1131
|
}
|
|
995
1132
|
|
|
@@ -1053,7 +1190,11 @@ export function openCalibrateDb(stateDir: string): Database {
|
|
|
1053
1190
|
dataset_provenance TEXT NOT NULL DEFAULT '{}',
|
|
1054
1191
|
human_labelled_case_count INTEGER NOT NULL DEFAULT 0,
|
|
1055
1192
|
imported_labelled_case_count INTEGER NOT NULL DEFAULT 0,
|
|
1056
|
-
recall_settings TEXT NOT NULL DEFAULT '{}'
|
|
1193
|
+
recall_settings TEXT NOT NULL DEFAULT '{}',
|
|
1194
|
+
tune_auto_match_precision_buffer REAL NOT NULL DEFAULT 0.03,
|
|
1195
|
+
tune_auto_match_count_buffer INTEGER NOT NULL DEFAULT 3,
|
|
1196
|
+
tune_delivered_shortlist_recall_buffer REAL NOT NULL DEFAULT 0.02,
|
|
1197
|
+
tune_gate_slack TEXT
|
|
1057
1198
|
)`);
|
|
1058
1199
|
db.run(
|
|
1059
1200
|
`INSERT INTO calibration_runs_new (${selectCols}) SELECT ${selectCols} FROM calibration_runs`,
|
|
@@ -1106,6 +1247,18 @@ export function openCalibrateDb(stateDir: string): Database {
|
|
|
1106
1247
|
if (!columns.some((column) => column.name === "recall_settings")) {
|
|
1107
1248
|
db.run("ALTER TABLE calibration_runs ADD COLUMN recall_settings TEXT NOT NULL DEFAULT '{}'");
|
|
1108
1249
|
}
|
|
1250
|
+
if (!columns.some((column) => column.name === "tune_auto_match_precision_buffer")) {
|
|
1251
|
+
db.run("ALTER TABLE calibration_runs ADD COLUMN tune_auto_match_precision_buffer REAL NOT NULL DEFAULT 0.03");
|
|
1252
|
+
}
|
|
1253
|
+
if (!columns.some((column) => column.name === "tune_auto_match_count_buffer")) {
|
|
1254
|
+
db.run("ALTER TABLE calibration_runs ADD COLUMN tune_auto_match_count_buffer INTEGER NOT NULL DEFAULT 3");
|
|
1255
|
+
}
|
|
1256
|
+
if (!columns.some((column) => column.name === "tune_delivered_shortlist_recall_buffer")) {
|
|
1257
|
+
db.run("ALTER TABLE calibration_runs ADD COLUMN tune_delivered_shortlist_recall_buffer REAL NOT NULL DEFAULT 0.02");
|
|
1258
|
+
}
|
|
1259
|
+
if (!columns.some((column) => column.name === "tune_gate_slack")) {
|
|
1260
|
+
db.run("ALTER TABLE calibration_runs ADD COLUMN tune_gate_slack TEXT");
|
|
1261
|
+
}
|
|
1109
1262
|
|
|
1110
1263
|
db.run(`CREATE TABLE IF NOT EXISTS calibration_observations (
|
|
1111
1264
|
run_id TEXT NOT NULL,
|
|
@@ -1135,6 +1288,9 @@ export interface CreateInitialCalibrationRunOptions {
|
|
|
1135
1288
|
min_auto_match_count?: number;
|
|
1136
1289
|
min_delivered_shortlist_recall_at_k?: number;
|
|
1137
1290
|
min_shortlist_recall_at_5: number;
|
|
1291
|
+
tune_auto_match_precision_buffer?: number;
|
|
1292
|
+
tune_auto_match_count_buffer?: number;
|
|
1293
|
+
tune_delivered_shortlist_recall_buffer?: number;
|
|
1138
1294
|
dataset_provenance?: DatasetProvenanceSummary;
|
|
1139
1295
|
recall_settings?: {
|
|
1140
1296
|
k_lexical: number;
|
|
@@ -1161,8 +1317,10 @@ export function createInitialCalibrationRun(
|
|
|
1161
1317
|
min_delivered_shortlist_recall_at_k, min_shortlist_recall_at_5, failed_reason,
|
|
1162
1318
|
selected_thresholds, tune_metrics, test_metrics, observations,
|
|
1163
1319
|
dataset_provenance, human_labelled_case_count, imported_labelled_case_count,
|
|
1164
|
-
recall_settings
|
|
1165
|
-
|
|
1320
|
+
recall_settings,
|
|
1321
|
+
tune_auto_match_precision_buffer, tune_auto_match_count_buffer,
|
|
1322
|
+
tune_delivered_shortlist_recall_buffer, tune_gate_slack
|
|
1323
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1166
1324
|
[
|
|
1167
1325
|
run.run_id,
|
|
1168
1326
|
run.created_at,
|
|
@@ -1186,6 +1344,10 @@ export function createInitialCalibrationRun(
|
|
|
1186
1344
|
run.dataset_provenance?.human_labelled_case_count ?? 0,
|
|
1187
1345
|
run.dataset_provenance?.imported_labelled_case_count ?? 0,
|
|
1188
1346
|
JSON.stringify(run.recall_settings ?? {}),
|
|
1347
|
+
run.tune_auto_match_precision_buffer ?? 0.03,
|
|
1348
|
+
run.tune_auto_match_count_buffer ?? 3,
|
|
1349
|
+
run.tune_delivered_shortlist_recall_buffer ?? 0.02,
|
|
1350
|
+
null,
|
|
1189
1351
|
],
|
|
1190
1352
|
);
|
|
1191
1353
|
}
|
|
@@ -1251,6 +1413,7 @@ export interface FinalizeCalibrationRunOptions {
|
|
|
1251
1413
|
selected_thresholds?: SelectedThresholds;
|
|
1252
1414
|
tune_metrics?: CalibrationMetrics;
|
|
1253
1415
|
test_metrics?: CalibrationTestMetrics;
|
|
1416
|
+
tune_gate_slack?: TuneGateSlack;
|
|
1254
1417
|
observations: QueryObservation[];
|
|
1255
1418
|
}
|
|
1256
1419
|
|
|
@@ -1266,7 +1429,8 @@ export function finalizeCalibrationRun(
|
|
|
1266
1429
|
selected_thresholds = ?,
|
|
1267
1430
|
tune_metrics = ?,
|
|
1268
1431
|
test_metrics = ?,
|
|
1269
|
-
observations =
|
|
1432
|
+
observations = ?,
|
|
1433
|
+
tune_gate_slack = ?
|
|
1270
1434
|
WHERE run_id = ?`,
|
|
1271
1435
|
[
|
|
1272
1436
|
run.status,
|
|
@@ -1275,6 +1439,7 @@ export function finalizeCalibrationRun(
|
|
|
1275
1439
|
run.tune_metrics != null ? JSON.stringify(run.tune_metrics) : null,
|
|
1276
1440
|
run.test_metrics != null ? JSON.stringify(run.test_metrics) : null,
|
|
1277
1441
|
JSON.stringify(run.observations),
|
|
1442
|
+
run.tune_gate_slack != null ? JSON.stringify(run.tune_gate_slack) : null,
|
|
1278
1443
|
run.run_id,
|
|
1279
1444
|
],
|
|
1280
1445
|
);
|
|
@@ -1296,8 +1461,10 @@ export function insertCalibrationRun(db: Database, run: CalibrationRunRecord): v
|
|
|
1296
1461
|
min_delivered_shortlist_recall_at_k, min_shortlist_recall_at_5, failed_reason,
|
|
1297
1462
|
selected_thresholds, tune_metrics, test_metrics, observations,
|
|
1298
1463
|
dataset_provenance, human_labelled_case_count, imported_labelled_case_count,
|
|
1299
|
-
recall_settings
|
|
1300
|
-
|
|
1464
|
+
recall_settings,
|
|
1465
|
+
tune_auto_match_precision_buffer, tune_auto_match_count_buffer,
|
|
1466
|
+
tune_delivered_shortlist_recall_buffer, tune_gate_slack
|
|
1467
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1301
1468
|
[
|
|
1302
1469
|
run.run_id,
|
|
1303
1470
|
run.created_at,
|
|
@@ -1321,6 +1488,10 @@ export function insertCalibrationRun(db: Database, run: CalibrationRunRecord): v
|
|
|
1321
1488
|
run.dataset_provenance?.human_labelled_case_count ?? 0,
|
|
1322
1489
|
run.dataset_provenance?.imported_labelled_case_count ?? 0,
|
|
1323
1490
|
JSON.stringify(run.recall_settings ?? {}),
|
|
1491
|
+
run.tune_auto_match_precision_buffer ?? 0.03,
|
|
1492
|
+
run.tune_auto_match_count_buffer ?? 3,
|
|
1493
|
+
run.tune_delivered_shortlist_recall_buffer ?? 0.02,
|
|
1494
|
+
run.tune_gate_slack != null ? JSON.stringify(run.tune_gate_slack) : null,
|
|
1324
1495
|
],
|
|
1325
1496
|
);
|
|
1326
1497
|
if (run.observations && run.observations.length > 0) {
|
|
@@ -1355,6 +1526,10 @@ interface RawCalibrationRow {
|
|
|
1355
1526
|
human_labelled_case_count: number;
|
|
1356
1527
|
imported_labelled_case_count: number;
|
|
1357
1528
|
recall_settings: string;
|
|
1529
|
+
tune_auto_match_precision_buffer: number | null;
|
|
1530
|
+
tune_auto_match_count_buffer: number | null;
|
|
1531
|
+
tune_delivered_shortlist_recall_buffer: number | null;
|
|
1532
|
+
tune_gate_slack: string | null;
|
|
1358
1533
|
}
|
|
1359
1534
|
|
|
1360
1535
|
function parseMetrics(json: string): CalibrationMetrics {
|
|
@@ -1403,6 +1578,10 @@ function rowToRecord(row: RawCalibrationRow): CalibrationRunRecord {
|
|
|
1403
1578
|
min_auto_match_count: row.min_auto_match_count,
|
|
1404
1579
|
min_delivered_shortlist_recall_at_k: row.min_delivered_shortlist_recall_at_k,
|
|
1405
1580
|
min_shortlist_recall_at_5: row.min_shortlist_recall_at_5,
|
|
1581
|
+
tune_auto_match_precision_buffer: row.tune_auto_match_precision_buffer ?? 0.03,
|
|
1582
|
+
tune_auto_match_count_buffer: row.tune_auto_match_count_buffer ?? 3,
|
|
1583
|
+
tune_delivered_shortlist_recall_buffer: row.tune_delivered_shortlist_recall_buffer ?? 0.02,
|
|
1584
|
+
tune_gate_slack: row.tune_gate_slack != null ? JSON.parse(row.tune_gate_slack) : undefined,
|
|
1406
1585
|
failed_reason: row.failed_reason as CalibrationFailureReason | null ?? undefined,
|
|
1407
1586
|
selected_thresholds: row.selected_thresholds != null
|
|
1408
1587
|
? (JSON.parse(row.selected_thresholds) as SelectedThresholds)
|
|
@@ -1445,7 +1624,9 @@ export function listCalibrationRuns(db: Database): CalibrationRunSummary[] {
|
|
|
1445
1624
|
reranker_fingerprint, embedding_fingerprint, corpus_fingerprint, dataset_hash,
|
|
1446
1625
|
candidate_limit,
|
|
1447
1626
|
attempt_count, min_auto_match_precision, min_auto_match_count,
|
|
1448
|
-
min_delivered_shortlist_recall_at_k, min_shortlist_recall_at_5,
|
|
1627
|
+
min_delivered_shortlist_recall_at_k, min_shortlist_recall_at_5,
|
|
1628
|
+
tune_auto_match_precision_buffer, tune_auto_match_count_buffer,
|
|
1629
|
+
tune_delivered_shortlist_recall_buffer, failed_reason,
|
|
1449
1630
|
human_labelled_case_count, imported_labelled_case_count
|
|
1450
1631
|
FROM calibration_runs ORDER BY created_at DESC`,
|
|
1451
1632
|
)
|
package/src/cli.ts
CHANGED
|
@@ -475,6 +475,9 @@ async function handleCalibrateCommand(
|
|
|
475
475
|
let minRetrievalRecallAtK: number | undefined;
|
|
476
476
|
let minDeliveredShortlistRecallAtK: number | undefined;
|
|
477
477
|
let minAutoMatchCount: number | undefined;
|
|
478
|
+
let tuneAutoMatchPrecisionBuffer: number | undefined;
|
|
479
|
+
let tuneAutoMatchCountBuffer: number | undefined;
|
|
480
|
+
let tuneDeliveredShortlistRecallBuffer: number | undefined;
|
|
478
481
|
let concurrency: number | undefined;
|
|
479
482
|
let resumeRunId: string | undefined;
|
|
480
483
|
let timing = false;
|
|
@@ -500,6 +503,15 @@ async function handleCalibrateCommand(
|
|
|
500
503
|
if (!Number.isInteger(minAutoMatchCount) || minAutoMatchCount < 1) {
|
|
501
504
|
throw new Error("--min-auto-match-count must be a positive integer");
|
|
502
505
|
}
|
|
506
|
+
} else if (option === "--tune-auto-match-precision-buffer") {
|
|
507
|
+
tuneAutoMatchPrecisionBuffer = readNumber(option, args[++i]);
|
|
508
|
+
} else if (option === "--tune-auto-match-count-buffer") {
|
|
509
|
+
tuneAutoMatchCountBuffer = readNumber(option, args[++i]);
|
|
510
|
+
if (!Number.isInteger(tuneAutoMatchCountBuffer) || tuneAutoMatchCountBuffer < 0) {
|
|
511
|
+
throw new Error("--tune-auto-match-count-buffer must be a non-negative integer");
|
|
512
|
+
}
|
|
513
|
+
} else if (option === "--tune-delivered-shortlist-recall-buffer") {
|
|
514
|
+
tuneDeliveredShortlistRecallBuffer = readNumber(option, args[++i]);
|
|
503
515
|
} else if (option === "--concurrency") {
|
|
504
516
|
const raw = args[++i];
|
|
505
517
|
if (raw === undefined) throw new Error("--concurrency requires a value");
|
|
@@ -513,6 +525,8 @@ async function handleCalibrateCommand(
|
|
|
513
525
|
if (!resumeRunId) throw new Error("--resume requires a run_id value");
|
|
514
526
|
} else if (option === "--timing") {
|
|
515
527
|
timing = true;
|
|
528
|
+
} else if (option === "--json") {
|
|
529
|
+
// Global flag accepted in the documented subcommand position
|
|
516
530
|
} else {
|
|
517
531
|
throw new Error(`unknown calibrate run option: ${option}`);
|
|
518
532
|
}
|
|
@@ -521,6 +535,8 @@ async function handleCalibrateCommand(
|
|
|
521
535
|
["--min-auto-match-precision", minAutoMatchPrecision],
|
|
522
536
|
["--min-retrieval-recall-at-k", minRetrievalRecallAtK],
|
|
523
537
|
["--min-delivered-shortlist-recall-at-k", minDeliveredShortlistRecallAtK],
|
|
538
|
+
["--tune-auto-match-precision-buffer", tuneAutoMatchPrecisionBuffer],
|
|
539
|
+
["--tune-delivered-shortlist-recall-buffer", tuneDeliveredShortlistRecallBuffer],
|
|
524
540
|
] as const) {
|
|
525
541
|
if (value !== undefined && (value < 0 || value > 1)) {
|
|
526
542
|
throw new Error(`${flag} must be between 0 and 1`);
|
|
@@ -532,6 +548,9 @@ async function handleCalibrateCommand(
|
|
|
532
548
|
minRetrievalRecallAtK,
|
|
533
549
|
minDeliveredShortlistRecallAtK,
|
|
534
550
|
minAutoMatchCount,
|
|
551
|
+
tuneAutoMatchPrecisionBuffer,
|
|
552
|
+
tuneAutoMatchCountBuffer,
|
|
553
|
+
tuneDeliveredShortlistRecallBuffer,
|
|
535
554
|
concurrency,
|
|
536
555
|
resumeRunId,
|
|
537
556
|
timing,
|
|
@@ -703,6 +722,12 @@ Setup:
|
|
|
703
722
|
|
|
704
723
|
Calibration:
|
|
705
724
|
skillmux calibrate run [--dataset <path>] [--concurrency <n>] [--resume <run_id>]
|
|
725
|
+
[--min-auto-match-precision <0..1>] [--min-auto-match-count <n>]
|
|
726
|
+
[--min-retrieval-recall-at-k <0..1>]
|
|
727
|
+
[--min-delivered-shortlist-recall-at-k <0..1>]
|
|
728
|
+
[--tune-auto-match-precision-buffer <0..1>]
|
|
729
|
+
[--tune-auto-match-count-buffer <n>]
|
|
730
|
+
[--tune-delivered-shortlist-recall-buffer <0..1>]
|
|
706
731
|
[--timing] [--json]
|
|
707
732
|
skillmux calibrate <list|show|apply|generate-dataset>
|
|
708
733
|
|