refactor(planning): consolidate plan observations in one module

Move time-off periods, work sequences, public-holiday weeks, balance
observations, the placement list and both tie keys into
plan-observations.ts. The code moves without behavior changes; all seven
benchmark scenarios produce byte-identical outcomes, explanations and final
states, and the frozen-engine check passes. Benchmark timings stay within
noise.

Refs #282

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nessarandClaude Opus 5.5 committed 2026-10-16 23:47:34 +02:00
1 parent 24c35542a8
commit 87d72064be
3 files changed
+384 -281

No files matched your search

+14 -281
View File
@@ -2,8 +2,21 @@ import {
DeclaredNonWorkingDay,
parseDeclaredDays,
frenchPublicHolidaysForYear,
isWorkingPlanningDate,
} from './declared-non-working-days';
import {
addDays,
compareDate,
dateRange,
evaluateStateOptimization,
expiryTieKey,
isAtRisk,
optimizationBalanceObservations,
optimizationObservations,
periodData,
placementTieKey,
utcDate,
workSequenceDates,
} from './plan-observations';
import { addPlanningYears, isPlanningDate, planningDateInParis } from './planning-date';
import {
compareObjectiveLists,
@@ -501,21 +514,6 @@ const occurrenceCache = new WeakMap<SearchContext, Map<string, string[]>>();
const calendarPatternCache = new WeakMap<SearchContext, Map<string, string[][]>>();
const preferencePatternCache = new WeakMap<SearchContext, Map<string, string[][][]>>();
const declaredWindowCache = new WeakMap<SearchContext, Map<string, DeclaredNonWorkingDay[]>>();
type CalendarOptimizationObservations = Partial<
Pick<OptimizationObservations, 'publicHolidayWeeks' | 'timeOffPeriods' | 'workSequences'>
>;
const calendarObservationCache = new WeakMap<
SearchContext,
Map<string, CalendarOptimizationObservations>
>();
interface CalendarPeriod {
start: string;
end: string;
duration: number;
nonWorkingDates: readonly string[];
declaredDates: readonly string[];
}
const calendarPeriodCache = new WeakMap<SearchContext, Map<string, CalendarPeriod>>();
interface SearchContext {
declaredDays: readonly DeclaredNonWorkingDay[];
@@ -587,7 +585,6 @@ class UserCancelled extends Error {}
// A true value asks the asynchronous driver to pause immediately at a phase boundary.
type PlanningSteps<T> = Generator<boolean | undefined, T, void>;
const DAY_MS = 86_400_000;
// ponytail: cap the number of feasible obligation branches previewed for a
// loaded year; replace with bounded branch-and-bound if mobile evidence shows
// that this ceiling misses a better lexicographic assignment.
@@ -635,29 +632,10 @@ function recordValue(value: unknown): RuntimeRecord | undefined {
return typeof value === 'object' && value !== null ? (value as RuntimeRecord) : undefined;
}
function utcDate(key: string): Date {
return new Date(`${key}T00:00:00Z`);
}
function addDays(key: string, days: number): string {
return new Date(utcDate(key).getTime() + days * DAY_MS).toISOString().slice(0, 10);
}
function compareDate(a: string, b: string): number {
return a < b ? -1 : a > b ? 1 : 0;
}
function compareText(a: string, b: string): number {
return a < b ? -1 : a > b ? 1 : 0;
}
function dateRange(start: string, endExclusive: string): string[] {
const result: string[] = [];
for (let date = start; compareDate(date, endExclusive) < 0; date = addDays(date, 1))
result.push(date);
return result;
}
function defaultHorizon(start: string): { start: string; end: string } {
return { start, end: addPlanningYears(start, 1) };
}
@@ -3318,20 +3296,6 @@ function preferenceStateKey(state: State): string {
]);
}
function placementTieKey(state: State): string {
return [...state.placements.keys()].sort().join('|');
}
function expiryTieKey(state: State): string {
return [...state.placements.values()]
.map(
(placement) =>
state.balances.find((item) => item.id === placement.balanceId)?.expiryDate ?? '',
)
.sort(compareDate)
.join('|');
}
function activeObjectiveVector(
state: State,
context: SearchContext,
@@ -3418,237 +3382,6 @@ function compareObligationPreviews(
);
}
function isAtRisk(balance: InternalBalance, context: SearchContext): boolean {
return (
compareDate(balance.expiryDate, context.horizon.start) >= 0 &&
compareDate(balance.expiryDate, context.horizon.end) <= 0
);
}
function isNaturalNonWorkingDate(date: string, context: SearchContext): boolean {
return context.calendarIndex.has(date)
? !context.workingCalendar.has(date)
: !isWorkingPlanningDate(date);
}
function calendarDateAt(index: number, context: SearchContext): string {
return context.calendarDays[index] ?? addDays(context.calendarDays[0], index);
}
function periodBounds(state: State, context: SearchContext) {
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: string[][] = [];
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: number,
endIndex: number,
context: SearchContext,
): CalendarPeriod {
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: State, context: SearchContext): TimeOffPeriod[] {
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: State, context: SearchContext) {
return state.balances.map((balance) => ({
unplacedQuantity: balance.remaining,
atRisk: isAtRisk(balance, context),
}));
}
function optimizationObservations(
state: State,
context: SearchContext,
periods?: TimeOffPeriod[],
): OptimizationObservations {
// Calendar facts depend only on occupied dates within this immutable request.
// Balance quantities, provenance and supporting status stay outside this cache.
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: ReturnType<typeof periodBounds> | undefined;
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: ReturnType<typeof periodBounds>,
context: SearchContext,
): OptimizationObservations['workSequences'] {
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: State, context: SearchContext) {
const sequences: string[][] = [];
let current: string[] = [];
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: OptimizationConfiguration,
state: State,
context: SearchContext,
observations = optimizationObservations(state, context),
): number {
return evaluateOptimization(configuration, observations);
}
function qualityMetrics(
state: State,
context: SearchContext,