diff --git a/src/history/store.ts b/src/history/store.ts index a4d0f2f..620c498 100644 --- a/src/history/store.ts +++ b/src/history/store.ts @@ -7,6 +7,43 @@ import { getHistoryPath, getConfigDir } from '../config/store.js'; const CONVENTIONAL_PREFIX_RE = /^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\([^)]+\))?:\s*/; +const BASE_FORM_VERBS_WITH_SUFFIXES = new Set([ + 'bleed', + 'breed', + 'bring', + 'cling', + 'ding', + 'embed', + 'exceed', + 'feed', + 'fling', + 'heed', + 'imbed', + 'king', + 'need', + 'ping', + 'proceed', + 'ring', + 'seed', + 'shed', + 'sing', + 'sling', + 'speed', + 'spring', + 'sting', + 'string', + 'swing', + 'wed', + 'wing', + 'wring', + 'zing', +]); + +function isDescriptiveVerb(verb: string): boolean { + verb = verb.toLowerCase(); + return !BASE_FORM_VERBS_WITH_SUFFIXES.has(verb) && (verb.endsWith('ed') || verb.endsWith('ing')); +} + const HISTORY_LOCK_RETRY_MS = 25; const HISTORY_LOCK_STALE_MS = 60_000; const HISTORY_LOCK_TIMEOUT_MS = HISTORY_LOCK_STALE_MS + 10_000; @@ -309,17 +346,11 @@ export async function buildProfile(historySize: number): Promise { const verbMatch = firstLine.match( /^(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(?:\([^)]+\))?:\s*(\w+)/, ); - if (verbMatch) { - const verb = verbMatch[1]!; - if (!verb.endsWith('ed') && !verb.endsWith('ing')) { - imperativeCount++; - imperativeSampleCount++; - } - } else { - const firstWord = firstLine.match(/^\w+/); - if (firstWord && !firstWord[0]!.endsWith('ed') && !firstWord[0]!.endsWith('ing')) { + const verb = verbMatch?.[1] ?? firstLine.match(/^\w+/)?.[0]; + if (verb) { + imperativeSampleCount++; + if (!isDescriptiveVerb(verb)) { imperativeCount++; - imperativeSampleCount++; } } diff --git a/tests/history-profile.test.mjs b/tests/history-profile.test.mjs index 41bde7a..b71d094 100644 --- a/tests/history-profile.test.mjs +++ b/tests/history-profile.test.mjs @@ -43,7 +43,7 @@ function restoreEnv(name, value) { } } -test('buildProfile excludes descriptive verb forms from the imperative-rate denominator', async () => { +test('buildProfile counts descriptive verb forms in the imperative-rate denominator', async () => { const originalHome = process.env.HOME; const originalAppData = process.env.APPDATA; const originalXdgConfigHome = process.env.XDG_CONFIG_HOME; @@ -58,7 +58,31 @@ test('buildProfile excludes descriptive verb forms from the imperative-rate deno const profile = await buildProfile(10); assert.equal(profile.totalCommits, 3); - assert.equal(profile.imperativeRate, 1); + assert.equal(profile.imperativeRate, 1 / 3); + } finally { + restoreEnv('HOME', originalHome); + restoreEnv('APPDATA', originalAppData); + restoreEnv('XDG_CONFIG_HOME', originalXdgConfigHome); + rmSync(tempHome, { recursive: true, force: true }); + } +}); + +test('buildProfile recognizes base-form verbs that end in descriptive suffixes', async () => { + const originalHome = process.env.HOME; + const originalAppData = process.env.APPDATA; + const originalXdgConfigHome = process.env.XDG_CONFIG_HOME; + const tempHome = mkdtempSync(join(tmpdir(), 'commit-echo-home-')); + + try { + process.env.HOME = tempHome; + process.env.APPDATA = join(tempHome, 'AppData', 'Roaming'); + process.env.XDG_CONFIG_HOME = join(tempHome, '.config'); + writeHistory(tempHome, ['fix: add retries', 'fix: Bring retries', 'fix: added retries']); + + const profile = await buildProfile(10); + + assert.equal(profile.totalCommits, 3); + assert.equal(profile.imperativeRate, 2 / 3); } finally { restoreEnv('HOME', originalHome); restoreEnv('APPDATA', originalAppData);