@jeffober/thatch 0.1.28 → 0.1.30
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-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/release +21 -0
- package/bin/thatch +17 -5
- package/docs/dev/README.md +71 -18
- 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 +6 -4
- 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 +55 -9
- package/src/mcp.ts +30 -1
- package/src/prediction.ts +127 -0
- package/src/prompts.ts +135 -188
- 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 +172 -25
- package/src/vector-math.ts +30 -0
- package/docs/qa/README.md +0 -175
package/src/db.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { blobToVector, cosineSimilarity } from "./vector-math";
|
|
3
|
+
import {
|
|
4
|
+
PredictionEngine,
|
|
5
|
+
} from "./prediction";
|
|
6
|
+
import { BehaviorEngine } from "./behavior";
|
|
7
|
+
import { PREDICTION_K, PREDICTION_P0, PREDICTION_W_SOFT } from "./scoring-engine";
|
|
8
|
+
|
|
9
|
+
export { cosineSimilarity } from "./vector-math";
|
|
10
|
+
export type { PredictionNudgeItem, MatcherRow, PredictionRow, ScoredPrediction } from "./prediction";
|
|
11
|
+
export type { BehaviorNudgeItem, BehaviorRow, ScoredBehavior } from "./behavior";
|
|
2
12
|
|
|
3
13
|
export interface MemoryRow {
|
|
4
14
|
slug: string;
|
|
@@ -34,46 +44,6 @@ export interface DedupCandidate {
|
|
|
34
44
|
score: number;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
export interface MatcherRow {
|
|
38
|
-
id: string;
|
|
39
|
-
store: string;
|
|
40
|
-
description: string;
|
|
41
|
-
embedding: Uint8Array | null;
|
|
42
|
-
model: string | null;
|
|
43
|
-
created_at: string;
|
|
44
|
-
updated_at: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface PredictionRow {
|
|
48
|
-
id: string;
|
|
49
|
-
store: string;
|
|
50
|
-
statement: string;
|
|
51
|
-
rationale: string | null;
|
|
52
|
-
confidence: number;
|
|
53
|
-
confirm_count: number;
|
|
54
|
-
disconfirm_count: number;
|
|
55
|
-
created_at: string;
|
|
56
|
-
updated_at: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface PredictionNudgeItem {
|
|
60
|
-
confidence: number;
|
|
61
|
-
evidence_count: number;
|
|
62
|
-
matcher_description: string;
|
|
63
|
-
statement: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface ScoredPrediction {
|
|
67
|
-
matcher_id: string;
|
|
68
|
-
matcher_description: string;
|
|
69
|
-
prediction_id: string;
|
|
70
|
-
statement: string;
|
|
71
|
-
confidence: number;
|
|
72
|
-
evidence_count: number;
|
|
73
|
-
score: number;
|
|
74
|
-
rationale: string | null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
47
|
/**
|
|
78
48
|
* SQLite-backed store for thatch. All stores live in a single database
|
|
79
49
|
* partitioned by a `store` column. Embeddings are raw Float32Array bytes
|
|
@@ -81,6 +51,8 @@ export interface ScoredPrediction {
|
|
|
81
51
|
*/
|
|
82
52
|
export class ThatchDB {
|
|
83
53
|
#db: Database;
|
|
54
|
+
#predictions: PredictionEngine;
|
|
55
|
+
#behaviors: BehaviorEngine;
|
|
84
56
|
|
|
85
57
|
constructor(path: string) {
|
|
86
58
|
this.#db = new Database(path, { create: true });
|
|
@@ -92,6 +64,8 @@ export class ThatchDB {
|
|
|
92
64
|
// existing FK to stores(name), which was declarative but unenforced
|
|
93
65
|
// before this branch.
|
|
94
66
|
this.#db.run("PRAGMA foreign_keys = ON");
|
|
67
|
+
this.#predictions = new PredictionEngine(this.#db);
|
|
68
|
+
this.#behaviors = new BehaviorEngine(this.#db);
|
|
95
69
|
this.#initSchema();
|
|
96
70
|
}
|
|
97
71
|
|
|
@@ -187,6 +161,63 @@ export class ThatchDB {
|
|
|
187
161
|
)
|
|
188
162
|
`);
|
|
189
163
|
|
|
164
|
+
// Behavior engine: the LLM's codified self-discipline rules. Same
|
|
165
|
+
// shape as the prediction engine (matchers, behaviors, edges,
|
|
166
|
+
// provenance) but separate tables because the semantics differ.
|
|
167
|
+
// Predictions model what the USER wants; behaviors model what the
|
|
168
|
+
// LLM should do. The LLM grades its own behaviors (ham/spam), while
|
|
169
|
+
// the user is the ground truth for predictions.
|
|
170
|
+
this.#db.run(`
|
|
171
|
+
CREATE TABLE IF NOT EXISTS behavior_matchers (
|
|
172
|
+
id TEXT PRIMARY KEY,
|
|
173
|
+
store TEXT NOT NULL REFERENCES stores(name),
|
|
174
|
+
description TEXT NOT NULL,
|
|
175
|
+
embedding BLOB,
|
|
176
|
+
model TEXT,
|
|
177
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
|
178
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
179
|
+
)
|
|
180
|
+
`);
|
|
181
|
+
|
|
182
|
+
this.#db.run(`
|
|
183
|
+
CREATE TABLE IF NOT EXISTS behaviors (
|
|
184
|
+
id TEXT PRIMARY KEY,
|
|
185
|
+
store TEXT NOT NULL REFERENCES stores(name),
|
|
186
|
+
statement TEXT NOT NULL,
|
|
187
|
+
rationale TEXT,
|
|
188
|
+
embedding BLOB,
|
|
189
|
+
model TEXT,
|
|
190
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
191
|
+
confirm_count REAL NOT NULL DEFAULT 0,
|
|
192
|
+
disconfirm_count REAL NOT NULL DEFAULT 0,
|
|
193
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
|
194
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
195
|
+
)
|
|
196
|
+
`);
|
|
197
|
+
|
|
198
|
+
this.#db.run(`
|
|
199
|
+
CREATE TABLE IF NOT EXISTS behavior_edges (
|
|
200
|
+
matcher_id TEXT NOT NULL,
|
|
201
|
+
behavior_id TEXT NOT NULL,
|
|
202
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
203
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
|
204
|
+
PRIMARY KEY (matcher_id, behavior_id),
|
|
205
|
+
FOREIGN KEY (matcher_id) REFERENCES behavior_matchers(id) ON DELETE CASCADE,
|
|
206
|
+
FOREIGN KEY (behavior_id) REFERENCES behaviors(id) ON DELETE CASCADE
|
|
207
|
+
)
|
|
208
|
+
`);
|
|
209
|
+
|
|
210
|
+
this.#db.run(`
|
|
211
|
+
CREATE TABLE IF NOT EXISTS behavior_provenance (
|
|
212
|
+
id TEXT PRIMARY KEY,
|
|
213
|
+
behavior_id TEXT NOT NULL,
|
|
214
|
+
signal TEXT NOT NULL,
|
|
215
|
+
detail TEXT,
|
|
216
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
|
217
|
+
FOREIGN KEY (behavior_id) REFERENCES behaviors(id) ON DELETE CASCADE
|
|
218
|
+
)
|
|
219
|
+
`);
|
|
220
|
+
|
|
190
221
|
this.#db.run("INSERT OR IGNORE INTO stores (name) VALUES ('global')");
|
|
191
222
|
|
|
192
223
|
this.#migrateColumns();
|
|
@@ -554,380 +585,127 @@ export class ThatchDB {
|
|
|
554
585
|
}
|
|
555
586
|
|
|
556
587
|
// ---------------------------------------------------------------------------
|
|
557
|
-
// Prediction engine:
|
|
588
|
+
// Prediction engine: delegates to PredictionEngine
|
|
558
589
|
// ---------------------------------------------------------------------------
|
|
559
590
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
// pseudo-evidence "anchors" the prior. P0 is the prior probability
|
|
564
|
-
// (p=0.5 = "no preference either way"). W_SOFT is the fractional
|
|
565
|
-
// weight for a soft (weak) signal: 0.25 means a soft disconfirm counts
|
|
566
|
-
// as 1/4 of a full disconfirm.
|
|
567
|
-
static readonly PREDICTION_K = 5;
|
|
568
|
-
static readonly PREDICTION_P0 = 0.5;
|
|
569
|
-
static readonly PREDICTION_W_SOFT = 0.25;
|
|
591
|
+
static readonly PREDICTION_K = PREDICTION_K;
|
|
592
|
+
static readonly PREDICTION_P0 = PREDICTION_P0;
|
|
593
|
+
static readonly PREDICTION_W_SOFT = PREDICTION_W_SOFT;
|
|
570
594
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
* filtered by a noise floor (cosine >= 0.01) to exclude near-zero
|
|
575
|
-
* and negative scores, and by model-space compatibility (dimension
|
|
576
|
-
* match). Callers apply the actual relevance threshold (0.45 for
|
|
577
|
-
* auto-fire in index.ts, caller-specified for sideband).
|
|
578
|
-
*/
|
|
579
|
-
findMatchers(
|
|
580
|
-
stores: string[],
|
|
581
|
-
queryEmbedding: Float32Array,
|
|
582
|
-
opts?: { limit?: number },
|
|
583
|
-
): { id: string; description: string; score: number }[] {
|
|
584
|
-
if (stores.length === 0) return [];
|
|
585
|
-
const limit = opts?.limit ?? 5;
|
|
586
|
-
const placeholders = stores.map(() => "?").join(", ");
|
|
587
|
-
const rows = this.#db
|
|
588
|
-
.query(
|
|
589
|
-
`SELECT id, description, embedding FROM prediction_matchers
|
|
590
|
-
WHERE store IN (${placeholders}) AND embedding IS NOT NULL`,
|
|
591
|
-
)
|
|
592
|
-
.all(...(stores as [string, ...string[]])) as any[];
|
|
595
|
+
findMatchers(stores: string[], queryEmbedding: Float32Array, opts?: { limit?: number }) {
|
|
596
|
+
return this.#predictions.findMatchers(stores, queryEmbedding, opts);
|
|
597
|
+
}
|
|
593
598
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const emb = blobToVector(r.embedding);
|
|
597
|
-
if (emb.length !== queryEmbedding.length) continue;
|
|
598
|
-
const score = cosineSimilarity(queryEmbedding, emb);
|
|
599
|
-
if (score >= 0.01) {
|
|
600
|
-
scored.push({ id: r.id, description: r.description, score: Math.round(score * 1000) / 1000 });
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
scored.sort((a, b) => b.score - a.score);
|
|
604
|
-
return scored.slice(0, limit);
|
|
599
|
+
scorePredictions(matchers: { id: string; description: string; score: number }[]) {
|
|
600
|
+
return this.#predictions.scorePredictions(matchers);
|
|
605
601
|
}
|
|
606
602
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
* cosine * weight * confidence.
|
|
611
|
-
*/
|
|
612
|
-
scorePredictions(
|
|
613
|
-
matchers: { id: string; description: string; score: number }[],
|
|
614
|
-
): ScoredPrediction[] {
|
|
615
|
-
if (matchers.length === 0) return [];
|
|
616
|
-
const matcherIds = matchers.map((m) => m.id);
|
|
617
|
-
const placeholders = matcherIds.map(() => "?").join(", ");
|
|
618
|
-
const rows = this.#db
|
|
619
|
-
.query(
|
|
620
|
-
`SELECT p.id, p.statement, p.rationale, p.confidence, p.confirm_count, p.disconfirm_count,
|
|
621
|
-
e.matcher_id, e.weight
|
|
622
|
-
FROM prediction_edges e
|
|
623
|
-
JOIN predictions p ON e.prediction_id = p.id
|
|
624
|
-
WHERE e.matcher_id IN (${placeholders})
|
|
625
|
-
ORDER BY p.statement`,
|
|
626
|
-
)
|
|
627
|
-
.all(...(matcherIds as [string, ...string[]])) as any[];
|
|
603
|
+
scorePredictionNudge(stores: string[], embedding: Float32Array, threshold: number, limit = 5) {
|
|
604
|
+
return this.#predictions.scorePredictionNudge(stores, embedding, threshold, limit);
|
|
605
|
+
}
|
|
628
606
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
for (const r of rows) {
|
|
632
|
-
const matcher = matcherMap.get(r.matcher_id);
|
|
633
|
-
if (!matcher) continue;
|
|
634
|
-
|
|
635
|
-
const confidence = r.confidence as number;
|
|
636
|
-
const evidence = Math.round(r.confirm_count + r.disconfirm_count);
|
|
637
|
-
const score = matcher.score * (r.weight as number) * confidence;
|
|
638
|
-
scored.push({
|
|
639
|
-
matcher_id: r.matcher_id,
|
|
640
|
-
matcher_description: matcher.description,
|
|
641
|
-
prediction_id: r.id,
|
|
642
|
-
statement: r.statement,
|
|
643
|
-
confidence: Math.round(confidence * 1000) / 1000,
|
|
644
|
-
evidence_count: evidence,
|
|
645
|
-
score: Math.round(score * 1000) / 1000,
|
|
646
|
-
rationale: r.rationale,
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
scored.sort((a, b) => b.score - a.score);
|
|
650
|
-
|
|
651
|
-
// Dedup by prediction_id: multiple matchers may link to the same
|
|
652
|
-
// prediction via separate edges. Keep only the highest-scoring
|
|
653
|
-
// entry per prediction so the nudge doesn't repeat the same
|
|
654
|
-
// prediction with different matcher contexts.
|
|
655
|
-
const seen = new Set<string>();
|
|
656
|
-
return scored.filter((s) => {
|
|
657
|
-
if (seen.has(s.prediction_id)) return false;
|
|
658
|
-
seen.add(s.prediction_id);
|
|
659
|
-
return true;
|
|
660
|
-
});
|
|
607
|
+
findNearestMatcher(store: string, embedding: Float32Array, threshold: number) {
|
|
608
|
+
return this.#predictions.findNearestMatcher(store, embedding, threshold);
|
|
661
609
|
}
|
|
662
610
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
* filter by threshold, scorePredictions (which includes dedup by
|
|
666
|
-
* prediction_id), slice, and map to PredictionNudgeItem. Shared by
|
|
667
|
-
* index.ts (auto-fire) and sideband.ts (MCP path) to prevent
|
|
668
|
-
* scoring-logic drift between the two host paths.
|
|
669
|
-
*/
|
|
670
|
-
scorePredictionNudge(
|
|
671
|
-
stores: string[],
|
|
672
|
-
embedding: Float32Array,
|
|
673
|
-
threshold: number,
|
|
674
|
-
limit = 5,
|
|
675
|
-
): PredictionNudgeItem[] {
|
|
676
|
-
const matchers = this.findMatchers(stores, embedding, { limit })
|
|
677
|
-
.filter((m) => m.score >= threshold);
|
|
678
|
-
if (matchers.length === 0) return [];
|
|
679
|
-
return this.scorePredictions(matchers)
|
|
680
|
-
.slice(0, limit)
|
|
681
|
-
.map((s) => ({
|
|
682
|
-
confidence: s.confidence,
|
|
683
|
-
evidence_count: s.evidence_count,
|
|
684
|
-
matcher_description: s.matcher_description,
|
|
685
|
-
statement: s.statement,
|
|
686
|
-
}));
|
|
611
|
+
createMatcher(store: string, description: string, embedding: Float32Array, model: string) {
|
|
612
|
+
return this.#predictions.createMatcher(store, description, embedding, model);
|
|
687
613
|
}
|
|
688
614
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
* (threshold 0.85), not auto-fire (use findMatchers for that).
|
|
693
|
-
*/
|
|
694
|
-
findNearestMatcher(
|
|
695
|
-
store: string,
|
|
696
|
-
embedding: Float32Array,
|
|
697
|
-
threshold: number,
|
|
698
|
-
): MatcherRow | null {
|
|
699
|
-
const rows = this.#db
|
|
700
|
-
.query("SELECT id, store, description, embedding, model, created_at, updated_at FROM prediction_matchers WHERE store = ? AND embedding IS NOT NULL")
|
|
701
|
-
.all(store) as any[];
|
|
615
|
+
findNearestPrediction(store: string, embedding: Float32Array, threshold: number) {
|
|
616
|
+
return this.#predictions.findNearestPrediction(store, embedding, threshold);
|
|
617
|
+
}
|
|
702
618
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const emb = blobToVector(r.embedding);
|
|
706
|
-
if (emb.length !== embedding.length) continue;
|
|
707
|
-
const score = cosineSimilarity(embedding, emb);
|
|
708
|
-
if (score >= threshold && (!best || score > best.score)) {
|
|
709
|
-
best = { row: r, score };
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
if (!best) return null;
|
|
713
|
-
return best.row as MatcherRow;
|
|
619
|
+
createPrediction(store: string, statement: string, rationale: string, embedding: Float32Array, model: string) {
|
|
620
|
+
return this.#predictions.createPrediction(store, statement, rationale, embedding, model);
|
|
714
621
|
}
|
|
715
622
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
store: string,
|
|
719
|
-
description: string,
|
|
720
|
-
embedding: Float32Array,
|
|
721
|
-
model: string,
|
|
722
|
-
): string {
|
|
723
|
-
this.ensureStore(store);
|
|
724
|
-
const id = crypto.randomUUID();
|
|
725
|
-
const blob = new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
726
|
-
const now = new Date().toISOString();
|
|
727
|
-
this.#db.run(
|
|
728
|
-
"INSERT INTO prediction_matchers (id, store, description, embedding, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
729
|
-
[id, store, description, blob, model, now, now] as any,
|
|
730
|
-
);
|
|
731
|
-
return id;
|
|
623
|
+
createEdge(matcherId: string, predictionId: string, weight: number) {
|
|
624
|
+
return this.#predictions.createEdge(matcherId, predictionId, weight);
|
|
732
625
|
}
|
|
733
626
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
* prevents duplicate predictions: when a new matcher is created,
|
|
738
|
-
* it can link to an existing prediction via a new edge rather
|
|
739
|
-
* than creating a second row with the same statement.
|
|
740
|
-
*/
|
|
741
|
-
findNearestPrediction(
|
|
742
|
-
store: string,
|
|
743
|
-
embedding: Float32Array,
|
|
744
|
-
threshold: number,
|
|
745
|
-
): PredictionRow | null {
|
|
746
|
-
const rows = this.#db
|
|
747
|
-
.query(
|
|
748
|
-
`SELECT id, store, statement, rationale, confidence, confirm_count, disconfirm_count, created_at, updated_at, embedding
|
|
749
|
-
FROM predictions
|
|
750
|
-
WHERE store = ? AND embedding IS NOT NULL`,
|
|
751
|
-
)
|
|
752
|
-
.all(store) as any[];
|
|
627
|
+
adjustConfidence(predictionId: string, signal: "confirm" | "disconfirm" | "soft") {
|
|
628
|
+
return this.#predictions.adjustConfidence(predictionId, signal);
|
|
629
|
+
}
|
|
753
630
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const emb = blobToVector(r.embedding);
|
|
757
|
-
if (emb.length !== embedding.length) continue;
|
|
758
|
-
const score = cosineSimilarity(embedding, emb);
|
|
759
|
-
if (score >= threshold && (!best || score > best.score)) {
|
|
760
|
-
best = { row: r, score };
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
if (!best) return null;
|
|
764
|
-
const { embedding: _, ...rest } = best.row;
|
|
765
|
-
return rest as PredictionRow;
|
|
631
|
+
getPrediction(predictionId: string) {
|
|
632
|
+
return this.#predictions.getPrediction(predictionId);
|
|
766
633
|
}
|
|
767
634
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
* at p0 (the population prior).
|
|
771
|
-
*/
|
|
772
|
-
createPrediction(
|
|
773
|
-
store: string,
|
|
774
|
-
statement: string,
|
|
775
|
-
rationale: string,
|
|
776
|
-
embedding: Float32Array,
|
|
777
|
-
model: string,
|
|
778
|
-
): string {
|
|
779
|
-
this.ensureStore(store);
|
|
780
|
-
const id = crypto.randomUUID();
|
|
781
|
-
const blob = new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
782
|
-
const now = new Date().toISOString();
|
|
783
|
-
this.#db.run(
|
|
784
|
-
`INSERT INTO predictions (id, store, statement, rationale, embedding, model, confidence, confirm_count, disconfirm_count, created_at, updated_at)
|
|
785
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)`,
|
|
786
|
-
[id, store, statement, rationale, blob, model, ThatchDB.PREDICTION_P0, now, now] as any,
|
|
787
|
-
);
|
|
788
|
-
return id;
|
|
635
|
+
addProvenance(predictionId: string, signal: string, detail: string) {
|
|
636
|
+
return this.#predictions.addProvenance(predictionId, signal, detail);
|
|
789
637
|
}
|
|
790
638
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const now = new Date().toISOString();
|
|
794
|
-
this.#db.run(
|
|
795
|
-
`INSERT INTO prediction_edges (matcher_id, prediction_id, weight, created_at)
|
|
796
|
-
VALUES (?, ?, ?, ?)
|
|
797
|
-
ON CONFLICT(matcher_id, prediction_id) DO NOTHING`,
|
|
798
|
-
[matcherId, predictionId, weight, now] as any,
|
|
799
|
-
);
|
|
639
|
+
getProvenance(predictionId: string) {
|
|
640
|
+
return this.#predictions.getProvenance(predictionId);
|
|
800
641
|
}
|
|
801
642
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
* recomputing the Bayesian posterior. Mirrors samskara's
|
|
805
|
-
* samskara_apply_evaluation: discount prior evidence is NOT applied
|
|
806
|
-
* in v1 (no wall-clock decay), so counts accumulate monotonically.
|
|
807
|
-
*
|
|
808
|
-
* Signal mapping: "confirm" adds 1 to confirm_count; "disconfirm"
|
|
809
|
-
* adds 1 to disconfirm_count; "soft" adds W_SOFT (0.25) to
|
|
810
|
-
* disconfirm_count. The asymmetry (soft is a weak disconfirm, not
|
|
811
|
-
* a weak confirm) is intentional: a soft signal means the user
|
|
812
|
-
* partially disagreed, not partially agreed. There is no soft
|
|
813
|
-
* confirm; use "confirm" for weak agreement.
|
|
814
|
-
*/
|
|
815
|
-
adjustConfidence(predictionId: string, signal: "confirm" | "disconfirm" | "soft"): void {
|
|
816
|
-
const deltaConfirm = signal === "confirm" ? 1 : 0;
|
|
817
|
-
const deltaDisconfirm = signal === "disconfirm" ? 1 : (signal === "soft" ? ThatchDB.PREDICTION_W_SOFT : 0);
|
|
818
|
-
const k = ThatchDB.PREDICTION_K;
|
|
819
|
-
const p0 = ThatchDB.PREDICTION_P0;
|
|
820
|
-
const now = new Date().toISOString();
|
|
821
|
-
// Atomic UPDATE: uses the OLD column values in the confidence
|
|
822
|
-
// expression, so (confirm_count + deltaConfirm) equals the new
|
|
823
|
-
// confirm_count. No read-modify-write race between connections.
|
|
824
|
-
this.#db.run(
|
|
825
|
-
`UPDATE predictions
|
|
826
|
-
SET confirm_count = confirm_count + ?,
|
|
827
|
-
disconfirm_count = disconfirm_count + ?,
|
|
828
|
-
confidence = (confirm_count + ? + ? * ?) / (confirm_count + ? + disconfirm_count + ? + ?),
|
|
829
|
-
updated_at = ?
|
|
830
|
-
WHERE id = ?`,
|
|
831
|
-
[deltaConfirm, deltaDisconfirm, deltaConfirm, k, p0, deltaConfirm, deltaDisconfirm, k, now, predictionId] as any,
|
|
832
|
-
);
|
|
643
|
+
deletePrediction(predictionId: string) {
|
|
644
|
+
return this.#predictions.deletePrediction(predictionId);
|
|
833
645
|
}
|
|
834
646
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
* PREDICTION_P0 (0.5) when total evidence < 20; otherwise returns
|
|
838
|
-
* confirm_count / total_evidence. v2 feature: not yet wired into
|
|
839
|
-
* createPrediction, which uses flat PREDICTION_P0. v1 uses the
|
|
840
|
-
* flat prior until enough evidence exists to derive a population rate.
|
|
841
|
-
*/
|
|
842
|
-
populationP0(store: string): number {
|
|
843
|
-
const row = this.#db
|
|
844
|
-
.query(
|
|
845
|
-
`SELECT SUM(confirm_count) AS c, SUM(disconfirm_count) AS d FROM predictions WHERE store = ?`,
|
|
846
|
-
)
|
|
847
|
-
.get(store) as { c: number | null; d: number | null } | null;
|
|
848
|
-
const total = (row?.c ?? 0) + (row?.d ?? 0);
|
|
849
|
-
if (total < 20) return ThatchDB.PREDICTION_P0;
|
|
850
|
-
return row!.c! / total;
|
|
647
|
+
listPredictions(store: string) {
|
|
648
|
+
return this.#predictions.listPredictions(store);
|
|
851
649
|
}
|
|
852
650
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const row = this.#db
|
|
860
|
-
.query(
|
|
861
|
-
"SELECT id, store, statement, rationale, confidence, confirm_count, disconfirm_count, created_at, updated_at FROM predictions WHERE id = ?",
|
|
862
|
-
)
|
|
863
|
-
.get(predictionId) as any;
|
|
864
|
-
if (!row) return null;
|
|
865
|
-
return row as PredictionRow;
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
// Behavior engine: delegates to BehaviorEngine
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
|
|
655
|
+
findBehaviorMatchers(stores: string[], queryEmbedding: Float32Array, opts?: { limit?: number }) {
|
|
656
|
+
return this.#behaviors.findBehaviorMatchers(stores, queryEmbedding, opts);
|
|
866
657
|
}
|
|
867
658
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
* Provenance is an audit trail for the inspector and agent.
|
|
871
|
-
*/
|
|
872
|
-
addProvenance(predictionId: string, signal: string, detail: string): void {
|
|
873
|
-
const id = crypto.randomUUID();
|
|
874
|
-
this.#db.run(
|
|
875
|
-
"INSERT INTO prediction_provenance (id, prediction_id, signal, detail) VALUES (?, ?, ?, ?)",
|
|
876
|
-
[id, predictionId, signal, detail] as any,
|
|
877
|
-
);
|
|
659
|
+
scoreBehaviors(matchers: { id: string; description: string; score: number }[]) {
|
|
660
|
+
return this.#behaviors.scoreBehaviors(matchers);
|
|
878
661
|
}
|
|
879
662
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
.query("SELECT signal, detail, created_at FROM prediction_provenance WHERE prediction_id = ? ORDER BY rowid DESC LIMIT 10")
|
|
884
|
-
.all(predictionId) as any[];
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/** Deletes a prediction. Edges and provenance cascade via FK ON DELETE CASCADE. */
|
|
888
|
-
deletePrediction(predictionId: string): boolean {
|
|
889
|
-
const result = this.#db.run("DELETE FROM predictions WHERE id = ?", [predictionId]);
|
|
890
|
-
return result.changes > 0;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
/** Lists all predictions with their matchers, for the inspector. */
|
|
894
|
-
listPredictions(store: string): {
|
|
895
|
-
id: string;
|
|
896
|
-
statement: string;
|
|
897
|
-
rationale: string | null;
|
|
898
|
-
confidence: number;
|
|
899
|
-
evidence_count: number;
|
|
900
|
-
matchers: { id: string; description: string; weight: number }[];
|
|
901
|
-
}[] {
|
|
902
|
-
const predRows = this.#db
|
|
903
|
-
.query(
|
|
904
|
-
`SELECT id, statement, rationale, confidence, confirm_count, disconfirm_count
|
|
905
|
-
FROM predictions WHERE store = ? ORDER BY confidence DESC`,
|
|
906
|
-
)
|
|
907
|
-
.all(store) as any[];
|
|
663
|
+
scoreBehaviorNudge(stores: string[], embedding: Float32Array, threshold: number, limit = 5) {
|
|
664
|
+
return this.#behaviors.scoreBehaviorNudge(stores, embedding, threshold, limit);
|
|
665
|
+
}
|
|
908
666
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
667
|
+
findNearestBehaviorMatcher(store: string, embedding: Float32Array, threshold: number) {
|
|
668
|
+
return this.#behaviors.findNearestBehaviorMatcher(store, embedding, threshold);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
createBehaviorMatcher(store: string, description: string, embedding: Float32Array, model: string) {
|
|
672
|
+
return this.#behaviors.createBehaviorMatcher(store, description, embedding, model);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
findNearestBehavior(store: string, embedding: Float32Array, threshold: number) {
|
|
676
|
+
return this.#behaviors.findNearestBehavior(store, embedding, threshold);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
createBehavior(store: string, statement: string, rationale: string, embedding: Float32Array, model: string) {
|
|
680
|
+
return this.#behaviors.createBehavior(store, statement, rationale, embedding, model);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
createBehaviorEdge(matcherId: string, behaviorId: string, weight: number) {
|
|
684
|
+
return this.#behaviors.createBehaviorEdge(matcherId, behaviorId, weight);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
adjustBehaviorConfidence(behaviorId: string, signal: "confirm" | "disconfirm" | "soft") {
|
|
688
|
+
return this.#behaviors.adjustBehaviorConfidence(behaviorId, signal);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
getBehavior(behaviorId: string) {
|
|
692
|
+
return this.#behaviors.getBehavior(behaviorId);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
addBehaviorProvenance(behaviorId: string, signal: string, detail: string) {
|
|
696
|
+
return this.#behaviors.addBehaviorProvenance(behaviorId, signal, detail);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
getBehaviorProvenance(behaviorId: string) {
|
|
700
|
+
return this.#behaviors.getBehaviorProvenance(behaviorId);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
deleteBehavior(behaviorId: string) {
|
|
704
|
+
return this.#behaviors.deleteBehavior(behaviorId);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
listBehaviors(store: string) {
|
|
708
|
+
return this.#behaviors.listBehaviors(store);
|
|
931
709
|
}
|
|
932
710
|
|
|
933
711
|
// ---------------------------------------------------------------------------
|
|
@@ -985,38 +763,3 @@ export class ThatchDB {
|
|
|
985
763
|
this.#db.close();
|
|
986
764
|
}
|
|
987
765
|
}
|
|
988
|
-
|
|
989
|
-
// ---------------------------------------------------------------------------
|
|
990
|
-
// Vector math
|
|
991
|
-
// ---------------------------------------------------------------------------
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* Reconstructs a Float32Array from a stored BLOB. The Uint8Array bun:sqlite
|
|
995
|
-
* hands back may itself be a view, so honor its offset and length rather than
|
|
996
|
-
* reading the whole backing buffer.
|
|
997
|
-
*/
|
|
998
|
-
function blobToVector(blob: Uint8Array): Float32Array {
|
|
999
|
-
return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
/**
|
|
1003
|
-
* Cosine similarity between two Float32Array vectors of equal dimension.
|
|
1004
|
-
* Returns a value in [-1, 1]. Normalized embeddings will be close to [0, 1].
|
|
1005
|
-
* Throws on dimension mismatch - comparing vectors from different embedding
|
|
1006
|
-
* spaces is always a caller bug; callers filter by length first.
|
|
1007
|
-
*/
|
|
1008
|
-
export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
|
|
1009
|
-
if (a.length !== b.length) {
|
|
1010
|
-
throw new Error(`cosineSimilarity: dimension mismatch (${a.length} vs ${b.length})`);
|
|
1011
|
-
}
|
|
1012
|
-
let dot = 0;
|
|
1013
|
-
let magA = 0;
|
|
1014
|
-
let magB = 0;
|
|
1015
|
-
for (let i = 0; i < a.length; i++) {
|
|
1016
|
-
dot += a[i] * b[i];
|
|
1017
|
-
magA += a[i] * a[i];
|
|
1018
|
-
magB += b[i] * b[i];
|
|
1019
|
-
}
|
|
1020
|
-
const denominator = Math.sqrt(magA) * Math.sqrt(magB);
|
|
1021
|
-
return denominator === 0 ? 0 : dot / denominator;
|
|
1022
|
-
}
|