From 901b997dc2a713b7f1869cd34fa1e3666aa06c60 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:38:14 +0000 Subject: [PATCH 1/2] fix: show an AI-only check that ran no rule as skipped, and group the PR title with the description Under the default ai_attribution = "ignore" the PR description check ran no rule but showed a pass. The title and description now share a Pull request group, so the commit group is no longer split in two. --- README.md | 5 +++-- main.py | 35 +++++++++++++++++++++++++++++++++-- main_test.py | 44 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4b1e606..0f198ed 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,9 @@ click away: > Show all 4 checks > > ```text -> Commit message +> Pull request > ✔ PR title (feat: add login page) +> Commit message > ✔ Commit 1/2 (d87faca) (feat: add login page) > ✖ Commit 2/2 (5584f46) (1 failure) > CC001 message @@ -172,7 +173,7 @@ names the line to remove: > | PR description | `Claude Code` | [CC013 ai-attribution](https://commit-check.com/rules/#cc013) | > > ```text -> PR description +> Pull request > ✖ PR description (1 failure) > CC013 ai-attribution > value: Claude Code diff --git a/main.py b/main.py index 4c49559..13c45c2 100755 --- a/main.py +++ b/main.py @@ -432,6 +432,35 @@ def skipped_author_scope(flag: str) -> ScopeResult: ) +#: Why an AI-only scope ran no rule: under the default policy there is none to run. +AI_POLICY_IGNORED = 'ai_attribution is "ignore": set it to "forbid" or "disclose"' + + +def skip_when_no_ai_rule_ran(scope: ScopeResult) -> ScopeResult: + """Report an AI-only scope that ran no rule as skipped, not passed. + + Under the default ``ai_attribution = "ignore"`` the CLI builds none of + the AI rules, so it returns an empty list of checks. Shown as a pass, that + read as "checked, nothing found" to someone who turned the input on and + forgot the policy. + """ + if scope.checks or scope.raw_text: + return scope + scope.checks = [ + { + "rule_id": "CC013", + "check": "ai_attribution", + "status": "skip", + "value": "", + "error": "", + "suggest": "", + "docs_url": "", + "reason": AI_POLICY_IGNORED, + } + ] + return scope + + def get_pr_title() -> str | None: """Read PR title from GitHub event payload.""" if not is_pr_event(): @@ -856,7 +885,7 @@ def run_commit_check() -> tuple[int, list[ScopeResult]]: ) if PR_BODY_AI_ATTRIBUTION_ENABLED: name_the_line_changes(scope, pr_body) - results.append(scope) + results.append(skip_when_no_ai_rule_ran(scope)) else: PR_BODY_AI_TOOLS[:] = scope.ai_tools @@ -924,7 +953,9 @@ def _rule_markdown_link(check: dict[str, str]) -> str: def _scope_group(label: str) -> str: """Group name for a scope label, used to fold the step log output.""" - if label == "PR title" or label.startswith("Commit"): + if label in ("PR title", "PR description"): + return "Pull request" + if label.startswith("Commit"): return "Commit message" if label.startswith("Author"): return "Author" diff --git a/main_test.py b/main_test.py index ad060cb..5ddc8a3 100644 --- a/main_test.py +++ b/main_test.py @@ -1735,11 +1735,22 @@ def test_groups_scopes_by_category(self): fail_scope("Branch"), ] output = self._run(results) - # One group for commit-message scopes, one for the branch scope. - self.assertEqual(output.count("::group::"), 2) + # One group each for the pull request, the commits and the branch. + self.assertEqual(output.count("::group::"), 3) + self.assertIn("::group::Pull request", output) self.assertIn("::group::Commit message", output) self.assertIn("::group::Branch", output) + def test_title_and_description_share_one_group(self): + """The commits come after both, so the commit group is not split.""" + results = [ + pass_scope("PR title"), + pass_scope("PR description"), + pass_scope("Commit 1/1"), + ] + groups = [name for name, _ in main._grouped(results)] + self.assertEqual(groups, ["Pull request", "Commit message"]) + def test_raw_text_fallback_is_printed(self): scope = main.ScopeResult(label="Branch", raw_text="unexpected output") output = self._run([scope]) @@ -1801,8 +1812,9 @@ def test_success_golden_output(self): "Show all 4 checks\n" "\n" "```text\n" - "Commit message\n" + "Pull request\n" " ✔ PR title (feat: add login page)\n" + "Commit message\n" " ✔ Commit 1/2 (feat: add user auth)\n" " ✔ Commit 2/2 (fix: resolve timeout)\n" "Branch\n" @@ -1839,8 +1851,9 @@ def test_failure_golden_output(self): "Show all 3 checks\n" "\n" "```text\n" - "Commit message\n" + "Pull request\n" " ✔ PR title (feat: add login page)\n" + "Commit message\n" " ✖ Commit 2/2 (1 failure)\n" " CC001 message\n" " value: bad message\n" @@ -1890,8 +1903,9 @@ def test_failure_golden_output_with_commits(self): "Show all 5 checks\n" "\n" "```text\n" - "Commit message\n" + "Pull request\n" " ✔ PR title (feat: Add login page)\n" + "Commit message\n" " ✖ Commit 1/3 (d87faca) (1 failure)\n" " CC001 message\n" " value: bad message\n" @@ -2950,8 +2964,9 @@ def test_all_skipped_golden_output(self): "Show all 2 checks\n" "\n" "```text\n" - "Commit message\n" + "Pull request\n" " ⊘ PR title (skipped)\n" + "Commit message\n" " ⊘ Commit 1/1 (skipped)\n" "```\n" "\n" @@ -3198,7 +3213,7 @@ def test_warning_only_golden_output(self): "Show all 2 checks\n" "\n" "```text\n" - "Commit message\n" + "Pull request\n" " ✔ PR title (feat: add login page)\n" "Branch\n" " ⚠ Branch (1 warning)\n" @@ -3720,6 +3735,21 @@ def test_pr_body_is_a_scope_checked_with_the_ai_rules_only(self): ) self.assertEqual([r.label for r in results], ["PR description"]) + def test_no_ai_rule_ran_reads_as_skipped_not_passed(self): + """Under ai_attribution = "ignore" the CLI runs no rule on the description.""" + results, _ = self._run(True, "false") + self.assertEqual(results[0].status, "skip") + self.assertEqual(results[0].skip_reason, main.AI_POLICY_IGNORED) + + def test_a_rule_that_ran_keeps_its_verdict(self): + checks = [{"rule_id": "CC013", "check": "ai_attribution", "status": "pass"}] + scope = main.ScopeResult(label="PR description", checks=list(checks)) + self.assertEqual(main.skip_when_no_ai_rule_ran(scope).checks, checks) + + def test_unparsable_output_still_fails(self): + scope = main.ScopeResult(label="PR description", raw_text="Error: bad config") + self.assertEqual(main.skip_when_no_ai_rule_ran(scope).status, "fail") + def test_ai_label_alone_reads_it_but_reports_no_scope(self): results, mock_scope = self._run(False, "true") mock_scope.assert_called_once() From 4e433ec22d040ecfcd2825252011cb62eb4e58f0 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:38:46 +0000 Subject: [PATCH 2/2] ci: forbid AI attribution in this repo so the self-test checks the PR description --- commit-check.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/commit-check.toml b/commit-check.toml index e49cac4..3e5f77c 100644 --- a/commit-check.toml +++ b/commit-check.toml @@ -2,6 +2,11 @@ inherit_from = "github:commit-check/.github:commit-check.toml" +[commit] +# Exercises pr-body-ai-attribution in the self-test: under the default +# "ignore" the PR description check runs no rule. +ai_attribution = "forbid" + [branch] require_rebase_target = "origin/main" allow_branch_names = ["create-pull-request/.+"]