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
« 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."""
3from __future__ import annotations
5from typing import TYPE_CHECKING
7from lilbee.core.config import cfg
9if TYPE_CHECKING:
10 from lilbee.data.store import SearchChunk
13def resolve_vault_path(source_filename: str) -> str | None:
14 """Return *source_filename* as a vault-relative path, or None if unresolvable.
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()
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