@fuzzle/opencode-accountant 0.0.12 → 0.0.13-next.1

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.
@@ -0,0 +1,404 @@
1
+ # classify-statements Tool
2
+
3
+ The `classify-statements` tool organizes bank statement CSV files by automatically detecting their provider and currency, then moves them to the appropriate directories for import processing.
4
+
5
+ This tool is **restricted to the accountant agent only**.
6
+
7
+ ## Arguments
8
+
9
+ | Argument | Type | Default | Description |
10
+ | -------- | ---- | ------- | ---------------------------- |
11
+ | (none) | - | - | This tool takes no arguments |
12
+
13
+ ## Output Format
14
+
15
+ **Note on paths:** All file paths use `{paths.*}` variables configured in `config/import/providers.yaml`. Default values:
16
+
17
+ - `{paths.import}` = `import/incoming`
18
+ - `{paths.pending}` = `import/pending`
19
+ - `{paths.unrecognized}` = `import/unrecognized`
20
+
21
+ ### Success - All Files Classified
22
+
23
+ When all CSV files are successfully classified:
24
+
25
+ ```json
26
+ {
27
+ "success": true,
28
+ "classified": [
29
+ {
30
+ "filename": "transactions-ubs-2026-02.csv",
31
+ "provider": "ubs",
32
+ "currency": "chf",
33
+ "targetPath": "{paths.pending}/ubs/chf/transactions-ubs-2026-02.csv"
34
+ },
35
+ {
36
+ "filename": "account-statement_2026-02.csv",
37
+ "provider": "revolut",
38
+ "currency": "eur",
39
+ "targetPath": "{paths.pending}/revolut/eur/account-statement_2026-02.csv"
40
+ }
41
+ ],
42
+ "unrecognized": [],
43
+ "summary": {
44
+ "total": 2,
45
+ "classified": 2,
46
+ "unrecognized": 0
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### Success - With Filename Renaming
52
+
53
+ When provider config includes `renamePattern` with metadata extraction:
54
+
55
+ ```json
56
+ {
57
+ "success": true,
58
+ "classified": [
59
+ {
60
+ "filename": "transactions-ubs-0235-90250546.csv",
61
+ "originalFilename": "export.csv",
62
+ "provider": "ubs",
63
+ "currency": "chf",
64
+ "targetPath": "{paths.pending}/ubs/chf/transactions-ubs-0235-90250546.csv"
65
+ }
66
+ ],
67
+ "unrecognized": [],
68
+ "summary": {
69
+ "total": 1,
70
+ "classified": 1,
71
+ "unrecognized": 0
72
+ }
73
+ }
74
+ ```
75
+
76
+ The `originalFilename` field appears when the file was renamed using metadata extraction.
77
+
78
+ ### Success - Some Files Unrecognized
79
+
80
+ When some files cannot be classified:
81
+
82
+ ```json
83
+ {
84
+ "success": true,
85
+ "classified": [
86
+ {
87
+ "filename": "transactions-ubs-2026-02.csv",
88
+ "provider": "ubs",
89
+ "currency": "chf",
90
+ "targetPath": "{paths.pending}/ubs/chf/transactions-ubs-2026-02.csv"
91
+ }
92
+ ],
93
+ "unrecognized": [
94
+ {
95
+ "filename": "mystery-bank.csv",
96
+ "targetPath": "{paths.unrecognized}/mystery-bank.csv"
97
+ }
98
+ ],
99
+ "summary": {
100
+ "total": 2,
101
+ "classified": 1,
102
+ "unrecognized": 1
103
+ }
104
+ }
105
+ ```
106
+
107
+ Unrecognized files are moved to `{paths.unrecognized}` for manual review.
108
+
109
+ ### Failure - File Collisions
110
+
111
+ When target files already exist (prevents overwriting):
112
+
113
+ ```json
114
+ {
115
+ "success": false,
116
+ "error": "Cannot classify: 1 file(s) would overwrite existing pending files.",
117
+ "collisions": [
118
+ {
119
+ "filename": "transactions.csv",
120
+ "existingPath": "{paths.pending}/ubs/chf/transactions.csv"
121
+ }
122
+ ],
123
+ "classified": [],
124
+ "unrecognized": []
125
+ }
126
+ ```
127
+
128
+ **Important:** The tool uses a two-pass approach (detect → check collisions → move) to prevent partial classification. If ANY collision is detected, NO files are moved.
129
+
130
+ ### Configuration Error
131
+
132
+ When `config/import/providers.yaml` is missing or invalid:
133
+
134
+ ```json
135
+ {
136
+ "success": false,
137
+ "error": "Failed to load configuration: config/import/providers.yaml not found",
138
+ "classified": [],
139
+ "unrecognized": []
140
+ }
141
+ ```
142
+
143
+ ### Agent Restriction Error
144
+
145
+ When called by the wrong agent:
146
+
147
+ ```json
148
+ {
149
+ "success": false,
150
+ "error": "This tool is restricted to the accountant agent only.",
151
+ "hint": "Use: Task(subagent_type='accountant', prompt='classify statements')",
152
+ "caller": "main assistant",
153
+ "classified": [],
154
+ "unrecognized": []
155
+ }
156
+ ```
157
+
158
+ ## Provider Detection
159
+
160
+ The tool detects providers using rules defined in `config/import/providers.yaml`:
161
+
162
+ ### Detection Methods
163
+
164
+ 1. **Filename Pattern** (optional): Regex match against filename
165
+ 2. **CSV Header** (required): Exact match of CSV header row
166
+ 3. **Currency Field** (required): Which column contains the currency
167
+
168
+ ### Detection Example
169
+
170
+ ```yaml
171
+ providers:
172
+ revolut:
173
+ detect:
174
+ - filenamePattern: '^account-statement_'
175
+ header: 'Type,Product,Started Date,Completed Date,Description,Amount,Fee,Currency,State,Balance'
176
+ currencyField: Currency
177
+ currencies:
178
+ CHF: chf
179
+ EUR: eur
180
+ ```
181
+
182
+ Detection process:
183
+
184
+ 1. Check if filename matches `filenamePattern` (if specified)
185
+ 2. Read CSV and check if header matches exactly
186
+ 3. Determine currency from `currencyField` column
187
+ 4. Map raw currency value (e.g., "EUR") to normalized folder name (e.g., "eur")
188
+
189
+ ### Filename Renaming
190
+
191
+ Providers can specify `renamePattern` with metadata extraction:
192
+
193
+ ```yaml
194
+ ubs:
195
+ detect:
196
+ - header: 'Trade date,Trade time,...'
197
+ currencyField: Currency
198
+ skipRows: 9
199
+ delimiter: ';'
200
+ renamePattern: 'transactions-ubs-{account-number}.csv'
201
+ metadata:
202
+ - field: account-number
203
+ row: 0
204
+ column: 1
205
+ normalize: spaces-to-dashes
206
+ ```
207
+
208
+ This extracts metadata from the CSV (e.g., account number from row 0, column 1) and uses it in the output filename.
209
+
210
+ ## Directory Structure
211
+
212
+ ```
213
+ your-project/
214
+ ├── config/
215
+ │ └── import/
216
+ │ └── providers.yaml # Defines all paths and detection rules
217
+ ├── {paths.import}/ # Drop CSV files here (default: import/incoming)
218
+ │ ├── bank1.csv
219
+ │ └── bank2.csv
220
+
221
+ ├── {paths.pending}/ # Classified files (default: import/pending)
222
+ │ ├── <provider>/ # e.g., revolut, ubs
223
+ │ │ └── <currency>/ # e.g., chf, eur, usd, btc
224
+ │ │ └── classified.csv
225
+ │ ├── ubs/
226
+ │ │ └── chf/
227
+ │ │ └── transactions-ubs-0235-90250546.csv
228
+ │ └── revolut/
229
+ │ └── eur/
230
+ │ └── account-statement_2026-02.csv
231
+
232
+ └── {paths.unrecognized}/ # Unclassified files (default: import/unrecognized)
233
+ └── mystery-bank.csv
234
+ ```
235
+
236
+ ## Typical Workflow
237
+
238
+ ### Scenario 1: Successful Classification
239
+
240
+ 1. Drop CSV files into `{paths.import}/` (e.g., `import/incoming/`)
241
+ 2. Run `classify-statements` tool (no arguments)
242
+ 3. Check output - all files classified successfully
243
+ 4. Files organized in `{paths.pending}/<provider>/<currency>/`
244
+ 5. Proceed to `import-statements` tool
245
+
246
+ ### Scenario 2: Handling Unrecognized Files
247
+
248
+ 1. Run `classify-statements` tool
249
+ 2. Review `unrecognized` array in output
250
+ 3. Check files in `{paths.unrecognized}/` directory
251
+ 4. Options to resolve:
252
+ - **Add provider config**: Update `config/import/providers.yaml` with detection rules
253
+ - **Manual classification**: Move file to correct `{paths.pending}/<provider>/<currency>/` directory
254
+ - **Investigate format**: Check if CSV format matches expected patterns
255
+ 5. Re-run tool after adding configuration
256
+
257
+ ### Scenario 3: Resolving Collisions
258
+
259
+ 1. Run `classify-statements` tool
260
+ 2. Tool reports collision - file would overwrite existing file
261
+ 3. Check `collisions` array for affected files
262
+ 4. Options to resolve:
263
+ - **Archive existing**: Move existing pending file to `{paths.done}/` if already processed
264
+ - **Rename**: Rename one of the conflicting files
265
+ - **Remove**: Delete duplicate file if confirmed redundant
266
+ 5. Re-run tool after resolving collision
267
+
268
+ **Important:** No files are moved until ALL collisions are resolved. This prevents partial/inconsistent state.
269
+
270
+ ## Handling Unrecognized Files
271
+
272
+ ### What "Unrecognized" Means
273
+
274
+ A file is unrecognized when:
275
+
276
+ - Filename doesn't match any `filenamePattern` (if patterns are specified)
277
+ - CSV header doesn't match any configured provider's `header`
278
+ - CSV is malformed or has unexpected structure
279
+ - Currency value doesn't map to configured currencies
280
+
281
+ ### Common Causes
282
+
283
+ | Cause | Solution |
284
+ | ------------------------- | ------------------------------------------------------------------------------- |
285
+ | New bank/provider | Add provider config to `config/import/providers.yaml` |
286
+ | Non-standard CSV format | Check CSV structure; add detection rules with correct header/skipRows/delimiter |
287
+ | Filename pattern mismatch | Update `filenamePattern` or remove it (header-only detection) |
288
+ | Unknown currency | Add currency mapping to provider's `currencies` section |
289
+ | Metadata in header rows | Use `skipRows` to skip non-CSV rows before header |
290
+ | Wrong delimiter | Specify `delimiter` (e.g., `";"` for semicolon-delimited) |
291
+
292
+ ### Adding Provider Detection
293
+
294
+ Example: Adding a new bank called "SwissBank":
295
+
296
+ ```yaml
297
+ providers:
298
+ swissbank:
299
+ detect:
300
+ - filenamePattern: '^swissbank-'
301
+ header: 'Date,Description,Amount,Balance,Currency'
302
+ currencyField: Currency
303
+ currencies:
304
+ CHF: chf
305
+ EUR: eur
306
+ ```
307
+
308
+ After updating config, re-run `classify-statements` to classify previously unrecognized files.
309
+
310
+ ## Collision Safety
311
+
312
+ The tool uses a **two-pass approach** to ensure atomic operations:
313
+
314
+ ### Two-Pass Process
315
+
316
+ **Pass 1: Detection & Planning**
317
+
318
+ - Scan all CSV files in `{paths.import}/`
319
+ - Detect provider/currency for each file
320
+ - Determine target path for each file
321
+ - Build complete move plan
322
+
323
+ **Pass 2: Collision Check**
324
+
325
+ - Check if ANY target file already exists
326
+ - If collisions found: abort with error (no files moved)
327
+ - If no collisions: proceed to Pass 3
328
+
329
+ **Pass 3: Move Files**
330
+
331
+ - Execute all planned moves atomically
332
+ - All files moved successfully or none at all
333
+
334
+ ### Why This Matters
335
+
336
+ Without collision checking, partial classification could occur:
337
+
338
+ - Some files moved, others fail mid-process
339
+ - Inconsistent state requiring manual cleanup
340
+ - Risk of lost data or confusion about what was processed
341
+
342
+ With two-pass approach:
343
+
344
+ - All-or-nothing operation
345
+ - Easy to retry after fixing collisions
346
+ - No partial/inconsistent states
347
+
348
+ ### Resolving Collisions
349
+
350
+ Check the `collisions` array in the error output:
351
+
352
+ ```json
353
+ "collisions": [
354
+ {
355
+ "filename": "transactions.csv",
356
+ "existingPath": "{paths.pending}/ubs/chf/transactions.csv"
357
+ }
358
+ ]
359
+ ```
360
+
361
+ Then:
362
+
363
+ 1. Inspect existing file: `cat {paths.pending}/ubs/chf/transactions.csv`
364
+ 2. Determine if it's already processed (check if transactions were imported)
365
+ 3. If processed: Move to `{paths.done}/` or delete
366
+ 4. If not processed: Rename one of the files or merge manually
367
+ 5. Re-run `classify-statements`
368
+
369
+ ## Error Handling
370
+
371
+ ### Common Errors
372
+
373
+ | Error | Cause | Solution |
374
+ | ------------------- | ------------------------------------------------- | --------------------------------------------------------------------- |
375
+ | File collision | Target file already exists in pending directory | Move existing file to done, rename, or delete; then re-run |
376
+ | Configuration error | Missing or invalid `config/import/providers.yaml` | Ensure config file exists with proper YAML syntax and required fields |
377
+ | Agent restriction | Called by wrong agent | Use `Task(subagent_type='accountant', prompt='classify statements')` |
378
+ | Permission error | Cannot read/write directories | Check file permissions on import/pending/unrecognized directories |
379
+ | No CSV files found | Import directory is empty | Add CSV files to `{paths.import}` directory first |
380
+ | CSV parsing error | Malformed CSV file | Check CSV structure; ensure proper delimiter and header row |
381
+
382
+ ### Configuration File Required Fields
383
+
384
+ Ensure `config/import/providers.yaml` contains:
385
+
386
+ ```yaml
387
+ paths:
388
+ import: <path> # Required
389
+ pending: <path> # Required
390
+ done: <path> # Required (used by import-statements tool)
391
+ unrecognized: <path> # Required
392
+ rules: <path> # Required (used by import-statements tool)
393
+
394
+ providers:
395
+ <provider-name>:
396
+ detect:
397
+ - header: <exact-csv-header> # Required
398
+ currencyField: <column-name> # Required
399
+ # Optional: filenamePattern, skipRows, delimiter, renamePattern, metadata
400
+ currencies:
401
+ <RAW-VALUE>: <normalized-folder> # Required (at least one)
402
+ ```
403
+
404
+ Missing any required field will cause a configuration error.
@@ -0,0 +1,305 @@
1
+ # import-statements Tool
2
+
3
+ The `import-statements` tool imports classified CSV bank statements into hledger using rules files. It operates in two modes:
4
+
5
+ - **Check mode** (`checkOnly: true`, default): Validates transactions and reports any that cannot be categorized
6
+ - **Import mode** (`checkOnly: false`): Imports validated transactions and moves processed files to the done directory
7
+
8
+ ## Year-Based Journal Routing
9
+
10
+ Transactions are automatically routed to year-specific journal files based on transaction dates:
11
+
12
+ - Transactions from 2025 → `ledger/2025.journal`
13
+ - Transactions from 2026 → `ledger/2026.journal`
14
+
15
+ **Automatic setup:**
16
+
17
+ - If the year journal doesn't exist, it is created automatically
18
+ - The include directive (`include ledger/YYYY.journal`) is added to `.hledger.journal` if not already present
19
+
20
+ **Constraint:** Each CSV file must contain transactions from a single year. CSVs with transactions spanning multiple years are rejected during check mode with an error message listing the years found.
21
+
22
+ ## Arguments
23
+
24
+ | Argument | Type | Default | Description |
25
+ | ----------- | ------- | ------- | ------------------------------------------- |
26
+ | `provider` | string | - | Filter by provider (e.g., `revolut`, `ubs`) |
27
+ | `currency` | string | - | Filter by currency (e.g., `chf`, `eur`) |
28
+ | `checkOnly` | boolean | `true` | If true, only validate without importing |
29
+
30
+ ## Output Format
31
+
32
+ **Note on paths:** All file paths in the examples below use `{paths.*}` variables. These are configured in `config/import/providers.yaml`. Default values are:
33
+
34
+ - `{paths.pending}` = `import/pending`
35
+ - `{paths.done}` = `import/done`
36
+ - `{paths.rules}` = `ledger/rules`
37
+
38
+ ### Check Mode - All Transactions Matched
39
+
40
+ When all transactions have matching rules:
41
+
42
+ ```json
43
+ {
44
+ "success": true,
45
+ "files": [
46
+ {
47
+ "csv": "{paths.pending}/ubs/chf/transactions-ubs-0235-90250546.0.csv",
48
+ "rulesFile": "{paths.rules}/ubs-0235-90250546.0.rules",
49
+ "transactions": 25,
50
+ "unknownPostings": [],
51
+ "transactionYear": 2026
52
+ }
53
+ ],
54
+ "summary": {
55
+ "filesProcessed": 1,
56
+ "totalTransactions": 25,
57
+ "matched": 25,
58
+ "unknown": 0
59
+ },
60
+ "message": "All transactions matched. Ready to import with checkOnly: false"
61
+ }
62
+ ```
63
+
64
+ ### Check Mode - Unknown Postings Found
65
+
66
+ When transactions don't match any `if` pattern in the rules file, the tool returns the full CSV row data for each unknown posting to provide context for classification:
67
+
68
+ ```json
69
+ {
70
+ "success": false,
71
+ "files": [
72
+ {
73
+ "csv": "{paths.pending}/ubs/chf/transactions-ubs-0235-90250546.0.csv",
74
+ "rulesFile": "{paths.rules}/ubs-0235-90250546.0.rules",
75
+ "transactions": 25,
76
+ "unknownPostings": [
77
+ {
78
+ "date": "2026-01-16",
79
+ "description": "Connor, John",
80
+ "amount": "CHF95.25",
81
+ "account": "income:unknown",
82
+ "csvRow": {
83
+ "trade_date": "2026-01-16",
84
+ "trade_time": "",
85
+ "booking_date": "2026-01-16",
86
+ "value_date": "2026-01-16",
87
+ "currency": "CHF",
88
+ "debit": "",
89
+ "credit": "95.25",
90
+ "individual_amount": "",
91
+ "balance": "4746.23",
92
+ "transaction_no": "ABC123",
93
+ "description1": "Connor, John",
94
+ "description2": "Twint deposit",
95
+ "description3": "Ref: TW-12345",
96
+ "footnotes": ""
97
+ }
98
+ },
99
+ {
100
+ "date": "2026-01-30",
101
+ "description": "Balance closing of service prices",
102
+ "amount": "CHF-10.00",
103
+ "account": "expenses:unknown",
104
+ "csvRow": {
105
+ "trade_date": "2026-01-30",
106
+ "trade_time": "",
107
+ "booking_date": "2026-01-30",
108
+ "value_date": "2026-01-30",
109
+ "currency": "CHF",
110
+ "debit": "10.00",
111
+ "credit": "",
112
+ "individual_amount": "",
113
+ "balance": "2364.69",
114
+ "transaction_no": "DEF456",
115
+ "description1": "Balance closing of service prices",
116
+ "description2": "",
117
+ "description3": "",
118
+ "footnotes": ""
119
+ }
120
+ }
121
+ ]
122
+ }
123
+ ],
124
+ "summary": {
125
+ "filesProcessed": 1,
126
+ "totalTransactions": 25,
127
+ "matched": 23,
128
+ "unknown": 2
129
+ }
130
+ }
131
+ ```
132
+
133
+ ### Check Mode - Missing Rules File
134
+
135
+ When a CSV file has no matching rules file:
136
+
137
+ ```json
138
+ {
139
+ "success": false,
140
+ "files": [
141
+ {
142
+ "csv": "{paths.pending}/ubs/chf/transactions.csv",
143
+ "error": "No matching rules file found. Create a rules file with 'source' directive pointing to this CSV."
144
+ }
145
+ ],
146
+ "summary": {
147
+ "filesProcessed": 1,
148
+ "filesWithoutRules": 1
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### Import Mode - Success
154
+
155
+ When importing with all transactions matched:
156
+
157
+ ```json
158
+ {
159
+ "success": true,
160
+ "files": [
161
+ {
162
+ "csv": "{paths.pending}/ubs/chf/transactions.csv",
163
+ "rulesFile": "{paths.rules}/ubs.rules",
164
+ "imported": true,
165
+ "movedTo": "{paths.done}/ubs/chf/transactions.csv"
166
+ }
167
+ ],
168
+ "summary": {
169
+ "filesProcessed": 1,
170
+ "filesImported": 1,
171
+ "totalTransactions": 25
172
+ },
173
+ "message": "Successfully imported 1 file(s)"
174
+ }
175
+ ```
176
+
177
+ ### Import Mode - Blocked by Unknown Postings
178
+
179
+ Import mode runs a check first and aborts if any unknowns exist:
180
+
181
+ ```json
182
+ {
183
+ "success": false,
184
+ "error": "Cannot import: 2 transactions have unknown accounts. Run with checkOnly: true to see details and add rules.",
185
+ "hint": "Run with checkOnly: true first to identify and fix unknown postings"
186
+ }
187
+ ```
188
+
189
+ ## Unknown Posting Types
190
+
191
+ hledger assigns transactions to `income:unknown` or `expenses:unknown` based on the direction:
192
+
193
+ | Transaction Type | Account Assigned |
194
+ | -------------------------- | ------------------ |
195
+ | Money coming in (positive) | `income:unknown` |
196
+ | Money going out (negative) | `expenses:unknown` |
197
+
198
+ ## Fixing Unknown Postings
199
+
200
+ When the tool reports unknown postings, the `csvRow` field contains all available data from the original CSV to help determine the correct account. This includes additional description fields, transaction references, and other metadata that may help with classification.
201
+
202
+ Add `if` rules to the appropriate rules file based on the posting details:
203
+
204
+ ```
205
+ # Example: Categorize a friend's reimbursement
206
+ # (csvRow showed description2: "Twint deposit" confirming it's a payment app transfer)
207
+ if Connor, John
208
+ account1 income:reimbursements
209
+
210
+ # Example: Categorize bank service charges
211
+ if Balance closing of service prices
212
+ account1 expenses:fees:bank
213
+ ```
214
+
215
+ Then run the tool again with `checkOnly: true` to verify the rules work.
216
+
217
+ ### CSV Row Field Names
218
+
219
+ The `csvRow` object uses field names from the `fields` directive in the rules file. Common fields include:
220
+
221
+ | Field | Description |
222
+ | ---------------- | ------------------------------------------------------- |
223
+ | `trade_date` | When the transaction occurred |
224
+ | `booking_date` | When it was booked |
225
+ | `description1` | Primary description |
226
+ | `description2` | Secondary description (often useful for classification) |
227
+ | `description3` | Additional reference information |
228
+ | `transaction_no` | Unique transaction identifier |
229
+ | `debit` | Debit amount (money out) |
230
+ | `credit` | Credit amount (money in) |
231
+
232
+ The exact field names depend on your rules file configuration.
233
+
234
+ ## Error Handling
235
+
236
+ ### hledger Errors
237
+
238
+ If hledger fails to parse a CSV or rules file:
239
+
240
+ ```json
241
+ {
242
+ "success": false,
243
+ "files": [
244
+ {
245
+ "csv": "{paths.pending}/ubs/chf/transactions.csv",
246
+ "rulesFile": "{paths.rules}/ubs.rules",
247
+ "error": "hledger error: Parse error at line 5: invalid date format"
248
+ }
249
+ ],
250
+ "summary": {
251
+ "filesProcessed": 1,
252
+ "filesWithErrors": 1
253
+ }
254
+ }
255
+ ```
256
+
257
+ ### Configuration Errors
258
+
259
+ If the config file is missing or invalid:
260
+
261
+ ```json
262
+ {
263
+ "success": false,
264
+ "error": "Failed to load configuration: Configuration file not found: config/import/providers.yaml",
265
+ "hint": "Ensure config/import/providers.yaml exists with a 'rules' path configured"
266
+ }
267
+ ```
268
+
269
+ ### Multi-Year CSV Error
270
+
271
+ If a CSV contains transactions from multiple years:
272
+
273
+ ```json
274
+ {
275
+ "success": false,
276
+ "files": [
277
+ {
278
+ "csv": "{paths.pending}/ubs/chf/transactions.csv",
279
+ "rulesFile": "{paths.rules}/ubs.rules",
280
+ "totalTransactions": 10,
281
+ "matchedTransactions": 10,
282
+ "unknownPostings": [],
283
+ "error": "CSV contains transactions from multiple years (2025, 2026). Split the CSV by year before importing."
284
+ }
285
+ ],
286
+ "summary": {
287
+ "filesProcessed": 1,
288
+ "filesWithErrors": 1,
289
+ "totalTransactions": 0,
290
+ "matched": 0,
291
+ "unknown": 0
292
+ }
293
+ }
294
+ ```
295
+
296
+ ### Missing Main Journal
297
+
298
+ If `.hledger.journal` doesn't exist when attempting import:
299
+
300
+ ```json
301
+ {
302
+ "success": false,
303
+ "error": ".hledger.journal not found at /path/to/.hledger.journal. Create it first with appropriate includes."
304
+ }
305
+ ```