///|
/// One weighted quality area in a Moon Doctor scorecard.
pub(all) struct QualityArea {
category : RuleCategory
possible : Int
achieved : Int
errors : Int
warnings : Int
infos : Int
notes : Array[String]
} derive(Debug, Eq)
///|
pub fn QualityArea::percent(self : QualityArea) -> Int {
if self.possible == 0 {
100
} else {
self.achieved * 100 / self.possible
}
}
///|
pub fn QualityArea::label(self : QualityArea) -> String {
self.category.label()
}
///|
/// Explainable quality score independent of strict pass/fail mode.
pub(all) struct QualityScorecard {
profile : String
areas : Array[QualityArea]
total_possible : Int
total_achieved : Int
percentage : Int
grade : String
} derive(Debug, Eq)
///|
fn empty_area(category : RuleCategory) -> QualityArea {
{
category,
possible: 100,
achieved: 100,
errors: 0,
warnings: 0,
infos: 0,
notes: [],
}
}
///|
fn category_of_diagnostic(diagnostic : Diagnostic) -> RuleCategory {
match find_rule_definition(diagnostic.code) {
Some(rule) => rule.category
None => Release
}
}
///|
fn penalty(severity : Severity) -> Int {
match severity {
Error => 30
Warn => 12
Info => 3
}
}
///|
fn update_area(area : QualityArea, diagnostic : Diagnostic) -> QualityArea {
let achieved = if area.achieved > penalty(diagnostic.severity) {
area.achieved - penalty(diagnostic.severity)
} else {
0
}
let notes = area.notes
if notes.length() < 5 {
notes.push(diagnostic.code + ": " + diagnostic.message)
}
match diagnostic.severity {
Error =>
{
category: area.category,
possible: area.possible,
achieved,
errors: area.errors + 1,
warnings: area.warnings,
infos: area.infos,
notes,
}
Warn =>
{
category: area.category,
possible: area.possible,
achieved,
errors: area.errors,
warnings: area.warnings + 1,
infos: area.infos,
notes,
}
Info =>
{
category: area.category,
possible: area.possible,
achieved,
errors: area.errors,
warnings: area.warnings,
infos: area.infos + 1,
notes,
}
}
}
///|
fn area_index(areas : Array[QualityArea], category : RuleCategory) -> Int {
let mut index = 0
for area in areas {
if area.category == category {
return index
}
index += 1
}
0
}
///|
fn score_grade(percentage : Int, errors : Int) -> String {
if errors > 0 {
"D"
} else if percentage >= 90 {
"A"
} else if percentage >= 75 {
"B"
} else if percentage >= 60 {
"C"
} else {
"D"
}
}
///|
pub fn build_quality_scorecard(report : DoctorReport) -> QualityScorecard {
let areas = [
empty_area(Manifest),
empty_area(Package),
empty_area(Documentation),
empty_area(Ci),
empty_area(Release),
]
for diagnostic in report.diagnostics {
let index = area_index(areas, category_of_diagnostic(diagnostic))
areas[index] = update_area(areas[index], diagnostic)
}
let mut possible = 0
let mut achieved = 0
let mut errors = 0
for area in areas {
possible += area.possible
achieved += area.achieved
errors += area.errors
}
let percentage = if possible == 0 { 100 } else { achieved * 100 / possible }
{
profile: report.profile,
areas,
total_possible: possible,
total_achieved: achieved,
percentage,
grade: score_grade(percentage, errors),
}
}
///|
pub fn quality_scorecard_to_json(scorecard : QualityScorecard) -> Json {
let areas = scorecard.areas.map(area => {
Json::object({
"category": Json::string(area.label()),
"possible": Json::number(area.possible.to_double()),
"achieved": Json::number(area.achieved.to_double()),
"percent": Json::number(area.percent().to_double()),
"errors": Json::number(area.errors.to_double()),
"warnings": Json::number(area.warnings.to_double()),
"infos": Json::number(area.infos.to_double()),
"notes": Json::array(area.notes.map(Json::string)),
})
})
Json::object({
"profile": Json::string(scorecard.profile),
"grade": Json::string(scorecard.grade),
"percentage": Json::number(scorecard.percentage.to_double()),
"areas": Json::array(areas),
})
}
///|
pub fn render_quality_scorecard(scorecard : QualityScorecard) -> String {
let builder = StringBuilder()
builder.write_string("Moon Doctor quality scorecard\n")
builder.write_string("Profile: " + scorecard.profile + "\n")
builder.write_string(
"Grade: " + scorecard.grade + " (\{scorecard.percentage}%)\n\n",
)
for area in scorecard.areas {
builder.write_string(
"- " +
area.label() +
": \{area.percent()}% (E:\{area.errors} W:\{area.warnings} I:\{area.infos})\n",
)
for note in area.notes {
builder.write_string(" - " + note + "\n")
}
}
builder.to_string()
}