@klhapp/skillmux 1.3.1 → 1.3.2
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 +12 -0
- package/package.json +1 -1
- package/src/adapters.ts +3 -4
- package/src/calibrate.ts +13 -0
- package/src/cli.ts +6 -0
- package/src/doctor.ts +79 -1
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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.3.2](https://github.com/klhq/skillmux/compare/v1.3.1...v1.3.2) (2026-07-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* **doctor:** flag uncalibrated or stale inference thresholds ([#99](https://github.com/klhq/skillmux/issues/99)) ([c8f684b](https://github.com/klhq/skillmux/commit/c8f684b6aedf6788def4cdc0fe49fff233569844))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Chores
|
|
17
|
+
|
|
18
|
+
* pin next release to 1.3.2 instead of 1.4.0 (take 2) ([#102](https://github.com/klhq/skillmux/issues/102)) ([bbbb7dd](https://github.com/klhq/skillmux/commit/bbbb7dd838d691735e797f39de7d4277b95f5187))
|
|
19
|
+
|
|
8
20
|
## [1.3.1](https://github.com/klhq/skillmux/compare/v1.3.0...v1.3.1) (2026-07-31)
|
|
9
21
|
|
|
10
22
|
|
package/package.json
CHANGED
package/src/adapters.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { applyCalibrationRun, getCalibrationRun, insertCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, summarizeDatasetProvenance, type CalibrationResult } from "./calibrate";
|
|
3
|
+
import { applyCalibrationRun, computeCorpusFingerprint, getCalibrationRun, insertCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, summarizeDatasetProvenance, type CalibrationResult } from "./calibrate";
|
|
4
4
|
import { createClients } from "./clients";
|
|
5
5
|
import { DEFAULT_CONFIG_PATH, embeddingFingerprint, expandHome, loadConfig, rerankerFingerprint } from "./config";
|
|
6
6
|
import { openIndex } from "./db";
|
|
@@ -141,10 +141,12 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
141
141
|
const datasetFile = opts?.datasetPath ?? join(expandHome(config.state_dir), "queries.json");
|
|
142
142
|
const indexDb = openIndex(expandHome(config.state_dir));
|
|
143
143
|
let indexedSkills: Array<{ skill_id: string; content_sha256: string }>;
|
|
144
|
+
let corpusFingerprint: string;
|
|
144
145
|
try {
|
|
145
146
|
indexedSkills = indexDb
|
|
146
147
|
.query("SELECT skill_id, content_sha256 FROM skills ORDER BY skill_id")
|
|
147
148
|
.all() as Array<{ skill_id: string; content_sha256: string }>;
|
|
149
|
+
corpusFingerprint = computeCorpusFingerprint(indexDb);
|
|
148
150
|
} finally {
|
|
149
151
|
indexDb.close();
|
|
150
152
|
}
|
|
@@ -180,9 +182,6 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
180
182
|
throw new Error("A configured remote reranker is required to record calibration.");
|
|
181
183
|
}
|
|
182
184
|
const datasetText = await Bun.file(datasetFile).text();
|
|
183
|
-
const corpusFingerprint =
|
|
184
|
-
"vault:" +
|
|
185
|
-
createHash("sha256").update(JSON.stringify(indexedSkills)).digest("hex");
|
|
186
185
|
const runId = `run_${crypto.randomUUID()}`;
|
|
187
186
|
const db = openCalibrateDb(expandHome(config.state_dir));
|
|
188
187
|
try {
|
package/src/calibrate.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { Database } from "bun:sqlite";
|
|
@@ -900,6 +901,18 @@ export interface CalibrationRunSummary {
|
|
|
900
901
|
failed_reason?: CalibrationFailureReason;
|
|
901
902
|
}
|
|
902
903
|
|
|
904
|
+
/**
|
|
905
|
+
* Fingerprint the indexed vault content so calibration runs can detect
|
|
906
|
+
* corpus drift. Must match how `insertCalibrationRun` computes it at
|
|
907
|
+
* `calibrate run` time.
|
|
908
|
+
*/
|
|
909
|
+
export function computeCorpusFingerprint(indexDb: Database): string {
|
|
910
|
+
const indexedSkills = indexDb
|
|
911
|
+
.query("SELECT skill_id, content_sha256 FROM skills ORDER BY skill_id")
|
|
912
|
+
.all() as Array<{ skill_id: string; content_sha256: string }>;
|
|
913
|
+
return "vault:" + createHash("sha256").update(JSON.stringify(indexedSkills)).digest("hex");
|
|
914
|
+
}
|
|
915
|
+
|
|
903
916
|
/**
|
|
904
917
|
* Open (or create) the calibration evidence database in `stateDir`.
|
|
905
918
|
* Uses a separate `calibrate.sqlite3` file — never the index.sqlite3 used
|
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
2
3
|
import { Database } from "bun:sqlite";
|
|
3
4
|
import { existsSync, lstatSync, mkdirSync, rmSync } from "node:fs";
|
|
4
5
|
import { hostname } from "node:os";
|
|
@@ -170,6 +171,11 @@ async function main() {
|
|
|
170
171
|
const commandArgs = rawArgv.slice(2);
|
|
171
172
|
|
|
172
173
|
const command = rawArgv[0];
|
|
174
|
+
if (command === "--version" || command === "-V") {
|
|
175
|
+
console.log(packageJson.version);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
if (!command || command === "--help" || command === "-h") {
|
|
174
180
|
printHelp();
|
|
175
181
|
return;
|
package/src/doctor.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { computeCorpusFingerprint, getCalibrationRun, openCalibrateDb } from "./calibrate";
|
|
2
3
|
import { createClients, RemoteInferenceError } from "./clients";
|
|
3
|
-
import { embeddingDimension, expandHome } from "./config";
|
|
4
|
+
import { embeddingDimension, embeddingFingerprint, expandHome, rerankerFingerprint } from "./config";
|
|
5
|
+
import { openIndex } from "./db";
|
|
4
6
|
import { parseManifest, resolveManifestPath, validateManifest } from "./manifest";
|
|
5
7
|
import { readSkillmuxMarker } from "./sync";
|
|
6
8
|
import type { Config } from "./types";
|
|
@@ -19,6 +21,78 @@ export interface DoctorReport {
|
|
|
19
21
|
checks: DoctorCheck[];
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Warn when live `inference.thresholds` didn't come from an applied
|
|
26
|
+
* `skillmux calibrate` run — e.g. hand-copied from an example config.
|
|
27
|
+
* Reranker score scales are not portable across models/adapters/corpora,
|
|
28
|
+
* so uncalibrated thresholds routinely make automatic matching unreachable
|
|
29
|
+
* without any visible error.
|
|
30
|
+
*/
|
|
31
|
+
function checkCalibration(config: Config): DoctorCheck {
|
|
32
|
+
const inference = config.inference;
|
|
33
|
+
if (inference.mode !== "remote") throw new Error("checkCalibration requires remote inference mode");
|
|
34
|
+
const runId = inference.calibration?.run_id;
|
|
35
|
+
if (!runId) {
|
|
36
|
+
return {
|
|
37
|
+
name: "calibration",
|
|
38
|
+
ok: false,
|
|
39
|
+
detail: "inference.thresholds are set but were never produced by `skillmux calibrate apply` — " +
|
|
40
|
+
"likely copied from an example config. Reranker scores are not portable across models, " +
|
|
41
|
+
"adapters, or corpora, so automatic matching may never trigger (or may trigger incorrectly). " +
|
|
42
|
+
"Run `skillmux calibrate`.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const calibrateDb = openCalibrateDb(expandHome(config.state_dir));
|
|
47
|
+
let run;
|
|
48
|
+
try {
|
|
49
|
+
run = getCalibrationRun(calibrateDb, runId);
|
|
50
|
+
} finally {
|
|
51
|
+
calibrateDb.close();
|
|
52
|
+
}
|
|
53
|
+
if (!run) {
|
|
54
|
+
return {
|
|
55
|
+
name: "calibration",
|
|
56
|
+
ok: false,
|
|
57
|
+
detail: `inference.calibration.run_id "${runId}" was not found in the local calibration ` +
|
|
58
|
+
"evidence store (state_dir may differ from where it was calibrated). Recalibrate.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (run.status !== "completed") {
|
|
62
|
+
return {
|
|
63
|
+
name: "calibration",
|
|
64
|
+
ok: false,
|
|
65
|
+
detail: `inference.calibration.run_id "${runId}" has status "${run.status}" and should never ` +
|
|
66
|
+
"have been applied. Recalibrate.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const indexDb = openIndex(expandHome(config.state_dir));
|
|
71
|
+
let currentCorpusFingerprint: string;
|
|
72
|
+
try {
|
|
73
|
+
currentCorpusFingerprint = computeCorpusFingerprint(indexDb);
|
|
74
|
+
} finally {
|
|
75
|
+
indexDb.close();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stale = [
|
|
79
|
+
rerankerFingerprint(config) !== run.reranker_fingerprint ? "reranker" : null,
|
|
80
|
+
embeddingFingerprint(config) !== run.embedding_fingerprint ? "embedding" : null,
|
|
81
|
+
currentCorpusFingerprint !== run.corpus_fingerprint ? "vault contents" : null,
|
|
82
|
+
].filter((part): part is string => part !== null);
|
|
83
|
+
|
|
84
|
+
if (stale.length > 0) {
|
|
85
|
+
return {
|
|
86
|
+
name: "calibration",
|
|
87
|
+
ok: false,
|
|
88
|
+
detail: `applied calibration run "${runId}" is stale — ${stale.join(", ")} changed since it was ` +
|
|
89
|
+
"calibrated. Recalibrate.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { name: "calibration", ok: true, detail: `thresholds from applied calibration run "${runId}"` };
|
|
94
|
+
}
|
|
95
|
+
|
|
22
96
|
export async function diagnose(config: Config): Promise<DoctorReport> {
|
|
23
97
|
const checks: DoctorCheck[] = [];
|
|
24
98
|
checks.push({ name: "vault", ok: existsSync(expandHome(config.vault_path)), detail: expandHome(config.vault_path) });
|
|
@@ -129,6 +203,10 @@ export async function diagnose(config: Config): Promise<DoctorReport> {
|
|
|
129
203
|
}
|
|
130
204
|
}
|
|
131
205
|
|
|
206
|
+
if (config.inference.mode === "remote" && config.inference.reranker && config.inference.thresholds) {
|
|
207
|
+
checks.push(checkCalibration(config));
|
|
208
|
+
}
|
|
209
|
+
|
|
132
210
|
const inferenceReady = checks.some((check) => check.name === "embedding" && check.ok);
|
|
133
211
|
const coreReady = checks.some((check) => check.name === "vault" && check.ok)
|
|
134
212
|
&& checks.some((check) => check.name === "state" && check.ok);
|
package/src/types.ts
CHANGED
|
@@ -73,6 +73,7 @@ export interface RemoteInferenceConfig {
|
|
|
73
73
|
embedding: RemoteEmbeddingConfig;
|
|
74
74
|
reranker?: RemoteRerankerConfig;
|
|
75
75
|
thresholds?: Required<Omit<Thresholds, "candidate_limit">>;
|
|
76
|
+
calibration?: { run_id: string };
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
export type InferenceConfig = LocalInferenceConfig | RemoteInferenceConfig;
|