last conformance run: 4/4 scenarios pass · 15 acts mapped 2026-09-07 02:01 UTC
Status: 0.1-draft. A spec on the record before a line of module code, so the shape can be argued with. No module implements it yet.
One person, a household, or a very small business that wants to know what it buys, what it pays for every month without noticing, and whether it has the receipt. The facts arrive as files: bank and card statements, receipts. Nobody parses them here. The agent reads the file and states what it read; the module keeps the record, checks that the statement adds up, refuses what it cannot reconcile, and remembers where every number came from. Agent first: the import is a batch of rows the agent hands over, never a parser we maintain per bank.
In: statements as sources with control totals, transactions traced to their source row, review and classification, purchases, subscriptions declared and then confirmed by what actually charges, receipts matched to transactions, spend by category and month, per currency.
Out (§9): bank connections and feeds (files only, by design), currency conversion, budgets, forecasts, advice, tax treatment of purchases, paying anything.
Writes (11): import_statement, rename_category (one word changed on every row that carries it), discard_source (a wrong read thrown out whole, with a reason, its hash freed), review_transaction, review_batch (many rows, one reasoned act, validated whole), set_vendor, add_receipt, match_receipt, declare_subscription, cancel_subscription, unmatch_receipt. Reads (8): vocabulary (the statuses with their meaning, the person's categories and vendors — what an agent proposes from), sources, transactions, purchases, subscriptions, receipts, spend, source.
sources (what was imported, when, by whom, reconciled), transactions (by period, status, source), purchases (by vendor, category, month), subscriptions (with next expected charge, missed periods, derived status), receipts (matched and unmatched), spend (by category and by month, per currency), source (one source with its rows and the reconciliation).
Contract: batch-or-nothing with control totals, provenance on every row, per-currency sums, declared-then-confirmed subscriptions, matching as a reasoned act, hash-refused re-imports. Freedom: category vocabulary (the person's words), vendor alias rules, tolerances, how the agent reads a given bank's layout, which files count as receipts.
01 a card statement imported whole: control totals reconcile; a second import of the same hash refused; a transposed amount refused with the gap named · 02 overlapping statements: rows already present skipped and listed, new rows in · 03 subscriptions: declared from a purchase, confirmed by the next month's charge, missed when it does not come, lapsed after two · 04 receipts: added, matched with a reason, a mismatched total refused, unmatched shown.
| Item | Why deferred, not rejected |
|---|---|
| Bank connections / feeds | Files are the honest boundary: the person chooses what the agent sees, and every import is a deliberate act. A feed is a standing permission, which is a different product. |
| Currency conversion | Same whole-area concern as everywhere; sums stay per currency. |
| Budgets, forecasts, advice | Saybooks records; it does not counsel. A budget is policy the person owns. |
| Business expense treatment | Tax deductibility varies by country and status; recording what was bought is our part. |
| Reading receipts (OCR) | The agent reads; the module records what it says. No image pipeline here. |
---
Change log: 0.1-draft (2026-09-06) — drafted from Peter's and Pavan's "my purchases" idea, with the agent-first import discipline agreed the same day: no parsers, batch-or-nothing, control totals, provenance, refusals.
| Act | Kind | Required |
|---|---|---|
import_statement | write | name hash kind currency period_start period_end opening_balance closing_balance row_count rows |
review_transaction | write | transaction status |
set_vendor | write | transaction vendor |
add_receipt | write | name hash total currency date |
match_receipt | write | receipt transaction |
unmatch_receipt | write | receipt |
declare_subscription | write | vendor cadence amount currency |
cancel_subscription | write | subscription reason |
sources | read | — |
source | read | source |
transactions | read | — |
purchases | read | — |
subscriptions | read | — |
receipts | read | — |
spend | read | — |
review_batch | write | rows |
vocabulary | read | — |
discard_source | write | source reason |
rename_category | write | from to |
| Id | Invariant |
|---|---|
P-1 | Money is integer minor units, signed; every source and transaction carries a currency; sums are per currency and never cross. |
P-2 | Every transaction traces to a source: source id, row index, and the raw line as read. |
P-3 | A statement is accepted whole or not at all: the rows must reconcile to the printed opening balance, closing balance and row count, or the batch is refused with the gap named. |
P-4 | The same source hash is refused a second time; rows already present are skipped and listed, never silently merged or duplicated. |
P-5 | Nothing is invented: category, vendor and status are empty or unreviewed until an act with a reason sets them. |
P-6 | A subscription is declared, then confirmed by the record: a period with no matching charge is missed, two make it lapsed; a charge is never assumed. |
P-7 | A receipt matches at most one transaction and vice versa; matching is a reasoned act, refused when the total or date does not fit unless overridden with a reason. |
P-8 | Every write is a logged act with an actor; refusals are logged too. |
P-9 | Files never live here: a source or a receipt is its hash, its metadata and what was read. |
Each scenario is a file of acts with expected outcomes. ok means the act must succeed with the listed fields; refused means the act must be refused with a sentence containing the listed text. Refusals are contract.
| # | Act | Expect | Why |
|---|---|---|---|
| 1 | import_statement | ok rows_in=4 rows_skipped=0 reconciled=true | |
| 2 | import_statement | refused already imported | P-4: same hash |
| 3 | import_statement | refused do not reconcile | P-3: a transposed amount — the rows sum to a different closing balance, the gap is named |
| 4 | transactions | ok count=4 | |
| 5 | review_transaction | ok status="transfer" | |
| 6 | review_transaction | ok status="purchase" category="groceries" | |
| 7 | purchases | ok count=1 | |
| 8 | review_batch | ok reviewed=2 vendors_named=2 still_unreviewed=0 | P-5: many rows, one reasoned act, validated whole |
| 9 | review_batch | refused money in | a batch with one bad row is refused whole |
| # | Act | Expect | Why |
|---|---|---|---|
| 1 | import_statement | ok | |
| 2 | import_statement | ok rows_in=1 rows_skipped=2 | |
| 3 | transactions | ok count=5 | |
| 4 | discard_source | ok rows_removed=1 | P-2: a wrong read is discarded whole and imported again; nothing is edited in place |
| 5 | transactions | ok count=4 |
| # | Act | Expect | Why |
|---|---|---|---|
| 1 | import_statement | ok | |
| 2 | vocabulary | ok unreviewed=4 | the agent reads the words in use before proposing |
| 3 | review_transaction | ok status="recurring" vendor="Netflix" subscription="SUB-0001" subscription_declared=true | P-6: recurring with a vendor declares the subscription |
| 4 | subscriptions | ok count=1 active_total=1 missed_total=0 | |
| 5 | subscriptions | ok missed_total=0 | P-6 coverage: no statement covers September yet, so the September period is 'no statement yet', not missed |
| 6 | import_statement | ok | September's statement arrives without a Netflix charge |
| 7 | subscriptions | ok missed_total=1 | September has not charged yet as of the 10th: one missed period, still active |
| 8 | import_statement | ok | October's statement, again without Netflix |
| 9 | subscriptions | ok lapsed_total=1 | two periods without a charge: lapsed, derived |
| 10 | cancel_subscription | ok status="cancelled" |
| # | Act | Expect | Why |
|---|---|---|---|
| 1 | import_statement | ok | |
| 2 | add_receipt | ok matched=false | |
| 3 | add_receipt | ok | |
| 4 | match_receipt | refused total | P-7: 12.50 is not 89.00 |
| 5 | match_receipt | ok matched=true | |
| 6 | receipts | ok count=1 | |
| 7 | unmatch_receipt | ok matched=false |