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:
1 parent
209bf05f36
commit
6811142299
45 files changed
+8795
-3075
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { createOptimizationPresetRules } from '../../services/rule-service';
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { vi } from 'vitest';
|
||||
@@ -407,6 +408,8 @@ function jointTrace(): PlanningTrace {
|
||||
};
|
||||
}
|
||||
|
||||
const presets = createOptimizationPresetRules();
|
||||
|
||||
describe('CalendarYear planning outcome boundary', () => {
|
||||
beforeEach(() => localStorage.clear());
|
||||
afterEach(() => {
|
||||
@@ -1342,7 +1345,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
);
|
||||
if (outcome.kind !== 'Plan') throw new Error(`Expected Plan, got ${outcome.kind}`);
|
||||
localStorage.setItem('leaveBalances', '[]');
|
||||
localStorage.setItem('vacationRules', '[]');
|
||||
localStorage.setItem('vacationRules', '{"version":1,"rules":[]}');
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear],
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
@@ -1421,7 +1424,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
);
|
||||
if (outcome.kind !== 'Plan') throw new Error(`Expected Plan, got ${outcome.kind}`);
|
||||
localStorage.setItem('leaveBalances', '[]');
|
||||
localStorage.setItem('vacationRules', '[]');
|
||||
localStorage.setItem('vacationRules', '{"version":1,"rules":[]}');
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear],
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
@@ -1496,7 +1499,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
expiryDate: placementDate,
|
||||
},
|
||||
],
|
||||
rules: [],
|
||||
rules: [presets[0]],
|
||||
declaredDays: [
|
||||
{ date: '2024-08-01', type: 'cp' as const },
|
||||
...(fixedAnchor ? [{ date: '2024-07-31', type: 'other' as const }] : []),
|
||||
@@ -1524,7 +1527,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
),
|
||||
);
|
||||
localStorage.setItem('leaveBalances', '[]');
|
||||
localStorage.setItem('vacationRules', '[]');
|
||||
localStorage.setItem('vacationRules', '{"version":1,"rules":[]}');
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear],
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
@@ -1582,7 +1585,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
it('previews a growing and shrinking range across months, then saves it once on release', async () => {
|
||||
localStorage.setItem('app.language', '"fr"');
|
||||
localStorage.setItem('leaveBalances', '[]');
|
||||
localStorage.setItem('vacationRules', '[]');
|
||||
localStorage.setItem('vacationRules', '{"version":1,"rules":[]}');
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear, UserInput],
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
@@ -1749,7 +1752,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
|
||||
it('edits declarations from configuration and restores the preceding result when switched off', async () => {
|
||||
localStorage.setItem('app.language', '"fr"');
|
||||
localStorage.setItem('vacationRules', '[]');
|
||||
localStorage.setItem('vacationRules', '{"version":1,"rules":[]}');
|
||||
localStorage.setItem('leaveBalances', '[]');
|
||||
localStorage.setItem(
|
||||
'declaredNonWorkingDays',
|
||||
@@ -1844,7 +1847,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
});
|
||||
|
||||
it('keeps the preceding calendar until a submitted snapshot returns and ignores later edits', async () => {
|
||||
localStorage.setItem('vacationRules', '[]');
|
||||
localStorage.setItem('vacationRules', '{"version":1,"rules":[]}');
|
||||
localStorage.setItem('leaveBalances', '[]');
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear, UserInput],
|
||||
@@ -1987,7 +1990,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
let now = 1_000;
|
||||
const clock = vi.spyOn(performance, 'now').mockImplementation(() => now);
|
||||
try {
|
||||
localStorage.setItem('vacationRules', '[]');
|
||||
localStorage.setItem('vacationRules', '{"version":1,"rules":[]}');
|
||||
const pending = deferred<PlanningOutcome>();
|
||||
const trace = deferred<PlanningTrace | undefined>();
|
||||
const { calendar, calculation } = calendarFor(
|
||||
@@ -2100,7 +2103,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
let now = 1_000;
|
||||
const clock = vi.spyOn(performance, 'now').mockImplementation(() => now);
|
||||
try {
|
||||
localStorage.setItem('vacationRules', '[]');
|
||||
localStorage.setItem('vacationRules', '{"version":1,"rules":[]}');
|
||||
const old = deferred<PlanningOutcome>();
|
||||
const latest = deferred<PlanningOutcome>();
|
||||
const { calendar, calculation } = calendarFor(plan);
|
||||
|
||||
@@ -1536,6 +1536,24 @@ export class CalendarYear {
|
||||
}
|
||||
|
||||
protected objectiveCopy(id: string) {
|
||||
if (id.startsWith('optimization:')) {
|
||||
const ruleId = id.slice('optimization:'.length);
|
||||
const rule = this.activeCalculation?.snapshot.rules.find(
|
||||
(candidate) => candidate.id === ruleId,
|
||||
);
|
||||
const copy = this.tr().planning.explanation.objectives['optimization'];
|
||||
return {
|
||||
...copy,
|
||||
label: copy.label
|
||||
.replace('{rule}', this.ruleLabel(ruleId))
|
||||
.replace(
|
||||
'{summary}',
|
||||
rule && 'configuration' in rule
|
||||
? this.translationService.optimizationSummary(rule.configuration)
|
||||
: '',
|
||||
),
|
||||
};
|
||||
}
|
||||
const key = id.startsWith('preference:') ? 'preference' : id;
|
||||
const copy = this.tr().planning.explanation.objectives[key];
|
||||
const rule =
|
||||
@@ -1645,7 +1663,7 @@ export class CalendarYear {
|
||||
if (stage === 'validation') return 5;
|
||||
if (stage === 'obligations') return 20;
|
||||
if (stage === 'at-risk') return 35;
|
||||
if (stage.startsWith('preference:')) return 50;
|
||||
if (stage.startsWith('preference:') || stage.startsWith('optimization:')) return 50;
|
||||
if (stage === 'holiday-week-value') return 58;
|
||||
if (stage === 'non-working-day-gain') return 65;
|
||||
if (stage === 'longest-work-sequence') return 72;
|
||||
|
||||
Reference in new issue
Block a user