A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live
commit.validate
Judge one commit message
What it is
One message against this repository's commit policy: the grammar, the subject width, whether the scope is one the history uses, whether every record the message names exists, whether a fix carries a test, and whether a breaking change explains itself — as typed findings with severities, or an exemption for a subject git composed. The same verdict the commit-msg hook refuses with and the gate reads the history through.
Descriptor
- Canonical id
commit.validate- Module
commit- Kind
- query
- Stability
- behaviorallyverified
- Provenance
- declared
- Source
apps/majordomus-cli/src/capability/builtin/commit.rs- Cache
disabled- Benchmark
required- Tags
- commitgitgovernance
Run
queryA query. It reads and changes nothing.
Projections
| Interface | As | Where |
|---|---|---|
| MCP tool | majordomus_commit_validate | - |
| MCP resource | not exposed | - |
| HTTP | GET /api/v1/commit/validate | Swagger |
| Command line | majordomus commit validate | - |
Schemas
Input · ValidateInput
{
"type": "object",
"additionalProperties": false,
"properties": {
"message": {
"type": "string",
"description": "The commit message, as it would be stored. Comment lines are ignored, as git ignores\nthem, so what is judged is what would be committed."
},
"paths": {
"type": [
"array",
"null"
],
"items": {
"type": "string"
},
"description": "The paths the commit would contain. Omit them and the judgements that need them —\nwhether a fix carries a test — are not made rather than guessed.",
"default": null
}
},
"required": [
"message"
],
"description": "A message to judge, and what is known about the commit it would make.\n```\nuse majordomus_cli::capability::builtin::commit::ValidateInput;\n// paths are optional: a caller that does not know them gets the judgements that need\n// none, rather than a guess\nlet v: ValidateInput = serde_json::from_str(r#\"{\"message\": \"feat(a): a thing\"}\"#).unwrap();\nassert!(v.paths.is_none());\n```",
"title": "ValidateInput"
}Output · CommitVerdict
{
"type": "object",
"properties": {
"message": {
"$ref": "#/$defs/CommitMessage",
"description": "The message as parsed; what was judged, not what was typed."
},
"passed": {
"type": "boolean",
"description": "Whether nothing of error severity was found."
},
"exempt": {
"anyOf": [
{
"$ref": "#/$defs/CommitExemption"
},
{
"type": "null"
}
],
"description": "Why the commit was not judged, when it was not."
},
"findings": {
"type": "array",
"items": {
"$ref": "#/$defs/Diagnostic"
},
"description": "Every finding, in `CommitRule::ALL` order."
}
},
"required": [
"message",
"passed",
"findings"
],
"description": "What the judge concluded: the message as it was parsed, whether anything of error\nseverity was found, the exemption when git composed the subject rather than a person, and\nevery finding in `CommitRule::ALL` order — which is the order the evidence for them\nappears in the message, so a person reads them in the order they would fix them.\n\n```\nuse majordomus_cli::commit::{judge, CommitPolicy, CommitSubject, CommitVerdict};\nlet v: CommitVerdict = judge(&CommitSubject::of(\"update stuff\"), &CommitPolicy::default());\nassert!(!v.passed);\nassert_eq!(v.findings[0].code, \"commit.not_conventional\");\nassert_eq!(v.message.header.subject, \"update stuff\", \"what was judged, not what was typed\");\n```",
"title": "CommitVerdict",
"$defs": {
"CommitMessage": {
"type": "object",
"properties": {
"header": {
"$ref": "#/$defs/CommitHeader",
"description": "The first line, parsed."
},
"body": {
"type": "string",
"description": "Everything between the header and the trailer block, with surrounding blank lines\nremoved. Empty when the message is a subject alone."
},
"trailers": {
"type": "array",
"items": {
"$ref": "#/$defs/CommitTrailer"
},
"description": "The `Key: value` lines of the final paragraph, in the order they were written."
}
},
"required": [
"header",
"body",
"trailers"
],
"description": "A commit message taken apart: header, body, trailers.\n```\nuse majordomus_cli::commit::CommitMessage;\nlet m = CommitMessage::parse(\"feat(a): a thing\\n\\nThe body.\\n\\nRefs: I1305\\n\");\nassert_eq!(m.header.subject, \"a thing\");\nassert_eq!(m.body, \"The body.\");\nassert_eq!(m.trailers.len(), 1);\n```"
},
"CommitHeader": {
"type": "object",
"properties": {
"word": {
"type": "string",
"description": "The type as written: `feat`, `fix`, `wip`, or the empty string when the subject\ncarries no `type: ` head at all."
},
"kind": {
"$ref": "#/$defs/ReleaseChangeKind",
"description": "What `Self::word` names; `ChangeKind::Other` for a word the convention does not\ndefine, and for a subject with no head."
},
"scope": {
"type": [
"string",
"null"
],
"description": "The parenthesised scope, when there is one."
},
"breaking": {
"type": "boolean",
"description": "Whether the head carried the `!` that marks a breaking change. The `BREAKING CHANGE:`\ntrailer is a property of the body and is read by `crate::commit::CommitMessage`."
},
"subject": {
"type": "string",
"description": "What the commit says it did. For a header that does not parse this is the whole\nfirst line, unchanged, and for an unknown type word it keeps the word: nothing that\nthe author wrote is dropped on the way through."
}
},
"required": [
"word",
"kind",
"breaking",
"subject"
],
"description": "The header of a commit message: its first line, taken apart.\n\n`word` is the type as the author wrote it and `kind` is what that word names; they\ndiffer exactly when the word is not one of the eleven the convention defines, and\nkeeping both is what lets a reader see `wip(site): halfway through` as its author typed\nit while a judge still reports that `wip` is not a type.\n```\nuse majordomus_cli::commit::CommitHeader;\nuse majordomus_cli::release::ChangeKind;\nlet h = CommitHeader::parse(\"fix(plan): the fingerprint names what moved\");\nassert_eq!(h.word, \"fix\");\nassert_eq!(h.kind, ChangeKind::Fix);\nassert_eq!(h.scope.as_deref(), Some(\"plan\"));\nassert!(!h.breaking);\nassert_eq!(h.subject, \"the fingerprint names what moved\");\n```"
},
"ReleaseChangeKind": {
"oneOf": [
{
"type": "string",
"const": "feat",
"description": "A capability a person did not have before."
},
{
"type": "string",
"const": "fix",
"description": "Behaviour that was wrong and is not any more."
},
{
"type": "string",
"const": "perf",
"description": "Performance, with the behaviour unchanged."
},
{
"type": "string",
"const": "refactor",
"description": "Structure, with the behaviour unchanged."
},
{
"type": "string",
"const": "docs",
"description": "Documentation."
},
{
"type": "string",
"const": "test",
"description": "Tests."
},
{
"type": "string",
"const": "chore",
"description": "The build, the pipeline, the tooling."
},
{
"type": "string",
"const": "ci",
"description": "Continuous integration."
},
{
"type": "string",
"const": "style",
"description": "Formatting, with the behaviour unchanged."
},
{
"type": "string",
"const": "build",
"description": "The build system or its dependencies."
},
{
"type": "string",
"const": "revert",
"description": "A change that undoes another."
},
{
"type": "string",
"const": "merge",
"description": "An integration: one line of work brought into another.\n\nNot in the conventional-commit specification, and in this repository's history 38\ntimes and counting, because integration here is a deliberate act with a subject\nsomebody writes — `merge: bring origin/master into feature/x` — and not the sentence\n`git merge` composes. Declaring it is what lets the validator hold those subjects to\nthe same width and the same references as every other commit, instead of either\nrefusing a convention this project practises or exempting it from being checked at\nall. The changelog never renders it: `commits::in_range` reads the log with\n`--no-merges`, so a merge commit is not a release entry however its subject is\nspelled."
},
{
"type": "string",
"const": "other",
"description": "A commit whose subject does not parse as a conventional commit."
}
],
"description": "What a conventional commit says it did.\n\nThe set is the one the repository's own commit convention uses; a commit whose subject\ndoes not parse is `ChangeKind::Other` and still appears, because a changelog that\nsilently drops what it cannot classify is a changelog that lies by omission."
},
"CommitTrailer": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key, as written: `Refs`, `Co-Authored-By`, `BREAKING CHANGE`."
},
"value": {
"type": "string",
"description": "Everything after the colon and one space."
}
},
"required": [
"key",
"value"
],
"description": "One `Key: value` line in the trailer block at the end of a commit message.\n```\nuse majordomus_cli::commit::CommitMessage;\nuse majordomus_cli::commit::CommitTrailer;\nlet m = CommitMessage::parse(\"fix(x): a thing\\n\\nWhy.\\n\\nRefs: I1305\\n\");\nlet t: &CommitTrailer = &m.trailers[0];\nassert_eq!(t.key, \"Refs\");\nassert_eq!(t.value, \"I1305\");\n```"
},
"CommitExemption": {
"oneOf": [
{
"type": "string",
"const": "git_authored",
"description": "git composed the subject: a merge, a revert, a fixup."
}
],
"description": "Why a commit was not judged.\n```\nuse majordomus_cli::commit::{judge, CommitExemption, CommitPolicy, CommitSubject};\nlet v = judge(&CommitSubject::of(\"Merge pull request #1 from a/b\"), &CommitPolicy::default());\nassert_eq!(v.exempt, Some(CommitExemption::GitAuthored));\n```"
},
"Diagnostic": {
"type": "object",
"properties": {
"severity": {
"$ref": "#/$defs/Severity",
"description": "How bad."
},
"code": {
"type": "string",
"description": "A stable machine-readable code, e.g. `unknown_key`, `duplicate_identity`."
},
"path": {
"type": [
"string",
"null"
],
"description": "Repository-relative path of the file concerned, when there is one."
},
"message": {
"type": "string",
"description": "What is wrong, for a person; the code is for a program."
}
},
"required": [
"severity",
"code",
"message"
],
"description": "One finding about the declarative state, named by a stable code, tied to a path where\nthere is one, and carrying the command that reproduces it where there is one."
},
"Severity": {
"oneOf": [
{
"type": "string",
"const": "info",
"description": "Worth knowing; changes nothing."
},
{
"type": "string",
"const": "warning",
"description": "Something to look at; the object is still served."
},
{
"type": "string",
"const": "error",
"description": "The file is excluded and the index is degraded."
}
],
"description": "How bad a diagnostic is. `Error` excludes the file it concerns from the index and puts\nthe index into the degraded state; `Warning` and `Info` do neither."
}
}
}Examples
These are the capability's own benchmark cases: the same inputs the benchmark runs and the OpenAPI document shows.
conventional
{
"message": "feat(commit): the rule is decided by a command",
"paths": null
}not-conventional
{
"message": "update stuff",
"paths": null
}merge
{
"message": "Merge pull request #249 from korczis/fix",
"paths": null
}