1mod contract;
2mod execution;
3mod schedule;
4pub use contract::WorkContract;
5pub use execution::ExecutionRecord;
6pub use schedule::ScheduleInputs;
7
8use crate::{ActorId, EstimateError, Key, ProjectId, WorkItemId};
9use serde::{Deserialize, Serialize};
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum WorkKind {
13 WorkPackage,
15 Task,
17 Milestone,
19}
20
21#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
22pub enum Priority {
24 P0,
26 P1,
28 #[default]
29 P2,
31 P3,
33 P4,
35}
36
37impl Priority {
38 #[must_use]
39 pub fn rank(self) -> u8 {
41 match self {
42 Self::P0 => 4,
43 Self::P1 => 3,
44 Self::P2 => 2,
45 Self::P3 => 1,
46 Self::P4 => 0,
47 }
48 }
49}
50
51#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52pub enum WorkStatus {
54 #[default]
56 Proposed,
57 Planned,
59 Claimed,
61 InProgress,
63 Blocked,
65 Submitted,
67 Verified,
69 Done,
71}
72
73impl WorkStatus {
74 #[must_use]
75 pub fn satisfies_dependency(self) -> bool {
77 matches!(self, Self::Verified | Self::Done)
78 }
79
80 #[must_use]
81 pub fn is_terminal(self) -> bool {
83 matches!(self, Self::Verified | Self::Done)
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
90pub struct AcceptanceCriterion {
91 pub text: String,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
96#[serde(deny_unknown_fields)]
98pub struct ThreePointEstimate {
99 pub optimistic_hours: f64,
101 pub likely_hours: f64,
103 pub pessimistic_hours: f64,
105}
106
107impl ThreePointEstimate {
108 pub fn validate(self) -> Result<(), EstimateError> {
110 if !self.optimistic_hours.is_finite()
111 || !self.likely_hours.is_finite()
112 || !self.pessimistic_hours.is_finite()
113 {
114 return Err(EstimateError::NonFinite);
115 }
116 if self.optimistic_hours < 0.0 {
117 return Err(EstimateError::Negative);
118 }
119 if self.optimistic_hours > self.likely_hours || self.likely_hours > self.pessimistic_hours {
120 return Err(EstimateError::Unordered);
121 }
122 Ok(())
123 }
124
125 #[must_use]
126 pub fn pert_expected_hours(self) -> f64 {
128 self.likely_hours
129 + (self.optimistic_hours - self.likely_hours) / 6.0
130 + (self.pessimistic_hours - self.likely_hours) / 6.0
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135#[serde(deny_unknown_fields)]
137pub struct WorkItem {
138 pub id: WorkItemId,
140 pub key: Key,
142 pub project: ProjectId,
144 pub parent: Option<WorkItemId>,
146 pub kind: WorkKind,
148 pub title: String,
150 pub order: crate::SiblingOrder,
152 pub contract: crate::WorkContract,
154 pub execution: crate::ExecutionRecord,
156 pub schedule: crate::ScheduleInputs,
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct ReviewRejection {
164 pub actor: ActorId,
166 pub at: chrono::DateTime<chrono::Utc>,
168 pub reason: String,
170}
171
172impl WorkItem {
173 #[must_use]
174 pub fn expected_duration_hours(&self) -> f64 {
176 match self.kind {
177 WorkKind::Task => self
178 .schedule
179 .estimate
180 .map_or(0.0, ThreePointEstimate::pert_expected_hours),
181 WorkKind::WorkPackage | WorkKind::Milestone => 0.0,
182 }
183 }
184
185 #[must_use]
186 pub fn is_executable(&self) -> bool {
188 self.kind == WorkKind::Task
189 }
190
191 #[must_use]
197 pub fn start_event(&self) -> Option<crate::EventTime> {
198 let occurred = match self.execution.status {
199 WorkStatus::InProgress
200 | WorkStatus::Submitted
201 | WorkStatus::Verified
202 | WorkStatus::Done => true,
203 WorkStatus::Blocked => self.execution.events.start_unrecorded,
204 WorkStatus::Proposed | WorkStatus::Planned | WorkStatus::Claimed => false,
205 };
206 match self.execution.events.started_at {
207 Some(at) => Some(crate::EventTime::Recorded(at)),
208 None => occurred.then_some(crate::EventTime::Unrecorded),
209 }
210 }
211}