1pub mod persistence;
12
13pub use persistence::OkrRepository;
14
15use chrono::{DateTime, Utc};
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Okr {
22 pub id: Uuid,
24
25 pub title: String,
27
28 pub description: String,
30
31 #[serde(default)]
33 pub status: OkrStatus,
34
35 #[serde(default)]
37 pub key_results: Vec<KeyResult>,
38
39 #[serde(default)]
41 pub owner: Option<String>,
42
43 #[serde(default)]
45 pub tenant_id: Option<String>,
46
47 #[serde(default)]
49 pub tags: Vec<String>,
50
51 #[serde(default = "utc_now")]
53 pub created_at: DateTime<Utc>,
54
55 #[serde(default = "utc_now")]
57 pub updated_at: DateTime<Utc>,
58
59 #[serde(default)]
61 pub target_date: Option<DateTime<Utc>>,
62}
63
64impl Okr {
65 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 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 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 pub fn is_complete(&self) -> bool {
108 self.key_results.iter().all(|kr| kr.is_complete())
109 }
110
111 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121#[derive(Default)]
122pub enum OkrStatus {
123 #[default]
125 Draft,
126
127 Active,
129
130 Completed,
132
133 Cancelled,
135
136 OnHold,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct KeyResult {
143 pub id: Uuid,
145
146 pub okr_id: Uuid,
148
149 pub title: String,
151
152 pub description: String,
154
155 pub target_value: f64,
157
158 #[serde(default)]
160 pub current_value: f64,
161
162 #[serde(default = "default_unit")]
164 pub unit: String,
165
166 #[serde(default)]
168 pub metric_type: KrMetricType,
169
170 #[serde(default)]
172 pub status: KeyResultStatus,
173
174 #[serde(default)]
176 pub outcomes: Vec<KrOutcome>,
177
178 #[serde(default = "utc_now")]
180 pub created_at: DateTime<Utc>,
181
182 #[serde(default = "utc_now")]
184 pub updated_at: DateTime<Utc>,
185}
186
187impl KeyResult {
188 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 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 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 pub fn is_complete(&self) -> bool {
233 self.status == KeyResultStatus::Completed || self.current_value >= self.target_value
234 }
235
236 pub fn add_outcome(&mut self, outcome: KrOutcome) {
238 self.outcomes.push(outcome);
239 self.updated_at = Utc::now();
240 }
241
242 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257#[derive(Default)]
258pub enum KrMetricType {
259 #[default]
261 Progress,
262
263 Count,
265
266 Binary,
268
269 Latency,
271
272 Quality,
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278#[serde(rename_all = "snake_case")]
279#[derive(Default)]
280pub enum KeyResultStatus {
281 #[default]
283 Pending,
284
285 InProgress,
287
288 Completed,
290
291 AtRisk,
293
294 Failed,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct OkrRun {
301 pub id: Uuid,
303
304 pub okr_id: Uuid,
306
307 pub name: String,
309
310 #[serde(default)]
312 pub status: OkrRunStatus,
313
314 #[serde(default)]
316 pub correlation_id: Option<String>,
317
318 #[serde(default)]
320 pub relay_checkpoint_id: Option<String>,
321
322 #[serde(default)]
324 pub session_id: Option<String>,
325
326 #[serde(default)]
328 pub kr_progress: std::collections::HashMap<String, f64>,
329
330 #[serde(default)]
332 pub approval: Option<ApprovalDecision>,
333
334 #[serde(default)]
336 pub outcomes: Vec<KrOutcome>,
337
338 #[serde(default)]
340 pub iterations: u32,
341
342 #[serde(default = "utc_now")]
344 pub started_at: DateTime<Utc>,
345
346 #[serde(default)]
348 pub completed_at: Option<DateTime<Utc>>,
349
350 #[serde(default = "utc_now")]
352 pub updated_at: DateTime<Utc>,
353}
354
355impl OkrRun {
356 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 pub fn validate(&self) -> Result<(), OkrValidationError> {
379 if self.name.trim().is_empty() {
380 return Err(OkrValidationError::EmptyRunName);
381 }
382 Ok(())
383 }
384
385 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 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 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 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 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 pub fn is_resumable(&self) -> bool {
434 matches!(
435 self.status,
436 OkrRunStatus::Running | OkrRunStatus::Paused | OkrRunStatus::WaitingApproval
437 )
438 }
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
443#[serde(rename_all = "snake_case")]
444#[derive(Default)]
445pub enum OkrRunStatus {
446 #[default]
448 Draft,
449
450 PendingApproval,
452
453 Approved,
455
456 Running,
458
459 Paused,
461
462 WaitingApproval,
464
465 Completed,
467
468 Failed,
470
471 Denied,
473
474 Cancelled,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct ApprovalDecision {
481 pub id: Uuid,
483
484 pub run_id: Uuid,
486
487 pub decision: ApprovalChoice,
489
490 #[serde(default)]
492 pub reason: String,
493
494 #[serde(default)]
496 pub approver: Option<String>,
497
498 #[serde(default)]
500 pub metadata: std::collections::HashMap<String, String>,
501
502 #[serde(default = "utc_now")]
504 pub decided_at: DateTime<Utc>,
505}
506
507impl ApprovalDecision {
508 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
537#[serde(rename_all = "snake_case")]
538pub enum ApprovalChoice {
539 Approved,
541
542 Denied,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct KrOutcome {
549 pub id: Uuid,
551
552 pub kr_id: Uuid,
554
555 #[serde(default)]
557 pub run_id: Option<Uuid>,
558
559 pub description: String,
561
562 #[serde(default)]
564 pub outcome_type: KrOutcomeType,
565
566 #[serde(default)]
568 pub value: Option<f64>,
569
570 #[serde(default)]
572 pub evidence: Vec<String>,
573
574 #[serde(default)]
576 pub source: String,
577
578 #[serde(default = "utc_now")]
580 pub created_at: DateTime<Utc>,
581}
582
583impl KrOutcome {
584 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 pub fn with_value(mut self, value: f64) -> Self {
601 self.value = Some(value);
602 self
603 }
604
605 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(rename_all = "snake_case")]
616#[derive(Default)]
617pub enum KrOutcomeType {
618 #[default]
620 Evidence,
621
622 TestPass,
624
625 CodeChange,
627
628 BugFix,
630
631 FeatureDelivered,
633
634 MetricAchievement,
636
637 ReviewPassed,
639
640 Deployment,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
646#[serde(rename_all = "snake_case")]
647pub enum OkrValidationError {
648 EmptyTitle,
650
651 NoKeyResults,
653
654 EmptyKeyResultTitle,
656
657 InvalidTargetValue,
659
660 EmptyRunName,
662
663 InvalidStatusTransition,
665
666 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
692fn utc_now() -> DateTime<Utc> {
694 Utc::now()
695}
696
697fn 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()); }
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 run.submit_for_approval().unwrap();
739 assert_eq!(run.status, OkrRunStatus::PendingApproval);
740
741 run.record_decision(ApprovalDecision::approve(run.id, "Looks good"));
743 assert_eq!(run.status, OkrRunStatus::Approved);
744
745 run.start().unwrap();
747 assert_eq!(run.status, OkrRunStatus::Running);
748
749 run.update_kr_progress("kr-1", 0.5);
751
752 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}