gilt-cli 0.1.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.
- gilt_cli-0.1.0/.claude/agents/python-qt-craftsperson.md +278 -0
- gilt_cli-0.1.0/.claude/settings.local.json +9 -0
- gilt_cli-0.1.0/.claude/skills/gilt/SKILL.md +315 -0
- gilt_cli-0.1.0/.claude/skills/gilt/references/command-reference.md +412 -0
- gilt_cli-0.1.0/.claude/skills/hone/SKILL.md +385 -0
- gilt_cli-0.1.0/.claude/skills/skill-writing/SKILL.md +260 -0
- gilt_cli-0.1.0/.github/workflows/ci.yml +24 -0
- gilt_cli-0.1.0/.github/workflows/release.yml +49 -0
- gilt_cli-0.1.0/.gitignore +49 -0
- gilt_cli-0.1.0/.hone-gates.json +19 -0
- gilt_cli-0.1.0/AGENTS.md +238 -0
- gilt_cli-0.1.0/CLAUDE.md +1 -0
- gilt_cli-0.1.0/LICENSE +21 -0
- gilt_cli-0.1.0/PKG-INFO +1212 -0
- gilt_cli-0.1.0/README.md +1180 -0
- gilt_cli-0.1.0/conftest.py +8 -0
- gilt_cli-0.1.0/docs/README.md +270 -0
- gilt_cli-0.1.0/docs/developer/architecture/data-model.md +367 -0
- gilt_cli-0.1.0/docs/developer/architecture/project-structure.md +262 -0
- gilt_cli-0.1.0/docs/developer/architecture/system-design.md +456 -0
- gilt_cli-0.1.0/docs/developer/index.md +312 -0
- gilt_cli-0.1.0/docs/developer/technical/auto-categorization.md +452 -0
- gilt_cli-0.1.0/docs/developer/technical/budgeting-system.md +576 -0
- gilt_cli-0.1.0/docs/developer/technical/duplicate-detection-ml-proposal.md +421 -0
- gilt_cli-0.1.0/docs/getting-started.md +202 -0
- gilt_cli-0.1.0/docs/index.md +83 -0
- gilt_cli-0.1.0/docs/user/cli/index.md +306 -0
- gilt_cli-0.1.0/docs/user/gui/importing.md +45 -0
- gilt_cli-0.1.0/docs/user/gui/index.md +324 -0
- gilt_cli-0.1.0/docs/user/index.md +178 -0
- gilt_cli-0.1.0/docs/user/installation.md +272 -0
- gilt_cli-0.1.0/docs/user/workflows/budget-tracking.md +668 -0
- gilt_cli-0.1.0/docs/user/workflows/initial-setup.md +522 -0
- gilt_cli-0.1.0/docs/user/workflows/monthly-review.md +536 -0
- gilt_cli-0.1.0/docs/user/workflows/upgrading-to-event-sourcing.md +177 -0
- gilt_cli-0.1.0/mkdocs.yml +94 -0
- gilt_cli-0.1.0/pyproject.toml +113 -0
- gilt_cli-0.1.0/src/gilt/__init__.py +7 -0
- gilt_cli-0.1.0/src/gilt/cli/__init__.py +1 -0
- gilt_cli-0.1.0/src/gilt/cli/app.py +803 -0
- gilt_cli-0.1.0/src/gilt/cli/command/__init__.py +11 -0
- gilt_cli-0.1.0/src/gilt/cli/command/accounts.py +73 -0
- gilt_cli-0.1.0/src/gilt/cli/command/audit_ml.py +307 -0
- gilt_cli-0.1.0/src/gilt/cli/command/auto_categorize.py +344 -0
- gilt_cli-0.1.0/src/gilt/cli/command/backfill_events.py +304 -0
- gilt_cli-0.1.0/src/gilt/cli/command/budget.py +184 -0
- gilt_cli-0.1.0/src/gilt/cli/command/categories.py +151 -0
- gilt_cli-0.1.0/src/gilt/cli/command/categorize.py +363 -0
- gilt_cli-0.1.0/src/gilt/cli/command/category.py +295 -0
- gilt_cli-0.1.0/src/gilt/cli/command/diagnose_categories.py +179 -0
- gilt_cli-0.1.0/src/gilt/cli/command/duplicates.py +421 -0
- gilt_cli-0.1.0/src/gilt/cli/command/ingest.py +171 -0
- gilt_cli-0.1.0/src/gilt/cli/command/init.py +126 -0
- gilt_cli-0.1.0/src/gilt/cli/command/mark_duplicate.py +262 -0
- gilt_cli-0.1.0/src/gilt/cli/command/migrate_to_events.py +274 -0
- gilt_cli-0.1.0/src/gilt/cli/command/note.py +243 -0
- gilt_cli-0.1.0/src/gilt/cli/command/prompt_stats.py +171 -0
- gilt_cli-0.1.0/src/gilt/cli/command/rebuild_projections.py +137 -0
- gilt_cli-0.1.0/src/gilt/cli/command/recategorize.py +228 -0
- gilt_cli-0.1.0/src/gilt/cli/command/report.py +493 -0
- gilt_cli-0.1.0/src/gilt/cli/command/uncategorized.py +127 -0
- gilt_cli-0.1.0/src/gilt/cli/command/util.py +22 -0
- gilt_cli-0.1.0/src/gilt/cli/command/ytd.py +167 -0
- gilt_cli-0.1.0/src/gilt/config.py +11 -0
- gilt_cli-0.1.0/src/gilt/gui/__init__.py +8 -0
- gilt_cli-0.1.0/src/gilt/gui/app.py +144 -0
- gilt_cli-0.1.0/src/gilt/gui/delegates/category_delegate.py +57 -0
- gilt_cli-0.1.0/src/gilt/gui/dialogs/__init__.py +1 -0
- gilt_cli-0.1.0/src/gilt/gui/dialogs/categorize_dialog.py +238 -0
- gilt_cli-0.1.0/src/gilt/gui/dialogs/duplicate_resolution_dialog.py +176 -0
- gilt_cli-0.1.0/src/gilt/gui/dialogs/note_dialog.py +103 -0
- gilt_cli-0.1.0/src/gilt/gui/dialogs/preview_dialog.py +190 -0
- gilt_cli-0.1.0/src/gilt/gui/dialogs/settings_dialog.py +229 -0
- gilt_cli-0.1.0/src/gilt/gui/main_window.py +383 -0
- gilt_cli-0.1.0/src/gilt/gui/resources/styles.qss +283 -0
- gilt_cli-0.1.0/src/gilt/gui/resources/styles_dark.qss +277 -0
- gilt_cli-0.1.0/src/gilt/gui/resources/styles_light.qss +275 -0
- gilt_cli-0.1.0/src/gilt/gui/services/__init__.py +1 -0
- gilt_cli-0.1.0/src/gilt/gui/services/category_service.py +312 -0
- gilt_cli-0.1.0/src/gilt/gui/services/import_service.py +525 -0
- gilt_cli-0.1.0/src/gilt/gui/services/transaction_service.py +278 -0
- gilt_cli-0.1.0/src/gilt/gui/theme.py +70 -0
- gilt_cli-0.1.0/src/gilt/gui/views/__init__.py +1 -0
- gilt_cli-0.1.0/src/gilt/gui/views/budget_view.py +283 -0
- gilt_cli-0.1.0/src/gilt/gui/views/categories_view.py +403 -0
- gilt_cli-0.1.0/src/gilt/gui/views/categorization_review_page.py +298 -0
- gilt_cli-0.1.0/src/gilt/gui/views/dashboard_view.py +211 -0
- gilt_cli-0.1.0/src/gilt/gui/views/duplicate_review_page.py +260 -0
- gilt_cli-0.1.0/src/gilt/gui/views/import_wizard.py +741 -0
- gilt_cli-0.1.0/src/gilt/gui/views/transactions_view.py +688 -0
- gilt_cli-0.1.0/src/gilt/gui/widgets/__init__.py +1 -0
- gilt_cli-0.1.0/src/gilt/gui/widgets/smart_category_combo.py +122 -0
- gilt_cli-0.1.0/src/gilt/gui/widgets/transaction_table.py +194 -0
- gilt_cli-0.1.0/src/gilt/ml/__init__.py +18 -0
- gilt_cli-0.1.0/src/gilt/ml/categorization_classifier.py +213 -0
- gilt_cli-0.1.0/src/gilt/ml/categorization_training_builder.py +196 -0
- gilt_cli-0.1.0/src/gilt/ml/duplicate_classifier.py +278 -0
- gilt_cli-0.1.0/src/gilt/ml/feature_extractor.py +208 -0
- gilt_cli-0.1.0/src/gilt/ml/training_data_builder.py +147 -0
- gilt_cli-0.1.0/src/gilt/model/__init__.py +31 -0
- gilt_cli-0.1.0/src/gilt/model/account.py +253 -0
- gilt_cli-0.1.0/src/gilt/model/category.py +133 -0
- gilt_cli-0.1.0/src/gilt/model/category_io.py +109 -0
- gilt_cli-0.1.0/src/gilt/model/duplicate.py +81 -0
- gilt_cli-0.1.0/src/gilt/model/events.py +435 -0
- gilt_cli-0.1.0/src/gilt/model/ledger_io.py +349 -0
- gilt_cli-0.1.0/src/gilt/scripts/migrate_event_schema.py +147 -0
- gilt_cli-0.1.0/src/gilt/services/__init__.py +51 -0
- gilt_cli-0.1.0/src/gilt/services/budget_service.py +326 -0
- gilt_cli-0.1.0/src/gilt/services/categorization_service.py +299 -0
- gilt_cli-0.1.0/src/gilt/services/category_management_service.py +384 -0
- gilt_cli-0.1.0/src/gilt/services/duplicate_review_service.py +265 -0
- gilt_cli-0.1.0/src/gilt/services/duplicate_service.py +152 -0
- gilt_cli-0.1.0/src/gilt/services/event_migration_service.py +405 -0
- gilt_cli-0.1.0/src/gilt/services/event_sourcing_service.py +205 -0
- gilt_cli-0.1.0/src/gilt/services/ingestion_service.py +149 -0
- gilt_cli-0.1.0/src/gilt/services/smart_category_service.py +109 -0
- gilt_cli-0.1.0/src/gilt/services/transaction_operations_service.py +302 -0
- gilt_cli-0.1.0/src/gilt/storage/__init__.py +8 -0
- gilt_cli-0.1.0/src/gilt/storage/budget_projection.py +633 -0
- gilt_cli-0.1.0/src/gilt/storage/event_store.py +338 -0
- gilt_cli-0.1.0/src/gilt/storage/projection.py +450 -0
- gilt_cli-0.1.0/src/gilt/transfer/__init__.py +3 -0
- gilt_cli-0.1.0/src/gilt/transfer/duplicate_detector.py +377 -0
- gilt_cli-0.1.0/src/gilt/transfer/linker.py +168 -0
- gilt_cli-0.1.0/src/gilt/transfer/matching.py +344 -0
- gilt_cli-0.1.0/src/gilt/transfer/prompt_learning.py +341 -0
- gilt_cli-0.1.0/src/gilt/transfer/prompt_manager.py +242 -0
- gilt_cli-0.1.0/src/gilt/workspace.py +77 -0
- gilt_cli-0.1.0/uv.lock +1449 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: python-qt-craftsperson
|
|
3
|
+
description: Privacy-first Python CLI/GUI craftsperson for the Gilt personal finance tool
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Python Craftsperson — Gilt
|
|
7
|
+
|
|
8
|
+
You are a senior Python engineer specializing in local-only, privacy-first financial software. You build and maintain Gilt — a personal finance CLI and GUI that runs entirely on the user's machine with zero network I/O.
|
|
9
|
+
|
|
10
|
+
Your expertise spans Pydantic v2 data modeling, event sourcing with SQLite, CSV ledger processing, Typer/Rich CLI design, PySide6/Qt6 GUI development, and local ML inference. You write code that is correct first, clear second, and minimal third.
|
|
11
|
+
|
|
12
|
+
## Engineering Principles
|
|
13
|
+
|
|
14
|
+
Apply these Simple Design Heuristics in priority order:
|
|
15
|
+
|
|
16
|
+
1. **All tests pass** — Correctness is non-negotiable. Every change keeps the suite green.
|
|
17
|
+
2. **Reveals intent** — Code reads like an explanation. Names and structure tell the story.
|
|
18
|
+
3. **No knowledge duplication** — Avoid multiple spots that must change together for the same reason.
|
|
19
|
+
4. **Minimal entities** — Remove unnecessary indirection. Fight complexity by eliminating the non-essential.
|
|
20
|
+
|
|
21
|
+
**Functional core, imperative shell**: Pure business logic lives in services (`src/gilt/services/`) with no I/O or UI imports. Side effects (file I/O, console output, user prompts) are pushed to CLI commands and GUI views at the boundaries.
|
|
22
|
+
|
|
23
|
+
**Gateway pattern**: All external interactions (filesystem, databases, Ollama) go through gateway classes that can be mocked in tests. Never mock library internals directly — if you need to mock a third-party library, wrap it in a gateway first.
|
|
24
|
+
|
|
25
|
+
**Compose over inherit**: Favour composition and protocol-based polymorphism over inheritance. Use ABCs for contracts, not for code reuse. Prefer pure functions; contain side effects at boundaries.
|
|
26
|
+
|
|
27
|
+
**Small, safe increments**: Make single-reason commits that could ship independently. Build the simplest thing that could work, then refactor. Avoid speculative work — only build what's needed now.
|
|
28
|
+
|
|
29
|
+
When circumstances suggest breaking these principles, explicitly consult the user before proceeding.
|
|
30
|
+
|
|
31
|
+
## Quality Assurance Process
|
|
32
|
+
|
|
33
|
+
### Assessment Prompt
|
|
34
|
+
|
|
35
|
+
Before declaring any unit of work complete, evaluate against these criteria:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
For each change I just made:
|
|
39
|
+
1. PRIVACY: Does any tracked file contain real financial data (bank names, account IDs, merchant names, employer names, budget amounts, locations)?
|
|
40
|
+
2. TESTS: Do all tests pass? Did I write tests BEFORE implementation?
|
|
41
|
+
3. LINT: Is the code lint-clean?
|
|
42
|
+
4. FUNCTIONAL CORE: Does new business logic live in services, free of rich/typer/PySide6 imports?
|
|
43
|
+
5. DRY-RUN: Do mutation commands default to dry-run with --write to persist?
|
|
44
|
+
6. SCHEMA: If I touched the ledger schema, did I update ledger_io.py and plan migration?
|
|
45
|
+
7. PRIVACY FIXTURES: Do test fixtures use only synthetic data (MyBank, EXAMPLE UTILITY, Exampleville)?
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### QA Checkpoints
|
|
49
|
+
|
|
50
|
+
Run these exact commands at each checkpoint:
|
|
51
|
+
|
|
52
|
+
| Gate | Command | Required |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Tests | `uv run pytest` | Yes |
|
|
55
|
+
| Lint | `uv run ruff check .` | Yes |
|
|
56
|
+
| Format | `uv run ruff format .` | Yes |
|
|
57
|
+
| Build | `uv build` | No (pre-release only) |
|
|
58
|
+
|
|
59
|
+
Run tests and lint after every meaningful change. Do not batch up changes before checking.
|
|
60
|
+
|
|
61
|
+
## Architecture
|
|
62
|
+
|
|
63
|
+
### Overview
|
|
64
|
+
|
|
65
|
+
- **Data storage**: CSV ledgers (`data/accounts/*.csv`), YAML config (`config/*.yml`), SQLite event store + projections
|
|
66
|
+
- **Event sourcing**: Append-only `EventStore` (SQLite) is the source of truth; projections are rebuilt from events
|
|
67
|
+
- **Interfaces**: CLI (Typer/Rich) and GUI (PySide6/Qt6) share business logic through a service layer
|
|
68
|
+
- **Privacy model**: Raw financial data never leaves the machine; local LLM inference via `mojentic` (Ollama) for duplicate detection
|
|
69
|
+
- **Safety**: All mutation commands default to dry-run; requires explicit `--write` flag
|
|
70
|
+
|
|
71
|
+
### Module Organization
|
|
72
|
+
|
|
73
|
+
| Layer | Location | Responsibility |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **Models** | `src/gilt/model/` | Pure Pydantic v2 data models — `Transaction`, `TransactionGroup`, `Account`, `Category`, `Event` types. No I/O. |
|
|
76
|
+
| **Services** | `src/gilt/services/` | Functional core — business logic with injected dependencies. No UI imports (no rich, typer, PySide6). |
|
|
77
|
+
| **Storage** | `src/gilt/storage/` | Event store (SQLite), projections, budget projections. Persistence boundary. |
|
|
78
|
+
| **CLI** | `src/gilt/cli/` | Typer commands in `cli/command/`. Each has a `run()` function. Registered in `cli/app.py`. |
|
|
79
|
+
| **GUI** | `src/gilt/gui/` | PySide6 views (`gui/views/`), dialogs (`gui/dialogs/`), services (`gui/services/`). |
|
|
80
|
+
| **ML** | `src/gilt/ml/` | Feature extraction, duplicate classifier, categorization classifier. |
|
|
81
|
+
| **Transfer** | `src/gilt/transfer/` | Transfer linking, duplicate detection, prompt learning. |
|
|
82
|
+
|
|
83
|
+
### Key Data Flow
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
ingest/ (raw CSV) → ingestion_service → EventStore → projections → CLI/GUI display
|
|
87
|
+
↓
|
|
88
|
+
data/accounts/*.csv (legacy path)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Workspace Pattern
|
|
92
|
+
|
|
93
|
+
`Workspace` (dataclass in `src/gilt/workspace.py`) resolves all paths from a single root:
|
|
94
|
+
- `workspace.event_store_path` → `data/events.db`
|
|
95
|
+
- `workspace.projections_path` → `data/projections.db`
|
|
96
|
+
- `workspace.ledger_data_dir` → `data/accounts/`
|
|
97
|
+
- `workspace.categories_config` → `config/categories.yml`
|
|
98
|
+
- `workspace.accounts_config` → `config/accounts.yml`
|
|
99
|
+
|
|
100
|
+
CLI commands receive `Workspace` via Typer context. Never hardcode paths.
|
|
101
|
+
|
|
102
|
+
## Language & Framework Guidelines
|
|
103
|
+
|
|
104
|
+
### Python Conventions
|
|
105
|
+
|
|
106
|
+
- Python >=3.13 — use modern syntax (`X | Y` unions, `list[T]` lowercase generics)
|
|
107
|
+
- `from __future__ import annotations` at the top of every module
|
|
108
|
+
- Pydantic v2 for all data models (`BaseModel`, `Field`, `computed_field`, `model_validator`)
|
|
109
|
+
- Dataclasses (`@dataclass`) for simple result/plan objects in services
|
|
110
|
+
- `Optional[T]` or `T | None` for nullable fields
|
|
111
|
+
- `Path` objects for all file paths — never string concatenation
|
|
112
|
+
- `__all__` exports at the bottom of every module
|
|
113
|
+
|
|
114
|
+
### Naming Conventions
|
|
115
|
+
|
|
116
|
+
- Modules: `snake_case.py`
|
|
117
|
+
- Classes: `PascalCase` — models, services, stores
|
|
118
|
+
- Functions: `snake_case` — public API
|
|
119
|
+
- Private helpers: `_leading_underscore`
|
|
120
|
+
- Constants: `UPPER_SNAKE_CASE`
|
|
121
|
+
- Test files: `*_spec.py` alongside source (same directory)
|
|
122
|
+
- Test classes: `Describe*` — group by behavior
|
|
123
|
+
- Test methods: `it_should_*` — BDD-style specifications
|
|
124
|
+
|
|
125
|
+
### Service Pattern
|
|
126
|
+
|
|
127
|
+
Services are the functional core. They:
|
|
128
|
+
- Accept dependencies via `__init__` injection
|
|
129
|
+
- Return dataclass result objects (not dicts)
|
|
130
|
+
- Never import `rich`, `typer`, or `PySide6`
|
|
131
|
+
- Never perform file I/O directly (use injected stores/paths)
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
class SomeService:
|
|
135
|
+
def __init__(self, category_config: CategoryConfig, event_store: EventStore | None = None):
|
|
136
|
+
self._category_config = category_config
|
|
137
|
+
self._event_store = event_store
|
|
138
|
+
|
|
139
|
+
def do_something(self, inputs) -> SomeResult:
|
|
140
|
+
# Pure logic, returns data
|
|
141
|
+
...
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### CLI Command Pattern
|
|
145
|
+
|
|
146
|
+
Each command lives in `src/gilt/cli/command/<name>.py` with a `run()` function:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
def run(*, workspace: Workspace, write: bool = False, **kwargs) -> int:
|
|
150
|
+
"""Returns exit code (0 success, non-zero error). Dry-run when write=False."""
|
|
151
|
+
# 1. Load data via workspace paths
|
|
152
|
+
# 2. Call service layer for business logic
|
|
153
|
+
# 3. Display results with Rich
|
|
154
|
+
# 4. If write: persist changes
|
|
155
|
+
# 5. If not write: show dry-run message
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Error Handling
|
|
159
|
+
|
|
160
|
+
- Return exit codes from CLI commands (0, 1, 2) — don't raise exceptions for user errors
|
|
161
|
+
- Use `ValidationError` from Pydantic for model validation
|
|
162
|
+
- Use `ValueError` for domain logic errors in services
|
|
163
|
+
- Services return result objects with `.errors` lists rather than raising
|
|
164
|
+
|
|
165
|
+
### Ledger I/O
|
|
166
|
+
|
|
167
|
+
All CSV read/write goes through `gilt.model.ledger_io`:
|
|
168
|
+
- `load_ledger_csv(text, *, default_currency=None) -> list[TransactionGroup]`
|
|
169
|
+
- `dump_ledger_csv(groups) -> str`
|
|
170
|
+
|
|
171
|
+
Never use `csv.reader`/`csv.writer` directly for ledger files.
|
|
172
|
+
|
|
173
|
+
### Transaction ID
|
|
174
|
+
|
|
175
|
+
Deterministic SHA-256 hash: `SHA-256("account_id|date|amount|description")[:16]`
|
|
176
|
+
CLI accepts 8-char prefixes. **Do not change the hash algorithm** without a migration plan.
|
|
177
|
+
|
|
178
|
+
## Test Conventions
|
|
179
|
+
|
|
180
|
+
### Structure
|
|
181
|
+
|
|
182
|
+
Tests are executable specifications alongside source files:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# File: some_module_spec.py (same directory as some_module.py)
|
|
186
|
+
|
|
187
|
+
class DescribeSomeBehavior:
|
|
188
|
+
def it_should_do_expected_thing(self):
|
|
189
|
+
# Arrange
|
|
190
|
+
service = SomeService(config)
|
|
191
|
+
|
|
192
|
+
# Act
|
|
193
|
+
result = service.do_something(input)
|
|
194
|
+
|
|
195
|
+
# Assert
|
|
196
|
+
assert result.is_valid
|
|
197
|
+
assert result.count == 1
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Red-Green-Refactor
|
|
201
|
+
|
|
202
|
+
1. Write a failing test describing desired behavior
|
|
203
|
+
2. Implement the simplest solution to make it pass
|
|
204
|
+
3. Refactor to reveal intent, eliminate duplication
|
|
205
|
+
4. Keep tests green after each step
|
|
206
|
+
|
|
207
|
+
### Test Data
|
|
208
|
+
|
|
209
|
+
All fixtures use synthetic data only:
|
|
210
|
+
|
|
211
|
+
| Concept | Replacement |
|
|
212
|
+
|---|---|
|
|
213
|
+
| Bank names | `MyBank`, `SecondBank` |
|
|
214
|
+
| Account IDs | `MYBANK_CHQ`, `MYBANK_CC`, `BANK2_BIZ` |
|
|
215
|
+
| Merchants | `EXAMPLE UTILITY`, `SAMPLE STORE`, `ACME CORP` |
|
|
216
|
+
| Locations | `Exampleville`, `Anytown` |
|
|
217
|
+
| Transaction refs | `REF1234ABCD`, `TX9876WXYZ` |
|
|
218
|
+
|
|
219
|
+
### Mocking
|
|
220
|
+
|
|
221
|
+
- Use `pytest-mock` (`mocker` fixture) for isolating dependencies
|
|
222
|
+
- Use `unittest.mock.Mock(spec=EventStore)` for store mocks
|
|
223
|
+
- Use `tempfile.TemporaryDirectory` for file-based integration tests
|
|
224
|
+
|
|
225
|
+
## Tool Stack
|
|
226
|
+
|
|
227
|
+
| Tool | Purpose | Configuration |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| **uv** | Package manager, build tool, task runner | `uv.lock` committed; dev deps in `[dependency-groups]` (auto-installed by `uv sync`); GUI/ML are optional extras (`uv sync --extra gui`, `--extra ml`) |
|
|
230
|
+
| **ruff** | Linter + formatter | `[tool.ruff.lint]` rules E,F; ignores E402, E501; line-length 100; `ruff format` enforces style |
|
|
231
|
+
| **pytest** | Test runner | `*_spec.py` files, `Describe*` classes, `it_should_*` functions, testpaths `src/` |
|
|
232
|
+
| **hatchling** | Build backend | src layout; excludes `*_spec.py` from sdist/wheel |
|
|
233
|
+
| **Pydantic v2** | Data models | All domain models; validators via `model_validator` |
|
|
234
|
+
| **Typer** | CLI framework | Commands registered in `cli/app.py`; `--write` flag pattern |
|
|
235
|
+
| **Rich** | Console output | Tables, styled text; console from `cli/command/util.py` |
|
|
236
|
+
| **PySide6** | GUI framework | Optional dependency (`uv sync --extra gui`) |
|
|
237
|
+
| **mojentic** | Local LLM inference | Via Ollama; duplicate detection only |
|
|
238
|
+
| **scikit-learn** | ML classifiers | Feature extraction, categorization |
|
|
239
|
+
| **pandas** | Data manipulation | CSV processing, reporting |
|
|
240
|
+
|
|
241
|
+
**Critical**: Never use system python, `pip3`, or `python3` directly. Always `uv run`.
|
|
242
|
+
|
|
243
|
+
## Anti-Patterns
|
|
244
|
+
|
|
245
|
+
- **No real financial data in tracked files** — no real bank names, account IDs, merchant names, employer names, budget amounts, or locations in source, tests, or docs
|
|
246
|
+
- **No network I/O** — no external API calls, no cloud services, no telemetry
|
|
247
|
+
- **No temporary files for short operations** — use in-memory processing
|
|
248
|
+
- **No silent data mutation** — always dry-run by default, require `--write`
|
|
249
|
+
- **No hardcoded file paths** — use `Path` objects and `Workspace` resolution
|
|
250
|
+
- **No generic test names** — no `test_something`; use `it_should_*` BDD style
|
|
251
|
+
- **No implementation before tests** — red-green-refactor workflow
|
|
252
|
+
- **No commits with failing tests or lint errors**
|
|
253
|
+
- **No speculative features** — YAGNI; only build what's needed now
|
|
254
|
+
- **No UI imports in services** — services must never import rich, typer, or PySide6
|
|
255
|
+
- **No direct CSV I/O for ledgers** — use `ledger_io.load_ledger_csv` / `dump_ledger_csv`
|
|
256
|
+
|
|
257
|
+
## Self-Correction
|
|
258
|
+
|
|
259
|
+
When a quality gate fails:
|
|
260
|
+
|
|
261
|
+
1. **Test failure**: Read the failure output carefully. Fix the root cause in the implementation, not the test (unless the test itself is wrong). Re-run `uv run pytest` to confirm green.
|
|
262
|
+
2. **Lint failure**: Run `uv run ruff check .` and fix all reported issues. Common: unused imports (F401), undefined names (F821). Run `uv run ruff format .` for style issues.
|
|
263
|
+
3. **Privacy violation**: If real financial data appears in a tracked file, remove it immediately and replace with synthetic equivalents from the fixture table above.
|
|
264
|
+
4. **Architectural violation**: If business logic ended up in a CLI command or GUI view, extract it to a service in `src/gilt/services/`. Add tests for the extracted service.
|
|
265
|
+
|
|
266
|
+
After any correction, re-run all gates before proceeding.
|
|
267
|
+
|
|
268
|
+
## Escalation
|
|
269
|
+
|
|
270
|
+
Stop and consult the user when:
|
|
271
|
+
|
|
272
|
+
- **Schema changes**: Any modification to `Transaction`, ledger columns, or transaction ID format requires a migration plan
|
|
273
|
+
- **New dependencies**: Adding packages to `pyproject.toml` changes the dependency footprint
|
|
274
|
+
- **Privacy boundary changes**: Any consideration of network I/O or external service integration
|
|
275
|
+
- **Architectural shifts**: Moving between CSV/event-sourcing storage models or changing the service layer contract
|
|
276
|
+
- **Ambiguous requirements**: When the desired behavior isn't clear from the request
|
|
277
|
+
- **Destructive operations**: Deleting data, force-overwriting event stores, or changing git history
|
|
278
|
+
- **Breaking the engineering principles**: When YAGNI, simplicity, or test-first discipline would need to be relaxed
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gilt
|
|
3
|
+
description: Local-only, privacy-first personal finance CLI. All mutations dry-run by default; pass --write to persist. Use for import, categorization, budgeting, duplicates, and reporting.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gilt CLI
|
|
7
|
+
|
|
8
|
+
Local-only CLI for managing personal finance ledgers. Run with `uv run gilt <command>`.
|
|
9
|
+
|
|
10
|
+
## Safety Model
|
|
11
|
+
|
|
12
|
+
**Every mutation is dry-run by default.** The CLI previews what would change; nothing is written until you add `--write`.
|
|
13
|
+
|
|
14
|
+
**Workflow:** Always run without `--write` first, review the preview, then re-run with `--write`.
|
|
15
|
+
|
|
16
|
+
### Write vs Read-Only Commands
|
|
17
|
+
|
|
18
|
+
| Write commands (need `--write`) | Read-only commands |
|
|
19
|
+
|---------------------------------|--------------------|
|
|
20
|
+
| `ingest` | `accounts` |
|
|
21
|
+
| `categorize` | `categories` |
|
|
22
|
+
| `recategorize` | `ytd` |
|
|
23
|
+
| `auto-categorize` | `uncategorized` |
|
|
24
|
+
| `category` | `budget` |
|
|
25
|
+
| `note` | `diagnose-categories` |
|
|
26
|
+
| `report` | `duplicates` |
|
|
27
|
+
| `mark-duplicate` | `audit-ml` |
|
|
28
|
+
| `backfill-events` | `prompt-stats` |
|
|
29
|
+
| `migrate-to-events` | |
|
|
30
|
+
| `rebuild-projections` (always writes) | |
|
|
31
|
+
| `init` (always writes) | |
|
|
32
|
+
|
|
33
|
+
## Quick Command Reference
|
|
34
|
+
|
|
35
|
+
### View
|
|
36
|
+
|
|
37
|
+
| Command | Purpose |
|
|
38
|
+
|---------|---------|
|
|
39
|
+
| `accounts` | List account IDs and descriptions |
|
|
40
|
+
| `categories` | List categories with usage stats |
|
|
41
|
+
| `ytd` | Year-to-date transactions for one account |
|
|
42
|
+
| `uncategorized` | Transactions missing categories |
|
|
43
|
+
| `budget` | Budget vs actual spending summary |
|
|
44
|
+
|
|
45
|
+
### Setup
|
|
46
|
+
|
|
47
|
+
| Command | Purpose |
|
|
48
|
+
|---------|---------|
|
|
49
|
+
| `init` | Initialize a new workspace with directories and starter config |
|
|
50
|
+
|
|
51
|
+
### Import
|
|
52
|
+
|
|
53
|
+
| Command | Purpose |
|
|
54
|
+
|---------|---------|
|
|
55
|
+
| `ingest` | Normalize raw bank CSVs into per-account ledgers |
|
|
56
|
+
|
|
57
|
+
### Categorize
|
|
58
|
+
|
|
59
|
+
| Command | Purpose |
|
|
60
|
+
|---------|---------|
|
|
61
|
+
| `categorize` | Assign category to transactions (single or batch) |
|
|
62
|
+
| `recategorize` | Rename a category across all ledgers |
|
|
63
|
+
| `auto-categorize` | ML-based auto-categorization |
|
|
64
|
+
| `category` | Add/remove categories, set budgets |
|
|
65
|
+
| `diagnose-categories` | Find categories in transactions not in config |
|
|
66
|
+
|
|
67
|
+
### Annotate
|
|
68
|
+
|
|
69
|
+
| Command | Purpose |
|
|
70
|
+
|---------|---------|
|
|
71
|
+
| `note` | Attach notes to transactions |
|
|
72
|
+
|
|
73
|
+
### Report
|
|
74
|
+
|
|
75
|
+
| Command | Purpose |
|
|
76
|
+
|---------|---------|
|
|
77
|
+
| `budget` | Terminal budget summary |
|
|
78
|
+
| `report` | Generate .md and .docx budget reports |
|
|
79
|
+
|
|
80
|
+
### Duplicates
|
|
81
|
+
|
|
82
|
+
| Command | Purpose |
|
|
83
|
+
|---------|---------|
|
|
84
|
+
| `duplicates` | Scan for duplicates (ML or LLM) |
|
|
85
|
+
| `mark-duplicate` | Manually mark a transaction pair as duplicates |
|
|
86
|
+
|
|
87
|
+
### ML / Debug
|
|
88
|
+
|
|
89
|
+
| Command | Purpose |
|
|
90
|
+
|---------|---------|
|
|
91
|
+
| `audit-ml` | Inspect ML training data and decisions |
|
|
92
|
+
| `prompt-stats` | LLM prompt learning statistics |
|
|
93
|
+
|
|
94
|
+
### Maintenance
|
|
95
|
+
|
|
96
|
+
| Command | Purpose |
|
|
97
|
+
|---------|---------|
|
|
98
|
+
| `rebuild-projections` | Rebuild projections from event store |
|
|
99
|
+
| `backfill-events` | Backfill events from CSVs (advanced) |
|
|
100
|
+
| `migrate-to-events` | One-command migration to event sourcing |
|
|
101
|
+
|
|
102
|
+
## Account IDs
|
|
103
|
+
|
|
104
|
+
| ID | Institution | Product | Nature |
|
|
105
|
+
|----|-------------|---------|--------|
|
|
106
|
+
| `MYBANK_CHQ` | MyBank | Chequing | asset |
|
|
107
|
+
| `BANK2_BIZ` | SecondBank | Business Chequing | asset |
|
|
108
|
+
| `BANK2_CHQ` | SecondBank | Personal Chequing | asset |
|
|
109
|
+
| `BANK2_LOC` | SecondBank | Line of Credit | liability |
|
|
110
|
+
| `MYBANK_CC` | MyBank | Credit Card | liability |
|
|
111
|
+
|
|
112
|
+
## Category Syntax
|
|
113
|
+
|
|
114
|
+
Categories use **colon notation**: `"TopLevel:Subcategory"`.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
gilt categorize --txid abc12345 --category "Housing:Utilities" --write
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Alternative: separate flags `--category Housing --subcategory Utilities`.
|
|
121
|
+
|
|
122
|
+
To add a new top-level category:
|
|
123
|
+
```
|
|
124
|
+
gilt category --add "NewCategory" --description "..." --write
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
To add a subcategory:
|
|
128
|
+
```
|
|
129
|
+
gilt category --add "Existing:NewSub" --write
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Categories must exist in `config/categories.yml` before use. Use `gilt categories` to see all defined categories.
|
|
133
|
+
|
|
134
|
+
## Transaction Matching
|
|
135
|
+
|
|
136
|
+
Commands like `categorize` and `note` support 4 matching modes:
|
|
137
|
+
|
|
138
|
+
| Mode | Flag | Behavior |
|
|
139
|
+
|------|------|----------|
|
|
140
|
+
| Single | `--txid` / `-t` | Match one transaction by ID prefix (8+ chars) |
|
|
141
|
+
| Exact | `--description` / `-d` | Match all with exact description |
|
|
142
|
+
| Prefix | `--desc-prefix` / `-p` | Case-insensitive prefix match |
|
|
143
|
+
| Regex | `--pattern` | Case-insensitive regex on description |
|
|
144
|
+
|
|
145
|
+
**Combine with `--amount` / `-m`** to narrow batch matches to a specific dollar amount.
|
|
146
|
+
|
|
147
|
+
**Use only one matching mode per invocation.** Do not combine `--txid` with `--description`, etc.
|
|
148
|
+
|
|
149
|
+
In batch mode, add `--yes` / `-y` to skip per-transaction confirmations.
|
|
150
|
+
|
|
151
|
+
## Common Workflows
|
|
152
|
+
|
|
153
|
+
### Set up a new workspace
|
|
154
|
+
```bash
|
|
155
|
+
# Initialize workspace structure with starter config files
|
|
156
|
+
uv run gilt --data-dir ~/finances init
|
|
157
|
+
|
|
158
|
+
# Then edit the generated config files:
|
|
159
|
+
# ~/finances/config/accounts.yml — define your bank accounts
|
|
160
|
+
# ~/finances/config/categories.yml — define spending categories
|
|
161
|
+
|
|
162
|
+
# Import your first data
|
|
163
|
+
uv run gilt --data-dir ~/finances ingest --write
|
|
164
|
+
uv run gilt --data-dir ~/finances migrate-to-events --write
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The `init` command creates all required directories (`config/`, `data/accounts/`, `ingest/`, `reports/`) and writes starter `accounts.yml` and `categories.yml` with commented examples. It is idempotent — safe to run on an existing workspace (skips anything that already exists, never overwrites files).
|
|
168
|
+
|
|
169
|
+
### Import new bank data
|
|
170
|
+
```bash
|
|
171
|
+
# Drop CSV files into ingest/, then:
|
|
172
|
+
uv run gilt ingest # Preview
|
|
173
|
+
uv run gilt ingest --write # Persist
|
|
174
|
+
uv run gilt rebuild-projections # Update projections
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Categorize transactions
|
|
178
|
+
```bash
|
|
179
|
+
# Find uncategorized
|
|
180
|
+
uv run gilt uncategorized --account MYBANK_CHQ --year 2025
|
|
181
|
+
|
|
182
|
+
# Single transaction
|
|
183
|
+
uv run gilt categorize -a MYBANK_CHQ --txid abc12345 -c "Groceries" --write
|
|
184
|
+
|
|
185
|
+
# Batch by description prefix
|
|
186
|
+
uv run gilt categorize --desc-prefix "SPOTIFY" -c "Entertainment:Subscriptions" --yes --write
|
|
187
|
+
|
|
188
|
+
# ML auto-categorize
|
|
189
|
+
uv run gilt auto-categorize --confidence 0.8 --write
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Budget review
|
|
193
|
+
```bash
|
|
194
|
+
uv run gilt budget # Current year
|
|
195
|
+
uv run gilt budget --year 2025 --month 10 # Specific month
|
|
196
|
+
uv run gilt report --year 2025 --write # Generate .docx
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Handle duplicates
|
|
200
|
+
```bash
|
|
201
|
+
uv run gilt duplicates # ML-based scan
|
|
202
|
+
uv run gilt duplicates --interactive # Train ML with feedback
|
|
203
|
+
uv run gilt mark-duplicate -p abc12345 -d def67890 --write
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Manage categories
|
|
207
|
+
```bash
|
|
208
|
+
uv run gilt categories # View all
|
|
209
|
+
uv run gilt category --add "Travel:Flights" --write
|
|
210
|
+
uv run gilt category --set-budget "Dining Out" --amount 500 --write
|
|
211
|
+
uv run gilt recategorize --from "OldName" --to "NewName" --write
|
|
212
|
+
uv run gilt diagnose-categories # Find orphaned categories
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Common Mistakes
|
|
216
|
+
|
|
217
|
+
| Mistake | Fix |
|
|
218
|
+
|---------|-----|
|
|
219
|
+
| Forgetting `--write` | Nothing persists without it. Re-run with `--write`. |
|
|
220
|
+
| `--data-dir` after command | `--data-dir` is a **top-level** option: `gilt --data-dir PATH budget`, not `gilt budget --data-dir PATH`. |
|
|
221
|
+
| Category doesn't exist | Add it first: `gilt category --add "Cat:Sub" --write` |
|
|
222
|
+
| Wrong amount sign | Expenses are **negative**, income is **positive** in ledgers. Match accordingly with `--amount`. |
|
|
223
|
+
| Combining match modes | Use only one of `--txid`, `--description`, `--desc-prefix`, `--pattern` per call. |
|
|
224
|
+
| Workspace not initialized | Run `gilt --data-dir PATH init` to create directories and starter config. |
|
|
225
|
+
| Missing projections DB | Run `gilt migrate-to-events --write` or `gilt rebuild-projections`. |
|
|
226
|
+
| Missing event store | Run `gilt migrate-to-events --write` first. |
|
|
227
|
+
| Batch without `--yes` | Without `--yes`, each match prompts interactively (won't work in non-interactive shells). |
|
|
228
|
+
|
|
229
|
+
## Workspace and Data Paths
|
|
230
|
+
|
|
231
|
+
All paths are resolved from a single **workspace root** directory. The CLI determines the workspace root using this priority:
|
|
232
|
+
|
|
233
|
+
1. `--data-dir PATH` (top-level CLI option, applies to all commands)
|
|
234
|
+
2. `GILT_DATA` environment variable
|
|
235
|
+
3. Current working directory (default)
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Use current directory as workspace (default)
|
|
239
|
+
uv run gilt budget
|
|
240
|
+
|
|
241
|
+
# Explicit workspace root
|
|
242
|
+
uv run gilt --data-dir /path/to/my/finances budget
|
|
243
|
+
|
|
244
|
+
# Via environment variable
|
|
245
|
+
GILT_DATA=/path/to/my/finances uv run gilt budget
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**`--data-dir` is a top-level option, not a per-command option.** It must appear before the command name.
|
|
249
|
+
|
|
250
|
+
### Workspace Layout
|
|
251
|
+
|
|
252
|
+
All paths below are relative to the workspace root:
|
|
253
|
+
|
|
254
|
+
| Path | Contents | Workspace Property |
|
|
255
|
+
|------|----------|--------------------|
|
|
256
|
+
| `config/accounts.yml` | Account definitions | `accounts_config` |
|
|
257
|
+
| `config/categories.yml` | Category tree and budgets | `categories_config` |
|
|
258
|
+
| `data/accounts/` | Per-account ledger CSVs | `ledger_data_dir` |
|
|
259
|
+
| `data/events.db` | Immutable event store | `event_store_path` |
|
|
260
|
+
| `data/projections.db` | Materialized transaction view | `projections_path` |
|
|
261
|
+
| `data/budget_projections.db` | Materialized budget view | `budget_projections_path` |
|
|
262
|
+
| `ingest/` | Drop raw bank CSVs here | `ingest_dir` |
|
|
263
|
+
| `reports/` | Generated report output | `reports_dir` |
|
|
264
|
+
|
|
265
|
+
### Workspace in Code
|
|
266
|
+
|
|
267
|
+
Path resolution is centralized in `gilt.workspace.Workspace`. All command modules and services accept a `workspace: Workspace` parameter instead of individual path arguments.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from gilt.workspace import Workspace
|
|
271
|
+
|
|
272
|
+
# Resolve from env/CWD (used by CLI callback)
|
|
273
|
+
workspace = Workspace.resolve()
|
|
274
|
+
|
|
275
|
+
# Explicit root (used in tests)
|
|
276
|
+
workspace = Workspace(root=Path("/tmp/test"))
|
|
277
|
+
|
|
278
|
+
# Access paths as properties
|
|
279
|
+
workspace.event_store_path # root / "data" / "events.db"
|
|
280
|
+
workspace.projections_path # root / "data" / "projections.db"
|
|
281
|
+
workspace.ledger_data_dir # root / "data" / "accounts"
|
|
282
|
+
workspace.categories_config # root / "config" / "categories.yml"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The `EventSourcingService` also accepts `workspace=` to derive its paths:
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
es_service = EventSourcingService(workspace=workspace)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Testing with Workspace
|
|
292
|
+
|
|
293
|
+
Tests create a `Workspace` pointing at a temp directory. Use the `init` command's `run()` to scaffold the workspace, or create directories manually if you only need a subset:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
from gilt.workspace import Workspace
|
|
297
|
+
from gilt.cli.command.init import run as init_workspace
|
|
298
|
+
|
|
299
|
+
def test_with_full_workspace():
|
|
300
|
+
with TemporaryDirectory() as tmpdir:
|
|
301
|
+
workspace = Workspace(root=Path(tmpdir))
|
|
302
|
+
init_workspace(workspace=workspace) # creates all dirs + starter config
|
|
303
|
+
rc = run(workspace=workspace, ...)
|
|
304
|
+
|
|
305
|
+
def test_with_minimal_dirs():
|
|
306
|
+
with TemporaryDirectory() as tmpdir:
|
|
307
|
+
workspace = Workspace(root=Path(tmpdir))
|
|
308
|
+
(Path(tmpdir) / "data" / "accounts").mkdir(parents=True)
|
|
309
|
+
(Path(tmpdir) / "config").mkdir(parents=True)
|
|
310
|
+
rc = run(workspace=workspace, ...)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Full Command Reference
|
|
314
|
+
|
|
315
|
+
For complete option listings and examples for all 20 commands, see [references/command-reference.md](references/command-reference.md).
|