diff --git a/nexus/mnemosyne/archive.py b/nexus/mnemosyne/archive.py index 76e2f7e4..d71ac54f 100644 --- a/nexus/mnemosyne/archive.py +++ b/nexus/mnemosyne/archive.py @@ -1308,3 +1308,88 @@ class MnemosyneArchive: self._save() return total_links + # ─── Discovery ────────────────────────────────────────────── + + def discover( + self, + count: int = 5, + prefer_fading: bool = True, + topic: Optional[str] = None, + ) -> list[dict]: + """Serendipitous entry discovery — surface forgotten knowledge. + + Selects entries probabilistically, weighting toward fading (low vitality) + entries when prefer_fading=True, or toward vibrant entries when False. + Optionally filter by topic. + + Touches selected entries to boost their vitality, preventing the same + entries from being repeatedly surfaced. + + Args: + count: Number of entries to discover. + prefer_fading: If True, weight toward neglected entries. If False, + weight toward vibrant entries. + topic: Optional topic filter — only discover entries with this tag. + + Returns: + List of dicts with keys: entry_id, title, content_preview, topics, + vitality, age_days, last_accessed + """ + import random + + candidates = list(self._entries.values()) + + # Filter by topic if specified + if topic: + topic_lower = topic.lower() + candidates = [ + e for e in candidates + if topic_lower in [t.lower() for t in e.topics] + ] + + if not candidates: + return [] + + # Compute vitality for each candidate + scored = [] + for entry in candidates: + v = self._compute_vitality(entry) + scored.append((entry, v)) + + # Build selection weights + if prefer_fading: + # Lower vitality = higher weight. Invert and normalize. + weights = [max(0.01, 1.0 - v) for _, v in scored] + else: + # Higher vitality = higher weight + weights = [max(0.01, v) for _, v in scored] + + # Sample without replacement + k = min(count, len(scored)) + selected_indices = random.choices(range(len(scored)), weights=weights, k=k) + # Deduplicate while preserving order + seen = set() + unique_indices = [] + for idx in selected_indices: + if idx not in seen: + seen.add(idx) + unique_indices.append(idx) + + results = [] + for idx in unique_indices: + entry, v = scored[idx] + # Touch to boost vitality + self.touch(entry.id) + created = self._parse_dt(entry.created_at) + age_days = (datetime.now(timezone.utc) - created).days + results.append({ + "entry_id": entry.id, + "title": entry.title, + "content_preview": entry.content[:200] + "..." if len(entry.content) > 200 else entry.content, + "topics": entry.topics, + "vitality": round(v, 4), + "age_days": age_days, + "last_accessed": entry.last_accessed, + }) + + return results