Files
DashDescomplicar/api/routes/sessions.ts
T
ealmeida 810401e4e7 feat(pipeline): F4.1 — rotas GET /patterns e GET /stats (contrato C4-análise) #2415
- routes/sessions.ts: createSessionsAnalysisRouter com GET /patterns (filtros
  week/severity/key/days, paginado, Zod) e GET /stats (agregados: sessões
  totais e por dia, by_source, by_outcome, tool_top, skill_top,
  patterns_by_type, gate_blocks, avg_duration_sec, last_indexed_at,
  latest_pattern_week); montado em /api/sessions (C4) e /api (alias F4.1)
- services/sessions/db.ts: listPatterns/countPatterns/getStats +
  rowToPattern/buildPatternsWhere; gate_blocks devolve null enquanto a
  tabela gate_events não existir (F1 arco C3-gates)
- tests/sessions-analysis-route.test.ts: 15 testes (filtros, paginação,
  validação, gate_blocks condicional, não-colisão com /:id)
- inclui camada session_events pendente de v2.7.3 (db.ts/indexer.ts —
  storeSessionEvents no indexAll; rota /:id lê da BD) já em execução, para
  a árvore commitada compilar coerente

Validado contra BD real (11459 sessões, 160 padrões): curl 200 com JSON
não-vazio em /api/patterns, /api/stats, /api/sessions/patterns?week=2026-W26,
/api/sessions/stats?days=7.
2026-07-03 06:01:22 +01:00

111 lines
3.8 KiB
TypeScript

/**
* 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
}