/** * Reflection Store — 업무 turn 회고 기록 + Failure Pattern 집계. * * Self-Evolving OS 마스터 플랜 Phase 1 / Track 2-4 (Reflection Engine v1) + * Phase 3 / Track 3-6 (Failure Pattern DB v1 시드). 신뢰 조건 T5 * "같은 실수를 반복하지 않는다" 의 데이터 기반. * * v1 은 결정론적 신호만 기록 (LLM 회고 질문은 후속 증분): * 업무 turn 종료 → {업무유형, 확신도, 누락 요소, 에스컬레이션 여부, Critic 이슈 수} * 를 /.astra/growth/reflections.jsonl 에 append. * * 이 파일이 쌓이면: * - summarizeFailurePatterns() → "회의록·기한 누락 N회" 류 반복 실수 집계 * - formatGrowthReport() → 기간별 확신도/누락률 추이 = *성장세 그래프의 원천* */ import * as fs from 'fs'; import * as path from 'path'; export const REFLECTIONS_REL_PATH = path.join('.astra', 'growth', 'reflections.jsonl'); export interface ReflectionRecord { /** ISO timestamp. */ ts: string; taskId: string | null; taskLabel: string | null; confidenceScore: number; confidenceBand: string; /** 커버리지 누락 요소 label 목록. */ missing: string[]; escalated: boolean; /** Critic 검수가 돌았으면 발견 이슈 수, 안 돌았으면 null. */ criticIssues: number | null; /** 요청 미리보기 (디버그·회고용, 120자). */ promptPreview: string; // ── Decision Journal v1 (Track 3-7) — "왜 이 확신도/판단이었나" 역추적 필드 ── /** 확신도 기여 요인 label 목록 (confidenceEngine factors). */ factors?: string[]; /** 답변에 쓰인 상위 출처 title (citation/selfCheckSources 기준). */ usedSources?: string[]; // ── Gap Detector v1 (Track 3-2) — Need Engine 입력 신호 ── /** 검색 그라운딩: 청크 수·최고 score. */ retrieval?: { chunkCount: number; topScore: number }; /** 검색 근거 없이/약하게 수행한 업무 turn (지식 갭 신호). */ weakGrounding?: boolean; /** 갭 심각도 (none/low/medium/high). */ gapSeverity?: string; } /** 회고 1건 append — 실패해도 throw 하지 않음 (회고가 turn 을 막으면 안 됨). */ export function appendReflection(brainPath: string, record: ReflectionRecord): boolean { try { if (!brainPath) return false; const file = path.join(brainPath, REFLECTIONS_REL_PATH); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.appendFileSync(file, JSON.stringify(record) + '\n', 'utf8'); return true; } catch { return false; } } /** 회고 로드 — 깨진 줄은 무시. limit 은 *최근* N건. */ export function loadReflections(brainPath: string, limit?: number): ReflectionRecord[] { try { const file = path.join(brainPath, REFLECTIONS_REL_PATH); if (!fs.existsSync(file)) return []; const lines = fs.readFileSync(file, 'utf8').split('\n').filter((l) => l.trim()); const records: ReflectionRecord[] = []; for (const line of lines) { try { const obj = JSON.parse(line); if (obj && typeof obj.ts === 'string') records.push(obj as ReflectionRecord); } catch { /* skip broken line */ } } return limit && limit > 0 ? records.slice(-limit) : records; } catch { return []; } } export interface FailurePattern { taskId: string; taskLabel: string; element: string; count: number; } /** * Failure Pattern 집계 — (업무유형 × 누락 요소) 별 반복 횟수, 많은 순. * "시장규모 누락 27회" 류의 반복 실수를 수치로 노출 (설계서 12장). */ export function summarizeFailurePatterns(records: ReflectionRecord[]): FailurePattern[] { const counts = new Map(); for (const r of records) { if (!r.taskId) continue; for (const el of r.missing || []) { const key = `${r.taskId}::${el}`; const cur = counts.get(key); if (cur) cur.count++; else counts.set(key, { taskId: r.taskId, taskLabel: r.taskLabel || r.taskId, element: el, count: 1 }); } } return Array.from(counts.values()).sort((a, b) => b.count - a.count); } /** * 반복 실수 경고 — 같은 (업무 × 요소) 누락이 threshold 회 이상이면 해당 요소를 * 시스템 프롬프트 강조 대상으로 반환. Requirement Graph 블록이 이걸 받아 * "특히 자주 누락되는 요소" 로 표시 (T5 루프의 첫 닫힘). */ export function recurrentMisses(records: ReflectionRecord[], taskId: string, threshold = 3): string[] { return summarizeFailurePatterns(records) .filter((p) => p.taskId === taskId && p.count >= threshold) .map((p) => p.element); } /** 기간(주) 단위 성장 리포트 — 확신도 평균·누락률 추이. */ export function formatGrowthReport(records: ReflectionRecord[]): string { if (records.length === 0) return '# 성장 리포트\n\n기록 없음 — 업무 turn 이 쌓이면 추이가 표시됩니다.\n'; // 주 단위 버킷 (ISO week 근사 — ts 앞 10자의 날짜 기준 7일 묶음). const byWeek = new Map(); for (const r of records) { const d = new Date(r.ts); if (isNaN(d.getTime())) continue; const weekStart = new Date(d); weekStart.setDate(d.getDate() - d.getDay()); // 일요일 기준 const key = weekStart.toISOString().slice(0, 10); const arr = byWeek.get(key) || []; arr.push(r); byWeek.set(key, arr); } const lines: string[] = []; lines.push('# ASTRA 성장 리포트 (Reflection 기반)'); lines.push(''); lines.push(`총 업무 turn: ${records.length}`); lines.push(''); lines.push('| 주 (시작일) | 업무 수 | 평균 확신도 | 요소 누락률 | 에스컬레이션 |'); lines.push('|---|---|---|---|---|'); const weeks = Array.from(byWeek.keys()).sort(); for (const w of weeks) { const rs = byWeek.get(w)!; const avgConf = rs.reduce((s, r) => s + (r.confidenceScore || 0), 0) / rs.length; const missRate = rs.filter((r) => (r.missing || []).length > 0).length / rs.length; const escCount = rs.filter((r) => r.escalated).length; lines.push(`| ${w} | ${rs.length} | ${avgConf.toFixed(0)} | ${(missRate * 100).toFixed(0)}% | ${escCount} |`); } lines.push(''); lines.push('## 반복 실수 Top (Failure Patterns)'); const patterns = summarizeFailurePatterns(records).slice(0, 10); if (patterns.length === 0) { lines.push('- 없음'); } else { for (const p of patterns) lines.push(`- ${p.taskLabel} · **${p.element}** 누락 ${p.count}회`); } lines.push(''); return lines.join('\n'); }