JsonProject.evaluate#

Scores alternative change sets against the solved baseline of a JsonProject. The project isn't modified. Use it to compare options, such as which resource should take a task, before committing one with apply. Needs a solved project with no edits applied since the last solve or level.

Signature#

evaluate(candidates_json: string, options_json: string): string

Synchronous and read-only. Bad input comes back as an error envelope. The call doesn't throw.

Request#

Argument Payload Required Description
candidates_json Candidate[] yes The change sets to score, as a JSON array. "[]" scores the baseline only
options_json EvaluateOptions yes "{}" for defaults

Candidate#

One alternative to score. Unknown fields are rejected with a parse error.

Field Type Required Default Description
id Id yes Echoed in the matching CandidateResult. Uniqueness isn't checked
changes SchedulingChanges yes The edits to try. {} scores the plan as it is

EvaluateOptions#

The options object for evaluate. Every field can be omitted, and null reads as the default. Unknown fields are rejected with a parse error.

Field Type Required Default Description
baselineId string no absent (not checked) Must name the current baseline, or the call fails with stale-baseline
level LevelingOptions | false no false Levels each candidate after scheduling and adds the leveled stage. false scores the scheduled stage only. true is a validation error
focus Id | Id[] no [] Tasks to report finish and overload for. A single id is read as a one-item list
clearFirst boolean no false Scores against a copy of the baseline with leveling_delay and pauses cleared on the tasks in scope: level.scope.tasks when given, else every task

Behavior#

Each candidate runs on a disposable copy of the reference state: the accepted baseline, or its cleared copy with clearFirst.

  1. apply the candidate's changes.
  2. solve without data and measure the scheduled stage.
  3. With level set, level with those options and measure the leveled stage.
  • Every candidate starts from the reference state, never from the previous candidate.
  • A failure inside one candidate goes into that candidate's error and doesn't fail the call. That covers a rejected operation in its changes (the first one is reported) and a solve or level input error.
  • baselineId, each candidate's changes.baselineId, focus ids and level options are checked against the accepted state before any candidate runs.
  • The result has scores, not dates: no task updates and no overallocation windows. To see a candidate's dates, pass its changes to preview.

What the metrics measure and how cost is computed: Metrics and Evaluating Alternatives.

Note

Candidates are scored one after another.

Response#

The success envelope carries an EvaluateResult. Envelope shape: Wire format.

EvaluateResult#

The scores of the baseline and of every candidate.

Field Type Description
baselineId string The accepted baseline the candidates were scored against. Always present, also with clearFirst
baseline Metrics The reference state itself. All its deltas are 0
candidates CandidateResult[] One entry per candidate, in input order

CandidateResult#

The outcome of one candidate. A failed candidate has id and error only.

Field Type Description
id Id The candidate's id
error { path, message } Present when the candidate failed. path points into its changes, e.g. tasks.update[0]
scheduled Metrics The stage after apply and solve. Present unless the candidate failed
leveled LeveledMetrics The stage after level. Present only with level set, and absent when the candidate's plan is cyclic
cost number The cost of the last stage present. Lower is better. Absent when the candidate failed

Errors#

Kind Code When
project no-baseline The project has never been solved, or apply ran since the last solve
project stale-baseline options.baselineId or any candidate's changes.baselineId doesn't name the current baseline
validation null An unknown task in focus (path focus[i]), level options that are invalid for the baseline, or level: true (path: null)
parse null Malformed JSON, candidates_json isn't an array, or an unknown field in a candidate or the options

Failed candidates aren't errors: they come back in CandidateResult.error inside an ok: true envelope. See Error handling.

Example#

Compare leaving the plan as it is with moving task B from dev1 to the idle dev2. The third candidate updates a task that doesn't exist.

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

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" }, { id: "dev2" }],
  assignments: [
    { id: "aA", task: "A", resource: "dev1", value: 1 },
    { id: "aB", task: "B", resource: "dev1", value: 1 },
  ],
};

const candidates = [
  { id: "stay", changes: {} },
  { id: "dev2", changes: { assignments: { update: [{ id: "aB", resource: "dev2" }] } } },
  { id: "bad", changes: { tasks: { update: [{ id: "Z", duration: 1 }] } } },
];

const project = new JsonProject("{}");
try {
  const solved = JSON.parse(project.solve(JSON.stringify(data), "{}")).result;

  const options = { baselineId: solved.baselineId };
  const response = JSON.parse(
    project.evaluate(JSON.stringify(candidates), JSON.stringify(options)),
  );

  if (response.ok) {
    for (const candidate of response.result.candidates) {
      console.log(candidate.id, candidate.error ? candidate.error.message : candidate.cost);
    }
    // stay 2
    // dev2 0
    // bad unknown task id
  } else {
    const { kind, code, message, path } = response.error;
    console.error(kind, code, message, path);
  }
} finally {
  project.free();
}

candidates_json:

[
  { "id": "stay", "changes": {} },
  { "id": "dev2", "changes": { "assignments": { "update": [{ "id": "aB", "resource": "dev2" }] } } },
  { "id": "bad", "changes": { "tasks": { "update": [{ "id": "Z", "duration": 1 }] } } }
]

options_json (the token comes from the solve result):

{ "baselineId": "1-b0605af8cba3d7c9" }

Response:

{
  "ok": true,
  "result": {
    "baseline": {
      "conflicts": { "delta": 0, "value": 0 },
      "cost": 2,
      "cycles": { "delta": 0, "value": 0 },
      "finish": { "project": { "date": "2026-08-12 08:00", "delta": 0 }, "tasks": [] },
      "overload": {
        "focus": [],
        "resources": [{ "overload": { "delta": 0, "value": 2 }, "resource": "dev1" }],
        "total": { "delta": 0, "value": 2 }
      }
    },
    "baselineId": "1-b0605af8cba3d7c9",
    "candidates": [
      {
        "cost": 2,
        "id": "stay",
        "scheduled": {
          "conflicts": { "delta": 0, "value": 0 },
          "cost": 2,
          "cycles": { "delta": 0, "value": 0 },
          "finish": { "project": { "date": "2026-08-12 08:00", "delta": 0 }, "tasks": [] },
          "overload": {
            "focus": [],
            "resources": [{ "overload": { "delta": 0, "value": 2 }, "resource": "dev1" }],
            "total": { "delta": 0, "value": 2 }
          }
        }
      },
      {
        "cost": 0,
        "id": "dev2",
        "scheduled": {
          "conflicts": { "delta": 0, "value": 0 },
          "cost": 0,
          "cycles": { "delta": 0, "value": 0 },
          "finish": { "project": { "date": "2026-08-12 08:00", "delta": 0 }, "tasks": [] },
          "overload": {
            "focus": [],
            "resources": [{ "overload": { "delta": -2, "value": 0 }, "resource": "dev1" }],
            "total": { "delta": -2, "value": 0 }
          }
        }
      },
      {
        "error": { "message": "unknown task id", "path": "tasks.update[0]" },
        "id": "bad"
      }
    ]
  }
}