feat(planning): make optimization rules composable

Evaluate explicit, ordered optimization configurations through one shared
planner model. Add six editable presets, durable one-time legacy migration,
and consistent completed and cancelled plan provenance.

Include functional regressions and the agreed trace-removal and benchmark
follow-up specs. Performance acceptance remains in #78 under unchanged gates.

Refs: #70, #71, #72, #73, #74, #75, #76, #77, #78
This commit is contained in:
nessar committed 2026-10-10 16:07:34 +02:00
1 parent 209bf05f36
commit 6811142299
45 files changed
+8795 -3075

No files matched your search

@@ -5,11 +5,12 @@ import { RuleService, RuleType } from './rule-service';
import { UserInputService } from './user-input-service';
import { createPlanningCalculation } from './leave-planner.service';
import { PlanningDurationService } from './planning-duration.service';
import { OPTIMIZATION_PRESETS } from './planning-objectives';
describe('Planning duration estimate', () => {
beforeEach(() => {
localStorage.clear();
localStorage.setItem('vacationRules', '[]');
localStorage.setItem('vacationRules', '{"version":1,"rules":[]}');
TestBed.inject(CalendarSettingsService).setPlanningDates('2024-01-01', '2024-12-31');
TestBed.inject(UserInputService).updateBalances(() => [
{
@@ -34,6 +35,22 @@ describe('Planning duration estimate', () => {
expect(TestBed.inject(RuleService).rules).toEqual([]);
});
it('estimates and calibrates optimization rules without a requested leave quantity', () => {
const rules = TestBed.inject(RuleService);
rules.addRule({
id: 'loss',
name: 'Loss',
type: RuleType.OPTIMIZATION,
enabled: true,
force: 'preference',
configuration: structuredClone(OPTIMIZATION_PRESETS.unplacedAtRisk),
});
const duration = TestBed.inject(PlanningDurationService);
expect(Number.isFinite(duration.estimate()!.maximum)).toBe(true);
duration.recordCompletion(duration.request()!, 20_000);
expect(duration.estimate()).toEqual({ minimum: 10, maximum: 60, unit: 'seconds' });
});
it('updates for dates, balances and active rules without requesting a calculation', () => {
const duration = TestBed.inject(PlanningDurationService);
const settings = TestBed.inject(CalendarSettingsService);