From 6213d5e5dbf48a78bad861e3220da4b2a5ba4fca Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Mon, 7 Sep 2026 10:50:49 +0000 Subject: [PATCH] docs: the comparison table claims 24 rules, the package defines 22 The competitor table is where a reader checks the claim hardest, so an inflated count there costs more than it gains. docs/rules.md carries 22 { #ccNNN } anchors and ALL_RULES has 22 entries with a rule ID (CC001-CC013, CC101-CC102, CC201-CC202, CC301-CC304, CC401). The existing tests prove every rule is documented; none of them read a sentence that counts them, which is why this survived. A new test does, over every page outside docs/blog/posts (dated records, per AGENTS.md). It fails on the old number. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018xYa7m3qup5wyN5MaXFgf6 --- docs/compare/tools.md | 2 +- tests/docs_sync_test.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) 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)