From 7239ac8bdfabfd069fab494345a6d6a74e5d2cf8 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 5 Aug 2026 08:56:04 +0000 Subject: [PATCH 1/3] fix: recognise 133 more imperative verbs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CC003 matches the subject's first word against a fixed list, so a verb missing from it rejects a subject that was written correctly. The contributor then has to reword something that was never wrong, and the only way to learn which words are acceptable is trial and error. Measured against 40,000 git.git subjects — a project that writes strictly imperative subjects, with its area prefixes stripped so only the verb is compared — the list rejected 17.9% of them. This brings that to 10.5%. The additions come from that corpus, filtered by hand to unambiguous verbs: avoid, clarify, teach, factor, free, mention, plug, inline, restore, retire, treat, tighten and the rest. Adverb-led subjects ("always quote the path") and non-verbs were left out — the first are correct English the list cannot represent, and the second would weaken the check. Both spellings of every -ize/-ise verb are now present. The file had sixteen -ize verbs and four -ise ones, so a project writing British English was being told "normalise" is not imperative. Wrong forms still fail, which is the property that matters: "updated the parser", "adding a new flag", "fixes the crash" and "removed the dead code" are covered by a test alongside the recovered words. The remaining 10.5% is the point of the follow-up issue: a list can only ever approximate "is this an English imperative verb". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn --- commit_check/imperatives.py | 168 +++++++++++++++++++++++++++++++++--- tests/engine_test.py | 50 +++++++++++ 2 files changed, 205 insertions(+), 13 deletions(-) diff --git a/commit_check/imperatives.py b/commit_check/imperatives.py index 3704ee79..f3af9e08 100644 --- a/commit_check/imperatives.py +++ b/commit_check/imperatives.py @@ -1,20 +1,38 @@ +# Imperative forms of verbs, seeded from # https://github.com/crate-ci/imperative/blob/master/assets/imperatives.txt -# Imperative forms of verbs +# and extended since. # -# This file contains the imperative form of frequently encountered -# docstring verbs. Some of these may be more commonly encountered as -# nouns, but blacklisting them for this may cause false positives. +# Some of these are more commonly encountered as nouns, but leaving them out +# rejects a subject that is written correctly, which is the worse failure: the +# contributor has to reword something that was never wrong, and the only way +# they learn which words are acceptable is by trial and error. +# +# For the same reason both spellings of every -ize/-ise verb are listed. A +# project writing British English is not making a mistake. +# +# Additions are welcome and cheap. The list can only ever approximate "is this +# an English imperative verb", so treat a rejected-but-correct subject as a bug +# in this file rather than as something the author should work around. IMPERATIVES = { "abort", "absorb", + "abstract", "accept", "access", + "accommodate", + "acknowledge", + "activate", + "adapt", "add", + "address", "adjust", + "advertise", "aggregate", "align", "allow", + "amend", + "annotate", "append", "apply", "archive", @@ -22,38 +40,53 @@ "assign", "attach", "attempt", + "audit", "authenticate", + "authorise", "authorize", "auto", + "automate", + "avoid", + "await", "backport", + "balance", "batch", + "be", "bind", "block", "break", "broadcast", + "broaden", "build", "bump", + "bypass", "cache", "calculate", "call", "cancel", "capture", + "centralise", + "centralize", "change", "check", + "clarify", "clean", "clear", "close", + "collapse", "collect", "combine", "comment", "commit", "compare", + "complete", "compose", "compress", "compute", "configure", "confirm", "connect", + "consolidate", "construct", "consume", "control", @@ -62,36 +95,49 @@ "correct", "count", "create", + "customise", "customize", "debug", "declare", "decode", "decompress", "decorate", - "decrypt", "decrease", + "decrypt", + "deduplicate", + "defer", "define", "delegate", "delete", + "demonstrate", "deprecate", "derive", "describe", + "deselect", "detach", "detect", "determine", + "diagnose", + "die", + "differentiate", "disable", + "disallow", "discard", "disconnect", "dispatch", "display", "dispose", + "distinguish", "distribute", + "do", "document", - "download", "downgrade", + "download", "drop", - "duplicate", "dump", + "duplicate", + "elaborate", + "eliminate", "embed", "emit", "empty", @@ -114,24 +160,31 @@ "exit", "expand", "expect", + "expire", + "explain", "export", "expose", "extend", "extract", + "factor", "feed", "fetch", "fill", "filter", + "finalise", "finalize", "find", "fire", "fix", "flag", + "flatten", "flush", "fold", + "forbid", "force", "format", "forward", + "free", "freeze", "generate", "get", @@ -139,15 +192,21 @@ "go", "grant", "group", + "guard", "halt", "handle", + "harden", "hash", "help", "hide", "highlight", + "hoist", "hold", + "honor", + "honour", "identify", "ignore", + "illustrate", "implement", "import", "improve", @@ -162,9 +221,11 @@ "initialize", "initiate", "inject", + "inline", "input", "insert", "instantiate", + "instrument", "integrate", "intercept", "introduce", @@ -175,6 +236,8 @@ "join", "keep", "launch", + "let", + "lift", "link", "list", "listen", @@ -192,19 +255,32 @@ "marshal", "mask", "match", + "maximise", "maximize", "measure", + "memoise", + "memoize", + "mention", "merge", "migrate", + "minimise", "minimize", "mirror", "mock", + "modernise", + "modernize", "modify", + "modularise", + "modularize", "monitor", "mount", "move", "name", + "narrow", "navigate", + "neutralise", + "neutralize", + "normalise", "normalize", "note", "notify", @@ -212,45 +288,69 @@ "offset", "omit", "open", + "optimise", "optimize", + "orchestrate", + "organise", + "organize", "outline", "output", - "organize", - "orchestrate", + "overhaul", "override", "overwrite", "package", "pad", + "paginate", + "parameterise", "parameterize", "parse", "partial", "pass", "pause", "perform", + "permit", "persist", "pick", + "pin", "ping", "pipe", "plot", + "plug", + "pluralise", + "pluralize", "poll", "polyfill", "populate", + "port", "post", + "postpone", + "precompute", + "prefer", "prefix", "prepare", + "prepend", + "preserve", "prevent", "print", + "prioritise", + "prioritize", "process", "produce", - "prune", + "propagate", "provide", + "proxy", + "prune", "publish", "pull", "purge", "push", "put", + "qualify", + "quarantine", "query", + "quote", "raise", + "randomise", "randomize", "rank", "read", @@ -259,11 +359,13 @@ "rebuild", "recall", "receive", + "reclaim", "recommend", "reconcile", "reconnect", "record", "recover", + "redact", "redesign", "redirect", "reduce", @@ -272,35 +374,49 @@ "reformat", "refresh", "register", + "reinstate", "reject", "relate", + "relax", "release", "reload", "relocate", + "remap", "remove", "rename", "render", "reorder", + "reorganise", "reorganize", "repeat", + "rephrase", "replace", "replay", "reply", "report", "represent", + "repurpose", "request", "require", + "rescue", "reset", "resolve", + "respect", + "restore", "restrict", "resume", + "retarget", + "rethrow", + "retire", "retrieve", "retry", "return", "reuse", + "revalidate", "revamp", "revert", "revoke", + "rewire", "rework", "rewrite", "roll", @@ -309,10 +425,13 @@ "route", "run", "sample", + "sanitise", "sanitize", "save", "scan", "schedule", + "scope", + "seal", "search", "select", "send", @@ -321,9 +440,13 @@ "serialize", "serve", "set", + "settle", "setup", + "shard", + "shorten", "show", "shuffle", + "silence", "simplify", "simulate", "skip", @@ -332,42 +455,56 @@ "source", "spawn", "specify", + "speed", + "spell", "split", "spread", "squash", + "stabilise", + "stabilize", + "standardise", "standardize", "start", "step", "stop", "store", + "streamline", "strip", + "stub", + "subclass", "submit", "subscribe", "substitute", "suggest", "sum", - "suppress", + "supersede", "support", + "suppress", "suspend", "swap", "switch", "sync", "synchronise", "synchronize", - "terminate", + "tag", "take", + "teach", "tear", + "terminate", "test", - "throw", "throttle", + "throw", + "tighten", "time", "toggle", + "tolerate", "trace", "track", "transfer", "transform", "translate", "transmit", + "treat", "trigger", "trim", "truncate", @@ -382,10 +519,12 @@ "unmarshal", "unpack", "unsubscribe", + "untangle", "unwind", "unwrap", "update", "upgrade", + "uphold", "upload", "use", "validate", @@ -396,8 +535,11 @@ "walk", "warm", "warn", + "weaken", + "widen", "wire", "withdraw", + "work", "wrap", "write", "yield", diff --git a/tests/engine_test.py b/tests/engine_test.py index f5b514e3..2870833b 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -1349,6 +1349,56 @@ def test_validate_with_common_imperative_subjects(self, subject): assert result == ValidationResult.PASS + @pytest.mark.benchmark + @pytest.mark.parametrize( + "subject", + [ + # Rejected before the list was extended, every one of them written + # in correct imperative mood. + "feat: settle the report format", + "fix: avoid a second lookup", + "docs: clarify the default value", + "refactor: factor out the helper", + "chore: teach the parser about tabs", + "fix: free the buffer on the error path", + "refactor: inline the wrapper", + "fix: restore the previous behaviour", + "chore: retire the legacy flag", + # British spelling is not a mistake. + "refactor: normalise the path separators", + "chore: prioritise the queue", + "refactor: serialise the payload", + ], + ) + def test_correct_imperative_subjects_are_not_rejected(self, subject): + """Words a contributor would have had to reword around must pass. + + A whitelist can only approximate "is this an imperative verb", and the + cost of a gap falls on someone who wrote the subject correctly. + """ + rule = ValidationRule(check="subject_imperative") + validator = SubjectImperativeValidator(rule) + context = ValidationContext(stdin_text=subject) + + assert validator.validate(context) == ValidationResult.PASS + + @pytest.mark.parametrize( + "subject", + [ + "fix: updated the parser", + "feat: adding a new flag", + "fix: fixes the crash", + "chore: removed the dead code", + ], + ) + def test_wrong_verb_forms_still_fail(self, subject): + """Extending the list must not weaken what the rule is there to catch.""" + rule = ValidationRule(check="subject_imperative") + validator = SubjectImperativeValidator(rule) + context = ValidationContext(stdin_text=subject) + + assert validator.validate(context) == ValidationResult.FAIL + @pytest.mark.benchmark def test_validate_with_imperative_subject(self): """Test validation with proper imperative subject.""" From 8a016f5a26260b1d9ef5fe84d9e7330103e5dbef Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 5 Aug 2026 09:05:38 +0000 Subject: [PATCH 2/3] test: keep the new imperative cases out of the benchmark suite They assert which subjects are accepted, not how fast the check runs, and marking them adds twelve identical-cost cases to what CodSpeed measures. The companion test for wrong verb forms was already unmarked, so the pair was inconsistent as well. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn --- tests/engine_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/engine_test.py b/tests/engine_test.py index 2870833b..c71049fd 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -1349,7 +1349,6 @@ def test_validate_with_common_imperative_subjects(self, subject): assert result == ValidationResult.PASS - @pytest.mark.benchmark @pytest.mark.parametrize( "subject", [ From 5289553e2184f3704a82c01af751a10083ca8194 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 5 Aug 2026 09:13:58 +0000 Subject: [PATCH 3/3] test: assert a spelling pair this change actually completed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The British-spelling case used serialise, which the file already contained before this change, so it proved nothing about the addition. customise is the useful one: customize was listed and customise was not, which is exactly the half-a-pair bug the sweep was meant to close. Both spellings are now asserted. Reported by CodeRabbit on #527, and correct — verified against the merge base that serialise predates the change while customise does not. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn --- tests/engine_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/engine_test.py b/tests/engine_test.py index c71049fd..3a1e7534 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -1363,10 +1363,13 @@ def test_validate_with_common_imperative_subjects(self, subject): "refactor: inline the wrapper", "fix: restore the previous behaviour", "chore: retire the legacy flag", - # British spelling is not a mistake. + # British spelling is not a mistake. The last pair is the case the + # file used to get wrong most often: the -ize form was listed and + # the -ise one was not, so only half of a spelling pair worked. "refactor: normalise the path separators", "chore: prioritise the queue", - "refactor: serialise the payload", + "feat: customise the template", + "feat: customize the template", ], ) def test_correct_imperative_subjects_are_not_rejected(self, subject):