Files
F-r-ldraledigplannerare/CLAUDE.md
T
jacobh2 97a7200c1a Parse transferable days from FK instead of deriving them
The scraper never read "överlåtbara dagar" from Försäkringskassan. It only
visited ansok-och-planera-foraldrapenning#/valjdagar and derived the
reserved/transferable split in build_snapshot, assuming transferable days
are spent before reserved ones. On real data that gave 49 for the user and
0 for the partner where FK states 129 and 71.

The assumption cannot be repaired: reserved-first, transferable-first,
proportional and cap-based models were all checked against real figures and
none reproduces FK's accounting (reserved-first fits the partner but is 10
days off for the user). FK publishes the real numbers per parent per tier on
the min-foraldrapenning page, which the scraper had never visited.

- Scrape the "Dagar ni kan föra över" panel after the valjdagar extraction,
  summing sjukpenningnivå + lägstanivå per parent. The totals are not
  literals on the page. Parse only direct-child <p> elements: the first
  group embeds a tooltip with decoy "45 dagar"/"90 dagar" text.
- build_snapshot accepts the reported value and derives reserved days from
  it; the old arithmetic remains only as a fallback. An implausible FK value
  degrades to that fallback rather than failing the fetch.
- Record provenance in transferable_days_reported (migration 006) and badge
  derived values as estimates in the dashboard breakdown. Drop the footnote's
  claim that at most 150 days are transferable — real data contradicts it as
  a per-family figure.
- Select the child's section by name when FK lists several; never guess.

Verified against a live BankID scrape: scraped 240/101/139 with 129
transferable, deduced partner 240/169/71 with 71, both flagged as reported.

Incidental fixes found along the way:

- The fetch router never forwarded the partner's transferable figure to
  persist_partner_deduction, so the partner's stored snapshot would have kept
  the flawed split even once the parse landed.
- 'deduced' snapshots were labelled "Manually entered" by a two-way ternary
  over a three-value union. Replaced with a compiler-checked exhaustive map.
- Alembic resolved its database from alembic.ini alone, ignoring
  APP_SQLITE_PATH, so migrations silently targeted the developer's real
  database even when pointed elsewhere. env.py now resolves it, preferring
  -x sqlalchemy.url, then a real APP_SQLITE_PATH, then the ini value.
- backend/captures/ was not ignored: the root pattern was anchored as
  /captures/ despite a comment saying scraper dumps must never be tracked.
- CLAUDE.md quoted test counts and coverage percentages that had gone stale,
  which made a correct verification report look suspect. Gate thresholds kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXPBfr1v7F71t6QcbHCaaT
2026-09-20 09:11:53 +02:00

204 lines
13 KiB
Markdown

# AGENTS.md
Operating guide for humans and AI agents in **Föräldraledigplannerare** — a web app for planning Swedish parental leave (solo or with a partner), folding in vacation days to optimize leave until the child turns 4 (after which only 96 days may be saved).
**Full-stack monorepo:** TypeScript/React/Vite frontend + Python/FastAPI backend.
---
## 🛑 DIRECTORY NAVIGATION & FILE DISCOVERY
**Context:** huge generated/dependency folders (`node_modules`, `dist`, `build`, `.venv`/`venv`, `__pycache__`, `.mypy_cache`, `.ruff_cache`, `graphify-out`, …). Unrestricted listings bloat context, crash sessions, waste tokens.
* **Never `ls -R`** — forbidden here.
* **Never `find` without `-maxdepth`** (use `-maxdepth 3` or tighter); unbounded `find` crawls deps like `ls -R`.
* **Discovery:** default to **`git ls-files`** — source code only.
* **Structure:** only when you need a visual layout, `tree` with mandatory ignores:
```bash
tree -I 'node_modules|.git|dist|build|__pycache__|venv|.venv|env|.pytest_cache|.mypy_cache|.ruff_cache|graphify-out|*.egg-info'
```
* **Search:** **`git grep "term"`**, never `grep -r` (crawls ignored/minified assets).
* **Never leave the current directory**; use relative paths from "here".
---
## graphify
Knowledge graph at `graphify-out/`: god nodes, community structure, cross-file relationships.
On `/graphify`, first invoke the `skill` tool with `skill: "graphify"`.
* Codebase questions: run `graphify query "<question>"` when `graphify-out/graph.json` exists; `graphify path "<A>" "<B>"` for relationships, `graphify explain "<concept>"` for concepts. These return a scoped subgraph — usually far smaller than `GRAPH_REPORT.md` or raw grep.
* Dirty `graphify-out/` files are expected after hooks/incremental updates and are **not** a reason to skip graphify. Skip only if the task is about stale/incorrect graph output, or the user says not to use it.
* Prefer `graphify-out/wiki/index.md` (if present) over raw source browsing for broad navigation.
* Read `graphify-out/GRAPH_REPORT.md` only for broad architecture review or when query/path/explain lack context.
* After code changes: `graphify update .` (AST-only, no API cost).
---
## QUICK START
Frontend `package.json` orchestrates both services. From `frontend/`:
```bash
npm install # frontend deps
npm run dev # BOTH: FastAPI (:8000) + Vite dev server, concurrently
```
Backend standalone, from `backend/`:
```bash
uv sync # deps incl. dev group
uv run uvicorn app.main:app --reload --port 8000 # serve API
```
Scraper (FK login via Playwright) is **off by default**. Locally: `npm run login` from `frontend/` (sets `APP_SCRAPER_ENABLED=true APP_SCRAPER_HEADLESS=true`). Chromium installs separately: `uv run playwright install chromium`.
---
## REPOSITORY LAYOUT
```
backend/ Python 3.14 · FastAPI · uv (Hatchling build backend)
app/
entities/ Persistence-layer row shapes (DB-facing)
models/ Pydantic v2 request/response schemas (API-facing)
repositories/ Abstract repository interfaces
repositories_impl/
sqlite/ Default aiosqlite implementations
postgres/ Optional asyncpg implementations (extra: `postgres`)
routers/ FastAPI route handlers (thin)
services/ Business logic (planning, tips, analytics, auth, …)
scrapers/ Försäkringskassan (FK) scraper — Playwright
utils/ dates, holidays, errors, tip predicates
db/ Connection management
migrations/ Alembic migrations (NOT auto-generated from ORM models)
tests/ pytest + pytest-asyncio (asyncio_mode = auto)
frontend/ TypeScript · React 18.3 · Vite 5 · Vitest
src/
pages/ Route-level screens (Dashboard, Calendar, Settings, …)
components/ Presentational + composite components (BEM CSS classes)
hooks/ Data-fetching/state hooks (the data layer — no TanStack Query)
services/ api.ts (HTTP client) + errors.ts
context/ Auth + Workspace React context
types/ Shared TS types (api.ts, entities.ts)
utils/ dates, formatting
tests/ Vitest setup + fixtures; co-located *.test.tsx also used
.github/workflows/ CI (GitHub Actions) — backend + frontend gates
```
**Backend layering:** `routers` → `services` → `repositories` (abstract). Concrete DB code only in `repositories_impl/`. Routers stay thin, logic in services; never skip layers (e.g. router → `repositories_impl`).
**Frontend data layer:** components → `src/hooks/*` → `src/services/api.ts`. Components never call `fetch`/the API client directly.
**Captures never tracked:** `captures/` (scraper page dumps, verification screenshots) is gitignored — real FK pages hold personal data. Parser tests use the synthetic fixture `backend/tests/fixtures/fk_overview_synthetic.html`.
---
## CONVENTIONS
* **Backend:** FastAPI + Pydantic v2 models with a **hand-rolled repository pattern** (interfaces in `app/repositories/`, impls in `app/repositories_impl/{sqlite,postgres}/`). **No ORM** (no SQLModel/SQLAlchemy). Default: SQLite via async **`aiosqlite`**; Postgres via `asyncpg` is an optional extra.
* **Migrations:** Alembic (`backend/migrations/`, `backend/alembic.ini`), hand-written against the schema, not generated from ORM metadata. Add a revision for every schema change.
* **Auth:** `argon2-cffi` password hashing, `PyJWT` sessions/tokens, `slowapi` rate limiting.
* **Python:** `>=3.14`, managed with **`uv`** + **Hatchling**. Lint/format: **`ruff`** (line length 100, target `py314`). Tests: **`pytest` + `pytest-asyncio`** with `asyncio_mode = "auto"` (no `@pytest.mark.asyncio` needed).
* **Frontend:** **React 18.3**, **react-router-dom v6** (`BrowserRouter`/`Routes`/`Route`), **TypeScript 5.6**, **Vite 5**, **Vitest** + Testing Library. State/data fetching via **custom hooks** in `src/hooks/` (no TanStack Query, no Redux).
* **Styling:** plain CSS, **BEM class names** (e.g. `calendar-day__number`, `calendar-day--disabled`). No CSS framework (no Tailwind), no CSS-in-JS. Follow existing components.
* **Linting:** ESLint **is** configured (flat config `frontend/eslint.config.js`, ESLint 9). `@typescript-eslint/no-explicit-any` is an **error**. CI runs `npm run lint --max-warnings 0` — zero warnings.
* **Scripts:** backend helper scripts go in `backend/scripts/` (create if missing); frontend scripts via `package.json`.
* **CI/CD:** **GitHub Actions** (`.github/workflows/`) only. Don't add `.gitea/`, `.gitlab-ci.yml`, or `Makefile`s — extend the existing workflow.
---
## TESTS, LINT & CI GATES
Run CI's checks before pushing.
**Backend** (from `backend/`):
```bash
uv run ruff check . # lint
uv run ruff format --check . # format check
uv run pytest # tests + coverage (config in pyproject.toml)
```
Gate: `--cov-fail-under=80`, climbing toward **90**. Never lower it; raise only as coverage grows.
**Frontend** (from `frontend/`):
```bash
npm run typecheck # never `npx tsc` — resolves a newer TS, emits bogus TS5101
npm run lint # ESLint, zero warnings
npm run coverage # Vitest with coverage
```
Gates (baseline measured after including `src/pages/**`): lines 96 | functions 91 | branches 92 | statements 96.
A regression ratchet, not a target. Verify current coverage is above these thresholds; never lower gates to pass.
### WHO RUNS VERIFICATION (cost rule)
These checks are mechanical: fixed commands, judgement limited to reading pass/fail and failure output. Paying a large model to watch `pytest`, `vitest`, `tsc`, `ruff` or coverage is waste.
* **Haiku subagent** — run them yourself, immediately; no delegation (you're the cheap tier; spawning another agent only adds latency).
* **Sonnet or larger subagent** — don't run them. Finish the change, spawn a **Haiku** subagent to run them and report raw output, then act on it.
* **Orchestrating/reviewing agent** — same: delegate to Haiku.
Applies to full-suite runs: `uv run pytest`, `uv run ruff check .`, `uv run ruff format --check .`, `npm run typecheck`, `npm run lint`, `npm run coverage`, `npx vitest run`, `uv run alembic upgrade head`.
Does **not** apply to quick mid-task diagnostics (reading one file, a `git grep`, one focused test) — delegating those costs more than it saves. The rule targets long whole-suite passes.
**Brief the Haiku verifier with:** exact commands, directory to run from, and an instruction to report exact pass/fail counts and coverage percentages from their run, then compare them against the current branch state to verify no regression; require **raw output verbatim**, failures included, not a summary. "All green" without output is useless — several agents have reported gates passing that were failing.
### MODEL TIERS
Binding for all work here, not just verification.
| Role | Model | Effort | Allowed to |
|---|---|---|---|
| Coordinator | **Opus** | **medium** | Dispatch tasks, review diffs, make product calls, maintain the plan. **Writes no implementation code.** |
| Implementer | **Sonnet** | — | Features and bugfixes. Only tier allowed to write non-trivial code. |
| Verifier / mechanical | **Haiku** | — | Run full suites and paste raw output; small mechanical edits (renames, i18n string moves, test-fixture churn). |
* **No subagent runs on Opus** — not for hard tasks, review, or browser verification. Too hard for Sonnet means too big: split it, don't escalate.
* The coordinator never runs verification suites itself (see cost rule).
* Browser verification is delegated too — Sonnet drives Chrome, never the coordinator.
---
## ANTI-PATTERNS (CRITICAL GUARDRAILS)
* **TS type safety:** no `as any`, `@ts-ignore`, `@ts-expect-error`. `no-explicit-any` is an ESLint error and CI fails on warnings. **Tests included — no exemption.** Build mocks with the typed factories in `frontend/tests/fixtures/` (`makeWorkspaceOverview`, `makePlanProposal`, `make*Result`, …), which return complete, correctly-typed objects you override via `Partial<T>`; mock functions with `vi.mocked(fn)`, not casts. A cast in a test defeats the type check exactly where it guards the frontend/backend contract — this project's most common bug. Sole exception: `as unknown as T` in a test *deliberately* exercising invalid input (missing field, bogus enum value) — keep it to that expression and comment why.
* **Python type safety:** no bare `# type: ignore`; fix the type or model. Prefer precise Pydantic models over `dict[str, Any]` at API boundaries.
* **Error handling:** no empty `catch`/`except`; handle or log explicitly. Backend errors flow through `app/utils/errors.py`, frontend through `src/services/errors.ts`.
* **Testing integrity:** never delete or comment out failing tests to make a suite "pass". Fix the code, or update the test properly with a clear reason.
* **No JS slop:** never create `.js`/`.jsx` in the frontend — TypeScript (`.ts`, `.tsx`) only.
* **Respect layering:** routers never touch `repositories_impl`; components never call `fetch`. Go through services/hooks.
* **No ORM creep:** no SQLModel/SQLAlchemy models — extend the repository pattern.
* **No assumptions:** never assume any file's contents. Always read it before modifying or referencing it.
---
## MANDATORY EXECUTION PROTOCOLS
* **Read before edit:** read the whole target file first.
* **Post-edit verification:** check immediately for duplicated lines and intact imports.
* **Stability check:** after any change, run that side's lint + typecheck + tests. For cross-cutting changes (e.g. API contract edits in `app/models` and `src/types`), run **both** suites — they must agree on the wire format.
* **Keep the graph current:** `graphify update .` after code changes.
---
## DOMAIN RULES (Swedish parental leave)
* **390 + 90 model:** of the parental-benefit days, 390 are at sickness-benefit level and 90 at minimum (lägstanivå) rate. Track day *types*, not one total.
* **Reserved days:** some days are reserved per parent and non-transferable — surface this in shared/partner planning.
* **Dubbeldagar:** both parents may take leave the same day for a limited number of days early in the child's life; model as consuming each parent's balance.
* **Whole days only:** no fractional/percentage days in the model.
* **Age-4 cutoff:** core purpose is optimizing leave over time. After the child turns **4**, only **96 days** may be saved; show how many days remain at that boundary.
* **Money/SGI abstracted:** benefit amounts/SGI stay abstract — no exact kronor payouts, no hardcoded rates; keep the agreed abstraction level.
Unsure about a rule? Ask before encoding a number — Swedish rules change, and wrong figures are worse than a TODO.
---
## WHERE TO LOOK FIRST
* `ARCHITECTURE.md` — system design and rationale.
* `graphify-out/wiki/index.md` — generated codebase navigation.
* `backend/README.md` — backend setup notes.
* Root `*_REPORT.md` / `*_TODO.md` / `*_ONBOARDING.md` — contributor handoff notes: **historical context**, not authoritative spec.
---
## OUTPUTS
Be concise: no preamble, caveats, or restating the request. Sentence fragments and short bullets where clarity holds. Explain reasoning only when it aids debugging or isn't obvious from the code/diff.