///|
pub fn audit_project(input : AuditInput) -> AuditReport {
let manifest = parse_manifest(input.moon_mod)
let checks : Array[AuditCheck] = []
audit_manifest(checks, manifest)
audit_readme(checks, input.readme, manifest)
audit_ci(checks, input.ci)
audit_license(checks, input.license_text)
audit_repository(checks, input)
audit_release(checks, input, manifest)
let package_name = manifest.package_name()
let version = manifest.version()
audit_report(package_name, version, checks, compute_score(checks))
}
///|
fn audit_manifest(checks : Array[AuditCheck], manifest : ManifestInfo) -> Unit {
require_field(checks, manifest, "name")
require_field(checks, manifest, "version")
require_field(checks, manifest, "readme")
require_field(checks, manifest, "repository")
require_field(checks, manifest, "license")
require_field(checks, manifest, "description")
require_field(checks, manifest, "preferred_target")
let mut malformed_index = 0
while malformed_index < manifest.malformed_lines.length() {
checks.push(
check(
Manifest,
Error,
"moon-mod-malformed-line",
"moon.mod line \{manifest.malformed_lines[malformed_index]} is not a key-value field",
"Use key = value syntax for MoonBit package metadata.",
),
)
malformed_index += 1
}
let mut duplicate_index = 0
while duplicate_index < manifest.duplicates.length() {
checks.push(
check(
Manifest,
Warning,
"moon-mod-duplicate-field",
"moon.mod has duplicate field '\{manifest.duplicates[duplicate_index]}'",
"Keep one source of truth for each package metadata field.",
),
)
duplicate_index += 1
}
match manifest.get("name") {
None => ()
Some(name) => {
if valid_package_name(name) {
checks.push(
check(
Manifest,
Pass,
"package-name-format",
"package name uses owner/name format",
"",
),
)
} else {
checks.push(
check(
Manifest,
Error,
"package-name-format",
"package name must use owner/package format",
"Example: owner/cakecheck.",
),
)
}
if has_placeholder(name) {
checks.push(
check(
Manifest,
Warning,
"package-name-placeholder",
"package name still contains a placeholder",
"Replace owner and package placeholders before publishing.",
),
)
}
}
}
match manifest.get("version") {
None => ()
Some(version) =>
if valid_semver(version) {
checks.push(
check(
Manifest,
Pass,
"version-semver",
"version uses SemVer 2.0.0 shape",
"",
),
)
} else {
checks.push(
check(
Manifest,
Error,
"version-semver",
"version is not valid SemVer 2.0.0",
"Use a release version such as 0.1.0 or 1.2.3-beta.1.",
),
)
}
}
match manifest.get("repository") {
None => ()
Some(repository) =>
if starts_with(repository, "https://github.com/") &&
ends_with(repository, ".git") {
checks.push(
check(
Manifest,
Pass,
"repository-url",
"repository points to a GitHub .git URL",
"",
),
)
} else {
checks.push(
check(
Manifest,
Warning,
"repository-url",
"repository should point to a GitHub .git URL",
"Use https://github.com//.git.",
),
)
}
}
match manifest.get("license") {
None => ()
Some(license) =>
if is_known_osi_license(license) {
checks.push(
check(
Manifest,
Pass,
"manifest-license",
"moon.mod declares an OSI-compatible license",
"",
),
)
} else {
checks.push(
check(
Manifest,
Error,
"manifest-license",
"moon.mod license is not in the recognized allowlist",
"Use MIT, Apache-2.0, BSD-3-Clause, MPL-2.0 or another OSI license.",
),
)
}
}
}
///|
fn require_field(
checks : Array[AuditCheck],
manifest : ManifestInfo,
key : String,
) -> Unit {
match manifest.get(key) {
None =>
checks.push(
check(
Manifest,
Error,
"moon-mod-missing-\{key}",
"moon.mod is missing '\{key}'",
"Add the field before publishing to Mooncakes.",
),
)
Some(value) =>
if trim_ascii(value).length() == 0 {
checks.push(
check(
Manifest,
Error,
"moon-mod-empty-\{key}",
"moon.mod field '\{key}' is empty",
"Fill a concrete value before release.",
),
)
} else {
checks.push(
check(
Manifest,
Pass,
"moon-mod-has-\{key}",
"moon.mod has '\{key}'",
"",
),
)
}
}
}
///|
fn audit_readme(
checks : Array[AuditCheck],
readme : String,
manifest : ManifestInfo,
) -> Unit {
let lower = ascii_lower(readme)
if trim_ascii(readme).length() == 0 {
checks.push(
check(
Readme,
Error,
"readme-empty",
"README is empty",
"Write installation, usage and validation docs.",
),
)
return
}
checks.push(check(Readme, Pass, "readme-present", "README is present", ""))
require_readme_any(
checks, readme, lower, "installation", "安装", "readme-install", "README describes installation",
)
require_readme_any(
checks, readme, lower, "usage", "使用", "readme-usage", "README describes usage",
)
require_readme_any(
checks, readme, lower, "example", "示例", "readme-example", "README includes an example",
)
require_readme_text(
checks, lower, "moon check", "readme-check-command", "README includes moon check",
)
require_readme_text(
checks, lower, "moon build", "readme-build-command", "README includes moon build",
)
require_readme_text(
checks, lower, "moon test", "readme-test-command", "README includes moon test",
)
require_readme_text(
checks, lower, "moon publish --dry-run", "readme-dry-run-command", "README includes publish dry-run",
)
require_readme_any(
checks, readme, lower, "license", "许可证", "readme-license", "README mentions license",
)
match manifest.get("name") {
None => ()
Some(name) =>
if contains(readme, name) {
checks.push(
check(
Readme,
Pass,
"readme-package-name",
"README mentions the Mooncakes package name",
"",
),
)
} else {
checks.push(
check(
Readme,
Warning,
"readme-package-name",
"README does not mention the package name from moon.mod",
"Keep README and moon.mod package names consistent.",
),
)
}
}
if has_placeholder(readme) {
checks.push(
check(
Readme,
Warning,
"readme-placeholder",
"README contains placeholder text",
"Replace placeholders before final submission.",
),
)
}
if contains(readme, "AI 编写") || contains(readme, "AI编写") {
checks.push(
check(
Readme,
Error,
"readme-ai-trace",
"README contains disallowed authorship wording",
"Remove process notes that are not part of the project documentation.",
),
)
}
}
///|
fn require_readme_text(
checks : Array[AuditCheck],
lower_readme : String,
needle : String,
code : String,
message : String,
) -> Unit {
if contains(lower_readme, needle) {
checks.push(check(Readme, Pass, code, message, ""))
} else {
checks.push(
check(
Readme,
Warning,
code,
message + " is missing",
"Add a concise README section for this requirement.",
),
)
}
}
///|
fn require_readme_any(
checks : Array[AuditCheck],
readme : String,
lower_readme : String,
ascii_needle : String,
unicode_needle : String,
code : String,
message : String,
) -> Unit {
if contains(lower_readme, ascii_needle) || contains(readme, unicode_needle) {
checks.push(check(Readme, Pass, code, message, ""))
} else {
checks.push(
check(
Readme,
Warning,
code,
message + " is missing",
"Add a concise README section for this requirement.",
),
)
}
}
///|
fn audit_ci(checks : Array[AuditCheck], ci : String) -> Unit {
let lower = ascii_lower(ci)
if trim_ascii(ci).length() == 0 {
checks.push(
check(
CI,
Error,
"ci-missing",
"GitHub Actions workflow is missing",
"Add .github/workflows/ci.yml.",
),
)
return
}
checks.push(check(CI, Pass, "ci-present", "CI workflow is present", ""))
require_ci_command(checks, lower, "moon check", "ci-moon-check")
require_ci_command(checks, lower, "moon build", "ci-moon-build")
require_ci_command(checks, lower, "moon test", "ci-moon-test")
require_ci_command(checks, lower, "moon run", "ci-moon-run")
if contains(lower, "cli.moonbitlang.com") || contains(lower, "moon version") {
checks.push(
check(
CI,
Pass,
"ci-installs-moonbit",
"CI installs or verifies MoonBit",
"",
),
)
} else {
checks.push(
check(
CI,
Error,
"ci-installs-moonbit",
"CI does not install or verify MoonBit",
"Install MoonBit before running moon commands.",
),
)
}
}
///|
fn require_ci_command(
checks : Array[AuditCheck],
lower_ci : String,
command : String,
code : String,
) -> Unit {
if contains(lower_ci, command) {
checks.push(check(CI, Pass, code, "CI runs \{command}", ""))
} else {
checks.push(
check(
CI,
Error,
code,
"CI does not run \{command}",
"Add this command to the workflow.",
),
)
}
}
///|
fn audit_license(checks : Array[AuditCheck], license_text : String) -> Unit {
let lower = ascii_lower(license_text)
if trim_ascii(license_text).length() == 0 {
checks.push(
check(
License,
Error,
"license-missing",
"LICENSE file is missing",
"Add an OSI-approved license file.",
),
)
} else if contains(lower, "mit license") || contains(lower, "apache license") {
checks.push(
check(
License,
Pass,
"license-recognized",
"LICENSE text is recognized",
"",
),
)
} else {
checks.push(
check(
License,
Warning,
"license-unrecognized",
"LICENSE text was not recognized by the lightweight scanner",
"Check that the project uses an OSI-approved license.",
),
)
}
}
///|
fn audit_repository(checks : Array[AuditCheck], input : AuditInput) -> Unit {
if input.repository_public {
checks.push(
check(Repository, Pass, "repo-public", "repository is marked public", ""),
)
} else {
checks.push(
check(
Repository,
Error,
"repo-public",
"repository is not marked public",
"Make the GitHub repository public before submission.",
),
)
}
if input.commit_count >= 6 {
checks.push(
check(
Repository,
Pass,
"commit-count",
"commit history has at least 6 commits",
"",
),
)
} else {
checks.push(
check(
Repository,
Warning,
"commit-count",
"commit history is too short",
"Keep meaningful commits so development can be traced.",
),
)
}
if trim_ascii(input.changelog).length() > 0 {
checks.push(
check(Repository, Pass, "changelog-present", "CHANGELOG is present", ""),
)
} else {
checks.push(
check(
Repository,
Info,
"changelog-present",
"CHANGELOG is not provided",
"A small changelog helps reviewers understand release history.",
),
)
}
}
///|
fn audit_release(
checks : Array[AuditCheck],
input : AuditInput,
manifest : ManifestInfo,
) -> Unit {
if input.package_published {
checks.push(
check(
Release,
Pass,
"mooncakes-published",
"package is marked as published",
"",
),
)
} else {
checks.push(
check(
Release,
Warning,
"mooncakes-published",
"package is not marked as published",
"Run moon login, moon publish --dry-run and moon publish with the owner account.",
),
)
}
match manifest.get("name") {
None => ()
Some(name) =>
if has_placeholder(name) {
checks.push(
check(
Release,
Warning,
"release-placeholder-name",
"release package name still contains a placeholder",
"Replace placeholder owner before moon publish.",
),
)
} else {
checks.push(
check(
Release,
Pass,
"release-package-name",
"release package name is concrete",
"",
),
)
}
}
}
///|
fn is_known_osi_license(value : String) -> Bool {
value == "MIT" ||
value == "Apache-2.0" ||
value == "BSD-2-Clause" ||
value == "BSD-3-Clause" ||
value == "MPL-2.0"
}
///|
fn compute_score(checks : Array[AuditCheck]) -> Score {
let mut pass_count = 0
let mut info_count = 0
let mut warning_count = 0
let mut error_count = 0
let mut index = 0
while index < checks.length() {
match checks[index].severity {
Pass => pass_count += 1
Info => info_count += 1
Warning => warning_count += 1
Error => error_count += 1
}
index += 1
}
let total = checks.length()
let readiness = if total == 0 {
0
} else {
let penalty = error_count * 20 + warning_count * 6 + info_count * 2
let raw = 100 - penalty
if raw < 0 {
0
} else {
raw
}
}
score(pass_count, info_count, warning_count, error_count, total, readiness)
}