Skip to main content

dpm_model/
plan.rs

1mod decode;
2
3use crate::*;
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6#[derive(Debug, Clone, PartialEq, Serialize)]
7/// Authoritative graph and revision; scheduling and completion views are derived.
8#[serde(deny_unknown_fields)]
9pub struct Plan {
10    /// Portable domain format version; unsupported formats are rejected.
11    #[serde(deserialize_with = "read_format_version")]
12    pub format_version: u32,
13    /// Assets shared across code and non-code work.
14    pub assets: BTreeMap<AssetId, WorkspaceAsset>,
15    /// Namespace containing the graph.
16    pub workspace: Workspace,
17    /// Wrapping operation sequence for optimistic concurrency.
18    pub revision: u64,
19    /// Projects indexed by stable identifier.
20    pub projects: BTreeMap<ProjectId, Project>,
21    /// Work indexed by stable identifier.
22    pub work_items: BTreeMap<WorkItemId, WorkItem>,
23    /// Requirements indexed by stable identifier.
24    pub requirements: BTreeMap<RequirementId, Requirement>,
25    /// Evidence indexed by stable identifier.
26    pub artifacts: BTreeMap<ArtifactId, Artifact>,
27    /// Decision gates indexed by stable identifier.
28    pub decisions: BTreeMap<DecisionId, Decision>,
29    /// Risks indexed by stable identifier.
30    pub risks: BTreeMap<RiskId, Risk>,
31    /// Directed temporal constraints; cycles are rejected.
32    pub dependencies: Vec<Dependency>,
33    /// External tracker objects linked to work; context only, never evidence or gates.
34    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
35    pub external_references: BTreeMap<ExternalReferenceId, ExternalReference>,
36    /// Contextual work relationships that never gate readiness, scheduling or progress.
37    #[serde(default, skip_serializing_if = "Vec::is_empty")]
38    pub links: Vec<WorkLink>,
39    /// Working calendars; every hour is working time when absent.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub calendars: Option<Calendars>,
42}
43
44impl Plan {
45    /// Create an empty namespace at revision zero.
46    pub fn empty(name: impl Into<String>) -> Self {
47        Self {
48            format_version: 3,
49            assets: BTreeMap::new(),
50            workspace: Workspace {
51                id: WorkspaceId::new(),
52                name: name.into(),
53            },
54            revision: 0,
55            projects: BTreeMap::new(),
56            work_items: BTreeMap::new(),
57            requirements: BTreeMap::new(),
58            artifacts: BTreeMap::new(),
59            decisions: BTreeMap::new(),
60            risks: BTreeMap::new(),
61            dependencies: Vec::new(),
62            external_references: BTreeMap::new(),
63            links: Vec::new(),
64            calendars: None,
65        }
66    }
67
68    /// Find authoritative work by its human-readable key.
69    pub fn find_work_by_key(&self, key: &str) -> Option<&WorkItem> {
70        self.work_items.values().find(|work| work.key.0 == key)
71    }
72
73    /// Mutably find authoritative work; callers must validate edits.
74    pub fn find_work_by_key_mut(&mut self, key: &str) -> Option<&mut WorkItem> {
75        self.work_items.values_mut().find(|work| work.key.0 == key)
76    }
77
78    /// Find a temporal constraint by its stable identity.
79    pub fn find_dependency(&self, id: DependencyId) -> Option<&Dependency> {
80        self.dependencies.iter().find(|edge| edge.id == id)
81    }
82
83    /// Constraints that currently gate execution and remaining projections; waived edges are omitted.
84    pub fn enforced_dependencies(&self) -> impl Iterator<Item = &Dependency> {
85        self.dependencies
86            .iter()
87            .filter(|edge| edge.waiver.is_none())
88    }
89
90    /// Find a gate by its human-readable key.
91    pub fn find_decision_by_key(&self, key: &str) -> Option<&Decision> {
92        self.decisions
93            .values()
94            .find(|decision| decision.key.0 == key)
95    }
96}
97
98fn read_format_version<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<u32, D::Error> {
99    let version = u32::deserialize(deserializer)?;
100    if version != 3 {
101        return Err(serde::de::Error::custom(format!(
102            "unsupported plan format {version}; expected 3. Preserve the original database and export with its original DPM release; convert that export before initializing a new store"
103        )));
104    }
105    Ok(version)
106}
107
108#[cfg(test)]
109mod tests;