@dizzlkheinz/ynab-mcpb 0.17.0 → 0.17.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.
Files changed (142) hide show
  1. package/.env.example +33 -33
  2. package/.github/workflows/ci-tests.yml +45 -45
  3. package/.github/workflows/claude-code-review.yml +57 -57
  4. package/.github/workflows/claude.yml +50 -50
  5. package/.github/workflows/full-integration.yml +22 -22
  6. package/.github/workflows/publish.yml +11 -2
  7. package/CLAUDE.md +7 -6
  8. package/dist/bundle/index.cjs +52 -52
  9. package/dist/server/YNABMCPServer.d.ts +120 -54
  10. package/dist/server/securityMiddleware.d.ts +37 -8
  11. package/dist/tools/schemas/outputs/index.d.ts +2 -2
  12. package/dist/tools/schemas/outputs/index.js +2 -2
  13. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +0 -15
  14. package/dist/tools/schemas/outputs/utilityOutputs.js +0 -9
  15. package/dist/tools/utilityTools.d.ts +0 -7
  16. package/dist/tools/utilityTools.js +1 -50
  17. package/docs/maintainers/npm-publishing.md +27 -0
  18. package/docs/reference/API.md +15 -70
  19. package/docs/technical/reconciliation-system-architecture.md +2251 -2251
  20. package/package.json +5 -5
  21. package/scripts/analyze-bundle.mjs +41 -41
  22. package/scripts/generate-mcpb.ps1 +95 -95
  23. package/scripts/watch-and-restart.ps1 +49 -49
  24. package/src/__tests__/comprehensive.integration.test.ts +0 -28
  25. package/src/__tests__/performance.test.ts +4 -12
  26. package/src/__tests__/setup.ts +45 -14
  27. package/src/__tests__/workflows.e2e.test.ts +0 -44
  28. package/src/server/__tests__/YNABMCPServer.test.ts +0 -1
  29. package/src/server/__tests__/toolRegistration.test.ts +2 -2
  30. package/src/tools/__tests__/transactionTools.integration.test.ts +63 -3
  31. package/src/tools/__tests__/utilityTools.integration.test.ts +1 -85
  32. package/src/tools/__tests__/utilityTools.test.ts +1 -123
  33. package/src/tools/schemas/outputs/index.ts +0 -3
  34. package/src/tools/schemas/outputs/utilityOutputs.ts +2 -43
  35. package/src/tools/toolCategories.ts +0 -1
  36. package/src/tools/utilityTools.ts +5 -76
  37. package/vitest.config.ts +2 -1
  38. package/.chunkhound.json +0 -11
  39. package/.code/agents/0098661e-0fa3-4990-beb9-c0cbf3f123aa/status.txt +0 -1
  40. package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +0 -3
  41. package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +0 -3
  42. package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +0 -1
  43. package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +0 -5
  44. package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +0 -1569
  45. package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +0 -3
  46. package/.code/agents/1324/exec-call_tIpx9uV1TpARbAMZonRQm8AO.txt +0 -757
  47. package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +0 -2832
  48. package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +0 -2709
  49. package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +0 -2832
  50. package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +0 -2832
  51. package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +0 -2709
  52. package/.code/agents/1572/exec-call_GjVFBFOWcY7lE0idc5nWlLNh.txt +0 -781
  53. package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +0 -1
  54. package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +0 -5217
  55. package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +0 -2594
  56. package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +0 -2594
  57. package/.code/agents/1846/exec-call_1YNAVD18RjrMN7JnfkkQhUP3.txt +0 -766
  58. package/.code/agents/1846/exec-call_lh3lDzE4WJAh1lFiomiiZ73D.txt +0 -766
  59. package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +0 -231
  60. package/.code/agents/2038/exec-call_DYwOukaYsL8VCONWmV2rUW5u.txt +0 -766
  61. package/.code/agents/2038/exec-call_c7fOQ7UrpVcTtvdfGBRM146V.txt +0 -652
  62. package/.code/agents/2038/exec-call_ySNyq9Mm55jWE480s54r5QcA.txt +0 -766
  63. package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +0 -2590
  64. package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +0 -5195
  65. package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +0 -286
  66. package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +0 -218
  67. package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +0 -180
  68. package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +0 -3
  69. package/.code/agents/2256/exec-call_AtPcRWPmFPMcmX6qOFm1fCEY.txt +0 -766
  70. package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +0 -1
  71. package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +0 -747
  72. package/.code/agents/2454/exec-call_aFJpupwjfZeOBm7ixI5Vc8z2.txt +0 -766
  73. package/.code/agents/2454/exec-call_wogZ4HfXTodTEXvdgXlVUBpv.txt +0 -766
  74. package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +0 -1
  75. package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +0 -2594
  76. package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +0 -2594
  77. package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +0 -144
  78. package/.code/agents/2e905864-aa07-4314-bcf9-c5b32277e4ac/result.txt +0 -36
  79. package/.code/agents/3073/exec-call_Peeagc9DxGYLgE6pNdMZhqIE.txt +0 -766
  80. package/.code/agents/3073/exec-call_d2YSE3hXF08KRSoUM3qd8Z3x.txt +0 -766
  81. package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +0 -416
  82. package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +0 -2590
  83. package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +0 -2590
  84. package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +0 -2590
  85. package/.code/agents/335aa031-466d-4fb7-925f-3cd864e264d0/result.txt +0 -191
  86. package/.code/agents/3364/exec-call_NbhIrsM5HhyDZDmJZG5CuCYL.txt +0 -766
  87. package/.code/agents/3364/exec-call_cKtJg0NrXiwXEFwlsE3uPZRA.txt +0 -766
  88. package/.code/agents/36d98414-5cde-4d9d-9a67-a240a18c1f07/result.txt +0 -189
  89. package/.code/agents/4604e866-b7b8-44f5-992f-2f683b0a523b/status.txt +0 -1
  90. package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +0 -790
  91. package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +0 -766
  92. package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +0 -790
  93. package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +0 -2594
  94. package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +0 -1000
  95. package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +0 -3489
  96. package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +0 -766
  97. package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +0 -2594
  98. package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +0 -2456
  99. package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +0 -2594
  100. package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +0 -18
  101. package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +0 -48
  102. package/.code/agents/5f8dc01c-47b3-4163-b0b3-aa31be89fcdc/status.txt +0 -1
  103. package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +0 -1
  104. package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +0 -1
  105. package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +0 -1
  106. package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +0 -1
  107. package/.code/agents/7/exec-call_HltHpkDox0Zm1vGEjdksUgpE.txt +0 -1120
  108. package/.code/agents/7/exec-call_LCATrOPPAgbxW9Q1z0XaVi2E.txt +0 -2646
  109. package/.code/agents/7/exec-call_W8DeRfNG9hvbgVFvf0clBf6R.txt +0 -2646
  110. package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +0 -1271
  111. package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +0 -1570
  112. package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +0 -2590
  113. package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +0 -3
  114. package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +0 -3299
  115. package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +0 -3299
  116. package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +0 -1882
  117. package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +0 -2594
  118. package/.code/agents/94a0ddf3-a304-4ec3-913e-3cceef509948/error.txt +0 -1
  119. package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +0 -1
  120. package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +0 -170
  121. package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +0 -1
  122. package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +0 -3
  123. package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +0 -1
  124. package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +0 -1
  125. package/.code/agents/e2c752b7-711d-423a-af57-f53c809deb84/result.txt +0 -160
  126. package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +0 -3
  127. package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +0 -1
  128. package/.code/agents/e6601719-c31f-4a0e-8c71-d70787d0ab71/status.txt +0 -1
  129. package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +0 -1
  130. package/.code/agents/f250b7ed-5bd5-4036-aa8c-ce63caee7d61/result.txt +0 -20
  131. package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +0 -5
  132. package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +0 -1
  133. package/AGENTS.md +0 -1
  134. package/NUL +0 -0
  135. package/package.json.tmp +0 -105
  136. package/temp-recon.ts +0 -126
  137. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +0 -23
  138. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +0 -23
  139. package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +0 -44
  140. package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +0 -58
  141. package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +0 -3662
  142. package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +0 -115
@@ -1,2251 +1,2251 @@
1
- # YNAB Reconciliation System - Technical Architecture Documentation
2
-
3
- **Document Version:** 2.0
4
- **Last Updated:** 2025-12-01
5
- **Code Version Compatibility:** v0.16.0+
6
- **Last Verified Against Code:** v0.16.0 (2025-12-01)
7
- **Status:** Active Implementation
8
-
9
- ---
10
-
11
- ## Executive Summary
12
-
13
- The YNAB Reconciliation System is a sophisticated transaction matching and balance verification engine that automatically reconciles bank statement CSV files against YNAB transactions. The system achieves 90%+ auto-match accuracy through intelligent fuzzy matching, progressive confidence scoring, and robust CSV parsing with multi-bank format support.
14
-
15
- **Key Capabilities:**
16
- - Automated transaction matching with 85%+ confidence threshold
17
- - Multi-bank CSV format detection (TD, RBC, Scotiabank, Wealthsimple, Tangerine)
18
- - Integer-based milliunit arithmetic (eliminates floating-point errors)
19
- - Bulk transaction operations with correlation tracking
20
- - Smart sign detection for liability accounts
21
- - Actionable recommendations with prioritization
22
-
23
- **Target Accuracy:** 90%+ auto-match rate for Canadian bank statements
24
-
25
- ---
26
-
27
- ## Table of Contents
28
-
29
- 1. [System Architecture](#1-system-architecture)
30
- 2. [Core Components](#2-core-components)
31
- 3. [Data Flow](#3-data-flow)
32
- 4. [Transaction Matching Algorithm](#4-transaction-matching-algorithm)
33
- 5. [CSV Parsing Engine](#5-csv-parsing-engine)
34
- 6. [Execution Engine](#6-execution-engine)
35
- 7. [Type System](#7-type-system)
36
- 8. [Integration Points](#8-integration-points)
37
- 9. [Performance Characteristics](#9-performance-characteristics)
38
- 10. [Testing Strategy](#10-testing-strategy)
39
-
40
- ---
41
-
42
- ## 1. System Architecture
43
-
44
- ### 1.1 High-Level Architecture
45
-
46
- ```mermaid
47
- graph TB
48
- A[MCP Tool Entry Point] --> B[index.ts Handler]
49
- B --> C{Parse CSV}
50
- C --> D[csvParser.ts]
51
- D --> E[BankTransaction[]]
52
-
53
- B --> F{Fetch YNAB Data}
54
- F --> G[DeltaFetcher]
55
- G --> H[ynabAdapter.ts]
56
- H --> I[NormalizedYNABTransaction[]]
57
-
58
- E --> J[analyzer.ts]
59
- I --> J
60
-
61
- J --> K[matcher.ts]
62
- K --> L[Match Results]
63
-
64
- J --> M[recommendationEngine.ts]
65
- M --> N[Actionable Recommendations]
66
-
67
- L --> O{Execute Actions?}
68
- N --> O
69
-
70
- O -->|Yes| P[executor.ts]
71
- P --> Q[Bulk Operations]
72
- Q --> R[YNAB API Updates]
73
-
74
- O -->|No| S[reportFormatter.ts]
75
- L --> S
76
- N --> S
77
- S --> T[Human-Readable Report]
78
-
79
- R --> S
80
-
81
- T --> U[MCP Response]
82
- ```
83
-
84
- ### 1.2 Component Responsibility Matrix
85
-
86
- | Component | Primary Role | Inputs | Outputs | Dependencies |
87
- |-----------|-------------|--------|---------|--------------|
88
- | **index.ts** | Orchestration | MCP Tool Request | CallToolResult | All components |
89
- | **csvParser.ts** | CSV Parsing | Raw CSV string | BankTransaction[] | PapaParse, chrono-node |
90
- | **ynabAdapter.ts** | YNAB Normalization | TransactionDetail[] | NormalizedYNABTransaction[] | YNAB SDK |
91
- | **matcher.ts** | Matching Algorithm | Bank + YNAB Transactions | MatchResult[] | fuzzball |
92
- | **analyzer.ts** | Analysis Orchestration | Parsed Data | ReconciliationAnalysis | matcher, recommendationEngine |
93
- | **executor.ts** | Action Execution | Analysis + Params | ExecutionResult | YNAB API, transactionTools |
94
- | **recommendationEngine.ts** | Recommendation Generation | Analysis | ActionableRecommendation[] | None (pure logic) |
95
- | **reportFormatter.ts** | Human-Readable Output | Analysis + Execution | Formatted Report | None (pure formatting) |
96
-
97
- ### 1.3 Data Architecture
98
-
99
- ```mermaid
100
- graph LR
101
- A[CSV Float Dollars] -->|parseAmount| B[BankTransaction Milliunits]
102
- C[YNAB API Milliunits] -->|normalizeYNABTransaction| D[NormalizedYNABTransaction]
103
-
104
- B --> E[Matcher Integer Comparison]
105
- D --> E
106
-
107
- E -->|amount === amount| F[Exact Match 100 score]
108
- E -->|Math.abs diff <= tolerance| G[Tolerance Match 95 score]
109
-
110
- F --> H[High Confidence 85+]
111
- G --> H
112
-
113
- H --> I[Auto-Match Execution]
114
- ```
115
-
116
- **Critical Design Decision:** All amounts use **milliunits (integers)** throughout the system. Conversion from CSV floats happens once at the parser boundary, then all comparisons use exact integer arithmetic (`===` instead of `Math.abs(a - b) < epsilon`).
117
-
118
- ---
119
-
120
- ## 2. Core Components
121
-
122
- ### 2.1 Entry Point: index.ts
123
-
124
- **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\index.ts`
125
-
126
- **Responsibilities:**
127
- - MCP tool interface implementation
128
- - Schema validation (ReconcileAccountSchema)
129
- - CSV vs File input handling
130
- - Date range auto-detection
131
- - Smart sign inversion for liability accounts
132
- - Orchestration of analysis and execution phases
133
-
134
- **Key Functions:**
135
-
136
- ```typescript
137
- export async function handleReconcileAccount(
138
- ynabAPI: ynab.API,
139
- deltaFetcher: DeltaFetcher,
140
- params: ReconcileAccountRequest,
141
- ): Promise<CallToolResult>
142
- ```
143
-
144
- **Decision Tree:**
145
-
146
- ```mermaid
147
- graph TD
148
- A[handleReconcileAccount] --> B{CSV Source?}
149
- B -->|csv_data| C[Use inline CSV]
150
- B -->|csv_file_path| D[Read from filesystem]
151
-
152
- C --> E{Account Type?}
153
- D --> E
154
-
155
- E -->|Liability| F[Negate statement_balance]
156
- E -->|Asset| G[Use statement_balance as-is]
157
-
158
- F --> H{invert_bank_amounts set?}
159
- G --> H
160
-
161
- H -->|Yes| I[Use explicit value]
162
- H -->|No| J[Auto-detect sign inversion]
163
-
164
- I --> K[analyzeReconciliation]
165
- J --> K
166
-
167
- K --> L{Execute Actions?}
168
- L -->|auto_create/update enabled| M[executeReconciliation]
169
- L -->|Analysis only| N[Skip execution]
170
-
171
- M --> O[Build Response Payload]
172
- N --> O
173
-
174
- O --> P[Return MCP Response]
175
- ```
176
-
177
- **Configuration Mapping:**
178
-
179
- ```typescript
180
- // V2 Matching Configuration from Request Parameters
181
- const config: MatchingConfig = {
182
- weights: {
183
- amount: 0.5, // 50% weight
184
- date: 0.15, // 15% weight
185
- payee: 0.35, // 35% weight
186
- },
187
- dateToleranceDays: params.date_tolerance_days ?? 5,
188
- amountToleranceMilliunits: (params.amount_tolerance_cents ?? 1) * 10,
189
- autoMatchThreshold: params.auto_match_threshold ?? 90,
190
- suggestedMatchThreshold: params.suggestion_threshold ?? 60,
191
- minimumCandidateScore: 40,
192
- exactAmountBonus: 10,
193
- exactDateBonus: 5,
194
- exactPayeeBonus: 10,
195
- };
196
- ```
197
-
198
- ---
199
-
200
- ### 2.2 CSV Parser: csvParser.ts
201
-
202
- **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\csvParser.ts`
203
-
204
- **Purpose:** Parse bank CSV exports into standardized BankTransaction objects with robust error handling and multi-format support.
205
-
206
- **Architecture:**
207
-
208
- ```mermaid
209
- graph TD
210
- A[Raw CSV String] --> B{Security Check}
211
- B -->|Size > 10MB| C[Throw Error]
212
- B -->|Size OK| D[Auto-Detect Format]
213
-
214
- D --> E{Preset Detected?}
215
- E -->|Yes| F[Load Bank Preset]
216
- E -->|No| G[Use Default Columns]
217
-
218
- F --> H[PapaParse]
219
- G --> H
220
-
221
- H --> I{Header Row?}
222
- I -->|Yes| J[Parse with Headers]
223
- I -->|No| K[Parse by Index]
224
-
225
- J --> L[Process Each Row]
226
- K --> L
227
-
228
- L --> M{Parse Date}
229
- M -->|ISO YYYY-MM-DD| N[Direct Parse]
230
- M -->|Format Hint| O[Apply Preset Format]
231
- M -->|Fallback| P[chrono-node Parse]
232
-
233
- N --> Q{Parse Amount}
234
- O --> Q
235
- P --> Q
236
-
237
- Q -->|Single Column| R[parseAmount]
238
- Q -->|Debit/Credit| S[Combine Columns]
239
-
240
- R --> T{Warnings?}
241
- S --> U{Both Populated?}
242
-
243
- U -->|Yes| V[Emit Warning]
244
- U -->|No| T
245
- V --> T
246
-
247
- T --> W[Create BankTransaction]
248
- W --> X[Sanitize Description]
249
- X --> Y[Return Results + Errors + Warnings]
250
- ```
251
-
252
- **Bank Presets:**
253
-
254
- ```typescript
255
- export const BANK_PRESETS: Record<string, BankPreset> = {
256
- 'td': {
257
- name: 'TD Canada Trust',
258
- header: false, // Headerless CSV support
259
- dateColumn: ['0', 'Date'],
260
- debitColumn: '2',
261
- creditColumn: '3',
262
- descriptionColumn: ['1', 'Description'],
263
- dateFormat: 'MDY', // MM/DD/YYYY
264
- },
265
- 'rbc': {
266
- name: 'RBC Royal Bank',
267
- dateColumn: ['Transaction Date', 'Date'],
268
- debitColumn: 'Debit',
269
- creditColumn: 'Credit',
270
- descriptionColumn: ['Description 1', 'Description'],
271
- dateFormat: 'YMD', // YYYY-MM-DD
272
- },
273
- 'wealthsimple': {
274
- name: 'Wealthsimple',
275
- dateColumn: ['Date'],
276
- amountColumn: ['Amount'],
277
- descriptionColumn: ['Description', 'Payee'],
278
- amountMultiplier: 1, // Already in correct sign
279
- dateFormat: 'YMD',
280
- },
281
- // ... scotiabank, tangerine
282
- };
283
- ```
284
-
285
- **Amount Conversion Logic (validated via `parseAmount`):**
286
-
287
- ```typescript
288
- function parseAmount(str: string): {
289
- valid: boolean;
290
- valueMilliunits: number;
291
- reason?: string;
292
- } {
293
- if (!str || !str.trim()) {
294
- return { valid: false, valueMilliunits: 0, reason: 'Missing amount value' };
295
- }
296
-
297
- let cleaned = str.replace(/[$€£¥]/g, '').replace(/\b(CAD|USD|EUR|GBP)\b/gi, '').trim();
298
-
299
- if (cleaned.startsWith('(') && cleaned.endsWith(')')) {
300
- cleaned = '-' + cleaned.slice(1, -1);
301
- }
302
-
303
- if (/^-?\d{1,3}(\.\d{3})+,\d{2}$/.test(cleaned)) {
304
- cleaned = cleaned.replace(/\./g, '').replace(',', '.');
305
- }
306
-
307
- if (cleaned.includes('.')) {
308
- cleaned = cleaned.replace(/,/g, '');
309
- }
310
-
311
- const dollars = parseFloat(cleaned);
312
- if (!Number.isFinite(dollars)) {
313
- return { valid: false, valueMilliunits: 0, reason: `Invalid amount: "${str}"` };
314
- }
315
-
316
- return { valid: true, valueMilliunits: Math.round(dollars * 1000) };
317
- }
318
- ```
319
-
320
- **Security Measures:**
321
-
322
- ```typescript
323
- // Security: Remove malicious Unicode characters
324
- rawDesc = rawDesc
325
- .replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // ASCII + C1 control chars
326
- .replace(/[\u202A-\u202E\u2066-\u2069]/g, '') // Bidirectional overrides
327
- .replace(/[\u200B-\u200D\uFEFF]/g, '') // Zero-width chars
328
- .replace(/[\u2028-\u2029]/g, '') // Line/paragraph separators
329
- .substring(0, 500); // YNAB max memo length
330
- ```
331
-
332
- ---
333
-
334
- ### 2.3 Transaction Matcher: matcher.ts
335
-
336
- **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\matcher.ts`
337
-
338
- **Purpose:** Match bank transactions to YNAB transactions using multi-dimensional scoring with configurable thresholds.
339
-
340
- **Matching Pipeline:**
341
-
342
- ```mermaid
343
- graph TD
344
- A[BankTransaction] --> B{Sign Check}
345
- B -->|Signs Differ| C[Skip Candidate]
346
- B -->|Signs Match| D{Amount Tolerance}
347
-
348
- D -->|Diff > Tolerance| C
349
- D -->|Diff <= Tolerance| E[Calculate Scores]
350
-
351
- E --> F[Amount Score 0-100]
352
- E --> G[Date Score 0-100]
353
- E --> H[Payee Score 0-100]
354
-
355
- F --> I[Weighted Combination]
356
- G --> I
357
- H --> I
358
-
359
- I --> J{Apply Bonuses}
360
- J -->|Amount === exact| K[+10 bonus]
361
- J -->|Date === exact| L[+5 bonus]
362
- J -->|Payee >= 95| M[+10 bonus]
363
-
364
- K --> N[Final Combined Score]
365
- L --> N
366
- M --> N
367
-
368
- N --> O{Score >= Min Candidate?}
369
- O -->|No| C
370
- O -->|Yes| P[Add to Candidates]
371
-
372
- P --> Q[Sort by Score Descending]
373
- Q --> R{Top Score >= Auto-Match?}
374
-
375
- R -->|Yes| S[confidence: high]
376
- R -->|No| T{Score >= Suggested?}
377
-
378
- T -->|Yes| U[confidence: medium]
379
- T -->|No| V{Score >= Min?}
380
-
381
- V -->|Yes| W[confidence: low]
382
- V -->|No| X[confidence: none]
383
- ```
384
-
385
- **Scoring Algorithms:**
386
-
387
- **Amount Score (Integer Comparison):**
388
-
389
- ```typescript
390
- const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
391
- let amountScore: number;
392
-
393
- if (amountDiff === 0) {
394
- // Exact integer match - no floating point issues!
395
- amountScore = 100;
396
- } else if (amountDiff <= config.amountToleranceMilliunits) { // Default: 10 (1 cent)
397
- amountScore = 95;
398
- } else if (amountDiff <= 1000) { // Within $1
399
- amountScore = 80 - (amountDiff / 1000 * 20);
400
- } else {
401
- amountScore = Math.max(0, 60 - (amountDiff / 1000 * 5));
402
- }
403
- ```
404
-
405
- **Date Score (Days Difference):**
406
-
407
- ```typescript
408
- const daysDiff = Math.abs(bankDate.getTime() - ynabDate.getTime()) / (1000 * 60 * 60 * 24);
409
- let dateScore: number;
410
-
411
- if (daysDiff < 0.5) {
412
- dateScore = 100; // Same day
413
- } else if (daysDiff <= 1) {
414
- dateScore = 95; // 1 day
415
- } else if (daysDiff <= config.dateToleranceDays) { // Default: 7 days
416
- dateScore = 90 - ((daysDiff - 1) * (40 / config.dateToleranceDays));
417
- } else {
418
- dateScore = Math.max(0, 50 - ((daysDiff - config.dateToleranceDays) * 5));
419
- }
420
- ```
421
-
422
- **Payee Score (Fuzzy Matching with Fuzzball):**
423
-
424
- ```typescript
425
- import * as fuzz from 'fuzzball';
426
-
427
- function calculatePayeeScore(bankPayee: string, ynabPayee: string | null): number {
428
- if (!ynabPayee) return 30;
429
-
430
- const scores = [
431
- fuzz.token_set_ratio(bankPayee, ynabPayee), // Handles word order
432
- fuzz.token_sort_ratio(bankPayee, ynabPayee), // Alphabetizes then compares
433
- fuzz.partial_ratio(bankPayee, ynabPayee), // Best substring match
434
- fuzz.WRatio(bankPayee, ynabPayee), // Weighted combination
435
- ];
436
-
437
- return Math.max(...scores);
438
- }
439
- ```
440
-
441
- **Why Fuzzball?**
442
-
443
- | Input Bank | Input YNAB | Levenshtein | Fuzzball token_set_ratio |
444
- |------------|------------|-------------|--------------------------|
445
- | "AMZN MKTP CA*123456" | "Amazon" | 45 | 90 |
446
- | "SQ *COFFEE SHOP TORONTO" | "Square Coffee" | 38 | 85 |
447
- | "PAYPAL *NETFLIX" | "Netflix" | 54 | 100 |
448
-
449
- **Tie-Breaking Rules:**
450
-
451
- ```typescript
452
- candidates.sort((a, b) => {
453
- // 1. Sort by combined score
454
- const scoreDiff = b.scores.combined - a.scores.combined;
455
- if (scoreDiff !== 0) return scoreDiff;
456
-
457
- // 2. Prefer uncleared over cleared (expecting confirmation)
458
- const aUncleared = a.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
459
- const bUncleared = b.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
460
- if (aUncleared !== bUncleared) return bUncleared - aUncleared;
461
-
462
- // 3. Prefer closer date proximity
463
- const bankTime = new Date(bankTxn.date).getTime();
464
- const aDiff = Math.abs(bankTime - new Date(a.ynabTransaction.date).getTime());
465
- const bDiff = Math.abs(bankTime - new Date(b.ynabTransaction.date).getTime());
466
- if (aDiff !== bDiff) return aDiff - bDiff;
467
-
468
- return 0;
469
- });
470
- ```
471
-
472
- ---
473
-
474
- ### 2.4 Analyzer: analyzer.ts
475
-
476
- **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\analyzer.ts`
477
-
478
- **Purpose:** Orchestrate the analysis phase, coordinating CSV parsing, YNAB normalization, matching, and insight generation.
479
-
480
- **Analysis Pipeline:**
481
-
482
- ```mermaid
483
- graph TD
484
- A[analyzeReconciliation] --> B{CSV Pre-Parsed?}
485
- B -->|Yes| C[Use Provided Result]
486
- B -->|No| D[parseCSV]
487
-
488
- C --> E[BankTransaction[]]
489
- D --> E
490
-
491
- A --> F[normalizeYNABTransactions]
492
- F --> G[NormalizedYNABTransaction[]]
493
-
494
- E --> H[findMatches]
495
- G --> H
496
-
497
- H --> I[MatchResult[]]
498
-
499
- I --> J[Categorize by Confidence]
500
- J --> K[auto_matches: high]
501
- J --> L[suggested_matches: medium]
502
- J --> M[unmatchedBank: low/none]
503
-
504
- G --> N[Find Unmatched YNAB]
505
- I --> N
506
- N --> O[unmatched_ynab]
507
-
508
- K --> P[calculateBalances]
509
- G --> P
510
- P --> Q[BalanceInfo]
511
-
512
- K --> R[generateSummary]
513
- L --> R
514
- M --> R
515
- O --> R
516
- Q --> R
517
- R --> S[ReconciliationSummary]
518
-
519
- S --> T[generateNextSteps]
520
- T --> U[next_steps: string[]]
521
-
522
- M --> V[detectInsights]
523
- S --> V
524
- Q --> V
525
- V --> W[insights: ReconciliationInsight[]]
526
-
527
- K --> X{Recommendations Enabled?}
528
- L --> X
529
- M --> X
530
- O --> X
531
-
532
- X -->|Yes| Y[generateRecommendations]
533
- Y --> Z[recommendations: ActionableRecommendation[]]
534
-
535
- S --> AA[Build ReconciliationAnalysis]
536
- U --> AA
537
- W --> AA
538
- Z --> AA
539
-
540
- AA --> AB[Return Analysis]
541
- ```
542
-
543
- **Insight Detection:**
544
-
545
- ```typescript
546
- function detectInsights(
547
- unmatchedBank: BankTransaction[],
548
- summary: ReconciliationSummary,
549
- balances: BalanceInfo,
550
- currency: string,
551
- csvErrors: ParseError[],
552
- csvWarnings: ParseWarning[],
553
- ): ReconciliationInsight[]
554
- ```
555
-
556
- **Insight Types:**
557
-
558
- | Type | Severity | Trigger | Purpose |
559
- |------|----------|---------|---------|
560
- | `csv-parse-errors` | critical/warning | CSV parsing errors | Surface parsing failures |
561
- | `csv-parse-warnings` | info | CSV parsing warnings | Surface ambiguous data |
562
- | `repeat_amount` | critical/warning | 2+ unmatched txns same amount | Highlight quick wins |
563
- | `balance-gap` | critical/warning | Discrepancy > $1.00 | Focus attention on gap |
564
-
565
- **Example Insight:**
566
-
567
- ```typescript
568
- {
569
- id: 'repeat--45230',
570
- type: 'repeat_amount',
571
- severity: 'warning',
572
- title: '3 unmatched transactions at -$45.23',
573
- description: 'The bank statement shows 3 unmatched transaction(s) at -$45.23. Repeated amounts are usually the quickest wins — reconcile these first.',
574
- evidence: {
575
- amount: -45230, // Milliunits
576
- occurrences: 3,
577
- dates: ['2025-09-15', '2025-09-20', '2025-09-25'],
578
- csv_rows: [2, 5, 8],
579
- },
580
- }
581
- ```
582
-
583
- ---
584
-
585
- ### 2.5 Recommendation Engine: recommendationEngine.ts
586
-
587
- **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\recommendationEngine.ts`
588
-
589
- **Purpose:** Generate actionable, prioritized recommendations from reconciliation analysis results.
590
-
591
- **Recommendation Types:**
592
-
593
- ```mermaid
594
- graph TD
595
- A[ReconciliationAnalysis] --> B{Process Insights}
596
- B --> C{Insight Type?}
597
-
598
- C -->|near_match| D[createNearMatchRecommendation]
599
- C -->|repeat_amount| E[createRepeatAmountRecommendations]
600
- C -->|anomaly| F[createManualReviewRecommendation]
601
-
602
- D --> G[ManualReviewRecommendation]
603
- E --> G
604
- F --> G
605
-
606
- A --> H{Process Unmatched Bank}
607
- H --> I[createUnmatchedBankRecommendation]
608
- I --> J[CreateTransactionRecommendation]
609
-
610
- A --> K{Process Suggested Matches}
611
- K --> L{Has YNAB Transaction?}
612
-
613
- L -->|Yes + Confidence high/medium| M[createSuggestedMatchRecommendation]
614
- L -->|No| N[Create Missing Transaction]
615
- L -->|Multiple Candidates| O[Combination Match]
616
-
617
- M --> P[ReviewDuplicateRecommendation]
618
- N --> J
619
- O --> Q[createCombinationReviewRecommendation]
620
- Q --> G
621
-
622
- A --> R{Process Unmatched YNAB}
623
- R -->|Uncleared| S[createUpdateClearedRecommendation]
624
- S --> T[UpdateClearedRecommendation]
625
-
626
- G --> U[All Recommendations]
627
- J --> U
628
- P --> U
629
- T --> U
630
-
631
- U --> V[sortRecommendations]
632
- V --> W{Sort Order}
633
-
634
- W -->|1. Priority| X[High > Medium > Low]
635
- W -->|2. Confidence| Y[0.95 > 0.80 > 0.60]
636
-
637
- X --> Z[Sorted Recommendations]
638
- Y --> Z
639
- ```
640
-
641
- **Recommendation Schema:**
642
-
643
- ```typescript
644
- export type ActionableRecommendation =
645
- | CreateTransactionRecommendation
646
- | UpdateClearedRecommendation
647
- | ReviewDuplicateRecommendation
648
- | ManualReviewRecommendation;
649
-
650
- interface CreateTransactionRecommendation {
651
- action_type: 'create_transaction';
652
- priority: 'high' | 'medium' | 'low';
653
- confidence: number; // 0-1
654
- message: string;
655
- reason: string;
656
- estimated_impact: MoneyValue;
657
- parameters: {
658
- account_id: string;
659
- date: string;
660
- amount: number; // Milliunits
661
- payee_name: string;
662
- memo?: string;
663
- cleared: 'cleared' | 'uncleared';
664
- approved: boolean;
665
- };
666
- }
667
- ```
668
-
669
- **Confidence Levels:**
670
-
671
- ```typescript
672
- const CONFIDENCE = {
673
- CREATE_EXACT_MATCH: 0.95,
674
- NEAR_MATCH_REVIEW: 0.7,
675
- REPEAT_AMOUNT: 0.75,
676
- ANOMALY_REVIEW: 0.5,
677
- UNMATCHED_BANK: 0.8,
678
- UPDATE_CLEARED: 0.6,
679
- } as const;
680
- ```
681
-
682
- **Priority Assignment:**
683
-
684
- | Scenario | Action Type | Priority | Confidence | Rationale |
685
- |----------|-------------|----------|------------|-----------|
686
- | Unmatched bank txn | create_transaction | medium | 0.80 | Safe to create, medium urgency |
687
- | Exact match suggestion | review_duplicate | high | 0.95 | High confidence match needs confirmation |
688
- | Combination match | manual_review | medium | 0.70 | Complex scenario, needs human judgment |
689
- | Uncleared YNAB txn | update_cleared | low | 0.60 | Cleanup action, low urgency |
690
- | Anomaly insight | manual_review | low | 0.50 | Investigation needed |
691
-
692
- ---
693
-
694
- ### 2.6 Executor: executor.ts
695
-
696
- **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\executor.ts`
697
-
698
- **Purpose:** Execute reconciliation actions (create, update, clear, reconcile transactions) with bulk operation support and error handling.
699
-
700
- **Execution Phases:**
701
-
702
- ```mermaid
703
- graph TD
704
- A[executeReconciliation] --> B[Initialize State]
705
- B --> C[clearedDeltaMilli = cleared - statement]
706
-
707
- C --> D{Balance Aligned?}
708
- D -->|Yes| E[Skip Execution]
709
- D -->|No| F[PHASE 1: Auto-Create Missing]
710
-
711
- F --> G{auto_create_transactions?}
712
- G -->|No| H[Skip Phase 1]
713
- G -->|Yes| I{2+ Unmatched Bank?}
714
-
715
- I -->|Yes| J[Bulk Create Path]
716
- I -->|No| K[Sequential Create Path]
717
-
718
- J --> L[Build Batches Until Balance Aligns]
719
- L --> M[Chunk by MAX_BULK_CREATE_CHUNK = 100]
720
- M --> N[Process Each Chunk]
721
-
722
- N --> O{Bulk API Success?}
723
- O -->|Yes| P[Correlate Results]
724
- O -->|No| Q[Sequential Fallback]
725
-
726
- P --> R[Update clearedDeltaMilli]
727
- Q --> R
728
- K --> R
729
-
730
- R --> S{Balance Aligned?}
731
- S -->|Yes| T[Skip Remaining Phases]
732
- S -->|No| U[PHASE 2: Update Matched Txns]
733
-
734
- U --> V{auto_update_cleared_status?}
735
- V -->|Yes| W[Collect Updates]
736
- V -->|No| X[Skip Phase 2]
737
-
738
- W --> Y[Chunk by MAX_BULK_UPDATE_CHUNK = 100]
739
- Y --> Z[Batch Update API Call]
740
-
741
- Z --> AA{Balance Aligned?}
742
- AA -->|Yes| T
743
- AA -->|No| AB[PHASE 3: Auto-Unclear Missing]
744
-
745
- AB --> AC{auto_unclear_missing?}
746
- AC -->|Yes| AD[Collect Unclear Updates]
747
- AC -->|No| AE[Skip Phase 3]
748
-
749
- AD --> AF[Batch Unclear API Call]
750
-
751
- AF --> AG{Balance Aligned?}
752
- AG -->|Yes| T
753
- AG -->|No| AH[PHASE 4: Mark as Reconciled]
754
-
755
- AH --> AI[Batch Reconcile Matched Txns]
756
-
757
- T --> AJ[PHASE 5: Balance Reconciliation]
758
- AI --> AJ
759
- AH --> AJ
760
-
761
- AJ --> AK{statement_date provided?}
762
- AK -->|Yes| AL[buildBalanceReconciliation]
763
- AK -->|No| AM[Skip Balance Snapshot]
764
-
765
- AL --> AN[Refresh Account Snapshot]
766
- AM --> AN
767
-
768
- AN --> AO[Build Recommendations]
769
- AO --> AP[Return ExecutionResult]
770
- ```
771
-
772
- **Bulk Operation Correlation:**
773
-
774
- ```typescript
775
- // Generate deterministic import_id for deduplication
776
- function generateBulkImportId(
777
- accountId: string,
778
- date: string,
779
- amountMilli: number,
780
- payee?: string | null,
781
- ): string {
782
- const normalizedPayee = (payee ?? '').trim().toLowerCase();
783
- const raw = `${accountId}|${date}|${amountMilli}|${normalizedPayee}`;
784
- const digest = createHash('sha256').update(raw).digest('hex').slice(0, 24);
785
- return `YNAB:bulk:${digest}`;
786
- }
787
-
788
- // Correlate bulk API response with requests
789
- const correlated = correlateResults(
790
- chunk.map(entry => toCorrelationPayload(entry.saveTransaction)),
791
- response.data,
792
- duplicateImportIds
793
- );
794
-
795
- for (const result of correlated) {
796
- if (result.status === 'created') {
797
- // Success - record action
798
- } else if (result.status === 'duplicate') {
799
- // Duplicate detected - emit warning
800
- bulkDetails.duplicates_detected += 1;
801
- } else {
802
- // Failure - record error
803
- bulkDetails.transaction_failures += 1;
804
- }
805
- }
806
- ```
807
-
808
- **Error Handling Strategy:**
809
-
810
- ```mermaid
811
- graph TD
812
- A[Bulk Chunk Failure] --> B[normalizeYnabError]
813
- B --> C{shouldPropagateYnabError?}
814
-
815
- C -->|Yes 400/401/403/404/429/500/503| D[Throw Error]
816
- C -->|No Other Status| E[Log Warning]
817
-
818
- E --> F[Sequential Fallback]
819
- F --> G[Process Each Transaction Individually]
820
-
821
- G --> H{Individual Success?}
822
- H -->|Yes| I[Record Success]
823
- H -->|No| J{shouldPropagateYnabError?}
824
-
825
- J -->|Yes| D
826
- J -->|No| K[Record Failure + Continue]
827
-
828
- I --> L[Update Metrics]
829
- K --> L
830
-
831
- L --> M{More Transactions?}
832
- M -->|Yes| G
833
- M -->|No| N[Return with Partial Success]
834
- ```
835
-
836
- **Bulk Operation Metrics:**
837
-
838
- ```typescript
839
- export interface BulkOperationDetails {
840
- chunks_processed: number;
841
- bulk_successes: number;
842
- sequential_fallbacks: number;
843
- duplicates_detected: number;
844
- failed_transactions: number; // Backward-compatible alias
845
- bulk_chunk_failures: number; // API-level failures (entire chunk)
846
- transaction_failures: number; // Canonical per-transaction failures
847
- sequential_attempts?: number; // Fallback creation attempts
848
- }
849
- ```
850
-
851
- ---
852
-
853
- ### 2.7 Report Formatter: reportFormatter.ts
854
-
855
- **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\reportFormatter.ts`
856
-
857
- **Purpose:** Format reconciliation results into human-readable reports with structured sections.
858
-
859
- **Report Structure:**
860
-
861
- ```
862
- 📊 [Account Name] Reconciliation Report
863
- ═══════════════════════════════════════════════════════════════
864
-
865
- Statement Period: [date_range]
866
-
867
- BALANCE CHECK
868
- ═══════════════════════════════════════════════════════════════
869
- ✓ YNAB Cleared Balance: [amount]
870
- ✓ Statement Balance: [amount]
871
-
872
- [✅ BALANCES MATCH PERFECTLY | ❌ DISCREPANCY: [amount]]
873
-
874
- TRANSACTION ANALYSIS
875
- ═══════════════════════════════════════════════════════════════
876
- ✓ Automatically matched: [count] of [total] transactions
877
- ✓ Suggested matches: [count]
878
- ✓ Unmatched bank: [count]
879
- ✓ Unmatched YNAB: [count]
880
-
881
- ❌ UNMATCHED BANK TRANSACTIONS:
882
- [date] - [payee] [±amount]
883
- ...
884
-
885
- 💡 SUGGESTED MATCHES:
886
- [date] - [payee] [±amount] ([confidence]% confidence)
887
- ...
888
-
889
- KEY INSIGHTS
890
- ═══════════════════════════════════════════════════════════════
891
- [🚨|⚠️|ℹ️] [insight.title]
892
- [insight.description]
893
- Evidence: [summary]
894
-
895
- EXECUTION SUMMARY
896
- ═══════════════════════════════════════════════════════════════
897
- • Transactions created: [count]
898
- • Transactions updated: [count]
899
- • Date adjustments: [count]
900
-
901
- Recommendations:
902
- • [recommendation 1]
903
- • [recommendation 2]
904
- ...
905
-
906
- [⚠️ Dry run only — no YNAB changes were applied. | ✅ Changes applied to YNAB.]
907
-
908
- RECOMMENDED ACTIONS
909
- ═══════════════════════════════════════════════════════════════
910
- • [next_step 1]
911
- • [next_step 2]
912
- ...
913
- ```
914
-
915
- **Format Options:**
916
-
917
- ```typescript
918
- export interface ReportFormatterOptions {
919
- accountName?: string;
920
- accountId?: string;
921
- currencyCode?: string;
922
- includeDetailedMatches?: boolean;
923
- maxUnmatchedToShow?: number; // Default: 5
924
- maxInsightsToShow?: number; // Default: 3
925
- }
926
- ```
927
-
928
- ---
929
-
930
- ## 3. Data Flow
931
-
932
- ### 3.1 End-to-End Data Flow Diagram
933
-
934
- ```mermaid
935
- sequenceDiagram
936
- participant User
937
- participant MCP as MCP Tool Interface
938
- participant Handler as index.ts Handler
939
- participant Parser as csvParser.ts
940
- participant Fetcher as DeltaFetcher
941
- participant Adapter as ynabAdapter.ts
942
- participant Analyzer as analyzer.ts
943
- participant Matcher as matcher.ts
944
- participant RecEngine as recommendationEngine.ts
945
- participant Executor as executor.ts
946
- participant YNAB as YNAB API
947
- participant Formatter as reportFormatter.ts
948
-
949
- User->>MCP: reconcile_account(csv_data, statement_balance, ...)
950
- MCP->>Handler: handleReconcileAccount(params)
951
-
952
- Handler->>Parser: parseCSV(csvContent, options)
953
- Parser->>Parser: Auto-detect format
954
- Parser->>Parser: PapaParse
955
- Parser->>Parser: parseAmount
956
- Parser-->>Handler: { transactions: BankTransaction[], errors, warnings }
957
-
958
- Handler->>Fetcher: fetchTransactionsByAccount(budget_id, account_id, since_date)
959
- Fetcher->>YNAB: GET /budgets/{id}/accounts/{id}/transactions
960
- YNAB-->>Fetcher: TransactionDetail[]
961
- Fetcher-->>Handler: { data: TransactionDetail[], wasCached, usedDelta }
962
-
963
- Handler->>Adapter: normalizeYNABTransactions(ynabTransactions)
964
- Adapter-->>Handler: NormalizedYNABTransaction[]
965
-
966
- Handler->>Analyzer: analyzeReconciliation(bankTxns, ynabTxns, config)
967
-
968
- Analyzer->>Matcher: findMatches(bankTxns, ynabTxns, config)
969
-
970
- loop For each bank transaction
971
- Matcher->>Matcher: findCandidates(bankTxn, ynabTxns, usedIds)
972
- Matcher->>Matcher: calculateScores(amount, date, payee)
973
- Matcher->>Matcher: Sort by combined score
974
- end
975
-
976
- Matcher-->>Analyzer: MatchResult[]
977
-
978
- Analyzer->>Analyzer: Categorize matches (auto, suggested, unmatched)
979
- Analyzer->>Analyzer: calculateBalances()
980
- Analyzer->>Analyzer: generateSummary()
981
- Analyzer->>Analyzer: detectInsights()
982
-
983
- Analyzer->>RecEngine: generateRecommendations(context)
984
-
985
- loop For each insight
986
- RecEngine->>RecEngine: processInsight()
987
- end
988
-
989
- loop For each unmatched bank txn
990
- RecEngine->>RecEngine: createUnmatchedBankRecommendation()
991
- end
992
-
993
- loop For each suggested match
994
- RecEngine->>RecEngine: createSuggestedMatchRecommendation()
995
- end
996
-
997
- RecEngine-->>Analyzer: ActionableRecommendation[]
998
-
999
- Analyzer-->>Handler: ReconciliationAnalysis
1000
-
1001
- alt auto_create_transactions || auto_update_cleared_status
1002
- Handler->>Executor: executeReconciliation(analysis, params)
1003
-
1004
- loop Phase 1: Auto-create missing transactions
1005
- Executor->>Executor: Build bulk create batches
1006
- Executor->>YNAB: POST /budgets/{id}/transactions (bulk)
1007
- YNAB-->>Executor: BulkResponse
1008
- Executor->>Executor: Correlate results
1009
- Executor->>Executor: Update clearedDeltaMilli
1010
- end
1011
-
1012
- loop Phase 2: Update matched transactions
1013
- Executor->>Executor: Collect updates (cleared, date adjustments)
1014
- Executor->>YNAB: PATCH /budgets/{id}/transactions (bulk)
1015
- YNAB-->>Executor: UpdateResponse
1016
- end
1017
-
1018
- loop Phase 3: Auto-unclear missing
1019
- Executor->>Executor: Collect unclear updates
1020
- Executor->>YNAB: PATCH /budgets/{id}/transactions (bulk)
1021
- YNAB-->>Executor: UpdateResponse
1022
- end
1023
-
1024
- alt Balance aligned
1025
- Executor->>YNAB: PATCH /budgets/{id}/transactions (mark reconciled)
1026
- YNAB-->>Executor: UpdateResponse
1027
- end
1028
-
1029
- Executor->>YNAB: GET /budgets/{id}/accounts/{id} (refresh snapshot)
1030
- YNAB-->>Executor: Account
1031
-
1032
- Executor-->>Handler: ExecutionResult
1033
- end
1034
-
1035
- Handler->>Formatter: formatHumanReadableReport(analysis, execution)
1036
- Formatter-->>Handler: Formatted report string
1037
-
1038
- Handler->>Handler: buildReconciliationPayload(analysis, execution)
1039
- Handler-->>MCP: CallToolResult { human, structured }
1040
- MCP-->>User: Reconciliation report + structured data
1041
- ```
1042
-
1043
- ### 3.2 Data Transformation Chain
1044
-
1045
- ```mermaid
1046
- graph LR
1047
- A[CSV String] -->|parseCSV| B[BankTransaction[]]
1048
- C[YNAB API] -->|normalizeYNABTransactions| D[NormalizedYNABTransaction[]]
1049
-
1050
- B --> E{Matching}
1051
- D --> E
1052
-
1053
- E -->|findMatches| F[MatchResult[]]
1054
-
1055
- F -->|Categorize| G[auto_matches]
1056
- F -->|Categorize| H[suggested_matches]
1057
- F -->|Categorize| I[unmatched_bank]
1058
-
1059
- D -->|Filter| J[unmatched_ynab]
1060
-
1061
- G --> K[ReconciliationAnalysis]
1062
- H --> K
1063
- I --> K
1064
- J --> K
1065
-
1066
- K -->|generateRecommendations| L[ActionableRecommendation[]]
1067
-
1068
- K --> M{Execute?}
1069
- L --> M
1070
-
1071
- M -->|Yes| N[executeReconciliation]
1072
- N -->|YNAB API| O[ExecutionResult]
1073
-
1074
- M -->|No| P[Skip Execution]
1075
-
1076
- K --> Q[formatHumanReadableReport]
1077
- O --> Q
1078
- P --> Q
1079
-
1080
- Q --> R[Human-Readable Report]
1081
-
1082
- K --> S[buildReconciliationPayload]
1083
- O --> S
1084
-
1085
- S --> T[Structured JSON Payload]
1086
-
1087
- R --> U[MCP Response]
1088
- T --> U
1089
- ```
1090
-
1091
- ---
1092
-
1093
- ## 4. Transaction Matching Algorithm
1094
-
1095
- ### 4.1 Matching Strategy
1096
-
1097
- The reconciliation system uses a **multi-dimensional weighted scoring algorithm** with configurable thresholds and bonuses.
1098
-
1099
- **Core Principles:**
1100
- 1. **Amount is King** (50% weight) - Most reliable signal
1101
- 2. **Dates are Unreliable** (15% weight) - Banks delay posting 3-7 days
1102
- 3. **Payees are Fuzzy** (35% weight) - Merchant names vary significantly
1103
-
1104
- **Default Configuration:**
1105
-
1106
- ```typescript
1107
- export const DEFAULT_CONFIG: MatchingConfig = {
1108
- weights: {
1109
- amount: 0.5, // 50%
1110
- date: 0.15, // 15%
1111
- payee: 0.35, // 35%
1112
- },
1113
- amountToleranceMilliunits: 10, // 1 cent (10 milliunits)
1114
- dateToleranceDays: 7,
1115
- autoMatchThreshold: 85,
1116
- suggestedMatchThreshold: 60,
1117
- minimumCandidateScore: 40,
1118
- exactAmountBonus: 10,
1119
- exactDateBonus: 5,
1120
- exactPayeeBonus: 10,
1121
- };
1122
- ```
1123
-
1124
- ### 4.2 Scoring Examples
1125
-
1126
- **Example 1: Perfect Match**
1127
-
1128
- ```
1129
- Bank: { date: '2025-09-15', amount: -45230, payee: 'Shell Gas' }
1130
- YNAB: { date: '2025-09-15', amount: -45230, payee: 'Shell' }
1131
-
1132
- Scores:
1133
- Amount: 100 (exact match: -45230 === -45230)
1134
- Date: 100 (same day)
1135
- Payee: 85 (fuzz.token_set_ratio('Shell Gas', 'Shell'))
1136
-
1137
- Combined = (100 * 0.5) + (100 * 0.15) + (85 * 0.35) = 94.75
1138
- Bonuses = +10 (exact amount) + 5 (exact date) = +15
1139
- Final = 94.75 + 15 = 109.75 → capped at 100
1140
-
1141
- Confidence: HIGH (≥ 85)
1142
- ```
1143
-
1144
- **Example 2: Date Mismatch**
1145
-
1146
- ```
1147
- Bank: { date: '2025-09-15', amount: -12799, payee: 'Amazon Marketplace' }
1148
- YNAB: { date: '2025-09-20', amount: -12799, payee: 'Amazon' }
1149
-
1150
- Scores:
1151
- Amount: 100 (exact match)
1152
- Date: 55 (5 days diff within tolerance)
1153
- Payee: 92 (fuzz.token_set_ratio)
1154
-
1155
- Combined = (100 * 0.5) + (55 * 0.15) + (92 * 0.35) = 90.45
1156
- Bonuses = +10 (exact amount) + 10 (payee ≥ 95) = +20
1157
- Final = 90.45 + 20 = 110.45 → capped at 100
1158
-
1159
- Confidence: HIGH (≥ 85)
1160
- ```
1161
-
1162
- **Example 3: Suggested Match**
1163
-
1164
- ```
1165
- Bank: { date: '2025-09-15', amount: -4520, payee: 'Coffee Shop A' }
1166
- YNAB: { date: '2025-09-16', amount: -4530, payee: 'Coffee Shop B' }
1167
-
1168
- Scores:
1169
- Amount: 95 (10 milliunits diff within tolerance)
1170
- Date: 95 (1 day diff)
1171
- Payee: 70 (partial match)
1172
-
1173
- Combined = (95 * 0.5) + (95 * 0.15) + (70 * 0.35) = 86.25
1174
- Bonuses = 0 (no exact matches)
1175
- Final = 86.25
1176
-
1177
- Confidence: HIGH (≥ 85) but close to threshold
1178
- ```
1179
-
1180
- **Example 4: Low Confidence**
1181
-
1182
- ```
1183
- Bank: { date: '2025-09-15', amount: -5000, payee: 'Transfer' }
1184
- YNAB: { date: '2025-09-22', amount: -5005, payee: 'Payment' }
1185
-
1186
- Scores:
1187
- Amount: 95 (5 milliunits diff)
1188
- Date: 35 (7 days diff at edge of tolerance)
1189
- Payee: 40 (weak match)
1190
-
1191
- Combined = (95 * 0.5) + (35 * 0.15) + (40 * 0.35) = 66.25
1192
- Bonuses = 0
1193
- Final = 66.25
1194
-
1195
- Confidence: MEDIUM (60-84)
1196
- ```
1197
-
1198
- ### 4.3 Candidate Filtering
1199
-
1200
- **Pre-Filtering (Before Scoring):**
1201
-
1202
- ```typescript
1203
- // 1. Sign Check - both must be same sign
1204
- const bankSign = Math.sign(bankTxn.amount);
1205
- const ynabSign = Math.sign(ynabTxn.amount);
1206
- if (bankSign !== ynabSign && bankSign !== 0 && ynabSign !== 0) {
1207
- continue; // Skip candidate
1208
- }
1209
-
1210
- // 2. Amount Tolerance Check - hard filter
1211
- const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
1212
- if (amountDiff > config.amountToleranceMilliunits) {
1213
- continue; // Skip candidate
1214
- }
1215
- ```
1216
-
1217
- **Post-Filtering (After Scoring):**
1218
-
1219
- ```typescript
1220
- // Only include if score ≥ minimumCandidateScore (default: 40)
1221
- if (scores.combined >= config.minimumCandidateScore) {
1222
- candidates.push({ ynabTransaction, scores, matchReasons });
1223
- }
1224
- ```
1225
-
1226
- ### 4.4 Guardrails & Failure Modes
1227
-
1228
- **Auto-Match Disabled When:**
1229
-
1230
- | Condition | Rationale |
1231
- |-----------|-----------|
1232
- | Amount score < 80 | Amount is most reliable signal |
1233
- | Date gap > 14 days | Even with delays, 2+ weeks is suspicious |
1234
- | Multiple candidates within 5 points | Ambiguous, needs human review |
1235
- | Payee score < 40 AND date score < 60 | Neither secondary signal is strong |
1236
- | Transaction has warnings | Parser detected ambiguity |
1237
-
1238
- **Known Failure Modes:**
1239
-
1240
- | Failure Mode | Mitigation Strategy |
1241
- |--------------|---------------------|
1242
- | Similar merchants (Starbucks #1234 vs #5678) | Require exact amount + date ≤1 day |
1243
- | Recurring subscriptions | Use date as primary discriminator |
1244
- | Refunds | Sign check prevents cross-matching |
1245
- | Split transactions | Combination matching (future) |
1246
- | Merchant name drift | Payee normalization + token_set_ratio |
1247
- | Duplicate entries | Prefer uncleared; flag for review |
1248
-
1249
- ---
1250
-
1251
- ## 5. CSV Parsing Engine
1252
-
1253
- ### 5.1 Multi-Format Support
1254
-
1255
- The CSV parser supports both **headered** and **headerless** CSV files with bank-specific presets.
1256
-
1257
- **Auto-Detection Strategy:**
1258
-
1259
- ```mermaid
1260
- graph TD
1261
- A[Raw CSV] --> B[Parse First 5 Lines]
1262
- B --> C{Header Match?}
1263
-
1264
- C -->|RBC: Description 1| D[RBC Preset]
1265
- C -->|Contains CAD$| E[TD Preset]
1266
- C -->|No Match| F{Headerless Pattern?}
1267
-
1268
- F -->|MM/DD/YYYY + 4+ columns| G[TD Headerless Preset]
1269
- F -->|No Pattern| H[Default Auto-Detect]
1270
-
1271
- D --> I[Apply Preset]
1272
- E --> I
1273
- G --> I
1274
- H --> I
1275
-
1276
- I --> J[PapaParse with Detected Format]
1277
- ```
1278
-
1279
- ### 5.2 Date Parsing Priority
1280
-
1281
- ```mermaid
1282
- graph TD
1283
- A[Raw Date String] --> B{ISO Format?}
1284
- B -->|YYYY-MM-DD| C[Direct Parse UTC]
1285
- B -->|No| D{Format Hint from Preset?}
1286
-
1287
- D -->|YMD| E[Apply YYYY-MM-DD Pattern]
1288
- D -->|MDY| F[Apply MM/DD/YYYY Pattern]
1289
- D -->|DMY| G[Apply DD/MM/YYYY Pattern]
1290
- D -->|No Hint| H[chrono-node Fallback]
1291
-
1292
- C --> I[formatLocalDate]
1293
- E --> I
1294
- F --> I
1295
- G --> I
1296
- H --> I
1297
-
1298
- I --> J[YYYY-MM-DD String]
1299
- ```
1300
-
1301
- **Why This Priority?**
1302
-
1303
- 1. **ISO format first** - Unambiguous, no parsing errors
1304
- 2. **Preset format hint** - Handles ambiguous dates like 02/03/2025 correctly
1305
- 3. **chrono-node fallback** - Handles natural language and weird formats
1306
-
1307
- ### 5.3 Amount Parsing Edge Cases
1308
-
1309
- **Supported Formats:**
1310
-
1311
- | Input | Detected Format | Output (Milliunits) |
1312
- |-------|-----------------|---------------------|
1313
- | `45.23` | Standard | 45230 |
1314
- | `$45.23` | USD Symbol | 45230 |
1315
- | `CAD 45.23` | Currency Code | 45230 |
1316
- | `1,234.56` | Thousands Separator | 1234560 |
1317
- | `1.234,56` | European Format | 1234560 |
1318
- | `(45.23)` | Parentheses Negative | -45230 |
1319
- | `-45.23` | Explicit Negative | -45230 |
1320
-
1321
- **Debit/Credit Column Handling:**
1322
-
1323
- ```typescript
1324
- if (Math.abs(debitMilliunits) > 0 && Math.abs(creditMilliunits) > 0) {
1325
- // WARNING: Both columns populated - use debit
1326
- rowWarnings.push(`Both Debit (${debit}) and Credit (${credit}) have values - using Debit`);
1327
- warnings.push({ row: rowNum, message: rowWarnings[0] });
1328
- }
1329
-
1330
- if (Math.abs(debitMilliunits) > 0) {
1331
- amountMilliunits = -Math.abs(debitMilliunits); // Debits are outflows
1332
- } else if (Math.abs(creditMilliunits) > 0) {
1333
- amountMilliunits = Math.abs(creditMilliunits); // Credits are inflows
1334
- } else {
1335
- amountMilliunits = 0;
1336
- }
1337
- ```
1338
-
1339
- ### 5.4 Security Measures
1340
-
1341
- **Input Validation:**
1342
-
1343
- ```typescript
1344
- // File size limit: 10MB default
1345
- const MAX_BYTES = options.maxBytes ?? 10 * 1024 * 1024;
1346
- if (content.length > MAX_BYTES) {
1347
- throw new Error(`File size exceeds limit of ${Math.round(MAX_BYTES / 1024 / 1024)}MB`);
1348
- }
1349
-
1350
- // Row limit: 10,000 default
1351
- const maxRows = options.maxRows ?? 10000;
1352
- ```
1353
-
1354
- **Unicode Sanitization:**
1355
-
1356
- ```typescript
1357
- // Remove malicious/confusing characters
1358
- rawDesc = rawDesc
1359
- .replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // Control chars
1360
- .replace(/[\u202A-\u202E\u2066-\u2069]/g, '') // Bidirectional overrides
1361
- .replace(/[\u200B-\u200D\uFEFF]/g, '') // Zero-width chars
1362
- .replace(/[\u2028-\u2029]/g, '') // Line/paragraph separators
1363
- .substring(0, 500); // YNAB max memo length
1364
- ```
1365
-
1366
- ---
1367
-
1368
- ## 6. Execution Engine
1369
-
1370
- ### 6.1 Execution Phases
1371
-
1372
- The executor implements a **4-phase execution strategy** with early termination once balance aligns.
1373
-
1374
- **Phase Overview:**
1375
-
1376
- | Phase | Trigger | Purpose | Balance Check |
1377
- |-------|---------|---------|---------------|
1378
- | 1 | `auto_create_transactions` | Create missing bank transactions | After each create |
1379
- | 2 | `auto_update_cleared_status` | Mark matched YNAB txns as cleared | After each update |
1380
- | 3 | `auto_unclear_missing` | Un-clear YNAB txns not on statement | After each unclear |
1381
- | 4 | Balance aligned | Mark all matched txns as reconciled | N/A |
1382
-
1383
- **Early Termination:**
1384
-
1385
- ```typescript
1386
- const recordAlignmentIfNeeded = (trigger: string, { log = true } = {}) => {
1387
- if (balanceAligned) return true;
1388
-
1389
- if (Math.abs(clearedDeltaMilli) <= balanceToleranceMilli) {
1390
- balanceAligned = true;
1391
- if (log) {
1392
- actions_taken.push({
1393
- type: 'balance_checkpoint',
1394
- transaction: null,
1395
- reason: `Cleared delta ${deltaDisplay} within ±${toleranceDisplay} after ${trigger} - halting`,
1396
- });
1397
- }
1398
- return true;
1399
- }
1400
- return false;
1401
- };
1402
- ```
1403
-
1404
- ### 6.2 Bulk vs Sequential Operations
1405
-
1406
- **Decision Matrix:**
1407
-
1408
- | Scenario | Strategy | Rationale |
1409
- |----------|----------|-----------|
1410
- | 2+ unmatched bank txns | Bulk create | API efficiency |
1411
- | 1 unmatched bank txn | Sequential create | Simpler error handling |
1412
- | Bulk API failure | Sequential fallback | Resilience |
1413
- | Updates (cleared, date) | Always bulk (chunked) | YNAB supports batch updates |
1414
-
1415
- **Bulk Create Flow:**
1416
-
1417
- ```typescript
1418
- // Build batches until balance aligns or all processed
1419
- while (nextBankIndex < orderedUnmatchedBank.length && !balanceAligned) {
1420
- const batch: PreparedBulkCreateEntry[] = [];
1421
- let projectedDelta = clearedDeltaMilli;
1422
-
1423
- // Greedy batch: add transactions until balance would align
1424
- while (nextBankIndex < orderedUnmatchedBank.length) {
1425
- const entry = buildPreparedEntry(orderedUnmatchedBank[nextBankIndex]);
1426
- batch.push(entry);
1427
- nextBankIndex += 1;
1428
- projectedDelta = addMilli(projectedDelta, entry.amountMilli);
1429
-
1430
- if (Math.abs(projectedDelta) <= balanceToleranceMilli) {
1431
- break; // This batch should align balance
1432
- }
1433
- }
1434
-
1435
- // Process batch in chunks of MAX_BULK_CREATE_CHUNK (100)
1436
- const chunks = chunkArray(batch, MAX_BULK_CREATE_CHUNK);
1437
- for (const chunk of chunks) {
1438
- try {
1439
- await processBulkChunk(chunk, chunkIndex);
1440
- } catch (error) {
1441
- // Fallback to sequential
1442
- await processSequentialEntries(chunk, { chunkIndex, fallbackError: error });
1443
- }
1444
- }
1445
- }
1446
- ```
1447
-
1448
- ### 6.3 Correlation Tracking
1449
-
1450
- **Problem:** Bulk API returns transactions in arbitrary order. How to match responses to requests?
1451
-
1452
- **Solution:** Hash-based correlation keys
1453
-
1454
- ```typescript
1455
- // Generate correlation key from transaction attributes
1456
- export function generateCorrelationKey(txn: CorrelationPayload): string {
1457
- const key = `${txn.account_id}|${txn.date}|${txn.amount}|${normalizePayee(txn.payee_name)}`;
1458
- return createHash('sha256').update(key).digest('hex').slice(0, 16);
1459
- }
1460
-
1461
- // Correlate responses
1462
- export function correlateResults(
1463
- requests: CorrelationPayload[],
1464
- response: BulkResponse,
1465
- duplicateImportIds: Set<string>,
1466
- ): CorrelationResult[] {
1467
- const results: CorrelationResult[] = [];
1468
-
1469
- for (let i = 0; i < requests.length; i++) {
1470
- const request = requests[i];
1471
- const correlationKey = generateCorrelationKey(request);
1472
-
1473
- // Try to find in created transactions
1474
- const created = response.transactions?.find(t =>
1475
- generateCorrelationKey(toCorrelationPayload(t)) === correlationKey
1476
- );
1477
-
1478
- if (created) {
1479
- results.push({ status: 'created', request_index: i, transaction_id: created.id, correlation_key: correlationKey });
1480
- } else if (duplicateImportIds.has(request.import_id ?? '')) {
1481
- results.push({ status: 'duplicate', request_index: i, correlation_key: correlationKey });
1482
- } else {
1483
- results.push({ status: 'failed', request_index: i, correlation_key: correlationKey, error: 'Not in response' });
1484
- }
1485
- }
1486
-
1487
- return results;
1488
- }
1489
- ```
1490
-
1491
- ### 6.4 Error Recovery
1492
-
1493
- **Retry Strategy:**
1494
-
1495
- ```mermaid
1496
- graph TD
1497
- A[Bulk Chunk Failure] --> B[normalizeYnabError]
1498
- B --> C{FATAL_STATUS_CODES?}
1499
-
1500
- C -->|400/401/403/404/429/500/503| D[Propagate Error]
1501
- C -->|Other| E[Log + Continue]
1502
-
1503
- E --> F[Sequential Fallback]
1504
- F --> G[Process Individually]
1505
-
1506
- G --> H{Individual Success?}
1507
- H -->|Yes| I[Record Success]
1508
- H -->|No| J{FATAL?}
1509
-
1510
- J -->|Yes| D
1511
- J -->|No| K[Record Failure + Continue]
1512
-
1513
- I --> L[Update Metrics]
1514
- K --> L
1515
- ```
1516
-
1517
- **Fatal vs Recoverable Errors:**
1518
-
1519
- ```typescript
1520
- const FATAL_YNAB_STATUS_CODES = new Set([
1521
- 400, // Bad Request - malformed payload
1522
- 401, // Unauthorized - invalid token
1523
- 403, // Forbidden - insufficient permissions
1524
- 404, // Not Found - budget/account doesn't exist
1525
- 429, // Too Many Requests - rate limited
1526
- 500, // Internal Server Error - YNAB backend issue
1527
- 503, // Service Unavailable - YNAB maintenance
1528
- ]);
1529
- ```
1530
-
1531
- ---
1532
-
1533
- ## 7. Type System
1534
-
1535
- ### 7.1 Core Types
1536
-
1537
- **Canonical Transaction Types:**
1538
-
1539
- ```typescript
1540
- // File: src/types/reconciliation.ts
1541
-
1542
- /**
1543
- * Bank transaction from CSV parsing.
1544
- * CRITICAL: amount is in MILLIUNITS (integers, 1000 = $1.00)
1545
- */
1546
- export interface BankTransaction {
1547
- id: string; // UUID
1548
- date: string; // YYYY-MM-DD
1549
- amount: number; // Milliunits (integer)
1550
- payee: string;
1551
- memo?: string;
1552
- sourceRow: number; // CSV row number (1-indexed)
1553
- raw: {
1554
- date: string;
1555
- amount: string;
1556
- description: string;
1557
- };
1558
- warnings?: string[];
1559
- }
1560
-
1561
- /**
1562
- * Normalized YNAB transaction for matching.
1563
- * CRITICAL: amount is in MILLIUNITS (same as YNAB API)
1564
- */
1565
- export interface NormalizedYNABTransaction {
1566
- id: string;
1567
- date: string; // YYYY-MM-DD
1568
- amount: number; // Milliunits (integer)
1569
- payee: string | null;
1570
- memo: string | null;
1571
- categoryName: string | null;
1572
- cleared: 'cleared' | 'uncleared' | 'reconciled';
1573
- approved: boolean;
1574
- }
1575
- ```
1576
-
1577
- **Match Result Types:**
1578
-
1579
- ```typescript
1580
- // File: src/tools/reconciliation/types.ts
1581
-
1582
- export type MatchConfidence = 'high' | 'medium' | 'low' | 'none';
1583
-
1584
- export interface TransactionMatch {
1585
- bankTransaction: BankTransaction;
1586
- ynabTransaction?: YNABTransaction;
1587
- candidates?: MatchCandidate[];
1588
- confidence: MatchConfidence;
1589
- confidenceScore: number; // 0-100
1590
- matchReason: string;
1591
- topConfidence?: number;
1592
- actionHint?: string;
1593
- recommendation?: string;
1594
- }
1595
-
1596
- export interface MatchCandidate {
1597
- ynab_transaction: YNABTransaction;
1598
- confidence: number;
1599
- match_reason: string;
1600
- explanation: string;
1601
- }
1602
- ```
1603
-
1604
- **Recommendation Types:**
1605
-
1606
- ```typescript
1607
- // File: src/tools/reconciliation/types.ts
1608
-
1609
- export type ActionableRecommendation =
1610
- | CreateTransactionRecommendation
1611
- | UpdateClearedRecommendation
1612
- | ReviewDuplicateRecommendation
1613
- | ManualReviewRecommendation;
1614
-
1615
- export interface CreateTransactionRecommendation {
1616
- id: string;
1617
- action_type: 'create_transaction';
1618
- priority: 'high' | 'medium' | 'low';
1619
- confidence: number; // 0-1
1620
- message: string;
1621
- reason: string;
1622
- estimated_impact: MoneyValue;
1623
- account_id: string;
1624
- parameters: {
1625
- account_id: string;
1626
- date: string;
1627
- amount: number; // Milliunits
1628
- payee_name: string;
1629
- memo?: string;
1630
- cleared: 'cleared' | 'uncleared';
1631
- approved: boolean;
1632
- };
1633
- }
1634
- ```
1635
-
1636
- ### 7.2 Analysis Result Type
1637
-
1638
- ```typescript
1639
- export interface ReconciliationAnalysis {
1640
- success: true;
1641
- phase: 'analysis';
1642
- summary: ReconciliationSummary;
1643
- auto_matches: TransactionMatch[];
1644
- suggested_matches: TransactionMatch[];
1645
- unmatched_bank: BankTransaction[];
1646
- unmatched_ynab: YNABTransaction[];
1647
- balance_info: BalanceInfo;
1648
- next_steps: string[];
1649
- insights: ReconciliationInsight[];
1650
- recommendations?: ActionableRecommendation[];
1651
- }
1652
-
1653
- export interface ReconciliationSummary {
1654
- statement_date_range: string;
1655
- bank_transactions_count: number;
1656
- ynab_transactions_count: number;
1657
- auto_matched: number;
1658
- suggested_matches: number;
1659
- unmatched_bank: number;
1660
- unmatched_ynab: number;
1661
- current_cleared_balance: MoneyValue;
1662
- target_statement_balance: MoneyValue;
1663
- discrepancy: MoneyValue;
1664
- discrepancy_explanation: string;
1665
- }
1666
- ```
1667
-
1668
- ---
1669
-
1670
- ## 8. Integration Points
1671
-
1672
- ### 8.1 YNAB API Integration
1673
-
1674
- **Delta Fetching:**
1675
-
1676
- ```typescript
1677
- // Fetch transactions with delta support for efficiency
1678
- const transactionsResult = forceFullRefresh
1679
- ? await deltaFetcher.fetchTransactionsByAccountFull(budget_id, account_id, since_date)
1680
- : await deltaFetcher.fetchTransactionsByAccount(budget_id, account_id, since_date);
1681
-
1682
- // Result includes metadata
1683
- {
1684
- data: TransactionDetail[],
1685
- wasCached: boolean,
1686
- usedDelta: boolean,
1687
- serverKnowledge: number,
1688
- }
1689
- ```
1690
-
1691
- **Bulk Operations:**
1692
-
1693
- ```typescript
1694
- // Create multiple transactions
1695
- const response = await ynabAPI.transactions.createTransactions(budgetId, {
1696
- transactions: [
1697
- { account_id, date, amount, payee_name, cleared: 'cleared', approved: true, import_id },
1698
- // ... more transactions
1699
- ],
1700
- });
1701
-
1702
- // Update multiple transactions
1703
- const response = await ynabAPI.transactions.updateTransactions(budgetId, {
1704
- transactions: [
1705
- { id: txn_id_1, cleared: 'cleared' },
1706
- { id: txn_id_2, date: '2025-09-20' },
1707
- // ... more updates
1708
- ],
1709
- });
1710
- ```
1711
-
1712
- ### 8.2 MCP Tool Interface
1713
-
1714
- **Request Schema:**
1715
-
1716
- ```typescript
1717
- export const ReconcileAccountSchema = z.object({
1718
- budget_id: z.string().min(1),
1719
- account_id: z.string().min(1),
1720
-
1721
- // CSV Input (one required)
1722
- csv_file_path: z.string().optional(),
1723
- csv_data: z.string().optional(),
1724
-
1725
- csv_format: z.object({
1726
- date_column: z.union([z.string(), z.number()]).optional(),
1727
- amount_column: z.union([z.string(), z.number()]).optional(),
1728
- debit_column: z.union([z.string(), z.number()]).optional(),
1729
- credit_column: z.union([z.string(), z.number()]).optional(),
1730
- description_column: z.union([z.string(), z.number()]).optional(),
1731
- date_format: z.string().optional(),
1732
- has_header: z.boolean().optional(),
1733
- delimiter: z.string().optional(),
1734
- }).optional(),
1735
-
1736
- // Statement Information
1737
- statement_balance: z.number(),
1738
- statement_start_date: z.string().optional(),
1739
- statement_end_date: z.string().optional(),
1740
- statement_date: z.string().optional(),
1741
-
1742
- // Matching Configuration
1743
- date_tolerance_days: z.number().min(0).max(7).default(7),
1744
- amount_tolerance_cents: z.number().min(0).max(100).default(1),
1745
- auto_match_threshold: z.number().min(0).max(100).default(85),
1746
- suggestion_threshold: z.number().min(0).max(100).default(60),
1747
-
1748
- // Automation Toggles
1749
- auto_create_transactions: z.boolean().default(false),
1750
- auto_update_cleared_status: z.boolean().default(false),
1751
- auto_unclear_missing: z.boolean().default(true),
1752
- auto_adjust_dates: z.boolean().default(false),
1753
- invert_bank_amounts: z.boolean().optional(),
1754
- dry_run: z.boolean().default(true),
1755
-
1756
- // Response Options
1757
- include_structured_data: z.boolean().default(false),
1758
- force_full_refresh: z.boolean().default(true),
1759
- });
1760
- ```
1761
-
1762
- **Response Format:**
1763
-
1764
- ```typescript
1765
- {
1766
- content: [
1767
- {
1768
- type: 'text',
1769
- text: responseFormatter.format({
1770
- human: formatHumanReadableReport(analysis, execution),
1771
- structured: { // Only if include_structured_data = true
1772
- version: '2.0',
1773
- analysis: ReconciliationAnalysis,
1774
- execution: ExecutionResult,
1775
- audit_metadata: { ... },
1776
- },
1777
- }),
1778
- },
1779
- ],
1780
- }
1781
- ```
1782
-
1783
- ---
1784
-
1785
- ## 9. Performance Characteristics
1786
-
1787
- ### 9.1 Complexity Analysis
1788
-
1789
- | Operation | Complexity | Notes |
1790
- |-----------|------------|-------|
1791
- | CSV Parsing | O(N) | N = rows; PapaParse streams large files |
1792
- | YNAB Normalization | O(M) | M = YNAB transactions |
1793
- | Basic Matching | O(N × M) | N = bank txns, M = YNAB txns |
1794
- | Payee Fuzzy Matching | O(L²) per pair | L = string length; fuzzball optimized |
1795
- | Bulk Create | O(N/100) API calls | Chunked by MAX_BULK_CREATE_CHUNK |
1796
- | Bulk Update | O(M/100) API calls | Chunked by MAX_BULK_UPDATE_CHUNK |
1797
-
1798
- ### 9.2 Scale Limits
1799
-
1800
- | CSV Size | Expected Performance | Recommendations |
1801
- |----------|---------------------|------------------|
1802
- | <100 txns | <500ms | Default settings |
1803
- | 100-500 txns | <2s | Default settings |
1804
- | 500-1000 txns | 2-10s | Consider chunking by month |
1805
- | >1000 txns | May timeout | Process in batches |
1806
-
1807
- ### 9.3 Optimization Strategies
1808
-
1809
- **Current Optimizations:**
1810
-
1811
- 1. **Integer Arithmetic** - No floating-point comparisons
1812
- 2. **Early Termination** - Stop when balance aligns
1813
- 3. **Bulk Operations** - Minimize API calls
1814
- 4. **Delta Fetching** - Only fetch changed transactions
1815
-
1816
- **Future Optimizations:**
1817
-
1818
- 1. **Amount Bucketing** - Index YNAB transactions by amount for O(1) lookup
1819
- 2. **Date Windowing** - Only compare transactions within ±14 days
1820
- 3. **Streaming CSV** - Parse in chunks for large files
1821
- 4. **Parallel Matching** - Use worker threads for CPU-bound fuzzy matching
1822
-
1823
- ### 9.4 Memory Usage
1824
-
1825
- **Estimated Memory Footprint:**
1826
-
1827
- ```
1828
- CSV (500 txns): ~100 KB raw text
1829
- BankTransaction[]: ~200 KB (400 bytes/txn × 500)
1830
- YNAB API (1000): ~500 KB raw JSON
1831
- NormalizedYNAB[]: ~400 KB (400 bytes/txn × 1000)
1832
- MatchResults[]: ~300 KB (600 bytes/match × 500)
1833
- Total: ~1.5 MB for 500-txn reconciliation
1834
- ```
1835
-
1836
- **Memory Safety:**
1837
-
1838
- ```typescript
1839
- // Security limits in csvParser.ts
1840
- const MAX_BYTES = options.maxBytes ?? 10 * 1024 * 1024; // 10MB
1841
- const maxRows = options.maxRows ?? 10000;
1842
-
1843
- // PapaParse preview limits rows in memory
1844
- const parsed = Papa.parse(content, {
1845
- preview: maxRows + (hasHeader ? 1 : 0),
1846
- skipEmptyLines: true,
1847
- });
1848
- ```
1849
-
1850
- ---
1851
-
1852
- ## 10. Testing Strategy
1853
-
1854
- ### 10.1 Test Coverage
1855
-
1856
- **Unit Tests:**
1857
-
1858
- | Component | Test File | Coverage Focus |
1859
- |-----------|-----------|----------------|
1860
- | csvParser | csvParser.test.ts | Format detection, amount parsing, date parsing |
1861
- | matcher | matcher.test.ts | Scoring algorithm, candidate filtering, tie-breaking |
1862
- | analyzer | analyzer.test.ts | Orchestration, insight generation, balance calculation |
1863
- | executor | executor.test.ts | Bulk operations, correlation, error handling |
1864
- | recommendationEngine | recommendationEngine.test.ts | Recommendation generation, prioritization |
1865
-
1866
- **Integration Tests:**
1867
-
1868
- | Scenario | Test File | Purpose |
1869
- |----------|-----------|---------|
1870
- | TD Bank CSV | csvParser.integration.test.ts | Real-world TD format |
1871
- | RBC Debit/Credit | csvParser.integration.test.ts | Debit/credit column handling |
1872
- | Bulk Create | executor.integration.test.ts | Bulk API with mocked YNAB |
1873
- | End-to-End | reconciliation.e2e.test.ts | Full flow with real YNAB API |
1874
-
1875
- ### 10.2 Test Data
1876
-
1877
- **Fixtures:**
1878
-
1879
- ```
1880
- test-exports/csv/
1881
- ├── td-credit-card.csv # TD headerless format
1882
- ├── rbc-checking.csv # RBC debit/credit columns
1883
- ├── wealthsimple-cash.csv # Wealthsimple headered format
1884
- ├── scotiabank-savings.csv # Scotiabank format
1885
- ├── tangerine-checking.csv # Tangerine format
1886
- └── edge-cases/
1887
- ├── european-numbers.csv # 1.234,56 format
1888
- ├── both-debit-credit.csv # Ambiguous columns
1889
- └── malformed.csv # Missing fields, bad dates
1890
- ```
1891
-
1892
- ### 10.3 Accuracy Metrics
1893
-
1894
- **Evaluation Dataset:**
1895
-
1896
- ```typescript
1897
- // test-exports/csv/labeled/
1898
- {
1899
- "csv_file": "td-credit-card.csv",
1900
- "ground_truth": [
1901
- {
1902
- "bank_row": 2,
1903
- "ynab_transaction_id": "abc123",
1904
- "expected_confidence": "high",
1905
- "notes": "Exact amount and payee match"
1906
- },
1907
- {
1908
- "bank_row": 3,
1909
- "ynab_transaction_id": null,
1910
- "expected_confidence": "none",
1911
- "notes": "New transaction, should create"
1912
- }
1913
- ]
1914
- }
1915
- ```
1916
-
1917
- **Benchmark Metrics:**
1918
-
1919
- ```bash
1920
- npm run benchmark:reconciliation -- --dataset=test-exports/csv/labeled/
1921
-
1922
- Output:
1923
- Auto-Match Precision: 95.2% (20/21 correct)
1924
- Auto-Match Recall: 90.9% (20/22 true matches)
1925
- Overall Match Rate: 96.0% (48/50 matched at any level)
1926
- False Positive Rate: 4.8% (1/21 incorrect)
1927
- P95 Latency: 1.2s (500-txn CSV)
1928
- ```
1929
-
1930
- ---
1931
-
1932
- ## Appendix A: Sequence Diagrams
1933
-
1934
- ### A.1 Create Transaction Flow
1935
-
1936
- ```mermaid
1937
- sequenceDiagram
1938
- participant E as executor.ts
1939
- participant B as BankTransaction[]
1940
- participant C as Correlation
1941
- participant Y as YNAB API
1942
-
1943
- E->>B: Filter unmatched_bank
1944
- B->>E: orderedUnmatchedBank (sorted by date desc)
1945
-
1946
- loop For each batch until balance aligns
1947
- E->>E: Build batch (greedy until projected balance aligns)
1948
- E->>E: Chunk batch by MAX_BULK_CREATE_CHUNK (100)
1949
-
1950
- loop For each chunk
1951
- E->>C: Generate import_id for each txn
1952
- C-->>E: import_id = YNAB:bulk:{hash}
1953
-
1954
- E->>C: Generate correlation_key
1955
- C-->>E: correlation_key = {hash}
1956
-
1957
- E->>Y: POST /budgets/{id}/transactions (bulk)
1958
-
1959
- alt Bulk Success
1960
- Y-->>E: BulkResponse { transactions[], duplicate_import_ids[] }
1961
- E->>C: correlateResults(requests, response, duplicates)
1962
-
1963
- loop For each correlated result
1964
- alt Status: created
1965
- C-->>E: { status: 'created', transaction_id, correlation_key }
1966
- E->>E: Record success action
1967
- E->>E: Update clearedDeltaMilli
1968
- else Status: duplicate
1969
- C-->>E: { status: 'duplicate', correlation_key }
1970
- E->>E: Record duplicate warning
1971
- E->>E: Increment duplicates_detected
1972
- else Status: failed
1973
- C-->>E: { status: 'failed', correlation_key, error }
1974
- E->>E: Record failure action
1975
- E->>E: Increment transaction_failures
1976
- end
1977
- end
1978
-
1979
- E->>E: Check if balance aligned
1980
- else Bulk Failure
1981
- Y-->>E: Error response
1982
- E->>E: Increment bulk_chunk_failures
1983
- E->>E: Log fallback action
1984
-
1985
- loop For each txn in chunk (Sequential Fallback)
1986
- E->>Y: POST /budgets/{id}/transactions (single)
1987
-
1988
- alt Success
1989
- Y-->>E: { transaction: {...} }
1990
- E->>E: Record success action
1991
- E->>E: Update clearedDeltaMilli
1992
- else Failure
1993
- Y-->>E: Error
1994
- E->>E: normalizeYnabError
1995
-
1996
- alt Fatal Error (400/401/403/404/429/500/503)
1997
- E->>E: Throw error (halt execution)
1998
- else Recoverable Error
1999
- E->>E: Record failure action
2000
- E->>E: Increment transaction_failures
2001
- E->>E: Continue to next txn
2002
- end
2003
- end
2004
-
2005
- E->>E: Check if balance aligned
2006
- end
2007
- end
2008
- end
2009
- end
2010
- ```
2011
-
2012
- ### A.2 Match Scoring Flow
2013
-
2014
- ```mermaid
2015
- sequenceDiagram
2016
- participant M as matcher.ts
2017
- participant B as BankTransaction
2018
- participant Y as NormalizedYNABTransaction[]
2019
- participant F as fuzzball
2020
-
2021
- M->>Y: Loop through YNAB transactions
2022
-
2023
- loop For each YNAB transaction
2024
- M->>M: Check if already used
2025
-
2026
- alt Already matched
2027
- M->>M: Skip candidate
2028
- else Not matched
2029
- M->>M: Sign check (bankSign === ynabSign)
2030
-
2031
- alt Signs differ
2032
- M->>M: Skip candidate
2033
- else Signs match
2034
- M->>M: Calculate amountDiff = |bank - ynab|
2035
-
2036
- alt amountDiff > tolerance
2037
- M->>M: Skip candidate
2038
- else Within tolerance
2039
- M->>M: Calculate amount score (0-100)
2040
-
2041
- Note over M: Amount Scoring:<br/>diff === 0 → 100<br/>diff <= tolerance → 95<br/>diff <= $1 → 80-100<br/>else → 60-0
2042
-
2043
- M->>M: Calculate date score (0-100)
2044
-
2045
- Note over M: Date Scoring:<br/>same day → 100<br/>1 day → 95<br/>≤ tolerance → 90-50<br/>else → 50-0
2046
-
2047
- M->>F: calculatePayeeScore(bankPayee, ynabPayee)
2048
-
2049
- F->>F: token_set_ratio
2050
- F->>F: token_sort_ratio
2051
- F->>F: partial_ratio
2052
- F->>F: WRatio
2053
-
2054
- F-->>M: max(scores)
2055
-
2056
- M->>M: combined = amount×0.5 + date×0.15 + payee×0.35
2057
-
2058
- M->>M: Apply bonuses
2059
-
2060
- Note over M: Bonuses:<br/>amount === 100 → +10<br/>date === 100 → +5<br/>payee >= 95 → +10
2061
-
2062
- M->>M: combined = min(100, combined + bonuses)
2063
-
2064
- alt combined >= minimumCandidateScore (40)
2065
- M->>M: Add to candidates[]
2066
- else Below threshold
2067
- M->>M: Skip candidate
2068
- end
2069
- end
2070
- end
2071
- end
2072
- end
2073
-
2074
- M->>M: Sort candidates by combined score (desc)
2075
- M->>M: Tie-break: prefer uncleared, then closer date
2076
-
2077
- alt Top candidate >= autoMatchThreshold (85)
2078
- M-->>M: confidence = 'high'
2079
- M->>M: Mark YNAB transaction as used
2080
- else Top >= suggestedMatchThreshold (60)
2081
- M-->>M: confidence = 'medium'
2082
- else Top >= minimumCandidateScore (40)
2083
- M-->>M: confidence = 'low'
2084
- else No candidates
2085
- M-->>M: confidence = 'none'
2086
- end
2087
-
2088
- M-->>M: Return MatchResult { bankTransaction, bestMatch, candidates, confidence, confidenceScore }
2089
- ```
2090
-
2091
- ---
2092
-
2093
- ## Appendix B: Configuration Examples
2094
-
2095
- ### B.1 Conservative Matching
2096
-
2097
- ```json
2098
- {
2099
- "date_tolerance_days": 3,
2100
- "amount_tolerance_cents": 0,
2101
- "auto_match_threshold": 95,
2102
- "suggestion_threshold": 75,
2103
- "auto_create_transactions": false,
2104
- "auto_update_cleared_status": false,
2105
- "dry_run": true
2106
- }
2107
- ```
2108
-
2109
- ### B.2 Aggressive Matching
2110
-
2111
- ```json
2112
- {
2113
- "date_tolerance_days": 14,
2114
- "amount_tolerance_cents": 5,
2115
- "auto_match_threshold": 75,
2116
- "suggestion_threshold": 50,
2117
- "auto_create_transactions": true,
2118
- "auto_update_cleared_status": true,
2119
- "auto_unclear_missing": true,
2120
- "dry_run": false
2121
- }
2122
- ```
2123
-
2124
- ### B.3 Manual Review Only
2125
-
2126
- ```json
2127
- {
2128
- "auto_create_transactions": false,
2129
- "auto_update_cleared_status": false,
2130
- "auto_unclear_missing": false,
2131
- "dry_run": true,
2132
- "include_structured_data": true
2133
- }
2134
- ```
2135
-
2136
- ---
2137
-
2138
- ## Appendix C: Troubleshooting
2139
-
2140
- ### C.1 Common Issues
2141
-
2142
- | Issue | Likely Cause | Solution |
2143
- |-------|-------------|----------|
2144
- | Low match rate (<50%) | CSV format not detected | Specify `csv_format.preset` explicitly |
2145
- | All matches "none" | Sign inversion needed | Set `invert_bank_amounts: true` |
2146
- | Date parsing errors | Ambiguous date format | Specify `csv_format.date_format` |
2147
- | Amount parsing errors | European number format | Parser auto-detects, check warnings |
2148
- | Balance never aligns | Missing transactions | Check `unmatched_bank` and `unmatched_ynab` |
2149
- | Bulk create failures | Import ID collisions | Check `bulk_operation_details.duplicates_detected` |
2150
-
2151
- ### C.2 Diagnostic Output
2152
-
2153
- ```typescript
2154
- {
2155
- include_diagnostics: true
2156
- }
2157
- ```
2158
-
2159
- Returns:
2160
-
2161
- ```json
2162
- {
2163
- "diagnostics": {
2164
- "csvParsing": {
2165
- "detectedDelimiter": ",",
2166
- "detectedColumns": ["Date", "Description", "Amount"],
2167
- "totalRows": 50,
2168
- "validRows": 48,
2169
- "errors": [
2170
- { "row": 5, "field": "date", "message": "Could not parse date: '99/99/9999'", "rawValue": "99/99/9999" }
2171
- ],
2172
- "warnings": [
2173
- { "row": 12, "message": "Both Debit ($50.00) and Credit ($25.00) have values - using Debit" }
2174
- ]
2175
- },
2176
- "matchingDetails": [
2177
- {
2178
- "bankTxn": { "date": "2025-09-15", "amount": -45230, "payee": "Shell Gas" },
2179
- "bestMatch": {
2180
- "ynabTxn": { "date": "2025-09-15", "amount": -45230, "payee": "Shell" },
2181
- "scores": { "amount": 100, "date": 100, "payee": 85, "combined": 100 }
2182
- },
2183
- "confidence": "high"
2184
- }
2185
- ]
2186
- }
2187
- }
2188
- ```
2189
-
2190
- ---
2191
-
2192
- ## Appendix D: Future Enhancements
2193
-
2194
- ### D.1 Roadmap
2195
-
2196
- | Priority | Feature | Description | Complexity |
2197
- |----------|---------|-------------|------------|
2198
- | P0 | Split Transaction Detection | Match 1 bank txn to multiple YNAB txns | High |
2199
- | P1 | Merchant Learning | Cache successful payee mappings per user | Medium |
2200
- | P1 | Adaptive Thresholds | Learn from user confirmations/rejections | Medium |
2201
- | P2 | Vector Embeddings | Semantic merchant matching with embeddings | High |
2202
- | P2 | Recurring Pattern Detection | Use historical patterns to boost confidence | Medium |
2203
- | P3 | Multi-Currency Support | Handle FX transactions and conversions | High |
2204
-
2205
- ### D.2 Split Transaction Detection
2206
-
2207
- **Design Sketch:**
2208
-
2209
- ```typescript
2210
- // Detect when one bank transaction = multiple YNAB transactions
2211
- function findCombinationMatches(
2212
- bankTxn: BankTransaction,
2213
- ynabTransactions: NormalizedYNABTransaction[],
2214
- maxCombinationSize: number = 3,
2215
- ): CombinationMatch[] {
2216
- // Generate all combinations of size 2 to maxCombinationSize
2217
- const combinations = generateCombinations(ynabTransactions, maxCombinationSize);
2218
-
2219
- for (const combo of combinations) {
2220
- const totalAmount = combo.reduce((sum, txn) => sum + txn.amount, 0);
2221
-
2222
- if (Math.abs(totalAmount - bankTxn.amount) <= AMOUNT_TOLERANCE) {
2223
- // Found matching combination
2224
- return {
2225
- bankTransaction: bankTxn,
2226
- ynabTransactions: combo,
2227
- confidence: calculateCombinationConfidence(combo, bankTxn),
2228
- };
2229
- }
2230
- }
2231
-
2232
- return [];
2233
- }
2234
- ```
2235
-
2236
- ---
2237
-
2238
- **End of Document**
2239
-
2240
- ---
2241
-
2242
- ## Document Metadata
2243
-
2244
- - **File Path:** `C:\Users\ksutk\projects\ynab-mcpb\docs\technical\reconciliation-system-architecture.md`
2245
- - **Related Files:**
2246
- - Implementation: `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\`
2247
- - Existing Docs: `C:\Users\ksutk\projects\ynab-mcpb\docs\reconciliation-flow.md`
2248
- - Design Doc: `C:\Users\ksutk\projects\ynab-mcpb\docs\plans\reconciliation-v2-redesign.md`
2249
- - **Version:** 2.0
2250
- - **Status:** Active Implementation
2251
- - **Last Verified:** 2025-11-30
1
+ # YNAB Reconciliation System - Technical Architecture Documentation
2
+
3
+ **Document Version:** 2.0
4
+ **Last Updated:** 2025-12-01
5
+ **Code Version Compatibility:** v0.16.0+
6
+ **Last Verified Against Code:** v0.16.0 (2025-12-01)
7
+ **Status:** Active Implementation
8
+
9
+ ---
10
+
11
+ ## Executive Summary
12
+
13
+ The YNAB Reconciliation System is a sophisticated transaction matching and balance verification engine that automatically reconciles bank statement CSV files against YNAB transactions. The system achieves 90%+ auto-match accuracy through intelligent fuzzy matching, progressive confidence scoring, and robust CSV parsing with multi-bank format support.
14
+
15
+ **Key Capabilities:**
16
+ - Automated transaction matching with 85%+ confidence threshold
17
+ - Multi-bank CSV format detection (TD, RBC, Scotiabank, Wealthsimple, Tangerine)
18
+ - Integer-based milliunit arithmetic (eliminates floating-point errors)
19
+ - Bulk transaction operations with correlation tracking
20
+ - Smart sign detection for liability accounts
21
+ - Actionable recommendations with prioritization
22
+
23
+ **Target Accuracy:** 90%+ auto-match rate for Canadian bank statements
24
+
25
+ ---
26
+
27
+ ## Table of Contents
28
+
29
+ 1. [System Architecture](#1-system-architecture)
30
+ 2. [Core Components](#2-core-components)
31
+ 3. [Data Flow](#3-data-flow)
32
+ 4. [Transaction Matching Algorithm](#4-transaction-matching-algorithm)
33
+ 5. [CSV Parsing Engine](#5-csv-parsing-engine)
34
+ 6. [Execution Engine](#6-execution-engine)
35
+ 7. [Type System](#7-type-system)
36
+ 8. [Integration Points](#8-integration-points)
37
+ 9. [Performance Characteristics](#9-performance-characteristics)
38
+ 10. [Testing Strategy](#10-testing-strategy)
39
+
40
+ ---
41
+
42
+ ## 1. System Architecture
43
+
44
+ ### 1.1 High-Level Architecture
45
+
46
+ ```mermaid
47
+ graph TB
48
+ A[MCP Tool Entry Point] --> B[index.ts Handler]
49
+ B --> C{Parse CSV}
50
+ C --> D[csvParser.ts]
51
+ D --> E[BankTransaction[]]
52
+
53
+ B --> F{Fetch YNAB Data}
54
+ F --> G[DeltaFetcher]
55
+ G --> H[ynabAdapter.ts]
56
+ H --> I[NormalizedYNABTransaction[]]
57
+
58
+ E --> J[analyzer.ts]
59
+ I --> J
60
+
61
+ J --> K[matcher.ts]
62
+ K --> L[Match Results]
63
+
64
+ J --> M[recommendationEngine.ts]
65
+ M --> N[Actionable Recommendations]
66
+
67
+ L --> O{Execute Actions?}
68
+ N --> O
69
+
70
+ O -->|Yes| P[executor.ts]
71
+ P --> Q[Bulk Operations]
72
+ Q --> R[YNAB API Updates]
73
+
74
+ O -->|No| S[reportFormatter.ts]
75
+ L --> S
76
+ N --> S
77
+ S --> T[Human-Readable Report]
78
+
79
+ R --> S
80
+
81
+ T --> U[MCP Response]
82
+ ```
83
+
84
+ ### 1.2 Component Responsibility Matrix
85
+
86
+ | Component | Primary Role | Inputs | Outputs | Dependencies |
87
+ |-----------|-------------|--------|---------|--------------|
88
+ | **index.ts** | Orchestration | MCP Tool Request | CallToolResult | All components |
89
+ | **csvParser.ts** | CSV Parsing | Raw CSV string | BankTransaction[] | PapaParse, chrono-node |
90
+ | **ynabAdapter.ts** | YNAB Normalization | TransactionDetail[] | NormalizedYNABTransaction[] | YNAB SDK |
91
+ | **matcher.ts** | Matching Algorithm | Bank + YNAB Transactions | MatchResult[] | fuzzball |
92
+ | **analyzer.ts** | Analysis Orchestration | Parsed Data | ReconciliationAnalysis | matcher, recommendationEngine |
93
+ | **executor.ts** | Action Execution | Analysis + Params | ExecutionResult | YNAB API, transactionTools |
94
+ | **recommendationEngine.ts** | Recommendation Generation | Analysis | ActionableRecommendation[] | None (pure logic) |
95
+ | **reportFormatter.ts** | Human-Readable Output | Analysis + Execution | Formatted Report | None (pure formatting) |
96
+
97
+ ### 1.3 Data Architecture
98
+
99
+ ```mermaid
100
+ graph LR
101
+ A[CSV Float Dollars] -->|parseAmount| B[BankTransaction Milliunits]
102
+ C[YNAB API Milliunits] -->|normalizeYNABTransaction| D[NormalizedYNABTransaction]
103
+
104
+ B --> E[Matcher Integer Comparison]
105
+ D --> E
106
+
107
+ E -->|amount === amount| F[Exact Match 100 score]
108
+ E -->|Math.abs diff <= tolerance| G[Tolerance Match 95 score]
109
+
110
+ F --> H[High Confidence 85+]
111
+ G --> H
112
+
113
+ H --> I[Auto-Match Execution]
114
+ ```
115
+
116
+ **Critical Design Decision:** All amounts use **milliunits (integers)** throughout the system. Conversion from CSV floats happens once at the parser boundary, then all comparisons use exact integer arithmetic (`===` instead of `Math.abs(a - b) < epsilon`).
117
+
118
+ ---
119
+
120
+ ## 2. Core Components
121
+
122
+ ### 2.1 Entry Point: index.ts
123
+
124
+ **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\index.ts`
125
+
126
+ **Responsibilities:**
127
+ - MCP tool interface implementation
128
+ - Schema validation (ReconcileAccountSchema)
129
+ - CSV vs File input handling
130
+ - Date range auto-detection
131
+ - Smart sign inversion for liability accounts
132
+ - Orchestration of analysis and execution phases
133
+
134
+ **Key Functions:**
135
+
136
+ ```typescript
137
+ export async function handleReconcileAccount(
138
+ ynabAPI: ynab.API,
139
+ deltaFetcher: DeltaFetcher,
140
+ params: ReconcileAccountRequest,
141
+ ): Promise<CallToolResult>
142
+ ```
143
+
144
+ **Decision Tree:**
145
+
146
+ ```mermaid
147
+ graph TD
148
+ A[handleReconcileAccount] --> B{CSV Source?}
149
+ B -->|csv_data| C[Use inline CSV]
150
+ B -->|csv_file_path| D[Read from filesystem]
151
+
152
+ C --> E{Account Type?}
153
+ D --> E
154
+
155
+ E -->|Liability| F[Negate statement_balance]
156
+ E -->|Asset| G[Use statement_balance as-is]
157
+
158
+ F --> H{invert_bank_amounts set?}
159
+ G --> H
160
+
161
+ H -->|Yes| I[Use explicit value]
162
+ H -->|No| J[Auto-detect sign inversion]
163
+
164
+ I --> K[analyzeReconciliation]
165
+ J --> K
166
+
167
+ K --> L{Execute Actions?}
168
+ L -->|auto_create/update enabled| M[executeReconciliation]
169
+ L -->|Analysis only| N[Skip execution]
170
+
171
+ M --> O[Build Response Payload]
172
+ N --> O
173
+
174
+ O --> P[Return MCP Response]
175
+ ```
176
+
177
+ **Configuration Mapping:**
178
+
179
+ ```typescript
180
+ // V2 Matching Configuration from Request Parameters
181
+ const config: MatchingConfig = {
182
+ weights: {
183
+ amount: 0.5, // 50% weight
184
+ date: 0.15, // 15% weight
185
+ payee: 0.35, // 35% weight
186
+ },
187
+ dateToleranceDays: params.date_tolerance_days ?? 5,
188
+ amountToleranceMilliunits: (params.amount_tolerance_cents ?? 1) * 10,
189
+ autoMatchThreshold: params.auto_match_threshold ?? 90,
190
+ suggestedMatchThreshold: params.suggestion_threshold ?? 60,
191
+ minimumCandidateScore: 40,
192
+ exactAmountBonus: 10,
193
+ exactDateBonus: 5,
194
+ exactPayeeBonus: 10,
195
+ };
196
+ ```
197
+
198
+ ---
199
+
200
+ ### 2.2 CSV Parser: csvParser.ts
201
+
202
+ **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\csvParser.ts`
203
+
204
+ **Purpose:** Parse bank CSV exports into standardized BankTransaction objects with robust error handling and multi-format support.
205
+
206
+ **Architecture:**
207
+
208
+ ```mermaid
209
+ graph TD
210
+ A[Raw CSV String] --> B{Security Check}
211
+ B -->|Size > 10MB| C[Throw Error]
212
+ B -->|Size OK| D[Auto-Detect Format]
213
+
214
+ D --> E{Preset Detected?}
215
+ E -->|Yes| F[Load Bank Preset]
216
+ E -->|No| G[Use Default Columns]
217
+
218
+ F --> H[PapaParse]
219
+ G --> H
220
+
221
+ H --> I{Header Row?}
222
+ I -->|Yes| J[Parse with Headers]
223
+ I -->|No| K[Parse by Index]
224
+
225
+ J --> L[Process Each Row]
226
+ K --> L
227
+
228
+ L --> M{Parse Date}
229
+ M -->|ISO YYYY-MM-DD| N[Direct Parse]
230
+ M -->|Format Hint| O[Apply Preset Format]
231
+ M -->|Fallback| P[chrono-node Parse]
232
+
233
+ N --> Q{Parse Amount}
234
+ O --> Q
235
+ P --> Q
236
+
237
+ Q -->|Single Column| R[parseAmount]
238
+ Q -->|Debit/Credit| S[Combine Columns]
239
+
240
+ R --> T{Warnings?}
241
+ S --> U{Both Populated?}
242
+
243
+ U -->|Yes| V[Emit Warning]
244
+ U -->|No| T
245
+ V --> T
246
+
247
+ T --> W[Create BankTransaction]
248
+ W --> X[Sanitize Description]
249
+ X --> Y[Return Results + Errors + Warnings]
250
+ ```
251
+
252
+ **Bank Presets:**
253
+
254
+ ```typescript
255
+ export const BANK_PRESETS: Record<string, BankPreset> = {
256
+ 'td': {
257
+ name: 'TD Canada Trust',
258
+ header: false, // Headerless CSV support
259
+ dateColumn: ['0', 'Date'],
260
+ debitColumn: '2',
261
+ creditColumn: '3',
262
+ descriptionColumn: ['1', 'Description'],
263
+ dateFormat: 'MDY', // MM/DD/YYYY
264
+ },
265
+ 'rbc': {
266
+ name: 'RBC Royal Bank',
267
+ dateColumn: ['Transaction Date', 'Date'],
268
+ debitColumn: 'Debit',
269
+ creditColumn: 'Credit',
270
+ descriptionColumn: ['Description 1', 'Description'],
271
+ dateFormat: 'YMD', // YYYY-MM-DD
272
+ },
273
+ 'wealthsimple': {
274
+ name: 'Wealthsimple',
275
+ dateColumn: ['Date'],
276
+ amountColumn: ['Amount'],
277
+ descriptionColumn: ['Description', 'Payee'],
278
+ amountMultiplier: 1, // Already in correct sign
279
+ dateFormat: 'YMD',
280
+ },
281
+ // ... scotiabank, tangerine
282
+ };
283
+ ```
284
+
285
+ **Amount Conversion Logic (validated via `parseAmount`):**
286
+
287
+ ```typescript
288
+ function parseAmount(str: string): {
289
+ valid: boolean;
290
+ valueMilliunits: number;
291
+ reason?: string;
292
+ } {
293
+ if (!str || !str.trim()) {
294
+ return { valid: false, valueMilliunits: 0, reason: 'Missing amount value' };
295
+ }
296
+
297
+ let cleaned = str.replace(/[$€£¥]/g, '').replace(/\b(CAD|USD|EUR|GBP)\b/gi, '').trim();
298
+
299
+ if (cleaned.startsWith('(') && cleaned.endsWith(')')) {
300
+ cleaned = '-' + cleaned.slice(1, -1);
301
+ }
302
+
303
+ if (/^-?\d{1,3}(\.\d{3})+,\d{2}$/.test(cleaned)) {
304
+ cleaned = cleaned.replace(/\./g, '').replace(',', '.');
305
+ }
306
+
307
+ if (cleaned.includes('.')) {
308
+ cleaned = cleaned.replace(/,/g, '');
309
+ }
310
+
311
+ const dollars = parseFloat(cleaned);
312
+ if (!Number.isFinite(dollars)) {
313
+ return { valid: false, valueMilliunits: 0, reason: `Invalid amount: "${str}"` };
314
+ }
315
+
316
+ return { valid: true, valueMilliunits: Math.round(dollars * 1000) };
317
+ }
318
+ ```
319
+
320
+ **Security Measures:**
321
+
322
+ ```typescript
323
+ // Security: Remove malicious Unicode characters
324
+ rawDesc = rawDesc
325
+ .replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // ASCII + C1 control chars
326
+ .replace(/[\u202A-\u202E\u2066-\u2069]/g, '') // Bidirectional overrides
327
+ .replace(/[\u200B-\u200D\uFEFF]/g, '') // Zero-width chars
328
+ .replace(/[\u2028-\u2029]/g, '') // Line/paragraph separators
329
+ .substring(0, 500); // YNAB max memo length
330
+ ```
331
+
332
+ ---
333
+
334
+ ### 2.3 Transaction Matcher: matcher.ts
335
+
336
+ **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\matcher.ts`
337
+
338
+ **Purpose:** Match bank transactions to YNAB transactions using multi-dimensional scoring with configurable thresholds.
339
+
340
+ **Matching Pipeline:**
341
+
342
+ ```mermaid
343
+ graph TD
344
+ A[BankTransaction] --> B{Sign Check}
345
+ B -->|Signs Differ| C[Skip Candidate]
346
+ B -->|Signs Match| D{Amount Tolerance}
347
+
348
+ D -->|Diff > Tolerance| C
349
+ D -->|Diff <= Tolerance| E[Calculate Scores]
350
+
351
+ E --> F[Amount Score 0-100]
352
+ E --> G[Date Score 0-100]
353
+ E --> H[Payee Score 0-100]
354
+
355
+ F --> I[Weighted Combination]
356
+ G --> I
357
+ H --> I
358
+
359
+ I --> J{Apply Bonuses}
360
+ J -->|Amount === exact| K[+10 bonus]
361
+ J -->|Date === exact| L[+5 bonus]
362
+ J -->|Payee >= 95| M[+10 bonus]
363
+
364
+ K --> N[Final Combined Score]
365
+ L --> N
366
+ M --> N
367
+
368
+ N --> O{Score >= Min Candidate?}
369
+ O -->|No| C
370
+ O -->|Yes| P[Add to Candidates]
371
+
372
+ P --> Q[Sort by Score Descending]
373
+ Q --> R{Top Score >= Auto-Match?}
374
+
375
+ R -->|Yes| S[confidence: high]
376
+ R -->|No| T{Score >= Suggested?}
377
+
378
+ T -->|Yes| U[confidence: medium]
379
+ T -->|No| V{Score >= Min?}
380
+
381
+ V -->|Yes| W[confidence: low]
382
+ V -->|No| X[confidence: none]
383
+ ```
384
+
385
+ **Scoring Algorithms:**
386
+
387
+ **Amount Score (Integer Comparison):**
388
+
389
+ ```typescript
390
+ const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
391
+ let amountScore: number;
392
+
393
+ if (amountDiff === 0) {
394
+ // Exact integer match - no floating point issues!
395
+ amountScore = 100;
396
+ } else if (amountDiff <= config.amountToleranceMilliunits) { // Default: 10 (1 cent)
397
+ amountScore = 95;
398
+ } else if (amountDiff <= 1000) { // Within $1
399
+ amountScore = 80 - (amountDiff / 1000 * 20);
400
+ } else {
401
+ amountScore = Math.max(0, 60 - (amountDiff / 1000 * 5));
402
+ }
403
+ ```
404
+
405
+ **Date Score (Days Difference):**
406
+
407
+ ```typescript
408
+ const daysDiff = Math.abs(bankDate.getTime() - ynabDate.getTime()) / (1000 * 60 * 60 * 24);
409
+ let dateScore: number;
410
+
411
+ if (daysDiff < 0.5) {
412
+ dateScore = 100; // Same day
413
+ } else if (daysDiff <= 1) {
414
+ dateScore = 95; // 1 day
415
+ } else if (daysDiff <= config.dateToleranceDays) { // Default: 7 days
416
+ dateScore = 90 - ((daysDiff - 1) * (40 / config.dateToleranceDays));
417
+ } else {
418
+ dateScore = Math.max(0, 50 - ((daysDiff - config.dateToleranceDays) * 5));
419
+ }
420
+ ```
421
+
422
+ **Payee Score (Fuzzy Matching with Fuzzball):**
423
+
424
+ ```typescript
425
+ import * as fuzz from 'fuzzball';
426
+
427
+ function calculatePayeeScore(bankPayee: string, ynabPayee: string | null): number {
428
+ if (!ynabPayee) return 30;
429
+
430
+ const scores = [
431
+ fuzz.token_set_ratio(bankPayee, ynabPayee), // Handles word order
432
+ fuzz.token_sort_ratio(bankPayee, ynabPayee), // Alphabetizes then compares
433
+ fuzz.partial_ratio(bankPayee, ynabPayee), // Best substring match
434
+ fuzz.WRatio(bankPayee, ynabPayee), // Weighted combination
435
+ ];
436
+
437
+ return Math.max(...scores);
438
+ }
439
+ ```
440
+
441
+ **Why Fuzzball?**
442
+
443
+ | Input Bank | Input YNAB | Levenshtein | Fuzzball token_set_ratio |
444
+ |------------|------------|-------------|--------------------------|
445
+ | "AMZN MKTP CA*123456" | "Amazon" | 45 | 90 |
446
+ | "SQ *COFFEE SHOP TORONTO" | "Square Coffee" | 38 | 85 |
447
+ | "PAYPAL *NETFLIX" | "Netflix" | 54 | 100 |
448
+
449
+ **Tie-Breaking Rules:**
450
+
451
+ ```typescript
452
+ candidates.sort((a, b) => {
453
+ // 1. Sort by combined score
454
+ const scoreDiff = b.scores.combined - a.scores.combined;
455
+ if (scoreDiff !== 0) return scoreDiff;
456
+
457
+ // 2. Prefer uncleared over cleared (expecting confirmation)
458
+ const aUncleared = a.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
459
+ const bUncleared = b.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
460
+ if (aUncleared !== bUncleared) return bUncleared - aUncleared;
461
+
462
+ // 3. Prefer closer date proximity
463
+ const bankTime = new Date(bankTxn.date).getTime();
464
+ const aDiff = Math.abs(bankTime - new Date(a.ynabTransaction.date).getTime());
465
+ const bDiff = Math.abs(bankTime - new Date(b.ynabTransaction.date).getTime());
466
+ if (aDiff !== bDiff) return aDiff - bDiff;
467
+
468
+ return 0;
469
+ });
470
+ ```
471
+
472
+ ---
473
+
474
+ ### 2.4 Analyzer: analyzer.ts
475
+
476
+ **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\analyzer.ts`
477
+
478
+ **Purpose:** Orchestrate the analysis phase, coordinating CSV parsing, YNAB normalization, matching, and insight generation.
479
+
480
+ **Analysis Pipeline:**
481
+
482
+ ```mermaid
483
+ graph TD
484
+ A[analyzeReconciliation] --> B{CSV Pre-Parsed?}
485
+ B -->|Yes| C[Use Provided Result]
486
+ B -->|No| D[parseCSV]
487
+
488
+ C --> E[BankTransaction[]]
489
+ D --> E
490
+
491
+ A --> F[normalizeYNABTransactions]
492
+ F --> G[NormalizedYNABTransaction[]]
493
+
494
+ E --> H[findMatches]
495
+ G --> H
496
+
497
+ H --> I[MatchResult[]]
498
+
499
+ I --> J[Categorize by Confidence]
500
+ J --> K[auto_matches: high]
501
+ J --> L[suggested_matches: medium]
502
+ J --> M[unmatchedBank: low/none]
503
+
504
+ G --> N[Find Unmatched YNAB]
505
+ I --> N
506
+ N --> O[unmatched_ynab]
507
+
508
+ K --> P[calculateBalances]
509
+ G --> P
510
+ P --> Q[BalanceInfo]
511
+
512
+ K --> R[generateSummary]
513
+ L --> R
514
+ M --> R
515
+ O --> R
516
+ Q --> R
517
+ R --> S[ReconciliationSummary]
518
+
519
+ S --> T[generateNextSteps]
520
+ T --> U[next_steps: string[]]
521
+
522
+ M --> V[detectInsights]
523
+ S --> V
524
+ Q --> V
525
+ V --> W[insights: ReconciliationInsight[]]
526
+
527
+ K --> X{Recommendations Enabled?}
528
+ L --> X
529
+ M --> X
530
+ O --> X
531
+
532
+ X -->|Yes| Y[generateRecommendations]
533
+ Y --> Z[recommendations: ActionableRecommendation[]]
534
+
535
+ S --> AA[Build ReconciliationAnalysis]
536
+ U --> AA
537
+ W --> AA
538
+ Z --> AA
539
+
540
+ AA --> AB[Return Analysis]
541
+ ```
542
+
543
+ **Insight Detection:**
544
+
545
+ ```typescript
546
+ function detectInsights(
547
+ unmatchedBank: BankTransaction[],
548
+ summary: ReconciliationSummary,
549
+ balances: BalanceInfo,
550
+ currency: string,
551
+ csvErrors: ParseError[],
552
+ csvWarnings: ParseWarning[],
553
+ ): ReconciliationInsight[]
554
+ ```
555
+
556
+ **Insight Types:**
557
+
558
+ | Type | Severity | Trigger | Purpose |
559
+ |------|----------|---------|---------|
560
+ | `csv-parse-errors` | critical/warning | CSV parsing errors | Surface parsing failures |
561
+ | `csv-parse-warnings` | info | CSV parsing warnings | Surface ambiguous data |
562
+ | `repeat_amount` | critical/warning | 2+ unmatched txns same amount | Highlight quick wins |
563
+ | `balance-gap` | critical/warning | Discrepancy > $1.00 | Focus attention on gap |
564
+
565
+ **Example Insight:**
566
+
567
+ ```typescript
568
+ {
569
+ id: 'repeat--45230',
570
+ type: 'repeat_amount',
571
+ severity: 'warning',
572
+ title: '3 unmatched transactions at -$45.23',
573
+ description: 'The bank statement shows 3 unmatched transaction(s) at -$45.23. Repeated amounts are usually the quickest wins — reconcile these first.',
574
+ evidence: {
575
+ amount: -45230, // Milliunits
576
+ occurrences: 3,
577
+ dates: ['2025-09-15', '2025-09-20', '2025-09-25'],
578
+ csv_rows: [2, 5, 8],
579
+ },
580
+ }
581
+ ```
582
+
583
+ ---
584
+
585
+ ### 2.5 Recommendation Engine: recommendationEngine.ts
586
+
587
+ **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\recommendationEngine.ts`
588
+
589
+ **Purpose:** Generate actionable, prioritized recommendations from reconciliation analysis results.
590
+
591
+ **Recommendation Types:**
592
+
593
+ ```mermaid
594
+ graph TD
595
+ A[ReconciliationAnalysis] --> B{Process Insights}
596
+ B --> C{Insight Type?}
597
+
598
+ C -->|near_match| D[createNearMatchRecommendation]
599
+ C -->|repeat_amount| E[createRepeatAmountRecommendations]
600
+ C -->|anomaly| F[createManualReviewRecommendation]
601
+
602
+ D --> G[ManualReviewRecommendation]
603
+ E --> G
604
+ F --> G
605
+
606
+ A --> H{Process Unmatched Bank}
607
+ H --> I[createUnmatchedBankRecommendation]
608
+ I --> J[CreateTransactionRecommendation]
609
+
610
+ A --> K{Process Suggested Matches}
611
+ K --> L{Has YNAB Transaction?}
612
+
613
+ L -->|Yes + Confidence high/medium| M[createSuggestedMatchRecommendation]
614
+ L -->|No| N[Create Missing Transaction]
615
+ L -->|Multiple Candidates| O[Combination Match]
616
+
617
+ M --> P[ReviewDuplicateRecommendation]
618
+ N --> J
619
+ O --> Q[createCombinationReviewRecommendation]
620
+ Q --> G
621
+
622
+ A --> R{Process Unmatched YNAB}
623
+ R -->|Uncleared| S[createUpdateClearedRecommendation]
624
+ S --> T[UpdateClearedRecommendation]
625
+
626
+ G --> U[All Recommendations]
627
+ J --> U
628
+ P --> U
629
+ T --> U
630
+
631
+ U --> V[sortRecommendations]
632
+ V --> W{Sort Order}
633
+
634
+ W -->|1. Priority| X[High > Medium > Low]
635
+ W -->|2. Confidence| Y[0.95 > 0.80 > 0.60]
636
+
637
+ X --> Z[Sorted Recommendations]
638
+ Y --> Z
639
+ ```
640
+
641
+ **Recommendation Schema:**
642
+
643
+ ```typescript
644
+ export type ActionableRecommendation =
645
+ | CreateTransactionRecommendation
646
+ | UpdateClearedRecommendation
647
+ | ReviewDuplicateRecommendation
648
+ | ManualReviewRecommendation;
649
+
650
+ interface CreateTransactionRecommendation {
651
+ action_type: 'create_transaction';
652
+ priority: 'high' | 'medium' | 'low';
653
+ confidence: number; // 0-1
654
+ message: string;
655
+ reason: string;
656
+ estimated_impact: MoneyValue;
657
+ parameters: {
658
+ account_id: string;
659
+ date: string;
660
+ amount: number; // Milliunits
661
+ payee_name: string;
662
+ memo?: string;
663
+ cleared: 'cleared' | 'uncleared';
664
+ approved: boolean;
665
+ };
666
+ }
667
+ ```
668
+
669
+ **Confidence Levels:**
670
+
671
+ ```typescript
672
+ const CONFIDENCE = {
673
+ CREATE_EXACT_MATCH: 0.95,
674
+ NEAR_MATCH_REVIEW: 0.7,
675
+ REPEAT_AMOUNT: 0.75,
676
+ ANOMALY_REVIEW: 0.5,
677
+ UNMATCHED_BANK: 0.8,
678
+ UPDATE_CLEARED: 0.6,
679
+ } as const;
680
+ ```
681
+
682
+ **Priority Assignment:**
683
+
684
+ | Scenario | Action Type | Priority | Confidence | Rationale |
685
+ |----------|-------------|----------|------------|-----------|
686
+ | Unmatched bank txn | create_transaction | medium | 0.80 | Safe to create, medium urgency |
687
+ | Exact match suggestion | review_duplicate | high | 0.95 | High confidence match needs confirmation |
688
+ | Combination match | manual_review | medium | 0.70 | Complex scenario, needs human judgment |
689
+ | Uncleared YNAB txn | update_cleared | low | 0.60 | Cleanup action, low urgency |
690
+ | Anomaly insight | manual_review | low | 0.50 | Investigation needed |
691
+
692
+ ---
693
+
694
+ ### 2.6 Executor: executor.ts
695
+
696
+ **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\executor.ts`
697
+
698
+ **Purpose:** Execute reconciliation actions (create, update, clear, reconcile transactions) with bulk operation support and error handling.
699
+
700
+ **Execution Phases:**
701
+
702
+ ```mermaid
703
+ graph TD
704
+ A[executeReconciliation] --> B[Initialize State]
705
+ B --> C[clearedDeltaMilli = cleared - statement]
706
+
707
+ C --> D{Balance Aligned?}
708
+ D -->|Yes| E[Skip Execution]
709
+ D -->|No| F[PHASE 1: Auto-Create Missing]
710
+
711
+ F --> G{auto_create_transactions?}
712
+ G -->|No| H[Skip Phase 1]
713
+ G -->|Yes| I{2+ Unmatched Bank?}
714
+
715
+ I -->|Yes| J[Bulk Create Path]
716
+ I -->|No| K[Sequential Create Path]
717
+
718
+ J --> L[Build Batches Until Balance Aligns]
719
+ L --> M[Chunk by MAX_BULK_CREATE_CHUNK = 100]
720
+ M --> N[Process Each Chunk]
721
+
722
+ N --> O{Bulk API Success?}
723
+ O -->|Yes| P[Correlate Results]
724
+ O -->|No| Q[Sequential Fallback]
725
+
726
+ P --> R[Update clearedDeltaMilli]
727
+ Q --> R
728
+ K --> R
729
+
730
+ R --> S{Balance Aligned?}
731
+ S -->|Yes| T[Skip Remaining Phases]
732
+ S -->|No| U[PHASE 2: Update Matched Txns]
733
+
734
+ U --> V{auto_update_cleared_status?}
735
+ V -->|Yes| W[Collect Updates]
736
+ V -->|No| X[Skip Phase 2]
737
+
738
+ W --> Y[Chunk by MAX_BULK_UPDATE_CHUNK = 100]
739
+ Y --> Z[Batch Update API Call]
740
+
741
+ Z --> AA{Balance Aligned?}
742
+ AA -->|Yes| T
743
+ AA -->|No| AB[PHASE 3: Auto-Unclear Missing]
744
+
745
+ AB --> AC{auto_unclear_missing?}
746
+ AC -->|Yes| AD[Collect Unclear Updates]
747
+ AC -->|No| AE[Skip Phase 3]
748
+
749
+ AD --> AF[Batch Unclear API Call]
750
+
751
+ AF --> AG{Balance Aligned?}
752
+ AG -->|Yes| T
753
+ AG -->|No| AH[PHASE 4: Mark as Reconciled]
754
+
755
+ AH --> AI[Batch Reconcile Matched Txns]
756
+
757
+ T --> AJ[PHASE 5: Balance Reconciliation]
758
+ AI --> AJ
759
+ AH --> AJ
760
+
761
+ AJ --> AK{statement_date provided?}
762
+ AK -->|Yes| AL[buildBalanceReconciliation]
763
+ AK -->|No| AM[Skip Balance Snapshot]
764
+
765
+ AL --> AN[Refresh Account Snapshot]
766
+ AM --> AN
767
+
768
+ AN --> AO[Build Recommendations]
769
+ AO --> AP[Return ExecutionResult]
770
+ ```
771
+
772
+ **Bulk Operation Correlation:**
773
+
774
+ ```typescript
775
+ // Generate deterministic import_id for deduplication
776
+ function generateBulkImportId(
777
+ accountId: string,
778
+ date: string,
779
+ amountMilli: number,
780
+ payee?: string | null,
781
+ ): string {
782
+ const normalizedPayee = (payee ?? '').trim().toLowerCase();
783
+ const raw = `${accountId}|${date}|${amountMilli}|${normalizedPayee}`;
784
+ const digest = createHash('sha256').update(raw).digest('hex').slice(0, 24);
785
+ return `YNAB:bulk:${digest}`;
786
+ }
787
+
788
+ // Correlate bulk API response with requests
789
+ const correlated = correlateResults(
790
+ chunk.map(entry => toCorrelationPayload(entry.saveTransaction)),
791
+ response.data,
792
+ duplicateImportIds
793
+ );
794
+
795
+ for (const result of correlated) {
796
+ if (result.status === 'created') {
797
+ // Success - record action
798
+ } else if (result.status === 'duplicate') {
799
+ // Duplicate detected - emit warning
800
+ bulkDetails.duplicates_detected += 1;
801
+ } else {
802
+ // Failure - record error
803
+ bulkDetails.transaction_failures += 1;
804
+ }
805
+ }
806
+ ```
807
+
808
+ **Error Handling Strategy:**
809
+
810
+ ```mermaid
811
+ graph TD
812
+ A[Bulk Chunk Failure] --> B[normalizeYnabError]
813
+ B --> C{shouldPropagateYnabError?}
814
+
815
+ C -->|Yes 400/401/403/404/429/500/503| D[Throw Error]
816
+ C -->|No Other Status| E[Log Warning]
817
+
818
+ E --> F[Sequential Fallback]
819
+ F --> G[Process Each Transaction Individually]
820
+
821
+ G --> H{Individual Success?}
822
+ H -->|Yes| I[Record Success]
823
+ H -->|No| J{shouldPropagateYnabError?}
824
+
825
+ J -->|Yes| D
826
+ J -->|No| K[Record Failure + Continue]
827
+
828
+ I --> L[Update Metrics]
829
+ K --> L
830
+
831
+ L --> M{More Transactions?}
832
+ M -->|Yes| G
833
+ M -->|No| N[Return with Partial Success]
834
+ ```
835
+
836
+ **Bulk Operation Metrics:**
837
+
838
+ ```typescript
839
+ export interface BulkOperationDetails {
840
+ chunks_processed: number;
841
+ bulk_successes: number;
842
+ sequential_fallbacks: number;
843
+ duplicates_detected: number;
844
+ failed_transactions: number; // Backward-compatible alias
845
+ bulk_chunk_failures: number; // API-level failures (entire chunk)
846
+ transaction_failures: number; // Canonical per-transaction failures
847
+ sequential_attempts?: number; // Fallback creation attempts
848
+ }
849
+ ```
850
+
851
+ ---
852
+
853
+ ### 2.7 Report Formatter: reportFormatter.ts
854
+
855
+ **File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\reportFormatter.ts`
856
+
857
+ **Purpose:** Format reconciliation results into human-readable reports with structured sections.
858
+
859
+ **Report Structure:**
860
+
861
+ ```
862
+ 📊 [Account Name] Reconciliation Report
863
+ ═══════════════════════════════════════════════════════════════
864
+
865
+ Statement Period: [date_range]
866
+
867
+ BALANCE CHECK
868
+ ═══════════════════════════════════════════════════════════════
869
+ ✓ YNAB Cleared Balance: [amount]
870
+ ✓ Statement Balance: [amount]
871
+
872
+ [✅ BALANCES MATCH PERFECTLY | ❌ DISCREPANCY: [amount]]
873
+
874
+ TRANSACTION ANALYSIS
875
+ ═══════════════════════════════════════════════════════════════
876
+ ✓ Automatically matched: [count] of [total] transactions
877
+ ✓ Suggested matches: [count]
878
+ ✓ Unmatched bank: [count]
879
+ ✓ Unmatched YNAB: [count]
880
+
881
+ ❌ UNMATCHED BANK TRANSACTIONS:
882
+ [date] - [payee] [±amount]
883
+ ...
884
+
885
+ 💡 SUGGESTED MATCHES:
886
+ [date] - [payee] [±amount] ([confidence]% confidence)
887
+ ...
888
+
889
+ KEY INSIGHTS
890
+ ═══════════════════════════════════════════════════════════════
891
+ [🚨|⚠️|ℹ️] [insight.title]
892
+ [insight.description]
893
+ Evidence: [summary]
894
+
895
+ EXECUTION SUMMARY
896
+ ═══════════════════════════════════════════════════════════════
897
+ • Transactions created: [count]
898
+ • Transactions updated: [count]
899
+ • Date adjustments: [count]
900
+
901
+ Recommendations:
902
+ • [recommendation 1]
903
+ • [recommendation 2]
904
+ ...
905
+
906
+ [⚠️ Dry run only — no YNAB changes were applied. | ✅ Changes applied to YNAB.]
907
+
908
+ RECOMMENDED ACTIONS
909
+ ═══════════════════════════════════════════════════════════════
910
+ • [next_step 1]
911
+ • [next_step 2]
912
+ ...
913
+ ```
914
+
915
+ **Format Options:**
916
+
917
+ ```typescript
918
+ export interface ReportFormatterOptions {
919
+ accountName?: string;
920
+ accountId?: string;
921
+ currencyCode?: string;
922
+ includeDetailedMatches?: boolean;
923
+ maxUnmatchedToShow?: number; // Default: 5
924
+ maxInsightsToShow?: number; // Default: 3
925
+ }
926
+ ```
927
+
928
+ ---
929
+
930
+ ## 3. Data Flow
931
+
932
+ ### 3.1 End-to-End Data Flow Diagram
933
+
934
+ ```mermaid
935
+ sequenceDiagram
936
+ participant User
937
+ participant MCP as MCP Tool Interface
938
+ participant Handler as index.ts Handler
939
+ participant Parser as csvParser.ts
940
+ participant Fetcher as DeltaFetcher
941
+ participant Adapter as ynabAdapter.ts
942
+ participant Analyzer as analyzer.ts
943
+ participant Matcher as matcher.ts
944
+ participant RecEngine as recommendationEngine.ts
945
+ participant Executor as executor.ts
946
+ participant YNAB as YNAB API
947
+ participant Formatter as reportFormatter.ts
948
+
949
+ User->>MCP: reconcile_account(csv_data, statement_balance, ...)
950
+ MCP->>Handler: handleReconcileAccount(params)
951
+
952
+ Handler->>Parser: parseCSV(csvContent, options)
953
+ Parser->>Parser: Auto-detect format
954
+ Parser->>Parser: PapaParse
955
+ Parser->>Parser: parseAmount
956
+ Parser-->>Handler: { transactions: BankTransaction[], errors, warnings }
957
+
958
+ Handler->>Fetcher: fetchTransactionsByAccount(budget_id, account_id, since_date)
959
+ Fetcher->>YNAB: GET /budgets/{id}/accounts/{id}/transactions
960
+ YNAB-->>Fetcher: TransactionDetail[]
961
+ Fetcher-->>Handler: { data: TransactionDetail[], wasCached, usedDelta }
962
+
963
+ Handler->>Adapter: normalizeYNABTransactions(ynabTransactions)
964
+ Adapter-->>Handler: NormalizedYNABTransaction[]
965
+
966
+ Handler->>Analyzer: analyzeReconciliation(bankTxns, ynabTxns, config)
967
+
968
+ Analyzer->>Matcher: findMatches(bankTxns, ynabTxns, config)
969
+
970
+ loop For each bank transaction
971
+ Matcher->>Matcher: findCandidates(bankTxn, ynabTxns, usedIds)
972
+ Matcher->>Matcher: calculateScores(amount, date, payee)
973
+ Matcher->>Matcher: Sort by combined score
974
+ end
975
+
976
+ Matcher-->>Analyzer: MatchResult[]
977
+
978
+ Analyzer->>Analyzer: Categorize matches (auto, suggested, unmatched)
979
+ Analyzer->>Analyzer: calculateBalances()
980
+ Analyzer->>Analyzer: generateSummary()
981
+ Analyzer->>Analyzer: detectInsights()
982
+
983
+ Analyzer->>RecEngine: generateRecommendations(context)
984
+
985
+ loop For each insight
986
+ RecEngine->>RecEngine: processInsight()
987
+ end
988
+
989
+ loop For each unmatched bank txn
990
+ RecEngine->>RecEngine: createUnmatchedBankRecommendation()
991
+ end
992
+
993
+ loop For each suggested match
994
+ RecEngine->>RecEngine: createSuggestedMatchRecommendation()
995
+ end
996
+
997
+ RecEngine-->>Analyzer: ActionableRecommendation[]
998
+
999
+ Analyzer-->>Handler: ReconciliationAnalysis
1000
+
1001
+ alt auto_create_transactions || auto_update_cleared_status
1002
+ Handler->>Executor: executeReconciliation(analysis, params)
1003
+
1004
+ loop Phase 1: Auto-create missing transactions
1005
+ Executor->>Executor: Build bulk create batches
1006
+ Executor->>YNAB: POST /budgets/{id}/transactions (bulk)
1007
+ YNAB-->>Executor: BulkResponse
1008
+ Executor->>Executor: Correlate results
1009
+ Executor->>Executor: Update clearedDeltaMilli
1010
+ end
1011
+
1012
+ loop Phase 2: Update matched transactions
1013
+ Executor->>Executor: Collect updates (cleared, date adjustments)
1014
+ Executor->>YNAB: PATCH /budgets/{id}/transactions (bulk)
1015
+ YNAB-->>Executor: UpdateResponse
1016
+ end
1017
+
1018
+ loop Phase 3: Auto-unclear missing
1019
+ Executor->>Executor: Collect unclear updates
1020
+ Executor->>YNAB: PATCH /budgets/{id}/transactions (bulk)
1021
+ YNAB-->>Executor: UpdateResponse
1022
+ end
1023
+
1024
+ alt Balance aligned
1025
+ Executor->>YNAB: PATCH /budgets/{id}/transactions (mark reconciled)
1026
+ YNAB-->>Executor: UpdateResponse
1027
+ end
1028
+
1029
+ Executor->>YNAB: GET /budgets/{id}/accounts/{id} (refresh snapshot)
1030
+ YNAB-->>Executor: Account
1031
+
1032
+ Executor-->>Handler: ExecutionResult
1033
+ end
1034
+
1035
+ Handler->>Formatter: formatHumanReadableReport(analysis, execution)
1036
+ Formatter-->>Handler: Formatted report string
1037
+
1038
+ Handler->>Handler: buildReconciliationPayload(analysis, execution)
1039
+ Handler-->>MCP: CallToolResult { human, structured }
1040
+ MCP-->>User: Reconciliation report + structured data
1041
+ ```
1042
+
1043
+ ### 3.2 Data Transformation Chain
1044
+
1045
+ ```mermaid
1046
+ graph LR
1047
+ A[CSV String] -->|parseCSV| B[BankTransaction[]]
1048
+ C[YNAB API] -->|normalizeYNABTransactions| D[NormalizedYNABTransaction[]]
1049
+
1050
+ B --> E{Matching}
1051
+ D --> E
1052
+
1053
+ E -->|findMatches| F[MatchResult[]]
1054
+
1055
+ F -->|Categorize| G[auto_matches]
1056
+ F -->|Categorize| H[suggested_matches]
1057
+ F -->|Categorize| I[unmatched_bank]
1058
+
1059
+ D -->|Filter| J[unmatched_ynab]
1060
+
1061
+ G --> K[ReconciliationAnalysis]
1062
+ H --> K
1063
+ I --> K
1064
+ J --> K
1065
+
1066
+ K -->|generateRecommendations| L[ActionableRecommendation[]]
1067
+
1068
+ K --> M{Execute?}
1069
+ L --> M
1070
+
1071
+ M -->|Yes| N[executeReconciliation]
1072
+ N -->|YNAB API| O[ExecutionResult]
1073
+
1074
+ M -->|No| P[Skip Execution]
1075
+
1076
+ K --> Q[formatHumanReadableReport]
1077
+ O --> Q
1078
+ P --> Q
1079
+
1080
+ Q --> R[Human-Readable Report]
1081
+
1082
+ K --> S[buildReconciliationPayload]
1083
+ O --> S
1084
+
1085
+ S --> T[Structured JSON Payload]
1086
+
1087
+ R --> U[MCP Response]
1088
+ T --> U
1089
+ ```
1090
+
1091
+ ---
1092
+
1093
+ ## 4. Transaction Matching Algorithm
1094
+
1095
+ ### 4.1 Matching Strategy
1096
+
1097
+ The reconciliation system uses a **multi-dimensional weighted scoring algorithm** with configurable thresholds and bonuses.
1098
+
1099
+ **Core Principles:**
1100
+ 1. **Amount is King** (50% weight) - Most reliable signal
1101
+ 2. **Dates are Unreliable** (15% weight) - Banks delay posting 3-7 days
1102
+ 3. **Payees are Fuzzy** (35% weight) - Merchant names vary significantly
1103
+
1104
+ **Default Configuration:**
1105
+
1106
+ ```typescript
1107
+ export const DEFAULT_CONFIG: MatchingConfig = {
1108
+ weights: {
1109
+ amount: 0.5, // 50%
1110
+ date: 0.15, // 15%
1111
+ payee: 0.35, // 35%
1112
+ },
1113
+ amountToleranceMilliunits: 10, // 1 cent (10 milliunits)
1114
+ dateToleranceDays: 7,
1115
+ autoMatchThreshold: 85,
1116
+ suggestedMatchThreshold: 60,
1117
+ minimumCandidateScore: 40,
1118
+ exactAmountBonus: 10,
1119
+ exactDateBonus: 5,
1120
+ exactPayeeBonus: 10,
1121
+ };
1122
+ ```
1123
+
1124
+ ### 4.2 Scoring Examples
1125
+
1126
+ **Example 1: Perfect Match**
1127
+
1128
+ ```
1129
+ Bank: { date: '2025-09-15', amount: -45230, payee: 'Shell Gas' }
1130
+ YNAB: { date: '2025-09-15', amount: -45230, payee: 'Shell' }
1131
+
1132
+ Scores:
1133
+ Amount: 100 (exact match: -45230 === -45230)
1134
+ Date: 100 (same day)
1135
+ Payee: 85 (fuzz.token_set_ratio('Shell Gas', 'Shell'))
1136
+
1137
+ Combined = (100 * 0.5) + (100 * 0.15) + (85 * 0.35) = 94.75
1138
+ Bonuses = +10 (exact amount) + 5 (exact date) = +15
1139
+ Final = 94.75 + 15 = 109.75 → capped at 100
1140
+
1141
+ Confidence: HIGH (≥ 85)
1142
+ ```
1143
+
1144
+ **Example 2: Date Mismatch**
1145
+
1146
+ ```
1147
+ Bank: { date: '2025-09-15', amount: -12799, payee: 'Amazon Marketplace' }
1148
+ YNAB: { date: '2025-09-20', amount: -12799, payee: 'Amazon' }
1149
+
1150
+ Scores:
1151
+ Amount: 100 (exact match)
1152
+ Date: 55 (5 days diff within tolerance)
1153
+ Payee: 92 (fuzz.token_set_ratio)
1154
+
1155
+ Combined = (100 * 0.5) + (55 * 0.15) + (92 * 0.35) = 90.45
1156
+ Bonuses = +10 (exact amount) + 10 (payee ≥ 95) = +20
1157
+ Final = 90.45 + 20 = 110.45 → capped at 100
1158
+
1159
+ Confidence: HIGH (≥ 85)
1160
+ ```
1161
+
1162
+ **Example 3: Suggested Match**
1163
+
1164
+ ```
1165
+ Bank: { date: '2025-09-15', amount: -4520, payee: 'Coffee Shop A' }
1166
+ YNAB: { date: '2025-09-16', amount: -4530, payee: 'Coffee Shop B' }
1167
+
1168
+ Scores:
1169
+ Amount: 95 (10 milliunits diff within tolerance)
1170
+ Date: 95 (1 day diff)
1171
+ Payee: 70 (partial match)
1172
+
1173
+ Combined = (95 * 0.5) + (95 * 0.15) + (70 * 0.35) = 86.25
1174
+ Bonuses = 0 (no exact matches)
1175
+ Final = 86.25
1176
+
1177
+ Confidence: HIGH (≥ 85) but close to threshold
1178
+ ```
1179
+
1180
+ **Example 4: Low Confidence**
1181
+
1182
+ ```
1183
+ Bank: { date: '2025-09-15', amount: -5000, payee: 'Transfer' }
1184
+ YNAB: { date: '2025-09-22', amount: -5005, payee: 'Payment' }
1185
+
1186
+ Scores:
1187
+ Amount: 95 (5 milliunits diff)
1188
+ Date: 35 (7 days diff at edge of tolerance)
1189
+ Payee: 40 (weak match)
1190
+
1191
+ Combined = (95 * 0.5) + (35 * 0.15) + (40 * 0.35) = 66.25
1192
+ Bonuses = 0
1193
+ Final = 66.25
1194
+
1195
+ Confidence: MEDIUM (60-84)
1196
+ ```
1197
+
1198
+ ### 4.3 Candidate Filtering
1199
+
1200
+ **Pre-Filtering (Before Scoring):**
1201
+
1202
+ ```typescript
1203
+ // 1. Sign Check - both must be same sign
1204
+ const bankSign = Math.sign(bankTxn.amount);
1205
+ const ynabSign = Math.sign(ynabTxn.amount);
1206
+ if (bankSign !== ynabSign && bankSign !== 0 && ynabSign !== 0) {
1207
+ continue; // Skip candidate
1208
+ }
1209
+
1210
+ // 2. Amount Tolerance Check - hard filter
1211
+ const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
1212
+ if (amountDiff > config.amountToleranceMilliunits) {
1213
+ continue; // Skip candidate
1214
+ }
1215
+ ```
1216
+
1217
+ **Post-Filtering (After Scoring):**
1218
+
1219
+ ```typescript
1220
+ // Only include if score ≥ minimumCandidateScore (default: 40)
1221
+ if (scores.combined >= config.minimumCandidateScore) {
1222
+ candidates.push({ ynabTransaction, scores, matchReasons });
1223
+ }
1224
+ ```
1225
+
1226
+ ### 4.4 Guardrails & Failure Modes
1227
+
1228
+ **Auto-Match Disabled When:**
1229
+
1230
+ | Condition | Rationale |
1231
+ |-----------|-----------|
1232
+ | Amount score < 80 | Amount is most reliable signal |
1233
+ | Date gap > 14 days | Even with delays, 2+ weeks is suspicious |
1234
+ | Multiple candidates within 5 points | Ambiguous, needs human review |
1235
+ | Payee score < 40 AND date score < 60 | Neither secondary signal is strong |
1236
+ | Transaction has warnings | Parser detected ambiguity |
1237
+
1238
+ **Known Failure Modes:**
1239
+
1240
+ | Failure Mode | Mitigation Strategy |
1241
+ |--------------|---------------------|
1242
+ | Similar merchants (Starbucks #1234 vs #5678) | Require exact amount + date ≤1 day |
1243
+ | Recurring subscriptions | Use date as primary discriminator |
1244
+ | Refunds | Sign check prevents cross-matching |
1245
+ | Split transactions | Combination matching (future) |
1246
+ | Merchant name drift | Payee normalization + token_set_ratio |
1247
+ | Duplicate entries | Prefer uncleared; flag for review |
1248
+
1249
+ ---
1250
+
1251
+ ## 5. CSV Parsing Engine
1252
+
1253
+ ### 5.1 Multi-Format Support
1254
+
1255
+ The CSV parser supports both **headered** and **headerless** CSV files with bank-specific presets.
1256
+
1257
+ **Auto-Detection Strategy:**
1258
+
1259
+ ```mermaid
1260
+ graph TD
1261
+ A[Raw CSV] --> B[Parse First 5 Lines]
1262
+ B --> C{Header Match?}
1263
+
1264
+ C -->|RBC: Description 1| D[RBC Preset]
1265
+ C -->|Contains CAD$| E[TD Preset]
1266
+ C -->|No Match| F{Headerless Pattern?}
1267
+
1268
+ F -->|MM/DD/YYYY + 4+ columns| G[TD Headerless Preset]
1269
+ F -->|No Pattern| H[Default Auto-Detect]
1270
+
1271
+ D --> I[Apply Preset]
1272
+ E --> I
1273
+ G --> I
1274
+ H --> I
1275
+
1276
+ I --> J[PapaParse with Detected Format]
1277
+ ```
1278
+
1279
+ ### 5.2 Date Parsing Priority
1280
+
1281
+ ```mermaid
1282
+ graph TD
1283
+ A[Raw Date String] --> B{ISO Format?}
1284
+ B -->|YYYY-MM-DD| C[Direct Parse UTC]
1285
+ B -->|No| D{Format Hint from Preset?}
1286
+
1287
+ D -->|YMD| E[Apply YYYY-MM-DD Pattern]
1288
+ D -->|MDY| F[Apply MM/DD/YYYY Pattern]
1289
+ D -->|DMY| G[Apply DD/MM/YYYY Pattern]
1290
+ D -->|No Hint| H[chrono-node Fallback]
1291
+
1292
+ C --> I[formatLocalDate]
1293
+ E --> I
1294
+ F --> I
1295
+ G --> I
1296
+ H --> I
1297
+
1298
+ I --> J[YYYY-MM-DD String]
1299
+ ```
1300
+
1301
+ **Why This Priority?**
1302
+
1303
+ 1. **ISO format first** - Unambiguous, no parsing errors
1304
+ 2. **Preset format hint** - Handles ambiguous dates like 02/03/2025 correctly
1305
+ 3. **chrono-node fallback** - Handles natural language and weird formats
1306
+
1307
+ ### 5.3 Amount Parsing Edge Cases
1308
+
1309
+ **Supported Formats:**
1310
+
1311
+ | Input | Detected Format | Output (Milliunits) |
1312
+ |-------|-----------------|---------------------|
1313
+ | `45.23` | Standard | 45230 |
1314
+ | `$45.23` | USD Symbol | 45230 |
1315
+ | `CAD 45.23` | Currency Code | 45230 |
1316
+ | `1,234.56` | Thousands Separator | 1234560 |
1317
+ | `1.234,56` | European Format | 1234560 |
1318
+ | `(45.23)` | Parentheses Negative | -45230 |
1319
+ | `-45.23` | Explicit Negative | -45230 |
1320
+
1321
+ **Debit/Credit Column Handling:**
1322
+
1323
+ ```typescript
1324
+ if (Math.abs(debitMilliunits) > 0 && Math.abs(creditMilliunits) > 0) {
1325
+ // WARNING: Both columns populated - use debit
1326
+ rowWarnings.push(`Both Debit (${debit}) and Credit (${credit}) have values - using Debit`);
1327
+ warnings.push({ row: rowNum, message: rowWarnings[0] });
1328
+ }
1329
+
1330
+ if (Math.abs(debitMilliunits) > 0) {
1331
+ amountMilliunits = -Math.abs(debitMilliunits); // Debits are outflows
1332
+ } else if (Math.abs(creditMilliunits) > 0) {
1333
+ amountMilliunits = Math.abs(creditMilliunits); // Credits are inflows
1334
+ } else {
1335
+ amountMilliunits = 0;
1336
+ }
1337
+ ```
1338
+
1339
+ ### 5.4 Security Measures
1340
+
1341
+ **Input Validation:**
1342
+
1343
+ ```typescript
1344
+ // File size limit: 10MB default
1345
+ const MAX_BYTES = options.maxBytes ?? 10 * 1024 * 1024;
1346
+ if (content.length > MAX_BYTES) {
1347
+ throw new Error(`File size exceeds limit of ${Math.round(MAX_BYTES / 1024 / 1024)}MB`);
1348
+ }
1349
+
1350
+ // Row limit: 10,000 default
1351
+ const maxRows = options.maxRows ?? 10000;
1352
+ ```
1353
+
1354
+ **Unicode Sanitization:**
1355
+
1356
+ ```typescript
1357
+ // Remove malicious/confusing characters
1358
+ rawDesc = rawDesc
1359
+ .replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // Control chars
1360
+ .replace(/[\u202A-\u202E\u2066-\u2069]/g, '') // Bidirectional overrides
1361
+ .replace(/[\u200B-\u200D\uFEFF]/g, '') // Zero-width chars
1362
+ .replace(/[\u2028-\u2029]/g, '') // Line/paragraph separators
1363
+ .substring(0, 500); // YNAB max memo length
1364
+ ```
1365
+
1366
+ ---
1367
+
1368
+ ## 6. Execution Engine
1369
+
1370
+ ### 6.1 Execution Phases
1371
+
1372
+ The executor implements a **4-phase execution strategy** with early termination once balance aligns.
1373
+
1374
+ **Phase Overview:**
1375
+
1376
+ | Phase | Trigger | Purpose | Balance Check |
1377
+ |-------|---------|---------|---------------|
1378
+ | 1 | `auto_create_transactions` | Create missing bank transactions | After each create |
1379
+ | 2 | `auto_update_cleared_status` | Mark matched YNAB txns as cleared | After each update |
1380
+ | 3 | `auto_unclear_missing` | Un-clear YNAB txns not on statement | After each unclear |
1381
+ | 4 | Balance aligned | Mark all matched txns as reconciled | N/A |
1382
+
1383
+ **Early Termination:**
1384
+
1385
+ ```typescript
1386
+ const recordAlignmentIfNeeded = (trigger: string, { log = true } = {}) => {
1387
+ if (balanceAligned) return true;
1388
+
1389
+ if (Math.abs(clearedDeltaMilli) <= balanceToleranceMilli) {
1390
+ balanceAligned = true;
1391
+ if (log) {
1392
+ actions_taken.push({
1393
+ type: 'balance_checkpoint',
1394
+ transaction: null,
1395
+ reason: `Cleared delta ${deltaDisplay} within ±${toleranceDisplay} after ${trigger} - halting`,
1396
+ });
1397
+ }
1398
+ return true;
1399
+ }
1400
+ return false;
1401
+ };
1402
+ ```
1403
+
1404
+ ### 6.2 Bulk vs Sequential Operations
1405
+
1406
+ **Decision Matrix:**
1407
+
1408
+ | Scenario | Strategy | Rationale |
1409
+ |----------|----------|-----------|
1410
+ | 2+ unmatched bank txns | Bulk create | API efficiency |
1411
+ | 1 unmatched bank txn | Sequential create | Simpler error handling |
1412
+ | Bulk API failure | Sequential fallback | Resilience |
1413
+ | Updates (cleared, date) | Always bulk (chunked) | YNAB supports batch updates |
1414
+
1415
+ **Bulk Create Flow:**
1416
+
1417
+ ```typescript
1418
+ // Build batches until balance aligns or all processed
1419
+ while (nextBankIndex < orderedUnmatchedBank.length && !balanceAligned) {
1420
+ const batch: PreparedBulkCreateEntry[] = [];
1421
+ let projectedDelta = clearedDeltaMilli;
1422
+
1423
+ // Greedy batch: add transactions until balance would align
1424
+ while (nextBankIndex < orderedUnmatchedBank.length) {
1425
+ const entry = buildPreparedEntry(orderedUnmatchedBank[nextBankIndex]);
1426
+ batch.push(entry);
1427
+ nextBankIndex += 1;
1428
+ projectedDelta = addMilli(projectedDelta, entry.amountMilli);
1429
+
1430
+ if (Math.abs(projectedDelta) <= balanceToleranceMilli) {
1431
+ break; // This batch should align balance
1432
+ }
1433
+ }
1434
+
1435
+ // Process batch in chunks of MAX_BULK_CREATE_CHUNK (100)
1436
+ const chunks = chunkArray(batch, MAX_BULK_CREATE_CHUNK);
1437
+ for (const chunk of chunks) {
1438
+ try {
1439
+ await processBulkChunk(chunk, chunkIndex);
1440
+ } catch (error) {
1441
+ // Fallback to sequential
1442
+ await processSequentialEntries(chunk, { chunkIndex, fallbackError: error });
1443
+ }
1444
+ }
1445
+ }
1446
+ ```
1447
+
1448
+ ### 6.3 Correlation Tracking
1449
+
1450
+ **Problem:** Bulk API returns transactions in arbitrary order. How to match responses to requests?
1451
+
1452
+ **Solution:** Hash-based correlation keys
1453
+
1454
+ ```typescript
1455
+ // Generate correlation key from transaction attributes
1456
+ export function generateCorrelationKey(txn: CorrelationPayload): string {
1457
+ const key = `${txn.account_id}|${txn.date}|${txn.amount}|${normalizePayee(txn.payee_name)}`;
1458
+ return createHash('sha256').update(key).digest('hex').slice(0, 16);
1459
+ }
1460
+
1461
+ // Correlate responses
1462
+ export function correlateResults(
1463
+ requests: CorrelationPayload[],
1464
+ response: BulkResponse,
1465
+ duplicateImportIds: Set<string>,
1466
+ ): CorrelationResult[] {
1467
+ const results: CorrelationResult[] = [];
1468
+
1469
+ for (let i = 0; i < requests.length; i++) {
1470
+ const request = requests[i];
1471
+ const correlationKey = generateCorrelationKey(request);
1472
+
1473
+ // Try to find in created transactions
1474
+ const created = response.transactions?.find(t =>
1475
+ generateCorrelationKey(toCorrelationPayload(t)) === correlationKey
1476
+ );
1477
+
1478
+ if (created) {
1479
+ results.push({ status: 'created', request_index: i, transaction_id: created.id, correlation_key: correlationKey });
1480
+ } else if (duplicateImportIds.has(request.import_id ?? '')) {
1481
+ results.push({ status: 'duplicate', request_index: i, correlation_key: correlationKey });
1482
+ } else {
1483
+ results.push({ status: 'failed', request_index: i, correlation_key: correlationKey, error: 'Not in response' });
1484
+ }
1485
+ }
1486
+
1487
+ return results;
1488
+ }
1489
+ ```
1490
+
1491
+ ### 6.4 Error Recovery
1492
+
1493
+ **Retry Strategy:**
1494
+
1495
+ ```mermaid
1496
+ graph TD
1497
+ A[Bulk Chunk Failure] --> B[normalizeYnabError]
1498
+ B --> C{FATAL_STATUS_CODES?}
1499
+
1500
+ C -->|400/401/403/404/429/500/503| D[Propagate Error]
1501
+ C -->|Other| E[Log + Continue]
1502
+
1503
+ E --> F[Sequential Fallback]
1504
+ F --> G[Process Individually]
1505
+
1506
+ G --> H{Individual Success?}
1507
+ H -->|Yes| I[Record Success]
1508
+ H -->|No| J{FATAL?}
1509
+
1510
+ J -->|Yes| D
1511
+ J -->|No| K[Record Failure + Continue]
1512
+
1513
+ I --> L[Update Metrics]
1514
+ K --> L
1515
+ ```
1516
+
1517
+ **Fatal vs Recoverable Errors:**
1518
+
1519
+ ```typescript
1520
+ const FATAL_YNAB_STATUS_CODES = new Set([
1521
+ 400, // Bad Request - malformed payload
1522
+ 401, // Unauthorized - invalid token
1523
+ 403, // Forbidden - insufficient permissions
1524
+ 404, // Not Found - budget/account doesn't exist
1525
+ 429, // Too Many Requests - rate limited
1526
+ 500, // Internal Server Error - YNAB backend issue
1527
+ 503, // Service Unavailable - YNAB maintenance
1528
+ ]);
1529
+ ```
1530
+
1531
+ ---
1532
+
1533
+ ## 7. Type System
1534
+
1535
+ ### 7.1 Core Types
1536
+
1537
+ **Canonical Transaction Types:**
1538
+
1539
+ ```typescript
1540
+ // File: src/types/reconciliation.ts
1541
+
1542
+ /**
1543
+ * Bank transaction from CSV parsing.
1544
+ * CRITICAL: amount is in MILLIUNITS (integers, 1000 = $1.00)
1545
+ */
1546
+ export interface BankTransaction {
1547
+ id: string; // UUID
1548
+ date: string; // YYYY-MM-DD
1549
+ amount: number; // Milliunits (integer)
1550
+ payee: string;
1551
+ memo?: string;
1552
+ sourceRow: number; // CSV row number (1-indexed)
1553
+ raw: {
1554
+ date: string;
1555
+ amount: string;
1556
+ description: string;
1557
+ };
1558
+ warnings?: string[];
1559
+ }
1560
+
1561
+ /**
1562
+ * Normalized YNAB transaction for matching.
1563
+ * CRITICAL: amount is in MILLIUNITS (same as YNAB API)
1564
+ */
1565
+ export interface NormalizedYNABTransaction {
1566
+ id: string;
1567
+ date: string; // YYYY-MM-DD
1568
+ amount: number; // Milliunits (integer)
1569
+ payee: string | null;
1570
+ memo: string | null;
1571
+ categoryName: string | null;
1572
+ cleared: 'cleared' | 'uncleared' | 'reconciled';
1573
+ approved: boolean;
1574
+ }
1575
+ ```
1576
+
1577
+ **Match Result Types:**
1578
+
1579
+ ```typescript
1580
+ // File: src/tools/reconciliation/types.ts
1581
+
1582
+ export type MatchConfidence = 'high' | 'medium' | 'low' | 'none';
1583
+
1584
+ export interface TransactionMatch {
1585
+ bankTransaction: BankTransaction;
1586
+ ynabTransaction?: YNABTransaction;
1587
+ candidates?: MatchCandidate[];
1588
+ confidence: MatchConfidence;
1589
+ confidenceScore: number; // 0-100
1590
+ matchReason: string;
1591
+ topConfidence?: number;
1592
+ actionHint?: string;
1593
+ recommendation?: string;
1594
+ }
1595
+
1596
+ export interface MatchCandidate {
1597
+ ynab_transaction: YNABTransaction;
1598
+ confidence: number;
1599
+ match_reason: string;
1600
+ explanation: string;
1601
+ }
1602
+ ```
1603
+
1604
+ **Recommendation Types:**
1605
+
1606
+ ```typescript
1607
+ // File: src/tools/reconciliation/types.ts
1608
+
1609
+ export type ActionableRecommendation =
1610
+ | CreateTransactionRecommendation
1611
+ | UpdateClearedRecommendation
1612
+ | ReviewDuplicateRecommendation
1613
+ | ManualReviewRecommendation;
1614
+
1615
+ export interface CreateTransactionRecommendation {
1616
+ id: string;
1617
+ action_type: 'create_transaction';
1618
+ priority: 'high' | 'medium' | 'low';
1619
+ confidence: number; // 0-1
1620
+ message: string;
1621
+ reason: string;
1622
+ estimated_impact: MoneyValue;
1623
+ account_id: string;
1624
+ parameters: {
1625
+ account_id: string;
1626
+ date: string;
1627
+ amount: number; // Milliunits
1628
+ payee_name: string;
1629
+ memo?: string;
1630
+ cleared: 'cleared' | 'uncleared';
1631
+ approved: boolean;
1632
+ };
1633
+ }
1634
+ ```
1635
+
1636
+ ### 7.2 Analysis Result Type
1637
+
1638
+ ```typescript
1639
+ export interface ReconciliationAnalysis {
1640
+ success: true;
1641
+ phase: 'analysis';
1642
+ summary: ReconciliationSummary;
1643
+ auto_matches: TransactionMatch[];
1644
+ suggested_matches: TransactionMatch[];
1645
+ unmatched_bank: BankTransaction[];
1646
+ unmatched_ynab: YNABTransaction[];
1647
+ balance_info: BalanceInfo;
1648
+ next_steps: string[];
1649
+ insights: ReconciliationInsight[];
1650
+ recommendations?: ActionableRecommendation[];
1651
+ }
1652
+
1653
+ export interface ReconciliationSummary {
1654
+ statement_date_range: string;
1655
+ bank_transactions_count: number;
1656
+ ynab_transactions_count: number;
1657
+ auto_matched: number;
1658
+ suggested_matches: number;
1659
+ unmatched_bank: number;
1660
+ unmatched_ynab: number;
1661
+ current_cleared_balance: MoneyValue;
1662
+ target_statement_balance: MoneyValue;
1663
+ discrepancy: MoneyValue;
1664
+ discrepancy_explanation: string;
1665
+ }
1666
+ ```
1667
+
1668
+ ---
1669
+
1670
+ ## 8. Integration Points
1671
+
1672
+ ### 8.1 YNAB API Integration
1673
+
1674
+ **Delta Fetching:**
1675
+
1676
+ ```typescript
1677
+ // Fetch transactions with delta support for efficiency
1678
+ const transactionsResult = forceFullRefresh
1679
+ ? await deltaFetcher.fetchTransactionsByAccountFull(budget_id, account_id, since_date)
1680
+ : await deltaFetcher.fetchTransactionsByAccount(budget_id, account_id, since_date);
1681
+
1682
+ // Result includes metadata
1683
+ {
1684
+ data: TransactionDetail[],
1685
+ wasCached: boolean,
1686
+ usedDelta: boolean,
1687
+ serverKnowledge: number,
1688
+ }
1689
+ ```
1690
+
1691
+ **Bulk Operations:**
1692
+
1693
+ ```typescript
1694
+ // Create multiple transactions
1695
+ const response = await ynabAPI.transactions.createTransactions(budgetId, {
1696
+ transactions: [
1697
+ { account_id, date, amount, payee_name, cleared: 'cleared', approved: true, import_id },
1698
+ // ... more transactions
1699
+ ],
1700
+ });
1701
+
1702
+ // Update multiple transactions
1703
+ const response = await ynabAPI.transactions.updateTransactions(budgetId, {
1704
+ transactions: [
1705
+ { id: txn_id_1, cleared: 'cleared' },
1706
+ { id: txn_id_2, date: '2025-09-20' },
1707
+ // ... more updates
1708
+ ],
1709
+ });
1710
+ ```
1711
+
1712
+ ### 8.2 MCP Tool Interface
1713
+
1714
+ **Request Schema:**
1715
+
1716
+ ```typescript
1717
+ export const ReconcileAccountSchema = z.object({
1718
+ budget_id: z.string().min(1),
1719
+ account_id: z.string().min(1),
1720
+
1721
+ // CSV Input (one required)
1722
+ csv_file_path: z.string().optional(),
1723
+ csv_data: z.string().optional(),
1724
+
1725
+ csv_format: z.object({
1726
+ date_column: z.union([z.string(), z.number()]).optional(),
1727
+ amount_column: z.union([z.string(), z.number()]).optional(),
1728
+ debit_column: z.union([z.string(), z.number()]).optional(),
1729
+ credit_column: z.union([z.string(), z.number()]).optional(),
1730
+ description_column: z.union([z.string(), z.number()]).optional(),
1731
+ date_format: z.string().optional(),
1732
+ has_header: z.boolean().optional(),
1733
+ delimiter: z.string().optional(),
1734
+ }).optional(),
1735
+
1736
+ // Statement Information
1737
+ statement_balance: z.number(),
1738
+ statement_start_date: z.string().optional(),
1739
+ statement_end_date: z.string().optional(),
1740
+ statement_date: z.string().optional(),
1741
+
1742
+ // Matching Configuration
1743
+ date_tolerance_days: z.number().min(0).max(7).default(7),
1744
+ amount_tolerance_cents: z.number().min(0).max(100).default(1),
1745
+ auto_match_threshold: z.number().min(0).max(100).default(85),
1746
+ suggestion_threshold: z.number().min(0).max(100).default(60),
1747
+
1748
+ // Automation Toggles
1749
+ auto_create_transactions: z.boolean().default(false),
1750
+ auto_update_cleared_status: z.boolean().default(false),
1751
+ auto_unclear_missing: z.boolean().default(true),
1752
+ auto_adjust_dates: z.boolean().default(false),
1753
+ invert_bank_amounts: z.boolean().optional(),
1754
+ dry_run: z.boolean().default(true),
1755
+
1756
+ // Response Options
1757
+ include_structured_data: z.boolean().default(false),
1758
+ force_full_refresh: z.boolean().default(true),
1759
+ });
1760
+ ```
1761
+
1762
+ **Response Format:**
1763
+
1764
+ ```typescript
1765
+ {
1766
+ content: [
1767
+ {
1768
+ type: 'text',
1769
+ text: responseFormatter.format({
1770
+ human: formatHumanReadableReport(analysis, execution),
1771
+ structured: { // Only if include_structured_data = true
1772
+ version: '2.0',
1773
+ analysis: ReconciliationAnalysis,
1774
+ execution: ExecutionResult,
1775
+ audit_metadata: { ... },
1776
+ },
1777
+ }),
1778
+ },
1779
+ ],
1780
+ }
1781
+ ```
1782
+
1783
+ ---
1784
+
1785
+ ## 9. Performance Characteristics
1786
+
1787
+ ### 9.1 Complexity Analysis
1788
+
1789
+ | Operation | Complexity | Notes |
1790
+ |-----------|------------|-------|
1791
+ | CSV Parsing | O(N) | N = rows; PapaParse streams large files |
1792
+ | YNAB Normalization | O(M) | M = YNAB transactions |
1793
+ | Basic Matching | O(N × M) | N = bank txns, M = YNAB txns |
1794
+ | Payee Fuzzy Matching | O(L²) per pair | L = string length; fuzzball optimized |
1795
+ | Bulk Create | O(N/100) API calls | Chunked by MAX_BULK_CREATE_CHUNK |
1796
+ | Bulk Update | O(M/100) API calls | Chunked by MAX_BULK_UPDATE_CHUNK |
1797
+
1798
+ ### 9.2 Scale Limits
1799
+
1800
+ | CSV Size | Expected Performance | Recommendations |
1801
+ |----------|---------------------|------------------|
1802
+ | <100 txns | <500ms | Default settings |
1803
+ | 100-500 txns | <2s | Default settings |
1804
+ | 500-1000 txns | 2-10s | Consider chunking by month |
1805
+ | >1000 txns | May timeout | Process in batches |
1806
+
1807
+ ### 9.3 Optimization Strategies
1808
+
1809
+ **Current Optimizations:**
1810
+
1811
+ 1. **Integer Arithmetic** - No floating-point comparisons
1812
+ 2. **Early Termination** - Stop when balance aligns
1813
+ 3. **Bulk Operations** - Minimize API calls
1814
+ 4. **Delta Fetching** - Only fetch changed transactions
1815
+
1816
+ **Future Optimizations:**
1817
+
1818
+ 1. **Amount Bucketing** - Index YNAB transactions by amount for O(1) lookup
1819
+ 2. **Date Windowing** - Only compare transactions within ±14 days
1820
+ 3. **Streaming CSV** - Parse in chunks for large files
1821
+ 4. **Parallel Matching** - Use worker threads for CPU-bound fuzzy matching
1822
+
1823
+ ### 9.4 Memory Usage
1824
+
1825
+ **Estimated Memory Footprint:**
1826
+
1827
+ ```
1828
+ CSV (500 txns): ~100 KB raw text
1829
+ BankTransaction[]: ~200 KB (400 bytes/txn × 500)
1830
+ YNAB API (1000): ~500 KB raw JSON
1831
+ NormalizedYNAB[]: ~400 KB (400 bytes/txn × 1000)
1832
+ MatchResults[]: ~300 KB (600 bytes/match × 500)
1833
+ Total: ~1.5 MB for 500-txn reconciliation
1834
+ ```
1835
+
1836
+ **Memory Safety:**
1837
+
1838
+ ```typescript
1839
+ // Security limits in csvParser.ts
1840
+ const MAX_BYTES = options.maxBytes ?? 10 * 1024 * 1024; // 10MB
1841
+ const maxRows = options.maxRows ?? 10000;
1842
+
1843
+ // PapaParse preview limits rows in memory
1844
+ const parsed = Papa.parse(content, {
1845
+ preview: maxRows + (hasHeader ? 1 : 0),
1846
+ skipEmptyLines: true,
1847
+ });
1848
+ ```
1849
+
1850
+ ---
1851
+
1852
+ ## 10. Testing Strategy
1853
+
1854
+ ### 10.1 Test Coverage
1855
+
1856
+ **Unit Tests:**
1857
+
1858
+ | Component | Test File | Coverage Focus |
1859
+ |-----------|-----------|----------------|
1860
+ | csvParser | csvParser.test.ts | Format detection, amount parsing, date parsing |
1861
+ | matcher | matcher.test.ts | Scoring algorithm, candidate filtering, tie-breaking |
1862
+ | analyzer | analyzer.test.ts | Orchestration, insight generation, balance calculation |
1863
+ | executor | executor.test.ts | Bulk operations, correlation, error handling |
1864
+ | recommendationEngine | recommendationEngine.test.ts | Recommendation generation, prioritization |
1865
+
1866
+ **Integration Tests:**
1867
+
1868
+ | Scenario | Test File | Purpose |
1869
+ |----------|-----------|---------|
1870
+ | TD Bank CSV | csvParser.integration.test.ts | Real-world TD format |
1871
+ | RBC Debit/Credit | csvParser.integration.test.ts | Debit/credit column handling |
1872
+ | Bulk Create | executor.integration.test.ts | Bulk API with mocked YNAB |
1873
+ | End-to-End | reconciliation.e2e.test.ts | Full flow with real YNAB API |
1874
+
1875
+ ### 10.2 Test Data
1876
+
1877
+ **Fixtures:**
1878
+
1879
+ ```
1880
+ test-exports/csv/
1881
+ ├── td-credit-card.csv # TD headerless format
1882
+ ├── rbc-checking.csv # RBC debit/credit columns
1883
+ ├── wealthsimple-cash.csv # Wealthsimple headered format
1884
+ ├── scotiabank-savings.csv # Scotiabank format
1885
+ ├── tangerine-checking.csv # Tangerine format
1886
+ └── edge-cases/
1887
+ ├── european-numbers.csv # 1.234,56 format
1888
+ ├── both-debit-credit.csv # Ambiguous columns
1889
+ └── malformed.csv # Missing fields, bad dates
1890
+ ```
1891
+
1892
+ ### 10.3 Accuracy Metrics
1893
+
1894
+ **Evaluation Dataset:**
1895
+
1896
+ ```typescript
1897
+ // test-exports/csv/labeled/
1898
+ {
1899
+ "csv_file": "td-credit-card.csv",
1900
+ "ground_truth": [
1901
+ {
1902
+ "bank_row": 2,
1903
+ "ynab_transaction_id": "abc123",
1904
+ "expected_confidence": "high",
1905
+ "notes": "Exact amount and payee match"
1906
+ },
1907
+ {
1908
+ "bank_row": 3,
1909
+ "ynab_transaction_id": null,
1910
+ "expected_confidence": "none",
1911
+ "notes": "New transaction, should create"
1912
+ }
1913
+ ]
1914
+ }
1915
+ ```
1916
+
1917
+ **Benchmark Metrics:**
1918
+
1919
+ ```bash
1920
+ npm run benchmark:reconciliation -- --dataset=test-exports/csv/labeled/
1921
+
1922
+ Output:
1923
+ Auto-Match Precision: 95.2% (20/21 correct)
1924
+ Auto-Match Recall: 90.9% (20/22 true matches)
1925
+ Overall Match Rate: 96.0% (48/50 matched at any level)
1926
+ False Positive Rate: 4.8% (1/21 incorrect)
1927
+ P95 Latency: 1.2s (500-txn CSV)
1928
+ ```
1929
+
1930
+ ---
1931
+
1932
+ ## Appendix A: Sequence Diagrams
1933
+
1934
+ ### A.1 Create Transaction Flow
1935
+
1936
+ ```mermaid
1937
+ sequenceDiagram
1938
+ participant E as executor.ts
1939
+ participant B as BankTransaction[]
1940
+ participant C as Correlation
1941
+ participant Y as YNAB API
1942
+
1943
+ E->>B: Filter unmatched_bank
1944
+ B->>E: orderedUnmatchedBank (sorted by date desc)
1945
+
1946
+ loop For each batch until balance aligns
1947
+ E->>E: Build batch (greedy until projected balance aligns)
1948
+ E->>E: Chunk batch by MAX_BULK_CREATE_CHUNK (100)
1949
+
1950
+ loop For each chunk
1951
+ E->>C: Generate import_id for each txn
1952
+ C-->>E: import_id = YNAB:bulk:{hash}
1953
+
1954
+ E->>C: Generate correlation_key
1955
+ C-->>E: correlation_key = {hash}
1956
+
1957
+ E->>Y: POST /budgets/{id}/transactions (bulk)
1958
+
1959
+ alt Bulk Success
1960
+ Y-->>E: BulkResponse { transactions[], duplicate_import_ids[] }
1961
+ E->>C: correlateResults(requests, response, duplicates)
1962
+
1963
+ loop For each correlated result
1964
+ alt Status: created
1965
+ C-->>E: { status: 'created', transaction_id, correlation_key }
1966
+ E->>E: Record success action
1967
+ E->>E: Update clearedDeltaMilli
1968
+ else Status: duplicate
1969
+ C-->>E: { status: 'duplicate', correlation_key }
1970
+ E->>E: Record duplicate warning
1971
+ E->>E: Increment duplicates_detected
1972
+ else Status: failed
1973
+ C-->>E: { status: 'failed', correlation_key, error }
1974
+ E->>E: Record failure action
1975
+ E->>E: Increment transaction_failures
1976
+ end
1977
+ end
1978
+
1979
+ E->>E: Check if balance aligned
1980
+ else Bulk Failure
1981
+ Y-->>E: Error response
1982
+ E->>E: Increment bulk_chunk_failures
1983
+ E->>E: Log fallback action
1984
+
1985
+ loop For each txn in chunk (Sequential Fallback)
1986
+ E->>Y: POST /budgets/{id}/transactions (single)
1987
+
1988
+ alt Success
1989
+ Y-->>E: { transaction: {...} }
1990
+ E->>E: Record success action
1991
+ E->>E: Update clearedDeltaMilli
1992
+ else Failure
1993
+ Y-->>E: Error
1994
+ E->>E: normalizeYnabError
1995
+
1996
+ alt Fatal Error (400/401/403/404/429/500/503)
1997
+ E->>E: Throw error (halt execution)
1998
+ else Recoverable Error
1999
+ E->>E: Record failure action
2000
+ E->>E: Increment transaction_failures
2001
+ E->>E: Continue to next txn
2002
+ end
2003
+ end
2004
+
2005
+ E->>E: Check if balance aligned
2006
+ end
2007
+ end
2008
+ end
2009
+ end
2010
+ ```
2011
+
2012
+ ### A.2 Match Scoring Flow
2013
+
2014
+ ```mermaid
2015
+ sequenceDiagram
2016
+ participant M as matcher.ts
2017
+ participant B as BankTransaction
2018
+ participant Y as NormalizedYNABTransaction[]
2019
+ participant F as fuzzball
2020
+
2021
+ M->>Y: Loop through YNAB transactions
2022
+
2023
+ loop For each YNAB transaction
2024
+ M->>M: Check if already used
2025
+
2026
+ alt Already matched
2027
+ M->>M: Skip candidate
2028
+ else Not matched
2029
+ M->>M: Sign check (bankSign === ynabSign)
2030
+
2031
+ alt Signs differ
2032
+ M->>M: Skip candidate
2033
+ else Signs match
2034
+ M->>M: Calculate amountDiff = |bank - ynab|
2035
+
2036
+ alt amountDiff > tolerance
2037
+ M->>M: Skip candidate
2038
+ else Within tolerance
2039
+ M->>M: Calculate amount score (0-100)
2040
+
2041
+ Note over M: Amount Scoring:<br/>diff === 0 → 100<br/>diff <= tolerance → 95<br/>diff <= $1 → 80-100<br/>else → 60-0
2042
+
2043
+ M->>M: Calculate date score (0-100)
2044
+
2045
+ Note over M: Date Scoring:<br/>same day → 100<br/>1 day → 95<br/>≤ tolerance → 90-50<br/>else → 50-0
2046
+
2047
+ M->>F: calculatePayeeScore(bankPayee, ynabPayee)
2048
+
2049
+ F->>F: token_set_ratio
2050
+ F->>F: token_sort_ratio
2051
+ F->>F: partial_ratio
2052
+ F->>F: WRatio
2053
+
2054
+ F-->>M: max(scores)
2055
+
2056
+ M->>M: combined = amount×0.5 + date×0.15 + payee×0.35
2057
+
2058
+ M->>M: Apply bonuses
2059
+
2060
+ Note over M: Bonuses:<br/>amount === 100 → +10<br/>date === 100 → +5<br/>payee >= 95 → +10
2061
+
2062
+ M->>M: combined = min(100, combined + bonuses)
2063
+
2064
+ alt combined >= minimumCandidateScore (40)
2065
+ M->>M: Add to candidates[]
2066
+ else Below threshold
2067
+ M->>M: Skip candidate
2068
+ end
2069
+ end
2070
+ end
2071
+ end
2072
+ end
2073
+
2074
+ M->>M: Sort candidates by combined score (desc)
2075
+ M->>M: Tie-break: prefer uncleared, then closer date
2076
+
2077
+ alt Top candidate >= autoMatchThreshold (85)
2078
+ M-->>M: confidence = 'high'
2079
+ M->>M: Mark YNAB transaction as used
2080
+ else Top >= suggestedMatchThreshold (60)
2081
+ M-->>M: confidence = 'medium'
2082
+ else Top >= minimumCandidateScore (40)
2083
+ M-->>M: confidence = 'low'
2084
+ else No candidates
2085
+ M-->>M: confidence = 'none'
2086
+ end
2087
+
2088
+ M-->>M: Return MatchResult { bankTransaction, bestMatch, candidates, confidence, confidenceScore }
2089
+ ```
2090
+
2091
+ ---
2092
+
2093
+ ## Appendix B: Configuration Examples
2094
+
2095
+ ### B.1 Conservative Matching
2096
+
2097
+ ```json
2098
+ {
2099
+ "date_tolerance_days": 3,
2100
+ "amount_tolerance_cents": 0,
2101
+ "auto_match_threshold": 95,
2102
+ "suggestion_threshold": 75,
2103
+ "auto_create_transactions": false,
2104
+ "auto_update_cleared_status": false,
2105
+ "dry_run": true
2106
+ }
2107
+ ```
2108
+
2109
+ ### B.2 Aggressive Matching
2110
+
2111
+ ```json
2112
+ {
2113
+ "date_tolerance_days": 14,
2114
+ "amount_tolerance_cents": 5,
2115
+ "auto_match_threshold": 75,
2116
+ "suggestion_threshold": 50,
2117
+ "auto_create_transactions": true,
2118
+ "auto_update_cleared_status": true,
2119
+ "auto_unclear_missing": true,
2120
+ "dry_run": false
2121
+ }
2122
+ ```
2123
+
2124
+ ### B.3 Manual Review Only
2125
+
2126
+ ```json
2127
+ {
2128
+ "auto_create_transactions": false,
2129
+ "auto_update_cleared_status": false,
2130
+ "auto_unclear_missing": false,
2131
+ "dry_run": true,
2132
+ "include_structured_data": true
2133
+ }
2134
+ ```
2135
+
2136
+ ---
2137
+
2138
+ ## Appendix C: Troubleshooting
2139
+
2140
+ ### C.1 Common Issues
2141
+
2142
+ | Issue | Likely Cause | Solution |
2143
+ |-------|-------------|----------|
2144
+ | Low match rate (<50%) | CSV format not detected | Specify `csv_format.preset` explicitly |
2145
+ | All matches "none" | Sign inversion needed | Set `invert_bank_amounts: true` |
2146
+ | Date parsing errors | Ambiguous date format | Specify `csv_format.date_format` |
2147
+ | Amount parsing errors | European number format | Parser auto-detects, check warnings |
2148
+ | Balance never aligns | Missing transactions | Check `unmatched_bank` and `unmatched_ynab` |
2149
+ | Bulk create failures | Import ID collisions | Check `bulk_operation_details.duplicates_detected` |
2150
+
2151
+ ### C.2 Diagnostic Output
2152
+
2153
+ ```typescript
2154
+ {
2155
+ include_diagnostics: true
2156
+ }
2157
+ ```
2158
+
2159
+ Returns:
2160
+
2161
+ ```json
2162
+ {
2163
+ "diagnostics": {
2164
+ "csvParsing": {
2165
+ "detectedDelimiter": ",",
2166
+ "detectedColumns": ["Date", "Description", "Amount"],
2167
+ "totalRows": 50,
2168
+ "validRows": 48,
2169
+ "errors": [
2170
+ { "row": 5, "field": "date", "message": "Could not parse date: '99/99/9999'", "rawValue": "99/99/9999" }
2171
+ ],
2172
+ "warnings": [
2173
+ { "row": 12, "message": "Both Debit ($50.00) and Credit ($25.00) have values - using Debit" }
2174
+ ]
2175
+ },
2176
+ "matchingDetails": [
2177
+ {
2178
+ "bankTxn": { "date": "2025-09-15", "amount": -45230, "payee": "Shell Gas" },
2179
+ "bestMatch": {
2180
+ "ynabTxn": { "date": "2025-09-15", "amount": -45230, "payee": "Shell" },
2181
+ "scores": { "amount": 100, "date": 100, "payee": 85, "combined": 100 }
2182
+ },
2183
+ "confidence": "high"
2184
+ }
2185
+ ]
2186
+ }
2187
+ }
2188
+ ```
2189
+
2190
+ ---
2191
+
2192
+ ## Appendix D: Future Enhancements
2193
+
2194
+ ### D.1 Roadmap
2195
+
2196
+ | Priority | Feature | Description | Complexity |
2197
+ |----------|---------|-------------|------------|
2198
+ | P0 | Split Transaction Detection | Match 1 bank txn to multiple YNAB txns | High |
2199
+ | P1 | Merchant Learning | Cache successful payee mappings per user | Medium |
2200
+ | P1 | Adaptive Thresholds | Learn from user confirmations/rejections | Medium |
2201
+ | P2 | Vector Embeddings | Semantic merchant matching with embeddings | High |
2202
+ | P2 | Recurring Pattern Detection | Use historical patterns to boost confidence | Medium |
2203
+ | P3 | Multi-Currency Support | Handle FX transactions and conversions | High |
2204
+
2205
+ ### D.2 Split Transaction Detection
2206
+
2207
+ **Design Sketch:**
2208
+
2209
+ ```typescript
2210
+ // Detect when one bank transaction = multiple YNAB transactions
2211
+ function findCombinationMatches(
2212
+ bankTxn: BankTransaction,
2213
+ ynabTransactions: NormalizedYNABTransaction[],
2214
+ maxCombinationSize: number = 3,
2215
+ ): CombinationMatch[] {
2216
+ // Generate all combinations of size 2 to maxCombinationSize
2217
+ const combinations = generateCombinations(ynabTransactions, maxCombinationSize);
2218
+
2219
+ for (const combo of combinations) {
2220
+ const totalAmount = combo.reduce((sum, txn) => sum + txn.amount, 0);
2221
+
2222
+ if (Math.abs(totalAmount - bankTxn.amount) <= AMOUNT_TOLERANCE) {
2223
+ // Found matching combination
2224
+ return {
2225
+ bankTransaction: bankTxn,
2226
+ ynabTransactions: combo,
2227
+ confidence: calculateCombinationConfidence(combo, bankTxn),
2228
+ };
2229
+ }
2230
+ }
2231
+
2232
+ return [];
2233
+ }
2234
+ ```
2235
+
2236
+ ---
2237
+
2238
+ **End of Document**
2239
+
2240
+ ---
2241
+
2242
+ ## Document Metadata
2243
+
2244
+ - **File Path:** `C:\Users\ksutk\projects\ynab-mcpb\docs\technical\reconciliation-system-architecture.md`
2245
+ - **Related Files:**
2246
+ - Implementation: `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\`
2247
+ - Existing Docs: `C:\Users\ksutk\projects\ynab-mcpb\docs\reconciliation-flow.md`
2248
+ - Design Doc: `C:\Users\ksutk\projects\ynab-mcpb\docs\plans\reconciliation-v2-redesign.md`
2249
+ - **Version:** 2.0
2250
+ - **Status:** Active Implementation
2251
+ - **Last Verified:** 2025-11-30