docs(goals): split architecture note into user-facing + mechanics

The user-facing note focuses on how a Goal's balance gets computed
and what changes for the user. The mechanics doc covers the schema,
pro-rata math, pace window, and the operational details engineers
need but most readers don't.
This commit is contained in:
Guillem Arias
2026-05-12 16:30:25 +02:00
parent 870fbcd976
commit 7e464a4b1f
2 changed files with 100 additions and 46 deletions

View File

@@ -0,0 +1,72 @@
# Goals: account-linked model — engineering mechanics
*Posted 2026-05-12. Companion to [`goals-architecture.md`](goals-architecture.md). Engineering-facing detail behind the user-facing summary.*
## What changes for users (recap)
You set a goal. You link the accounts that hold the money for it. Whatever those accounts hold is your goal's balance. No "add contribution" step. The goal updates next time Sure syncs.
## Schema
```
goals: (unchanged from current branch)
goal_accounts: id, goal_id, account_id, allocated_amount NULL, currency
goal_contributions: dropped
```
`Goal#allocated`: sum of allocation amounts (NULL means full balance).
`Goal#backed`: pro-rata of allocations against actual account balance when contended.
## Pro-rata under contention
Account balance B, allocations a₁ … aₙ:
- `Σ aᵢ ≤ B`: each goal backed by aᵢ.
- `Σ aᵢ > B`: each goal backed by `aᵢ × B / Σ aᵢ`.
Fair-share, no priority. Priority is a v2 question and is explicitly deferred. The schema doesn't lock the door.
## Over-allocation
When `allocated > backed`, the goal shows: "Allocated $5K · Backed by $3K · Uncovered by $2K." Projection chart and status pill use intent. The user has three one-click affordances: reduce allocation, transfer in, accept.
## Unallocated
Per account: `balance Σ allocations`. Top-level on `/goals`: sum across linked savings. Label: "Unallocated."
## Defaults
Single goal on an account, no explicit allocation: `allocated_amount = NULL`, full balance counts.
Second goal added to the same account triggers a split prompt. Two goals: slider, defaults to 50/50. Three or more: numeric inputs summing to ≤ balance, defaults to equal split.
## Pace and projection
Pace is the rolling 90-day average of total linked-account balance change, weighted by allocation.
Accounts with less than 90 days of history use whatever's available, down to a 30-day minimum. The 30-day threshold is where short-term volatility (one payday, one large transfer) stops dominating the slope. Below 30 days: no projection, just the saved area.
Net negative growth over the window: projection line goes flat or down. Status reads "Behind."
## Manual accounts
A manual account works identically. The user maintains the balance; the goal follows.
## Un-link and delete
Un-link: allocation row removed, goal balance drops by the allocation amount.
Delete: prompt to re-link or remove the goals. No silent cascade.
## Gains and losses
- Loss: the "I saved $200 today" ritual.
- Gain: the goal balance never lies. Spend from savings, goal shrinks.
- Loss: the add-contribution modal and live impact preview.
- Gain: the projection chart reflects reality. Income drops, projection drops.
- Loss: roughly 30% of the v1 surface (contribution model, controller, views, Stimulus).
- Gain: no double-entry. What's in the bank is what's in the goal.
## What stays
Goal model, AASM states, index page, KPI strip, status pills, projection chart visual, color and icon picker, avatar component, AI tool, demo seed.

View File

@@ -1,75 +1,57 @@
# Goals architecture: account-linked, or what we have now?
# Goals: how the balance is computed
*Posted 2026-05-12. Tied to PR [#1757](https://github.com/we-promise/sure/pull/1757) on branch `feat/savings-goals`.*
A Discord thread with Juanjo and CrossDrain surfaced a question worth resolving before PR #1757 merges: does the Goals data model accurately reflect what it claims to track? The branch ships a working version, but the underlying shape may need to change before users see it.
A Goal is a target. The number on its page is the live balance of the savings accounts you link to it, minus what other goals have claimed from those same accounts.
This document is not an ADR. The repo doesn't run that practice. Treat it as a discussion doc. Opinions welcome.
That's it. No "log a contribution" step. No parallel ledger.
## What's on the branch
## What this looks like in practice
A Goal has a name, a target ("save $50K for a house"), one or more linked savings accounts, and a manual contribution log. Contributions live in their own table, separate from real transactions. Adding a contribution increments the goal balance but doesn't touch the bank balance or transaction list. The result is a parallel ledger.
You make a goal called House, target $50K. You link your Ally savings ($13K). The goal shows $13K, 26% to target.
The stepper currently displays the string "Balances in these accounts will count toward the goal." Under the current model, balances don't count. Only manually logged contributions count.
Two months later, your Ally savings has grown to $15K because you've been saving. The goal shows $15K, 30%. You did nothing in the app for that to happen.
Other gaps surface in extended use.
Three months later, you transfer $3K out for a car repair. The goal shows $12K. If you were on track before, the projection chart now reflects the setback.
Example: a user adds a $5K contribution toward a savings goal, then in a later month transfers $3K of that balance from savings to checking to pay rent. The goal continues to show $5K saved, even though the money is gone.
## When one account funds two goals
Account selection at goal creation appears to be a commitment, but functionally it only filters which accounts appear in the contribution dropdown later.
If you also want a Vacation goal funded from Ally, you'll be asked to split it. "Of Ally's $15K, how much for House, how much for Vacation?" Two sliders, or a list if you have three or more goals on the same account.
The question: is this the model to ship, or should it change first?
The split is stored as a dollar allocation per goal per account.
## The three shapes other apps use
## When the math doesn't work out
Personal-finance apps land in one of three answers to "what does a goal's balance represent."
If your allocations exceed your balance (House $10K, Vacation $6K, but Ally holds $13K), Sure shows two numbers per goal: "Allocated $10K · Backed by $8.13K." You see the gap and decide what to do: reduce, top up, or accept it.
**Account-linked.** The goal references one or more savings accounts and derives its balance from them. The number on the goal page is the live balance of the account. No manual logging. Monarch, Copilot, and the old Mint use this shape. When one account funds multiple goals, the account balance is split via explicit allocations: "$5K of this $20K is for House, $3K is for Vacation."
The split when over-allocated is pro-rata to allocation. No priority ordering in v1.
**Tag-based.** A contribution is a real transaction with a tag. When money moves into savings, the inflow transaction is tagged "Goal: House," and the goal's balance is the sum of tagged inflows. Closer to YNAB's envelope model, but built on Sure's existing tag system. This is the direction Juanjo proposed on Discord.
## What you give up
**Free-form ledger.** Goals have their own contribution table, decoupled from real transactions. This is the shape on the branch. Mainly seen in lower-end aggregators.
The act of logging a contribution. If that was the part of the feature you used, this model removes it. The replacement is watching your account grow.
## Trade-offs
## What you gain
Account-linked aligns the goal balance with the real account state. Nothing requires maintenance once the link is set up. Spending from a linked account reduces the goal balance automatically. The constraint is that a shared account requires an explicit allocation mechanism for splitting across goals.
The goal balance can't be a fiction. Spend from your savings, it shrinks. Save more, it grows. The system keeps you honest by construction.
Tag-based reuses an existing Sure primitive (tags) and ties goal progress to real money flow. Limitations: "earmarking $5K of an existing balance for House" isn't a transaction event but a snapshot, which tags don't model. Tags fit new inflows; they don't fit pre-existing balances. The tagging UX on the transaction side is itself a feature that doesn't exist today and would need to be built first.
## What stays
Free-form ledger has the lowest implementation cost. It is also the only shape of the three that does not reconcile with the user's actual account balances.
Status pills, projection chart, color and icon picker, AI assistant tool, demo data, every fix from the last week of work. The only thing changing is how the balance gets computed.
## Proposed direction
## What's deferred and why
Account-linked, applied before merge.
Priority ordering for the over-allocation split. Pro-rata is fair-share; priority would let users say "House first." A real feature, not in v1.
Migration cost: zero. The branch is pre-ship. No user data needs migration.
Tag-based contribution annotation (Juanjo's Discord proposal). Annotations on real transactions can layer on later without changing the model.
Code cost: mostly deletion. Drop the contribution model, the contribution controller and views, the live impact preview, and the parts of the stepper that assume contributions. What stays is the goal model itself, the projection chart (whose data source becomes account balance history, which Sure already tracks via the `balances` table), the status pills, the color and icon picker, the AI tool, and the demo seed.
Auto-fund from budget surplus. Was in the closed PR #1569. Belongs in a Budgets-aware follow-up.
Post-ship cost of the alternative: each user whose goal balance diverges from their account balance becomes a support question. A later migration off the parallel ledger is more expensive than a pre-ship change.
## Still being decided
A counter-argument exists for shipping the current model and iterating. The trade-off is that the contribution-logging UX requires the user to manually keep two systems in sync.
The pace calculation window. 90-day rolling average is the proposal, with a 30-day minimum for short-history accounts.
## What stays the same either way
The split-prompt UX for the second-goal-on-an-account case. Slider vs. inputs, default proportions.
The decision is about how a goal's balance gets computed. The feature itself remains.
Whether manual accounts should carry any "this is a goal-only ledger" treatment, or just behave like any other account.
- The `/goals` page, KPI strip, ongoing / completed / archived sections.
- Status pills (`On track`, `Behind`, `Reached`, `Open`, `Paused`, `Archived`) and the `Goal#display_status` logic.
- Projection chart: same shape, same interactivity, same theme-aware repaint logic. Different data source.
- Color and icon picker (shared with Categories).
- Avatar component with its light-mode contrast adjustments.
- AI assistant tool (`create_goal`).
- 7-goal demo seed.
- Recent fixes (chart morph survival, label collision, picker popup, etc.).
## Open questions
Conditional on the account-linked direction:
1. When a single account is linked to a single goal, should the default be "the whole balance counts" (simple, but ambiguous when the account is later shared), or require an explicit allocation amount upfront?
2. When goals total more than the linked savings (e.g. $30K of goals against $20K of savings), should the system hard-block, show a soft warning, or stay silent?
3. The stepper's current optional "starting contribution" step does not map to the new model. Options: replace it with an optional "earmark $X of <selected account> right now" step, or drop step 2's disclosure entirely.
4. Should the schema leave a forward-compatible hook (e.g. a nullable `transaction_id` on the contributions-replacement table) so tags can layer in later without a schema rewrite?
The PR is on hold until the model decision lands.
Open for feedback. The PR is on hold until the model is settled. Engineering mechanics in the [companion mechanics doc](goals-architecture-mechanics.md).