Files
budget2.0/docs/TODO.md
T
Lucas WintherandClaude Opus 5.5 933027cb1b Merge main into the anchored model
Main moved on by 27 commits since this branch forked, including three schema
steps of its own (budget folders, placement, nested bills). Resolutions worth
knowing:

- The anchored-model migration becomes 10 -> 11; main's 7 -> 10 run first.
- Main's nextUnconfirmedOccurrenceOnOrAfter walked the calendar grid for
  every cost, which would have shown the slot-model "next due" for an
  anchored bill in the budget tree. Both walks are now generators and the
  lookup dispatches on anchoring, like occurrencesOf.
- A folder's "Bills due" summed only occurrences, and an anchored bill's
  charges are never occurrences, so a charged phone plan read as 0 due.
  budgetTree now adds an anchored bill's charges in the month.
- Main's RecurringSection (with RecurringSplitEditor) supersedes this
  branch's split editor; the anchoring rules are ported onto it: start-date
  validation via startDateNeedsDay, month precision hidden for lastCharge,
  the anchoring control locked once a cost has charges.
- PurchaseDialog keeps main's placement control. The "Recurring cost" select
  replaces main's "Unlink from bill" checkbox, since "None" does the same.
  A bill created from the dialog nests under its post like one from Settings.
- Dropped main's "bounded projection ignores invalid confirmations" test: it
  exercised slot confirmations on a lastCharge cost, which no longer exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:51:17 +02:00

263 lines
15 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 870 tests passing across 61 files. `bunx tsc --noEmit` is clean;
`bun run --bun build` succeeds using the committed seed rates when live-fetch
is unavailable. The runtime override bypasses a broken local Bun shim.
**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. 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
`<Meter>` 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 `<table>`
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.
## 5. Visual redesign, sub-project 4 — dialogs and fast entry
Untouched. Native `<select>` elements are still browser-default, and
`up` / `down` / `archive` in Settings are bare text buttons. Covers the purchase
dialog, the split editor, the plan editor and bulk entry.
There is already a spec for fast-entry *behaviour* —
`2026-09-02-fast-entry-refinements-design.md`. Read it before restyling, so the
visual work does not fight it.
## 6. Currency work deliberately left undone
All three were decided against at the time, not forgotten. Reopen only with a
reason.
- **The income field and the split editor's fixed-amount fields do not parse a
currency** — only the purchase total does. Same helper (`parseMoneyInput`),
same three-line pattern. The income field is arguably wrong to make
multi-currency at all, since a month has one income.
- **No currency picker convenience.** Adding a currency means typing the code,
name, symbol and decimals by hand. A bundled ISO 4217 list would be nicer, and
is pure data.
- **Symbol collisions resolve to the first currency defined with that symbol.**
Fine for one "$" currency; if the owner adds two, typing the code
disambiguates and nothing warns them. Tested, documented, not solved.
## 7. Still needs a human
`scripts/screenshot.ts` drives headless Chrome, so this list is much shorter
than it was. Already seen and confirmed working: the carry meter (including red
where a post is overspent), the rule-history editor, "change from here", the
baked FX rates arriving in a fresh dataset, the reset writing a fresh dataset to
IndexedDB, and every route at desktop and mobile widths in both themes.
Genuinely outstanding:
- **How it looks and feels.** A screenshot proves a layout is not broken. It
cannot tell you the design is good, and the container's fonts are not the
owner's fonts.
- **Offline behaviour** and the **install prompt**. The service worker itself is
no longer unverified: it demonstrably installs, activates, claims the page,
precaches all 11 assets, and applies an update cleanly (`e6da85e`). What is
still unverified is a real offline load with the network actually cut, and
Chrome's install prompt appearing.
- **The pre-destructive backup actually landing as a file.** Import and reset
both download a JSON backup before replacing the dataset, and only proceed
when that reports success — but headless Chrome's download is not observable
here, so the backup is *issued*, never confirmed openable.
- **The generated `.ods` opening in a real spreadsheet.** Validated
structurally: mimetype first and stored, well-formed XML, numeric cells.
- **Keyboard flow in fast entry** — Tab traversal, Backspace-to-remove, and
focus placement after a row auto-appends. Drivable over CDP in principle; not
yet done.
- **GitHub Actions** has never been executed.
- **Whether two-stage confirmation matches how the owner actually reconciles a
bank statement.** Recurring costs (`docs/specs/2026-09-03-recurring-costs-design.md`)
project expected occurrences and require a click to turn one into a real
purchase, rather than charging automatically. That is a product bet a week of
real use answers and no test can.
- **Whether the anchored (`lastCharge`) model actually matches how the
owner's phone plan bills.** The design
(`docs/specs/2026-09-05-recurring-costs-anchored-model.md`) rebases the
series on whatever was last charged, restarting the clock every time — a
reasonable model of "refreshes when the data runs out", built from the
owner's own description rather than from watching a real bill arrive. Only
a cycle or two of real use confirms the shape is right.
- **The overdue count against a large, real backlog.** `ExpectedBand` counts
unconfirmed occurrences from earlier months, and demo data does now
accumulate a modest one (a few months of an unconfirmed monthly bill plus a
biweekly one, ~14 occurrences by September) that renders without incident.
Nobody has seen the count or the layout against years of neglect or dozens of
distinct bills.