///|
/// Aggregate release statistics derived from a manifest.
pub(all) struct ManifestStatistics {
file_count : Int
total_bytes : Int64
largest_path : String
largest_bytes : Int64
sha256_count : Int
generated_count : Int
}
///|
/// Calculate deterministic statistics without reading the original files again.
pub fn Manifest::statistics(self : Manifest) -> ManifestStatistics {
let mut total_bytes : Int64 = 0L
let mut largest_path = ""
let mut largest_bytes : Int64 = -1L
let mut sha256_count = 0
let mut generated_count = 0
for file in self.files {
total_bytes = total_bytes + file.size
if file.size > largest_bytes {
largest_bytes = file.size
largest_path = file.path
}
if file.hash_sha256.length() == 64 {
sha256_count = sha256_count + 1
}
if report_has_suffix(file.path) {
generated_count = generated_count + 1
}
}
{
file_count: self.files.length(),
total_bytes,
largest_path,
largest_bytes,
sha256_count,
generated_count,
}
}
///|
/// Return a stable human-readable summary for release notes and CI artifacts.
pub fn Manifest::summary(self : Manifest) -> String {
let stats = self.statistics()
let out = StringBuilder::new()
out.write_string("name=" + self.name + "\n")
out.write_string("version=" + self.version + "\n")
out.write_string("created_at=" + self.created_at + "\n")
out.write_string("merkle_root=" + self.merkle_root + "\n")
out.write_string("files=" + stats.file_count.to_string() + "\n")
out.write_string("bytes=" + stats.total_bytes.to_string() + "\n")
out.write_string("largest=" + stats.largest_path + "\n")
out.write_string("largest_bytes=" + stats.largest_bytes.to_string() + "\n")
out.write_string("sha256_records=" + stats.sha256_count.to_string() + "\n")
out.write_string(
"generated_records=" + stats.generated_count.to_string() + "\n",
)
out.to_string()
}
///|
/// Find a snapshot by exact repository-relative path.
pub fn Manifest::find(self : Manifest, path : String) -> FileSnapshot? {
for file in self.files {
if file.path == path {
return Some(file)
}
}
None
}
///|
/// Return all snapshots whose path begins with a normalized directory prefix.
pub fn Manifest::files_under(
self : Manifest,
directory : String,
) -> Array[FileSnapshot] {
let prefix = if ends_with_report(directory, "/") {
directory
} else {
directory + "/"
}
let result : Array[FileSnapshot] = []
for file in self.files {
if starts_with_report(file.path, prefix) {
result.push(file)
}
}
result
}
///|
/// Return a copy containing only files under the requested directory.
pub fn Manifest::submanifest(self : Manifest, directory : String) -> Manifest {
let files = self.files_under(directory)
Manifest::new(self.name, self.version, self.created_at, files)
}
///|
/// Format a diff in a stable form suitable for a change review.
pub fn ManifestDiff::summary(self : ManifestDiff) -> String {
let out = StringBuilder::new()
out.write_string("added=" + self.added.length().to_string() + "\n")
for file in self.added {
out.write_string("+ " + file.path + "\n")
}
out.write_string("modified=" + self.modified.length().to_string() + "\n")
for file in self.modified {
out.write_string("~ " + file.path + "\n")
}
out.write_string("removed=" + self.removed.length().to_string() + "\n")
for file in self.removed {
out.write_string("- " + file.path + "\n")
}
out.to_string()
}
///|
/// Return true if two manifests differ in any file content or path.
pub fn ManifestDiff::has_changes(self : ManifestDiff) -> Bool {
self.added.length() > 0 ||
self.removed.length() > 0 ||
self.modified.length() > 0
}
///|
fn report_has_suffix(path : String) -> Bool {
ends_with_report(path, ".wasm") ||
ends_with_report(path, ".map") ||
ends_with_report(path, ".mbti") ||
starts_with_report(path, "_build/") ||
starts_with_report(path, "target/")
}
///|
fn starts_with_report(value : String, prefix : String) -> Bool {
if value.length() < prefix.length() {
return false
}
for i = 0; i < prefix.length(); i = i + 1 {
if value[i] != prefix[i] {
return false
}
}
true
}
///|
fn ends_with_report(value : String, suffix : String) -> Bool {
if value.length() < suffix.length() {
return false
}
let offset = value.length() - suffix.length()
for i = 0; i < suffix.length(); i = i + 1 {
if value[offset + i] != suffix[i] {
return false
}
}
true
}