Accept messy input#
Schedule project data that strict validation rejects, such as data exported from a UI with duplicate ids or missing dates, and get a list of every fix the engine made.
By default a Project uses Validation.Strict and throws InputException for the first invalid field. Validation.Repair coerces the data instead (deduplicates ids, derives missing dates and durations, fixes inconsistent date triples), then schedules the result.
Steps#
-
Create the project with repair validation:
var project = new Project(new ProjectOptions { Validation = Validation.Repair }); -
Pass your data to
Solve(data, …)as usual.Project.Analyzewith non-nulldatarepairs supplied data the same way. The one-shotEngine.Schedulealways uses strict validation, so use aProjecthere. -
Read the
Note.Repairedentries inresult.Notesand show them to the user, or log them. Each one has aPathand aMessage.
The full example below has four kinds of problems: a duplicate id, a task with no dates, a dated constraint with no date, and a task whose start, end and duration disagree. Strict validation rejects this data. Repair schedules it:
using Plen;
var jan1 = Moment.FromYmdHm(2026, 1, 1, 0, 0) ?? throw new InvalidOperationException("invalid date");
var jan4 = Moment.FromYmdHm(2026, 1, 4, 0, 0) ?? throw new InvalidOperationException("invalid date");
var data = new ProjectData
{
Tasks =
[
// Start and end without a duration: valid, nothing to repair.
new TaskData("design") { StartDate = jan1, EndDate = jan4 },
// "build" appears twice. The later record wins.
new TaskData("build") { StartDate = jan1, Duration = 1.0 },
// No dates, no duration, and a dated constraint without ConstraintDate.
new TaskData("review") { ConstraintType = ConstraintType.Snet },
new TaskData("build") { StartDate = jan1, Duration = 2.0 },
// Three days between start and end, but Duration says five.
new TaskData("deploy") { StartDate = jan1, EndDate = jan4, Duration = 5.0 },
],
};
// 24/7 time keeps the example dates easy to check.
data.Config.UseCalendars = false;
var project = new Project(new ProjectOptions { Validation = Validation.Repair });
var result = project.Solve(data, new SolveOptions { IncludeUnchanged = true });
foreach (var note in result.Notes)
{
if (note is Note.Repaired repaired)
{
Console.WriteLine($"{repaired.Path}: {repaired.Message}");
}
}
foreach (var m in result.Updates)
{
Console.WriteLine($"{m.Id} {m.StartDate}..{m.EndDate} {m.Duration}");
}
What repair changes#
Repair reports each change below as one Note.Repaired, with the wire-spelled field path in Path:
- Duplicate ids in
Tasks,Links,Resources,AssignmentsandCalendars: the last record replaces the earlier one and takes its position. The note is attasks[i].id, whereiis the index of the duplicate in your input. - A task with no start and no end gets
StartDateset toConfig.ProjectStart, or to the earliest date of any task ifProjectStartisn't set. - A leaf task (not a milestone) with no
Durationand only one date getsDuration = 1. - Start, end and duration that disagree: the dates win, and the duration becomes the working time between them. If the end is in non-working time, or the dates were computed on a different calendar, the duration is dropped and the task is resolved from its dates. An end that falls inside a pause moves to the pause start, with a second note at
tasks[i].end_date. - A summary with only one date gets the other date computed from that date and its duration.
- A dated constraint (
Snet,Snlt,Fnet,Fnlt,Mso,Mfo) withoutConstraintDateis cleared. - Invalid
Pauseson a task: the whole list is cleared, with a note attasks[i].pauses. - An invalid assignment
OffsetorDurationis cleared, so the assignment books the whole task.
A task that already has two of start, end and duration is valid input. Repair doesn't touch it and doesn't report it.
Only the duplicate-id notes use input indexes. Every other note indexes the deduplicated list. In the example, the two build records become one, so deploy is tasks[4] in your input but tasks[3] in its note.
What repair doesn't fix#
- An entirely undated project, with no
Config.ProjectStartand no task that has a date, still throwsInputExceptionwithPathtasks[i].start_date. The engine never falls back to today's date. - Every other invalid input, such as a cycle in the
Parenthierarchy or a calendar with no working time, still throwsInputException. See Error handling. - If repair or validation fails,
Solvethrows and the project keeps its previous state and cached result.
Result#
The example prints the fixes, then the scheduled dates (the date lines may come in a different order):
tasks[3].id: duplicate id replaced the earlier record, retaining its position
tasks[2].constraint_type: dated constraint without date cleared
tasks[2].start_date: missing date defaulted to project anchor
tasks[2].duration: missing duration defaulted to one
tasks[3].duration: duration recomputed from supplied start and end
design 2026-01-01 00:00..2026-01-04 00:00 3
build 2026-01-01 00:00..2026-01-03 00:00 2
review 2026-01-01 00:00..2026-01-02 00:00 1
deploy 2026-01-01 00:00..2026-01-04 00:00 3
build uses the second record's duration of 2, and deploy has a duration of 3, taken from its dates. With new Project(), the same Solve call throws InputException at tasks[2].constraint_date.