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.
This commit is contained in:
2026-09-15 21:18:38 +02:00
parent cc3ced905e
commit 0e14b4dac8
2 changed files with 43 additions and 1 deletions
+15 -1
View File
@@ -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**.
+28
View File
@@ -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