dpm-schedule 0.1.0

Deterministic and probabilistic scheduling for DPM
Documentation
//! Temporal-constraint contract for the deterministic CPM projection, exercised via the public API.
#![cfg(test)]

#[path = "temporal_contract/drag.rs"]
mod drag;
#[path = "temporal_contract/examples.rs"]
mod examples;
#[path = "temporal_contract/properties.rs"]
mod properties;
#[path = "temporal_contract/textbook.rs"]
mod textbook;

use dpm_model::{
    AcceptanceCriterion, Dependency, DependencyKind, Key, Plan, Priority, Project, ProjectId,
    ThreePointEstimate, WorkItem, WorkItemId, WorkKind, WorkStatus,
};
use std::collections::{BTreeMap, BTreeSet};

/// Tolerance the contract promises for derived hours.
pub(crate) const TOLERANCE: f64 = 1e-8;

pub(crate) const KINDS: [DependencyKind; 4] = [
    DependencyKind::FinishStart,
    DependencyKind::StartStart,
    DependencyKind::FinishFinish,
    DependencyKind::StartFinish,
];

/// Identities are derived from a counter so a failing case reproduces with the same graph order.
pub(crate) fn stable_id(n: u64) -> WorkItemId {
    serde_json::from_str(&format!("\"00000000-0000-4000-8000-{n:012x}\"")).expect("stable id")
}

/// Small plan builder whose identities are reproducible across runs.
pub(crate) struct Network {
    pub plan: Plan,
    project: ProjectId,
    durations: BTreeMap<WorkItemId, f64>,
}

impl Network {
    pub fn new() -> Self {
        let mut plan = Plan::empty("temporal-contract");
        let project = Project {
            id: ProjectId::new(),
            key: Key::new("P"),
            parent: None,
            title: "Temporal contract".into(),
            objective: "Exercise temporal bounds".into(),
        };
        let id = project.id;
        plan.projects.insert(id, project);
        Self {
            plan,
            project: id,
            durations: BTreeMap::new(),
        }
    }

    fn insert(&mut self, id: WorkItemId, kind: WorkKind, hours: f64) -> WorkItemId {
        let key = format!("W{}", self.plan.work_items.len());
        let estimate = (kind == WorkKind::Task).then_some(ThreePointEstimate {
            optimistic_hours: hours,
            likely_hours: hours,
            pessimistic_hours: hours,
        });
        let work = WorkItem {
            id,
            key: Key::new(key.clone()),
            project: self.project,
            parent: None,
            kind,
            title: key.clone(),
            order: Default::default(),
            contract: dpm_model::WorkContract {
                objective: key,
                acceptance: vec![AcceptanceCriterion {
                    text: "observable result exists".into(),
                }],
                instructions: None,
                capabilities: BTreeSet::new(),
                requirement_ids: BTreeSet::new(),
                condition: None,
                join: dpm_model::JoinPolicy::default(),
                assets: Vec::new(),
            },
            execution: dpm_model::ExecutionRecord {
                status: WorkStatus::Planned,
                reported_progress_percent: 0,
                artifact_ids: BTreeSet::new(),
                owner: None,
                handoffs: Vec::new(),
                releases: Vec::new(),
                block_reason: None,
                events: Default::default(),
                last_rejection: None,
                attempts: Vec::new(),
                basis: Vec::new(),
            },
            schedule: dpm_model::ScheduleInputs {
                priority: Priority::P2,
                estimate,
                executor: None,
                calendar: None,
            },
        };
        self.plan.work_items.insert(id, work);
        self.durations.insert(id, hours);
        id
    }

    /// Add a task with a point estimate, so its expected duration equals `hours`.
    pub fn task(&mut self, id: WorkItemId, hours: f64) -> WorkItemId {
        self.insert(id, WorkKind::Task, hours)
    }

    /// Add a task with a three-point estimate; the projection uses its PERT mean.
    pub fn pert_task(&mut self, id: WorkItemId, o: f64, m: f64, p: f64) -> WorkItemId {
        self.insert(id, WorkKind::Task, (o + 4.0 * m + p) / 6.0);
        if let Some(work) = self.plan.work_items.get_mut(&id) {
            work.schedule.estimate = Some(ThreePointEstimate {
                optimistic_hours: o,
                likely_hours: m,
                pessimistic_hours: p,
            });
        }
        id
    }

    /// Add a zero-duration milestone.
    pub fn milestone(&mut self, id: WorkItemId) -> WorkItemId {
        self.insert(id, WorkKind::Milestone, 0.0)
    }

    pub fn link(&mut self, from: WorkItemId, to: WorkItemId, kind: DependencyKind, lag: f64) {
        self.plan
            .dependencies
            .push(Dependency::new(from, to, kind, lag));
    }

    pub fn durations(&self) -> &BTreeMap<WorkItemId, f64> {
        &self.durations
    }
}