Files
jacobh2 4fa79bad77 Merge branch 'worktree-check-security-audit'
Security and frontend-correctness hardening: analytics IDOR fix, scraper
log scrubbing, lowercase email normalisation (migration 009), stale-response
guards in the plan hooks, busy flags cleared unconditionally across all
useLatestRequest consumers, Calendar no longer blanks on mutation, plus
dependabot/CI config and removal of the stray frontend/out.js.

Conflicts came from main's later "day assertions" restructure, which made
usePlannedDays read-only and replaced the calendar's per-day add/update with
useDayAssertions.assertDay. Both intents kept:

- usePlannedDays: branch's fetch machinery (isRefreshing, in-flight dedup,
  unguarded busy clear); mutators dropped as on main.
- usePlanSync: branch's useLatestRequest guard also applied to main's new
  syncWithWish.
- useDayAssertions / useLatestBalance (new on main): busy flag now cleared
  unconditionally, as the branch did for every other consumer.
- useDayAssertions.assertDay gains the branch's `skipRefresh` option and a
  `refresh`, so Calendar's multi-day save keeps the batch shape (N writes,
  one trailing bumpPlanVersion + one assertions refresh) on main's
  assertDay path. Branch's M7 tests ported from the removed add/update API
  to assertDay.
- PlanSyncProvider: branch's isAuthenticated gate kept; eviction-preview
  state stays removed as on main.
- usePlannedDayExclusions.ts stays deleted (superseded by useDayAssertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4wjgbVL4HAC5Rdaw75VbV
2026-09-21 07:40:09 +02:00

218 lines
14 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
# --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<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.
* **Always commit:** commit finished work without being asked — this overrides any default to wait for permission. Commit once the gates are green, never mid-change or over a failing suite. **Push and PRs still require an explicit ask.** Never commit on the default branch; branch first.
---
## 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
**Default reply: 1-5 lines.** Bullets or a small table. No preamble, no recap, no restating the request.
Hard rules:
* Lead with the answer. A yes/no question gets yes or no on line one.
* One line per finding. Name it, say it's fixed, move on.
* Never re-narrate a subagent report or a diff — extract the decision, drop the rest.
* Never explain reasoning that wasn't asked for, and never justify a choice in more than one clause.
* No "want me to X?" when X is obvious.
Length is **not** licensed by a finding being important, security-related, or surprising. An important finding gets a short, blunt line — that is what makes it get read. The only things that may run long: raw failure output, and an open product trade-off that needs a decision.
Explain reasoning only when it aids debugging or isn't obvious from the code/diff.