Skip to content

fix: stop reporting a partial-coverage sum as net worth - #97

Closed
KenTaniguchi-R wants to merge 1 commit into
fix/liability-sign-conventionfrom
fix/net-worth-coverage-boundary
Closed

KenTaniguchi-R wants to merge 1 commit into
fix/liability-sign-conventionfrom
fix/net-worth-coverage-boundary

Conversation

@KenTaniguchi-R

Copy link
Copy Markdown
Owner

Fixes #86.

Stacked on #95 (fix/liability-sign-convention). Both touch getNetWorthHistory and NetWorthPoint; building this on main would have meant developing a net-worth chart against a net-worth function I already knew inverted the sign. Merge #95 first and the diff here collapses to just these changes.

The bug

The dashboard read:

Net worth $52,942.44 — ↑ $55,214.00 (2430.7%) past 6 months

Nothing earned that. Five accounts holding $51,703 — both Robinhood accounts, the IRA, Crypto, the Wells Fargo card — have their first-ever balance snapshot on 2026-08-28. Every point before that counted them as $0, so the chart plotted the credit cards alone and labelled the result net worth, then jumped $55K when the rest appeared.

Coverage on the real household steps:

2026-05-31    2 of 10 accounts
2026-06-01    3 of 10
2026-06-07    4 of 10
2026-07-29    5 of 10
2026-08-28   10 of 10   <- boundary

Carry-forward (#68) fixed gaps between an account's snapshots. It cannot fix a gap before the first one — there is nothing to carry backward. So this is a presentation problem, not a reconstruction one, and per the #67 precedent the fix does not invent the missing history; it says which stretch is missing.

What changed

QuerygetNetWorthHistory emits coveredAccounts / totalAccounts per point. lastBalanceByAccount.size is already exactly the number of accounts we hold any balance for as of that date, so this is free; the synthetic today point counts accounts reporting a live balance.

Delta — new coveredTrendDelta measures across the fully covered span only and returns null with fewer than two such points. A delta from a partial baseline reports accounts appearing, not money arriving.

Chart — splits at the boundary: muted dashed line plus a hatched band for the partial span, solid area after, boundary rule between. The boundary point belongs to both dataKeys or the segments wouldn't meet. The tooltip drops the null half so a date isn't listed twice.

Caption — names the shortfall in words ("2–5 of 10 accounts had balance history before Aug 28, so it is not yet net worth"), so the treatment is never color-alone.

Zero deltas lose the arrow — "↑ $0.00" pointed somewhere the number didn't. This case is live right now: the real household has exactly two fully-covered points at the same value.

Design

Three treatments were mocked against the real 84-point series before building; this is option B. A (trim to full coverage) is the most literal reading of "net worth" but collapses 6M/1Y/All to two points and discards three months of accurate credit-card history. C (suppress the percentage only) is two lines but leaves a line diving to −$13,456 with nothing on screen explaining why.

Typing

NetWorthPoint splits into NetWorthSeriesPoint (date/assets/liabilities/netWorth) plus the coverage-carrying dashboard shape. The reports series has the same leading-gap exposure and keeps the base type — surfacing coverage there is separate work, and coverageBoundary treats absent coverage fields as "fully covered" rather than guessing.

Known limitation

Nothing distinguishes "the account didn't exist yet" from "we have no data yet." accounts.createdAt is when Ledgr learned of an account, not when it opened. So a genuinely new account is also marked partial. That is the conservative direction, but if an "opened on" field is ever added, the band should respect it.

Verification

Full suite green: 113/113 test files. New tests cover the boundary helper (7 cases incl. never-completes, no-coverage-data, and the exact −$2,271 → $52,942 series that produced 2430.7%) and three integration tests on coverage reporting, including seeding from before the window.

Verified in the running app that a fully-covered household (the demo seed) shows no band and keeps its normal range label — the negative case doesn't regress.

🤖 Generated with Claude Code

The MCP server SDK was pinned to the 2.0.0-beta.5 pre-release. Stable
2.0.0 has since shipped, so move off the beta.

No API drift: createMcpHandler and McpServer in src/lib/mcp/server.ts
are unchanged, and the bundled protocol revisions stay the same
(2026-07-28 current, 2025-11-25 legacy fallback).
@KenTaniguchi-R

Copy link
Copy Markdown
Owner Author

Superseded by #95. The coverage-boundary commit (6ed2d68) landed on fix/liability-sign-convention rather than this branch, and this branch ended up pointing at main's head. Rather than rewrite two branches with open PRs, #95 now carries the whole set — see its updated description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant