///|
/// A compact dashboard model for release pages and CI summaries.
pub(all) struct QualityDashboard {
project : String
version : String
score : Int
label : String
source_files : Int
test_files : Int
package_count : Int
mutation_candidates : Int
mutation_score : Int
coverage_percentage : Int
warning_count : Int
recommendation_count : Int
risks : Array[String]
} derive(Eq)
///|
/// Create a dashboard without running any additional commands.
pub fn dashboard(report : QualityReport) -> QualityDashboard {
let risks : Array[String] = []
if report.test_files.length() == 0 {
risks.push("no project tests")
} else if report.test_files.length() < 2 {
risks.push("fewer than two test files")
}
for pkg in report.packages {
if pkg.source_count > 0 && pkg.test_count == 0 {
risks.push("untested package: " + pkg.path)
}
}
if has_warning(report, "missing README.md") {
risks.push("README is missing")
}
if has_warning(report, "missing LICENSE") {
risks.push("LICENSE is missing")
}
if has_warning(report, "missing CI workflow") {
risks.push("CI workflow is missing")
}
if report.mutation_candidates.length() > 0 && report.mutation_score == 0 {
risks.push("mutation score has not been measured")
}
if report.coverage_total_percentage == 0 {
risks.push("coverage has not been measured")
}
let items = recommendations(report)
let score = health_score(report)
{
project: report.name,
version: report.version,
score,
label: health_label(score),
source_files: report.source_files.length(),
test_files: report.test_files.length(),
package_count: report.packages.length(),
mutation_candidates: report.mutation_candidates.length(),
mutation_score: report.mutation_score,
coverage_percentage: report.coverage_total_percentage,
warning_count: report.warnings.length(),
recommendation_count: items.length(),
risks,
}
}
///|
/// Render a dashboard for release notes or a job summary.
pub fn render_dashboard(value : QualityDashboard) -> String {
let out = StringBuilder()
out.write_string("MoonSeal Quality Dashboard\n")
out.write_string("project: " + value.project + "\n")
out.write_string("version: " + value.version + "\n")
out.write_string("health-score: " + value.score.to_string() + "/100\n")
out.write_string("health-label: " + value.label + "\n")
out.write_string("source-files: " + value.source_files.to_string() + "\n")
out.write_string("test-files: " + value.test_files.to_string() + "\n")
out.write_string("packages: " + value.package_count.to_string() + "\n")
out.write_string(
"mutation: candidates=" +
value.mutation_candidates.to_string() +
" score=" +
value.mutation_score.to_string() +
"%\n",
)
out.write_string("coverage: " + value.coverage_percentage.to_string() + "%\n")
out.write_string("warnings: " + value.warning_count.to_string() + "\n")
out.write_string(
"recommendations: " + value.recommendation_count.to_string() + "\n",
)
if value.risks.length() == 0 {
out.write_string("risks: none\n")
} else {
for risk in value.risks {
out.write_string("risk: " + risk + "\n")
}
}
out.to_string()
}
///|
/// Emit SARIF 2.1.0 so GitHub code scanning or another CI viewer can display
/// MoonSeal warnings and gate failures as structured findings.
pub fn render_sarif(report : QualityReport, gate : GateResult) -> String {
let results = Array::new()
for warning in report.warnings {
results.push(sarif_result("moonseal-warning", "warning", warning))
}
for failure in gate.failures {
results.push(sarif_result("moonseal-gate", "error", failure))
}
let rules = Array::new()
rules.push(sarif_rule("moonseal-warning", "Repository quality warning"))
rules.push(sarif_rule("moonseal-gate", "Release quality gate failure"))
let rule_map = Json::object({
"name": Json::string("MoonSeal"),
"informationUri": Json::string("https://github.com/LL728/moonseal"),
"rules": Json::array(rules),
})
let run = Json::object({
"tool": Json::object({ "driver": rule_map }),
"results": Json::array(results),
"properties": Json::object({
"project": Json::string(report.name),
"version": Json::string(report.version),
"passed": Json::boolean(gate.passed),
}),
})
Json::object({
"$schema": Json::string("https://json.schemastore.org/sarif-2.1.0.json"),
"version": Json::string("2.1.0"),
"runs": Json::array([run]),
}).stringify(indent=2)
}
///|
fn sarif_rule(id : String, name : String) -> Json {
Json::object({
"id": Json::string(id),
"name": Json::string(name),
"shortDescription": Json::object({ "text": Json::string(name) }),
})
}
///|
fn sarif_result(id : String, level : String, message : String) -> Json {
Json::object({
"ruleId": Json::string(id),
"level": Json::string(level),
"message": Json::object({ "text": Json::string(message) }),
})
}
///|
/// Serialize a dashboard for small status badges and web clients.
pub impl ToJson for QualityDashboard with fn to_json(self : QualityDashboard) -> Json {
let risks = Array::new()
for item in self.risks {
risks.push(Json::string(item))
}
Json::object({
"project": Json::string(self.project),
"version": Json::string(self.version),
"score": Json::number(self.score.to_double()),
"label": Json::string(self.label),
"source_files": Json::number(self.source_files.to_double()),
"test_files": Json::number(self.test_files.to_double()),
"package_count": Json::number(self.package_count.to_double()),
"mutation_candidates": Json::number(self.mutation_candidates.to_double()),
"mutation_score": Json::number(self.mutation_score.to_double()),
"coverage_percentage": Json::number(self.coverage_percentage.to_double()),
"warning_count": Json::number(self.warning_count.to_double()),
"recommendation_count": Json::number(self.recommendation_count.to_double()),
"risks": Json::array(risks),
})
}