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.
Files changed (130) hide show
  1. gilt_cli-0.1.0/.claude/agents/python-qt-craftsperson.md +278 -0
  2. gilt_cli-0.1.0/.claude/settings.local.json +9 -0
  3. gilt_cli-0.1.0/.claude/skills/gilt/SKILL.md +315 -0
  4. gilt_cli-0.1.0/.claude/skills/gilt/references/command-reference.md +412 -0
  5. gilt_cli-0.1.0/.claude/skills/hone/SKILL.md +385 -0
  6. gilt_cli-0.1.0/.claude/skills/skill-writing/SKILL.md +260 -0
  7. gilt_cli-0.1.0/.github/workflows/ci.yml +24 -0
  8. gilt_cli-0.1.0/.github/workflows/release.yml +49 -0
  9. gilt_cli-0.1.0/.gitignore +49 -0
  10. gilt_cli-0.1.0/.hone-gates.json +19 -0
  11. gilt_cli-0.1.0/AGENTS.md +238 -0
  12. gilt_cli-0.1.0/CLAUDE.md +1 -0
  13. gilt_cli-0.1.0/LICENSE +21 -0
  14. gilt_cli-0.1.0/PKG-INFO +1212 -0
  15. gilt_cli-0.1.0/README.md +1180 -0
  16. gilt_cli-0.1.0/conftest.py +8 -0
  17. gilt_cli-0.1.0/docs/README.md +270 -0
  18. gilt_cli-0.1.0/docs/developer/architecture/data-model.md +367 -0
  19. gilt_cli-0.1.0/docs/developer/architecture/project-structure.md +262 -0
  20. gilt_cli-0.1.0/docs/developer/architecture/system-design.md +456 -0
  21. gilt_cli-0.1.0/docs/developer/index.md +312 -0
  22. gilt_cli-0.1.0/docs/developer/technical/auto-categorization.md +452 -0
  23. gilt_cli-0.1.0/docs/developer/technical/budgeting-system.md +576 -0
  24. gilt_cli-0.1.0/docs/developer/technical/duplicate-detection-ml-proposal.md +421 -0
  25. gilt_cli-0.1.0/docs/getting-started.md +202 -0
  26. gilt_cli-0.1.0/docs/index.md +83 -0
  27. gilt_cli-0.1.0/docs/user/cli/index.md +306 -0
  28. gilt_cli-0.1.0/docs/user/gui/importing.md +45 -0
  29. gilt_cli-0.1.0/docs/user/gui/index.md +324 -0
  30. gilt_cli-0.1.0/docs/user/index.md +178 -0
  31. gilt_cli-0.1.0/docs/user/installation.md +272 -0
  32. gilt_cli-0.1.0/docs/user/workflows/budget-tracking.md +668 -0
  33. gilt_cli-0.1.0/docs/user/workflows/initial-setup.md +522 -0
  34. gilt_cli-0.1.0/docs/user/workflows/monthly-review.md +536 -0
  35. gilt_cli-0.1.0/docs/user/workflows/upgrading-to-event-sourcing.md +177 -0
  36. gilt_cli-0.1.0/mkdocs.yml +94 -0
  37. gilt_cli-0.1.0/pyproject.toml +113 -0
  38. gilt_cli-0.1.0/src/gilt/__init__.py +7 -0
  39. gilt_cli-0.1.0/src/gilt/cli/__init__.py +1 -0
  40. gilt_cli-0.1.0/src/gilt/cli/app.py +803 -0
  41. gilt_cli-0.1.0/src/gilt/cli/command/__init__.py +11 -0
  42. gilt_cli-0.1.0/src/gilt/cli/command/accounts.py +73 -0
  43. gilt_cli-0.1.0/src/gilt/cli/command/audit_ml.py +307 -0
  44. gilt_cli-0.1.0/src/gilt/cli/command/auto_categorize.py +344 -0
  45. gilt_cli-0.1.0/src/gilt/cli/command/backfill_events.py +304 -0
  46. gilt_cli-0.1.0/src/gilt/cli/command/budget.py +184 -0
  47. gilt_cli-0.1.0/src/gilt/cli/command/categories.py +151 -0
  48. gilt_cli-0.1.0/src/gilt/cli/command/categorize.py +363 -0
  49. gilt_cli-0.1.0/src/gilt/cli/command/category.py +295 -0
  50. gilt_cli-0.1.0/src/gilt/cli/command/diagnose_categories.py +179 -0
  51. gilt_cli-0.1.0/src/gilt/cli/command/duplicates.py +421 -0
  52. gilt_cli-0.1.0/src/gilt/cli/command/ingest.py +171 -0
  53. gilt_cli-0.1.0/src/gilt/cli/command/init.py +126 -0
  54. gilt_cli-0.1.0/src/gilt/cli/command/mark_duplicate.py +262 -0
  55. gilt_cli-0.1.0/src/gilt/cli/command/migrate_to_events.py +274 -0
  56. gilt_cli-0.1.0/src/gilt/cli/command/note.py +243 -0
  57. gilt_cli-0.1.0/src/gilt/cli/command/prompt_stats.py +171 -0
  58. gilt_cli-0.1.0/src/gilt/cli/command/rebuild_projections.py +137 -0
  59. gilt_cli-0.1.0/src/gilt/cli/command/recategorize.py +228 -0
  60. gilt_cli-0.1.0/src/gilt/cli/command/report.py +493 -0
  61. gilt_cli-0.1.0/src/gilt/cli/command/uncategorized.py +127 -0
  62. gilt_cli-0.1.0/src/gilt/cli/command/util.py +22 -0
  63. gilt_cli-0.1.0/src/gilt/cli/command/ytd.py +167 -0
  64. gilt_cli-0.1.0/src/gilt/config.py +11 -0
  65. gilt_cli-0.1.0/src/gilt/gui/__init__.py +8 -0
  66. gilt_cli-0.1.0/src/gilt/gui/app.py +144 -0
  67. gilt_cli-0.1.0/src/gilt/gui/delegates/category_delegate.py +57 -0
  68. gilt_cli-0.1.0/src/gilt/gui/dialogs/__init__.py +1 -0
  69. gilt_cli-0.1.0/src/gilt/gui/dialogs/categorize_dialog.py +238 -0
  70. gilt_cli-0.1.0/src/gilt/gui/dialogs/duplicate_resolution_dialog.py +176 -0
  71. gilt_cli-0.1.0/src/gilt/gui/dialogs/note_dialog.py +103 -0
  72. gilt_cli-0.1.0/src/gilt/gui/dialogs/preview_dialog.py +190 -0
  73. gilt_cli-0.1.0/src/gilt/gui/dialogs/settings_dialog.py +229 -0
  74. gilt_cli-0.1.0/src/gilt/gui/main_window.py +383 -0
  75. gilt_cli-0.1.0/src/gilt/gui/resources/styles.qss +283 -0
  76. gilt_cli-0.1.0/src/gilt/gui/resources/styles_dark.qss +277 -0
  77. gilt_cli-0.1.0/src/gilt/gui/resources/styles_light.qss +275 -0
  78. gilt_cli-0.1.0/src/gilt/gui/services/__init__.py +1 -0
  79. gilt_cli-0.1.0/src/gilt/gui/services/category_service.py +312 -0
  80. gilt_cli-0.1.0/src/gilt/gui/services/import_service.py +525 -0
  81. gilt_cli-0.1.0/src/gilt/gui/services/transaction_service.py +278 -0
  82. gilt_cli-0.1.0/src/gilt/gui/theme.py +70 -0
  83. gilt_cli-0.1.0/src/gilt/gui/views/__init__.py +1 -0
  84. gilt_cli-0.1.0/src/gilt/gui/views/budget_view.py +283 -0
  85. gilt_cli-0.1.0/src/gilt/gui/views/categories_view.py +403 -0
  86. gilt_cli-0.1.0/src/gilt/gui/views/categorization_review_page.py +298 -0
  87. gilt_cli-0.1.0/src/gilt/gui/views/dashboard_view.py +211 -0
  88. gilt_cli-0.1.0/src/gilt/gui/views/duplicate_review_page.py +260 -0
  89. gilt_cli-0.1.0/src/gilt/gui/views/import_wizard.py +741 -0
  90. gilt_cli-0.1.0/src/gilt/gui/views/transactions_view.py +688 -0
  91. gilt_cli-0.1.0/src/gilt/gui/widgets/__init__.py +1 -0
  92. gilt_cli-0.1.0/src/gilt/gui/widgets/smart_category_combo.py +122 -0
  93. gilt_cli-0.1.0/src/gilt/gui/widgets/transaction_table.py +194 -0
  94. gilt_cli-0.1.0/src/gilt/ml/__init__.py +18 -0
  95. gilt_cli-0.1.0/src/gilt/ml/categorization_classifier.py +213 -0
  96. gilt_cli-0.1.0/src/gilt/ml/categorization_training_builder.py +196 -0
  97. gilt_cli-0.1.0/src/gilt/ml/duplicate_classifier.py +278 -0
  98. gilt_cli-0.1.0/src/gilt/ml/feature_extractor.py +208 -0
  99. gilt_cli-0.1.0/src/gilt/ml/training_data_builder.py +147 -0
  100. gilt_cli-0.1.0/src/gilt/model/__init__.py +31 -0
  101. gilt_cli-0.1.0/src/gilt/model/account.py +253 -0
  102. gilt_cli-0.1.0/src/gilt/model/category.py +133 -0
  103. gilt_cli-0.1.0/src/gilt/model/category_io.py +109 -0
  104. gilt_cli-0.1.0/src/gilt/model/duplicate.py +81 -0
  105. gilt_cli-0.1.0/src/gilt/model/events.py +435 -0
  106. gilt_cli-0.1.0/src/gilt/model/ledger_io.py +349 -0
  107. gilt_cli-0.1.0/src/gilt/scripts/migrate_event_schema.py +147 -0
  108. gilt_cli-0.1.0/src/gilt/services/__init__.py +51 -0
  109. gilt_cli-0.1.0/src/gilt/services/budget_service.py +326 -0
  110. gilt_cli-0.1.0/src/gilt/services/categorization_service.py +299 -0
  111. gilt_cli-0.1.0/src/gilt/services/category_management_service.py +384 -0
  112. gilt_cli-0.1.0/src/gilt/services/duplicate_review_service.py +265 -0
  113. gilt_cli-0.1.0/src/gilt/services/duplicate_service.py +152 -0
  114. gilt_cli-0.1.0/src/gilt/services/event_migration_service.py +405 -0
  115. gilt_cli-0.1.0/src/gilt/services/event_sourcing_service.py +205 -0
  116. gilt_cli-0.1.0/src/gilt/services/ingestion_service.py +149 -0
  117. gilt_cli-0.1.0/src/gilt/services/smart_category_service.py +109 -0
  118. gilt_cli-0.1.0/src/gilt/services/transaction_operations_service.py +302 -0
  119. gilt_cli-0.1.0/src/gilt/storage/__init__.py +8 -0
  120. gilt_cli-0.1.0/src/gilt/storage/budget_projection.py +633 -0
  121. gilt_cli-0.1.0/src/gilt/storage/event_store.py +338 -0
  122. gilt_cli-0.1.0/src/gilt/storage/projection.py +450 -0
  123. gilt_cli-0.1.0/src/gilt/transfer/__init__.py +3 -0
  124. gilt_cli-0.1.0/src/gilt/transfer/duplicate_detector.py +377 -0
  125. gilt_cli-0.1.0/src/gilt/transfer/linker.py +168 -0
  126. gilt_cli-0.1.0/src/gilt/transfer/matching.py +344 -0
  127. gilt_cli-0.1.0/src/gilt/transfer/prompt_learning.py +341 -0
  128. gilt_cli-0.1.0/src/gilt/transfer/prompt_manager.py +242 -0
  129. gilt_cli-0.1.0/src/gilt/workspace.py +77 -0
  130. 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,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python:*)"
5
+ ],
6
+ "deny": [],
7
+ "ask": []
8
+ }
9
+ }
@@ -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).