Spend vacation on the days worth having, and name the ones about to expire
The planner knew exactly how many semesterdagar a parent was about to forfeit and never placed a single day to prevent it. It also had no weekend-wrap logic at all: REASON_VACATION_WRAP_WEEKEND was stamped on every tier-3 interior vacation day, including a lone Tuesday in February. Two changes. Spend better. A workday adjacent to a free stretch, or in the middle of a gap of three or fewer fenced by free days, now takes vacation -- _vacation_bridge_reason, consulted for every parent type. This departs from spec 6.1's "wrapping suppressed for hourly": the FP-preservation argument is salary-type independent, only the pay argument was hourly-moot. In a both-parents window, both now take vacation on such a date rather than one paying with FP. After funding, reallocate_vacation_by_value swaps vacation onto higher-ranked dates 1-for-1 with FP days, so which dates hold vacation improves while how many never drops -- greedy first, re-timed after, never withheld. An invariant test asserts the count can only rise. Name the losses. A new pass proposes concrete dates for vacation that would otherwise expire, current year only, ranked red-day bridge then weekend bridge then interior, later dates first. They ride on PlanProposal.rescue_days, kept off `days` so nothing downstream commits them, and surface on the Dashboard with accept-one, accept-all (one batched /api/plan/apply, not N) and dismiss. Dismissal reuses the tip_dismissal table, keyed on (parent, year) identity rather than the group's dates -- those are recomputed from the whole plan's residual on every propose, so date-hashing made dismissal self-defeating. Dismiss does not bump the plan version; it changes no data and must not trigger a write. Measured on the seeded demo: 12 forfeiting days for one parent now yield 12 proposed dates, all Fridays or Mondays. Total vacation placed is unchanged at 203 days, confirming the reallocation only re-times. Three defects here were found by driving a browser, while 2273 unit tests passed -- all three lived in the seam between layers that unit tests mock away. Adds API-level tests over the real request sequence. Rate limiting is now off in tests (one file consumed the whole registration budget) with the suite's first real 429 assertion. PROPOSE_VS_SYNC_BUG.md documents a pre-existing, unfixed issue found along the way: Dashboard figures swing on the first sync with no user action. Backend 946 tests, frontend 1341, coverage above all gates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZaX6xbuMEMXuyKA1fq2Mm
This commit is contained in:
+24
-12
@@ -236,27 +236,39 @@ choice is genuinely ambiguous:
|
||||
|
||||
**Tier 1 — free days.** Saturday or in the holiday set. Cost nothing. Never spend a resource here.
|
||||
|
||||
**Tier 2 — dominant plays.** Days where one resource wins on *every* axis, regardless of goal. The
|
||||
main case is a **klämdag inside a red-day cluster** → spend **semester**:
|
||||
**Tier 2 — dominant plays (§weekend-wrap-bridge, revised 2026-09-20).** Days where vacation is
|
||||
spent to **bridge or extend a free stretch**, regardless of goal *or* `salary_type`. This tier used
|
||||
to fire only for a **klämdag inside a real red-day cluster**; it now also covers the far more common
|
||||
case of a bare weekend wrap (Friday before / Monday after a plain Sat+Sun) and the middle day of any
|
||||
short (≤3-day) non-working gap, whether or not a real red day is involved. See
|
||||
`_vacation_bridge_reason` in `allocation_service.py` for the exact rule (adjacency first, then
|
||||
short-run-middle) and its reason codes (`vacation_wrap_red_day` when the adjacent/fencing free run
|
||||
contains a genuine holiday, `vacation_wrap_weekend` otherwise):
|
||||
|
||||
| Benefit | Monthly salary | Hourly salary |
|
||||
|---|---|---|
|
||||
| Preserves an FP day for later (time) | ✅ | ✅ |
|
||||
| Protects pay you'd lose on an FP day (money) | ✅ | ❌ |
|
||||
|
||||
Note the asymmetry, which §6.3 hints at but does not spell out: the **FP-preservation** half is
|
||||
salary-type independent, so red-day-cluster vacation is favourable for hourly parents too. Only the
|
||||
**pay-protection** half is monthly-only. This is why the §6.1 suppression for hourly applies to
|
||||
*weekend wrapping* specifically — for an hourly parent there is no weekend salary to protect, so
|
||||
bracketing a bare Sat/Sun with Fri+Mon vacation buys nothing — but it does **not** justify suppressing
|
||||
red-day-cluster vacation.
|
||||
**Superseded reasoning (kept for history):** this tier used to be suppressed for weekend-only
|
||||
wraps on an hourly parent, on the theory that "there is no weekend salary to protect, so bracketing
|
||||
a bare Sat/Sun with Fri+Mon vacation buys nothing." That argument only weighed the pay-protection
|
||||
half of the benefit table and ignored the FP-preservation half, which — as the row above already
|
||||
shows — is salary-type independent. **Decision (2026-09-20):** the tier now fires ungated, for every
|
||||
`salary_type` and regardless of `vacation_forfeiting`. For an hourly parent whose vacation is not
|
||||
otherwise expiring, this is a genuine behaviour change: vacation now pays the Friday/Monday instead
|
||||
of an FP day (or, previously, instead of nothing at all — a hollow "buys nothing" framing that
|
||||
missed the FP saved for later), trading a day that costs the household nothing extra for a day kept
|
||||
in the bank. The old `REASON_VACATION_WRAP_WEEKEND` reason code, retired when this suppression was
|
||||
introduced, is reinstated for exactly this case.
|
||||
|
||||
> **SUPERSEDED (2026-09-15).** Tier 3 no longer consults a goal at all — there is a single policy,
|
||||
> branching only on `salary_type`, that maximises value extracted from FP + vacation combined. The
|
||||
> table below has been replaced to describe it; see `_assign_tier3` in `allocation_service.py`.
|
||||
|
||||
**Tier 3 — salary-type interior.** Ordinary working days with no red-day adjacency, where semester
|
||||
vs FP is a real tradeoff:
|
||||
**Tier 3 — salary-type interior.** Ordinary working days that tier 2 does not claim as a bridge
|
||||
(see `_vacation_bridge_reason`: not adjacent to a free day, and not the middle day of a ≤3-day
|
||||
gap) — genuinely interior weekdays where semester vs FP is a real tradeoff:
|
||||
|
||||
| `salary_type` | Interior choice | Why |
|
||||
|---|---|---|
|
||||
@@ -348,7 +360,7 @@ Constraints — all hard unless marked:
|
||||
`employment_status`, never a block.
|
||||
|
||||
Every assigned day carries a machine-readable `reason` so the UI can explain itself:
|
||||
`free_weekend`, `free_red_day`, `vacation_wrap_weekend`, `vacation_wrap_red_day`, `fp_interior`,
|
||||
`free_weekend`, `free_red_day`, `vacation_interior`, `vacation_wrap_red_day`, `fp_interior`,
|
||||
`fp_lagsta_stretch`, `swap_first_half`, `swap_second_half`, `dubbeldag`.
|
||||
|
||||
### 3.6 Shortfall, not failure
|
||||
@@ -422,7 +434,7 @@ Against the **pure** `Allocator`, with a frozen `today`. Target ≥90% on new ba
|
||||
| 1 | solo, `max_time`, 6-week summer, child < 12 months | **zero** FP days assigned (SGI protected) |
|
||||
| 2 | couple, `balanced`, 6-week summer | `parent_swap`, 3+3, ≈6 weeks together |
|
||||
| 3 | `max_money` + monthly salary | vacation lands on Fri/Mon around weekends |
|
||||
| 4 | `max_money` + **hourly** salary | wrapping **suppressed**; no vacation-wrap reasons emitted |
|
||||
| 4 | `max_money` + **hourly** salary | weekend/red-day **wrapping still fires** (vacation on Fri/Mon and short fenced gaps, `vacation_wrap_*` reasons emitted, §weekend-wrap-bridge below) — only genuinely **interior** mid-week days still prefer FP |
|
||||
| 5 | wishes exceed budget | lower-priority window shows a shortfall; higher ones are full |
|
||||
| 6 | Jullov 12-20 → 01-07 | window wraps years; vacation debited from **both** years |
|
||||
| 7 | two children, overlapping windows | pools kept separate per `(parent, child)` |
|
||||
|
||||
Reference in New Issue
Block a user