ADR 0011. External destinations are not spend

Status: accepted. Implemented 2026-08-24 in kevinvu184/up. Refines ADR 0005's category-spend model; same curation philosophy as ADR 0010. Amended same day: the counterparty list moved from a code constant into private data, and named institutions and figures were removed from this record — the code is public, the list is personal.

Context

After the transfer-exclusion fix, category spend still counted large outflows that are not consumption: movements of the owner's own money to accounts outside Up — savings banks, a broker, a super fund, including a recurring auto-invest that quietly inflated every routine month. These made a handful of months look like enormous spending when the real consumption was ordinary. Two alternatives were rejected: timing heuristics that match inflows to nearby outflows (ambiguous matches misclassify silently — the failure class this project is built against), and Up app tags (no webhook fires on tag edits and the poller re-reads only 30 days, so tags on historical rows never reach bronze without a history re-walk tool).

Decisions

Curation is private data, not public code

The list of external destinations reveals which institutions the owner banks and invests with, so it cannot live in a public file. Migration 12 creates curation_external_destinations (transaction_type, description) — an empty shape in public code — and the spend model anti-joins against it with NOT EXISTS. The rows are inserted through the SQLite panel and exist only in the private database. Adding a new external account is a row insert, no deploy; an empty table excludes nothing, so a remix starts neutral; a NULL transaction_type never matches, so untyped rows stay counted as spend. This supersedes the first implementation, a hardcoded predicate constant, which was equivalent in behaviour (verified: identical mart totals) but leaked the institution names into public code.

Deliberately kept as spend

Rent paid through agents, conveyancing, and two-way person-to-person shared-expense flows all stay counted. The person-to-person flows have money moving both directions; netting them properly is a separate problem for a future record if it matters.

Exclusion now, savings-flow mart later

The curation table defines precisely the outbound half of a savings-and-investment flow metric. When savings rate needs it, a gold_savings_flow model anti-joins the same table inverted — one shared definition, so the two surfaces can never disagree about what "external destination" means.

Consequences

Spend now measures consumption: mean and median monthly figures converge instead of the mean running several times the median, and the former outlier months resolve to ordinary values — most of what they held was money moving to savings. The cost is the usual curation trade: an outflow to a new, unlisted external destination counts as spend until a suspicious month prompts a row insert — the same accepted, visible failure mode as the income definition, and exactly how the original list was discovered. Personal specifics (institutions, counterparties, amounts) now live only in the private database, never in code or docs.