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` (total now ~84%), 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. Current: 97.26% lines/statements, 93.05% functions, 93.51% branches — above 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, 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.
|
|
|
|
### 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.
|