Skip to main content

dpm_model/
work.rs

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)]
11/// Whether work is executable or a derived grouping/condition.
12pub enum WorkKind {
13    /// Non-executable container completed by all its children.
14    WorkPackage,
15    /// Executable work that can be claimed, submitted, and verified.
16    Task,
17    /// Zero-duration condition completed by verified prerequisites.
18    Milestone,
19}
20
21#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
22/// Human priority, separate from scheduling criticality.
23pub enum Priority {
24    /// Highest human priority.
25    P0,
26    /// High human priority.
27    P1,
28    #[default]
29    /// Normal human priority and the default.
30    P2,
31    /// Low human priority.
32    P3,
33    /// Lowest human priority.
34    P4,
35}
36
37impl Priority {
38    #[must_use]
39    /// Numeric ranking weight; larger values mean greater priority.
40    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)]
52/// Authoritative lifecycle for tasks; aggregate completion is a query projection.
53pub enum WorkStatus {
54    /// Work not yet approved for execution.
55    #[default]
56    Proposed,
57    /// Approved work eligible for readiness evaluation.
58    Planned,
59    /// Work reserved by its owner.
60    Claimed,
61    /// Work being performed by its owner.
62    InProgress,
63    /// Work suspended with a reason.
64    Blocked,
65    /// Owner reports completion and requests independent verification.
66    Submitted,
67    /// Independent verification succeeded.
68    Verified,
69    /// Finalized completed work.
70    Done,
71}
72
73impl WorkStatus {
74    #[must_use]
75    /// Whether a task has passed verification.
76    pub fn satisfies_dependency(self) -> bool {
77        matches!(self, Self::Verified | Self::Done)
78    }
79
80    #[must_use]
81    /// Whether a task has completed execution.
82    pub fn is_terminal(self) -> bool {
83        matches!(self, Self::Verified | Self::Done)
84    }
85}
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88/// Observable evidence required for submitted work.
89#[serde(deny_unknown_fields)]
90pub struct AcceptanceCriterion {
91    /// Concrete acceptance condition.
92    pub text: String,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
96/// Ordered non-negative duration estimates in elapsed hours.
97#[serde(deny_unknown_fields)]
98pub struct ThreePointEstimate {
99    /// Optimistic duration in elapsed hours.
100    pub optimistic_hours: f64,
101    /// Most-likely duration in elapsed hours.
102    pub likely_hours: f64,
103    /// Pessimistic duration in elapsed hours.
104    pub pessimistic_hours: f64,
105}
106
107impl ThreePointEstimate {
108    /// Reject non-finite, negative, or unordered duration bounds.
109    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    /// Weighted PERT estimate in elapsed hours.
127    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/// Execution contract and authoritative lifecycle for a unit of work.
136#[serde(deny_unknown_fields)]
137pub struct WorkItem {
138    /// Stable entity identity; it must match its containing map key.
139    pub id: WorkItemId,
140    /// Human-readable key, unique in this entity category.
141    pub key: Key,
142    /// Project that owns this entity.
143    pub project: ProjectId,
144    /// Optional parent; containment must be acyclic.
145    pub parent: Option<WorkItemId>,
146    /// Domain category of this value.
147    pub kind: WorkKind,
148    /// Non-empty human-readable title.
149    pub title: String,
150    /// Stable sibling position, independent of titles and keys.
151    pub order: crate::SiblingOrder,
152    /// Author-approved scope, requirements and applicability.
153    pub contract: crate::WorkContract,
154    /// Lifecycle and evidence recorded only by semantic commands.
155    pub execution: crate::ExecutionRecord,
156    /// Authoritative scheduling inputs; derived dates remain projections.
157    pub schedule: crate::ScheduleInputs,
158}
159
160/// Independent review explaining why submitted work needs another attempt.
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct ReviewRejection {
164    /// Principal who rejected the result.
165    pub actor: ActorId,
166    /// Caller-supplied UTC review time.
167    pub at: chrono::DateTime<chrono::Utc>,
168    /// Concrete unmet acceptance or evidence requirement.
169    pub reason: String,
170}
171
172impl WorkItem {
173    #[must_use]
174    /// Expected task duration; containers and milestones contribute zero.
175    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    /// Whether this work kind supports task lifecycle commands.
187    pub fn is_executable(&self) -> bool {
188        self.kind == WorkKind::Task
189    }
190
191    /// When the task started: its recorded time, or an unknown time when it started before start
192    /// times were recorded.
193    ///
194    /// Gate evaluation, resuming blocked work and progress reports all read this, so a legacy
195    /// start is never forgotten by one of them while another still honours it.
196    #[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}