///|
/// A compact, versioned record of the quality signals that matter in CI.
/// It is deliberately smaller than `QualityReport`, so it remains stable when
/// the analyzer gains more diagnostic detail.
pub(all) struct PackageSnapshot {
path : String
source_count : Int
test_count : Int
public_api_count : Int
warning_count : Int
} derive(Eq)
///|
/// A serializable quality baseline suitable for committing to a repository.
pub(all) struct QualitySnapshot {
schema_version : Int
name : String
version : String
source_files : Int
test_files : Int
package_count : Int
mutation_candidates : Int
mutants_tested : Int
mutation_score : Int
coverage_total_percentage : Int
warning_count : Int
packages : Array[PackageSnapshot]
} derive(Eq)
///|
/// One metric changed between a baseline and the current scan.
pub(all) struct SnapshotDelta {
metric : String
baseline : Int
current : Int
delta : Int
direction : String
} derive(Eq)
///|
/// A quality regression that should be visible in a pull request.
pub(all) struct SnapshotRegression {
metric : String
baseline : Int
current : Int
delta : Int
message : String
} derive(Eq)
///|
/// The result of comparing a scan with a committed quality baseline.
pub(all) struct SnapshotComparison {
passed : Bool
baseline_version : String
current_version : String
deltas : Array[SnapshotDelta]
regressions : Array[SnapshotRegression]
improvements : Array[SnapshotDelta]
summary : String
} derive(Eq)
///|
/// An actionable hint generated from observable quality signals.
pub(all) struct QualityRecommendation {
id : String
priority : String
title : String
detail : String
} derive(Eq)
///|
/// Build a baseline from a quality report.
pub fn make_snapshot(report : QualityReport) -> QualitySnapshot {
let packages : Array[PackageSnapshot] = []
for pkg in report.packages {
packages.push({
path: pkg.path,
source_count: pkg.source_count,
test_count: pkg.test_count,
public_api_count: pkg.public_api_count,
warning_count: pkg.warnings.length(),
})
}
{
schema_version: 1,
name: report.name,
version: report.version,
source_files: report.source_files.length(),
test_files: report.test_files.length(),
package_count: report.packages.length(),
mutation_candidates: report.mutation_candidates.length(),
mutants_tested: report.mutants_tested,
mutation_score: report.mutation_score,
coverage_total_percentage: report.coverage_total_percentage,
warning_count: report.warnings.length(),
packages,
}
}
///|
/// Compare quality signals. A decrease in tests, mutation score, coverage, or
/// package-level test count is a regression; more warnings are also treated as
/// a regression. Source and public API changes are reported but are neutral.
pub fn compare_snapshot(
current : QualitySnapshot,
baseline : QualitySnapshot,
) -> SnapshotComparison {
let deltas : Array[SnapshotDelta] = []
let regressions : Array[SnapshotRegression] = []
let improvements : Array[SnapshotDelta] = []
compare_metric(
"source-files",
current.source_files,
baseline.source_files,
false,
deltas,
regressions,
improvements,
)
compare_metric(
"test-files",
current.test_files,
baseline.test_files,
true,
deltas,
regressions,
improvements,
)
compare_metric(
"packages",
current.package_count,
baseline.package_count,
false,
deltas,
regressions,
improvements,
)
compare_metric(
"mutation-candidates",
current.mutation_candidates,
baseline.mutation_candidates,
false,
deltas,
regressions,
improvements,
)
compare_metric(
"mutation-score",
current.mutation_score,
baseline.mutation_score,
true,
deltas,
regressions,
improvements,
)
compare_metric(
"mutants-tested",
current.mutants_tested,
baseline.mutants_tested,
baseline.mutants_tested > 0,
deltas,
regressions,
improvements,
)
compare_metric(
"coverage",
current.coverage_total_percentage,
baseline.coverage_total_percentage,
true,
deltas,
regressions,
improvements,
)
compare_metric(
"warnings",
current.warning_count,
baseline.warning_count,
false,
deltas,
regressions,
improvements,
)
compare_packages(current.packages, baseline.packages, regressions)
let passed = regressions.length() == 0
let summary = if passed {
if improvements.length() == 0 {
"no quality regressions"
} else {
"no quality regressions; improvements=" +
improvements.length().to_string()
}
} else {
"quality regressions=" + regressions.length().to_string()
}
{
passed,
baseline_version: baseline.version,
current_version: current.version,
deltas,
regressions,
improvements,
summary,
}
}
///|
/// Return whether a named regression exists.
pub fn has_regression(result : SnapshotComparison, metric : String) -> Bool {
for item in result.regressions {
if item.metric == metric {
return true
}
}
false
}
///|
/// Render a human-readable comparison for CI logs and pull requests.
pub fn render_comparison(result : SnapshotComparison) -> String {
let out = StringBuilder()
out.write_string(
if result.passed {
"MoonSeal baseline: PASS\n"
} else {
"MoonSeal baseline: FAIL\n"
},
)
out.write_string("baseline-version: " + result.baseline_version + "\n")
out.write_string("current-version: " + result.current_version + "\n")
out.write_string("summary: " + result.summary + "\n")
for item in result.regressions {
out.write_string(
"regression: " +
item.metric +
" " +
item.baseline.to_string() +
" -> " +
item.current.to_string() +
" (" +
item.message +
")\n",
)
}
for item in result.improvements {
out.write_string(
"improvement: " +
item.metric +
" " +
item.baseline.to_string() +
" -> " +
item.current.to_string() +
"\n",
)
}
for item in result.deltas {
if item.direction == "changed" {
out.write_string(
"changed: " +
item.metric +
" " +
item.baseline.to_string() +
" -> " +
item.current.to_string() +
"\n",
)
}
}
out.to_string()
}
///|
/// Read a baseline JSON document without making the caller depend on JS APIs.
pub fn parse_snapshot(input : String) -> Result[QualitySnapshot, String] {
try {
let value = @json.parse(input)
match value {
Object(map) => parse_snapshot_object(map)
_ => Err("baseline must be a JSON object")
}
} catch {
_ => Err("baseline is not valid JSON")
}
}
///|
/// Read a baseline file produced by `write_snapshot`.
pub fn read_snapshot(path : String) -> Result[QualitySnapshot, String] {
if !fs_exists(path) {
return Err("baseline file not found: " + path)
}
parse_snapshot(fs_read(path))
}
///|
/// Write a baseline file. The parent directory must already exist.
pub fn write_snapshot(
path : String,
snapshot : QualitySnapshot,
) -> Result[Unit, String] {
write_text(path, snapshot.to_json().stringify(indent=2) + "\n")
}
///|
/// Write a generated integration artifact using the same JS filesystem bridge
/// as snapshots. Keeping this operation in the library makes CLI wrappers and
/// host integrations follow the same error contract.
pub fn write_text(path : String, content : String) -> Result[Unit, String] {
fs_write(path, content)
Ok(())
}
///|
/// Generate prioritized remediation advice from a report.
pub fn recommendations(report : QualityReport) -> Array[QualityRecommendation] {
let result : Array[QualityRecommendation] = []
if report.test_files.length() < 2 {
result.push({
id: "tests-project",
priority: "high",
title: "Add project-level tests",
detail: "Add black-box or white-box tests until the project has at least two test files.",
})
}
for pkg in report.packages {
if pkg.source_count > 0 && pkg.test_count == 0 {
result.push({
id: "tests-package:" + pkg.path,
priority: "high",
title: "Test package " + pkg.path,
detail: "Add a package test file so its source behavior is exercised in CI.",
})
}
}
if has_warning(report, "missing README.md") {
result.push({
id: "docs-readme",
priority: "high",
title: "Document installation and usage",
detail: "Add the README declared by moon.mod, including environment, commands, and expected output.",
})
}
if has_warning(report, "missing LICENSE") {
result.push({
id: "legal-license",
priority: "high",
title: "Add an open-source license",
detail: "Place a complete LICENSE file at the project root and keep it synchronized with moon.mod.",
})
}
if has_warning(report, "missing CI workflow") {
result.push({
id: "ci-workflow",
priority: "high",
title: "Add a reproducible CI workflow",
detail: "Run MoonBit checks, formatting, generated interfaces, and tests on the supported platforms.",
})
}
if report.mutation_candidates.length() > 0 && report.mutation_score == 0 {
result.push({
id: "mutation-baseline",
priority: "medium",
title: "Measure mutation adequacy",
detail: "Run the gate with --mutate and use survived mutants to add boundary assertions.",
})
}
if report.coverage_total_percentage == 0 {
result.push({
id: "coverage-baseline",
priority: "medium",
title: "Measure code coverage",
detail: "Run the gate with --coverage and set a project-appropriate minimum in moonseal.json.",
})
}
if report.warnings.length() == 0 && report.mutation_candidates.length() == 0 {
result.push({
id: "maintain-regression",
priority: "low",
title: "Maintain a quality baseline",
detail: "Commit a snapshot and compare it in CI so future changes cannot silently reduce quality.",
})
}
result
}
///|
/// Render remediation advice for terminal output.
pub fn render_recommendations(items : Array[QualityRecommendation]) -> String {
if items.length() == 0 {
return "MoonSeal recommendations: none\n"
}
let out = StringBuilder()
out.write_string("MoonSeal recommendations:\n")
for item in items {
out.write_string(
"- [" + item.priority + "] " + item.title + ": " + item.detail + "\n",
)
}
out.to_string()
}
///|
/// Return a small health score for dashboards. The score is explainable and
/// intentionally does not replace the strict gate.
pub fn health_score(report : QualityReport) -> Int {
let mut score = 100
if report.test_files.length() == 0 {
score -= 25
} else if report.test_files.length() < 2 {
score -= 10
}
for pkg in report.packages {
if pkg.source_count > 0 && pkg.test_count == 0 {
score -= 15
}
}
if report.readme.length() == 0 {
score -= 10
}
if report.license.length() == 0 {
score -= 10
}
if has_warning(report, "missing CI workflow") {
score -= 15
}
if report.mutation_candidates.length() > 0 && report.mutation_score == 0 {
score -= 10
}
if score < 0 {
0
} else {
score
}
}
///|
/// Provide a stable label for simple dashboards.
pub fn health_label(score : Int) -> String {
if score >= 90 {
"excellent"
} else if score >= 75 {
"good"
} else if score >= 50 {
"needs-attention"
} else {
"critical"
}
}
///|
fn compare_metric(
metric : String,
current : Int,
baseline : Int,
lower_is_regression : Bool,
deltas : Array[SnapshotDelta],
regressions : Array[SnapshotRegression],
improvements : Array[SnapshotDelta],
) -> Unit {
let delta = current - baseline
let direction = if delta == 0 { "unchanged" } else { "changed" }
if delta != 0 {
let item = { metric, baseline, current, delta, direction }
deltas.push(item)
let bad = if lower_is_regression {
delta < 0
} else {
metric == "warnings" && delta > 0
}
if bad {
regressions.push({
metric,
baseline,
current,
delta,
message: if metric == "warnings" {
"warning count increased"
} else {
"quality metric decreased"
},
})
} else if lower_is_regression && delta > 0 {
improvements.push(item)
}
}
}
///|
fn compare_packages(
current : Array[PackageSnapshot],
baseline : Array[PackageSnapshot],
regressions : Array[SnapshotRegression],
) -> Unit {
for old_package in baseline {
match find_package(current, old_package.path) {
Some(new_package) => {
if old_package.source_count > 0 &&
old_package.test_count > 0 &&
new_package.test_count < old_package.test_count {
let delta = new_package.test_count - old_package.test_count
regressions.push({
metric: "package:" + old_package.path,
baseline: old_package.test_count,
current: new_package.test_count,
delta,
message: "package test count decreased",
})
}
if new_package.warning_count > old_package.warning_count {
let delta = new_package.warning_count - old_package.warning_count
regressions.push({
metric: "package-warnings:" + old_package.path,
baseline: old_package.warning_count,
current: new_package.warning_count,
delta,
message: "package warnings increased",
})
}
}
None =>
regressions.push({
metric: "package:" + old_package.path,
baseline: old_package.source_count,
current: 0,
delta: 0 - old_package.source_count,
message: "package disappeared",
})
}
}
}
///|
fn find_package(
packages : Array[PackageSnapshot],
path : String,
) -> PackageSnapshot? {
for pkg in packages {
if pkg.path == path {
return Some(pkg)
}
}
None
}
///|
fn parse_snapshot_object(
map : Map[String, Json],
) -> Result[QualitySnapshot, String] {
let schema_version = match required_int(map, "schema_version") {
Ok(value) => value
Err(message) => return Err(message)
}
if schema_version != 1 {
return Err(
"unsupported baseline schema_version: " + schema_version.to_string(),
)
}
let name = match required_string(map, "name") {
Ok(value) => value
Err(message) => return Err(message)
}
let version = match required_string(map, "version") {
Ok(value) => value
Err(message) => return Err(message)
}
let source_files = match required_int(map, "source_files") {
Ok(value) => value
Err(message) => return Err(message)
}
let test_files = match required_int(map, "test_files") {
Ok(value) => value
Err(message) => return Err(message)
}
let package_count = match required_int(map, "package_count") {
Ok(value) => value
Err(message) => return Err(message)
}
let mutation_candidates = match required_int(map, "mutation_candidates") {
Ok(value) => value
Err(message) => return Err(message)
}
let mutants_tested = match required_int(map, "mutants_tested") {
Ok(value) => value
Err(message) => return Err(message)
}
let mutation_score = match required_int(map, "mutation_score") {
Ok(value) => value
Err(message) => return Err(message)
}
let coverage_total_percentage = match
required_int(map, "coverage_total_percentage") {
Ok(value) => value
Err(message) => return Err(message)
}
let warning_count = match required_int(map, "warning_count") {
Ok(value) => value
Err(message) => return Err(message)
}
let packages = match required_packages(map, "packages") {
Ok(value) => value
Err(message) => return Err(message)
}
Ok({
schema_version,
name,
version,
source_files,
test_files,
package_count,
mutation_candidates,
mutants_tested,
mutation_score,
coverage_total_percentage,
warning_count,
packages,
})
}
///|
fn required_string(
map : Map[String, Json],
key : String,
) -> Result[String, String] {
match map.get(key) {
Some(String(value)) => Ok(value)
_ => Err("baseline field is missing or not a string: " + key)
}
}
///|
fn required_int(map : Map[String, Json], key : String) -> Result[Int, String] {
match map.get(key) {
Some(Number(value, ..)) => Ok(value.to_int())
_ => Err("baseline field is missing or not an integer: " + key)
}
}
///|
fn required_packages(
map : Map[String, Json],
key : String,
) -> Result[Array[PackageSnapshot], String] {
match map.get(key) {
Some(Array(items)) => {
let packages : Array[PackageSnapshot] = []
for item in items {
match item {
Object(package_map) => {
let path = match required_string(package_map, "path") {
Ok(value) => value
Err(message) => return Err(message)
}
let source_count = match required_int(package_map, "source_count") {
Ok(value) => value
Err(message) => return Err(message)
}
let test_count = match required_int(package_map, "test_count") {
Ok(value) => value
Err(message) => return Err(message)
}
let public_api_count = match
required_int(package_map, "public_api_count") {
Ok(value) => value
Err(message) => return Err(message)
}
let warning_count = match
required_int(package_map, "warning_count") {
Ok(value) => value
Err(message) => return Err(message)
}
packages.push({
path,
source_count,
test_count,
public_api_count,
warning_count,
})
}
_ => return Err("baseline packages must contain objects")
}
}
Ok(packages)
}
_ => Err("baseline field is missing or not an array: " + key)
}
}
///|
pub impl ToJson for PackageSnapshot with fn to_json(self : PackageSnapshot) -> Json {
Json::object({
"path": Json::string(self.path),
"source_count": Json::number(self.source_count.to_double()),
"test_count": Json::number(self.test_count.to_double()),
"public_api_count": Json::number(self.public_api_count.to_double()),
"warning_count": Json::number(self.warning_count.to_double()),
})
}
///|
pub impl ToJson for QualitySnapshot with fn to_json(self : QualitySnapshot) -> Json {
let packages = Array::new()
for pkg in self.packages {
packages.push(pkg.to_json())
}
Json::object({
"schema_version": Json::number(self.schema_version.to_double()),
"name": Json::string(self.name),
"version": Json::string(self.version),
"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()),
"mutants_tested": Json::number(self.mutants_tested.to_double()),
"mutation_score": Json::number(self.mutation_score.to_double()),
"coverage_total_percentage": Json::number(
self.coverage_total_percentage.to_double(),
),
"warning_count": Json::number(self.warning_count.to_double()),
"packages": Json::array(packages),
})
}
///|
pub impl ToJson for SnapshotDelta with fn to_json(self : SnapshotDelta) -> Json {
Json::object({
"metric": Json::string(self.metric),
"baseline": Json::number(self.baseline.to_double()),
"current": Json::number(self.current.to_double()),
"delta": Json::number(self.delta.to_double()),
"direction": Json::string(self.direction),
})
}
///|
pub impl ToJson for SnapshotRegression with fn to_json(
self : SnapshotRegression,
) -> Json {
Json::object({
"metric": Json::string(self.metric),
"baseline": Json::number(self.baseline.to_double()),
"current": Json::number(self.current.to_double()),
"delta": Json::number(self.delta.to_double()),
"message": Json::string(self.message),
})
}
///|
pub impl ToJson for SnapshotComparison with fn to_json(
self : SnapshotComparison,
) -> Json {
let deltas = Array::new()
for item in self.deltas {
deltas.push(item.to_json())
}
let regressions = Array::new()
for item in self.regressions {
regressions.push(item.to_json())
}
let improvements = Array::new()
for item in self.improvements {
improvements.push(item.to_json())
}
Json::object({
"passed": Json::boolean(self.passed),
"baseline_version": Json::string(self.baseline_version),
"current_version": Json::string(self.current_version),
"deltas": Json::array(deltas),
"regressions": Json::array(regressions),
"improvements": Json::array(improvements),
"summary": Json::string(self.summary),
})
}
///|
pub impl ToJson for QualityRecommendation with fn to_json(
self : QualityRecommendation,
) -> Json {
Json::object({
"id": Json::string(self.id),
"priority": Json::string(self.priority),
"title": Json::string(self.title),
"detail": Json::string(self.detail),
})
}