perf(planning): plan from the best-ranked obligation solution
With several obligation solutions, the planner ran the whole preference chain on candidate solutions as previews: every solution after a complete enumeration, or the eight best-ranked after a truncated window search. It now orders all solutions by the existing pre-preference ranking (active objectives, then tie-breaks, of each obligation-only plan; equal ranks keep enumeration order) and runs the preference stages once, on the first. If that run ends with earned leave available only after its final merged time-off period, the stages restart on the next solution, so a request that planned before never becomes a conflict. The loaded year drops from 43 to 5.5 seconds and typed obligations from 3.3 to 0.1 seconds, with byte-identical benchmark plans. Over the 5,000 seeded requests, 23 plans become worse (9 on a real objective) and 1 better; over 50 loaded-year-sized requests, 4 become worse on a real objective. ADR 0010 records the decision; docs/benchmark/issue-302.md records the comparison and the opt-in runner that reproduces it. The frozen-engine check fails until the frozen engine is rebuilt from this commit in the next one. Part of #302. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa6a0c6f1b
commit
8168137cd5
13 files changed
+579
-278
No files matched your search
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { calculatePlanningInternal, type PlanningRequest } from './leave-planner.core';
|
||||
import { createOptimizationPresetRules } from './rule-service';
|
||||
|
||||
const presets = createOptimizationPresetRules();
|
||||
const preset = (id: string) => presets.find((rule) => rule.id === id)!;
|
||||
|
||||
describe('choosing among obligation solutions', () => {
|
||||
it('plans from the solution ranked best before the preferences', () => {
|
||||
// The obligation day can take either balance. Before the preferences, the
|
||||
// at-risk RTT ranks first because it leaves no at-risk leave unplaced. Funding
|
||||
// it with the CP instead would let the RTT gain three days around Easter
|
||||
// later; ADR 0010 accepts that loss to run the preferences only once.
|
||||
const request: PlanningRequest & { calculationDate: string } = {
|
||||
calculationDate: '2026-04-03',
|
||||
horizon: { start: '2026-04-03', end: '2026-05-24' },
|
||||
balances: [
|
||||
{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2026-06-30' },
|
||||
{ id: 'rtt', type: 'rtt', quantity: 1, expiryDate: '2026-05-23' },
|
||||
],
|
||||
rules: [
|
||||
preset('default-unplaced-at-risk'),
|
||||
preset('default-supporting-placements'),
|
||||
preset('default-non-working-day-gain'),
|
||||
{
|
||||
id: 'thursday',
|
||||
type: 'time_window',
|
||||
force: 'obligation',
|
||||
startDate: '2026-04-09',
|
||||
endDate: '2026-04-09',
|
||||
placeDays: 1,
|
||||
placeType: 'any',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const outcome = calculatePlanningInternal(request);
|
||||
|
||||
expect(outcome.kind).toBe('Plan');
|
||||
if (outcome.kind !== 'Plan') return;
|
||||
expect(outcome.placements).toEqual([
|
||||
{
|
||||
date: '2026-04-09',
|
||||
balanceId: 'rtt',
|
||||
type: 'rtt',
|
||||
ruleIds: ['thursday'],
|
||||
supporting: false,
|
||||
},
|
||||
]);
|
||||
expect(outcome.metrics.unplacedAtRisk).toBe(0);
|
||||
});
|
||||
|
||||
it('plans from the next solution when the best-ranked one earns its leave too late', () => {
|
||||
const request: PlanningRequest & { calculationDate: string } = {
|
||||
calculationDate: '2026-07-09',
|
||||
horizon: { start: '2026-07-09', end: '2026-07-25' },
|
||||
balances: [
|
||||
{
|
||||
id: 'cp',
|
||||
type: 'cp',
|
||||
quantity: 3,
|
||||
availableFrom: '2026-07-13',
|
||||
expiryDate: '2026-08-10',
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
id: 'tuesday',
|
||||
type: 'if_then',
|
||||
force: 'preference',
|
||||
dayOfWeek: 2,
|
||||
placeDays: 1,
|
||||
placeType: 'any',
|
||||
placement: 'auto',
|
||||
rewardDays: 2,
|
||||
rewardType: 'rtt',
|
||||
},
|
||||
{
|
||||
id: 'late-july',
|
||||
type: 'time_window',
|
||||
force: 'obligation',
|
||||
startDate: '2026-07-23',
|
||||
endDate: '2026-08-02',
|
||||
placeDays: 2,
|
||||
placeType: 'any',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const outcome = calculatePlanningInternal(request);
|
||||
|
||||
expect(outcome.kind).toBe('Plan');
|
||||
if (outcome.kind !== 'Plan') return;
|
||||
expect(
|
||||
outcome.placements.map(({ date, balanceId, ruleIds }) => [date, balanceId, ruleIds]),
|
||||
).toEqual([
|
||||
['2026-07-13', 'cp', ['tuesday']],
|
||||
['2026-07-23', 'cp', ['late-july']],
|
||||
['2026-07-24', 'cp', ['late-july']],
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user