From 86770b1570b2396d9cd91624a66eb17f95f0d65b Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Thu, 23 Apr 2026 02:33:57 +0100 Subject: [PATCH] feat(observabilidade): adiciona flag --backfill ao sessions-patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Itera semanas ISO desde a primeira sessão até (excluindo) a semana corrente, detectando padrões e fazendo upsert com consecutive_weeks acumulado. Nunca publica comentários nem abre tickets — apenas povoa a tabela patterns com histórico. Output JSON por semana e sumário final. Permite popular retroactivamente a tabela patterns após nova instalação ou reset, dando base imediata ao detector de padrões persistentes. --- api/scripts/sessions-patterns.ts | 111 ++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/api/scripts/sessions-patterns.ts b/api/scripts/sessions-patterns.ts index 34150f9..6f34cf9 100644 --- a/api/scripts/sessions-patterns.ts +++ b/api/scripts/sessions-patterns.ts @@ -4,6 +4,12 @@ * * Uso: * sessions-patterns.ts [--week YYYY-Www] [--publish] [--force] + * sessions-patterns.ts --backfill + * + * Backfill: + * Itera todas as semanas ISO desde a primeira sessão na BD até (excluindo) + * a semana corrente, detecta padrões e faz upsert. Nunca publica nem abre + * tickets. Output: linha JSON por semana + sumário total no fim. * * Fluxo: * 1. Resolver intervalo da semana (segunda 00:00 UTC → domingo 23:59 UTC) @@ -18,11 +24,12 @@ * MCP_GATEWAY_TOKEN Bearer token do gateway MCP * MCP_GATEWAY_URL URL do MCP desk-crm (default https://gateway.descomplicar.pt/v1/desk-crm/mcp) */ -import { openSessionsDb, type PatternRecord } from '../services/sessions/db.js' +import { openSessionsDb, type PatternRecord, type SessionsDb } from '../services/sessions/db.js' import { DEFAULT_DB_PATH } from '../services/sessions/indexer.js' import { detectPatterns, toPatternRecord, + weekIso, weekRange, type Pattern, } from '../services/sessions/patterns.js' @@ -31,6 +38,7 @@ interface Args { week?: string publish: boolean force: boolean + backfill: boolean } interface MCPToolCallResult { @@ -46,15 +54,104 @@ const OBSERVABILIDADE_DISCUSSION_ID = 32 const OBSERVABILIDADE_DEPARTMENT_ID = 1 function parseArgs(argv: string[]): Args { - const a: Args = { publish: false, force: false } + const a: Args = { publish: false, force: false, backfill: false } for (let i = 0; i < argv.length; i++) { if (argv[i] === '--week') a.week = argv[++i] else if (argv[i] === '--publish') a.publish = true else if (argv[i] === '--force') a.force = true + else if (argv[i] === '--backfill') a.backfill = true } return a } +/** Detecta e persiste padrões para uma única semana (sem publicar). */ +function processWeek(db: SessionsDb, monday: Date): { + week_iso: string + detected: number + by_severity: { action: number; warning: number; info: number } +} { + const range = weekRange(monday) + const weekIsoStr = range.iso + const detected: Pattern[] = detectPatterns(db, range.start, range.end) + const records: PatternRecord[] = [] + for (const p of detected) { + const tmpRec = toPatternRecord(p, weekIsoStr, 1) + db.upsertPattern(tmpRec) + const consecutive = db.getConsecutiveWeeks(p.pattern_key, weekIsoStr) + const rec = toPatternRecord(p, weekIsoStr, consecutive) + db.upsertPattern(rec) + records.push(rec) + } + return { + week_iso: weekIsoStr, + detected: records.length, + by_severity: { + action: records.filter((r) => r.severity === 'action').length, + warning: records.filter((r) => r.severity === 'warning').length, + info: records.filter((r) => r.severity === 'info').length, + }, + } +} + +/** Executa backfill desde a primeira sessão até (excl.) semana corrente. */ +function runBackfill(db: SessionsDb, dbPath: string): void { + const raw = db.rawDb() + const row = raw.prepare('SELECT MIN(started_at) as min_started FROM sessions').get() as + | { min_started: string | null } + | undefined + if (!row || !row.min_started) { + console.error('[patterns] backfill: BD sem sessões. Nada a fazer.') + console.log(JSON.stringify({ backfill: true, weeks_processed: 0, total_detected: 0 })) + return + } + const firstStart = new Date(row.min_started) + const firstRange = weekRange(firstStart) + const currentRange = weekRange(new Date()) + const currentIso = currentRange.iso + + console.error( + `[patterns] backfill: primeira sessão ${row.min_started}, semana inicial ${firstRange.iso}, ` + + `semana corrente ${currentIso} (excluída) db=${dbPath}`, + ) + + let cursor = new Date(firstRange.start) + let weeksProcessed = 0 + let totalDetected = 0 + const bySev = { action: 0, warning: 0, info: 0 } + const perWeek: Array<{ week_iso: string; detected: number }> = [] + + // Iteração semanal: cursor é sempre segunda 00:00 UTC + while (weekIso(cursor) !== currentIso) { + const summary = processWeek(db, cursor) + console.log(JSON.stringify({ backfill: true, ...summary })) + weeksProcessed++ + totalDetected += summary.detected + bySev.action += summary.by_severity.action + bySev.warning += summary.by_severity.warning + bySev.info += summary.by_severity.info + perWeek.push({ week_iso: summary.week_iso, detected: summary.detected }) + // Avançar 7 dias + cursor = new Date(cursor) + cursor.setUTCDate(cursor.getUTCDate() + 7) + // Safety: evitar loop infinito em caso de bug + if (weeksProcessed > 520) { + console.error('[patterns] backfill: safety break após 520 semanas') + break + } + } + + console.log( + JSON.stringify({ + backfill_summary: true, + weeks_processed: weeksProcessed, + total_detected: totalDetected, + by_severity: bySev, + first_week: perWeek[0]?.week_iso ?? null, + last_week: perWeek[perWeek.length - 1]?.week_iso ?? null, + }), + ) +} + /** Converte YYYY-Www em intervalo {start,end} UTC. */ function weekIsoToRange(weekIsoStr: string): { start: Date; end: Date; iso: string } { const m = weekIsoStr.match(/^(\d{4})-W(\d{2})$/) @@ -198,6 +295,16 @@ async function main(): Promise { const dbPath = process.env.OBSERVABILIDADE_DB ?? DEFAULT_DB_PATH const db = openSessionsDb(dbPath) + if (args.backfill) { + if (args.publish) { + console.error('[patterns] --backfill é incompatível com --publish. Aborta.') + process.exit(1) + } + runBackfill(db, dbPath) + db.close() + return + } + const range = args.week ? weekIsoToRange(args.week) : weekRange(new Date()) const weekIso = args.week ?? range.iso console.error(`[patterns] semana ${weekIso} range=${range.start.toISOString()}..${range.end.toISOString()} db=${dbPath} publish=${args.publish}`)