# INTERFACE VERSION: 1 """SearchBackend — the frozen search contract (SPEC §5.2). Both backends (``pg.py`` for M0-M1, ``meili.py`` for M2) index the same :class:`SearchDoc` and honour invariant I-7 (every query carries ``user_id``). """ from __future__ import annotations from dataclasses import dataclass, field from typing import Protocol from uuid import UUID @dataclass(frozen=True) class SearchQuery: text: str | None # free-text remainder after operator extraction filters: dict[str, object] # parsed operators, keys per §8.2 user_id: UUID # ALWAYS set (invariant I-7) @dataclass(frozen=True) class SearchHit: message_id: UUID thread_id: UUID score: float highlights: dict[str, str] # field -> HTML-escaped snippet with @dataclass(frozen=True) class SearchDoc: """The document indexed by every backend (SPEC §5.2).""" message_id: UUID thread_id: UUID user_id: UUID account_id: UUID mailbox_special_use: str | None from_: list[dict[str, str]] # name+email to: list[dict[str, str]] cc: list[dict[str, str]] subject: str body_text: str attachment_names: list[str] = field(default_factory=list) has_attachments: bool = False is_unread: bool = False is_starred: bool = False date: int = 0 # unix seconds size_bytes: int = 0 class SearchBackend(Protocol): async def index(self, doc: SearchDoc) -> None: ... async def delete(self, message_id: UUID) -> None: ... async def delete_user(self, user_id: UUID) -> None: ... async def query( self, q: SearchQuery, *, limit: int, cursor: str | None ) -> tuple[list[SearchHit], str | None]: ...