Preview leveling#

Show the user what leveling would change before it touches the project, then keep the leveled plan or throw it away.

level commits. It writes leveling_delay and pauses into the project and accepts the leveled dates as the new baseline. To get a proposal instead, level a clone(). A clone is cheap because the data stays shared until one copy is edited. It's a separate handle in wasm memory, though, so free it like any other.

Prerequisites#

Steps#

  1. Clone the solved project:

    const draft = project.clone();
    
  2. Level the clone. The original keeps its dates, its baseline and its overallocation:

    const proposal = unwrap(draft.level("{}"));
    

    unwrap parses the envelope and throws on ok: false. It's defined in the full example below. If the call fails, free draft before you rethrow.

  3. Show the proposal. proposal.updates lists the tasks leveling moved, with their new dates, leveling_delay and, when they changed, pauses. The diff is relative to the state the call started from. That state matches the original, so you can apply the entries to the model you already hold.

    for (const update of proposal.updates) {
      console.log(
        `${update.id}: start ${update.start_date}, delay ${update.leveling_delay}, pauses ${JSON.stringify(update.pauses)}`,
      );
    }
    
  4. Show what leveling couldn't fix. proposal.overallocations holds the windows left after leveling. Each window has a reason, such as fixed-task or priority-1000. Both overallocations and windows can be absent, so default them:

    for (const over of proposal.overallocations ?? []) {
      for (const window of over.windows ?? []) {
        console.log(`${over.resource}: ${window.from} .. ${window.to}, ${window.reason}`);
      }
    }
    
  5. Apply the user's choice:

    • Accept: free the original and keep the clone in its place. The clone holds the leveled baseline.
    • Reject: free draft. Nothing else needs undoing.
    if (userAccepts) {
      project.free();
      project = draft;
    } else {
      draft.free();
    }
    

    Swap only if the original hasn't been edited since you cloned it. Otherwise the proposal was computed from an older state, so level a new clone.

The full example levels two tasks that book the same developer full time on Monday and Tuesday:

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;
}

// Two 2-day tasks, both booked full time on dev1 from Monday, August 10, 2026,
// on the default calendar.
const data = {
  tasks: [
    { id: "A", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "B", start_date: "2026-08-10 08:00", duration: 2 },
  ],
  resources: [{ id: "dev1" }],
  assignments: [
    { task: "A", resource: "dev1", value: 1 },
    { task: "B", resource: "dev1", value: 1 },
  ],
};

let project = new JsonProject("{}");
try {
  unwrap(project.solve(JSON.stringify(data), "{}"));

  const draft = project.clone();
  let proposal;
  try {
    proposal = unwrap(draft.level("{}"));
  } catch (error) {
    draft.free();
    throw error;
  }

  for (const update of proposal.updates) {
    console.log(`${update.id}: ${update.start_date} .. ${update.end_date}, delay ${update.leveling_delay}`);
  }
  const left = (proposal.overallocations ?? []).filter((o) => (o.windows ?? []).length > 0).length;
  console.log(`overallocated resources left: ${left}`);

  const userAccepts = true;
  if (userAccepts) {
    project.free();
    project = draft;
  } else {
    draft.free();
  }

  const plan = unwrap(project.solve(undefined, "{}"));
  console.log(`baseline matches proposal: ${plan.baselineId === proposal.baselineId}`);
} finally {
  project.free();
}

Result#

Run the example with node. It prints the one task leveling moved, and nothing is left overallocated:

B: 2026-08-12 08:00 .. 2026-08-14 08:00, delay 2
overallocated resources left: 0
baseline matches proposal: true

B waits two days for A, so its leveling_delay is 2. Until you swap, the original still reports the Monday-Tuesday window on dev1, and a solve without data on it returns no updates. After the swap, baselineId is the token from the leveling result.

The clone is a full solved project, so you can also call loadProfile on it to draw the leveled load. See Resource load. To compare leveling across several alternative change sets with scores, use evaluate with the level option of EvaluateOptions instead. See Evaluating alternatives. For options, reasons and splits, see Leveling resources.