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 JsonProject uses "validation": "strict" and returns a validation error envelope for the first invalid field. "validation": "repair" coerces invalid data instead (deduplicates ids, derives missing dates, fixes inconsistent dates), then schedules the result.

Steps#

  1. Create the project with repair validation:

    const project = new JsonProject(JSON.stringify({ validation: "repair" }));
    

    The constructor throws the error envelope as a string if the options are invalid, for example "validation": "fix".

  2. Pass your data to solve as usual. analyze with a data string repairs supplied data the same way. The one-shot schedule export always uses strict validation, so use a JsonProject here.

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

    { "kind": "repaired", "message": "duplicate id replaced the earlier record, retaining its position", "path": "tasks[3].id" }
    

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:

const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

const data = {
  // 24/7 time keeps the example dates easy to check.
  config: { use_calendars: false },
  tasks: [
    // Start and end without a duration: valid, nothing to repair.
    { id: "design", start_date: "2026-01-01 00:00", end_date: "2026-01-04 00:00" },
    // "build" appears twice. The later record wins.
    { id: "build", start_date: "2026-01-01 00:00", 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 00:00", duration: 2 },
    // Three days between start and end, but `duration` says five.
    { id: "deploy", start_date: "2026-01-01 00:00", end_date: "2026-01-04 00:00", duration: 5 },
  ],
};

const options = JSON.stringify({ includeUnchanged: true });

const project = new JsonProject(JSON.stringify({ validation: "repair" }));
try {
  const result = unwrap(project.solve(JSON.stringify(data), options));
  for (const note of result.notes) {
    if (note.kind === "repaired") {
      console.log(`${note.path}: ${note.message}`);
    }
  }
  for (const u of result.updates) {
    console.log(`${u.id} ${u.start_date}..${u.end_date} ${u.duration}`);
  }
} finally {
  project.free();
}

What repair changes#

Repair reports each change below as one repaired note in ScheduleResult.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 the 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 fails with a validation error 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 returns a validation error. See Error handling.
  • If repair or validation fails, solve returns an error envelope (ok: false) and the project keeps its previous state.

Result#

Run the example with node. It prints the fixes, then the scheduled dates:

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 JsonProject("{}"), the same solve call returns a validation error at tasks[2].constraint_date.