# TODO Work still queued, for whoever picks it up next, human or agent. Ordered. Each item says enough to act on without re-deriving the reasoning. Finished work is not listed here. It is in the commit messages, and the decisions behind it are in `docs/specs/` — one spec per sub-project, each recording the alternatives that were rejected. Rules that outlived their task moved to `AGENTS.md`. **Current state:** the anchored model is merged with budget folders and placement. The month pie showcase, browser-stored session backups, and unassigned recurring costs are implemented. 881 tests pass across 64 files; `bunx tsc --noEmit` is clean, and `SKIP_FX_FETCH=1 bun run build` succeeds using the committed seed rates. **Schema is at version 11.** v1 -> v2 made allocation rules a dated series; v2 -> v3 moved currencies into the dataset; v3 -> v4 drops a stored rate-service URL that can no longer work; v4 -> v5 adds sterling to an existing dataset; v5 -> v6 collapses per-currency decimals into one dataset-wide setting; v6 -> v7 adds `Dataset.recurring`; v7 -> v8 adds budget folders and assigns existing posts and recurring bills a deterministic mixed sibling order under Unfiled. v8 -> v9 adds optional purchase and bill placement fields without moving old rows; v9 -> v10 nests existing unfiled single-post bills under their post. v10 -> v11 moves a `lastCharge` cost off slots — it strips `occurrenceDate` from every anchored charge, expands a month-only `startDate` on an anchored `everyNMonths` cost to a day-granular one, and expands a month-only `Purchase.date` on every anchored charge (the day comes from the `occurrenceDate` being stripped when that names one in the same month, otherwise the 1st). The first nine preserve financial behaviour — not one changes a stored figure. The tenth changes no stored figure either, but it is NOT behaviour-preserving by construction: an anchored cost's projected due dates genuinely change shape, because they no longer come from a grid at all. All ten are tested. A migration applies **on load, in memory**; it is written back on the next mutation, not by `load()` itself. So a freshly migrated dataset shows the new shape immediately while IndexedDB still holds the old version for a while. That is not a bug, but it will mislead you if you verify a migration by reading IndexedDB instead of the app. --- ## Optional monthly income forecast Add a user-controlled toggle that predicts a month's income from earlier months. For each earlier month, divide its recorded income by that month's number of workdays; average those per-workday figures, then multiply by the target month's workdays. Let the owner adjust the prediction when income changes, for example after a raise, without rewriting recorded income or earlier months. Show predicted and entered income distinctly, and keep budgeting based on entered income unless the owner explicitly chooses otherwise. Before implementation, settle whether workdays mean Monday–Friday only or exclude local holidays, and whether an adjustment applies to one month or all future predictions. ## 1. Folder rename blur during an in-flight save A folder name draft can remain unsaved if the owner changes it while a rename save is in flight and then blurs the field before that save finishes. Preserve the newer draft and ensure the blur requests a subsequent save after the pending write completes. Cover the delayed-write sequence with a UI interaction test, including the final persisted name and any surfaced write error. ## 2. Recurring split editor interaction coverage `RecurringSplitEditor` has coverage for destination selection and dedicated post creation, but its rendered split mode and value controls lack a focused interaction test. Exercise the real controls to switch percentage/fixed mode and edit a split value, then assert the resulting splits and remainder behavior. Keep this scoped to the editor's user interaction; domain split calculations already have their own tests. ## 3. Two things the recurring-cost model gets wrong, both known Neither is a bug introduced by the anchored model. Both are recorded here because they were found while reviewing it, and both are the kind of thing a future reader would otherwise rediscover the hard way. ### Editing a live calendar cost re-grids it and orphans its charges `updateRecurringCost` refuses an `anchoring` change once a cost has charges (AGENTS.md §2 says why), but it ACCEPTS `recurrence` and `startDate` changes on the same charged cost. For a `calendar` cost those two fields ARE the grid, so changing either moves every slot — and every existing charge still names, in `source.occurrenceDate`, a slot that no longer exists. The confirmation stops matching anything, the slot it settled reappears as unpaid, and the bill is counted twice: once as the purchase that was really paid, once as an expectation that never was. Reproduction, run and confirmed: 1. A `calendar` cost, `everyNDays` with `n: 7`, 1000 kr, starting 2026-09-01, allocated 5000 kr in a 2026-09 with 20000 income. 2. Confirm the SECOND slot — `confirmOccurrence(cost, "2026-09-08")`, one real purchase carrying `occurrenceDate: "2026-09-08"`. 3. September reads `expected 4000`, `projected 0`, `remaining 4000`. 4. Change `n` to 5. Nothing else. 5. September reads `expected 6000`, `projected -2000`, `remaining 4000`. The paid bill now counts as both a charge and an expectation. Nothing warns. The second slot, not the first, on purpose: the new grid (01, 06, 11, …) still contains 2026-09-01, so a confirmation of the FIRST slot survives the re-grid by luck and the damage is smaller (`expected 5000`, `projected -1000` — still wrong, because the grid gained a slot). Any confirmed slot the new grid does not happen to contain is orphaned outright. Do NOT patch this by silently rewriting the stored `occurrenceDate`s — there is no honest mapping from one grid to another, which is exactly the reasoning that made an anchoring change refusable. The real question is a product one: what should editing a live bill MEAN? Plausible answers are refusing the edit the way an anchoring change is refused, ending the cost and starting a new one (what the owner is told to do today for anchoring), or giving `RecurringCost` a dated series the way `Post.rules` already has, so a rent increase or a schedule change takes effect from a month rather than retroactively. That is a design decision with a spec behind it, not a patch. ### An anchored cost has no HISTORY of expectations This follows from the spec and is not a defect, but it sits in tension with AGENTS.md §1's "archiving must not retroactively remove past expected charges", so it deserves saying out loud. The anchored walk emits occurrences from ONE anchor: the latest charge. Recording a charge therefore erases every earlier month's expectation for that cost — there is nothing left in the dataset that says a charge was once expected in July. Consequences, all by design: - `overdue` in `ExpectedBand` is 0 for any anchored cost that has ever been charged — which is every real one, after its first charge. It counts unconfirmed occurrences in earlier months, and an anchored cost projects none behind its anchor. (An anchored cost with NO charges yet does project into the past from its `startDate`, and those do count, so the figure is not structurally zero; it collapses to zero the moment a charge is recorded.) - A PAST month's `projected` figure moves when a charge is recorded today, because the expectation that month used to carry stops being projected. `remaining` — the real, spent-money figure — is untouched, so the historical record of what happened is intact; it is the projection that is not historical. Measured on a monthly anchored cost of 300 kr starting 2026-06-01, in a dataset folding from 2026-06 with the current month at 2026-09. Before any charge: July reads `expected 300`, `projected 9400`, `remaining 10000`, and `overdue` is 3. After one charge dated 2026-09-04: July reads `expected 0`, `projected 10000`, `remaining 10000`, and `overdue` is 0. A `calendar` cost behaves the way §1 describes: its grid is fixed, so past slots stay put whether they were ever settled or not. The two anchorings differ here on purpose (AGENTS.md §1's schedule invariant), and unifying them would reintroduce the bug the anchored model exists to fix. ## 4. Anchored-model follow-ups left after the merge with main The anchored model was merged on 2026-09-30, after main had gained budget folders, placement and nested bills. Everything below is known and unfinished. ### A bill created from the purchase dialog has no folder choice Settings' new-bill form picks a folder; the dialog's "+ New recurring cost…" does not. The new bill nests under its post when it has exactly one split (the same default Settings applies), otherwise it lands in Unfiled and has to be moved from Budget folders. Decide whether the dialog needs the picker or whether moving it afterwards is fine for an occasional action. ### "Unlink from bill" was replaced by the Recurring cost select Main had an "Unlink from bill" checkbox on an edited purchase; the merge dropped it because choosing "None" in the anchored model's "Recurring cost" select does the same thing (and `updatePurchase` now honours an explicit `source: undefined`). Its copy said "the occurrence will show as expected again", which is true for a `calendar` bill and meaningless for a `lastCharge` one, whose next due date simply re-derives from the remaining charges. Confirm the owner does not miss the checkbox; nothing tests its absence. ### Not yet driven in a browser The merge was checked by the full suite, `tsc`, a build, and screenshots of the month page and Settings with demo data (both render, no console errors, Car insurance shows "Next 2026-10-19" after charges on 3 and 19 September). Not yet clicked through by anyone: - tagging a new purchase to an existing anchored bill and seeing its next due date move in the Budget tree and in Expected; - "+ New recurring cost…" end to end, including where the new bill appears in Budget folders; - untagging an edited purchase via "None"; - the Settings anchoring control switching a never-charged monthly bill to "the last charge" (its month-only start should expand to the 1st and the Month/Date precision control should disappear), and being locked once the bill has a charge. ## 5. Visual redesign, sub-project 3 — Year, Summary, Post detail The owner's brief for the whole redesign: *"right now it looks a bit boring, simple and ugly. We want sleek, modern, and nice to use."* On scoping: both themes with a toggle, a full rethink of every screen, purchases compact and grouped by date, ship a webfont — and *"override everything you want for this step."* Sub-projects 1 (design foundation, `bb3c661`) and 2 (month view, `7b71811`) are done, each with a spec in `docs/specs/`. These three screens are on the new foundation but have not been rethought. The open question is the 12-month matrix: it only fits by scrolling, and compact number formatting may be what replaces that rather than a scroll container. **The diagnosis that shaped the decomposition, which still applies:** a design system already existed and was never wired up. `src/index.css` defined the paper/ink/teal palette with a contract document behind it, while the app painted shadcn's default white and near-black; the tokens reached the screen in two places only. Doing the foundation last is how that happened. Do not repeat it — extend the foundation rather than styling a screen locally. **Three things sub-project 2 settled that these screens should inherit rather than re-decide** (spec: `2026-09-02-month-view-design.md`): - **Meter geometry lives in `src/ui/meterSegments.ts`**, pure and tested, with `` as the only thing that paints it. A third meter belongs there too, not inline in a component. Negatives clamp rather than invert, because a segment of negative width puts the gradient's stops out of order and paints something arbitrary instead of failing. - **Two markups, not one reflowing grid.** The post table is a real `` from `sm:` up and a list of two-line blocks below it. At 390px the layout genuinely stops being a table, and `role="table"` on it would describe rows and columns that no longer exist on screen. The year matrix faces the same question with more columns to lose. - **State the currency once per table, not per cell.** Every figure in the post table is base currency, so `formatSignedAmount` prints bare numbers and the legend names DKK once. This is what stopped "+4,219.61 DKK" wrapping on a phone. `formatMoney` is still right wherever an amount can be in some other currency — every purchase row, for one. ## 6. Visual redesign, sub-project 4 — dialogs and fast entry Untouched. Native `