ScheduleResult#

The result of every scheduling call. Project::solve returns it as Arc<ScheduleResult>; Project::preview, Project::analyze and schedule return it by value. LevelingResult::schedule holds one too.

Cycles, conflicts and a planner that didn't converge are reported in the result, not as an Err.

Declaration#

#[derive(Clone, Debug, PartialEq)]
pub struct ScheduleResult {
    pub baseline_id: Option<String>,
    pub updates: Vec<Mutation>,
    pub cycles: Vec<Cycle>,
    pub conflicts: Vec<Conflict>,
    pub notes: Vec<Note>,
    pub converged: bool,
    pub analytics: Option<Analytics>,
    pub overallocations: Option<Vec<ResourceOverallocation>>,
    pub profile: Option<LoadProfile>,
}

With the serde feature, ScheduleResult implements Serialize and Deserialize. Fields use camelCase, and None fields are left out. See Serialize results to JSON.

Fields#

Field Type Description
baseline_id Option<String> Token of the baseline this result produced or read.
updates Vec<Mutation> New dates and fields of the tasks that changed.
cycles Vec<Cycle> Dependency cycles found in the project.
conflicts Vec<Conflict> Constraint and calendar conflicts the planner couldn't resolve.
notes Vec<Note> Dropped input records and repairs made on import.
converged bool true when the planner reached a stable schedule.
analytics Option<Analytics> Critical path and slack. Some when requested.
overallocations Option<Vec<ResourceOverallocation>> Resource overload windows. Some when the project has assignments.
profile Option<LoadProfile> Per-bucket resource load. Only from preview with a profile query.

baseline_id#

pub baseline_id: Option<String>

An opaque token that names an accepted baseline.

  • solve and Project::level: always Some, the baseline the call accepted.
  • preview: always Some, the baseline the preview read. Pass it to the next preview or apply.
  • analyze(None, ..): Some only on a solved project with no unsolved edits.
  • analyze(Some(data), ..): always None.

The token changes whenever the accepted data changes. It isn't stable across runs, so don't compare tokens from separate runs or store them. See Project::solve for which calls check it.

updates#

pub updates: Vec<Mutation>

One Mutation per reported task, each carrying only the fields that changed.

  • By default the list is sparse: it holds only the tasks that changed. solve(None) and Project::level also report each task touched by earlier apply calls.
  • With SolveOptions::include_unchanged, it also holds every unchanged scheduled task.
  • preview reports updates relative to the baseline it read.
  • On a project with cycles, the list is empty unless include_unchanged is set.
  • analyze always returns an empty list.

cycles#

pub cycles: Vec<Cycle>

Every unique simple dependency cycle, one Cycle each. Empty when the project has no cycles. When it isn't empty, no task is moved.

conflicts#

pub conflicts: Vec<Conflict>

Constraint violations and non-working-time placements, one Conflict each. analyze always returns an empty list.

notes#

pub notes: Vec<Note>

Links, tasks and assignments dropped from the input, and fixes made under Validation::Repair, one Note each.

converged#

pub converged: bool

true when the planner reached a fixed point within SchedulingConfig::max_iterations. analyze always returns true.

analytics#

pub analytics: Option<Analytics>

Some when the call's AnalyticsOptions set slack or critical_path, otherwise None. Check Analytics::status before reading the data.

overallocations#

pub overallocations: Option<Vec<ResourceOverallocation>>

Some when the project has at least one assignment, None otherwise. Each entry lists one resource's overload windows; see OverallocationWindow.

profile#

pub profile: Option<LoadProfile>

Some only from preview when PreviewOptions::profile is set. It describes the load of the previewed dates. Every other call leaves it None. For the load of the accepted dates, call Project::load_profile.

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, 1, 5, 8, 0).ok_or("invalid date")?;

    let mut design = TaskData::new("design");
    design.start_date = Some(monday);
    design.duration = Some(2.0);

    let mut build = TaskData::new("build");
    build.start_date = Some(monday);
    build.duration = Some(3.0);

    let data = ProjectData {
        tasks: vec![design, 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 result = schedule(data, &SolveOptions::default())?;

    if !result.cycles.is_empty() {
        println!("{} dependency cycle(s), dates unchanged", result.cycles.len());
    }
    for conflict in &result.conflicts {
        println!("conflict: {conflict:?}");
    }
    if !result.converged {
        println!("planner did not converge");
    }

    // Only "build" moved: it now starts after "design" ends.
    for update in &result.updates {
        if let (Some(start), Some(end)) = (update.start_date, update.end_date) {
            println!("{}: {} - {}", update.id, start, end);
        }
    }
    Ok(())
}