Atomic Commit Gate — Design & Rule Rationale
Implements: ADR-0014 — One-Feature-Per-Commit Policy Source:
tools/check_atomic_commit.pyHook:.git/hooks/commit-msg(installed bytools/install_hooks.sh)
Why Enforce Atomic Commits?
"Atomic" does not mean small — it means coherent. Every file changed in the commit should serve one explicitly stated purpose. The discipline pays dividends in three concrete ways:
git bisectreliability — bisect can pin a regression to one commit only when each commit has a single identifiable purpose.- Focused code review — reviewers can evaluate a change without mentally separating two independent concerns.
- Safe revert surgery — reverting a bug fix does not accidentally revert an unrelated refactor that was bundled into the same commit.
Rule Set (Applied in Order)
Rule 0 — Bypass ([skip-atomic])
If the subject line (first line only) contains [skip-atomic], all checks
are skipped and the bypass is logged to .atomic-commit-bypasses.log.
Why subject-line only?
The body may legitimately reference [skip-atomic] when explaining why a
bypass was used. Checking only the subject prevents false-positive bypasses
triggered by explanatory text.
Why log bypasses?
The escape hatch is intentionally low-friction but must remain auditable.
Teams can review .atomic-commit-bypasses.log in retrospectives and tighten
the policy when patterns emerge.
Rule 1 — Conventional Commits Format
The subject line must match:
<type>[(<scope>)][!]: <description>
Valid types: feat, fix, docs, style, refactor, perf, test,
build, ci, chore, revert, sec.
Why Conventional Commits? A structured subject makes the commit machine-readable. CI tools, changelog generators, and the bundled-concern detector (Rule 2) all depend on this structure. Without it, mechanical reasoning about commit intent is impossible.
Implementation — _CC_RE:
_CC_RE = re.compile(
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?\s*:\s*(?P<desc>.+)$"
)
Named groups (type, scope, breaking, desc) are extracted for reuse in
later rules without re-parsing the subject.
Rule 2 — No Bundled Concerns in Body
The commit body must not contain lines that begin with a conventional-commit
prefix (e.g., fix: resolve import error or feat(auth): add token refresh).
Why this is the strongest signal: When a developer mentally writes two commit messages and concatenates them, the result almost always has two conventional-commit-style headers — one in the subject, one somewhere in the body. This pattern is unambiguous and mechanically detectable.
Implementation — _BUNDLED_RE:
_BUNDLED_RE = re.compile(
r"^(?:feat|fix|docs|...|sec)(?:\([^)]+\))?!?\s*:",
re.MULTILINE,
)
re.MULTILINE makes ^ match at the start of every line, not just the start
of the string, so the regex correctly detects the pattern anywhere in the body.
Rule 3 — "and" Heuristic (Warning Only, Non-Blocking)
If the description contains action verbs connected by "and" (e.g., "add validation and fix serialisation bug"), a warning is printed but the commit is not blocked.
Why non-blocking? Many valid descriptions use "and" to describe a single cohesive change (e.g., "add endpoint and update OpenAPI spec"). Making this rule blocking would generate false positives at an unacceptable rate. A warning prompts developers to reflect without imposing a hard stop.
Input Channels
The CLI accepts the commit message via three channels (in priority order):
| Priority | Channel | Use case |
|---|---|---|
| 1 | --message / -m flag |
Manual testing |
| 2 | commit_msg_file positional arg |
Git hook ("$1") |
| 3 | Standard input (non-TTY) | Pipe-based CI testing |
This design allows the same binary to serve as both a git hook and a standalone CLI tool without code duplication.
Audit Log Format
Each bypass appends one line to .atomic-commit-bypasses.log:
2026-03-26T14:32:01+00:00 | reason=[skip-atomic] | subject='feat: scaffold [skip-atomic]'
Fields: ISO-8601 UTC timestamp, reason label, subject (truncated to 120 chars).
The log is version-controlled so bypasses are visible in git log.
Extending the Rule Set
To add a new rule:
- Implement a check inside
run_gateafter the existing rules. - Print a
[FAIL]message with a clear fix instruction and return1. - Update the module docstring rule list.
- Add a test case to
tests/test_check_atomic_commit.py. - Update ADR-0014 if the new rule changes the policy semantics.