Skip to main content

dpm_model/work/
execution.rs

1//! Recorded task lifecycle, ownership and evidence.
2
3use crate::*;
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeSet;
6
7/// Execution records protected as one value by reviewed plan changes.
8#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct ExecutionRecord {
11    /// Stored lifecycle; aggregate work has derived completion.
12    pub status: WorkStatus,
13    /// Owner-reported task execution progress; verification remains a separate condition.
14    #[serde(default)]
15    pub reported_progress_percent: u8,
16    /// Attached evidence identifiers.
17    pub artifact_ids: BTreeSet<ArtifactId>,
18    /// Principal that claimed the task and owns submission.
19    pub owner: Option<ActorId>,
20    /// Authorized ownership transfers in order; append-only, so every earlier holder stays known.
21    #[serde(default, skip_serializing_if = "Vec::is_empty")]
22    pub handoffs: Vec<crate::Handoff>,
23    /// Claims given back before a start, in order; append-only, so a releaser stays a holder.
24    #[serde(default, skip_serializing_if = "Vec::is_empty")]
25    pub releases: Vec<crate::ClaimRelease>,
26    /// Execution event times recorded by lifecycle commands.
27    #[serde(default, skip_serializing_if = "crate::ExecutionEvents::is_empty")]
28    pub events: crate::ExecutionEvents,
29    /// Most recent independent rejection; retained across resubmission as review context.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub last_rejection: Option<ReviewRejection>,
32    /// Every submission in order; reviews close attempts but never remove or renumber them.
33    #[serde(default, skip_serializing_if = "Vec::is_empty")]
34    pub attempts: Vec<crate::SubmissionAttempt>,
35    /// Predecessor attempts this task's execution relies on; append-only, latest per edge applies.
36    #[serde(default, skip_serializing_if = "Vec::is_empty")]
37    pub basis: Vec<crate::DependencyBasis>,
38    /// Non-empty reason while the task is blocked.
39    pub block_reason: Option<String>,
40}
41
42impl ExecutionRecord {
43    /// Latest time carried by any lifecycle, ownership, review or basis record of this execution.
44    ///
45    /// An unstarted claim contributes its reservation time; after the start, the start time, which
46    /// cannot precede the claim, orders the history instead. A backfilled event must not precede
47    /// anything the task's history already orders.
48    #[must_use]
49    pub fn latest_recorded_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
50        let events = [
51            self.events.claimed_at,
52            self.events.started_at,
53            self.events.submitted_at,
54            self.events.verified_at,
55            self.last_rejection.as_ref().map(|r| r.at),
56        ];
57        let reviews = self.attempts.iter().flat_map(|a| {
58            let closed = match &a.outcome {
59                AttemptOutcome::Pending => None,
60                AttemptOutcome::Rejected { at, .. } | AttemptOutcome::Verified { at, .. } => {
61                    Some(*at)
62                }
63            };
64            [Some(a.submitted_at), closed]
65        });
66        events
67            .into_iter()
68            .chain(reviews)
69            .flatten()
70            .chain(self.handoffs.iter().map(|h| h.at))
71            .chain(self.releases.iter().map(|r| r.at))
72            .chain(self.basis.iter().map(|b| b.recorded_at))
73            .max()
74    }
75}
76
77#[cfg(test)]
78mod tests;