/** One indexed message document. `at` is epoch-ms so Meilisearch can sort recency-first. */ export interface SearchDoc { id: string; // interactionId scopeId: string; threadId: string; text: string; subject: string | null; source: string | null; // opaque app source tag (e.g. crm-messenger / crm-mail) — drives the surface actorId: string | null; at: number; } /** A search hit with a highlighted snippet. */ export interface SearchHit { id: string; threadId: string; text: string; subject: string | null; source: string | null; at: number; /** The match with `` around the query terms (from the engine's highlighter). */ snippet: string; } /** * The message-search seam. IIOS owns egress-style search: a message index + a permission-scoped * query. The concrete engine (Meilisearch) sits behind this; an unconfigured deployment binds a * no-op so search degrades to empty rather than erroring. */ export interface MessageSearchPort { /** True only when a real engine is configured (else index/search are inert). */ ready(): boolean; index(docs: SearchDoc[]): Promise; remove(ids: string[]): Promise; /** Search WITHIN the given scope and thread set only (the permission fence — enforced here). */ search(scopeId: string, threadIds: string[], query: string, limit: number): Promise; } export const MESSAGE_SEARCH_PORT = Symbol('MESSAGE_SEARCH_PORT');