76b1e712b9
Works through every numbered item in NextSteps260913.md section 3, plus the three known verification issues in section 4.1. Surface what the backend already computed: - 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies - 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible - 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt) Calendar cluster (one pass over the holiday data): - new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides, paginated four years at a time - 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal - 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact - 3.7 click month/year to advance - 3.8 per-year red-days list in Settings, statutory days plus overrides i18n: - 3.5 five backend-generated Swedish surfaces converted to message_key + data, following the tips_service pattern; old string fields replaced, not kept Flexible wishes (3.9): - PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function, deterministic tie-break, two-pass ordering, test matrix rows 15-24 - backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator - frontend: season-preset toggle in WishEditor, unplaced outcome rendering Fixes (4.1): - pin typecheck to the local tsc rather than npx resolving a newer TypeScript - ignore generated coverage/ and dist/ in the ESLint flat config - fix the flaky usePlanApply test properly, with waitFor rather than a retry Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines. Not yet verified in a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
223 lines
14 KiB
Markdown
223 lines
14 KiB
Markdown
# 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).
|
|
|
|
This is a **full-stack monorepo**: a TypeScript/React/Vite frontend and a 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:
|
|
|
|
```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".
|
|
|
|
---
|
|
|
|
## graphify
|
|
|
|
This project has a knowledge graph at `graphify-out/` with god nodes, community structure, and cross-file relationships.
|
|
|
|
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).
|
|
|
|
---
|
|
|
|
## QUICK START
|
|
|
|
The 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
|
|
```
|
|
|
|
Backend can also be run on its own. From `backend/`:
|
|
|
|
```bash
|
|
uv sync # install 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`.
|
|
|
|
---
|
|
|
|
## 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
|
|
```
|
|
|
|
**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`).
|
|
|
|
**Data layer (frontend):** components consume `src/hooks/*`, which call `src/services/api.ts`. Components should not call `fetch`/the API client directly.
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
---
|
|
|
|
## TESTS, LINT & CI GATES
|
|
|
|
Run the same checks CI runs 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)
|
|
```
|
|
|
|
CI gate: `--cov-fail-under=50` today (current total ~52%), climbing toward **90** as tests land. Don't lower it — only raise when coverage grows.
|
|
|
|
**Frontend** (from `frontend/`):
|
|
|
|
```bash
|
|
npx tsc --noEmit # type check
|
|
npm run lint # ESLint, zero warnings
|
|
npm run coverage # Vitest with coverage
|
|
```
|
|
|
|
CI gates (measured baseline after `src/pages/**` inclusion):
|
|
- lines: **80%** | functions: **85%** | branches: **85%** | statements: **80%**
|
|
- These act as a regression ratchet, not a target. Current state (82.36% lines, 88.5% functions) shows pages are now visible and poorly covered (Auth, Tips, DevFetch at 0%); the intent is to raise thresholds over time, not to churn tests today.
|
|
|
|
### WHO RUNS THE VERIFICATION COMMANDS (cost rule)
|
|
|
|
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.
|
|
|
|
**Rule, by who you are:**
|
|
|
|
* **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.
|
|
|
|
This applies to the full-suite verification runs: `uv run pytest`, `uv run ruff check .`,
|
|
`uv run ruff format --check .`, `npx tsc --noEmit`, `npm run lint`, `npm run coverage`,
|
|
`npx vitest run`, and `uv run alembic upgrade head`.
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## 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.
|
|
* **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.
|
|
|
|
---
|
|
|
|
## 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 .`.
|
|
|
|
---
|
|
|
|
## DOMAIN RULES (Swedish parental leave)
|
|
|
|
Useful context so calculations and UI copy stay correct:
|
|
|
|
* **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.
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
---
|
|
|
|
## 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.
|