mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c919576cce |
@@ -69,11 +69,26 @@ class LinkResolver:
|
||||
logger.debug(f"Found entity with path (with .md): {found_path_md.file_path}")
|
||||
return found_path_md
|
||||
|
||||
# 5. Try permalink variations with underscored folders (fixes #416)
|
||||
# This handles cases where the permalink was generated from a path like
|
||||
# "_archive/note.md" which becomes "archive/note" in the permalink,
|
||||
# but the user provides "archive/note" and we need to find the original file
|
||||
underscored_variants = self._generate_underscored_variants(clean_text)
|
||||
for variant in underscored_variants:
|
||||
logger.trace(f"Trying underscored variant: {variant}")
|
||||
|
||||
# Try as file path with .md extension
|
||||
variant_with_md = f"{variant}.md"
|
||||
found_variant = await self.entity_repository.get_by_file_path(variant_with_md)
|
||||
if found_variant:
|
||||
logger.debug(f"Found entity with underscored path variant: {found_variant.file_path}")
|
||||
return found_variant
|
||||
|
||||
# In strict mode, don't try fuzzy search - return None if no exact match found
|
||||
if strict:
|
||||
return None
|
||||
|
||||
# 5. Fall back to search for fuzzy matching (only if not in strict mode)
|
||||
# 6. Fall back to search for fuzzy matching (only if not in strict mode)
|
||||
if use_search and "*" not in clean_text:
|
||||
results = await self.search_service.search(
|
||||
query=SearchQuery(text=clean_text, entity_types=[SearchItemType.ENTITY]),
|
||||
@@ -91,6 +106,47 @@ class LinkResolver:
|
||||
# if we couldn't find anything then return None
|
||||
return None
|
||||
|
||||
def _generate_underscored_variants(self, path: str) -> list[str]:
|
||||
"""Generate permalink variants with underscored folder prefixes.
|
||||
|
||||
This handles cases where folder names start with underscores (e.g., _archive, _drafts)
|
||||
but the permalink has them stripped. We generate variants to try and find the original file.
|
||||
|
||||
Args:
|
||||
path: The permalink path (e.g., "archive/articles/example-note")
|
||||
|
||||
Returns:
|
||||
List of path variants with underscores prepended to various segments
|
||||
(e.g., ["_archive/articles/example-note", "archive/_articles/example-note", etc.])
|
||||
"""
|
||||
if "/" not in path:
|
||||
# Single segment, just try with underscore prefix
|
||||
return [f"_{path}"]
|
||||
|
||||
segments = path.split("/")
|
||||
variants = []
|
||||
|
||||
# Generate variants by prepending underscore to each segment
|
||||
# For path "archive/articles/note", generate:
|
||||
# - "_archive/articles/note"
|
||||
# - "archive/_articles/note"
|
||||
# - "_archive/_articles/note"
|
||||
for i in range(len(segments)):
|
||||
# Try prepending underscore to this segment
|
||||
variant_segments = segments.copy()
|
||||
variant_segments[i] = f"_{segments[i]}"
|
||||
variants.append("/".join(variant_segments))
|
||||
|
||||
# For paths with 2+ folders, also try combinations of underscored first segments
|
||||
if len(segments) >= 2:
|
||||
# Try first two segments underscored
|
||||
variant_segments = segments.copy()
|
||||
variant_segments[0] = f"_{segments[0]}"
|
||||
variant_segments[1] = f"_{segments[1]}"
|
||||
variants.append("/".join(variant_segments))
|
||||
|
||||
return variants
|
||||
|
||||
def _normalize_link_text(self, link_text: str) -> Tuple[str, Optional[str]]:
|
||||
"""Normalize link text and extract alias if present.
|
||||
|
||||
|
||||
@@ -356,3 +356,91 @@ async def test_duplicate_title_handling_in_strict_mode(link_resolver, test_entit
|
||||
assert result is not None
|
||||
# Should return the first match (components/core-service based on test fixture order)
|
||||
assert result.permalink == "components/core-service"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_underscored_folder_resolution(entity_service, entity_repository, search_service):
|
||||
"""Test resolving permalinks from files in underscored folders.
|
||||
|
||||
This tests the fix for issue #416 where read_note fails to find notes
|
||||
when given permalinks from underscored folder names (e.g., _archive/, _drafts/).
|
||||
The permalink normalization strips underscores, but link resolution should
|
||||
still be able to find the original files.
|
||||
"""
|
||||
# Create an entity in an underscored folder
|
||||
entity, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Example Note",
|
||||
entity_type="note",
|
||||
folder="_archive/articles",
|
||||
project=entity_service.repository.project_id,
|
||||
)
|
||||
)
|
||||
|
||||
# The entity's file path should have the underscore
|
||||
assert entity.file_path == "_archive/articles/Example Note.md"
|
||||
|
||||
# The permalink should have the underscore stripped (normalized)
|
||||
assert entity.permalink == "archive/articles/example-note"
|
||||
|
||||
# Index the entity for search
|
||||
await search_service.index_entity(entity)
|
||||
|
||||
# Create link resolver
|
||||
link_resolver = LinkResolver(entity_repository, search_service)
|
||||
|
||||
# Test 1: Using the normalized permalink (what the user would copy from YAML frontmatter)
|
||||
# This should now work with our fix
|
||||
result = await link_resolver.resolve_link("archive/articles/example-note")
|
||||
assert result is not None
|
||||
assert result.file_path == "_archive/articles/Example Note.md"
|
||||
assert result.permalink == "archive/articles/example-note"
|
||||
|
||||
# Test 2: Using the title
|
||||
result = await link_resolver.resolve_link("Example Note")
|
||||
assert result is not None
|
||||
assert result.file_path == "_archive/articles/Example Note.md"
|
||||
|
||||
# Test 3: Using the actual file path (with underscore)
|
||||
result = await link_resolver.resolve_link("_archive/articles/Example Note.md")
|
||||
assert result is not None
|
||||
assert result.file_path == "_archive/articles/Example Note.md"
|
||||
|
||||
# Test 4: Multiple underscored folders
|
||||
entity2, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Draft Post",
|
||||
entity_type="note",
|
||||
folder="_drafts/_private",
|
||||
project=entity_service.repository.project_id,
|
||||
)
|
||||
)
|
||||
await search_service.index_entity(entity2)
|
||||
|
||||
result = await link_resolver.resolve_link("drafts/private/draft-post")
|
||||
assert result is not None
|
||||
assert result.file_path == "_drafts/_private/Draft Post.md"
|
||||
assert result.permalink == "drafts/private/draft-post"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_underscored_variants_generation(entity_repository, search_service):
|
||||
"""Test the _generate_underscored_variants helper method."""
|
||||
link_resolver = LinkResolver(entity_repository, search_service)
|
||||
|
||||
# Test single segment
|
||||
variants = link_resolver._generate_underscored_variants("archive")
|
||||
assert "_archive" in variants
|
||||
|
||||
# Test two segments
|
||||
variants = link_resolver._generate_underscored_variants("archive/articles")
|
||||
assert "_archive/articles" in variants
|
||||
assert "archive/_articles" in variants
|
||||
assert "_archive/_articles" in variants
|
||||
|
||||
# Test three segments
|
||||
variants = link_resolver._generate_underscored_variants("archive/articles/note")
|
||||
assert "_archive/articles/note" in variants
|
||||
assert "archive/_articles/note" in variants
|
||||
assert "archive/articles/_note" in variants
|
||||
assert "_archive/_articles/note" in variants # First two underscored
|
||||
|
||||
Reference in New Issue
Block a user