// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 clbbbb
///|
pub struct FixSuggestion {
target : String
severity : String
action : String
} derive(Debug, Eq)
///|
pub fn fix_suggestion(
target : String,
severity : String,
action : String,
) -> FixSuggestion {
{ target, severity, action }
}
///|
pub fn suggestions_for_audit(audit : ProjectAudit) -> Array[FixSuggestion] {
let rows : Array[FixSuggestion] = []
if audit.package_license == "" {
rows.push(
fix_suggestion("moon.mod", "high", "add an OSI-approved license field"),
)
}
if !audit.readme_ok {
rows.push(
fix_suggestion("README", "medium", "mention the declared project license"),
)
}
if audit.missing_headers > 0 {
rows.push(
fix_suggestion(
"source files", "medium", "add SPDX-License-Identifier headers to files missing license metadata",
),
)
}
if audit.invalid_headers > 0 {
rows.push(
fix_suggestion(
"source files", "high", "fix invalid SPDX expressions before release",
),
)
}
for error in audit.policy_errors {
rows.push(
fix_suggestion(
"policy",
severity_for_error(error),
action_for_error(error),
),
)
}
rows
}
///|
pub fn severity_for_error(error : String) -> String {
let lower = lower_ascii(error)
if lower.find("unknown") is Some(_) || lower.find("denied") is Some(_) {
"high"
} else if lower.find("copyleft") is Some(_) {
"high"
} else if lower.find("not allowed") is Some(_) {
"medium"
} else {
"medium"
}
}
///|
pub fn action_for_error(error : String) -> String {
let lower = lower_ascii(error)
if lower.find("unknown") is Some(_) {
"replace unknown license IDs with canonical SPDX IDs"
} else if lower.find("denied") is Some(_) {
"remove denied license or update policy with explicit approval"
} else if lower.find("copyleft") is Some(_) {
"review copyleft obligations and switch policy mode only if acceptable"
} else if lower.find("not allowed") is Some(_) {
"choose an allowed license alternative or update the allow list"
} else {
"review the reported license issue"
}
}
///|
pub fn remediation_report(audit : ProjectAudit) -> String {
let rows : Array[String] = []
for item in suggestions_for_audit(audit) {
rows.push(item.severity + ": " + item.target + " - " + item.action)
}
if rows.is_empty() {
"no remediation needed"
} else {
join_lines(rows)
}
}
///|
pub fn remediation_markdown(audit : ProjectAudit) -> String {
let rows : Array[String] = ["# Remediation plan", ""]
let suggestions = suggestions_for_audit(audit)
if suggestions.is_empty() {
rows.push("- no remediation needed")
} else {
for item in suggestions {
rows.push("- `" + item.severity + "` " + item.target + ": " + item.action)
}
}
join_lines(rows)
}
///|
pub fn audit_bundle_report(audit : ProjectAudit) -> String {
join_lines([project_audit_report(audit), "", remediation_report(audit)])
}