# 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 ""` when `graphify-out/graph.json` exists; `graphify path "" ""` for relationships, `graphify explain ""` 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 # --reload-exclude keeps the dev SQLite file out of the watched tree — see # backend/README.md for why. uv run uvicorn app.main:app --reload --reload-exclude '*.db' --reload-exclude '*.db-shm' --reload-exclude '*.db-wal' --reload-exclude '*.db-journal' --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`; 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.