diff --git a/docs/assets/demo-poster.png b/docs/assets/demo-poster.png new file mode 100644 index 0000000..8401e26 Binary files /dev/null and b/docs/assets/demo-poster.png differ diff --git a/docs/assets/demo.gif b/docs/assets/demo.gif new file mode 100644 index 0000000..506fa04 Binary files /dev/null and b/docs/assets/demo.gif differ diff --git a/docs/assets/merge-box.png b/docs/assets/merge-box.png new file mode 100644 index 0000000..a4e12f9 Binary files /dev/null and b/docs/assets/merge-box.png differ diff --git a/docs/compare/tools.md b/docs/compare/tools.md index 6c5bd0b..e9fb82a 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 [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 | +| **Commit Check** | One rule engine, [documented rule by rule](../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/docs/index.md b/docs/index.md index 4dd651e..ef1d3cd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,267 +2,42 @@ hide: - navigation - toc -template: home.html +template: landing.html title: Commit Check -description: Enforce commit message, branch naming, author and signoff standards across your CLI, pre-commit hooks, CI, and AI agents. +description: One config file enforced in your commit-msg hook, in CI, on every pull request and in your AI agent. Rules for commit messages, branch names, author identity and signoff. --- - - -# Commit Check { .cc-page-title } - -## One config, enforced everywhere - -Write the policy once. The same rules run on a developer's laptop, in CI, and in -whatever your AI agent is committing on your behalf. - -=== "Command line" - - ```console - $ commit-check --message --branch - CC003 subject-imperative check failed ==> docs: revamped the profile - Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug') - Suggest: Change the first verb to imperative form - Docs: https://commit-check.com/rules/#cc003 - ``` - -=== "pre-commit" - - ```yaml title=".pre-commit-config.yaml" - repos: - - repo: https://github.com/commit-check/commit-check - rev: v2.17.0 - hooks: - - id: check-message - - id: check-branch - ``` - -=== "GitHub Actions" - - ```yaml title=".github/workflows/commit-check.yml" - - uses: commit-check/commit-check-action@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - message: true - branch: true - pr-comments: ${{ github.event_name == 'pull_request' }} - ``` - -=== "AI agents" - - ```json title="MCP server" - { - "mcpServers": { - "commit-check": { - "command": "uvx", - "args": ["commit-check-mcp"] - } - } - } - ``` +
+
-## Start with two commands - -```console -$ pip install commit-check -$ commit-check --message --branch -``` +# One config file. Every place your team commits. -No configuration file needed to start — sensible defaults apply immediately, and -you tighten them when you are ready. Releases carry -[SLSA Level 3](https://slsa.dev) build provenance, so you can verify an artifact -came from this repository's pipeline before you install it. +Laptop, CI, pull request, AI agent — the same `cchk.toml`, the same rules, +the same diagnostics, with a fix you can paste. [Get started :octicons-arrow-right-24:](getting-started.md){ .md-button .md-button--primary } -[Rules reference](rules.md){ .md-button } - -## Why it exists - -Git history is a database that every team writes to and almost nobody validates. - -The cost shows up later, and indirectly. Release notes get written by hand -because commit subjects cannot be grouped. A `git bisect` ends on a merge -commit, where the change that broke the build could be in either parent or in -the resolution. A commit is attributed to `ec2-user` because a build box had no -`user.name`. A branch has its history rewritten months later because none of its -commits carried a `Signed-off-by` trailer. - -None of these are caught by a linter, a type checker, or a test suite. They are -all caught by review — which means inconsistently, by whoever happens to be -looking, and only after the work is done. - -Commit Check makes them mechanical instead, and catches them where it is -cheapest: the check that runs in CI is the same one that runs in your -`commit-msg` hook, where a malformed subject costs a second to fix rather than a -full CI cycle and a force-push. - -It treats commit metadata the way linters treat code — a policy written down -once, enforced identically everywhere, with a stable identifier for every -diagnostic so findings can be discussed, cited, and tracked. - -Not all of that policy is on to begin with. Two of the four problems above are -decisions rather than defects — whether merge commits belong in your history, -and whether contributors must sign off — and they stay off until you make them. -The [rules reference](rules.md#rule-index) marks which rules start on. - -## What it checks - -
- -- :material-message-text-outline:{ .lg .middle } __Commit messages__ - - --- - - Conventional Commits by default, or your own pattern. Subject length, mood, - capitalisation, required body, forbidden merge/fixup/WIP commits. - - [:octicons-arrow-right-24: CC001–CC013](rules.md#commit-message-rules) - -- :material-source-branch:{ .lg .middle } __Branch names__ - - --- - - Conventional Branch naming, plus rebase checks that catch a branch drifting - behind its target before CI wastes a run on stale code. - - [:octicons-arrow-right-24: CC201–CC202](rules.md#branch-rules) - -- :material-account-check-outline:{ .lg .middle } __Committer identity__ - - --- - - Catch commits authored by `ec2-user` on a build box, or require everyone to - contribute from a company address. - - [:octicons-arrow-right-24: CC101–CC102](rules.md#author-rules) - -- :material-file-sign:{ .lg .middle } __Signoff and DCO__ - - --- - - Require the `Signed-off-by` trailer locally, so contributors find out before - CI rejects the pull request. - - [:octicons-arrow-right-24: Policy guides](guides/policies.md#require-signoff-dco) - -- :material-robot-outline:{ .lg .middle } __AI attribution__ - - --- - - Whatever your project has decided about AI-assisted commits, enforce it - mechanically instead of relitigating it in review. - - [:octicons-arrow-right-24: Policy guides](guides/policies.md#ai-attribution) - -- :material-office-building-outline:{ .lg .middle } __Org-wide policy__ - - --- - - Inherit a base config from a shared repository, then let each project - override only what it needs. - - [:octicons-arrow-right-24: Across an organization](guides/organization.md) +[Browse the rules](rules.md){ .md-button }
+
-## What it is not - -Commit Check is deliberately narrow: it validates *metadata*, not code. - -- **Not a code linter.** It never reads your source files. -- **Not a replacement for review.** It enforces the mechanical rules so review - can spend its attention on the change itself. -- **Not opinionated by default.** Most rules are off until you turn them on. See - the [rules reference](rules.md) for what applies out of the box. - -It is a lightweight, open alternative to -[GitHub Enterprise metadata restrictions](https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#metadata-restrictions) -and Bitbucket's paid -[Yet Another Commit Checker](https://marketplace.atlassian.com/apps/1211854/yet-another-commit-checker), -without requiring a particular forge or an enterprise plan. If you already run -`ruff`, `eslint` or `golangci-lint` on your source, Commit Check is the -equivalent for the commits that carry it. - -## The rules GitHub sells by the seat - -GitHub can enforce some of the same policies natively, but the commit-metadata -rules sit behind its Enterprise plan, and they report a bare regex mismatch -where Commit Check reports a rule ID, a suggestion and a link. The -[comparison](compare/github-rules.md) has the table and the arithmetic. - -## Ecosystem - -One policy engine, multiple enforcement surfaces. Write your `cchk.toml` once — -every surface reads the same file. - -```mermaid -graph LR - Config["cchk.toml"] --> Engine["commit-check"] - - Engine --> Hook["CLI and pre-commit hook"] - Engine --> Action["commit-check-action"] - Engine --> App["Commit Check GitHub App"] - Engine --> MCP["commit-check-mcp"] - - Hook --> Dev["Your laptop"] - Action --> CI["CI pipeline"] - App --> PR["Every push and pull request, hosted"] - MCP --> Agent["AI coding agent"] +```console +$ echo 'Fix: add streaming support' | commit-check --message +CC001 message check failed ==> Fix: add streaming support +The commit message should follow Conventional Commits. See https://www.conventionalcommits.org +Suggest: Use "fix: add streaming support" +Docs: https://commit-check.com/rules/#cc001 ``` -
- -- :fontawesome-brands-python: __commit-check__ - - --- - - **Core engine** — Python CLI, library and pre-commit hooks. Runs every - validation the other surfaces expose. - - [:octicons-arrow-right-24: Getting started](getting-started.md) - [:octicons-arrow-right-24: Repo](https://github.com/commit-check/commit-check) - -- :material-github: __commit-check-action__ - - --- - - **GitHub Action** — CI integration that posts results as check runs, job - summaries and pull request comments. - - [:octicons-arrow-right-24: Guide](guides/github-actions.md) - [:octicons-arrow-right-24: Repo](https://github.com/commit-check/commit-check-action) - -- :material-check-decagram: __Commit Check App__ - - --- - - **GitHub App** — hosted; install once on an organization and every push - and pull request gets a check run, with no workflow file and no CI minutes. - - [:octicons-arrow-right-24: Guide](guides/github-app.md) - [:octicons-arrow-right-24: Marketplace](https://github.com/marketplace/commit-check) - -- :material-robot: __commit-check-mcp__ - - --- - - **MCP server** — exposes the validations as structured tools for AI coding - agents such as Claude Code, Cursor and Copilot. - - [:octicons-arrow-right-24: Guide](guides/mcp.md) - [:octicons-arrow-right-24: Repo](https://github.com/commit-check/commit-check-mcp) +It does not just say no. When the correction is unambiguous, it hands you +the line. +
-## Used by - -
+
**Commit Check runs in repositories across these organizations, and in [many more](https://github.com/commit-check/commit-check-action/network/dependents).** @@ -344,9 +119,245 @@ graph LR
---- +## Why check commit metadata at all + +
+ +- __Changelog tools have nothing to group by__ + + --- + + `git-cliff` and `semantic-release` read the `type:` prefix on the subject + line to decide what a commit was. Without a consistent subject there is + nothing to read, and the release notes get written by hand from `git log`. + +- __`git bisect` stops at a merge commit__ + + --- + + When the first bad commit is a merge, the change is in one of two parents + or in the conflict resolution. Bisect cannot narrow it any further. + +- __The author is `ec2-user`__ + + --- + + A build box with no `user.name` set writes itself into the history. + `git log --author` finds the commit; there is no person on the other end + of it. + +- __A DCO check fails on a branch you already wrote__ + + --- + + `Signed-off-by` costs one `-s` at commit time. Adding it afterwards means + `git rebase --signoff` across the whole branch and a force-push. + +
+ +None of these are caught by a linter, a type checker or a test suite. They are +caught in review — which means inconsistently, and after the work is done. + +The check that runs in CI is the same one that runs in your `commit-msg` hook. +Fixing a subject line at commit time costs a second; fixing it after CI costs a +full run and a force-push. + +## What your team actually sees + +On a pull request, every finding carries a rule ID, the value that failed, and +what to do about it — in the job summary, as annotations on the changed files, +and as a single comment that is edited in place rather than added to. + +| Scope | Checked value | Failed checks | +|---|---|---| +| Commit 2/2 (5584f46) | `bad msg` | CC001 message | +| Branch | `Feature/Add-Login` | CC201 branch | + +```text +Commit message + ✔ PR title (feat: add login page) + ✔ Commit 1/2 (d87faca) (feat: add login page) + ✖ Commit 2/2 (5584f46) (1 failure) + CC001 message + value: bad msg + The commit message should follow Conventional Commits. + Suggest: Use (): +Branch + ✖ Branch (1 failure) + CC201 branch + value: Feature/Add-Login + The branch should follow Conventional Branch. + Suggest: Rename the branch to "feature/Add-Login" (git branch -m feature/Add-Login) + Fix: feature/Add-Login +``` + +And in the merge box, where the decision actually gets made: + +
+![The Commit Check check run in a pull request merge box, reporting 2 of 4 checks failed](assets/merge-box.png){ loading=lazy } +
The hosted GitHub App reports one check run per commit. The title +names what failed, so nobody opens Details to learn whether it was the message, +the branch or an author email.
+
+ +
+![A terminal recording: a commit message and a branch name are rejected, then accepted once corrected](assets/demo.gif){ .cc-motion loading=lazy } +![The same terminal at the end of the recording, both checks having run](assets/demo-poster.png){ .cc-still loading=lazy } +
The same engine on the command line. The recording is replaced by a +still frame when your system asks for reduced motion.
+
+ +## Start with two commands + +```console +$ pip install commit-check +$ commit-check --message --branch +``` + +No configuration file needed to start — sensible defaults apply immediately, and +you tighten them when you are ready. Releases carry +[SLSA Level 3](https://slsa.dev) build provenance, so you can verify an artifact +came from this repository's pipeline before you install it. + +## Pick where it runs + +One policy engine, five places to enforce it. Every one of them reads the same +`cchk.toml`. + +
+ +- __Command line__ + + --- + + The engine itself. Any forge, any CI, plus a JSON mode and a Python API for + scripts and agents. + + [:octicons-arrow-right-24: Getting started](getting-started.md) + +- __pre-commit hook__ + + --- + + The fastest feedback there is: the commit is rejected before it exists. + Opt-in by nature, so pair it with one of the enforced surfaces. + + [:octicons-arrow-right-24: Guide](guides/pre-commit.md) + +- __GitHub Action__ + + --- + + Enforcement in CI that a contributor cannot skip, with per-rule outputs + later steps can gate on. + + [:octicons-arrow-right-24: Guide](guides/github-actions.md) + +- __GitHub App__ + + --- + + No workflow file and no CI minutes. Install it once and every push and pull + request gets a check run. + + [:octicons-arrow-right-24: Guide](guides/github-app.md) + +- __MCP server__ + + --- + + The validations as structured tools, so an AI coding agent checks its own + commit before it writes it. + + [:octicons-arrow-right-24: Guide](guides/mcp.md) + +
+ +## Pricing + +
+ +- __Free__ + + --- + + The CLI, the pre-commit hook, the GitHub Action and the MCP server. MIT + licensed, no account, no limits. + + The hosted GitHub App is free too on public repositories and personal + accounts. + +- __Team__ + + --- + + The hosted GitHub App on an organization's private repositories, with a + 14-day free trial. + + [:octicons-arrow-right-24: See the plan on the Marketplace](https://github.com/marketplace/commit-check) + +
+ +Nothing is blocked while you try it. Without a config file the App reports its +findings but leaves the check run neutral, and it never rejects a push — the +only way Commit Check blocks a merge is if you make it a required check +yourself. + +GitHub can enforce some of the same policies natively, but the commit-metadata +rules sit behind its Enterprise plan. For a twenty-person team that is the +difference between $4 and $21 a seat — about $340 a month for a regular +expression, which reports a bare mismatch where Commit Check reports a rule ID, +a suggestion and a link. [The arithmetic and the honest caveats](compare/github-rules.md). + +## Questions + +??? question "Does it read my source code?" + + No. The CLI validates commit metadata and never opens your files. The + hosted App uses a blob-filtered fetch and a sparse checkout that + materializes only the config files, so no other repository content is ever + downloaded. Content scanning is deliberately out of scope. + +??? question "Can a developer bypass it?" + + The pre-commit hook, yes — `git commit --no-verify` is one flag, and that + is by design: a local hook is fast feedback, not a gate. The enforcement + boundary is CI. Make the Action or the App a required status check and a + violating change cannot merge, however it was committed. + +??? question "Will turning it on block everyone tomorrow?" + + No. Without a config file the App reports in full but leaves the check + neutral, and it never rejects a push. Most rules are off until you turn + them on — the [rules reference](rules.md#rule-index) marks which start on. + +??? question "What about the history I already have?" + + Only new commits are checked. Nothing asks you to rewrite what is already + merged. + +??? question "Does it only work on GitHub?" + + The CLI and the pre-commit hook run anywhere Git does — GitLab, Gitea, + Bitbucket, a local machine. The Action and the App are GitHub-specific + because they integrate with GitHub's check runs. + +??? question "Do I need Node.js?" + + No. On a modern Python there are no runtime dependencies at all. + +??? question "Which Python versions are supported?" + + 3.10 through 3.14. CI runs the suite on all five, across Linux, macOS and + Windows — fifteen combinations on every change. + +??? question "How do I know the package I installed is the one you built?" + + Releases carry [SLSA Level 3](https://slsa.dev) build provenance. The + GitHub Action verifies the attestation with `gh attestation verify` before + it installs anything, and fails the step if verification does not pass. -
+
## Questions, bugs, contributions @@ -360,8 +371,8 @@ something is broken or missing — include the output of failed. **Send a pull request** to any of the -[repositories](https://github.com/commit-check). The engine, the Action and the -MCP server are separate — [Ecosystem](#ecosystem) above shows which is which. +[repositories](https://github.com/commit-check). The engine, the Action, the App +and the MCP server are separate projects. [Discussions :fontawesome-brands-github:](https://github.com/commit-check/commit-check/discussions){ .md-button .md-button--primary } [Issues :fontawesome-brands-github:](https://github.com/commit-check/commit-check/issues){ .md-button } diff --git a/docs/overrides/landing.html b/docs/overrides/landing.html new file mode 100644 index 0000000..1ba82d9 --- /dev/null +++ b/docs/overrides/landing.html @@ -0,0 +1,33 @@ +{% extends "main.html" %} + +{# + The landing page takes over the content area but deliberately leaves the + header alone. + + Instant navigation swaps a fixed list of elements between documents + (see `bs()` in the theme bundle): announce, container, header-topic, + outdated, logo, skip. `{% block header %}` renders *outside* + `[data-md-component=container]`, so a header overridden here would not be + swapped on navigation: it would follow the reader onto every docs page, + and the docs header would follow them back here. The landing look is + applied to the stock header from `landing.css` instead, which is added to + and removed from `` as the reader arrives and leaves — instant + navigation diffs the head, so that part does work. +#} + +{% block extrahead %} + {{ super() }} + +{% endblock %} + +{% block site_nav %}{% endblock %} + +{% block container %} +
+
+ {% block content %} + {% include "partials/content.html" %} + {% endblock %} +
+
+{% endblock %} diff --git a/docs/stylesheets/landing.css b/docs/stylesheets/landing.css new file mode 100644 index 0000000..2cfa79e --- /dev/null +++ b/docs/stylesheets/landing.css @@ -0,0 +1,278 @@ +/* + * Landing page only. + * + * This file is linked from docs/overrides/landing.html rather than listed in + * mkdocs.yml's extra_css, so the twenty-odd documentation pages never load it. + * Instant navigation diffs between documents, so the link is added when + * a reader arrives here and removed when they leave -- which is also why rules + * below may safely reach outside the content area, to .md-header. A body class + * could not do that: instant navigation does not touch . + */ + +/* ---------------------------------------------------------------- header -- */ + +/* Marc Lou's first rule for a landing page: the brand is small and out of the + way, because nobody arrives caring about it. The stock header already puts + the logo and the name top left; the search box is what competes with the + headline, and a reader who wants search is one click into the docs. */ +.md-header__title .md-header__topic:first-child { + font-size: 0.9rem; +} + +.md-header .md-search { + display: none; +} + +@media screen and (min-width: 76.25em) { + .md-header .md-search { + display: block; + } +} + +/* ------------------------------------------------------------------ hero -- */ + +.cc-hero { + display: grid; + gap: 2.4rem; + align-items: center; + margin: 1.2rem 0 3.2rem; +} + +@media screen and (min-width: 60em) { + .cc-hero { + grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); + gap: 3.2rem; + } +} + +/* The h1 is the promise, so it gets the size budget the site name used to + spend. clamp() keeps it from wrapping to four lines on a phone. */ +.md-typeset .cc-hero__copy h1 { + margin: 0 0 0.8rem; + font-size: clamp(1.7rem, 5.2vw, 2.6rem); + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.01em; + color: var(--md-default-fg-color); +} + +.md-typeset .cc-hero__copy > p:first-of-type { + font-size: 0.82rem; + line-height: 1.65; + color: var(--md-default-fg-color--light); +} + +.cc-hero__copy .md-button { + margin: 0.4rem 0.4rem 0 0; +} + +/* The terminal sample is the proof, so it carries the visual weight of a + screenshot without being one: real text, selectable, and it recolours with + the palette instead of burning a light theme into a PNG. */ +.md-typeset .cc-hero__demo > .highlight, +.md-typeset .cc-hero__demo pre { + margin: 0; +} + +.md-typeset .cc-hero__demo > .highlight { + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 0.4rem; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); +} + +/* The transcript is real output, so its longest line is 93 characters and + cannot be shortened without making it a fake. In the hero it has to be + readable at a glance rather than scrolled to, so it soft-wraps -- which is + what an 80-column terminal does with that line anyway. The source in + index.md is untouched, so tests/docs_sync_test.py still checks the rule + name against the installed package. + + Not `overflow: hidden` on the wrapper: an earlier version did that and + silently cut the line off at the border. */ +.md-typeset .cc-hero__demo pre > code { + font-size: 0.65rem; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.md-typeset .cc-hero__demo > p { + margin: 0.8rem 0 0; + font-size: 0.72rem; + color: var(--md-default-fg-color--light); +} + +/* ------------------------------------------------------------ full bleed -- */ + +/* .cc-proof and .cc-community are tinted bands that span the whole window + while their text stays in the content column. They do it with the pair + below, and the units matter more than they look: + + margin-inline: calc(50% - 50vw) pull each edge out to the window + padding-inline: calc(50vw - 50% + gutter) put the text back + + The percentages are load-bearing. `.md-content` is a flex item, so its + min-content width is the widest thing inside it; a viewport-unit margin + like `-100vw` counts towards that and stretched the column to 2754px on a + 1440px viewport. The document itself did not scroll -- the width went into + the column, which then rendered its own content off the right edge. A + percentage margin resolves against the containing block and contributes + nothing to intrinsic width, so the column stays viewport-sized. + + `overflow-x: clip` is the belt to that pair of braces: 100vw includes the + classic scrollbar, so the bands can still end a few pixels wide of the + viewport. `clip` rather than `hidden` because `hidden` would make .md-main + a scroll container and break the sticky header and in-page anchors. */ +.md-main { + overflow-x: clip; +} + +/* ---------------------------------------------------- social proof strip -- */ + +.cc-proof { + margin: 0 calc(50% - 50vw) 3.2rem; + padding: 2rem calc(50vw - 50% + 0.8rem); + background: var(--md-code-bg-color); + text-align: center; +} + +.md-typeset .cc-proof > p { + margin: 0 auto 1.6rem; + max-width: 34rem; + font-size: 0.72rem; + color: var(--md-default-fg-color--light); +} + +.cc-proof .logo-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); + gap: 1.2rem 0.8rem; + max-width: 52rem; + margin: 0 auto; +} + +.cc-proof .logo-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; +} + +.cc-proof .logo-item img { + width: 44px; + height: 44px; + border-radius: 50%; + filter: grayscale(1); + opacity: 0.7; + transition: filter 125ms, opacity 125ms; +} + +.cc-proof .logo-item:hover img { + filter: grayscale(0); + opacity: 1; +} + +.cc-proof .logo-item span { + font-size: 0.6rem; + line-height: 1.3; + color: var(--md-default-fg-color--light); +} + +/* ----------------------------------------------------------------- cards -- */ + +/* `.md-typeset ul:not([hidden])` in the theme sets `display: flow-root`, and + that `:not()` counts as an attribute selector -- so a plain `.cc-cards > ul` + loses the cascade and every card stacks full width. Matching the theme's + shape wins on specificity instead of relying on load order. */ +.md-typeset .cc-cards > ul:not([hidden]), +.md-typeset .cc-pricing > ul:not([hidden]) { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); + gap: 0.8rem; + padding: 0; + margin: 1.2rem 0 2rem; + list-style: none; +} + +.md-typeset .cc-cards > ul > li, +.md-typeset .cc-pricing > ul > li { + margin: 0; + padding: 1rem 1.1rem; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 0.4rem; + transition: border-color 125ms, box-shadow 125ms; +} + +.md-typeset .cc-cards > ul > li:hover, +.md-typeset .cc-pricing > ul > li:hover { + border-color: var(--cc-brand, #2c9ccd); + box-shadow: 0 0 0 1px var(--cc-brand, #2c9ccd); +} + +.md-typeset .cc-cards > ul > li > hr, +.md-typeset .cc-pricing > ul > li > hr { + margin: 0.6rem 0; + border-color: var(--md-default-fg-color--lightest); +} + +.md-typeset .cc-cards > ul > li > p, +.md-typeset .cc-pricing > ul > li > p { + font-size: 0.72rem; + line-height: 1.6; +} + +/* ---------------------------------------------------------- screenshots -- */ + +.md-typeset .cc-shot { + margin: 1.6rem 0 2rem; + text-align: center; +} + +.md-typeset .cc-shot img { + max-width: 100%; + height: auto; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 0.4rem; +} + +.md-typeset .cc-shot figcaption { + margin-top: 0.6rem; + font-size: 0.66rem; + color: var(--md-default-fg-color--light); +} + +/* The recording autoplays and cannot be paused, so a reader who has asked for + reduced motion gets the last frame of it instead. */ +.md-typeset .cc-shot .cc-still { + display: none; +} + +@media (prefers-reduced-motion: reduce) { + .md-typeset .cc-shot .cc-motion { + display: none; + } + + .md-typeset .cc-shot .cc-still { + display: inline-block; + } +} + +/* ------------------------------------------------------------- community -- */ + +.cc-community { + margin: 3.2rem calc(50% - 50vw) 0; + padding: 2rem calc(50vw - 50% + 0.8rem) 2.4rem; + background: var(--md-code-bg-color); +} + +.md-typeset .cc-community h2 { + margin-top: 0; +} + +/* ------------------------------------------------------------------ chrome -- */ + +/* The "edit this page" pencil belongs on a documentation page. Here it is the + only control in the hero and it points at index.md, which is not what a + first-time reader wants to do next. */ +.cc-landing .md-content__button { + display: none; +} diff --git a/mkdocs.yml b/mkdocs.yml index ae638bb..cd1755e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,7 @@ site_name: Commit Check site_url: !ENV [SITE_URL, 'https://commit-check.com/'] site_description: >- Enforce commit message, branch naming, author and signoff standards — - one policy, across your CLI, pre-commit hooks, CI, and AI agents. + one config file, across your CLI, pre-commit hooks, CI, and AI agents. site_author: shenxianpeng copyright: Copyright © 2022 - 2026 Commit Check diff --git a/tests/docs_sync_test.py b/tests/docs_sync_test.py index 357e279..f85f897 100644 --- a/tests/docs_sync_test.py +++ b/tests/docs_sync_test.py @@ -133,28 +133,40 @@ 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. + def test_no_page_hardcodes_a_rule_count(self): + """No page states how many rules there are. + + A count in prose is a fact with a shelf life: it is correct on the + day it is written and wrong the next time a rule is added, and + nothing about the sentence invites anyone to re-check it. The + comparison table used to claim 24 when the package defined 22. The + rules page is generated from the catalog and is the only honest + place to answer "how many"; everywhere else says what the rules + cover, not how many there are. + + Blog posts and the changelog are exempt. Both are dated records of + what was true when they were written -- the same reason the version + pins skip them (see AGENTS.md) -- so "adds two rules" is a fact + about a release, not a claim about today. """ - expected = len(ALL_RULES) + offenders = [] 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}" + if path.parent.name == "posts" or path.name == "changelog.md": + continue + for match in _PROSE_RULE_COUNT.finditer(path.read_text(encoding="utf-8")): + offenders.append( + f"{path.relative_to(DOCS.parent)}: {match.group(0)!r}" ) + assert not offenders, ( + "these pages hardcode a rule count, which goes stale silently -- " + "say what the rules cover and link to the rules page instead:\n " + + "\n ".join(offenders) + ) -#: A prose claim about how many rules exist, e.g. ``24 documented rules``. -_PROSE_RULE_COUNT = re.compile(r"(\d+) documented rules") +#: A prose claim about how many rules exist, e.g. ``24 documented rules`` +#: or ``the same 22 rules``. +_PROSE_RULE_COUNT = re.compile(r"\b\d+\s+(?:documented\s+)?rules?\b") #: 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)