@devflow-tools/memory-engine 0.16.17 → 0.16.19

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 (42) hide show
  1. package/dist/explicit-memory-enricher.d.ts +9 -0
  2. package/dist/explicit-memory-enricher.d.ts.map +1 -1
  3. package/dist/explicit-memory-enricher.js +25 -5
  4. package/dist/explicit-memory-enricher.js.map +1 -1
  5. package/dist/hybrid-search.d.ts.map +1 -1
  6. package/dist/hybrid-search.js +20 -1
  7. package/dist/hybrid-search.js.map +1 -1
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/memory-engine.d.ts.map +1 -1
  13. package/dist/memory-engine.js +6 -0
  14. package/dist/memory-engine.js.map +1 -1
  15. package/dist/memory-gate.d.ts.map +1 -1
  16. package/dist/memory-gate.js +54 -31
  17. package/dist/memory-gate.js.map +1 -1
  18. package/dist/memory-relevance.js +2 -2
  19. package/dist/memory-store.d.ts +40 -0
  20. package/dist/memory-store.d.ts.map +1 -1
  21. package/dist/memory-store.js +329 -9
  22. package/dist/memory-store.js.map +1 -1
  23. package/dist/migrations/v9-to-v10.d.ts +3 -0
  24. package/dist/migrations/v9-to-v10.d.ts.map +1 -0
  25. package/dist/migrations/v9-to-v10.js +45 -0
  26. package/dist/migrations/v9-to-v10.js.map +1 -0
  27. package/dist/migrations/v9-to-v10.test.d.ts +2 -0
  28. package/dist/migrations/v9-to-v10.test.d.ts.map +1 -0
  29. package/dist/migrations/v9-to-v10.test.js +40 -0
  30. package/dist/migrations/v9-to-v10.test.js.map +1 -0
  31. package/dist/semantic-memory.d.ts +38 -0
  32. package/dist/semantic-memory.d.ts.map +1 -0
  33. package/dist/semantic-memory.js +205 -0
  34. package/dist/semantic-memory.js.map +1 -0
  35. package/dist/semantic-memory.test.d.ts +2 -0
  36. package/dist/semantic-memory.test.d.ts.map +1 -0
  37. package/dist/semantic-memory.test.js +39 -0
  38. package/dist/semantic-memory.test.js.map +1 -0
  39. package/dist/session-summarizer.d.ts +2 -1
  40. package/dist/session-summarizer.d.ts.map +1 -1
  41. package/dist/session-summarizer.js.map +1 -1
  42. package/package.json +3 -3
@@ -9,8 +9,10 @@ import { applyV4ToV5Migration } from './migrations/v4-to-v5.js';
9
9
  import { applyV6ToV7Migration } from './migrations/v6-to-v7.js';
10
10
  import { applyV7ToV8Migration } from './migrations/v7-to-v8.js';
11
11
  import { applyV8ToV9Migration } from './migrations/v8-to-v9.js';
12
+ import { applyV9ToV10Migration } from './migrations/v9-to-v10.js';
12
13
  import { observationDedupKey, } from './observation-types.js';
13
14
  import { analysesConflict, analyzePreference, analyzePreferenceObservation, } from './preference-conflicts.js';
15
+ import { mergeSemanticEntities, normalizeEntityKey, normalizeSemanticMemory, } from './semantic-memory.js';
14
16
  // ── Schema ─────────────────────────────────────────────
15
17
  const SCHEMA_V2 = `
16
18
  -- ============================================================
@@ -176,7 +178,17 @@ CREATE TABLE IF NOT EXISTS observations (
176
178
  timestamp INTEGER NOT NULL,
177
179
  explicit_accepted_at INTEGER,
178
180
  evidence_status TEXT NOT NULL DEFAULT 'unverified',
179
- evidence_details TEXT NOT NULL DEFAULT '[]'
181
+ evidence_details TEXT NOT NULL DEFAULT '[]',
182
+ semantic_subject TEXT NOT NULL DEFAULT '',
183
+ semantic_predicate TEXT NOT NULL DEFAULT '',
184
+ semantic_value TEXT NOT NULL DEFAULT '',
185
+ semantic_polarity TEXT NOT NULL DEFAULT 'neutral',
186
+ semantic_key TEXT,
187
+ facet_key TEXT,
188
+ entities TEXT NOT NULL DEFAULT '[]',
189
+ temporal TEXT,
190
+ reinforce_count INTEGER NOT NULL DEFAULT 0,
191
+ last_reinforced_at INTEGER
180
192
  );
181
193
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
182
194
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
@@ -411,6 +423,7 @@ export class MemoryStore {
411
423
  applyV6ToV7Migration(this.db);
412
424
  applyV7ToV8Migration(this.db);
413
425
  applyV8ToV9Migration(this.db);
426
+ applyV9ToV10Migration(this.db);
414
427
  this.repairOrphanSessions(rootPath);
415
428
  console.log('[MemoryStore] Schema initialized', {
416
429
  dbPath: this.dbPath,
@@ -1232,8 +1245,8 @@ export class MemoryStore {
1232
1245
  this.ensureSession(obs.sessionId, obs.project ?? this.defaultProjectRoot);
1233
1246
  const id = obs.id ?? `obs:${Date.now()}:${randomUUID().slice(0, 8)}`;
1234
1247
  const now = obs.timestamp ?? Date.now();
1235
- const dedupKey = observationDedupKey(obs.type, obs.title);
1236
- const duplicate = this.findObservationByDedupKey(dedupKey);
1248
+ const dedupKey = obs.semantic?.semanticKey ?? observationDedupKey(obs.type, obs.title);
1249
+ const duplicate = obs.semantic ? null : this.findObservationByDedupKey(dedupKey);
1237
1250
  if (duplicate) {
1238
1251
  this.linkObservationToEvents(duplicate.id, obs.sourceEventIds ?? []);
1239
1252
  this.incrementObservationAccess(duplicate.id, now);
@@ -1244,8 +1257,10 @@ export class MemoryStore {
1244
1257
  (id, session_id, project, type, title, narrative, facts, files, tools, concepts,
1245
1258
  importance, confidence, source, dedup_key, source_event_ids, timestamp, updated_at,
1246
1259
  quality_status, quarantine_reason, quality_details, quarantined_at,
1247
- failure_category, resolution_state, evidence_status, evidence_details)
1248
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1260
+ failure_category, resolution_state, evidence_status, evidence_details,
1261
+ semantic_subject, semantic_predicate, semantic_value, semantic_polarity,
1262
+ semantic_key, facet_key, entities, temporal, reinforce_count, last_reinforced_at)
1263
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1249
1264
  `).run(id, obs.sessionId, obs.project ?? this.defaultProjectRoot, obs.type, obs.title, obs.narrative ?? '', JSON.stringify(obs.facts ?? []), JSON.stringify(obs.files ?? []), JSON.stringify(obs.tools ?? []), JSON.stringify(obs.concepts ?? []), obs.importance ?? 5, obs.confidence ?? 0.5, obs.source ?? 'distill', dedupKey, JSON.stringify(obs.sourceEventIds ?? []), now, now, obs.qualityStatus ?? 'accepted', obs.quarantineReason ?? null, obs.qualityDetails ? JSON.stringify(obs.qualityDetails) : null, obs.qualityStatus === 'quarantined' ? now : null, obs.failureCategory ?? null, obs.resolutionState ?? 'current', obs.evidenceStatus
1250
1265
  ?? (obs.source === 'explicit_intent'
1251
1266
  ? 'unverified'
@@ -1255,7 +1270,7 @@ export class MemoryStore {
1255
1270
  kind: 'source_event',
1256
1271
  source,
1257
1272
  summary: 'Observation was accepted from an evidence-bound session event.',
1258
- }))));
1273
+ }))), obs.semantic?.subject ?? '', obs.semantic?.predicate ?? '', obs.semantic?.value ?? '', obs.semantic?.polarity ?? 'neutral', obs.semantic?.semanticKey ?? null, obs.semantic?.facetKey ?? null, JSON.stringify(obs.semantic?.entities ?? []), obs.semantic?.temporal ? JSON.stringify(obs.semantic.temporal) : null, 0, null);
1259
1274
  // Insert concept junction rows
1260
1275
  if (obs.concepts && obs.concepts.length > 0) {
1261
1276
  const insertConcept = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
@@ -1263,11 +1278,184 @@ export class MemoryStore {
1263
1278
  insertConcept.run(id, concept);
1264
1279
  }
1265
1280
  }
1281
+ this.replaceObservationEntities(id, obs.project ?? this.defaultProjectRoot, obs.semantic?.entities ?? []);
1266
1282
  console.log('[MemoryStore] Observation added', {
1267
1283
  id, sessionId: obs.sessionId, type: obs.type, conceptCount: obs.concepts?.length ?? 0,
1268
1284
  });
1269
1285
  return this.getObservation(id);
1270
1286
  }
1287
+ acceptSemanticObservation(obs) {
1288
+ return this.db.transaction(() => {
1289
+ const project = obs.project ?? this.defaultProjectRoot;
1290
+ const now = obs.timestamp ?? Date.now();
1291
+ const exact = this.findCurrentObservationBySemanticKey(project, obs.semantic.semanticKey)
1292
+ ?? this.findCurrentObservationByNarrative(project, obs.narrative ?? '');
1293
+ if (exact) {
1294
+ const sourceEventIds = uniqueStrings([
1295
+ ...parseStringArray(exact.source_event_ids),
1296
+ ...(obs.sourceEventIds ?? []),
1297
+ ]);
1298
+ const facts = uniqueStrings([...parseStringArray(exact.facts), ...(obs.facts ?? [])]);
1299
+ const concepts = uniqueStrings([
1300
+ ...parseStringArray(exact.concepts),
1301
+ ...(obs.concepts ?? []),
1302
+ obs.semantic.subject,
1303
+ obs.semantic.predicate,
1304
+ obs.semantic.value,
1305
+ ...obs.semantic.entities.map(normalizeEntityKey),
1306
+ ]);
1307
+ const entities = mergeSemanticEntities(parseStringArray(exact.entities), obs.semantic.entities);
1308
+ const narrative = semanticRichness(obs.narrative ?? '') > semanticRichness(exact.narrative)
1309
+ ? obs.narrative ?? ''
1310
+ : exact.narrative;
1311
+ const title = semanticRichness(obs.title) > semanticRichness(exact.title) ? obs.title : exact.title;
1312
+ const contentChanged = exact.title !== title
1313
+ || exact.narrative !== narrative;
1314
+ this.db.prepare(`
1315
+ UPDATE observations
1316
+ SET type = ?, title = ?, narrative = ?, facts = ?, concepts = ?, importance = MAX(importance, ?),
1317
+ confidence = MAX(confidence, ?), source_event_ids = ?, updated_at = ?,
1318
+ access_count = access_count + 1, reinforce_count = reinforce_count + 1, last_reinforced_at = ?,
1319
+ source = CASE WHEN ? = 'explicit_intent' THEN 'explicit_intent' ELSE source END,
1320
+ quality_status = CASE WHEN quality_status = 'quarantined' AND ? = 'accepted' THEN 'accepted' ELSE quality_status END,
1321
+ quarantine_reason = CASE WHEN ? = 'accepted' THEN NULL ELSE quarantine_reason END,
1322
+ resolution_state = 'current', superseded_by_observation = NULL,
1323
+ semantic_subject = ?, semantic_predicate = ?, semantic_value = ?, semantic_polarity = ?,
1324
+ semantic_key = ?, facet_key = ?, dedup_key = ?, entities = ?,
1325
+ temporal = COALESCE(?, temporal), evidence_status = ?, evidence_details = ?,
1326
+ embedding = CASE WHEN ? = 1 THEN NULL ELSE embedding END
1327
+ WHERE id = ?
1328
+ `).run(obs.type, title, narrative, JSON.stringify(facts), JSON.stringify(concepts), obs.importance ?? 5, obs.confidence ?? 0.5, JSON.stringify(sourceEventIds), now, now, obs.source ?? 'distill', obs.qualityStatus ?? 'accepted', obs.qualityStatus ?? 'accepted', obs.semantic.subject, obs.semantic.predicate, obs.semantic.value, obs.semantic.polarity, obs.semantic.semanticKey, obs.semantic.facetKey, obs.semantic.semanticKey, JSON.stringify(entities), obs.semantic.temporal ? JSON.stringify(obs.semantic.temporal) : null, strongerEvidenceStatus(exact.evidence_status, obs.evidenceStatus), JSON.stringify(mergeEvidenceDetails(exact.evidence_details, obs.evidenceDetails ?? [])), contentChanged ? 1 : 0, exact.id);
1329
+ if (contentChanged) {
1330
+ this.db.prepare("DELETE FROM embeddings WHERE source_type = 'observation' AND source_id = ?").run(exact.id);
1331
+ }
1332
+ this.replaceObservationConcepts(exact.id, concepts);
1333
+ this.replaceObservationEntities(exact.id, project, entities);
1334
+ for (const eventId of obs.sourceEventIds ?? [])
1335
+ this.linkObservationToEvents(exact.id, [eventId]);
1336
+ const supersededIds = this.supersedeSemanticFacetConflicts(project, exact.id, obs.semantic, now);
1337
+ return { row: this.getObservation(exact.id), action: 'REINFORCE', supersededIds, linkedIds: [] };
1338
+ }
1339
+ const row = this.addObservation(obs);
1340
+ if ((obs.qualityStatus ?? 'accepted') === 'quarantined') {
1341
+ return { row, action: 'HOLD', supersededIds: [], linkedIds: [] };
1342
+ }
1343
+ const supersededIds = this.supersedeSemanticFacetConflicts(project, row.id, obs.semantic, now);
1344
+ const relatedIds = this.findCurrentObservationIdsByEntities(project, obs.semantic.entities, new Set([row.id, ...supersededIds])).slice(0, 12);
1345
+ for (const relatedId of relatedIds) {
1346
+ this.addObservationRelation(row.id, relatedId, 'related_to', now);
1347
+ }
1348
+ return {
1349
+ row: this.getObservation(row.id),
1350
+ action: supersededIds.length > 0 ? 'SUPERSEDE' : relatedIds.length > 0 ? 'LINK' : 'ADD',
1351
+ supersededIds,
1352
+ linkedIds: relatedIds,
1353
+ };
1354
+ })();
1355
+ }
1356
+ findCurrentObservationBySemanticKey(project, semanticKey) {
1357
+ return this.db.prepare(`
1358
+ SELECT * FROM observations
1359
+ WHERE project = ? AND semantic_key = ?
1360
+ ORDER BY CASE WHEN resolution_state = 'current' THEN 0 ELSE 1 END,
1361
+ COALESCE(explicit_accepted_at, timestamp) DESC, id DESC
1362
+ LIMIT 1
1363
+ `).get(project, semanticKey) ?? null;
1364
+ }
1365
+ findCurrentObservationByNarrative(project, narrative) {
1366
+ if (!narrative.trim())
1367
+ return null;
1368
+ return this.db.prepare(`
1369
+ SELECT * FROM observations
1370
+ WHERE project = ? AND narrative = ? AND resolution_state = 'current'
1371
+ AND quality_status = 'accepted'
1372
+ ORDER BY COALESCE(explicit_accepted_at, timestamp) DESC, id DESC
1373
+ LIMIT 1
1374
+ `).get(project, narrative) ?? null;
1375
+ }
1376
+ findCurrentObservationsByFacetKey(project, facetKey) {
1377
+ return this.db.prepare(`
1378
+ SELECT * FROM observations
1379
+ WHERE project = ? AND facet_key = ? AND resolution_state = 'current'
1380
+ AND quality_status = 'accepted'
1381
+ ORDER BY COALESCE(explicit_accepted_at, timestamp) DESC, id DESC
1382
+ `).all(project, facetKey);
1383
+ }
1384
+ searchObservationEntities(project, entities, limit = 30) {
1385
+ const normalized = uniqueStrings(entities.map(normalizeEntityKey)).filter(Boolean);
1386
+ if (normalized.length === 0)
1387
+ return [];
1388
+ const placeholders = normalized.map(() => '?').join(', ');
1389
+ return this.db.prepare(`
1390
+ SELECT o.*, COUNT(DISTINCT oe.normalized_entity) AS entity_match_count
1391
+ FROM observation_entities oe
1392
+ JOIN observations o ON o.id = oe.observation_id
1393
+ WHERE oe.project = ? AND oe.normalized_entity IN (${placeholders})
1394
+ AND o.quality_status = 'accepted' AND o.resolution_state = 'current'
1395
+ GROUP BY o.id
1396
+ ORDER BY entity_match_count DESC, COALESCE(o.explicit_accepted_at, o.timestamp) DESC
1397
+ LIMIT ?
1398
+ `).all(project, ...normalized, limit);
1399
+ }
1400
+ searchDefaultProjectObservationEntities(entities, limit = 30) {
1401
+ return this.searchObservationEntities(this.defaultProjectRoot, entities, limit);
1402
+ }
1403
+ listObservationRelations(observationId) {
1404
+ return this.db.prepare(`
1405
+ SELECT * FROM observation_relations
1406
+ WHERE source_observation_id = ? OR target_observation_id = ?
1407
+ ORDER BY created_at ASC, source_observation_id ASC, target_observation_id ASC
1408
+ `).all(observationId, observationId);
1409
+ }
1410
+ replaceObservationConcepts(observationId, concepts) {
1411
+ this.db.prepare('DELETE FROM observation_concepts WHERE observation_id = ?').run(observationId);
1412
+ const insert = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
1413
+ for (const concept of uniqueStrings(concepts))
1414
+ insert.run(observationId, concept);
1415
+ }
1416
+ replaceObservationEntities(observationId, project, entities) {
1417
+ this.db.prepare('DELETE FROM observation_entities WHERE observation_id = ?').run(observationId);
1418
+ const insert = this.db.prepare(`
1419
+ INSERT OR IGNORE INTO observation_entities
1420
+ (observation_id, project, normalized_entity, display_entity)
1421
+ VALUES (?, ?, ?, ?)
1422
+ `);
1423
+ for (const entity of mergeSemanticEntities(entities)) {
1424
+ const normalized = normalizeEntityKey(entity);
1425
+ if (normalized)
1426
+ insert.run(observationId, project, normalized, entity);
1427
+ }
1428
+ }
1429
+ findCurrentObservationIdsByEntities(project, entities, excludedIds) {
1430
+ return this.searchObservationEntities(project, entities, 50)
1431
+ .map(observation => observation.id)
1432
+ .filter(id => !excludedIds.has(id));
1433
+ }
1434
+ addObservationRelation(sourceObservationId, targetObservationId, type, createdAt = Date.now()) {
1435
+ if (sourceObservationId === targetObservationId)
1436
+ return;
1437
+ this.db.prepare(`
1438
+ INSERT OR IGNORE INTO observation_relations
1439
+ (source_observation_id, target_observation_id, type, created_at)
1440
+ VALUES (?, ?, ?, ?)
1441
+ `).run(sourceObservationId, targetObservationId, type, createdAt);
1442
+ }
1443
+ supersedeSemanticFacetConflicts(project, winnerId, semantic, now) {
1444
+ const conflicts = this.findCurrentObservationsByFacetKey(project, semantic.facetKey)
1445
+ .filter(candidate => candidate.id !== winnerId)
1446
+ .filter(candidate => semanticObservationsConflict(candidate, semantic));
1447
+ const supersededIds = [];
1448
+ for (const conflict of conflicts) {
1449
+ this.db.prepare(`
1450
+ UPDATE observations
1451
+ SET resolution_state = 'superseded', superseded_by_observation = ?, dedup_key = NULL, updated_at = ?
1452
+ WHERE id = ? AND resolution_state = 'current'
1453
+ `).run(winnerId, now, conflict.id);
1454
+ this.addObservationRelation(winnerId, conflict.id, 'supersedes', now);
1455
+ supersededIds.push(conflict.id);
1456
+ }
1457
+ return supersededIds;
1458
+ }
1271
1459
  addObservationWithEmbedding(obs, embedding) {
1272
1460
  return this.db.transaction(() => {
1273
1461
  const row = this.addObservation(obs);
@@ -1276,6 +1464,67 @@ export class MemoryStore {
1276
1464
  })();
1277
1465
  }
1278
1466
  acceptExplicitPreference(input) {
1467
+ if (input.enrichment) {
1468
+ this.ensureSession(input.sessionId, input.project);
1469
+ const preferenceAnalysis = analyzePreference(input.content);
1470
+ const hold = preferenceAnalysis.status === 'ambiguous';
1471
+ const baseSemantic = semanticFromExplicitEnrichment(input.enrichment);
1472
+ const semantic = hold
1473
+ ? normalizeSemanticMemory({
1474
+ project: input.project,
1475
+ type: input.enrichment.type,
1476
+ title: input.enrichment.title,
1477
+ narrative: input.enrichment.narrative,
1478
+ subject: baseSemantic.subject,
1479
+ predicate: baseSemantic.predicate,
1480
+ value: input.content,
1481
+ polarity: 'neutral',
1482
+ entities: baseSemantic.entities,
1483
+ temporal: baseSemantic.temporal ?? undefined,
1484
+ })
1485
+ : baseSemantic;
1486
+ const result = this.acceptSemanticObservation({
1487
+ sessionId: input.sessionId,
1488
+ project: input.project,
1489
+ type: input.enrichment.type,
1490
+ title: input.enrichment.title,
1491
+ narrative: input.enrichment.narrative,
1492
+ facts: input.enrichment.facts,
1493
+ files: input.enrichment.files,
1494
+ tools: input.enrichment.tools,
1495
+ concepts: input.enrichment.concepts,
1496
+ importance: 10,
1497
+ confidence: 0.95,
1498
+ source: 'explicit_intent',
1499
+ sourceEventIds: input.sourceEventId ? [input.sourceEventId] : [],
1500
+ qualityStatus: hold ? 'quarantined' : 'accepted',
1501
+ quarantineReason: hold ? 'ambiguous_explicit_preference' : undefined,
1502
+ evidenceStatus: 'verified',
1503
+ evidenceDetails: [
1504
+ ...input.enrichment.evidence.map(item => ({ ...item })),
1505
+ ...(input.enrichment.evidence.length === 0 ? [{
1506
+ kind: 'source_event',
1507
+ source: input.sourceEventId ?? `explicit:${input.sessionId}`,
1508
+ summary: 'Observation was accepted from a direct explicit-memory request.',
1509
+ }] : []),
1510
+ ],
1511
+ semantic,
1512
+ });
1513
+ const latest = this.db.prepare(`
1514
+ SELECT MAX(COALESCE(explicit_accepted_at, timestamp)) AS latest
1515
+ FROM observations
1516
+ WHERE project = ? AND source = 'explicit_intent'
1517
+ `).get(input.project);
1518
+ const acceptedAt = Math.max(Date.now(), (latest.latest ?? 0) + 1);
1519
+ this.db.prepare(`
1520
+ UPDATE observations
1521
+ SET explicit_accepted_at = MAX(COALESCE(explicit_accepted_at, 0), ?), updated_at = MAX(updated_at, ?)
1522
+ WHERE id = ?
1523
+ `).run(acceptedAt, acceptedAt, result.row.id);
1524
+ if (input.sourceEventId)
1525
+ this.markEventsProcessed([input.sourceEventId]);
1526
+ return this.getObservation(result.row.id);
1527
+ }
1279
1528
  return this.db.transaction(() => {
1280
1529
  this.ensureSession(input.sessionId, input.project);
1281
1530
  const latest = this.db.prepare(`
@@ -1341,7 +1590,7 @@ export class MemoryStore {
1341
1590
  if (current.source !== 'explicit_intent') {
1342
1591
  throw new Error(`Observation ${observationId} is not an explicit memory`);
1343
1592
  }
1344
- const dedupKey = observationDedupKey(enrichment.type, enrichment.title);
1593
+ const dedupKey = enrichment.semanticKey;
1345
1594
  const collisions = this.db.prepare(`
1346
1595
  SELECT id, timestamp, explicit_accepted_at FROM observations
1347
1596
  WHERE project = ? AND dedup_key = ? AND id <> ?
@@ -1390,9 +1639,11 @@ export class MemoryStore {
1390
1639
  UPDATE observations
1391
1640
  SET type = ?, title = ?, narrative = ?, facts = ?, files = ?, tools = ?, concepts = ?,
1392
1641
  dedup_key = ?, evidence_status = ?, evidence_details = ?, updated_at = ?,
1393
- embedding = CASE WHEN ? = 1 THEN NULL ELSE embedding END
1642
+ embedding = CASE WHEN ? = 1 THEN NULL ELSE embedding END,
1643
+ semantic_subject = ?, semantic_predicate = ?, semantic_value = ?, semantic_polarity = ?,
1644
+ semantic_key = ?, facet_key = ?, entities = ?, temporal = ?
1394
1645
  WHERE id = ?
1395
- `).run(enrichment.type, enrichment.title, enrichment.narrative, nextFacts, nextFiles, nextTools, nextConcepts, dedupKeyForCurrent, enrichment.evidenceStatus, JSON.stringify(enrichment.evidence), now, semanticContentChanged ? 1 : 0, observationId);
1646
+ `).run(enrichment.type, enrichment.title, enrichment.narrative, nextFacts, nextFiles, nextTools, nextConcepts, dedupKeyForCurrent, enrichment.evidenceStatus, JSON.stringify(enrichment.evidence), now, semanticContentChanged ? 1 : 0, enrichment.subject, enrichment.predicate, enrichment.value, enrichment.polarity, enrichment.semanticKey, enrichment.facetKey, JSON.stringify(enrichment.entities), enrichment.temporal ? JSON.stringify(enrichment.temporal) : null, observationId);
1396
1647
  if (semanticContentChanged) {
1397
1648
  this.db.prepare("DELETE FROM embeddings WHERE source_type = 'observation' AND source_id = ?").run(observationId);
1398
1649
  }
@@ -1400,6 +1651,7 @@ export class MemoryStore {
1400
1651
  const insertConcept = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
1401
1652
  for (const concept of enrichment.concepts)
1402
1653
  insertConcept.run(observationId, concept);
1654
+ this.replaceObservationEntities(observationId, current.project, enrichment.entities);
1403
1655
  this.reconcileExplicitPreferenceConflicts(current.project);
1404
1656
  return this.getObservation(observationId);
1405
1657
  })();
@@ -1971,6 +2223,74 @@ export class MemoryStore {
1971
2223
  }
1972
2224
  }
1973
2225
  // ── Helpers ────────────────────────────────────────────
2226
+ function uniqueStrings(values) {
2227
+ const result = [];
2228
+ const seen = new Set();
2229
+ for (const value of values) {
2230
+ const normalized = value.normalize('NFKC').trim();
2231
+ const key = normalized.toLocaleLowerCase('en-US');
2232
+ if (!normalized || seen.has(key))
2233
+ continue;
2234
+ seen.add(key);
2235
+ result.push(normalized);
2236
+ }
2237
+ return result;
2238
+ }
2239
+ function semanticRichness(value) {
2240
+ const exactIdentifiers = value.match(/@[a-z0-9._-]+\/[a-z0-9._-]+|--[a-z0-9-]+|[a-z0-9]+(?:[-./][a-z0-9]+)+/giu)?.length ?? 0;
2241
+ return [...value.normalize('NFKC').trim()].length + exactIdentifiers * 40;
2242
+ }
2243
+ function semanticObservationsConflict(existing, incoming) {
2244
+ if (existing.semantic_key === incoming.semanticKey)
2245
+ return false;
2246
+ if (existing.semantic_polarity === 'neutral' || incoming.polarity === 'neutral')
2247
+ return false;
2248
+ const existingValue = normalizeEntityKey(existing.semantic_value);
2249
+ const incomingValue = normalizeEntityKey(incoming.value);
2250
+ if (existingValue === incomingValue)
2251
+ return existing.semantic_polarity !== incoming.polarity;
2252
+ const subject = normalizeEntityKey(incoming.subject);
2253
+ if (new Set(['package manager', 'test runner', 'build tool']).has(subject))
2254
+ return true;
2255
+ if (incoming.predicate === 'version')
2256
+ return true;
2257
+ return ['use', 'prefer', 'require'].includes(incoming.predicate);
2258
+ }
2259
+ function strongerEvidenceStatus(existing, incoming) {
2260
+ return existing === 'verified' || incoming === 'verified' ? 'verified' : 'unverified';
2261
+ }
2262
+ function mergeEvidenceDetails(existing, incoming) {
2263
+ let parsed = [];
2264
+ try {
2265
+ const value = JSON.parse(existing);
2266
+ if (Array.isArray(value)) {
2267
+ parsed = value.filter(item => item && typeof item === 'object' && !Array.isArray(item));
2268
+ }
2269
+ }
2270
+ catch { /* malformed historical evidence is replaced by valid evidence */ }
2271
+ const result = [];
2272
+ const seen = new Set();
2273
+ for (const item of [...parsed, ...incoming]) {
2274
+ const key = JSON.stringify(item);
2275
+ if (seen.has(key))
2276
+ continue;
2277
+ seen.add(key);
2278
+ result.push(item);
2279
+ }
2280
+ return result.slice(0, 64);
2281
+ }
2282
+ function semanticFromExplicitEnrichment(enrichment) {
2283
+ return {
2284
+ subject: enrichment.subject,
2285
+ predicate: enrichment.predicate,
2286
+ value: enrichment.value,
2287
+ polarity: enrichment.polarity,
2288
+ entities: enrichment.entities,
2289
+ temporal: enrichment.temporal,
2290
+ semanticKey: enrichment.semanticKey,
2291
+ facetKey: enrichment.facetKey,
2292
+ };
2293
+ }
1974
2294
  function camelToSnake(str) {
1975
2295
  return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
1976
2296
  }