From eb43c5be1a6ca81f28e7eabe56ea2c800f10f282 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 16 May 2026 15:16:14 +0200 Subject: [PATCH] Rename MemoriesManager -> MemoryManager --- src/serena/agent.py | 6 +- src/serena/cli.py | 6 +- src/serena/dashboard.py | 12 +- src/serena/memories/memory_manager.py | 24 ++-- .../memories/memory_reference_analysis.py | 4 +- src/serena/project.py | 4 +- src/serena/tools/memory_tools.py | 12 +- src/serena/tools/tools_base.py | 7 +- src/serena/tools/workflow_tools.py | 2 +- test/serena/config/test_serena_config.py | 8 +- test/serena/test_memories_manager.py | 118 +++++++++--------- 11 files changed, 101 insertions(+), 102 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 6555dc93..c8e0d1e5 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -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" diff --git a/src/serena/cli.py b/src/serena/cli.py index 5f87a36b..71d7da38 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -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( diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 9a35806c..66c93827 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -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") diff --git a/src/serena/memories/memory_manager.py b/src/serena/memories/memory_manager.py index ab97abc1..ce1f8a43 100644 --- a/src/serena/memories/memory_manager.py +++ b/src/serena/memories/memory_manager.py @@ -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) diff --git a/src/serena/memories/memory_reference_analysis.py b/src/serena/memories/memory_reference_analysis.py index 47b61f56..9846a4b9 100644 --- a/src/serena/memories/memory_reference_analysis.py +++ b/src/serena/memories/memory_reference_analysis.py @@ -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( diff --git a/src/serena/project.py b/src/serena/project.py index 94e59a44..f202af96 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -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, diff --git a/src/serena/tools/memory_tools.py b/src/serena/tools/memory_tools.py index f7be84d0..6ebffd9b 100644 --- a/src/serena/tools/memory_tools.py +++ b/src/serena/tools/memory_tools.py @@ -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 ) diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 33b47d15..89d064c3 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -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 diff --git a/src/serena/tools/workflow_tools.py b/src/serena/tools/workflow_tools.py index 18ed224b..6a0f51d9 100644 --- a/src/serena/tools/workflow_tools.py +++ b/src/serena/tools/workflow_tools.py @@ -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) diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index 75c1e143..02e2851c 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -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() diff --git a/test/serena/test_memories_manager.py b/test/serena/test_memories_manager.py index 98b04a8d..2ba70196 100644 --- a/test/serena/test_memories_manager.py +++ b/test/serena/test_memories_manager.py @@ -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()