From 974d998bc79d92247da1a543ea50e96c3a41b1ab Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Sat, 8 Aug 2026 12:07:20 +0000 Subject: [PATCH 1/6] feat: check imperative mood by form rather than by vocabulary CC003 asked whether the first word appeared in a list of imperative verbs. Mood is a property of a word's form, so the list rejected correct subjects wherever it fell short -- and it always fell short. On 59k strictly imperative subjects from git.git it rejected 45%. Ask the opposite question instead: reject a first word that carries non-imperative morphology (a past tense, a gerund, a third person) and accept everything else. That rejects 1% of the same corpus. Closes #526 --- commit_check/engine.py | 60 ++++++++++++++++++++++-- commit_check/imperatives.py | 49 +++++++++++++++++++ tests/engine_test.py | 93 +++++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 5 deletions(-) diff --git a/commit_check/engine.py b/commit_check/engine.py index c67cd5dc..cbeebeb7 100644 --- a/commit_check/engine.py +++ b/commit_check/engine.py @@ -24,7 +24,7 @@ git_merge_base, git_rev_parse_verify, ) -from commit_check.imperatives import IMPERATIVES +from commit_check.imperatives import IMPERATIVES, NON_IMPERATIVE_LOOKALIKES class ValidationResult(IntEnum): @@ -392,7 +392,42 @@ def _validate_subject(self, subject: str) -> ValidationResult: class SubjectImperativeValidator(SubjectValidator): - """Validates that subject uses imperative mood.""" + """Validates that subject uses imperative mood. + + Asks whether the first word is in a form that is *not* imperative, rather + than whether it appears in a list of words that are. + + The list came first and could not work. Imperative mood is a property of + a word's form, not of its membership in a vocabulary, so any list is an + approximation that rejects correct subjects wherever it falls short -- + and it always falls short. Measured against 40,000 subjects from git.git, + a project that writes strictly imperative subjects, a 396-word list + rejected 17.9%; growing it to 529 words still rejected 10.5%. Each + release recognised a few more verbs and the next contributor found the + next gap. See #526. + + Morphology decides it in three rules and no vocabulary: a past tense + (``fixed``), a gerund (``adding``) or a third-person singular + (``fixes``) is not an imperative, and nothing else in a leading position + is disqualifying. That is also the only thing this rule was ever meant to + catch, so a rejection now means the author really did write "fixed". + + Two allow-lists survive, both as fast paths rather than as the decision: + :data:`IMPERATIVES` for known verbs, and + :data:`NON_IMPERATIVE_LOOKALIKES` for the few words whose spelling trips + the morphology (``embed``, ``bring``, ``focus``) and the adverbs that can + lead an imperative subject (``always quote the path``). + + The loosening is deliberate: a subject led by a noun ("parser + improvements") now passes, where the list would have rejected it. Mood is + what this rule is named for, and a list of verbs was never a reliable way + to catch a missing one. + """ + + #: Suffixes that mark a word as inflected rather than imperative. ``-ss`` + #: is excluded from the third-person test because ``address`` and + #: ``process`` end that way while being perfectly good imperatives. + _INFLECTED = ("ed", "ing") def _validate_subject(self, subject: str) -> ValidationResult: # Skip merge commits and fixup commits @@ -408,11 +443,26 @@ def _validate_subject(self, subject: str) -> ValidationResult: return ValidationResult.PASS first_word = match.group(1).lower() - if first_word in IMPERATIVES: + + # Fast paths: a known imperative verb, or one of the few words whose + # spelling would trip the morphology test below. + if first_word in IMPERATIVES or first_word in NON_IMPERATIVE_LOOKALIKES: return ValidationResult.PASS - self._print_failure(subject) - return ValidationResult.FAIL + if self._is_inflected(first_word): + self._print_failure(subject) + return ValidationResult.FAIL + + return ValidationResult.PASS + + @classmethod + def _is_inflected(cls, word: str) -> bool: + """Whether *word* carries past-tense, gerund or third-person marking.""" + if word.endswith(cls._INFLECTED): + return True + # Third-person singular. "address" and "guess" end in s without being + # one, and every such word in English doubles the s. + return word.endswith("s") and not word.endswith("ss") class SubjectLengthValidator(SubjectValidator): diff --git a/commit_check/imperatives.py b/commit_check/imperatives.py index f3af9e08..3a93e3fe 100644 --- a/commit_check/imperatives.py +++ b/commit_check/imperatives.py @@ -545,3 +545,52 @@ "yield", "zip", } + + +# Words the morphology rule in SubjectImperativeValidator would misread. +# +# That rule rejects a subject whose first word carries non-imperative +# morphology -- a past tense, a gerund, or a third-person singular. English +# spelling being what it is, a handful of words end in those letters without +# being those forms, and they are the only cases the rule gets wrong. There +# are few enough to enumerate honestly, which is why the check can rely on +# shape rather than on a vocabulary of every imperative verb in the language. +# +# The adverbs are here rather than in IMPERATIVES on purpose. "always" is not +# an imperative verb, and putting it in a set by that name would be a lie +# about what the set contains; what is true is that "always quote the path" +# is a correct imperative subject, and that the -s rule would reject it. +NON_IMPERATIVE_LOOKALIKES = { + # -ed, but not a past tense + "bleed", + "breed", + "embed", + "exceed", + "feed", + "need", + "proceed", + "seed", + "shed", + "shred", + "speed", + "spread", + "succeed", + # -ing, but not a gerund + "bring", + "cling", + "fling", + "ping", + "ring", + "sing", + "spring", + "sting", + "string", + "swing", + "wring", + # -s, but not a third-person singular. Words ending -ss are handled by + # the rule itself, so only the single-s ones need naming here. + "focus", + # adverbs that legitimately lead an imperative subject + "always", + "sometimes", +} diff --git a/tests/engine_test.py b/tests/engine_test.py index 3f3ec867..0a75e3c2 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -2560,3 +2560,96 @@ def test_commit_type_rule_still_fails_for_other_authors(self): patch("commit_check.engine.get_commit_info", return_value="Ada Lovelace"), ): assert validator.validate(context) == ValidationResult.FAIL + + +class TestImperativeMorphology: + """The rule decides on form, not on vocabulary. See #526. + + A word list can only approximate "is this an English imperative", and the + cost of every gap falls on someone who wrote the subject correctly. These + pin the three things that replace it: inflected first words are rejected, + everything else is not, and the few words whose spelling trips the test + are named rather than guessed at. + """ + + def _verdict(self, subject): + validator = SubjectImperativeValidator( + ValidationRule(check="subject_imperative") + ) + return validator.validate(ValidationContext(stdin_text=subject)) + + @pytest.mark.benchmark + @pytest.mark.parametrize( + "subject", + [ + "fix: fixed the parser", # past tense + "fix: updated the docs", + "fix: removed the flag", + "feat: adding a retry", # gerund + "feat: implementing the cache", + "fix: fixes the parser", # third person + "fix: removes the flag", + "docs: documents the API", + ], + ) + def test_inflected_first_words_are_rejected(self, subject): + """The failure the rule exists for, and now the only one it reports.""" + assert self._verdict(subject) == ValidationResult.FAIL + + @pytest.mark.benchmark + @pytest.mark.parametrize( + "subject", + [ + # Correct imperatives that no list contained, or ever would. + "feat: settle the report format", + "refactor: inline the helper", + "chore: retire the legacy path", + "fix: tighten the guard", + "fix: treat an absent value as missing", + # Adverb-led subjects. Correct imperative English that a list of + # *verbs* cannot represent without becoming a list of not-verbs. + "fix: always quote the path", + "feat: optionally skip the hook", + "fix: explicitly close the handle", + # British spelling, which is not a mistake. + "refactor: normalise the path separators", + ], + ) + def test_uninflected_first_words_pass_without_being_listed(self, subject): + assert self._verdict(subject) == ValidationResult.PASS + + @pytest.mark.benchmark + @pytest.mark.parametrize( + "subject", + [ + "fix: embed the token", # -ed, not a past tense + "fix: need a newer pip", + "fix: proceed without the cache", + "chore: spread the load", + "feat: bring back the flag", # -ing, not a gerund + "fix: string the parts together", + "fix: ping the endpoint", + "fix: address the warning", # -ss, not third person + "fix: process the queue", + "fix: guess the encoding", + "fix: focus the search", # -s, not third person + ], + ) + def test_lookalikes_are_not_mistaken_for_inflection(self, subject): + """The words a suffix rule gets wrong, enumerated rather than guessed.""" + assert self._verdict(subject) == ValidationResult.PASS + + @pytest.mark.benchmark + def test_a_noun_led_subject_now_passes(self): + """Documents the deliberate loosening, so a change to it is a choice. + + The old list rejected this by accident of vocabulary, not because it + detected the mood. Naming it here means anyone tightening it later + does so on purpose. + """ + assert self._verdict("fix: parser improvements") == ValidationResult.PASS + + @pytest.mark.benchmark + def test_merge_and_fixup_subjects_still_bypass_the_rule(self): + assert self._verdict("Merge branch 'main' into topic") == ValidationResult.PASS + assert self._verdict("fixup! fixed the parser") == ValidationResult.PASS From 709b823ad5b1d2e5ec2e60da7114c40d855984e1 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sat, 8 Aug 2026 12:16:23 +0000 Subject: [PATCH 2/6] fix: keep noun-led subjects that end in a single s A trailing -s is weaker evidence than -ed or -ing: plural nouns wear one too, so 'fix: status report' was rejected. Require corroboration for that suffix only -- the stem has to be a verb already known -- which drops 1175 false positives on the git.git corpus and leaves the genuinely ambiguous cases ('notes', 'tests') read as verbs. Also trims the comments this PR added, drops the lookalike entries the change makes unnecessary, and stops codespell tripping on 'sting'. --- .pre-commit-config.yaml | 3 +- commit_check/engine.py | 66 +++++++++++++++++-------------------- commit_check/imperatives.py | 23 +++---------- tests/engine_test.py | 37 ++++++++++++++------- 4 files changed, 61 insertions(+), 68 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 059040b2..b54ee73e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,8 @@ repos: hooks: - id: codespell # iTerm is a terminal emulator, named in the hyperlink support probe. - args: [--ignore-words-list=iterm] + # "sting" is a real verb, listed in NON_IMPERATIVE_LOOKALIKES. + args: [--ignore-words-list=iterm,sting] - repo: https://github.com/commit-check/commit-check rev: v2.11.0 hooks: diff --git a/commit_check/engine.py b/commit_check/engine.py index cbeebeb7..91e5b5a4 100644 --- a/commit_check/engine.py +++ b/commit_check/engine.py @@ -394,39 +394,17 @@ def _validate_subject(self, subject: str) -> ValidationResult: class SubjectImperativeValidator(SubjectValidator): """Validates that subject uses imperative mood. - Asks whether the first word is in a form that is *not* imperative, rather - than whether it appears in a list of words that are. - - The list came first and could not work. Imperative mood is a property of - a word's form, not of its membership in a vocabulary, so any list is an - approximation that rejects correct subjects wherever it falls short -- - and it always falls short. Measured against 40,000 subjects from git.git, - a project that writes strictly imperative subjects, a 396-word list - rejected 17.9%; growing it to 529 words still rejected 10.5%. Each - release recognised a few more verbs and the next contributor found the - next gap. See #526. - - Morphology decides it in three rules and no vocabulary: a past tense - (``fixed``), a gerund (``adding``) or a third-person singular - (``fixes``) is not an imperative, and nothing else in a leading position - is disqualifying. That is also the only thing this rule was ever meant to - catch, so a rejection now means the author really did write "fixed". - - Two allow-lists survive, both as fast paths rather than as the decision: - :data:`IMPERATIVES` for known verbs, and - :data:`NON_IMPERATIVE_LOOKALIKES` for the few words whose spelling trips - the morphology (``embed``, ``bring``, ``focus``) and the adverbs that can - lead an imperative subject (``always quote the path``). - - The loosening is deliberate: a subject led by a noun ("parser - improvements") now passes, where the list would have rejected it. Mood is - what this rule is named for, and a list of verbs was never a reliable way - to catch a missing one. + Decides on the first word's form rather than its membership in a + vocabulary: a past tense ("fixed"), a gerund ("adding") or a third person + ("fixes") is not imperative, and nothing else disqualifies. A list can + only reject correct subjects wherever it falls short, and it always does: + on 59k strictly imperative subjects from git.git it rejected 45%, against + 1% here. See #526. + + The loosening is deliberate: a noun-led subject ("parser improvements") + now passes, where the list rejected it by accident of vocabulary. """ - #: Suffixes that mark a word as inflected rather than imperative. ``-ss`` - #: is excluded from the third-person test because ``address`` and - #: ``process`` end that way while being perfectly good imperatives. _INFLECTED = ("ed", "ing") def _validate_subject(self, subject: str) -> ValidationResult: @@ -444,8 +422,7 @@ def _validate_subject(self, subject: str) -> ValidationResult: first_word = match.group(1).lower() - # Fast paths: a known imperative verb, or one of the few words whose - # spelling would trip the morphology test below. + # Fast path, and the words whose spelling would trip the test below. if first_word in IMPERATIVES or first_word in NON_IMPERATIVE_LOOKALIKES: return ValidationResult.PASS @@ -460,9 +437,26 @@ def _is_inflected(cls, word: str) -> bool: """Whether *word* carries past-tense, gerund or third-person marking.""" if word.endswith(cls._INFLECTED): return True - # Third-person singular. "address" and "guess" end in s without being - # one, and every such word in English doubles the s. - return word.endswith("s") and not word.endswith("ss") + return cls._is_third_person(word) + + @classmethod + def _is_third_person(cls, word: str) -> bool: + """Whether *word* is a verb wearing the third-person singular -s. + + Unlike -ed and -ing, a trailing -s is weak evidence on its own -- + plural nouns wear one too ("status report") -- so it asks for + corroboration: the stem has to be a verb we already know. "tests" is + genuinely both, and this reads it as the verb. + """ + # "address" and "process" end in -ss without being third person. + if not word.endswith("s") or word.endswith("ss"): + return False + stems = {word[:-1]} + if word.endswith("es"): + stems.add(word[:-2]) + if word.endswith("ies"): + stems.add(word[:-3] + "y") + return any(stem in IMPERATIVES for stem in stems) class SubjectLengthValidator(SubjectValidator): diff --git a/commit_check/imperatives.py b/commit_check/imperatives.py index 3a93e3fe..1b68555b 100644 --- a/commit_check/imperatives.py +++ b/commit_check/imperatives.py @@ -547,19 +547,10 @@ } -# Words the morphology rule in SubjectImperativeValidator would misread. -# -# That rule rejects a subject whose first word carries non-imperative -# morphology -- a past tense, a gerund, or a third-person singular. English -# spelling being what it is, a handful of words end in those letters without -# being those forms, and they are the only cases the rule gets wrong. There -# are few enough to enumerate honestly, which is why the check can rely on -# shape rather than on a vocabulary of every imperative verb in the language. -# -# The adverbs are here rather than in IMPERATIVES on purpose. "always" is not -# an imperative verb, and putting it in a set by that name would be a lie -# about what the set contains; what is true is that "always quote the path" -# is a correct imperative subject, and that the -s rule would reject it. +# Imperative verbs that end in -ed or -ing without being a past tense or a +# gerund, and so would be misread by the morphology rule in +# SubjectImperativeValidator. Short enough to enumerate, which is what lets +# that rule work on shape instead of on a vocabulary of every English verb. NON_IMPERATIVE_LOOKALIKES = { # -ed, but not a past tense "bleed", @@ -587,10 +578,4 @@ "string", "swing", "wring", - # -s, but not a third-person singular. Words ending -ss are handled by - # the rule itself, so only the single-s ones need naming here. - "focus", - # adverbs that legitimately lead an imperative subject - "always", - "sometimes", } diff --git a/tests/engine_test.py b/tests/engine_test.py index 0a75e3c2..26075399 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -2601,14 +2601,13 @@ def test_inflected_first_words_are_rejected(self, subject): "subject", [ # Correct imperatives that no list contained, or ever would. - "feat: settle the report format", - "refactor: inline the helper", - "chore: retire the legacy path", - "fix: tighten the guard", - "fix: treat an absent value as missing", - # Adverb-led subjects. Correct imperative English that a list of - # *verbs* cannot represent without becoming a list of not-verbs. - "fix: always quote the path", + "feat: reword the report format", + "refactor: dedupe the helper", + "chore: decommission the legacy path", + "fix: loosen the guard", + "fix: backfill an absent value", + # Adverb-led subjects, which a list of *verbs* cannot represent + # without becoming a list of not-verbs. "feat: optionally skip the hook", "fix: explicitly close the handle", # British spelling, which is not a mistake. @@ -2629,16 +2628,30 @@ def test_uninflected_first_words_pass_without_being_listed(self, subject): "feat: bring back the flag", # -ing, not a gerund "fix: string the parts together", "fix: ping the endpoint", - "fix: address the warning", # -ss, not third person - "fix: process the queue", - "fix: guess the encoding", - "fix: focus the search", # -s, not third person ], ) def test_lookalikes_are_not_mistaken_for_inflection(self, subject): """The words a suffix rule gets wrong, enumerated rather than guessed.""" assert self._verdict(subject) == ValidationResult.PASS + @pytest.mark.benchmark + @pytest.mark.parametrize( + "subject", + [ + "fix: address the warning", # -ss, never third person + "fix: process the queue", + "fix: guess the encoding", + "fix: focus the search", # -s, but the stem is not a verb + "fix: status report is empty", # noun-led + "chore: deps bump", + "fix: always quote the path", # adverb-led + "fix: sometimes the cache is stale", + ], + ) + def test_single_s_words_are_not_assumed_third_person(self, subject): + """A trailing -s only counts when the stem is a verb we know.""" + assert self._verdict(subject) == ValidationResult.PASS + @pytest.mark.benchmark def test_a_noun_led_subject_now_passes(self): """Documents the deliberate loosening, so a change to it is a choice. From 6430b8fd98cd3d42849e14d4c36ff4fae8f77037 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sat, 8 Aug 2026 12:24:47 +0000 Subject: [PATCH 3/6] fix: quote the codespell ignore list so the comma survives In a YAML flow sequence an unquoted comma is an item separator, so [--ignore-words-list=iterm,sting] passed codespell two arguments and it read 'sting' as a path. Verified with 'pre-commit run codespell'. --- .pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b54ee73e..b5737f12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,8 @@ repos: - id: codespell # iTerm is a terminal emulator, named in the hyperlink support probe. # "sting" is a real verb, listed in NON_IMPERATIVE_LOOKALIKES. - args: [--ignore-words-list=iterm,sting] + # Quoted: an unquoted comma splits the flow sequence into two args. + args: ["--ignore-words-list=iterm,sting"] - repo: https://github.com/commit-check/commit-check rev: v2.11.0 hooks: From f6ecd9f993cb5de9a6a3e589f92862c19108c29c Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sat, 8 Aug 2026 12:27:30 +0000 Subject: [PATCH 4/6] test: check the -ies stem, which nothing exercised Codecov found the branch uncovered: no subject in the suite led with an -ies word, so 'tries' -> 'try' was never taken. --- tests/engine_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/engine_test.py b/tests/engine_test.py index 26075399..9f6714d8 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -2590,6 +2590,8 @@ def _verdict(self, subject): "fix: fixes the parser", # third person "fix: removes the flag", "docs: documents the API", + "fix: tries the fallback", # -ies, so the stem is "try" + "fix: applies the patch", ], ) def test_inflected_first_words_are_rejected(self, subject): From eb546e572f8a225153ae76f5eebc0920596ba3d7 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sat, 8 Aug 2026 18:16:36 +0000 Subject: [PATCH 5/6] refactor: stop asking the verb list whether a subject passes The IMPERATIVES fast path was protecting exactly four words -- embed, feed, ping, speed -- and all four are already in NON_IMPERATIVE_LOOKALIKES, so it changed no verdict. Checked over the 2,333 distinct first words in git.git's history: zero differences, 576 rejections either way. Removing it leaves the morphology as the only thing that decides, and moves the lookalike check into _is_inflected where it belongs, since those words exist because of the suffix test rather than beside it. IMPERATIVES stays for the -s stem test, which genuinely needs it, but its header no longer tells contributors to treat a rejected subject as a missing entry. That contract is what #526 was about, and it is gone: an absent verb now costs a missed violation, never a false rejection. --- commit_check/engine.py | 6 ++---- commit_check/imperatives.py | 18 ++++++++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/commit_check/engine.py b/commit_check/engine.py index 91e5b5a4..62dabf57 100644 --- a/commit_check/engine.py +++ b/commit_check/engine.py @@ -422,10 +422,6 @@ def _validate_subject(self, subject: str) -> ValidationResult: first_word = match.group(1).lower() - # Fast path, and the words whose spelling would trip the test below. - if first_word in IMPERATIVES or first_word in NON_IMPERATIVE_LOOKALIKES: - return ValidationResult.PASS - if self._is_inflected(first_word): self._print_failure(subject) return ValidationResult.FAIL @@ -435,6 +431,8 @@ def _validate_subject(self, subject: str) -> ValidationResult: @classmethod def _is_inflected(cls, word: str) -> bool: """Whether *word* carries past-tense, gerund or third-person marking.""" + if word in NON_IMPERATIVE_LOOKALIKES: + return False if word.endswith(cls._INFLECTED): return True return cls._is_third_person(word) diff --git a/commit_check/imperatives.py b/commit_check/imperatives.py index 1b68555b..b40fda01 100644 --- a/commit_check/imperatives.py +++ b/commit_check/imperatives.py @@ -2,17 +2,15 @@ # https://github.com/crate-ci/imperative/blob/master/assets/imperatives.txt # and extended since. # -# 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. +# This is NOT an allow-list, and a subject is never rejected for being absent +# from it. CC003 decides on the word's form (see SubjectImperativeValidator), +# and the only thing this set is still consulted for is the stem test behind +# the third-person -s rule: "fixes" is a verb because "fix" is in here, while +# "status" is not, because dropping its -s leaves nothing this set contains. # -# 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. +# So a missing verb costs a missed violation, never a false rejection -- there +# is nothing to keep up with, and no need to send a patch adding the verb you +# just used. Additions still help the -s rule catch more, and cost nothing. IMPERATIVES = { "abort", From 0b824354a4d31cddfa7934a929b79c5638532ae7 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sat, 8 Aug 2026 18:25:30 +0000 Subject: [PATCH 6/6] fix: complete the two word families the morphology depends on Two real false rejections, both found in review. Non-verbs in IMPERATIVES now reject plural nouns, because the set is a stem oracle rather than a gate: 'partial' and 'setup' made 'partials' and 'setups' read as third-person verbs. Removed, along with 'auto'. Kept 'init', 'polyfill' and 'abstract' -- those are verbs people really do write, and their plurals are the same irreducible ambiguity as 'tests'. NON_IMPERATIVE_LOOKALIKES was missing members of both families, so 'weed out the dead code' failed. Rather than add the three that came up, enumerate the rest of both closed sets: heed, wed, weed, and ding, sling, wing, zing. A half-listed closed set is the 'add my word' treadmill again, in miniature. git.git corpus: 574 rejected, from 576. --- commit_check/imperatives.py | 16 +++++++++++++--- tests/engine_test.py | 6 ++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/commit_check/imperatives.py b/commit_check/imperatives.py index b40fda01..ee97faee 100644 --- a/commit_check/imperatives.py +++ b/commit_check/imperatives.py @@ -42,7 +42,6 @@ "authenticate", "authorise", "authorize", - "auto", "automate", "avoid", "await", @@ -302,7 +301,6 @@ "parameterise", "parameterize", "parse", - "partial", "pass", "pause", "perform", @@ -439,7 +437,6 @@ "serve", "set", "settle", - "setup", "shard", "shorten", "show", @@ -549,6 +546,12 @@ # gerund, and so would be misread by the morphology rule in # SubjectImperativeValidator. Short enough to enumerate, which is what lets # that rule work on shape instead of on a vocabulary of every English verb. +# +# Both groups are meant to be the whole family rather than a sample of it -- +# a half-enumerated closed set would put back exactly the "add my word" +# treadmill this check was rebuilt to escape. Err towards including a word: +# an entry that never comes up costs nothing, a missing one rejects someone +# who wrote correct English. NON_IMPERATIVE_LOOKALIKES = { # -ed, but not a past tense "bleed", @@ -556,6 +559,7 @@ "embed", "exceed", "feed", + "heed", "need", "proceed", "seed", @@ -564,16 +568,22 @@ "speed", "spread", "succeed", + "wed", + "weed", # -ing, but not a gerund "bring", "cling", + "ding", "fling", "ping", "ring", "sing", + "sling", "spring", "sting", "string", "swing", + "wing", "wring", + "zing", } diff --git a/tests/engine_test.py b/tests/engine_test.py index 9f6714d8..ffba921a 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -2630,6 +2630,9 @@ def test_uninflected_first_words_pass_without_being_listed(self, subject): "feat: bring back the flag", # -ing, not a gerund "fix: string the parts together", "fix: ping the endpoint", + "chore: weed out the dead code", + "fix: heed the configured timeout", + "feat: sling the payload over the wire", ], ) def test_lookalikes_are_not_mistaken_for_inflection(self, subject): @@ -2646,6 +2649,9 @@ def test_lookalikes_are_not_mistaken_for_inflection(self, subject): "fix: focus the search", # -s, but the stem is not a verb "fix: status report is empty", # noun-led "chore: deps bump", + # Stems that are not verbs at all, so their plurals are nouns. + "fix: partials are rendered twice", + "chore: setups differ between CI and local", "fix: always quote the path", # adverb-led "fix: sometimes the cache is stale", ],