7f031f2087
Restructures the handover so each task is a self-contained brief that can be lifted straight into a subagent prompt. The previous narrative format meant every dispatched agent read the whole document to use one paragraph of it. Shared preamble is read once by the orchestrator and never forwarded. Tasks are grouped into waves by file ownership so a wave can be dispatched in parallel without two agents touching the same file. Each brief carries its own goal, owned files, constraints and traps repeated verbatim, done criteria with numeric baselines, and any product decision that must be made before starting. Trap duplication across briefs is deliberate: an agent that has not been told about a trap reintroduces the bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
439 lines
27 KiB
Markdown
439 lines
27 KiB
Markdown
# Next Steps — handoff, 2026-09-15
|
||
|
||
**Format note for whoever dispatches this:** each task below (§2 onward) is written as a
|
||
self-contained brief. Copy ONE brief verbatim into a subagent prompt — it does not need the rest
|
||
of this file, `NextSteps260914.md`, or `NextSteps260913.md`. Only the orchestrator needs §0/§1.
|
||
`NextSteps260913.md` and `NextSteps260914.md` are history — do not edit them.
|
||
|
||
---
|
||
|
||
## 0. State as of now (all committed, working tree clean)
|
||
|
||
Commits this session: `e3d19f2`, `42cebba`, `dd7f0ff`, `084ee91`, `760b312`.
|
||
|
||
Gates, all currently green — **these are the baselines to paste into every verifier**:
|
||
- Backend: **439 tests passing**, `ruff check` clean, `ruff format --check` clean (128 files)
|
||
- Frontend: **710 tests passing / 56 files**, `npm run typecheck` clean, `npm run lint` 0 issues
|
||
- Coverage (frontend): 96.54% lines/statements, 91.08% functions, 86.84% branches. Thresholds are
|
||
a **regression ratchet — never lower them**: lines 95 / statements 95 / functions 89 / branches
|
||
85. Backend CI gate `--cov-fail-under=75` (measured ~81), set in `.github/workflows/ci.yml`.
|
||
|
||
Everything in `NextSteps260914.md`'s roadmap is DONE, including full browser verification.
|
||
|
||
---
|
||
|
||
## 1. Shared preamble (orchestrator reads this once; do not forward to subagents)
|
||
|
||
**Stack:** Python 3.14 / FastAPI / uv backend (no ORM, hand-rolled repositories, Alembic
|
||
migrations); TypeScript / React 18.3 / Vite 5 / Vitest frontend (no TanStack Query, no Redux,
|
||
hand-rolled i18n). Full conventions in `/Users/jhag/projects/Föräldraledigplannerare/CLAUDE.md`.
|
||
|
||
**Run the app:**
|
||
```bash
|
||
cd frontend && npm run dev # starts BOTH vite (5173) and uvicorn (8000)
|
||
```
|
||
Vite binds IPv6 `::1` — use `http://localhost:5173`, **not** `127.0.0.1`. If the API behaves
|
||
impossibly, check `lsof -nP -iTCP:8000 -sTCP:LISTEN` for a squatter (macOS resolves `localhost` to
|
||
`::1` first).
|
||
|
||
**Seed demo data:**
|
||
```bash
|
||
cd backend && uv run python scripts/seed_demo.py --reset
|
||
```
|
||
Login `demo@example.com` / `DemoPass123!`. Creates a couple workspace: Alex (has a login, a
|
||
near-drained pool) and Sam (no-login partner, created via `add_placeholder_parent`, large
|
||
surplus), child Nova, vacation rows for only some years (so `is_assumed` badges show), a worked
|
||
override and an off override, and four wishes including a flexible one and a deliberately
|
||
underfunded one (produces both shortfalls and surplus advice).
|
||
|
||
**Sanity-check the engine without a browser:** `backend/scripts/demo_plan.py`.
|
||
|
||
**Verification commands:**
|
||
```bash
|
||
cd backend && uv run ruff check . && uv run ruff format --check . && uv run pytest
|
||
cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage
|
||
```
|
||
|
||
**Cost rule (from `CLAUDE.md`):** a Haiku subagent runs these itself, immediately. A Sonnet-or-larger
|
||
subagent must NOT run full-suite verification itself — it finishes the code change, then spawns a
|
||
Haiku subagent with the exact commands, the directory to run them from, the numeric baseline above,
|
||
and an instruction to paste **raw verbatim output**, not a summary. Several agents have reported
|
||
gates as "all green" that were in fact failing — a report without pasted output is not accepted.
|
||
This rule does not apply to a single focused test run while mid-task.
|
||
|
||
**Browser verification matters.** Every significant bug found this project — the calendar
|
||
overshoot, the flexible-wish double-booking, the doubled age-4 entitlement, two stale-data bugs —
|
||
was found by driving the running app while the entire suite was green. Suites confirm the code
|
||
does what it was written to do, not what it should do. `backend/tests/integration/test_user_journey.py`
|
||
guards the main journey but is not a substitute for clicking.
|
||
|
||
---
|
||
|
||
## 2. Waves
|
||
|
||
- **Wave 1 — investigation, no code guessed.** Task A only. It touches read paths across
|
||
frontend and backend but the brief explicitly forbids shipping a fix before the behaviour is
|
||
confirmed, so it cannot safely run in parallel with a wave that edits the same files for a
|
||
different reason. Not blocked on anything.
|
||
- **Wave 2 — independent file-owning tasks, can run fully in parallel.** Tasks B (frontend
|
||
`WishEditor.tsx` UI, blocked on a product decision — see brief), C (frontend test files only,
|
||
raises branch coverage), and D (backend `analytics_service.py` docs/decision framing, blocked on
|
||
a product decision). B, C, D touch disjoint files (B: `WishEditor.tsx` + its test + i18n; C: test
|
||
files across the frontend under existing components; D: `analytics_service.py` region + possibly
|
||
`Dashboard.tsx`) and none touches a file Task A investigates, so all three can be dispatched
|
||
together once Wave 1's finding is known (Task A might turn out to change what Task B/D need, but
|
||
neither B nor D depends on A's *outcome* to start — see each brief's decision-flag).
|
||
|
||
If Task A concludes "this is a real bug and the fix is obvious and small," that fix is a **new**
|
||
task, not folded into A's brief — A is investigation-only per the instructions below.
|
||
|
||
---
|
||
|
||
## Task A — Investigate the Settings partner section
|
||
|
||
### Goal
|
||
Determine, with evidence (not assumption), whether the Inställningar (Settings) page's partner UI
|
||
is broken or working as designed. In the seeded couple workspace, partner "Sam" exists in the
|
||
workspace and is planned around (wishes, allocations, the surplus/shortfall picture all reference
|
||
Sam), but the Settings page currently shows only an empty "Add a partner" form with no visible
|
||
indication a partner is already linked. This was spotted in passing during a previous session and
|
||
was **not** dug into — it may be correct behaviour with unusual seed data, or it may be a real gap.
|
||
Done looks like: a clear, evidenced verdict — "this is a bug, here is exactly what's wrong and
|
||
where" or "this is correct, here is why the seed data made it look wrong" — with no code changed
|
||
unless the fix is trivial and you have explicit confirmation from whoever dispatched you that
|
||
shipping it is in scope.
|
||
|
||
### Owned files
|
||
Read-only investigation. If (and only if) you get explicit go-ahead to fix something, the only
|
||
files in scope are:
|
||
- `frontend/src/pages/Settings.tsx`
|
||
- `frontend/src/context/WorkspaceContext.tsx`
|
||
- `backend/app/routers/workspace.py`
|
||
|
||
Do not touch anything else without asking first.
|
||
|
||
### Constraints and traps that apply to THIS task
|
||
- **`vi.mock('@/context')` does NOT intercept direct submodule imports** like
|
||
`@/context/AuthContext`. Mock the exact specifier the component imports — this caused 14 test
|
||
failures once. `Calendar.test.tsx` / `Settings.test.tsx` show the right idiom.
|
||
- **`localStorage` is unreliable in this test sandbox** (Node's experimental webstorage shadows
|
||
jsdom's). Use `vi.stubGlobal('localStorage', mock)`.
|
||
- **Every user-facing string must be `message_key` + `data`**, never a hardcoded Swedish string.
|
||
`backend/tests/integration/test_user_journey.py::TestNoSwedishOnTheWire` walks responses for
|
||
å/ä/ö and will fail violations. `frontend/src/i18n/sv.ts` is the source of truth; `en.ts` derives
|
||
its type from it, so a key missing from either is a compile error.
|
||
- **No `any`, `@ts-ignore` or `@ts-expect-error`** — `no-explicit-any` is an ESLint error and CI
|
||
runs `--max-warnings 0`.
|
||
- **Use `npm run typecheck`, never `npx tsc`** — npx resolves a newer TypeScript and emits a bogus
|
||
`TS5101 baseUrl is deprecated` error.
|
||
- **`git grep` only searches tracked files** — use plain `grep` when checking new or uncommitted
|
||
work.
|
||
- **Never delete, skip or weaken a test to make a suite pass.**
|
||
|
||
### Facts already gathered (starting point — verify, don't re-derive from scratch)
|
||
- `frontend/src/pages/Settings.tsx` has an "Add partner" form (`handleAddPartner`, around line 230;
|
||
the JSX section starts around line 397 with `settings.partnerSectionTitle`) that POSTs via
|
||
`api.addPartner`. It does **not** appear to call `api.listWorkspaceMembers` anywhere, nor render
|
||
any existing-member list — grep confirms `members`/`Members` do not appear in `Settings.tsx`.
|
||
- `frontend/src/services/api.ts` around line 868–872 has `listWorkspaceMembers(workspaceId)` which
|
||
calls `GET /workspace/{id}/members` — this function exists and is used elsewhere (for picking
|
||
which parent a wish belongs to in wish editing) but is a candidate for what Settings is missing.
|
||
- `backend/app/routers/workspace.py` has `GET /api/workspace/{workspace_id}/members` (returns all
|
||
`PersonalDataResponse` rows, i.e. both the logged-in user and any placeholder partner) and
|
||
`POST /api/workspace/{workspace_id}/parents` (`add_placeholder_parent`) which is how the seeded
|
||
partner "Sam" was created — a `PersonalData` row with **no login**, per
|
||
`backend/scripts/seed_demo.py` (`PARTNER_NAME = "Sam"`, `add_placeholder_parent`).
|
||
- `frontend/src/context/WorkspaceContext.tsx` exposes `parentName` (the *current logged-in user's*
|
||
name) and `personalDataUuid`, but has no field for "other members" or "partner name" — check
|
||
whether this is intentional (Settings might be expected to fetch members itself) or a real gap.
|
||
|
||
### What to actually check
|
||
1. Run the app with seeded demo data, log in as `demo@example.com`, open Inställningar, and look
|
||
at the partner section with your own eyes (or via `mcp__claude-in-chrome__*` browser tools).
|
||
Confirm what is and isn't shown.
|
||
2. Read all of `Settings.tsx` to see if there's member-fetching logic you missed (e.g. behind a
|
||
loading state, an error being swallowed, or a condition that's never true for this seed).
|
||
3. Check the network tab / add a temporary log to see if `GET /workspace/{id}/members` is even
|
||
called from Settings, and if so, what it returns for the demo workspace.
|
||
4. Check `Settings.test.tsx` for existing coverage of the partner section — does a test already
|
||
assert "no member list is shown," which would mean it's a deliberate v1 scope cut rather than a
|
||
bug that slipped through?
|
||
5. Decide: is the correct behaviour "show existing partner(s) with a way to manage/remove them" and
|
||
Settings simply never got that view built (a real gap), or is the current one-shot "add a
|
||
partner" form intentional because a workspace only ever gets one partner added once, after
|
||
which the UI doesn't need to display them here (they show up in the Plan/Calendar contexts
|
||
instead)? Look for the second parent's name surfacing anywhere else in Settings (e.g. a
|
||
per-parent vacation table) as evidence for the second reading.
|
||
|
||
### Done criteria
|
||
No code changes are the default outcome. Report:
|
||
- The concrete behaviour observed (with a screenshot or copied DOM/text if using the browser
|
||
tools).
|
||
- Whether `GET /workspace/{id}/members` is called from Settings, and what it returns.
|
||
- A clear verdict: bug vs. by-design vs. "by-design but confusing enough to flag for a product
|
||
decision."
|
||
- If you conclude it's a bug and the fix is small and obvious (e.g. "just render the existing
|
||
members list that `listWorkspaceMembers` already returns"), you may implement it, but only after
|
||
making the verdict explicit in your own reasoning — do not silently patch it as a side effect of
|
||
investigating. If you do fix it, run:
|
||
```bash
|
||
cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage
|
||
```
|
||
and confirm no regression against the frontend baseline (710 tests passing / 56 files, coverage
|
||
≥ 96.54/91.08/86.84 for lines/functions/branches) before reporting done. Per the cost rule, if
|
||
you are a Sonnet-or-larger agent, delegate that run to a Haiku subagent and require verbatim
|
||
output.
|
||
|
||
### Anything needing a product decision
|
||
If Task A finds the display is ambiguous-but-defensible (e.g. the partner does show up elsewhere,
|
||
just not in Settings), flag to the product owner: *should Settings show a "Partner: Sam" line with
|
||
a remove/replace action once a partner exists, instead of (or alongside) the add-partner form?*
|
||
Do not guess an answer and build it.
|
||
|
||
---
|
||
|
||
## Task B — Custom bounding-range picker for flexible wishes
|
||
|
||
### Goal
|
||
A flexible wish ("när det passar bäst") today can only be bounded by one of four season presets
|
||
(vår 03-01→05-31, sommar 06-01→08-31, höst 09-01→10-31, jul 12-01→01-31, which wraps the year
|
||
end). This was a deliberate v1 scope cut, not a rejection. Done looks like: a family can pick an
|
||
arbitrary date range (e.g. "sometime between March 15 and April 20") as the bounding range for a
|
||
flexible wish, in addition to keeping the four season presets as one-click shortcuts. The engine
|
||
side needs no changes — a flexible wish already reuses the existing `window_start_month/day` /
|
||
`window_end_month/day` fields as its search bound; this is a UI-only addition.
|
||
|
||
### Owned files
|
||
- `frontend/src/components/planning/WishEditor.tsx`
|
||
- `frontend/src/components/planning/WishEditor.test.tsx` (or wherever its existing test file is —
|
||
confirm the exact name with `git ls-files | grep -i wisheditor` before starting)
|
||
- `frontend/src/i18n/sv.ts` and `frontend/src/i18n/en.ts` (new keys only, additive)
|
||
|
||
Do not touch `backend/app/services/allocation_service.py`, `backend/app/entities/leave_wish.py`,
|
||
or `PLANNING_ENGINE_SPEC.md` — no engine or spec change is needed for this task.
|
||
|
||
### Constraints and traps that apply to THIS task
|
||
- **A literal `*/` inside a JSDoc block comment prematurely closes it and breaks the whole file** —
|
||
this already bit this exact file (`WishEditor.tsx`) once. Avoid nested JSDoc strings containing
|
||
`*/`, or escape them carefully, in any comment you add or edit.
|
||
- **Every user-facing string must be `message_key` + `data`**, never a hardcoded Swedish string in
|
||
the frontend component — use `frontend/src/i18n/sv.ts` as the source of truth; `en.ts` derives
|
||
its type from it, so a key missing from either is a compile error.
|
||
- **No `any`, `@ts-ignore` or `@ts-expect-error`** — `no-explicit-any` is an ESLint error and CI
|
||
runs `--max-warnings 0`.
|
||
- **Use `npm run typecheck`, never `npx tsc`** — npx resolves a newer TypeScript and emits a bogus
|
||
`TS5101 baseUrl is deprecated` error.
|
||
- **Never delete, skip or weaken a test to make a suite pass.**
|
||
- **`localStorage` is unreliable in this test sandbox** — use `vi.stubGlobal('localStorage', mock)`
|
||
if the component or its tests touch it.
|
||
- The bounding-range fields already exist and are named `windowStartMonth` / `windowStartDay` /
|
||
`windowEndMonth` / `windowEndDay` in the wish object built by `WishEditor.tsx` (see
|
||
`addPreset`/`addSeasonPreset`, around lines 162–182) — a custom picker must populate exactly
|
||
these same fields, in the same month/day (not full-date) shape, since that's what the backend
|
||
entity and allocator expect. Do not invent a new field or a full ISO-date representation.
|
||
- Season presets fill the bounding range only, **not** the final placement (see the existing
|
||
`PLANNING_ENGINE_SPEC.md §10.1`/§10.7 comments already in the file around lines 58–65 and
|
||
179–182) — a custom range picker must follow the same contract: it sets the search bound, the
|
||
engine still picks the actual placement inside it at propose time.
|
||
|
||
### Done criteria
|
||
```bash
|
||
cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage
|
||
```
|
||
Must show: typecheck clean, lint 0 issues, all tests passing (baseline 710 passing / 56 files, this
|
||
task adds tests so the count should only go up), and coverage not regressing below the current
|
||
ratchet (lines 95 / statements 95 / functions 89 / branches 85 — current measured 96.54 / 96.54 /
|
||
91.08 / 86.84). Per the cost rule, if you are a Sonnet-or-larger agent, delegate this run to a
|
||
Haiku subagent and require verbatim output, not a summary.
|
||
|
||
Also do a manual browser pass (seed demo data per §1, log in, open a wish editor, add a flexible
|
||
wish, use the custom picker, propose a plan) — this project's history is that suites pass while the
|
||
feature is subtly broken in the browser (double-booking, overshoot, etc. were all browser-only
|
||
finds).
|
||
|
||
### Anything needing a product decision
|
||
**This entire task is blocked on a product decision that has not been made:** how much freedom to
|
||
expose in the custom picker. Options include (not exhaustive, do not pick one yourself):
|
||
- Any two dates, unconstrained (family could pick a 300-day bounding range).
|
||
- A minimum/maximum span (e.g. must be between 1 and ~90 days) to keep the engine's
|
||
window-scanning inside sane limits.
|
||
- Whether the picker allows a range that wraps the calendar year end, the way the `jul` preset
|
||
does, or restricts custom ranges to a single calendar year for simplicity.
|
||
- Whether the four season presets stay as visible quick-picks alongside the custom picker, or the
|
||
custom picker replaces them once available.
|
||
Do not guess and build; get this decided (or explicitly ask the user) before writing UI, since it
|
||
changes the validation logic and the i18n copy.
|
||
|
||
---
|
||
|
||
## Task C — Raise frontend branch coverage
|
||
|
||
### Goal
|
||
Frontend branch coverage (86.84%) is the weakest of the four coverage metrics and has the least
|
||
headroom above its ratchet floor (85%). Done looks like: branch coverage measurably higher than
|
||
86.84% (ideally comfortably clear of 85, e.g. 88%+), achieved by adding real tests for genuinely
|
||
untested branches — not by deleting or weakening any existing test, and not by chasing the number
|
||
with low-value tests that don't exercise a real decision path.
|
||
|
||
### Owned files
|
||
Test files only — do not modify any non-test source file to make coverage easier to hit. Before
|
||
starting, run `npm run coverage` and read the generated HTML/text report
|
||
(`frontend/coverage/`) to find the actual weakly-covered branches (this project's own history notes
|
||
that `Auth.tsx`, `Tips.tsx`, `DevFetch.tsx` were previously at or near 0% — check whether that is
|
||
still true or has since improved before assuming those are still the gaps). Pick specific files
|
||
based on the report, then add or extend the corresponding `*.test.tsx`/`*.test.ts` file for each.
|
||
Do not touch files owned by Task A (`Settings.tsx`) or Task B (`WishEditor.tsx`) — if the coverage
|
||
report points you at either of those, pick a different low-coverage file first and leave a note
|
||
about the overlap instead of racing another task's edit.
|
||
|
||
### Constraints and traps that apply to THIS task
|
||
- **`vi.mock('@/context')` does NOT intercept direct submodule imports** like
|
||
`@/context/AuthContext`. Mock the exact specifier the component imports — this caused 14 test
|
||
failures once. `Calendar.test.tsx` / `Settings.test.tsx` show the right idiom.
|
||
- **`localStorage` is unreliable in this test sandbox** (Node's experimental webstorage shadows
|
||
jsdom's). Use `vi.stubGlobal('localStorage', mock)`.
|
||
- **No `any`, `@ts-ignore` or `@ts-expect-error`** — `no-explicit-any` is an ESLint error and CI
|
||
runs `--max-warnings 0`.
|
||
- **Use `npm run typecheck`, never `npx tsc`** — npx resolves a newer TypeScript and emits a bogus
|
||
`TS5101 baseUrl is deprecated` error.
|
||
- **Never delete, skip or weaken a test to make a suite pass.** This applies doubly here: the whole
|
||
point of this task is more real coverage, not a smaller denominator.
|
||
- **A literal `*/` inside a JSDoc block comment prematurely closes it** — be careful if any test
|
||
file you touch has JSDoc-style comments.
|
||
- **`git grep` only searches tracked files** — use plain `grep` when checking new or uncommitted
|
||
work by other in-flight tasks so you don't collide with them.
|
||
|
||
### Done criteria
|
||
```bash
|
||
cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage
|
||
```
|
||
Baseline to compare against: 710 tests / 56 files passing, coverage 96.54% lines/statements,
|
||
91.08% functions, 86.84% branches. After this task: test count should increase, all four coverage
|
||
numbers should be equal or higher (branches specifically should move up meaningfully, not by
|
||
0.01%), and none of the ratchet floors (lines 95 / statements 95 / functions 89 / branches 85)
|
||
should regress. Per the cost rule, if you are a Sonnet-or-larger agent, delegate this run to a
|
||
Haiku subagent and require verbatim output, not a summary.
|
||
|
||
### Anything needing a product decision
|
||
None — this is pure test-quality work with no user-visible behaviour change. If while adding tests
|
||
you discover a branch that looks like a real bug (not just untested), stop and report it separately
|
||
rather than writing a test that locks in wrong behaviour.
|
||
|
||
---
|
||
|
||
## Task D — Per-parent age-4 breakdown on the Dashboard
|
||
|
||
### Goal
|
||
The Dashboard's age-4 stat ("Förfaller vid 4 års ålder") is workspace-wide — it sums both parents'
|
||
projected expiring days into one number and the label says "all parents". The Plan tab, by
|
||
contrast, already breaks this down per parent. Both numbers are currently correct (a prior bug that
|
||
doubled one parent's entitlement was fixed in commit `760b312`), so this is **not** a bug fix — it
|
||
is a product decision about whether the Dashboard should also show a per-parent breakdown for
|
||
clarity, the way the Plan tab does. Done looks like, IF the product owner confirms they want it: the
|
||
Dashboard shows each parent's own expiring-day count (not just the combined total) using the
|
||
per-parent projection the backend already computes, with the combined total kept or clearly
|
||
labelled as a sum, whichever the product owner picks.
|
||
|
||
### Owned files
|
||
- `backend/app/services/analytics_service.py` (read-only reference; a per-parent breakdown is
|
||
likely already computed here — do not change its arithmetic without a specific reason)
|
||
- `frontend/src/pages/Dashboard.tsx`
|
||
- `frontend/src/pages/Dashboard.test.tsx`
|
||
- `frontend/src/i18n/sv.ts` / `frontend/src/i18n/en.ts` (new keys only, additive, if new labels are
|
||
needed)
|
||
|
||
Do not touch `backend/app/models/plan.py`'s dataclass→response mapping unless the per-parent field
|
||
needed on the Dashboard genuinely isn't on the wire yet — check first (see traps below).
|
||
|
||
### Constraints and traps that apply to THIS task
|
||
- **The 390 + 90 tier totals are the whole CHILD's entitlement across BOTH parents**, not one
|
||
parent's. One parent's no-snapshot default is `days_per_parent` (240 = 195 + 45). Getting this
|
||
wrong doubled a parent's entitlement and made two screens disagree — this was fixed in `760b312`
|
||
precisely in this area of the code (`analytics_service.py`), so be careful not to reintroduce it
|
||
while restructuring how the numbers are displayed.
|
||
- **The dataclass→response mapping in `backend/app/models/plan.py` is hand-written** and has
|
||
silently dropped new engine fields twice before. If you need a new field on the wire, add an
|
||
**API-level** test, not just a service test, to prove it actually reaches the response body.
|
||
- **Every user-facing string must be `message_key` + `data`**, never a hardcoded Swedish string.
|
||
`frontend/src/i18n/sv.ts` is the source of truth; `en.ts` derives its type from it, so a key
|
||
missing from either is a compile error.
|
||
- **No `any`, `@ts-ignore` or `@ts-expect-error`** — `no-explicit-any` is an ESLint error and CI
|
||
runs `--max-warnings 0`.
|
||
- **Use `npm run typecheck`, never `npx tsc`** — npx resolves a newer TypeScript and emits a bogus
|
||
`TS5101 baseUrl is deprecated` error.
|
||
- **Never delete, skip or weaken a test to make a suite pass.**
|
||
- Reference facts already confirmed in `backend/app/services/analytics_service.py`: it has
|
||
`expiring_at_b4` per parent (a single-parent projection, `max(projected_left - 96, 0)`) and a
|
||
combined view with `combined_days_used`, `combined_days_left`, `combined_expiring_at_b4` (sum
|
||
across all parents' projections) — the per-parent numbers this task needs already exist
|
||
server-side; check whether they already reach the Dashboard's API response or need adding to the
|
||
relevant response model.
|
||
|
||
### Done criteria
|
||
Backend (only if you touch it):
|
||
```bash
|
||
cd backend && uv run ruff check . && uv run ruff format --check . && uv run pytest
|
||
```
|
||
Baseline: 439 tests passing, ruff check clean, ruff format --check clean (128 files); count should
|
||
only increase.
|
||
|
||
Frontend:
|
||
```bash
|
||
cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage
|
||
```
|
||
Baseline: 710 tests / 56 files passing, coverage 96.54%/96.54%/91.08%/86.84% (lines/statements/
|
||
functions/branches); none of the ratchet floors (95/95/89/85) may regress.
|
||
|
||
Per the cost rule, if you are a Sonnet-or-larger agent, delegate both runs to a Haiku subagent and
|
||
require verbatim output, not a summary. Also do a manual browser pass with seeded demo data (see
|
||
§1) comparing the Dashboard and Plan tab numbers for Alex and Sam side by side — this is exactly
|
||
the kind of two-screens-disagree bug that has bitten this project before (`760b312`).
|
||
|
||
### Anything needing a product decision
|
||
**This entire task is blocked on a product decision:** does the product owner actually want a
|
||
per-parent breakdown on the Dashboard, or is the current combined "all parents" framing intentional
|
||
(e.g. because the Dashboard is meant to be an at-a-glance household view and per-parent detail
|
||
belongs only in the Plan tab)? If yes, also decide: does the combined total stay visible alongside
|
||
the per-parent numbers, or does the per-parent breakdown replace it? Do not guess and build either
|
||
answer.
|
||
|
||
---
|
||
|
||
## 3. Traps reference (for the orchestrator only — already duplicated into each brief above)
|
||
|
||
Kept here once for the orchestrator's own awareness; do not point a subagent at this section
|
||
instead of the duplicated version in its brief — that defeats the point of self-contained briefs.
|
||
|
||
- `holidays` library combined names (`"Påskdagen; Söndag"`) — exact-string match only.
|
||
- `_FULL_SCHEMA` in `backend/app/db/connection.py` must track Alembic migrations.
|
||
- `vi.mock('@/context')` doesn't intercept direct submodule imports.
|
||
- `backend/app/models/plan.py` dataclass→response mapping is hand-written and has dropped fields
|
||
twice.
|
||
- `localStorage` unreliable in the test sandbox — `vi.stubGlobal`.
|
||
- `git grep` only searches tracked files.
|
||
- Literal `*/` inside JSDoc breaks the file (bit `WishEditor.tsx` once).
|
||
- `npm run typecheck`, never `npx tsc` (npx resolves a newer TS, bogus `TS5101`).
|
||
- No `any` / `@ts-ignore` / `@ts-expect-error` — ESLint error, CI `--max-warnings 0`.
|
||
- Never delete/skip/weaken a test to make a suite pass.
|
||
- Keep the `Allocator` pure — no DB, no `date.today()`.
|
||
- Every user-facing string is `message_key` + `data`; `TestNoSwedishOnTheWire` enforces it.
|
||
- 390+90 tiers are the whole child's entitlement across both parents, not one parent's.
|
||
|
||
## 4. Decisions already made — do not relitigate (for the orchestrator's awareness)
|
||
|
||
- Planning horizon runs past B4; the FP pool is capped at `min(remaining, 96)` after — show the
|
||
squeeze, don't refuse.
|
||
- Valborgsmässoafton is NOT a default day off.
|
||
- No API calls at runtime in the planner; calendar cache covers 2026–2055, `strict=True` raises
|
||
`UncachedYearError` → 422 rather than silently degrading.
|
||
- 2049 and 2055 legitimately have 17 red days (Pingstdagen coincides with Sveriges nationaldag).
|
||
- Flexible wishes are propose-time suggestions, never auto-applied; fixed wishes always win.
|
||
- An applied flexible occurrence may move on a later re-propose (no `wish_id` on
|
||
`planned_leave_day`).
|
||
- WishEditor v1 ships season presets only — a custom range picker (Task B above) is deferred, not
|
||
rejected.
|
||
- Coverage thresholds are a regression ratchet, never lowered to make a gate pass.
|
||
- i18n is hand-rolled on purpose — no `react-i18next`.
|
||
- Verification cost rule: Haiku runs the suites itself; Sonnet+ delegates and requires verbatim
|
||
output.
|