diff --git a/src/classifier.ts b/src/classifier.ts index ca7854b..9255f84 100644 --- a/src/classifier.ts +++ b/src/classifier.ts @@ -208,10 +208,8 @@ export function classifyTurn(turn: ParsedTurn): ClassifiedTurn { const result: ClassifiedTurn = { ...turn, category, retries: countRetries(turn), hasEdits: turnHasEdits(turn) } - if (category === 'general') { - const skills = getAllSkills(turn) - if (skills.length > 0) result.subCategory = skills[0] - } + const skills = getAllSkills(turn) + if (skills.length > 0) result.subCategory = skills[0] return result } diff --git a/src/session-cache.ts b/src/session-cache.ts index d189251..41efc1e 100644 --- a/src/session-cache.ts +++ b/src/session-cache.ts @@ -141,7 +141,7 @@ export const DURABLE_PROVIDER_NAMES: ReadonlySet = new Set(['copilot']) // re-parse, which lands the flag too, and durable orphans now survive // fingerprint changes (the carry-forward in getOrCreateProviderSection). export const PROVIDER_PARSE_VERSIONS: Record = { - claude: 'advisor-usage-v1', + claude: 'advisor-usage-v1-skills', cline: 'worktree-project-grouping-v1', codewhale: 'aggregate-session-v1-est-cost', // Bump when the Codex parser changes attribution so unchanged, already-cached diff --git a/tests/classifier.test.ts b/tests/classifier.test.ts index 3ef82b5..6b4730c 100644 --- a/tests/classifier.test.ts +++ b/tests/classifier.test.ts @@ -79,11 +79,11 @@ describe('classifyTurn — Skill subCategory', () => { expect(c.subCategory).toBeUndefined() }) - it('does not attach subCategory when category is not general (e.g. Skill alongside Edit promotes to coding)', () => { + it('attaches subCategory without changing the category when Skill fires alongside Edit', () => { const turn = makeTurn([makeCall({ tools: ['Skill', 'Edit'], skills: ['init'] })]) const c = classifyTurn(turn) expect(c.category).toBe('coding') - expect(c.subCategory).toBeUndefined() + expect(c.subCategory).toBe('init') }) it('does not attach subCategory for non-Skill general turns', () => { diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 56ffc26..5de2dad 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -455,3 +455,36 @@ describe('(f) durable orphans survive a parse-version bump', () => { expect(again).toBe(200) }) }) + +// ═══════════════════════════════════════════════════════════════════════════ +// (g) Skill attribution is independent of turn category +// ═══════════════════════════════════════════════════════════════════════════ +describe('(g) skill attribution is independent of turn category', () => { + it('puts a Skill + Edit turn in skillBreakdown while preserving coding category', async () => { + const synthFile = join(tmpHome, 'synth-skill.txt') + await writeFile(synthFile, 'placeholder') + + _synthSources = [{ path: synthFile, project: 'test', provider: 'test-synthetic' }] + _synthYields = [{ + provider: 'test-synthetic', model: 'gpt-4o', + inputTokens: 10, outputTokens: 5, + cacheCreationInputTokens: 0, cacheReadInputTokens: 0, + cachedInputTokens: 0, reasoningTokens: 0, webSearchRequests: 0, + costUSD: 0.001, tools: ['Skill', 'Edit'], bashCommands: [], + skills: ['telemetry-review'], + timestamp: '2026-07-18T12:00:00.000Z', + speed: 'standard', + deduplicationKey: 'synth-skill-edit', + userMessage: '', sessionId: 'synth-skill-session', + }] + + const projects = await parseAllSessions(undefined, 'test-synthetic') + const session = projects.flatMap(project => project.sessions)[0] + + expect(session).toBeDefined() + expect(session!.turns[0]!.category).toBe('coding') + expect(session!.turns[0]!.subCategory).toBe('telemetry-review') + expect(session!.categoryBreakdown.coding.turns).toBe(1) + expect(session!.skillBreakdown['telemetry-review']?.turns).toBe(1) + }) +})