///|
/// One score item in a project rubric.
pub(all) struct ScoreItem {
id : String
title : String
description : String
max_score : Int
earned_score : Int
passed : Bool
evidence : String
required : Bool
} derive(Eq, Debug)
///|
/// Create a score item.
pub fn ScoreItem::new(
id : String,
title : String,
description? : String = "",
max_score? : Int = 1,
earned_score? : Int = 0,
passed? : Bool = false,
evidence? : String = "",
required? : Bool = true,
) -> ScoreItem {
let max_score = clamp_non_negative(max_score)
let earned_score = clamp_score(earned_score, max_score)
{
id,
title,
description,
max_score,
earned_score,
passed,
evidence,
required,
}
}
///|
fn clamp_score(value : Int, max_score : Int) -> Int {
if value < 0 {
0
} else if value > max_score {
max_score
} else {
value
}
}
///|
/// Mark an item as fully passed.
pub fn ScoreItem::pass(self : ScoreItem, evidence : String) -> ScoreItem {
{ ..self, earned_score: self.max_score, passed: true, evidence }
}
///|
/// Mark an item as partially earned.
pub fn ScoreItem::partial(
self : ScoreItem,
score : Int,
evidence : String,
) -> ScoreItem {
let earned = clamp_score(score, self.max_score)
{ ..self, earned_score: earned, passed: earned == self.max_score, evidence }
}
///|
/// Mark an item as failed.
pub fn ScoreItem::fail(self : ScoreItem, evidence : String) -> ScoreItem {
{ ..self, earned_score: 0, passed: false, evidence }
}
///|
/// Completion ratio for the item.
pub fn ScoreItem::ratio(self : ScoreItem) -> Double {
if self.max_score == 0 {
1.0
} else {
self.earned_score.to_double() / self.max_score.to_double()
}
}
///|
/// Render score item as Markdown row.
pub fn ScoreItem::to_markdown_row(self : ScoreItem) -> String {
"| \{escape_markdown(self.id)} | \{escape_markdown(self.title)} | \{self.earned_score}/\{self.max_score} | \{self.passed} | \{self.required} | \{escape_markdown(self.evidence)} |\n"
}
///|
/// Render score item as JSON.
pub fn ScoreItem::to_json(self : ScoreItem) -> String {
"{" +
"\"id\":\"\{escape_json(self.id)}\"," +
"\"title\":\"\{escape_json(self.title)}\"," +
"\"description\":\"\{escape_json(self.description)}\"," +
"\"max_score\":\{self.max_score}," +
"\"earned_score\":\{self.earned_score}," +
"\"passed\":\{self.passed}," +
"\"evidence\":\"\{escape_json(self.evidence)}\"," +
"\"required\":\{self.required}" +
"}"
}
///|
/// A score section in a project rubric.
pub(all) struct ScoreSection {
id : String
title : String
items : Array[ScoreItem]
} derive(Eq, Debug)
///|
/// Create an empty score section.
pub fn ScoreSection::new(id : String, title : String) -> ScoreSection {
{ id, title, items: [] }
}
///|
/// Append an item.
pub fn ScoreSection::add(self : ScoreSection, item : ScoreItem) -> ScoreSection {
let items = self.items.copy()
items.push(item)
{ ..self, items, }
}
///|
/// Number of score items.
pub fn ScoreSection::count(self : ScoreSection) -> Int {
self.items.length()
}
///|
/// Maximum score in this section.
pub fn ScoreSection::max_score(self : ScoreSection) -> Int {
for item in self.items; acc = 0 {
continue acc + item.max_score
} nobreak {
acc
}
}
///|
/// Earned score in this section.
pub fn ScoreSection::earned_score(self : ScoreSection) -> Int {
for item in self.items; acc = 0 {
continue acc + item.earned_score
} nobreak {
acc
}
}
///|
/// Missing required items in this section.
pub fn ScoreSection::missing_required_count(self : ScoreSection) -> Int {
for item in self.items; acc = 0 {
if item.required && !item.passed {
continue acc + 1
} else {
continue acc
}
} nobreak {
acc
}
}
///|
/// Ratio for this section.
pub fn ScoreSection::ratio(self : ScoreSection) -> Double {
let max_score = self.max_score()
if max_score == 0 {
1.0
} else {
self.earned_score().to_double() / max_score.to_double()
}
}
///|
/// Whether all required items in this section pass.
pub fn ScoreSection::required_passed(self : ScoreSection) -> Bool {
self.missing_required_count() == 0
}
///|
/// Render section as Markdown.
pub fn ScoreSection::to_markdown(self : ScoreSection) -> String {
let mut body = "### \{escape_markdown(self.title)}\n\n"
body = body + "- Score: \{self.earned_score()}/\{self.max_score()}\n"
body = body + "- Missing required: \{self.missing_required_count()}\n\n"
body = body + "| id | item | score | passed | required | evidence |\n"
body = body + "| --- | --- | ---: | --- | --- | --- |\n"
for item in self.items {
body = body + item.to_markdown_row()
}
body
}
///|
/// Render section as JSON.
pub fn ScoreSection::to_json(self : ScoreSection) -> String {
let mut body = "{"
body = body + "\"id\":\"\{escape_json(self.id)}\","
body = body + "\"title\":\"\{escape_json(self.title)}\","
body = body + "\"max_score\":\{self.max_score()},"
body = body + "\"earned_score\":\{self.earned_score()},"
body = body + "\"missing_required\":\{self.missing_required_count()},"
body = body + "\"items\":["
for i in 0.. 0 {
body = body + ","
}
body = body + self.items[i].to_json()
}
body + "]}"
}
///|
/// Full project scorecard.
pub(all) struct ProjectScorecard {
title : String
target_loc : Int
actual_loc : Int
sections : Array[ScoreSection]
} derive(Eq, Debug)
///|
/// Create an empty project scorecard.
pub fn ProjectScorecard::new(
title? : String = "MoonBench Scorecard",
target_loc? : Int = 4800,
actual_loc? : Int = 0,
) -> ProjectScorecard {
{ title, target_loc, actual_loc, sections: [] }
}
///|
/// Append a score section.
pub fn ProjectScorecard::add_section(
self : ProjectScorecard,
section : ScoreSection,
) -> ProjectScorecard {
let sections = self.sections.copy()
sections.push(section)
{ ..self, sections, }
}
///|
/// Number of sections.
pub fn ProjectScorecard::section_count(self : ProjectScorecard) -> Int {
self.sections.length()
}
///|
/// Number of score items.
pub fn ProjectScorecard::item_count(self : ProjectScorecard) -> Int {
for section in self.sections; acc = 0 {
continue acc + section.count()
} nobreak {
acc
}
}
///|
/// Total maximum score.
pub fn ProjectScorecard::max_score(self : ProjectScorecard) -> Int {
for section in self.sections; acc = 0 {
continue acc + section.max_score()
} nobreak {
acc
}
}
///|
/// Total earned score.
pub fn ProjectScorecard::earned_score(self : ProjectScorecard) -> Int {
for section in self.sections; acc = 0 {
continue acc + section.earned_score()
} nobreak {
acc
}
}
///|
/// Missing required item count.
pub fn ProjectScorecard::missing_required_count(self : ProjectScorecard) -> Int {
for section in self.sections; acc = 0 {
continue acc + section.missing_required_count()
} nobreak {
acc
}
}
///|
/// Score ratio.
pub fn ProjectScorecard::ratio(self : ProjectScorecard) -> Double {
let max_score = self.max_score()
if max_score == 0 {
1.0
} else {
self.earned_score().to_double() / max_score.to_double()
}
}
///|
/// Whether LOC target is reached.
pub fn ProjectScorecard::loc_ready(self : ProjectScorecard) -> Bool {
self.actual_loc >= self.target_loc
}
///|
/// Whether all required items and LOC target pass.
pub fn ProjectScorecard::ready(self : ProjectScorecard) -> Bool {
self.loc_ready() && self.missing_required_count() == 0
}
///|
/// Grade from the current score ratio.
pub fn ProjectScorecard::grade(self : ProjectScorecard) -> String {
let ratio = self.ratio()
if !self.ready() {
"incomplete"
} else if ratio >= 0.95 {
"excellent"
} else if ratio >= 0.85 {
"strong"
} else if ratio >= 0.70 {
"accepted"
} else {
"weak"
}
}
///|
/// Render scorecard as Markdown.
pub fn ProjectScorecard::to_markdown(self : ProjectScorecard) -> String {
let mut body = "## \{escape_markdown(self.title)}\n\n"
body = body + "- Target MoonBit LOC: \{self.target_loc}\n"
body = body + "- Actual MoonBit LOC: \{self.actual_loc}\n"
body = body + "- LOC ready: \{self.loc_ready()}\n"
body = body + "- Score: \{self.earned_score()}/\{self.max_score()}\n"
body = body + "- Missing required: \{self.missing_required_count()}\n"
body = body + "- Ready: \{self.ready()}\n"
body = body + "- Grade: \{self.grade()}\n\n"
for section in self.sections {
body = body + section.to_markdown() + "\n"
}
body
}
///|
/// Render scorecard as JSON.
pub fn ProjectScorecard::to_json(self : ProjectScorecard) -> String {
let mut body = "{"
body = body + "\"title\":\"\{escape_json(self.title)}\","
body = body + "\"target_loc\":\{self.target_loc},"
body = body + "\"actual_loc\":\{self.actual_loc},"
body = body + "\"loc_ready\":\{self.loc_ready()},"
body = body + "\"max_score\":\{self.max_score()},"
body = body + "\"earned_score\":\{self.earned_score()},"
body = body + "\"missing_required\":\{self.missing_required_count()},"
body = body + "\"ready\":\{self.ready()},"
body = body + "\"grade\":\"\{escape_json(self.grade())}\","
body = body + "\"sections\":["
for i in 0.. 0 {
body = body + ","
}
body = body + self.sections[i].to_json()
}
body + "]}"
}
///|
/// Build the standard excellent-work scorecard.
pub fn ProjectScorecard::moonbench_standard(
actual_loc? : Int = 0,
) -> ProjectScorecard {
ProjectScorecard::new(actual_loc~)
.add_section(
ScoreSection::new("official", "官方验收底线")
.add(
ScoreItem::new(
"moonbit-primary",
"MoonBit 为主要实现语言",
max_score=10,
).pass("核心库、测试、示例均为 MoonBit"),
)
.add(
ScoreItem::new("public-repo", "公开仓库", max_score=10).pass(
"https://github.com/Han-Wentao/moonbench",
),
)
.add(
ScoreItem::new("readme", "清晰 README", max_score=10).pass(
"中文 README.md 和 README.mbt.md",
),
)
.add(
ScoreItem::new("examples", "可运行示例", max_score=10).pass(
"cmd/main 与 examples/basic",
),
)
.add(
ScoreItem::new("ci", "CI", max_score=10).pass(".github/workflows/ci.yml"),
)
.add(ScoreItem::new("tests", "测试", max_score=10).pass("moon test"))
.add(
ScoreItem::new("mooncakes", "mooncakes.io 发布", max_score=10).pass(
"https://mooncakes.io/docs/Han-Wentao/moonbench",
),
)
.add(
ScoreItem::new("license", "开源许可证", max_score=5).pass(
"MIT License",
),
),
)
.add_section(
ScoreSection::new("excellent", "优秀作品增强项")
.add(
ScoreItem::new("loc", "4800+ MoonBit LOC", max_score=15).partial(
if actual_loc >= 4800 {
15
} else {
0
},
"target=4800 actual=\{actual_loc}",
),
)
.add(
ScoreItem::new("report-chain", "完整性能报告链路", max_score=10).pass(
"suite, trend, gate, snapshot, artifact",
),
)
.add(
ScoreItem::new("formats", "多格式输出", max_score=10).pass(
"Markdown, JSON, CSV, badge, GitHub summary",
),
)
.add(
ScoreItem::new("chinese-docs", "中文正式文档", max_score=10).pass(
"README, project proposal, development report, acceptance matrix",
),
)
.add(
ScoreItem::new(
"submission-ready",
"提交材料闭环",
max_score=10,
required=false,
).partial(8, "GitLink 与最终表单需用户确认"),
)
.add(
ScoreItem::new("maintainability", "可维护性", max_score=10).pass(
"模块化 API、测试覆盖、CI 验证",
),
),
)
}