From 0e14b4dac8db38f70eca71e22664a7070f3c87f0 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Tue, 15 Sep 2026 21:18:38 +0200 Subject: [PATCH] Flag the superseded sections of the design docs ARCHITECTURE.md and PLANNING_ENGINE_SPEC.md still described the planning goal, the per-wish goal override, and a per-parent age-4 ceiling of 96 days as current. Rather than rewrite two long documents that remain useful as history, each stale section now carries a SUPERSEDED banner naming what is true instead. The banners sit above the code fences they annotate, not inside them. --- ARCHITECTURE.md | 16 +++++++++++++++- PLANNING_ENGINE_SPEC.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a47db5d..f169fa5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,5 +1,7 @@ # Föräldraledigplannerare - Architecture & Implementation Plan +> Parts of this document are superseded — search for **SUPERSEDED** for the current behaviour. + **Date:** 2026-05-31 (updated 2026-09-13) **Status:** Planning phase - reviewed, decisions incorporated (see §0). Planning engine drafted in **PLANNING_ENGINE_SPEC.md**. **Tech Stack:** TypeScript + React + Vite (frontend), FastAPI + Python 3.14 (backend), SQLite → Postgres @@ -276,6 +278,8 @@ Two ways a second parent enters a workspace: ### 3.1 Entities (Domain Layer) +> **SUPERSEDED (2026-09-15).** `goal_override` was removed entirely (migration 009). The allocator now applies a single policy based solely on `salary_type`. `PlannedLeaveDay` gained `wish_id` (migration 008) linking it back to the wish that produced it (nullable, for ad-hoc/legacy days), and `is_pinned` to mark days the user hand-edited so the allocator won't retype them. + ```typescript // backend/app/entities/personal_data.py from dataclasses import dataclass @@ -458,6 +462,8 @@ class PlannedLeaveDay: ### 3.2 Database Schema (SQLite/Postgres) +> **SUPERSEDED (2026-09-15).** `planned_leave_day` gained `wish_id` (migration 008) to link persisted days back to the wish that produced them, and `is_pinned` to mark hand-edited days the allocator should not retype. `workspace.planning_goal` was removed entirely (migration 009). `leave_wish` gained nullable `target_days` (migration 008, overrides `target_weeks`), and `goal_override` was removed entirely (migration 009). + ```sql -- personal_data: a parent profile (login optional). Isolated for GDPR. CREATE TABLE personal_data ( @@ -777,6 +783,8 @@ class PlanningService: ## 5. API Endpoints +> **SUPERSEDED (2026-09-15).** The `planning_goal` endpoint was removed entirely (migration 009). + ``` # Authentication POST /api/auth/register @@ -854,6 +862,8 @@ The app provides contextual recommendations. Because the app does **not** comput - During the child's **first year**, SGI is protected regardless of how few days are taken — so a parent can take little or no föräldrapenning that year and preserve days for later without losing SGI. - After the child turns **1**, a non-working parent generally must take out parental benefit **at least 5 days/week** (or be a registered jobseeker) to keep full SGI. +> **SUPERSEDED (2026-09-15).** `planning_goal` was removed entirely (migration 009). The engine now applies a single allocation policy based solely on `salary_type`, as described in PLANNING_ENGINE_SPEC.md §3.3. + The engine derives these from the child's birth date and the planned weekly intensity; **no salary figure is needed** — only the categorical `salary_type` (monthly vs hourly) and `employment_status` on each parent, plus the workspace `planning_goal`. Tips are ranked by the active goal (`max_time` surfaces day-preserving tips; `max_money` surfaces salary-protecting tips). The rules below reference helper predicates that operate on the combined plan (fetched snapshot + planned days). ### 6.1 Tip catalogue (incl. user-specified strategies) @@ -995,9 +1005,11 @@ The weekend/red-day "wrapping" tips reflect a common monthly-salary pattern, but This is the app's reason to exist, but the original draft never defined it. It combines three inputs **per parent, per child**: +> **SUPERSEDED (2026-09-15).** The age-4 savable cap was incorrectly applied as 96 per parent. It is now 48 per parent in couple mode with two parents, and 96 otherwise (commit 6da947f, `resolve_age4_savable_cap`). + 1. **Actual usage** from the latest `ParentalLeaveFetch` (or manual entry): `days_used`, `days_left`, tier breakdown. 2. **Planned future days** the workspace has laid out in the calendar (not yet reported to FK). -3. **Statutory caps**: the 480/parent pool, the 96-day age-4 ceiling, and the age-12 hard expiry. +3. **Statutory caps**: the 480/parent pool, the 96-day age-4 ceiling (or 48 in couple mode with two parents), and the age-12 hard expiry. **Definitions** (compute as of the child's 4th birthday `B4 = birth_date + 4 years`): @@ -1439,6 +1451,8 @@ The planner never makes API calls at runtime — it uses a committed calendar ca 1. **`AllocationInputLoader`** (async) — the ONLY part that touches repositories or the wall clock. Assembles a frozen `AllocationInput` from database state. +> **SUPERSEDED (2026-09-15).** The allocator no longer uses `planning_goal`. It now applies a single policy based solely on `salary_type` (PLANNING_ENGINE_SPEC.md §3.3). + 2. **`Allocator`** (pure) — no `await`, no repository access, no `date.today()`. Given the same `AllocationInput`, always produces identical output. This is the engine's heart: it turns `LeaveWish` intents into `ProposedDay` rows, choosing between föräldrapenning tiers and vacation based on the workspace `planning_goal` and parent `salary_type`, modeling parent-swap, dubbeldagar, and preschool day reserves along the way. The separation is critical: the pure allocator is cheap, re-runnable on every wish edit, and trivially unit-testable without a database. For the full design (window expansion, tier selection, shortfall remedies, warnings), see **PLANNING_ENGINE_SPEC.md**. diff --git a/PLANNING_ENGINE_SPEC.md b/PLANNING_ENGINE_SPEC.md index 680fb0c..30d7830 100644 --- a/PLANNING_ENGINE_SPEC.md +++ b/PLANNING_ENGINE_SPEC.md @@ -1,5 +1,7 @@ # Planning Engine — Design Spec +> Parts of this document are superseded — search for **SUPERSEDED** for the current behaviour. + **Status:** Draft for review. No code written yet. **Author:** Senior (design) — implementation to be handed to Sonnet subagents, phase by phase. **Supersedes:** nothing. **Extends:** ARCHITECTURE.md §6 (tips engine) and §6.5 (age-4 projection). @@ -18,6 +20,8 @@ That decision is the product. ARCHITECTURE.md §6 already states the principle: > Because the app does not compute kronor, the "money" dimension is expressed qualitatively through > two related levers: **weekly intensity** and **SGI protection**. +> **SUPERSEDED (2026-09-15).** `planning_goal` was removed entirely (migration 009). The engine now applies a single policy based solely on `salary_type`. + So the engine must not have a fixed spend order. Choosing semester vs föräldrapenning on a given day *is* the optimisation, and it is driven by `planning_goal` (`max_time` / `max_money` / `balanced`) crossed with the parent's `salary_type` (`monthly` / `hourly`). @@ -64,6 +68,8 @@ is **zero today — `select count(*) from break_period` returns 0.** If we defer `LeaveWish`. Rejected — the materialised rows are derivable, and persisting derived state invites drift. +> **SUPERSEDED (2026-09-15).** `goal_override` was removed entirely (migration 009). + ```python # backend/app/entities/leave_wish.py @dataclass @@ -125,6 +131,8 @@ ALTER TABLE workspace ADD COLUMN planning_mode TEXT NOT NULL DEFAULT 'solo' ### 2.3 No change to `planned_leave_day` +> **SUPERSEDED (2026-09-15).** `planned_leave_day` gained `wish_id` (migration 008) linking days back to the wish that produced them, and `is_pinned` to mark hand-edited days the allocator should not retype. + The proposal writes ordinary `planned_leave_day` rows on apply (`day_type` stays `'sjukpenning' | 'lagsta' | 'vacation'`). The existing calendar, dashboard and analytics then work unchanged. This is deliberate: the engine produces *nothing the user could not have entered by hand*. @@ -148,6 +156,8 @@ step that runs before funding, for that subset of wishes. ### 3.1 Input +> **SUPERSEDED (2026-09-15).** `planning_goal` was removed entirely (migration 009). + ```python @dataclass(frozen=True) class AllocationInput: @@ -186,6 +196,8 @@ year-wrap/vacation-straddle rules above without any change. ### 3.3 Choose a strategy per window — dominance first, goal only for ties +> **SUPERSEDED (2026-09-15).** `planning_goal` was removed entirely (migration 009). The allocator now applies a single policy based solely on `salary_type`, skipping the goal-driven tier-3 choice entirely. + A naive design makes `planning_goal` a hard switch between "spend semester" and "spend FP". That is wrong, and the Jullov case shows why: @@ -213,6 +225,8 @@ salary-type independent, so red-day-cluster vacation is favourable for hourly pa bracketing a bare Sat/Sun with Fri+Mon vacation buys nothing — but it does **not** justify suppressing red-day-cluster vacation. +> **SUPERSEDED (2026-09-15).** Tier 3 with goal-dependent choices was removed entirely. All tier-3 decisions now depend solely on `salary_type`. + **Tier 3 — goal-dependent interior.** Ordinary working days with no red-day adjacency, where semester vs FP is a real tradeoff. Here, and only here, consult the resolved goal (`wish.goal_override or planning_goal`): @@ -230,6 +244,8 @@ vs FP is a real tradeoff. Here, and only here, consult the resolved goal then resolved through the tiers above. This remains the single biggest lever in the app — per §6.1, two halves of length L yield ≈2×L weeks of family time (3+3 weeks ≈ 6 weeks together). +> **SUPERSEDED (2026-09-15).** `goal_override` was removed entirely (migration 009). This test is no longer applicable. + **Corollary worth testing:** because tiers 1–2 are goal-independent, setting `goal_override = 'max_money'` on a red-day-dense window like Jullov should produce **the same allocation** as `max_time` would. The override is a UI convenience for the user's mental model, not a semantic @@ -274,6 +290,8 @@ Constraints — all hard unless marked: being modelled correctly.)* - **Dubbeldagar**: only within the child's first 15 months, ≤60 total, consumes **both** parents' pools for that date. Only reachable via `parent_mode == 'both'`. +> **SUPERSEDED (2026-09-15).** The age-4 savable cap was incorrectly applied as 96 per parent. It is now 48 per parent in couple mode with two parents, and 96 otherwise. + - **Age-4 is an objective *before* B4 and a hard cap *after* it** (§9.1). Before the 4th birthday, days above 96 that remain will be *lost*, so the allocator mildly **prefers** spending before B4 and warns when `projected_left_at_b4 > 96`. From B4 onward the available pool is hard-capped at @@ -338,6 +356,8 @@ with existing planned days unless `replace: true` is passed. Routers stay thin; ## 5. Test matrix (the acceptance criteria) +> **SUPERSEDED (2026-09-15).** Several test cases are outdated: (1) Tests 1, 3–4 assume `planning_goal` is operative (removed migration 009); (2) Test 14 refers to `goal_override` (removed); (3) Tests 8 and 12 use the incorrect 96-day-per-parent cap (now 48 per parent in couple mode with two parents, 96 otherwise). + Against the **pure** `Allocator`, with a frozen `today`. Target ≥90% on new backend code. | # | Scenario | Expected | @@ -432,6 +452,8 @@ test matrix lives. Phases A–E each end with `graphify update .` per CLAUDE.md. 1. **Horizon runs past age 4.** Wishes may extend beyond the 4th birthday and the app must not block that. Consequence — the FP budget becomes **time-dependent**, which the allocator must model: + > **SUPERSEDED (2026-09-15).** The 96-day cap is now 48 per parent in couple mode with two parents, 96 otherwise. + ``` FP available for a day d: d < fourth_birthday → full remaining pool d >= fourth_birthday → min(remaining, 96) @@ -450,6 +472,8 @@ test matrix lives. Phases A–E each end with `graphify update .` per CLAUDE.md. reserve is **user-configurable** (`preschool_planning_days_per_year`, default 6 — preschools differ), so it needs a per-child settings field, not just the config default. +> **SUPERSEDED (2026-09-15).** `goal_override` was removed entirely (migration 009). + 4. **Wish-level goal override: yes, as a nullable field now, no UI in v1.** `LeaveWish.goal_override: str | None`; the allocator resolves `goal = wish.goal_override or input.planning_goal`. Costs one field and one test, and avoids a second migration when the UI arrives. @@ -517,6 +541,8 @@ open. One new field on `LeaveWish`, added to the trailing defaulted group so every existing construction site (fixtures, the repo, the service) keeps compiling unchanged: +> **SUPERSEDED (2026-09-15).** `goal_override` was removed entirely (migration 009). + ```python @dataclass class LeaveWish: @@ -650,6 +676,8 @@ one collision key are unchanged. know about the 15-month rule at all. - **Reserved days.** No interaction — reserved days are a transfer constraint on `parent_swap` composition (§3.5), orthogonal to which window was picked. +> **SUPERSEDED (2026-09-15).** `planned_leave_day` gained `wish_id` (migration 008) linking days back to the wish that produced them, and `is_pinned` to mark hand-edited days the allocator should not retype. + - **Applied flexible wishes participate in re-search.** On a subsequent `propose()` call (after the family edits wishes), a previously-applied flexible wish is re-searched from scratch within its bound and may land in a different window if the occupied set has changed (other wishes added, edited or