contradiction-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +536 -0
  3. package/bin/cli.js +182 -0
  4. package/dist/analysis/claimMatcher.d.ts +48 -0
  5. package/dist/analysis/claimMatcher.js +131 -0
  6. package/dist/analysis/claimMatcher.js.map +1 -0
  7. package/dist/analysis/claimRelationship.d.ts +15 -0
  8. package/dist/analysis/claimRelationship.js +12 -0
  9. package/dist/analysis/claimRelationship.js.map +1 -0
  10. package/dist/analysis/confidenceScorer.d.ts +40 -0
  11. package/dist/analysis/confidenceScorer.js +59 -0
  12. package/dist/analysis/confidenceScorer.js.map +1 -0
  13. package/dist/analysis/contextAnalyzer.d.ts +13 -0
  14. package/dist/analysis/contextAnalyzer.js +187 -0
  15. package/dist/analysis/contextAnalyzer.js.map +1 -0
  16. package/dist/analysis/contradictionClassifier.d.ts +16 -0
  17. package/dist/analysis/contradictionClassifier.js +141 -0
  18. package/dist/analysis/contradictionClassifier.js.map +1 -0
  19. package/dist/analysis/contradictionEngine.d.ts +29 -0
  20. package/dist/analysis/contradictionEngine.js +284 -0
  21. package/dist/analysis/contradictionEngine.js.map +1 -0
  22. package/dist/analysis/explanationBuilder.d.ts +36 -0
  23. package/dist/analysis/explanationBuilder.js +77 -0
  24. package/dist/analysis/explanationBuilder.js.map +1 -0
  25. package/dist/analysis/valueComparator.d.ts +38 -0
  26. package/dist/analysis/valueComparator.js +418 -0
  27. package/dist/analysis/valueComparator.js.map +1 -0
  28. package/dist/config/env.d.ts +46 -0
  29. package/dist/config/env.js +34 -0
  30. package/dist/config/env.js.map +1 -0
  31. package/dist/connectors/base/connectorUtils.d.ts +26 -0
  32. package/dist/connectors/base/connectorUtils.js +63 -0
  33. package/dist/connectors/base/connectorUtils.js.map +1 -0
  34. package/dist/connectors/base/security.d.ts +11 -0
  35. package/dist/connectors/base/security.js +9 -0
  36. package/dist/connectors/base/security.js.map +1 -0
  37. package/dist/connectors/connectorRegistry.d.ts +43 -0
  38. package/dist/connectors/connectorRegistry.js +91 -0
  39. package/dist/connectors/connectorRegistry.js.map +1 -0
  40. package/dist/connectors/document/documentConnector.d.ts +45 -0
  41. package/dist/connectors/document/documentConnector.js +410 -0
  42. package/dist/connectors/document/documentConnector.js.map +1 -0
  43. package/dist/connectors/github/githubClient.d.ts +52 -0
  44. package/dist/connectors/github/githubClient.js +207 -0
  45. package/dist/connectors/github/githubClient.js.map +1 -0
  46. package/dist/connectors/github/githubConnector.d.ts +26 -0
  47. package/dist/connectors/github/githubConnector.js +169 -0
  48. package/dist/connectors/github/githubConnector.js.map +1 -0
  49. package/dist/connectors/github/githubExtractor.d.ts +26 -0
  50. package/dist/connectors/github/githubExtractor.js +469 -0
  51. package/dist/connectors/github/githubExtractor.js.map +1 -0
  52. package/dist/connectors/github/githubNormalizer.d.ts +11 -0
  53. package/dist/connectors/github/githubNormalizer.js +24 -0
  54. package/dist/connectors/github/githubNormalizer.js.map +1 -0
  55. package/dist/connectors/github/githubTypes.d.ts +55 -0
  56. package/dist/connectors/github/githubTypes.js +2 -0
  57. package/dist/connectors/github/githubTypes.js.map +1 -0
  58. package/dist/connectors/syncService.d.ts +60 -0
  59. package/dist/connectors/syncService.js +176 -0
  60. package/dist/connectors/syncService.js.map +1 -0
  61. package/dist/connectors/types/connector.d.ts +34 -0
  62. package/dist/connectors/types/connector.js +2 -0
  63. package/dist/connectors/types/connector.js.map +1 -0
  64. package/dist/connectors/types/fetchResult.d.ts +39 -0
  65. package/dist/connectors/types/fetchResult.js +2 -0
  66. package/dist/connectors/types/fetchResult.js.map +1 -0
  67. package/dist/connectors/website/ssrfGuard.d.ts +11 -0
  68. package/dist/connectors/website/ssrfGuard.js +122 -0
  69. package/dist/connectors/website/ssrfGuard.js.map +1 -0
  70. package/dist/connectors/website/websiteConnector.d.ts +30 -0
  71. package/dist/connectors/website/websiteConnector.js +231 -0
  72. package/dist/connectors/website/websiteConnector.js.map +1 -0
  73. package/dist/discovery/candidateGenerator.d.ts +12 -0
  74. package/dist/discovery/candidateGenerator.js +110 -0
  75. package/dist/discovery/candidateGenerator.js.map +1 -0
  76. package/dist/discovery/contradictionScanner.d.ts +30 -0
  77. package/dist/discovery/contradictionScanner.js +47 -0
  78. package/dist/discovery/contradictionScanner.js.map +1 -0
  79. package/dist/discovery/discoveryService.d.ts +37 -0
  80. package/dist/discovery/discoveryService.js +131 -0
  81. package/dist/discovery/discoveryService.js.map +1 -0
  82. package/dist/discovery/rankingService.d.ts +34 -0
  83. package/dist/discovery/rankingService.js +52 -0
  84. package/dist/discovery/rankingService.js.map +1 -0
  85. package/dist/discovery/similarityIndex.d.ts +20 -0
  86. package/dist/discovery/similarityIndex.js +89 -0
  87. package/dist/discovery/similarityIndex.js.map +1 -0
  88. package/dist/domain/entities/claim.d.ts +99 -0
  89. package/dist/domain/entities/claim.js +92 -0
  90. package/dist/domain/entities/claim.js.map +1 -0
  91. package/dist/domain/entities/contradiction.d.ts +110 -0
  92. package/dist/domain/entities/contradiction.js +74 -0
  93. package/dist/domain/entities/contradiction.js.map +1 -0
  94. package/dist/domain/entities/source.d.ts +43 -0
  95. package/dist/domain/entities/source.js +35 -0
  96. package/dist/domain/entities/source.js.map +1 -0
  97. package/dist/domain/types/common.d.ts +46 -0
  98. package/dist/domain/types/common.js +74 -0
  99. package/dist/domain/types/common.js.map +1 -0
  100. package/dist/httpServer.d.ts +25 -0
  101. package/dist/httpServer.js +164 -0
  102. package/dist/httpServer.js.map +1 -0
  103. package/dist/index.d.ts +1 -0
  104. package/dist/index.js +94 -0
  105. package/dist/index.js.map +1 -0
  106. package/dist/intelligence/authorityScorer.d.ts +20 -0
  107. package/dist/intelligence/authorityScorer.js +103 -0
  108. package/dist/intelligence/authorityScorer.js.map +1 -0
  109. package/dist/intelligence/entityResolver.d.ts +33 -0
  110. package/dist/intelligence/entityResolver.js +152 -0
  111. package/dist/intelligence/entityResolver.js.map +1 -0
  112. package/dist/intelligence/evidenceEvaluator.d.ts +15 -0
  113. package/dist/intelligence/evidenceEvaluator.js +63 -0
  114. package/dist/intelligence/evidenceEvaluator.js.map +1 -0
  115. package/dist/intelligence/freshnessScorer.d.ts +25 -0
  116. package/dist/intelligence/freshnessScorer.js +90 -0
  117. package/dist/intelligence/freshnessScorer.js.map +1 -0
  118. package/dist/intelligence/provenanceAnalyzer.d.ts +19 -0
  119. package/dist/intelligence/provenanceAnalyzer.js +33 -0
  120. package/dist/intelligence/provenanceAnalyzer.js.map +1 -0
  121. package/dist/intelligence/resolutionAdvisor.d.ts +40 -0
  122. package/dist/intelligence/resolutionAdvisor.js +118 -0
  123. package/dist/intelligence/resolutionAdvisor.js.map +1 -0
  124. package/dist/intelligence/semanticMatcher.d.ts +21 -0
  125. package/dist/intelligence/semanticMatcher.js +71 -0
  126. package/dist/intelligence/semanticMatcher.js.map +1 -0
  127. package/dist/scripts/comprehensiveVerification.d.ts +5 -0
  128. package/dist/scripts/comprehensiveVerification.js +961 -0
  129. package/dist/scripts/comprehensiveVerification.js.map +1 -0
  130. package/dist/scripts/finalLiveDemo.d.ts +5 -0
  131. package/dist/scripts/finalLiveDemo.js +226 -0
  132. package/dist/scripts/finalLiveDemo.js.map +1 -0
  133. package/dist/scripts/liveMcpSmoke.d.ts +1 -0
  134. package/dist/scripts/liveMcpSmoke.js +89 -0
  135. package/dist/scripts/liveMcpSmoke.js.map +1 -0
  136. package/dist/scripts/seedDemoData.d.ts +6 -0
  137. package/dist/scripts/seedDemoData.js +179 -0
  138. package/dist/scripts/seedDemoData.js.map +1 -0
  139. package/dist/server.d.ts +28 -0
  140. package/dist/server.js +1264 -0
  141. package/dist/server.js.map +1 -0
  142. package/dist/services/analysisService.d.ts +40 -0
  143. package/dist/services/analysisService.js +65 -0
  144. package/dist/services/analysisService.js.map +1 -0
  145. package/dist/services/healthService.d.ts +42 -0
  146. package/dist/services/healthService.js +50 -0
  147. package/dist/services/healthService.js.map +1 -0
  148. package/dist/services/metricsService.d.ts +35 -0
  149. package/dist/services/metricsService.js +57 -0
  150. package/dist/services/metricsService.js.map +1 -0
  151. package/dist/services/rateLimiter.d.ts +23 -0
  152. package/dist/services/rateLimiter.js +45 -0
  153. package/dist/services/rateLimiter.js.map +1 -0
  154. package/dist/services/reviewService.d.ts +32 -0
  155. package/dist/services/reviewService.js +94 -0
  156. package/dist/services/reviewService.js.map +1 -0
  157. package/dist/services/schedulerService.d.ts +30 -0
  158. package/dist/services/schedulerService.js +89 -0
  159. package/dist/services/schedulerService.js.map +1 -0
  160. package/dist/storage/backupService.d.ts +14 -0
  161. package/dist/storage/backupService.js +90 -0
  162. package/dist/storage/backupService.js.map +1 -0
  163. package/dist/storage/database.d.ts +119 -0
  164. package/dist/storage/database.js +935 -0
  165. package/dist/storage/database.js.map +1 -0
  166. package/dist/storage/migrations/001_initial.sql +63 -0
  167. package/dist/storage/migrations/002_discovery_uniqueness.sql +8 -0
  168. package/dist/storage/migrations/003_connector_identity.sql +7 -0
  169. package/dist/storage/migrations/004_claim_context.sql +15 -0
  170. package/dist/storage/migrations/005_claim_history.sql +26 -0
  171. package/dist/storage/migrations/006_contradiction_resolution.sql +27 -0
  172. package/dist/utils/logger.d.ts +20 -0
  173. package/dist/utils/logger.js +48 -0
  174. package/dist/utils/logger.js.map +1 -0
  175. package/package.json +73 -0
@@ -0,0 +1,935 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import crypto from 'node:crypto';
6
+ import { DatabaseError, NotFoundError, ValidationError } from '../domain/types/common.js';
7
+ import { SourceSchema, CreateSourceInputSchema, } from '../domain/entities/source.js';
8
+ import { ClaimSchema, CreateClaimInputSchema, } from '../domain/entities/claim.js';
9
+ import { ContradictionSchema, CreateContradictionInputSchema, } from '../domain/entities/contradiction.js';
10
+ import { logger } from '../utils/logger.js';
11
+ export class DatabaseManager {
12
+ db = null;
13
+ dbPath;
14
+ constructor(dbPath = ':memory:') {
15
+ this.dbPath = dbPath;
16
+ }
17
+ initialize() {
18
+ if (this.db && this.db.open) {
19
+ return;
20
+ }
21
+ try {
22
+ if (this.dbPath !== ':memory:') {
23
+ const dir = path.dirname(this.dbPath);
24
+ if (!fs.existsSync(dir)) {
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ }
27
+ }
28
+ this.db = new Database(this.dbPath);
29
+ // Enforce foreign key constraints
30
+ this.db.pragma('foreign_keys = ON');
31
+ // Set WAL mode for persistent files for concurrency and speed
32
+ if (this.dbPath !== ':memory:') {
33
+ this.db.pragma('journal_mode = WAL');
34
+ }
35
+ this.runMigrations();
36
+ logger.info('Database initialized successfully', { path: this.dbPath });
37
+ }
38
+ catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ logger.error('Failed to initialize database', { path: this.dbPath, error: message });
41
+ throw new DatabaseError(`Failed to initialize database at ${this.dbPath}: ${message}`, error);
42
+ }
43
+ }
44
+ getRawDb() {
45
+ if (!this.db || !this.db.open) {
46
+ throw new DatabaseError('Database connection is not open');
47
+ }
48
+ return this.db;
49
+ }
50
+ runMigrations() {
51
+ const rawDb = this.getRawDb();
52
+ // Locate migrations directory relative to current file or src/storage/migrations
53
+ const __filename = fileURLToPath(import.meta.url);
54
+ const __dirname = path.dirname(__filename);
55
+ const candidateDirs = [
56
+ path.resolve(__dirname, 'migrations'),
57
+ path.resolve(__dirname, '..', 'src', 'storage', 'migrations'),
58
+ path.resolve(process.cwd(), 'src', 'storage', 'migrations'),
59
+ path.resolve(process.cwd(), 'contradiction-mcp', 'src', 'storage', 'migrations'),
60
+ ];
61
+ let migrationsDir = null;
62
+ for (const dir of candidateDirs) {
63
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
64
+ migrationsDir = dir;
65
+ break;
66
+ }
67
+ }
68
+ if (migrationsDir) {
69
+ rawDb.exec(`
70
+ CREATE TABLE IF NOT EXISTS schema_migrations (
71
+ id TEXT PRIMARY KEY,
72
+ applied_at TEXT NOT NULL
73
+ );
74
+ `);
75
+ const files = fs
76
+ .readdirSync(migrationsDir)
77
+ .filter((f) => f.endsWith('.sql'))
78
+ .sort();
79
+ for (const file of files) {
80
+ const row = rawDb.prepare('SELECT 1 FROM schema_migrations WHERE id = ?').get(file);
81
+ if (!row) {
82
+ const filePath = path.join(migrationsDir, file);
83
+ const sql = fs.readFileSync(filePath, 'utf-8');
84
+ try {
85
+ rawDb.exec(sql);
86
+ }
87
+ catch (err) {
88
+ const msg = err instanceof Error ? err.message : String(err);
89
+ if (!msg.includes('duplicate column name')) {
90
+ throw err;
91
+ }
92
+ }
93
+ rawDb
94
+ .prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, datetime('now'))")
95
+ .run(file);
96
+ }
97
+ }
98
+ return;
99
+ }
100
+ // Fallback DDL in case migration files are not found on disk
101
+ const fallbackSql = `
102
+ PRAGMA foreign_keys = ON;
103
+ CREATE TABLE IF NOT EXISTS sources (
104
+ id TEXT PRIMARY KEY,
105
+ external_id TEXT,
106
+ type TEXT NOT NULL,
107
+ name TEXT NOT NULL,
108
+ uri TEXT,
109
+ last_fetched_at TEXT,
110
+ created_at TEXT NOT NULL,
111
+ updated_at TEXT NOT NULL,
112
+ trust_score REAL NOT NULL DEFAULT 1.0 CHECK (trust_score >= 0.0 AND trust_score <= 1.0),
113
+ metadata TEXT NOT NULL DEFAULT '{}'
114
+ );
115
+ CREATE INDEX IF NOT EXISTS idx_sources_type ON sources(type);
116
+ CREATE INDEX IF NOT EXISTS idx_sources_created_at ON sources(created_at);
117
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sources_external_id ON sources(external_id);
118
+
119
+ CREATE TABLE IF NOT EXISTS claims (
120
+ id TEXT PRIMARY KEY,
121
+ external_id TEXT,
122
+ source_id TEXT NOT NULL,
123
+ subject TEXT NOT NULL,
124
+ predicate TEXT NOT NULL,
125
+ value TEXT NOT NULL,
126
+ value_type TEXT NOT NULL,
127
+ normalized_value TEXT,
128
+ confidence REAL NOT NULL DEFAULT 1.0 CHECK (confidence >= 0.0 AND confidence <= 1.0),
129
+ environment TEXT DEFAULT 'unknown',
130
+ scope TEXT DEFAULT 'unknown',
131
+ source_role TEXT DEFAULT 'unknown',
132
+ is_historical INTEGER NOT NULL DEFAULT 0,
133
+ valid_from TEXT,
134
+ valid_until TEXT,
135
+ value_constraint TEXT,
136
+ multi_value_context TEXT,
137
+ first_seen_at TEXT,
138
+ last_seen_at TEXT,
139
+ superseded_by TEXT,
140
+ observed_at TEXT NOT NULL,
141
+ created_at TEXT NOT NULL,
142
+ metadata TEXT NOT NULL DEFAULT '{}',
143
+ FOREIGN KEY (source_id) REFERENCES sources(id) ON DELETE CASCADE
144
+ );
145
+ CREATE INDEX IF NOT EXISTS idx_claims_source_id ON claims(source_id);
146
+ CREATE INDEX IF NOT EXISTS idx_claims_subject_predicate ON claims(subject, predicate);
147
+ CREATE INDEX IF NOT EXISTS idx_claims_value_type ON claims(value_type);
148
+ CREATE INDEX IF NOT EXISTS idx_claims_created_at ON claims(created_at);
149
+ CREATE INDEX IF NOT EXISTS idx_claims_context ON claims(environment, scope, source_role);
150
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_claims_external_id ON claims(external_id);
151
+
152
+ CREATE TABLE IF NOT EXISTS claim_history (
153
+ id TEXT PRIMARY KEY,
154
+ claim_id TEXT NOT NULL,
155
+ source_id TEXT NOT NULL,
156
+ value TEXT NOT NULL,
157
+ normalized_value TEXT,
158
+ observed_at TEXT NOT NULL,
159
+ first_seen_at TEXT NOT NULL,
160
+ last_seen_at TEXT NOT NULL,
161
+ superseded_at TEXT,
162
+ superseded_by TEXT,
163
+ metadata TEXT NOT NULL DEFAULT '{}',
164
+ FOREIGN KEY (claim_id) REFERENCES claims(id) ON DELETE CASCADE,
165
+ FOREIGN KEY (source_id) REFERENCES sources(id) ON DELETE CASCADE
166
+ );
167
+ CREATE INDEX IF NOT EXISTS idx_claim_history_claim_id ON claim_history(claim_id);
168
+ CREATE INDEX IF NOT EXISTS idx_claim_history_source_id ON claim_history(source_id);
169
+
170
+ CREATE TABLE IF NOT EXISTS contradictions (
171
+ id TEXT PRIMARY KEY,
172
+ claim_a_id TEXT NOT NULL,
173
+ claim_b_id TEXT NOT NULL,
174
+ contradiction_type TEXT NOT NULL,
175
+ severity TEXT NOT NULL,
176
+ confidence REAL NOT NULL DEFAULT 1.0 CHECK (confidence >= 0.0 AND confidence <= 1.0),
177
+ explanation TEXT NOT NULL,
178
+ status TEXT NOT NULL DEFAULT 'OPEN',
179
+ detected_at TEXT NOT NULL,
180
+ resolved_at TEXT,
181
+ resolution_reason TEXT,
182
+ resolved_by TEXT,
183
+ resolution_source TEXT,
184
+ chosen_claim_id TEXT,
185
+ audit_notes TEXT,
186
+ metadata TEXT NOT NULL DEFAULT '{}',
187
+ CHECK (claim_a_id != claim_b_id),
188
+ FOREIGN KEY (claim_a_id) REFERENCES claims(id) ON DELETE CASCADE,
189
+ FOREIGN KEY (claim_b_id) REFERENCES claims(id) ON DELETE CASCADE
190
+ );
191
+ CREATE INDEX IF NOT EXISTS idx_contradictions_claims ON contradictions(claim_a_id, claim_b_id);
192
+ CREATE INDEX IF NOT EXISTS idx_contradictions_type ON contradictions(contradiction_type);
193
+ CREATE INDEX IF NOT EXISTS idx_contradictions_severity ON contradictions(severity);
194
+ CREATE INDEX IF NOT EXISTS idx_contradictions_status ON contradictions(status);
195
+ CREATE INDEX IF NOT EXISTS idx_contradictions_detected_at ON contradictions(detected_at);
196
+
197
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_contradictions_pair_unique ON contradictions (
198
+ CASE WHEN claim_a_id < claim_b_id THEN claim_a_id ELSE claim_b_id END,
199
+ CASE WHEN claim_a_id < claim_b_id THEN claim_b_id ELSE claim_a_id END
200
+ );
201
+ CREATE INDEX IF NOT EXISTS idx_contradictions_confidence ON contradictions(confidence);
202
+
203
+ CREATE TABLE IF NOT EXISTS contradiction_history (
204
+ id TEXT PRIMARY KEY,
205
+ contradiction_id TEXT NOT NULL,
206
+ action TEXT NOT NULL,
207
+ previous_status TEXT NOT NULL,
208
+ new_status TEXT NOT NULL,
209
+ performed_by TEXT NOT NULL,
210
+ performed_at TEXT NOT NULL,
211
+ reason TEXT,
212
+ chosen_claim_id TEXT,
213
+ notes TEXT,
214
+ FOREIGN KEY (contradiction_id) REFERENCES contradictions(id) ON DELETE CASCADE
215
+ );
216
+ CREATE INDEX IF NOT EXISTS idx_contradiction_history_contradiction_id ON contradiction_history(contradiction_id);
217
+ `;
218
+ rawDb.exec(fallbackSql);
219
+ }
220
+ checkHealth() {
221
+ const startTime = performance.now();
222
+ try {
223
+ if (!this.db || !this.db.open) {
224
+ return {
225
+ status: 'unreachable',
226
+ latencyMs: 0,
227
+ tablesCount: 0,
228
+ error: 'Database is closed or not initialized',
229
+ };
230
+ }
231
+ // Execute lightweight query
232
+ const result = this.db.prepare('SELECT 1 as ok').get();
233
+ if (!result || result.ok !== 1) {
234
+ throw new Error('Database ping query returned unexpected result');
235
+ }
236
+ const tables = this.db
237
+ .prepare("SELECT count(*) as count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'schema_migrations'")
238
+ .get();
239
+ const latencyMs = Number((performance.now() - startTime).toFixed(2));
240
+ return {
241
+ status: 'connected',
242
+ latencyMs,
243
+ tablesCount: tables?.count ?? 0,
244
+ };
245
+ }
246
+ catch (error) {
247
+ const latencyMs = Number((performance.now() - startTime).toFixed(2));
248
+ const message = error instanceof Error ? error.message : String(error);
249
+ return {
250
+ status: 'unreachable',
251
+ latencyMs,
252
+ tablesCount: 0,
253
+ error: message,
254
+ };
255
+ }
256
+ }
257
+ close() {
258
+ if (this.db && this.db.open) {
259
+ this.db.close();
260
+ this.db = null;
261
+ logger.info('Database connection closed');
262
+ }
263
+ }
264
+ // --- SOURCE REPOSITORY ---
265
+ createSource(input) {
266
+ const parsedInput = CreateSourceInputSchema.safeParse(input);
267
+ if (!parsedInput.success) {
268
+ throw new ValidationError('Invalid source input data', parsedInput.error.format());
269
+ }
270
+ const rawDb = this.getRawDb();
271
+ const now = new Date();
272
+ const id = parsedInput.data.id || crypto.randomUUID();
273
+ const entity = {
274
+ id,
275
+ externalId: parsedInput.data.externalId ?? null,
276
+ type: parsedInput.data.type,
277
+ name: parsedInput.data.name,
278
+ uri: parsedInput.data.uri ?? null,
279
+ lastFetchedAt: parsedInput.data.lastFetchedAt ?? null,
280
+ createdAt: now,
281
+ updatedAt: now,
282
+ trustScore: parsedInput.data.trustScore ?? 1.0,
283
+ metadata: parsedInput.data.metadata ?? {},
284
+ };
285
+ try {
286
+ const stmt = rawDb.prepare(`
287
+ INSERT INTO sources (id, external_id, type, name, uri, last_fetched_at, created_at, updated_at, trust_score, metadata)
288
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
289
+ `);
290
+ stmt.run(entity.id, entity.externalId, entity.type, entity.name, entity.uri, entity.lastFetchedAt ? entity.lastFetchedAt.toISOString() : null, entity.createdAt.toISOString(), entity.updatedAt.toISOString(), entity.trustScore, JSON.stringify(entity.metadata));
291
+ return entity;
292
+ }
293
+ catch (error) {
294
+ throw new DatabaseError(`Failed to create source: ${error instanceof Error ? error.message : error}`, error);
295
+ }
296
+ }
297
+ getSourceById(id) {
298
+ const rawDb = this.getRawDb();
299
+ const row = rawDb.prepare('SELECT * FROM sources WHERE id = ?').get(id);
300
+ if (!row) {
301
+ return null;
302
+ }
303
+ return this.mapRowToSource(row);
304
+ }
305
+ getSourceByExternalId(externalId) {
306
+ const rawDb = this.getRawDb();
307
+ const row = rawDb.prepare('SELECT * FROM sources WHERE external_id = ?').get(externalId);
308
+ if (!row) {
309
+ return null;
310
+ }
311
+ return this.mapRowToSource(row);
312
+ }
313
+ upsertSource(input) {
314
+ if (input.externalId) {
315
+ const existing = this.getSourceByExternalId(input.externalId);
316
+ if (existing) {
317
+ const updated = this.updateSource(existing.id, {
318
+ name: input.name,
319
+ uri: input.uri,
320
+ lastFetchedAt: input.lastFetchedAt ?? new Date(),
321
+ metadata: input.metadata,
322
+ });
323
+ return { source: updated, isNew: false };
324
+ }
325
+ }
326
+ const created = this.createSource(input);
327
+ return { source: created, isNew: true };
328
+ }
329
+ listSources(options) {
330
+ const rawDb = this.getRawDb();
331
+ let query = 'SELECT * FROM sources';
332
+ const params = [];
333
+ if (options?.type) {
334
+ query += ' WHERE type = ?';
335
+ params.push(options.type);
336
+ }
337
+ query += ' ORDER BY created_at DESC';
338
+ if (options?.limit && options.limit > 0) {
339
+ query += ' LIMIT ?';
340
+ params.push(options.limit);
341
+ }
342
+ const rows = rawDb.prepare(query).all(...params);
343
+ return rows.map((r) => this.mapRowToSource(r));
344
+ }
345
+ updateSource(id, input) {
346
+ const existing = this.getSourceById(id);
347
+ if (!existing) {
348
+ throw new NotFoundError('Source', id);
349
+ }
350
+ const rawDb = this.getRawDb();
351
+ const updatedAt = new Date();
352
+ const updated = {
353
+ ...existing,
354
+ ...input,
355
+ updatedAt,
356
+ metadata: input.metadata ? { ...existing.metadata, ...input.metadata } : existing.metadata,
357
+ };
358
+ try {
359
+ rawDb
360
+ .prepare(`
361
+ UPDATE sources
362
+ SET type = ?, name = ?, uri = ?, last_fetched_at = ?, updated_at = ?, trust_score = ?, metadata = ?
363
+ WHERE id = ?
364
+ `)
365
+ .run(updated.type, updated.name, updated.uri, updated.lastFetchedAt ? updated.lastFetchedAt.toISOString() : null, updated.updatedAt.toISOString(), updated.trustScore, JSON.stringify(updated.metadata), id);
366
+ return updated;
367
+ }
368
+ catch (error) {
369
+ throw new DatabaseError(`Failed to update source ${id}: ${error instanceof Error ? error.message : error}`, error);
370
+ }
371
+ }
372
+ deleteSource(id) {
373
+ const rawDb = this.getRawDb();
374
+ const result = rawDb.prepare('DELETE FROM sources WHERE id = ?').run(id);
375
+ return result.changes > 0;
376
+ }
377
+ // --- CLAIM REPOSITORY ---
378
+ createClaim(input) {
379
+ const parsed = CreateClaimInputSchema.safeParse(input);
380
+ if (!parsed.success) {
381
+ throw new ValidationError('Invalid claim input data', parsed.error.format());
382
+ }
383
+ const rawDb = this.getRawDb();
384
+ const now = new Date();
385
+ const id = parsed.data.id || crypto.randomUUID();
386
+ const entity = {
387
+ id,
388
+ externalId: parsed.data.externalId ?? null,
389
+ sourceId: parsed.data.sourceId,
390
+ subject: parsed.data.subject,
391
+ predicate: parsed.data.predicate,
392
+ value: parsed.data.value,
393
+ valueType: parsed.data.valueType,
394
+ normalizedValue: parsed.data.normalizedValue ?? null,
395
+ confidence: parsed.data.confidence ?? 1.0,
396
+ environment: parsed.data.environment ?? 'unknown',
397
+ scope: parsed.data.scope ?? 'unknown',
398
+ sourceRole: parsed.data.sourceRole ?? 'unknown',
399
+ isHistorical: parsed.data.isHistorical ?? false,
400
+ validFrom: parsed.data.validFrom ?? null,
401
+ validUntil: parsed.data.validUntil ?? null,
402
+ valueConstraint: parsed.data.valueConstraint ?? null,
403
+ multiValueContext: parsed.data.multiValueContext ?? null,
404
+ observedAt: parsed.data.observedAt ?? now,
405
+ firstSeenAt: parsed.data.firstSeenAt ?? parsed.data.observedAt ?? now,
406
+ lastSeenAt: parsed.data.lastSeenAt ?? parsed.data.observedAt ?? now,
407
+ supersededBy: parsed.data.supersededBy ?? null,
408
+ createdAt: now,
409
+ metadata: parsed.data.metadata ?? {},
410
+ };
411
+ try {
412
+ rawDb
413
+ .prepare(`
414
+ INSERT INTO claims (
415
+ id, external_id, source_id, subject, predicate, value, value_type, normalized_value, confidence,
416
+ environment, scope, source_role, is_historical, valid_from, valid_until, value_constraint, multi_value_context,
417
+ first_seen_at, last_seen_at, superseded_by, observed_at, created_at, metadata
418
+ )
419
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
420
+ `)
421
+ .run(entity.id, entity.externalId, entity.sourceId, entity.subject, entity.predicate, entity.value, entity.valueType, entity.normalizedValue, entity.confidence, entity.environment, entity.scope, entity.sourceRole, entity.isHistorical ? 1 : 0, entity.validFrom ? entity.validFrom.toISOString() : null, entity.validUntil ? entity.validUntil.toISOString() : null, entity.valueConstraint, entity.multiValueContext, entity.firstSeenAt ? entity.firstSeenAt.toISOString() : null, entity.lastSeenAt ? entity.lastSeenAt.toISOString() : null, entity.supersededBy, entity.observedAt.toISOString(), entity.createdAt.toISOString(), JSON.stringify(entity.metadata));
422
+ return entity;
423
+ }
424
+ catch (error) {
425
+ throw new DatabaseError(`Failed to create claim: ${error instanceof Error ? error.message : error}`, error);
426
+ }
427
+ }
428
+ getClaimById(id) {
429
+ const rawDb = this.getRawDb();
430
+ const row = rawDb.prepare('SELECT * FROM claims WHERE id = ?').get(id);
431
+ if (!row) {
432
+ return null;
433
+ }
434
+ return this.mapRowToClaim(row);
435
+ }
436
+ getClaimByExternalId(externalId) {
437
+ const rawDb = this.getRawDb();
438
+ const row = rawDb.prepare('SELECT * FROM claims WHERE external_id = ?').get(externalId);
439
+ if (!row) {
440
+ return null;
441
+ }
442
+ return this.mapRowToClaim(row);
443
+ }
444
+ updateClaim(id, updates) {
445
+ const existing = this.getClaimById(id);
446
+ if (!existing) {
447
+ throw new NotFoundError('Claim', id);
448
+ }
449
+ const rawDb = this.getRawDb();
450
+ const updated = {
451
+ ...existing,
452
+ ...updates,
453
+ metadata: updates.metadata
454
+ ? { ...existing.metadata, ...updates.metadata }
455
+ : existing.metadata,
456
+ };
457
+ try {
458
+ rawDb
459
+ .prepare(`
460
+ UPDATE claims
461
+ SET value = ?, value_type = ?, normalized_value = ?, confidence = ?,
462
+ environment = ?, scope = ?, source_role = ?, is_historical = ?,
463
+ valid_from = ?, valid_until = ?, value_constraint = ?, multi_value_context = ?,
464
+ first_seen_at = ?, last_seen_at = ?, superseded_by = ?,
465
+ observed_at = ?, metadata = ?
466
+ WHERE id = ?
467
+ `)
468
+ .run(updated.value, updated.valueType, updated.normalizedValue, updated.confidence, updated.environment ?? 'unknown', updated.scope ?? 'unknown', updated.sourceRole ?? 'unknown', updated.isHistorical ? 1 : 0, updated.validFrom ? updated.validFrom.toISOString() : null, updated.validUntil ? updated.validUntil.toISOString() : null, updated.valueConstraint, updated.multiValueContext, updated.firstSeenAt ? updated.firstSeenAt.toISOString() : null, updated.lastSeenAt ? updated.lastSeenAt.toISOString() : null, updated.supersededBy ?? null, updated.observedAt.toISOString(), JSON.stringify(updated.metadata), id);
469
+ return updated;
470
+ }
471
+ catch (error) {
472
+ throw new DatabaseError(`Failed to update claim ${id}: ${error instanceof Error ? error.message : error}`, error);
473
+ }
474
+ }
475
+ upsertClaim(input) {
476
+ if (input.externalId) {
477
+ const existing = this.getClaimByExternalId(input.externalId);
478
+ if (existing) {
479
+ const isValueChanged = existing.value !== input.value ||
480
+ (input.normalizedValue !== undefined &&
481
+ existing.normalizedValue !== input.normalizedValue);
482
+ if (isValueChanged) {
483
+ // Record historical state transition in claim_history
484
+ this.recordClaimHistory(existing.id, existing.value, existing.normalizedValue ?? null, existing.sourceId, existing.observedAt, existing.firstSeenAt ?? existing.observedAt, new Date(), new Date(), null, existing.metadata);
485
+ const updatedClaim = this.updateClaim(existing.id, {
486
+ value: input.value,
487
+ valueType: input.valueType,
488
+ normalizedValue: input.normalizedValue,
489
+ confidence: input.confidence ?? existing.confidence,
490
+ environment: input.environment ?? existing.environment,
491
+ scope: input.scope ?? existing.scope,
492
+ sourceRole: input.sourceRole ?? existing.sourceRole,
493
+ isHistorical: input.isHistorical ?? existing.isHistorical,
494
+ validFrom: input.validFrom !== undefined ? input.validFrom : existing.validFrom,
495
+ validUntil: input.validUntil !== undefined ? input.validUntil : existing.validUntil,
496
+ valueConstraint: input.valueConstraint !== undefined
497
+ ? input.valueConstraint
498
+ : existing.valueConstraint,
499
+ multiValueContext: input.multiValueContext !== undefined
500
+ ? input.multiValueContext
501
+ : existing.multiValueContext,
502
+ observedAt: input.observedAt ?? new Date(),
503
+ metadata: {
504
+ ...existing.metadata,
505
+ ...input.metadata,
506
+ history: [
507
+ ...(existing.metadata.history ?? []),
508
+ {
509
+ previousValue: existing.value,
510
+ previousNormalizedValue: existing.normalizedValue,
511
+ changedAt: new Date().toISOString(),
512
+ },
513
+ ],
514
+ },
515
+ });
516
+ return { claim: updatedClaim, isNew: false, updated: true };
517
+ }
518
+ else {
519
+ const refreshedClaim = this.updateClaim(existing.id, {
520
+ observedAt: input.observedAt ?? new Date(),
521
+ environment: input.environment ?? existing.environment,
522
+ scope: input.scope ?? existing.scope,
523
+ sourceRole: input.sourceRole ?? existing.sourceRole,
524
+ isHistorical: input.isHistorical ?? existing.isHistorical,
525
+ validFrom: input.validFrom !== undefined ? input.validFrom : existing.validFrom,
526
+ validUntil: input.validUntil !== undefined ? input.validUntil : existing.validUntil,
527
+ valueConstraint: input.valueConstraint !== undefined
528
+ ? input.valueConstraint
529
+ : existing.valueConstraint,
530
+ multiValueContext: input.multiValueContext !== undefined
531
+ ? input.multiValueContext
532
+ : existing.multiValueContext,
533
+ metadata: {
534
+ ...existing.metadata,
535
+ ...input.metadata,
536
+ },
537
+ });
538
+ return { claim: refreshedClaim, isNew: false, updated: false };
539
+ }
540
+ }
541
+ }
542
+ const created = this.createClaim(input);
543
+ return { claim: created, isNew: true, updated: false };
544
+ }
545
+ listClaims(options) {
546
+ const rawDb = this.getRawDb();
547
+ const conditions = [];
548
+ const params = [];
549
+ if (options?.sourceId) {
550
+ conditions.push('source_id = ?');
551
+ params.push(options.sourceId);
552
+ }
553
+ if (options?.subject) {
554
+ conditions.push('subject = ?');
555
+ params.push(options.subject);
556
+ }
557
+ let query = 'SELECT * FROM claims';
558
+ if (conditions.length > 0) {
559
+ query += ` WHERE ${conditions.join(' AND ')}`;
560
+ }
561
+ query += ' ORDER BY created_at DESC';
562
+ if (options?.limit && options.limit > 0) {
563
+ query += ' LIMIT ?';
564
+ params.push(options.limit);
565
+ }
566
+ const rows = rawDb.prepare(query).all(...params);
567
+ return rows.map((r) => this.mapRowToClaim(r));
568
+ }
569
+ deleteClaim(id) {
570
+ const rawDb = this.getRawDb();
571
+ const result = rawDb.prepare('DELETE FROM claims WHERE id = ?').run(id);
572
+ return result.changes > 0;
573
+ }
574
+ // --- CLAIM HISTORY REPOSITORY ---
575
+ recordClaimHistory(claimId, value, normalizedValue, sourceId, observedAt, firstSeenAt, lastSeenAt, supersededAt, supersededBy, metadata = {}) {
576
+ const rawDb = this.getRawDb();
577
+ const id = crypto.randomUUID();
578
+ const entry = {
579
+ id,
580
+ claimId,
581
+ sourceId,
582
+ value,
583
+ normalizedValue,
584
+ observedAt,
585
+ firstSeenAt,
586
+ lastSeenAt,
587
+ supersededAt: supersededAt ?? null,
588
+ supersededBy: supersededBy ?? null,
589
+ metadata,
590
+ };
591
+ try {
592
+ rawDb
593
+ .prepare(`
594
+ INSERT INTO claim_history (
595
+ id, claim_id, source_id, value, normalized_value,
596
+ observed_at, first_seen_at, last_seen_at, superseded_at, superseded_by, metadata
597
+ )
598
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
599
+ `)
600
+ .run(entry.id, entry.claimId, entry.sourceId, entry.value, entry.normalizedValue, entry.observedAt.toISOString(), entry.firstSeenAt.toISOString(), entry.lastSeenAt.toISOString(), entry.supersededAt ? entry.supersededAt.toISOString() : null, entry.supersededBy, JSON.stringify(entry.metadata));
601
+ return entry;
602
+ }
603
+ catch (error) {
604
+ throw new DatabaseError(`Failed to record claim history: ${error instanceof Error ? error.message : error}`, error);
605
+ }
606
+ }
607
+ getClaimHistory(claimId) {
608
+ const rawDb = this.getRawDb();
609
+ const rows = rawDb
610
+ .prepare('SELECT * FROM claim_history WHERE claim_id = ? ORDER BY observed_at DESC, rowid DESC')
611
+ .all(claimId);
612
+ return rows.map((r) => ({
613
+ id: r.id,
614
+ claimId: r.claim_id,
615
+ sourceId: r.source_id,
616
+ value: r.value,
617
+ normalizedValue: r.normalized_value ?? null,
618
+ observedAt: new Date(r.observed_at),
619
+ firstSeenAt: new Date(r.first_seen_at),
620
+ lastSeenAt: new Date(r.last_seen_at),
621
+ supersededAt: r.superseded_at ? new Date(r.superseded_at) : null,
622
+ supersededBy: r.superseded_by ?? null,
623
+ metadata: JSON.parse(r.metadata || '{}'),
624
+ }));
625
+ }
626
+ // --- CONTRADICTION REPOSITORY ---
627
+ createContradiction(input) {
628
+ const parsed = CreateContradictionInputSchema.safeParse(input);
629
+ if (!parsed.success) {
630
+ throw new ValidationError('Invalid contradiction input data', parsed.error.format());
631
+ }
632
+ const rawDb = this.getRawDb();
633
+ const now = new Date();
634
+ const id = parsed.data.id || crypto.randomUUID();
635
+ const entity = {
636
+ id,
637
+ claimAId: parsed.data.claimAId,
638
+ claimBId: parsed.data.claimBId,
639
+ contradictionType: parsed.data.contradictionType,
640
+ severity: parsed.data.severity,
641
+ confidence: parsed.data.confidence ?? 1.0,
642
+ explanation: parsed.data.explanation,
643
+ status: parsed.data.status ?? 'OPEN',
644
+ detectedAt: parsed.data.detectedAt ?? now,
645
+ resolvedAt: parsed.data.resolvedAt ?? null,
646
+ metadata: parsed.data.metadata ?? {},
647
+ };
648
+ try {
649
+ rawDb
650
+ .prepare(`
651
+ INSERT INTO contradictions (id, claim_a_id, claim_b_id, contradiction_type, severity, confidence, explanation, status, detected_at, resolved_at, metadata)
652
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
653
+ `)
654
+ .run(entity.id, entity.claimAId, entity.claimBId, entity.contradictionType, entity.severity, entity.confidence, entity.explanation, entity.status, entity.detectedAt.toISOString(), entity.resolvedAt ? entity.resolvedAt.toISOString() : null, JSON.stringify(entity.metadata));
655
+ return entity;
656
+ }
657
+ catch (error) {
658
+ throw new DatabaseError(`Failed to create contradiction: ${error instanceof Error ? error.message : error}`, error);
659
+ }
660
+ }
661
+ getContradictionById(id) {
662
+ const rawDb = this.getRawDb();
663
+ const row = rawDb.prepare('SELECT * FROM contradictions WHERE id = ?').get(id);
664
+ if (!row) {
665
+ return null;
666
+ }
667
+ return this.mapRowToContradiction(row);
668
+ }
669
+ findContradictionByClaimPair(claimAId, claimBId) {
670
+ const rawDb = this.getRawDb();
671
+ const row = rawDb
672
+ .prepare(`
673
+ SELECT * FROM contradictions
674
+ WHERE (claim_a_id = ? AND claim_b_id = ?)
675
+ OR (claim_a_id = ? AND claim_b_id = ?)
676
+ LIMIT 1
677
+ `)
678
+ .get(claimAId, claimBId, claimBId, claimAId);
679
+ if (!row) {
680
+ return null;
681
+ }
682
+ return this.mapRowToContradiction(row);
683
+ }
684
+ saveDiscoveredContradiction(input) {
685
+ const existing = this.findContradictionByClaimPair(input.claimAId, input.claimBId);
686
+ if (existing) {
687
+ return { contradiction: existing, isNew: false };
688
+ }
689
+ const created = this.createContradiction(input);
690
+ return { contradiction: created, isNew: true };
691
+ }
692
+ listContradictions(options) {
693
+ const rawDb = this.getRawDb();
694
+ const conditions = [];
695
+ const params = [];
696
+ if (options?.status) {
697
+ conditions.push('status = ?');
698
+ params.push(options.status);
699
+ }
700
+ if (options?.severity) {
701
+ conditions.push('severity = ?');
702
+ params.push(options.severity);
703
+ }
704
+ if (options?.type) {
705
+ conditions.push('contradiction_type = ?');
706
+ params.push(options.type);
707
+ }
708
+ if (options?.minConfidence !== undefined) {
709
+ conditions.push('confidence >= ?');
710
+ params.push(options.minConfidence);
711
+ }
712
+ let query = 'SELECT * FROM contradictions';
713
+ if (conditions.length > 0) {
714
+ query += ` WHERE ${conditions.join(' AND ')}`;
715
+ }
716
+ query += ' ORDER BY detected_at DESC';
717
+ if (options?.limit && options.limit > 0) {
718
+ query += ' LIMIT ?';
719
+ params.push(options.limit);
720
+ if (options?.offset && options.offset > 0) {
721
+ query += ' OFFSET ?';
722
+ params.push(options.offset);
723
+ }
724
+ }
725
+ const rows = rawDb.prepare(query).all(...params);
726
+ return rows.map((r) => this.mapRowToContradiction(r));
727
+ }
728
+ getContradictionWithDetails(id) {
729
+ const contradiction = this.getContradictionById(id);
730
+ if (!contradiction) {
731
+ return null;
732
+ }
733
+ const claimA = this.getClaimById(contradiction.claimAId);
734
+ const claimB = this.getClaimById(contradiction.claimBId);
735
+ if (!claimA || !claimB) {
736
+ throw new DatabaseError(`Integrity error: contradiction ${id} references missing claims (${contradiction.claimAId}, ${contradiction.claimBId})`);
737
+ }
738
+ const sourceA = this.getSourceById(claimA.sourceId);
739
+ const sourceB = this.getSourceById(claimB.sourceId);
740
+ return {
741
+ contradiction,
742
+ claimA,
743
+ claimB,
744
+ sourceA,
745
+ sourceB,
746
+ };
747
+ }
748
+ updateContradictionStatus(id, status, details) {
749
+ const existing = this.getContradictionById(id);
750
+ if (!existing) {
751
+ throw new NotFoundError('Contradiction', id);
752
+ }
753
+ const rawDb = this.getRawDb();
754
+ const resolvedTimestamp = status === 'RESOLVED'
755
+ ? details?.resolvedAt
756
+ ? details.resolvedAt.toISOString()
757
+ : new Date().toISOString()
758
+ : null;
759
+ try {
760
+ rawDb
761
+ .prepare(`
762
+ UPDATE contradictions
763
+ SET status = ?, resolved_at = ?, resolution_reason = ?, resolved_by = ?,
764
+ resolution_source = ?, chosen_claim_id = ?, audit_notes = ?
765
+ WHERE id = ?
766
+ `)
767
+ .run(status, resolvedTimestamp, details?.reason ?? existing.resolutionReason ?? null, details?.resolvedBy ?? existing.resolvedBy ?? null, details?.resolutionSource ?? existing.resolutionSource ?? null, details?.chosenClaimId ?? existing.chosenClaimId ?? null, details?.auditNotes ?? existing.auditNotes ?? null, id);
768
+ // Record audit history entry
769
+ this.recordContradictionAudit({
770
+ contradictionId: id,
771
+ action: status === 'RESOLVED'
772
+ ? 'RESOLVED'
773
+ : status === 'REVIEWED'
774
+ ? 'REVIEWED'
775
+ : status === 'DISMISSED'
776
+ ? 'DISMISSED'
777
+ : 'REOPENED',
778
+ previousStatus: existing.status,
779
+ newStatus: status,
780
+ performedBy: details?.resolvedBy ?? 'system',
781
+ reason: details?.reason ?? null,
782
+ chosenClaimId: details?.chosenClaimId ?? null,
783
+ notes: details?.auditNotes ?? null,
784
+ });
785
+ return {
786
+ ...existing,
787
+ status,
788
+ resolvedAt: resolvedTimestamp ? new Date(resolvedTimestamp) : null,
789
+ resolutionReason: details?.reason ?? existing.resolutionReason ?? null,
790
+ resolvedBy: details?.resolvedBy ?? existing.resolvedBy ?? null,
791
+ resolutionSource: details?.resolutionSource ?? existing.resolutionSource ?? null,
792
+ chosenClaimId: details?.chosenClaimId ?? existing.chosenClaimId ?? null,
793
+ auditNotes: details?.auditNotes ?? existing.auditNotes ?? null,
794
+ };
795
+ }
796
+ catch (error) {
797
+ throw new DatabaseError(`Failed to update contradiction ${id}: ${error instanceof Error ? error.message : error}`, error);
798
+ }
799
+ }
800
+ recordContradictionAudit(input) {
801
+ const rawDb = this.getRawDb();
802
+ const id = crypto.randomUUID();
803
+ const now = new Date();
804
+ const entry = {
805
+ id,
806
+ contradictionId: input.contradictionId,
807
+ action: input.action,
808
+ previousStatus: input.previousStatus,
809
+ newStatus: input.newStatus,
810
+ performedBy: input.performedBy,
811
+ performedAt: now,
812
+ reason: input.reason ?? null,
813
+ chosenClaimId: input.chosenClaimId ?? null,
814
+ notes: input.notes ?? null,
815
+ };
816
+ try {
817
+ rawDb
818
+ .prepare(`
819
+ INSERT INTO contradiction_history (
820
+ id, contradiction_id, action, previous_status, new_status,
821
+ performed_by, performed_at, reason, chosen_claim_id, notes
822
+ )
823
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
824
+ `)
825
+ .run(entry.id, entry.contradictionId, entry.action, entry.previousStatus, entry.newStatus, entry.performedBy, entry.performedAt.toISOString(), entry.reason, entry.chosenClaimId, entry.notes);
826
+ return entry;
827
+ }
828
+ catch (error) {
829
+ throw new DatabaseError(`Failed to record contradiction audit history: ${error instanceof Error ? error.message : error}`, error);
830
+ }
831
+ }
832
+ getContradictionHistory(contradictionId) {
833
+ const rawDb = this.getRawDb();
834
+ const rows = rawDb
835
+ .prepare('SELECT * FROM contradiction_history WHERE contradiction_id = ? ORDER BY performed_at DESC, rowid DESC')
836
+ .all(contradictionId);
837
+ return rows.map((r) => ({
838
+ id: r.id,
839
+ contradictionId: r.contradiction_id,
840
+ action: r.action,
841
+ previousStatus: r.previous_status,
842
+ newStatus: r.new_status,
843
+ performedBy: r.performed_by,
844
+ performedAt: new Date(r.performed_at),
845
+ reason: r.reason ?? null,
846
+ chosenClaimId: r.chosen_claim_id ?? null,
847
+ notes: r.notes ?? null,
848
+ }));
849
+ }
850
+ async backup(targetPath) {
851
+ const rawDb = this.getRawDb();
852
+ const dir = path.dirname(targetPath);
853
+ if (!fs.existsSync(dir)) {
854
+ fs.mkdirSync(dir, { recursive: true });
855
+ }
856
+ await rawDb.backup(targetPath);
857
+ }
858
+ checkIntegrity() {
859
+ const rawDb = this.getRawDb();
860
+ const rows = rawDb.pragma('integrity_check');
861
+ const ok = rows.length === 1 && rows[0].integrity_check === 'ok';
862
+ return {
863
+ ok,
864
+ details: rows.map((r) => r.integrity_check),
865
+ };
866
+ }
867
+ deleteContradiction(id) {
868
+ const rawDb = this.getRawDb();
869
+ const result = rawDb.prepare('DELETE FROM contradictions WHERE id = ?').run(id);
870
+ return result.changes > 0;
871
+ }
872
+ // --- MAPPING HELPERS ---
873
+ mapRowToSource(row) {
874
+ return SourceSchema.parse({
875
+ id: row.id,
876
+ externalId: row.external_id ?? null,
877
+ type: row.type,
878
+ name: row.name,
879
+ uri: row.uri,
880
+ lastFetchedAt: row.last_fetched_at ? new Date(row.last_fetched_at) : null,
881
+ createdAt: new Date(row.created_at),
882
+ updatedAt: new Date(row.updated_at),
883
+ trustScore: Number(row.trust_score),
884
+ metadata: JSON.parse(row.metadata || '{}'),
885
+ });
886
+ }
887
+ mapRowToClaim(row) {
888
+ return ClaimSchema.parse({
889
+ id: row.id,
890
+ externalId: row.external_id ?? null,
891
+ sourceId: row.source_id,
892
+ subject: row.subject,
893
+ predicate: row.predicate,
894
+ value: row.value,
895
+ valueType: row.value_type,
896
+ normalizedValue: row.normalized_value,
897
+ confidence: Number(row.confidence),
898
+ environment: row.environment ?? 'unknown',
899
+ scope: row.scope ?? 'unknown',
900
+ sourceRole: row.source_role ?? 'unknown',
901
+ isHistorical: Boolean(row.is_historical),
902
+ validFrom: row.valid_from ? new Date(row.valid_from) : null,
903
+ validUntil: row.valid_until ? new Date(row.valid_until) : null,
904
+ valueConstraint: row.value_constraint ?? null,
905
+ multiValueContext: row.multi_value_context ?? null,
906
+ firstSeenAt: row.first_seen_at ? new Date(row.first_seen_at) : null,
907
+ lastSeenAt: row.last_seen_at ? new Date(row.last_seen_at) : null,
908
+ supersededBy: row.superseded_by ?? null,
909
+ observedAt: new Date(row.observed_at),
910
+ createdAt: new Date(row.created_at),
911
+ metadata: JSON.parse(row.metadata || '{}'),
912
+ });
913
+ }
914
+ mapRowToContradiction(row) {
915
+ return ContradictionSchema.parse({
916
+ id: row.id,
917
+ claimAId: row.claim_a_id,
918
+ claimBId: row.claim_b_id,
919
+ contradictionType: row.contradiction_type,
920
+ severity: row.severity,
921
+ confidence: Number(row.confidence),
922
+ explanation: row.explanation,
923
+ status: row.status,
924
+ detectedAt: new Date(row.detected_at),
925
+ resolvedAt: row.resolved_at ? new Date(row.resolved_at) : null,
926
+ resolutionReason: row.resolution_reason ?? null,
927
+ resolvedBy: row.resolved_by ?? null,
928
+ resolutionSource: row.resolution_source ?? null,
929
+ chosenClaimId: row.chosen_claim_id ?? null,
930
+ auditNotes: row.audit_notes ?? null,
931
+ metadata: JSON.parse(row.metadata || '{}'),
932
+ });
933
+ }
934
+ }
935
+ //# sourceMappingURL=database.js.map