Skip to main content

kaizen/retro/
types.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Pure data for the retro engine (`Report`, `Bet`, `Inputs`).
3
4use crate::core::event::{Event, SessionRecord};
5use crate::feedback::types::FeedbackRecord;
6use crate::metrics::types::{FileFact, ToolSpanView};
7use crate::store::{SessionOutcomeRow, SessionSampleAgg};
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10
11/// Workspace-local facts assembled once at the IO boundary.
12#[derive(Debug, Clone)]
13pub struct Inputs {
14    pub window_start_ms: u64,
15    pub window_end_ms: u64,
16    /// Joined rows time-ordered.
17    pub events: Vec<(SessionRecord, Event)>,
18    pub files_touched: Vec<(String, String)>,
19    pub skills_used: Vec<(String, String)>,
20    pub tool_spans: Vec<ToolSpanView>,
21    /// Skills referenced in the last `usage_lookback_ms` window (for H1).
22    pub skills_used_recent_slugs: HashSet<String>,
23    pub usage_lookback_ms: u64,
24    pub skill_files_on_disk: Vec<SkillFileOnDisk>,
25    /// `.cursor/rules/*.mdc` stems (same shape as [`SkillFileOnDisk`]).
26    pub rule_files_on_disk: Vec<SkillFileOnDisk>,
27    pub rules_used_recent_slugs: HashSet<String>,
28    pub file_facts: HashMap<String, FileFact>,
29    pub aggregates: RetroAggregates,
30    /// LLM-as-Judge eval scores for sessions in the window: (session_id, score 0..1).
31    pub eval_scores: Vec<(String, f64)>,
32    /// Sessions with a recorded prompt fingerprint: (session_id, fingerprint).
33    pub prompt_fingerprints: Vec<(String, String)>,
34    /// Human feedback records in the window.
35    pub feedback: Vec<FeedbackRecord>,
36    /// Measured test/lint outcomes (Tier C) for sessions in the window.
37    pub session_outcomes: Vec<SessionOutcomeRow>,
38    /// Aggregated process samples (Tier D) for sessions in the window.
39    pub session_sample_aggs: Vec<SessionSampleAgg>,
40}
41
42#[derive(Debug, Clone)]
43pub struct SkillFileOnDisk {
44    pub slug: String,
45    /// Bytes of frontmatter + body (rough token proxy).
46    pub size_bytes: u64,
47    pub mtime_ms: u64,
48}
49
50#[derive(Debug, Clone, Default)]
51pub struct RetroAggregates {
52    pub unique_session_ids: HashSet<String>,
53    pub tool_event_counts: HashMap<String, u64>,
54    pub tool_cost_usd_e6: HashMap<String, i64>,
55    pub model_session_counts: HashMap<String, u64>,
56    pub total_cost_usd_e6: i64,
57    pub span_tree_stats: Option<SpanTreeStats>,
58}
59
60#[derive(Debug, Clone)]
61pub struct SpanTreeStats {
62    pub max_depth: u32,
63    pub max_fan_out: u32,
64    pub deepest_span_id: String,
65}
66
67/// Strength of evidence behind a bet.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum Confidence {
71    High,
72    Medium,
73    Low,
74}
75
76impl Confidence {
77    pub fn weight(self) -> f64 {
78        match self {
79            Self::High => 1.0,
80            Self::Medium => 0.6,
81            Self::Low => 0.3,
82        }
83    }
84
85    pub fn label(self) -> &'static str {
86        match self {
87            Self::High => "High",
88            Self::Medium => "Medium",
89            Self::Low => "Low",
90        }
91    }
92}
93
94/// Action shape for grouping retro output.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "snake_case")]
97pub enum BetCategory {
98    QuickWin,
99    Investigation,
100    Hygiene,
101}
102
103impl BetCategory {
104    pub fn label(self) -> &'static str {
105        match self {
106            Self::QuickWin => "quick_win",
107            Self::Investigation => "investigation",
108            Self::Hygiene => "hygiene",
109        }
110    }
111}
112
113/// One ranked improvement bet.
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
115pub struct Bet {
116    /// Stable id for dedup (`H2:foo.rs|bar.rs`).
117    pub id: String,
118    pub heuristic_id: String,
119    pub title: String,
120    pub hypothesis: String,
121    pub expected_tokens_saved_per_week: f64,
122    pub effort_minutes: u32,
123    pub evidence: Vec<String>,
124    pub apply_step: String,
125    #[serde(default)]
126    pub evidence_recency_ms: u64,
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub confidence: Option<Confidence>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub category: Option<BetCategory>,
131}
132
133impl Bet {
134    pub fn score(&self) -> f64 {
135        let weight = self.confidence.map_or(1.0, Confidence::weight);
136        weight * self.expected_tokens_saved_per_week / (self.effort_minutes as f64 + 1.0)
137    }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
141pub struct RetroMeta {
142    pub week_label: String,
143    pub span_start_ms: u64,
144    pub span_end_ms: u64,
145    pub session_count: u64,
146    pub total_cost_usd_e6: i64,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150pub struct RetroStats {
151    pub sessions: u64,
152    pub total_cost_usd_e6: i64,
153    pub top_model: Option<String>,
154    pub top_model_pct: Option<u64>,
155    pub top_tool: Option<String>,
156    pub top_tool_pct: Option<u64>,
157    pub median_session_minutes: Option<u64>,
158}
159
160/// JSON + markdown source of truth for CLI `--json` and reports.
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
162pub struct Report {
163    pub meta: RetroMeta,
164    pub top_bets: Vec<Bet>,
165    pub skipped_deduped: Vec<String>,
166    pub stats: RetroStats,
167}