From f87b4e4d7abb3a1293c8b2c2b22c84147b89cb80 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Sat, 11 Apr 2026 23:24:59 +0000 Subject: [PATCH] =?UTF-8?q?feat(mnemosyne):=20add=20tag=20management=20?= =?UTF-8?q?=E2=80=94=20add=5Ftags,=20remove=5Ftags,=20retag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1236. Three new methods on MnemosyneArchive: - add_tags: add new tags (dedup, case-insensitive) - remove_tags: remove specific tags - retag: replace all tags at once All methods return the entry's final topic list. --- nexus/mnemosyne/archive.py | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/nexus/mnemosyne/archive.py b/nexus/mnemosyne/archive.py index a84a676..4d719c0 100644 --- a/nexus/mnemosyne/archive.py +++ b/nexus/mnemosyne/archive.py @@ -212,6 +212,79 @@ class MnemosyneArchive: def count(self) -> int: return len(self._entries) + + + def add_tags(self, entry_id: str, tags: list[str]) -> list[str]: + """Add tags to an existing entry. Returns the entry's full topic list after addition. + + Args: + entry_id: The entry ID to modify. + tags: List of tags to add (duplicates are ignored). + + Raises: + KeyError: If entry_id does not exist. + """ + entry = self._entries.get(entry_id) + if entry is None: + raise KeyError(f"Entry not found: {entry_id}") + + existing_lower = {t.lower() for t in entry.topics} + for tag in tags: + tag = tag.strip() + if tag and tag.lower() not in existing_lower: + entry.topics.append(tag) + existing_lower.add(tag.lower()) + + self._save() + return entry.topics + + def remove_tags(self, entry_id: str, tags: list[str]) -> list[str]: + """Remove tags from an existing entry. Returns the entry's topic list after removal. + + Args: + entry_id: The entry ID to modify. + tags: List of tags to remove (case-insensitive). + + Raises: + KeyError: If entry_id does not exist. + """ + entry = self._entries.get(entry_id) + if entry is None: + raise KeyError(f"Entry not found: {entry_id}") + + remove_lower = {t.strip().lower() for t in tags} + entry.topics = [t for t in entry.topics if t.lower() not in remove_lower] + + self._save() + return entry.topics + + def retag(self, entry_id: str, tags: list[str]) -> list[str]: + """Replace all tags on an existing entry. Returns the new topic list. + + Args: + entry_id: The entry ID to modify. + tags: The new complete tag list (replaces all existing tags). + + Raises: + KeyError: If entry_id does not exist. + """ + entry = self._entries.get(entry_id) + if entry is None: + raise KeyError(f"Entry not found: {entry_id}") + + # Deduplicate, preserve order + seen: set[str] = set() + deduped: list[str] = [] + for tag in tags: + tag = tag.strip() + if tag and tag.lower() not in seen: + deduped.append(tag) + seen.add(tag.lower()) + + entry.topics = deduped + self._save() + return entry.topics + def graph_data( self, topic_filter: Optional[str] = None,