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:
+15
-1
@@ -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**.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user