///|
pub(all) enum DecommissionGate {
PrimaryWorkflowReady
LegacyReadOnly
ImportedRecordsPreserved
KnownDifferencesArchived
RollbackWindowPreserved
ArchiveManifestReady
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) enum DecommissionStatus {
DecommissionPass
DecommissionBlocked
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) enum ArchiveItemKind {
MigrationPlan
KnownDifferenceReport
ImportedDataManifest
HandoffRunbook
RollbackReport
LegacySourceReference
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct DecommissionFinding {
gate : DecommissionGate
status : DecommissionStatus
path : String
message : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct ArchiveItem {
kind : ArchiveItemKind
path : String
owner : String
retained : Bool
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct DecommissionInput {
cutover_plan : @cutover.CutoverPlan
handoff_report : @handoff.OperatorHandoffReport
import_projection : @migration.LegacyImportProjection
parity_report : @parity.ParityReport
comparison_report : @comparison.CutoverComparisonReport
legacy_app_ref : String
archive_root : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct DecommissionReport {
ready : Bool
legacy_app_ref : String
archive_root : String
pass_count : Int
blocked_count : Int
findings : Array[DecommissionFinding]
archive_items : Array[ArchiveItem]
final_note : String
summary : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn decommission_input(
cutover_plan : @cutover.CutoverPlan,
handoff_report : @handoff.OperatorHandoffReport,
import_projection : @migration.LegacyImportProjection,
parity_report : @parity.ParityReport,
comparison_report : @comparison.CutoverComparisonReport,
legacy_app_ref? : String = "../paa",
archive_root? : String = "records/decommission",
) -> DecommissionInput {
{
cutover_plan,
handoff_report,
import_projection,
parity_report,
comparison_report,
legacy_app_ref,
archive_root,
}
}
///|
fn finding(
gate : DecommissionGate,
status : DecommissionStatus,
path : String,
message : String,
) -> DecommissionFinding {
{ gate, status, path, message }
}
///|
fn archive_item(
kind : ArchiveItemKind,
path : String,
owner : String,
retained? : Bool = true,
) -> ArchiveItem {
{ kind, path, owner, retained }
}
///|
fn primary_workflow_finding(input : DecommissionInput) -> DecommissionFinding {
if input.cutover_plan.ready_to_cutover && input.handoff_report.ready {
finding(
PrimaryWorkflowReady,
DecommissionPass,
input.handoff_report.launch_path,
"Moonfish handoff and cutover plan prove the primary operator workflow is ready",
)
} else {
finding(
PrimaryWorkflowReady,
DecommissionBlocked,
input.handoff_report.launch_path,
"primary Moonfish workflow is not ready for decommission",
)
}
}
///|
fn legacy_read_only_finding(input : DecommissionInput) -> DecommissionFinding {
if input.legacy_app_ref != "" &&
input.cutover_plan.checklist.any(fn(step) {
step.kind is FreezeLegacyApp && step.path == input.legacy_app_ref
}) &&
input.comparison_report.cases.any(fn(item) {
item.kind is LegacyReadOnlyRollback && item.status is ComparisonPass
}) {
finding(
LegacyReadOnly,
DecommissionPass,
input.legacy_app_ref,
"PA Agent is retained only as a read-only rollback and audit reference",
)
} else {
finding(
LegacyReadOnly,
DecommissionBlocked,
input.legacy_app_ref,
"legacy PA Agent read-only boundary is not proven",
)
}
}
///|
fn imported_records_finding(input : DecommissionInput) -> DecommissionFinding {
if input.import_projection.record_count > 0 &&
input.import_projection.review_count == 0 &&
input.import_projection.records.all(fn(record) {
record.source_path.contains(input.legacy_app_ref) &&
record.replay_anchor_path != ""
}) {
finding(
ImportedRecordsPreserved,
DecommissionPass,
"records/imports/\{input.import_projection.batch_id}.json",
"legacy records are inspectable in MoonBook and retain replay anchors",
)
} else {
finding(
ImportedRecordsPreserved,
DecommissionBlocked,
"legacy-import-projection",
"legacy records are missing, under review, or lack replay anchors",
)
}
}
///|
fn known_difference_finding(input : DecommissionInput) -> DecommissionFinding {
if input.parity_report.intentional_difference_count > 0 &&
input.parity_report.untraced_case_count == 0 &&
input.parity_report.cases.any(fn(item) {
item.expectation is IntentionalDifference &&
item.evidence_refs.any(fn(ref_item) {
ref_item.kind is KnownDifferenceNote
})
}) {
finding(
KnownDifferencesArchived,
DecommissionPass,
"docs/pa-agent-migration-plan.md#phase-7",
"intentional differences are traced to archived migration notes",
)
} else {
finding(
KnownDifferencesArchived,
DecommissionBlocked,
"parity-report",
"known differences are not fully traced",
)
}
}
///|
fn rollback_window_finding(input : DecommissionInput) -> DecommissionFinding {
if input.cutover_plan.rollback_days >= 1 &&
input.cutover_plan.rollback_plan.any(fn(step) {
step.kind is MonitorRollbackWindow && step.path == input.legacy_app_ref
}) {
finding(
RollbackWindowPreserved,
DecommissionPass,
"cutover-plan.rollback_plan",
"rollback window keeps PA Agent read-only while Moonfish evidence remains replayable",
)
} else {
finding(
RollbackWindowPreserved,
DecommissionBlocked,
"cutover-plan.rollback_plan",
"rollback window is not preserved",
)
}
}
///|
fn archive_manifest(input : DecommissionInput) -> Array[ArchiveItem] {
[
archive_item(MigrationPlan, "docs/pa-agent-migration-plan.md", "MoonBook"),
archive_item(
KnownDifferenceReport,
"records/parity/golden-fixture-report.json",
"MoonBook",
),
archive_item(
ImportedDataManifest,
input.cutover_plan.imported_data_manifest_path,
"MoonBook",
),
archive_item(
HandoffRunbook,
"\{input.archive_root}/operator-handoff.json",
"MoonBook",
),
archive_item(
RollbackReport,
"records/evaluations/rollback-report.json",
"MoonBook",
),
archive_item(LegacySourceReference, input.legacy_app_ref, "PA Agent"),
]
}
///|
fn archive_finding(
items : Array[ArchiveItem],
root : String,
) -> DecommissionFinding {
if root != "" && items.all(fn(item) { item.retained && item.path != "" }) {
finding(
ArchiveManifestReady,
DecommissionPass,
root,
"decommission archive manifest retains migration, parity, import, handoff, rollback, and legacy references",
)
} else {
finding(
ArchiveManifestReady,
DecommissionBlocked,
root,
"decommission archive manifest is incomplete",
)
}
}
///|
fn count_status(
findings : Array[DecommissionFinding],
status : DecommissionStatus,
) -> Int {
findings.fold(init=0, fn(count, item) {
if item.status == status {
count + 1
} else {
count
}
})
}
///|
pub fn prepare_decommission_report(
input : DecommissionInput,
) -> DecommissionReport {
let archive_items = archive_manifest(input)
let findings = [
primary_workflow_finding(input),
legacy_read_only_finding(input),
imported_records_finding(input),
known_difference_finding(input),
rollback_window_finding(input),
archive_finding(archive_items, input.archive_root),
]
let blocked_count = count_status(findings, DecommissionBlocked)
let pass_count = count_status(findings, DecommissionPass)
let ready = blocked_count == 0
{
ready,
legacy_app_ref: input.legacy_app_ref,
archive_root: input.archive_root,
pass_count,
blocked_count,
findings,
archive_items,
final_note: if ready {
"PA Agent can remain read-only for rollback and audit while Moonfish is the primary price-action workflow."
} else {
"PA Agent decommission is blocked until archive, import, handoff, and rollback evidence pass."
},
summary: if ready {
"decommission report ready with \{pass_count}/\{findings.length()} gate(s) and \{archive_items.length()} archive item(s)"
} else {
"decommission report blocked by \{blocked_count} gate(s)"
},
}
}