Skip to main content

codetether_agent/okr/
mod.rs

1//! OKR (Objectives and Key Results) domain models
2//!
3//! This module provides first-class OKR entities for operational control:
4//! - `Okr` - An objective with measurable key results
5//! - `KeyResult` - Quantifiable outcomes tied to objectives
6//! - `OkrRun` - An execution instance of an OKR
7//! - `ApprovalDecision` - Approve/deny gate decisions
8//! - `KrOutcome` - Evidence-backed outcomes for key results
9//! - `OkrRepository` - File-based persistence with CRUD operations
10
11pub mod persistence;
12
13pub use persistence::OkrRepository;
14
15use chrono::{DateTime, Utc};
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19/// A high-level objective with associated key results
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Okr {
22    /// Unique identifier for this OKR
23    pub id: Uuid,
24
25    /// Human-readable title of the objective
26    pub title: String,
27
28    /// Detailed description of what this objective aims to achieve
29    pub description: String,
30
31    /// Current status of the OKR
32    #[serde(default)]
33    pub status: OkrStatus,
34
35    /// Key results that measure success
36    #[serde(default)]
37    pub key_results: Vec<KeyResult>,
38
39    /// Owner of this OKR (user ID or team)
40    #[serde(default)]
41    pub owner: Option<String>,
42
43    /// Tenant ID for multi-tenant isolation
44    #[serde(default)]
45    pub tenant_id: Option<String>,
46
47    /// Tags for categorization
48    #[serde(default)]
49    pub tags: Vec<String>,
50
51    /// Creation timestamp
52    #[serde(default = "utc_now")]
53    pub created_at: DateTime<Utc>,
54
55    /// Last update timestamp
56    #[serde(default = "utc_now")]
57    pub updated_at: DateTime<Utc>,
58
59    /// Target completion date
60    #[serde(default)]
61    pub target_date: Option<DateTime<Utc>>,
62}
63
64impl Okr {
65    /// Create a new OKR with a generated UUID
66    pub fn new(title: impl Into<String>, description: impl Into<String>) -> Self {
67        let now = Utc::now();
68        Self {
69            id: Uuid::new_v4(),
70            title: title.into(),
71            description: description.into(),
72            status: OkrStatus::Draft,
73            key_results: Vec::new(),
74            owner: None,
75            tenant_id: None,
76            tags: Vec::new(),
77            created_at: now,
78            updated_at: now,
79            target_date: None,
80        }
81    }
82
83    /// Validate the OKR structure
84    pub fn validate(&self) -> Result<(), OkrValidationError> {
85        if self.title.trim().is_empty() {
86            return Err(OkrValidationError::EmptyTitle);
87        }
88        if self.key_results.is_empty() {
89            return Err(OkrValidationError::NoKeyResults);
90        }
91        for kr in &self.key_results {
92            kr.validate()?;
93        }
94        Ok(())
95    }
96
97    /// Calculate overall progress (0.0 to 1.0) across all key results
98    pub fn progress(&self) -> f64 {
99        if self.key_results.is_empty() {
100            return 0.0;
101        }
102        let total: f64 = self.key_results.iter().map(|kr| kr.progress()).sum();
103        total / self.key_results.len() as f64
104    }
105
106    /// Check if all key results are complete
107    pub fn is_complete(&self) -> bool {
108        self.key_results.iter().all(|kr| kr.is_complete())
109    }
110
111    /// Add a key result to this OKR
112    pub fn add_key_result(&mut self, kr: KeyResult) {
113        self.key_results.push(kr);
114        self.updated_at = Utc::now();
115    }
116}
117
118/// OKR status enum
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121#[derive(Default)]
122pub enum OkrStatus {
123    /// Draft - not yet approved
124    #[default]
125    Draft,
126
127    /// Active - approved and in progress
128    Active,
129
130    /// Completed - all key results achieved
131    Completed,
132
133    /// Cancelled - abandoned before completion
134    Cancelled,
135
136    /// OnHold - temporarily paused
137    OnHold,
138}
139
140/// A measurable key result within an OKR
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct KeyResult {
143    /// Unique identifier for this key result
144    pub id: Uuid,
145
146    /// Parent OKR ID
147    pub okr_id: Uuid,
148
149    /// Human-readable title
150    pub title: String,
151
152    /// Detailed description
153    pub description: String,
154
155    /// Target value (numeric for percentage/count metrics)
156    pub target_value: f64,
157
158    /// Current value (progress)
159    #[serde(default)]
160    pub current_value: f64,
161
162    /// Unit of measurement (e.g., "%", "count", "files", "tests")
163    #[serde(default = "default_unit")]
164    pub unit: String,
165
166    /// Type of metric
167    #[serde(default)]
168    pub metric_type: KrMetricType,
169
170    /// Current status
171    #[serde(default)]
172    pub status: KeyResultStatus,
173
174    /// Evidence/outcomes linked to this KR
175    #[serde(default)]
176    pub outcomes: Vec<KrOutcome>,
177
178    /// Creation timestamp
179    #[serde(default = "utc_now")]
180    pub created_at: DateTime<Utc>,
181
182    /// Last update timestamp
183    #[serde(default = "utc_now")]
184    pub updated_at: DateTime<Utc>,
185}
186
187impl KeyResult {
188    /// Create a new key result
189    pub fn new(
190        okr_id: Uuid,
191        title: impl Into<String>,
192        target_value: f64,
193        unit: impl Into<String>,
194    ) -> Self {
195        let now = Utc::now();
196        Self {
197            id: Uuid::new_v4(),
198            okr_id,
199            title: title.into(),
200            description: String::new(),
201            target_value,
202            current_value: 0.0,
203            unit: unit.into(),
204            metric_type: KrMetricType::Progress,
205            status: KeyResultStatus::Pending,
206            outcomes: Vec::new(),
207            created_at: now,
208            updated_at: now,
209        }
210    }
211
212    /// Validate the key result
213    pub fn validate(&self) -> Result<(), OkrValidationError> {
214        if self.title.trim().is_empty() {
215            return Err(OkrValidationError::EmptyKeyResultTitle);
216        }
217        if self.target_value < 0.0 {
218            return Err(OkrValidationError::InvalidTargetValue);
219        }
220        Ok(())
221    }
222
223    /// Calculate progress as a ratio (0.0 to 1.0)
224    pub fn progress(&self) -> f64 {
225        if self.target_value == 0.0 {
226            return 0.0;
227        }
228        (self.current_value / self.target_value).clamp(0.0, 1.0)
229    }
230
231    /// Check if the key result is complete
232    pub fn is_complete(&self) -> bool {
233        self.status == KeyResultStatus::Completed || self.current_value >= self.target_value
234    }
235
236    /// Add an outcome to this key result
237    pub fn add_outcome(&mut self, outcome: KrOutcome) {
238        self.outcomes.push(outcome);
239        self.updated_at = Utc::now();
240    }
241
242    /// Update current value and recalculate status
243    pub fn update_progress(&mut self, value: f64) {
244        self.current_value = value;
245        self.updated_at = Utc::now();
246        if self.is_complete() {
247            self.status = KeyResultStatus::Completed;
248        } else if self.current_value > 0.0 {
249            self.status = KeyResultStatus::InProgress;
250        }
251    }
252}
253
254/// Type of metric for a key result
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257#[derive(Default)]
258pub enum KrMetricType {
259    /// Progress percentage (0-100)
260    #[default]
261    Progress,
262
263    /// Count of items
264    Count,
265
266    /// Boolean completion
267    Binary,
268
269    /// Latency/duration (lower is better)
270    Latency,
271
272    /// Quality score (0-1 or 0-100)
273    Quality,
274}
275
276/// Status of a key result
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278#[serde(rename_all = "snake_case")]
279#[derive(Default)]
280pub enum KeyResultStatus {
281    /// Not yet started
282    #[default]
283    Pending,
284
285    /// Actively being worked on
286    InProgress,
287
288    /// Achieved the target
289    Completed,
290
291    /// At risk of missing target
292    AtRisk,
293
294    /// Failed to achieve target
295    Failed,
296}
297
298/// An execution run of an OKR (multiple runs per OKR are allowed)
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct OkrRun {
301    /// Unique identifier for this run
302    pub id: Uuid,
303
304    /// Parent OKR ID
305    pub okr_id: Uuid,
306
307    /// Human-readable name for this run
308    pub name: String,
309
310    /// Current status of the run
311    #[serde(default)]
312    pub status: OkrRunStatus,
313
314    /// Correlation ID linking to relay/session
315    #[serde(default)]
316    pub correlation_id: Option<String>,
317
318    /// Relay checkpoint ID for resume capability
319    #[serde(default)]
320    pub relay_checkpoint_id: Option<String>,
321
322    /// Session ID if applicable
323    #[serde(default)]
324    pub session_id: Option<String>,
325
326    /// Progress per key result (kr_id -> progress)
327    #[serde(default)]
328    pub kr_progress: std::collections::HashMap<String, f64>,
329
330    /// Approval decision for this run
331    #[serde(default)]
332    pub approval: Option<ApprovalDecision>,
333
334    /// List of outcomes achieved in this run
335    #[serde(default)]
336    pub outcomes: Vec<KrOutcome>,
337
338    /// Iteration count for this run
339    #[serde(default)]
340    pub iterations: u32,
341
342    /// Started timestamp
343    #[serde(default = "utc_now")]
344    pub started_at: DateTime<Utc>,
345
346    /// Completed timestamp (if finished)
347    #[serde(default)]
348    pub completed_at: Option<DateTime<Utc>>,
349
350    /// Last update timestamp
351    #[serde(default = "utc_now")]
352    pub updated_at: DateTime<Utc>,
353}
354
355impl OkrRun {
356    /// Create a new OKR run
357    pub fn new(okr_id: Uuid, name: impl Into<String>) -> Self {
358        let now = Utc::now();
359        Self {
360            id: Uuid::new_v4(),
361            okr_id,
362            name: name.into(),
363            status: OkrRunStatus::Draft,
364            correlation_id: None,
365            relay_checkpoint_id: None,
366            session_id: None,
367            kr_progress: std::collections::HashMap::new(),
368            approval: None,
369            outcomes: Vec::new(),
370            iterations: 0,
371            started_at: now,
372            completed_at: None,
373            updated_at: now,
374        }
375    }
376
377    /// Validate the run
378    pub fn validate(&self) -> Result<(), OkrValidationError> {
379        if self.name.trim().is_empty() {
380            return Err(OkrValidationError::EmptyRunName);
381        }
382        Ok(())
383    }
384
385    /// Submit for approval
386    pub fn submit_for_approval(&mut self) -> Result<(), OkrValidationError> {
387        if self.status != OkrRunStatus::Draft {
388            return Err(OkrValidationError::InvalidStatusTransition);
389        }
390        self.status = OkrRunStatus::PendingApproval;
391        self.updated_at = Utc::now();
392        Ok(())
393    }
394
395    /// Record an approval decision
396    pub fn record_decision(&mut self, decision: ApprovalDecision) {
397        self.approval = Some(decision.clone());
398        self.updated_at = Utc::now();
399        match decision.decision {
400            ApprovalChoice::Approved => {
401                self.status = OkrRunStatus::Approved;
402            }
403            ApprovalChoice::Denied => {
404                self.status = OkrRunStatus::Denied;
405            }
406        }
407    }
408
409    /// Start execution
410    pub fn start(&mut self) -> Result<(), OkrValidationError> {
411        if self.status != OkrRunStatus::Approved {
412            return Err(OkrValidationError::NotApproved);
413        }
414        self.status = OkrRunStatus::Running;
415        self.updated_at = Utc::now();
416        Ok(())
417    }
418
419    /// Mark as complete
420    pub fn complete(&mut self) {
421        self.status = OkrRunStatus::Completed;
422        self.completed_at = Some(Utc::now());
423        self.updated_at = Utc::now();
424    }
425
426    /// Update key result progress
427    pub fn update_kr_progress(&mut self, kr_id: &str, progress: f64) {
428        self.kr_progress.insert(kr_id.to_string(), progress);
429        self.updated_at = Utc::now();
430    }
431
432    /// Check if run can be resumed
433    pub fn is_resumable(&self) -> bool {
434        matches!(
435            self.status,
436            OkrRunStatus::Running | OkrRunStatus::Paused | OkrRunStatus::WaitingApproval
437        )
438    }
439}
440
441/// Status of an OKR run
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
443#[serde(rename_all = "snake_case")]
444#[derive(Default)]
445pub enum OkrRunStatus {
446    /// Draft - not yet submitted
447    #[default]
448    Draft,
449
450    /// Pending approval
451    PendingApproval,
452
453    /// Approved to run
454    Approved,
455
456    /// Actively running
457    Running,
458
459    /// Paused (can resume)
460    Paused,
461
462    /// Waiting for something
463    WaitingApproval,
464
465    /// Successfully completed
466    Completed,
467
468    /// Failed to complete
469    Failed,
470
471    /// Denied approval
472    Denied,
473
474    /// Cancelled
475    Cancelled,
476}
477
478/// Approval decision for an OKR run
479#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct ApprovalDecision {
481    /// Unique identifier for this decision
482    pub id: Uuid,
483
484    /// ID of the run this decision applies to
485    pub run_id: Uuid,
486
487    /// The actual decision
488    pub decision: ApprovalChoice,
489
490    /// Reason for the decision
491    #[serde(default)]
492    pub reason: String,
493
494    /// Who made the decision
495    #[serde(default)]
496    pub approver: Option<String>,
497
498    /// Additional context/metadata
499    #[serde(default)]
500    pub metadata: std::collections::HashMap<String, String>,
501
502    /// Timestamp of the decision
503    #[serde(default = "utc_now")]
504    pub decided_at: DateTime<Utc>,
505}
506
507impl ApprovalDecision {
508    /// Create an approval
509    pub fn approve(run_id: Uuid, reason: impl Into<String>) -> Self {
510        Self {
511            id: Uuid::new_v4(),
512            run_id,
513            decision: ApprovalChoice::Approved,
514            reason: reason.into(),
515            approver: None,
516            metadata: std::collections::HashMap::new(),
517            decided_at: Utc::now(),
518        }
519    }
520
521    /// Create a denial
522    pub fn deny(run_id: Uuid, reason: impl Into<String>) -> Self {
523        Self {
524            id: Uuid::new_v4(),
525            run_id,
526            decision: ApprovalChoice::Denied,
527            reason: reason.into(),
528            approver: None,
529            metadata: std::collections::HashMap::new(),
530            decided_at: Utc::now(),
531        }
532    }
533}
534
535/// Approval choice enum
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
537#[serde(rename_all = "snake_case")]
538pub enum ApprovalChoice {
539    /// Approved to proceed
540    Approved,
541
542    /// Denied
543    Denied,
544}
545
546/// Evidence-backed outcome for a key result
547#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct KrOutcome {
549    /// Unique identifier for this outcome
550    pub id: Uuid,
551
552    /// Key result this outcome belongs to
553    pub kr_id: Uuid,
554
555    /// OKR run this outcome is part of
556    #[serde(default)]
557    pub run_id: Option<Uuid>,
558
559    /// Description of the outcome/evidence
560    pub description: String,
561
562    /// Type of outcome
563    #[serde(default)]
564    pub outcome_type: KrOutcomeType,
565
566    /// Numeric value contributed (if applicable)
567    #[serde(default)]
568    pub value: Option<f64>,
569
570    /// Evidence links (URLs, file paths, etc.)
571    #[serde(default)]
572    pub evidence: Vec<String>,
573
574    /// Who/what generated this outcome
575    #[serde(default)]
576    pub source: String,
577
578    /// Timestamp
579    #[serde(default = "utc_now")]
580    pub created_at: DateTime<Utc>,
581}
582
583impl KrOutcome {
584    /// Create a new outcome
585    pub fn new(kr_id: Uuid, description: impl Into<String>) -> Self {
586        Self {
587            id: Uuid::new_v4(),
588            kr_id,
589            run_id: None,
590            description: description.into(),
591            outcome_type: KrOutcomeType::Evidence,
592            value: None,
593            evidence: Vec::new(),
594            source: String::new(),
595            created_at: Utc::now(),
596        }
597    }
598
599    /// Create a metric outcome with a value
600    pub fn with_value(mut self, value: f64) -> Self {
601        self.value = Some(value);
602        self
603    }
604
605    /// Add evidence link
606    #[allow(dead_code)]
607    pub fn add_evidence(mut self, evidence: impl Into<String>) -> Self {
608        self.evidence.push(evidence.into());
609        self
610    }
611}
612
613/// Type of key result outcome
614#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(rename_all = "snake_case")]
616#[derive(Default)]
617pub enum KrOutcomeType {
618    /// Evidence/documentation
619    #[default]
620    Evidence,
621
622    /// Test pass
623    TestPass,
624
625    /// Code change
626    CodeChange,
627
628    /// Bug fix
629    BugFix,
630
631    /// Feature delivered
632    FeatureDelivered,
633
634    /// Metric achievement
635    MetricAchievement,
636
637    /// Review passed
638    ReviewPassed,
639
640    /// Deployment
641    Deployment,
642}
643
644/// Validation errors for OKR entities
645#[derive(Debug, Clone, Serialize, Deserialize)]
646#[serde(rename_all = "snake_case")]
647pub enum OkrValidationError {
648    /// Objective title is empty
649    EmptyTitle,
650
651    /// No key results defined
652    NoKeyResults,
653
654    /// Key result title is empty
655    EmptyKeyResultTitle,
656
657    /// Target value is invalid
658    InvalidTargetValue,
659
660    /// Run name is empty
661    EmptyRunName,
662
663    /// Invalid status transition
664    InvalidStatusTransition,
665
666    /// Run not approved
667    NotApproved,
668}
669
670impl std::fmt::Display for OkrValidationError {
671    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
672        match self {
673            OkrValidationError::EmptyTitle => write!(f, "objective title cannot be empty"),
674            OkrValidationError::NoKeyResults => write!(f, "at least one key result is required"),
675            OkrValidationError::EmptyKeyResultTitle => {
676                write!(f, "key result title cannot be empty")
677            }
678            OkrValidationError::InvalidTargetValue => {
679                write!(f, "target value must be non-negative")
680            }
681            OkrValidationError::EmptyRunName => write!(f, "run name cannot be empty"),
682            OkrValidationError::InvalidStatusTransition => {
683                write!(f, "invalid status transition")
684            }
685            OkrValidationError::NotApproved => write!(f, "run must be approved before starting"),
686        }
687    }
688}
689
690impl std::error::Error for OkrValidationError {}
691
692/// Helper to get current UTC time
693fn utc_now() -> DateTime<Utc> {
694    Utc::now()
695}
696
697/// Default unit value
698fn default_unit() -> String {
699    "%".to_string()
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    #[test]
707    fn test_okr_creation() {
708        let okr = Okr::new("Test Objective", "Description");
709        assert_eq!(okr.title, "Test Objective");
710        assert_eq!(okr.status, OkrStatus::Draft);
711        assert!(okr.validate().is_err()); // No key results
712    }
713
714    #[test]
715    fn test_okr_with_key_results() {
716        let mut okr = Okr::new("Test Objective", "Description");
717        let kr = KeyResult::new(okr.id, "KR1", 100.0, "%");
718        okr.add_key_result(kr);
719        assert!(okr.validate().is_ok());
720    }
721
722    #[test]
723    fn test_key_result_progress() {
724        let kr = KeyResult::new(Uuid::new_v4(), "Test KR", 100.0, "%");
725        assert_eq!(kr.progress(), 0.0);
726
727        let mut kr = kr;
728        kr.update_progress(50.0);
729        assert!((kr.progress() - 0.5).abs() < 0.001);
730    }
731
732    #[test]
733    fn test_okr_run_workflow() {
734        let okr_id = Uuid::new_v4();
735        let mut run = OkrRun::new(okr_id, "Q1 2024 Run");
736
737        // Submit for approval
738        run.submit_for_approval().unwrap();
739        assert_eq!(run.status, OkrRunStatus::PendingApproval);
740
741        // Record approval
742        run.record_decision(ApprovalDecision::approve(run.id, "Looks good"));
743        assert_eq!(run.status, OkrRunStatus::Approved);
744
745        // Start execution
746        run.start().unwrap();
747        assert_eq!(run.status, OkrRunStatus::Running);
748
749        // Update progress
750        run.update_kr_progress("kr-1", 0.5);
751
752        // Complete
753        run.complete();
754        assert_eq!(run.status, OkrRunStatus::Completed);
755    }
756
757    #[test]
758    fn test_outcome_creation() {
759        let outcome = KrOutcome::new(Uuid::new_v4(), "Fixed bug in auth")
760            .with_value(1.0)
761            .add_evidence("commit:abc123");
762
763        assert_eq!(outcome.value, Some(1.0));
764        assert!(outcome.evidence.contains(&"commit:abc123".to_string()));
765    }
766}