COD-9 add cross-session search backend (GET /api/search) v1

Bounded federated search over in-memory stores (sessions/cases, run-summary
events, file paths). Zod-validated query (q 1-200 chars, types csv, limit 1-60),
grouped session->event->file with exact-match-first + recency tiebreak, total
cap 60 + per-group cap 25, snippet cap 200, path-safety (relativePath only).
Frontend search box (history panel) deferred to next cycle; resume/history-prompt
text matching deferred to v1.1 (lives in large on-disk files, out of v1 bounded scope).

New: src/search-service.ts (pure core), src/types/search.ts, src/web/routes/search-routes.ts.
Tests: test/search-service.test.ts (14), test/routes/search-routes.test.ts (10).
This commit is contained in:
Aamer Akhter
2026-06-19 12:35:16 -04:00
parent 1255e28f6f
commit 95df96e06a
9 changed files with 925 additions and 0 deletions
+32
View File
@@ -708,3 +708,35 @@ export const OrchestratorStartSchema = z.object({
export const OrchestratorRejectSchema = z.object({
feedback: z.string().min(1).max(10000),
});
// ========== Cross-Session Search (COD-9) ==========
/** Valid federated source kinds for `GET /api/search?types=`. */
export const SEARCH_SOURCE_TYPES = ['session', 'event', 'file'] as const;
/**
* GET /api/search query validation.
*
* Query params arrive as strings: `q` is bounded (1..200 chars), `types` is an
* optional comma-separated allowlisted CSV, and `limit` is an optional coerced
* integer clamped to 1..60. Validation is the first line of defense — a missing
* or oversized `q`, an unknown type, or a non-numeric limit is rejected with 400.
*/
export const SearchQuerySchema = z.object({
q: z.string().trim().min(1, 'Query is required').max(200, 'Query too long (max 200 chars)'),
types: z
.string()
.max(100)
.optional()
.refine(
(v) =>
v === undefined ||
v
.split(',')
.map((t) => t.trim())
.filter(Boolean)
.every((t) => (SEARCH_SOURCE_TYPES as readonly string[]).includes(t)),
{ message: 'Invalid types value' }
),
limit: z.coerce.number().int().min(1).max(60).optional(),
});