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>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-09-30 16:51:17 +02:00
commit 933027cb1b
89 files changed
+4023 -651

No files matched your search

+42 -2
View File
@@ -58,8 +58,9 @@ remaining = carriedIn + allocation − charges (carried forward, monthly)
Unspent carries; overspend carries as debt. **Nothing else may recompute this.**
The month view, year matrix, summary, per-post views and the ODS export all
aggregate over `foldBalances` / `figuresFor`. A closing balance is read from
`byMonth[11].remaining`, never re-summed.
aggregate envelope figures over `foldBalances` / `figuresFor`. Overall Spent
also includes unassigned purchase slices, which have no envelope to fold. A
closing balance is read from `byMonth[11].remaining`, never re-summed.
A recurring cost adds a SECOND balance beside it, on its own carry:
@@ -117,6 +118,21 @@ uses on a first run, so "reset" and "a brand-new browser" cannot drift apart.
Both are preceded in the UI by a JSON backup download that must succeed before
the destructive write is attempted.
## Session undo and redo
The snapshot store keeps up to 50 prior saved datasets in memory. A changed
`mutate` adds one step after its write succeeds; an unchanged mutation adds
none. Undo and redo run through the same queue and persist their target dataset
before changing the visible snapshot or either history stack. A failed write
therefore leaves both the data and the available undo/redo actions intact.
Import and reset clear the stacks only after their write succeeds. They are
whole-dataset replacement flows with backup-first protection, not ordinary
undoable edits. History is not in `Dataset` or IndexedDB and vanishes on reload.
The header reads a stable history capability snapshot from the store to enable
its controls; keyboard shortcuts skip editable fields so browser text undo
still works.
## Money
A float, deliberately, with two rules that make it safe:
@@ -157,6 +173,20 @@ those parts*. Both split modes collapse to one code path per slice.
Applying a split's raw value per-slice looks simpler and passes every percentage
test — and charges a fixed "2000 to Video Games" in full on every slice.
A purchase with no post has an empty `splits` array. Its dated amount counts in
monthly, yearly and summary Spent totals, but charges no envelope and changes no
rollover balance. Recurring bills still require a destination post. JSON import
and ODS export preserve these unassigned purchases. In the month Budget tree,
they appear in the permanent, derived Unassigned section for each month their
purchase or finance plan touches. Creating a post from one assigns its full
amount to the new post without changing the purchase total or schedule.
A post with no applicable allocation rule has no overspend status, even if its
balance is negative. The balance remains visible and carries normally. An
explicit zero rule, including a zero monthly override, does have an overspend
status when its balance is negative. The views use the presence of a rule rather
than infer it from the numeric allocation, since both cases fold to zero.
## Things the app deliberately does NOT validate
Adding a "missing" guard here is a regression, not a fix:
@@ -220,6 +250,16 @@ is first and stored, every XML part parses, and numeric cells carry
behavioural one, because the bug it prevents depends on real browser render
timing that happy-dom does not reproduce.
## Budget placement
Folders organize posts and recurring bills in one sibling order. The pure
`budgetTree` selector combines month post rows with computed scheduled slots;
folder Bills due sums bill prices in the selected slot month, including
descendants, using confirmed purchase totals where available. Folder placement
is independent of bill split destinations. Only posts have allocations and
rollover balances. The month UI renders the same tree as a desktop table and a
mobile list, and keeps occurrence confirmation in the Expected band.
## Where the reasoning lives
- `AGENTS.md` — the rules, in enforceable one-line form, and which test fails