@klhapp/skillmux 1.0.1 → 1.2.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/src/adapters.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { Database } from "bun:sqlite";
1
+ import { createHash } from "node:crypto";
2
2
  import { join } from "node:path";
3
- import { applyCalibrationRun, getCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, type CalibrationResult } from "./calibrate";
3
+ import { applyCalibrationRun, getCalibrationRun, insertCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, summarizeDatasetProvenance, type CalibrationResult } from "./calibrate";
4
4
  import { createClients } from "./clients";
5
- import { DEFAULT_CONFIG_PATH, expandHome, loadConfig } from "./config";
5
+ import { DEFAULT_CONFIG_PATH, embeddingFingerprint, expandHome, loadConfig, rerankerFingerprint } from "./config";
6
+ import { openIndex } from "./db";
6
7
  import { CliError } from "./output";
7
8
  import {
8
9
  computeHash,
@@ -18,7 +19,7 @@ import {
18
19
  type SetConfigResult,
19
20
  } from "./config-service";
20
21
  import type { ResolvedTarget } from "./context";
21
- import { resolveSkill } from "./router-core";
22
+ import { configure, retrieveAndRerank } from "./router-core";
22
23
  import type { Config } from "./types";
23
24
 
24
25
  export interface Capabilities {
@@ -43,7 +44,13 @@ export interface TargetAdapter {
43
44
  configDiff(): Promise<{ diff: Record<string, { prior: unknown; resulting: unknown }> }>;
44
45
  configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult>;
45
46
  configStatus(): Promise<ConfigStatusResponse>;
46
- calibrateRun(opts?: { datasetPath?: string }): Promise<{ run_id?: string; result?: CalibrationResult }>;
47
+ calibrateRun(opts?: {
48
+ datasetPath?: string;
49
+ minAutoMatchPrecision?: number;
50
+ minRetrievalRecallAtK?: number;
51
+ minDeliveredShortlistRecallAtK?: number;
52
+ minAutoMatchCount?: number;
53
+ }): Promise<{ run_id?: string; result?: CalibrationResult }>;
47
54
  calibrateList(): Promise<any[]>;
48
55
  calibrateShow(runId: string): Promise<any>;
49
56
  calibrateApply(runId: string): Promise<any>;
@@ -123,26 +130,89 @@ export class LocalAdapter implements TargetAdapter {
123
130
  return getLocalConfigStatus(this.configPath);
124
131
  }
125
132
 
126
- async calibrateRun(opts?: { datasetPath?: string }): Promise<{ run_id?: string; result?: CalibrationResult }> {
133
+ async calibrateRun(opts?: {
134
+ datasetPath?: string;
135
+ minAutoMatchPrecision?: number;
136
+ minRetrievalRecallAtK?: number;
137
+ minDeliveredShortlistRecallAtK?: number;
138
+ minAutoMatchCount?: number;
139
+ }): Promise<{ run_id?: string; result?: CalibrationResult }> {
127
140
  const config = await loadConfig(this.configPath);
128
141
  const datasetFile = opts?.datasetPath ?? join(expandHome(config.state_dir), "queries.json");
129
- const cases = loadDecisionCasesFromFile(datasetFile);
142
+ const indexDb = openIndex(expandHome(config.state_dir));
143
+ let indexedSkills: Array<{ skill_id: string; content_sha256: string }>;
144
+ try {
145
+ indexedSkills = indexDb
146
+ .query("SELECT skill_id, content_sha256 FROM skills ORDER BY skill_id")
147
+ .all() as Array<{ skill_id: string; content_sha256: string }>;
148
+ } finally {
149
+ indexDb.close();
150
+ }
151
+ const cases = loadDecisionCasesFromFile(
152
+ datasetFile,
153
+ indexedSkills.map((skill) => skill.skill_id),
154
+ );
130
155
  const clients = createClients(config);
156
+ configure({ config, clients });
131
157
  const result = await runCalibration({
132
158
  cases,
133
- getCandidates: async (query: string) => {
134
- const res = await resolveSkill({ query, forceLexical: false });
135
- if (res.outcome === "matched") {
136
- return [{ skill_id: res.skill_id, text: `${res.title} ${res.body}` }];
159
+ getRankedCandidates: async (query: string) => {
160
+ const result = await retrieveAndRerank({ query, forceLexical: false });
161
+ if (result.retrieval !== "reranked") {
162
+ throw new Error(
163
+ "Calibration requires successful hybrid retrieval and reranking for every query.",
164
+ );
137
165
  }
138
- if (res.outcome === "ambiguous") {
139
- return res.candidates.map((c) => ({ skill_id: c.skill_id, text: `${c.title} ${c.description}` }));
140
- }
141
- return [];
166
+ return result.candidates.map((candidate) => ({
167
+ skill_id: candidate.skill_id,
168
+ score: candidate.score ?? 0,
169
+ }));
142
170
  },
143
171
  reranker: clients.rerank,
172
+ candidateLimit: config.thresholds.candidate_limit,
173
+ minAutoMatchPrecision: opts?.minAutoMatchPrecision,
174
+ minRetrievalRecallAtK: opts?.minRetrievalRecallAtK,
175
+ minDeliveredShortlistRecallAtK: opts?.minDeliveredShortlistRecallAtK,
176
+ minAutoMatchCount: opts?.minAutoMatchCount,
144
177
  });
145
- return { result };
178
+ const fingerprint = rerankerFingerprint(config);
179
+ if (!fingerprint) {
180
+ throw new Error("A configured remote reranker is required to record calibration.");
181
+ }
182
+ const datasetText = await Bun.file(datasetFile).text();
183
+ const corpusFingerprint =
184
+ "vault:" +
185
+ createHash("sha256").update(JSON.stringify(indexedSkills)).digest("hex");
186
+ const runId = `run_${crypto.randomUUID()}`;
187
+ const db = openCalibrateDb(expandHome(config.state_dir));
188
+ try {
189
+ insertCalibrationRun(db, {
190
+ run_id: runId,
191
+ created_at: new Date().toISOString(),
192
+ status: result.status,
193
+ reranker_fingerprint: fingerprint,
194
+ embedding_fingerprint: embeddingFingerprint(config),
195
+ corpus_fingerprint: corpusFingerprint,
196
+ dataset_hash: createHash("sha256").update(datasetText).digest("hex"),
197
+ dataset_provenance: summarizeDatasetProvenance(cases),
198
+ candidate_limit: config.thresholds.candidate_limit,
199
+ min_auto_match_precision: opts?.minAutoMatchPrecision ?? 0.99,
200
+ min_auto_match_count: opts?.minAutoMatchCount ?? 30,
201
+ min_delivered_shortlist_recall_at_k:
202
+ opts?.minDeliveredShortlistRecallAtK ??
203
+ opts?.minRetrievalRecallAtK ??
204
+ 0.95,
205
+ min_shortlist_recall_at_5: opts?.minRetrievalRecallAtK ?? 0.95,
206
+ failed_reason: result.failed_reason,
207
+ selected_thresholds: result.selected_thresholds,
208
+ tune_metrics: result.tune_metrics,
209
+ test_metrics: result.test_metrics,
210
+ observations: result.observations,
211
+ });
212
+ } finally {
213
+ db.close();
214
+ }
215
+ return { run_id: runId, result };
146
216
  }
147
217
 
148
218
  async calibrateList(): Promise<any[]> {
@@ -173,7 +243,9 @@ export class LocalAdapter implements TargetAdapter {
173
243
  try {
174
244
  const run = getCalibrationRun(db, runId);
175
245
  if (!run) throw new Error(`Calibration run "${runId}" not found`);
176
- await applyCalibrationRun(db, runId, expandHome(this.configPath), {});
246
+ await applyCalibrationRun(db, runId, expandHome(this.configPath), {
247
+ currentRerankerFingerprint: rerankerFingerprint(config),
248
+ });
177
249
  return { ok: true, run_id: runId };
178
250
  } finally {
179
251
  db.close();
@@ -345,36 +417,37 @@ export class RemoteAdapter implements TargetAdapter {
345
417
  return data.runtime;
346
418
  }
347
419
 
348
- async calibrateRun(opts?: { datasetPath?: string }): Promise<{ run_id?: string; result?: CalibrationResult }> {
349
- const { status, data } = await this.fetchJson("/admin/v1/calibrations", {
350
- method: "POST",
351
- headers: { "Content-Type": "application/json" },
352
- body: JSON.stringify({ dataset_path: opts?.datasetPath }),
353
- });
354
- if (status !== 202) {
355
- throw new Error(`Remote calibration start failed (${status}): ${data?.message || data}`);
356
- }
357
- return data;
420
+ async calibrateRun(opts?: {
421
+ datasetPath?: string;
422
+ minAutoMatchPrecision?: number;
423
+ minRetrievalRecallAtK?: number;
424
+ minDeliveredShortlistRecallAtK?: number;
425
+ minAutoMatchCount?: number;
426
+ }): Promise<{ run_id?: string; result?: CalibrationResult }> {
427
+ void opts;
428
+ throw this.remoteCalibrationNotImplemented();
358
429
  }
359
430
 
360
431
  async calibrateList(): Promise<any[]> {
361
- const { status, data } = await this.fetchJson("/admin/v1/calibrations");
362
- if (status !== 200) throw new Error(`Remote calibration list failed (${status}): ${data?.message || data}`);
363
- return data;
432
+ throw this.remoteCalibrationNotImplemented();
364
433
  }
365
434
 
366
435
  async calibrateShow(runId: string): Promise<any> {
367
- const { status, data } = await this.fetchJson(`/admin/v1/calibrations/${runId}`);
368
- if (status !== 200) throw new Error(`Remote calibration show failed (${status}): ${data?.message || data}`);
369
- return data;
436
+ void runId;
437
+ throw this.remoteCalibrationNotImplemented();
370
438
  }
371
439
 
372
440
  async calibrateApply(runId: string): Promise<any> {
373
- const { status, data } = await this.fetchJson(`/admin/v1/calibrations/${runId}/apply`, {
374
- method: "POST",
375
- });
376
- if (status !== 200) throw new Error(`Remote calibration apply failed (${status}): ${data?.message || data}`);
377
- return data;
441
+ void runId;
442
+ throw this.remoteCalibrationNotImplemented();
443
+ }
444
+
445
+ private remoteCalibrationNotImplemented(): CliError {
446
+ return new CliError(
447
+ `Remote calibration is not implemented for target ${this.serverUrl}; ` +
448
+ "run `skillmux calibrate` against a local target.",
449
+ 2,
450
+ );
378
451
  }
379
452
  }
380
453