From 6c9ff4ae347d87e4282129b56a2d5ffeb6a0599b Mon Sep 17 00:00:00 2001
From: Nessar
Date: Sun, 18 Oct 2026 21:18:40 +0200
Subject: [PATCH] test(planning): rebuild the frozen engine from the obligation
ranking
The frozen engine of the differential check is rebuilt from 8168137,
which plans from the best-ranked obligation solution, with the
documented build command. The check passes again, including its
cancelled asynchronous comparisons and generator-coverage assertions.
The calibration engine stays at a871f8f.
Part of #302.
Co-Authored-By: Claude Opus 5.5
---
docs/benchmark/README.md | 7 +-
.../leave-planner.core.frozen.mjs | 952 +++++++++++-------
2 files changed, 621 insertions(+), 338 deletions(-)
diff --git a/docs/benchmark/README.md b/docs/benchmark/README.md
index 499893b..29f998a 100644
--- a/docs/benchmark/README.md
+++ b/docs/benchmark/README.md
@@ -120,8 +120,11 @@ trace counters and timings are historical; current evidence is in
`src/app/services/frozen-engine.spec.ts` runs 5,000 seeded random planning
requests (seeds 280000 to 284999, one test per 200 seeds) through the current
-engine and through a frozen copy of the engine at `a871f8f`, develop after
-PR #278. For each request, the planning outcome, stable plan explanation,
+engine and through a frozen copy of the engine. The copy was rebuilt for #302
+from `8168137`, which plans from the best-ranked obligation solution
+([comparison record](issue-302.md#comparison-record)). Until then it was the
+engine at `a871f8f`, develop after PR #278, which remains the calibration
+engine. For each request, the planning outcome, stable plan explanation,
explanation error and final-state snapshot must be strictly deep-equal,
including `PlanningConflict` and `InvalidInput` outcomes. Timings are not
compared.
diff --git a/src/app/services/frozen-engine/leave-planner.core.frozen.mjs b/src/app/services/frozen-engine/leave-planner.core.frozen.mjs
index 54ef602..7ac350d 100644
--- a/src/app/services/frozen-engine/leave-planner.core.frozen.mjs
+++ b/src/app/services/frozen-engine/leave-planner.core.frozen.mjs
@@ -195,10 +195,7 @@ var OPTIMIZATION_PRESETS = {
}
};
var nonWorkingDateCounts = new WeakMap;
-function evaluateOptimization(configuration, observations) {
- const validation = validateOptimizationConfiguration(configuration);
- if (!validation.valid)
- throw new Error(`${validation.code}: ${validation.message}`);
+function evaluateValidatedOptimization(configuration, observations) {
const selected = (condition) => configuration.conditions.includes(condition);
let values;
switch (configuration.elements) {
@@ -248,14 +245,445 @@ function compareObjectiveLists(left, right) {
return index < 0 ? 0 : compareObjectiveValues(left[index].direction, left[index].value, right[index].value);
}
+// src/app/services/plan-observations.ts
+var DAY_MS = 86400000;
+function utcDate(key) {
+ return new Date(`${key}T00:00:00Z`);
+}
+function addDays(key, days) {
+ return new Date(utcDate(key).getTime() + days * DAY_MS).toISOString().slice(0, 10);
+}
+function compareDate(a, b) {
+ return a < b ? -1 : a > b ? 1 : 0;
+}
+function dateRange(start, endExclusive) {
+ const result = [];
+ for (let date = start;compareDate(date, endExclusive) < 0; date = addDays(date, 1))
+ result.push(date);
+ return result;
+}
+var calendarPeriodCache = new WeakMap;
+var occupancies = new WeakMap;
+var derivations = new WeakMap;
+var shadowMode = globalThis.process?.env?.["VAC_OBSERVATION_SHADOW"] === "1";
+function note(parent, child, op, date) {
+ occupancies.delete(child);
+ if (parent === child)
+ return;
+ derivations.set(child, { parent, op, date });
+}
+function derivePlacements(parent, child) {
+ note(parent, child, "same");
+}
+function notePlacementAdded(parent, child, date) {
+ note(parent, child, "add", date);
+}
+function notePlacementRemoved(parent, child, date) {
+ note(parent, child, "remove", date);
+}
+function insertionPoint(sorted, date) {
+ let low = 0;
+ let high = sorted.length;
+ while (low < high) {
+ const mid = low + high >> 1;
+ if (sorted[mid] < date)
+ low = mid + 1;
+ else
+ high = mid;
+ }
+ return low;
+}
+function occupancyFromScratch(placements, context) {
+ const dates = [...placements.keys()].sort();
+ return { calendar: context, dates, groups: groupsFromScratch(dates, context), observed: {} };
+}
+function deriveOccupancy(parent, { op, date }, context) {
+ if (op === "same")
+ return parent;
+ const at = insertionPoint(parent.dates, date);
+ const present = parent.dates[at] === date;
+ if (present === (op === "add"))
+ return;
+ const dates = parent.dates.slice();
+ if (op === "add")
+ dates.splice(at, 0, date);
+ else
+ dates.splice(at, 1);
+ const touchesGroups = date >= context.horizon.start && date < context.horizon.end && !context.declaredDates.has(date);
+ if (touchesGroups && !context.workingCalendar.has(date))
+ return;
+ const groups = !touchesGroups ? parent.groups : op === "add" ? groupsWithDate(parent.groups, date, context) : groupsWithoutDate(parent.groups, date, context);
+ if (!groups)
+ return;
+ const observed = {};
+ if (parent.observed.publicHolidayWeeks)
+ observed.publicHolidayWeeks = derivedHolidayWeeks(parent.observed.publicHolidayWeeks, dates, date, context);
+ return { calendar: context, dates, groups, observed };
+}
+var weeksByDate = new WeakMap;
+function derivedHolidayWeeks(weeks, dates, date, context) {
+ let index = weeksByDate.get(context);
+ if (!index) {
+ index = new Map;
+ context.holidayWeeks.forEach((week, i) => week.dates.forEach((d) => {
+ const list = index.get(d);
+ if (list)
+ list.push(i);
+ else
+ index.set(d, [i]);
+ }));
+ weeksByDate.set(context, index);
+ }
+ const affected = index.get(date);
+ if (!affected)
+ return weeks;
+ const next = weeks.slice();
+ for (const i of affected) {
+ const week = context.holidayWeeks[i];
+ const complete = week.dates.every((d) => {
+ const at = insertionPoint(dates, d);
+ return dates[at] === d || context.declaredDates.has(d);
+ });
+ if (complete !== weeks[i].complete)
+ next[i] = Object.freeze({ value: week.value, complete });
+ }
+ return Object.freeze(next);
+}
+function groupsWithDate(groups, date, context) {
+ let low = 0;
+ let high = groups.length;
+ while (low < high) {
+ const mid = low + high >> 1;
+ if (groups[mid].anchors[0] < date)
+ low = mid + 1;
+ else
+ high = mid;
+ }
+ const previous = low > 0 ? groups[low - 1] : undefined;
+ const next = low < groups.length ? groups[low] : undefined;
+ const index = context.calendarIndex.get(date);
+ const joinsPrevious = !!previous && context.workingPrefix[index] - context.workingPrefix[context.calendarIndex.get(previous.anchors[previous.anchors.length - 1]) + 1] === 0;
+ const joinsNext = !!next && context.workingPrefix[context.calendarIndex.get(next.anchors[0])] - context.workingPrefix[index + 1] === 0;
+ const merged = {
+ anchors: [
+ ...joinsPrevious ? previous.anchors : [],
+ date,
+ ...joinsNext ? next.anchors : []
+ ],
+ startIndex: joinsPrevious ? previous.startIndex : extendStart(index, context),
+ endIndex: joinsNext ? next.endIndex : extendEnd(index, context)
+ };
+ const from = joinsPrevious ? low - 1 : low;
+ return [...groups.slice(0, from), merged, ...groups.slice(joinsNext ? low + 1 : low)];
+}
+function groupsWithoutDate(groups, date, context) {
+ let low = 0;
+ let high = groups.length;
+ while (low < high) {
+ const mid = low + high >> 1;
+ if (groups[mid].anchors[0] <= date)
+ low = mid + 1;
+ else
+ high = mid;
+ }
+ const group = groups[low - 1];
+ if (!group)
+ return;
+ const cut = insertionPoint(group.anchors, date);
+ if (group.anchors[cut] !== date)
+ return;
+ const leftAnchors = group.anchors.slice(0, cut);
+ const rightAnchors = group.anchors.slice(cut + 1);
+ const split = [];
+ if (leftAnchors.length)
+ split.push({
+ anchors: leftAnchors,
+ startIndex: group.startIndex,
+ endIndex: extendEnd(context.calendarIndex.get(leftAnchors[leftAnchors.length - 1]), context)
+ });
+ if (rightAnchors.length)
+ split.push({
+ anchors: rightAnchors,
+ startIndex: extendStart(context.calendarIndex.get(rightAnchors[0]), context),
+ endIndex: group.endIndex
+ });
+ return [...groups.slice(0, low - 1), ...split, ...groups.slice(low)];
+}
+function occupancyOf(placements, context) {
+ const chain = [];
+ let current = placements;
+ let occupancy;
+ for (;; ) {
+ const known = occupancies.get(current);
+ if (known?.calendar === context) {
+ occupancy = known;
+ break;
+ }
+ const derivation = derivations.get(current);
+ if (!derivation)
+ break;
+ chain.push([current, derivation]);
+ current = derivation.parent;
+ }
+ if (!occupancy) {
+ occupancy = occupancyFromScratch(current, context);
+ occupancies.set(current, occupancy);
+ }
+ for (let i = chain.length - 1;i >= 0; i--) {
+ const [map, derivation] = chain[i];
+ derivations.delete(map);
+ occupancy = deriveOccupancy(occupancy, derivation, context) ?? occupancyFromScratch(map, context);
+ occupancies.set(map, occupancy);
+ }
+ return occupancy;
+}
+function tieKeyOf(occupancy) {
+ return occupancy.tieKey ??= occupancy.dates.join("|");
+}
+function placementTieKey(state, context) {
+ const occupancy = occupancyOf(state.placements, context);
+ if (shadowMode)
+ verifyOccupancy(state, context, occupancy);
+ return tieKeyOf(occupancy);
+}
+function expiryTieKey(state) {
+ return [...state.placements.values()].map((placement) => state.balances.find((item) => item.id === placement.balanceId)?.expiryDate ?? "").sort(compareDate).join("|");
+}
+function isAtRisk(balance, context) {
+ return compareDate(balance.expiryDate, context.horizon.start) >= 0 && compareDate(balance.expiryDate, context.horizon.end) <= 0;
+}
+function isNaturalNonWorkingDate(date, context) {
+ return context.calendarIndex.has(date) ? !context.workingCalendar.has(date) : !isWorkingPlanningDate(date);
+}
+function calendarDateAt(index, context) {
+ return context.calendarDays[index] ?? addDays(context.calendarDays[0], index);
+}
+function extendStart(index, context) {
+ while (context.declaredDates.has(calendarDateAt(index - 1, context)) || isNaturalNonWorkingDate(calendarDateAt(index - 1, context), context)) {
+ context.check();
+ index--;
+ }
+ return index;
+}
+function extendEnd(index, context) {
+ while (context.declaredDates.has(calendarDateAt(index + 1, context)) || isNaturalNonWorkingDate(calendarDateAt(index + 1, context), context)) {
+ context.check();
+ index++;
+ }
+ return index;
+}
+function groupsFromScratch(placementDates, context) {
+ const dates = [...new Set([...placementDates, ...context.declaredDates])].filter((date) => date >= context.horizon.start && date < context.horizon.end).sort(compareDate);
+ if (!dates.length)
+ return [];
+ const groups = [];
+ let group = [dates[0]];
+ for (let i = 1;i < dates.length; i++) {
+ context.check();
+ const previousIndex = context.calendarIndex.get(dates[i - 1]);
+ const currentIndex = context.calendarIndex.get(dates[i]);
+ const workingInGap = context.workingPrefix[currentIndex] - context.workingPrefix[previousIndex + 1];
+ if (workingInGap === 0)
+ group.push(dates[i]);
+ else {
+ groups.push(group);
+ group = [dates[i]];
+ }
+ }
+ groups.push(group);
+ return groups.map((anchors) => {
+ context.check();
+ return {
+ anchors,
+ startIndex: extendStart(context.calendarIndex.get(anchors[0]), context),
+ endIndex: extendEnd(context.calendarIndex.get(anchors[anchors.length - 1]), context)
+ };
+ });
+}
+function calendarPeriod(startIndex, endIndex, context) {
+ let periods = calendarPeriodCache.get(context);
+ if (!periods) {
+ periods = new Map;
+ calendarPeriodCache.set(context, periods);
+ }
+ const key = startIndex * 1048576 + endIndex;
+ let period = periods.get(key);
+ if (period)
+ periods.delete(key);
+ else {
+ const start = calendarDateAt(startIndex, context);
+ const end = calendarDateAt(endIndex, context);
+ const dates = startIndex >= 0 && endIndex < context.calendarDays.length ? context.calendarDays.slice(startIndex, endIndex + 1) : dateRange(start, addDays(end, 1));
+ period = Object.freeze({
+ start,
+ end,
+ duration: dates.length,
+ nonWorkingDates: Object.freeze(dates.filter((date) => isNaturalNonWorkingDate(date, context))),
+ declaredDates: Object.freeze(dates.filter((date) => context.declaredDates.has(date)))
+ });
+ }
+ periods.set(key, period);
+ if (periods.size > 4096)
+ periods.delete(periods.keys().next().value);
+ return period;
+}
+function periodsOf(state, groups, context) {
+ return groups.map((group) => {
+ const period = group.period ??= calendarPeriod(group.startIndex, group.endIndex, context);
+ return {
+ start: period.start,
+ end: period.end,
+ duration: period.duration,
+ placementDates: group.anchors.filter((date) => state.placements.has(date)),
+ ...period.declaredDates.length ? { declaredDates: [...period.declaredDates] } : {},
+ nonWorkingDates: [...period.nonWorkingDates],
+ nonWorkingDayGain: period.nonWorkingDates.length
+ };
+ });
+}
+function periodData(state, context) {
+ const occupancy = occupancyOf(state.placements, context);
+ if (shadowMode)
+ verifyOccupancy(state, context, occupancy);
+ return periodsOf(state, occupancy.groups, context);
+}
+function observationsFromScratch(state, context) {
+ const dates = [...state.placements.keys()].sort();
+ const groups = groupsFromScratch(dates, context);
+ return {
+ tieKey: dates.join("|"),
+ groups: groups.map(({ anchors, startIndex, endIndex }) => ({
+ anchors,
+ startIndex,
+ endIndex
+ })),
+ periods: periodsOf(state, groups, context),
+ ...calendarObservations(state, groups, context)
+ };
+}
+function holidayWeeksFor(state, context) {
+ return Object.freeze(context.holidayWeeks.map((week) => Object.freeze({
+ value: week.value,
+ complete: week.dates.every((date) => state.placements.has(date) || context.declaredDates.has(date))
+ })));
+}
+function timeOffPeriodsFor(groups, context, periods) {
+ return Object.freeze(periods ? periods.map((period) => Object.freeze({ nonWorkingDates: Object.freeze([...period.nonWorkingDates]) })) : groups.map((group) => group.timeOff ??= Object.freeze({
+ nonWorkingDates: (group.period ??= calendarPeriod(group.startIndex, group.endIndex, context)).nonWorkingDates
+ })));
+}
+function workSequencesFor(groups, context) {
+ return Object.freeze(workSequenceLengths(groups, context).map((sequence) => Object.freeze(sequence)));
+}
+function calendarObservations(state, groups, context) {
+ return {
+ publicHolidayWeeks: holidayWeeksFor(state, context),
+ timeOffPeriods: timeOffPeriodsFor(groups, context),
+ workSequences: workSequencesFor(groups, context)
+ };
+}
+function verifyOccupancy(state, context, occupancy) {
+ const expected = observationsFromScratch(state, context);
+ const fail = (field, got, want) => {
+ throw new Error(`Observation shadow mismatch on ${field} for placements [${[...state.placements.keys()].sort().join(", ")}]: got ${JSON.stringify(got)}, expected ${JSON.stringify(want)}`);
+ };
+ if (tieKeyOf(occupancy) !== expected.tieKey)
+ fail("tieKey", tieKeyOf(occupancy), expected.tieKey);
+ const groups = occupancy.groups.map(({ anchors, startIndex, endIndex }) => ({
+ anchors,
+ startIndex,
+ endIndex
+ }));
+ if (JSON.stringify(groups) !== JSON.stringify(expected.groups))
+ fail("groups", groups, expected.groups);
+ for (const field of ["publicHolidayWeeks", "timeOffPeriods", "workSequences"]) {
+ const cached = occupancy.observed[field];
+ if (cached && JSON.stringify(cached) !== JSON.stringify(expected[field]))
+ fail(field, cached, expected[field]);
+ }
+}
+function optimizationBalanceObservations(state, context) {
+ return state.balances.map((balance) => ({
+ unplacedQuantity: balance.remaining,
+ atRisk: isAtRisk(balance, context)
+ }));
+}
+function optimizationObservations(state, context, periods) {
+ const occupancy = occupancyOf(state.placements, context);
+ const calendar = occupancy.observed;
+ if (shadowMode) {
+ verifyOccupancy(state, context, occupancy);
+ calendar.publicHolidayWeeks ??= holidayWeeksFor(state, context);
+ calendar.workSequences ??= workSequencesFor(occupancy.groups, context);
+ if (!periods)
+ calendar.timeOffPeriods ??= timeOffPeriodsFor(occupancy.groups, context);
+ verifyOccupancy(state, context, occupancy);
+ }
+ return {
+ leaveBalances: optimizationBalanceObservations(state, context),
+ get publicHolidayWeeks() {
+ return calendar.publicHolidayWeeks ??= holidayWeeksFor(state, context);
+ },
+ get timeOffPeriods() {
+ return calendar.timeOffPeriods ??= timeOffPeriodsFor(occupancy.groups, context, periods);
+ },
+ get workSequences() {
+ return calendar.workSequences ??= workSequencesFor(occupancy.groups, context);
+ },
+ leavePlacements: [...state.placements.values()]
+ };
+}
+function workSequenceLengths(bounds, context) {
+ if (!context.workingDays.length)
+ return [];
+ let nextIndex = context.calendarIndex.get(context.horizon.start);
+ const sequences = bounds.map(({ anchors }, index) => {
+ const previousIndex = nextIndex;
+ nextIndex = context.calendarIndex.get(anchors[anchors.length - 1]) + 1;
+ return {
+ length: context.workingPrefix[context.calendarIndex.get(anchors[0])] - context.workingPrefix[previousIndex],
+ internal: index > 0
+ };
+ });
+ sequences.push({
+ length: context.workingPrefix[context.calendarIndex.get(context.horizon.end)] - context.workingPrefix[nextIndex],
+ internal: false
+ });
+ return sequences;
+}
+function workSequenceDates(state, context) {
+ const sequences = [];
+ let current = [];
+ let inTimeOffPeriod = false;
+ for (const date of context.workingDays) {
+ context.check();
+ if (state.placements.has(date) || context.declaredDates.has(date)) {
+ if (!inTimeOffPeriod)
+ sequences.push(current);
+ current = [];
+ inTimeOffPeriod = true;
+ } else {
+ current.push(date);
+ inTimeOffPeriod = false;
+ }
+ }
+ if (context.workingDays.length)
+ sequences.push(current);
+ return sequences.map((dates, index) => ({
+ dates,
+ internal: index > 0 && index < sequences.length - 1
+ }));
+}
+function evaluateStateOptimization(configuration, state, context, observations = optimizationObservations(state, context)) {
+ return evaluateValidatedOptimization(configuration, observations);
+}
+
// src/app/services/leave-planner.core.ts
var obligationRequirementCache = new WeakMap;
var occurrenceCache = new WeakMap;
var calendarPatternCache = new WeakMap;
var preferencePatternCache = new WeakMap;
var declaredWindowCache = new WeakMap;
-var calendarObservationCache = new WeakMap;
-var calendarPeriodCache = new WeakMap;
function now() {
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
}
@@ -273,10 +701,8 @@ class DeadlineReached extends Error {
class UserCancelled extends Error {
}
-var DAY_MS = 86400000;
var OBLIGATION_SOLUTION_LIMIT = 4096;
var EXACT_OBLIGATION_BRANCH_LIMIT = 1e4;
-var WINDOW_OBLIGATION_PREVIEW_LIMIT = 8;
var VALID_TYPES = new Set(["cp", "rtt", "other"]);
var VALID_SELECTORS = new Set(["cp", "rtt", "other", "any"]);
var VALID_MODES = new Set(["before", "after", "around", "auto"]);
@@ -309,24 +735,9 @@ function runtimeRequest(value, calculationDate) {
function recordValue(value) {
return typeof value === "object" && value !== null ? value : undefined;
}
-function utcDate(key) {
- return new Date(`${key}T00:00:00Z`);
-}
-function addDays(key, days) {
- return new Date(utcDate(key).getTime() + days * DAY_MS).toISOString().slice(0, 10);
-}
-function compareDate(a, b) {
- return a < b ? -1 : a > b ? 1 : 0;
-}
function compareText(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
-function dateRange(start, endExclusive) {
- const result = [];
- for (let date = start;compareDate(date, endExclusive) < 0; date = addDays(date, 1))
- result.push(date);
- return result;
-}
function defaultHorizon(start) {
return { start, end: addPlanningYears(start, 1) };
}
@@ -367,22 +778,30 @@ function holidayWeekCandidates(context) {
function reason(code, message, ruleIds) {
return ruleIds?.length ? { code, message, ruleIds } : { code, message };
}
-function cloneState(state, placements) {
+function copyPlacements(state) {
+ return new Map([...state.placements.entries()].map(([date, placement]) => [
+ date,
+ {
+ ...placement,
+ ruleIds: new Set(placement.ruleIds)
+ }
+ ]));
+}
+function cloneState(state) {
+ const placements = copyPlacements(state);
+ derivePlacements(state.placements, placements);
return {
balances: state.balances.map((balance) => ({ ...balance })),
- placements: placements ?? new Map([...state.placements.entries()].map(([date, placement]) => [
- date,
- {
- ...placement,
- ruleIds: new Set(placement.ruleIds)
- }
- ])),
+ placements,
earnedActivations: state.earnedActivations.map((activation) => ({
...activation,
dates: [...activation.dates]
}))
};
}
+function debitBalance(state, balanceId) {
+ return state.balances.map((balance) => balance.id === balanceId ? { ...balance, remaining: balance.remaining - 1 } : balance);
+}
function planningStateSnapshot(state, context) {
return {
calculationDate: context.request.calculationDate,
@@ -855,17 +1274,20 @@ function assignDateVariants(state, date, selector, ruleId, context) {
return compareDate(a.expiryDate, b.expiryDate) || compareText(a.id, b.id);
});
return balances.map((balance) => {
- const result = cloneState(state);
- const target = result.balances.find((item) => item.id === balance.id);
- target.remaining--;
- result.placements.set(date, {
+ const placements = copyPlacements(state);
+ notePlacementAdded(state.placements, placements, date);
+ placements.set(date, {
date,
- balanceId: target.id,
- type: target.type,
+ balanceId: balance.id,
+ type: balance.type,
ruleIds: ruleId ? new Set([ruleId]) : new Set,
supporting: false
});
- return result;
+ return {
+ balances: debitBalance(state, balance.id),
+ placements,
+ earnedActivations: state.earnedActivations
+ };
});
}
function assignDateToBalance(state, date, balanceId, context, ruleIds = new Set) {
@@ -881,17 +1303,20 @@ function assignDateToBalance(state, date, balanceId, context, ruleIds = new Set)
}
if (!mandatoryRelocationDate(date, balance, ruleIds, context))
return null;
- const result = cloneState(state, new Map(state.placements));
- const target = result.balances.find((item) => item.id === balanceId);
- target.remaining--;
- result.placements.set(date, {
+ const placements = new Map(state.placements);
+ notePlacementAdded(state.placements, placements, date);
+ placements.set(date, {
date,
balanceId,
type: balance.type,
ruleIds: new Set(ruleIds),
supporting: false
});
- return result;
+ return {
+ balances: debitBalance(state, balanceId),
+ placements,
+ earnedActivations: state.earnedActivations
+ };
}
function addEarnedBalance(state, rule, occurrence, dates, context) {
if (!rule.rewardDays || !rule.rewardType)
@@ -955,12 +1380,12 @@ function finalizeEarnedState(state, context) {
if (bounds.availableFrom <= context.request.calculationDate) {
if ([...result.placements.values()].some((placement) => placement.balanceId === activation.id))
return null;
- result.balances = result.balances.filter((balance2) => balance2.id !== activation.id);
+ result.balances = result.balances.filter((balance) => balance.id !== activation.id);
continue;
}
let balance = result.balances.find((item) => item.id === activation.id);
if (!balance) {
- const rule = context.rules.find((rule2) => rule2.type === "if_then" && rule2.id === activation.ruleId);
+ const rule = context.rules.find((rule) => rule.type === "if_then" && rule.id === activation.ruleId);
if (!rule)
return null;
result = addEarnedBalance(result, rule, activation.occurrence, activation.dates, context);
@@ -1093,6 +1518,7 @@ function releasePlacement(state, date) {
}))
};
const placement = result.placements.get(date);
+ notePlacementRemoved(state.placements, result.placements, date);
result.placements.delete(date);
result.balances.find((balance) => balance.id === placement.balanceId).remaining++;
return result;
@@ -1110,19 +1536,21 @@ function reconcileOptimizationProvenance(state, context, before, objectives = ac
if (!credits.length)
continue;
const withoutState = releasePlacement(state, placement.date);
- const withoutPlacement = activeObjectiveVector(withoutState, context);
+ const withoutPlacement = [
+ ...activeObjectives(withoutState, context, Number.POSITIVE_INFINITY, new Set(credits.map(({ id }) => id)))
+ ];
const ruleIds = new Set([...placement.ruleIds].filter((id) => !credits.some((objective) => objective.id === `optimization:${id}`)));
for (const objective of credits) {
- const without = withoutPlacement.find(({ id: id2 }) => id2 === objective.id);
+ const without = withoutPlacement.find(({ id }) => id === objective.id);
const id = objective.id.slice("optimization:".length);
- const rule = context.rules.find((rule2) => rule2.id === id && rule2.type === "optimization");
+ const rule = context.rules.find((rule) => rule.id === id && rule.type === "optimization");
const compared = compareObjectiveValues(objective.direction, objective.value, without.value);
let contributes = compared < 0;
if (compared === 0 && rule.configuration.aggregation === "maximum") {
observations ??= optimizationObservations(state, context);
const withoutObservations = optimizationObservations(withoutState, context);
const summed = { ...rule.configuration, aggregation: "sum" };
- const summedComparison = compareObjectiveValues(objective.direction, evaluateOptimization(summed, observations), evaluateOptimization(summed, withoutObservations));
+ const summedComparison = compareObjectiveValues(objective.direction, evaluateValidatedOptimization(summed, observations), evaluateValidatedOptimization(summed, withoutObservations));
contributes = summedComparison < 0 || summedComparison === 0 && rule.configuration.elements === "time-off-periods" && objective.direction === "maximize" && observations.timeOffPeriods.length < withoutObservations.timeOffPeriods.length;
}
if (contributes)
@@ -1138,7 +1566,7 @@ function reconcileOptimizationProvenance(state, context, before, objectives = ac
}
function supportingCalendarBenefit(before, after, context) {
const observations = optimizationObservations(after, context);
- return evaluateOptimization(OPTIMIZATION_PRESETS.holidayWeekValue, observations) > evaluateOptimization(OPTIMIZATION_PRESETS.holidayWeekValue, before) || evaluateOptimization(OPTIMIZATION_PRESETS.nonWorkingDayGain, observations) > evaluateOptimization(OPTIMIZATION_PRESETS.nonWorkingDayGain, before) || evaluateOptimization(OPTIMIZATION_PRESETS.longestWorkSequence, observations) < evaluateOptimization(OPTIMIZATION_PRESETS.longestWorkSequence, before) || evaluateOptimization(OPTIMIZATION_PRESETS.shortSequenceDeficit, observations) < evaluateOptimization(OPTIMIZATION_PRESETS.shortSequenceDeficit, before);
+ return evaluateValidatedOptimization(OPTIMIZATION_PRESETS.holidayWeekValue, observations) > evaluateValidatedOptimization(OPTIMIZATION_PRESETS.holidayWeekValue, before) || evaluateValidatedOptimization(OPTIMIZATION_PRESETS.nonWorkingDayGain, observations) > evaluateValidatedOptimization(OPTIMIZATION_PRESETS.nonWorkingDayGain, before) || evaluateValidatedOptimization(OPTIMIZATION_PRESETS.longestWorkSequence, observations) < evaluateValidatedOptimization(OPTIMIZATION_PRESETS.longestWorkSequence, before) || evaluateValidatedOptimization(OPTIMIZATION_PRESETS.shortSequenceDeficit, observations) < evaluateValidatedOptimization(OPTIMIZATION_PRESETS.shortSequenceDeficit, before);
}
function mandatoryRelocationDate(date, balance, ruleIds, context) {
return [...ruleIds].every((id) => {
@@ -1300,7 +1728,7 @@ function typedCalendarDates(context) {
}
return typedDates;
}
-function* calendarPlacementCandidates(state, targetDates, ruleId, context, accept) {
+function* calendarPlacementCandidates(state, targetDates, ruleId, context, accept, donorObjectives = new Map) {
const missing = targetDates.filter((date) => !state.placements.has(date) && !context.declaredDates.has(date));
if (!missing.length)
return false;
@@ -1313,10 +1741,12 @@ function* calendarPlacementCandidates(state, targetDates, ruleId, context, accep
context.check();
if (targetDates.includes(placement.date))
continue;
- rankedDonors.push({
- date: placement.date,
- objectives: activeObjectiveVector(releasePlacement(state, placement.date), context)
- });
+ let objectives = donorObjectives.get(placement.date);
+ if (!objectives) {
+ objectives = activeObjectiveVector(releasePlacement(state, placement.date), context);
+ donorObjectives.set(placement.date, objectives);
+ }
+ rankedDonors.push({ date: placement.date, objectives });
}
rankedDonors.sort((a, b) => compareObjectiveLists(a.objectives, b.objectives) || compareDate(b.date, a.date));
const fill = function* (index, current, additions = []) {
@@ -1338,9 +1768,9 @@ function* calendarPlacementCandidates(state, targetDates, ruleId, context, accep
return false;
if (index === missing.length) {
const benefit = supportingCalendarBenefit(before, current, context);
- for (const date2 of additions) {
- const placement = { ...current.placements.get(date2) };
- current.placements.set(date2, placement);
+ for (const date of additions) {
+ const placement = { ...current.placements.get(date) };
+ current.placements.set(date, placement);
const balance = current.balances.find((item) => item.id === placement.balanceId);
placement.supporting = benefit && !isAtRisk(balance, context);
}
@@ -1391,8 +1821,9 @@ function* calendarPlacementCandidates(state, targetDates, ruleId, context, accep
return yield* fill(0, state);
}
function* holidayWeekPlacementCandidates(state, ruleId, context, accept) {
+ const donorObjectives = new Map;
for (const week of context.holidayWeeks)
- yield* calendarPlacementCandidates(state, week.dates, ruleId, context, accept);
+ yield* calendarPlacementCandidates(state, week.dates, ruleId, context, accept, donorObjectives);
}
function* timeOffPeriodPlacementCandidates(state, ruleId, context, accept) {
const periods = periodData(state, context);
@@ -1402,9 +1833,48 @@ function* timeOffPeriodPlacementCandidates(state, ruleId, context, accept) {
yield* calendarPlacementCandidates(state, dates, ruleId, context, accept);
}
}
+var finalizeInvarianceCache = new WeakMap;
+function finalizeInvariance(context) {
+ let entries = finalizeInvarianceCache.get(context);
+ if (!entries) {
+ const earning = context.rules.some((rule) => rule.enabled && rule.type === "if_then" && rule.rewardDays);
+ entries = context.rules.filter((rule) => rule.enabled && rule.force === "preference").map((rule) => {
+ if (rule.type !== "optimization")
+ return "exact";
+ switch (rule.configuration.elements) {
+ case "public-holiday-weeks":
+ case "time-off-periods":
+ case "work-sequences":
+ return "exact";
+ case "leave-balances":
+ return earning ? "unknown" : "without-earning";
+ default:
+ return "unknown";
+ }
+ });
+ finalizeInvarianceCache.set(context, entries);
+ }
+ return entries;
+}
+function provablyWorseThanProtectedPrefix(candidate, context, throughIndex, baseline) {
+ const invariance = finalizeInvariance(context);
+ let index = 0;
+ for (const objective of activeObjectives(candidate, context, throughIndex)) {
+ const kind = invariance[index];
+ if (kind === "unknown" || kind === "without-earning" && candidate.earnedActivations.length)
+ return false;
+ const comparison = compareObjectiveValues(objective.direction, objective.value, baseline[index].value);
+ if (comparison !== 0)
+ return comparison > 0;
+ index++;
+ }
+ return false;
+}
function evaluateRuleCandidate(before, candidate, context, throughIndex, addition = false, baseline = activeObjectiveVector(before, context, throughIndex), incumbentVector = objectiveVector(before, context)) {
if (!candidate)
return;
+ if (provablyWorseThanProtectedPrefix(candidate, context, throughIndex, baseline))
+ return;
const state = finalizeRuleCandidate(candidate, context, before);
if (!state)
return;
@@ -1426,7 +1896,7 @@ function evaluateRuleCandidate(before, candidate, context, throughIndex, additio
}
const vector = objectiveVector(state, context, active);
if (compareObjectiveLists(vector.objectives, incumbentVector.objectives) < 0)
- context.retain?.(state, undefined, vector);
+ context.retain?.(state, vector);
if (addition && comparison === 0)
return;
return { state, vector };
@@ -1531,7 +2001,7 @@ function* optimizeRulePlacements(state, context, throughIndex, allowAdd, rule) {
if (item.type === "time_window")
return item.placeDays;
if (item.type === "optimization" && item.configuration.elements === "public-holiday-weeks" && item.configuration.measure === "public-holiday-week-value" && item.configuration.direction === "maximize")
- return evaluateOptimization(item.configuration, {
+ return evaluateValidatedOptimization(item.configuration, {
leaveBalances: [],
publicHolidayWeeks: context.holidayWeeks.map(({ value }) => ({
value,
@@ -1550,7 +2020,7 @@ function* optimizeRulePlacements(state, context, throughIndex, allowAdd, rule) {
const active = activeObjectiveVector(result, context);
const baseline = active.slice(0, throughIndex + 1);
const beforeVector = objectiveVector(result, context, active);
- context.retain?.(result, undefined, beforeVector);
+ context.retain?.(result, beforeVector);
if (allowAdd && prefixBounds.length > 0 && prefixBounds.every((bound, index) => bound !== undefined && active[index].value === bound)) {
return result;
}
@@ -1646,7 +2116,7 @@ function* optimizeRulePlacements(state, context, throughIndex, allowAdd, rule) {
const moved = assignDateToBalance(without, destination, existing.balanceId, context, existing.ruleIds);
if (moved)
moved.placements.get(destination).supporting = existing.supporting;
- if (moved && dateInvariantObjectives && compareObjectiveLists(tieBreakObjectives(moved), beforeVector.objectives.slice(-2)) >= 0) {
+ if (moved && dateInvariantObjectives && compareObjectiveLists(tieBreakObjectives(moved, context), beforeVector.objectives.slice(-2)) >= 0) {
continue;
}
consider(moved);
@@ -1848,15 +2318,6 @@ function preferenceReason(rule, count, context, state) {
code = "capacity";
return reason(code, `Preference "${rule.name}" could not be completely satisfied.`, [rule.id]);
}
-function* applyPreferenceRules(state, preferenceRules, context) {
- let result = state;
- const preferences = [];
- for (let preferenceIndex = 0;preferenceIndex < preferenceRules.length; preferenceIndex++) {
- yield;
- result = yield* applyPreferenceStep(result, preferenceRules, preferenceIndex, preferences, context);
- }
- return { state: result, preferences };
-}
function* applyPreferenceStep(state, preferenceRules, preferenceIndex, preferences, context) {
let result = state;
const apply = function* (index) {
@@ -1901,16 +2362,10 @@ function preferenceStateKey(state) {
state.earnedActivations
]);
}
-function placementTieKey(state) {
- return [...state.placements.keys()].sort().join("|");
-}
-function expiryTieKey(state) {
- return [...state.placements.values()].map((placement) => state.balances.find((item) => item.id === placement.balanceId)?.expiryDate ?? "").sort(compareDate).join("|");
-}
function activeObjectiveVector(state, context, throughIndex = Number.POSITIVE_INFINITY) {
return [...activeObjectives(state, context, throughIndex)];
}
-function* activeObjectives(state, context, throughIndex = Number.POSITIVE_INFINITY) {
+function* activeObjectives(state, context, throughIndex = Number.POSITIVE_INFINITY, only) {
let preferences;
let balances;
let calendar;
@@ -1920,6 +2375,8 @@ function* activeObjectives(state, context, throughIndex = Number.POSITIVE_INFINI
continue;
if (index++ > throughIndex)
return;
+ if (only && !only.has(`${rule.type === "optimization" ? "optimization" : "preference"}:${rule.id}`))
+ continue;
if (rule.type === "optimization") {
const observations = rule.configuration.elements === "leave-balances" ? balances ??= {
leaveBalances: optimizationBalanceObservations(state, context),
@@ -1945,7 +2402,7 @@ function* activeObjectives(state, context, throughIndex = Number.POSITIVE_INFINI
}
}
}
-function tieBreakObjectives(state) {
+function tieBreakObjectives(state, context) {
return [
{
id: "balance-expiry",
@@ -1956,186 +2413,20 @@ function tieBreakObjectives(state) {
{
id: "placement-date",
direction: "minimize",
- value: placementTieKey(state),
+ value: placementTieKey(state, context),
label: "Placement date tie-break"
}
];
}
function obligationPreviewObjectives(preview, context) {
- return [...activeObjectiveVector(preview.state, context), ...tieBreakObjectives(preview.state)];
+ return [
+ ...activeObjectiveVector(preview.state, context),
+ ...tieBreakObjectives(preview.state, context)
+ ];
}
function compareObligationPreviews(a, b, context) {
return compareObjectiveLists(obligationPreviewObjectives(a, context), obligationPreviewObjectives(b, context));
}
-function isAtRisk(balance, context) {
- return compareDate(balance.expiryDate, context.horizon.start) >= 0 && compareDate(balance.expiryDate, context.horizon.end) <= 0;
-}
-function isNaturalNonWorkingDate(date, context) {
- return context.calendarIndex.has(date) ? !context.workingCalendar.has(date) : !isWorkingPlanningDate(date);
-}
-function calendarDateAt(index, context) {
- return context.calendarDays[index] ?? addDays(context.calendarDays[0], index);
-}
-function periodBounds(state, context) {
- const dates = [...new Set([...state.placements.keys(), ...context.declaredDates])].filter((date) => date >= context.horizon.start && date < context.horizon.end).sort(compareDate);
- if (!dates.length)
- return [];
- const groups = [];
- let group = [dates[0]];
- for (let i = 1;i < dates.length; i++) {
- context.check();
- const previousIndex = context.calendarIndex.get(dates[i - 1]);
- const currentIndex = context.calendarIndex.get(dates[i]);
- const workingInGap = context.workingPrefix[currentIndex] - context.workingPrefix[previousIndex + 1];
- if (workingInGap === 0)
- group.push(dates[i]);
- else {
- groups.push(group);
- group = [dates[i]];
- }
- }
- groups.push(group);
- return groups.map((anchors) => {
- context.check();
- let startIndex = context.calendarIndex.get(anchors[0]);
- let endIndex = context.calendarIndex.get(anchors[anchors.length - 1]);
- while (context.declaredDates.has(calendarDateAt(startIndex - 1, context)) || isNaturalNonWorkingDate(calendarDateAt(startIndex - 1, context), context)) {
- context.check();
- startIndex--;
- }
- while (context.declaredDates.has(calendarDateAt(endIndex + 1, context)) || isNaturalNonWorkingDate(calendarDateAt(endIndex + 1, context), context)) {
- context.check();
- endIndex++;
- }
- return { anchors, startIndex, endIndex };
- });
-}
-function calendarPeriod(startIndex, endIndex, context) {
- let periods = calendarPeriodCache.get(context);
- if (!periods) {
- periods = new Map;
- calendarPeriodCache.set(context, periods);
- }
- const key = `${startIndex}:${endIndex}`;
- let period = periods.get(key);
- if (period)
- periods.delete(key);
- else {
- const start = calendarDateAt(startIndex, context);
- const end = calendarDateAt(endIndex, context);
- const dates = startIndex >= 0 && endIndex < context.calendarDays.length ? context.calendarDays.slice(startIndex, endIndex + 1) : dateRange(start, addDays(end, 1));
- period = Object.freeze({
- start,
- end,
- duration: dates.length,
- nonWorkingDates: Object.freeze(dates.filter((date) => isNaturalNonWorkingDate(date, context))),
- declaredDates: Object.freeze(dates.filter((date) => context.declaredDates.has(date)))
- });
- }
- periods.set(key, period);
- if (periods.size > 4096)
- periods.delete(periods.keys().next().value);
- return period;
-}
-function periodData(state, context) {
- return periodBounds(state, context).map(({ anchors, startIndex, endIndex }) => {
- const period = calendarPeriod(startIndex, endIndex, context);
- return {
- start: period.start,
- end: period.end,
- duration: period.duration,
- placementDates: anchors.filter((date) => state.placements.has(date)),
- ...period.declaredDates.length ? { declaredDates: [...period.declaredDates] } : {},
- nonWorkingDates: [...period.nonWorkingDates],
- nonWorkingDayGain: period.nonWorkingDates.length
- };
- });
-}
-function optimizationBalanceObservations(state, context) {
- return state.balances.map((balance) => ({
- unplacedQuantity: balance.remaining,
- atRisk: isAtRisk(balance, context)
- }));
-}
-function optimizationObservations(state, context, periods) {
- const key = placementTieKey(state);
- let byDates = calendarObservationCache.get(context);
- if (!byDates) {
- byDates = new Map;
- calendarObservationCache.set(context, byDates);
- }
- const calendar = byDates.get(key) ?? {};
- if (byDates.has(key))
- byDates.delete(key);
- else if (byDates.size >= 4096)
- byDates.delete(byDates.keys().next().value);
- byDates.set(key, calendar);
- let bounds;
- return {
- leaveBalances: optimizationBalanceObservations(state, context),
- get publicHolidayWeeks() {
- return calendar.publicHolidayWeeks ??= Object.freeze(context.holidayWeeks.map((week) => Object.freeze({
- value: week.value,
- complete: week.dates.every((date) => state.placements.has(date) || context.declaredDates.has(date))
- })));
- },
- get timeOffPeriods() {
- return calendar.timeOffPeriods ??= Object.freeze(periods ? periods.map((period) => Object.freeze({
- nonWorkingDates: Object.freeze([...period.nonWorkingDates])
- })) : (bounds ??= periodBounds(state, context)).map(({ startIndex, endIndex }) => Object.freeze({
- nonWorkingDates: calendarPeriod(startIndex, endIndex, context).nonWorkingDates
- })));
- },
- get workSequences() {
- return calendar.workSequences ??= Object.freeze(workSequenceLengths(bounds ??= periodBounds(state, context), context).map((sequence) => Object.freeze(sequence)));
- },
- leavePlacements: [...state.placements.values()]
- };
-}
-function workSequenceLengths(bounds, context) {
- if (!context.workingDays.length)
- return [];
- let nextIndex = context.calendarIndex.get(context.horizon.start);
- const sequences = bounds.map(({ anchors }, index) => {
- const previousIndex = nextIndex;
- nextIndex = context.calendarIndex.get(anchors[anchors.length - 1]) + 1;
- return {
- length: context.workingPrefix[context.calendarIndex.get(anchors[0])] - context.workingPrefix[previousIndex],
- internal: index > 0
- };
- });
- sequences.push({
- length: context.workingPrefix[context.calendarIndex.get(context.horizon.end)] - context.workingPrefix[nextIndex],
- internal: false
- });
- return sequences;
-}
-function workSequenceDates(state, context) {
- const sequences = [];
- let current = [];
- let inTimeOffPeriod = false;
- for (const date of context.workingDays) {
- context.check();
- if (state.placements.has(date) || context.declaredDates.has(date)) {
- if (!inTimeOffPeriod)
- sequences.push(current);
- current = [];
- inTimeOffPeriod = true;
- } else {
- current.push(date);
- inTimeOffPeriod = false;
- }
- }
- if (context.workingDays.length)
- sequences.push(current);
- return sequences.map((dates, index) => ({
- dates,
- internal: index > 0 && index < sequences.length - 1
- }));
-}
-function evaluateStateOptimization(configuration, state, context, observations = optimizationObservations(state, context)) {
- return evaluateOptimization(configuration, observations);
-}
function qualityMetrics(state, context, preferences) {
const periods = periodData(state, context);
const observations = optimizationObservations(state, context, periods);
@@ -2219,7 +2510,7 @@ function objectiveVector(state, context, active = activeObjectiveVector(state, c
label: "Unsatisfied obligations"
},
...active,
- ...tieBreakObjectives(state)
+ ...tieBreakObjectives(state, context)
]
};
}
@@ -2309,6 +2600,7 @@ function planExplanation(state, context, placements, periods, preferences, remai
});
const atRisk = normalizedBalance ? compareDate(normalizedBalance.expiryDate, context.horizon.start) >= 0 && compareDate(normalizedBalance.expiryDate, context.horizon.end) <= 0 : false;
const withoutPlacement = cloneState(state);
+ notePlacementRemoved(state.placements, withoutPlacement.placements, placement.date);
withoutPlacement.placements.delete(placement.date);
const released = withoutPlacement.balances.find((candidate) => candidate.id === placement.balanceId);
if (released)
@@ -2502,16 +2794,30 @@ function calculatePlanningInternal(request, checkpointCallback, artifactsCallbac
}
async function calculatePlanningAsyncInternal(request, checkpointCallback, artifactsCallback, outcomeCallback, explanationCallback, isCancelled = () => false) {
const steps = calculatePlanningSteps(request, checkpointCallback, artifactsCallback, outcomeCallback, explanationCallback, true);
- let next = steps.next();
- let pauseAt = now() + 8;
- while (!next.done) {
- if (next.value || now() >= pauseAt) {
- await new Promise((resolve) => setTimeout(resolve, 0));
- pauseAt = now() + 8;
+ const channel = typeof setImmediate === "function" ? undefined : new MessageChannel;
+ let resume = () => {};
+ if (channel)
+ channel.port1.onmessage = () => resume();
+ const pause = () => new Promise((resolve) => {
+ if (!channel)
+ return void setImmediate(resolve);
+ resume = resolve;
+ channel.port2.postMessage(undefined);
+ });
+ try {
+ let next = steps.next();
+ let pauseAt = now() + 8;
+ while (!next.done) {
+ if (next.value || now() >= pauseAt) {
+ await pause();
+ pauseAt = now() + 8;
+ }
+ next = isCancelled() ? steps.throw(new UserCancelled) : steps.next();
}
- next = isCancelled() ? steps.throw(new UserCancelled) : steps.next();
+ return next.value;
+ } finally {
+ channel?.port1.close();
}
- return next.value;
}
function* prepareCalendar(horizon, progress) {
const start = addDays(horizon.start, -400);
@@ -2639,13 +2945,13 @@ function* calculatePlanningSteps(request, checkpointCallback, artifactsCallback,
let checkpointState;
let retained;
if (retainIncumbents)
- context.retain = (candidate, finalizedMetrics, candidateVector) => {
+ context.retain = (candidate, candidateVector) => {
const check = context.check;
context.check = () => {
return;
};
try {
- const finalized = finalizedMetrics || candidateVector ? candidate : finalizeEarnedState(candidate, context);
+ const finalized = candidateVector ? candidate : finalizeEarnedState(candidate, context);
if (!finalized)
return;
if (unsatisfiedObligationCount(finalized, context))
@@ -2665,11 +2971,7 @@ function* calculatePlanningSteps(request, checkpointCallback, artifactsCallback,
retained = {
state: cloneState(reconcileOptimizationProvenance(finalized, context, retained?.state ?? initialState, vector.objectives)),
vector,
- preferences: candidatePreferences,
- metrics: finalizedMetrics ? {
- ...finalizedMetrics,
- preferenceSatisfaction: candidatePreferences.map(({ satisfied }) => satisfied)
- } : qualityMetrics(finalized, context, candidatePreferences)
+ preferences: candidatePreferences
};
if (firstValidCandidate && checkpointCallback)
observeCallback(context, () => checkpointCallback(makePlan(retained.state, context, retained.preferences, stages), planningStateSnapshot(retained.state, context)));
@@ -2789,8 +3091,8 @@ function* calculatePlanningSteps(request, checkpointCallback, artifactsCallback,
return finish(obligationConflict(obligationRequirements, context));
}
const preferenceRules = normalized.rules.filter((item) => item.enabled && item.force === "preference");
- let previewSolutions = obligationSolutions;
- if (obligationSolutionLimit > 0 && obligationRequirements.every((requirement) => requirement.rule.type === "time_window") && obligationSolutions.length > WINDOW_OBLIGATION_PREVIEW_LIMIT) {
+ let rankedSolutions = obligationSolutions;
+ if (obligationSolutions.length > 1) {
const ranked = [];
for (const candidate of obligationSolutions) {
yield;
@@ -2801,72 +3103,48 @@ function* calculatePlanningSteps(request, checkpointCallback, artifactsCallback,
});
}
ranked.sort((a, b) => compareObjectiveLists(a.objectives, b.objectives));
- previewSolutions = ranked.slice(0, WINDOW_OBLIGATION_PREVIEW_LIMIT).map(({ state: state2 }) => state2);
+ rankedSolutions = ranked.map(({ state }) => state);
}
- const soleSolution = obligationSolutions.length === 1 && (obligationSolutionLimit === 0 || obligationSolutions.length < obligationSolutionLimit) ? obligationSolutions[0] : undefined;
- const solePreferences = soleSolution ? preferenceResultsForState(soleSolution, context) : [];
- let bestPreview = soleSolution ? {
- obligationState: soleSolution,
- state: soleSolution,
- preferences: solePreferences,
- metrics: qualityMetrics(soleSolution, context, solePreferences)
- } : null;
- for (const obligationSolution of soleSolution ? [] : previewSolutions) {
- yield;
- context.check();
- const preview = yield* applyPreferenceRules(cloneState(obligationSolution), preferenceRules, context);
- const previewState = finalizeEarnedState(preview.state, context);
- if (!previewState)
- continue;
- const candidate = {
- obligationState: obligationSolution,
- state: previewState,
- preferences: preview.preferences,
- metrics: qualityMetrics(previewState, context, preview.preferences)
- };
- const compared = bestPreview ? compareObligationPreviews(candidate, bestPreview, context) : undefined;
- if (!bestPreview || compared < 0)
- bestPreview = candidate;
- }
- if (!bestPreview) {
+ for (const obligationSolution of rankedSolutions) {
+ state = obligationSolution;
+ stages.length = 1;
+ preferences.length = 0;
+ checkpointState = undefined;
stages.push("obligations");
- return finish(obligationConflict(obligationRequirements, context));
- }
- state = bestPreview.obligationState;
- stages.push("obligations");
- publishCheckpoint();
- for (let preferenceIndex = 0;preferenceIndex < preferenceRules.length; preferenceIndex++) {
- yield;
- const rule = preferenceRules[preferenceIndex];
- markProgress(context, ruleStage(rule));
- yield true;
- state = yield* applyPreferenceStep(state, preferenceRules, preferenceIndex, preferences, context);
- stages.push(ruleStage(rule));
publishCheckpoint();
+ for (let preferenceIndex = 0;preferenceIndex < preferenceRules.length; preferenceIndex++) {
+ yield;
+ const rule = preferenceRules[preferenceIndex];
+ markProgress(context, ruleStage(rule));
+ yield true;
+ state = yield* applyPreferenceStep(state, preferenceRules, preferenceIndex, preferences, context);
+ stages.push(ruleStage(rule));
+ publishCheckpoint();
+ }
+ markProgress(context, "expiry");
+ yield true;
+ state = yield* optimizeRulePlacements(state, context, preferenceRules.length - 1, false);
+ stages.push("expiry");
+ publishCheckpoint();
+ markProgress(context, "dates");
+ yield true;
+ stages.push("dates");
+ const finalized = finalizeEarnedState(state, context);
+ if (!finalized)
+ continue;
+ state = finalized;
+ publishCheckpoint();
+ markProgress(context, "complete");
+ yield true;
+ return finish(checkpoint, state);
}
- markProgress(context, "expiry");
- yield true;
- state = yield* optimizeRulePlacements(state, context, preferenceRules.length - 1, false);
- stages.push("expiry");
- publishCheckpoint();
- markProgress(context, "dates");
- yield true;
- stages.push("dates");
- const finalized = finalizeEarnedState(state, context);
- if (!finalized) {
- return finish({
- kind: "PlanningConflict",
- conflictingRuleIds: state.earnedActivations.map((activation) => activation.ruleId),
- reasons: [
- reason("earned-entitlement-order", "Generated leave became available only after its final merged time-off period.")
- ]
- });
- }
- state = finalized;
- publishCheckpoint();
- markProgress(context, "complete");
- yield true;
- return finish(checkpoint, state);
+ return finish({
+ kind: "PlanningConflict",
+ conflictingRuleIds: state.earnedActivations.map((activation) => activation.ruleId),
+ reasons: [
+ reason("earned-entitlement-order", "Generated leave became available only after its final merged time-off period.")
+ ]
+ });
} catch (error) {
if (error instanceof UserCancelled && retained) {
context.check = () => {
@@ -3137,8 +3415,10 @@ function analyzeCounterfactualInternal(request, plan, selected, finalStateSnapsh
const baseline = cloneState(finalState);
const removeDate = selected.date;
const removed = removeDate ? baseline.placements.get(removeDate) : undefined;
- if (removeDate)
+ if (removeDate) {
+ notePlacementRemoved(finalState.placements, baseline.placements, removeDate);
baseline.placements.delete(removeDate);
+ }
const released = baseline.balances.find((balance) => balance.id === selected.balanceId);
if (released && removed?.balanceId === selected.balanceId)
released.remaining++;
@@ -3214,9 +3494,9 @@ function analyzeCounterfactualInternal(request, plan, selected, finalStateSnapsh
};
}
export {
- invalidInputFor,
- explainIncompleteCheckpoint,
- calculatePlanningInternal,
+ analyzeCounterfactualInternal,
calculatePlanningAsyncInternal,
- analyzeCounterfactualInternal
+ calculatePlanningInternal,
+ explainIncompleteCheckpoint,
+ invalidInputFor
};