From 23922f291511c178a2dc19d4599fa0e6fe30df5c Mon Sep 17 00:00:00 2001 From: phernandez Date: Wed, 22 Jan 2025 17:59:46 -0600 Subject: [PATCH] set permalink in frontmatter, re-index file_path on move during sync --- src/basic_memory/markdown/knowledge_writer.py | 54 +++++++++++-------- src/basic_memory/sync/sync_service.py | 4 +- tests/markdown/test_knowledge_writer.py | 4 +- tests/services/test_entity_service.py | 4 +- tests/sync/test_sync_service.py | 38 +++++++++++++ 5 files changed, 76 insertions(+), 28 deletions(-) diff --git a/src/basic_memory/markdown/knowledge_writer.py b/src/basic_memory/markdown/knowledge_writer.py index 335bbea7..f23da3b1 100644 --- a/src/basic_memory/markdown/knowledge_writer.py +++ b/src/basic_memory/markdown/knowledge_writer.py @@ -1,14 +1,16 @@ """Writer for knowledge entity markdown files.""" + from typing import Optional from loguru import logger +from basic_memory.markdown import EntityFrontmatter from basic_memory.models import Entity as EntityModel, Observation class KnowledgeWriter: """Formats entities into markdown files. - + Content handling: 1. If raw content is provided, use it directly 2. If structured data exists (observations/relations), generate structured content @@ -18,7 +20,7 @@ class KnowledgeWriter: async def format_frontmatter(self, entity: EntityModel) -> dict: """Generate frontmatter metadata for entity.""" frontmatter = { - "id": entity.permalink, + "permalink": entity.permalink, "type": entity.entity_type, "created": entity.created_at.isoformat(), "modified": entity.updated_at.isoformat(), @@ -35,19 +37,19 @@ class KnowledgeWriter: if obs.tags: line += " " + " ".join(f"#{tag}" for tag in sorted(obs.tags)) - # Add context if present + # Add context if present if obs.context: line += f" ({obs.context})" return line - + async def format_content(self, entity: EntityModel, content: Optional[str] = None) -> str: """Format entity content as markdown. - + Args: entity: Entity to format content: Optional raw content to use instead of generating structured content - + Returns: Formatted markdown content """ @@ -58,25 +60,29 @@ class KnowledgeWriter: # Otherwise, build structured content from entity data sections = [] - + # Only add entity title if we don't have structured content # This prevents duplicate titles when raw content already has a title if not (entity.observations or entity.outgoing_relations): - sections.extend([ - f"# {entity.title}", - "", # Empty line after title - ]) - + sections.extend( + [ + f"# {entity.title}", + "", # Empty line after title + ] + ) + if entity.summary: sections.extend([entity.summary, ""]) # Add observations if present if entity.observations: - sections.extend([ - "## Observations", - "", - "", - ]) + sections.extend( + [ + "## Observations", + "", + "", + ] + ) for obs in entity.observations: sections.append(await self.format_observation(obs)) @@ -84,12 +90,14 @@ class KnowledgeWriter: # Add relations if present if entity.outgoing_relations: - sections.extend([ - "## Relations", - "", - "", # Empty line after format comment - ]) - + sections.extend( + [ + "## Relations", + "", + "", # Empty line after format comment + ] + ) + for rel in entity.outgoing_relations: line = f"- {rel.relation_type} [[{rel.to_entity.title}]]" if rel.context: diff --git a/src/basic_memory/sync/sync_service.py b/src/basic_memory/sync/sync_service.py index 45fa4a49..c75cce78 100644 --- a/src/basic_memory/sync/sync_service.py +++ b/src/basic_memory/sync/sync_service.py @@ -69,9 +69,11 @@ class SyncService: entity = await self.entity_repository.get_by_file_path(old_path) if entity: # Update file_path but keep the same permalink for link stability - await self.entity_repository.update( + updated = await self.entity_repository.update( entity.id, {"file_path": new_path, "checksum": changes.checksums[new_path]} ) + # update search index + await self.search_service.index_entity(updated) # Handle deletions next # remove rows from db for files no longer present diff --git a/tests/markdown/test_knowledge_writer.py b/tests/markdown/test_knowledge_writer.py index bf816fbe..890a62e2 100644 --- a/tests/markdown/test_knowledge_writer.py +++ b/tests/markdown/test_knowledge_writer.py @@ -57,7 +57,7 @@ async def test_format_frontmatter_basic(knowledge_writer: KnowledgeWriter, sampl """Test basic frontmatter formatting.""" frontmatter = await knowledge_writer.format_frontmatter(sample_entity) - assert frontmatter["id"] == "knowledge/test-entity" + assert frontmatter["permalink"] == "knowledge/test-entity" assert frontmatter["type"] == "test" assert frontmatter["created"] == "2025-01-01T00:00:00+00:00" assert frontmatter["modified"] == "2025-01-02T00:00:00+00:00" @@ -74,7 +74,7 @@ async def test_format_frontmatter_with_metadata( assert frontmatter["status"] == "active" assert frontmatter["priority"] == "high" - assert frontmatter["id"] == "knowledge/test-entity" + assert frontmatter["permalink"] == "knowledge/test-entity" @pytest.mark.asyncio diff --git a/tests/services/test_entity_service.py b/tests/services/test_entity_service.py index 24772e3c..611eb559 100644 --- a/tests/services/test_entity_service.py +++ b/tests/services/test_entity_service.py @@ -55,7 +55,7 @@ async def test_create_entity(entity_service: EntityService, file_service: FileSe metadata = yaml.safe_load(frontmatter) # Verify frontmatter contents - assert metadata["id"] == entity.permalink + assert metadata["permalink"] == entity.permalink assert metadata["type"] == entity.entity_type assert "created" in metadata assert "modified" in metadata @@ -440,7 +440,7 @@ async def test_update_entity_name(entity_service: EntityService, file_service: F _, frontmatter, _ = content.split("---", 2) metadata = yaml.safe_load(frontmatter) - assert metadata["id"] == entity.permalink + assert metadata["permalink"] == entity.permalink # And verify content uses new name for title assert "# new-name" in content diff --git a/tests/sync/test_sync_service.py b/tests/sync/test_sync_service.py index d6dc37d1..5470231f 100644 --- a/tests/sync/test_sync_service.py +++ b/tests/sync/test_sync_service.py @@ -544,6 +544,44 @@ Testing file timestamps assert abs((file_entity.created_at.timestamp() - file_stats.st_ctime)) < 1 # Allow 1s difference assert abs((file_entity.updated_at.timestamp() - file_stats.st_mtime)) < 1 # Allow 1s difference +@pytest.mark.asyncio +async def test_file_move_updates_search_index( + sync_service: SyncService, + test_config: ProjectConfig, + search_service: SearchService, +): + """Test that moving a file updates its path in the search index.""" + project_dir = test_config.home + + # Create initial file + content = """ +--- +type: knowledge +--- +# Test Move +Content for move test +""" + old_path = project_dir / "old" / "test_move.md" + old_path.parent.mkdir(parents=True) + await create_test_file(old_path, content) + + # Initial sync + await sync_service.sync(test_config.home) + + # Move the file + new_path = project_dir / "new" / "moved_file.md" + new_path.parent.mkdir(parents=True) + old_path.rename(new_path) + + # Sync again + await sync_service.sync(test_config.home) + + # Check search index has updated path + results = await search_service.search(SearchQuery(text="Content for move test")) + assert len(results) == 1 + assert results[0].file_path == str(new_path.relative_to(project_dir)) + + @pytest.mark.asyncio async def test_sync_null_checksum_cleanup( sync_service: SyncService, test_config: ProjectConfig, entity_service: EntityService