From a3e4badcf953bc85437b231bad72b1288a36f03f Mon Sep 17 00:00:00 2001 From: phernandez Date: Mon, 13 Apr 2026 09:54:44 -0500 Subject: [PATCH] fix(core): preserve empty frontmatter permalink semantics Signed-off-by: phernandez --- src/basic_memory/services/entity_service.py | 53 ++++++++++++------- .../test_entity_service_write_result.py | 22 ++++++++ 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/basic_memory/services/entity_service.py b/src/basic_memory/services/entity_service.py index 9d7f294b..51e9876a 100644 --- a/src/basic_memory/services/entity_service.py +++ b/src/basic_memory/services/entity_service.py @@ -60,6 +60,11 @@ class EntityWriteResult: search_content: str +def _frontmatter_permalink(value: object) -> str | None: + """Return an explicit frontmatter permalink only when YAML parsed a real string.""" + return value if isinstance(value, str) and value else None + + class EntityService(BaseService[EntityModel]): """Service for managing entities in the database.""" @@ -287,11 +292,13 @@ class EntityService(BaseService[EntityModel]): schema.note_type = content_frontmatter["type"] if "permalink" in content_frontmatter: - content_markdown = self._build_frontmatter_markdown( - schema.title, - schema.note_type, - _coerce_to_string(content_frontmatter["permalink"]), - ) + content_permalink = _frontmatter_permalink(content_frontmatter["permalink"]) + if content_permalink is not None: + content_markdown = self._build_frontmatter_markdown( + schema.title, + schema.note_type, + content_permalink, + ) # Get unique permalink (prioritizing content frontmatter) unless disabled if self.app_config and self.app_config.disable_permalinks: @@ -394,11 +401,13 @@ class EntityService(BaseService[EntityModel]): schema.note_type = content_frontmatter["type"] if "permalink" in content_frontmatter: - content_markdown = self._build_frontmatter_markdown( - schema.title, - schema.note_type, - _coerce_to_string(content_frontmatter["permalink"]), - ) + content_permalink = _frontmatter_permalink(content_frontmatter["permalink"]) + if content_permalink is not None: + content_markdown = self._build_frontmatter_markdown( + schema.title, + schema.note_type, + content_permalink, + ) # Check if we need to update the permalink based on content frontmatter (unless disabled) new_permalink = entity.permalink # Default to existing @@ -525,11 +534,13 @@ class EntityService(BaseService[EntityModel]): schema.note_type = content_frontmatter["type"] if "permalink" in content_frontmatter: - content_markdown = self._build_frontmatter_markdown( - schema.title, - schema.note_type, - _coerce_to_string(content_frontmatter["permalink"]), - ) + content_permalink = _frontmatter_permalink(content_frontmatter["permalink"]) + if content_permalink is not None: + content_markdown = self._build_frontmatter_markdown( + schema.title, + schema.note_type, + content_permalink, + ) # --- Permalink Resolution --- if self.app_config and self.app_config.disable_permalinks: @@ -668,11 +679,13 @@ class EntityService(BaseService[EntityModel]): update_data["note_type"] = _coerce_to_string(content_frontmatter["type"]) if "permalink" in content_frontmatter: - content_markdown = self._build_frontmatter_markdown( - _coerce_to_string(update_data.get("title", entity.title)), - _coerce_to_string(update_data.get("note_type", entity.note_type)), - _coerce_to_string(content_frontmatter["permalink"]), - ) + content_permalink = _frontmatter_permalink(content_frontmatter["permalink"]) + if content_permalink is not None: + content_markdown = self._build_frontmatter_markdown( + _coerce_to_string(update_data.get("title", entity.title)), + _coerce_to_string(update_data.get("note_type", entity.note_type)), + content_permalink, + ) metadata = normalize_frontmatter_metadata(content_frontmatter or {}) update_data["entity_metadata"] = {k: v for k, v in metadata.items() if v is not None} diff --git a/tests/services/test_entity_service_write_result.py b/tests/services/test_entity_service_write_result.py index 31bd6228..95daac42 100644 --- a/tests/services/test_entity_service_write_result.py +++ b/tests/services/test_entity_service_write_result.py @@ -27,6 +27,28 @@ async def test_create_entity_with_content_returns_full_and_search_content( assert result.search_content == "Create body content" +@pytest.mark.asyncio +@pytest.mark.parametrize("permalink_line", ["permalink:", "permalink: null", 'permalink: ""']) +async def test_create_entity_ignores_empty_frontmatter_permalink( + entity_service, file_service, permalink_line: str +) -> None: + result = await entity_service.create_entity_with_content( + EntitySchema( + title="Empty Frontmatter Permalink", + directory="notes", + note_type="note", + content=f"---\n{permalink_line}\n---\nCreate body content", + ) + ) + + file_path = file_service.get_entity_path(result.entity) + file_content, _ = await file_service.read_file(file_path) + + assert result.entity.permalink == "test-project/notes/empty-frontmatter-permalink" + assert "permalink: test-project/notes/empty-frontmatter-permalink" in file_content + assert "permalink: None" not in file_content + + @pytest.mark.asyncio async def test_update_entity_with_content_returns_full_and_search_content( entity_service, file_service