Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Change classification

monochange change classify produces a release-aware severity report for packages affected by a pull request or local worktree.

Comparisons

The classifier resolves these comparisons:

KindBaseHeadPurpose
pullRequestThe remote default branch, or --baseThe synthetic merge result of the base and --headChanges introduced after the pull request merges
sourceDeltaThe merge base of the default branch and the source candidateThe source candidateChanges authored on the pull request branch
workingTreeHEADThe staged, unstaged, deleted, and untracked filesA diagnostic view of local changes
releaseThe package release owner’s latest reachable tag, or --releaseThe synthetic merge resultAccumulated change since the latest release
releaseToDefaultThe same release tagThe default branchChange that has already accumulated before the pull request

pullRequest and release are exact two-endpoint comparisons. sourceDelta uses the merge base only to identify work authored on the branch. When --head is HEAD, the source candidate materializes committed and local changes into one temporary Git commit. The bump comes from the net candidate comparison, while workingTree remains a diagnostic view. A local edit that reverses a committed breaking change therefore removes that break from the proposal instead of adding a second, contradictory signal.

The candidate is a tree object created by git merge-tree --write-tree. monochange uses a temporary index for local changes and does not change the real index or worktree. If Git cannot create the merge tree, the report marks the comparison as conflicted, falls back to the source candidate, and requires human review.

Package decision

Each package has a decision object with these fields:

FieldMeaning
compatibility_impactbreaking, additive, compatible, or unmodeled between the default branch and the candidate
release_impactThe same verdict between the latest release and the candidate; absent when no release tag matched
pull_request_changesWhether the net candidate or the local working tree contains files that belong to this package
proposed_changeset_bumpThe highest current finding after the release cap: major, minor, patch, or none
enforceable_minimumThe highest bump supported by high-confidence evidence, after the same release cap
release_floorThe highest bump found between the latest release and the candidate
confidenceConfidence of the finding that determines proposed_changeset_bump
completenessWhether the analyzer claims complete, partial, or unsupported coverage
review_requiredWhether the proposal needs human or agent review before it becomes release intent
finding_idsStable identifiers for the findings that determine the proposal

compatibility_impact describes the default-branch comparison, so it also sees breaks against an API the default branch has not released yet. release_impact describes the same candidate against the package’s latest release. When a release comparison is analyzed, a modeled finding cannot propose a bump higher than the release-relative bump for the package, because nobody holding the latest release can observe a break in an item that release never shipped. A pull request that refines an unreleased API therefore reports compatibility_impact: breaking with release_impact: additive and a minor proposal instead of an inflated major. Unmodeled findings stay uncapped: they are the review floor for surfaces the analyzers cannot model, and the release comparison cannot refute them. release_floor still reports the complete unreleased interval, so a pull request can propose patch while the release floor is major because an earlier merged pull request introduced the breaking change.

pull_request_changes decides what the contribution itself may propose. Findings from the release and releaseToDefault comparisons describe what the default branch accumulated since the latest release, so a package the pull request does not modify reports proposed_changeset_bump: none, enforceable_minimum: none, completeness: complete, and review_required: false while release_floor and release_impact keep the accumulated change. A pending changeset for such a package still reports action: review, because a changeset may intentionally describe a consumer-facing effect implemented in another package, but the report no longer escalates the bump or the review verdict for work someone else already merged. A package becomes pull-request-scoped through the net candidate or through the local working tree, so a branch change that a local edit reverts stays in scope and keeps requiring review.

none is conclusive only when completeness is complete and review_required is false. A changed package without modeled semantic evidence receives a low-confidence patch proposal instead of a false none result. A decision is also complete when every current finding is complete. A high-confidence major finding makes the bump decision complete even when another analyzer is partial because no unmodeled finding can require a higher bump.

The package-level action is create, update, keep, review, or no_changeset. The report includes packages targeted only by a pending changeset and marks them review; a changeset can intentionally describe a consumer-facing effect implemented in another package, so monochange does not assume that unmatched intent is stale. Public dependency propagation retains the dependent package’s release owner, comparisons, and existing changesets.

Skipping a pull request

Some pull requests have no pending work to classify. The release pull request monochange opens only bumps versions and deletes consumed changesets, so every package would report the unmodeled fallback and bury the real signal.

Configure [changesets.classification].skip_labels and pass the pull request labels with --label:

[changesets.classification]
skip_labels = ["release"]
monochange change classify --format json --label release

When any configured label is present, the report sets skipped: true, analyzes no packages, names the matched labels in matched_skip_labels, and exits successfully. The recommended bump is none. Set skip_labels = [] to classify every pull request.

Findings

Each finding records its rule_id, API surface, change kind, compatibility impact, bump, confidence, analyzer id, engine and version, coverage note, optional fallback reason, source location, before and after signatures, and comparison membership. Markdown and text reports print the evidence directly below each finding so pull request comments retain the same provenance as JSON.

unmodeled means the change is real but sits outside the public surface the analyzer models, so no compatibility verdict applies. The package is supported; only that file change is outside the model. A changed package with no modeled finding receives the low-confidence monochange/unclassified-source fallback, which proposes patch and requires review because no analyzer can rule out a break.

Identical evidence found in several comparisons shares one finding and lists every comparison. If the same item has different before or after signatures across the pull-request and release intervals, monochange emits distinct comparison-qualified finding ids so that an agent never applies one interval’s signature evidence to another interval.

monochange compares package manifests at both endpoints. Adding or removing a package produces a monochange/package-lifecycle finding with complete coverage and high confidence. Removing a package proposes major, even when the package has no modeled public symbols. Adding one proposes minor.

The built-in Cargo, JavaScript, Deno, and Dart source analyzers inspect syntax and package metadata. Their findings are partial and medium-confidence because they do not prove every language compatibility rule. Cargo packages can opt into a cargo-semver-checks matrix for stronger Rust evidence.

One Rust case carries a stronger verdict than the syntax default. A public const or static whose declared type is a slice (&[T]), a fixed-size array ([T; N]), or Vec<T> and whose initializer is a literal is compared element by element. Adding elements to the end is additive with a minor proposal and high confidence, matching cargo semver-checks, because the element type and every existing element stay the same. Removing an element, reordering elements, editing an existing element, or changing the declared element type stays breaking, and a non-literal initializer such as a path or a vec![value; count] repeat stays conservative because its elements cannot be enumerated.

CLI command-surface findings

Packages that register a CLI under [package.<id>].cli get their command surface classified automatically. monochange diffs the committed baseline in .monochange/cli-snapshots/<name>.json against a fresh capture from the configured snapshot command and appends findings with surface: "cli". Removed commands, options, or positionals and value narrowing are breaking and propose major; additions are additive and propose minor; description-only changes are compatible. See package CLI registration.

Skip the comparison with --skip-cli-snapshots or MONOCHANGE_SKIP_CLI_SNAPSHOTS=1.

TypeScript declaration compatibility

Use semantic detection when an npm package publishes TypeScript types:

monochange change classify \
	--detection-level semantic \
	--format json \
	--dependency-propagation public

The npm adapter resolves the package’s explicit exports, types, or typings entrypoints, emits declarations for the before and after package snapshots, and asks the workspace’s TypeScript compiler to compare the resulting consumer contracts. It keeps import and require conditions separate. Declaration-only packages do not need a tsconfig.json.

The analyzer classifies evidence as follows:

EvidenceImpactProposed bump
Removed entrypoint, export, or non-assignable consumer contractbreakingmajor
Added entrypoint/export, overload, optional member, or input capabilityadditiveminor
Changed source with an equivalent or consumer-compatible declaration APIcompatiblenone
Unresolved config, wildcard export, generic/nominal identity, or failureunmodeledpatch

Complete TypeScript evidence requires node and a locally resolvable typescript package. Install TypeScript and the package’s dependencies in the workspace before classification. monochange reports the exact compiler version in finding.analyzer.version.

pnpm add --save-dev --workspace-root typescript
pnpm install --frozen-lockfile

Snapshots contain package files from each Git endpoint. monochange never substitutes a candidate package file into the baseline. An inherited config or dependency declaration that only exists in the current checkout is allowed so the compiler can proceed, but the finding records partial coverage and a fallback reason. Wildcard exports are also partial because an export pattern cannot be enumerated conclusively from package metadata alone.

Changed generic declarations and classes with private or protected identity are reported as inconclusive when TypeScript cannot compare the two snapshot identities safely. Runtime behavior, side effects, JavaScript-only packages, and unlisted dynamic entrypoints remain outside declaration compatibility. Review those changes even when the declaration result is none.

When the repository defines [package.*] entries, classification is limited to those configured packages. Package additional_paths and ignored_paths, plus [changesets.affected].ignored_paths, use the same path policy as changeset coverage. This keeps fixtures, tests, generated output, and other explicitly ignored paths from producing release recommendations.

Package discovery reads both comparison endpoints and joins packages by ecosystem and repository-relative manifest path. A package that exists only in the base remains in the report with its baseline package id, path policy, release owner, and tag format. Its before snapshot contains the package files, and its after snapshot is empty. This behavior also applies when the pull request removes the package’s [package.*] entry from monochange.toml.

Rust compatibility matrix

Install cargo-semver-checks, then opt into it for semantic classification:

cargo install cargo-semver-checks --locked
[ecosystems.cargo.semver_checks]
enabled = true
timeout_seconds = 300

[[ecosystems.cargo.semver_checks.matrix]]
name = "default"
feature_mode = "default"

[[ecosystems.cargo.semver_checks.matrix]]
name = "all-features"
feature_mode = "all"

[[ecosystems.cargo.semver_checks.matrix]]
name = "wasm"
feature_mode = "none"
features = ["wasm"]
target = "wasm32-unknown-unknown"

Install every configured target before classification, for example rustup target add wasm32-unknown-unknown. A matrix may contain at most 16 uniquely named cells. timeout_seconds applies to each cell and must be between 1 and 1800. Feature mode is default, all, none, or heuristic; the features, baseline_features, and current_features lists add common or endpoint-specific features.

The integration runs only with --detection-level semantic. monochange materializes each Git endpoint as a complete, isolated repository tree so workspace inheritance and path dependencies remain available. Each cell receives an isolated CARGO_TARGET_DIR. The JSON finding exposes the exact cell inputs, checked, failed, or skipped status, minimum bump, lint ids, authoritative lint references, and cargo-semver-checks version under coverage.checks.

Results merge conservatively:

Matrix resultClassification behavior
Any checked cell proves a breakPropose major, even if another cell fails
A checked cell reports a minor requirement and none breakPropose at least minor
Every configured cell is checked and requires no bumpReport compatible Rust API evidence; retain syntax-derived additions as minor evidence
A tool, target, build, timeout, or matrix cell is incompleteKeep conservative syntax findings and require review

cargo-semver-checks’ active lint set is authoritative for the rules it runs, but compatible additions are not exhaustively enabled by default. monochange therefore retains syntax-derived additions even after a clean matrix. Runtime behavior, undocumented supported configurations, and downstream build behavior remain outside this proof.

Security: cargo-semver-checks asks Cargo and rustdoc to build both endpoints. Build scripts and procedural macros can execute repository code. Enable this only for code you are prepared to execute. In CI, use a normal pull_request job with least-privilege credentials; do not run semantic classification on untrusted changes through pull_request_target or with publishing secrets.

Output and validation

Markdown output is intended for terminal output, pull request comments, and job summaries:

monochange change classify --format markdown --dependency-propagation public

JSON is the stable agent and automation interface. The top-level schema_version changes when the JSON contract changes:

  • 0.3 adds decision.pull_request_changes and scopes decision.proposed_changeset_bump, decision.enforceable_minimum, and decision.review_required to packages the pull request actually touches.
  • 0.2 adds decision.release_impact and caps decision.proposed_changeset_bump, decision.enforceable_minimum, and decision.release_floor with the release comparison.
  • 0.1 is the first contract published by the monochange_classification crate: snake_case keys, unmodeled instead of unknown, and the top-level skipped, summary, and matched_skip_labels fields.
monochange change classify --format json --dependency-propagation public

monochange changeset validate --api fails only when a pending changeset is lower than enforceable_minimum. --strict compares pending changesets with proposed_changeset_bump, including partial and medium-confidence evidence.

monochange changeset validate --api --format markdown
monochange changeset validate --api --strict --format markdown

The monochange_classify_changes MCP tool returns the same report under its report field. It accepts base, head, release, packages, detection_level, include_unchanged, and dependency_propagation inputs. Agents should use dependency_propagation: "public" for the same package coverage as the canonical CLI workflow.

CI requirements

Release comparison needs the relevant tags and history. A shallow checkout can make the latest release unavailable or produce an incomplete merge base. CI jobs that publish classification reports fetch the default branch and tags before running the command.

Creating or updating the pull request comment needs the pull-requests: write scope. With only issues: write the API returns 403 Resource not accessible by integration, and the action warns instead of publishing the comment; fork pull requests receive a read-only token, which produces the same warning. A classification job can always write the Markdown report to the job summary and expose it as an output even when the provider refuses the comment.

The change-classification GitHub Action runs the canonical JSON command, writes a job summary, and creates or updates one marker comment:

permissions:
  contents: read
  pull-requests: write

steps:
  - uses: actions/checkout@v6
    with:
      fetch-depth: 0
      ref: ${{ github.event.pull_request.head.sha }}
  - name: Install Rust semantic analyzer
    if: ${{ hashFiles('**/Cargo.toml') != '' }}
    run: cargo install cargo-semver-checks --locked
  - id: classify
    uses: monochange/actions/change-classification@v0
    with:
      detection-level: semantic
      dependency-propagation: public

Checking out the pull request head SHA keeps GitHub’s synthetic test-merge commit out of the source candidate. The action exposes json, markdown, recommendation, review-required, and summary outputs. Use recommendation for routing, but inspect the package decisions in json before writing changesets whenever review-required is true. The action accepts every report with classification schema_version 0.1 or newer. The evidence fields the action reads (packages, decisions, findings) are stable across those schema versions.

For complete TypeScript evidence, install the repository dependencies before this step. For configured Rust target cells, install those targets before the action. Keep the workflow on pull_request; the analyzer may execute changed build scripts and procedural macros. Comment creation is best-effort. Fork pull requests with read-only tokens still receive the action outputs and job summary.

The changeset-policy action can turn the same classification into an enforced gate. Pass from instead of changed-paths so monochange step affected-packages --verify --from <ref> derives changed packages from git history and compares each attached changeset bump with the classified change type:

steps:
  - uses: actions/checkout@v6
    with:
      fetch-depth: 0
  - uses: monochange/actions/changeset-policy@v0
    with:
      from: origin/main
      comment-on-failure: true

A changeset that understates the classified change type fails the policy and names the package with the requested and recommended bumps. A higher bump only warns, and changesets with bump: none or an explicit target version skip the alignment check. from requires a full-history checkout and takes priority over changed-paths.