ScheduleResult#

The output of one scheduling call: the tasks that changed, plus the cycles, conflicts and notes found on the way. Project.Solve, Project.Preview, Project.Analyze and Engine.Schedule return one; Project.Level returns one in LevelingResult.Schedule. Cycles, conflicts and non-convergence are reported here as data, not thrown.

Declaration#

namespace Plen;

public sealed class ScheduleResult : IDeepCloneable<ScheduleResult>, IEquatable<ScheduleResult>
{
    public string? BaselineId { get; set; }
    public List<Mutation> Updates { get; set; } = [];
    public List<Cycle> Cycles { get; set; } = [];
    public List<Conflict> Conflicts { get; set; } = [];
    public List<Note> Notes { get; set; } = [];
    public bool Converged { get; set; }
    public Analytics? Analytics { get; set; }
    public List<ResourceOverallocation>? Overallocations { get; set; }
    public LoadProfile? Profile { get; set; }

    public ScheduleResult DeepClone();
    public bool Equals(ScheduleResult? other);
}

A ScheduleResult is read-only by contract. The project keeps the result of Solve and Level as its baseline, returns the same object from an unchanged re-solve, shares it with its clones, and reads it in Evaluate and SuggestAssignments. Call DeepClone before changing one.

Properties#

Property Type Description
BaselineId string? Token of the baseline this result produced or read. null when there is none.
Updates List<Mutation> Tasks with new dates or fields. Only changed tasks unless IncludeUnchanged is set.
Cycles List<Cycle> Every unique dependency cycle in the project. Empty for an acyclic project.
Conflicts List<Conflict> Constraint violations and calendar conflicts.
Notes List<Note> Non-fatal input notes: dropped links, tasks and assignments, and repairs.
Converged bool true when the planner reached a fixed point within the iteration limit.
Analytics Analytics? Critical path and slack. null unless the call's options request them.
Overallocations List<ResourceOverallocation>? Overallocated resources. null when the project has no assignments.
Profile LoadProfile? Resource load of the previewed dates. Set only by Preview with a Profile query.

BaselineId#

public string? BaselineId { get; set; }

The opaque token of the accepted baseline:

  • Solve and Level set the baseline they accepted.
  • Preview sets the baseline it read.
  • Analyze sets it only when called with null data on a solved project without unsolved edits.
  • Otherwise it's null.

Pass the token as BaselineId in options and change sets to detect a stale baseline. Tokens are unique per process: don't store them or compare them across runs. With IncludeUnchanged, the extra entries in Updates don't change the token.

Updates#

public List<Mutation> Updates { get; set; } = [];

One Mutation per reported task, carrying only the fields that changed. With SolveOptions.IncludeUnchanged, every scheduled task is reported. Updates is empty when:

  • Cycles isn't empty and IncludeUnchanged is false, because a cyclic project keeps its dates
  • the result comes from Analyze, which doesn't schedule

What the entries are relative to depends on the call; see Project.Preview and Project.Level.

Cycles#

public List<Cycle> Cycles { get; set; } = [];

One Cycle per unique simple dependency cycle. Rotations of the same cycle are reported once; two cycles that differ only in a parallel link are reported separately. Detection includes fixed and completed tasks and summary containment. When the list isn't empty, the call leaves all dates unchanged.

The number of cycles can grow exponentially in a densely cyclic graph.

Conflicts#

public List<Conflict> Conflicts { get; set; } = [];

Constraint violations and calendar conflicts found by the planner. Always empty from Analyze. The variants are on the Conflict page.

Notes#

public List<Note> Notes { get; set; } = [];

Input the engine ignored or fixed instead of rejecting. The variants are on the Note page.

Converged#

public bool Converged { get; set; }

false when the planner stopped at SchedulingConfig.MaxIterations before its dates settled. The result is still valid. Always true from Analyze.

Analytics#

public Analytics? Analytics { get; set; }

null when neither Slack nor CriticalPath is set in the call's AnalyticsOptions. Otherwise an Analytics value; check its Status before reading the data.

Overallocations#

public List<ResourceOverallocation>? Overallocations { get; set; }

null when the project has no assignments. Otherwise one ResourceOverallocation per overallocated resource; the list is empty when no resource is overallocated. Windows carry a Reason only in a result from Level.

Profile#

public LoadProfile? Profile { get; set; }

Set only by Project.Preview when PreviewOptions.Profile is set; null otherwise. To read the load of the solved baseline, call Project.LoadProfile.

Methods#

DeepClone#

public ScheduleResult DeepClone()

Returns a copy that shares no list or nested object with this one. Use it before changing a result.

Equals#

public bool Equals(ScheduleResult? other)
public override bool Equals(object? obj)

Value equality: all properties are equal, with lists compared element by element. GetHashCode is consistent with it.

ToString#

public override string ToString()

Returns a readable dump of all properties, for logging.

Cycle#

One dependency cycle: the tasks and links that form it.

public sealed class Cycle : IDeepCloneable<Cycle>, IEquatable<Cycle>
{
    public List<Id> Tasks { get; set; } = [];
    public List<Id> Links { get; set; } = [];
}
Property Type Description
Tasks List<Id> Ids of the tasks on the cycle.
Links List<Id> Ids of the links on the cycle.

Cycle has value equality (both lists compared element by element), DeepClone() and ToString(), like ScheduleResult.

Example#

Solve a project with a dependency cycle and read the result:

using Plen;

var monday = Moment.FromYmdHm(2026, 1, 5, 8, 0)
    ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("a") { StartDate = monday, Duration = 2.0 },
        new TaskData("b") { StartDate = monday, Duration = 3.0 },
    ],
    Links =
    [
        new LinkData { Id = "l1", Source = "a", Target = "b" },
        new LinkData { Id = "l2", Source = "b", Target = "a" },
    ],
};

var result = new Project().Solve(data, new SolveOptions());

foreach (var cycle in result.Cycles)
    Console.WriteLine($"cycle: tasks {string.Join(", ", cycle.Tasks)}; links {string.Join(", ", cycle.Links)}");

Console.WriteLine($"updates: {result.Updates.Count}, converged: {result.Converged}");

foreach (var update in result.Updates)
    Console.WriteLine($"{update.Id}: {update.StartDate} - {update.EndDate}");