///|
pub(all) enum ReleaseDecision {
ReleasePass
ReleaseWarn
ReleaseBlock
} derive(Eq)
///|
pub(all) enum ReleaseImpact {
ImpactNone
ImpactPatch
ImpactMinor
ImpactMajor
} derive(Eq)
///|
pub(all) struct ReleaseGate {
min_score : Int
allow_warnings : Bool
max_warnings : Int
max_entries : Int
max_total_bytes : Int
max_asset_bytes : Int
require_tags : Bool
forbid_runtime : Bool
forbid_no_store : Bool
require_readme_asset : Bool
require_license_asset : Bool
} derive(Eq)
///|
pub(all) struct ReleaseGateResult {
decision : ReleaseDecision
quality : QualityReport
blockers : Array[String]
warnings : Array[String]
notes : Array[String]
} derive(Eq)
///|
pub(all) struct ReleaseDelta {
added : Int
removed : Int
changed : Int
byte_delta : Int
policy_changes : Int
kind_changes : Int
fingerprint_changes : Int
tag_changes : Int
impact : ReleaseImpact
} derive(Eq)
///|
pub fn ReleaseDecision::to_wire(self : ReleaseDecision) -> String {
match self {
ReleasePass => "pass"
ReleaseWarn => "warn"
ReleaseBlock => "block"
}
}
///|
pub fn ReleaseImpact::to_wire(self : ReleaseImpact) -> String {
match self {
ImpactNone => "none"
ImpactPatch => "patch"
ImpactMinor => "minor"
ImpactMajor => "major"
}
}
///|
pub fn default_release_gate() -> ReleaseGate {
{
min_score: 70,
allow_warnings: true,
max_warnings: 12,
max_entries: 5000,
max_total_bytes: 52428800,
max_asset_bytes: 5242880,
require_tags: false,
forbid_runtime: false,
forbid_no_store: false,
require_readme_asset: false,
require_license_asset: false,
}
}
///|
pub fn strict_release_gate() -> ReleaseGate {
{
min_score: 85,
allow_warnings: false,
max_warnings: 0,
max_entries: 2000,
max_total_bytes: 20971520,
max_asset_bytes: 2097152,
require_tags: true,
forbid_runtime: true,
forbid_no_store: true,
require_readme_asset: true,
require_license_asset: true,
}
}
///|
pub fn evaluate_release_gate(
manifest : Manifest,
gate : ReleaseGate,
) -> ReleaseGateResult {
let quality = assess_quality(manifest)
let blockers : Array[String] = []
let warnings : Array[String] = []
let notes : Array[String] = []
if quality.errors > 0 {
blockers.push("manifest has validation errors")
}
if quality.score < gate.min_score {
blockers.push("quality score is below release gate")
}
if quality.entries == 0 {
blockers.push("manifest has no assets")
}
if quality.entries > gate.max_entries {
blockers.push("manifest has too many assets for this gate")
}
if quality.total_bytes > gate.max_total_bytes {
blockers.push("manifest exceeds total byte budget")
}
if quality.oversized_assets > 0 {
warnings.push(
"some assets are larger than the recommended single asset budget",
)
}
if has_asset_over(manifest, gate.max_asset_bytes) {
blockers.push("at least one asset exceeds the gate single asset budget")
}
if gate.require_tags && quality.missing_tag_assets > 0 {
blockers.push("gate requires every asset to have at least one tag")
}
if gate.forbid_runtime && quality.runtime_assets > 0 {
blockers.push("gate forbids runtime assets")
}
if gate.forbid_no_store && quality.no_store_assets > 0 {
blockers.push("gate forbids no-store assets")
}
if !gate.allow_warnings && quality.warnings > 0 {
blockers.push("gate does not allow warnings")
} else if quality.warnings > gate.max_warnings {
warnings.push("warning count is above gate preference")
}
if gate.require_readme_asset && !manifest_has_readme_asset(manifest) {
blockers.push("gate requires a README-like asset")
}
if gate.require_license_asset && !manifest_has_license_asset(manifest) {
blockers.push("gate requires a license-like asset")
}
if quality.policy_advice.length() > 0 {
warnings.push("cache policy advice is available")
}
if quality.generated_assets > 0 {
notes.push(
"manifest includes generated assets; verify producer determinism",
)
}
if quality.duplicate_fingerprint_assets > 0 {
notes.push(
"some fingerprints are reused; this can be intentional for duplicated content",
)
}
if quality.directory_count <= 1 && quality.entries > 4 {
notes.push(
"most assets live in one directory; directory structure may be too flat",
)
}
{
decision: release_decision(blockers, warnings),
quality,
blockers,
warnings,
notes,
}
}
///|
pub fn evaluate_default_release_gate(manifest : Manifest) -> ReleaseGateResult {
evaluate_release_gate(manifest, default_release_gate())
}
///|
pub fn evaluate_strict_release_gate(manifest : Manifest) -> ReleaseGateResult {
evaluate_release_gate(manifest, strict_release_gate())
}
///|
pub fn release_gate_passed(result : ReleaseGateResult) -> Bool {
result.decision == ReleasePass
}
///|
pub fn release_gate_blocked(result : ReleaseGateResult) -> Bool {
result.decision == ReleaseBlock
}
///|
pub fn release_gate_warned(result : ReleaseGateResult) -> Bool {
result.decision == ReleaseWarn
}
///|
pub fn render_release_gate(result : ReleaseGateResult) -> String {
let out = StringBuilder()
out.write_string("release ")
out.write_string(result.decision.to_wire())
out.write_string("\nscore ")
out.write_string(result.quality.score.to_string())
out.write_string("\nrisk ")
out.write_string(result.quality.risk.to_wire())
out.write_string("\nentries ")
out.write_string(result.quality.entries.to_string())
out.write_string("\nbytes ")
out.write_string(result.quality.total_bytes.to_string())
out.write_string("\n")
render_release_section(out, "blockers", result.blockers)
render_release_section(out, "warnings", result.warnings)
render_release_section(out, "notes", result.notes)
out.to_string()
}
///|
pub fn summarize_release_delta(
before : Manifest,
after : Manifest,
) -> ReleaseDelta {
let diffs = diff_manifest(before, after)
release_delta_from_diffs(diffs)
}
///|
pub fn release_delta_from_diffs(diffs : Array[EntryDiff]) -> ReleaseDelta {
let mut added = 0
let mut removed = 0
let mut changed = 0
let mut byte_delta = 0
let mut policy_changes = 0
let mut kind_changes = 0
let mut fingerprint_changes = 0
let mut tag_changes = 0
for diff in diffs {
match diff.kind {
Added => {
added = added + 1
byte_delta = byte_delta + asset_bytes_or_zero(diff.after)
}
Removed => {
removed = removed + 1
byte_delta = byte_delta - asset_bytes_or_zero(diff.before)
}
Changed => {
changed = changed + 1
byte_delta = byte_delta +
asset_bytes_or_zero(diff.after) -
asset_bytes_or_zero(diff.before)
if diff.reasons.contains("policy") {
policy_changes = policy_changes + 1
}
if diff.reasons.contains("kind") {
kind_changes = kind_changes + 1
}
if diff.reasons.contains("fingerprint") {
fingerprint_changes = fingerprint_changes + 1
}
if diff.reasons.contains("tags") {
tag_changes = tag_changes + 1
}
}
}
}
let impact = release_impact(
added, removed, changed, policy_changes, kind_changes,
)
{
added,
removed,
changed,
byte_delta,
policy_changes,
kind_changes,
fingerprint_changes,
tag_changes,
impact,
}
}
///|
pub fn release_impact(
added : Int,
removed : Int,
changed : Int,
policy_changes : Int,
kind_changes : Int,
) -> ReleaseImpact {
if added == 0 && removed == 0 && changed == 0 {
return ImpactNone
}
if removed > 0 || kind_changes > 0 {
return ImpactMajor
}
if added > 0 || policy_changes > 0 {
return ImpactMinor
}
ImpactPatch
}
///|
pub fn render_release_delta(delta : ReleaseDelta) -> String {
let out = StringBuilder()
out.write_string("impact ")
out.write_string(delta.impact.to_wire())
out.write_string("\nadded ")
out.write_string(delta.added.to_string())
out.write_string("\nremoved ")
out.write_string(delta.removed.to_string())
out.write_string("\nchanged ")
out.write_string(delta.changed.to_string())
out.write_string("\nbyte-delta ")
out.write_string(delta.byte_delta.to_string())
out.write_string("\npolicy-changes ")
out.write_string(delta.policy_changes.to_string())
out.write_string("\nkind-changes ")
out.write_string(delta.kind_changes.to_string())
out.write_string("\nfingerprint-changes ")
out.write_string(delta.fingerprint_changes.to_string())
out.write_string("\ntag-changes ")
out.write_string(delta.tag_changes.to_string())
out.write_string("\n")
out.to_string()
}
///|
pub fn manifest_has_readme_asset(manifest : Manifest) -> Bool {
for entry in manifest.entries {
if path_is_readme_like(entry.path) is Ok(true) {
return true
}
}
false
}
///|
pub fn manifest_has_license_asset(manifest : Manifest) -> Bool {
for entry in manifest.entries {
if path_is_license_like(entry.path) is Ok(true) {
return true
}
}
false
}
///|
pub fn manifest_release_notes(before : Manifest, after : Manifest) -> String {
let delta = summarize_release_delta(before, after)
let out = StringBuilder()
out.write_string("Release impact: ")
out.write_string(delta.impact.to_wire())
out.write_string("\n")
out.write_string(render_release_delta(delta))
let diffs = diff_manifest(before, after)
if !diffs.is_empty() {
out.write_string("\nChanged assets:\n")
out.write_string(render_diff(diffs))
}
out.to_string()
}
///|
pub fn gate_with_score(gate : ReleaseGate, min_score : Int) -> ReleaseGate {
{
min_score,
allow_warnings: gate.allow_warnings,
max_warnings: gate.max_warnings,
max_entries: gate.max_entries,
max_total_bytes: gate.max_total_bytes,
max_asset_bytes: gate.max_asset_bytes,
require_tags: gate.require_tags,
forbid_runtime: gate.forbid_runtime,
forbid_no_store: gate.forbid_no_store,
require_readme_asset: gate.require_readme_asset,
require_license_asset: gate.require_license_asset,
}
}
///|
pub fn gate_with_size_budget(
gate : ReleaseGate,
max_total_bytes : Int,
max_asset_bytes : Int,
) -> ReleaseGate {
{
min_score: gate.min_score,
allow_warnings: gate.allow_warnings,
max_warnings: gate.max_warnings,
max_entries: gate.max_entries,
max_total_bytes,
max_asset_bytes,
require_tags: gate.require_tags,
forbid_runtime: gate.forbid_runtime,
forbid_no_store: gate.forbid_no_store,
require_readme_asset: gate.require_readme_asset,
require_license_asset: gate.require_license_asset,
}
}
///|
pub fn gate_requiring_metadata(gate : ReleaseGate) -> ReleaseGate {
{
min_score: gate.min_score,
allow_warnings: gate.allow_warnings,
max_warnings: gate.max_warnings,
max_entries: gate.max_entries,
max_total_bytes: gate.max_total_bytes,
max_asset_bytes: gate.max_asset_bytes,
require_tags: true,
forbid_runtime: gate.forbid_runtime,
forbid_no_store: gate.forbid_no_store,
require_readme_asset: true,
require_license_asset: true,
}
}
///|
fn release_decision(
blockers : Array[String],
warnings : Array[String],
) -> ReleaseDecision {
if !blockers.is_empty() {
ReleaseBlock
} else if !warnings.is_empty() {
ReleaseWarn
} else {
ReleasePass
}
}
///|
fn has_asset_over(manifest : Manifest, max_asset_bytes : Int) -> Bool {
for entry in manifest.entries {
if entry.bytes > max_asset_bytes {
return true
}
}
false
}
///|
fn asset_bytes_or_zero(asset : Asset?) -> Int {
match asset {
Some(entry) => entry.bytes
None => 0
}
}
///|
fn render_release_section(
out : StringBuilder,
title : String,
rows : Array[String],
) -> Unit {
out.write_string("\n[")
out.write_string(title)
out.write_string("]\n")
if rows.is_empty() {
out.write_string("(none)\n")
} else {
for row in rows {
out.write_string("- ")
out.write_string(row)
out.write_string("\n")
}
}
}