@jeffober/thatch 0.1.27 → 0.1.29
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/README.md +24 -10
- package/artifacts/skills/thatch-code-review.md +9 -7
- package/artifacts/skills/thatch-fact-extractor.md +2 -2
- package/artifacts/skills/thatch-review-economy.md +97 -0
- package/artifacts/skills/thatch-review-followup.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +15 -5
- package/bin/qa-run +38 -0
- package/bin/thatch +13 -7
- package/docs/dev/README.md +76 -22
- package/docs/dev/features/README.md +33 -0
- package/docs/dev/features/behavior-engine.md +271 -0
- package/docs/dev/features/cicd.md +89 -0
- package/docs/dev/features/cli.md +81 -0
- package/docs/dev/features/compaction-recovery.md +48 -0
- package/docs/dev/features/database.md +244 -0
- package/docs/dev/features/deduplication.md +102 -0
- package/docs/dev/features/extraction.md +253 -0
- package/docs/dev/features/hygiene.md +81 -0
- package/docs/dev/features/memory-store.md +154 -0
- package/docs/dev/features/multi-host.md +228 -0
- package/docs/dev/features/nudge-pipeline.md +282 -0
- package/docs/dev/features/prediction-engine.md +253 -0
- package/docs/dev/features/qa-system.md +205 -0
- package/docs/dev/features/session-lifecycle.md +110 -0
- package/docs/dev/features/setup.md +199 -0
- package/docs/dev/features/sideband.md +104 -0
- package/docs/dev/gotchas.md +3 -3
- package/docs/dev/mcp-parity.md +7 -5
- package/docs/dev/setup-and-hooks.md +4 -4
- package/docs/dev/skills.md +10 -8
- package/docs/plans/README.md +2 -2
- package/docs/user/README.md +58 -20
- package/docs/user/behavior-engine.md +85 -0
- package/docs/user/cli.md +98 -0
- package/docs/user/code-review.md +73 -0
- package/docs/user/deduplication.md +70 -0
- package/docs/user/default-behaviors.md +75 -0
- package/docs/user/extraction.md +74 -0
- package/docs/user/hygiene.md +76 -0
- package/docs/user/memory.md +81 -0
- package/docs/user/prediction-engine.md +87 -0
- package/docs/user/setup.md +110 -0
- package/docs/user/skills.md +97 -0
- package/package.json +2 -2
- package/src/behavior.ts +120 -0
- package/src/db.ts +160 -417
- package/src/embeddings.ts +0 -41
- package/src/extraction.ts +2 -2
- package/src/git.ts +3 -1
- package/src/index.ts +66 -14
- package/src/mcp.ts +59 -2
- package/src/prediction.ts +127 -0
- package/src/prompts.ts +155 -200
- package/src/scoring-engine.ts +374 -0
- package/src/seed-behaviors.ts +339 -0
- package/src/setup.ts +2 -6
- package/src/sideband.ts +36 -8
- package/src/skills.ts +1 -0
- package/src/tool-defs.ts +235 -9
- package/src/tools.ts +11 -1
- package/src/vector-math.ts +30 -0
- package/docs/qa/README.md +0 -175
package/src/embeddings.ts
CHANGED
|
@@ -82,44 +82,3 @@ export class BgeEmbeddingModel implements EmbeddingModel {
|
|
|
82
82
|
return output.data as Float32Array;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Mock embedding model for tests. Returns deterministic vectors derived from
|
|
88
|
-
* a hash of the input text via xorshift, so identical texts embed identically
|
|
89
|
-
* and distinct texts land near-orthogonal - mirroring how unrelated content
|
|
90
|
-
* behaves in a real embedding space. Never loads a model or touches the network.
|
|
91
|
-
*/
|
|
92
|
-
export class MockEmbeddingModel implements EmbeddingModel {
|
|
93
|
-
readonly dims = 384;
|
|
94
|
-
readonly name = "mock";
|
|
95
|
-
loaded = true;
|
|
96
|
-
|
|
97
|
-
async load(): Promise<void> {}
|
|
98
|
-
|
|
99
|
-
async queryEmbed(text: string): Promise<Float32Array> {
|
|
100
|
-
return this.#embed(text);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async passageEmbed(text: string): Promise<Float32Array> {
|
|
104
|
-
return this.#embed(text);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
#embed(text: string): Float32Array {
|
|
108
|
-
let h = 0;
|
|
109
|
-
for (let i = 0; i < text.length; i++) {
|
|
110
|
-
h = ((h << 5) - h) + text.charCodeAt(i);
|
|
111
|
-
h |= 0;
|
|
112
|
-
}
|
|
113
|
-
h ^= 0x9e3779b9; // avoid the degenerate all-zero state for empty input
|
|
114
|
-
|
|
115
|
-
const vec = new Float32Array(this.dims);
|
|
116
|
-
for (let i = 0; i < this.dims; i++) {
|
|
117
|
-
h ^= h << 13;
|
|
118
|
-
h ^= h >>> 17;
|
|
119
|
-
h ^= h << 5;
|
|
120
|
-
h |= 0;
|
|
121
|
-
vec[i] = h / 0x80000000;
|
|
122
|
-
}
|
|
123
|
-
return vec;
|
|
124
|
-
}
|
|
125
|
-
}
|
package/src/extraction.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface ToolInteraction {
|
|
|
11
11
|
* opencode plugin (in-process) and the Claude Code CLI (file-backed queue) so
|
|
12
12
|
* the payload the agent ultimately receives has the same shape from either path.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
function summarizeArgs(tool: string, args: Record<string, unknown>): string {
|
|
15
15
|
const file = args.filePath ?? args.file_path;
|
|
16
16
|
switch (tool.toLowerCase()) {
|
|
17
17
|
case "read":
|
|
@@ -54,7 +54,7 @@ export function deriveTitle(tool: string, args: Record<string, unknown>): string
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
function truncate(text: string, maxLen: number): string {
|
|
58
58
|
if (text.length <= maxLen) return text;
|
|
59
59
|
return text.slice(0, maxLen) + "...";
|
|
60
60
|
}
|
package/src/git.ts
CHANGED
|
@@ -69,8 +69,10 @@ export async function detectRepo(cwd?: string): Promise<string> {
|
|
|
69
69
|
try {
|
|
70
70
|
const gitDir = await $`git rev-parse --git-common-dir`.cwd(dir).quiet();
|
|
71
71
|
if (gitDir.exitCode === 0) {
|
|
72
|
+
// Handle both "/path/to/.git" (absolute) and ".git" (relative,
|
|
73
|
+
// when cwd is the repo root itself).
|
|
72
74
|
const d = gitDir.stdout.toString().trim();
|
|
73
|
-
const parent = d.endsWith("/.git") ? d.slice(0, -5) : d;
|
|
75
|
+
const parent = d.endsWith("/.git") ? d.slice(0, -5) : (d === ".git" ? "" : d);
|
|
74
76
|
const name = parent.split("/").pop() || "unknown";
|
|
75
77
|
if (name !== "unknown") return name;
|
|
76
78
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,11 +12,13 @@ import {
|
|
|
12
12
|
extractionNudge,
|
|
13
13
|
extractionDirectPrompt,
|
|
14
14
|
predictionNudge,
|
|
15
|
+
behaviorNudge,
|
|
15
16
|
type NudgeMatch,
|
|
16
17
|
} from "./prompts";
|
|
17
18
|
import { ExtractionPipeline, type ToolInteraction } from "./extraction";
|
|
18
19
|
import { installSkills, SHARED_SKILLS, OPENCODE_ONLY_SKILLS } from "./skills";
|
|
19
20
|
import { hygieneReport } from "./hygiene";
|
|
21
|
+
import { seedDefaultBehaviors } from "./seed-behaviors";
|
|
20
22
|
|
|
21
23
|
// ---------------------------------------------------------------------------
|
|
22
24
|
// V1 server export - tools, prompt injection, session hooks
|
|
@@ -31,10 +33,17 @@ const RECALL_THRESHOLD = parseFloat(process.env.THATCH_RECALL_THRESHOLD ?? "0.55
|
|
|
31
33
|
// like "yes" or "ok" match too broadly to be useful.
|
|
32
34
|
const MIN_PROMPT_LEN = 10;
|
|
33
35
|
|
|
34
|
-
// Minimum cosine score for the prediction auto-fire.
|
|
35
|
-
//
|
|
36
|
-
// a
|
|
37
|
-
|
|
36
|
+
// Minimum cosine score for the prediction auto-fire. Higher than the
|
|
37
|
+
// recall nudge (0.55) because surfacing a user-preference nudge is more
|
|
38
|
+
// disruptive than surfacing a memory -- the agent may act on it or
|
|
39
|
+
// surface it to the user. 0.60 cuts noise in dense embedding spaces
|
|
40
|
+
// while still catching genuinely related contexts.
|
|
41
|
+
const PREDICTION_THRESHOLD = parseFloat(process.env.THATCH_PREDICTION_THRESHOLD ?? "0.60");
|
|
42
|
+
|
|
43
|
+
// Same threshold for behavior auto-fire. Same rationale: surfacing a
|
|
44
|
+
// self-discipline rule is disruptive and should only fire when the
|
|
45
|
+
// situation genuinely matches.
|
|
46
|
+
const BEHAVIOR_THRESHOLD = parseFloat(process.env.THATCH_BEHAVIOR_THRESHOLD ?? "0.60");
|
|
38
47
|
|
|
39
48
|
export const server: Plugin = async ({ client, worktree }) => {
|
|
40
49
|
// The opencode server's cwd is wherever the server happened to start;
|
|
@@ -49,6 +58,13 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
49
58
|
const model = new BgeEmbeddingModel(modelName);
|
|
50
59
|
const extraction = new ExtractionPipeline();
|
|
51
60
|
|
|
61
|
+
// Seed default behaviors into the global store on first run, and
|
|
62
|
+
// update them when their content changes across releases. Idempotent:
|
|
63
|
+
// behaviors are matched by key stamp, not by cosine similarity, so
|
|
64
|
+
// user-codified behaviors are never overwritten. See seed-behaviors.ts
|
|
65
|
+
// for the full mechanism.
|
|
66
|
+
await seedDefaultBehaviors(db, model);
|
|
67
|
+
|
|
52
68
|
// Sessions currently being compacted. chat.message nudges are skipped while
|
|
53
69
|
// a session is in this set - the agent can't call tools during summary
|
|
54
70
|
// generation, so a recall or extraction nudge would cause a blocked-tool
|
|
@@ -141,8 +157,7 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
141
157
|
extracting.add(parentID);
|
|
142
158
|
|
|
143
159
|
const batch = extraction.peek(parentID);
|
|
144
|
-
const
|
|
145
|
-
const promptText = extractionDirectPrompt(batch.length, payload);
|
|
160
|
+
const promptText = extractionDirectPrompt(batch.length, parentID);
|
|
146
161
|
|
|
147
162
|
const result = await client.session.create({
|
|
148
163
|
body: { parentID, title: "thatch-extraction" },
|
|
@@ -201,7 +216,15 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
201
216
|
}
|
|
202
217
|
|
|
203
218
|
return {
|
|
204
|
-
tool: createTools(db, model, repo
|
|
219
|
+
tool: createTools(db, model, repo, {
|
|
220
|
+
extractionPayloadProvider: (sessionID: string): string | null => {
|
|
221
|
+
const interactions = extraction.peek(sessionID);
|
|
222
|
+
const accepted = extraction.peekAccepted(sessionID);
|
|
223
|
+
const all = [...accepted, ...interactions];
|
|
224
|
+
if (all.length === 0) return null;
|
|
225
|
+
return extraction.buildPayload(all, repo);
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
205
228
|
|
|
206
229
|
// 1. System prompt - always in context.
|
|
207
230
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
@@ -321,13 +344,14 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
321
344
|
// 4. Per-message nudge - two priority tiers:
|
|
322
345
|
// a. Extraction nudge: prior tool interactions are queued for fact
|
|
323
346
|
// extraction (carries the JSON payload for thatch-fact-extractor).
|
|
324
|
-
// b. Recall + prediction nudge: when no extraction is pending, embed
|
|
325
|
-
// the user's prompt (shared embedding for
|
|
347
|
+
// b. Recall + prediction + behavior nudge: when no extraction is pending, embed
|
|
348
|
+
// the user's prompt (shared embedding for all three) and:
|
|
326
349
|
// - search memories by cosine (recall nudge)
|
|
327
350
|
// - search matchers by cosine, score predictions (prediction fire)
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
351
|
+
// - search behavior matchers by cosine, score behaviors (behavior fire)
|
|
352
|
+
// All three fire independently; any subset may inject.
|
|
353
|
+
// The auto-fires reuse the embedding already computed for recall,
|
|
354
|
+
// adding only cosine scans against the matchers tables.
|
|
331
355
|
// No extra model calls.
|
|
332
356
|
//
|
|
333
357
|
// Skipped during compaction: the agent can't call tools while generating
|
|
@@ -352,9 +376,8 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
352
376
|
// here only when direct extraction was never triggered or threw.
|
|
353
377
|
if (!extracting.has(input.sessionID) && extraction.pending(input.sessionID)) {
|
|
354
378
|
const batch = extraction.peek(input.sessionID);
|
|
355
|
-
const payload = extraction.buildPayload(batch, repo);
|
|
356
379
|
const missed = missedNudges.get(input.sessionID) ?? 0;
|
|
357
|
-
const text = extractionNudge(batch.length, missed, "thatch_memory_remember",
|
|
380
|
+
const text = extractionNudge(batch.length, missed, "thatch_memory_remember", input.sessionID);
|
|
358
381
|
missedNudges.set(input.sessionID, missed + 1);
|
|
359
382
|
|
|
360
383
|
output.parts.push({
|
|
@@ -442,6 +465,35 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
442
465
|
} catch (err) {
|
|
443
466
|
console.error(`[thatch] prediction nudge failed: ${err}`);
|
|
444
467
|
}
|
|
468
|
+
|
|
469
|
+
// Behavior fire: independent of recall and prediction. Same
|
|
470
|
+
// embedding, one more cosine scan against behavior_matchers.
|
|
471
|
+
try {
|
|
472
|
+
const behaviorItems = db.scoreBehaviorNudge([repo, "global"], embedding, BEHAVIOR_THRESHOLD);
|
|
473
|
+
if (behaviorItems.length > 0) {
|
|
474
|
+
output.parts.push({
|
|
475
|
+
id: `prt_thatch_${Math.random().toString(36).slice(2)}`,
|
|
476
|
+
sessionID: input.sessionID,
|
|
477
|
+
messageID: input.messageID ?? output.message.id,
|
|
478
|
+
type: "text",
|
|
479
|
+
text: behaviorNudge(behaviorItems),
|
|
480
|
+
synthetic: true,
|
|
481
|
+
});
|
|
482
|
+
try {
|
|
483
|
+
await client.tui.showToast({
|
|
484
|
+
body: {
|
|
485
|
+
message: `\u{1F4AD} ${behaviorItems.length} behavior${behaviorItems.length === 1 ? "" : "s"} surfaced`,
|
|
486
|
+
variant: "info",
|
|
487
|
+
duration: 3000,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
} catch {
|
|
491
|
+
// TUI may not be connected. Best-effort.
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch (err) {
|
|
495
|
+
console.error(`[thatch] behavior nudge failed: ${err}`);
|
|
496
|
+
}
|
|
445
497
|
} catch (err) {
|
|
446
498
|
console.error(`[thatch] nudge hook failed: ${err}`);
|
|
447
499
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { ThatchDB } from "./db";
|
|
3
4
|
import { BgeEmbeddingModel } from "./embeddings";
|
|
4
5
|
import { detectRepo } from "./git";
|
|
5
|
-
import { checkSetup } from "./setup";
|
|
6
|
+
import { checkSetup, setupClaudeCode, setupCursor } from "./setup";
|
|
6
7
|
import { TOOL_DEFS, type CoreContext, type ToolDef } from "./tool-defs";
|
|
7
8
|
import { SidebandServer, sidebandSocketPath } from "./sideband";
|
|
9
|
+
import { peekQueue, consumeQueue, resetMissedCount } from "./extract-queue";
|
|
10
|
+
import { buildExtractionPayload } from "./extraction";
|
|
11
|
+
import { seedDefaultBehaviors } from "./seed-behaviors";
|
|
8
12
|
import pkg from "../package.json";
|
|
9
13
|
|
|
10
14
|
// ---------------------------------------------------------------------------
|
|
@@ -98,7 +102,35 @@ export async function runMcpServer(): Promise<void> {
|
|
|
98
102
|
|
|
99
103
|
const db = new ThatchDB(dbPath);
|
|
100
104
|
const model = new BgeEmbeddingModel(modelName);
|
|
101
|
-
|
|
105
|
+
|
|
106
|
+
await seedDefaultBehaviors(db, model);
|
|
107
|
+
|
|
108
|
+
// Extraction payload provider: peeks the file-backed queue and returns the
|
|
109
|
+
// serialized JSON payload. Called by the get_extraction_payload tool when a
|
|
110
|
+
// sub-agent needs to fetch the parent's queued interactions. Returns null
|
|
111
|
+
// when no interactions are queued.
|
|
112
|
+
const extractionPayloadProvider = (sessionID: string): string | null => {
|
|
113
|
+
const interactions = peekQueue(sessionID);
|
|
114
|
+
if (interactions.length === 0) return null;
|
|
115
|
+
return buildExtractionPayload(interactions, repo);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Drain a session's file-backed queue and reset its missed-nudge counter.
|
|
119
|
+
// Called by extraction_done when a sub-agent passes the parent's session_id.
|
|
120
|
+
// Without this, the MCP path's appendBatch self-detection would drain the
|
|
121
|
+
// sub-agent's (empty) queue instead of the parent's.
|
|
122
|
+
const drainExtractionQueue = (sessionID: string): void => {
|
|
123
|
+
resetMissedCount(sessionID);
|
|
124
|
+
consumeQueue(sessionID);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const ctx: CoreContext = {
|
|
128
|
+
db,
|
|
129
|
+
model,
|
|
130
|
+
defaultStore: repo,
|
|
131
|
+
extractionPayloadProvider,
|
|
132
|
+
drainExtractionQueue,
|
|
133
|
+
};
|
|
102
134
|
const tools = compileTools();
|
|
103
135
|
|
|
104
136
|
// Check whether `thatch setup` was run for the current host. If not, or if
|
|
@@ -112,6 +144,31 @@ export async function runMcpServer(): Promise<void> {
|
|
|
112
144
|
console.error(`[thatch] ${setupWarning}`);
|
|
113
145
|
}
|
|
114
146
|
|
|
147
|
+
// Auto-refresh: if setup was previously run (markers exist), re-run the
|
|
148
|
+
// install functions to update skills, instructions, and hooks that may have
|
|
149
|
+
// drifted since the last `thatch setup`. All operations are idempotent --
|
|
150
|
+
// they only write when content differs. This keeps MCP-host installations
|
|
151
|
+
// up to date without requiring the user to manually re-run setup after
|
|
152
|
+
// upgrading thatch. The MCP config itself is also re-written, but it
|
|
153
|
+
// writes the same command that's already running, so it's a no-op in
|
|
154
|
+
// practice.
|
|
155
|
+
if (setupStatus && setupStatus.status === "installed") {
|
|
156
|
+
const thatchBin = resolve(process.argv[1] ?? process.execPath);
|
|
157
|
+
const isGlobal = setupStatus.scope === "global";
|
|
158
|
+
try {
|
|
159
|
+
if (setupStatus.host === "claude") {
|
|
160
|
+
setupClaudeCode(thatchBin, isGlobal, projectDir);
|
|
161
|
+
} else if (setupStatus.host === "cursor") {
|
|
162
|
+
setupCursor(thatchBin, isGlobal, projectDir);
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
// Auto-refresh is best-effort. A failure here does not affect the
|
|
166
|
+
// MCP server's ability to serve tool calls -- it just means skills
|
|
167
|
+
// or instructions may be stale until the user re-runs setup.
|
|
168
|
+
console.error(`[thatch] auto-refresh of skills/instructions failed: ${err}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
115
172
|
// The sideband socket lets one-shot hook processes (flush-tools) ask the
|
|
116
173
|
// warm MCP server to embed a prompt and search for matches - without
|
|
117
174
|
// loading the ~34 MB model themselves. If the socket can't be opened
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { ScoringEngine, type NudgeItem, PREDICTION_K, PREDICTION_P0, PREDICTION_W_SOFT } from "./scoring-engine";
|
|
3
|
+
|
|
4
|
+
export { PREDICTION_K, PREDICTION_P0, PREDICTION_W_SOFT };
|
|
5
|
+
|
|
6
|
+
export interface MatcherRow {
|
|
7
|
+
id: string;
|
|
8
|
+
store: string;
|
|
9
|
+
description: string;
|
|
10
|
+
embedding: Uint8Array | null;
|
|
11
|
+
model: string | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
updated_at: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PredictionRow {
|
|
17
|
+
id: string;
|
|
18
|
+
store: string;
|
|
19
|
+
statement: string;
|
|
20
|
+
rationale: string | null;
|
|
21
|
+
confidence: number;
|
|
22
|
+
confirm_count: number;
|
|
23
|
+
disconfirm_count: number;
|
|
24
|
+
created_at: string;
|
|
25
|
+
updated_at: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PredictionNudgeItem extends NudgeItem {}
|
|
29
|
+
|
|
30
|
+
export interface ScoredPrediction {
|
|
31
|
+
matcher_id: string;
|
|
32
|
+
matcher_description: string;
|
|
33
|
+
prediction_id: string;
|
|
34
|
+
statement: string;
|
|
35
|
+
confidence: number;
|
|
36
|
+
evidence_count: number;
|
|
37
|
+
score: number;
|
|
38
|
+
rationale: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const config = {
|
|
42
|
+
matchersTable: "prediction_matchers",
|
|
43
|
+
itemsTable: "predictions",
|
|
44
|
+
edgesTable: "prediction_edges",
|
|
45
|
+
provenanceTable: "prediction_provenance",
|
|
46
|
+
itemForeignKey: "prediction_id",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Prediction engine: matchers (context patterns), predictions (graded
|
|
51
|
+
* confidence statements), edges (weighted matcher-to-prediction links),
|
|
52
|
+
* and provenance (audit trail). Delegates to ScoringEngine for all
|
|
53
|
+
* SQL and scoring logic. Wraps the generic return types with
|
|
54
|
+
* prediction-specific type names for caller clarity.
|
|
55
|
+
*/
|
|
56
|
+
export class PredictionEngine {
|
|
57
|
+
#engine: ScoringEngine;
|
|
58
|
+
|
|
59
|
+
constructor(db: Database) {
|
|
60
|
+
this.#engine = new ScoringEngine(db, config);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
findMatchers(stores: string[], queryEmbedding: Float32Array, opts?: { limit?: number }) {
|
|
64
|
+
return this.#engine.findMatchers(stores, queryEmbedding, opts);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
scorePredictions(matchers: { id: string; description: string; score: number }[]): ScoredPrediction[] {
|
|
68
|
+
return this.#engine.scoreItems(matchers).map((s) => ({
|
|
69
|
+
matcher_id: s.matcher_id,
|
|
70
|
+
matcher_description: s.matcher_description,
|
|
71
|
+
prediction_id: s.item_id,
|
|
72
|
+
statement: s.statement,
|
|
73
|
+
confidence: s.confidence,
|
|
74
|
+
evidence_count: s.evidence_count,
|
|
75
|
+
score: s.score,
|
|
76
|
+
rationale: s.rationale,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
scorePredictionNudge(stores: string[], embedding: Float32Array, threshold: number, limit = 5): PredictionNudgeItem[] {
|
|
81
|
+
return this.#engine.scoreNudge(stores, embedding, threshold, limit);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
findNearestMatcher(store: string, embedding: Float32Array, threshold: number): { id: string; description: string } | null {
|
|
85
|
+
return this.#engine.findNearestMatcher(store, embedding, threshold);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
createMatcher(store: string, description: string, embedding: Float32Array, model: string): string {
|
|
89
|
+
return this.#engine.createMatcher(store, description, embedding, model);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
findNearestPrediction(store: string, embedding: Float32Array, threshold: number): PredictionRow | null {
|
|
93
|
+
return this.#engine.findNearestItem(store, embedding, threshold) as PredictionRow | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
createPrediction(store: string, statement: string, rationale: string, embedding: Float32Array, model: string): string {
|
|
97
|
+
return this.#engine.createItem(store, statement, rationale, embedding, model);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
createEdge(matcherId: string, predictionId: string, weight: number): void {
|
|
101
|
+
return this.#engine.createEdge(matcherId, predictionId, weight);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
adjustConfidence(predictionId: string, signal: "confirm" | "disconfirm" | "soft"): void {
|
|
105
|
+
return this.#engine.adjustConfidence(predictionId, signal);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getPrediction(predictionId: string): PredictionRow | null {
|
|
109
|
+
return this.#engine.getItem(predictionId) as PredictionRow | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
addProvenance(predictionId: string, signal: string, detail: string): void {
|
|
113
|
+
return this.#engine.addProvenance(predictionId, signal, detail);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getProvenance(predictionId: string): { signal: string; detail: string | null; created_at: string }[] {
|
|
117
|
+
return this.#engine.getProvenance(predictionId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
deletePrediction(predictionId: string): boolean {
|
|
121
|
+
return this.#engine.deleteItem(predictionId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
listPredictions(store: string) {
|
|
125
|
+
return this.#engine.listItems(store);
|
|
126
|
+
}
|
|
127
|
+
}
|