/** * Rota /api/sessions — lista e detalhe de sessões Claude Code. * Inclui /patterns e /stats (contrato C4-análise, Pipeline QT F4.1) — o 05 * (stack-evaluator) consome só via API, nunca o SQLite directo. * Validação Zod em query params; detalhe carrega eventos via parseSessionFile. * @author Descomplicar® | Projecto Observabilidade (Espelho) */ import { Router } from 'express' import { z } from 'zod' import type { SessionsDb } from '../services/sessions/db.js' const ListQuerySchema = z.object({ days: z.coerce.number().int().min(1).max(3650).optional(), project: z.string().min(1).max(200).optional(), tool: z.string().min(1).max(100).optional(), skill: z.string().min(1).max(200).optional(), q: z.string().max(500).optional(), limit: z.coerce.number().int().min(1).max(200).default(50), offset: z.coerce.number().int().min(0).default(0), }) const IdParamSchema = z.object({ id: z.string().min(1).max(200) }) const PatternsQuerySchema = z.object({ week: z.string().regex(/^\d{4}-W\d{2}$/, 'Formato esperado: YYYY-Www').optional(), severity: z.enum(['info', 'warning', 'action']).optional(), key: z.string().min(1).max(200).optional(), days: z.coerce.number().int().min(1).max(3650).optional(), limit: z.coerce.number().int().min(1).max(200).default(50), offset: z.coerce.number().int().min(0).default(0), }) const StatsQuerySchema = z.object({ days: z.coerce.number().int().min(1).max(365).default(30), }) /** * Router de análise (contrato C4): GET /patterns e GET /stats. * Montado em /api/sessions (contrato C4) e em /api (alias curto F4.1). */ export function createSessionsAnalysisRouter(db: SessionsDb): Router { const router = Router() router.get('/patterns', (req, res) => { const parsed = PatternsQuerySchema.safeParse(req.query) if (!parsed.success) { return res.status(400).json({ error: 'Invalid query', details: parsed.error.format() }) } const { week, severity, key, days, limit, offset } = parsed.data const filters = { week, severity, pattern_key: key, sinceIso: days ? new Date(Date.now() - days * 86400_000).toISOString() : undefined, limit, offset, } const items = db.listPatterns(filters) const total = db.countPatterns(filters) return res.json({ total, week: week ?? null, items }) }) router.get('/stats', (req, res) => { const parsed = StatsQuerySchema.safeParse(req.query) if (!parsed.success) { return res.status(400).json({ error: 'Invalid query', details: parsed.error.format() }) } return res.json(db.getStats(parsed.data.days)) }) return router } export function createSessionsRouter(db: SessionsDb): Router { const router = Router() // C4-análise: registado ANTES de /:id para não ser apanhado pelo param route router.use(createSessionsAnalysisRouter(db)) router.get('/', (req, res) => { const parsed = ListQuerySchema.safeParse(req.query) if (!parsed.success) { return res.status(400).json({ error: 'Invalid query', details: parsed.error.format() }) } const filters = parsed.data const items = db.listSessions(filters) const total = db.countSessions(filters) return res.json({ total, items }) }) router.get('/:id', (req, res) => { const parsed = IdParamSchema.safeParse(req.params) if (!parsed.success) { return res.status(400).json({ error: 'Invalid id' }) } const session = db.getSession(parsed.data.id) if (!session) return res.status(404).json({ error: 'Session not found' }) const events = db.getSessionEvents(parsed.data.id) if (events.length === 0 && session.source === 'claude') { return res.status(410).json({ error: 'Session events not yet indexed', session_id: parsed.data.id, }) } return res.json({ meta: session, events }) }) return router }