Rename MemoriesManager -> MemoryManager

This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-05-16 15:16:14 +02:00
1 parent 14e939ee1c
commit eb43c5be1a
11 files changed
+101 -102

No files matched your search

+3 -3
View File
@@ -41,7 +41,7 @@ from serena.config.serena_config import (
)
from serena.dashboard import SerenaDashboardAPI, SerenaDashboardTrayManager, SerenaDashboardViewer, open_url_in_browser
from serena.ls_manager import LanguageServerManager
from serena.memories.memory_manager import MemoriesManager
from serena.memories.memory_manager import MemoryManager
from serena.project import Project
from serena.prompt_factory import SerenaPromptFactory
from serena.task_executor import TaskExecutor
@@ -919,7 +919,7 @@ class SerenaAgent:
"""
available_tools = self._active_tools
available_markers = available_tools.tool_marker_names
global_memories = MemoriesManager(
global_memories = MemoryManager(
serena_data_folder=None, read_only_memory_patterns=self.serena_config.read_only_memory_patterns
).list_global_memories()
global_memories_str = dict_string(global_memories.to_dict()) if len(global_memories) > 0 else ""
@@ -977,7 +977,7 @@ class SerenaAgent:
# add list of memories (if memories are enabled)
include_memories = self._active_tools.contains_tool_class(ReadMemoryTool)
if include_memories:
project_memories = proj.memories_manager.list_project_memories()
project_memories = proj.memory_manager.list_project_memories()
if project_memories:
msg += (
f"\n{json.dumps(project_memories.to_dict())}\n"
+3 -3
View File
@@ -44,7 +44,7 @@ from solidlsp.ls_types import SymbolKind
from solidlsp.util.subprocess_util import subprocess_kwargs
if TYPE_CHECKING:
from serena.project import MemoriesManager
from serena.memories.memory_manager import MemoryManager
log = logging.getLogger(__name__)
@@ -1127,7 +1127,7 @@ class MemoryCommands(AutoRegisteringGroup):
)
@staticmethod
def _load_memories_manager(project: str) -> "MemoriesManager":
def _load_memories_manager(project: str) -> "MemoryManager":
"""
Load the project at ``project`` and return its memories manager.
@@ -1145,7 +1145,7 @@ class MemoryCommands(AutoRegisteringGroup):
raise click.UsageError(
f"{e}\nNo Serena project found at {project_path}. Create one first with:\n serena project create {project_path}"
) from e
return proj.memories_manager
return proj.memory_manager
@staticmethod
@click.command(
+5 -7
View File
@@ -566,7 +566,7 @@ class SerenaDashboardAPI:
# Get available memories if ReadMemoryTool is active
available_memories = None
if self._agent.tool_is_active("read_memory") and project is not None:
available_memories = project.memories_manager.list_memories().get_full_list()
available_memories = project.memory_manager.list_memories().get_full_list()
# Get list of languages for the active project
languages = []
@@ -620,7 +620,7 @@ class SerenaDashboardAPI:
if project is None:
raise ValueError("No active project")
content = project.memories_manager.load_memory(request_get_memory.memory_name)
content = project.memory_manager.load_memory(request_get_memory.memory_name)
return ResponseGetMemory(content=content, memory_name=request_get_memory.memory_name)
return self._agent.execute_task(run, logged=False)
@@ -630,7 +630,7 @@ class SerenaDashboardAPI:
project = self._agent.get_active_project()
if project is None:
raise ValueError("No active project")
project.memories_manager.save_memory(request_save_memory.memory_name, request_save_memory.content, is_tool_context=False)
project.memory_manager.save_memory(request_save_memory.memory_name, request_save_memory.content, is_tool_context=False)
self._agent.execute_task(run, logged=True, name="SaveMemory")
@@ -639,7 +639,7 @@ class SerenaDashboardAPI:
project = self._agent.get_active_project()
if project is None:
raise ValueError("No active project")
project.memories_manager.delete_memory(request_delete_memory.memory_name, is_tool_context=False)
project.memory_manager.delete_memory(request_delete_memory.memory_name, is_tool_context=False)
self._agent.execute_task(run, logged=True, name="DeleteMemory")
@@ -649,9 +649,7 @@ class SerenaDashboardAPI:
if project is None:
raise ValueError("No active project")
return project.memories_manager.move_memory(
request_rename_memory.old_name, request_rename_memory.new_name, is_tool_context=False
)
return project.memory_manager.move_memory(request_rename_memory.old_name, request_rename_memory.new_name, is_tool_context=False)
return self._agent.execute_task(run, logged=True, name="RenameMemory")
+12 -12
View File
@@ -21,7 +21,7 @@ from .memory_reference_analysis import (
log = logging.getLogger(__name__)
class MemoriesManager:
class MemoryManager:
GLOBAL_TOPIC = "global"
_global_memory_dir = SerenaPaths().global_memories_path
_MEMORY_REF_PREFIX = "mem:"
@@ -70,7 +70,7 @@ class MemoriesManager:
return name == self.GLOBAL_TOPIC or name.startswith(self.GLOBAL_TOPIC + "/")
@classmethod
def _prepare_name(cls, name: str) -> str:
def _sanitize_name(cls, name: str) -> str:
"""Corrects the name for common mistakes made by LLMs (``mem:`` prefix, ``.md`` suffix, OS-specific separators)."""
name = name.removeprefix(cls._MEMORY_REF_PREFIX)
if name.endswith(".md"):
@@ -79,7 +79,7 @@ class MemoriesManager:
@classmethod
def _add_reference_prefix(cls, name: str) -> str:
name = cls._prepare_name(name)
name = cls._sanitize_name(name)
return cls._MEMORY_REF_PREFIX + name
MEMORY_MAINTENANCE_NAME: str = "memory_maintenance"
@@ -147,7 +147,7 @@ class MemoriesManager:
return re.subn(pattern, lambda _m: ref_new, content)
def get_memory_file_path(self, name: str) -> Path:
name = self._prepare_name(name)
name = self._sanitize_name(name)
parts = name.split("/")
if ".." in parts:
raise ValueError(f"Memory name cannot contain '..' segments for security reasons. Got: {name}")
@@ -186,7 +186,7 @@ class MemoriesManager:
raise PermissionError(f"Attempted to write to read_only memory: '{name}')")
def load_memory(self, name: str) -> str:
name = self._prepare_name(name)
name = self._sanitize_name(name)
self._check_not_ignored(name)
memory_file_path = self.get_memory_file_path(name)
if not memory_file_path.exists():
@@ -195,7 +195,7 @@ class MemoriesManager:
return f.read()
def save_memory(self, name: str, content: str, is_tool_context: bool) -> str:
name = self._prepare_name(name)
name = self._sanitize_name(name)
self._check_not_ignored(name)
self._check_write_access(name, is_tool_context)
memory_file_path = self.get_memory_file_path(name)
@@ -217,7 +217,7 @@ class MemoriesManager:
else:
self.memories.append(memory_name)
def extend(self, other: "MemoriesManager.MemoriesList") -> None:
def extend(self, other: "MemoryManager.MemoriesList") -> None:
self.memories.extend(other.memories)
self.read_only_memories.extend(other.read_only_memories)
@@ -262,7 +262,7 @@ class MemoriesManager:
Lists all memories, optionally filtered by topic.
If the topic is omitted, both global and project-specific memories are returned.
"""
memories: MemoriesManager.MemoriesList
memories: MemoryManager.MemoriesList
if topic:
if self._is_global(topic):
@@ -278,7 +278,7 @@ class MemoriesManager:
return memories
def delete_memory(self, name: str, is_tool_context: bool) -> str:
name = self._prepare_name(name)
name = self._sanitize_name(name)
self._check_not_ignored(name)
self._check_write_access(name, is_tool_context)
memory_file_path = self.get_memory_file_path(name)
@@ -292,8 +292,8 @@ class MemoriesManager:
Rename or move a memory file.
Moving between global and project scope (e.g. "global/foo" -> "bar") is supported.
"""
old_name = self._prepare_name(old_name)
new_name = self._prepare_name(new_name)
old_name = self._sanitize_name(old_name)
new_name = self._sanitize_name(new_name)
self._check_not_ignored(old_name)
self._check_not_ignored(new_name)
self._check_write_access(new_name, is_tool_context)
@@ -357,7 +357,7 @@ class MemoriesManager:
:param is_tool_context: whether the call originates from a tool invocation (affects write-access checks)
:param regex_multiline: whether to apply multi-line regex matching, enabling the flags re.DOTALL and re.MULTILINE
"""
name = self._prepare_name(name)
name = self._sanitize_name(name)
self._check_not_ignored(name)
self._check_write_access(name, is_tool_context)
memory_file_path = self.get_memory_file_path(name)
@@ -22,7 +22,7 @@ from typing import TYPE_CHECKING
from sensai.util.string import TextBuilder
if TYPE_CHECKING:
from serena.project import MemoriesManager
from .memory_manager import MemoryManager
# ---------------------------------------------------------------------------
@@ -516,7 +516,7 @@ class MemoryReferenceAnalyzer:
independent of project-management concerns.
"""
def __init__(self, manager: MemoriesManager):
def __init__(self, manager: MemoryManager):
self._manager = manager
def validate_referential_integrity(
+2 -2
View File
@@ -13,7 +13,7 @@ from serena.config.serena_config import (
SerenaConfig,
)
from serena.ls_manager import LanguageServerFactory, LanguageServerManager
from serena.memories.memory_manager import MemoriesManager
from serena.memories.memory_manager import MemoryManager
from serena.util.file_system import GitignoreParser, match_path
from serena.util.text_utils import MatchedConsecutiveLines, search_files
from solidlsp import SolidLanguageServer
@@ -44,7 +44,7 @@ class Project(ToStringMixin):
read_only_memory_patterns = serena_config.read_only_memory_patterns + project_config.read_only_memory_patterns
ignored_memory_patterns = serena_config.ignored_memory_patterns + project_config.ignored_memory_patterns
self.memories_manager = MemoriesManager(
self.memory_manager = MemoryManager(
self._serena_data_folder,
read_only_memory_patterns=read_only_memory_patterns,
ignored_memory_patterns=ignored_memory_patterns,
+6 -6
View File
@@ -32,7 +32,7 @@ class WriteMemoryTool(Tool, ToolMarkerCanEdit):
f"Content for {memory_name} is too long. Max length is {max_chars} characters. " + "Please make the content shorter."
)
return self.memories_manager.save_memory(memory_name, content, is_tool_context=True)
return self.memory_manager.save_memory(memory_name, content, is_tool_context=True)
class ReadMemoryTool(Tool):
@@ -44,7 +44,7 @@ class ReadMemoryTool(Tool):
"""
Use to read a memory that is likely to be relevant to the current task, inferring relevance e.g. from the name.
"""
return self.memories_manager.load_memory(memory_name)
return self.memory_manager.load_memory(memory_name)
class ListMemoriesTool(Tool):
@@ -56,7 +56,7 @@ class ListMemoriesTool(Tool):
"""
Lists available memories, optionally filtered by topic.
"""
return self._to_json(self.memories_manager.list_memories(topic).to_dict())
return self._to_json(self.memory_manager.list_memories(topic).to_dict())
class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
@@ -68,7 +68,7 @@ class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
"""
Delete a memory, only call if instructed explicitly or permission was granted by the user.
"""
return self.memories_manager.delete_memory(memory_name, is_tool_context=True)
return self.memory_manager.delete_memory(memory_name, is_tool_context=True)
class RenameMemoryTool(Tool, ToolMarkerCanEdit):
@@ -83,7 +83,7 @@ class RenameMemoryTool(Tool, ToolMarkerCanEdit):
References to other memories that are marked with the `mem:` prefix will be updated accordingly.
References in read-only memories are not affected.
"""
renaming_message, n_references_updated = self.memories_manager.rename_memory_and_propagate_references(
renaming_message, n_references_updated = self.memory_manager.rename_memory_and_propagate_references(
old_name, new_name, is_tool_context=True
)
if n_references_updated > 0:
@@ -117,6 +117,6 @@ class EditMemoryTool(Tool, ToolMarkerCanEdit):
:param allow_multiple_occurrences: whether to allow matching and replacing multiple occurrences.
If false and multiple occurrences are found, an error will be returned.
"""
return self.memories_manager.edit_memory(
return self.memory_manager.edit_memory(
memory_name, needle, repl, mode, allow_multiple_occurrences, is_tool_context=True, regex_multiline=True
)
+4 -3
View File
@@ -14,7 +14,8 @@ from sensai.util import logging
from sensai.util.string import dict_string
from serena.config.serena_config import LanguageBackend
from serena.project import MemoriesManager, Project
from serena.memories.memory_manager import MemoryManager
from serena.project import Project
from serena.prompt_factory import PromptFactory
from serena.util.class_decorators import singleton
from serena.util.inspection import iter_subclasses
@@ -46,8 +47,8 @@ class Component(ABC):
return self.agent.prompt_factory
@property
def memories_manager(self) -> "MemoriesManager":
return self.project.memories_manager
def memory_manager(self) -> "MemoryManager":
return self.project.memory_manager
def create_language_server_symbol_retriever(self) -> "LanguageServerSymbolRetriever":
from serena.symbol import LanguageServerSymbolRetriever
+1 -1
View File
@@ -25,7 +25,7 @@ class OnboardingTool(Tool):
system = platform.system()
# seed the project-local memory-maintenance memory (or detect a global override) so
# the prompt can point the agent at the conventions before it writes anything
memory_maintenance_name = self.memories_manager.ensure_memory_maintenance_memory()
memory_maintenance_name = self.memory_manager.ensure_memory_maintenance_memory()
return self.prompt_factory.create_onboarding_prompt(system=system, memory_maintenance_name=memory_maintenance_name)
+4 -4
View File
@@ -16,7 +16,7 @@ from serena.config.serena_config import (
SerenaConfigError,
)
from serena.constants import PROJECT_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME
from serena.project import MemoriesManager, Project
from serena.project import MemoryManager, Project
from solidlsp.ls_config import Language
from test.conftest import create_default_serena_config
@@ -567,17 +567,17 @@ class TestMemoriesManagerCustomPath:
def test_memories_subdir_is_created(self):
assert not self.data_folder.exists()
MemoriesManager(str(self.data_folder))
MemoryManager(str(self.data_folder))
assert (self.data_folder / "memories").exists()
def test_save_and_load_memory(self):
manager = MemoriesManager(str(self.data_folder))
manager = MemoryManager(str(self.data_folder))
manager.save_memory("test_topic", "test content", is_tool_context=False)
content = manager.load_memory("test_topic")
assert content == "test content"
def test_list_memories(self):
manager = MemoriesManager(str(self.data_folder))
manager = MemoryManager(str(self.data_folder))
manager.save_memory("topic_a", "content a", is_tool_context=False)
manager.save_memory("topic_b", "content b", is_tool_context=False)
memories = manager.list_project_memories()
+59 -59
View File
@@ -15,13 +15,13 @@ from serena.memories.memory_reference_analysis import (
compute_name_similarity,
find_stale_reference_candidates,
)
from serena.project import MemoriesManager
from serena.project import MemoryManager
@pytest.fixture
def manager() -> MemoriesManager:
def manager() -> MemoryManager:
# serena_data_folder=None: the tested helpers do not access the filesystem
return MemoriesManager(serena_data_folder=None)
return MemoryManager(serena_data_folder=None)
class TestPrepareName:
@@ -40,9 +40,9 @@ class TestPrepareName:
":colon_prefixed_unlikely",
],
)
def test_does_not_strip_partial_prefix_chars_from_real_names(self, manager: MemoriesManager, name: str) -> None:
def test_does_not_strip_partial_prefix_chars_from_real_names(self, manager: MemoryManager, name: str) -> None:
"""A regression guard: previously used ``str.lstrip("mem:")`` which strips any leading m/e/:."""
assert manager._prepare_name(name) == name
assert manager._sanitize_name(name) == name
@pytest.mark.parametrize(
("input_name", "expected"),
@@ -53,8 +53,8 @@ class TestPrepareName:
("mem:", ""),
],
)
def test_strips_mem_prefix_only_when_actually_a_prefix(self, manager: MemoriesManager, input_name: str, expected: str) -> None:
assert manager._prepare_name(input_name) == expected
def test_strips_mem_prefix_only_when_actually_a_prefix(self, manager: MemoryManager, input_name: str, expected: str) -> None:
assert manager._sanitize_name(input_name) == expected
@pytest.mark.parametrize(
("input_name", "expected"),
@@ -66,14 +66,14 @@ class TestPrepareName:
(".md", ""),
],
)
def test_strips_md_only_as_suffix(self, manager: MemoriesManager, input_name: str, expected: str) -> None:
assert manager._prepare_name(input_name) == expected
def test_strips_md_only_as_suffix(self, manager: MemoryManager, input_name: str, expected: str) -> None:
assert manager._sanitize_name(input_name) == expected
def test_normalizes_os_separator(self, manager: MemoriesManager) -> None:
assert manager._prepare_name(f"topic{os.sep}sub{os.sep}name") == "topic/sub/name"
def test_normalizes_os_separator(self, manager: MemoryManager) -> None:
assert manager._sanitize_name(f"topic{os.sep}sub{os.sep}name") == "topic/sub/name"
def test_combined_normalizations(self, manager: MemoriesManager) -> None:
assert manager._prepare_name(f"mem:topic{os.sep}foo.md") == "topic/foo"
def test_combined_normalizations(self, manager: MemoryManager) -> None:
assert manager._sanitize_name(f"mem:topic{os.sep}foo.md") == "topic/foo"
class TestRenameReferencesToMemory:
@@ -127,7 +127,7 @@ class TestRenameReferencesToMemory:
)
def test_rename(
self,
manager: MemoriesManager,
manager: MemoryManager,
content: str,
old_name: str,
new_name: str,
@@ -150,7 +150,7 @@ class TestRenameReferencesToMemory:
("", ""), # raw, no delimiters
],
)
def test_replacement_inside_various_delimiters(self, manager: MemoriesManager, delimiter_pair: tuple[str, str]) -> None:
def test_replacement_inside_various_delimiters(self, manager: MemoryManager, delimiter_pair: tuple[str, str]) -> None:
left, right = delimiter_pair
content = f"{left}mem:foo{right}"
result, n = manager.rename_references_to_memory(content, "foo", "bar")
@@ -176,7 +176,7 @@ class TestNameSimilarity:
pytest.param("auth/login", "auth/v2/login", 0.75, id="basename_match_shared_prefix"),
],
)
def test_high_similarity_cases(self, manager: MemoriesManager, a: str, b: str, expected: float) -> None:
def test_high_similarity_cases(self, manager: MemoryManager, a: str, b: str, expected: float) -> None:
assert compute_name_similarity(a, b) == pytest.approx(expected)
@pytest.mark.parametrize(
@@ -195,7 +195,7 @@ class TestNameSimilarity:
pytest.param("topic1/topic2/core", "topic3/topic5/core", id="same_basename_disjoint_prefix_tokens"),
],
)
def test_below_threshold_cases(self, manager: MemoriesManager, a: str, b: str) -> None:
def test_below_threshold_cases(self, manager: MemoryManager, a: str, b: str) -> None:
assert compute_name_similarity(a, b) < NAME_SIMILARITY_THRESHOLD
@pytest.mark.parametrize(
@@ -210,14 +210,14 @@ class TestNameSimilarity:
pytest.param("user_login_flow", "login_flow", id="partial_token_overlap"),
],
)
def test_above_threshold_cases(self, manager: MemoriesManager, a: str, b: str) -> None:
def test_above_threshold_cases(self, manager: MemoryManager, a: str, b: str) -> None:
assert compute_name_similarity(a, b) >= NAME_SIMILARITY_THRESHOLD
def test_symmetry(self, manager: MemoriesManager) -> None:
def test_symmetry(self, manager: MemoryManager) -> None:
# similarity is order-independent
assert compute_name_similarity("auth/login", "security/login") == compute_name_similarity("security/login", "auth/login")
def test_cross_topic_suffix_coincidence_is_rejected(self, manager: MemoriesManager) -> None:
def test_cross_topic_suffix_coincidence_is_rejected(self, manager: MemoryManager) -> None:
"""
Regression: when two long names live under *different* topic prefixes and share
only a single trailing token (e.g. ``-subtleties``), the score must collapse to
@@ -238,7 +238,7 @@ class TestNameSimilarity:
same_prefix_score = compute_name_similarity(target, "frontend/routing-app-shell-subtleties")
assert same_prefix_score > 0.0
def test_find_candidates_ranks_by_descending_similarity(self, manager: MemoriesManager) -> None:
def test_find_candidates_ranks_by_descending_similarity(self, manager: MemoryManager) -> None:
existing = ["auth/login", "security/login", "database", "authentication"]
candidates = find_stale_reference_candidates("login", existing)
# both auth/login and security/login share basename `login` -> tied at 1.0; expect them first, alphabetical tiebreak
@@ -246,11 +246,11 @@ class TestNameSimilarity:
# `database` is unrelated and must not appear
assert "database" not in candidates
def test_find_candidates_returns_empty_below_threshold(self, manager: MemoriesManager) -> None:
def test_find_candidates_returns_empty_below_threshold(self, manager: MemoryManager) -> None:
existing = ["completely_unrelated", "another_thing"]
assert find_stale_reference_candidates("foo", existing) == []
def test_find_candidates_custom_threshold(self, manager: MemoriesManager) -> None:
def test_find_candidates_custom_threshold(self, manager: MemoryManager) -> None:
# raising the threshold filters out borderline matches
existing = ["authentication"]
loose = find_stale_reference_candidates("auth", existing, threshold=0.2)
@@ -260,24 +260,24 @@ class TestNameSimilarity:
@pytest.fixture
def fs_manager(tmp_path) -> MemoriesManager:
def fs_manager(tmp_path) -> MemoryManager:
"""A MemoriesManager backed by tmp_path with no read-only patterns."""
return MemoriesManager(serena_data_folder=tmp_path)
return MemoryManager(serena_data_folder=tmp_path)
def _write(manager: MemoriesManager, name: str, content: str) -> None:
def _write(manager: MemoryManager, name: str, content: str) -> None:
manager.save_memory(name, content, is_tool_context=False)
class TestValidateReferentialIntegrity:
def test_clean_report_when_all_references_resolve(self, fs_manager: MemoriesManager) -> None:
def test_clean_report_when_all_references_resolve(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth", "# auth notes")
_write(fs_manager, "login_flow", "see `mem:auth` for details")
report = fs_manager.validate_referential_integrity()
assert report.is_clean()
assert "No referential integrity issues" in report.format()
def test_stale_reference_with_topic_move_candidate(self, fs_manager: MemoriesManager) -> None:
def test_stale_reference_with_topic_move_candidate(self, fs_manager: MemoryManager) -> None:
# original memory `login` was moved to `auth/login`; a reference still points to bare `login`
_write(fs_manager, "auth/login", "# moved here")
_write(fs_manager, "docs", "old reference: `mem:login`")
@@ -288,14 +288,14 @@ class TestValidateReferentialIntegrity:
assert stale.referenced_name == "login"
assert "auth/login" in stale.candidates
def test_stale_reference_with_no_candidate(self, fs_manager: MemoriesManager) -> None:
def test_stale_reference_with_no_candidate(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "completely_unrelated", "# content")
_write(fs_manager, "docs", "broken: `mem:xyz_unknown_thing`")
report = fs_manager.validate_referential_integrity()
assert len(report.stale_references) == 1
assert report.stale_references[0].candidates == []
def test_high_confidence_unmarked_warning_for_topic_path(self, fs_manager: MemoriesManager) -> None:
def test_high_confidence_unmarked_warning_for_topic_path(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth/login", "# notes")
_write(fs_manager, "docs", "the auth/login process is documented elsewhere")
report = fs_manager.validate_referential_integrity()
@@ -306,7 +306,7 @@ class TestValidateReferentialIntegrity:
assert warning.occurrences == 1
assert report.low_confidence_unmarked_memories == []
def test_low_confidence_unmarked_warning_for_short_flat_name(self, fs_manager: MemoriesManager) -> None:
def test_low_confidence_unmarked_warning_for_short_flat_name(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth", "# notes")
_write(fs_manager, "docs", "you must use auth here")
report = fs_manager.validate_referential_integrity()
@@ -315,7 +315,7 @@ class TestValidateReferentialIntegrity:
assert report.low_confidence_unmarked_memories[0].suspected_name == "auth"
assert report.low_confidence_unmarked_memories[0].is_high_confidence is False
def test_long_flat_name_is_high_confidence(self, fs_manager: MemoriesManager) -> None:
def test_long_flat_name_is_high_confidence(self, fs_manager: MemoryManager) -> None:
# a flat name >= _HIGH_CONFIDENCE_NAME_LENGTH chars is unlikely to be coincidental prose
long_name = "authentication_flow"
assert len(long_name) >= HIGH_CONFIDENCE_NAME_LENGTH
@@ -324,7 +324,7 @@ class TestValidateReferentialIntegrity:
report = fs_manager.validate_referential_integrity()
assert len(report.high_confidence_unmarked_memories) == 1
def test_fuzzy_near_miss_rejects_substring_only_matches(self, fs_manager: MemoriesManager) -> None:
def test_fuzzy_near_miss_rejects_substring_only_matches(self, fs_manager: MemoryManager) -> None:
"""
Regression: fuzzy bare-text matching must require meaningful token overlap, not just
substring containment. Without the token-Jaccard floor, generic words like
@@ -353,7 +353,7 @@ class TestValidateReferentialIntegrity:
f"low={report.low_confidence_unmarked_memories}, stale={report.stale_references}"
)
def test_long_flat_name_near_miss_is_high_confidence(self, fs_manager: MemoriesManager) -> None:
def test_long_flat_name_near_miss_is_high_confidence(self, fs_manager: MemoryManager) -> None:
"""
Regression: bare text that is a *near-match* of an existing long memory name
(e.g. the same name with a missing suffix) should be flagged as a high-confidence
@@ -380,7 +380,7 @@ class TestValidateReferentialIntegrity:
assert warning.suspected_name == existing
assert warning.source_memory == "docs"
def test_fuzzy_accepts_bare_token_when_jaccard_just_above_floor(self, fs_manager: MemoriesManager) -> None:
def test_fuzzy_accepts_bare_token_when_jaccard_just_above_floor(self, fs_manager: MemoryManager) -> None:
"""
Edge case: bare token is a clean substring of the existing memory name AND
their tokenized Jaccard sits just above ``_FUZZY_BARE_TOKEN_JACCARD_FLOOR``
@@ -398,7 +398,7 @@ class TestValidateReferentialIntegrity:
assert fuzzy[0].suspected_name == existing
assert fuzzy[0].actual_token == bare
def test_fuzzy_rejects_bare_token_when_jaccard_just_below_floor(self, fs_manager: MemoriesManager) -> None:
def test_fuzzy_rejects_bare_token_when_jaccard_just_below_floor(self, fs_manager: MemoryManager) -> None:
"""
Edge case: bare token is still a clean substring of the existing memory name,
but their tokenized Jaccard is just below ``_FUZZY_BARE_TOKEN_JACCARD_FLOOR``
@@ -414,7 +414,7 @@ class TestValidateReferentialIntegrity:
assert report.high_confidence_unmarked_memories == []
assert report.low_confidence_unmarked_memories == []
def test_fuzzy_requires_substring_containment(self, fs_manager: MemoriesManager) -> None:
def test_fuzzy_requires_substring_containment(self, fs_manager: MemoryManager) -> None:
"""
Edge case: bare token and existing name share several tokens (and even a topic
prefix) but neither is a substring of the other. Without the containment
@@ -430,7 +430,7 @@ class TestValidateReferentialIntegrity:
assert report.high_confidence_unmarked_memories == []
assert report.low_confidence_unmarked_memories == []
def test_fuzzy_accepts_reverse_containment(self, fs_manager: MemoriesManager) -> None:
def test_fuzzy_accepts_reverse_containment(self, fs_manager: MemoryManager) -> None:
"""
Edge case: the *existing* memory name is a substring of the longer bare token
(rather than the other way around). The containment gate must hold in both
@@ -447,7 +447,7 @@ class TestValidateReferentialIntegrity:
assert fuzzy[0].suspected_name == existing
assert fuzzy[0].actual_token == bare
def test_unmarked_exact_match_suppressed_for_ignored_word(self, fs_manager: MemoriesManager) -> None:
def test_unmarked_exact_match_suppressed_for_ignored_word(self, fs_manager: MemoryManager) -> None:
"""
Names in ``_WORDS_TO_IGNORE_AS_MEMORY_NAME_CANDIDATES`` (e.g. ``core``) are
common-word collisions and must not produce bare-occurrence warnings even
@@ -460,7 +460,7 @@ class TestValidateReferentialIntegrity:
assert all(w.suspected_name != "core" for w in report.high_confidence_unmarked_memories)
assert all(w.suspected_name != "core" for w in report.low_confidence_unmarked_memories)
def test_unmarked_exact_match_suppressed_for_topic_with_ignored_basename(self, fs_manager: MemoriesManager) -> None:
def test_unmarked_exact_match_suppressed_for_topic_with_ignored_basename(self, fs_manager: MemoryManager) -> None:
"""
The ignore filter applies to the *basename* of a candidate name, so a memory
named ``frontend/core`` is also exempt from bare-occurrence warnings (the
@@ -473,7 +473,7 @@ class TestValidateReferentialIntegrity:
assert all(w.suspected_name != "frontend/core" for w in report.high_confidence_unmarked_memories)
assert all(w.suspected_name != "frontend/core" for w in report.low_confidence_unmarked_memories)
def test_unmarked_exact_match_not_suppressed_for_non_ignored_basename(self, fs_manager: MemoriesManager) -> None:
def test_unmarked_exact_match_not_suppressed_for_non_ignored_basename(self, fs_manager: MemoryManager) -> None:
"""
Sanity: the ignore filter must not over-apply to names whose basename happens
to share a prefix with an ignored word.
@@ -483,7 +483,7 @@ class TestValidateReferentialIntegrity:
report = fs_manager.validate_referential_integrity()
assert any(w.suspected_name == "frontend/core-utils" for w in report.high_confidence_unmarked_memories)
def test_stale_reference_candidates_are_capped(self, fs_manager: MemoriesManager) -> None:
def test_stale_reference_candidates_are_capped(self, fs_manager: MemoryManager) -> None:
"""
The candidate list proposed for an unresolved ``mem:`` reference is capped at
:attr:`MAX_STALE_REFERENCE_CANDIDATES`. Beyond a small number the list stops
@@ -502,7 +502,7 @@ class TestValidateReferentialIntegrity:
assert len(candidates) == cap
assert candidates == candidate_names[:cap]
def test_include_unmarked_false_skips_both_scans(self, fs_manager: MemoriesManager) -> None:
def test_include_unmarked_false_skips_both_scans(self, fs_manager: MemoryManager) -> None:
"""
With ``include_unmarked=False`` only stale references are reported; both the
exact unmarked scan and the fuzzy near-miss scan are entirely skipped.
@@ -521,7 +521,7 @@ class TestValidateReferentialIntegrity:
assert len(report.stale_references) == 1
assert report.stale_references[0].referenced_name == "nope"
def test_include_fuzzy_matching_false_keeps_exact_only(self, fs_manager: MemoriesManager) -> None:
def test_include_fuzzy_matching_false_keeps_exact_only(self, fs_manager: MemoryManager) -> None:
"""
With ``include_unmarked=True`` but ``include_fuzzy_matching=False``, exact
bare-occurrence warnings still fire but fuzzy near-misses are suppressed.
@@ -542,7 +542,7 @@ class TestValidateReferentialIntegrity:
# fuzzy near-miss for `playwright-gestures-guide` is suppressed
assert fuzzy == []
def test_self_reference_is_ignored(self, fs_manager: MemoriesManager) -> None:
def test_self_reference_is_ignored(self, fs_manager: MemoryManager) -> None:
# the basename `login` appears in `auth/login`'s own content -> must not be flagged
_write(fs_manager, "auth/login", "this login flow ...")
_write(fs_manager, "auth", "# auth notes")
@@ -553,14 +553,14 @@ class TestValidateReferentialIntegrity:
for w in report.high_confidence_unmarked_memories + report.low_confidence_unmarked_memories:
assert not (w.source_memory == "auth/login" and w.suspected_name in ("login", "auth/login"))
def test_marked_reference_does_not_produce_unmarked_warning(self, fs_manager: MemoriesManager) -> None:
def test_marked_reference_does_not_produce_unmarked_warning(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth/login", "# notes")
_write(fs_manager, "docs", "see `mem:auth/login` for details")
report = fs_manager.validate_referential_integrity()
# the marked reference is valid; no warning should fire on that occurrence
assert report.is_clean()
def test_mixed_marked_and_bare_in_same_content(self, fs_manager: MemoriesManager) -> None:
def test_mixed_marked_and_bare_in_same_content(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth/login", "# notes")
_write(fs_manager, "docs", "we have `mem:auth/login` plus a bare auth/login mention")
report = fs_manager.validate_referential_integrity()
@@ -568,7 +568,7 @@ class TestValidateReferentialIntegrity:
assert len(report.high_confidence_unmarked_memories) == 1
assert report.high_confidence_unmarked_memories[0].occurrences == 1
def test_empty_memory_skipped(self, fs_manager: MemoriesManager) -> None:
def test_empty_memory_skipped(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth/login", "")
_write(fs_manager, "docs", "# nothing relevant")
# must not raise even though one memory is empty
@@ -576,7 +576,7 @@ class TestValidateReferentialIntegrity:
assert report.is_clean()
def test_read_only_status_propagated(self, tmp_path) -> None:
manager = MemoriesManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
manager = MemoryManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
_write(manager, "frozen/notes", "see bare auth/login mention")
_write(manager, "auth/login", "# notes")
report = manager.validate_referential_integrity()
@@ -587,7 +587,7 @@ class TestValidateReferentialIntegrity:
class TestAutoPrefixBareReferences:
def test_prefixes_high_confidence_warning_in_writable_memory(self, fs_manager: MemoriesManager) -> None:
def test_prefixes_high_confidence_warning_in_writable_memory(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth/login", "# notes")
_write(fs_manager, "docs", "the auth/login process is documented here")
result = fs_manager.auto_prefix_bare_references()
@@ -596,7 +596,7 @@ class TestAutoPrefixBareReferences:
# verify the file content was actually mutated
assert fs_manager.load_memory("docs") == "the mem:auth/login process is documented here"
def test_skips_low_confidence_by_default(self, fs_manager: MemoriesManager) -> None:
def test_skips_low_confidence_by_default(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth", "# notes")
_write(fs_manager, "docs", "use auth here")
result = fs_manager.auto_prefix_bare_references()
@@ -605,7 +605,7 @@ class TestAutoPrefixBareReferences:
# content unchanged
assert fs_manager.load_memory("docs") == "use auth here"
def test_include_flat_names_widens_scope(self, fs_manager: MemoriesManager) -> None:
def test_include_flat_names_widens_scope(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth", "# notes")
_write(fs_manager, "docs", "use auth here")
result = fs_manager.auto_prefix_bare_references(include_flat_names=True)
@@ -613,7 +613,7 @@ class TestAutoPrefixBareReferences:
assert fs_manager.load_memory("docs") == "use mem:auth here"
def test_skips_read_only_source_by_default(self, tmp_path) -> None:
manager = MemoriesManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
manager = MemoryManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
_write(manager, "frozen/notes", "the auth/login process")
_write(manager, "auth/login", "# notes")
result = manager.auto_prefix_bare_references()
@@ -623,21 +623,21 @@ class TestAutoPrefixBareReferences:
assert manager.load_memory("frozen/notes") == "the auth/login process"
def test_include_read_only_modifies_read_only_source(self, tmp_path) -> None:
manager = MemoriesManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
manager = MemoryManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
_write(manager, "frozen/notes", "the auth/login process")
_write(manager, "auth/login", "# notes")
result = manager.auto_prefix_bare_references(include_read_only=True)
assert result.total_replacements == 1
assert manager.load_memory("frozen/notes") == "the mem:auth/login process"
def test_multiple_occurrences_in_same_memory(self, fs_manager: MemoriesManager) -> None:
def test_multiple_occurrences_in_same_memory(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth/login", "# notes")
_write(fs_manager, "docs", "auth/login here and auth/login there")
result = fs_manager.auto_prefix_bare_references()
assert result.total_replacements == 2
assert fs_manager.load_memory("docs") == "mem:auth/login here and mem:auth/login there"
def test_marked_references_left_alone(self, fs_manager: MemoriesManager) -> None:
def test_marked_references_left_alone(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth/login", "# notes")
_write(fs_manager, "docs", "see `mem:auth/login` and also auth/login bare")
result = fs_manager.auto_prefix_bare_references()
@@ -645,7 +645,7 @@ class TestAutoPrefixBareReferences:
assert result.total_replacements == 1
assert fs_manager.load_memory("docs") == "see `mem:auth/login` and also mem:auth/login bare"
def test_dry_run_does_not_modify_files(self, fs_manager: MemoriesManager) -> None:
def test_dry_run_does_not_modify_files(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth/login", "# notes")
_write(fs_manager, "docs", "the auth/login process is documented here")
result = fs_manager.auto_prefix_bare_references(dry_run=True)
@@ -660,7 +660,7 @@ class TestAutoPrefixBareReferences:
assert result.dry_run is False
assert fs_manager.load_memory("docs") == "the mem:auth/login process is documented here"
def test_fuzzy_warnings_are_routed_to_skipped_fuzzy(self, fs_manager: MemoriesManager) -> None:
def test_fuzzy_warnings_are_routed_to_skipped_fuzzy(self, fs_manager: MemoryManager) -> None:
# set up a fuzzy near-miss case (token shares all parts with a longer existing name)
existing = "adding_new_language_support_guide"
bare = "adding_new_language_support"
@@ -676,7 +676,7 @@ class TestAutoPrefixBareReferences:
# source unchanged
assert fs_manager.load_memory("docs") == f"See {bare} for details"
def test_no_double_prefix_on_repeated_runs(self, fs_manager: MemoriesManager) -> None:
def test_no_double_prefix_on_repeated_runs(self, fs_manager: MemoryManager) -> None:
_write(fs_manager, "auth/login", "# notes")
_write(fs_manager, "docs", "the auth/login process")
fs_manager.auto_prefix_bare_references()