/** 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');