feat(tag-vocab): telemetryStats — hit/miss 누적 + summary 적중률 (#3 v0.2.3)

- DailyRow +2 cols (tag_vocab_hit, tag_vocab_miss)
- accumulators + branches
- table 컬럼 +2
- summary "- 태그 vocab: hit/miss = N/M (적중률 X%)" 또는 "(데이터 없음)"
- 단위 +2 cases

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
altair823
2026-05-02 12:27:22 +09:00
parent b81fc82621
commit 973cb1d08d
2 changed files with 44 additions and 4 deletions

View File

@@ -24,6 +24,8 @@ interface DailyRow {
ollama_recovered: number;
ollama_recheck_manual: number;
ai_retry_manual: number;
tag_vocab_hit: number;
tag_vocab_miss: number;
}
export interface StatsResult {
@@ -47,6 +49,8 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta
let ollamaRecheckManualCount = 0;
let aiRetryManualCount = 0;
let aiRetryManualFailedSum = 0;
let tagVocabHitCount = 0;
let tagVocabMissCount = 0;
for (const ev of events) {
const day = kstDate(ev.ts);
let row = byDay.get(day);
@@ -57,7 +61,8 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta
trash: 0, restore: 0, permanent_delete: 0, empty_trash: 0,
expired_banner_shown: 0, expired_batch_trash: 0,
ollama_unreachable: 0, ollama_recovered: 0, ollama_recheck_manual: 0,
ai_retry_manual: 0
ai_retry_manual: 0,
tag_vocab_hit: 0, tag_vocab_miss: 0
};
byDay.set(day, row);
}
@@ -99,6 +104,12 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta
row.ai_retry_manual += 1;
aiRetryManualCount += 1;
aiRetryManualFailedSum += ev.payload.failedCount;
} else if (ev.kind === 'tag_vocab_hit') {
row.tag_vocab_hit += 1;
tagVocabHitCount += 1;
} else if (ev.kind === 'tag_vocab_miss') {
row.tag_vocab_miss += 1;
tagVocabMissCount += 1;
}
}
const days = Array.from(byDay.values()).sort((a, b) => a.date.localeCompare(b.date));
@@ -115,6 +126,10 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta
? 'N/A'
: `${Math.round(ollamaDowntimeSum / ollamaRecoveredCount)}`;
const totalUnreachable = days.reduce((s, r) => s + r.ollama_unreachable, 0);
const tagVocabTotal = tagVocabHitCount + tagVocabMissCount;
const tagVocabSummary = tagVocabTotal === 0
? '(데이터 없음)'
: `hit/miss = ${tagVocabHitCount}/${tagVocabMissCount} (적중률 ${(tagVocabHitCount / tagVocabTotal * 100).toFixed(1)}%)`;
const lines: string[] = [];
lines.push('# Inkling Telemetry Stats');
lines.push('');
@@ -123,10 +138,10 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta
lines.push('');
lines.push('## 일자별 카운트');
lines.push('');
lines.push('| 일자 | capture | ai_succeeded | ai_failed | trash | restore | permanent_delete | empty_trash | expired_banner_shown | expired_batch_trash | ollama_unreachable | ollama_recovered | ollama_recheck_manual | ai_retry_manual |');
lines.push('|------|---------|--------------|-----------|-------|---------|------------------|-------------|----------------------|---------------------|--------------------|------------------|----------------------|-----------------|');
lines.push('| 일자 | capture | ai_succeeded | ai_failed | trash | restore | permanent_delete | empty_trash | expired_banner_shown | expired_batch_trash | ollama_unreachable | ollama_recovered | ollama_recheck_manual | ai_retry_manual | tag_vocab_hit | tag_vocab_miss |');
lines.push('|------|---------|--------------|-----------|-------|---------|------------------|-------------|----------------------|---------------------|--------------------|------------------|----------------------|-----------------|---------------|----------------|');
for (const row of days) {
lines.push(`| ${row.date} | ${row.capture} | ${row.ai_succeeded} | ${row.ai_failed} | ${row.trash} | ${row.restore} | ${row.permanent_delete} | ${row.empty_trash} | ${row.expired_banner_shown} | ${row.expired_batch_trash} | ${row.ollama_unreachable} | ${row.ollama_recovered} | ${row.ollama_recheck_manual} | ${row.ai_retry_manual} |`);
lines.push(`| ${row.date} | ${row.capture} | ${row.ai_succeeded} | ${row.ai_failed} | ${row.trash} | ${row.restore} | ${row.permanent_delete} | ${row.empty_trash} | ${row.expired_banner_shown} | ${row.expired_batch_trash} | ${row.ollama_unreachable} | ${row.ollama_recovered} | ${row.ollama_recheck_manual} | ${row.ai_retry_manual} | ${row.tag_vocab_hit} | ${row.tag_vocab_miss} |`);
}
lines.push('');
lines.push('## 핵심 ratio');
@@ -139,6 +154,7 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta
lines.push(`- 평균 downtimeMs (recovered): ${avgDowntime}`);
lines.push(`- 수동 recheck 사용량: ${ollamaRecheckManualCount}`);
lines.push(`- AI 수동 재시도: ${aiRetryManualCount}회 / 누적 ${aiRetryManualFailedSum}`);
lines.push(`- 태그 vocab: ${tagVocabSummary}`);
lines.push('');
return { md: lines.join('\n'), eventCount };
}

View File

@@ -164,3 +164,27 @@ describe('aggregateStats — ai_retry_manual', () => {
expect(r.md).toMatch(/AI 수동 재시도.*2회.*10건/);
});
});
describe('aggregateStats — tag_vocab hit/miss', () => {
it('aggregates tag_vocab hit/miss with success rate', () => {
const events: TelemetryEvent[] = [
e('2026-05-02T00:00:00Z', 'tag_vocab_hit', { tagId: 1, vocabSize: 10 }),
e('2026-05-02T00:00:01Z', 'tag_vocab_hit', { tagId: 2, vocabSize: 10 }),
e('2026-05-02T00:00:02Z', 'tag_vocab_hit', { tagId: 3, vocabSize: 10 }),
e('2026-05-02T00:00:03Z', 'tag_vocab_hit', { tagId: 4, vocabSize: 10 }),
e('2026-05-02T00:00:04Z', 'tag_vocab_hit', { tagId: 5, vocabSize: 10 }),
e('2026-05-02T00:00:05Z', 'tag_vocab_miss', { vocabSize: 10 }),
e('2026-05-02T00:00:06Z', 'tag_vocab_miss', { vocabSize: 10 }),
e('2026-05-02T00:00:07Z', 'tag_vocab_miss', { vocabSize: 10 })
];
const r = aggregateStats(events, new Date('2026-05-03T00:00:00Z'));
expect(r.md).toContain('태그 vocab: hit/miss = 5/3');
expect(r.md).toContain('적중률 62.5%');
});
it('태그 vocab summary shows 데이터 없음 when no events', () => {
const r = aggregateStats([], new Date('2026-05-03T00:00:00Z'));
expect(r.md).toContain('태그 vocab');
expect(r.md).toContain('데이터 없음');
});
});