feat(mnemosyne): add tag management — add_tags, remove_tags, retag

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.
This commit is contained in:
2026-04-11 23:24:59 +00:00
parent 09ccf52645
commit f87b4e4d7a

View File

@@ -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,