Coverage for src/lilbee/app/search.py: 100%

24 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-14 11:46 +0000

1"""Search-result post-processing shared by CLI, HTTP, and MCP.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING 

6 

7from lilbee.core.config import cfg 

8 

9if TYPE_CHECKING: 

10 from lilbee.data.store import SearchChunk 

11 

12 

13def resolve_vault_path(source_filename: str) -> str | None: 

14 """Return *source_filename* as a vault-relative path, or None if unresolvable. 

15 

16 Resolves symlinks on both sides and rejects ``..`` escapes from 

17 ``documents_dir``. 

18 """ 

19 if cfg.vault_base is None: 

20 return None 

21 from lilbee.data.ingest.discovery import resolve_source_path 

22 

23 try: 

24 vault_base = cfg.vault_base.resolve() 

25 documents_dir = cfg.documents_dir.resolve() 

26 source_path = resolve_source_path(source_filename).resolve() 

27 source_path.relative_to(documents_dir) 

28 relative_docs_dir = documents_dir.relative_to(vault_base) 

29 except (OSError, ValueError): 

30 return None 

31 if not source_path.is_file(): 

32 return None 

33 return (relative_docs_dir / source_path.relative_to(documents_dir)).as_posix() 

34 

35 

36def clean_result(result: SearchChunk) -> dict: 

37 """Return SearchChunk as a JSON dict, stamping vault_path when resolvable.""" 

38 payload = result.model_dump(exclude={"vector"}, exclude_none=True) 

39 vault_path = resolve_vault_path(result.source) 

40 if vault_path is not None: 

41 payload["vault_path"] = vault_path 

42 return payload