///|
pub(all) enum RollbackWindowGate {
WindowConfigured
LegacyReadOnly
MoonfishPrimaryReady
ReplayAndExportReady
EscalationPathReady
ArchiveEvidenceReady
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) enum RollbackWindowStatus {
RollbackPass
RollbackBlocked
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct RollbackWindowFinding {
gate : RollbackWindowGate
status : RollbackWindowStatus
evidence_path : String
message : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct RollbackWindowInput {
product_status : @product.MoonfishProductStatus
release_certificate : @release.ReleaseCertificate
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct RollbackWindowReport {
ready : Bool
run_id : String
rollback_days : Int
legacy_app_ref : String
pass_count : Int
blocked_count : Int
findings : Array[RollbackWindowFinding]
summary : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn rollback_window_input(
product_status : @product.MoonfishProductStatus,
release_certificate : @release.ReleaseCertificate,
) -> RollbackWindowInput {
{ product_status, release_certificate }
}
///|
pub fn rollback_window_finding(
gate : RollbackWindowGate,
status : RollbackWindowStatus,
evidence_path : String,
message : String,
) -> RollbackWindowFinding {
{ gate, status, evidence_path, message }
}
///|
fn release_pass(
certificate : @release.ReleaseCertificate,
gate : @release.ReleaseGate,
) -> Bool {
certificate.findings.any(fn(finding) {
finding.gate == gate && finding.status is ReleasePass
})
}
///|
fn checklist_step(
status : @product.MoonfishProductStatus,
kind : @cutover.CutoverStepKind,
path : String,
) -> Bool {
status.cutover_plan.checklist.any(fn(step) {
step.kind == kind && step.path == path && step.status is Planned
})
}
///|
fn rollback_step(
status : @product.MoonfishProductStatus,
kind : @cutover.CutoverStepKind,
path : String,
) -> Bool {
status.cutover_plan.rollback_plan.any(fn(step) {
step.kind == kind && step.path == path && step.status is Planned
})
}
///|
fn window_configured_finding(
status : @product.MoonfishProductStatus,
) -> RollbackWindowFinding {
if status.cutover_plan.rollback_days >= 1 &&
rollback_step(status, MonitorRollbackWindow, "../paa") &&
rollback_step(
status,
ArchiveLegacyNotes,
"records/evaluations/rollback-report.json",
) {
rollback_window_finding(
WindowConfigured,
RollbackPass,
"cutover-plan.rollback_plan",
"rollback window has duration, legacy read-only monitor, and rollback-report evidence path",
)
} else {
rollback_window_finding(
WindowConfigured,
RollbackBlocked,
"cutover-plan.rollback_plan",
"rollback window is missing duration or monitor evidence paths",
)
}
}
///|
fn legacy_read_only_finding(
status : @product.MoonfishProductStatus,
certificate : @release.ReleaseCertificate,
) -> RollbackWindowFinding {
if status.decommission_report.legacy_app_ref == "../paa" &&
checklist_step(status, FreezeLegacyApp, "../paa") &&
release_pass(certificate, LegacyAppArchived) {
rollback_window_finding(
LegacyReadOnly,
RollbackPass,
"../paa",
"PA Agent is frozen and retained only as read-only rollback evidence",
)
} else {
rollback_window_finding(
LegacyReadOnly,
RollbackBlocked,
"../paa",
"PA Agent read-only rollback boundary is incomplete",
)
}
}
///|
fn moonfish_primary_finding(
status : @product.MoonfishProductStatus,
certificate : @release.ReleaseCertificate,
) -> RollbackWindowFinding {
if certificate.ready &&
release_pass(certificate, ProductStatusReady) &&
release_pass(certificate, RehearsalReady) &&
checklist_step(status, SwitchOperatorSurface, "apps/moondesk-price-action") {
rollback_window_finding(
MoonfishPrimaryReady,
RollbackPass,
certificate.release_path,
"Moonfish release certificate and cutover checklist make the app-tool the primary workflow",
)
} else {
rollback_window_finding(
MoonfishPrimaryReady,
RollbackBlocked,
"release-certificate",
"Moonfish is not yet proven as the primary rollback-window workflow",
)
}
}
///|
fn replay_export_finding(
status : @product.MoonfishProductStatus,
certificate : @release.ReleaseCertificate,
) -> RollbackWindowFinding {
if status.workspace.replay_plan.can_replay &&
!status.workspace.replay_plan.live_market_required &&
status.workspace.export_manifest.item_count > 0 &&
release_pass(certificate, ExportEvidenceReady) {
rollback_window_finding(
ReplayAndExportReady,
RollbackPass,
status.workspace.export_manifest.destination_path,
"Moonfish evidence can be exported and replayed without live market data during rollback",
)
} else {
rollback_window_finding(
ReplayAndExportReady,
RollbackBlocked,
"export-manifest",
"rollback evidence lacks replay or export readiness",
)
}
}
///|
fn escalation_path_finding(
status : @product.MoonfishProductStatus,
) -> RollbackWindowFinding {
if rollback_step(status, SwitchOperatorSurface, "apps/moondesk-price-action") &&
rollback_step(
status,
ArchiveLegacyNotes,
"records/evaluations/rollback-report.json",
) {
rollback_window_finding(
EscalationPathReady,
RollbackPass,
"records/evaluations/rollback-report.json",
"rollback plan records fallback reason, affected runs, and required Moonfish fixes",
)
} else {
rollback_window_finding(
EscalationPathReady,
RollbackBlocked,
"cutover-plan.rollback_plan",
"rollback escalation path is missing",
)
}
}
///|
fn archive_evidence_finding(
status : @product.MoonfishProductStatus,
certificate : @release.ReleaseCertificate,
) -> RollbackWindowFinding {
if status.decommission_report.ready &&
status.decommission_report.archive_items.all(fn(item) {
item.retained && item.path != ""
}) &&
release_pass(certificate, HistoricalRecordsAvailable) {
rollback_window_finding(
ArchiveEvidenceReady,
RollbackPass,
status.decommission_report.archive_root,
"decommission archive and historical record evidence are retained for rollback audit",
)
} else {
rollback_window_finding(
ArchiveEvidenceReady,
RollbackBlocked,
status.decommission_report.archive_root,
"rollback archive evidence is incomplete",
)
}
}
///|
fn count_status(
findings : Array[RollbackWindowFinding],
status : RollbackWindowStatus,
) -> Int {
findings.fold(init=0, fn(count, finding) {
if finding.status == status {
count + 1
} else {
count
}
})
}
///|
pub fn prepare_rollback_window_report(
input : RollbackWindowInput,
) -> RollbackWindowReport {
let status = input.product_status
let certificate = input.release_certificate
let findings = [
window_configured_finding(status),
legacy_read_only_finding(status, certificate),
moonfish_primary_finding(status, certificate),
replay_export_finding(status, certificate),
escalation_path_finding(status),
archive_evidence_finding(status, certificate),
]
let blocked_count = count_status(findings, RollbackBlocked)
let pass_count = count_status(findings, RollbackPass)
let ready = blocked_count == 0
{
ready,
run_id: status.workspace.bundle.request.run_id,
rollback_days: status.cutover_plan.rollback_days,
legacy_app_ref: status.decommission_report.legacy_app_ref,
pass_count,
blocked_count,
findings,
summary: if ready {
"rollback window ready for \{status.cutover_plan.rollback_days} day(s) with \{pass_count}/\{findings.length()} gate(s)"
} else {
"rollback window blocked by \{blocked_count} gate(s)"
},
}
}