spendsignal 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. spendsignal-0.3.0/.gitignore +54 -0
  2. spendsignal-0.3.0/CHANGELOG.md +97 -0
  3. spendsignal-0.3.0/LICENSE +21 -0
  4. spendsignal-0.3.0/PKG-INFO +390 -0
  5. spendsignal-0.3.0/README.md +352 -0
  6. spendsignal-0.3.0/fixtures/README.md +163 -0
  7. spendsignal-0.3.0/pyproject.toml +126 -0
  8. spendsignal-0.3.0/schemas/evidence-summary.schema.json +144 -0
  9. spendsignal-0.3.0/schemas/feedback-event.schema.json +122 -0
  10. spendsignal-0.3.0/schemas/purchase-event.schema.json +127 -0
  11. spendsignal-0.3.0/schemas/reflection-exposure.schema.json +97 -0
  12. spendsignal-0.3.0/src/spendsignal/__init__.py +48 -0
  13. spendsignal-0.3.0/src/spendsignal/adapters/__init__.py +20 -0
  14. spendsignal-0.3.0/src/spendsignal/adapters/actual_budget.py +179 -0
  15. spendsignal-0.3.0/src/spendsignal/adapters/extract.py +272 -0
  16. spendsignal-0.3.0/src/spendsignal/adapters/match_deterministic.py +227 -0
  17. spendsignal-0.3.0/src/spendsignal/adapters/mcp_server.py +301 -0
  18. spendsignal-0.3.0/src/spendsignal/adapters/retrieve_nn.py +131 -0
  19. spendsignal-0.3.0/src/spendsignal/aggregate.py +267 -0
  20. spendsignal-0.3.0/src/spendsignal/cli.py +275 -0
  21. spendsignal-0.3.0/src/spendsignal/models.py +253 -0
  22. spendsignal-0.3.0/src/spendsignal/py.typed +0 -0
  23. spendsignal-0.3.0/src/spendsignal/storage.py +61 -0
  24. spendsignal-0.3.0/tests/__init__.py +0 -0
  25. spendsignal-0.3.0/tests/test_adapter_actual_budget.py +98 -0
  26. spendsignal-0.3.0/tests/test_adapter_extract.py +155 -0
  27. spendsignal-0.3.0/tests/test_adapter_match_deterministic.py +208 -0
  28. spendsignal-0.3.0/tests/test_adapter_mcp_server.py +115 -0
  29. spendsignal-0.3.0/tests/test_adapter_retrieve_nn.py +309 -0
  30. spendsignal-0.3.0/tests/test_aggregate.py +676 -0
  31. spendsignal-0.3.0/tests/test_cli.py +287 -0
  32. spendsignal-0.3.0/tests/test_models.py +388 -0
  33. spendsignal-0.3.0/tests/test_smoke.py +25 -0
  34. spendsignal-0.3.0/tests/test_storage.py +141 -0
@@ -0,0 +1,54 @@
1
+ # macOS
2
+ .DS_Store
3
+ .AppleDouble
4
+ .LSOverride
5
+
6
+ # Editors
7
+ .idea/
8
+ .vscode/
9
+ *.swp
10
+ *.swo
11
+
12
+ # Python
13
+ __pycache__/
14
+ *.py[cod]
15
+ *$py.class
16
+ *.egg-info/
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+ .tox/
21
+ .coverage
22
+ htmlcov/
23
+ build/
24
+ dist/
25
+ .venv/
26
+ venv/
27
+
28
+ # Node
29
+ node_modules/
30
+ .pnpm-store/
31
+ npm-debug.log*
32
+ pnpm-debug.log*
33
+ yarn-debug.log*
34
+ yarn-error.log*
35
+
36
+ # Env / secrets
37
+ .env
38
+ .env.*
39
+ !.env.example
40
+
41
+ # Local artifacts / scratch
42
+ tmp/
43
+ scratch/
44
+ *.local
45
+
46
+ # Per-machine Claude Code settings (project-shared settings live in .claude/settings.json)
47
+ .claude/settings.local.json
48
+
49
+ # Internal working notes, scope research, and drafts kept locally, not published
50
+ .research/
51
+
52
+ # Logs
53
+ *.log
54
+ logs/
@@ -0,0 +1,97 @@
1
+ # Changelog
2
+
3
+ Notable changes to this project. Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ---
6
+
7
+ ## [Unreleased]
8
+
9
+ _No changes yet._
10
+
11
+ ---
12
+
13
+ ## [0.3.0] — 2026-08-20
14
+
15
+ Phase 2 complete and Phase 3 shipped in the same release.
16
+
17
+ ### Added
18
+
19
+ - `spendsignal.adapters.extract` - receipt text → PurchaseEvent. `ExtractionProvider` protocol. `MockExtractionProvider` (deterministic, default). `TextParserProvider` (stdlib regex, best-effort). `Invoice2DataProvider` optional under `spendsignal[extract]`. 13 tests.
20
+ - `spendsignal.adapters.mcp_server` - MCP server exposing `summarize_subject`, `retrieve_similar`, `match_receipts` as MCP tools. `spendsignal serve` CLI subcommand. Compatible with Claude Desktop, Continue, Cursor. Under `spendsignal[mcp]`. 5 tests against the tool functions directly (no transport needed).
21
+ - `spendsignal.adapters.actual_budget` - pulls `BankTransaction` records from Actual Budget. `MockActualProvider` (no creds). `ActualBudgetProvider` wraps `actualpy` under `spendsignal[actual]`. 8 tests including import guard.
22
+ - `pyproject.toml` extras: `extract = ["invoice2data>=1.0"]`, `mcp = ["mcp>=1.0"]`, `actual = ["actualpy>=0.20"]`, `all` updated.
23
+ - `spendsignal serve` CLI subcommand added.
24
+ - `README.md` updated: extraction, serve, and Actual Budget adapter sections. Status updated to Phase 2 + 3 complete.
25
+ - `ROADMAP.md` Phase 3 checked off. Phase 4 section added.
26
+
27
+ ### Changed
28
+
29
+ - Version bumped from `0.2.0` to `0.3.0`.
30
+
31
+ ---
32
+
33
+ ## [0.2.0] — 2026-08-20
34
+
35
+ Phase 2 adapters: matching, retrieval, and the CLI surface that exposes them.
36
+
37
+ ### Added
38
+
39
+ - `ADR-0005` — adapter architecture. Adapters live under `src/spendsignal/adapters/`, define their own local pydantic types, and may not modify core schemas or the `summarize` API without a superseding ADR. Optional deps live under `[project.optional-dependencies]` extras named after the adapter (`D-007`, `D-008`).
40
+ - `src/spendsignal/adapters/` package with two adapters: `match_deterministic` and `retrieve_nn`. Deterministic receipt-to-transaction matcher using amount + date + fuzzy-merchant scoring. Amount and date are hard gates; merchant is a soft ranking signal. Greedy highest-confidence assignment. Stdlib only (no new runtime deps).
41
+ - `spendsignal.adapters.match_deterministic.BankTransaction`, `Receipt`, `MatchResult` - adapter-local pydantic types.
42
+ - `tests/test_adapter_match_deterministic.py` - 14 tests covering exact match, amount / date tolerances, hard-gate behavior when amount or date is out of range, merchant fuzzy scoring, greedy assignment, currency mismatch, min_confidence threshold, deterministic output regardless of input order, and empty inputs.
43
+ - `fixtures/transactions.jsonl` - 4 bank transactions. Three pair with the existing Costco receipts; a fourth (SHELL) stays unmatched to exercise the negative case.
44
+ - `fixtures/README.md` gains a matching-adapter demo section showing library use against the committed fixtures.
45
+ - `pyproject.toml` gains `[project.optional-dependencies]` with `match`, `extract`, `retrieval`, and `all` extras. Base install stays minimal.
46
+ - `spendsignal.storage.load_purchases` / `load_exposures` / `load_feedback` widened to accept `str | Path` for library ergonomics.
47
+ - `.github/ISSUE_TEMPLATE/reviewer_feedback.md` - structured template for Phase 2 reviewer feedback with sections for what worked, what did not, differentiation argument, 30-second scan, and OSS the prior-art scan may have missed.
48
+
49
+ ### Server-side (repo settings, no code change)
50
+
51
+ - GitHub Discussions enabled.
52
+ - Private vulnerability reporting enabled (referenced in `SECURITY.md`, now actually wired).
53
+ - Repo topics added: `personal-finance`, `local-first`, `receipts`, `python`, `personal-informatics`.
54
+
55
+ ---
56
+
57
+ ---
58
+
59
+ ## [0.1.0] — 2026-08-19
60
+
61
+ First public release. Ships the deterministic purchase-outcome evidence engine, four JSON contracts, a runnable CLI, and a committed synthetic Costco fixture.
62
+
63
+ ### Added
64
+
65
+ - Initial pre-v0.1 repository skeleton. Root docs: `README`, `LICENSE` (MIT), `CONTRIBUTING`, `SECURITY`, `CODE_OF_CONDUCT`, `CHANGELOG`, `ROADMAP`, `OPEN_ITEMS`, `DECISIONS`, `AGENTS`, `CLAUDE`, `.env.example`, `.gitignore`. GitHub templates: issue (bug, feature, discussion) and PR.
66
+ - `ADR-0001` — project name (`SpendSignal`) and public positioning language locked (`D-001`, `D-002`).
67
+ - `ADR-0002` — v0.1 module boundary locked as the purchase-outcome evidence engine, framing A (`D-003`). v0.2 direction reserved for framing B as adapters that feed the same engine (`D-004`).
68
+ - `ADR-0003` — language and stack locked as Python 3.12+ with `uv`, `pydantic` 2, `pytest`, `mypy`, and `ruff` (`D-005`).
69
+ - `ADR-0004` — license confirmed as MIT (`D-006`).
70
+ - `pyproject.toml` with PEP 621 metadata, hatchling build backend, dev dependency group.
71
+ - `src/spendsignal/` package skeleton with `__init__.py`, `cli.py`, and `py.typed` marker.
72
+ - `schemas/purchase-event.schema.json` - first of four v0.1 event contracts, JSON Schema Draft 2020-12.
73
+ - `schemas/reflection-exposure.schema.json` - records that the user was asked for feedback about a purchase or line item. Needed to detect sampling bias.
74
+ - `schemas/feedback-event.schema.json` - one axis of retrospective feedback per event. Append-only, revisable via supersession. Discriminated `value` union: bool, likert_5, categorical.
75
+ - `schemas/evidence-summary.schema.json` - deterministic output shape for the summarize query. Explicit abstention when evidence is insufficient.
76
+ - `src/spendsignal/models.py` - pydantic 2 models for all four contracts. `extra=forbid`. `FeedbackValue` is a discriminated union.
77
+ - `src/spendsignal/aggregate.py` - deterministic point-in-time aggregator. Pure functions. `summarize(purchases, exposures, feedback, subject, as_of)` returns an `EvidenceSummary`. Honors supersession only within scope, excludes whole-purchase feedback from line-scoped queries, and explicitly abstains with one of three reasons when evidence is insufficient.
78
+ - `src/spendsignal/storage.py` - JSONL load helpers (`load_purchases`, `load_exposures`, `load_feedback`). Blank lines are skipped. Errors report `path:line_number` for both JSON decode failures and pydantic validation failures.
79
+ - `src/spendsignal/cli.py` - `spendsignal summarize --purchases X --exposures Y --feedback Z --subject S --as-of T [--now T]`. Reads three JSONL files, produces an indented JSON `EvidenceSummary` on stdout. Requires timezone-aware `--as-of`. Exit code 2 on file / parse / validation errors; 0 on success.
80
+ - `spendsignal.summarize` and the main model types re-exported at package top level.
81
+ - `fixtures/` - committed synthetic Costco basket. 3 purchases, 6 reflection exposures (one deliberately skipped), 12 feedback events (including two supersession pairs). Exercises all four ADR-0002 acceptance properties: mixed outcomes in the same basket, later-corrected label, skipped reflection, item with no feedback.
82
+ - `scripts/generate_fixtures.py` - deterministic regenerator for the fixtures. Same generator + same inputs produces byte-identical JSONL.
83
+ - `fixtures/README.md` - explains the scenario, lists five interesting `summarize` queries, and documents how to regenerate.
84
+ - `docs/what-is-spendsignal.md` - plain-English overview for non-technical readers. Concrete Costco example. Explains the personal informatics and just-in-time adaptive intervention research areas that shape the design.
85
+ - `tests/test_smoke.py`, `tests/test_models.py`, `tests/test_aggregate.py`, `tests/test_storage.py`, `tests/test_cli.py` - 54 tests total. Under 1 second locally.
86
+ - `.github/workflows/ci.yml` runs `ruff check`, `ruff format --check`, `mypy` strict, and `pytest` on Python 3.12 and 3.13.
87
+ - `.python-version` pins the default local Python to 3.12.
88
+ - README rewritten as a real quick-start with clone / venv / pip commands, full CLI examples, verbatim JSON output, point-in-time demo, and abstention demo.
89
+
90
+ ### Changed
91
+
92
+ - Phase 0 (scope pinning) closed. Phase 1 (v0.1 shippable) closed with this release.
93
+
94
+ [Unreleased]: https://github.com/YemaneSG/SpendSignal/compare/v0.3.0...HEAD
95
+ [0.3.0]: https://github.com/YemaneSG/SpendSignal/compare/v0.2.0...v0.3.0
96
+ [0.2.0]: https://github.com/YemaneSG/SpendSignal/compare/v0.1.0...v0.2.0
97
+ [0.1.0]: https://github.com/YemaneSG/SpendSignal/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yemane and SpendSignal contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,390 @@
1
+ Metadata-Version: 2.5
2
+ Name: spendsignal
3
+ Version: 0.3.0
4
+ Summary: A small local-first library for logging line-item purchases with your own retrospective outcomes, and retrieving that history when you're about to buy something similar.
5
+ Project-URL: Homepage, https://github.com/YemaneSG/SpendSignal
6
+ Project-URL: Issues, https://github.com/YemaneSG/SpendSignal/issues
7
+ Project-URL: Source, https://github.com/YemaneSG/SpendSignal
8
+ Project-URL: Changelog, https://github.com/YemaneSG/SpendSignal/blob/main/CHANGELOG.md
9
+ Author: Yemane
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: evidence,local-first,personal-finance,personal-informatics,purchase-history,receipts
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Office/Business :: Financial
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: pydantic<3,>=2.5
25
+ Provides-Extra: actual
26
+ Requires-Dist: actualpy>=0.20; extra == 'actual'
27
+ Provides-Extra: all
28
+ Requires-Dist: actualpy>=0.20; extra == 'all'
29
+ Requires-Dist: invoice2data>=1.0; extra == 'all'
30
+ Requires-Dist: mcp>=1.0; extra == 'all'
31
+ Provides-Extra: extract
32
+ Requires-Dist: invoice2data>=1.0; extra == 'extract'
33
+ Provides-Extra: match
34
+ Provides-Extra: mcp
35
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
36
+ Provides-Extra: retrieval
37
+ Description-Content-Type: text/markdown
38
+
39
+ # SpendSignal
40
+
41
+ A small local-first library for logging line-item purchases with your own retrospective outcomes, and retrieving that history when you're about to buy something similar.
42
+
43
+ `v0.3` (dev). Runs on synthetic data. No cloud, no accounts, no API keys.
44
+
45
+ Non-technical reader? Start with [docs/what-is-spendsignal.md](docs/what-is-spendsignal.md) - a plain-English walkthrough with a concrete example and the research ideas behind the design.
46
+
47
+ ---
48
+
49
+ ## Try it in 5 minutes
50
+
51
+ Requires Python 3.12+.
52
+
53
+ ```bash
54
+ git clone git@github.com:YemaneSG/SpendSignal.git
55
+ cd SpendSignal
56
+ python3 -m venv .venv && source .venv/bin/activate
57
+ pip install .
58
+
59
+ # Given my three past protein-powder purchases, what does my current view say?
60
+ spendsignal summarize \
61
+ --purchases fixtures/purchases.jsonl \
62
+ --exposures fixtures/exposures.jsonl \
63
+ --feedback fixtures/feedback.jsonl \
64
+ --subject protein-powder \
65
+ --as-of 2026-12-01T00:00:00+00:00
66
+ ```
67
+
68
+ If you have `uv`, replace the `venv` + `pip install` steps with `uv sync`.
69
+
70
+ Real output:
71
+
72
+ ```json
73
+ {
74
+ "schema_version": "1",
75
+ "generated_at": "2026-12-01T00:00:00Z",
76
+ "query": {"subject": "protein-powder", "as_of": "2026-12-01T00:00:00Z"},
77
+ "coverage": {
78
+ "comparable_purchase_count": 3,
79
+ "exposure_count": 3,
80
+ "feedback_count": 6
81
+ },
82
+ "distributions": [
83
+ {"axis": "worth_it", "count": 3, "distribution": {"5": 2, "4": 1}},
84
+ {"axis": "would_buy_again", "count": 3, "distribution": {"true": 3}}
85
+ ],
86
+ "contradictions": [],
87
+ "revisions": 0,
88
+ "contributing_event_ids": [
89
+ "fb-p1-again", "fb-p1-worth",
90
+ "fb-p2-again", "fb-p2-worth",
91
+ "fb-p3-again", "fb-p3-worth"
92
+ ],
93
+ "abstention": null
94
+ }
95
+ ```
96
+
97
+ Three past purchases, six feedback events, no contradictions, no revisions. Every event ID supporting the summary is enumerated. Nothing invented.
98
+
99
+ ---
100
+
101
+ ## What SpendSignal does
102
+
103
+ - Logs line items with typed retrospective labels (`would_buy_again`, `worth_it`, `still_using`, `returned`, `regretted`, `planned`, ...).
104
+ - Runs locally on synthetic data. No cloud, no accounts, no API keys required for the demo.
105
+ - Deterministic first, LLM second. Financial truth lives in typed data structures. LLMs interpret.
106
+ - Interoperates with mature tools (Actual Budget for the ledger, existing OCR libraries for image ingest, MCP for assistant surfaces) instead of replacing them.
107
+
108
+ ## What SpendSignal is not
109
+
110
+ - Not a budgeting app. No envelopes, no forecasts, no net worth.
111
+ - Not an accounting engine. Use Actual, hledger, or beancount.
112
+ - Not an AI financial advisor. Nothing here recommends stocks or predicts markets.
113
+ - Not a receipt-photo app. Ingest lives at the edges. This library is what happens after the receipt is text.
114
+
115
+ ---
116
+
117
+ ## The loop
118
+
119
+ ```
120
+ bank transaction
121
+ → matched receipt
122
+ → individual line items
123
+ → your retrospective labels (would_buy_again, worth_it, still_using,
124
+ returned, regretted, planned, ...)
125
+ → a queryable log of what actually happened
126
+ → retrieval when you're about to buy something similar
127
+ → new outcome feedback
128
+ ```
129
+
130
+ Retrieval over your own past, not prediction. The tool does not decide whether you should buy something. You do.
131
+
132
+ ---
133
+
134
+ ## Point-in-time is real
135
+
136
+ The same query at an earlier `--as-of` returns the state as of that date. In the fixture, the wireless-headphones feedback was initially positive in July and revised to negative in October via supersession. Ask "as of August" and you get the pre-revision state.
137
+
138
+ ```bash
139
+ # Latest view - both labels revised, revisions=2
140
+ spendsignal summarize \
141
+ --purchases fixtures/purchases.jsonl \
142
+ --exposures fixtures/exposures.jsonl \
143
+ --feedback fixtures/feedback.jsonl \
144
+ --subject wireless-headphones \
145
+ --as-of 2026-12-01T00:00:00+00:00
146
+ # → distributions: worth_it {1: 1}, would_buy_again {false: 1}, revisions: 2
147
+
148
+ # Earlier view - original opinion, revisions=0
149
+ spendsignal summarize \
150
+ --purchases fixtures/purchases.jsonl \
151
+ --exposures fixtures/exposures.jsonl \
152
+ --feedback fixtures/feedback.jsonl \
153
+ --subject wireless-headphones \
154
+ --as-of 2026-08-01T00:00:00+00:00
155
+ # → distributions: worth_it {4: 1}, would_buy_again {true: 1}, revisions: 0
156
+ ```
157
+
158
+ History is append-only. Old opinions are preserved next to new ones. The past does not change.
159
+
160
+ ---
161
+
162
+ ## Explicit abstention when evidence is insufficient
163
+
164
+ SpendSignal refuses to invent a score when it has none. Ask about a subject with a comparable purchase but no feedback and you get an explicit refusal with a reason and a still-populated coverage count.
165
+
166
+ ```bash
167
+ spendsignal summarize \
168
+ --purchases fixtures/purchases.jsonl \
169
+ --exposures fixtures/exposures.jsonl \
170
+ --feedback fixtures/feedback.jsonl \
171
+ --subject chicken \
172
+ --as-of 2026-12-01T00:00:00+00:00
173
+ ```
174
+
175
+ ```json
176
+ {
177
+ "coverage": {"comparable_purchase_count": 1, "exposure_count": 1, "feedback_count": 0},
178
+ "distributions": [],
179
+ "revisions": 0,
180
+ "contributing_event_ids": [],
181
+ "abstention": {
182
+ "reason": "no_feedback",
183
+ "detail": "Comparable purchases exist but no feedback has been recorded on them."
184
+ }
185
+ }
186
+ ```
187
+
188
+ Three abstention reasons are possible - `no_comparable_purchases`, `no_feedback`, `no_feedback_within_scope` - each with a human-readable `detail`.
189
+
190
+ ---
191
+
192
+ ## Adapters
193
+
194
+ Two adapters ship in Phase 2, building on the same evidence contracts. Both stdlib-only — no extra dependencies.
195
+
196
+ ### `spendsignal extract` — receipt text → PurchaseEvent
197
+
198
+ Convert raw receipt text into a `PurchaseEvent`. Ships with a deterministic mock (default) and a stdlib regex `TextParserProvider`. `Invoice2DataProvider` available under `pip install spendsignal[extract]`.
199
+
200
+ ```python
201
+ from spendsignal.adapters.extract import TextParserProvider, extract
202
+
203
+ ev = extract("COSTCO\n01/15/2026\nProtein powder $49.99\nTotal $49.99", "receipt-001")
204
+ # PurchaseEvent with line_items and comparison_keys
205
+
206
+ ev = extract("receipt text", "r1") # MockExtractionProvider by default — CI-safe
207
+ ```
208
+
209
+ ---
210
+
211
+ ### `spendsignal retrieve` — "have I bought anything like this?"
212
+
213
+ Fuzzy-matches a free-text query against all known `comparison_key`s in your purchase history, then returns each matched subject's `EvidenceSummary`.
214
+
215
+ ```bash
216
+ spendsignal retrieve \
217
+ --purchases fixtures/purchases.jsonl \
218
+ --exposures fixtures/exposures.jsonl \
219
+ --feedback fixtures/feedback.jsonl \
220
+ --query "protein shake" \
221
+ --as-of 2026-12-01T00:00:00+00:00 \
222
+ --top-n 3
223
+ ```
224
+
225
+ Returns a JSON array. Each element has `comparison_key`, `similarity`, and a full `EvidenceSummary`. Abstentions pass through - if a match has no feedback yet, the result still surfaces it with `abstention.reason = "no_feedback"` rather than silently dropping it.
226
+
227
+ Hyphens and spaces normalised: `"protein shake"` matches `"protein-powder"` near 1.0.
228
+
229
+ ### `spendsignal match` — pair bank transactions with receipts
230
+
231
+ Deterministic amount + date + fuzzy merchant scoring. Amount and date are hard gates; merchant is a soft ranking signal. Greedy highest-confidence assignment. One-to-one.
232
+
233
+ ```bash
234
+ spendsignal match \
235
+ --transactions fixtures/transactions.jsonl \
236
+ --purchases fixtures/purchases.jsonl
237
+ ```
238
+
239
+ ```
240
+ txn-2026-08-15-costco <-> costco-3 confidence=1.0 (exact amount, date, merchant)
241
+ txn-2026-06-15-costco <-> costco-2 confidence=0.9118 (exact amount, date, fuzzy merchant)
242
+ txn-2026-01-15-costco <-> costco-1 confidence=0.7667 (exact amount, 1-day gap, fuzzy merchant)
243
+ ```
244
+
245
+ The SHELL transaction has no matching receipt and stays unmatched.
246
+
247
+ ### `spendsignal serve` — MCP server
248
+
249
+ Exposes `summarize_subject`, `retrieve_similar`, and `match_receipts` as MCP tools. Compatible with Claude Desktop, Continue, Cursor, and any MCP-capable LLM host.
250
+
251
+ ```bash
252
+ pip install spendsignal[mcp]
253
+
254
+ spendsignal serve \
255
+ --purchases fixtures/purchases.jsonl \
256
+ --exposures fixtures/exposures.jsonl \
257
+ --feedback fixtures/feedback.jsonl
258
+ ```
259
+
260
+ Add to your Claude Desktop `claude_desktop_config.json`:
261
+
262
+ ```json
263
+ {
264
+ "mcpServers": {
265
+ "spendsignal": {
266
+ "command": "spendsignal",
267
+ "args": [
268
+ "serve",
269
+ "--purchases", "/path/to/purchases.jsonl",
270
+ "--exposures", "/path/to/exposures.jsonl",
271
+ "--feedback", "/path/to/feedback.jsonl"
272
+ ]
273
+ }
274
+ }
275
+ }
276
+ ```
277
+
278
+ Then ask Claude: *"What does my purchase history say about protein powder?"*
279
+
280
+ ### Actual Budget integration
281
+
282
+ Pull bank transactions directly from a running Actual Budget instance for use with the matching adapter.
283
+
284
+ ```python
285
+ from spendsignal.adapters.actual_budget import ActualBudgetProvider, load_actual_transactions
286
+
287
+ # Mock — no credentials needed (default for demos and CI)
288
+ txns = load_actual_transactions()
289
+
290
+ # Real Actual Budget (requires pip install spendsignal[actual])
291
+ provider = ActualBudgetProvider(
292
+ server_url="http://localhost:5006",
293
+ password="your-password",
294
+ budget_id="your-budget-id",
295
+ )
296
+ txns = load_actual_transactions(provider=provider)
297
+ ```
298
+
299
+ ### Library use
300
+
301
+ ```python
302
+ from spendsignal.adapters.extract import TextParserProvider, extract
303
+ from spendsignal.adapters.retrieve_nn import retrieve
304
+ from spendsignal.adapters.match_deterministic import BankTransaction, Receipt, match
305
+ from spendsignal.adapters.actual_budget import load_actual_transactions
306
+ ```
307
+
308
+ See [`src/spendsignal/adapters/`](src/spendsignal/adapters/) and [`fixtures/README.md`](fixtures/README.md) for usage examples.
309
+
310
+ ---
311
+
312
+ ## Library use
313
+
314
+ `spendsignal.summarize` is a pure function that takes iterables of validated pydantic models and returns an `EvidenceSummary`.
315
+
316
+ ```python
317
+ from datetime import UTC, datetime
318
+
319
+ from spendsignal import summarize
320
+ from spendsignal.storage import load_exposures, load_feedback, load_purchases
321
+
322
+ summary = summarize(
323
+ purchases=load_purchases("fixtures/purchases.jsonl"),
324
+ exposures=load_exposures("fixtures/exposures.jsonl"),
325
+ feedback=load_feedback("fixtures/feedback.jsonl"),
326
+ subject="protein-powder",
327
+ as_of=datetime(2026, 12, 1, tzinfo=UTC),
328
+ )
329
+ print(summary.model_dump_json(indent=2))
330
+ ```
331
+
332
+ Same inputs always produce byte-identical output. `generated_at` defaults to `as_of` for reproducibility - pass `now=` when a real wall clock matters.
333
+
334
+ ---
335
+
336
+ ## Contracts
337
+
338
+ Four JSON schemas under [`schemas/`](schemas/) define the cross-language wire format.
339
+
340
+ | Schema | What it carries |
341
+ |---|---|
342
+ | [`purchase-event`](schemas/purchase-event.schema.json) | Source-neutral purchase record with optional line items and caller-supplied comparison keys. |
343
+ | [`reflection-exposure`](schemas/reflection-exposure.schema.json) | Records that the user was asked for feedback about a purchase or line item. Selection reason and policy version enable bias detection. |
344
+ | [`feedback-event`](schemas/feedback-event.schema.json) | One axis of retrospective feedback per event. Append-only; revisions use `supersedes_event_id`. Discriminated `value`: `bool` / `likert_5` / `categorical`. |
345
+ | [`evidence-summary`](schemas/evidence-summary.schema.json) | Deterministic output shape. Coverage, per-axis distributions, contradictions, revisions, contributing event IDs, explicit abstention. |
346
+
347
+ JSON Schema Draft 2020-12. Python consumers use the pydantic mirrors in [`src/spendsignal/models.py`](src/spendsignal/models.py). Other languages read the schemas directly.
348
+
349
+ ---
350
+
351
+ ## Development
352
+
353
+ ```bash
354
+ python3 -m venv .venv && source .venv/bin/activate
355
+ pip install -e .
356
+ pip install ruff pytest hypothesis mypy # dev tools; or `uv sync` if you have uv
357
+
358
+ pytest -q # 113 tests, under 1 second
359
+ ruff check .
360
+ ruff format --check .
361
+ mypy
362
+ ```
363
+
364
+ Regenerate the fixtures:
365
+
366
+ ```bash
367
+ python scripts/generate_fixtures.py
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Status
373
+
374
+ Phase 2 and Phase 3 complete. Five adapters shipped: `match_deterministic`, `retrieve_nn`, `extract`, `mcp_server`, `actual_budget`. See [`ROADMAP.md`](ROADMAP.md) for what's next.
375
+
376
+ Design decisions are recorded as ADRs in [`docs/decisions/`](docs/decisions/). The load-bearing one is [`ADR-0002`](docs/decisions/ADR-0002-v0.1-module-boundary.md) - the v0.1 module boundary.
377
+
378
+ ---
379
+
380
+ ## License
381
+
382
+ MIT. See [LICENSE](LICENSE) and [ADR-0004](docs/decisions/ADR-0004-license.md).
383
+
384
+ ## Contributing
385
+
386
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
387
+
388
+ ## Security
389
+
390
+ See [SECURITY.md](SECURITY.md).