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
« 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."""
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 from lilbee.data.ingest.discovery import resolve_source_path
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()
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