Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@
period-level `activities[]` rollup so a consumer can sum across days and
reconcile. Closes #279.

### Fixed (CLI)
- **Activity classifier no longer mislabels feature work as debugging.**
Messages like "add error handling", "create an issue tracker", or
"implement the 404 page" used to land in the Debugging bucket because
the classifier checked the debug-keyword regex (which matches `error`,
`issue`, `404`) before the feature regex. Now the keyword that appears
earliest in the user message wins, so "add" beats "error", "create"
beats "issue", etc. A real bug report ("login is broken, traceback
below") still classifies as debugging because the debug word leads.
Fixes the activity-misattribution half of #196.

### Changed (CLI)
- **`optimize` suggestions now declare their destination.** Every paste-style
fix carries an explicit destination — `claude-md` (permanent project rule),
Expand Down
44 changes: 38 additions & 6 deletions src/classifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,38 @@ function classifyByToolPattern(turn: ParsedTurn): TaskCategory | null {
return null
}

/// Picks the category whose keyword pattern matches earliest in the message.
/// On a tie (same start index) the candidate listed first in `candidates` wins,
/// so callers control tie-break priority by ordering. Returns null when no
/// pattern matches. The first-match heuristic fixes the long-standing problem
/// where "add error handling" was tagged Debugging because the DEBUG regex was
/// checked before FEATURE; now FEATURE wins because "add" appears before
/// "error". Issue #196.
function firstMatchingCategory(
text: string,
candidates: ReadonlyArray<{ regex: RegExp; category: TaskCategory }>,
): TaskCategory | null {
let best: { index: number; order: number; category: TaskCategory } | null = null
for (let i = 0; i < candidates.length; i++) {
const c = candidates[i]!
const m = c.regex.exec(text)
if (!m) continue
if (!best || m.index < best.index || (m.index === best.index && i < best.order)) {
best = { index: m.index, order: i, category: c.category }
}
}
return best?.category ?? null
}

function refineByKeywords(category: TaskCategory, userMessage: string): TaskCategory {
if (category === 'coding') {
if (DEBUG_KEYWORDS.test(userMessage)) return 'debugging'
if (REFACTOR_KEYWORDS.test(userMessage)) return 'refactoring'
if (FEATURE_KEYWORDS.test(userMessage)) return 'feature'
return 'coding'
// Tie-break order (when two keywords match at the same index): refactoring
// first because its words are the most specific, then feature, then debug.
return firstMatchingCategory(userMessage, [
{ regex: REFACTOR_KEYWORDS, category: 'refactoring' },
{ regex: FEATURE_KEYWORDS, category: 'feature' },
{ regex: DEBUG_KEYWORDS, category: 'debugging' },
]) ?? 'coding'
}

if (category === 'exploration') {
Expand All @@ -113,8 +139,14 @@ function refineByKeywords(category: TaskCategory, userMessage: string): TaskCate
function classifyConversation(userMessage: string): TaskCategory {
if (BRAINSTORM_KEYWORDS.test(userMessage)) return 'brainstorming'
if (RESEARCH_KEYWORDS.test(userMessage)) return 'exploration'
if (DEBUG_KEYWORDS.test(userMessage)) return 'debugging'
if (FEATURE_KEYWORDS.test(userMessage)) return 'feature'
// Same first-match-wins logic as refineByKeywords so a chat-only message
// starting with a feature verb does not flip to debugging because of an
// incidental "error" or "fix" word later in the same sentence.
const debugOrFeature = firstMatchingCategory(userMessage, [
{ regex: FEATURE_KEYWORDS, category: 'feature' },
{ regex: DEBUG_KEYWORDS, category: 'debugging' },
])
if (debugOrFeature) return debugOrFeature
if (FILE_PATTERNS.test(userMessage)) return 'coding'
if (SCRIPT_PATTERNS.test(userMessage)) return 'coding'
if (URL_PATTERN.test(userMessage)) return 'exploration'
Expand Down
50 changes: 50 additions & 0 deletions tests/classifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,53 @@ describe('classifyTurn — Skill subCategory', () => {
expect(c.subCategory).toBeUndefined()
})
})

// Regression coverage for issue #196: feature verbs that lead a message
// were previously hijacked into 'debugging' just because the message contained
// an incidental "error" / "fix" / "issue" word later in the same sentence.
// Now whichever keyword pattern matches earliest wins.
describe('classifyTurn — feature vs debugging precedence (#196)', () => {
function codingTurn(userMessage: string): ParsedTurn {
return makeTurn([makeCall({ tools: ['Edit'] })], userMessage)
}

it('classifies "add error handling" as feature, not debugging', () => {
const c = classifyTurn(codingTurn('add error handling to the auth module'))
expect(c.category).toBe('feature')
})

it('classifies "create an issue tracker" as feature, not debugging', () => {
const c = classifyTurn(codingTurn('create an issue tracker page in the dashboard'))
expect(c.category).toBe('feature')
})

it('classifies "implement the 404 page" as feature, not debugging', () => {
const c = classifyTurn(codingTurn('implement the 404 page with a friendly redirect'))
expect(c.category).toBe('feature')
})

it('still classifies "fix the layout for the new feature" as debugging', () => {
const c = classifyTurn(codingTurn('fix the layout for the new feature'))
expect(c.category).toBe('debugging')
})

it('still classifies a plain bug report as debugging', () => {
const c = classifyTurn(codingTurn('login is broken, traceback below'))
expect(c.category).toBe('debugging')
})

it('classifies "refactor the error handling" as refactoring', () => {
const c = classifyTurn(codingTurn('refactor the error handling so it is cleaner'))
expect(c.category).toBe('refactoring')
})

it('chat-only message starting with "add" stays feature even with "fix" later', () => {
const c = classifyTurn(makeTurn([], 'add a setting page; we will fix the styles after'))
expect(c.category).toBe('feature')
})

it('chat-only message starting with "fix" stays debugging even with "add" later', () => {
const c = classifyTurn(makeTurn([], 'fix the bug introduced when we added the new flag'))
expect(c.category).toBe('debugging')
})
})
Loading