///|
pub(all) enum ProductRehearsalCheck {
LaunchSurfaceReady
DailyWorkflowExecutable
ExportReplayEvidence
GoldenAndLiveComparison
LegacyReadOnlyBoundary
ArchiveReady
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) enum ProductRehearsalStatus {
RehearsalPass
RehearsalBlocked
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct ProductRehearsalFinding {
check : ProductRehearsalCheck
status : ProductRehearsalStatus
evidence_path : String
message : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct ProductRehearsalReport {
ready : Bool
run_id : @domain.RunId
launch_path : String
pass_count : Int
blocked_count : Int
findings : Array[ProductRehearsalFinding]
summary : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn product_rehearsal_finding(
check : ProductRehearsalCheck,
status : ProductRehearsalStatus,
evidence_path : String,
message : String,
) -> ProductRehearsalFinding {
{ check, status, evidence_path, message }
}
///|
fn enabled_action(
manifest : ProductLaunchManifest,
action : ProductLaunchAction,
) -> Bool {
manifest.actions.contains(action)
}
///|
fn ready_panel(manifest : ProductLaunchManifest, id : String) -> Bool {
manifest.panels.any(fn(panel) { panel.id == id && panel.ready })
}
///|
fn app_surface_ready(
status : MoonfishProductStatus,
surface_id : String,
) -> Bool {
status.workspace.app_tool_manifest.surfaces.any(fn(surface) {
surface.id == surface_id && surface.enabled
})
}
///|
fn comparison_pass(
status : MoonfishProductStatus,
kind : @comparison.CutoverComparisonKind,
) -> Bool {
status.comparison_report.cases.any(fn(item) {
item.kind == kind && item.status is ComparisonPass
})
}
///|
fn decommission_pass(
status : MoonfishProductStatus,
gate : @decommission.DecommissionGate,
) -> Bool {
status.decommission_report.findings.any(fn(item) {
item.gate == gate && item.status is DecommissionPass
})
}
///|
fn launch_surface_finding(
status : MoonfishProductStatus,
manifest : ProductLaunchManifest,
) -> ProductRehearsalFinding {
if manifest.ready &&
manifest.blocking_gate_count == 0 &&
manifest.status_path ==
"records/product-status/\{status.workspace.bundle.request.run_id}.json" &&
ready_panel(manifest, "launch-overview") &&
ready_panel(manifest, "cutover-checklist") &&
ready_panel(manifest, "handoff-runbook") &&
enabled_action(manifest, OpenOperatorWorkspace) &&
enabled_action(manifest, RunCutoverChecklist) &&
enabled_action(manifest, OpenHandoffRunbook) &&
enabled_action(manifest, ExportLaunchEvidence) {
product_rehearsal_finding(
LaunchSurfaceReady,
RehearsalPass,
manifest.status_path,
"product launch surface is ready and exposes workspace, checklist, handoff, and export actions",
)
} else {
product_rehearsal_finding(
LaunchSurfaceReady,
RehearsalBlocked,
manifest.status_path,
"product launch surface is blocked or missing a required operator action",
)
}
}
///|
fn daily_workflow_finding(
status : MoonfishProductStatus,
) -> ProductRehearsalFinding {
if status.handoff_report.ready &&
status.handoff_report.runbook.length() >= 5 &&
app_surface_ready(status, "run-analysis") &&
app_surface_ready(status, "inspect-chart") &&
app_surface_ready(status, "inspect-events") &&
app_surface_ready(status, "start-follow-up") &&
app_surface_ready(status, "export-run") &&
app_surface_ready(status, "replay-run") &&
status.workspace.run_view.event_count > 0 &&
status.workspace.run_view.followup_topic_count > 0 {
product_rehearsal_finding(
DailyWorkflowExecutable,
RehearsalPass,
status.handoff_report.launch_path,
"operator handoff runbook can drive run, inspect, follow-up, export, and replay surfaces",
)
} else {
product_rehearsal_finding(
DailyWorkflowExecutable,
RehearsalBlocked,
status.handoff_report.launch_path,
"operator daily workflow is missing launch runbook or app-tool surfaces",
)
}
}
///|
fn export_replay_finding(
status : MoonfishProductStatus,
) -> ProductRehearsalFinding {
if status.workspace.replay_plan.can_replay &&
!status.workspace.replay_plan.live_market_required &&
status.workspace.export_manifest.item_count > 0 &&
status.workspace.export_manifest.items.any(fn(item) {
item.kind is ReplayPlanArtifact && item.required
}) &&
status.workspace.export_manifest.items.any(fn(item) {
item.kind is ExportManifestArtifact && item.required
}) {
product_rehearsal_finding(
ExportReplayEvidence,
RehearsalPass,
status.workspace.export_manifest.destination_path,
"export manifest includes replay evidence and can replay without live market data",
)
} else {
product_rehearsal_finding(
ExportReplayEvidence,
RehearsalBlocked,
status.workspace.export_manifest.destination_path,
"export or replay evidence is incomplete for launch rehearsal",
)
}
}
///|
fn comparison_rehearsal_finding(
status : MoonfishProductStatus,
) -> ProductRehearsalFinding {
if status.comparison_report.ready &&
comparison_pass(status, GoldenFixtureRun) &&
comparison_pass(status, LiveSnapshotRehearsal) {
product_rehearsal_finding(
GoldenAndLiveComparison,
RehearsalPass,
"records/comparisons/live-snapshot-rehearsal.json",
"golden fixture and live snapshot rehearsal evidence both pass",
)
} else {
product_rehearsal_finding(
GoldenAndLiveComparison,
RehearsalBlocked,
"cutover-comparison-report",
"golden fixture or live snapshot comparison is not ready",
)
}
}
///|
fn legacy_boundary_finding(
status : MoonfishProductStatus,
) -> ProductRehearsalFinding {
if status.cutover_plan.checklist.any(fn(step) {
step.kind is FreezeLegacyApp &&
step.path == "../paa" &&
step.status is Planned
}) &&
status.cutover_plan.rollback_plan.any(fn(step) {
step.kind is MonitorRollbackWindow &&
step.path == "../paa" &&
step.status is Planned
}) &&
comparison_pass(status, LegacyReadOnlyRollback) {
product_rehearsal_finding(
LegacyReadOnlyBoundary,
RehearsalPass,
"../paa",
"legacy PA Agent is retained only as a read-only rollback and audit reference",
)
} else {
product_rehearsal_finding(
LegacyReadOnlyBoundary,
RehearsalBlocked,
"../paa",
"legacy PA Agent is not proven read-only during launch rehearsal",
)
}
}
///|
fn archive_ready_finding(
status : MoonfishProductStatus,
) -> ProductRehearsalFinding {
if status.decommission_report.ready &&
decommission_pass(status, ArchiveManifestReady) &&
decommission_pass(status, ImportedRecordsPreserved) &&
status.decommission_report.archive_items.all(fn(item) {
item.retained && item.path != ""
}) {
product_rehearsal_finding(
ArchiveReady,
RehearsalPass,
status.decommission_report.archive_root,
"decommission archive retains migration, parity, imported data, handoff, rollback, and legacy references",
)
} else {
product_rehearsal_finding(
ArchiveReady,
RehearsalBlocked,
status.decommission_report.archive_root,
"decommission archive is incomplete for launch rehearsal",
)
}
}
///|
fn count_rehearsal_status(
findings : Array[ProductRehearsalFinding],
status : ProductRehearsalStatus,
) -> Int {
findings.fold(init=0, fn(count, item) {
if item.status == status {
count + 1
} else {
count
}
})
}
///|
pub fn prepare_product_rehearsal_report(
status : MoonfishProductStatus,
) -> ProductRehearsalReport {
let manifest = prepare_product_launch_manifest(status)
let findings = [
launch_surface_finding(status, manifest),
daily_workflow_finding(status),
export_replay_finding(status),
comparison_rehearsal_finding(status),
legacy_boundary_finding(status),
archive_ready_finding(status),
]
let blocked_count = count_rehearsal_status(findings, RehearsalBlocked)
let pass_count = count_rehearsal_status(findings, RehearsalPass)
let ready = blocked_count == 0
{
ready,
run_id: status.workspace.bundle.request.run_id,
launch_path: manifest.mount_path,
pass_count,
blocked_count,
findings,
summary: if ready {
"product rehearsal ready with \{pass_count}/\{findings.length()} check(s)"
} else {
"product rehearsal blocked by \{blocked_count} check(s)"
},
}
}