[Canonical page]()
[Full documentation index]()
# Accept messy input
Schedule project data that strict validation rejects, such as data exported from dhtmlxGantt with duplicate ids or missing dates, and get a list of every fix the engine made.
By default a [`Project`]() uses [`Validation::Strict`]() and returns [`Error::Input`]() for the first invalid field. `Validation::Repair` applies the same coercions dhtmlxGantt applies when it imports data, then schedules the result.
## Steps
1. Create the project with repair validation:
```rust
use plen_core::{Project, ProjectOptions, Validation};
let mut project = Project::new(ProjectOptions {
validation: Validation::Repair,
});
```
2. Pass your data to [`solve(Some(data), ...)`]() as usual. [`Project::analyze`]() with `Some(data)` repairs supplied data the same way. The one-shot [`schedule`]() function always uses strict validation, so use a `Project` here.
3. Read the `Note::Repaired { path, message }` entries in `result.notes` and 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:
```rust
use plen_core::{
ConstraintType, DateTime, Note, Project, ProjectData, ProjectOptions, SolveOptions, TaskData,
Validation,
};
fn main() -> Result<(), Box> {
let jan1 = DateTime::from_ymd_hm(2026, 1, 1, 0, 0).ok_or("invalid date")?;
let jan4 = DateTime::from_ymd_hm(2026, 1, 4, 0, 0).ok_or("invalid date")?;
// Start and end without a duration: valid, nothing to repair.
let mut design = TaskData::new("design");
design.start_date = Some(jan1);
design.end_date = Some(jan4);
// "build" appears twice. The later record wins.
let mut build_old = TaskData::new("build");
build_old.start_date = Some(jan1);
build_old.duration = Some(1.0);
let mut build = TaskData::new("build");
build.start_date = Some(jan1);
build.duration = Some(2.0);
// No dates, no duration, and a dated constraint without `constraint_date`.
let mut review = TaskData::new("review");
review.constraint_type = Some(ConstraintType::Snet);
// Three days between start and end, but `duration` says five.
let mut deploy = TaskData::new("deploy");
deploy.start_date = Some(jan1);
deploy.end_date = Some(jan4);
deploy.duration = Some(5.0);
let mut data = ProjectData {
tasks: vec![design, build_old, review, build, deploy],
..Default::default()
};
// 24/7 time keeps the example dates easy to check.
data.config.use_calendars = false;
let mut project = Project::new(ProjectOptions {
validation: Validation::Repair,
});
let options = SolveOptions {
include_unchanged: true,
..Default::default()
};
let result = project.solve(Some(data), &options)?;
for note in &result.notes {
if let Note::Repaired { path, message } = note {
println!("{path}: {message}");
}
}
for m in &result.updates {
if let (Some(start), Some(end), Some(duration)) = (m.start_date, m.end_date, m.duration) {
println!("{} {}..{} {}", m.id, start, end, duration);
}
}
Ok(())
}
```
## What repair changes
Repair reports each change below as one [`Note::Repaired`](), 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 [`Error::Input`]() 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 `Error::Input`. See [Error handling]().
- If repair or validation fails, [`solve`]() returns `Err` 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):
```text
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 [`Project::default()`](), the same [`solve`]() call fails with [`Error::Input`]().