@stackmemoryai/stackmemory 1.8.1 → 1.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -627,6 +627,72 @@ class LinearClient {
627
627
  };
628
628
  }
629
629
  }
630
+ // --- Comment CRUD ---
631
+ async createComment(issueId, body) {
632
+ const mutation = `
633
+ mutation CreateComment($input: CommentCreateInput!) {
634
+ commentCreate(input: $input) {
635
+ success
636
+ comment {
637
+ id
638
+ body
639
+ createdAt
640
+ user { id name }
641
+ }
642
+ }
643
+ }
644
+ `;
645
+ const result = await this.graphql(mutation, { input: { issueId, body } });
646
+ if (!result.commentCreate.success) {
647
+ throw new IntegrationError(
648
+ "Failed to create comment",
649
+ ErrorCode.LINEAR_API_ERROR,
650
+ { issueId }
651
+ );
652
+ }
653
+ return result.commentCreate.comment;
654
+ }
655
+ async updateComment(commentId, body) {
656
+ const mutation = `
657
+ mutation UpdateComment($id: String!, $input: CommentUpdateInput!) {
658
+ commentUpdate(id: $id, input: $input) {
659
+ success
660
+ comment {
661
+ id
662
+ body
663
+ updatedAt
664
+ }
665
+ }
666
+ }
667
+ `;
668
+ const result = await this.graphql(mutation, { id: commentId, input: { body } });
669
+ if (!result.commentUpdate.success) {
670
+ throw new IntegrationError(
671
+ "Failed to update comment",
672
+ ErrorCode.LINEAR_API_ERROR,
673
+ { commentId }
674
+ );
675
+ }
676
+ return result.commentUpdate.comment;
677
+ }
678
+ async getComments(issueId) {
679
+ const query = `
680
+ query GetComments($issueId: String!) {
681
+ issue(id: $issueId) {
682
+ comments(first: 100) {
683
+ nodes {
684
+ id
685
+ body
686
+ createdAt
687
+ user { name }
688
+ }
689
+ }
690
+ }
691
+ }
692
+ `;
693
+ const result = await this.graphql(query, { issueId });
694
+ return result.issue.comments.nodes;
695
+ }
630
696
  }
631
697
  export {
632
698
  LinearClient
@@ -20,6 +20,9 @@ import {
20
20
  } from "./provider-handlers.js";
21
21
  import { TeamHandlers } from "./team-handlers.js";
22
22
  import { CordHandlers } from "./cord-handlers.js";
23
+ import {
24
+ ProvenantHandlers
25
+ } from "./provenant-handlers.js";
23
26
  import {
24
27
  ContextHandlers as ContextHandlers2
25
28
  } from "./context-handlers.js";
@@ -31,6 +34,7 @@ import { TraceHandlers as TraceHandlers2 } from "./trace-handlers.js";
31
34
  import { ProviderHandlers as ProviderHandlers2 } from "./provider-handlers.js";
32
35
  import { TeamHandlers as TeamHandlers2 } from "./team-handlers.js";
33
36
  import { CordHandlers as CordHandlers2 } from "./cord-handlers.js";
37
+ import { ProvenantHandlers as ProvenantHandlers2 } from "./provenant-handlers.js";
34
38
  class MCPHandlerFactory {
35
39
  contextHandlers;
36
40
  taskHandlers;
@@ -39,6 +43,7 @@ class MCPHandlerFactory {
39
43
  providerHandlers;
40
44
  teamHandlers;
41
45
  cordHandlers;
46
+ provenantHandlers;
42
47
  constructor(deps) {
43
48
  this.contextHandlers = new ContextHandlers2({
44
49
  frameManager: deps.frameManager
@@ -67,6 +72,11 @@ class MCPHandlerFactory {
67
72
  dbAdapter: deps.dbAdapter
68
73
  });
69
74
  }
75
+ if (deps.projectDir) {
76
+ this.provenantHandlers = new ProvenantHandlers2({
77
+ projectDir: deps.projectDir
78
+ });
79
+ }
70
80
  }
71
81
  /**
72
82
  * Get handler for a specific tool
@@ -166,6 +176,31 @@ class MCPHandlerFactory {
166
176
  case "cord_tree":
167
177
  if (!this.cordHandlers) throw new Error("Cord tools require dbAdapter");
168
178
  return this.cordHandlers.handleCordTree.bind(this.cordHandlers);
179
+ // Provenant decision graph handlers
180
+ case "provenant_search":
181
+ if (!this.provenantHandlers)
182
+ throw new Error("Provenant tools require projectDir");
183
+ return this.provenantHandlers.handleSearch.bind(this.provenantHandlers);
184
+ case "provenant_log":
185
+ if (!this.provenantHandlers)
186
+ throw new Error("Provenant tools require projectDir");
187
+ return this.provenantHandlers.handleLog.bind(this.provenantHandlers);
188
+ case "provenant_status":
189
+ if (!this.provenantHandlers)
190
+ throw new Error("Provenant tools require projectDir");
191
+ return this.provenantHandlers.handleStatus.bind(this.provenantHandlers);
192
+ case "provenant_contradictions":
193
+ if (!this.provenantHandlers)
194
+ throw new Error("Provenant tools require projectDir");
195
+ return this.provenantHandlers.handleContradictions.bind(
196
+ this.provenantHandlers
197
+ );
198
+ case "provenant_resolve":
199
+ if (!this.provenantHandlers)
200
+ throw new Error("Provenant tools require projectDir");
201
+ return this.provenantHandlers.handleResolve.bind(
202
+ this.provenantHandlers
203
+ );
169
204
  default:
170
205
  throw new Error(`Unknown tool: ${toolName}`);
171
206
  }
@@ -211,7 +246,13 @@ class MCPHandlerFactory {
211
246
  "cord_fork",
212
247
  "cord_complete",
213
248
  "cord_ask",
214
- "cord_tree"
249
+ "cord_tree",
250
+ // Provenant decision graph tools
251
+ "provenant_search",
252
+ "provenant_log",
253
+ "provenant_status",
254
+ "provenant_contradictions",
255
+ "provenant_resolve"
215
256
  ];
216
257
  }
217
258
  /**
@@ -227,6 +268,7 @@ export {
227
268
  DiscoveryHandlers,
228
269
  LinearHandlers,
229
270
  MCPHandlerFactory,
271
+ ProvenantHandlers,
230
272
  ProviderHandlers,
231
273
  TaskHandlers,
232
274
  TeamHandlers,
@@ -218,6 +218,108 @@ ${issueLines.join("\n")}` : "No issues found matching filters.";
218
218
  };
219
219
  }
220
220
  }
221
+ /**
222
+ * Create a comment on a Linear issue
223
+ */
224
+ async handleLinearCreateComment(args) {
225
+ try {
226
+ const { issue_id, body } = args;
227
+ if (!issue_id || !body) {
228
+ throw new Error("issue_id and body are required");
229
+ }
230
+ const client = await this.getClient();
231
+ const comment = await client.createComment(issue_id, body);
232
+ return {
233
+ content: [
234
+ {
235
+ type: "text",
236
+ text: `Comment created on ${issue_id}
237
+ ID: ${comment.id}
238
+ Preview: ${body.slice(0, 100)}${body.length > 100 ? "..." : ""}`
239
+ }
240
+ ],
241
+ metadata: {
242
+ id: comment.id,
243
+ issueId: issue_id,
244
+ createdAt: comment.createdAt
245
+ }
246
+ };
247
+ } catch (error) {
248
+ logger.error(
249
+ "Error creating Linear comment",
250
+ error instanceof Error ? error : new Error(String(error))
251
+ );
252
+ throw error;
253
+ }
254
+ }
255
+ /**
256
+ * Update an existing comment on a Linear issue
257
+ */
258
+ async handleLinearUpdateComment(args) {
259
+ try {
260
+ const { comment_id, body } = args;
261
+ if (!comment_id || !body) {
262
+ throw new Error("comment_id and body are required");
263
+ }
264
+ const client = await this.getClient();
265
+ const comment = await client.updateComment(comment_id, body);
266
+ return {
267
+ content: [
268
+ {
269
+ type: "text",
270
+ text: `Comment ${comment_id} updated
271
+ Preview: ${body.slice(0, 100)}${body.length > 100 ? "..." : ""}`
272
+ }
273
+ ],
274
+ metadata: {
275
+ id: comment.id,
276
+ updatedAt: comment.updatedAt
277
+ }
278
+ };
279
+ } catch (error) {
280
+ logger.error(
281
+ "Error updating Linear comment",
282
+ error instanceof Error ? error : new Error(String(error))
283
+ );
284
+ throw error;
285
+ }
286
+ }
287
+ /**
288
+ * List comments on a Linear issue
289
+ */
290
+ async handleLinearListComments(args) {
291
+ try {
292
+ const { issue_id } = args;
293
+ if (!issue_id) {
294
+ throw new Error("issue_id is required");
295
+ }
296
+ const client = await this.getClient();
297
+ const comments = await client.getComments(issue_id);
298
+ const lines = comments.map(
299
+ (c) => `${c.id.slice(0, 8)} | ${c.user?.name ?? "unknown"} | ${new Date(c.createdAt).toISOString().slice(0, 10)} | ${c.body.slice(0, 60).replace(/\n/g, " ")}${c.body.length > 60 ? "..." : ""}`
300
+ );
301
+ const text = comments.length > 0 ? `${comments.length} comments on ${issue_id}:
302
+ ${lines.join("\n")}` : `No comments on ${issue_id}`;
303
+ return {
304
+ content: [{ type: "text", text }],
305
+ metadata: {
306
+ count: comments.length,
307
+ comments: comments.map((c) => ({
308
+ id: c.id,
309
+ author: c.user?.name,
310
+ createdAt: c.createdAt,
311
+ bodyPreview: c.body.slice(0, 200)
312
+ }))
313
+ }
314
+ };
315
+ } catch (error) {
316
+ logger.error(
317
+ "Error listing Linear comments",
318
+ error instanceof Error ? error : new Error(String(error))
319
+ );
320
+ throw error;
321
+ }
322
+ }
221
323
  }
222
324
  export {
223
325
  LinearHandlers
@@ -0,0 +1,398 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { existsSync, mkdirSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { randomUUID, createHash } from "node:crypto";
8
+ import BetterSqlite3 from "better-sqlite3";
9
+ import { logger } from "../../../core/monitoring/logger.js";
10
+ const SCHEMA_SQL = `
11
+ CREATE TABLE IF NOT EXISTS nodes (
12
+ id TEXT PRIMARY KEY, type TEXT NOT NULL, content TEXT NOT NULL,
13
+ embedding BLOB, actor TEXT, confidence REAL NOT NULL DEFAULT 0.0,
14
+ version INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
15
+ );
16
+ CREATE TABLE IF NOT EXISTS edges (
17
+ id TEXT PRIMARY KEY, from_node TEXT NOT NULL, to_node TEXT NOT NULL,
18
+ rel_type TEXT NOT NULL, confidence REAL NOT NULL DEFAULT 1.0,
19
+ version INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL
20
+ );
21
+ CREATE TABLE IF NOT EXISTS sources (
22
+ id TEXT PRIMARY KEY, system TEXT NOT NULL, external_id TEXT NOT NULL,
23
+ raw_payload TEXT NOT NULL, hash TEXT NOT NULL, fetched_at INTEGER NOT NULL
24
+ );
25
+ CREATE TABLE IF NOT EXISTS source_edges (
26
+ id TEXT PRIMARY KEY, node_id TEXT NOT NULL, source_id TEXT NOT NULL,
27
+ system TEXT NOT NULL, external_id TEXT NOT NULL
28
+ );
29
+ CREATE TABLE IF NOT EXISTS contradictions (
30
+ id TEXT PRIMARY KEY, node_a TEXT NOT NULL, node_b TEXT NOT NULL,
31
+ conflict_score REAL NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
32
+ resolved_by TEXT, resolved_at INTEGER
33
+ );
34
+ CREATE TABLE IF NOT EXISTS stale_flags (
35
+ id TEXT PRIMARY KEY, node_id TEXT NOT NULL, triggered_by_source TEXT NOT NULL,
36
+ flagged_at INTEGER NOT NULL, resolved_at INTEGER
37
+ );
38
+ CREATE TABLE IF NOT EXISTS rejection_log (
39
+ id TEXT PRIMARY KEY, suggestion_node TEXT NOT NULL, override_node TEXT,
40
+ reasoning TEXT, reasoning_resolved INTEGER NOT NULL DEFAULT 0,
41
+ actor TEXT, created_at INTEGER NOT NULL
42
+ );
43
+ CREATE TABLE IF NOT EXISTS review_queue (
44
+ id TEXT PRIMARY KEY, source_id TEXT NOT NULL, candidate_content TEXT NOT NULL,
45
+ confidence REAL NOT NULL, queue_reason TEXT NOT NULL,
46
+ created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, resolved_at INTEGER
47
+ );
48
+ CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL);
49
+ CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type);
50
+ CREATE INDEX IF NOT EXISTS idx_nodes_created ON nodes(created_at DESC);
51
+ CREATE INDEX IF NOT EXISTS idx_contradictions_status ON contradictions(status);
52
+ INSERT OR IGNORE INTO schema_version (version, applied_at) VALUES (1, unixepoch() * 1000);
53
+ `;
54
+ class ProvenantHandlers {
55
+ constructor(deps) {
56
+ this.deps = deps;
57
+ this.dbPath = join(deps.projectDir, ".provenant", "graph.db");
58
+ }
59
+ dbPath;
60
+ openDb() {
61
+ mkdirSync(dirname(this.dbPath), { recursive: true });
62
+ const db = new BetterSqlite3(this.dbPath);
63
+ db.pragma("journal_mode = WAL");
64
+ db.pragma("foreign_keys = ON");
65
+ try {
66
+ db.prepare("SELECT 1 FROM schema_version").get();
67
+ } catch {
68
+ db.exec(SCHEMA_SQL);
69
+ }
70
+ return db;
71
+ }
72
+ async handleSearch(args) {
73
+ try {
74
+ const { query, actor, since, limit = 10 } = args;
75
+ if (!query) throw new Error("query is required");
76
+ const db = this.openDb();
77
+ try {
78
+ const stopwords = /* @__PURE__ */ new Set([
79
+ "the",
80
+ "a",
81
+ "an",
82
+ "is",
83
+ "was",
84
+ "were",
85
+ "are",
86
+ "we",
87
+ "our",
88
+ "this",
89
+ "that",
90
+ "it",
91
+ "to",
92
+ "of",
93
+ "in",
94
+ "for",
95
+ "on",
96
+ "with",
97
+ "did",
98
+ "do",
99
+ "not",
100
+ "why",
101
+ "how",
102
+ "what",
103
+ "when",
104
+ "and",
105
+ "or",
106
+ "but"
107
+ ]);
108
+ const keywords = query.toLowerCase().replace(/[^\w\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopwords.has(w));
109
+ let sql;
110
+ const params = [];
111
+ if (keywords.length === 0) {
112
+ sql = "SELECT * FROM nodes ORDER BY created_at DESC LIMIT ?";
113
+ params.push(limit);
114
+ } else {
115
+ const scoreParts = keywords.map(
116
+ () => "(CASE WHEN LOWER(content) LIKE ? THEN 1 ELSE 0 END)"
117
+ );
118
+ for (const kw of keywords) params.push(`%${kw}%`);
119
+ sql = `SELECT *, (${scoreParts.join(" + ")}) as match_score FROM nodes WHERE (${scoreParts.join(" + ")}) > 0`;
120
+ for (const kw of keywords) params.push(`%${kw}%`);
121
+ if (actor) {
122
+ sql += " AND actor = ?";
123
+ params.push(actor);
124
+ }
125
+ if (since) {
126
+ sql += " AND created_at >= ?";
127
+ params.push(new Date(since).getTime());
128
+ }
129
+ sql += " ORDER BY match_score DESC, created_at DESC LIMIT ?";
130
+ params.push(limit);
131
+ }
132
+ const nodes = db.prepare(sql).all(...params);
133
+ const results = nodes.map((n) => ({
134
+ id: n.id.slice(0, 8),
135
+ type: n.type,
136
+ content: n.content.slice(0, 200),
137
+ actor: n.actor,
138
+ confidence: n.confidence,
139
+ created: new Date(n.created_at).toISOString()
140
+ }));
141
+ logger.info("Provenant search", { query, resultCount: results.length });
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: results.length === 0 ? `No decisions found for: "${query}"` : results.map(
147
+ (r) => `[${r.id}] (${r.confidence.toFixed(2)}) ${r.content}${r.actor ? ` \u2014 ${r.actor}` : ""}`
148
+ ).join("\n\n")
149
+ }
150
+ ],
151
+ metadata: { query, results }
152
+ };
153
+ } finally {
154
+ db.close();
155
+ }
156
+ } catch (error) {
157
+ logger.error(
158
+ "Provenant search error",
159
+ error instanceof Error ? error : new Error(String(error))
160
+ );
161
+ throw error;
162
+ }
163
+ }
164
+ async handleLog(args) {
165
+ try {
166
+ const { content, actor, reasoning } = args;
167
+ if (!content) throw new Error("content is required");
168
+ const db = this.openDb();
169
+ try {
170
+ const now = Date.now();
171
+ const hash = createHash("sha256").update(content).digest("hex");
172
+ const externalId = `manual-${now}`;
173
+ let confidence = 0.6;
174
+ if (reasoning) confidence += 0.15;
175
+ if (actor) confidence += 0.1;
176
+ const sourceId = randomUUID();
177
+ db.prepare(
178
+ "INSERT INTO sources (id, system, external_id, raw_payload, hash, fetched_at) VALUES (?, ?, ?, ?, ?, ?)"
179
+ ).run(
180
+ sourceId,
181
+ "manual",
182
+ externalId,
183
+ JSON.stringify({ content, actor, reasoning }),
184
+ hash,
185
+ now
186
+ );
187
+ const nodeId = randomUUID();
188
+ db.prepare(
189
+ "INSERT INTO nodes (id, type, content, embedding, actor, confidence, version, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
190
+ ).run(
191
+ nodeId,
192
+ "decision",
193
+ content,
194
+ null,
195
+ actor ?? null,
196
+ confidence,
197
+ 1,
198
+ now,
199
+ now
200
+ );
201
+ db.prepare(
202
+ "INSERT INTO source_edges (id, node_id, source_id, system, external_id) VALUES (?, ?, ?, ?, ?)"
203
+ ).run(randomUUID(), nodeId, sourceId, "manual", externalId);
204
+ logger.info("Provenant decision logged", { nodeId, confidence });
205
+ return {
206
+ content: [
207
+ {
208
+ type: "text",
209
+ text: `Logged decision: ${nodeId.slice(0, 8)} (confidence: ${confidence.toFixed(2)})`
210
+ }
211
+ ],
212
+ metadata: { nodeId, confidence }
213
+ };
214
+ } finally {
215
+ db.close();
216
+ }
217
+ } catch (error) {
218
+ logger.error(
219
+ "Provenant log error",
220
+ error instanceof Error ? error : new Error(String(error))
221
+ );
222
+ throw error;
223
+ }
224
+ }
225
+ async handleStatus(_args) {
226
+ try {
227
+ if (!existsSync(this.dbPath)) {
228
+ return {
229
+ content: [
230
+ {
231
+ type: "text",
232
+ text: "No Provenant database found. Use provenant_log to create one."
233
+ }
234
+ ]
235
+ };
236
+ }
237
+ const db = this.openDb();
238
+ try {
239
+ const count = (sql) => db.prepare(sql).get().c;
240
+ const s = {
241
+ nodeCount: count("SELECT COUNT(*) as c FROM nodes"),
242
+ edgeCount: count("SELECT COUNT(*) as c FROM edges"),
243
+ pendingQueue: count(
244
+ "SELECT COUNT(*) as c FROM review_queue WHERE resolved_at IS NULL"
245
+ ),
246
+ unresolvedContradictions: count(
247
+ "SELECT COUNT(*) as c FROM contradictions WHERE status = 'pending'"
248
+ ),
249
+ unresolvedStaleFlags: count(
250
+ "SELECT COUNT(*) as c FROM stale_flags WHERE resolved_at IS NULL"
251
+ ),
252
+ unresolvedRejections: count(
253
+ "SELECT COUNT(*) as c FROM rejection_log WHERE reasoning_resolved = 0"
254
+ )
255
+ };
256
+ return {
257
+ content: [
258
+ {
259
+ type: "text",
260
+ text: `Nodes: ${s.nodeCount}
261
+ Edges: ${s.edgeCount}
262
+ Review queue: ${s.pendingQueue}
263
+ Contradictions: ${s.unresolvedContradictions}
264
+ Stale flags: ${s.unresolvedStaleFlags}
265
+ Rejections needing reasoning: ${s.unresolvedRejections}`
266
+ }
267
+ ],
268
+ metadata: s
269
+ };
270
+ } finally {
271
+ db.close();
272
+ }
273
+ } catch (error) {
274
+ logger.error(
275
+ "Provenant status error",
276
+ error instanceof Error ? error : new Error(String(error))
277
+ );
278
+ throw error;
279
+ }
280
+ }
281
+ async handleContradictions(_args) {
282
+ try {
283
+ if (!existsSync(this.dbPath)) {
284
+ return { content: [{ type: "text", text: "No Provenant database." }] };
285
+ }
286
+ const db = this.openDb();
287
+ try {
288
+ const contras = db.prepare("SELECT * FROM contradictions WHERE status = 'pending'").all();
289
+ if (contras.length === 0) {
290
+ return {
291
+ content: [{ type: "text", text: "No open contradictions." }]
292
+ };
293
+ }
294
+ const lines = contras.map((c) => {
295
+ const a = db.prepare("SELECT content FROM nodes WHERE id = ?").get(c.node_a);
296
+ const b = db.prepare("SELECT content FROM nodes WHERE id = ?").get(c.node_b);
297
+ return `${c.node_a.slice(0, 8)} \u2194 ${c.node_b.slice(0, 8)} (score: ${c.conflict_score.toFixed(2)})
298
+ A: ${a?.content?.slice(0, 100) ?? "?"}
299
+ B: ${b?.content?.slice(0, 100) ?? "?"}`;
300
+ });
301
+ return {
302
+ content: [
303
+ {
304
+ type: "text",
305
+ text: `${contras.length} open contradictions:
306
+
307
+ ${lines.join("\n\n")}`
308
+ }
309
+ ],
310
+ metadata: { count: contras.length }
311
+ };
312
+ } finally {
313
+ db.close();
314
+ }
315
+ } catch (error) {
316
+ logger.error(
317
+ "Provenant contradictions error",
318
+ error instanceof Error ? error : new Error(String(error))
319
+ );
320
+ throw error;
321
+ }
322
+ }
323
+ async handleResolve(args) {
324
+ try {
325
+ const { node_a, node_b, winner, dismiss } = args;
326
+ if (!node_a || !node_b) throw new Error("node_a and node_b are required");
327
+ if (!winner && !dismiss) throw new Error("Specify winner or dismiss");
328
+ const db = this.openDb();
329
+ try {
330
+ const contras = db.prepare("SELECT * FROM contradictions WHERE status = 'pending'").all();
331
+ const contradiction = contras.find(
332
+ (c) => c.node_a.startsWith(node_a) && c.node_b.startsWith(node_b) || c.node_a.startsWith(node_b) && c.node_b.startsWith(node_a)
333
+ );
334
+ if (!contradiction) {
335
+ throw new Error(
336
+ `No pending contradiction between ${node_a} and ${node_b}`
337
+ );
338
+ }
339
+ if (dismiss) {
340
+ db.prepare(
341
+ "UPDATE contradictions SET status = ?, resolved_by = ?, resolved_at = ? WHERE id = ?"
342
+ ).run("dismissed", "human", Date.now(), contradiction.id);
343
+ return {
344
+ content: [
345
+ {
346
+ type: "text",
347
+ text: `Dismissed contradiction ${contradiction.id.slice(0, 8)}`
348
+ }
349
+ ]
350
+ };
351
+ }
352
+ const winnerNode = [contradiction.node_a, contradiction.node_b].find(
353
+ (id) => id.startsWith(winner)
354
+ );
355
+ if (!winnerNode) {
356
+ throw new Error(
357
+ `Winner must match one of: ${contradiction.node_a.slice(0, 8)}, ${contradiction.node_b.slice(0, 8)}`
358
+ );
359
+ }
360
+ const loserNode = winnerNode === contradiction.node_a ? contradiction.node_b : contradiction.node_a;
361
+ db.prepare(
362
+ "UPDATE contradictions SET status = ?, resolved_by = ?, resolved_at = ? WHERE id = ?"
363
+ ).run("resolved", winnerNode, Date.now(), contradiction.id);
364
+ db.prepare(
365
+ "INSERT INTO edges (id, from_node, to_node, rel_type, confidence, version, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
366
+ ).run(
367
+ randomUUID(),
368
+ winnerNode,
369
+ loserNode,
370
+ "supersedes",
371
+ 1,
372
+ 1,
373
+ Date.now()
374
+ );
375
+ return {
376
+ content: [
377
+ {
378
+ type: "text",
379
+ text: `Resolved: ${winnerNode.slice(0, 8)} supersedes ${loserNode.slice(0, 8)}`
380
+ }
381
+ ],
382
+ metadata: { winner: winnerNode, loser: loserNode }
383
+ };
384
+ } finally {
385
+ db.close();
386
+ }
387
+ } catch (error) {
388
+ logger.error(
389
+ "Provenant resolve error",
390
+ error instanceof Error ? error : new Error(String(error))
391
+ );
392
+ throw error;
393
+ }
394
+ }
395
+ }
396
+ export {
397
+ ProvenantHandlers
398
+ };