moon_api_guard

Public API compatibility checks for MoonBit packages.

api-compatibility
semver
mbti
ci
moonbit
moon add FidollarinLA/moon_api_guard@0.6.0
Download zip
Version
0.6.0
License
Apache-2.0
Last updated
23 hours ago
Downloads
43

Dependencies

README

#moon_api_guard

MoonBit CI MoonBit License

面向 MoonBit 包的公共 API 兼容性守卫:比较新旧 .mbti 接口快照,在破坏性变更进入发布版本前给出明确报告、SemVer 建议和 CI 退出码。

A public API compatibility guard for MoonBit packages, powered by the formal .mbti interfaces generated by moon info.

#一眼了解

项目能力
输入两个 .mbti 文件,或两个包含 .mbti 的目录
检测函数、关联方法、struct、enum、type alias、trait、impl、公开字段等
结果breaking / compatible 分类、变更明细、SemVer 建议
报告Text、Markdown、JSON、HTML、SARIF 2.1.0
CI 行为兼容返回 0;发现破坏返回 1;参数或文件错误返回 2
策略默认严格保护穷举匹配,也支持 allow / ignore 和 JSON 策略文件
验证115 个测试;Linux、macOS、Windows CI;check / test / build / fmt / info

#OSC 2026 验收入口

本项目参加 MoonBit 国产开源生态大赛 OSC 2026。在仓库根目录运行一条命令即可复现完整验收:

./scripts/acceptance.sh

针对正式验收反馈,仓库已经补齐以下回归证据:

验收反馈当前实现与证据
不同类型的同名关联方法产生误报比较标识使用 Type::method 限定名;真实解析回归夹具位于 fixtures/regression/
enum 新增 variant 被误判为兼容variant-added 默认是 breaking,避免下游穷举匹配漏报;payload 改动另报 variant-changed
CI 未覆盖 moon build,格式门禁失败check.yml 在三种系统运行 check / test / build / examples,并用 git diff --exit-code 验证 fmt / info
GitHub 无工作流.github/workflows/ 包含主 CI、API Guard、可复用工作流和 Pages 部署
包发布、许可证、参考范围不完整包页面、Apache-2.0 LICENSE 与含许可证/参考范围的 References 均已列明

完整逐项矩阵和预期结果见 docs/acceptance.md,参赛材料见 OSC2026_项目申报书.md

#工作原理

moon info 会为 MoonBit 包生成正式接口快照。moon_api_guard 解析两个版本的快照,用“声明种类 + 限定名”识别 API,再应用兼容规则与自定义策略。

flowchart LR A["旧版 .mbti"] --> P["解析与规范化"] B["新版 .mbti"] --> P P --> I["ApiItem<br/>kind + qualified identity"] I --> R["语义兼容规则"] R --> C["策略覆盖<br/>allow / ignore / strict"] C --> O["ApiReport"] O --> E["退出码与 SemVer 建议"] O --> F["Text / Markdown / JSON<br/>HTML / SARIF"]

这使它适合三个常见场景:

  • 发布前检查:比较已发布基线与当前接口。
  • PR 审查:直接展示新增、删除和签名变化。
  • CI 门禁:未按策略处理的破坏性变更会阻止流水线。

#快速开始

#运行 CLI

CLI 当前随源码仓库提供:

git clone https://github.com/FidollarinLA/moon_api_guard.git cd moon_api_guard moon update # 比较两个文件;示例夹具包含破坏性变更,因此预期退出码为 1 moon run cmd/main -- check fixtures/old_api.mbti fixtures/new_api.mbti # 比较目录并生成 SARIF moon run cmd/main -- check-dir fixtures/dir_old fixtures/dir_new \ --format sarif --output api-guard.sarif

也可以运行交互式演示和库示例:

./scripts/demo.sh moon run examples/basic moon run examples/semantics

#作为 MoonBit 库使用

moon add FidollarinLA/moon_api_guard@0.6.0

let report = @lib.compare_mbti_content(old_mbti_text, new_mbti_text)
println(report.breaking_count())
println(report.compatible_count())
println(report.semver_suggestion())
println(report.release_blocked())

也可以先解析,再执行比较:

///|
let old_api = @lib.parse_mbti_content(old_mbti_text)

///|
let new_api = @lib.parse_mbti_content(new_mbti_text)

///|
let report = @lib.compare_api(old_api, new_api)

#CLI

moon run cmd/main -- check <old.mbti> <new.mbti> [options] moon run cmd/main -- check-dir <old-dir> <new-dir> [options] moon run cmd/main -- baseline update <source.mbti> <baseline.mbti>

常用示例:

# 只输出破坏性变化 moon run cmd/main -- check old.mbti new.mbti --breaking-only --format json # 显式允许新增 enum variant,同时启用严格策略 moon run cmd/main -- check old.mbti new.mbti --allow variant-added --strict # 从 JSON 文件载入团队策略 moon run cmd/main -- check old.mbti new.mbti --policy policies/api-policy.json # 忽略内部目录并输出 HTML moon run cmd/main -- check-dir old_dir new_dir \ --ignore-path '**/internal/**' --format html --output api-report.html

退出码含义
0未发现破坏性变更
1发现破坏性变更,release_blocked = true
2参数无效或文件读取失败

#报告格式

ApiReport 可输出适合终端、评审和自动化系统的多种格式:

println(report.markdown_summary())
println(report.json_summary())
println(report.html_summary())
println(report.sarif_summary())

JSON 中每个变化均包含 categorydetailkindnamemessage。SARIF 2.1.0 使用 error 表示 breaking、note 表示 compatible,可作为 GitHub Code Scanning 输入或 CI 构件保存。

#CI 集成

仓库提供三种可直接复用的材料:

最小门禁命令:

moon info moon run cmd/main -- check baseline/pkg.generated.mbti pkg.generated.mbti

如果公共 API 的变化是有意为之,应在同一个 PR 中更新基线:

moon run cmd/main -- baseline update \ pkg.generated.mbti baseline/pkg.generated.mbti

#兼容规则

默认规则以“下游代码是否可能停止编译”为判断标准。典型 breaking 明细包括:

  • 函数与方法:return-type-changedparameter-removedparameter-addedparameter-changed
  • 可见性与属性:visibility-tightenedderive-removedderive-changed
  • 类型:type-kind-changedtype-alias-target-changedtuple-field-changedstruct-shape-changed
  • 字段与关联方法:field-removedfield-type-changedmethod-removedmethod-changed
  • enum:variant-removedvariant-changedvariant-added
  • trait / impl:trait-bound-changedimpl-header-changedimpl-method-changed

新增普通 API、放宽可见性和 deprecated 属性变化等会作为 compatible 报告。完整规则、原因与策略覆盖方式见 docs/rules.md

#Library API

API用途
parse_mbti_content / parse_mbti_items.mbti 文本解析成规范化 API 项
compare_api / compare_mbti_content生成完整兼容性报告
compare_mbti_content_with_policy使用团队自定义策略比较
default_compat_policy / strict_compat_policy获取内置策略
policy_allow / policy_ignore / policy_from_json_text构建或加载策略覆盖
ApiReport::breaking_only / ApiReport::scoped过滤结果或附加文件作用域
merge_api_reports合并目录内多个报告
ApiReport::semver_suggestion / release_blocked生成版本建议与门禁信号
ApiReport::*_summary生成 Markdown、JSON、HTML、SARIF 等报告

前端静态演示位于 web/

#开发与验证

moon check --deny-warn moon test --deny-warn moon build moon run examples/basic moon run examples/semantics moon info moon fmt git diff --exit-code

或者直接运行与 CI 同口径的验收脚本:

./scripts/acceptance.sh

#发布

模块名为 FidollarinLA/moon_api_guard,与 GitHub、GitLink 和 mooncakes 账号一致。

moon login moon publish --dry-run moon publish

发布步骤、双远端同步和检查清单见 docs/publishing.md

#Roadmap

  • 为 SARIF 结果补充更精确的源码行列映射。
  • 扩展 ignore-path glob 语义和跨平台一致性测试。
  • 增加更多真实 MoonBit 包的版本回归语料。

#License

本项目以 Apache License 2.0 开源。

#References

本项目为原创 MoonBit 实现,没有移植下列项目的代码。设计阶段仅参考它们在 SemVer 分类、报告表达和 CI 集成方面的公开思路:

ProjectLinkLicenseScope of reference
cargo-semver-checkshttps://github.com/obi1kenobi/cargo-semver-checksApache-2.0 / MITSemVer 与 breaking-change 分类思路
japicmphttps://github.com/siom79/japicmpApache-2.0库发布时的 API diff 报告组织方式
Revapihttps://github.com/revapi/revapiApache-2.0CI 集成与版本升级建议

作者与参赛身份见 AUTHORS.md

#
ApiChange

pub(all) struct ApiChange {
category : String
detail : String
kind : String
name : String
message : String
old_signature : String
new_signature : String
} derive(Eq,
Debug
)

A single compatibility finding between two API snapshots.

  • category is "breaking" or "compatible".
  • detail is a machine-readable reason, such as removed, return-type-changed, parameter-removed, visibility-tightened, field-removed, method-changed, or variant-added.
  • kind / name identify the affected API item.
  • message is a human-readable explanation for reports.

#
ApiChange::json_object

fn ApiChange::json_object(self : ApiChange) -> String

Render one change as a JSON object with category, detail, kind, name, and message fields. String values are escaped.

#
ApiChange::markdown_line

fn ApiChange::markdown_line(self : ApiChange) -> String

Render one change as a Markdown list line with its kind, name, and human-readable message.

#
ApiItem

pub(all) struct ApiItem {
kind : String
name : String
signature : String
} derive(Eq,
Debug
)

A public API item extracted from a MoonBit interface file.

  • kind is the item category, such as fn, struct, enum, trait, type, alias, fnalias, const, let, suberror, or impl.
  • name is the normalized item name used to match old and new snapshots.
  • signature is the full declaration text, including a multi-line body for structs, enums, suberrors, and traits.

#
ApiReport

pub(all) struct ApiReport {
changes : Array[ApiChange]
} derive(
Debug
)

Compatibility report for one old/new API comparison.

Wraps the list of findings and derives release signals from it: breaking/compatible counts, a SemVer suggestion, a release blocking flag for CI, and Markdown/JSON summaries.

#
ApiReport::breaking_count

fn ApiReport::breaking_count(self : ApiReport) -> Int

Count breaking changes in the report.

#
ApiReport::breaking_only

fn ApiReport::breaking_only(self : ApiReport) -> ApiReport

Keep only breaking changes. Useful for CI summaries that should ignore compatible additions.

#
ApiReport::compatible_count

fn ApiReport::compatible_count(self : ApiReport) -> Int

Count compatible changes (additions only).

#
ApiReport::github_summary

fn ApiReport::github_summary(self : ApiReport) -> String

Compact Markdown suitable for GitHub Actions job summaries.

#
ApiReport::html_summary

fn ApiReport::html_summary(self : ApiReport) -> String

Render a self-contained interactive HTML report suitable for local demo / CI artifact upload. Includes client-side filter and search.

#
ApiReport::json_summary

fn ApiReport::json_summary(self : ApiReport) -> String

Render the report as a single-line JSON object for CI consumption. The output contains breaking, compatible, semver, release_blocked, and a changes array of per-change objects.

#
ApiReport::markdown_summary

fn ApiReport::markdown_summary(self : ApiReport) -> String

Render the report as a Markdown document: a summary header with breaking/compatible counts and the SemVer suggestion, followed by a ## Changes section listing every change when any exist. Multi-file scopes (path.mbti::item) are grouped under ### path headings.

#
ApiReport::release_blocked

fn ApiReport::release_blocked(self : ApiReport) -> Bool

Whether a release should be blocked in CI. Returns true when the report contains at least one breaking change.

#
ApiReport::sarif_summary

fn ApiReport::sarif_summary(self : ApiReport) -> String

Render a SARIF 2.1.0 log suitable for GitHub Code Scanning and CI artifact consumers. Breaking changes use SARIF error level while compatible findings use note. Scoped directory comparisons attach the originating .mbti file as an artifact location.

#
ApiReport::scoped

fn ApiReport::scoped(self : ApiReport, scope : String) -> ApiReport

Prefix every change name with scope:: so multi-file reports remain attributable to a concrete .mbti path.

#
ApiReport::semver_advice

fn ApiReport::semver_advice(self : ApiReport) -> String

Short human-readable SemVer advice for CI logs and HTML reports.

#
ApiReport::semver_suggestion

fn ApiReport::semver_suggestion(self : ApiReport) -> String

Suggest the smallest SemVer bump implied by the report: "major" when any breaking change exists, "minor" when only compatible changes exist, and "patch" when nothing changed.

#
CompatPolicy

pub(all) struct CompatPolicy {
overrides : Array[RuleOverride]
} derive(
Debug
)

Compatibility policy applied after structural classification.

Use default_compat_policy() for MoonBit-oriented defaults, or strict_compat_policy() to treat nearly all signature diffs as breaking unless overridden.

#
RuleOverride

pub(all) struct RuleOverride {
detail : String
category : String
} derive(Eq,
Debug
)

Severity override for one classification detail key.

  • category is "breaking", "compatible", or "ignore".
  • "ignore" drops matching findings from the report entirely.

#
api_item

fn api_item(kind : String, name : String, signature : String) -> ApiItem

Create a normalized public API item.

Example:

let item = api_item("fn", "parse", "pub fn parse(String) -> Int")

#
compare_api

fn compare_api(old_items : Array[ApiItem], new_items : Array[ApiItem]) -> ApiReport

Compare two public API snapshots and produce a compatibility report.

Classification rules:
  • An item present in old_items but missing in new_items is a breaking removed change.
  • An item present in both but with a different signature is classified further (return type, parameters, visibility, fields, methods, variants). Enum variant additions are breaking because exhaustive pattern matching in downstream code may no longer compile.
  • An item only present in new_items is a compatible added change.
  • Marking an existing item with #deprecated (body unchanged) is a compatible deprecated change.

Uses default_compat_policy() for severity remaps (for example, labeled optional parameter additions are compatible).

#
compare_api_with_policy

fn compare_api_with_policy(old_items : Array[ApiItem], new_items : Array[ApiItem], policy : CompatPolicy) -> ApiReport

Compare two API snapshots with an explicit compatibility policy.

#
compare_mbti_content

fn compare_mbti_content(old_content : String, new_content : String) -> ApiReport

Parse and compare two .mbti file contents in one step.

This is the most convenient library entry when callers already have old/new interface text and do not need intermediate ApiItem lists.

Example:

let report = compare_mbti_content(old_mbti, new_mbti)
assert_eq(report.semver_suggestion(), "major")

#
compare_mbti_content_with_policy

fn compare_mbti_content_with_policy(old_content : String, new_content : String, policy : CompatPolicy) -> ApiReport

Parse and compare two .mbti contents with an explicit policy.

#
default_compat_policy

fn default_compat_policy() -> CompatPolicy

MoonBit-oriented defaults: adding labeled optional parameters is compatible; enum variant additions remain breaking.

#
file_added_report

fn file_added_report(path : String) -> ApiReport

Compatible report for a .mbti file present only in the new snapshot.

#
file_removed_report

fn file_removed_report(path : String) -> ApiReport

Breaking report for a .mbti file present in the old snapshot but missing from the new one.

#
merge_api_reports

fn merge_api_reports(reports : Array[ApiReport]) -> ApiReport

Merge multiple reports into one by concatenating their changes.

#
parse_mbti_content

fn parse_mbti_content(content : String) -> Array[ApiItem]

Parse the full text of a .mbti file (as produced by moon info) into normalized public API items.

This is the most convenient entry point when the caller has already read an interface file into a string.

#
parse_mbti_items

fn parse_mbti_items(lines : Array[String]) -> Array[ApiItem]

Parse .mbti interface lines into normalized public API items.

Supported declarations include functions, associated functions (Type::method), fnalias, typealias, type, const, impl, and multi-line struct / enum / suberror / trait blocks. Comment lines, package / import headers, and unknown lines are skipped. Attribute lines starting with # (such as #deprecated) are attached to the declaration that follows them.

#
path_is_ignored

fn path_is_ignored(path : String, patterns_csv : String) -> Bool

True when path matches any pattern in a comma-separated list.

#
path_matches_ignore_pattern

fn path_matches_ignore_pattern(path : String, pattern : String) -> Bool

Whether path matches a simple ignore-path pattern. Supports exact match, one * wildcard, and **/ prefix forms.

#
policy_allow

fn policy_allow(policy : CompatPolicy, detail : String) -> CompatPolicy

Remap one detail key to "compatible".

#
policy_from_json_text

fn policy_from_json_text(text : String) -> CompatPolicy?

Build a CompatPolicy from a JSON policy document.

Expected shape:
{ "strict": false, "allow": ["variant-added"], "ignore": ["deprecated"] }

Missing fields default to strict=false and empty allow/ignore lists. Returns None when the text is not valid policy JSON.

#
policy_ignore

fn policy_ignore(policy : CompatPolicy, detail : String) -> CompatPolicy

Drop findings with the given detail key from reports.

#
policy_to_json_text

fn policy_to_json_text(policy : CompatPolicy, strict? : Bool) -> String

Serialize a policy into the JSON shape accepted by policy_from_json_text.

#
policy_with_override

fn policy_with_override(policy : CompatPolicy, detail : String, category : String) -> CompatPolicy

Insert or replace an override for detail.

#
strict_compat_policy

fn strict_compat_policy() -> CompatPolicy

Strict policy with no built-in remaps. Attribute-only deprecations remain compatible because they are classified before policy remaps.