Validation#

Selects how a Project treats invalid ProjectData: reject it with an exception, or coerce it into valid data. It's set through the Validation property of ProjectOptions and applies to the data passed to Project.Solve and Project.Analyze.

Declaration#

namespace Plen;

public enum Validation
{
    Strict,
    Repair,
}

Variants#

Variant Description
Strict Default. Invalid input throws InputException with the path of the bad field. The rules are listed on ProjectData.
Repair Fixes the input listed below before scheduling and reports each change as a Note.Repaired in the result's Notes. Input it can't fix still throws InputException. The fixes are made on the project's own copy, so the caller's ProjectData isn't modified.

What Repair changes#

Each row is one fix. The note's Path uses wire spelling and points at the field that changed. For duplicate ids the index is the record's position in the input. For the other fixes it's the position after duplicates are removed.

Input Fix Note Path
Duplicate Id in Tasks, Links, Resources, Assignments or Calendars The last record wins and takes the position of the first one tasks[i].id, links[i].id, ...
Invalid Pauses on a task The task's pause list is cleared tasks[i].pauses
Dated ConstraintType without ConstraintDate ConstraintType is set to null tasks[i].constraint_type
Task with neither StartDate nor EndDate StartDate is set to Config.ProjectStart, or to the earliest date of any task when ProjectStart is null tasks[i].start_date
Task (not a milestone) with no Duration and only one of its dates Duration is set to 1 tasks[i].duration
StartDate, EndDate and Duration that don't agree The dates win: Duration becomes the working time between them. When no duration from the start lands on the end (an end in non-working time, or dates computed on other calendars), Duration is set to null and the task is resolved from its dates tasks[i].duration
EndDate inside a work gap of a task with inconsistent dates The end moves to the start of the gap tasks[i].end_date
Summary with only one of its dates The missing date is computed from the other date and the duration tasks[i].start_date or tasks[i].end_date
Assignment Offset that isn't finite or is negative Offset is set to null assignments[i].offset
Assignment Duration that isn't finite or isn't positive Duration is set to null assignments[i].duration

Unscheduled tasks and summaries without dates stay without dates. Repair never falls back to the current date. When an undated task needs a start and the project has neither Config.ProjectStart nor any dated task, the call throws InputException with Path tasks[i].start_date.

Example#

using Plen;

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

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("design") { StartDate = monday, Duration = 2.0 },
        // Same id as the first task: this record replaces it.
        new TaskData("design") { StartDate = monday, Duration = 3.0 },
        // No dates: the start defaults to the earliest task date.
        new TaskData("build") { Duration = 2.0 },
    ],
};

var project = new Project(new ProjectOptions { Validation = Validation.Repair });
var result = project.Solve(data, new SolveOptions());

foreach (var note in result.Notes)
{
    if (note is Note.Repaired repaired)
        Console.WriteLine($"{repaired.Path}: {repaired.Message}");
}
// tasks[1].id: duplicate id replaced the earlier record, retaining its position
// tasks[1].start_date: missing date defaulted to project anchor