diff --git a/nexus/mnemosyne/archive.py b/nexus/mnemosyne/archive.py index 16b145c2..e8973032 100644 --- a/nexus/mnemosyne/archive.py +++ b/nexus/mnemosyne/archive.py @@ -1059,6 +1059,52 @@ class MnemosyneArchive: return merges + + def shortest_path(self, start_id: str, end_id: str) -> list[str] | None: + """Find shortest path between two entries through the connection graph. + + Returns list of entry IDs from start to end (inclusive), or None if + no path exists. Uses BFS for unweighted shortest path. + """ + if start_id == end_id: + return [start_id] if start_id in self._entries else None + if start_id not in self._entries or end_id not in self._entries: + return None + + adj = self._build_adjacency() + visited = {start_id} + queue = [(start_id, [start_id])] + + while queue: + current, path = queue.pop(0) + for neighbor in adj.get(current, []): + if neighbor == end_id: + return path + [neighbor] + if neighbor not in visited: + visited.add(neighbor) + queue.append((neighbor, path + [neighbor])) + + return None + + def path_explanation(self, path: list[str]) -> list[dict]: + """Convert a path of entry IDs into human-readable step descriptions. + + Returns list of dicts with 'id', 'title', and 'topics' for each step. + """ + steps = [] + for entry_id in path: + entry = self._entries.get(entry_id) + if entry: + steps.append({ + "id": entry.id, + "title": entry.title, + "topics": entry.topics, + "content_preview": entry.content[:120] + "..." if len(entry.content) > 120 else entry.content, + }) + else: + steps.append({"id": entry_id, "title": "[unknown]", "topics": []}) + return steps + def rebuild_links(self, threshold: Optional[float] = None) -> int: """Recompute all links from scratch.