Skip to content
Cockpit0.29.0
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

query
A query. It reads and changes nothing.

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.

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.

GET /api/v1/commit/validate

Projections

InterfaceAsWhere
MCP toolmajordomus_commit_validate-
MCP resourcenot exposed-
HTTPGET /api/v1/commit/validateSwagger
Command linemajordomus 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
}