Accept messy input#

Schedule project data that strict validation rejects, such as data exported from another tool 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 an InputError for the first invalid field. validation: "repair" coerces the data into valid input, then schedules the result.

Steps#

  1. Create the project with repair validation:

    import { Project } from "plen-js";
    
    const project = new Project({ validation: "repair" });
    

    new Project(options) throws an InputError if an option is unknown or has an invalid value.

  2. Pass your data to project.solve(data, options) as usual. project.analyze(data) repairs supplied data the same way. The one-shot schedule function always uses strict validation, so use a Project here.

  3. Read the entries of result.notes with kind: "repaired". Each one has a path and a message. Show them to the user, or log them.

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:

import { Project } from "plen-js";

const data = {
  tasks: [
    // Start and end without a duration: valid, nothing to repair.
    { id: "design", start_date: "2026-01-01", end_date: "2026-01-04" },
    // "build" appears twice. The later record wins.
    { id: "build", start_date: "2026-01-01", duration: 1 },
    // No dates, no duration, and a dated constraint without constraint_date.
    { id: "review", constraint_type: "snet" },
    { id: "build", start_date: "2026-01-01", duration: 2 },
    // Three days between start and end, but duration says five.
    { id: "deploy", start_date: "2026-01-01", end_date: "2026-01-04", duration: 5 },
  ],
  // 24/7 time keeps the example dates easy to check.
  config: { use_calendars: false },
};

const project = new Project({ validation: "repair" });
const result = project.solve(data, { includeUnchanged: true });

for (const note of result.notes) {
  if (note.kind === "repaired") {
    console.log(`${note.path}: ${note.message}`);
  }
}
for (const update of result.updates) {
  console.log(update.id, `${update.start_date}..${update.end_date}`, update.duration);
}

Repair works on a copy. Your data object stays as you passed it, duplicates included.

What repair changes#

Repair reports each change below as one repaired note in result.notes, with the field path in path:

  • Duplicate ids in tasks, links, resources, assignments and calendars: the last record replaces the earlier one and takes its position. The note is at tasks[i].id, where i is the index of the duplicate in your input.
  • A task with no start and no end gets start_date set to config.project_start, or to the earliest date of any task if project_start isn't set.
  • A leaf task (not a milestone) with no duration and only one date gets duration: 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") without constraint_date is cleared.
  • Invalid pauses on a task: the whole list is cleared, with a note at tasks[i].pauses.
  • An invalid assignment offset or duration is 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.project_start and no task that has a date, still throws an InputError at tasks[i].start_date. The engine never falls back to today's date.
  • Every other invalid input, such as a cycle in the parent hierarchy or a calendar with no working time, still throws an InputError. See Error handling.
  • If repair or validation fails, solve throws and the project keeps its previous state.

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 an InputError at tasks[2].constraint_date.