97a7200c1a
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
204 lines
13 KiB
Markdown
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.
|