Specification · purchases @ 0.1

purchases — What You Buy · Area Specification

last conformance run: 4/4 scenarios pass · 15 acts mapped 2026-09-07 02:01 UTC

SpecActsInvariantsScenarios

Spec

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.

1. Calibration: who this is for

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.

2. Scope

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.

3. Entities and lifecycles

4. The acts

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.

5. Invariants

6. Required read models

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).

7. Contract vs freedom

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.

8. Conformance (scenarios)

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.

9. Deferred — with reasons

ItemWhy deferred, not rejected
Bank connections / feedsFiles 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 conversionSame whole-area concern as everywhere; sums stay per currency.
Budgets, forecasts, adviceSaybooks records; it does not counsel. A budget is policy the person owns.
Business expense treatmentTax 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.

Acts

ActKindRequired
import_statementwritename hash kind currency period_start period_end opening_balance closing_balance row_count rows
review_transactionwritetransaction status
set_vendorwritetransaction vendor
add_receiptwritename hash total currency date
match_receiptwritereceipt transaction
unmatch_receiptwritereceipt
declare_subscriptionwritevendor cadence amount currency
cancel_subscriptionwritesubscription reason
sourcesread
sourcereadsource
transactionsread
purchasesread
subscriptionsread
receiptsread
spendread
review_batchwriterows
vocabularyread
discard_sourcewritesource reason
rename_categorywritefrom to

Invariants

IdInvariant
P-1Money is integer minor units, signed; every source and transaction carries a currency; sums are per currency and never cross.
P-2Every transaction traces to a source: source id, row index, and the raw line as read.
P-3A 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-4The same source hash is refused a second time; rows already present are skipped and listed, never silently merged or duplicated.
P-5Nothing is invented: category, vendor and status are empty or unreviewed until an act with a reason sets them.
P-6A 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-7A 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-8Every write is a logged act with an actor; refusals are logged too.
P-9Files never live here: a source or a receipt is its hash, its metadata and what was read.

Scenarios

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.

a card statement imported whole, reconciled, never twice pass

P-2 provenance on every row, P-3 batch-or-nothing against control totals, P-4 the same hash refused.
#ActExpectWhy
1import_statementok rows_in=4 rows_skipped=0 reconciled=true
2import_statementrefused already importedP-4: same hash
3import_statementrefused do not reconcileP-3: a transposed amount — the rows sum to a different closing balance, the gap is named
4transactionsok count=4
5review_transactionok status="transfer"
6review_transactionok status="purchase" category="groceries"
7purchasesok count=1
8review_batchok reviewed=2 vendors_named=2 still_unreviewed=0P-5: many rows, one reasoned act, validated whole
9review_batchrefused money ina batch with one bad row is refused whole

overlapping statements: present rows skipped and listed, new rows in pass

P-4: overlap is reported, never merged silently, never duplicated.
#ActExpectWhy
1import_statementok
2import_statementok rows_in=1 rows_skipped=2
3transactionsok count=5
4discard_sourceok rows_removed=1P-2: a wrong read is discarded whole and imported again; nothing is edited in place
5transactionsok count=4

subscriptions: declared, confirmed, missed, lapsed pass

P-6: the record confirms a subscription; the module never assumes a charge.
#ActExpectWhy
1import_statementok
2vocabularyok unreviewed=4the agent reads the words in use before proposing
3review_transactionok status="recurring" vendor="Netflix" subscription="SUB-0001" subscription_declared=trueP-6: recurring with a vendor declares the subscription
4subscriptionsok count=1 active_total=1 missed_total=0
5subscriptionsok missed_total=0P-6 coverage: no statement covers September yet, so the September period is 'no statement yet', not missed
6import_statementokSeptember's statement arrives without a Netflix charge
7subscriptionsok missed_total=1September has not charged yet as of the 10th: one missed period, still active
8import_statementokOctober's statement, again without Netflix
9subscriptionsok lapsed_total=1two periods without a charge: lapsed, derived
10cancel_subscriptionok status="cancelled"

receipts: added, matched with a reason, mismatch refused, unmatched shown pass

P-7: one receipt, one transaction, a reasoned act; P-9 the file stays with the person.
#ActExpectWhy
1import_statementok
2add_receiptok matched=false
3add_receiptok
4match_receiptrefused totalP-7: 12.50 is not 89.00
5match_receiptok matched=true
6receiptsok count=1
7unmatch_receiptok matched=false