# Issue #302: plan from the best-ranked obligation solution Step-by-step record for [issue #302](https://gitea.nessar.fr/nessar/vac-optimizer/issues/302). The exact steps keep planning outcomes, stable explanations and final states identical to the frozen engine at `a871f8f`. The [frozen-engine differential check](README.md#frozen-engine-differential-check), including its cancelled asynchronous comparisons, passes after every step. ## Per-step measurements Each step was timed against its parent in the same session, on the development machine (AMD Ryzen 7 7800X3D, WSL2, Bun 1.4.3), with no other test, build or profiling workload running. Both commits' cores were bundled with the frozen-engine build command and called through `calculatePlanningAsyncInternal` in one Bun process, with `calculationDate: '2024-01-01'`. This is the benchmark's in-thread asynchronous path, without the planning-calculation service. For every scenario, each engine ran one warm-up and then one measured calculation, alternating engines. Outcomes, explanations and final states were hashed; every pair below matched. Only same-session pairs are comparable. ### Reuse the donor ranking across public-holiday weeks For every public-holiday week, the holiday-week stage ranked every placement as a potential donor by releasing it and evaluating the active objectives. That ranking depends only on the plan, so the weeks of one plan now share each placement's released objectives. Each week still skips its own dates and walks the placements in the same order with the same yields. | Scenario | Before (s) | After (s) | | --------------------------- | ---------: | --------: | | representative year | 0.66 | 0.66 | | typed obligations | 5.11 | 4.89 | | chained earned entitlements | 0.02 | 0.02 | | deliberate conflict | 0.01 | 0.01 | | loaded year | 58.26 | 57.75 | | two years | 2.27 | 1.82 | | three years | 7.45 | 5.52 | ### Retain the best valid plan more cheaply Retaining a better valid plan for cancellation reconciled its optimization provenance. For every placement credited by an optimization rule, that evaluated all active objectives with the placement released. Only the credited objectives are compared, so the released plan now evaluates just those, and the calendar observations of the other objectives are no longer computed. Retention also no longer computes quality metrics that nothing read. An instrumented three-year run spent 1.16 of its 1.22 retention seconds in that reconciliation; afterwards retention took 0.26 seconds. | Scenario | Before (s) | After (s) | | --------------------------- | ---------: | --------: | | representative year | 0.45 | 0.37 | | typed obligations | 4.76 | 5.03 | | chained earned entitlements | 0.02 | 0.02 | | deliberate conflict | 0.01 | 0.01 | | loaded year | 55.59 | 53.58 | | two years | 1.68 | 1.22 | | three years | 5.29 | 3.85 | The typed-obligations difference is run-to-run noise; its retention cost was 22 milliseconds. ### Yield between asynchronous slices without a timer The asynchronous adapter yields roughly every eight milliseconds. Each yield waited for a zero-delay timer: about 1.1 ms in Bun, and at least 4 ms in browsers once timers nest. Browsers now yield through a message-channel task. Bun runs port messages ahead of timers and incoming worker messages, so a message-channel yield there kept cancellation and the heartbeat from running until the calculation ended. Bun therefore yields with `setImmediate`, which costs about 0.01 ms and lets both run between slices. | Scenario | Before (s) | After (s) | | --------------------------- | ---------: | --------: | | representative year | 0.37 | 0.31 | | typed obligations | 4.74 | 4.02 | | chained earned entitlements | 0.02 | 0.01 | | deliberate conflict | 0.01 | 0.00 | | loaded year | 53.80 | 46.90 | | two years | 1.23 | 1.04 | | three years | 3.86 | 3.21 | The browser path was checked in headless Chromium 149 on the same machine, in a dedicated worker with a 50 ms heartbeat timer, running the bundled core before and after this step, alternating, twice each. The `three years` calculation took 3.92 and 3.89 seconds before and 2.70 and 2.66 seconds after; `representative year` took 0.51 and 0.45 seconds before and 0.30 seconds after. The heartbeat's largest gap stayed between 58 and 67 ms in every run. Cancelling `three years` one second after the request returned the retained plan 3.9 to 4.0 ms after the cancel message reached the worker, against 7.2 to 8.7 ms before. This is desktop evidence, not a reference-phone measurement. ### Validate each optimization configuration once Every objective evaluation revalidated its optimization configuration against the catalog. Rule normalization already validates each configuration once per calculation and keeps its own copy. The presets are constant, and the summed variant of a valid configuration is valid because every catalog row accepts the sum. The core therefore scores them without revalidating; `evaluateOptimization` still validates for its other callers. | Scenario | Before (s) | After (s) | | --------------------------- | ---------: | --------: | | representative year | 0.35 | 0.30 | | typed obligations | 4.03 | 3.46 | | chained earned entitlements | 0.01 | 0.01 | | deliberate conflict | 0.00 | 0.00 | | loaded year | 48.12 | 44.03 | | two years | 1.04 | 0.96 | | three years | 3.35 | 3.03 |