scripts.check_ai_attribution

Reference › scripts › check_ai_attribution

The AI-attribution gate, the executable mechanism for ruling RUL-007. It scans commit messages and tag messages for AI-attribution text, so that Mohamed remains the sole stated author. tests/scripts/test_check_ai_attribution.py imports it. It is meant to run from a commit-message hook, from a gate over a revision range, or by hand.

Invocation: main takes --repo-root (default the current directory) and at least one mode, otherwise exits 2. The modes are --message-file PATH (a message being committed), a revision range option or --all (existing commits), --tags (tag messages) and --stdin. It also takes a baseline file of already-known findings (--baseline, with a default location under the repo) and --json for a machine-readable report. The exit code comes from the report: findings not covered by the baseline fail the gate, and operational errors return 2.

scan_text applies the patterns to one text and returns Finding records. strip_commit_comments removes lines Git treats as comments before scanning, using the repository's comment character (_comment_char). iter_commit_messages and iter_tag_messages read history through git (_git). check_message_file, check_stdin, check_range and check_tags wrap those for each mode, and build_report and print_text_report produce the output.

At a glance

Source scripts/check_ai_attribution.py
Lines of code 460
Classes 1
Functions 17
Imports (internal) none
Imported by none
Tests that import it tests/scripts/test_check_ai_attribution.py

Class diagram

Class diagram scripts.check_ai_attribution

Dependency diagram

No internal imports and no importers.

API

Constants

Name Type Value
_RULES tuple[tuple[str, re.Pattern[str]], ...] (('R1', _R1), ('R2', _R2))

Classes

Finding (dataclass)

Decorators: dataclasses.dataclass(frozen=True) · Line 100

One violating line, located precisely enough to fix by hand.

Attribute Type Default
kind str
ref str
rule str
line_no int
line str
match str
baselined bool False
baseline_reason str ''
Method Signature Summary
as_dict as_dict() -> dict[str, object] Plain-dict form for --json output.
render render() -> str One-line human-readable form: where it is and which rule caught it.

Functions

Function Signature Summary
scan_text scan_text(text: str, *, kind: str, ref: str) -> list[Finding] Return every violating line in text (one finding per line).
strip_commit_comments strip_commit_comments(text: str, comment_char: str = '#') -> str Blank out git comment lines and truncate at the verbose-diff scissors.
load_baseline load_baseline(path: Path) -> dict[str, str] Parse <40-hex-sha> <reason> lines.
_git _git(repo_root: Path, **args: str) -> str
_comment_char _comment_char(repo_root: Path) -> str
iter_commit_messages iter_commit_messages(repo_root: Path, rev_range: str | None) -> Iterator[tuple[str, str]] Yield (full_sha, message) for every commit in rev_range.
iter_tag_messages iter_tag_messages(repo_root: Path) -> Iterator[tuple[str, str]] Yield (tag_name, message) for every ANNOTATED tag.
check_message_file check_message_file(path: Path, comment_char: str = '#') -> list[Finding] Scan a commit-message file, minus git comments and the verbose diff.
check_stdin check_stdin(text: str) -> list[Finding] Scan arbitrary text verbatim — PR bodies are markdown, so # is a heading, not a comment, and nothing is stripped.
check_range check_range(repo_root: Path, rev_range: str | None, baseline: dict[str, str]) -> list[Finding] Scan commit messages in rev_range, marking baselined shas.
check_tags check_tags(repo_root: Path) -> list[Finding] Scan annotated tag messages.
build_report build_report(findings: Sequence[Finding], modes: Sequence[str]) -> dict Split findings into new vs baselined and derive the exit code.
print_text_report print_text_report(report: dict, findings: Sequence[Finding]) -> None Human-readable report: every finding, then the PASS/FAIL verdict.
_add_mode_arguments _add_mode_arguments(parser: argparse.ArgumentParser) -> None Register the four scan modes; at least one is required at runtime.
build_parser build_parser() -> argparse.ArgumentParser The CLI surface: four scan modes plus output/scoping options.
collect_findings collect_findings(args: argparse.Namespace, repo_root: Path, baseline: dict[str, str]) -> tuple[list[Finding], list[str]] Run every requested mode; return its findings and the mode names.
main main(argv: list[str] | None = None) -> int Entry point: parse, scan, report, and return the gate's exit code.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/check_ai_attribution.py

#!/usr/bin/env python3
"""AI-attribution gate — the executable mechanism for ruling RUL-007.

Scans **commit / tag / PR message TEXT only** — never file contents, never the
diff. That scoping decision is what keeps the gate silent on its own source, on
``CLAUDE.md``, on the wiki ruling page that quotes the forbidden trailer
verbatim, and on the whole SPEC-021 / SPEC-025 Claude-Code-producer subsystem,
all of which legitimately contain the exact strings this gate forbids in a
message.

Modes (combinable):

``--message-file PATH``
    The ``commit-msg`` hook moment: git passes ``$1``. Blocks *before* the
    commit object exists, the only point where blocking costs nothing.
``--range [REV_RANGE]``
    Repo-history scan for CI. With no value (or with ``--all``) every commit
    reachable from every ref is scanned. Baseline-aware.
``--tags``
    Annotated tag messages via ``git for-each-ref``. Strict, no baseline.
``--stdin``
    Arbitrary text, e.g. ``gh pr create --body-file -``.

Exit codes: ``0`` clean (baselined findings are reported but do not fail),
``1`` at least one new violation, ``2`` usage / environment error.
"""

from __future__ import annotations

import argparse
import dataclasses
import json
import re
import subprocess
import sys
from collections.abc import Iterator, Sequence
from pathlib import Path

# ---------------------------------------------------------------------------
# The ruling this script mechanises. Kept verbatim so `scripts/rulings_lint.py`
# can prove the `gate scripts/check_ai_attribution.py RUL-007` declaration on
# the wiki page resolves to a script that actually names its rule.
# ---------------------------------------------------------------------------
RULING_TEXT = (
    "RUL-007 — No AI attribution in commits, tags, or PRs. Do not add "
    "`Co-Authored-By: Claude ...` (or any equivalent attribution to any AI "
    "assistant) to git commits, tags, PR bodies, or any other artifact. Every "
    "commit/tag carries Mohamed's identity only. Currently declared "
    "`none (judgment)`; the page's own Verification section admits "
    '"no mechanism currently strips a stray Co-Authored-By trailer '
    'automatically." Ruling page: '
    "/home/mohamed/workspace/knowledge-wiki/rulings/RUL-007-no-ai-attribution.md"
)

RULE_ID = "RUL-007"

DEFAULT_BASELINE = Path("scripts") / "rul007-baseline.txt"

# ---------------------------------------------------------------------------
# Grammar. Both families match attribution SHAPE plus an AI-identity token —
# never a bare vendor word, so a commit whose SUBJECT is Claude Code stays legal.
# ---------------------------------------------------------------------------
_AI_IDENT = (
    r"\b(?:claude|anthropic|chatgpt|openai|gpt-[0-9]|copilot|gemini|codex"
    r"|cursor|devin|aider|kimi|deepseek|qwen|minimax|codewhale|llm"
    r"|ai assistant)"
)

_TRAILER_KEY = (
    r"(?:co-authored-by|co-authored-with|assisted-by|generated-by"
    r"|authored-by|claude-session|ai-assisted-by)"
)

# R1 — attribution trailer. `^[ \t]*` anchoring is load-bearing: a trailer
# quoted MID-LINE (e.g. a commit message describing this very gate) passes.
_R1 = re.compile(rf"^[ \t]*{_TRAILER_KEY}[ \t]*:.*{_AI_IDENT}", re.IGNORECASE)

# R2 — generator marker. `\bgenerated with\b` is load-bearing: the leading `e`
# of "regenerated" kills the word boundary, so "regenerated with file_meta"
# — a real commit subject in this repo — never matches.
_R2 = re.compile(
    r"\U0001f916[ \t]*\bgenerated with\b"
    rf"|\bgenerated with \[?{_AI_IDENT}"
    r"|https?://(?:claude\.ai/code|claude\.com/claude-code)"
    rf"|\bco-authored with\b[ \t]+{_AI_IDENT}",
    re.IGNORECASE,
)

_RULES: tuple[tuple[str, re.Pattern[str]], ...] = (
    ("R1", _R1),
    ("R2", _R2),
)

# git's scissors marker: everything below it is the verbose diff, not the
# message. Scanning past it would smuggle file content into a message-only gate.
_SCISSORS = re.compile(r"^\s*\S?\s*-{3,}\s*>8\s*-{3,}")


@dataclasses.dataclass(frozen=True)
class Finding:
    """One violating line, located precisely enough to fix by hand."""

    kind: str
    ref: str
    rule: str
    line_no: int
    line: str
    match: str
    baselined: bool = False
    baseline_reason: str = ""

    def as_dict(self) -> dict[str, object]:
        """Plain-dict form for ``--json`` output."""
        return dataclasses.asdict(self)

    def render(self) -> str:
        """One-line human-readable form: where it is and which rule caught it."""
        head = f"{self.kind} {self.ref} L{self.line_no}: {self.line.strip()}"
        tail = f"  [{self.rule}]"
        if self.baselined:
            tail += f" BASELINED ({self.baseline_reason})"
        return head + tail


# ---------------------------------------------------------------------------
# Text scanning
# ---------------------------------------------------------------------------
def scan_text(text: str, *, kind: str, ref: str) -> list[Finding]:
    """Return every violating line in ``text`` (one finding per line)."""
    findings: list[Finding] = []
    for line_no, line in enumerate(text.splitlines(), start=1):
        for rule, pattern in _RULES:
            match = pattern.search(line)
            if match is not None:
                findings.append(
                    Finding(
                        kind=kind,
                        ref=ref,
                        rule=rule,
                        line_no=line_no,
                        line=line.rstrip(),
                        match=match.group(0),
                    )
                )
                break
    return findings


def strip_commit_comments(text: str, comment_char: str = "#") -> str:
    """Blank out git comment lines and truncate at the verbose-diff scissors.

    Line numbering is preserved (comments become empty lines) so reported
    positions still point at the right line of the on-disk message file.
    """
    kept: list[str] = []
    for line in text.splitlines():
        if _SCISSORS.match(line):
            break
        if comment_char and line.lstrip().startswith(comment_char):
            kept.append("")
        else:
            kept.append(line)
    return "\n".join(kept)


# ---------------------------------------------------------------------------
# Baseline
# ---------------------------------------------------------------------------
_FULL_SHA = re.compile(r"^[0-9a-f]{40}$")


def load_baseline(path: Path) -> dict[str, str]:
    """Parse ``<40-hex-sha>  <reason>`` lines. Missing file => empty baseline.

    Full-SHA pinning is deliberate: a baseline entry can never widen to cover a
    future commit.
    """
    if not path.exists():
        return {}
    baseline: dict[str, str] = {}
    for lineno, raw in enumerate(
        path.read_text(encoding="utf-8").splitlines(), start=1
    ):
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        sha, _, reason = line.partition(" ")
        if not _FULL_SHA.match(sha):
            raise ValueError(
                f"{path}:{lineno}: baseline entries must start with a full "
                f"40-hex commit sha, got {sha!r}"
            )
        baseline[sha] = reason.strip() or "(no reason recorded)"
    return baseline


# ---------------------------------------------------------------------------
# git plumbing
# ---------------------------------------------------------------------------
def _git(repo_root: Path, *args: str) -> str:
    proc = subprocess.run(
        ["git", *args],
        cwd=str(repo_root),
        capture_output=True,
        text=True,
        check=False,
    )
    if proc.returncode != 0:
        raise RuntimeError(
            f"git {' '.join(args)} failed (rc={proc.returncode}): "
            f"{proc.stderr.strip()}"
        )
    return proc.stdout


def _comment_char(repo_root: Path) -> str:
    proc = subprocess.run(
        ["git", "config", "--get", "core.commentChar"],
        cwd=str(repo_root),
        capture_output=True,
        text=True,
        check=False,
    )
    value = proc.stdout.strip()
    if not value or value == "auto":
        return "#"
    return value


def iter_commit_messages(
    repo_root: Path, rev_range: str | None
) -> Iterator[tuple[str, str]]:
    """Yield ``(full_sha, message)`` for every commit in ``rev_range``.

    ``rev_range is None`` means every commit reachable from every ref.
    """
    args = ["log", "--no-color", "--format=%H%n%B%x00"]
    args.append(rev_range if rev_range else "--all")
    for record in _git(repo_root, *args).split("\0"):
        record = record.lstrip("\n")
        if not record.strip():
            continue
        sha, _, message = record.partition("\n")
        yield sha.strip(), message


def iter_tag_messages(repo_root: Path) -> Iterator[tuple[str, str]]:
    """Yield ``(tag_name, message)`` for every ANNOTATED tag."""
    fmt = "--format=%(objecttype)%09%(refname:short)%n%(contents)%00"
    for record in _git(repo_root, "for-each-ref", fmt, "refs/tags").split("\0"):
        record = record.lstrip("\n")
        if not record.strip():
            continue
        header, _, contents = record.partition("\n")
        objtype, _, name = header.partition("\t")
        if objtype.strip() != "tag":
            continue
        yield name.strip(), contents


# ---------------------------------------------------------------------------
# Modes
# ---------------------------------------------------------------------------
def check_message_file(path: Path, comment_char: str = "#") -> list[Finding]:
    """Scan a commit-message file, minus git comments and the verbose diff."""
    text = path.read_text(encoding="utf-8", errors="replace")
    return scan_text(
        strip_commit_comments(text, comment_char),
        kind="message-file",
        ref=str(path),
    )


def check_stdin(text: str) -> list[Finding]:
    """Scan arbitrary text verbatim — PR bodies are markdown, so `#` is a
    heading, not a comment, and nothing is stripped."""
    return scan_text(text, kind="stdin", ref="-")


def check_range(
    repo_root: Path, rev_range: str | None, baseline: dict[str, str]
) -> list[Finding]:
    """Scan commit messages in ``rev_range``, marking baselined shas."""
    findings: list[Finding] = []
    for sha, message in iter_commit_messages(repo_root, rev_range):
        for finding in scan_text(message, kind="commit", ref=sha[:12]):
            if sha in baseline:
                finding = dataclasses.replace(
                    finding, baselined=True, baseline_reason=baseline[sha]
                )
            findings.append(finding)
    return findings


def check_tags(repo_root: Path) -> list[Finding]:
    """Scan annotated tag messages. Strict: tag messages carry no baseline."""
    return [
        finding
        for name, message in iter_tag_messages(repo_root)
        for finding in scan_text(message, kind="tag", ref=name)
    ]


# ---------------------------------------------------------------------------
# Report
# ---------------------------------------------------------------------------
def build_report(findings: Sequence[Finding], modes: Sequence[str]) -> dict:
    """Split findings into new vs baselined and derive the exit code."""
    new = [f for f in findings if not f.baselined]
    baselined = [f for f in findings if f.baselined]
    return {
        "rule_id": RULE_ID,
        "modes": list(modes),
        "ok": not new,
        "exit_code": 1 if new else 0,
        "new_violations": [f.as_dict() for f in new],
        "baselined": [f.as_dict() for f in baselined],
    }


def print_text_report(report: dict, findings: Sequence[Finding]) -> None:
    """Human-readable report: every finding, then the PASS/FAIL verdict."""
    modes = ", ".join(report["modes"]) or "(none)"
    print(f"{RULE_ID} AI-attribution gate — modes: {modes}")
    for finding in findings:
        print(f"  {finding.render()}")
    new_count = len(report["new_violations"])
    base_count = len(report["baselined"])
    if new_count:
        print(
            f"FAIL: {new_count} AI-attribution violation(s) "
            f"({base_count} baselined, not counted)."
        )
        print(
            "  Remove the attribution trailer/marker. "
            "Every commit and tag carries Mohamed's identity only."
        )
    else:
        print(f"PASS: no new AI-attribution violations ({base_count} baselined).")


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def _add_mode_arguments(parser: argparse.ArgumentParser) -> None:
    """Register the four scan modes; at least one is required at runtime."""
    parser.add_argument(
        "--message-file",
        default=None,
        help="Scan a commit-message file (the commit-msg hook's $1).",
    )
    parser.add_argument(
        "--range",
        dest="rev_range",
        nargs="?",
        const="",
        default=None,
        help="Scan commit messages in a rev-range (default: every ref).",
    )
    parser.add_argument(
        "--all",
        action="store_true",
        help="With --range: scan every commit reachable from every ref.",
    )
    parser.add_argument(
        "--tags", action="store_true", help="Scan annotated tag messages."
    )
    parser.add_argument(
        "--stdin", action="store_true", help="Scan arbitrary text from stdin."
    )


def build_parser() -> argparse.ArgumentParser:
    """The CLI surface: four scan modes plus output/scoping options."""
    parser = argparse.ArgumentParser(
        prog="check_ai_attribution.py",
        description=(
            f"{RULE_ID} gate — reject AI attribution in commit / tag / PR "
            "message text. Scans messages only, never file contents."
        ),
    )
    _add_mode_arguments(parser)
    parser.add_argument(
        "--repo-root", default=None, help="Repository to scan (default: cwd)."
    )
    parser.add_argument(
        "--baseline",
        default=None,
        help=f"Baseline file (default: {DEFAULT_BASELINE}).",
    )
    parser.add_argument("--json", action="store_true", help="Machine-readable output.")
    return parser


def collect_findings(
    args: argparse.Namespace, repo_root: Path, baseline: dict[str, str]
) -> tuple[list[Finding], list[str]]:
    """Run every requested mode; return its findings and the mode names."""
    findings: list[Finding] = []
    modes: list[str] = []
    if args.message_file:
        modes.append("message-file")
        findings.extend(
            check_message_file(Path(args.message_file), _comment_char(repo_root))
        )
    if args.rev_range is not None or args.all:
        modes.append("range")
        findings.extend(check_range(repo_root, args.rev_range or None, baseline))
    if args.tags:
        modes.append("tags")
        findings.extend(check_tags(repo_root))
    if args.stdin:
        modes.append("stdin")
        findings.extend(check_stdin(sys.stdin.read()))
    return findings, modes


def main(argv: list[str] | None = None) -> int:
    """Entry point: parse, scan, report, and return the gate's exit code."""
    args = build_parser().parse_args(argv)

    repo_root = Path(args.repo_root).resolve() if args.repo_root else Path.cwd()
    if not repo_root.is_dir():
        print(f"ERROR: repo root does not exist: {repo_root}", file=sys.stderr)
        return 2

    do_range = args.rev_range is not None or args.all
    if not (args.message_file or do_range or args.tags or args.stdin):
        print(
            "ERROR: pick at least one mode "
            "(--message-file / --range / --tags / --stdin).",
            file=sys.stderr,
        )
        return 2

    baseline_path = (
        Path(args.baseline) if args.baseline else repo_root / DEFAULT_BASELINE
    )
    try:
        baseline = load_baseline(baseline_path)
    except (OSError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2

    try:
        findings, modes = collect_findings(args, repo_root, baseline)
    except (OSError, RuntimeError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2

    report = build_report(findings, modes)
    if args.json:
        print(json.dumps(report, indent=2, default=str))
    else:
        print_text_report(report, findings)
    return int(report["exit_code"])


if __name__ == "__main__":
    raise SystemExit(main())