crbro-memory 2.9.0 → 2.10.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 (80) hide show
  1. package/README.md +25 -16
  2. package/SECURITY.md +27 -2
  3. package/bin/crbro.mjs +18 -1
  4. package/dist/daemon/endpoint.d.ts +1 -0
  5. package/dist/daemon/endpoint.d.ts.map +1 -1
  6. package/dist/daemon/endpoint.js +4 -13
  7. package/dist/daemon/endpoint.js.map +1 -1
  8. package/dist/engine/backup.d.ts.map +1 -1
  9. package/dist/engine/backup.js +4 -0
  10. package/dist/engine/backup.js.map +1 -1
  11. package/dist/engine/cortex.d.ts +18 -6
  12. package/dist/engine/cortex.d.ts.map +1 -1
  13. package/dist/engine/cortex.js +119 -92
  14. package/dist/engine/cortex.js.map +1 -1
  15. package/dist/engine/doctor.d.ts +93 -0
  16. package/dist/engine/doctor.d.ts.map +1 -0
  17. package/dist/engine/doctor.js +378 -0
  18. package/dist/engine/doctor.js.map +1 -0
  19. package/dist/engine/evidence.d.ts +26 -0
  20. package/dist/engine/evidence.d.ts.map +1 -0
  21. package/dist/engine/evidence.js +171 -0
  22. package/dist/engine/evidence.js.map +1 -0
  23. package/dist/engine/hippocampus.d.ts.map +1 -1
  24. package/dist/engine/hippocampus.js +32 -29
  25. package/dist/engine/hippocampus.js.map +1 -1
  26. package/dist/engine/keychain.d.ts.map +1 -1
  27. package/dist/engine/keychain.js +52 -20
  28. package/dist/engine/keychain.js.map +1 -1
  29. package/dist/engine/maintenance.d.ts.map +1 -1
  30. package/dist/engine/maintenance.js +3 -3
  31. package/dist/engine/maintenance.js.map +1 -1
  32. package/dist/engine/prefrontal.d.ts +4 -1
  33. package/dist/engine/prefrontal.d.ts.map +1 -1
  34. package/dist/engine/prefrontal.js +100 -66
  35. package/dist/engine/prefrontal.js.map +1 -1
  36. package/dist/engine/projects.d.ts +20 -0
  37. package/dist/engine/projects.d.ts.map +1 -0
  38. package/dist/engine/projects.js +80 -0
  39. package/dist/engine/projects.js.map +1 -0
  40. package/dist/engine/secrets.d.ts.map +1 -1
  41. package/dist/engine/secrets.js +12 -1
  42. package/dist/engine/secrets.js.map +1 -1
  43. package/dist/engine/shelf.d.ts +66 -8
  44. package/dist/engine/shelf.d.ts.map +1 -1
  45. package/dist/engine/shelf.js +350 -12
  46. package/dist/engine/shelf.js.map +1 -1
  47. package/dist/search/index.d.ts +5 -0
  48. package/dist/search/index.d.ts.map +1 -1
  49. package/dist/search/index.js +21 -8
  50. package/dist/search/index.js.map +1 -1
  51. package/dist/search/quality.d.ts +18 -0
  52. package/dist/search/quality.d.ts.map +1 -0
  53. package/dist/search/quality.js +31 -0
  54. package/dist/search/quality.js.map +1 -0
  55. package/dist/server.d.ts.map +1 -1
  56. package/dist/server.js +123 -34
  57. package/dist/server.js.map +1 -1
  58. package/dist/sync/materialize.d.ts +1 -0
  59. package/dist/sync/materialize.d.ts.map +1 -1
  60. package/dist/sync/materialize.js +33 -8
  61. package/dist/sync/materialize.js.map +1 -1
  62. package/dist/sync/ops.d.ts +7 -2
  63. package/dist/sync/ops.d.ts.map +1 -1
  64. package/dist/sync/ops.js.map +1 -1
  65. package/dist/sync/space.d.ts +5 -3
  66. package/dist/sync/space.d.ts.map +1 -1
  67. package/dist/sync/space.js +187 -77
  68. package/dist/sync/space.js.map +1 -1
  69. package/dist/types/index.d.ts +30 -5
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/dist/utils/lock.d.ts +2 -0
  72. package/dist/utils/lock.d.ts.map +1 -1
  73. package/dist/utils/lock.js +132 -52
  74. package/dist/utils/lock.js.map +1 -1
  75. package/dist/version.d.ts +26 -0
  76. package/dist/version.d.ts.map +1 -0
  77. package/dist/version.js +65 -0
  78. package/dist/version.js.map +1 -0
  79. package/hooks/crbro-lifecycle.mjs +612 -608
  80. package/package.json +5 -5
@@ -12,8 +12,10 @@ const fs_js_1 = require("../utils/fs.js");
12
12
  const ids_js_1 = require("../utils/ids.js");
13
13
  const hash_js_1 = require("../utils/hash.js");
14
14
  const ops_js_1 = require("../sync/ops.js");
15
+ const materialize_js_1 = require("../sync/materialize.js");
15
16
  const secrets_js_1 = require("./secrets.js");
16
17
  const shelf_js_1 = require("./shelf.js");
18
+ const evidence_js_1 = require("./evidence.js");
17
19
  const TYPE_PREFIX_RE = /^(project_|tech_|lang_|person_|domain_|process_|protocol_)/;
18
20
  /**
19
21
  * Similarity above which a new fact is flagged as a near-duplicate of one
@@ -290,6 +292,10 @@ function earliestOf(a, b) {
290
292
  }
291
293
  function copyFact(f) {
292
294
  const out = { ...f };
295
+ if (f.reference)
296
+ out.reference = { ...f.reference };
297
+ if (f.verification)
298
+ out.verification = { ...f.verification, ...(f.verification.reference ? { reference: { ...f.verification.reference } } : {}) };
293
299
  if (f.keys)
294
300
  out.keys = [...f.keys];
295
301
  if (f.supersedes)
@@ -363,9 +369,7 @@ function unionNeuron(target, source) {
363
369
  delete hit.keys;
364
370
  // The later check is the newer evidence; the more volatile explicit class
365
371
  // wins, as in a team space (a needless warning is cheaper than a missing one).
366
- const verificado = (0, shelf_js_1.latestOf)(hit.verified, sf.verified);
367
- if (verificado)
368
- hit.verified = verificado;
372
+ (0, evidence_js_1.mergeFactEvidence)(hit, sf);
369
373
  const vida = (0, shelf_js_1.mostVolatile)(hit.shelf_life, sf.shelf_life);
370
374
  if (vida)
371
375
  hit.shelf_life = vida;
@@ -444,22 +448,10 @@ function unionNeuron(target, source) {
444
448
  neuron.entry_status = estadosVivos;
445
449
  else
446
450
  delete neuron.entry_status;
447
- const verificadas = { ...(source.entry_verified || {}) };
448
- for (const [k, v] of Object.entries(target.entry_verified || {})) {
449
- const l = (0, shelf_js_1.latestOf)(verificadas[k], v);
450
- if (l)
451
- verificadas[k] = l;
452
- else if (!verificadas[k])
453
- verificadas[k] = v;
454
- }
455
- const verificadasVivas = {};
456
- for (const k of Object.keys(verificadas).sort())
457
- if (vivos.has(k))
458
- verificadasVivas[k] = verificadas[k];
459
- if (Object.keys(verificadasVivas).length)
460
- neuron.entry_verified = verificadasVivas;
461
- else
462
- delete neuron.entry_verified;
451
+ // Usar ambos pares originales: una fecha nueva y una fuente vieja no se mezclan.
452
+ neuron.entry_verified = target.entry_verified;
453
+ neuron.entry_verification = target.entry_verification;
454
+ (0, evidence_js_1.mergeEntryEvidence)(neuron, source, vivos);
463
455
  const origenes = { ...(source.entry_source || {}), ...(target.entry_source || {}) };
464
456
  const origenesVivos = {};
465
457
  for (const k of Object.keys(origenes).sort())
@@ -578,33 +570,28 @@ class Cortex {
578
570
  await this.emitter(neuronId, change);
579
571
  }
580
572
  catch {
581
- // Sharing must never break a local write. The note can be re-emitted
582
- // later; a lost fact cannot be recovered.
573
+ // El emisor solo falla si no logró registrar la entrega persistente.
574
+ // La nota local sigue guardada, pero no se puede confirmar éxito total.
575
+ throw new Error('Local change saved, but shared delivery could not be recorded. Check local storage before retrying.');
583
576
  }
584
577
  }
585
578
  /**
586
- * Overwrite a neuron with the result of merging the team's notes.
587
- * Only the sync layer calls this, and only with a neuron that already
588
- * contains everything local plus whatever arrived.
579
+ * Fusionar contra la versión leída dentro del mismo bloqueo de escritura.
580
+ * Un snapshot calculado antes del bloqueo puede borrar un learn o retiro.
589
581
  */
590
- async replaceFromSync(neuron) {
591
- const saved = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(neuron.id), current => {
592
- // Local-only observations stay local: they describe this machine's use
593
- // of the memory, not the knowledge itself.
594
- if (current) {
595
- neuron.heat = current.heat;
596
- neuron.access_count = current.access_count;
597
- neuron.last_accessed = current.last_accessed;
598
- neuron.summary = current.summary || neuron.summary;
599
- neuron.preferences = current.preferences;
600
- neuron.connections = current.connections;
601
- neuron.created = current.created;
602
- }
582
+ async mergeFromSync(nid, ops) {
583
+ let report;
584
+ let created = false;
585
+ const saved = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(nid), current => {
586
+ created = current === null;
587
+ const merged = (0, materialize_js_1.applyOps)(current, ops, { id: nid });
588
+ report = merged.report;
589
+ const neuron = merged.neuron;
603
590
  return neuron;
604
591
  });
605
- const final = (saved || neuron);
606
- await this.reindex(final);
607
- return final;
592
+ if (saved)
593
+ await this.reindex(saved);
594
+ return { report, created };
608
595
  }
609
596
  async reindex(neuron) {
610
597
  if (!this.indexer)
@@ -739,6 +726,8 @@ class Cortex {
739
726
  * Creates the neuron if it does not exist.
740
727
  */
741
728
  async learn(topic, type, content, options) {
729
+ const reference = options?.reference === undefined ? undefined : (0, evidence_js_1.sanitizeReference)(options.reference);
730
+ const temporal = (0, evidence_js_1.sanitizeTemporal)(options?.temporal);
742
731
  // Credentials never make it to disk. The sentence around them survives, so
743
732
  // "the deploy token is [REDACTED: npm token]" still records that a token
744
733
  // exists and what kind — the knowledge without the liability.
@@ -776,9 +765,7 @@ class Cortex {
776
765
  let updatedInPlace = false;
777
766
  let confirmations;
778
767
  let shelf;
779
- let reconfirmed = false;
780
768
  let shelfLengthened = false;
781
- let verificar = null;
782
769
  const vidaPedida = (0, shelf_js_1.isShelfLife)(options?.shelfLife) ? options.shelfLife : undefined;
783
770
  let skippedRetired = null;
784
771
  // A retired decision, pattern, error or debt must not come back through
@@ -883,23 +870,21 @@ class Cortex {
883
870
  cambiado = true;
884
871
  }
885
872
  }
886
- // Saying the same line again, and nothing else, is a check
887
- // (shelf life): the clock restarts. Not when the call also
888
- // brings keywords, a confidence or a class — that is editing the
889
- // line, and the server itself asks for exactly that re-learn to
890
- // add keywords, so counting it would mark an unchecked line as
891
- // checked. Not for the miner either: re-reading an old
892
- // transcript is not looking at the source.
893
- if (options?.source !== 'miner' && !cambiado) {
894
- existente.verified = (0, fs_js_1.now)();
895
- reconfirmed = true;
896
- // One op per line per session: a retry or the re-save after a
897
- // compaction must not add a line to the team's log each time.
898
- const yaVerificadas = (this.tally.verifiedEmitted ??= new Set());
899
- if (!yaVerificadas.has(testigo)) {
900
- yaVerificadas.add(testigo);
901
- verificar = { eid: existente.id || (0, hash_js_1.factId)(existente.text), at: existente.verified };
902
- }
873
+ const referenceChanged = reference && JSON.stringify(reference) !== JSON.stringify(existente.reference);
874
+ const temporalChanged = temporal && temporal !== existente.temporal;
875
+ if (referenceChanged || temporalChanged) {
876
+ const previous = (0, evidence_js_1.mergeCheck)(existente, {});
877
+ existente.verification_invalidated_at = new Date(Math.max(Date.now(), Date.parse(previous.verified || '') || 0, Date.parse(previous.verification_invalidated_at || '') || 0)).toISOString();
878
+ delete existente.verified;
879
+ delete existente.verification;
880
+ }
881
+ if (referenceChanged) {
882
+ existente.reference = reference;
883
+ cambiado = true;
884
+ }
885
+ if (temporalChanged) {
886
+ existente.temporal = temporal;
887
+ cambiado = true;
903
888
  }
904
889
  if (cambiado) {
905
890
  // Note for shared neurons: materialize takes max(conf) and
@@ -907,10 +892,17 @@ class Cortex {
907
892
  // stays local. Documented, not fought — the log is append-only.
908
893
  updatedInPlace = true;
909
894
  this.tally.topics.add(n.id);
910
- emitir = { kind: 'fact', text: existente.text, fid: existente.id || id,
911
- conf: existente.confidence ?? 1, at: existente.added, src: existente.source, keys: existente.keys,
912
- ...(existente.shelf_life ? { shelf: existente.shelf_life } : {}) };
913
895
  }
896
+ // Un intento anterior pudo guardar la neurona pero fallar antes
897
+ // de persistir su entrega. Reemitir permite recuperarla incluso
898
+ // tras reiniciar; applyOps deduplica por fid. No es verificación
899
+ // nueva y conserva la fecha original, no la del reintento.
900
+ emitir = { kind: 'fact', text: existente.text, fid: existente.id || id,
901
+ conf: existente.confidence ?? 1, at: existente.added, src: existente.source, keys: existente.keys,
902
+ ...(existente.reference ? { reference: existente.reference } : {}),
903
+ ...(existente.temporal ? { temporal: existente.temporal } : {}),
904
+ ...(existente.verification_invalidated_at ? { verification_invalidated_at: existente.verification_invalidated_at } : {}),
905
+ ...(existente.shelf_life ? { shelf: existente.shelf_life } : {}) };
914
906
  const sv = (0, shelf_js_1.shelfOfFact)(existente);
915
907
  shelf = { shelf_life: sv.shelf, inferred: sv.inferred, ...(sv.reason ? { reason: sv.reason } : {}) };
916
908
  }
@@ -939,6 +931,10 @@ class Cortex {
939
931
  fact.keys = keys;
940
932
  if (vidaPedida)
941
933
  fact.shelf_life = vidaPedida;
934
+ if (reference)
935
+ fact.reference = reference;
936
+ if (temporal)
937
+ fact.temporal = temporal;
942
938
  const sv = (0, shelf_js_1.shelfOfFact)(fact);
943
939
  shelf = { shelf_life: sv.shelf, inferred: sv.inferred, ...(sv.reason ? { reason: sv.reason } : {}) };
944
940
  n.facts.push(fact);
@@ -948,6 +944,8 @@ class Cortex {
948
944
  this.tally.topics.add(n.id);
949
945
  emitir = { kind: 'fact', text: content, fid: id,
950
946
  conf: fact.confidence, at: fact.added, src: fact.source, keys: fact.keys,
947
+ ...(fact.reference ? { reference: fact.reference } : {}),
948
+ ...(fact.temporal ? { temporal: fact.temporal } : {}),
951
949
  ...(fact.shelf_life ? { shelf: fact.shelf_life } : {}) };
952
950
  }
953
951
  break;
@@ -975,8 +973,8 @@ class Cortex {
975
973
  n.patterns.push(content);
976
974
  fechar(n, content);
977
975
  origen(n, content);
978
- emitir = { kind: 'pattern', text: content, at: (0, fs_js_1.now)() };
979
976
  }
977
+ emitir = { kind: 'pattern', text: content, at: n.entry_dates?.[(0, ops_js_1.entryId)(content)] || (0, fs_js_1.now)() };
980
978
  break;
981
979
  }
982
980
  case 'preference': {
@@ -996,8 +994,8 @@ class Cortex {
996
994
  fechar(n, content);
997
995
  origen(n, content);
998
996
  this.tally.topics.add(n.id);
999
- emitir = { kind: 'error', text: content, at: (0, fs_js_1.now)() };
1000
997
  }
998
+ emitir = { kind: 'error', text: content, at: n.entry_dates?.[(0, ops_js_1.entryId)(content)] || (0, fs_js_1.now)() };
1001
999
  break;
1002
1000
  }
1003
1001
  case 'debt': {
@@ -1010,8 +1008,8 @@ class Cortex {
1010
1008
  fechar(n, content);
1011
1009
  origen(n, content);
1012
1010
  this.tally.topics.add(n.id);
1013
- emitir = { kind: 'debt', text: content, at: (0, fs_js_1.now)() };
1014
1011
  }
1012
+ emitir = { kind: 'debt', text: content, at: n.entry_dates?.[(0, ops_js_1.entryId)(content)] || (0, fs_js_1.now)() };
1015
1013
  break;
1016
1014
  }
1017
1015
  }
@@ -1036,17 +1034,12 @@ class Cortex {
1036
1034
  // key and the least likely to be worth sharing.
1037
1035
  if (emitir)
1038
1036
  await this.emit(final.id, emitir);
1039
- // A reconfirming re-learn reaches the team as a check: the latest wins.
1040
- const v = verificar;
1041
- if (v)
1042
- await this.emit(final.id, { kind: 'verify', eid: v.eid, ekind: 'fact', at: v.at });
1043
1037
  return {
1044
1038
  neuron: final, action, superseded, supersedes_unmatched: supersedesUnmatched,
1045
1039
  near_duplicates: nearDuplicates, redacted: limpio.found,
1046
1040
  duplicate, updated_in_place: updatedInPlace, skipped_retired: null,
1047
1041
  ...(confirmations !== undefined ? { confirmations } : {}),
1048
1042
  ...(shelf ? { shelf } : {}),
1049
- ...(reconfirmed ? { reconfirmed } : {}),
1050
1043
  ...(shelfLengthened ? { shelf_lengthened: true } : {}),
1051
1044
  };
1052
1045
  }
@@ -1114,6 +1107,8 @@ class Cortex {
1114
1107
  * clock restarts too: the fact is shared, and so is the world it describes.
1115
1108
  */
1116
1109
  async verify(neuronRef, targets) {
1110
+ // Validar antes de cualquier escritura, incluida la creación de neuronas.
1111
+ const evidence = (0, evidence_js_1.makeVerification)(targets.evidence, (0, fs_js_1.now)());
1117
1112
  const facts = (targets.facts || []).filter(t => typeof t === 'string');
1118
1113
  const entries = (targets.entries || []).filter(t => typeof t === 'string');
1119
1114
  const neuron = (await this.peek(neuronRef)) || (await this.findByName(neuronRef));
@@ -1143,10 +1138,13 @@ class Cortex {
1143
1138
  }
1144
1139
  for (const f of vivos) {
1145
1140
  f.id = f.id || (0, hash_js_1.factId)(f.text);
1146
- f.verified = cuando;
1141
+ // Un check explícito posterior al cambio debe sobrevivir incluso
1142
+ // cuando ambas llamadas caben en el mismo milisegundo.
1143
+ f.verified = new Date(Math.max(Date.parse(cuando), (Date.parse(f.verification_invalidated_at || '') || 0) + 1)).toISOString();
1144
+ f.verification = { ...evidence, at: f.verified };
1147
1145
  if (!verified.includes(f.id))
1148
1146
  verified.push(f.id);
1149
- emitir.push({ eid: f.id, ekind: 'fact', at: cuando });
1147
+ emitir.push({ eid: f.id, ekind: 'fact', at: f.verified, verification: f.verification });
1150
1148
  }
1151
1149
  }
1152
1150
  const candidatas = [
@@ -1168,9 +1166,10 @@ class Cortex {
1168
1166
  for (const c of vivas) {
1169
1167
  const k = (0, ops_js_1.entryId)(c.text);
1170
1168
  (n.entry_verified ||= {})[k] = cuando;
1169
+ (n.entry_verification ||= {})[k] = { ...evidence, at: cuando };
1171
1170
  if (!verified.includes(k))
1172
1171
  verified.push(k);
1173
- emitir.push({ eid: k, ekind: 'entry', at: cuando });
1172
+ emitir.push({ eid: k, ekind: 'entry', at: cuando, verification: n.entry_verification[k] });
1174
1173
  }
1175
1174
  }
1176
1175
  if (verified.length === 0)
@@ -1539,6 +1538,17 @@ class Cortex {
1539
1538
  const backup = await this.quarantine(found);
1540
1539
  let removed = 0;
1541
1540
  const wanted = targets.map(t => t.trim().toLowerCase());
1541
+ // An entry goes when a target names its text or its id: the entryId the
1542
+ // neuron index and recall show, or a decision's own id. Until 2.9.1 only
1543
+ // facts matched by id, so taking a credential out of a preference meant
1544
+ // pasting the credential back into the conversation to name it.
1545
+ const matches = (text, ownId) => {
1546
+ const t = (text || '').trim().toLowerCase();
1547
+ if (!t)
1548
+ return false;
1549
+ return wanted.includes(t) || wanted.includes((0, ops_js_1.entryId)(text).toLowerCase())
1550
+ || (!!ownId && wanted.includes(ownId.toLowerCase()));
1551
+ };
1542
1552
  const after = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(found.id), current => {
1543
1553
  if (!current)
1544
1554
  return null;
@@ -1555,16 +1565,16 @@ class Cortex {
1555
1565
  });
1556
1566
  // Decisions, patterns and preferences too. A credential is no less
1557
1567
  // exposed for sitting in one of those, and nothing else could remove it.
1558
- current.decisions = current.decisions.filter(d => !wanted.includes((d.text || '').trim().toLowerCase()));
1559
- current.patterns = current.patterns.filter(p => !wanted.includes((p || '').trim().toLowerCase()));
1560
- current.preferences = current.preferences.filter(p => !wanted.includes((p || '').trim().toLowerCase()));
1568
+ current.decisions = current.decisions.filter(d => !matches(d.text, d.id));
1569
+ current.patterns = current.patterns.filter(p => !matches(p));
1570
+ current.preferences = current.preferences.filter(p => !matches(p));
1561
1571
  const erroresAntes = (current.errors || []).length;
1562
1572
  if (current.errors) {
1563
- current.errors = current.errors.filter(e => !wanted.includes((e || '').trim().toLowerCase()));
1573
+ current.errors = current.errors.filter(e => !matches(e));
1564
1574
  }
1565
1575
  const deudasAntes = (current.debts || []).length;
1566
1576
  if (current.debts) {
1567
- current.debts = current.debts.filter(d => !wanted.includes((d || '').trim().toLowerCase()));
1577
+ current.debts = current.debts.filter(d => !matches(d));
1568
1578
  }
1569
1579
  let mapaBorrado = 0;
1570
1580
  if (current.map && wanted.includes(current.map.text.trim().toLowerCase())) {
@@ -1608,6 +1618,15 @@ class Cortex {
1608
1618
  if (Object.keys(current.entry_verified).length === 0)
1609
1619
  delete current.entry_verified;
1610
1620
  }
1621
+ if (current.entry_verification) {
1622
+ const vivos = liveEntryKeys(current);
1623
+ for (const k of Object.keys(current.entry_verification)) {
1624
+ if (!vivos.has(k))
1625
+ delete current.entry_verification[k];
1626
+ }
1627
+ if (Object.keys(current.entry_verification).length === 0)
1628
+ delete current.entry_verification;
1629
+ }
1611
1630
  // And for provenance: a forgotten teammate line that comes back through
1612
1631
  // a sync replay must be labelled by that replay, not by a stale key.
1613
1632
  if (current.entry_source) {
@@ -1637,22 +1656,22 @@ class Cortex {
1637
1656
  }
1638
1657
  }
1639
1658
  for (const d of found.decisions || []) {
1640
- if (wanted.includes((d.text || '').trim().toLowerCase())) {
1659
+ if (matches(d.text, d.id)) {
1641
1660
  await this.emit(found.id, { kind: 'decision_purge', key: (0, ops_js_1.entryId)(d.text), at: cuando });
1642
1661
  }
1643
1662
  }
1644
1663
  for (const p of found.patterns || []) {
1645
- if (wanted.includes((p || '').trim().toLowerCase())) {
1664
+ if (matches(p)) {
1646
1665
  await this.emit(found.id, { kind: 'pattern_purge', key: (0, ops_js_1.entryId)(p), at: cuando });
1647
1666
  }
1648
1667
  }
1649
1668
  for (const e of found.errors || []) {
1650
- if (wanted.includes((e || '').trim().toLowerCase())) {
1669
+ if (matches(e)) {
1651
1670
  await this.emit(found.id, { kind: 'error_purge', key: (0, ops_js_1.entryId)(e), at: cuando });
1652
1671
  }
1653
1672
  }
1654
1673
  for (const d of found.debts || []) {
1655
- if (wanted.includes((d || '').trim().toLowerCase())) {
1674
+ if (matches(d)) {
1656
1675
  await this.emit(found.id, { kind: 'debt_purge', key: (0, ops_js_1.entryId)(d), at: cuando });
1657
1676
  }
1658
1677
  }
@@ -1792,18 +1811,24 @@ class Cortex {
1792
1811
  */
1793
1812
  async emitAdded(antes, final) {
1794
1813
  const cuando = (0, fs_js_1.now)();
1795
- const teniaFact = new Set(antes.facts.map(f => f.id || (0, hash_js_1.factId)(f.text)));
1814
+ const teniaFact = new Map(antes.facts.map(f => [f.id || (0, hash_js_1.factId)(f.text), f]));
1796
1815
  for (const f of final.facts) {
1797
1816
  const fid = f.id || (0, hash_js_1.factId)(f.text);
1798
- if (teniaFact.has(fid))
1799
- continue;
1800
- await this.emit(final.id, { kind: 'fact', text: f.text, fid, conf: f.confidence ?? 1, at: f.added, src: f.source, keys: f.keys,
1801
- ...(f.shelf_life ? { shelf: f.shelf_life } : {}) });
1802
- if (f.verified)
1803
- await this.emit(final.id, { kind: 'verify', eid: fid, ekind: 'fact', at: f.verified });
1817
+ const previo = teniaFact.get(fid) || antes.facts.find(p => (0, ops_js_1.normalizeText)(p.text) === (0, ops_js_1.normalizeText)(f.text));
1818
+ const metadata = (v) => JSON.stringify([v.keys, v.shelf_life, v.reference, v.temporal, v.verification_invalidated_at]);
1819
+ if (!previo || metadata(previo) !== metadata(f)) {
1820
+ await this.emit(final.id, { kind: 'fact', text: f.text, fid, conf: f.confidence ?? 1, at: f.added, src: f.source, keys: f.keys,
1821
+ ...(f.reference ? { reference: f.reference } : {}), ...(f.temporal ? { temporal: f.temporal } : {}),
1822
+ ...(f.verification_invalidated_at ? { verification_invalidated_at: f.verification_invalidated_at } : {}),
1823
+ ...(f.shelf_life ? { shelf: f.shelf_life } : {}) });
1824
+ }
1825
+ if (f.verified && (previo?.verified !== f.verified || JSON.stringify(previo?.verification) !== JSON.stringify(f.verification))) {
1826
+ await this.emit(final.id, { kind: 'verify', eid: fid, ekind: 'fact', at: f.verified,
1827
+ ...(f.verification ? { verification: f.verification } : {}) });
1828
+ }
1804
1829
  // A moved fact that was already retired must arrive retired, or the
1805
1830
  // teammates would see as current what the source had superseded.
1806
- if (f.status === 'superseded' || f.status === 'retracted') {
1831
+ if (previo?.status !== f.status && (f.status === 'superseded' || f.status === 'retracted')) {
1807
1832
  await this.emit(final.id, { kind: 'status', fid, to: f.status, at: f.revised || cuando, why: f.revision_note });
1808
1833
  }
1809
1834
  }
@@ -1831,9 +1856,10 @@ class Cortex {
1831
1856
  }
1832
1857
  // Checks of decisions and patterns that arrived with them, or are newer than the target's.
1833
1858
  for (const [k, at] of Object.entries(final.entry_verified || {})) {
1834
- if (antes.entry_verified?.[k] === at)
1859
+ if (antes.entry_verified?.[k] === at && JSON.stringify(antes.entry_verification?.[k]) === JSON.stringify(final.entry_verification?.[k]))
1835
1860
  continue;
1836
- await this.emit(final.id, { kind: 'verify', eid: k, ekind: 'entry', at });
1861
+ await this.emit(final.id, { kind: 'verify', eid: k, ekind: 'entry', at,
1862
+ ...(final.entry_verification?.[k] ? { verification: final.entry_verification[k] } : {}) });
1837
1863
  }
1838
1864
  // Preferences are never emitted, here as in learn.
1839
1865
  }
@@ -1880,6 +1906,7 @@ class Cortex {
1880
1906
  entry_dates: { ...(from.entry_dates || {}) },
1881
1907
  entry_status: { ...(from.entry_status || {}) },
1882
1908
  entry_verified: { ...(from.entry_verified || {}) },
1909
+ entry_verification: structuredClone(from.entry_verification || {}),
1883
1910
  };
1884
1911
  delete parte.map;
1885
1912
  const textos = [