From cdb1b7e302e4ca86956bc230139ebb2ae7ec57b6 Mon Sep 17 00:00:00 2001 From: 404-Page-Found Date: Tue, 28 Jul 2026 18:13:32 +1000 Subject: [PATCH 1/3] fix(history): count all imperative samples Include descriptive verb forms in the imperative-rate denominator. Closes #204 --- src/history/store.ts | 8 +++++--- tests/history-profile.test.mjs | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/history/store.ts b/src/history/store.ts index a4d0f2f..952f242 100644 --- a/src/history/store.ts +++ b/src/history/store.ts @@ -311,15 +311,17 @@ export async function buildProfile(historySize: number): Promise { ); if (verbMatch) { const verb = verbMatch[1]!; + imperativeSampleCount++; 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')) { - imperativeCount++; + if (firstWord) { imperativeSampleCount++; + if (!firstWord[0]!.endsWith('ed') && !firstWord[0]!.endsWith('ing')) { + imperativeCount++; + } } } diff --git a/tests/history-profile.test.mjs b/tests/history-profile.test.mjs index 41bde7a..0c2ff4b 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,7 @@ 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); From 9a355bbdd5e4d0ac2cbbcf52f94c3f932920264c Mon Sep 17 00:00:00 2001 From: 404-Page-Found Date: Tue, 28 Jul 2026 19:19:27 +1000 Subject: [PATCH 2/3] fix: exclude base-form verbs from descriptive verb detection Add a set of base-form verbs that end with 'ed' or 'ing' (e.g., 'bleed', 'bring') to avoid counting them as descriptive in the imperative rate calculation. Refactor verb extraction to use optional chaining and fallback. Add test to verify correct behavior. --- src/history/store.ts | 50 ++++++++++++++++++++++++++-------- tests/history-profile.test.mjs | 24 ++++++++++++++++ 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/history/store.ts b/src/history/store.ts index 952f242..4774487 100644 --- a/src/history/store.ts +++ b/src/history/store.ts @@ -7,6 +7,42 @@ 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 { + 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,20 +345,12 @@ 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]!; + const verb = verbMatch?.[1] ?? firstLine.match(/^\w+/)?.[0]; + if (verb) { imperativeSampleCount++; - if (!verb.endsWith('ed') && !verb.endsWith('ing')) { + if (!isDescriptiveVerb(verb)) { imperativeCount++; } - } else { - const firstWord = firstLine.match(/^\w+/); - if (firstWord) { - imperativeSampleCount++; - if (!firstWord[0]!.endsWith('ed') && !firstWord[0]!.endsWith('ing')) { - imperativeCount++; - } - } } if (/^[A-Z]/.test(firstLine)) { diff --git a/tests/history-profile.test.mjs b/tests/history-profile.test.mjs index 0c2ff4b..356cd3f 100644 --- a/tests/history-profile.test.mjs +++ b/tests/history-profile.test.mjs @@ -67,6 +67,30 @@ test('buildProfile counts descriptive verb forms in the imperative-rate denomina } }); +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); + restoreEnv('XDG_CONFIG_HOME', originalXdgConfigHome); + rmSync(tempHome, { recursive: true, force: true }); + } +}); + test('formatProfile reports the empty-history fallback', () => { const output = formatProfile({ avgLength: 0, From 9973b1dea5b23101c98ca3cc2c81ff034491725a Mon Sep 17 00:00:00 2001 From: 404-Page-Found Date: Tue, 28 Jul 2026 19:56:01 +1000 Subject: [PATCH 3/3] fix: normalize verb case before checking descriptive suffixes The lowercase conversion was missing when checking if a verb ends with 'ed' or 'ing', causing capitalized verbs like 'Bring' to be incorrectly classified as descriptive verbs. --- src/history/store.ts | 1 + tests/history-profile.test.mjs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/history/store.ts b/src/history/store.ts index 4774487..620c498 100644 --- a/src/history/store.ts +++ b/src/history/store.ts @@ -40,6 +40,7 @@ const BASE_FORM_VERBS_WITH_SUFFIXES = new Set([ ]); function isDescriptiveVerb(verb: string): boolean { + verb = verb.toLowerCase(); return !BASE_FORM_VERBS_WITH_SUFFIXES.has(verb) && (verb.endsWith('ed') || verb.endsWith('ing')); } diff --git a/tests/history-profile.test.mjs b/tests/history-profile.test.mjs index 356cd3f..b71d094 100644 --- a/tests/history-profile.test.mjs +++ b/tests/history-profile.test.mjs @@ -77,7 +77,7 @@ test('buildProfile recognizes base-form verbs that end in descriptive suffixes', 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']); + writeHistory(tempHome, ['fix: add retries', 'fix: Bring retries', 'fix: added retries']); const profile = await buildProfile(10);