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

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 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:
    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/:

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 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 Makefiles — 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-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.