Mutation#

The new values of one task after a scheduling call. Each entry in ScheduleResult::updates is a Mutation. A field is Some only when the result reports it. The rest are None.

Which tasks get an entry, and what the values are compared against, is described on ScheduleResult.

Declaration#

#[derive(Clone, Debug, PartialEq)]
pub struct Mutation {
    pub id: Id,
    pub kind: MutationKind,
    pub start_date: Option<DateTime>,
    pub end_date: Option<DateTime>,
    pub duration: Option<f64>,
    pub leveling_delay: Option<f64>,
    pub pauses: Option<Vec<Pause>>,
    pub reason: Option<Reason>,
}

Fields#

The date and leveling fields have the same names and units as on TaskData.

Field Type Present when Description
id Id always Id of the task.
kind MutationKind always Whether the task is a plain task, a summary or a milestone.
start_date Option<DateTime> the start changed, or the task is reported in full New start.
end_date Option<DateTime> the end changed, or the task is reported in full New end.
duration Option<f64> the duration changed, or the task is reported in full New working time in the task's duration unit.
leveling_delay Option<f64> the leveling delay changed New leveling delay, elapsed time in the task's duration unit.
pauses Option<Vec<Pause>> the pause list changed The complete new pause list. Some(vec![]) when every pause was removed. A task that had no pauses and still has none never gets this field.
reason Option<Reason> the call ran with reasons: true What placed the task. None when no planner ran (a cyclic project).

A task is reported in full when include_unchanged is set on SolveOptions, or when an apply touched it since the last solve. Such an entry has start_date, end_date and duration all set, even if none of them changed.

Writing back#

The field names match TaskUpdate: each Some(v) maps to Patch::Value(v) on the field of the same name. kind and reason have no counterpart there.

With the serde feature, Mutation implements Serialize and Deserialize. Field names stay snake_case, None fields are left out, kind is kebab-case and reason is an object tagged by kind. A serialized entry decodes as a JSON task update.

Example#

use plen_core::{
    schedule, DateTime, LinkData, LinkType, ProjectData, SolveOptions, TaskData,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let monday = DateTime::from_ymd_hm(2026, 4, 6, 8, 0).ok_or("invalid date")?;

    let data = ProjectData {
        tasks: vec![
            TaskData {
                start_date: Some(monday),
                duration: Some(2.0),
                ..TaskData::new("design")
            },
            TaskData {
                start_date: Some(monday),
                duration: Some(3.0),
                ..TaskData::new("build")
            },
        ],
        links: vec![LinkData {
            id: "l1".into(),
            source: "design".into(),
            target: "build".into(),
            link_type: LinkType::Fs,
            lag: 0.0,
            lag_unit: None,
        }],
        ..Default::default()
    };

    let options = SolveOptions {
        reasons: true,
        ..Default::default()
    };
    let result = schedule(data, &options)?;

    for update in &result.updates {
        if let Some(start) = update.start_date {
            println!("{} starts {} ({:?})", update.id, start, update.reason);
        }
    }
    Ok(())
}