Formalize C0/C1/C2 change classification #259

Merged
eblume merged 8 commits from formalize-change-classification into main 2026-02-23 16:19:54 -08:00
3 changed files with 42 additions and 4 deletions
Showing only changes of commit f1c2605331 - Show all commits

Fix: switch invariant check from pre-commit to commit-msg hook

The pre-commit stage runs before the commit is created, so it can't
validate the commit being made. The commit-msg stage receives the
message file as argv[1], allowing the hook to include the pending
commit in its invariant check.

Also works standalone (no args) for validating existing branch history.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Erich Blume 2026-02-23 16:18:29 -08:00

View file

@ -1,7 +1,7 @@
---
# See https://pre-commit.com for more information
# Run: uvx pre-commit run --all-files
# Install: uvx pre-commit install
# Install: uvx pre-commit install && uvx pre-commit install --hook-type commit-msg
repos:
# General file hygiene
@ -118,6 +118,7 @@ repos:
language: system
always_run: true
pass_filenames: false
stages: [commit-msg]
# Documentation validation
- repo: local

View file

@ -151,7 +151,7 @@ C2(deploy-authentik): close configure-postgres
C2(deploy-authentik): finalize rewrite cards as historical documentation
```
The `mikado-branch-invariant-check` pre-commit hook validates this convention and the invariant ordering.
The `mikado-branch-invariant-check` commit-msg hook validates this convention and the invariant ordering.
### Process
@ -260,7 +260,7 @@ tags:
| `mise run docs-mikado --resume` | Resume a chain: detect branch, show state and next steps |
| `mise run docs-mikado --resume <chain>` | Resume a specific chain with branch consistency check |
The `mikado-branch-invariant-check` pre-commit hook runs automatically on `mikado/*` branches, validating commit message conventions and invariant ordering.
The `mikado-branch-invariant-check` commit-msg hook runs automatically on `mikado/*` branches, validating commit message conventions and invariant ordering. Requires `uvx pre-commit install --hook-type commit-msg`.
## Related

View file

@ -6,11 +6,18 @@
#MISE description="Validate Mikado Branch Invariant on mikado/* branches"
"""Validate the Mikado Branch Invariant for C2 change branches.
Runs as a pre-commit hook on mikado/* branches. Checks:
Runs as a commit-msg hook on mikado/* branches. Receives the commit message
file as its first argument, classifies the incoming commit, appends it to the
existing branch history, and validates the full sequence.
Checks:
1. All commits follow the C2(<chain>): <verb> <description> convention
2. The invariant ordering is maintained: plan commits come before impl/close
3. No plan commits appear after any impl or close commits
4. Close commits don't appear before impl commits in the same cycle
5. The chain stem in commit messages matches the branch name
Can also be run standalone (no arguments) to validate existing branch history.
Exit code 0 if valid (or not on a mikado/* branch), 1 if violations found.
"""
@ -71,6 +78,29 @@ def get_branch_commits(branch: str) -> list[dict]:
return commits
def parse_commit_message(msg_path: str) -> str:
"""Read and return the first line of a commit message file."""
with open(msg_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
return line
return ""
def make_pending_commit(subject: str) -> dict:
"""Create a pseudo-commit dict for the incoming (not yet created) commit."""
match = C2_COMMIT_RE.match(subject)
return {
"sha": "(pending)",
"subject": subject,
"chain": match.group(1) if match else None,
"verb": match.group(2) if match else None,
"description": match.group(3) if match else None,
"conventional": match is not None,
}
def check_invariant(commits: list[dict], chain_stem: str) -> list[str]:
"""Check the Mikado Branch Invariant. Returns list of violation messages."""
errors: list[str] = []
@ -136,6 +166,13 @@ def main() -> None:
chain_stem = branch.removeprefix("mikado/")
commits = get_branch_commits(branch)
# If called with a commit message file (commit-msg hook), include the
# pending commit in the validation
if len(sys.argv) > 1:
subject = parse_commit_message(sys.argv[1])
if subject:
commits.append(make_pending_commit(subject))
if not commits:
# No commits on branch yet — valid (length-zero case)
sys.exit(0)