commit.validate
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.
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.
- query
- behaviorally_verified
- module commit
- #commit
- #git
- #governance
Exposure
Where this one definition is reachable. Absence is explicit: a surface not listed does not carry it.
| surface | as |
|---|---|
| MCP | tool majordomus_commit_validate |
| HTTP | GET /api/v1/commit/validate operationId commit.validate |
| command line | majordomus commit validate |
Schemas
The canonical input and output, from the Rust types; the MCP tool schema and the OpenAPI parameters and responses are derived from these.
input · ValidateInput
use majordomus_cli::capability::builtin::commit::ValidateInput;
// paths are optional: a caller that does not know them gets the judgements that need
// none, rather than a guess
let v: ValidateInput = serde_json::from_str(r#"{"message": "feat(a): a thing"}"#).unwrap();
assert!(v.paths.is_none());| property | type | required | description |
|---|---|---|---|
| message | string | yes | The commit message, as it would be stored. Comment lines are ignored, as git ignores them, so what is judged is what would be committed. |
| paths | array | null | no | The paths the commit would contain. Omit them and the judgements that need them — whether a fix carries a test — are not made rather than guessed. default null |
JSON Schema
{
"additionalProperties": false,
"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```",
"properties": {
"message": {
"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.",
"type": "string"
},
"paths": {
"default": null,
"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.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"required": [
"message"
],
"title": "ValidateInput",
"type": "object"
}output · CommitVerdict
CommitRule::ALL order — which is the order the evidence for them
appears in the message, so a person reads them in the order they would fix them.
use majordomus_cli::commit::{judge, CommitPolicy, CommitSubject, CommitVerdict};
let v: CommitVerdict = judge(&CommitSubject::of("update stuff"), &CommitPolicy::default());
assert!(!v.passed);
assert_eq!(v.findings[0].code, "commit.not_conventional");
assert_eq!(v.message.header.subject, "update stuff", "what was judged, not what was typed");| property | type | required | description |
|---|---|---|---|
| exempt | one of 2 | no | Why the commit was not judged, when it was not. |
| findings | array | yes | Every finding, in CommitRule::ALL order. |
| message | reference | yes | The message as parsed; what was judged, not what was typed. |
| passed | boolean | yes | Whether nothing of error severity was found. |
JSON Schema
{
"$defs": {
"CommitExemption": {
"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```",
"oneOf": [
{
"const": "git_authored",
"description": "git composed the subject: a merge, a revert, a fixup.",
"type": "string"
}
]
},
"CommitHeader": {
"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```",
"properties": {
"breaking": {
"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`.",
"type": "boolean"
},
"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": {
"description": "The parenthesised scope, when there is one.",
"type": [
"string",
"null"
]
},
"subject": {
"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.",
"type": "string"
},
"word": {
"description": "The type as written: `feat`, `fix`, `wip`, or the empty string when the subject\ncarries no `type: ` head at all.",
"type": "string"
}
},
"required": [
"word",
"kind",
"breaking",
"subject"
],
"type": "object"
},
"CommitMessage": {
"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```",
"properties": {
"body": {
"description": "Everything between the header and the trailer block, with surrounding blank lines\nremoved. Empty when the message is a subject alone.",
"type": "string"
},
"header": {
"$ref": "#/$defs/CommitHeader",
"description": "The first line, parsed."
},
"trailers": {
"description": "The `Key: value` lines of the final paragraph, in the order they were written.",
"items": {
"$ref": "#/$defs/CommitTrailer"
},
"type": "array"
}
},
"required": [
"header",
"body",
"trailers"
],
"type": "object"
},
"CommitTrailer": {
"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```",
"properties": {
"key": {
"description": "The key, as written: `Refs`, `Co-Authored-By`, `BREAKING CHANGE`.",
"type": "string"
},
"value": {
"description": "Everything after the colon and one space.",
"type": "string"
}
},
"required": [
"key",
"value"
],
"type": "object"
},
"Diagnostic": {
"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.",
"properties": {
"code": {
"description": "A stable machine-readable code, e.g. `unknown_key`, `duplicate_identity`.",
"type": "string"
},
"message": {
"description": "What is wrong, for a person; the code is for a program.",
"type": "string"
},
"path": {
"description": "Repository-relative path of the file concerned, when there is one.",
"type": [
"string",
"null"
]
},
"severity": {
"$ref": "#/$defs/Severity",
"description": "How bad."
}
},
"required": [
"severity",
"code",
"message"
],
"type": "object"
},
"ReleaseChangeKind": {
"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.",
"oneOf": [
{
"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.",
"type": "string"
}
]
},
"Severity": {
"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.",
"oneOf": [
{
"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.",
"type": "string"
}
]
}
},
"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```",
"properties": {
"exempt": {
"anyOf": [
{
"$ref": "#/$defs/CommitExemption"
},
{
"type": "null"
}
],
"description": "Why the commit was not judged, when it was not."
},
"findings": {
"description": "Every finding, in `CommitRule::ALL` order.",
"items": {
"$ref": "#/$defs/Diagnostic"
},
"type": "array"
},
"message": {
"$ref": "#/$defs/CommitMessage",
"description": "The message as parsed; what was judged, not what was typed."
},
"passed": {
"description": "Whether nothing of error severity was found.",
"type": "boolean"
}
},
"required": [
"message",
"passed",
"findings"
],
"title": "CommitVerdict",
"type": "object"
}Policies
- benchmark
- required — a target on every transport the exposure declares; the cases are the input type's
- cache
- disabled — every call runs the handler
Benchmark targets
Derived from the registry for this repository: one requirement per transport, and the cases the input type provides. The whole matrix is on the benchmarks page.
| transport | state | cases | targets |
|---|---|---|---|
| direct | covered | 3 | conventional not-conventional merge |
| mcp | covered | 3 | conventional not-conventional merge |
| http | covered | 3 | conventional not-conventional merge |