MoonGuard is a MoonBit public API compatibility and SemVer guard. It compares two MoonBit interface snapshots, reports public API additions, removals, and signature changes, then recommends whether the next release should be patch, minor, or major.
The project is being built for the MoonBit open-source ecosystem competition as engineering infrastructure for package authors and CI workflows.
The competition submission, validation evidence, design report, compatibility rules, and package-release checklist are indexed in docs/README.md. This index is the recommended starting point for reviewers.
- Parse public declarations from
.mbti-style interface text. - Normalize whitespace and declaration order.
- Track public
fn,type,typealias,struct,enum,trait,impl,let, andconstdeclarations. - Track common generated
.mbtimembers including struct fields, enum constructors, trait methods, generic methods,suberror, andpub usingtype or trait exports. - Classify public API changes:
- added top-level public API -> minor
- added struct fields, enum/suberror constructors, or required trait methods -> major (conservative)
- removed public API -> major
- changed signature -> major
- unchanged public API -> patch
- Render Markdown and JSON compatibility reports for PR comments, release notes, and downstream tooling.
- Validate whether a proposed SemVer bump satisfies the recommended impact for single-file or package-directory comparisons.
- Evaluate compatibility exceptions with auditable policy rules that retain the original report, accepted changes, reasons, deadlines, match budgets, and diagnostics. Legacy ignore rules remain available for compatibility.
- Share repeated CLI defaults through simple config files.
- Compare package directories, detect duplicate symbols and directory diagnostics, ignore generated/vendor/tool directories, and render snapshot inventories.
- Identify directory APIs by parent-directory package scope plus symbol, so
moving a declaration between
.mbtifiles inside one package is not reported as an API change while same-named symbols in different packages remain distinct. - Read
.mbtifiles and directories from the CLI when running on the JS backend. - Render package release plans that combine API impact, diagnostics, SemVer validation, and maintainer checklist items for release PRs.
Install the library from mooncakes.io:
moon add 918154429/moonguard@0.2.0Install the command-line package:
moon install 918154429/moonguard/cmd/main@0.2.0For source development, install the MoonBit toolchain, then clone this repository:
git clone https://github.com/918154429/moonguard.git
cd moonguard
moon check
moon test
moon test --target jslet old_api = "pub fn render(String) -> String"
let new_api = "pub fn render(String, Options) -> String"
let report = @moonguard.diff_interfaces(old_api, new_api)
let markdown = @moonguard.render_markdown_report(report)The report recommendation is patch, minor, or major.
Compare two .mbti files with the default Markdown report:
moon run --target js cmd/main -- report fixtures/old.mbti fixtures/new.mbtiOutput:
# MoonGuard API Compatibility Report
- Recommendation: **major**
- Changes: 2
| Impact | Change | Symbol | Details |
| --- | --- | --- | --- |
| major | changed | `fn render` | `render(String) -> String` -> `render(String, Options) -> String` |
| minor | added | `fn parse` | `parse(String) -> Unit` |Render the same report as JSON:
moon run --target js cmd/main -- report fixtures/old.mbti fixtures/new.mbti --format jsonCheck whether a planned version bump is sufficient:
moon run --target js cmd/main -- check fixtures/old.mbti fixtures/new.mbti --current 0.1.0 --next 0.2.0Use a config file for repeated CI defaults:
format = json
current = 0.1.0
next = 1.0.0
baseline = fixtures/old.mbti
target = fixtures/new.mbti
baseline_dir = fixtures/dir-old
target_dir = fixtures/dir-new
policy_file = fixtures/allow-render.policy
policy_version = 0.2.0
moon run --target js cmd/main -- report --config fixtures/moonguard-ci.conf
moon run --target js cmd/main -- check --config fixtures/moonguard-ci.confCompare package directories and inspect a generated interface inventory:
moon run --target js cmd/main -- report-dir fixtures/dir-old fixtures/dir-new
moon run --target js cmd/main -- check-dir fixtures/dir-old fixtures/dir-new --current 0.1.0 --next 1.0.0
moon run --target js cmd/main -- release-plan fixtures/dir-old fixtures/dir-new --current 0.1.0 --next 1.0.0
moon run --target js cmd/main -- inventory-dir fixtures/dir-new --format json
moon run --target js cmd/main -- report-dir --config fixtures/moonguard-ci.conf
moon run --target js cmd/main -- check-dir --config fixtures/moonguard-ci.conf
moon run --target js cmd/main -- release-plan --config fixtures/moonguard-ci.conf
moon run --target js cmd/main -- inventory-dir --config fixtures/moonguard-ci.confThe release-plan command is intended for application-facing package workflows: it prints whether a release is ready, blocked by snapshot diagnostics, or needs a larger version bump, then includes checklist items for release notes and migration review.
Ignore files filter report changes without changing the parsed API model:
ignore fn debug_tmp temporary demo API
ignore field Options.experimental
ignore * pkg.generated.mbti::internal_*
moon run --target js cmd/main -- report fixtures/old.mbti fixtures/new.mbti --ignore-file fixtures/ignore-render.rulesFor release governance, prefer an auditable policy file:
allow changed fn render until 0.2.0 max_matches 1 reason render migration reviewed
allow removed fn legacy_* max_matches 2 reason legacy cleanup approved
The syntax is allow CHANGE_KIND ITEM_KIND NAME [until VERSION] [max_matches N] reason TEXT.... CHANGE_KIND is added, removed,
changed, or *; item kind and name also support * matching. A non-empty
reason is mandatory and max_matches defaults to 1. Expired rules, malformed
rules, missing policy versions, and exceeded budgets fail closed with exit code
2. Unmatched and overlapping rules remain visible as warnings.
moon run --target js cmd/main -- report fixtures/old.mbti fixtures/new.mbti --policy-file fixtures/allow-render.policy --policy-version 0.2.0
moon run --target js cmd/main -- check fixtures/old.mbti fixtures/new.mbti --current 0.1.0 --next 0.2.0 --policy-file fixtures/allow-render.policyPolicy output preserves both original_report and effective_report, plus the
accepted changes and rule diagnostics. check, check-dir, and release-plan
use --next as the policy version when --policy-version is omitted.
--ignore-file and --policy-file are mutually exclusive, and
inventory-dir does not accept policy files.
File and directory commands currently require the JS backend because the CLI
uses Node fs.readFileSync and directory APIs through MoonBit JS externs.
For strict CI exit-code checks, run the generated JS with Node directly after a
JS-target command has built it:
node _build/js/debug/build/cmd/main/main.js check fixtures/old.mbti fixtures/new.mbti --current 0.1.0 --next 0.2.0
node _build/js/debug/build/cmd/main/main.js check-dir fixtures/dir-old fixtures/dir-new --current 0.1.0 --next 1.0.0For quick demos without files, use report-text:
moon run cmd/main -- report-text "pub fn render(String) -> String" "pub fn render(String, Options) -> String" --format jsonMoonGuard can compare a committed release baseline with interfaces generated in
a pull request, fail on an insufficient version bump, and upload a JSON release
plan. See docs/github-actions.md for a complete
downstream workflow pinned to the v0.1.0 release.
MoonGuard starts with conservative rules:
- Removing a public declaration is a breaking change.
- Changing a public signature is a breaking change.
- Adding an ordinary top-level public declaration is minor-compatible.
- Adding a struct field, enum/suberror constructor, or required trait method is conservatively breaking because it can invalidate construction, exhaustive matching, or existing trait implementations.
- Reordering declarations, changing comments, or changing whitespace does not affect the public API model.
The parser intentionally covers high-frequency .mbti declarations first,
including common nested members from generated interface files. Any
unrecognized pub line is retained as unknown so that public surface changes
remain visible instead of being silently ignored.
For directory snapshots, a symbol's logical identity is its parent-directory
scope plus its declaration identity; the .mbti filename is provenance, not
identity. Therefore moving pkg/a.mbti::render to pkg/b.mbti::render produces
no compatibility change. Moving it to another package scope does change its
logical name, and duplicate definitions within one package are still diagnosed.
The complete rule table and rationale are in docs/api-compat-rules.md.
The repository includes 15 pinned public pkg.generated.mbti samples from
official and community MoonBit projects. MoonGuard currently extracts 6819 API
items from this corpus with zero unknown declarations and zero snapshot
diagnostics. All 15 samples are fully modeled, including one historical sample
whose generated interface contains 119 associated fn/impl lines without a
public visibility prefix. Legacy inference is restricted to files identified
by the moon info generator header and package declaration.
- Sources, revisions, and licenses: fixtures/real/SOURCES.md
- Per-sample compatibility matrix: docs/real-world-compatibility.md
- Reproducible demo and validation record: docs/demo-report.md
- Self-hosted and external API evolution reports: docs/evidence/README.md
The implementation is split by responsibility: api_model.mbt, parser.mbt,
snapshot.mbt, diff.mbt, semver.mbt, policy.mbt,
report_markdown.mbt, and report_json.mbt. The CLI is likewise separated
into argument parsing, configuration, commands, output, snapshot I/O, and
backend-specific file I/O under cmd/main.
Current validation result: 164/164 default-target tests and 165/165 JS-target tests pass.
Common checks:
moon fmt
moon info
moon check
moon test
moon test --target js
moon run --target js cmd/main -- report fixtures/old.mbti fixtures/new.mbti --format json
moon run --target js cmd/main -- report --config fixtures/moonguard-ci.conf
moon run --target js cmd/main -- report-dir fixtures/dir-old fixtures/dir-new
moon run --target js cmd/main -- report-dir --config fixtures/moonguard-ci.conf
moon run --target js cmd/main -- check-dir fixtures/dir-old fixtures/dir-new --current 0.1.0 --next 1.0.0
moon run --target js cmd/main -- check-dir --config fixtures/moonguard-ci.conf
moon run --target js cmd/main -- release-plan --config fixtures/moonguard-ci.conf
moon run --target js cmd/main -- inventory-dir --config fixtures/moonguard-ci.conf
node _build/js/debug/build/cmd/main/main.js check fixtures/old.mbti fixtures/new.mbti --current 0.1.0 --next 0.2.0
node _build/js/debug/build/cmd/main/main.js check --config fixtures/moonguard-ci.conf
node _build/js/debug/build/cmd/main/main.js check-dir fixtures/dir-old fixtures/dir-new --current 0.1.0 --next 1.0.0
node _build/js/debug/build/cmd/main/main.js check-dir --config fixtures/moonguard-ci.confGenerated pkg.generated.mbti files are kept in the repository so interface
changes are reviewable after moon info.
Competition source-line tracking counts repository .mbt source files and
excludes generated _build output. The current tracked source total is 8580
lines, including 5215 non-test lines. The confirmed competition threshold is
4000 lines.
See docs/competition-plan.md for the competition plan.