feat(observabilidade): tabela patterns + 6 detectores SQL
Adiciona tabela 'patterns' à BD sessions (UNIQUE por week_iso+pattern_key) e helpers upsertPattern/getPatternsByWeek/getConsecutiveWeeks no SessionsDb. Módulo patterns.ts implementa 6 detectores heurísticos para deteccão semanal: 1. skills_with_high_error_rate (ratio > 0.2, severity warning|action) 2. tools_low_efficiency (tool_calls/event_count médio > 0.5) 3. skill_tool_pairs (top 5 co-ocorrências) 4. duration_outliers (sessões > p95 com outcome != completed) 5. abandoned_sessions (event_count<3 AND outcome=unknown, >=5) 6. growing_complexity (avg tool_calls actual > anterior*1.3) 5 testes cobrem detector de erro, abandonadas, consecutive_weeks, idempotência do upsert e toPatternRecord. Refs Fase 6A · Desk #2059 · Project #65 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mkdtempSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { openSessionsDb, type SessionsDb, type PatternRecord } from '../services/sessions/db.js'
|
||||
import { detectPatterns, weekRange, toPatternRecord } from '../services/sessions/patterns.js'
|
||||
import type { SessionMeta } from '../types/session.js'
|
||||
|
||||
function meta(overrides: Partial<SessionMeta>): SessionMeta {
|
||||
return {
|
||||
session_id: 's-' + Math.random().toString(36).slice(2, 10),
|
||||
project_path: '/tmp/project',
|
||||
project_slug: 'project',
|
||||
jsonl_path: '/tmp/' + Math.random().toString(36).slice(2) + '.jsonl',
|
||||
started_at: '2026-04-20T10:00:00Z', // segunda de 2026-W17
|
||||
ended_at: '2026-04-20T10:30:00Z',
|
||||
duration_sec: 1800,
|
||||
event_count: 50,
|
||||
user_messages: 5,
|
||||
assistant_msgs: 10,
|
||||
tool_calls: 20,
|
||||
first_prompt: 'olá',
|
||||
tools_used: ['Bash'],
|
||||
skills_invoked: [],
|
||||
outcome: 'completed',
|
||||
permission_mode: 'default',
|
||||
file_size: 10000,
|
||||
indexed_at: '2026-04-20T10:31:00Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('patterns detector', () => {
|
||||
let db: SessionsDb
|
||||
beforeEach(() => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'obs-pat-'))
|
||||
db = openSessionsDb(join(dir, 'sessions.db'))
|
||||
})
|
||||
|
||||
it('detecta skill com taxa elevada de erro (action)', () => {
|
||||
// 3 sessões skill X: 2 error, 1 completed → ratio 0.67 → severity=action
|
||||
db.upsertSession(meta({ session_id: 'a', skills_invoked: ['skillX'], outcome: 'error' }))
|
||||
db.upsertSession(meta({ session_id: 'b', skills_invoked: ['skillX'], outcome: 'interrupted' }))
|
||||
db.upsertSession(meta({ session_id: 'c', skills_invoked: ['skillX'], outcome: 'completed' }))
|
||||
const { start, end } = weekRange(new Date('2026-04-22T00:00:00Z'))
|
||||
const patterns = detectPatterns(db, start, end)
|
||||
const errorRate = patterns.find((p) => p.pattern_key === 'skill_error_rate:skillX')
|
||||
expect(errorRate).toBeDefined()
|
||||
expect(errorRate!.severity).toBe('action')
|
||||
expect(errorRate!.affected_count).toBe(2)
|
||||
})
|
||||
|
||||
it('detecta sessões abandonadas', () => {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
db.upsertSession(meta({ session_id: `ab-${i}`, event_count: 1, outcome: 'unknown' }))
|
||||
}
|
||||
const { start, end } = weekRange(new Date('2026-04-22T00:00:00Z'))
|
||||
const patterns = detectPatterns(db, start, end)
|
||||
expect(patterns.some((p) => p.pattern_key === 'abandoned_sessions')).toBe(true)
|
||||
})
|
||||
|
||||
it('getConsecutiveWeeks devolve 3 após upserts em semanas sucessivas', () => {
|
||||
const key = 'skill_error_rate:Y'
|
||||
const weeks = ['2026-W15', '2026-W16', '2026-W17']
|
||||
for (const w of weeks) {
|
||||
db.upsertPattern({
|
||||
detected_at: new Date().toISOString(),
|
||||
week_iso: w,
|
||||
pattern_key: key,
|
||||
title: 't',
|
||||
description: 'd',
|
||||
severity: 'warning',
|
||||
metric_value: 0.5,
|
||||
sample_session_ids: ['x'],
|
||||
affected_count: 1,
|
||||
consecutive_weeks: 1,
|
||||
})
|
||||
}
|
||||
expect(db.getConsecutiveWeeks(key, '2026-W17')).toBe(3)
|
||||
expect(db.getConsecutiveWeeks(key, '2026-W16')).toBe(2)
|
||||
})
|
||||
|
||||
it('upsertPattern é idempotente por (week_iso, pattern_key)', () => {
|
||||
const base: PatternRecord = {
|
||||
detected_at: '2026-04-20T00:00:00Z',
|
||||
week_iso: '2026-W17',
|
||||
pattern_key: 'test',
|
||||
title: 'v1',
|
||||
description: 'd',
|
||||
severity: 'info',
|
||||
metric_value: 1,
|
||||
sample_session_ids: ['a'],
|
||||
affected_count: 1,
|
||||
consecutive_weeks: 1,
|
||||
}
|
||||
db.upsertPattern(base)
|
||||
db.upsertPattern({ ...base, title: 'v2', affected_count: 5, consecutive_weeks: 2 })
|
||||
const rows = db.getPatternsByWeek('2026-W17')
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0].title).toBe('v2')
|
||||
expect(rows[0].affected_count).toBe(5)
|
||||
expect(rows[0].consecutive_weeks).toBe(2)
|
||||
})
|
||||
|
||||
it('toPatternRecord propaga week_iso e consecutive_weeks', () => {
|
||||
const rec = toPatternRecord(
|
||||
{
|
||||
pattern_key: 'k',
|
||||
title: 't',
|
||||
description: 'd',
|
||||
severity: 'warning',
|
||||
metric_value: 0.42,
|
||||
sample_session_ids: ['a', 'b'],
|
||||
affected_count: 2,
|
||||
},
|
||||
'2026-W17',
|
||||
3,
|
||||
)
|
||||
expect(rec.week_iso).toBe('2026-W17')
|
||||
expect(rec.consecutive_weeks).toBe(3)
|
||||
expect(rec.severity).toBe('warning')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user