///|
/// Lifecycle states for a continuity exercise.
pub(all) enum DrillStatus {
Planned
Running
Completed
}
///|
/// Actions captured in a drill audit timeline.
pub(all) enum AuditAction {
DrillStarted
StepStarted
StepApproved
StepCompleted
DrillFinished
}
///|
/// One immutable entry in a drill's ordered audit timeline.
pub(all) struct AuditEvent {
sequence : Int
timestamp_minutes : Int
actor : String
action : AuditAction
step_id : String?
}
///|
/// Creates a validated audit event.
pub fn AuditEvent::new(
sequence~ : Int,
timestamp_minutes~ : Int,
actor~ : String,
action~ : AuditAction,
step_id~ : String?,
) -> AuditEvent raise ModelError {
if sequence < 1 {
raise ModelError::DrillValidation("audit sequence must be positive")
}
if timestamp_minutes < 0 {
raise ModelError::InvalidDuration(
"audit.timestamp_minutes must be non-negative",
)
}
if actor == "" {
raise ModelError::EmptyField("drill.actor")
}
match step_id {
Some(value) =>
if value == "" {
raise ModelError::EmptyField("drill.step_id")
}
None => ()
}
{ sequence, timestamp_minutes, actor, action, step_id, }
}
///|
/// A recovery playbook execution with a reproducible audit timeline.
pub(all) struct RecoveryDrill {
id : String
playbook : RecoveryPlaybook
status : DrillStatus
timeline : Array[AuditEvent]
}
///|
/// Creates a planned drill with an empty audit timeline.
pub fn RecoveryDrill::new(
id~ : String,
playbook~ : RecoveryPlaybook,
) -> RecoveryDrill raise ModelError {
if id == "" {
raise ModelError::EmptyField("drill.id")
}
{ id, playbook, status: Planned, timeline: [], }
}
///|
fn RecoveryDrill::append_event(
self : RecoveryDrill,
playbook~ : RecoveryPlaybook,
status~ : DrillStatus,
actor~ : String,
timestamp_minutes~ : Int,
action~ : AuditAction,
step_id~ : String?,
) -> RecoveryDrill raise ModelError {
if self.timeline.length() > 0 {
let previous = self.timeline[self.timeline.length() - 1]
if timestamp_minutes < previous.timestamp_minutes {
raise ModelError::DrillValidation(
"audit timestamp must not move backwards",
)
}
}
let event = AuditEvent::new(
sequence=self.timeline.length() + 1,
timestamp_minutes~,
actor~,
action~,
step_id~,
)
let timeline : Array[AuditEvent] = []
for existing in self.timeline {
timeline.push(existing)
}
timeline.push(event)
{ id: self.id, playbook, status, timeline, }
}
///|
/// Returns a copy of the ordered audit entries for reporting.
pub fn RecoveryDrill::audit_timeline(self : RecoveryDrill) -> Array[AuditEvent] {
let timeline : Array[AuditEvent] = []
for event in self.timeline {
timeline.push(event)
}
timeline
}
///|
/// Starts a planned drill and records the operator and logical timestamp.
pub fn RecoveryDrill::start(
self : RecoveryDrill,
actor~ : String,
timestamp_minutes~ : Int,
) -> RecoveryDrill raise ModelError {
match self.status {
Planned =>
self.append_event(
playbook=self.playbook,
status=Running,
actor~,
timestamp_minutes~,
action=DrillStarted,
step_id=None,
)
Running =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} is already running",
)
Completed =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} has already completed",
)
}
}
///|
/// Begins one playbook step and records the step start.
pub fn RecoveryDrill::begin_step(
self : RecoveryDrill,
step_id~ : String,
actor~ : String,
timestamp_minutes~ : Int,
) -> RecoveryDrill raise ModelError {
match self.status {
Running => {
let playbook = self.playbook.begin_step(step_id~)
self.append_event(
playbook~,
status=self.status,
actor~,
timestamp_minutes~,
action=StepStarted,
step_id=Some(step_id),
)
}
Planned =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} has not started",
)
Completed =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} has already completed",
)
}
}
///|
/// Approves a step waiting at its manual approval gate.
pub fn RecoveryDrill::approve_step(
self : RecoveryDrill,
step_id~ : String,
actor~ : String,
timestamp_minutes~ : Int,
) -> RecoveryDrill raise ModelError {
match self.status {
Running => {
let playbook = self.playbook.approve_step(step_id~)
self.append_event(
playbook~,
status=self.status,
actor~,
timestamp_minutes~,
action=StepApproved,
step_id=Some(step_id),
)
}
Planned =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} has not started",
)
Completed =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} has already completed",
)
}
}
///|
/// Completes one in-progress playbook step and records the result.
pub fn RecoveryDrill::complete_step(
self : RecoveryDrill,
step_id~ : String,
actor~ : String,
timestamp_minutes~ : Int,
) -> RecoveryDrill raise ModelError {
match self.status {
Running => {
let playbook = self.playbook.complete_step(step_id~)
self.append_event(
playbook~,
status=self.status,
actor~,
timestamp_minutes~,
action=StepCompleted,
step_id=Some(step_id),
)
}
Planned =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} has not started",
)
Completed =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} has already completed",
)
}
}
///|
/// Finishes a drill after every playbook step has succeeded.
pub fn RecoveryDrill::finish(
self : RecoveryDrill,
actor~ : String,
timestamp_minutes~ : Int,
) -> RecoveryDrill raise ModelError {
match self.status {
Running =>
match self.playbook.status {
PlaybookStatus::Completed =>
self.append_event(
playbook=self.playbook,
status=Completed,
actor~,
timestamp_minutes~,
action=DrillFinished,
step_id=None,
)
_ =>
raise ModelError::DrillValidation(
"cannot finish drill before all playbook steps succeed",
)
}
Planned =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} has not started",
)
Completed =>
raise ModelError::InvalidDrillTransition(
"drill \{self.id} has already completed",
)
}
}