@jeffober/thatch 0.1.28 → 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-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 +10 -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/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
|
|
@@ -328,13 +344,14 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
328
344
|
// 4. Per-message nudge - two priority tiers:
|
|
329
345
|
// a. Extraction nudge: prior tool interactions are queued for fact
|
|
330
346
|
// extraction (carries the JSON payload for thatch-fact-extractor).
|
|
331
|
-
// b. Recall + prediction nudge: when no extraction is pending, embed
|
|
332
|
-
// 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:
|
|
333
349
|
// - search memories by cosine (recall nudge)
|
|
334
350
|
// - search matchers by cosine, score predictions (prediction fire)
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
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.
|
|
338
355
|
// No extra model calls.
|
|
339
356
|
//
|
|
340
357
|
// Skipped during compaction: the agent can't call tools while generating
|
|
@@ -448,6 +465,35 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
448
465
|
} catch (err) {
|
|
449
466
|
console.error(`[thatch] prediction nudge failed: ${err}`);
|
|
450
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
|
+
}
|
|
451
497
|
} catch (err) {
|
|
452
498
|
console.error(`[thatch] nudge hook failed: ${err}`);
|
|
453
499
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -1,12 +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";
|
|
8
9
|
import { peekQueue, consumeQueue, resetMissedCount } from "./extract-queue";
|
|
9
10
|
import { buildExtractionPayload } from "./extraction";
|
|
11
|
+
import { seedDefaultBehaviors } from "./seed-behaviors";
|
|
10
12
|
import pkg from "../package.json";
|
|
11
13
|
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
@@ -101,6 +103,8 @@ export async function runMcpServer(): Promise<void> {
|
|
|
101
103
|
const db = new ThatchDB(dbPath);
|
|
102
104
|
const model = new BgeEmbeddingModel(modelName);
|
|
103
105
|
|
|
106
|
+
await seedDefaultBehaviors(db, model);
|
|
107
|
+
|
|
104
108
|
// Extraction payload provider: peeks the file-backed queue and returns the
|
|
105
109
|
// serialized JSON payload. Called by the get_extraction_payload tool when a
|
|
106
110
|
// sub-agent needs to fetch the parent's queued interactions. Returns null
|
|
@@ -140,6 +144,31 @@ export async function runMcpServer(): Promise<void> {
|
|
|
140
144
|
console.error(`[thatch] ${setupWarning}`);
|
|
141
145
|
}
|
|
142
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
|
+
|
|
143
172
|
// The sideband socket lets one-shot hook processes (flush-tools) ask the
|
|
144
173
|
// warm MCP server to embed a prompt and search for matches - without
|
|
145
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
|
+
}
|