diff --git a/docs/compare/tools.md b/docs/compare/tools.md index df29a16..6c5bd0b 100644 --- a/docs/compare/tools.md +++ b/docs/compare/tools.md @@ -16,7 +16,7 @@ for the current state. GitHub's own rulesets are on | [commitcheck](https://github.com/marketplace/commitcheck) | A hosted App that applies one regular expression, set in its web admin | Commit message, pull request title or description, by regex | GitHub App | Free for public repositories; $25 a month for private ones | | [DCO app](https://github.com/apps/dco) and [DCO-2](https://github.com/cncf/dco2) | The sign-off check the Linux kernel and the CNCF use, with a remediation flow for missing sign-offs | `Signed-off-by` trailers | GitHub App | Free, open source | | [PRLint](https://github.com/ewolfe/prlint), [PR Title Checker](https://github.com/marketplace/actions/pr-title-checker) | Regular expressions over pull request fields | Title, body, labels, branch | App / Action | Free, open source | -| **Commit Check** | One rule engine with [24 documented rules](../rules.md), run as a hook, a CLI, an Action, a hosted App or an MCP tool | Commit message, branch name, author name and email, sign-off, AI attribution, force pushes, file size and paths, tag names; the squash message of a pull request | All of the above | Free, open source; the App's private organization repositories move to a paid plan later | +| **Commit Check** | One rule engine with [22 documented rules](../rules.md), run as a hook, a CLI, an Action, a hosted App or an MCP tool | Commit message, branch name, author name and email, sign-off, AI attribution, force pushes, file size and paths, tag names; the squash message of a pull request | All of the above | Free, open source; the App's private organization repositories move to a paid plan later | ## What is different about Commit Check diff --git a/tests/docs_sync_test.py b/tests/docs_sync_test.py index 33edb9f..357e279 100644 --- a/tests/docs_sync_test.py +++ b/tests/docs_sync_test.py @@ -133,6 +133,28 @@ def test_every_rule_explains_itself(self): f"{entry.rule_id} ({entry.check}) section is missing {required}" ) + def test_prose_rule_counts_match_the_catalog(self): + """A page that counts the rules out loud must count them correctly. + + The comparison table sells the engine on how much it covers, so a + stale number there is worse than no number: it is the one figure a + reader checks against the rules page. Every other test here proves a + rule is *documented*; none of them read a sentence that says how + many there are. + """ + expected = len(ALL_RULES) + for path in sorted(DOCS.rglob("*.md")): + if path.parent.name == "posts": + continue # blog posts are dated records; see AGENTS.md + for claimed in _PROSE_RULE_COUNT.findall(path.read_text(encoding="utf-8")): + assert int(claimed) == expected, ( + f"{path.relative_to(DOCS.parent)} claims {claimed} rules, " + f"but the package defines {expected}" + ) + + +#: A prose claim about how many rules exist, e.g. ``24 documented rules``. +_PROSE_RULE_COUNT = re.compile(r"(\d+) documented rules") #: A pre-commit revision pin, e.g. ``rev: v2.13.1``. _REV_PIN = re.compile(r"^\s*rev:\s*v(\d+\.\d+\.\d+)\s*$", re.M)