The three NextSteps*.md files were per-session handoff notes that had gone stale; the plan they described is superseded. CLAUDE.md gains a MODEL TIERS section recording who is allowed to run as what: Opus coordinates, Sonnet implements, Haiku verifies, and no subagent runs on Opus.
16 KiB
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
findwithout-maxdepth: Unboundedfindcrawls dependency trees just likels -R. Always add-maxdepth 3or 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
treewith the mandatory ignore flags: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 ofgrep -rto 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>"whengraphify-out/graph.jsonexists. Usegraphify path "<A>" "<B>"for relationships andgraphify explain "<concept>"for focused concepts. These return a scoped subgraph, usually much smaller thanGRAPH_REPORT.mdor 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.mdexists, use it for broad navigation instead of raw source browsing. - Read
graphify-out/GRAPH_REPORT.mdonly 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/:
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/:
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 inapp/repositories_impl/{sqlite,postgres}/. There is no ORM (no SQLModel, no SQLAlchemy models). SQLite via asyncaiosqliteis the default; Postgres viaasyncpgis an optional extra. - Migrations: Alembic (
backend/migrations/, configbackend/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 withPyJWT. Rate limiting viaslowapi. - Python:
>=3.14required. Managed viauvwith the Hatchling build backend. Lint/format withruff(line length 100, targetpy314). Tests withpytest+pytest-asyncio(asyncio_mode = "auto", so no@pytest.mark.asyncioneeded). - 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 insrc/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-anyis an error. CI runsnpm run lintwith--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 throughpackage.json. - CI/CD: Deployment/CI is managed via GitHub Actions (
.github/workflows/). Do not introduce.gitea/,.gitlab-ci.yml, orMakefiles — extend the existing GitHub workflow instead.
TESTS, LINT & CI GATES
Run the same checks CI runs before pushing.
Backend (from backend/):
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/):
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.
MODEL TIERS (who is allowed to be what)
Binding for all work in this repo, not just verification.
| Role | Model | Effort | Allowed to |
|---|---|---|---|
| Coordinator | Opus | medium | Dispatch tasks, review returned diffs, make product calls, maintain the plan. Writes no implementation code. |
| Implementer | Sonnet | — | Feature and bugfix work. The only tier allowed to write non-trivial code. |
| Verifier / mechanical | Haiku | — | Run the full suites and paste raw output; small mechanical edits (renames, i18n string moves, test-fixture churn). |
- No subagent may run on Opus. Not for "a hard task", not for review, not for browser verification. If a task looks too hard for Sonnet, it is too big — split it, do not escalate it.
- The coordinator never runs the verification suites itself (see the cost rule above).
- Browser verification is delegated too — Sonnet drives Chrome, never the coordinator.
ANTI-PATTERNS (CRITICAL GUARDRAILS)
- Type Safety (TS): No type suppression — no
as any,@ts-ignore, or@ts-expect-error.no-explicit-anyis an ESLint error and CI fails on warnings. This applies to test files too — there is no exemption. Build mock data with the typed factories infrontend/tests/fixtures/(makeWorkspaceOverview,makePlanProposal,make*Result, …), which return complete, correctly-typed objects you override with aPartial<T>, and mock functions withvi.mocked(fn)rather than casting. A cast in a test defeats the type check exactly where it guards the frontend/backend contract — which is this project's most common bug. The one accepted escape isas unknown as Tin a test that deliberately exercises invalid input (a missing field, a bogus enum value); keep it to that expression and comment why. - Type Safety (Python): No bare
# type: ignoreto silence the type checker; fix the underlying type or model instead. Prefer precise Pydantic models overdict[str, Any]at API boundaries. - Error Handling: No empty
catch/exceptblocks. Explicit handling/logging is required. Backend errors should flow throughapp/utils/errors.py; frontend errors throughsrc/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/.jsxfiles in the frontend. Use only TypeScript (.ts,.tsx). - Respect the layering: routers don't talk to
repositories_impldirectly; components don't callfetchdirectly. 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/modelsandsrc/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.mdfiles 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.