@kybernesis/brain-core 0.2.0 → 0.5.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.
@@ -1,299 +1,30 @@
1
- import { getDb } from '@kybernesis/brain-storage-sqlite';
2
- import { DIRECTIONAL_RELATIONSHIPS } from '@kybernesis/brain-contracts';
3
- function rowToEntity(row) {
4
- return {
5
- id: row.id,
6
- name: row.name,
7
- normalized_name: row.normalized_name,
8
- aliases: JSON.parse(row.aliases || '[]'),
9
- type: row.type,
10
- first_seen: row.first_seen,
11
- last_seen: row.last_seen,
12
- mention_count: row.mention_count,
13
- priority: row.priority ?? 0.5,
14
- decay_score: row.decay_score ?? 0.0,
15
- tier: (row.tier ?? 'warm'),
16
- last_accessed: row.last_accessed ?? undefined,
17
- access_count: row.access_count ?? 0,
18
- is_pinned: (row.is_pinned ?? 0) === 1,
19
- last_reasoned_at: row.last_reasoned_at ?? undefined,
20
- };
21
- }
22
- function rowToMention(row) {
23
- return {
24
- id: row.id,
25
- entity_id: row.entity_id,
26
- conversation_id: row.conversation_id,
27
- source_path: row.source_path,
28
- context: row.context ?? undefined,
29
- timestamp: row.timestamp,
30
- source_type: row.source_type ?? 'chat',
31
- confidence: row.confidence ?? 0.85,
32
- };
33
- }
34
- function rowToInsight(row) {
35
- return {
36
- id: row.id,
37
- entity_id: row.entity_id,
38
- insight_type: row.insight_type,
39
- insight: row.insight,
40
- reasoning: row.reasoning,
41
- confidence: row.confidence,
42
- source_entity_ids: JSON.parse(row.source_entity_ids || '[]'),
43
- created_at: row.created_at,
44
- expires_at: row.expires_at ?? undefined,
45
- is_stale: (row.is_stale ?? 0) === 1,
46
- };
47
- }
48
- // ─── Schema initialization ────────────────────────────────────────────────────
49
- const initialized = new Set();
50
- function ensureSchema(t) {
51
- if (initialized.has(t.slug))
52
- return;
53
- const db = getDb(t, 'entityGraph');
54
- db.exec(`
55
- CREATE TABLE IF NOT EXISTS entities (
56
- id INTEGER PRIMARY KEY AUTOINCREMENT,
57
- name TEXT NOT NULL,
58
- normalized_name TEXT NOT NULL,
59
- aliases TEXT DEFAULT '[]',
60
- type TEXT NOT NULL CHECK(type IN ('person', 'company', 'project', 'place', 'topic')),
61
- first_seen TEXT NOT NULL,
62
- last_seen TEXT NOT NULL,
63
- mention_count INTEGER DEFAULT 1,
64
- priority REAL DEFAULT 0.5,
65
- decay_score REAL DEFAULT 0.0,
66
- tier TEXT DEFAULT 'warm',
67
- last_accessed TEXT,
68
- access_count INTEGER DEFAULT 0,
69
- is_pinned INTEGER DEFAULT 0,
70
- last_reasoned_at TEXT,
71
- UNIQUE(normalized_name, type)
72
- );
73
-
74
- CREATE INDEX IF NOT EXISTS idx_entities_normalized ON entities(normalized_name);
75
- CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
76
- CREATE INDEX IF NOT EXISTS idx_entities_tier ON entities(tier);
77
- CREATE INDEX IF NOT EXISTS idx_entities_priority ON entities(priority DESC);
78
-
79
- CREATE TABLE IF NOT EXISTS entity_mentions (
80
- id INTEGER PRIMARY KEY AUTOINCREMENT,
81
- entity_id INTEGER NOT NULL,
82
- conversation_id TEXT NOT NULL,
83
- source_path TEXT NOT NULL,
84
- context TEXT,
85
- timestamp TEXT NOT NULL,
86
- source_type TEXT DEFAULT 'chat',
87
- confidence REAL DEFAULT 0.85,
88
- FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
89
- );
90
-
91
- CREATE INDEX IF NOT EXISTS idx_mentions_conversation ON entity_mentions(conversation_id);
92
- CREATE INDEX IF NOT EXISTS idx_mentions_entity ON entity_mentions(entity_id);
93
- CREATE INDEX IF NOT EXISTS idx_mentions_timestamp ON entity_mentions(timestamp);
94
-
95
- CREATE TABLE IF NOT EXISTS entity_relations (
96
- source_id INTEGER NOT NULL,
97
- target_id INTEGER NOT NULL,
98
- relationship TEXT DEFAULT 'co-occurred',
99
- strength INTEGER DEFAULT 1,
100
- confidence REAL DEFAULT 0.5,
101
- method TEXT DEFAULT 'co-occurred',
102
- rationale TEXT,
103
- last_verified TEXT,
104
- PRIMARY KEY (source_id, target_id),
105
- FOREIGN KEY (source_id) REFERENCES entities(id) ON DELETE CASCADE,
106
- FOREIGN KEY (target_id) REFERENCES entities(id) ON DELETE CASCADE
107
- );
108
-
109
- CREATE INDEX IF NOT EXISTS idx_relations_source ON entity_relations(source_id);
110
- CREATE INDEX IF NOT EXISTS idx_relations_target ON entity_relations(target_id);
111
- CREATE INDEX IF NOT EXISTS idx_relations_confidence ON entity_relations(confidence DESC);
112
-
113
- CREATE TABLE IF NOT EXISTS entity_merges (
114
- id INTEGER PRIMARY KEY AUTOINCREMENT,
115
- keep_id INTEGER NOT NULL,
116
- remove_id INTEGER NOT NULL,
117
- keep_name TEXT, remove_name TEXT, keep_type TEXT, remove_type TEXT,
118
- reason TEXT NOT NULL,
119
- confidence REAL,
120
- ai_rationale TEXT,
121
- mentions_moved INTEGER DEFAULT 0,
122
- relations_moved INTEGER DEFAULT 0,
123
- merged_at TEXT DEFAULT (datetime('now')),
124
- merged_by TEXT DEFAULT 'sleep:entity-hygiene'
125
- );
126
-
127
- CREATE TABLE IF NOT EXISTS entity_profiles (
128
- entity_id INTEGER PRIMARY KEY REFERENCES entities(id) ON DELETE CASCADE,
129
- profile TEXT NOT NULL,
130
- generated_at TEXT DEFAULT (datetime('now')),
131
- fact_count INTEGER DEFAULT 0
132
- );
133
-
134
- CREATE TABLE IF NOT EXISTS contradictions (
135
- id INTEGER PRIMARY KEY AUTOINCREMENT,
136
- entity_id INTEGER,
137
- fact_a_id INTEGER,
138
- fact_b_id INTEGER,
139
- fact_a TEXT,
140
- fact_b TEXT,
141
- description TEXT,
142
- status TEXT DEFAULT 'open',
143
- resolved_by TEXT,
144
- created_at TEXT DEFAULT (datetime('now'))
145
- );
146
- CREATE INDEX IF NOT EXISTS idx_contradictions_entity ON contradictions(entity_id);
147
- CREATE INDEX IF NOT EXISTS idx_contradictions_status ON contradictions(status);
148
-
149
- CREATE TABLE IF NOT EXISTS entity_insights (
150
- id INTEGER PRIMARY KEY AUTOINCREMENT,
151
- entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
152
- insight_type TEXT NOT NULL,
153
- insight TEXT NOT NULL,
154
- reasoning TEXT NOT NULL,
155
- confidence REAL DEFAULT 0.70,
156
- source_entity_ids TEXT DEFAULT '[]',
157
- created_at TEXT DEFAULT (datetime('now')),
158
- expires_at TEXT,
159
- is_stale INTEGER DEFAULT 0
160
- );
161
- CREATE INDEX IF NOT EXISTS idx_insights_entity ON entity_insights(entity_id);
162
- CREATE INDEX IF NOT EXISTS idx_insights_type ON entity_insights(insight_type);
163
- `);
164
- initialized.add(t.slug);
165
- }
166
- // ─── Helpers ──────────────────────────────────────────────────────────────────
167
- function normalizeEntityName(name) {
168
- return name.toLowerCase().trim().replace(/\s+/g, ' ');
169
- }
170
- function escapeLike(value) {
171
- return value.replace(/[%_\\]/g, ch => `\\${ch}`);
172
- }
173
- function escapeRegex(str) {
174
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
175
- }
176
- // ─── Entity operations ────────────────────────────────────────────────────────
1
+ /**
2
+ * Entity graph public API. Storage now lives behind the seam
3
+ * (getStorage().entities); these are thin, signature-stable delegators plus the
4
+ * `linkEntitiesFromConversation` orchestrator that composes findOrCreateEntity +
5
+ * addEntityMention (Stage 1, G3). The two internal transactions (mergeEntities,
6
+ * deleteEntity) and the historical merge-edge-relocation fix live in the impl.
7
+ */
8
+ import { getStorage } from './storage.js';
177
9
  export async function findOrCreateEntity(t, name, type, timestamp) {
178
- ensureSchema(t);
179
- const db = getDb(t, 'entityGraph');
180
- const normalizedName = normalizeEntityName(name);
181
- let existing = db.prepare('SELECT * FROM entities WHERE normalized_name = ? AND type = ?')
182
- .get(normalizedName, type);
183
- if (!existing) {
184
- existing = db.prepare(`SELECT * FROM entities WHERE type = ? AND LOWER(aliases) LIKE ? ESCAPE '\\'`)
185
- .get(type, `%"${escapeLike(normalizedName)}"%`);
186
- }
187
- if (existing) {
188
- db.prepare('UPDATE entities SET last_seen = ?, mention_count = mention_count + 1 WHERE id = ?')
189
- .run(timestamp, existing.id);
190
- return rowToEntity({ ...existing, last_seen: timestamp, mention_count: existing.mention_count + 1 });
191
- }
192
- const result = db.prepare(`
193
- INSERT INTO entities (name, normalized_name, aliases, type, first_seen, last_seen, mention_count)
194
- VALUES (?, ?, ?, ?, ?, ?, 1)
195
- `).run(name, normalizedName, '[]', type, timestamp, timestamp);
196
- return {
197
- id: result.lastInsertRowid,
198
- name,
199
- normalized_name: normalizedName,
200
- aliases: [],
201
- type,
202
- first_seen: timestamp,
203
- last_seen: timestamp,
204
- mention_count: 1,
205
- priority: 0.5,
206
- decay_score: 0.0,
207
- tier: 'warm',
208
- access_count: 0,
209
- is_pinned: false,
210
- };
10
+ return getStorage().entities.findOrCreateEntity(t, name, type, timestamp);
211
11
  }
212
12
  export async function addEntityAlias(t, entityId, alias) {
213
- ensureSchema(t);
214
- const db = getDb(t, 'entityGraph');
215
- const entity = db.prepare('SELECT aliases FROM entities WHERE id = ?').get(entityId);
216
- if (!entity)
217
- return;
218
- const aliases = JSON.parse(entity.aliases);
219
- const normalizedAlias = normalizeEntityName(alias);
220
- if (!aliases.includes(normalizedAlias)) {
221
- aliases.push(normalizedAlias);
222
- db.prepare('UPDATE entities SET aliases = ? WHERE id = ?').run(JSON.stringify(aliases), entityId);
223
- }
13
+ return getStorage().entities.addEntityAlias(t, entityId, alias);
224
14
  }
225
15
  export async function addEntityMention(t, entityId, conversationId, sourcePath, context, timestamp, sourceType = 'chat', confidence = 0.85) {
226
- ensureSchema(t);
227
- const db = getDb(t, 'entityGraph');
228
- db.prepare(`
229
- INSERT INTO entity_mentions (entity_id, conversation_id, source_path, context, timestamp, source_type, confidence)
230
- VALUES (?, ?, ?, ?, ?, ?, ?)
231
- `).run(entityId, conversationId, sourcePath, context, timestamp, sourceType, confidence);
16
+ return getStorage().entities.addEntityMention(t, entityId, conversationId, sourcePath, context, timestamp, sourceType, confidence);
232
17
  }
233
18
  export async function linkEntities(t, sourceId, targetId, relationship = 'co-occurred') {
234
- ensureSchema(t);
235
- const db = getDb(t, 'entityGraph');
236
- const [id1, id2] = sourceId < targetId ? [sourceId, targetId] : [targetId, sourceId];
237
- db.prepare(`
238
- INSERT INTO entity_relations (source_id, target_id, relationship, strength)
239
- VALUES (?, ?, ?, 1)
240
- ON CONFLICT(source_id, target_id) DO UPDATE SET strength = strength + 1
241
- `).run(id1, id2, relationship);
19
+ return getStorage().entities.linkEntities(t, sourceId, targetId, relationship);
242
20
  }
243
21
  export async function linkEntitiesWithType(t, sourceId, targetId, options) {
244
- ensureSchema(t);
245
- const db = getDb(t, 'entityGraph');
246
- const isDirectional = DIRECTIONAL_RELATIONSHIPS.has(options.relationship);
247
- const [id1, id2] = isDirectional
248
- ? [sourceId, targetId]
249
- : sourceId < targetId ? [sourceId, targetId] : [targetId, sourceId];
250
- const confidence = options.confidence ?? 0.7;
251
- const method = options.method ?? 'ai-extraction';
252
- const now = new Date().toISOString();
253
- db.prepare(`
254
- INSERT INTO entity_relations (source_id, target_id, relationship, strength, confidence, method, rationale, last_verified)
255
- VALUES (?, ?, ?, 1, ?, ?, ?, ?)
256
- ON CONFLICT(source_id, target_id) DO UPDATE SET
257
- relationship = CASE
258
- WHEN excluded.confidence > entity_relations.confidence THEN excluded.relationship
259
- ELSE entity_relations.relationship
260
- END,
261
- strength = strength + 1,
262
- confidence = MAX(entity_relations.confidence, excluded.confidence),
263
- rationale = COALESCE(excluded.rationale, entity_relations.rationale),
264
- last_verified = excluded.last_verified
265
- `).run(id1, id2, options.relationship, confidence, method, options.rationale ?? null, now);
22
+ return getStorage().entities.linkEntitiesWithType(t, sourceId, targetId, options);
266
23
  }
267
24
  export async function getTypedRelationships(t, entityId) {
268
- ensureSchema(t);
269
- const db = getDb(t, 'entityGraph');
270
- const results = db.prepare(`
271
- SELECT
272
- er.source_id, er.target_id, er.relationship, er.confidence, er.rationale,
273
- e.id, e.name, e.normalized_name, e.type, e.aliases,
274
- e.first_seen, e.last_seen, e.mention_count, e.priority, e.decay_score,
275
- e.tier, e.last_accessed, e.access_count, e.is_pinned, e.last_reasoned_at
276
- FROM entity_relations er
277
- JOIN entities e ON (
278
- CASE WHEN er.source_id = ? THEN er.target_id ELSE er.source_id END = e.id
279
- )
280
- WHERE (er.source_id = ? OR er.target_id = ?)
281
- AND er.relationship != 'co-occurred'
282
- ORDER BY er.confidence DESC, er.strength DESC
283
- `).all(entityId, entityId, entityId);
284
- return results.map(row => ({
285
- entity: rowToEntity({
286
- id: row.id, name: row.name, normalized_name: row.normalized_name, type: row.type, aliases: row.aliases,
287
- first_seen: row.first_seen, last_seen: row.last_seen, mention_count: row.mention_count,
288
- priority: row.priority, decay_score: row.decay_score, tier: row.tier, last_accessed: row.last_accessed,
289
- access_count: row.access_count, is_pinned: row.is_pinned, last_reasoned_at: row.last_reasoned_at,
290
- }),
291
- relationship: row.relationship,
292
- direction: row.source_id === entityId ? 'outgoing' : 'incoming',
293
- confidence: row.confidence ?? 0.5,
294
- rationale: row.rationale ?? undefined,
295
- }));
25
+ return getStorage().entities.getTypedRelationships(t, entityId);
296
26
  }
27
+ /** Orchestrator: compose findOrCreateEntity + addEntityMention per entity. */
297
28
  export async function linkEntitiesFromConversation(t, conversationId, sourcePath, timestamp, entities) {
298
29
  if (entities.length === 0)
299
30
  return;
@@ -302,310 +33,67 @@ export async function linkEntitiesFromConversation(t, conversationId, sourcePath
302
33
  await addEntityMention(t, dbEntity.id, conversationId, sourcePath, entity.context ?? '', timestamp);
303
34
  }
304
35
  }
305
- // ─── Query operations ─────────────────────────────────────────────────────────
306
36
  export async function getEntityContext(t, nameOrId) {
307
- ensureSchema(t);
308
- const db = getDb(t, 'entityGraph');
309
- let entity;
310
- if (typeof nameOrId === 'number') {
311
- entity = db.prepare('SELECT * FROM entities WHERE id = ?').get(nameOrId);
312
- }
313
- else {
314
- const normalized = normalizeEntityName(nameOrId);
315
- entity = db.prepare(`SELECT * FROM entities WHERE normalized_name = ? OR aliases LIKE ? ESCAPE '\\'`)
316
- .get(normalized, `%"${escapeLike(normalized)}"%`);
317
- }
318
- if (!entity)
319
- return null;
320
- const mentions = db.prepare('SELECT * FROM entity_mentions WHERE entity_id = ? ORDER BY timestamp DESC').all(entity.id)
321
- .map(rowToMention);
322
- const relations = db.prepare(`
323
- SELECT
324
- CASE WHEN source_id = ? THEN target_id ELSE source_id END as related_id,
325
- relationship, strength
326
- FROM entity_relations
327
- WHERE source_id = ? OR target_id = ?
328
- ORDER BY strength DESC LIMIT 20
329
- `).all(entity.id, entity.id, entity.id);
330
- const relatedEntities = relations.flatMap(rel => {
331
- const related = db.prepare('SELECT * FROM entities WHERE id = ?').get(rel.related_id);
332
- if (!related)
333
- return [];
334
- return [{ entity: rowToEntity(related), relationship: rel.relationship, strength: rel.strength }];
335
- });
336
- return { entity: rowToEntity(entity), mentions, relatedEntities };
37
+ return getStorage().entities.getEntityContext(t, nameOrId);
337
38
  }
338
39
  export async function searchEntities(t, query, options = {}) {
339
- ensureSchema(t);
340
- const db = getDb(t, 'entityGraph');
341
- const normalized = normalizeEntityName(query);
342
- const escaped = escapeLike(normalized);
343
- const limit = options.limit ?? 20;
344
- let sql = `SELECT * FROM entities WHERE (normalized_name LIKE ? ESCAPE '\\' OR aliases LIKE ? ESCAPE '\\')`;
345
- const params = [`%${escaped}%`, `%${escaped}%`];
346
- if (options.type) {
347
- sql += ' AND type = ?';
348
- params.push(options.type);
349
- }
350
- sql += ' ORDER BY mention_count DESC LIMIT ?';
351
- params.push(limit);
352
- return db.prepare(sql).all(...params).map(rowToEntity);
40
+ return getStorage().entities.searchEntities(t, query, options);
353
41
  }
354
42
  export async function getRecentEntities(t, limit = 20) {
355
- ensureSchema(t);
356
- const db = getDb(t, 'entityGraph');
357
- return db.prepare('SELECT * FROM entities ORDER BY last_seen DESC LIMIT ?').all(limit).map(rowToEntity);
43
+ return getStorage().entities.getRecentEntities(t, limit);
358
44
  }
359
45
  export async function getMostMentionedEntities(t, options = {}) {
360
- ensureSchema(t);
361
- const db = getDb(t, 'entityGraph');
362
- const limit = options.limit ?? 20;
363
- let sql = 'SELECT * FROM entities';
364
- const params = [];
365
- if (options.type) {
366
- sql += ' WHERE type = ?';
367
- params.push(options.type);
368
- }
369
- sql += ' ORDER BY mention_count DESC LIMIT ?';
370
- params.push(limit);
371
- return db.prepare(sql).all(...params).map(rowToEntity);
46
+ return getStorage().entities.getMostMentionedEntities(t, options);
372
47
  }
373
48
  export async function getEntityGraphStats(t) {
374
- ensureSchema(t);
375
- const db = getDb(t, 'entityGraph');
376
- const { count: totalEntities } = db.prepare('SELECT COUNT(*) as count FROM entities').get();
377
- const { count: totalMentions } = db.prepare('SELECT COUNT(*) as count FROM entity_mentions').get();
378
- const { count: totalRelations } = db.prepare('SELECT COUNT(*) as count FROM entity_relations').get();
379
- const byTypeRows = db.prepare('SELECT type, COUNT(*) as count FROM entities GROUP BY type').all();
380
- const byType = { person: 0, company: 0, project: 0, place: 0, topic: 0 };
381
- for (const row of byTypeRows)
382
- byType[row.type] = row.count;
383
- return { totalEntities, totalMentions, totalRelations, byType };
49
+ return getStorage().entities.stats(t);
384
50
  }
385
51
  export async function detectEntitiesInQuery(t, query) {
386
- ensureSchema(t);
387
- const db = getDb(t, 'entityGraph');
388
- const allEntities = db.prepare(`
389
- SELECT name, normalized_name, type FROM entities
390
- WHERE type IN ('person', 'project', 'company')
391
- ORDER BY mention_count DESC
392
- `).all();
393
- const detectedEntities = [];
394
- let remainingQuery = query;
395
- for (const entity of allEntities) {
396
- const nameRegex = new RegExp(`\\b${escapeRegex(entity.name.toLowerCase())}\\b`, 'i');
397
- const normalizedRegex = new RegExp(`\\b${escapeRegex(entity.normalized_name.toLowerCase())}\\b`, 'i');
398
- if (nameRegex.test(query) || normalizedRegex.test(query)) {
399
- detectedEntities.push(entity.name);
400
- remainingQuery = remainingQuery.replace(nameRegex, '').replace(normalizedRegex, '');
401
- }
402
- }
403
- remainingQuery = remainingQuery.replace(/\b(and|or|with|about)\b/gi, '').replace(/\s+/g, ' ').trim();
404
- return { entities: detectedEntities, remainingQuery };
52
+ return getStorage().entities.detectEntitiesInQuery(t, query);
405
53
  }
406
- // ─── Merge + delete ───────────────────────────────────────────────────────────
407
54
  export async function mergeEntities(t, keepId, removeId, reason, confidence, aiRationale, mergedBy) {
408
- ensureSchema(t);
409
- const db = getDb(t, 'entityGraph');
410
- const keepEntity = db.prepare('SELECT * FROM entities WHERE id = ?').get(keepId);
411
- const removeEntity = db.prepare('SELECT * FROM entities WHERE id = ?').get(removeId);
412
- if (!keepEntity || !removeEntity)
413
- throw new Error(`Entity not found: keep=${keepId} remove=${removeId}`);
414
- let mentionsMoved = 0;
415
- let relationsMoved = 0;
416
- const doMerge = db.transaction(() => {
417
- const mentionResult = db.prepare('UPDATE entity_mentions SET entity_id = ? WHERE entity_id = ?').run(keepId, removeId);
418
- mentionsMoved = mentionResult.changes;
419
- const entityExists = (id) => db.prepare('SELECT 1 FROM entities WHERE id = ?').get(id) !== undefined;
420
- for (const rel of db.prepare('SELECT * FROM entity_relations WHERE source_id = ?').all(removeId)) {
421
- const targetId = rel.target_id === removeId ? keepId : rel.target_id;
422
- if (targetId === keepId || !entityExists(targetId))
423
- continue;
424
- const existing = db.prepare('SELECT 1 FROM entity_relations WHERE source_id = ? AND target_id = ?').get(keepId, targetId);
425
- if (existing) {
426
- db.prepare('UPDATE entity_relations SET strength = strength + ?, confidence = MAX(confidence, ?) WHERE source_id = ? AND target_id = ?')
427
- .run(rel.strength, rel.confidence ?? 0.5, keepId, targetId);
428
- }
429
- else {
430
- db.prepare('INSERT INTO entity_relations (source_id, target_id, relationship, strength, confidence, method, rationale) VALUES (?, ?, ?, ?, ?, ?, ?)')
431
- .run(keepId, targetId, rel.relationship, rel.strength, rel.confidence ?? 0.5, rel.method ?? 'merged', rel.rationale ?? null);
432
- relationsMoved++;
433
- }
434
- }
435
- for (const rel of db.prepare('SELECT * FROM entity_relations WHERE target_id = ?').all(removeId)) {
436
- const sourceId = rel.source_id === removeId ? keepId : rel.source_id;
437
- if (sourceId === keepId || !entityExists(sourceId))
438
- continue;
439
- const existing = db.prepare('SELECT 1 FROM entity_relations WHERE source_id = ? AND target_id = ?').get(sourceId, keepId);
440
- if (existing) {
441
- db.prepare('UPDATE entity_relations SET strength = strength + ?, confidence = MAX(confidence, ?) WHERE source_id = ? AND target_id = ?')
442
- .run(rel.strength, rel.confidence ?? 0.5, sourceId, keepId);
443
- }
444
- else {
445
- db.prepare('INSERT INTO entity_relations (source_id, target_id, relationship, strength, confidence, method, rationale) VALUES (?, ?, ?, ?, ?, ?, ?)')
446
- .run(sourceId, keepId, rel.relationship, rel.strength, rel.confidence ?? 0.5, rel.method ?? 'merged', rel.rationale ?? null);
447
- relationsMoved++;
448
- }
449
- }
450
- db.prepare('DELETE FROM entity_relations WHERE source_id = ? OR target_id = ?').run(removeId, removeId);
451
- db.prepare('DELETE FROM entity_relations WHERE source_id = ? AND target_id = ?').run(keepId, keepId);
452
- const keepAliases = JSON.parse(keepEntity.aliases || '[]');
453
- const removeAliases = JSON.parse(removeEntity.aliases || '[]');
454
- const allAliases = new Set([...keepAliases, ...removeAliases, normalizeEntityName(removeEntity.name)]);
455
- allAliases.delete(normalizeEntityName(keepEntity.name));
456
- db.prepare('UPDATE entities SET aliases = ? WHERE id = ?').run(JSON.stringify([...allAliases]), keepId);
457
- db.prepare(`
458
- UPDATE entities SET
459
- mention_count = mention_count + ?,
460
- first_seen = MIN(first_seen, ?),
461
- last_seen = MAX(last_seen, ?)
462
- WHERE id = ?
463
- `).run(removeEntity.mention_count, removeEntity.first_seen, removeEntity.last_seen, keepId);
464
- db.prepare(`
465
- INSERT INTO entity_merges (keep_id, remove_id, keep_name, remove_name, keep_type, remove_type, reason, confidence, ai_rationale, mentions_moved, relations_moved, merged_by)
466
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
467
- `).run(keepId, removeId, keepEntity.name, removeEntity.name, keepEntity.type, removeEntity.type, reason, confidence ?? null, aiRationale ?? null, mentionsMoved, relationsMoved, mergedBy ?? 'sleep:entity-hygiene');
468
- db.prepare('DELETE FROM entities WHERE id = ?').run(removeId);
469
- });
470
- doMerge();
471
- return { mentions_moved: mentionsMoved, relations_moved: relationsMoved };
55
+ return getStorage().entities.mergeEntities(t, keepId, removeId, reason, confidence, aiRationale, mergedBy);
472
56
  }
473
57
  export async function deleteEntity(t, entityId, reason, mergedBy) {
474
- ensureSchema(t);
475
- const db = getDb(t, 'entityGraph');
476
- const entity = db.prepare('SELECT * FROM entities WHERE id = ?').get(entityId);
477
- if (!entity)
478
- return;
479
- const doDelete = db.transaction(() => {
480
- const mentionCount = db.prepare('SELECT COUNT(*) as c FROM entity_mentions WHERE entity_id = ?').get(entityId).c;
481
- const relCount = db.prepare('SELECT COUNT(*) as c FROM entity_relations WHERE source_id = ? OR target_id = ?').get(entityId, entityId).c;
482
- db.prepare('DELETE FROM entity_mentions WHERE entity_id = ?').run(entityId);
483
- db.prepare('DELETE FROM entity_relations WHERE source_id = ? OR target_id = ?').run(entityId, entityId);
484
- db.prepare('DELETE FROM entities WHERE id = ?').run(entityId);
485
- db.prepare(`
486
- INSERT INTO entity_merges (keep_id, remove_id, keep_name, remove_name, keep_type, remove_type, reason, mentions_moved, relations_moved, merged_by)
487
- VALUES (0, ?, NULL, ?, NULL, ?, ?, ?, ?, ?)
488
- `).run(entityId, entity.name, entity.type, reason, mentionCount, relCount, mergedBy ?? 'sleep:entity-hygiene');
489
- });
490
- doDelete();
58
+ return getStorage().entities.deleteEntity(t, entityId, reason, mergedBy);
491
59
  }
492
- // ─── Entity profiles ──────────────────────────────────────────────────────────
493
60
  export async function getEntityProfile(t, entityId) {
494
- ensureSchema(t);
495
- const db = getDb(t, 'entityGraph');
496
- const row = db.prepare('SELECT profile, generated_at, fact_count FROM entity_profiles WHERE entity_id = ?')
497
- .get(entityId);
498
- if (!row)
499
- return null;
500
- return { profile: row.profile, generated_at: row.generated_at, fact_count: row.fact_count };
61
+ return getStorage().entities.getEntityProfile(t, entityId);
501
62
  }
502
63
  export async function saveEntityProfile(t, entityId, profile, factCount) {
503
- ensureSchema(t);
504
- const db = getDb(t, 'entityGraph');
505
- db.prepare(`
506
- INSERT INTO entity_profiles (entity_id, profile, generated_at, fact_count)
507
- VALUES (?, ?, datetime('now'), ?)
508
- ON CONFLICT(entity_id) DO UPDATE SET
509
- profile = excluded.profile,
510
- generated_at = excluded.generated_at,
511
- fact_count = excluded.fact_count
512
- `).run(entityId, profile, factCount);
64
+ return getStorage().entities.saveEntityProfile(t, entityId, profile, factCount);
513
65
  }
514
- // ─── Contradictions ───────────────────────────────────────────────────────────
515
66
  export async function createContradiction(t, entityId, factAId, factBId, factA, factB, description) {
516
- ensureSchema(t);
517
- const db = getDb(t, 'entityGraph');
518
- const result = db.prepare(`
519
- INSERT INTO contradictions (entity_id, fact_a_id, fact_b_id, fact_a, fact_b, description)
520
- VALUES (?, ?, ?, ?, ?, ?)
521
- `).run(entityId, factAId, factBId, factA, factB, description);
522
- return result.lastInsertRowid;
67
+ return getStorage().entities.createContradiction(t, entityId, factAId, factBId, factA, factB, description);
523
68
  }
524
69
  export async function getOpenContradictions(t, entityId) {
525
- ensureSchema(t);
526
- const db = getDb(t, 'entityGraph');
527
- const rows = db.prepare(`SELECT * FROM contradictions WHERE entity_id = ? AND status = 'open'`).all(entityId);
528
- return rows.map(r => ({
529
- id: r.id, entity_id: r.entity_id ?? undefined, fact_a_id: r.fact_a_id ?? undefined,
530
- fact_b_id: r.fact_b_id ?? undefined, fact_a: r.fact_a ?? undefined, fact_b: r.fact_b ?? undefined,
531
- description: r.description ?? undefined, status: r.status,
532
- resolved_by: r.resolved_by ?? undefined, created_at: r.created_at,
533
- }));
70
+ return getStorage().entities.getOpenContradictions(t, entityId);
534
71
  }
535
72
  export async function resolveContradiction(t, contradictionId, resolvedBy) {
536
- ensureSchema(t);
537
- const db = getDb(t, 'entityGraph');
538
- db.prepare(`UPDATE contradictions SET status = 'resolved', resolved_by = ? WHERE id = ?`).run(resolvedBy, contradictionId);
73
+ return getStorage().entities.resolveContradiction(t, contradictionId, resolvedBy);
539
74
  }
540
75
  export async function applyContradictions(t, entityId, factIds) {
541
- // Placeholder: contradiction application logic is in fact-contradiction module (Day 3).
542
- // This stub marks contradictions as resolved when caller provides the winning fact ids.
543
- ensureSchema(t);
544
- const db = getDb(t, 'entityGraph');
545
- if (factIds.length > 0) {
546
- db.prepare(`UPDATE contradictions SET status = 'resolved', resolved_by = 'sleep:observe' WHERE entity_id = ? AND status = 'open'`)
547
- .run(entityId);
548
- }
76
+ return getStorage().entities.applyContradictions(t, entityId, factIds);
549
77
  }
550
- // ─── Entity insights ──────────────────────────────────────────────────────────
551
78
  export async function saveEntityInsight(t, entityId, insightType, insight, reasoning, confidence, sourceEntityIds = [], expiresAt) {
552
- ensureSchema(t);
553
- const db = getDb(t, 'entityGraph');
554
- const result = db.prepare(`
555
- INSERT INTO entity_insights (entity_id, insight_type, insight, reasoning, confidence, source_entity_ids, expires_at)
556
- VALUES (?, ?, ?, ?, ?, ?, ?)
557
- `).run(entityId, insightType, insight, reasoning, confidence, JSON.stringify(sourceEntityIds), expiresAt ?? null);
558
- return result.lastInsertRowid;
79
+ return getStorage().entities.saveEntityInsight(t, entityId, insightType, insight, reasoning, confidence, sourceEntityIds, expiresAt);
559
80
  }
560
81
  export async function getEntityInsights(t, entityId, minConfidence = 0.60) {
561
- ensureSchema(t);
562
- const db = getDb(t, 'entityGraph');
563
- const rows = db.prepare(`
564
- SELECT * FROM entity_insights
565
- WHERE entity_id = ? AND is_stale = 0 AND confidence >= ?
566
- AND (expires_at IS NULL OR expires_at > datetime('now'))
567
- ORDER BY confidence DESC, created_at DESC
568
- `).all(entityId, minConfidence);
569
- return rows.map(rowToInsight);
82
+ return getStorage().entities.getEntityInsights(t, entityId, minConfidence);
570
83
  }
571
84
  export async function markInsightsStale(t, entityId) {
572
- ensureSchema(t);
573
- const db = getDb(t, 'entityGraph');
574
- db.prepare('UPDATE entity_insights SET is_stale = 1 WHERE entity_id = ?').run(entityId);
85
+ return getStorage().entities.markInsightsStale(t, entityId);
575
86
  }
576
87
  export async function getEntitiesForReasoning(t, limit = 5, staleDays = 7, recencyDays = 14) {
577
- ensureSchema(t);
578
- const db = getDb(t, 'entityGraph');
579
- return db.prepare(`
580
- SELECT id, name, type FROM entities
581
- WHERE mention_count >= 3
582
- AND (last_reasoned_at IS NULL OR last_reasoned_at < datetime('now', ?))
583
- AND (last_seen IS NULL OR last_seen > datetime('now', ?))
584
- ORDER BY mention_count DESC
585
- LIMIT ?
586
- `).all(`-${staleDays} days`, `-${recencyDays} days`, limit);
88
+ return getStorage().entities.getEntitiesForReasoning(t, limit, staleDays, recencyDays);
587
89
  }
588
90
  export async function markEntityReasoned(t, entityId) {
589
- ensureSchema(t);
590
- const db = getDb(t, 'entityGraph');
591
- db.prepare(`UPDATE entities SET last_reasoned_at = datetime('now') WHERE id = ?`).run(entityId);
91
+ return getStorage().entities.markEntityReasoned(t, entityId);
592
92
  }
593
- // ─── Pin operations ───────────────────────────────────────────────────────────
594
93
  export async function pinEntity(t, entityId, pinned = true) {
595
- ensureSchema(t);
596
- const db = getDb(t, 'entityGraph');
597
- db.prepare('UPDATE entities SET is_pinned = ? WHERE id = ?').run(pinned ? 1 : 0, entityId);
94
+ return getStorage().entities.pinEntity(t, entityId, pinned);
598
95
  }
599
96
  export async function listPinned(t, type) {
600
- ensureSchema(t);
601
- const db = getDb(t, 'entityGraph');
602
- let sql = 'SELECT * FROM entities WHERE is_pinned = 1';
603
- const params = [];
604
- if (type) {
605
- sql += ' AND type = ?';
606
- params.push(type);
607
- }
608
- sql += ' ORDER BY last_seen DESC';
609
- return db.prepare(sql).all(...params).map(rowToEntity);
97
+ return getStorage().entities.listPinned(t, type);
610
98
  }
611
99
  //# sourceMappingURL=entity-graph.js.map