1use crate::{ActorId, DependencyId, WorkItemId};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
5pub enum DependencyKind {
7 FinishStart,
9 StartStart,
11 FinishFinish,
13 StartFinish,
15}
16
17impl DependencyKind {
18 #[must_use]
20 pub fn abbreviation(self) -> &'static str {
21 match self {
22 Self::FinishStart => "FS",
23 Self::StartStart => "SS",
24 Self::FinishFinish => "FF",
25 Self::StartFinish => "SF",
26 }
27 }
28
29 #[must_use]
31 pub fn predecessor_endpoint(self) -> crate::Endpoint {
32 match self {
33 Self::FinishStart | Self::FinishFinish => crate::Endpoint::Finish,
34 Self::StartStart | Self::StartFinish => crate::Endpoint::Start,
35 }
36 }
37
38 #[must_use]
40 pub fn successor_endpoint(self) -> crate::Endpoint {
41 match self {
42 Self::FinishStart | Self::StartStart => crate::Endpoint::Start,
43 Self::FinishFinish | Self::StartFinish => crate::Endpoint::Finish,
44 }
45 }
46}
47
48#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49pub enum DependencyPolicy {
51 #[default]
53 Hard,
54 Soft,
56}
57
58#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub enum StartBasis {
64 #[default]
66 Verified,
67 Provisional,
70}
71
72impl StartBasis {
73 #[must_use]
75 pub fn is_verified(&self) -> bool {
76 *self == Self::Verified
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
83pub struct DependencyWaiver {
84 pub actor: ActorId,
86 pub at: chrono::DateTime<chrono::Utc>,
88 pub reason: String,
90}
91
92#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93pub enum LagBasis {
95 #[default]
97 Elapsed,
98 Working,
100}
101
102impl LagBasis {
103 #[must_use]
105 pub fn is_elapsed(&self) -> bool {
106 *self == Self::Elapsed
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111#[serde(deny_unknown_fields)]
113pub struct Dependency {
114 pub id: DependencyId,
116 pub predecessor: WorkItemId,
118 pub successor: WorkItemId,
120 pub kind: DependencyKind,
122 pub lag_hours: f64,
124 #[serde(default, skip_serializing_if = "LagBasis::is_elapsed")]
126 pub lag_basis: LagBasis,
127 #[serde(default)]
129 pub policy: DependencyPolicy,
130 #[serde(default, skip_serializing_if = "StartBasis::is_verified")]
132 pub start_basis: StartBasis,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub rationale: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub waiver: Option<DependencyWaiver>,
139}
140
141const FNV_OFFSET: u128 = 0x6c62_272e_07bb_0142_62b8_2175_6295_c58d;
142const FNV_PRIME: u128 = 0x0000_0000_0100_0000_0000_0000_0000_013b;
143const IDENTITY_DOMAIN: &[u8] = b"dpm.dependency.v1\0";
144
145fn avalanche(mut value: u64) -> u64 {
147 value ^= value >> 33;
148 value = value.wrapping_mul(0xff51_afd7_ed55_8ccd);
149 value ^= value >> 33;
150 value = value.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
151 value ^ (value >> 33)
152}
153
154impl Dependency {
155 #[must_use]
162 pub fn derived_id(
163 predecessor: WorkItemId,
164 successor: WorkItemId,
165 kind: DependencyKind,
166 ) -> DependencyId {
167 let mut hash = FNV_OFFSET;
168 let bytes = IDENTITY_DOMAIN
169 .iter()
170 .chain(predecessor.0.as_bytes())
171 .chain(successor.0.as_bytes())
172 .chain(kind.abbreviation().as_bytes());
173 for byte in bytes {
174 hash ^= u128::from(*byte);
175 hash = hash.wrapping_mul(FNV_PRIME);
176 }
177 let (high, low) = ((hash >> 64) as u64, hash as u64);
180 let high = avalanche(high ^ low.rotate_left(29));
181 let low = avalanche(low ^ high);
182 let high = avalanche(high ^ low);
183 let mixed = (u128::from(high) << 64) | u128::from(low);
184 DependencyId(uuid::Builder::from_custom_bytes(mixed.to_be_bytes()).into_uuid())
185 }
186
187 #[must_use]
189 pub fn new(
190 predecessor: WorkItemId,
191 successor: WorkItemId,
192 kind: DependencyKind,
193 lag_hours: f64,
194 ) -> Self {
195 Self {
196 id: Self::derived_id(predecessor, successor, kind),
197 predecessor,
198 successor,
199 kind,
200 lag_hours,
201 lag_basis: LagBasis::Elapsed,
202 policy: DependencyPolicy::Hard,
203 start_basis: StartBasis::Verified,
204 rationale: None,
205 waiver: None,
206 }
207 }
208
209 #[must_use]
211 pub fn is_waived(&self) -> bool {
212 self.waiver.is_some()
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
217pub enum WorkLinkKind {
219 RelatesTo,
221 Duplicates,
223 DerivedFrom,
225 Supersedes,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(deny_unknown_fields)]
232pub struct WorkLink {
233 pub kind: WorkLinkKind,
235 pub source: WorkItemId,
237 pub target: WorkItemId,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub note: Option<String>,
242}
243
244#[cfg(test)]
245mod tests;