Skip to content

fix(reports): the net-worth line disowned money the card beside it counted - #807

Merged
lessevv merged 2 commits into
mainfrom
wt/nwseries
Sep 13, 2026
Merged

lessevv merged 2 commits into
mainfrom
wt/nwseries

Conversation

@lessevv

@lessevv lessevv commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

The problem

NetWorthSeriesQuery was the only non-reconciliation caller of clearedBalanceAsOf(). Every other surface that states a balance — the dashboard card, the forecast anchor, the calendar's past-day line, the pot ledger — reads currentBalanceAsOf(). So the net-worth chart could draw a line that disagreed with the number printed directly above it, with nothing on screen to say which was which.

It now reads the same seam. clearedBalanceAsOf() is left to /reconcile, its only remaining caller, which is the one place the cleared/reconciled window is the right question.

Before / after, on the shipped sample dataset

demo-1, EUR, today = 2026-09-13. The dashboard card reads 613574 throughout — it is unchanged by this PR.

Point's own day Before (cleared) After (current) Card
2026-06-30 726745 726745 —
2026-07-31 918129 900530 —
2026-08-31 837048 809958 —
2026-09-13 (same day as the card) 643743 613574 613574
2026-09-30 (this_month bucket) 623714 593545 —

On the one day where the date difference is nil, the series was 30169 away from the card and is now equal to it. The gap was five uncleared JPY cash rows on Japan Trip Cash (−48000 JPY) that the line refused to count — money the reader had spent, that the card counted and the chart did not. 2026-06-30 does not move because nothing was still unconfirmed by then.

One difference is left, and it is the date: the card asks about today, a point about its bucket's last day, so a this_month report still samples the 30th. That residue is now the whole of it rather than one of two causes.

Why was it clearedBalanceAsOf()?

No stated reason exists, and the switch contradicts nothing.

  • git log -L puts it at ce23d0436 (fix: five surfaces derived the same balance five ways, and no two agreed #272, "five surfaces derived the same balance five ways, and no two agreed") — a sweep unifying five surfaces onto one balance seam. The line changed from reading account.default_currency straight to calling the seam; the cleared/current choice was never discussed in that commit, in a comment, or in a test name. Every later touch was an unrelated signature change.
  • No C7 acceptance criterion names cleared. C7-R7 governs on-demand sampling; C7-R8 requires the divergence to be documented. Neither prescribes which rows a point counts.
  • The reconciliation callers want the cleared window for a stated reason that is specific to them, and they keep it.
  • AccountBalanceQuery's own comment above currentBalanceAsOf() already read "Net worth asks this and not currentBalance()" — a line the code has contradicted ever since. The comment is now true.

Tests

One test asserted the disagreement, and it is changed deliberately. Modules/Reports/tests/Unit/TheNetWorthSeriesAndTheCardDifferInTwoPlacesTest.php pinned "the card counts every row while the series counts only the cleared ones" as one of two named causes. Renamed to TheNetWorthSeriesAndTheCardDifferOnlyInTheDayTheyAskAboutTest.php and rewritten: it now pins the agreement on a shared day and the date-only gap on a bucket that outruns it. Its name no longer states an invariant the code contradicts.

Red before, green after:

✓ it reads the card as of today, counting a row nobody has confirmed yet
⨯ it agrees with the card to the cent on a point whose own day is today
  Failed asserting that -10000 is identical to -35000.
⨯ it counts every row the bucket last day has reached, pending ones included
  Failed asserting that -15000 is identical to -40000.
⨯ it differs from the card by the rows between the two dates and by nothing else
  Failed asserting that -20000 is identical to 5000.

Tests: 3 failed, 1 passed

The series read -10000 on the same day the card read -35000 — a 25000 gap on one day, carried entirely by one uncleared row. After: 4 passed.

One arch guard was keyed on the old spelling. tests/Contracts/NoNetWorthHistoryIsStoredArchTest.php pinned the sampler to the literal clearedBalanceAsOf, while its own failure message claims only that the sampler still calls "the balance-as-of seam". Widened to either spelling — which rows a point counts is the sampler's choice, and that guard is not the place it is made. Confirmed load-bearing by inversion: the new code under the old regex goes red.

No other test moved. NetWorthSeriesQueryTest and FxExclusionTest insert rows without a status, and the column defaults to cleared; NetWorthIncludesStartingBalanceTest sets Cleared explicitly. All three are unaffected by the switch.

Run: 22 passed across the six affected files, plus the comment/helper arch guards. Pint and PHPStan (level 10, strict) clean on Modules/Reports and tests/Contracts.

Docs

.docs/features/reports/architecture.md described the gap as two differences whose sum it was, with measured figures. It is now one difference — the date — with the figures re-measured on the same dataset and the same date, and a paragraph recording what the cleared choice did and did not buy.

The .docs page was not the wrong one. It already said "Both read AccountBalanceQuery as of a date, so the anchor is not the difference" — it had been corrected when the card moved off BalanceAnchorResolver. The stale "different anchor path" sentence is in the spec, at 10-functional/features/c-insight/c7-reports.md, and is corrected in beatrax-app/spec#110 (auto-merge armed). That sentence was wrong before this PR and would have stayed wrong without it.

Spec: C7-R8

…unted

The series was the only non-reconciliation caller of clearedBalanceAsOf().
Every other surface that states a balance -- the dashboard card, the forecast
anchor, the calendar's past-day line, the pot ledger -- reads
currentBalanceAsOf(), so the chart could draw a line that contradicted the
number printed directly above it with nothing on screen to say which was which.

It now reads the same seam. On the shipped sample dataset in EUR on 2026-09-13
the point whose own day IS today moved 643743 -> 613574 and equals the card's
613574 exactly; the 30169 gap was five uncleared JPY cash rows the line refused
to count. Earlier points moved by whatever was still unconfirmed on their own
day (2026-07-31 918129 -> 900530, 2026-08-31 837048 -> 809958) and 2026-06-30
did not move at all.

One difference is left and it is the date: the card asks for today, a point
asks for its bucket's last day, so a this_month report still samples the 30th.
That residue is now the whole of it rather than one of two causes.

Nothing stated the cleared choice. It was introduced unremarked by #272 while
five surfaces were being unified onto one balance seam, no C7 acceptance
criterion names cleared, and AccountBalanceQuery's own comment on
currentBalanceAsOf() already read "Net worth asks this" -- a line the code has
contradicted ever since.

TheNetWorthSeriesAndTheCardDifferInTwoPlacesTest asserted the disagreement, so
it is renamed and rewritten: it now pins the agreement on a shared day and the
date-only gap on a bucket that outruns it. NoNetWorthHistoryIsStoredArchTest
pinned the sampler to the literal spelling clearedBalanceAsOf while its own
message claims only that the sampler still calls "the balance-as-of seam";
widened to either spelling, and confirmed red against the new code under the
old regex.

Spec: C7-R8
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Wessel Verheij <info@nightworks.io>
@lessevv
lessevv enabled auto-merge (squash) September 13, 2026 14:18
@github-actions github-actions Bot added documentation Docs only area:insight Recurring, drift, anomalies, forecasting, calendar, reports labels Sep 13, 2026
@github-actions

Copy link
Copy Markdown

📎 Spec references

This PR cites the following spec identifiers:

  • C7-R8 — The divergence between the sampled net-worth series and the dashboard card MUST be documented.

Updated automatically on each push.

@sonarqubecloud

Copy link
Copy Markdown

@lessevv
lessevv merged commit 92cc538 into main Sep 13, 2026
23 checks passed
@lessevv
lessevv deleted the wt/nwseries branch September 13, 2026 15:02
@github-actions github-actions Bot added awaiting-maintainer Green PR with no review — a maintainer needs to act and removed awaiting-maintainer Green PR with no review — a maintainer needs to act labels Sep 13, 2026
lessevv added a commit that referenced this pull request Sep 13, 2026
…unted (#807)

The series was the only non-reconciliation caller of clearedBalanceAsOf().
Every other surface that states a balance -- the dashboard card, the forecast
anchor, the calendar's past-day line, the pot ledger -- reads
currentBalanceAsOf(), so the chart could draw a line that contradicted the
number printed directly above it with nothing on screen to say which was which.

It now reads the same seam. On the shipped sample dataset in EUR on 2026-09-13
the point whose own day IS today moved 643743 -> 613574 and equals the card's
613574 exactly; the 30169 gap was five uncleared JPY cash rows the line refused
to count. Earlier points moved by whatever was still unconfirmed on their own
day (2026-07-31 918129 -> 900530, 2026-08-31 837048 -> 809958) and 2026-06-30
did not move at all.

One difference is left and it is the date: the card asks for today, a point
asks for its bucket's last day, so a this_month report still samples the 30th.
That residue is now the whole of it rather than one of two causes.

Nothing stated the cleared choice. It was introduced unremarked by #272 while
five surfaces were being unified onto one balance seam, no C7 acceptance
criterion names cleared, and AccountBalanceQuery's own comment on
currentBalanceAsOf() already read "Net worth asks this" -- a line the code has
contradicted ever since.

TheNetWorthSeriesAndTheCardDifferInTwoPlacesTest asserted the disagreement, so
it is renamed and rewritten: it now pins the agreement on a shared day and the
date-only gap on a bucket that outruns it. NoNetWorthHistoryIsStoredArchTest
pinned the sampler to the literal spelling clearedBalanceAsOf while its own
message claims only that the sampler still calls "the balance-as-of seam";
widened to either spelling, and confirmed red against the new code under the
old regex.

Spec: C7-R8

Signed-off-by: Wessel Verheij <info@nightworks.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:insight Recurring, drift, anomalies, forecasting, calendar, reports documentation Docs only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant