This commit is contained in:
2026-09-20 07:27:02 +02:00
parent 981b951847
commit ddb65018dd
6 changed files with 874 additions and 187 deletions
+79 -115
View File
@@ -1,62 +1,53 @@
# AGENTS.md
Operating guide for humans and AI agents working in **Föräldraledigplannerare** — a web app for planning Swedish parental leave (alone or with a partner), folding in vacation days to optimize leave over time until the child turns 4 (after which only 96 days may be saved).
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).
This is a **full-stack monorepo**: a TypeScript/React/Vite frontend and a Python/FastAPI backend.
**Full-stack monorepo:** TypeScript/React/Vite frontend + Python/FastAPI backend.
---
## 🛑 DIRECTORY NAVIGATION & FILE DISCOVERY
**Context:** This repo contains massive generated/dependency folders (`node_modules`, `dist`, `build`, `.venv`/`venv`, `__pycache__`, `.mypy_cache`, `.ruff_cache`, `graphify-out`, etc.). Unrestricted file listings will bloat your context window, crash your session, and cause severe token waste.
* **NEVER use `ls -R`:** This command is strictly forbidden in this repository.
* **NEVER use `find` without `-maxdepth`:** Unbounded `find` crawls dependency trees just like `ls -R`. Always add `-maxdepth 3` or tighter.
* **Primary Discovery:** To map out files or explore the project tree, **ALWAYS use `git ls-files`**. This is your default command and ensures you only interact with human-written source code.
* **Structural Context:** If you explicitly need a visual directory layout to understand architecture, use `tree` with the mandatory ignore flags:
**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'
```
* **Searching Code:** To search for text or code across the repo, **ALWAYS use `git grep "search_term"`** instead of `grep -r` to avoid crawling ignored or minified assets.
* **NEVER** go outside the current directory; always use relative paths based on "here".
* **Search:** **`git grep "term"`**, never `grep -r` (crawls ignored/minified assets).
* **Never leave the current directory**; use relative paths from "here".
---
## graphify
This project has a knowledge graph at `graphify-out/` with god nodes, community structure, and cross-file relationships.
Knowledge graph at `graphify-out/`: god nodes, community structure, cross-file relationships.
On `/graphify`, first invoke the `skill` tool with `skill: "graphify"`.
When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else.
Rules:
* For codebase questions, first run `graphify query "<question>"` when `graphify-out/graph.json` exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output.
* Dirty `graphify-out/` files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it.
* If `graphify-out/wiki/index.md` exists, use it for broad navigation instead of raw source browsing.
* Read `graphify-out/GRAPH_REPORT.md` only for broad architecture review or when query/path/explain do not surface enough context.
* After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
* 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
The frontend `package.json` orchestrates both services. From `frontend/`:
Frontend `package.json` orchestrates both services. From `frontend/`:
```bash
npm install # install frontend deps
npm run dev # runs BOTH: FastAPI (:8000) + Vite dev server concurrently
npm install # frontend deps
npm run dev # BOTH: FastAPI (:8000) + Vite dev server, concurrently
```
Backend can also be run on its own. From `backend/`:
Backend standalone, from `backend/`:
```bash
uv sync # install deps incl. dev group
uv run uvicorn app.main:app --reload --port 8000 # serve API
uv sync # deps incl. dev group
uv run uvicorn app.main:app --reload --port 8000 # serve API
```
The scraper (FK login flow via Playwright) is **off by default**. To exercise it locally, use `npm run login` from `frontend/`, which sets `APP_SCRAPER_ENABLED=true APP_SCRAPER_HEADLESS=true`. Chromium must be installed separately: `uv run playwright install chromium`.
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`.
---
@@ -78,7 +69,6 @@ backend/ Python 3.14 · FastAPI · uv (Hatchling build backend)
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, …)
@@ -89,151 +79,125 @@ frontend/ TypeScript · React 18.3 · Vite 5 · Vitest
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
```
**Layering rule (backend):** `routers` call `services`; `services` call `repositories` (the abstract interface). Concrete DB code lives only in `repositories_impl/`. Keep routers thin and put business logic in services. Don't reach across layers (e.g. a router talking directly to `repositories_impl`).
**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`).
**Data layer (frontend):** components consume `src/hooks/*`, which call `src/services/api.ts`. Components should not call `fetch`/the API client directly.
**Frontend data layer:** components → `src/hooks/*` → `src/services/api.ts`. Components never call `fetch`/the API client directly.
**Captures are never tracked:** `captures/` (scraper page dumps, verification screenshots) is gitignored because real FK pages contain personal data. Scraper parser tests use the synthetic fixture `backend/tests/fixtures/fk_overview_synthetic.html`.
**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 stack:** FastAPI + Pydantic v2 (request/response models) with a **hand-rolled repository pattern** — abstract interfaces in `app/repositories/`, concrete implementations in `app/repositories_impl/{sqlite,postgres}/`. There is **no ORM** (no SQLModel, no SQLAlchemy models). SQLite via **async `aiosqlite`** is the default; Postgres via `asyncpg` is an optional extra.
* **Migrations:** Alembic (`backend/migrations/`, config `backend/alembic.ini`). Migrations are written by hand against the schema — they are not generated from ORM metadata. Add a new revision when you change the schema.
* **Auth:** password hashing with `argon2-cffi`, sessions/tokens with `PyJWT`. Rate limiting via `slowapi`.
* **Python:** `>=3.14` required. Managed via **`uv`** with the **Hatchling** build backend. Lint/format with **`ruff`** (line length 100, target `py314`). Tests with **`pytest` + `pytest-asyncio`** (`asyncio_mode = "auto"`, so no `@pytest.mark.asyncio` needed).
* **Frontend stack:** **React 18.3**, **react-router-dom v6** (`BrowserRouter`/`Routes`/`Route`), **TypeScript 5.6**, **Vite 5**, **Vitest** + Testing Library. State/data fetching is done with **custom hooks** in `src/hooks/` (no TanStack Query, no Redux).
* **Styling:** plain CSS using **BEM-style class names** (e.g. `calendar-day__number`, `calendar-day--disabled`). There is **no CSS framework** (no Tailwind) and no CSS-in-JS. Match the conventions in existing components before inventing new ones.
* **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` with `--max-warnings 0` — zero warnings allowed.
* **Scripts:** any backend helper scripts must be placed explicitly in `backend/scripts/` (create the folder if needed). Frontend scripts go through `package.json`.
* **CI/CD:** Deployment/CI is managed via **GitHub Actions** (`.github/workflows/`). Do **not** introduce `.gitea/`, `.gitlab-ci.yml`, or `Makefile`s — extend the existing GitHub workflow instead.
* **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 the same checks CI runs before pushing.
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 with coverage (config in pyproject.toml)
uv run pytest # tests + coverage (config in pyproject.toml)
```
CI gate: `--cov-fail-under=80` today (current total ~84%), climbing toward **90** as tests land. Don't lower it — only raise when coverage grows.
Gate: `--cov-fail-under=80` (total now ~84%), climbing toward **90**. Never lower it; raise only as coverage grows.
**Frontend** (from `frontend/`):
```bash
npm run typecheck # type check (never npx tsc: it resolves a newer TS and emits a bogus TS5101)
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. Current: 97.26% lines/statements, 93.05% functions, 93.51% branches — above thresholds. Never lower gates to pass.
CI gates (measured baseline after `src/pages/**` inclusion):
- lines: 96 | functions: 91 | branches: 92 | statements: 96
- These act as a regression ratchet, not a target. Current coverage (97.26% lines/statements, 93.05% functions, 93.51% branches) exceeds thresholds; gates must never be lowered to pass.
### WHO RUNS VERIFICATION (cost rule)
### WHO RUNS THE VERIFICATION COMMANDS (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.
Running the checks above is mechanical work: the commands are fixed and the only judgement
needed is reading pass/fail and the failure output. Paying a large model to sit through
`pytest`, `vitest`, `tsc`, `ruff` or a coverage run 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.
**Rule, by who you are:**
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.
* **Haiku subagent** — run the commands yourself, immediately. No delegation. You are already the
cheap tier; spawning another agent just adds latency.
* **Sonnet (or any larger model) subagent** — do **not** run the verification commands yourself.
Finish the code change, then spawn a **Haiku** subagent to run them and report back the raw
output. Act on what it reports.
* **The orchestrating/reviewing agent** — same rule: delegate verification runs to Haiku rather
than burning a large model on them.
**Brief the Haiku verifier with:** exact commands, directory to run from, expected baseline (e.g. "currently 361 passed / 0 failed — must not regress"), and an instruction to paste **raw output verbatim**, failures included, not a summary. "All green" without output is useless — several agents have reported gates passing that were failing.
This applies to the full-suite verification 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`, and `uv run alembic upgrade head`.
### MODEL TIERS
It does **not** apply to quick diagnostics while you are mid-task — reading a single file, a
`git grep`, or running one focused test to debug what you are actively writing. Delegating those
costs more than it saves. The rule targets the long, whole-suite verification passes.
**What the Haiku verifier must be told:** the exact commands, the directory to run them from, the
expected baseline (e.g. "currently 361 passed / 0 failed — must not regress"), and an instruction
to paste the **raw output verbatim**, including failures, rather than summarising. A verifier that
reports "all green" without the output is useless — several agents have reported passing gates
that were in fact failing.
### MODEL TIERS (who is allowed to be what)
Binding for all work in this repo, not just verification.
Binding for all work here, not just verification.
| Role | Model | Effort | Allowed to |
|---|---|---|---|
| Coordinator | **Opus** | **medium** | Dispatch tasks, review returned diffs, make product calls, maintain the plan. **Writes no implementation code.** |
| Implementer | **Sonnet** | — | Feature and bugfix work. The only tier allowed to write non-trivial code. |
| Verifier / mechanical | **Haiku** | — | Run the full suites and paste raw output; small mechanical edits (renames, i18n string moves, test-fixture churn). |
| 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 may run on Opus.** Not for "a hard task", not for review, not for browser
verification. If a task looks too hard for Sonnet, it is too big — split it, do not escalate it.
* The coordinator never runs the verification suites itself (see the cost rule above).
* **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)
* **Type Safety (TS):** No type suppression — no `as any`, `@ts-ignore`, or `@ts-expect-error`. `no-explicit-any` is an ESLint error and CI fails on warnings. **This applies to test files too** — there is no exemption. Build mock data with the typed factories in `frontend/tests/fixtures/` (`makeWorkspaceOverview`, `makePlanProposal`, `make*Result`, …), which return complete, correctly-typed objects you override with a `Partial<T>`, and mock functions with `vi.mocked(fn)` rather than casting. A cast in a test defeats the type check exactly where it guards the frontend/backend contract — which is this project's most common bug. The one accepted escape is `as unknown as T` in a test that *deliberately* exercises invalid input (a missing field, a bogus enum value); keep it to that expression and comment why.
* **Type Safety (Python):** No bare `# type: ignore` to silence the type checker; fix the underlying type or model instead. Prefer precise Pydantic models over `dict[str, Any]` at API boundaries.
* **Error Handling:** No empty `catch`/`except` blocks. Explicit handling/logging is required. Backend errors should flow through `app/utils/errors.py`; frontend errors 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 (frontend):** NEVER create `.js`/`.jsx` files in the frontend. Use only TypeScript (`.ts`, `.tsx`).
* **Respect the layering:** routers don't talk to `repositories_impl` directly; components don't call `fetch` directly. Go through services / hooks.
* **No ORM creep:** don't add SQLModel/SQLAlchemy models — extend the existing repository pattern.
* **Assumption Error:** NEVER assume the contents of ANY file. ALWAYS read the target file to confirm its structure and implementation before modifying or referencing it.
* **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:** ALWAYS read the entire target file before performing any edit.
* **Post-Edit Verification:** Immediately after an edit, verify that no lines were duplicated and imports are intact.
* **Stability Check:** Following any code change, run the relevant side's checks above (lint + type check + tests). For cross-cutting changes (e.g. API contract edits in `app/models` and `src/types`), run **both** backend and frontend suites — they must agree on the wire format.
* **Keep the graph current:** after modifying code, run `graphify update .`.
* **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)
Useful context so calculations and UI copy stay correct:
* **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.
* **390 + 90 model:** of the parental-benefit days, 390 are sickness-benefit-level and 90 are at the minimum (lägstanivå) rate. Track day *types*, not just a single total.
* **Reserved days:** a portion of days is reserved per parent and cannot be transferred to the partner — surface this in shared/partner planning.
* **Dubbeldagar (double days):** both parents can take leave on the same day for a limited number of days early in the child's life; model these as consuming each parent's balance.
* **No partial days:** the planner works in **whole days** — do not introduce fractional/percentage days in the model.
* **The age-4 cutoff:** the core purpose is optimizing leave over time. After the child turns **4**, only **96 days** may be saved; the planner must show how many days remain at that boundary.
* **Money/SGI is abstracted:** the app treats benefit amounts/SGI abstractly rather than computing exact kronor payouts — keep money handling at the agreed abstraction level rather than hardcoding rates.
When in doubt about a rule, ask before encoding a number — Swedish parental-leave rules change and incorrect figures are worse than a TODO.
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 navigation over the codebase.
* `backend/README.md` — backend-specific setup notes.
* The various `*_REPORT.md` / `*_TODO.md` / `*_ONBOARDING.md` files at the root are working handoff notes between contributors; treat them as **historical context**, not authoritative spec.
* `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: skip preamble, caveats, and restating the request. Use sentence fragments and short bullet points where clarity isn't lost. Explain reasoning only when it aids debugging or isn't obvious from the code/diff itself.
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.