/** * Need Engine — 학습 필요성 산출 (설계서 7.6) + Knowledge Inventory v1 (7.3). * * Self-Evolving OS 마스터 플랜 Phase 3 / Track 3-3 + 3-1. Reflection 기록을 * 집계해 "무엇을 먼저 배워야 하는가" 를 점수로 산출한다 — 성장 루프의 두뇌. * * Need Score (설계서 공식, 0~100): * 정보 부족도 × 30% + 실패율 × 25% + 업무 빈도 × 20% + 확신도 부족 × 15% + 사용자 피드백 × 10% * * v1 신호 매핑 (전부 Reflection 에서 결정론적으로): * - 정보 부족도: weakGrounding 비율 (검색 근거 없이 수행한 turn 비중) * - 실패율: 필수 요소 누락이 있었던 turn 비율 * - 업무 빈도: 해당 업무 turn 수 / 전체 업무 turn 수 * - 확신도 부족: (100 − 평균 확신도) / 100 * - 사용자 피드백: v1 미수집 → 0 (필드는 유지, 후속 증분에서 연결) * * 출력은 Learning Queue 의 입력이 된다. 학습 실행은 승인 후 (Permission Based Learning). */ import type { ReflectionRecord } from './reflectionStore'; export interface NeedItem { /** 업무 유형 ID (학습 주제 단위 v1 — 후속: 요소/토픽 단위 세분화). */ taskId: string; taskLabel: string; /** 0~100. */ score: number; /** 가중치별 기여 내역 (사람이 읽는 근거). */ breakdown: { infoLack: number; // 0~1 failRate: number; // 0~1 frequency: number; // 0~1 confidenceLack: number; // 0~1 feedback: number; // 0~1 (v1 = 0) }; /** 집계 표본 수. */ sampleCount: number; /** 자주 누락된 요소 Top 3 — 학습 주제 구체화용. */ topMisses: string[]; reason: string; } export const NEED_WEIGHTS = { infoLack: 0.30, failRate: 0.25, frequency: 0.20, confidenceLack: 0.15, feedback: 0.10, } as const; export function computeNeeds(records: ReflectionRecord[]): NeedItem[] { const taskRecords = records.filter((r) => r.taskId); if (taskRecords.length === 0) return []; const byTask = new Map(); for (const r of taskRecords) { const arr = byTask.get(r.taskId!) || []; arr.push(r); byTask.set(r.taskId!, arr); } const needs: NeedItem[] = []; for (const [taskId, rs] of byTask) { const infoLack = rs.filter((r) => r.weakGrounding === true).length / rs.length; const failRate = rs.filter((r) => (r.missing || []).length > 0).length / rs.length; const frequency = rs.length / taskRecords.length; const avgConf = rs.reduce((s, r) => s + (r.confidenceScore || 0), 0) / rs.length; const confidenceLack = Math.max(0, Math.min(1, (100 - avgConf) / 100)); const feedback = 0; // v1 미수집 const score = Math.round(100 * ( infoLack * NEED_WEIGHTS.infoLack + failRate * NEED_WEIGHTS.failRate + frequency * NEED_WEIGHTS.frequency + confidenceLack * NEED_WEIGHTS.confidenceLack + feedback * NEED_WEIGHTS.feedback )); // 자주 누락된 요소 Top 3. const missCounts = new Map(); for (const r of rs) for (const m of r.missing || []) missCounts.set(m, (missCounts.get(m) || 0) + 1); const topMisses = Array.from(missCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([m]) => m); const reasonParts: string[] = []; if (infoLack > 0.3) reasonParts.push(`근거 없는 수행 ${(infoLack * 100).toFixed(0)}%`); if (failRate > 0.3) reasonParts.push(`요소 누락률 ${(failRate * 100).toFixed(0)}%`); if (confidenceLack > 0.3) reasonParts.push(`평균 확신도 ${avgConf.toFixed(0)}`); if (topMisses.length > 0) reasonParts.push(`자주 누락: ${topMisses.join(', ')}`); needs.push({ taskId, taskLabel: rs[0].taskLabel || taskId, score, breakdown: { infoLack, failRate, frequency, confidenceLack, feedback }, sampleCount: rs.length, topMisses, reason: reasonParts.join(' · ') || '특이 신호 없음 (빈도 기반)', }); } return needs.sort((a, b) => b.score - a.score); } /** * Knowledge Inventory v1 (Track 3-1) — 업무 유형별 지식 보유 상태. * 보유/부족/없음 3등급 (설계서 7.3) 을 그라운딩 신호로 판정. */ export interface InventoryItem { taskId: string; taskLabel: string; /** 'sufficient' | 'partial' | 'missing' */ status: 'sufficient' | 'partial' | 'missing'; avgChunkCount: number; avgTopScore: number; sampleCount: number; } export function knowledgeInventory(records: ReflectionRecord[]): InventoryItem[] { const withRetrieval = records.filter((r) => r.taskId && r.retrieval); const byTask = new Map(); for (const r of withRetrieval) { const arr = byTask.get(r.taskId!) || []; arr.push(r); byTask.set(r.taskId!, arr); } const items: InventoryItem[] = []; for (const [taskId, rs] of byTask) { const avgChunkCount = rs.reduce((s, r) => s + (r.retrieval!.chunkCount || 0), 0) / rs.length; const avgTopScore = rs.reduce((s, r) => s + (r.retrieval!.topScore || 0), 0) / rs.length; const status: InventoryItem['status'] = avgChunkCount >= 3 && avgTopScore >= 0.5 ? 'sufficient' : avgChunkCount >= 1 ? 'partial' : 'missing'; items.push({ taskId, taskLabel: rs[0].taskLabel || taskId, status, avgChunkCount, avgTopScore, sampleCount: rs.length }); } return items.sort((a, b) => a.avgTopScore - b.avgTopScore); } /** * Knowledge Debt (Track 4-4) — 부족 지식이 실제로 막은 업무 집계 (설계서 예: * "GA4 — Blocked Tasks 17, Impact 9"). v1 단위는 업무 유형: 근거 없이/약하게 * 수행된 turn 수 = blocked, 그 turn 들의 갭 심각도 평균 = impact (0~10). */ export interface DebtItem { taskId: string; taskLabel: string; /** 지식 부족 상태로 수행된 업무 turn 수. */ blockedTurns: number; /** 평균 갭 심각도 0~10. */ impact: number; /** blocked × impact — 정렬 키. */ debtScore: number; } const SEVERITY_SCORE: Record = { none: 0, low: 3, medium: 6, high: 10 }; export function computeKnowledgeDebt(records: ReflectionRecord[]): DebtItem[] { const blocked = records.filter((r) => r.taskId && r.weakGrounding === true); const byTask = new Map(); for (const r of blocked) { const arr = byTask.get(r.taskId!) || []; arr.push(r); byTask.set(r.taskId!, arr); } const items: DebtItem[] = []; for (const [taskId, rs] of byTask) { const impact = rs.reduce((s, r) => s + (SEVERITY_SCORE[r.gapSeverity || 'low'] ?? 3), 0) / rs.length; items.push({ taskId, taskLabel: rs[0].taskLabel || taskId, blockedTurns: rs.length, impact: Math.round(impact * 10) / 10, debtScore: Math.round(rs.length * impact), }); } return items.sort((a, b) => b.debtScore - a.debtScore); } export function formatNeedsMarkdown(needs: NeedItem[], inventory: InventoryItem[], debt: DebtItem[] = []): string { const lines: string[] = []; lines.push('# 학습 필요성 (Need Engine)'); lines.push(''); lines.push('공식: 정보부족 30% + 실패율 25% + 빈도 20% + 확신부족 15% + 피드백 10%'); lines.push(''); if (needs.length === 0) { lines.push('Reflection 기록 없음 — 업무 turn 이 쌓이면 학습 우선순위가 산출됩니다.'); } else { lines.push('| 우선순위 | 업무 | Need Score | 표본 | 근거 |'); lines.push('|---|---|---|---|---|'); needs.forEach((n, i) => { lines.push(`| ${i + 1} | ${n.taskLabel} | **${n.score}** | ${n.sampleCount} | ${n.reason} |`); }); } lines.push(''); lines.push('## Knowledge Inventory (지식 보유 상태)'); lines.push(''); if (inventory.length === 0) { lines.push('- 데이터 없음'); } else { const statusLabel = { sufficient: '보유', partial: '부족', missing: '없음' } as const; lines.push('| 업무 | 상태 | 평균 근거 수 | 평균 top score |'); lines.push('|---|---|---|---|'); for (const it of inventory) { lines.push(`| ${it.taskLabel} | ${statusLabel[it.status]} | ${it.avgChunkCount.toFixed(1)} | ${it.avgTopScore.toFixed(2)} |`); } } lines.push(''); lines.push('## Knowledge Debt (지식 부채)'); lines.push(''); if (debt.length === 0) { lines.push('- 부채 없음 — 지식 부족 상태로 수행된 업무가 없습니다.'); } else { lines.push('| 업무 | Blocked Turns | Impact (0~10) | Debt Score |'); lines.push('|---|---|---|---|'); for (const d of debt) { lines.push(`| ${d.taskLabel} | ${d.blockedTurns} | ${d.impact} | **${d.debtScore}** |`); } } lines.push(''); return lines.join('\n'); }