commit.plan
The working tree as git reports it — branch, upstream, divergence, every staged, unstaged and untracked path, and any merge or rebase in progress — divided into the commits the history's own scoping supports, each with the evidence for it, under a fingerprint of the repository, the worktree, HEAD and the exact change set, so that a plan acted on later can be refused rather than applied to a tree it was not made for.
The working tree as git reports it — branch, upstream, divergence, every staged, unstaged and untracked path, and any merge or rebase in progress — divided into the commits the history's own scoping supports, each with the evidence for it, under a fingerprint of the repository, the worktree, HEAD and the exact change set, so that a plan acted on later can be refused rather than applied to a tree it was not made for.
- query
- behaviorally_verified
- module commit
- #commit
- #git
- #plan
Exposure
Where this one definition is reachable. Absence is explicit: a surface not listed does not carry it.
| surface | as |
|---|---|
| MCP | tool majordomus_commit_plan resource majordomus://commit/plan |
| HTTP | GET /api/v1/commit/plan operationId commit.plan |
| command line | majordomus commit plan |
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 · Empty
No properties: the capability takes no input.
JSON Schema
{
"additionalProperties": false,
"description": "No input.",
"title": "Empty",
"type": "object"
}output · CommitPlan
use majordomus_cli::commit::{CommitPlan, PlanFingerprint, WorkingTreeState};
let plan = CommitPlan {
fingerprint: PlanFingerprint::of("/r/.git", "/r", &WorkingTreeState::default()),
tree: WorkingTreeState::default(),
groups: Vec::new(),
diagnostics: Vec::new(),
};
assert!(plan.groups.is_empty(), "a clean tree proposes nothing");| property | type | required | description |
|---|---|---|---|
| diagnostics | array | yes | What a person should know before acting on it. |
| fingerprint | reference | yes | What the plan was derived from, and what makes it stale. |
| groups | array | yes | The commits proposed, in the order they should be made. |
| tree | reference | yes | The tree as it stood. |
JSON Schema
{
"$defs": {
"ChangeStage": {
"description": "Where one path stands between the index and the working tree.\n```\nuse majordomus_cli::commit::ChangeStage;\n// ordered so that what would be in the next commit sorts first\nassert!(ChangeStage::Staged < ChangeStage::Unstaged);\nassert!(ChangeStage::Unstaged < ChangeStage::Untracked);\n```",
"oneOf": [
{
"const": "staged",
"description": "Different in the index from HEAD: it would be in the next commit.",
"type": "string"
},
{
"const": "unstaged",
"description": "Different in the working tree from the index: it would not.",
"type": "string"
},
{
"const": "untracked",
"description": "Not tracked at all.",
"type": "string"
}
]
},
"CommitGroup": {
"description": "One commit a plan proposes.\nOne commit a plan proposes: its message, its paths, and why those paths are one commit.\n\n```\nuse majordomus_cli::commit::{plan::group, CommitGroup, ScopeVocabulary};\nlet mut v = ScopeVocabulary::default();\nv.learn(\"commit\", \"src/commit\", 40);\nlet g: Vec<CommitGroup> = group(&[\"src/commit/a.rs\".into()], &v, &[]);\n// the subject is empty on purpose: what a change did is the one thing no evidence in\n// the tree can state\nassert_eq!(g[0].message.header.subject, \"\");\nassert_eq!(g[0].message.header.scope.as_deref(), Some(\"commit\"));\nassert!(g[0].rationale.contains(\"40 prior commit\"), \"{}\", g[0].rationale);\n```",
"properties": {
"message": {
"$ref": "#/$defs/CommitMessage",
"description": "The message as proposed. The summary is the one field a person is expected to\nreplace: nothing here can know *why* a change was made, and a planner that wrote a\nconfident sentence about it would be writing fiction into the history."
},
"paths": {
"description": "The paths, in order.",
"items": {
"type": "string"
},
"type": "array"
},
"rationale": {
"description": "Why these paths are one commit, in evidence a reader can check.",
"type": "string"
},
"scope_from": {
"anyOf": [
{
"$ref": "#/$defs/ScopeSuggestion"
},
{
"type": "null"
}
],
"description": "The scope suggestion and what it was derived from, when there was one."
}
},
"required": [
"message",
"paths",
"rationale"
],
"type": "object"
},
"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"
},
"PathChange": {
"description": "One path, and where it stands.\n```\nuse majordomus_cli::commit::ChangeStage;\nuse majordomus_cli::commit::plan::PathChange;\n// staged and modified again since: the commit would carry the staged half only\nlet c = PathChange {\n path: \"src/a.rs\".into(), stage: ChangeStage::Staged, status: \"MM\".into(), partial: true,\n};\nassert!(c.partial);\n```",
"properties": {
"partial": {
"description": "Set when the path is staged *and* modified again since: committing it commits the\nstaged half only, which is a thing a person should be told rather than discover.",
"type": "boolean"
},
"path": {
"description": "Repository-relative, forward slashes.",
"type": "string"
},
"stage": {
"$ref": "#/$defs/ChangeStage",
"description": "Whether it is staged, unstaged or untracked. A path modified both in the index and\nin the working tree appears once, as `ChangeStage::Staged`, with `partial` set."
},
"status": {
"description": "The two-letter status git reported, verbatim.",
"type": "string"
}
},
"required": [
"path",
"stage",
"status"
],
"type": "object"
},
"PlanFingerprint": {
"description": "What a plan was derived from.\n\nCompared, never trusted: a plan whose fingerprint differs from the tree in front of it\ndescribes a tree that no longer exists.\n```\nuse majordomus_cli::commit::{PlanFingerprint, WorkingTreeState};\nlet f = PlanFingerprint::of(\"/repo/.git\", \"/repo\", &WorkingTreeState::default());\nassert_eq!(f.head, \"unborn\", \"a repository with no commits says so\");\n```",
"properties": {
"changes": {
"description": "SHA-256 over every change, its stage and its status, in order. Restaging one file\nchanges it; touching a file the plan does not contain does not.",
"type": "string"
},
"head": {
"description": "HEAD when the plan was derived, or `unborn`.",
"type": "string"
},
"repository": {
"description": "The repository: git's common directory, which every worktree of one repository\nshares and no two repositories do.",
"type": "string"
},
"worktree": {
"description": "The worktree: this checkout's own path. Two worktrees of one repository have the\nsame repository and different worktrees, which is what keeps their plans apart.",
"type": "string"
}
},
"required": [
"repository",
"worktree",
"head",
"changes"
],
"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"
}
]
},
"ScopeSuggestion": {
"description": "Which scope a set of paths belongs to, and the evidence for it.\n```\nuse majordomus_cli::commit::{ScopeSuggestion, ScopeVocabulary};\nlet mut v = ScopeVocabulary::default();\n// two scopes with equal claim to one directory: reported, never broken silently\nv.learn(\"a\", \"src\", 7);\nv.learn(\"b\", \"src\", 7);\nlet s: ScopeSuggestion = v.suggest(&[\"src/x.rs\".into()]).expect(\"a tie\");\nassert!(s.ambiguous);\n```",
"properties": {
"ambiguous": {
"description": "Whether another scope was as strongly associated. A tie is reported rather than\nbroken silently: a planner that guessed would be inventing the one field of a commit\nmessage that a reader uses to navigate.",
"type": "boolean"
},
"commits": {
"description": "How many prior commits associate this scope with directories these paths are in.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"directory": {
"description": "The directory prefix the association was strongest at.",
"type": "string"
},
"scope": {
"description": "The scope.",
"type": "string"
}
},
"required": [
"scope",
"commits",
"directory",
"ambiguous"
],
"type": "object"
},
"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"
}
]
},
"WorkingTreeState": {
"description": "What git says about the working tree right now.\n```\nuse majordomus_cli::commit::WorkingTreeState;\nlet clean = WorkingTreeState::default();\nassert!(clean.is_clean());\nassert!(clean.staged().is_empty());\n```",
"properties": {
"ahead": {
"description": "Commits this branch has that its upstream does not.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"behind": {
"description": "Commits the upstream has that this branch does not.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"branch": {
"description": "The branch, or `None` when HEAD is detached.",
"type": [
"string",
"null"
]
},
"changes": {
"description": "Every change, ordered by stage then path.",
"items": {
"$ref": "#/$defs/PathChange"
},
"type": "array"
},
"head": {
"description": "HEAD's full name, or `None` in a repository with no commits.",
"type": [
"string",
"null"
]
},
"in_progress": {
"description": "A merge, rebase, cherry-pick or bisect in progress, when there is one.",
"type": [
"string",
"null"
]
},
"upstream": {
"description": "The upstream ref, when the branch has one.",
"type": [
"string",
"null"
]
}
},
"required": [
"ahead",
"behind",
"changes"
],
"type": "object"
}
},
"description": "A plan over a working tree.\n```\nuse majordomus_cli::commit::{CommitPlan, PlanFingerprint, WorkingTreeState};\nlet plan = CommitPlan {\n fingerprint: PlanFingerprint::of(\"/r/.git\", \"/r\", &WorkingTreeState::default()),\n tree: WorkingTreeState::default(),\n groups: Vec::new(),\n diagnostics: Vec::new(),\n};\nassert!(plan.groups.is_empty(), \"a clean tree proposes nothing\");\n```",
"properties": {
"diagnostics": {
"description": "What a person should know before acting on it.",
"items": {
"$ref": "#/$defs/Diagnostic"
},
"type": "array"
},
"fingerprint": {
"$ref": "#/$defs/PlanFingerprint",
"description": "What the plan was derived from, and what makes it stale."
},
"groups": {
"description": "The commits proposed, in the order they should be made.",
"items": {
"$ref": "#/$defs/CommitGroup"
},
"type": "array"
},
"tree": {
"$ref": "#/$defs/WorkingTreeState",
"description": "The tree as it stood."
}
},
"required": [
"fingerprint",
"tree",
"groups",
"diagnostics"
],
"title": "CommitPlan",
"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.