///|
fn push_manifest_findings(
findings : Array[Finding],
snapshot : ProjectSnapshot,
manifest : PackageManifest,
) -> Unit {
if manifest.module_name.is_empty() {
findings.push(
fail(
"moon.mod.name",
PackageConfig,
"moon.mod declares package name",
"The module name is missing.",
"Set name = \"/\" before publishing.",
"name",
penalty=12,
),
)
} else if !manifest.has_valid_name {
findings.push(
fail(
"moon.mod.name-format",
PackageConfig,
"moon.mod package name is publishable",
"The package name should be owner/package and use URL-safe characters.",
"Use the same owner and package name in moon.mod and Mooncakes.",
manifest.module_name,
penalty=8,
),
)
} else {
findings.push(
pass(
"moon.mod.name",
PackageConfig,
"moon.mod package name is present",
manifest.module_name,
),
)
}
if manifest.has_semver {
findings.push(
pass(
"moon.mod.version",
PackageConfig,
"Version is SemVer",
manifest.version,
),
)
} else {
findings.push(
fail(
"moon.mod.version",
PackageConfig,
"Version is SemVer",
"Mooncakes releases should use a concrete x.y.z version.",
"Use version = \"0.1.0\" for the first public release.",
manifest.version,
penalty=8,
),
)
}
if manifest.readme == "README.md" {
findings.push(
pass(
"moon.mod.readme",
PackageConfig,
"Readme points to README.md",
manifest.readme,
),
)
} else {
findings.push(
warn(
"moon.mod.readme",
PackageConfig,
"Readme metadata uses README.md",
"Reviewers expect a normal README.md file at the repository root.",
"Set readme = \"README.md\".",
manifest.readme,
penalty=4,
),
)
}
if manifest.repository.has_prefix("https://github.com/") &&
manifest.repository.has_suffix(".git") {
findings.push(
pass(
"moon.mod.repository",
PackageConfig,
"Repository URL is a GitHub git URL",
manifest.repository,
),
)
} else {
findings.push(
fail(
"moon.mod.repository",
PackageConfig,
"Repository URL is public and complete",
"The repository metadata is empty or not a GitHub git URL.",
"Set repository = \"https://github.com//.git\".",
manifest.repository,
penalty=10,
),
)
}
if known_osi_license(manifest.license) {
findings.push(
pass(
"moon.mod.license",
License,
"License is OSI-recognized",
manifest.license,
),
)
} else {
findings.push(
fail(
"moon.mod.license",
License,
"License is OSI-recognized",
"The package license is missing or not in the built-in SPDX allow list.",
"Use MIT or Apache-2.0 unless your project has a stronger reason.",
manifest.license,
penalty=10,
),
)
}
if manifest.description.length() >= 20 {
findings.push(
pass(
"moon.mod.description",
PackageConfig,
"Description explains the package",
manifest.description,
),
)
} else {
findings.push(
warn(
"moon.mod.description",
PackageConfig,
"Description explains the package",
"A very short description makes the Mooncakes package harder to evaluate.",
"Write one clear sentence about the reusable capability.",
manifest.description,
penalty=4,
),
)
}
if snapshot.mooncakes_owner == "" ||
snapshot.mooncakes_package == "" ||
(
snapshot.mooncakes_owner == manifest.owner &&
snapshot.mooncakes_package == manifest.package_name
) {
findings.push(
pass(
"mooncakes.name-sync",
Mooncakes,
"Mooncakes owner/package match moon.mod",
manifest.module_name,
),
)
} else {
findings.push(
fail(
"mooncakes.name-sync",
Mooncakes,
"Mooncakes owner/package match moon.mod",
"Snapshot metadata does not match moon.mod.",
"Keep Mooncakes, README and moon.mod names identical.",
snapshot.mooncakes_owner + "/" + snapshot.mooncakes_package,
penalty=10,
),
)
}
}
///|
fn push_readme_findings(
findings : Array[Finding],
snapshot : ProjectSnapshot,
manifest : PackageManifest,
) -> Unit {
let readme = snapshot.readme
if !has_text(readme) {
findings.push(
fail(
"readme.exists",
Readme,
"README is present",
"README content is empty.",
"Add a complete README with goal, installation, usage, API, tests and license.",
"README.md",
penalty=14,
),
)
return
}
findings.push(pass("readme.exists", Readme, "README is present", "README.md"))
if contains_ci(readme, snapshot.name) ||
contains_ci(readme, manifest.package_name) {
findings.push(
pass(
"readme.identity",
Readme,
"README identifies the project",
snapshot.name,
),
)
} else {
findings.push(
warn(
"readme.identity",
Readme,
"README identifies the project",
"The README does not clearly name the project.",
"Mention the package name near the top.",
snapshot.name,
penalty=3,
),
)
}
let required = [
(
"readme.problem",
"Problem statement",
["解决", "problem", "why", "用途"],
),
("readme.install", "Installation", ["安装", "moon add", "install"]),
(
"readme.usage",
"Minimum usage example",
["使用", "example", "moon run", "示例"],
),
("readme.api", "Core API list", ["api", "核心功能", "功能列表"]),
("readme.scope", "Supported scope", ["支持范围", "scope"]),
(
"readme.out-of-scope",
"Unsupported scope",
["暂不支持", "out of scope"],
),
(
"readme.tests",
"Verification commands",
["moon check", "moon build", "moon test"],
),
(
"readme.license",
"License and third-party notes",
["license", "许可证", "third-party", "第三方"],
),
]
for item in required {
let (id, title, needles) = item
if contains_any(readme, needles) {
findings.push(pass(id, Readme, title, needles[0]))
} else {
findings.push(
warn(
id,
Readme,
title,
"A required README section or keyword was not found.",
"Add a short section that covers this requirement.",
title,
penalty=3,
),
)
}
}
}
///|
fn push_ci_findings(
findings : Array[Finding],
snapshot : ProjectSnapshot,
) -> Unit {
let ci = snapshot.ci_workflow
if !has_text(ci) {
findings.push(
fail(
"ci.exists",
ContinuousIntegration,
"GitHub Actions workflow exists",
"No CI workflow content was provided.",
"Add .github/workflows/ci.yml with MoonBit install, check, build, test and smoke run.",
".github/workflows/ci.yml",
penalty=14,
),
)
return
}
findings.push(
pass(
"ci.exists",
ContinuousIntegration,
"GitHub Actions workflow exists",
".github/workflows/ci.yml",
),
)
let commands = [
("ci.install", "Installs MoonBit", ["cli.moonbitlang", "moonbit"]),
("ci.check", "Runs moon check", ["moon check"]),
("ci.build", "Runs moon build", ["moon build"]),
("ci.test", "Runs moon test", ["moon test"]),
(
"ci.example",
"Runs an example",
["moon run examples", "moon run cmd/main"],
),
]
for item in commands {
let (id, title, needles) = item
if contains_any(ci, needles) {
findings.push(pass(id, ContinuousIntegration, title, needles[0]))
} else {
findings.push(
fail(
id,
ContinuousIntegration,
title,
"The workflow does not show this verification command.",
"Add the missing CI step so reviewers can reproduce validation.",
title,
penalty=7,
),
)
}
}
}
///|
fn push_test_and_example_findings(
findings : Array[Finding],
snapshot : ProjectSnapshot,
) -> Unit {
if snapshot.tests.length() == 0 {
findings.push(
fail(
"tests.exists",
Tests,
"Tests are present",
"No test fixtures were provided.",
"Add tests for valid input, invalid input, boundaries, transformations and exports.",
"tests",
penalty=14,
),
)
} else {
findings.push(
pass(
"tests.exists",
Tests,
"Tests are present",
snapshot.tests.length().to_string(),
),
)
}
let normal = count_contains(snapshot.tests, "valid") +
count_contains(snapshot.tests, "pass")
let invalid = count_contains(snapshot.tests, "invalid") +
count_contains(snapshot.tests, "fail")
let boundary = count_contains(snapshot.tests, "boundary") +
count_contains(snapshot.tests, "empty")
let export_count = count_contains(snapshot.tests, "markdown") +
count_contains(snapshot.tests, "json")
if normal > 0 && invalid > 0 && boundary > 0 && export_count > 0 {
findings.push(
pass(
"tests.coverage-shape",
Tests,
"Tests cover core paths",
"valid/invalid/boundary/export",
),
)
} else {
findings.push(
warn(
"tests.coverage-shape",
Tests,
"Tests cover core paths",
"The test descriptions do not show all required path types.",
"Cover normal input, invalid input, boundary input, data conversion and export output.",
"normal=\{normal}, invalid=\{invalid}, boundary=\{boundary}, export=\{export_count}",
penalty=6,
),
)
}
if snapshot.examples.length() > 0 {
findings.push(
pass(
"examples.exists",
Examples,
"Runnable example is present",
snapshot.examples.length().to_string(),
),
)
} else {
findings.push(
fail(
"examples.exists",
Examples,
"Runnable example is present",
"No runnable example was declared.",
"Add examples/basic with moon.pkg and main.mbt.",
"examples",
penalty=12,
),
)
}
if contains_any(snapshot.commands.join("\n"), ["moon check"]) &&
contains_any(snapshot.commands.join("\n"), ["moon build"]) &&
contains_any(snapshot.commands.join("\n"), ["moon test"]) {
findings.push(
pass(
"commands.verify",
Build,
"Verification commands include check/build/test",
"moon check/build/test",
),
)
} else {
findings.push(
fail(
"commands.verify",
Build,
"Verification commands include check/build/test",
"The verification command list is incomplete.",
"Document and run moon check, moon build and moon test.",
snapshot.commands.join(", "),
penalty=10,
),
)
}
}
///|
fn push_release_findings(
findings : Array[Finding],
snapshot : ProjectSnapshot,
) -> Unit {
if snapshot.repository_public {
findings.push(
pass(
"git.repository-public",
GitTrace,
"Repository is public",
"public=true",
),
)
} else {
findings.push(
fail(
"git.repository-public",
GitTrace,
"Repository is public",
"The project snapshot says the repository is not public.",
"Publish the GitHub repository before final submission.",
"public=false",
penalty=12,
),
)
}
if snapshot.commit_count >= 6 {
findings.push(
pass(
"git.commit-count",
GitTrace,
"Development history has at least 6 commits",
snapshot.commit_count.to_string(),
),
)
} else {
findings.push(
warn(
"git.commit-count",
GitTrace,
"Development history has at least 6 commits",
"The commit count is below the suggested threshold.",
"Keep meaningful commits for model, rules, tests, docs, CI and release prep.",
snapshot.commit_count.to_string(),
penalty=5,
),
)
}
if snapshot.mooncakes_published {
findings.push(
pass(
"mooncakes.published",
Mooncakes,
"Package is published to Mooncakes",
"published=true",
),
)
} else {
findings.push(
fail(
"mooncakes.published",
Mooncakes,
"Package is published to Mooncakes",
"The snapshot marks Mooncakes publication as incomplete.",
"Run moon login, moon publish --dry-run and moon publish after GitHub is public.",
"published=false",
penalty=14,
),
)
}
if has_text(snapshot.license_text) &&
contains_any(snapshot.license_text, ["MIT License", "Apache License"]) {
findings.push(
pass("license.file", License, "License file is present", "LICENSE"),
)
} else {
findings.push(
fail(
"license.file",
License,
"License file is present",
"The license file is missing or does not look like MIT/Apache text.",
"Add the full license text at LICENSE.",
"LICENSE",
penalty=10,
),
)
}
if has_text(snapshot.changelog) {
findings.push(
pass(
"maint.changelog",
Boundary,
"Changelog or release notes exist",
"CHANGELOG.md",
),
)
} else {
findings.push(
warn(
"maint.changelog",
Boundary,
"Changelog or release notes exist",
"A changelog helps reviewers see maintenance value.",
"Add CHANGELOG.md with the 0.1.0 release notes.",
"CHANGELOG.md",
penalty=3,
),
)
}
if has_text(snapshot.design_notes) && has_text(snapshot.issue_notes) {
findings.push(
pass(
"maint.trace-notes",
Boundary,
"Design and issue notes are retained",
"docs/",
),
)
} else {
findings.push(
warn(
"maint.trace-notes",
Boundary,
"Design and issue notes are retained",
"Design notes or issue records are missing.",
"Add docs/design.md and docs/issues.md.",
"docs",
penalty=4,
),
)
}
}
///|
fn count_severity(findings : Array[Finding], severity : Severity) -> Int {
let mut total = 0
for finding in findings {
if finding.severity == severity {
total += 1
}
}
total
}
///|
fn score_findings(findings : Array[Finding]) -> Int {
let mut penalty = 0
for finding in findings {
penalty += finding.penalty
}
(100 - penalty).clamp(min=0, max=100)
}
///|
fn verdict_for(score : Int, fail_count : Int) -> String {
if fail_count == 0 && score >= 90 {
"ready"
} else if fail_count <= 2 && score >= 75 {
"almost-ready"
} else {
"needs-work"
}
}
///|
fn run_audit(snapshot : ProjectSnapshot) -> AuditReport {
let manifest = parse_manifest(snapshot.moon_mod)
let findings = Array::new(capacity=48)
push_manifest_findings(findings, snapshot, manifest)
push_readme_findings(findings, snapshot, manifest)
push_ci_findings(findings, snapshot)
push_test_and_example_findings(findings, snapshot)
push_release_findings(findings, snapshot)
let pass_count = count_severity(findings, Pass)
let warn_count = count_severity(findings, Warn)
let fail_count = count_severity(findings, Fail)
let score = score_findings(findings)
{
project_name: snapshot.name,
score,
max_score: 100,
verdict: verdict_for(score, fail_count),
package_name: manifest.module_name,
repository: manifest.repository,
findings,
metrics: {
pass_count,
warn_count,
fail_count,
command_count: snapshot.commands.length(),
test_fixture_count: snapshot.tests.length(),
example_count: snapshot.examples.length(),
},
}
}