// Search layer — 3-tier progressive disclosure. import { getObservationsByIds, getRecentObservations, getRecentSummaries, getTimelineAround, searchObservationsFts, searchObservationsRecency, } from "../store/db.js"; import { DEFAULT_SEARCH_LIMIT, DEFAULT_TIMELINE_WINDOW_MS } from "../config.js"; import type { SearchHit, FullObservation, Summary } from "../types.js"; export interface SearchResult { hits: SearchHit[]; query: string; totalCandidates: number; } /** * Hybrid search: union of FTS keyword matches + recency fallback. * De-duplicates by id, score = max(ftsScore, recencyScore). */ export function search( query: string, project: string | null, limit: number = DEFAULT_SEARCH_LIMIT, ): SearchResult { const ftsHits = query.trim().length > 0 ? searchObservationsFts(query, project, limit * 2) : []; const recencyHits = searchObservationsRecency(project, limit * 2); const byId = new Map(); for (const h of ftsHits) { byId.set(h.id, h); } for (const h of recencyHits) { const existing = byId.get(h.id); if (!existing) { byId.set(h.id, h); } else { byId.set(h.id, { ...existing, score: Math.max(existing.score, h.score), matchedVia: "both", }); } } const merged = Array.from(byId.values()).sort((a, b) => b.score - a.score); const total = merged.length; const hits = merged.slice(0, limit); return { hits, query, totalCandidates: total }; } export function timeline(id: number, windowMs: number = DEFAULT_TIMELINE_WINDOW_MS): FullObservation[] { return getTimelineAround(id, windowMs); } export function getByIds(ids: number[]): FullObservation[] { // Preserve input order. const byId = new Map(getObservationsByIds(ids).map((o) => [o.id, o])); return ids.map((id) => byId.get(id)).filter((o): o is FullObservation => o !== undefined); } export interface ContextSnapshot { recentObservations: FullObservation[]; recentSummaries: Summary[]; } /** * Pull a "recent context" snapshot for session-start injection. * Heuristic: recent observations in current project (if any) + recent summary. */ export function getRecentContext(project: string, limit: number = DEFAULT_SEARCH_LIMIT): ContextSnapshot { return { recentObservations: getRecentObservations(project, limit), recentSummaries: getRecentSummaries(project, 1), }; }