- 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.
111 lines
3.8 KiB
TypeScript
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
|
|
}
|