diff --git a/docs/DOMAIN_MODEL.md b/docs/DOMAIN_MODEL.md index 5f4b88304..b6847a077 100644 --- a/docs/DOMAIN_MODEL.md +++ b/docs/DOMAIN_MODEL.md @@ -52,6 +52,9 @@ which graph and search projections attach. - `permalink` is the human/agent-facing semantic address for Markdown content. It is project-scoped, may be absent when permalinks are disabled or inapplicable, and changes only according to the configured move and permalink policies. +- Non-Markdown entities always have `permalink=None`. `external_id` is their stable API identity, + and `file_path` locates the stored resource; a database-only permalink would not round-trip + through the source file. - `title` is mutable display metadata, not identity. - For Markdown notes, `created_at` and `updated_at` project the canonical `created` and `modified` frontmatter values. Missing values fall back independently to file ctime and mtime for legacy diff --git a/src/basic_memory/indexing/batch_indexer.py b/src/basic_memory/indexing/batch_indexer.py index c2d0fc43f..20f42fa90 100644 --- a/src/basic_memory/indexing/batch_indexer.py +++ b/src/basic_memory/indexing/batch_indexer.py @@ -438,7 +438,8 @@ async def _upsert_regular_file(self, file: IndexInputFile) -> _PreparedEntity: is_new_entity = existing is None if existing is None: - await self.entity_service.resolve_permalink(file.path, skip_conflict_check=True) + # Non-Markdown resources cannot persist a semantic address back to source bytes. + # Their stable API identity is external_id; file_path locates the stored resource. entity = Entity( note_type="file", file_path=file.path, diff --git a/tests/indexing/test_batch_indexer.py b/tests/indexing/test_batch_indexer.py index 1bd0cd177..00f410ed7 100644 --- a/tests/indexing/test_batch_indexer.py +++ b/tests/indexing/test_batch_indexer.py @@ -347,6 +347,7 @@ async def test_batch_indexer_indexes_non_markdown_files( search_service, file_service, project_config, + monkeypatch, ): pdf_path = "assets/doc.pdf" image_path = "assets/image.png" @@ -357,6 +358,10 @@ async def test_batch_indexer_indexes_non_markdown_files( pdf_path: await _load_input(file_service, pdf_path), image_path: await _load_input(file_service, image_path), } + resolve_permalink = AsyncMock( + side_effect=AssertionError("non-Markdown files must not resolve permalinks") + ) + monkeypatch.setattr(entity_service, "resolve_permalink", resolve_permalink) batch_indexer = _make_batch_indexer( app_config, entity_service, @@ -380,8 +385,11 @@ async def test_batch_indexer_indexes_non_markdown_files( image_entity = await entity_repository.get_by_file_path(session, image_path) assert pdf_entity is not None assert pdf_entity.content_type == "application/pdf" + assert pdf_entity.permalink is None assert image_entity is not None assert image_entity.content_type == "image/png" + assert image_entity.permalink is None + resolve_permalink.assert_not_awaited() @pytest.mark.asyncio