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
14 KiB
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
findwithout-maxdepth(use-maxdepth 3or tighter); unboundedfindcrawls deps likels -R. - Discovery: default to
git ls-files— source code only. - Structure: only when you need a visual layout,
treewith mandatory ignores: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", nevergrep -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>"whengraphify-out/graph.jsonexists;graphify path "<A>" "<B>"for relationships,graphify explain "<concept>"for concepts. These return a scoped subgraph — usually far smaller thanGRAPH_REPORT.mdor 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.mdonly 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/:
npm install # frontend deps
npm run dev # BOTH: FastAPI (:8000) + Vite dev server, concurrently
Backend standalone, from backend/:
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 inapp/repositories_impl/{sqlite,postgres}/). No ORM (no SQLModel/SQLAlchemy). Default: SQLite via asyncaiosqlite; Postgres viaasyncpgis 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-cffipassword hashing,PyJWTsessions/tokens,slowapirate limiting. - Python:
>=3.14, managed withuv+ Hatchling. Lint/format:ruff(line length 100, targetpy314). Tests:pytest+pytest-asynciowithasyncio_mode = "auto"(no@pytest.mark.asyncioneeded). - 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 insrc/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-anyis an error. CI runsnpm run lint --max-warnings 0— zero warnings. - Scripts: backend helper scripts go in
backend/scripts/(create if missing); frontend scripts viapackage.json. - CI/CD: GitHub Actions (
.github/workflows/) only. Don't add.gitea/,.gitlab-ci.yml, orMakefiles — extend the existing workflow.
TESTS, LINT & CI GATES
Run CI's checks before pushing.
Backend (from backend/):
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/):
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-anyis an ESLint error and CI fails on warnings. Tests included — no exemption. Build mocks with the typed factories infrontend/tests/fixtures/(makeWorkspaceOverview,makePlanProposal,make*Result, …), which return complete, correctly-typed objects you override viaPartial<T>; mock functions withvi.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 Tin 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 overdict[str, Any]at API boundaries. - Error handling: no empty
catch/except; handle or log explicitly. Backend errors flow throughapp/utils/errors.py, frontend 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: never create
.js/.jsxin the frontend — TypeScript (.ts,.tsx) only. - Respect layering: routers never touch
repositories_impl; components never callfetch. 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/modelsandsrc/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.