perf(planning): prune candidates that cannot beat the protected prefix

Finalizing a candidate never moves or retypes a placement, so preference
results and calendar optimizations already have their final values before
it, and leave-balance optimizations do too when nothing can be earned.
Reject a candidate before finalizing when the first differing such
objective of the protected prefix is worse than the baseline. Leave-placement
objectives and earning contexts stop the walk.

The frozen-engine check now also compares cancelled asynchronous runs,
including checkpoints and retained incumbents. The loaded year drops from
48.5 s to 44.3 s; outcomes, explanations and final states are unchanged.

Refs #284

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nessarandClaude Opus 5.5 committed 2026-10-17 00:45:28 +02:00
1 parent 7890a03a74
commit 68a9e33de6
4 files changed
+208 -4

No files matched your search

+74 -3
View File
@@ -3,11 +3,15 @@ import { createOptimizationPresetRules } from './rule-service';
import { frenchPublicHolidaysForYear, isWorkingPlanningDate } from './declared-non-working-days';
import { OPTIMIZATION_CATALOG } from './planning-objectives';
import {
calculatePlanningAsyncInternal,
calculatePlanningInternal,
type PlanningArtifacts,
type PlanningOutcome,
} from './leave-planner.core';
import { calculatePlanningInternal as frozenCalculate } from './frozen-engine/leave-planner.core.frozen.mjs';
import {
calculatePlanningAsyncInternal as frozenCalculateAsync,
calculatePlanningInternal as frozenCalculate,
} from './frozen-engine/leave-planner.core.frozen.mjs';
/** Rebuild instructions for the frozen engine: docs/benchmark/README.md. */
const BASE_SEED = 280_000;
@@ -214,13 +218,67 @@ function assertSameAsFrozen(seed: number): void {
}
}
type AsyncEngine = typeof calculatePlanningAsyncInternal;
/** Cancels after a seed-derived step count: during preparation, mid-search, or never. */
function cancelAfter(seed: number, total: number): number {
const rnd = mulberry32(seed ^ 0x5bd1e995);
const roll = rnd();
if (roll < 0.1) return Math.floor(rnd() * total);
if (roll < 0.25) return Number.POSITIVE_INFINITY;
// Preparation is most of the steps; spend the rest of the budget on the search.
return Math.floor(total * (0.9 + rnd() * 0.1));
}
async function runCancelled(engine: AsyncEngine, request: Req, limit: number) {
const calls: unknown[] = [];
const artifacts: unknown[] = [];
const explanations: unknown[] = [];
const outcomes: unknown[] = [];
let polls = 0;
const outcome = await engine(
structuredClone(request),
(plan, state) => calls.push({ plan, state }),
({ timings: _timings, ...rest }) => artifacts.push(rest),
(o) => outcomes.push(o),
(...args) => explanations.push(args),
() => ++polls > limit,
);
return { outcome, calls, artifacts, explanations, outcomes, polls };
}
const cancelledKinds = new Map<string, number>();
let incumbents = 0;
async function assertCancelledSameAsFrozen(seed: number): Promise<void> {
const request = generateRequest(seed);
const total = (await runCancelled(calculatePlanningAsyncInternal, request, Infinity)).polls;
const limit = cancelAfter(seed, total);
const current = await runCancelled(calculatePlanningAsyncInternal, request, limit);
const frozen = await runCancelled(frozenCalculateAsync, request, limit);
const kind = current.outcome.kind;
cancelledKinds.set(kind, (cancelledKinds.get(kind) ?? 0) + 1);
if (kind === 'PlanningIncomplete' && current.calls.length) incumbents++;
try {
expect(current).toStrictEqual(frozen);
} catch (error) {
throw new Error(
`Cancelled run differs from frozen engine. seed=${seed} limit=${limit}\nrequest=${JSON.stringify(request)}\n${
(error as Error).message
}`,
{ cause: error },
);
}
}
const ONLY_SEED = process.env['FROZEN_ENGINE_SEED'];
describe('frozen engine differential check', () => {
if (ONLY_SEED) {
it(`matches the frozen engine for seed ${ONLY_SEED}`, () => {
it(`matches the frozen engine for seed ${ONLY_SEED}`, async () => {
assertSameAsFrozen(Number(ONLY_SEED));
});
await assertCancelledSameAsFrozen(Number(ONLY_SEED));
}, 120_000);
return;
}
for (let from = 0; from < CASES; from += CHUNK) {
@@ -229,6 +287,19 @@ describe('frozen engine differential check', () => {
}, 120_000);
}
for (let from = 0; from < CASES; from += CHUNK * 2) {
it(`matches the frozen engine when cancelled, seeds ${BASE_SEED + from}.. every 10th`, async () => {
for (let i = from; i < from + CHUNK * 2; i += 10)
await assertCancelledSameAsFrozen(BASE_SEED + i);
}, 300_000);
}
it('retains incumbents of cancelled calculations', () => {
console.info('cancelled coverage', Object.fromEntries(cancelledKinds), { incumbents });
expect(cancelledKinds.get('PlanningIncomplete') ?? 0).toBeGreaterThan(20);
expect(incumbents).toBeGreaterThan(10);
});
afterAll(() => {
console.info(
'frozen-engine coverage of generated cases',
@@ -1,7 +1,22 @@
import type { PlanningArtifactsCallback, PlanningOutcome } from '../leave-planner.core';
import type {
PlanningArtifactsCallback,
PlanningCheckpoint,
PlanningExplanationCallback,
PlanningOutcome,
PlanningOutcomeCallback,
} from '../leave-planner.core';
export function calculatePlanningInternal(
request: unknown,
checkpointCallback?: undefined,
artifactsCallback?: PlanningArtifactsCallback,
): PlanningOutcome;
export function calculatePlanningAsyncInternal(
request: unknown,
checkpointCallback?: PlanningCheckpoint,
artifactsCallback?: PlanningArtifactsCallback,
outcomeCallback?: PlanningOutcomeCallback,
explanationCallback?: PlanningExplanationCallback,
isCancelled?: () => boolean,
): Promise<PlanningOutcome>;
+67
View File
@@ -2350,6 +2350,71 @@ function* timeOffPeriodPlacementCandidates(
}
}
type FinalizeInvariance = 'exact' | 'without-earning' | 'unknown';
const finalizeInvarianceCache = new WeakMap<SearchContext, FinalizeInvariance[]>();
/**
* Finalizing never moves or retypes placements; it only edits ruleIds, supporting, and earned
* activations/balances. Preference results and the calendar optimizations (public-holiday-weeks,
* time-off-periods, work-sequences) read only placement dates and types, so the pre-finalize
* value is already the final one. leave-balances is exact only when nothing can be earned (no
* rewarding if_then rule and no earned activation on the candidate). leave-placements reads
* `supporting`, which finalizing rewrites. Entries follow activeObjectives order.
*/
function finalizeInvariance(context: SearchContext): FinalizeInvariance[] {
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:
// New element kinds stay unpruned until their bound is argued.
return 'unknown';
}
});
finalizeInvarianceCache.set(context, entries);
}
return entries;
}
/** True when the first differing finalize-invariant protected objective is already worse than the baseline. */
function provablyWorseThanProtectedPrefix(
candidate: State,
context: SearchContext,
throughIndex: number,
baseline: readonly PlanningObjective[],
): boolean {
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: State,
candidate: State | null,
@@ -2360,6 +2425,8 @@ function evaluateRuleCandidate(
incumbentVector = objectiveVector(before, context),
): { state: State; vector?: PlanningObjectiveVector } | undefined {
if (!candidate) return undefined;
if (provablyWorseThanProtectedPrefix(candidate, context, throughIndex, baseline))
return undefined;
const state = finalizeRuleCandidate(candidate, context, before);
if (!state) return undefined;
const active: PlanningObjective[] = [];