# Commit Message Convention: [Repository or organisation]

> Team standard for writing commit messages: the message structure, allowed types and scopes, how to mark breaking changes, footers and trailers, how commits map to version numbers and changelogs, and the tooling that checks the rules. Based on Conventional Commits 1.0.0 (conventionalcommits.org, checked 30 October 2026) and its mapping to Semantic Versioning 2.0.0. Replace every `[bracketed]` value and every line marked *Example*. Worked examples use Brindlemark, a fictional open-source Markdown editor.

<!--
Guidance: How to use this template
- Fill the scope list (section 4) from your real code layout; a scope nobody can map to a directory or component is noise.
- Decide where the rule is enforced (section 7). With squash merging, the pull request title becomes the commit on the main branch, so check the title, not every work-in-progress commit.
- Keep boundaries: branch names, protection and merge methods live in Branching Strategy (`dev/branching-strategy`); version numbers and release cadence live in Versioning and Release Policy (`dev/versioning-release-policy`); the human-readable changelog lives in Changelog (`dev/changelog`).
-->

## Document control

<!-- Guidance: One owner, usually the tech lead or the release manager. Adding a type or scope is a minor change; changing what triggers a version bump is a major one. -->

| Field | Value |
|---|---|
| Document ID | [CMC-STD-001] |
| Version | [1.0] |
| Status | [Draft / Approved] |
| Owner | [@tech-lead] |
| Applies to | [*Example:* All repositories in the `brindlemark` organisation] |
| Enforced on | [*Example:* Pull request titles, checked in CI before squash merge] |
| Last updated | YYYY-MM-DD |

### Revision history

| Version | Date | Author | Change |
|---|---|---|---|
| 1.0 | YYYY-MM-DD | [@name] | First version |
| 1.1 | YYYY-MM-DD | [@name] | *Example:* Added `i18n` scope |

## 1. Message structure

<!-- Guidance: Show the shape once, then the rules. Most people learn the convention from examples, so keep section 6 accurate. -->

```text
<type>[optional scope][optional !]: <description>

[optional body]

[optional footer(s)]
```

| Part | Rule |
|---|---|
| Type | A noun from the list in section 3, followed by an optional scope in parentheses, an optional `!`, a colon and a space |
| Description | A short summary in the imperative mood ("add", not "added"), no full stop at the end, [*Example:* at most 72 characters for the whole first line] |
| Body | Optional; starts after one blank line; explains why the change was made and what it affects; may have several paragraphs |
| Footers | Optional; start after one blank line; each is a token, a separator (`: ` or ` #`) and a value; tokens use `-` instead of spaces |

The 72-character limit and the imperative mood are team rules, not part of Conventional Commits; they keep messages readable in `git log --oneline` and in hosting interfaces.

## 2. Breaking changes

<!-- Guidance: A breaking change is anything that forces users to change their code, configuration, files or habits. Questions: does it remove or rename a public API, a CLI flag, a setting or a file format field? Pitfall: hiding a breaking change in a `refactor` commit. -->

Mark a breaking change in one or both of these ways:

- Add `!` right before the colon: `feat(api)!: remove the legacy export endpoint`.
- Add a footer that starts with `BREAKING CHANGE:` (upper case, or `BREAKING-CHANGE:`) followed by a description of what breaks and how to migrate.

Use both when the migration needs explaining: the `!` makes it visible in one-line logs and the footer carries the detail into the changelog. Any type can be breaking, not only `feat`.

## 3. Types

<!-- Guidance: Conventional Commits defines only `feat` and `fix`; other types are allowed. The list below follows the widely used set from the commitlint conventional configuration. Keep the list short and say which types appear in the changelog. -->

| Type | Use for | Version effect | In changelog |
|---|---|---|---|
| `feat` | A new feature for users | MINOR | Yes, "Added" |
| `fix` | A bug fix for users | PATCH | Yes, "Fixed" |
| `perf` | A change that improves performance without changing behaviour | PATCH [team choice] | Yes, "Changed" |
| `refactor` | Code change that neither fixes a bug nor adds a feature | None | No |
| `docs` | Documentation only | None | No |
| `test` | Adding or correcting tests | None | No |
| `build` | Build system or dependency changes | None, unless user-facing | [Team choice] |
| `ci` | CI configuration and scripts | None | No |
| `style` | Formatting with no code meaning change | None | No |
| `chore` | Maintenance that fits no other type | None | No |
| `revert` | Reverts an earlier commit; footer `Refs:` lists the reverted hashes | Depends on the reverted change | Yes |

Any type with `!` or a `BREAKING CHANGE` footer means a MAJOR version bump (after 1.0.0).

## 4. Scopes

<!-- Guidance: Scopes name a part of the code base. Map each one to a directory or component and keep the list in this file, so that tooling can check it. -->

| Scope | Covers | Owner |
|---|---|---|
| *Example:* `editor` | `src/editor/` | [@editor-team] |
| *Example:* `preview` | `src/preview/`, renderers | [@preview-team] |
| *Example:* `storage` | `src/storage/`, migrations | [@platform-team] |
| *Example:* `i18n` | `messages/` | [@docs-team] |
| *Example:* `deps` | Dependency updates | [@platform-team] |
| [scope] | [Paths] | [@owner] |

Leave the scope out when a change spans many areas; do not invent a scope for one commit.

## 5. Footers and trailers

<!-- Guidance: Footers follow the Git trailer format, so `git interpret-trailers` and hosting platforms can read them. Agree which ones are mandatory. -->

| Footer | When | *Example* |
|---|---|---|
| `BREAKING CHANGE:` | Any breaking change | `BREAKING CHANGE: config key "theme" is now "appearance"` |
| `Refs:` | Link issues or reverted commits | `Refs: #482` |
| `Closes` | Close an issue on merge (hosting-platform keyword) | `Closes #482` |
| `Signed-off-by:` | Projects that require the Developer Certificate of Origin; added by `git commit -s` | `Signed-off-by: [Name] <[email]>` |
| `Co-authored-by:` | Credit a co-author on hosting platforms that support it | `Co-authored-by: [Name] <[email]>` |
| [*Example:* `AI-Assisted:`] | When the AI Coding Assistant Policy (`dev/ai-coding-assistant-policy`) asks for provenance | `AI-Assisted: TOOL-002 session 7f3c2a` |

## 6. Examples

<!-- Guidance: Keep five to eight real-looking examples, including one breaking change and one revert. Update them when the rules change. -->

Each block below is one complete commit message.

A feature with a scope:

```text
feat(preview): render footnotes in the side panel
```

A fix with a body and a closing footer:

```text
fix(storage): keep unsaved edits when the tab is restored

Autosave raced with restore and overwrote the newer draft.
Restore now compares revision numbers before writing.

Closes #482
```

A breaking change marked both ways:

```text
feat(api)!: remove the v1 export endpoint

BREAKING CHANGE: /api/v1/export is gone. Call /api/v2/export, which
returns the same document with front matter kept.
```

A revert that names the reverted commit:

```text
revert: feat(preview): render footnotes in the side panel

Refs: 3f9c2e1
```

A documentation change with no scope:

```text
docs: explain offline mode in the README
```

## 7. Tooling and enforcement

<!-- Guidance: Enforce in one place that cannot be skipped (CI on the pull request title for squash merges), and help in places that are optional (local hooks, editor plugins). -->

| Stage | Tool option | What it does |
|---|---|---|
| Writing | Commitizen (cz-cli or commitizen-tools), editor plugins | Prompts for type, scope and description |
| Local check | commitlint with the conventional configuration, run from a commit-msg hook | Rejects messages that break the rules before push |
| CI check | commitlint or a pull request title check | Blocks merge when the title, or each commit, breaks the rules |
| Versioning | semantic-release, release-please, Cocogitto or commitizen-tools | Calculates the next version from commit types |
| Changelog | conventional-changelog or git-cliff | Groups commits into release notes for the Changelog (`dev/changelog`) |

[*Example:* Brindlemark squash-merges every pull request, checks the title with commitlint in CI, and lets release-please open the release pull request with the version bump and changelog.]

A minimal commitlint configuration file (`commitlint.config.js`) that extends the conventional rules and restricts scopes to the list in section 4:

```js
export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'scope-enum': [2, 'always', ['editor', 'preview', 'storage', 'i18n', 'deps']],
    'header-max-length': [2, 'always', 72],
  },
};
```

## 8. Exceptions and fixes

<!-- Guidance: Mistakes happen. Say how to fix a bad message before and after merge, so nobody rewrites shared history in a panic. -->

- Before merge: fix the message with `git commit --amend` or by editing the pull request title.
- After merge to a protected branch: do not rewrite history. Correct the changelog entry by hand, or add an empty commit only if your release tool needs it, following Branching Strategy (`dev/branching-strategy`).
- Merge commits and automated dependency commits may use the tool's own format if the CI check is configured to allow it.

## Standard and licence note

Based on Conventional Commits 1.0.0 (conventionalcommits.org, specification licensed CC BY 3.0) and Semantic Versioning 2.0.0 (semver.org, CC BY 3.0), cited by name and rule; the rules are paraphrased, not copied. The type list follows the conventional configuration of commitlint as named on the Conventional Commits site; tool names belong to their projects. Subject length, scopes and the `AI-Assisted` trailer are team choices, not part of the specification. Brindlemark is fictional. Template licence: MIT.
