use crate::events::{Event, EventKind};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
const RUBBER_STAMP_THRESHOLD_MS: u64 = 10_000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TracedDefect {
pub ticket: String,
pub mission_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutonomyMetric {
pub closed_missions: u64,
pub zero_intervention_missions: u64,
pub zero_intervention_share: Option<f64>,
pub completed: AutonomyOutcomeSplit,
pub failed: AutonomyOutcomeSplit,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutonomyOutcomeSplit {
pub missions: u64,
pub zero_intervention: u64,
pub zero_intervention_share: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RubberStamp {
pub decided_grants: u64,
pub p50_ms: Option<u64>,
pub p90_ms: Option<u64>,
pub under_ten_seconds: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FalseGreenSplit {
pub completed_missions: u64,
pub false_greens: u64,
pub rate: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FalseGreens {
pub completed_missions: u64,
pub false_greens: u64,
pub false_green_rate: Option<f64>,
pub with_interventions: FalseGreenSplit,
pub zero_intervention: FalseGreenSplit,
pub traced_defects: Vec<TracedDefect>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LedgerKind {
Grant,
Steer,
}
impl LedgerKind {
pub fn as_str(&self) -> &'static str {
match self {
Self::Grant => "grant",
Self::Steer => "steer",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LedgerRow {
pub ts: DateTime<Utc>,
pub mission_id: String,
pub kind: LedgerKind,
pub milestone_id: Option<String>,
pub ask: String,
pub decision: String,
pub latency_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EscalationMetrics {
pub autonomy: AutonomyMetric,
pub rubber_stamp: RubberStamp,
pub false_greens: FalseGreens,
pub ledger: Vec<LedgerRow>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TerminalOutcome {
Completed,
Failed,
Abandoned,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MissionEscalation {
terminal: Option<TerminalOutcome>,
interventions: u64,
latencies_ms: Vec<u64>,
ledger: Vec<LedgerRow>,
}
pub(crate) fn grant_kind_str(kind: &crate::types::GrantKind) -> &'static str {
match kind {
crate::types::GrantKind::Command => "command",
crate::types::GrantKind::TouchPath => "touch-path",
crate::types::GrantKind::WorkerDeny => "worker-deny",
crate::types::GrantKind::Egress => "egress",
}
}
pub(crate) fn is_engine_lift(reason: &str, context: Option<&crate::types::BlockContext>) -> bool {
match context {
Some(context) => context.is_workspace_gate(),
None => reason == crate::workspace_gate::GATE_LIFT_REASON,
}
}
pub(crate) fn pair_grant_decisions(mission_events: &[&Event]) -> Vec<(usize, Option<usize>)> {
let mut used_decisions = vec![false; mission_events.len()];
let mut pairs = Vec::new();
for (req_idx, req) in mission_events.iter().enumerate() {
let EventKind::GrantRequested { command, .. } = &req.kind else {
continue;
};
let mut matched: Option<usize> = None;
for (i, cand) in mission_events.iter().enumerate() {
if i <= req_idx || used_decisions[i] {
continue;
}
let cand_command = match &cand.kind {
EventKind::GrantApproved { command, .. }
| EventKind::GrantDenied { command, .. } => command,
_ => continue,
};
if cand_command == command {
matched = Some(i);
break;
}
}
if matched.is_none() {
for (i, cand) in mission_events.iter().enumerate() {
if i <= req_idx || used_decisions[i] {
continue;
}
if matches!(
cand.kind,
EventKind::GrantApproved { .. } | EventKind::GrantDenied { .. }
) {
matched = Some(i);
break;
}
}
}
if let Some(i) = matched {
used_decisions[i] = true;
}
pairs.push((req_idx, matched));
}
pairs
}
pub fn mission_escalation(mission_id: &str, events: &[Event]) -> MissionEscalation {
let mission_events: Vec<&Event> = events
.iter()
.filter(|e| e.mission_id == mission_id)
.collect();
let terminal = mission_events.iter().find_map(|e| match &e.kind {
EventKind::MissionCompleted {} => Some(TerminalOutcome::Completed),
EventKind::MissionFailed { .. } => Some(TerminalOutcome::Failed),
EventKind::MissionAbandoned { .. } => Some(TerminalOutcome::Abandoned),
_ => None,
});
let plan_approved_seq = mission_events
.iter()
.find(|e| matches!(e.kind, EventKind::PlanApproved { .. }))
.map(|e| e.seq);
let mut interventions: u64 = 0;
let mut ledger = Vec::new();
for e in &mission_events {
match &e.kind {
EventKind::UserMessage { text, .. } => {
if let Some(approved_seq) = plan_approved_seq {
if e.seq >= approved_seq {
interventions += 1;
ledger.push(LedgerRow {
ts: e.ts,
mission_id: mission_id.to_string(),
kind: LedgerKind::Steer,
milestone_id: None,
ask: text.clone(),
decision: "steered".to_string(),
latency_ms: None,
});
}
}
}
EventKind::GrantApproved { .. }
| EventKind::GrantDenied { .. }
| EventKind::PlanRevised { .. }
| EventKind::PlanRevisionRejected { .. } => {
interventions += 1;
}
EventKind::MilestoneUnblocked {
reason,
block_context,
..
} if !is_engine_lift(reason, block_context.as_ref()) => {
interventions += 1;
}
_ => {}
}
}
let mut latencies_ms = Vec::new();
for (req_idx, matched) in pair_grant_decisions(&mission_events) {
let req = mission_events[req_idx];
let EventKind::GrantRequested {
milestone_id,
kind,
command,
} = &req.kind
else {
unreachable!("pair_grant_decisions only returns grant.requested indices")
};
let (decision, latency_ms) = match matched {
Some(i) => {
let decided = mission_events[i];
let latency = (decided.ts - req.ts).num_milliseconds();
let latency_ms = if latency >= 0 {
Some(latency as u64)
} else {
None
};
if let Some(l) = latency_ms {
latencies_ms.push(l);
}
let decision = match &decided.kind {
EventKind::GrantApproved { .. } => "approved".to_string(),
EventKind::GrantDenied { reason, .. } => format!("denied: {reason}"),
_ => unreachable!(),
};
(decision, latency_ms)
}
None => ("pending".to_string(), None),
};
ledger.push(LedgerRow {
ts: req.ts,
mission_id: mission_id.to_string(),
kind: LedgerKind::Grant,
milestone_id: Some(milestone_id.clone()),
ask: format!("{}: {command}", grant_kind_str(kind)),
decision,
latency_ms,
});
}
MissionEscalation {
terminal,
interventions,
latencies_ms,
ledger,
}
}
fn percentile_nearest_rank(sorted: &[u64], p: u64) -> Option<u64> {
if sorted.is_empty() {
return None;
}
let n = sorted.len() as u64;
let rank = p.saturating_mul(n).saturating_add(99) / 100;
let rank = rank.clamp(1, n);
Some(sorted[(rank - 1) as usize])
}
pub fn aggregate(
missions: &[(String, Vec<Event>)],
traced_defects: &[TracedDefect],
) -> EscalationMetrics {
let mut closed_missions: u64 = 0;
let mut zero_intervention_missions: u64 = 0;
let mut completed_missions: u64 = 0;
let mut completed_zero_intervention: u64 = 0;
let mut failed_missions: u64 = 0;
let mut failed_zero_intervention: u64 = 0;
let mut all_latencies_ms = Vec::new();
let mut ledger = Vec::new();
let mut completed_with_interventions: Vec<&str> = Vec::new();
let mut completed_clean: Vec<&str> = Vec::new();
for (mission_id, events) in missions {
let fold = mission_escalation(mission_id, events);
match fold.terminal {
Some(TerminalOutcome::Completed) => {
closed_missions += 1;
completed_missions += 1;
if fold.interventions == 0 {
zero_intervention_missions += 1;
completed_zero_intervention += 1;
completed_clean.push(mission_id);
} else {
completed_with_interventions.push(mission_id);
}
}
Some(TerminalOutcome::Failed) => {
closed_missions += 1;
failed_missions += 1;
if fold.interventions == 0 {
zero_intervention_missions += 1;
failed_zero_intervention += 1;
}
}
Some(TerminalOutcome::Abandoned) | None => {}
}
all_latencies_ms.extend(fold.latencies_ms);
ledger.extend(fold.ledger);
}
let mut joined: Vec<TracedDefect> = Vec::new();
let mut false_green_with_interventions: u64 = 0;
let mut false_green_clean: u64 = 0;
for defect in traced_defects {
let id = defect.mission_id.as_str();
let is_completed =
completed_with_interventions.contains(&id) || completed_clean.contains(&id);
if is_completed {
joined.push(defect.clone());
}
}
let counted: std::collections::HashSet<&str> =
joined.iter().map(|d| d.mission_id.as_str()).collect();
for id in &completed_with_interventions {
if counted.contains(id) {
false_green_with_interventions += 1;
}
}
for id in &completed_clean {
if counted.contains(id) {
false_green_clean += 1;
}
}
joined.sort_by(|a, b| {
a.mission_id
.cmp(&b.mission_id)
.then_with(|| a.ticket.cmp(&b.ticket))
});
let false_greens_total = false_green_with_interventions + false_green_clean;
all_latencies_ms.sort_unstable();
let decided = all_latencies_ms.len() as u64;
let under_ten_seconds = all_latencies_ms
.iter()
.filter(|&&l| l < RUBBER_STAMP_THRESHOLD_MS)
.count() as u64;
ledger.sort_by_key(|row| std::cmp::Reverse(row.ts));
EscalationMetrics {
autonomy: AutonomyMetric {
closed_missions,
zero_intervention_missions,
zero_intervention_share: (closed_missions > 0)
.then(|| zero_intervention_missions as f64 / closed_missions as f64),
completed: AutonomyOutcomeSplit {
missions: completed_missions,
zero_intervention: completed_zero_intervention,
zero_intervention_share: (completed_missions > 0)
.then(|| completed_zero_intervention as f64 / completed_missions as f64),
},
failed: AutonomyOutcomeSplit {
missions: failed_missions,
zero_intervention: failed_zero_intervention,
zero_intervention_share: (failed_missions > 0)
.then(|| failed_zero_intervention as f64 / failed_missions as f64),
},
},
rubber_stamp: RubberStamp {
decided_grants: decided,
p50_ms: percentile_nearest_rank(&all_latencies_ms, 50),
p90_ms: percentile_nearest_rank(&all_latencies_ms, 90),
under_ten_seconds,
},
false_greens: FalseGreens {
completed_missions,
false_greens: false_greens_total,
false_green_rate: (completed_missions > 0)
.then(|| false_greens_total as f64 / completed_missions as f64),
with_interventions: FalseGreenSplit {
completed_missions: completed_with_interventions.len() as u64,
false_greens: false_green_with_interventions,
rate: (!completed_with_interventions.is_empty()).then(|| {
false_green_with_interventions as f64
/ completed_with_interventions.len() as f64
}),
},
zero_intervention: FalseGreenSplit {
completed_missions: completed_clean.len() as u64,
false_greens: false_green_clean,
rate: (!completed_clean.is_empty())
.then(|| false_green_clean as f64 / completed_clean.len() as f64),
},
traced_defects: joined,
},
ledger,
}
}
pub(crate) fn traced_defects_from_tickets(repo_root: &std::path::Path) -> Vec<TracedDefect> {
let mut out: Vec<TracedDefect> = crate::ticket::Ticket::list(repo_root)
.into_iter()
.filter_map(|ticket| {
ticket.traced_from_mission.map(|mission_id| TracedDefect {
ticket: ticket.slug,
mission_id,
})
})
.collect();
out.sort_by(|a, b| {
a.mission_id
.cmp(&b.mission_id)
.then_with(|| a.ticket.cmp(&b.ticket))
});
out
}
pub fn compute_escalation_metrics(
repo_root: &std::path::Path,
) -> anyhow::Result<EscalationMetrics> {
let index_contents = std::fs::read_to_string(
crate::paths::MissionPaths::new(repo_root, "_")
.missions_dir()
.join("index.md"),
)
.unwrap_or_default();
let mut ids = crate::paths::MissionPaths::list_missions(repo_root);
for id in crate::mission_catalog::mission_index_ids(&index_contents) {
if !ids.contains(&id) {
ids.push(id);
}
}
ids.sort();
let mut missions = Vec::new();
for id in ids {
let paths = crate::paths::MissionPaths::new(repo_root, &id);
let events_path = paths.events_file();
if !events_path.is_file() {
continue;
}
if paths.require_no_follow().is_err() {
continue;
}
let events = match crate::event_log::EventLog::read_events(&events_path) {
Ok(events) => events,
Err(_) => continue, };
missions.push((id, events));
}
Ok(aggregate(
&missions,
&traced_defects_from_tickets(repo_root),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event_log::{EventLog, LockForce};
use crate::paths::MissionPaths;
use crate::types::{GrantKind, MissionConfig, Plan};
use std::time::Duration;
use tempfile::TempDir;
fn ev(seq: u64, mission_id: &str, ts_ms: i64, kind: EventKind) -> Event {
Event {
seq,
ts: DateTime::from_timestamp_millis(ts_ms).unwrap(),
mission_id: mission_id.to_string(),
kind,
}
}
fn sample_plan() -> Plan {
Plan {
goal: "g".into(),
validation_contract: vec![],
milestones: vec![],
considered_alternatives: None,
command_grants: vec![],
touch_set: vec![],
standards_manifest: None,
reviewer_independence: None,
}
}
fn created() -> EventKind {
EventKind::MissionCreated {
goal: "g".into(),
base_branch: "main".into(),
mission_branch: "kranz/mission-x".into(),
config: MissionConfig::default(),
}
}
fn anti_vacuity_missions() -> Vec<(String, Vec<Event>)> {
let m1 = vec![
ev(1, "m-1", 0, created()),
ev(2, "m-1", 1_000, EventKind::MissionCompleted {}),
];
let m2 = vec![
ev(1, "m-2", 0, created()),
ev(
2,
"m-2",
1_000,
EventKind::PlanApproved {
plan: sample_plan(),
base_sha: None,
},
),
ev(
3,
"m-2",
10_000,
EventKind::GrantRequested {
milestone_id: "ms-1".into(),
kind: GrantKind::Command,
command: "cargo test".into(),
},
),
ev(
4,
"m-2",
15_000,
EventKind::GrantApproved {
kind: GrantKind::Command,
command: "cargo test".into(),
},
),
ev(
5,
"m-2",
20_000,
EventKind::GrantRequested {
milestone_id: "ms-1".into(),
kind: GrantKind::Egress,
command: "registry.npmjs.org:443".into(),
},
),
ev(
6,
"m-2",
920_000,
EventKind::GrantDenied {
kind: GrantKind::Egress,
command: "registry.npmjs.org:443".into(),
reason: "not needed".into(),
},
),
ev(7, "m-2", 921_000, EventKind::MissionCompleted {}),
];
let m3 = vec![
ev(1, "m-3", 0, created()),
ev(
2,
"m-3",
1_000,
EventKind::PlanApproved {
plan: sample_plan(),
base_sha: None,
},
),
ev(
3,
"m-3",
2_000,
EventKind::UserMessage {
text: "skip the flaky test".into(),
interrupt: false,
},
),
ev(
4,
"m-3",
3_000,
EventKind::MissionFailed {
reason: "honest failure".into(),
},
),
];
vec![
("m-1".to_string(), m1),
("m-2".to_string(), m2),
("m-3".to_string(), m3),
]
}
#[test]
fn escalation_metrics_autonomy_ratio_split_by_outcome() {
let metrics = aggregate(&anti_vacuity_missions(), &[]);
let autonomy = &metrics.autonomy;
assert_eq!(autonomy.closed_missions, 3);
assert_eq!(autonomy.zero_intervention_missions, 1);
assert_eq!(autonomy.zero_intervention_share, Some(1.0 / 3.0));
assert_eq!(autonomy.completed.missions, 2);
assert_eq!(autonomy.completed.zero_intervention, 1);
assert_eq!(autonomy.completed.zero_intervention_share, Some(0.5));
assert_eq!(autonomy.failed.missions, 1);
assert_eq!(autonomy.failed.zero_intervention, 0);
assert_eq!(autonomy.failed.zero_intervention_share, Some(0.0));
}
#[test]
fn escalation_metrics_rubber_stamp_percentiles_and_under_10s() {
let metrics = aggregate(&anti_vacuity_missions(), &[]);
let stamp = &metrics.rubber_stamp;
assert_eq!(stamp.decided_grants, 2);
assert_eq!(stamp.p50_ms, Some(5_000));
assert_eq!(stamp.p90_ms, Some(900_000));
assert_eq!(stamp.under_ten_seconds, 1);
}
#[test]
fn escalation_metrics_percentiles_empty_population_is_none() {
let metrics = aggregate(&[], &[]);
assert_eq!(metrics.rubber_stamp.decided_grants, 0);
assert_eq!(metrics.rubber_stamp.p50_ms, None);
assert_eq!(metrics.rubber_stamp.p90_ms, None);
assert_eq!(metrics.autonomy.zero_intervention_share, None);
assert_eq!(metrics.false_greens.false_green_rate, None);
}
#[test]
fn escalation_metrics_false_greens_join_and_split() {
let traced = vec![
TracedDefect {
ticket: "defect-login-regression".into(),
mission_id: "m-1".into(),
},
TracedDefect {
ticket: "defect-on-a-failure".into(),
mission_id: "m-3".into(),
},
TracedDefect {
ticket: "defect-unknown-mission".into(),
mission_id: "m-999".into(),
},
];
let metrics = aggregate(&anti_vacuity_missions(), &traced);
let fg = &metrics.false_greens;
assert_eq!(fg.completed_missions, 2);
assert_eq!(fg.false_greens, 1);
assert_eq!(fg.false_green_rate, Some(0.5));
assert_eq!(fg.with_interventions.completed_missions, 1);
assert_eq!(fg.with_interventions.false_greens, 0);
assert_eq!(fg.with_interventions.rate, Some(0.0));
assert_eq!(fg.zero_intervention.completed_missions, 1);
assert_eq!(fg.zero_intervention.false_greens, 1);
assert_eq!(fg.zero_intervention.rate, Some(1.0));
assert_eq!(
fg.traced_defects,
vec![TracedDefect {
ticket: "defect-login-regression".into(),
mission_id: "m-1".into(),
}]
);
}
#[test]
fn escalation_metrics_two_defects_one_mission_count_once() {
let traced = vec![
TracedDefect {
ticket: "defect-a".into(),
mission_id: "m-1".into(),
},
TracedDefect {
ticket: "defect-b".into(),
mission_id: "m-1".into(),
},
];
let metrics = aggregate(&anti_vacuity_missions(), &traced);
assert_eq!(metrics.false_greens.false_greens, 1);
assert_eq!(metrics.false_greens.traced_defects.len(), 2);
}
#[test]
fn escalation_metrics_ledger_rows_grants_and_steers() {
let metrics = aggregate(&anti_vacuity_missions(), &[]);
assert_eq!(metrics.ledger.len(), 3);
let egress = &metrics.ledger[0];
assert_eq!(egress.kind, LedgerKind::Grant);
assert_eq!(egress.mission_id, "m-2");
assert_eq!(egress.milestone_id.as_deref(), Some("ms-1"));
assert_eq!(egress.ask, "egress: registry.npmjs.org:443");
assert_eq!(egress.decision, "denied: not needed");
assert_eq!(egress.latency_ms, Some(900_000));
let command = &metrics.ledger[1];
assert_eq!(command.kind, LedgerKind::Grant);
assert_eq!(command.ask, "command: cargo test");
assert_eq!(command.decision, "approved");
assert_eq!(command.latency_ms, Some(5_000));
let steer = &metrics.ledger[2];
assert_eq!(steer.kind, LedgerKind::Steer);
assert_eq!(steer.mission_id, "m-3");
assert_eq!(steer.milestone_id, None);
assert_eq!(steer.ask, "skip the flaky test");
assert_eq!(steer.decision, "steered");
assert_eq!(steer.latency_ms, None);
}
#[test]
fn escalation_metrics_pending_grant_rows_and_pre_approval_messages() {
let events = vec![
ev(
1,
"m-1",
0,
EventKind::UserMessage {
text: "draft note".into(),
interrupt: false,
},
),
ev(
2,
"m-1",
1_000,
EventKind::PlanApproved {
plan: sample_plan(),
base_sha: None,
},
),
ev(
3,
"m-1",
2_000,
EventKind::GrantRequested {
milestone_id: "ms-1".into(),
kind: GrantKind::TouchPath,
command: "docs/**".into(),
},
),
];
let fold = mission_escalation("m-1", &events);
assert_eq!(fold.interventions, 0);
assert!(fold.latencies_ms.is_empty());
assert_eq!(fold.ledger.len(), 1);
assert_eq!(fold.ledger[0].decision, "pending");
assert_eq!(fold.ledger[0].latency_ms, None);
assert_eq!(fold.ledger[0].ask, "touch-path: docs/**");
}
#[test]
fn escalation_metrics_operator_unblocks_count_engine_lifts_do_not() {
let events = vec![
ev(
1,
"m-1",
0,
EventKind::MilestoneBlocked {
block_context: None,
milestone_id: "ms-1".into(),
reason: "workspace gate: bootstrap failed".into(),
},
),
ev(
2,
"m-1",
1_000,
EventKind::MilestoneUnblocked {
block_context: None,
milestone_id: "ms-1".into(),
reason: crate::workspace_gate::GATE_LIFT_REASON.to_string(),
validator_guidance: None,
},
),
ev(
3,
"m-1",
2_000,
EventKind::MilestoneBlocked {
block_context: None,
milestone_id: "ms-1".into(),
reason: "fix-cycle cap".into(),
},
),
ev(
4,
"m-1",
3_000,
EventKind::MilestoneUnblocked {
block_context: None,
milestone_id: "ms-1".into(),
reason: "user skipped findings".into(),
validator_guidance: None,
},
),
];
let fold = mission_escalation("m-1", &events);
assert_eq!(fold.interventions, 1);
}
#[test]
fn escalation_metrics_revision_decisions_count_as_interventions() {
let events = vec![
ev(
1,
"m-1",
0,
EventKind::PlanRevised {
revision: 1,
plan: sample_plan(),
},
),
ev(
2,
"m-1",
1_000,
EventKind::PlanRevisionRejected {
revision: 2,
reason: "too risky".into(),
},
),
ev(3, "m-1", 2_000, EventKind::MissionCompleted {}),
];
let fold = mission_escalation("m-1", &events);
assert_eq!(fold.interventions, 2);
let metrics = aggregate(&[("m-1".to_string(), events)], &[]);
assert_eq!(metrics.autonomy.zero_intervention_missions, 0);
assert_eq!(metrics.autonomy.completed.zero_intervention, 0);
}
#[test]
fn escalation_metrics_abandoned_is_not_a_closed_mission() {
let events = vec![
ev(1, "m-1", 0, created()),
ev(
2,
"m-1",
1_000,
EventKind::MissionAbandoned {
reason: "operator retired it".into(),
},
),
];
let metrics = aggregate(&[("m-1".to_string(), events)], &[]);
assert_eq!(metrics.autonomy.closed_missions, 0);
assert_eq!(metrics.autonomy.zero_intervention_share, None);
assert_eq!(metrics.false_greens.completed_missions, 0);
}
#[test]
fn escalation_metrics_other_missions_events_are_filtered_out() {
let events = vec![
ev(1, "m-1", 0, created()),
ev(
2,
"m-2",
500,
EventKind::GrantApproved {
kind: GrantKind::Command,
command: "other".into(),
},
),
ev(3, "m-1", 1_000, EventKind::MissionCompleted {}),
];
let fold = mission_escalation("m-1", &events);
assert_eq!(fold.interventions, 0);
assert!(fold.terminal == Some(TerminalOutcome::Completed));
}
fn seed_mission(repo_root: &std::path::Path, id: &str, kinds: Vec<EventKind>) {
let paths = MissionPaths::new(repo_root, id);
let mut log = EventLog::acquire(&paths, id, Duration::ZERO, LockForce::No).unwrap();
for kind in kinds {
log.append(kind).unwrap();
}
}
fn seed_ticket(repo_root: &std::path::Path, slug: &str, frontmatter: &str) {
let dir = crate::ticket::Ticket::tickets_dir(repo_root);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join(format!("{slug}.md")),
format!("{frontmatter}\n## Goal\nfix it\n"),
)
.unwrap();
}
#[test]
fn escalation_metrics_compute_end_to_end_over_fixture_repo() {
let tmp = TempDir::new().unwrap();
seed_mission(
tmp.path(),
"m-1",
vec![created(), EventKind::MissionCompleted {}],
);
seed_mission(
tmp.path(),
"m-2",
vec![
created(),
EventKind::GrantRequested {
milestone_id: "ms-1".into(),
kind: GrantKind::Command,
command: "cargo test".into(),
},
EventKind::GrantApproved {
kind: GrantKind::Command,
command: "cargo test".into(),
},
EventKind::MissionCompleted {},
],
);
seed_ticket(
tmp.path(),
"defect-regression",
"---\ntitle: Login regression\ntraced-from-mission: m-1\n---\n",
);
seed_ticket(
tmp.path(),
"ordinary-task",
"---\ntitle: Ordinary task\n---\n",
);
let metrics = compute_escalation_metrics(tmp.path()).unwrap();
assert_eq!(metrics.autonomy.closed_missions, 2);
assert_eq!(metrics.autonomy.zero_intervention_missions, 1);
assert_eq!(metrics.false_greens.false_greens, 1);
assert_eq!(metrics.false_greens.false_green_rate, Some(0.5));
assert_eq!(
metrics.false_greens.traced_defects,
vec![TracedDefect {
ticket: "defect-regression".into(),
mission_id: "m-1".into(),
}]
);
assert_eq!(metrics.rubber_stamp.decided_grants, 1);
assert_eq!(metrics.rubber_stamp.under_ten_seconds, 1);
assert_eq!(metrics.ledger.len(), 1);
assert_eq!(metrics.ledger[0].decision, "approved");
}
}