@klhapp/skillmux 1.11.2 → 1.13.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -10
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +15 -5
  6. package/docs/concepts.md +4 -3
  7. package/docs/configuration.md +22 -5
  8. package/docs/deployment.md +3 -3
  9. package/docs/getting-started.md +32 -7
  10. package/docs/releasing.md +17 -2
  11. package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
  12. package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
  13. package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
  14. package/docs/sdd/cli-remote-target-parity/think.md +183 -0
  15. package/docs/sdd/cli-surface-consistency/think.md +149 -0
  16. package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
  17. package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
  18. package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
  19. package/docs/sdd/feat-packaging-deployment/review.md +95 -0
  20. package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
  21. package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
  22. package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
  23. package/docs/sdd/npm-native-binaries/review.md +103 -0
  24. package/docs/sdd/npm-native-binaries/spec.md +148 -0
  25. package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
  26. package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
  27. package/docs/sdd/runtime-resource-hardening/review.md +86 -0
  28. package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
  29. package/docs/sdd/security-hardening-phase/think.md +129 -0
  30. package/docs/sdd/security-observability/review.md +96 -0
  31. package/docs/sdd/security-observability/spec.md +55 -0
  32. package/docs/sdd/skill-provenance-update/review.md +152 -0
  33. package/docs/sdd/skill-provenance-update/schema.json +193 -0
  34. package/docs/sdd/skill-provenance-update/spec.md +134 -0
  35. package/docs/sdd/supply-chain-hardening/review.md +50 -0
  36. package/docs/sdd/supply-chain-hardening/spec.md +42 -0
  37. package/docs/sdd/target-marker-rehome/spec.md +33 -0
  38. package/docs/skill-management.md +1 -1
  39. package/docs/troubleshooting.md +18 -2
  40. package/package.json +15 -8
  41. package/src/adapters.ts +0 -438
  42. package/src/audit.ts +0 -21
  43. package/src/cli.ts +0 -928
  44. package/src/clients.ts +0 -368
  45. package/src/commands/audit.ts +0 -85
  46. package/src/commands/config.ts +0 -224
  47. package/src/commands/context.ts +0 -104
  48. package/src/commands/core.ts +0 -56
  49. package/src/commands/doctor.ts +0 -97
  50. package/src/commands/eval.ts +0 -88
  51. package/src/commands/init.ts +0 -672
  52. package/src/commands/install.ts +0 -155
  53. package/src/commands/local-vault.ts +0 -60
  54. package/src/commands/models.ts +0 -10
  55. package/src/commands/outdated.ts +0 -115
  56. package/src/commands/project.ts +0 -580
  57. package/src/commands/report.ts +0 -66
  58. package/src/commands/scan.ts +0 -71
  59. package/src/commands/shared.ts +0 -38
  60. package/src/commands/skill.ts +0 -33
  61. package/src/commands/sync.ts +0 -233
  62. package/src/commands/target.ts +0 -225
  63. package/src/commands/update.ts +0 -277
  64. package/src/completions.ts +0 -196
  65. package/src/concurrency-limiter.ts +0 -61
  66. package/src/config-service.ts +0 -398
  67. package/src/config-watcher.ts +0 -253
  68. package/src/config.ts +0 -583
  69. package/src/context.ts +0 -188
  70. package/src/db-audit.ts +0 -286
  71. package/src/db-index.ts +0 -238
  72. package/src/db.ts +0 -3
  73. package/src/deployment.ts +0 -39
  74. package/src/doctor.ts +0 -205
  75. package/src/eval.ts +0 -302
  76. package/src/global-flags.ts +0 -46
  77. package/src/init-agents.ts +0 -329
  78. package/src/init-instructions.ts +0 -192
  79. package/src/init.ts +0 -389
  80. package/src/install.ts +0 -232
  81. package/src/lifecycle.ts +0 -51
  82. package/src/logger.ts +0 -26
  83. package/src/manifest.ts +0 -352
  84. package/src/mcp-registration.ts +0 -89
  85. package/src/metrics.ts +0 -121
  86. package/src/models.ts +0 -20
  87. package/src/output.ts +0 -214
  88. package/src/project-setup.ts +0 -36
  89. package/src/prompts.ts +0 -124
  90. package/src/provenance.ts +0 -99
  91. package/src/rate-limiter.ts +0 -137
  92. package/src/readiness.ts +0 -30
  93. package/src/redact.ts +0 -52
  94. package/src/router-core.ts +0 -666
  95. package/src/rrf.ts +0 -31
  96. package/src/scan.ts +0 -300
  97. package/src/server.ts +0 -930
  98. package/src/setup.ts +0 -145
  99. package/src/snapshot.ts +0 -135
  100. package/src/stats.ts +0 -340
  101. package/src/sync.ts +0 -482
  102. package/src/toml-writer.ts +0 -51
  103. package/src/types.ts +0 -212
  104. package/src/vault.ts +0 -207
package/src/setup.ts DELETED
@@ -1,145 +0,0 @@
1
- import {
2
- existsSync,
3
- linkSync,
4
- lstatSync,
5
- mkdirSync,
6
- readdirSync,
7
- realpathSync,
8
- statSync,
9
- unlinkSync,
10
- rmSync,
11
- writeFileSync,
12
- } from "node:fs";
13
- import { basename, dirname, join } from "node:path";
14
- import { SKILL_ID_PATTERN } from "./vault";
15
-
16
- export interface VaultHealth {
17
- path: string;
18
- state: "missing" | "broken-symlink" | "not-directory" | "empty" | "ready";
19
- ok: boolean;
20
- skillCount: number;
21
- message: string;
22
- }
23
-
24
- export interface ConfigInitPlan {
25
- configPath: string;
26
- vaultPath: string;
27
- action: "create" | "preserve";
28
- content?: string;
29
- }
30
-
31
- export function inspectVault(path: string): VaultHealth {
32
- let stat;
33
- try {
34
- stat = lstatSync(path);
35
- } catch (error) {
36
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
37
- return {
38
- path,
39
- state: "missing",
40
- ok: false,
41
- skillCount: 0,
42
- message: `vault does not exist: ${path}`,
43
- };
44
- }
45
- throw error;
46
- }
47
-
48
- if (stat.isSymbolicLink()) {
49
- try {
50
- realpathSync(path);
51
- } catch (error) {
52
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
53
- return {
54
- path,
55
- state: "broken-symlink",
56
- ok: false,
57
- skillCount: 0,
58
- message: `vault is a dangling symlink: ${path}`,
59
- };
60
- }
61
- throw error;
62
- }
63
- }
64
-
65
- const resolvedStat = stat.isSymbolicLink() ? statSync(path) : stat;
66
- if (!resolvedStat.isDirectory()) {
67
- return {
68
- path,
69
- state: "not-directory",
70
- ok: false,
71
- skillCount: 0,
72
- message: `vault is not a directory: ${path}`,
73
- };
74
- }
75
-
76
- const skillCount = readdirSync(path, { withFileTypes: true }).filter(
77
- (entry) =>
78
- entry.isDirectory() &&
79
- SKILL_ID_PATTERN.test(entry.name) &&
80
- existsSync(join(path, entry.name, "SKILL.md")),
81
- ).length;
82
- if (skillCount === 0) {
83
- return {
84
- path,
85
- state: "empty",
86
- ok: false,
87
- skillCount,
88
- message: `vault contains no skill directories: ${path}`,
89
- };
90
- }
91
-
92
- return {
93
- path,
94
- state: "ready",
95
- ok: true,
96
- skillCount,
97
- message: `vault ready: ${path} (${skillCount} ${skillCount === 1 ? "skill" : "skills"})`,
98
- };
99
- }
100
-
101
- export function planConfigInit(configPath: string, vaultPath: string): ConfigInitPlan {
102
- if (existsSync(configPath)) {
103
- return { configPath, vaultPath, action: "preserve" };
104
- }
105
-
106
- const vaultHealth = inspectVault(vaultPath);
107
- if (!vaultHealth.ok) {
108
- throw new Error(vaultHealth.message);
109
- }
110
-
111
- return {
112
- configPath,
113
- vaultPath,
114
- action: "create",
115
- content: `vault_path = ${JSON.stringify(vaultPath)}\n`,
116
- };
117
- }
118
-
119
- export function applyConfigInit(plan: ConfigInitPlan): "created" | "preserved" {
120
- if (plan.action === "preserve" || existsSync(plan.configPath)) {
121
- return "preserved";
122
- }
123
-
124
- mkdirSync(dirname(plan.configPath), { recursive: true });
125
- const temporaryPath = join(
126
- dirname(plan.configPath),
127
- `.${basename(plan.configPath)}.${process.pid}.${crypto.randomUUID()}.tmp`,
128
- );
129
- writeFileSync(temporaryPath, plan.content as string, { encoding: "utf8", mode: 0o600 });
130
- try {
131
- linkSync(temporaryPath, plan.configPath);
132
- return "created";
133
- } catch (error) {
134
- if ((error as NodeJS.ErrnoException).code === "EEXIST") {
135
- return "preserved";
136
- }
137
- throw error;
138
- } finally {
139
- unlinkSync(temporaryPath);
140
- }
141
- }
142
-
143
- export function rollbackConfigInit(plan: ConfigInitPlan): void {
144
- if (plan.action === "create") rmSync(plan.configPath, { force: true });
145
- }
package/src/snapshot.ts DELETED
@@ -1,135 +0,0 @@
1
- import type { Database } from "bun:sqlite";
2
- import { expandHome } from "./config";
3
- import { openIndex } from "./db";
4
- import type { Clients, Config } from "./types";
5
-
6
- // ---------------------------------------------------------------------------
7
- // Immutable runtime snapshot (AC11)
8
- // ---------------------------------------------------------------------------
9
-
10
- /**
11
- * An immutable, reference-counted view of runtime resources.
12
- * All fields are frozen at construction — callers can never mutate the snapshot.
13
- * The underlying db handle is closed only after every holder calls release().
14
- */
15
- export interface RuntimeSnapshot {
16
- readonly config: Config;
17
- readonly clients: Clients;
18
- readonly db: Database;
19
- }
20
-
21
- /**
22
- * A handle returned by RuntimeSnapshotManager.acquire().
23
- * Call release() exactly once when the request (or operation) is done.
24
- */
25
- export interface SnapshotHandle {
26
- readonly snapshot: RuntimeSnapshot;
27
- release(): void;
28
- }
29
-
30
- // ---------------------------------------------------------------------------
31
- // Internal reference-counted slot
32
- // ---------------------------------------------------------------------------
33
-
34
- class SnapshotSlot {
35
- private refCount = 0;
36
- private closed = false;
37
- readonly snapshot: RuntimeSnapshot;
38
-
39
- constructor(config: Config, clients: Clients) {
40
- const stateDir = expandHome(config.state_dir);
41
- const db = openIndex(stateDir);
42
- // Freeze the snapshot object so callers cannot mutate it
43
- this.snapshot = Object.freeze({ config, clients, db });
44
- }
45
-
46
- acquire(): SnapshotHandle {
47
- this.refCount++;
48
- let released = false;
49
- return {
50
- snapshot: this.snapshot,
51
- release: () => {
52
- if (released) return; // idempotent
53
- released = true;
54
- this.refCount--;
55
- this.maybeClose();
56
- },
57
- };
58
- }
59
-
60
- /** Signal that no new acquires will come from the manager for this slot. */
61
- retire(): void {
62
- this.closed = true;
63
- this.maybeClose();
64
- }
65
-
66
- private maybeClose(): void {
67
- if (this.closed && this.refCount === 0) {
68
- try {
69
- this.snapshot.db.close();
70
- } catch {
71
- // already closed — idempotent
72
- }
73
- }
74
- }
75
- }
76
-
77
- // ---------------------------------------------------------------------------
78
- // Public manager
79
- // ---------------------------------------------------------------------------
80
-
81
- /**
82
- * RuntimeSnapshotManager holds one active SnapshotSlot at a time.
83
- *
84
- * - acquire() → returns a SnapshotHandle for the current slot; increments its refcount.
85
- * - replace() → installs a new slot; retires the previous one (closed when all
86
- * in-flight holders release, i.e. no handle leak).
87
- * - dispose() → retires the current slot and blocks future acquires.
88
- */
89
- export class RuntimeSnapshotManager {
90
- private current: SnapshotSlot;
91
- private disposed = false;
92
-
93
- private constructor(config: Config, clients: Clients) {
94
- this.current = new SnapshotSlot(config, clients);
95
- }
96
-
97
- static create(config: Config, clients: Clients): RuntimeSnapshotManager {
98
- return new RuntimeSnapshotManager(config, clients);
99
- }
100
-
101
- /**
102
- * Acquire a handle to the current snapshot.
103
- * The caller MUST call release() when done — even on error paths.
104
- */
105
- acquire(): SnapshotHandle {
106
- if (this.disposed) {
107
- throw new Error("RuntimeSnapshotManager has been disposed");
108
- }
109
- return this.current.acquire();
110
- }
111
-
112
- /**
113
- * Swap in a new configuration and clients.
114
- * The previous slot is retired and will close its db handle once all
115
- * in-flight holders release.
116
- */
117
- replace(config: Config, clients: Clients): void {
118
- if (this.disposed) {
119
- throw new Error("RuntimeSnapshotManager has been disposed");
120
- }
121
- const outgoing = this.current;
122
- this.current = new SnapshotSlot(config, clients);
123
- outgoing.retire(); // closes when refCount reaches zero
124
- }
125
-
126
- /**
127
- * Permanently shut down. Retires the current slot.
128
- * Any future acquire() will throw.
129
- */
130
- dispose(): void {
131
- if (this.disposed) return;
132
- this.disposed = true;
133
- this.current.retire();
134
- }
135
- }
package/src/stats.ts DELETED
@@ -1,340 +0,0 @@
1
- import type { Database } from "bun:sqlite";
2
- import type { AuditCandidate, AuditRow, FetchAuditRow } from "./types";
3
-
4
- export const SINCE_PATTERN = /^(\d+[hdwmy]|\d{4}-\d{2}-\d{2}([T ].+)?)$/;
5
-
6
- export interface SkillStat {
7
- skill_id: string;
8
- candidate_count: number;
9
- }
10
-
11
- export interface EmptyShortlistQuery {
12
- query: string;
13
- count: number;
14
- }
15
-
16
- export interface RetrievalTotals {
17
- exact: number;
18
- reranked: number;
19
- hybrid: number;
20
- lexical: number;
21
- }
22
-
23
- export type AcceptanceSignal =
24
- | { available: false; uncorrelated_fetch_count: number }
25
- | {
26
- available: true;
27
- resolves_with_candidates: number;
28
- accepted_count: number;
29
- acceptance_rate: number;
30
- observed_mrr: number;
31
- top1_acceptance_rate: number;
32
- uncorrelated_fetch_count: number;
33
- };
34
-
35
- export interface StatsResponse {
36
- since: string;
37
- until: string;
38
- total_requests: number;
39
- empty_shortlist_count: number;
40
- empty_shortlist_rate: number;
41
- retrieval_totals: RetrievalTotals;
42
- degraded_count: number;
43
- average_latency_ms: number;
44
- skills: SkillStat[];
45
- top_empty_shortlist_queries: EmptyShortlistQuery[];
46
- acceptance: AcceptanceSignal;
47
- top_unused_shortlist_queries: EmptyShortlistQuery[];
48
- }
49
-
50
- const RELATIVE_WINDOW = /^(\d+)([hdwmy])$/;
51
- const UNIT_MS: Record<string, number> = {
52
- h: 3_600_000,
53
- d: 86_400_000,
54
- w: 604_800_000,
55
- m: 2_592_000_000,
56
- y: 31_536_000_000,
57
- };
58
-
59
- export function parseSince(since: string, now: Date = new Date()): Date {
60
- if (!SINCE_PATTERN.test(since)) throw new Error(`invalid --since window: ${since}`);
61
-
62
- const relative = RELATIVE_WINDOW.exec(since);
63
- if (relative) {
64
- const amount = Number(relative[1]);
65
- const unitMs = UNIT_MS[relative[2]!]!;
66
- return new Date(now.getTime() - amount * unitMs);
67
- }
68
-
69
- const parsed = new Date(since);
70
- if (Number.isNaN(parsed.getTime())) throw new Error(`invalid --since window: ${since}`);
71
- return parsed;
72
- }
73
-
74
- function compareCodeUnits(a: string, b: string): number {
75
- if (a < b) return -1;
76
- if (a > b) return 1;
77
- return 0;
78
- }
79
-
80
- export function computeStats(
81
- rows: AuditRow[],
82
- since: Date,
83
- until: Date,
84
- fetchRows: FetchAuditRow[] = [],
85
- ): StatsResponse {
86
- const retrieval_totals: RetrievalTotals = { exact: 0, reranked: 0, hybrid: 0, lexical: 0 };
87
- const skillCounts = new Map<string, number>();
88
- const emptyShortlistCounts = new Map<string, number>();
89
- const resolvesWithCandidates = new Map<number, AuditRow>();
90
- let empty_shortlist_count = 0;
91
- let degraded_count = 0;
92
- let total_latency_ms = 0;
93
-
94
- for (const row of rows) {
95
- if (row.retrieval in retrieval_totals) {
96
- retrieval_totals[row.retrieval]++;
97
- }
98
-
99
- if (row.degraded_from || row.degradation_reason) {
100
- degraded_count++;
101
- }
102
-
103
- total_latency_ms += row.latency_ms;
104
-
105
- if (row.candidates.length === 0) {
106
- empty_shortlist_count++;
107
- emptyShortlistCounts.set(row.query, (emptyShortlistCounts.get(row.query) ?? 0) + 1);
108
- } else {
109
- resolvesWithCandidates.set(row.id, row);
110
- const seenInRow = new Set<string>();
111
- for (const candidate of row.candidates) {
112
- if (!candidate.skill_id) continue;
113
- if (seenInRow.has(candidate.skill_id)) continue;
114
- seenInRow.add(candidate.skill_id);
115
- skillCounts.set(candidate.skill_id, (skillCounts.get(candidate.skill_id) ?? 0) + 1);
116
- }
117
- }
118
- }
119
-
120
- let uncorrelated_fetch_count = 0;
121
- const firstFetchByResolve = new Map<number, FetchAuditRow>();
122
- for (const fetch of fetchRows) {
123
- if (fetch.resolve_audit_id === null) {
124
- uncorrelated_fetch_count++;
125
- continue;
126
- }
127
- if (!resolvesWithCandidates.has(fetch.resolve_audit_id)) continue;
128
- const existing = firstFetchByResolve.get(fetch.resolve_audit_id);
129
- if (!existing || fetch.ts < existing.ts) {
130
- firstFetchByResolve.set(fetch.resolve_audit_id, fetch);
131
- }
132
- }
133
-
134
- const acceptedResolveIds = new Set(firstFetchByResolve.keys());
135
- const accepted_count = acceptedResolveIds.size;
136
- const acceptance: AcceptanceSignal =
137
- accepted_count > 0
138
- ? (() => {
139
- let reciprocalRankSum = 0;
140
- let top1Count = 0;
141
- for (const fetch of firstFetchByResolve.values()) {
142
- const rank = fetch.rank_at_resolve;
143
- if (rank !== null) {
144
- reciprocalRankSum += 1 / rank;
145
- if (rank === 1) top1Count++;
146
- }
147
- }
148
- return {
149
- available: true as const,
150
- resolves_with_candidates: resolvesWithCandidates.size,
151
- accepted_count,
152
- acceptance_rate: accepted_count / resolvesWithCandidates.size,
153
- observed_mrr: reciprocalRankSum / accepted_count,
154
- top1_acceptance_rate: top1Count / accepted_count,
155
- uncorrelated_fetch_count,
156
- };
157
- })()
158
- : { available: false as const, uncorrelated_fetch_count };
159
-
160
- const unusedShortlistCounts = new Map<string, number>();
161
- for (const [id, row] of resolvesWithCandidates) {
162
- if (acceptedResolveIds.has(id)) continue;
163
- unusedShortlistCounts.set(row.query, (unusedShortlistCounts.get(row.query) ?? 0) + 1);
164
- }
165
-
166
- const total_requests = rows.length;
167
- const empty_shortlist_rate = total_requests > 0 ? empty_shortlist_count / total_requests : 0;
168
- const average_latency_ms = total_requests > 0 ? total_latency_ms / total_requests : 0;
169
-
170
- const skills: SkillStat[] = [...skillCounts.entries()]
171
- .map(([skill_id, candidate_count]) => ({ skill_id, candidate_count }))
172
- .sort((a, b) => {
173
- if (b.candidate_count !== a.candidate_count) {
174
- return b.candidate_count - a.candidate_count;
175
- }
176
- return compareCodeUnits(a.skill_id, b.skill_id);
177
- });
178
-
179
- const top_empty_shortlist_queries = topQueryCounts(emptyShortlistCounts);
180
- const top_unused_shortlist_queries = topQueryCounts(unusedShortlistCounts);
181
-
182
- return {
183
- since: since.toISOString(),
184
- until: until.toISOString(),
185
- total_requests,
186
- empty_shortlist_count,
187
- empty_shortlist_rate,
188
- retrieval_totals,
189
- degraded_count,
190
- average_latency_ms,
191
- skills,
192
- top_empty_shortlist_queries,
193
- acceptance,
194
- top_unused_shortlist_queries,
195
- };
196
- }
197
-
198
- function topQueryCounts(counts: Map<string, number>): EmptyShortlistQuery[] {
199
- return [...counts.entries()]
200
- .map(([query, count]) => ({ query, count }))
201
- .sort((a, b) => {
202
- if (b.count !== a.count) {
203
- return b.count - a.count;
204
- }
205
- return compareCodeUnits(a.query, b.query);
206
- })
207
- .slice(0, 20);
208
- }
209
-
210
- interface AuditTableRow {
211
- id: number;
212
- ts: string;
213
- request_id: string | null;
214
- query: string;
215
- retrieval: AuditRow["retrieval"];
216
- degraded_from: string | null;
217
- degradation_reason: string | null;
218
- candidates: string;
219
- latency_ms: number;
220
- }
221
-
222
- export function queryAuditRows(db: Database, sinceIso: string): AuditRow[] {
223
- const rows = db
224
- .query(
225
- "SELECT id, ts, request_id, query, retrieval, degraded_from, degradation_reason, candidates, latency_ms FROM audit WHERE ts >= ? ORDER BY ts ASC",
226
- )
227
- .all(sinceIso) as AuditTableRow[];
228
-
229
- return rows.map((row) => {
230
- let parsed: unknown;
231
- try {
232
- parsed = JSON.parse(row.candidates);
233
- } catch {
234
- throw new Error(`Failed to parse candidates JSON for audit row ${row.id}`);
235
- }
236
- if (!Array.isArray(parsed)) {
237
- throw new Error(`Invalid candidates JSON for audit row ${row.id}: expected array, got ${typeof parsed}`);
238
- }
239
- const candidates: AuditCandidate[] = parsed.map((c: any, index: number) => {
240
- if (!c || typeof c !== "object" || Array.isArray(c) || typeof c.skill_id !== "string") {
241
- throw new Error(`Invalid candidate at index ${index} for audit row ${row.id}: missing or invalid skill_id`);
242
- }
243
- const score = c.score;
244
- if (score !== null && (typeof score !== "number" || !Number.isFinite(score))) {
245
- throw new Error(`Invalid candidate at index ${index} for audit row ${row.id}: missing or invalid score`);
246
- }
247
- return {
248
- skill_id: c.skill_id,
249
- score,
250
- };
251
- });
252
-
253
- const result: AuditRow = {
254
- id: row.id,
255
- ts: row.ts,
256
- request_id: row.request_id,
257
- query: row.query,
258
- retrieval: row.retrieval,
259
- candidates,
260
- latency_ms: row.latency_ms,
261
- };
262
- if (row.degraded_from !== null && row.degraded_from !== undefined) {
263
- result.degraded_from = row.degraded_from as AuditRow["degraded_from"];
264
- }
265
- if (row.degradation_reason !== null && row.degradation_reason !== undefined) {
266
- result.degradation_reason = row.degradation_reason as AuditRow["degradation_reason"];
267
- }
268
- return result;
269
- });
270
- }
271
-
272
- export function queryFetchRows(db: Database, sinceIso: string): FetchAuditRow[] {
273
- return db
274
- .query(
275
- "SELECT id, ts, skill_id, request_id, resolve_audit_id, rank_at_resolve FROM fetch WHERE ts >= ? ORDER BY ts ASC",
276
- )
277
- .all(sinceIso) as FetchAuditRow[];
278
- }
279
-
280
- export function getStats(db: Database, since: string, now: Date = new Date()): StatsResponse {
281
- const sinceDate = parseSince(since, now);
282
- const rows = queryAuditRows(db, sinceDate.toISOString());
283
- const fetchRows = queryFetchRows(db, sinceDate.toISOString());
284
- return computeStats(rows, sinceDate, now, fetchRows);
285
- }
286
-
287
- export function renderStatsText(stats: StatsResponse): string {
288
- const lines: string[] = [];
289
- lines.push(`window: ${stats.since} .. ${stats.until}`);
290
- lines.push(
291
- `requests: total=${stats.total_requests} empty_shortlist=${stats.empty_shortlist_count} ` +
292
- `(empty_shortlist_rate=${stats.empty_shortlist_rate.toFixed(3)}) ` +
293
- `degraded=${stats.degraded_count} avg_latency_ms=${stats.average_latency_ms.toFixed(1)}`,
294
- );
295
- lines.push(
296
- `retrieval: exact=${stats.retrieval_totals.exact} reranked=${stats.retrieval_totals.reranked} ` +
297
- `hybrid=${stats.retrieval_totals.hybrid} lexical=${stats.retrieval_totals.lexical}`,
298
- );
299
-
300
- lines.push("skills:");
301
- if (stats.skills.length === 0) {
302
- lines.push(" (none)");
303
- } else {
304
- for (const skill of stats.skills) {
305
- lines.push(` ${skill.skill_id} candidate=${skill.candidate_count}`);
306
- }
307
- }
308
-
309
- lines.push("top empty shortlist queries:");
310
- if (stats.top_empty_shortlist_queries.length === 0) {
311
- lines.push(" (none)");
312
- } else {
313
- for (const entry of stats.top_empty_shortlist_queries) {
314
- lines.push(` "${entry.query}" (${entry.count})`);
315
- }
316
- }
317
-
318
- if (stats.acceptance.available) {
319
- lines.push(
320
- `acceptance: acceptance_rate=${stats.acceptance.acceptance_rate.toFixed(3)} ` +
321
- `observed_mrr=${stats.acceptance.observed_mrr.toFixed(3)} ` +
322
- `top1_acceptance_rate=${stats.acceptance.top1_acceptance_rate.toFixed(3)} ` +
323
- `(accepted=${stats.acceptance.accepted_count}/${stats.acceptance.resolves_with_candidates}, ` +
324
- `uncorrelated_fetch_count=${stats.acceptance.uncorrelated_fetch_count})`,
325
- );
326
- } else {
327
- lines.push(`acceptance: unavailable (uncorrelated_fetch_count=${stats.acceptance.uncorrelated_fetch_count})`);
328
- }
329
-
330
- lines.push("top unused shortlist queries:");
331
- if (stats.top_unused_shortlist_queries.length === 0) {
332
- lines.push(" (none)");
333
- } else {
334
- for (const entry of stats.top_unused_shortlist_queries) {
335
- lines.push(` "${entry.query}" (${entry.count})`);
336
- }
337
- }
338
-
339
- return lines.join("\n");
340
- }