perf(planning): reject impossible mandatory moves before copying
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
433a8b0ade
commit
c6610cd633
3 files changed
+104
No files matched your search
@@ -0,0 +1,65 @@
|
||||
# PR #278: reject impossible mandatory moves before copying
|
||||
|
||||
Measured on 2026-10-16 against `433a8b0`, which includes the
|
||||
[immutable date-count optimization](pr-278-immutable-date-counts.md).
|
||||
|
||||
A loaded-year CPU profile attributed 5.09 seconds to candidate assignment and
|
||||
6.53 seconds to final validation out of 58.24 seconds. Generic moves and swaps
|
||||
copied the candidate state before discovering that a mandatory placement's new
|
||||
date or leave type could never satisfy its rule.
|
||||
|
||||
The shared assignment function now calls the existing `mandatoryRelocationDate`
|
||||
check before copying. It checks only necessary date and type conditions. Complete
|
||||
multi-day patterns and earned-entitlement chronology are still validated after
|
||||
the candidate is assembled. Preferences remain movable, and valid alternatives,
|
||||
objective order and deterministic tie breaks remain unchanged.
|
||||
|
||||
## Before and after
|
||||
|
||||
Both protocol runs use Node 24.18.0, Bun 1.3.14 and Vitest 5.0.3 on the same local
|
||||
machine. This comparison uses a fresh baseline from this session. No other test,
|
||||
build or profiling workload runs alongside either protocol measurement.
|
||||
|
||||
```sh
|
||||
NODE_OPTIONS=--max-old-space-size=2048 timeout 300s bunx vitest run src/app/benchmark/leave-planner-benchmark.spec.ts --maxWorkers=1
|
||||
```
|
||||
|
||||
All seven warm-ups, fourteen measured calculations, the typed-obligation precheck,
|
||||
fixtures, assertions and resource limits remain unchanged.
|
||||
|
||||
| Measurement | Before | After |
|
||||
| ---------------------------------------------------- | --------: | --------: |
|
||||
| Vitest duration | 232.630 s | 228.130 s |
|
||||
| Benchmark test | 231.483 s | 227.011 s |
|
||||
| Loaded year, mean of two measured calculations | 59.255 s | 57.850 s |
|
||||
| Typed obligations, mean of two measured calculations | 4.552 s | 4.368 s |
|
||||
| Headroom before timeout (Vitest duration) | 67.370 s | 71.870 s |
|
||||
|
||||
The Vitest duration was 1.9% shorter in this pair. External wall time was not
|
||||
recorded for these runs. Both runs completed successfully, and every repeated
|
||||
scenario remained deterministic. The baseline is faster than the one recorded
|
||||
for the preceding optimization, so only same-session pairs are comparable.
|
||||
Live CI performance still needs confirmation.
|
||||
|
||||
## Correctness checks
|
||||
|
||||
Complete outcomes, stable explanations and final-state snapshots match the
|
||||
baseline for all seven scenarios. Baseline snapshots come from the preceding
|
||||
optimization's saved run, whose bundle matches a fresh build of `433a8b0` byte for
|
||||
byte; the loaded-year snapshot was also regenerated in this session. Diagnostic
|
||||
comparisons ran alongside functional checks, so their timings are not used for
|
||||
the protocol comparison.
|
||||
|
||||
The functional coverage suite passes all 640 tests across 33 files. Two additional
|
||||
regression cases cover obligations and lower-priority preferences when a calendar
|
||||
objective competes for the same leave balance. Both cases pass before and after
|
||||
the optimization. Existing tests also exercise typed reassignments, complete
|
||||
multi-day patterns, earned entitlements and counterfactual explanations.
|
||||
Application and spec type checks, lint, source formatting and `git diff --check`
|
||||
pass.
|
||||
|
||||
No browser timing run or live CI rerun was performed for this change.
|
||||
|
||||
Diagnostic sources, profiles, bundles and full logs are retained in
|
||||
`/tmp/vac-opt278-step3`. The original core diagnostic runner is documented in
|
||||
[issue-78.md](issue-78.md#reproduction).
|
||||
@@ -334,6 +334,43 @@ describe('composed calendar objectives', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['obligation', 'preference'] as const)(
|
||||
'keeps a lower %s only when required while completing a holiday week',
|
||||
(force) => {
|
||||
const { plan } = calculate({
|
||||
...concurrent,
|
||||
rules: [
|
||||
holiday,
|
||||
{
|
||||
id: 'monday',
|
||||
type: 'time_window',
|
||||
force,
|
||||
startDate: '2024-04-29',
|
||||
endDate: '2024-04-29',
|
||||
placeDays: 1,
|
||||
placeType: 'cp',
|
||||
},
|
||||
],
|
||||
});
|
||||
if (force === 'obligation') {
|
||||
expect(plan.placements).toMatchObject([
|
||||
{ date: '2024-04-29', type: 'cp', ruleIds: ['monday'] },
|
||||
]);
|
||||
expect(plan.metrics.holidayWeekValue).toBe(0);
|
||||
} else {
|
||||
expect(plan.placements.map(({ date }) => date)).toEqual([
|
||||
'2024-05-06',
|
||||
'2024-05-07',
|
||||
'2024-05-10',
|
||||
]);
|
||||
expect(plan.metrics.holidayWeekValue).toBe(5);
|
||||
expect(plan.placements.every(({ ruleIds }) => !ruleIds.includes('monday'))).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it('adds no residual loss-prevention or calendar placements when their rules are disabled', () => {
|
||||
const request: PlanningRequest = {
|
||||
horizon: { start: '2024-04-29', end: '2024-05-04' },
|
||||
|
||||
@@ -1514,6 +1514,8 @@ function assignDateToBalance(
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// Reject impossible mandatory moves before copying; validate complete patterns later.
|
||||
if (!mandatoryRelocationDate(date, balance, ruleIds, context)) return null;
|
||||
// Existing placements stay read-only; this assignment owns only its new placement.
|
||||
const result = cloneState(state, new Map(state.placements));
|
||||
const target = result.balances.find((item) => item.id === balanceId)!;
|
||||
|
||||
Reference in new issue
Block a user