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")]
121pub enum OkrStatus {
122 Draft,
124
125 Active,
127
128 Completed,
130
131 Cancelled,
133
134 OnHold,
136}
137
138impl Default for OkrStatus {
139 fn default() -> Self {
140 OkrStatus::Draft
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct KeyResult {
147 pub id: Uuid,
149
150 pub okr_id: Uuid,
152
153 pub title: String,
155
156 pub description: String,
158
159 pub target_value: f64,
161
162 #[serde(default)]
164 pub current_value: f64,
165
166 #[serde(default = "default_unit")]
168 pub unit: String,
169
170 #[serde(default)]
172 pub metric_type: KrMetricType,
173
174 #[serde(default)]
176 pub status: KeyResultStatus,
177
178 #[serde(default)]
180 pub outcomes: Vec<KrOutcome>,
181
182 #[serde(default = "utc_now")]
184 pub created_at: DateTime<Utc>,
185
186 #[serde(default = "utc_now")]
188 pub updated_at: DateTime<Utc>,
189}
190
191impl KeyResult {
192 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 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 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 pub fn is_complete(&self) -> bool {
237 self.status == KeyResultStatus::Completed || self.current_value >= self.target_value
238 }
239
240 pub fn add_outcome(&mut self, outcome: KrOutcome) {
242 self.outcomes.push(outcome);
243 self.updated_at = Utc::now();
244 }
245
246 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(rename_all = "snake_case")]
261pub enum KrMetricType {
262 Progress,
264
265 Count,
267
268 Binary,
270
271 Latency,
273
274 Quality,
276}
277
278impl Default for KrMetricType {
279 fn default() -> Self {
280 KrMetricType::Progress
281 }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
286#[serde(rename_all = "snake_case")]
287pub enum KeyResultStatus {
288 Pending,
290
291 InProgress,
293
294 Completed,
296
297 AtRisk,
299
300 Failed,
302}
303
304impl Default for KeyResultStatus {
305 fn default() -> Self {
306 KeyResultStatus::Pending
307 }
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct OkrRun {
313 pub id: Uuid,
315
316 pub okr_id: Uuid,
318
319 pub name: String,
321
322 #[serde(default)]
324 pub status: OkrRunStatus,
325
326 #[serde(default)]
328 pub correlation_id: Option<String>,
329
330 #[serde(default)]
332 pub relay_checkpoint_id: Option<String>,
333
334 #[serde(default)]
336 pub session_id: Option<String>,
337
338 #[serde(default)]
340 pub kr_progress: std::collections::HashMap<String, f64>,
341
342 #[serde(default)]
344 pub approval: Option<ApprovalDecision>,
345
346 #[serde(default)]
348 pub outcomes: Vec<KrOutcome>,
349
350 #[serde(default)]
352 pub iterations: u32,
353
354 #[serde(default = "utc_now")]
356 pub started_at: DateTime<Utc>,
357
358 #[serde(default)]
360 pub completed_at: Option<DateTime<Utc>>,
361
362 #[serde(default = "utc_now")]
364 pub updated_at: DateTime<Utc>,
365}
366
367impl OkrRun {
368 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 pub fn validate(&self) -> Result<(), OkrValidationError> {
391 if self.name.trim().is_empty() {
392 return Err(OkrValidationError::EmptyRunName);
393 }
394 Ok(())
395 }
396
397 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 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 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 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 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 pub fn is_resumable(&self) -> bool {
446 matches!(
447 self.status,
448 OkrRunStatus::Running | OkrRunStatus::Paused | OkrRunStatus::WaitingApproval
449 )
450 }
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
455#[serde(rename_all = "snake_case")]
456pub enum OkrRunStatus {
457 Draft,
459
460 PendingApproval,
462
463 Approved,
465
466 Running,
468
469 Paused,
471
472 WaitingApproval,
474
475 Completed,
477
478 Failed,
480
481 Denied,
483
484 Cancelled,
486}
487
488impl Default for OkrRunStatus {
489 fn default() -> Self {
490 OkrRunStatus::Draft
491 }
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct ApprovalDecision {
497 pub id: Uuid,
499
500 pub run_id: Uuid,
502
503 pub decision: ApprovalChoice,
505
506 #[serde(default)]
508 pub reason: String,
509
510 #[serde(default)]
512 pub approver: Option<String>,
513
514 #[serde(default)]
516 pub metadata: std::collections::HashMap<String, String>,
517
518 #[serde(default = "utc_now")]
520 pub decided_at: DateTime<Utc>,
521}
522
523impl ApprovalDecision {
524 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(rename_all = "snake_case")]
554pub enum ApprovalChoice {
555 Approved,
557
558 Denied,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct KrOutcome {
565 pub id: Uuid,
567
568 pub kr_id: Uuid,
570
571 #[serde(default)]
573 pub run_id: Option<Uuid>,
574
575 pub description: String,
577
578 #[serde(default)]
580 pub outcome_type: KrOutcomeType,
581
582 #[serde(default)]
584 pub value: Option<f64>,
585
586 #[serde(default)]
588 pub evidence: Vec<String>,
589
590 #[serde(default)]
592 pub source: String,
593
594 #[serde(default = "utc_now")]
596 pub created_at: DateTime<Utc>,
597}
598
599impl KrOutcome {
600 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 pub fn with_value(mut self, value: f64) -> Self {
617 self.value = Some(value);
618 self
619 }
620
621 pub fn add_evidence(mut self, evidence: impl Into<String>) -> Self {
623 self.evidence.push(evidence.into());
624 self
625 }
626}
627
628#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
630#[serde(rename_all = "snake_case")]
631pub enum KrOutcomeType {
632 Evidence,
634
635 TestPass,
637
638 CodeChange,
640
641 BugFix,
643
644 FeatureDelivered,
646
647 MetricAchievement,
649
650 ReviewPassed,
652
653 Deployment,
655}
656
657impl Default for KrOutcomeType {
658 fn default() -> Self {
659 KrOutcomeType::Evidence
660 }
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize)]
665#[serde(rename_all = "snake_case")]
666pub enum OkrValidationError {
667 EmptyTitle,
669
670 NoKeyResults,
672
673 EmptyKeyResultTitle,
675
676 InvalidTargetValue,
678
679 EmptyRunName,
681
682 InvalidStatusTransition,
684
685 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
711fn utc_now() -> DateTime<Utc> {
713 Utc::now()
714}
715
716fn 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()); }
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 run.submit_for_approval().unwrap();
758 assert_eq!(run.status, OkrRunStatus::PendingApproval);
759
760 run.record_decision(ApprovalDecision::approve(run.id, "Looks good"));
762 assert_eq!(run.status, OkrRunStatus::Approved);
763
764 run.start().unwrap();
766 assert_eq!(run.status, OkrRunStatus::Running);
767
768 run.update_kr_progress("kr-1", 0.5);
770
771 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}