Recover from rejected changes#
Get an editor working again after apply rejects part of a change set, without restarting the app or losing the user's edits.
When apply rejects an operation, the project sets needs_resync. From then on, solve(None, ..), apply and level return ProjectError::NeedsResync, and reads that need a baseline (preview, load_profile, evaluate, suggest_assignments) return ProjectError::NoBaseline. To clear it, reload the full data with solve(Some(data), ..). See Data Updates for why.
Prerequisites#
- A long-lived
Projectsolved once withsolve(Some(data), ..). - Your own model that can produce the full
ProjectDatathe user expects to see. This is the data you resync from.
Steps#
-
Apply the change set and keep the
ApplyReport:let report = project.apply?;An
Errhere means nothing was applied (NeedsResyncfrom an earlier rejection, orStaleBaseline). A rejection inside the batch isn't anErr. -
Check
needs_resync. If it'sfalse, continue as usual withsolve(None, ..):if !report.needs_resync -
Show the user what was refused. Each
RejectedOperationhas the operation'spath(such astasks.remove[0]) and amessage:for op in &report.rejectedThe other operations in the batch were applied. A rejected operation leaves no partial edit behind.
-
Make your model hold the state you want. Undo the rejected edits in it, or keep it as is if it's already correct. Don't try to repair the project with more
applycalls: they fail withNeedsResync. -
Resync with the full data. Set
include_unchangedso the result carries the dates of every task, not only the ones that moved:let resync = SolveOptions ; let result = project.solve?; -
Write every entry of
result.updatesback into your UI. The engine's state may differ from what the UI shows, so refresh all task dates, not just the edited ones. -
Take the new
baseline_idfromresultfor the nextpreviewor change set. Tokens from before the resync are stale.
The full example below stretches one task and, in the same batch, removes a task the engine doesn't have:
use ;
When the resync data is invalid#
The resync goes through the same validation as the first load. If your model produces invalid data, such as a duplicate task id, solve(Some(data), ..) returns Error::Input with the field path, for example tasks[1].id. The project keeps its previous state and still returns NeedsResync. Fix the data and call solve(Some(data), ..) again. See Error handling for the full list of validation errors.
Drop the edit instead of resyncing#
To avoid a full reload, apply the change set to a clone first. A clone is cheap, and a rejection there doesn't touch the original:
let mut draft = project.clone;
let report = draft.apply?;
if report.needs_resync else
Use this when the user should either get the whole change set or none of it.
Result#
The example prints the rejected removal on stderr and the refreshed date of every task on stdout:
rejected tasks.remove[0]: unknown task id
design 2026-01-05 00:00..2026-01-09 00:00
After the resync, apply, solve(None, ..), level and preview work again, and report.needs_resync is false for the next valid change set.