///|
fn push_error(
diagnostics : Array[Diagnostic],
code : String,
message : String,
detail : String,
) -> Unit {
diagnostics.push(Diagnostic::new(Error, code, message, detail))
}
///|
fn push_warn(
diagnostics : Array[Diagnostic],
code : String,
message : String,
detail : String,
) -> Unit {
diagnostics.push(Diagnostic::new(Warn, code, message, detail))
}
///|
fn push_info(
diagnostics : Array[Diagnostic],
code : String,
message : String,
detail : String,
) -> Unit {
diagnostics.push(Diagnostic::new(Info, code, message, detail))
}
///|
fn has_non_empty_field(project : ProjectSnapshot, key : String) -> Bool {
match project.moon_mod_field(key) {
Some(value) => value.trim().length() > 0
None => false
}
}
///|
fn check_required_files(
project : ProjectSnapshot,
diagnostics : Array[Diagnostic],
) -> Unit {
if !project.has_file("moon.mod") {
push_error(
diagnostics, "missing_moon_mod", "moon.mod is required", "Add moon.mod at the project root so moon can identify the module.",
)
}
if !project.has_file("moon.pkg") {
push_warn(
diagnostics, "missing_root_moon_pkg", "root moon.pkg is recommended for a reusable module",
"Add root moon.pkg so the module has an explicit public package boundary.",
)
}
if !project.has_file("LICENSE") {
push_error(
diagnostics, "missing_license", "LICENSE is required", "Add an MIT LICENSE file before uploading the repository.",
)
}
if !project.has_readme_file() {
push_warn(
diagnostics, "missing_readme", "README.md is recommended", "Add README.md so GitHub and mooncakes users can understand the package.",
)
} else if !project.has_file("README.md") && project.has_file("README.mbt.md") {
push_info(
diagnostics, "missing_github_readme", "README.mbt.md is present but README.md is missing",
"Add README.md or a repository-supported link so GitHub shows the project overview.",
)
}
}
///|
fn check_recommended_files(
project : ProjectSnapshot,
diagnostics : Array[Diagnostic],
) -> Unit {
if !project.has_file(".gitignore") {
push_info(
diagnostics, "missing_gitignore", ".gitignore is useful for MoonBit build artifacts",
"Ignore _build/, target/, .mooncakes/, and local editor files.",
)
} else {
if !project.gitignore_mentions("_build") {
push_info(
diagnostics, "gitignore_missing_build", ".gitignore should ignore _build/",
"Add _build/ to avoid committing generated build artifacts.",
)
}
if !project.gitignore_mentions(".mooncakes") {
push_info(
diagnostics, "gitignore_missing_mooncakes", ".gitignore should usually ignore .mooncakes/",
"Add .mooncakes/ unless the repository intentionally vendors fetched dependencies.",
)
}
}
if !project.has_workflow() {
push_info(
diagnostics, "missing_ci", "CI workflow is recommended", "Add a GitHub Actions workflow that runs moon check and moon test.",
)
} else {
if !project.workflow_mentions("moon check") {
push_warn(
diagnostics, "ci_missing_moon_check", "CI should run moon check", "Add moon check to the GitHub Actions workflow.",
)
}
if !project.workflow_mentions("moon test") {
push_warn(
diagnostics, "ci_missing_moon_test", "CI should run moon test", "Add moon test to the GitHub Actions workflow.",
)
}
}
if !project.has_dir("examples") {
push_info(
diagnostics, "missing_examples", "examples directory is recommended", "Add small runnable examples for users who want to try the package quickly.",
)
} else if !project.has_example_content() {
push_info(
diagnostics, "examples_missing_content", "examples should contain runnable code or documentation",
"Add an example package, .mbt file, or README under examples/.",
)
}
if !project.has_any_test_file() {
push_warn(
diagnostics, "missing_tests", "MoonBit tests are recommended", "Add *_test.mbt or *_wbtest.mbt files that cover the core behavior.",
)
}
}
///|
fn check_moon_mod_fields(
project : ProjectSnapshot,
diagnostics : Array[Diagnostic],
) -> Unit {
if project.has_file("moon.mod") {
for key in ["name", "version", "readme", "license"] {
if !has_non_empty_field(project, key) {
let rule_code = if key == "readme" {
"missing_readme_metadata"
} else {
"missing_" + key
}
push_warn(
diagnostics,
rule_code,
"moon.mod should define " + key,
"Fill the " + key + " field before publishing.",
)
}
}
for key in ["repository", "description", "keywords"] {
if !has_non_empty_field(project, key) {
push_info(
diagnostics,
"missing_" + key,
"moon.mod can be more discoverable with " + key,
"Fill the " + key + " field to improve package metadata.",
)
}
}
check_readme_target(project, diagnostics)
check_repository_url(project, diagnostics)
check_metadata_quality(project, diagnostics)
}
}
///|
fn check_readme_target(
project : ProjectSnapshot,
diagnostics : Array[Diagnostic],
) -> Unit {
match project.moon_mod_field("readme") {
Some(path) =>
if !project.has_exact_file(path) {
push_warn(
diagnostics,
"readme_target_missing",
"moon.mod readme should point to an existing file",
"moon.mod readme is " + path + ", but that file was not found.",
)
}
None => ()
}
}
///|
fn check_repository_url(
project : ProjectSnapshot,
diagnostics : Array[Diagnostic],
) -> Unit {
match project.moon_mod_field("repository") {
Some(url) =>
if url.contains("<") || url.contains("example.com") {
push_warn(
diagnostics, "repository_placeholder", "moon.mod repository should not be a placeholder",
"Replace the repository field with the final GitHub or Gitlink URL.",
)
} else if !(url.has_prefix("https://github.com/") ||
url.has_prefix("https://gitlink.org.cn/")) {
push_info(
diagnostics, "repository_unusual", "moon.mod repository should use a recognizable hosting URL",
"Use a GitHub or Gitlink repository URL when publishing community projects.",
)
}
None => ()
}
}
///|
fn check_metadata_quality(
project : ProjectSnapshot,
diagnostics : Array[Diagnostic],
) -> Unit {
if project.field_mentions("description", "TODO") ||
project.field_mentions("description", "todo") ||
project.field_mentions("description", "example") {
push_info(
diagnostics, "description_placeholder", "moon.mod description looks like a placeholder",
"Replace it with a concrete one-sentence project summary.",
)
}
if project.field_mentions("keywords", "TODO") ||
project.field_mentions("keywords", "todo") ||
project.field_mentions("keywords", "example") {
push_info(
diagnostics, "keywords_placeholder", "moon.mod keywords look like placeholders",
"Use searchable ecosystem terms such as moonbit, release, quality, or tooling.",
)
}
}
///|
fn check_license_consistency(
project : ProjectSnapshot,
diagnostics : Array[Diagnostic],
) -> Unit {
match project.moon_mod_field("license") {
Some(value) =>
if project.has_file("LICENSE") && !project.license_mentions(value) {
push_warn(
diagnostics,
"license_mismatch",
"moon.mod license should match LICENSE",
"moon.mod declares " + value + ", but LICENSE does not mention it.",
)
}
None => ()
}
}
///|
fn check_readme_sections(
project : ProjectSnapshot,
diagnostics : Array[Diagnostic],
) -> Unit {
if project.has_file("README.md") {
if !project.readme_mentions("Usage") && !project.readme_mentions("usage") {
push_info(
diagnostics, "readme_missing_usage", "README should include usage", "Show the exact command a user can run first.",
)
}
if !project.readme_mentions("Development") &&
!project.readme_mentions("development") {
push_info(
diagnostics, "readme_missing_development", "README should include development commands",
"List moon check and moon test so contributors know how to validate changes.",
)
}
if !project.readme_mentions("License") &&
!project.readme_mentions("license") {
push_info(
diagnostics, "readme_missing_license", "README should include license information",
"Mention the repository license in README.md.",
)
}
}
}
///|
fn apply_config(
diagnostics : Array[Diagnostic],
config : DoctorConfig,
) -> Array[Diagnostic] {
let configured : Array[Diagnostic] = []
for diagnostic in diagnostics {
if !config.is_rule_disabled(diagnostic.code) {
let severity = config.severity_for(diagnostic.code, diagnostic.severity)
configured.push({
severity,
code: diagnostic.code,
message: diagnostic.message,
detail: diagnostic.detail,
location: diagnostic.location,
remediation: diagnostic.remediation,
})
}
}
configured
}
///|
/// Run health checks with a project-local release policy.
pub fn diagnose_with_config(
project : ProjectSnapshot,
config : DoctorConfig,
) -> DoctorReport {
let diagnostics : Array[Diagnostic] = []
check_required_files(project, diagnostics)
check_recommended_files(project, diagnostics)
check_moon_mod_fields(project, diagnostics)
check_license_consistency(project, diagnostics)
check_readme_sections(project, diagnostics)
DoctorReport::with_context(
project.root,
apply_config(diagnostics, config),
config.profile.label(),
config.release_checklist(),
)
}
///|
/// Run all default-profile health checks. This preserves the original API.
pub fn diagnose(project : ProjectSnapshot) -> DoctorReport {
diagnose_with_config(project, DoctorConfig::default())
}