pub mod persistence;
pub use persistence::OkrRepository;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Okr {
pub id: Uuid,
pub title: String,
pub description: String,
#[serde(default)]
pub status: OkrStatus,
#[serde(default)]
pub key_results: Vec<KeyResult>,
#[serde(default)]
pub owner: Option<String>,
#[serde(default)]
pub tenant_id: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default = "utc_now")]
pub created_at: DateTime<Utc>,
#[serde(default = "utc_now")]
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub target_date: Option<DateTime<Utc>>,
}
impl Okr {
pub fn new(title: impl Into<String>, description: impl Into<String>) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
title: title.into(),
description: description.into(),
status: OkrStatus::Draft,
key_results: Vec::new(),
owner: None,
tenant_id: None,
tags: Vec::new(),
created_at: now,
updated_at: now,
target_date: None,
}
}
pub fn validate(&self) -> Result<(), OkrValidationError> {
if self.title.trim().is_empty() {
return Err(OkrValidationError::EmptyTitle);
}
if self.key_results.is_empty() {
return Err(OkrValidationError::NoKeyResults);
}
for kr in &self.key_results {
kr.validate()?;
}
Ok(())
}
pub fn progress(&self) -> f64 {
if self.key_results.is_empty() {
return 0.0;
}
let total: f64 = self.key_results.iter().map(|kr| kr.progress()).sum();
total / self.key_results.len() as f64
}
pub fn is_complete(&self) -> bool {
self.key_results.iter().all(|kr| kr.is_complete())
}
pub fn add_key_result(&mut self, kr: KeyResult) {
self.key_results.push(kr);
self.updated_at = Utc::now();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OkrStatus {
Draft,
Active,
Completed,
Cancelled,
OnHold,
}
impl Default for OkrStatus {
fn default() -> Self {
OkrStatus::Draft
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyResult {
pub id: Uuid,
pub okr_id: Uuid,
pub title: String,
pub description: String,
pub target_value: f64,
#[serde(default)]
pub current_value: f64,
#[serde(default = "default_unit")]
pub unit: String,
#[serde(default)]
pub metric_type: KrMetricType,
#[serde(default)]
pub status: KeyResultStatus,
#[serde(default)]
pub outcomes: Vec<KrOutcome>,
#[serde(default = "utc_now")]
pub created_at: DateTime<Utc>,
#[serde(default = "utc_now")]
pub updated_at: DateTime<Utc>,
}
impl KeyResult {
pub fn new(
okr_id: Uuid,
title: impl Into<String>,
target_value: f64,
unit: impl Into<String>,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
okr_id,
title: title.into(),
description: String::new(),
target_value,
current_value: 0.0,
unit: unit.into(),
metric_type: KrMetricType::Progress,
status: KeyResultStatus::Pending,
outcomes: Vec::new(),
created_at: now,
updated_at: now,
}
}
pub fn validate(&self) -> Result<(), OkrValidationError> {
if self.title.trim().is_empty() {
return Err(OkrValidationError::EmptyKeyResultTitle);
}
if self.target_value < 0.0 {
return Err(OkrValidationError::InvalidTargetValue);
}
Ok(())
}
pub fn progress(&self) -> f64 {
if self.target_value == 0.0 {
return 0.0;
}
(self.current_value / self.target_value).clamp(0.0, 1.0)
}
pub fn is_complete(&self) -> bool {
self.status == KeyResultStatus::Completed || self.current_value >= self.target_value
}
pub fn add_outcome(&mut self, outcome: KrOutcome) {
self.outcomes.push(outcome);
self.updated_at = Utc::now();
}
pub fn update_progress(&mut self, value: f64) {
self.current_value = value;
self.updated_at = Utc::now();
if self.is_complete() {
self.status = KeyResultStatus::Completed;
} else if self.current_value > 0.0 {
self.status = KeyResultStatus::InProgress;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KrMetricType {
Progress,
Count,
Binary,
Latency,
Quality,
}
impl Default for KrMetricType {
fn default() -> Self {
KrMetricType::Progress
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyResultStatus {
Pending,
InProgress,
Completed,
AtRisk,
Failed,
}
impl Default for KeyResultStatus {
fn default() -> Self {
KeyResultStatus::Pending
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OkrRun {
pub id: Uuid,
pub okr_id: Uuid,
pub name: String,
#[serde(default)]
pub status: OkrRunStatus,
#[serde(default)]
pub correlation_id: Option<String>,
#[serde(default)]
pub relay_checkpoint_id: Option<String>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub kr_progress: std::collections::HashMap<String, f64>,
#[serde(default)]
pub approval: Option<ApprovalDecision>,
#[serde(default)]
pub outcomes: Vec<KrOutcome>,
#[serde(default)]
pub iterations: u32,
#[serde(default = "utc_now")]
pub started_at: DateTime<Utc>,
#[serde(default)]
pub completed_at: Option<DateTime<Utc>>,
#[serde(default = "utc_now")]
pub updated_at: DateTime<Utc>,
}
impl OkrRun {
pub fn new(okr_id: Uuid, name: impl Into<String>) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
okr_id,
name: name.into(),
status: OkrRunStatus::Draft,
correlation_id: None,
relay_checkpoint_id: None,
session_id: None,
kr_progress: std::collections::HashMap::new(),
approval: None,
outcomes: Vec::new(),
iterations: 0,
started_at: now,
completed_at: None,
updated_at: now,
}
}
pub fn validate(&self) -> Result<(), OkrValidationError> {
if self.name.trim().is_empty() {
return Err(OkrValidationError::EmptyRunName);
}
Ok(())
}
pub fn submit_for_approval(&mut self) -> Result<(), OkrValidationError> {
if self.status != OkrRunStatus::Draft {
return Err(OkrValidationError::InvalidStatusTransition);
}
self.status = OkrRunStatus::PendingApproval;
self.updated_at = Utc::now();
Ok(())
}
pub fn record_decision(&mut self, decision: ApprovalDecision) {
self.approval = Some(decision.clone());
self.updated_at = Utc::now();
match decision.decision {
ApprovalChoice::Approved => {
self.status = OkrRunStatus::Approved;
}
ApprovalChoice::Denied => {
self.status = OkrRunStatus::Denied;
}
}
}
pub fn start(&mut self) -> Result<(), OkrValidationError> {
if self.status != OkrRunStatus::Approved {
return Err(OkrValidationError::NotApproved);
}
self.status = OkrRunStatus::Running;
self.updated_at = Utc::now();
Ok(())
}
pub fn complete(&mut self) {
self.status = OkrRunStatus::Completed;
self.completed_at = Some(Utc::now());
self.updated_at = Utc::now();
}
pub fn update_kr_progress(&mut self, kr_id: &str, progress: f64) {
self.kr_progress.insert(kr_id.to_string(), progress);
self.updated_at = Utc::now();
}
pub fn is_resumable(&self) -> bool {
matches!(
self.status,
OkrRunStatus::Running | OkrRunStatus::Paused | OkrRunStatus::WaitingApproval
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OkrRunStatus {
Draft,
PendingApproval,
Approved,
Running,
Paused,
WaitingApproval,
Completed,
Failed,
Denied,
Cancelled,
}
impl Default for OkrRunStatus {
fn default() -> Self {
OkrRunStatus::Draft
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalDecision {
pub id: Uuid,
pub run_id: Uuid,
pub decision: ApprovalChoice,
#[serde(default)]
pub reason: String,
#[serde(default)]
pub approver: Option<String>,
#[serde(default)]
pub metadata: std::collections::HashMap<String, String>,
#[serde(default = "utc_now")]
pub decided_at: DateTime<Utc>,
}
impl ApprovalDecision {
pub fn approve(run_id: Uuid, reason: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
run_id,
decision: ApprovalChoice::Approved,
reason: reason.into(),
approver: None,
metadata: std::collections::HashMap::new(),
decided_at: Utc::now(),
}
}
pub fn deny(run_id: Uuid, reason: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
run_id,
decision: ApprovalChoice::Denied,
reason: reason.into(),
approver: None,
metadata: std::collections::HashMap::new(),
decided_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalChoice {
Approved,
Denied,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KrOutcome {
pub id: Uuid,
pub kr_id: Uuid,
#[serde(default)]
pub run_id: Option<Uuid>,
pub description: String,
#[serde(default)]
pub outcome_type: KrOutcomeType,
#[serde(default)]
pub value: Option<f64>,
#[serde(default)]
pub evidence: Vec<String>,
#[serde(default)]
pub source: String,
#[serde(default = "utc_now")]
pub created_at: DateTime<Utc>,
}
impl KrOutcome {
pub fn new(kr_id: Uuid, description: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
kr_id,
run_id: None,
description: description.into(),
outcome_type: KrOutcomeType::Evidence,
value: None,
evidence: Vec::new(),
source: String::new(),
created_at: Utc::now(),
}
}
pub fn with_value(mut self, value: f64) -> Self {
self.value = Some(value);
self
}
pub fn add_evidence(mut self, evidence: impl Into<String>) -> Self {
self.evidence.push(evidence.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KrOutcomeType {
Evidence,
TestPass,
CodeChange,
BugFix,
FeatureDelivered,
MetricAchievement,
ReviewPassed,
Deployment,
}
impl Default for KrOutcomeType {
fn default() -> Self {
KrOutcomeType::Evidence
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OkrValidationError {
EmptyTitle,
NoKeyResults,
EmptyKeyResultTitle,
InvalidTargetValue,
EmptyRunName,
InvalidStatusTransition,
NotApproved,
}
impl std::fmt::Display for OkrValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OkrValidationError::EmptyTitle => write!(f, "objective title cannot be empty"),
OkrValidationError::NoKeyResults => write!(f, "at least one key result is required"),
OkrValidationError::EmptyKeyResultTitle => {
write!(f, "key result title cannot be empty")
}
OkrValidationError::InvalidTargetValue => {
write!(f, "target value must be non-negative")
}
OkrValidationError::EmptyRunName => write!(f, "run name cannot be empty"),
OkrValidationError::InvalidStatusTransition => {
write!(f, "invalid status transition")
}
OkrValidationError::NotApproved => write!(f, "run must be approved before starting"),
}
}
}
impl std::error::Error for OkrValidationError {}
fn utc_now() -> DateTime<Utc> {
Utc::now()
}
fn default_unit() -> String {
"%".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_okr_creation() {
let okr = Okr::new("Test Objective", "Description");
assert_eq!(okr.title, "Test Objective");
assert_eq!(okr.status, OkrStatus::Draft);
assert!(okr.validate().is_err()); }
#[test]
fn test_okr_with_key_results() {
let mut okr = Okr::new("Test Objective", "Description");
let kr = KeyResult::new(okr.id, "KR1", 100.0, "%");
okr.add_key_result(kr);
assert!(okr.validate().is_ok());
}
#[test]
fn test_key_result_progress() {
let kr = KeyResult::new(Uuid::new_v4(), "Test KR", 100.0, "%");
assert_eq!(kr.progress(), 0.0);
let mut kr = kr;
kr.update_progress(50.0);
assert!((kr.progress() - 0.5).abs() < 0.001);
}
#[test]
fn test_okr_run_workflow() {
let okr_id = Uuid::new_v4();
let mut run = OkrRun::new(okr_id, "Q1 2024 Run");
run.submit_for_approval().unwrap();
assert_eq!(run.status, OkrRunStatus::PendingApproval);
run.record_decision(ApprovalDecision::approve(run.id, "Looks good"));
assert_eq!(run.status, OkrRunStatus::Approved);
run.start().unwrap();
assert_eq!(run.status, OkrRunStatus::Running);
run.update_kr_progress("kr-1", 0.5);
run.complete();
assert_eq!(run.status, OkrRunStatus::Completed);
}
#[test]
fn test_outcome_creation() {
let outcome = KrOutcome::new(Uuid::new_v4(), "Fixed bug in auth")
.with_value(1.0)
.add_evidence("commit:abc123");
assert_eq!(outcome.value, Some(1.0));
assert!(outcome.evidence.contains(&"commit:abc123".to_string()));
}
}