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

23 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-05-15 20:55 +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 try: 

22 vault_base = cfg.vault_base.resolve() 

23 documents_dir = cfg.documents_dir.resolve() 

24 source_path = (cfg.documents_dir / source_filename).resolve() 

25 source_path.relative_to(documents_dir) 

26 relative_docs_dir = documents_dir.relative_to(vault_base) 

27 except (OSError, ValueError): 

28 return None 

29 if not source_path.is_file(): 

30 return None 

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

32 

33 

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

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

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

37 vault_path = resolve_vault_path(result.source) 

38 if vault_path is not None: 

39 payload["vault_path"] = vault_path 

40 return payload