///|
pub(all) enum TimelineEventKind {
EventPhaseStart
EventPhaseEnd
EventManualApproval
EventMetricCheckpoint
EventRollbackGuard
} derive(Eq, Debug)
///|
pub(all) struct TimelineEvent {
minute : Int
phase_name : String
kind : TimelineEventKind
mode : RolloutMode
percent_bp : Int
note : String
} derive(Eq, Debug)
///|
pub fn TimelineEvent::TimelineEvent(
minute? : Int = 0,
phase_name? : String = "",
kind? : TimelineEventKind = EventPhaseStart,
mode? : RolloutMode = Shadow,
percent_bp? : Int = 0,
note? : String = "",
) -> TimelineEvent {
{ minute, phase_name, kind, mode, percent_bp, note }
}
///|
pub fn rollout_timeline(plan : RolloutPlan) -> Array[TimelineEvent] {
let events = Array::new(capacity=plan.phases.length() * 4)
for phase in plan.phases {
events.push(
TimelineEvent(
minute=phase.start_minute,
phase_name=phase.name,
kind=EventPhaseStart,
mode=phase.mode,
percent_bp=phase.percent_bp,
note="phase starts",
),
)
if phase.require_manual_approval {
events.push(
TimelineEvent(
minute=phase.start_minute,
phase_name=phase.name,
kind=EventManualApproval,
mode=phase.mode,
percent_bp=phase.percent_bp,
note="manual approval required",
),
)
}
if phase.mode == Live || phase.mode == DryRun {
events.push(
TimelineEvent(
minute=phase.start_minute + phase.duration_minutes / 2,
phase_name=phase.name,
kind=EventMetricCheckpoint,
mode=phase.mode,
percent_bp=phase.percent_bp,
note="mid-phase metric checkpoint",
),
)
}
if phase.mode == KillSwitch {
events.push(
TimelineEvent(
minute=phase.start_minute,
phase_name=phase.name,
kind=EventRollbackGuard,
mode=phase.mode,
percent_bp=phase.percent_bp,
note="rollback guard active",
),
)
}
events.push(
TimelineEvent(
minute=phase.start_minute + phase.duration_minutes,
phase_name=phase.name,
kind=EventPhaseEnd,
mode=phase.mode,
percent_bp=phase.percent_bp,
note="phase ends",
),
)
}
events
}
///|
pub fn timeline_event_text(kind : TimelineEventKind) -> String {
match kind {
EventPhaseStart => "phase-start"
EventPhaseEnd => "phase-end"
EventManualApproval => "manual-approval"
EventMetricCheckpoint => "metric-checkpoint"
EventRollbackGuard => "rollback-guard"
}
}
///|
pub fn next_timeline_event(
events : Array[TimelineEvent],
minute : Int,
) -> TimelineEvent? {
let mut chosen : TimelineEvent? = None
let mut chosen_minute = 2147483
for event in events {
if event.minute >= minute && event.minute < chosen_minute {
chosen = Some(event)
chosen_minute = event.minute
}
}
chosen
}
///|
pub fn timeline_event_count_by_kind(
events : Array[TimelineEvent],
kind : TimelineEventKind,
) -> Int {
let mut total = 0
for event in events {
if event.kind == kind {
total += 1
}
}
total
}
///|
pub fn phase_minutes_by_mode(plan : RolloutPlan, mode : RolloutMode) -> Int {
let mut total = 0
for phase in plan.phases {
if phase.mode == mode {
total += phase.duration_minutes
}
}
total
}
///|
pub fn timeline_has_manual_gate(plan : RolloutPlan) -> Bool {
for event in rollout_timeline(plan) {
if event.kind == EventManualApproval {
return true
}
}
false
}
///|
pub fn support_handoff_minutes(
plan : RolloutPlan,
max_shift_minutes : Int,
) -> Array[Int] {
let out = Array::new(capacity=plan.phases.length())
let mut previous_end = 0
let mut initialized = false
for phase in plan.phases {
if initialized {
let gap = phase.start_minute - previous_end
if gap >= max_shift_minutes {
out.push(previous_end)
}
}
previous_end = phase.start_minute + phase.duration_minutes
initialized = true
}
out
}
///|
pub fn timeline_markdown(plan : RolloutPlan) -> String {
let events = rollout_timeline(plan)
let out = StringBuilder()
out.write_string("# CanaryRing Timeline\n\n")
out.write_string("- Plan: " + plan.name + "\n")
out.write_string(
"- Duration: " + plan_duration(plan).to_string() + " minutes\n",
)
out.write_string(
"- Shadow minutes: " +
phase_minutes_by_mode(plan, Shadow).to_string() +
"\n",
)
out.write_string(
"- Dry-run minutes: " +
phase_minutes_by_mode(plan, DryRun).to_string() +
"\n",
)
out.write_string(
"- Live minutes: " + phase_minutes_by_mode(plan, Live).to_string() + "\n\n",
)
for event in events {
out.write_string("- minute " + event.minute.to_string() + " ")
out.write_string(timeline_event_text(event.kind) + " ")
out.write_string(event.phase_name + " ")
out.write_string(mode_text(event.mode) + " ")
out.write_string(percent_text(event.percent_bp) + " ")
out.write_string(event.note + "\n")
}
out.to_string()
}
///|
pub fn timeline_json(plan : RolloutPlan) -> String {
let events = rollout_timeline(plan)
let out = StringBuilder()
out.write_string("{")
out.write_string("\"plan\":\"" + json_escape(plan.name) + "\",")
out.write_string(
"\"duration_minutes\":" + plan_duration(plan).to_string() + ",",
)
out.write_string("\"events\":[")
for i in 0.. 0 {
out.write_string(",")
}
out.write_string("{")
out.write_string("\"minute\":" + event.minute.to_string() + ",")
out.write_string("\"phase\":\"" + json_escape(event.phase_name) + "\",")
out.write_string("\"kind\":\"" + timeline_event_text(event.kind) + "\",")
out.write_string("\"mode\":\"" + mode_text(event.mode) + "\",")
out.write_string("\"percent_bp\":" + event.percent_bp.to_string())
out.write_string("}")
}
out.write_string("]}")
out.to_string()
}