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.
- spendsignal-0.3.0/.gitignore +54 -0
- spendsignal-0.3.0/CHANGELOG.md +97 -0
- spendsignal-0.3.0/LICENSE +21 -0
- spendsignal-0.3.0/PKG-INFO +390 -0
- spendsignal-0.3.0/README.md +352 -0
- spendsignal-0.3.0/fixtures/README.md +163 -0
- spendsignal-0.3.0/pyproject.toml +126 -0
- spendsignal-0.3.0/schemas/evidence-summary.schema.json +144 -0
- spendsignal-0.3.0/schemas/feedback-event.schema.json +122 -0
- spendsignal-0.3.0/schemas/purchase-event.schema.json +127 -0
- spendsignal-0.3.0/schemas/reflection-exposure.schema.json +97 -0
- spendsignal-0.3.0/src/spendsignal/__init__.py +48 -0
- spendsignal-0.3.0/src/spendsignal/adapters/__init__.py +20 -0
- spendsignal-0.3.0/src/spendsignal/adapters/actual_budget.py +179 -0
- spendsignal-0.3.0/src/spendsignal/adapters/extract.py +272 -0
- spendsignal-0.3.0/src/spendsignal/adapters/match_deterministic.py +227 -0
- spendsignal-0.3.0/src/spendsignal/adapters/mcp_server.py +301 -0
- spendsignal-0.3.0/src/spendsignal/adapters/retrieve_nn.py +131 -0
- spendsignal-0.3.0/src/spendsignal/aggregate.py +267 -0
- spendsignal-0.3.0/src/spendsignal/cli.py +275 -0
- spendsignal-0.3.0/src/spendsignal/models.py +253 -0
- spendsignal-0.3.0/src/spendsignal/py.typed +0 -0
- spendsignal-0.3.0/src/spendsignal/storage.py +61 -0
- spendsignal-0.3.0/tests/__init__.py +0 -0
- spendsignal-0.3.0/tests/test_adapter_actual_budget.py +98 -0
- spendsignal-0.3.0/tests/test_adapter_extract.py +155 -0
- spendsignal-0.3.0/tests/test_adapter_match_deterministic.py +208 -0
- spendsignal-0.3.0/tests/test_adapter_mcp_server.py +115 -0
- spendsignal-0.3.0/tests/test_adapter_retrieve_nn.py +309 -0
- spendsignal-0.3.0/tests/test_aggregate.py +676 -0
- spendsignal-0.3.0/tests/test_cli.py +287 -0
- spendsignal-0.3.0/tests/test_models.py +388 -0
- spendsignal-0.3.0/tests/test_smoke.py +25 -0
- 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).
|