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