///|
/// A stable release evidence record for package publication and acceptance.
pub(all) struct ReleaseCheck {
id : String
status : String
detail : String
} derive(Eq)
///|
/// A machine-readable summary of the checks that support a release decision.
pub(all) struct ReleaseManifest {
schema_version : Int
project : String
version : String
repository : String
source_files : Int
test_files : Int
api_coverage : Int
gate_passed : Bool
failed_checks : Int
checks : Array[ReleaseCheck]
} derive(Eq)
///|
/// Build a release evidence manifest from one project scan.
pub fn make_release_manifest(
report : QualityReport,
gate : GateResult,
api : ApiCoverage,
) -> ReleaseManifest {
let checks : Array[ReleaseCheck] = []
add_check(
checks,
"tests",
report.test_files.length() >= 2,
report.test_files.length().to_string() + " test files discovered",
)
add_check(
checks,
"readme",
report.readme.length() > 0,
if report.readme.length() > 0 {
"README is present"
} else {
"README is missing"
},
)
add_check(
checks,
"license",
report.license.length() > 0,
if report.license.length() > 0 {
"license is present"
} else {
"license is missing"
},
)
add_check(
checks,
"ci",
!has_warning(report, "missing CI workflow"),
if has_warning(report, "missing CI workflow") {
"CI workflow is missing"
} else {
"CI workflow is present"
},
)
add_check(
checks,
"api",
api.summary.total == 0 || api.summary.covered == api.summary.total,
api.summary.covered.to_string() +
"/" +
api.summary.total.to_string() +
" public symbols associated with tests",
)
add_check(
checks,
"gate",
gate.passed,
if gate.passed {
"quality gate passed"
} else {
"quality gate failures=" + gate.failures.length().to_string()
},
)
let failed = count_failed_checks(checks)
{
schema_version: 1,
project: report.name,
version: report.version,
repository: report.repository,
source_files: report.source_files.length(),
test_files: report.test_files.length(),
api_coverage: api.summary.percentage,
gate_passed: gate.passed,
failed_checks: failed,
checks,
}
}
///|
/// Return whether every release evidence check passed.
pub fn manifest_is_ready(manifest : ReleaseManifest) -> Bool {
manifest.schema_version == 1 && manifest.failed_checks == 0
}
///|
/// Write a release manifest for CI artifacts or a submission archive.
pub fn write_release_manifest(
path : String,
manifest : ReleaseManifest,
) -> Result[Unit, String] {
write_text(path, manifest.to_json().stringify(indent=2) + "\n")
}
///|
/// Read a release manifest produced by `write_release_manifest`.
pub fn read_release_manifest(path : String) -> Result[ReleaseManifest, String] {
if !fs_exists(path) {
return Err("release manifest file not found: " + path)
}
parse_release_manifest(fs_read(path))
}
///|
/// Parse a release manifest so a downstream publisher can enforce its checks.
pub fn parse_release_manifest(
input : String,
) -> Result[ReleaseManifest, String] {
try {
match @json.parse(input) {
Object(map) => parse_release_manifest_object(map)
_ => Err("release manifest must be a JSON object")
}
} catch {
_ => Err("release manifest is not valid JSON")
}
}
///|
/// Find one named check, returning a failed result when the id is absent.
pub fn manifest_check(manifest : ReleaseManifest, id : String) -> ReleaseCheck {
for check in manifest.checks {
if check.id == id {
return check
}
}
{ id, status: "fail", detail: "check is missing from manifest" }
}
///|
/// Return the percentage of evidence checks that passed.
pub fn manifest_pass_rate(manifest : ReleaseManifest) -> Int {
if manifest.checks.length() == 0 {
return 0
}
let passed = manifest.checks.length() - manifest.failed_checks
passed * 100 / manifest.checks.length()
}
///|
/// Return failed check ids in stable manifest order.
pub fn manifest_failed_ids(manifest : ReleaseManifest) -> Array[String] {
let result : Array[String] = []
for check in manifest.checks {
if check.status != "pass" {
result.push(check.id)
}
}
result
}
///|
/// Return a stable one-word status for dashboards and CI annotations.
pub fn manifest_status(manifest : ReleaseManifest) -> String {
if manifest_is_ready(manifest) {
"ready"
} else {
"blocked"
}
}
///|
/// Explain failed checks as a comma-separated reviewer hint.
pub fn manifest_failure_summary(manifest : ReleaseManifest) -> String {
let failed = manifest_failed_ids(manifest)
if failed.length() == 0 {
return "none"
}
let parts : Array[String] = []
for id in failed {
parts.push(id)
}
parts.join(", ")
}
///|
/// Return all check ids in the order shown to a reviewer.
pub fn manifest_check_ids(manifest : ReleaseManifest) -> Array[String] {
let result : Array[String] = []
for check in manifest.checks {
result.push(check.id)
}
result
}
///|
/// Return whether the manifest contains a named check.
pub fn manifest_has_check(manifest : ReleaseManifest, id : String) -> Bool {
for check in manifest.checks {
if check.id == id {
return true
}
}
false
}
///|
/// Return whether all expected evidence categories are represented.
pub fn manifest_is_complete(manifest : ReleaseManifest) -> Bool {
manifest.checks.length() >= 6 && manifest_has_check(manifest, "gate")
}
///|
/// Render a compact Markdown table suitable for a release PR description.
pub fn render_release_manifest_markdown(manifest : ReleaseManifest) -> String {
let out = StringBuilder()
out.write_string(
"## MoonSeal release manifest — " +
(if manifest_is_ready(manifest) { "READY" } else { "NOT READY" }) +
"\n\n",
)
out.write_string("| Check | Status | Detail |\n| --- | --- | --- |\n")
for check in manifest.checks {
out.write_string(
"| " + check.id + " | " + check.status + " | " + check.detail + " |\n",
)
}
out.write_string(
"\nProject: `" +
manifest.project +
"` \nVersion: `" +
manifest.version +
"` \nAPI coverage: " +
manifest.api_coverage.to_string() +
"% \nPass rate: " +
manifest_pass_rate(manifest).to_string() +
"%\n",
)
out.to_string()
}
///|
fn parse_release_manifest_object(
map : Map[String, Json],
) -> Result[ReleaseManifest, String] {
let schema_version = match required_int(map, "schema_version") {
Ok(value) => value
Err(message) => return Err("release manifest " + message)
}
if schema_version != 1 {
return Err(
"unsupported release manifest schema_version: " +
schema_version.to_string(),
)
}
let project = match required_string(map, "project") {
Ok(value) => value
Err(message) => return Err("release manifest " + message)
}
let version = match required_string(map, "version") {
Ok(value) => value
Err(message) => return Err("release manifest " + message)
}
let repository = match required_string(map, "repository") {
Ok(value) => value
Err(message) => return Err("release manifest " + message)
}
let source_files = match required_int(map, "source_files") {
Ok(value) => value
Err(message) => return Err("release manifest " + message)
}
let test_files = match required_int(map, "test_files") {
Ok(value) => value
Err(message) => return Err("release manifest " + message)
}
let api_coverage = match required_int(map, "api_coverage") {
Ok(value) => value
Err(message) => return Err("release manifest " + message)
}
let gate_passed = match required_bool(map, "gate_passed") {
Ok(value) => value
Err(message) => return Err("release manifest " + message)
}
let failed_checks = match required_int(map, "failed_checks") {
Ok(value) => value
Err(message) => return Err("release manifest " + message)
}
let checks = match map.get("checks") {
Some(Array(items)) => parse_release_checks(items)
_ => return Err("release manifest field is missing or not an array: checks")
}
Ok({
schema_version,
project,
version,
repository,
source_files,
test_files,
api_coverage,
gate_passed,
failed_checks,
checks,
})
}
///|
fn parse_release_checks(items : Array[Json]) -> Array[ReleaseCheck] {
let checks : Array[ReleaseCheck] = []
for item in items {
match item {
Object(map) => {
let id = match required_string(map, "id") {
Ok(value) => value
Err(message) => abort(message)
}
let status = match required_string(map, "status") {
Ok(value) => value
Err(message) => abort(message)
}
let detail = match required_string(map, "detail") {
Ok(value) => value
Err(message) => abort(message)
}
checks.push({ id, status, detail })
}
_ => abort("release manifest checks must contain objects")
}
}
checks
}
///|
fn required_bool(map : Map[String, Json], key : String) -> Result[Bool, String] {
match map.get(key) {
Some(True) => Ok(true)
Some(False) => Ok(false)
_ => Err("field is missing or not a boolean: " + key)
}
}
///|
/// Render an evidence manifest for a human reviewer.
pub fn render_release_manifest(manifest : ReleaseManifest) -> String {
let out = StringBuilder()
out.write_string(
if manifest_is_ready(manifest) {
"MoonSeal release manifest: READY\n"
} else {
"MoonSeal release manifest: NOT READY\n"
},
)
out.write_string("project: " + manifest.project + "\n")
out.write_string("version: " + manifest.version + "\n")
out.write_string("repository: " + manifest.repository + "\n")
out.write_string("source-files: " + manifest.source_files.to_string() + "\n")
out.write_string("test-files: " + manifest.test_files.to_string() + "\n")
out.write_string("api-coverage: " + manifest.api_coverage.to_string() + "%\n")
out.write_string(
"failed-checks: " + manifest.failed_checks.to_string() + "\n",
)
for check in manifest.checks {
out.write_string(
"- [" + check.status + "] " + check.id + ": " + check.detail + "\n",
)
}
out.to_string()
}
///|
fn add_check(
checks : Array[ReleaseCheck],
id : String,
passed : Bool,
detail : String,
) -> Unit {
checks.push({ id, status: if passed { "pass" } else { "fail" }, detail })
}
///|
fn count_failed_checks(checks : Array[ReleaseCheck]) -> Int {
let mut failed = 0
for check in checks {
if check.status != "pass" {
failed += 1
}
}
failed
}
///|
pub impl ToJson for ReleaseCheck with fn to_json(self : ReleaseCheck) -> Json {
Json::object({
"id": Json::string(self.id),
"status": Json::string(self.status),
"detail": Json::string(self.detail),
})
}
///|
pub impl ToJson for ReleaseManifest with fn to_json(self : ReleaseManifest) -> Json {
let checks = Array::new()
for check in self.checks {
checks.push(check.to_json())
}
Json::object({
"schema_version": Json::number(self.schema_version.to_double()),
"project": Json::string(self.project),
"version": Json::string(self.version),
"repository": Json::string(self.repository),
"source_files": Json::number(self.source_files.to_double()),
"test_files": Json::number(self.test_files.to_double()),
"api_coverage": Json::number(self.api_coverage.to_double()),
"gate_passed": Json::boolean(self.gate_passed),
"failed_checks": Json::number(self.failed_checks.to_double()),
"checks": Json::array(checks),
})
}