@dizzlkheinz/ynab-mcpb 0.12.2 → 0.15.0

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 (262) hide show
  1. package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +3 -0
  2. package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +3 -0
  3. package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +1 -0
  4. package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +5 -0
  5. package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +1569 -0
  6. package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +3 -0
  7. package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +2832 -0
  8. package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +2709 -0
  9. package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +2832 -0
  10. package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +2832 -0
  11. package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +2709 -0
  12. package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +1 -0
  13. package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +5217 -0
  14. package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +2594 -0
  15. package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +2594 -0
  16. package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +231 -0
  17. package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +2590 -0
  18. package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +5195 -0
  19. package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +286 -0
  20. package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +218 -0
  21. package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +180 -0
  22. package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +3 -0
  23. package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +1 -0
  24. package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +747 -0
  25. package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +1 -0
  26. package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +2594 -0
  27. package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +2594 -0
  28. package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +144 -0
  29. package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +416 -0
  30. package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +2590 -0
  31. package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +2590 -0
  32. package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +2590 -0
  33. package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +790 -0
  34. package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +766 -0
  35. package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +790 -0
  36. package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +2594 -0
  37. package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +1000 -0
  38. package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +3489 -0
  39. package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +766 -0
  40. package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +2594 -0
  41. package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +2456 -0
  42. package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +2594 -0
  43. package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +18 -0
  44. package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +48 -0
  45. package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +1 -0
  46. package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +1 -0
  47. package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +1 -0
  48. package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +1 -0
  49. package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +1271 -0
  50. package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +1570 -0
  51. package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +2590 -0
  52. package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +3 -0
  53. package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +3299 -0
  54. package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +3299 -0
  55. package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +1882 -0
  56. package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +2594 -0
  57. package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +1 -0
  58. package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +170 -0
  59. package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +1 -0
  60. package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +3 -0
  61. package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +1 -0
  62. package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +1 -0
  63. package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +3 -0
  64. package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +1 -0
  65. package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +1 -0
  66. package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +5 -0
  67. package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +1 -0
  68. package/.github/workflows/ci-tests.yml +6 -2
  69. package/.github/workflows/publish.yml +3 -3
  70. package/.github/workflows/release.yml +4 -0
  71. package/CHANGELOG.md +89 -1
  72. package/NUL +1 -1
  73. package/README.md +36 -10
  74. package/dist/bundle/index.cjs +65 -42
  75. package/dist/index.js +9 -20
  76. package/dist/server/YNABMCPServer.d.ts +2 -1
  77. package/dist/server/YNABMCPServer.js +61 -27
  78. package/dist/server/cacheKeys.d.ts +8 -0
  79. package/dist/server/cacheKeys.js +8 -0
  80. package/dist/server/config.d.ts +22 -3
  81. package/dist/server/config.js +16 -17
  82. package/dist/server/errorHandler.d.ts +2 -0
  83. package/dist/server/errorHandler.js +49 -5
  84. package/dist/server/securityMiddleware.js +3 -6
  85. package/dist/server/toolRegistry.js +8 -10
  86. package/dist/tools/accountTools.js +4 -3
  87. package/dist/tools/categoryTools.js +8 -7
  88. package/dist/tools/monthTools.js +2 -1
  89. package/dist/tools/payeeTools.js +2 -1
  90. package/dist/tools/reconcileAdapter.js +10 -5
  91. package/dist/tools/reconciliation/analyzer.d.ts +4 -2
  92. package/dist/tools/reconciliation/analyzer.js +120 -404
  93. package/dist/tools/reconciliation/csvParser.d.ts +51 -0
  94. package/dist/tools/reconciliation/csvParser.js +413 -0
  95. package/dist/tools/reconciliation/executor.d.ts +8 -0
  96. package/dist/tools/reconciliation/executor.js +277 -50
  97. package/dist/tools/reconciliation/index.d.ts +7 -7
  98. package/dist/tools/reconciliation/index.js +115 -39
  99. package/dist/tools/reconciliation/matcher.d.ts +24 -3
  100. package/dist/tools/reconciliation/matcher.js +175 -133
  101. package/dist/tools/reconciliation/recommendationEngine.js +22 -18
  102. package/dist/tools/reconciliation/reportFormatter.js +9 -8
  103. package/dist/tools/reconciliation/signDetector.d.ts +2 -0
  104. package/dist/tools/reconciliation/signDetector.js +54 -0
  105. package/dist/tools/reconciliation/types.d.ts +20 -34
  106. package/dist/tools/reconciliation/types.js +1 -7
  107. package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
  108. package/dist/tools/reconciliation/ynabAdapter.js +15 -0
  109. package/dist/tools/transactionTools.d.ts +3 -17
  110. package/dist/tools/transactionTools.js +5 -17
  111. package/dist/types/reconciliation.d.ts +24 -0
  112. package/dist/types/reconciliation.js +1 -0
  113. package/dist/utils/baseError.d.ts +3 -0
  114. package/dist/utils/baseError.js +7 -0
  115. package/dist/utils/errors.d.ts +13 -0
  116. package/dist/utils/errors.js +15 -0
  117. package/dist/utils/validationError.d.ts +3 -0
  118. package/dist/utils/validationError.js +3 -0
  119. package/docs/guides/ARCHITECTURE.md +12 -129
  120. package/docs/plans/2025-11-20-reloadable-config-token-validation.md +93 -0
  121. package/docs/plans/2025-11-21-fix-transaction-cached-property.md +362 -0
  122. package/docs/plans/2025-11-21-reconciliation-error-handling.md +90 -0
  123. package/docs/plans/2025-11-21-v014-hardening.md +153 -0
  124. package/docs/plans/reconciliation-v2-redesign.md +1571 -0
  125. package/package.json +8 -2
  126. package/scripts/run-throttled-integration-tests.js +9 -3
  127. package/scripts/test-recommendations.ts +1 -1
  128. package/src/__tests__/performance.test.ts +12 -5
  129. package/src/__tests__/testUtils.ts +62 -5
  130. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
  131. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
  132. package/src/__tests__/workflows.e2e.test.ts +33 -0
  133. package/src/index.ts +8 -31
  134. package/src/server/YNABMCPServer.ts +81 -42
  135. package/src/server/__tests__/YNABMCPServer.integration.test.ts +10 -12
  136. package/src/server/__tests__/YNABMCPServer.test.ts +27 -15
  137. package/src/server/__tests__/config.test.ts +76 -152
  138. package/src/server/__tests__/server-startup.integration.test.ts +42 -14
  139. package/src/server/__tests__/toolRegistry.test.ts +1 -1
  140. package/src/server/cacheKeys.ts +8 -0
  141. package/src/server/config.ts +20 -38
  142. package/src/server/errorHandler.ts +52 -5
  143. package/src/server/securityMiddleware.ts +3 -7
  144. package/src/server/toolRegistry.ts +14 -10
  145. package/src/tools/__tests__/categoryTools.test.ts +37 -19
  146. package/src/tools/__tests__/transactionTools.test.ts +58 -2
  147. package/src/tools/accountTools.ts +8 -3
  148. package/src/tools/categoryTools.ts +12 -7
  149. package/src/tools/monthTools.ts +7 -1
  150. package/src/tools/payeeTools.ts +7 -1
  151. package/src/tools/reconcileAdapter.ts +10 -5
  152. package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
  153. package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
  154. package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
  155. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +26 -6
  156. package/src/tools/reconciliation/__tests__/executor.test.ts +133 -60
  157. package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
  158. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
  159. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
  160. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
  161. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
  162. package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
  163. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
  164. package/src/tools/reconciliation/analyzer.ts +174 -545
  165. package/src/tools/reconciliation/csvParser.ts +617 -0
  166. package/src/tools/reconciliation/executor.ts +344 -58
  167. package/src/tools/reconciliation/index.ts +141 -48
  168. package/src/tools/reconciliation/matcher.ts +234 -214
  169. package/src/tools/reconciliation/recommendationEngine.ts +23 -19
  170. package/src/tools/reconciliation/reportFormatter.ts +16 -11
  171. package/src/tools/reconciliation/signDetector.ts +117 -0
  172. package/src/tools/reconciliation/types.ts +39 -61
  173. package/src/tools/reconciliation/ynabAdapter.ts +33 -0
  174. package/src/tools/schemas/outputs/utilityOutputs.ts +1 -1
  175. package/src/tools/transactionTools.ts +7 -18
  176. package/src/types/reconciliation.ts +49 -0
  177. package/src/utils/baseError.ts +7 -0
  178. package/src/utils/errors.ts +21 -0
  179. package/src/utils/validationError.ts +3 -0
  180. package/temp-recon.ts +126 -0
  181. package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
  182. package/test_mcp_tools.mjs +75 -0
  183. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
  184. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
  185. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
  186. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
  187. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
  188. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
  189. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
  190. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
  191. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
  192. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
  193. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
  194. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
  195. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
  196. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
  197. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
  198. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
  199. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
  200. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
  201. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
  202. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
  203. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
  204. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
  205. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
  206. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
  207. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
  208. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
  209. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
  210. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
  211. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
  212. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
  213. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
  214. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
  215. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
  216. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
  217. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
  218. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
  219. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
  220. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
  221. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
  222. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
  223. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
  224. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
  225. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
  226. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
  227. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
  228. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
  229. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
  230. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
  231. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
  232. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
  233. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
  234. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
  235. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
  236. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
  237. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
  238. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
  239. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
  240. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
  241. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
  242. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
  243. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
  244. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
  245. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
  246. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
  247. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
  248. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
  249. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
  250. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
  251. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
  252. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
  253. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
  254. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
  255. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
  256. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
  257. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
  258. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
  259. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
  260. package/.gemini/settings.json +0 -8
  261. package/ADOS-2-Module-1-Complete-Manual.md +0 -757
  262. package/WARP.md +0 -245
@@ -1,296 +1,39 @@
1
- import { randomUUID } from 'crypto';
2
- import * as bankParser from '../compareTransactions/parser.js';
3
- import { findMatches } from './matcher.js';
4
- import { DEFAULT_MATCHING_CONFIG } from './types.js';
5
- import { toMoneyValueFromDecimal } from '../../utils/money.js';
1
+ import { parseCSV } from './csvParser.js';
2
+ import { findMatches, normalizeConfig, DEFAULT_CONFIG } from './matcher.js';
3
+ import { normalizeYNABTransactions } from './ynabAdapter.js';
4
+ import { toMoneyValue } from '../../utils/money.js';
6
5
  import { generateRecommendations } from './recommendationEngine.js';
7
- function convertYNABTransaction(apiTxn) {
8
- return {
9
- id: apiTxn.id,
10
- date: apiTxn.date,
11
- amount: apiTxn.amount,
12
- payee_name: apiTxn.payee_name || null,
13
- category_name: apiTxn.category_name || null,
14
- cleared: apiTxn.cleared,
15
- approved: apiTxn.approved,
16
- memo: apiTxn.memo || null,
6
+ function mapToTransactionMatch(result) {
7
+ const candidates = result.candidates.map((c) => ({
8
+ ynab_transaction: c.ynabTransaction,
9
+ confidence: c.scores.combined,
10
+ match_reason: c.matchReasons.join(', '),
11
+ explanation: c.matchReasons.join(', '),
12
+ }));
13
+ const match = {
14
+ bankTransaction: result.bankTransaction,
15
+ candidates,
16
+ confidence: result.confidence,
17
+ confidenceScore: result.confidenceScore,
18
+ matchReason: result.bestMatch?.matchReasons.join(', ') ?? 'No match found',
19
+ actionHint: result.confidence === 'high' ? 'approve' : 'review',
17
20
  };
18
- }
19
- const FALLBACK_CSV_FORMAT = {
20
- date_column: 'Date',
21
- amount_column: 'Amount',
22
- description_column: 'Description',
23
- date_format: 'MM/DD/YYYY',
24
- has_header: true,
25
- delimiter: ',',
26
- };
27
- const ENABLE_COMBINATION_MATCHING = true;
28
- const DAYS_IN_MS = 24 * 60 * 60 * 1000;
29
- function toDollars(milliunits) {
30
- return milliunits / 1000;
31
- }
32
- function amountTolerance(config) {
33
- const toleranceCents = config.amountToleranceCents ?? DEFAULT_MATCHING_CONFIG.amountToleranceCents ?? 1;
34
- return Math.max(0, toleranceCents) / 100;
35
- }
36
- function dateTolerance(config) {
37
- return config.dateToleranceDays ?? DEFAULT_MATCHING_CONFIG.dateToleranceDays ?? 2;
38
- }
39
- function daysBetween(dateA, dateB) {
40
- const a = new Date(`${dateA}T00:00:00Z`).getTime();
41
- const b = new Date(`${dateB}T00:00:00Z`).getTime();
42
- if (Number.isNaN(a) || Number.isNaN(b))
43
- return Number.POSITIVE_INFINITY;
44
- return Math.abs(a - b) / DAYS_IN_MS;
45
- }
46
- function withinDateTolerance(bankDate, ynabTxns, toleranceDays) {
47
- return ynabTxns.every((txn) => daysBetween(bankDate, txn.date) <= toleranceDays);
48
- }
49
- function hasMatchingSign(bankAmount, ynabTxns) {
50
- const bankSign = Math.sign(bankAmount);
51
- const sumSign = Math.sign(ynabTxns.reduce((sum, txn) => sum + toDollars(txn.amount), 0));
52
- return bankSign === sumSign || Math.abs(bankAmount) === 0;
53
- }
54
- function computeCombinationConfidence(diff, tolerance, legCount) {
55
- const safeTolerance = tolerance > 0 ? tolerance : 0.01;
56
- const ratio = diff / safeTolerance;
57
- let base = legCount === 2 ? 75 : 70;
58
- if (ratio <= 0.25) {
59
- base += 5;
60
- }
61
- else if (ratio <= 0.5) {
62
- base += 3;
63
- }
64
- else if (ratio >= 0.9) {
65
- base -= 5;
66
- }
67
- return Math.max(65, Math.min(80, Math.round(base)));
68
- }
69
- function formatDifference(diff) {
70
- return formatCurrency(diff);
71
- }
72
- function findCombinationMatches(unmatchedBank, unmatchedYNAB, config) {
73
- if (!ENABLE_COMBINATION_MATCHING || unmatchedBank.length === 0 || unmatchedYNAB.length === 0) {
74
- return { matches: [], insights: [] };
75
- }
76
- const tolerance = amountTolerance(config);
77
- const toleranceDays = dateTolerance(config);
78
- const matches = [];
79
- const insights = [];
80
- const seenCombinations = new Set();
81
- for (const bankTxn of unmatchedBank) {
82
- const viableYnab = unmatchedYNAB.filter((txn) => hasMatchingSign(bankTxn.amount, [txn]));
83
- if (viableYnab.length < 2)
84
- continue;
85
- const evaluated = [];
86
- const addIfValid = (combo) => {
87
- const sum = combo.reduce((acc, txn) => acc + toDollars(txn.amount), 0);
88
- const diff = Math.abs(sum - bankTxn.amount);
89
- if (diff > tolerance)
90
- return;
91
- if (!withinDateTolerance(bankTxn.date, combo, toleranceDays))
92
- return;
93
- if (!hasMatchingSign(bankTxn.amount, combo))
94
- return;
95
- evaluated.push({ txns: combo, diff, sum });
96
- };
97
- const n = viableYnab.length;
98
- for (let i = 0; i < n - 1; i++) {
99
- for (let j = i + 1; j < n; j++) {
100
- addIfValid([viableYnab[i], viableYnab[j]]);
101
- }
102
- }
103
- if (n >= 3) {
104
- for (let i = 0; i < n - 2; i++) {
105
- for (let j = i + 1; j < n - 1; j++) {
106
- for (let k = j + 1; k < n; k++) {
107
- addIfValid([viableYnab[i], viableYnab[j], viableYnab[k]]);
108
- }
109
- }
110
- }
111
- }
112
- if (evaluated.length === 0)
113
- continue;
114
- evaluated.sort((a, b) => a.diff - b.diff);
115
- const recordedSizes = new Set();
116
- for (const combo of evaluated) {
117
- if (recordedSizes.has(combo.txns.length))
118
- continue;
119
- const comboIds = combo.txns.map((txn) => txn.id).sort();
120
- const key = `${bankTxn.id}|${comboIds.join('+')}`;
121
- if (seenCombinations.has(key))
122
- continue;
123
- seenCombinations.add(key);
124
- recordedSizes.add(combo.txns.length);
125
- const score = computeCombinationConfidence(combo.diff, tolerance, combo.txns.length);
126
- const candidateConfidence = Math.max(60, score - 5);
127
- const descriptionTotal = formatCurrency(combo.sum);
128
- const diffLabel = formatDifference(combo.diff);
129
- matches.push({
130
- bank_transaction: bankTxn,
131
- confidence: 'medium',
132
- confidence_score: score,
133
- match_reason: 'combination_match',
134
- top_confidence: score,
135
- candidates: combo.txns.map((txn) => ({
136
- ynab_transaction: txn,
137
- confidence: candidateConfidence,
138
- match_reason: 'combination_component',
139
- explanation: `Part of combination totaling ${descriptionTotal} (difference ${diffLabel}).`,
140
- })),
141
- action_hint: 'review_combination',
142
- recommendation: `Combination of ${combo.txns.length} YNAB transactions totals ${descriptionTotal} versus ` +
143
- `${formatCurrency(bankTxn.amount)} on the bank statement.`,
144
- });
145
- const insightId = `combination-${bankTxn.id}-${comboIds.join('+')}`;
146
- insights.push({
147
- id: insightId,
148
- type: 'combination_match',
149
- severity: 'info',
150
- title: `Combination of ${combo.txns.length} transactions matches ${formatCurrency(bankTxn.amount)}`,
151
- description: `${combo.txns.length} YNAB transactions totaling ${descriptionTotal} align with ` +
152
- `${formatCurrency(bankTxn.amount)} from ${bankTxn.payee}. Difference ${diffLabel}.`,
153
- evidence: {
154
- bank_transaction_id: bankTxn.id,
155
- bank_amount: bankTxn.amount,
156
- ynab_transaction_ids: comboIds,
157
- ynab_amounts_milliunits: combo.txns.map((txn) => txn.amount),
158
- combination_size: combo.txns.length,
159
- difference: combo.diff,
160
- },
161
- });
162
- }
163
- }
164
- return { matches, insights };
165
- }
166
- function isParsedCSVData(result) {
167
- return (typeof result === 'object' &&
168
- result !== null &&
169
- !Array.isArray(result) &&
170
- 'transactions' in result);
171
- }
172
- function normalizeDate(value) {
173
- if (value instanceof Date) {
174
- return value.toISOString().split('T')[0];
175
- }
176
- if (typeof value === 'string') {
177
- const trimmed = value.trim();
178
- if (!trimmed)
179
- return trimmed;
180
- const parsed = new Date(trimmed);
181
- if (!Number.isNaN(parsed.getTime())) {
182
- return parsed.toISOString().split('T')[0];
183
- }
184
- return trimmed;
185
- }
186
- return new Date().toISOString().split('T')[0];
187
- }
188
- function normalizeAmount(record) {
189
- const raw = record['amount'];
190
- if (typeof raw === 'number') {
191
- if (record['date'] instanceof Date || 'raw_amount' in record || 'raw_date' in record) {
192
- return Math.round(raw) / 1000;
193
- }
194
- return raw;
195
- }
196
- if (typeof raw === 'string') {
197
- const cleaned = raw.replace(/[$,\s]/g, '');
198
- const parsed = Number.parseFloat(cleaned);
199
- return Number.isFinite(parsed) ? parsed : 0;
200
- }
201
- return 0;
202
- }
203
- function normalizePayee(record) {
204
- const candidates = [record['payee'], record['description'], record['memo']];
205
- for (const candidate of candidates) {
206
- if (typeof candidate === 'string' && candidate.trim()) {
207
- return candidate.trim();
208
- }
209
- }
210
- return 'Unknown Payee';
211
- }
212
- function determineRow(record, index) {
213
- if (typeof record['original_csv_row'] === 'number') {
214
- return record['original_csv_row'];
215
- }
216
- if (typeof record['row_number'] === 'number') {
217
- return record['row_number'];
218
- }
219
- return index + 1;
220
- }
221
- function convertParserRecord(record, index) {
222
- const data = typeof record === 'object' && record !== null ? record : {};
223
- const dateValue = normalizeDate(data['date']);
224
- const amountValue = normalizeAmount(data);
225
- const payeeValue = normalizePayee(data);
226
- const memoValue = typeof data['memo'] === 'string' && data['memo'].trim() ? data['memo'].trim() : undefined;
227
- const originalRow = determineRow(data, index);
228
- const transaction = {
229
- id: randomUUID(),
230
- date: dateValue,
231
- amount: amountValue,
232
- payee: payeeValue,
233
- original_csv_row: originalRow,
234
- };
235
- if (memoValue !== undefined) {
236
- transaction.memo = memoValue;
237
- }
238
- return transaction;
239
- }
240
- function parseBankStatement(csvContent, csvFilePath) {
241
- const content = csvFilePath ? bankParser.readCSVFile(csvFilePath) : csvContent;
242
- let format = FALLBACK_CSV_FORMAT;
243
- let autoDetect;
244
- try {
245
- autoDetect = bankParser
246
- .autoDetectCSVFormat;
247
- }
248
- catch {
249
- autoDetect = undefined;
250
- }
251
- if (typeof autoDetect === 'function') {
252
- try {
253
- format = autoDetect(content);
254
- }
255
- catch {
256
- format = FALLBACK_CSV_FORMAT;
257
- }
21
+ if (result.bestMatch) {
22
+ match.ynabTransaction = result.bestMatch.ynabTransaction;
258
23
  }
259
- const rawResult = bankParser.parseBankCSV(content, format);
260
- const records = isParsedCSVData(rawResult) ? rawResult.transactions : rawResult;
261
- return records.map(convertParserRecord);
262
- }
263
- function categorizeMatches(matches) {
264
- const autoMatches = [];
265
- const suggestedMatches = [];
266
- const unmatchedBank = [];
267
- for (const match of matches) {
268
- if (match.confidence === 'high') {
269
- autoMatches.push(match);
270
- }
271
- else if (match.confidence === 'medium') {
272
- suggestedMatches.push(match);
273
- }
274
- else {
275
- unmatchedBank.push(match.bank_transaction);
276
- }
24
+ if (result.candidates[0]) {
25
+ match.topConfidence = result.candidates[0].scores.combined;
277
26
  }
278
- return { autoMatches, suggestedMatches, unmatchedBank };
279
- }
280
- function findUnmatchedYNAB(ynabTransactions, matches) {
281
- const matchedIds = new Set();
282
- for (const match of matches) {
283
- if (match.ynab_transaction) {
284
- matchedIds.add(match.ynab_transaction.id);
285
- }
27
+ if (result.confidence === 'none') {
28
+ match.recommendation = 'This bank transaction is not in YNAB. Consider adding it.';
286
29
  }
287
- return ynabTransactions.filter((txn) => !matchedIds.has(txn.id));
30
+ return match;
288
31
  }
289
- function calculateBalances(ynabTransactions, statementBalance, currency) {
32
+ function calculateBalances(ynabTransactions, statementBalanceDecimal, currency) {
290
33
  let clearedBalance = 0;
291
34
  let unclearedBalance = 0;
292
35
  for (const txn of ynabTransactions) {
293
- const amount = txn.amount / 1000;
36
+ const amount = txn.amount;
294
37
  if (txn.cleared === 'cleared' || txn.cleared === 'reconciled') {
295
38
  clearedBalance += amount;
296
39
  }
@@ -298,15 +41,16 @@ function calculateBalances(ynabTransactions, statementBalance, currency) {
298
41
  unclearedBalance += amount;
299
42
  }
300
43
  }
44
+ const statementBalanceMilli = Math.round(statementBalanceDecimal * 1000);
301
45
  const totalBalance = clearedBalance + unclearedBalance;
302
- const discrepancy = clearedBalance - statementBalance;
46
+ const discrepancy = clearedBalance - statementBalanceMilli;
303
47
  return {
304
- current_cleared: toMoneyValueFromDecimal(clearedBalance, currency),
305
- current_uncleared: toMoneyValueFromDecimal(unclearedBalance, currency),
306
- current_total: toMoneyValueFromDecimal(totalBalance, currency),
307
- target_statement: toMoneyValueFromDecimal(statementBalance, currency),
308
- discrepancy: toMoneyValueFromDecimal(discrepancy, currency),
309
- on_track: Math.abs(discrepancy) < 0.01,
48
+ current_cleared: toMoneyValue(clearedBalance, currency),
49
+ current_uncleared: toMoneyValue(unclearedBalance, currency),
50
+ current_total: toMoneyValue(totalBalance, currency),
51
+ target_statement: toMoneyValue(statementBalanceMilli, currency),
52
+ discrepancy: toMoneyValue(discrepancy, currency),
53
+ on_track: Math.abs(discrepancy) < 10,
310
54
  };
311
55
  }
312
56
  function generateSummary(bankTransactions, ynabTransactions, autoMatches, suggestedMatches, unmatchedBank, unmatchedYNAB, balances) {
@@ -363,23 +107,23 @@ function generateNextSteps(summary) {
363
107
  }
364
108
  return steps;
365
109
  }
366
- function formatCurrency(amount) {
110
+ function formatCurrency(amountMilli, currency = 'USD') {
367
111
  const formatter = new Intl.NumberFormat('en-US', {
368
112
  style: 'currency',
369
- currency: 'USD',
113
+ currency: currency,
370
114
  minimumFractionDigits: 2,
371
115
  maximumFractionDigits: 2,
372
116
  });
373
- return formatter.format(amount);
117
+ return formatter.format(amountMilli / 1000);
374
118
  }
375
- function repeatAmountInsights(unmatchedBank) {
119
+ function repeatAmountInsights(unmatchedBank, currency = 'USD') {
376
120
  const insights = [];
377
121
  if (unmatchedBank.length === 0) {
378
122
  return insights;
379
123
  }
380
124
  const frequency = new Map();
381
125
  for (const txn of unmatchedBank) {
382
- const key = txn.amount.toFixed(2);
126
+ const key = txn.amount;
383
127
  const entry = frequency.get(key) ?? { amount: txn.amount, txns: [] };
384
128
  entry.txns.push(txn);
385
129
  frequency.set(key, entry);
@@ -392,72 +136,29 @@ function repeatAmountInsights(unmatchedBank) {
392
136
  }
393
137
  const top = repeated[0];
394
138
  insights.push({
395
- id: `repeat-${top.amount.toFixed(2)}`,
139
+ id: `repeat-${top.amount}`,
396
140
  type: 'repeat_amount',
397
141
  severity: top.txns.length >= 4 ? 'critical' : 'warning',
398
- title: `${top.txns.length} unmatched transactions at ${formatCurrency(top.amount)}`,
399
- description: `The bank statement shows ${top.txns.length} unmatched transaction(s) at ${formatCurrency(top.amount)}. ` +
142
+ title: `${top.txns.length} unmatched transactions at ${formatCurrency(top.amount, currency)}`,
143
+ description: `The bank statement shows ${top.txns.length} unmatched transaction(s) at ${formatCurrency(top.amount, currency)}. ` +
400
144
  'Repeated amounts are usually the quickest wins — reconcile these first.',
401
145
  evidence: {
402
146
  amount: top.amount,
403
147
  occurrences: top.txns.length,
404
148
  dates: top.txns.map((txn) => txn.date),
405
- csv_rows: top.txns.map((txn) => txn.original_csv_row),
149
+ csv_rows: top.txns.map((txn) => txn.sourceRow),
406
150
  },
407
151
  });
408
152
  return insights;
409
153
  }
410
- function nearMatchInsights(matches, config) {
154
+ function anomalyInsights(balances) {
411
155
  const insights = [];
412
- for (const match of matches) {
413
- if (!match.candidates || match.candidates.length === 0)
414
- continue;
415
- if (match.confidence === 'high')
416
- continue;
417
- const topCandidate = match.candidates[0];
418
- const score = topCandidate.confidence;
419
- const highSignal = (match.confidence === 'medium' && score >= config.autoMatchThreshold - 5) ||
420
- (match.confidence === 'low' && score >= config.suggestionThreshold) ||
421
- (match.confidence === 'none' && score >= config.suggestionThreshold);
422
- if (!highSignal)
423
- continue;
424
- const bankTxn = match.bank_transaction;
425
- const ynabTxn = topCandidate.ynab_transaction;
426
- insights.push({
427
- id: `near-${bankTxn.id}`,
428
- type: 'near_match',
429
- severity: score >= config.autoMatchThreshold ? 'warning' : 'info',
430
- title: `${formatCurrency(bankTxn.amount)} nearly matches ${formatCurrency(ynabTxn.amount / 1000)}`,
431
- description: `Bank transaction on ${bankTxn.date} (${formatCurrency(bankTxn.amount)}) nearly matches ` +
432
- `${ynabTxn.payee_name ?? 'unknown payee'} on ${ynabTxn.date}. Confidence ${score}% — review and confirm.`,
433
- evidence: {
434
- bank_transaction: {
435
- id: bankTxn.id,
436
- date: bankTxn.date,
437
- amount: bankTxn.amount,
438
- payee: bankTxn.payee,
439
- },
440
- candidate: {
441
- id: ynabTxn.id,
442
- date: ynabTxn.date,
443
- amount_milliunits: ynabTxn.amount,
444
- payee_name: ynabTxn.payee_name,
445
- confidence: score,
446
- reasons: topCandidate.match_reason,
447
- },
448
- },
449
- });
450
- }
451
- return insights.slice(0, 3);
452
- }
453
- function anomalyInsights(summary, balances) {
454
- const insights = [];
455
- const discrepancyAbs = Math.abs(balances.discrepancy.value);
456
- if (discrepancyAbs >= 1) {
156
+ const discrepancyAbs = Math.abs(balances.discrepancy.value_milliunits);
157
+ if (discrepancyAbs >= 1000) {
457
158
  insights.push({
458
159
  id: 'balance-gap',
459
160
  type: 'anomaly',
460
- severity: discrepancyAbs >= 100 ? 'critical' : 'warning',
161
+ severity: discrepancyAbs >= 100000 ? 'critical' : 'warning',
461
162
  title: `Cleared balance off by ${balances.discrepancy.value_display}`,
462
163
  description: `YNAB cleared balance is ${balances.current_cleared.value_display} but the statement expects ` +
463
164
  `${balances.target_statement.value_display}. Focus on closing this gap.`,
@@ -468,22 +169,9 @@ function anomalyInsights(summary, balances) {
468
169
  },
469
170
  });
470
171
  }
471
- if (summary.unmatched_bank >= 5) {
472
- insights.push({
473
- id: 'bulk-missing-bank',
474
- type: 'anomaly',
475
- severity: summary.unmatched_bank >= 10 ? 'critical' : 'warning',
476
- title: `${summary.unmatched_bank} bank transactions still unmatched`,
477
- description: `There are ${summary.unmatched_bank} bank transactions without a match. ` +
478
- 'Consider bulk importing or reviewing by date sequence.',
479
- evidence: {
480
- unmatched_bank: summary.unmatched_bank,
481
- },
482
- });
483
- }
484
172
  return insights;
485
173
  }
486
- function detectInsights(matches, unmatchedBank, summary, balances, config) {
174
+ function detectInsights(unmatchedBank, _summary, balances, currency, csvErrors = [], csvWarnings = []) {
487
175
  const insights = [];
488
176
  const seen = new Set();
489
177
  const addUnique = (insight) => {
@@ -492,62 +180,90 @@ function detectInsights(matches, unmatchedBank, summary, balances, config) {
492
180
  seen.add(insight.id);
493
181
  insights.push(insight);
494
182
  };
495
- for (const insight of repeatAmountInsights(unmatchedBank)) {
496
- addUnique(insight);
183
+ if (csvErrors.length > 0) {
184
+ addUnique({
185
+ id: 'csv-parse-errors',
186
+ type: 'anomaly',
187
+ severity: csvErrors.length >= 5 ? 'critical' : 'warning',
188
+ title: `${csvErrors.length} CSV parsing error(s)`,
189
+ description: csvErrors
190
+ .slice(0, 3)
191
+ .map((e) => `Row ${e.row}: ${e.message}`)
192
+ .join('; ') + (csvErrors.length > 3 ? ` (+${csvErrors.length - 3} more)` : ''),
193
+ evidence: {
194
+ error_count: csvErrors.length,
195
+ errors: csvErrors.slice(0, 5),
196
+ },
197
+ });
198
+ }
199
+ if (csvWarnings.length > 0) {
200
+ addUnique({
201
+ id: 'csv-parse-warnings',
202
+ type: 'anomaly',
203
+ severity: 'info',
204
+ title: `${csvWarnings.length} CSV parsing warning(s)`,
205
+ description: csvWarnings
206
+ .slice(0, 3)
207
+ .map((w) => `Row ${w.row}: ${w.message}`)
208
+ .join('; ') + (csvWarnings.length > 3 ? ` (+${csvWarnings.length - 3} more)` : ''),
209
+ evidence: {
210
+ warning_count: csvWarnings.length,
211
+ warnings: csvWarnings.slice(0, 5),
212
+ },
213
+ });
497
214
  }
498
- for (const insight of nearMatchInsights(matches, config)) {
215
+ for (const insight of repeatAmountInsights(unmatchedBank, currency)) {
499
216
  addUnique(insight);
500
217
  }
501
- for (const insight of anomalyInsights(summary, balances)) {
218
+ for (const insight of anomalyInsights(balances)) {
502
219
  addUnique(insight);
503
220
  }
504
221
  return insights.slice(0, 5);
505
222
  }
506
- function mergeInsights(base, additional) {
507
- if (additional.length === 0) {
508
- return base;
509
- }
510
- const seen = new Set(base.map((insight) => insight.id));
511
- const merged = [...base];
512
- for (const insight of additional) {
513
- if (seen.has(insight.id))
514
- continue;
515
- seen.add(insight.id);
516
- merged.push(insight);
517
- }
518
- return merged.slice(0, 5);
519
- }
520
- export function analyzeReconciliation(csvContent, csvFilePath, ynabTransactions, statementBalance, config = DEFAULT_MATCHING_CONFIG, currency = 'USD', accountId, budgetId, invertBankAmounts = false) {
521
- let bankTransactions = parseBankStatement(csvContent, csvFilePath);
522
- if (invertBankAmounts) {
523
- bankTransactions = bankTransactions.map((txn) => ({
524
- ...txn,
525
- amount: -txn.amount,
526
- }));
527
- }
528
- const convertedYNABTxns = ynabTransactions.map(convertYNABTransaction);
529
- const matches = findMatches(bankTransactions, convertedYNABTxns, config);
530
- const { autoMatches, suggestedMatches, unmatchedBank } = categorizeMatches(matches);
531
- const unmatchedYNAB = findUnmatchedYNAB(convertedYNABTxns, matches);
532
- let combinationMatches = [];
533
- let combinationInsights = [];
534
- if (ENABLE_COMBINATION_MATCHING) {
535
- const combinationResult = findCombinationMatches(unmatchedBank, unmatchedYNAB, config);
536
- combinationMatches = combinationResult.matches;
537
- combinationInsights = combinationResult.insights;
223
+ export function analyzeReconciliation(csvContentOrParsed, _csvFilePath, ynabTransactions, statementBalance, config = DEFAULT_CONFIG, currency = 'USD', accountId, budgetId, invertBankAmounts = false, csvOptions) {
224
+ let parseResult;
225
+ if (typeof csvContentOrParsed === 'string') {
226
+ parseResult = parseCSV(csvContentOrParsed, {
227
+ ...csvOptions,
228
+ invertAmounts: invertBankAmounts,
229
+ });
538
230
  }
539
- const enrichedSuggestedMatches = [...suggestedMatches, ...combinationMatches];
540
- const balances = calculateBalances(convertedYNABTxns, statementBalance, currency);
541
- const summary = generateSummary(bankTransactions, convertedYNABTxns, autoMatches, enrichedSuggestedMatches, unmatchedBank, unmatchedYNAB, balances);
231
+ else {
232
+ parseResult = csvContentOrParsed;
233
+ }
234
+ const newBankTransactions = parseResult.transactions;
235
+ const csvParseErrors = parseResult.errors;
236
+ const csvParseWarnings = parseResult.warnings;
237
+ const newYNABTransactions = normalizeYNABTransactions(ynabTransactions);
238
+ const normalizedConfig = normalizeConfig(config);
239
+ const newMatches = findMatches(newBankTransactions, newYNABTransactions, normalizedConfig);
240
+ const matches = newMatches.map(mapToTransactionMatch);
241
+ const autoMatches = matches.filter((m) => m.confidence === 'high');
242
+ const autoMatchedYnabIds = new Set();
243
+ autoMatches.forEach((m) => {
244
+ if (m.ynabTransaction)
245
+ autoMatchedYnabIds.add(m.ynabTransaction.id);
246
+ });
247
+ const suggestedMatches = matches.filter((m) => m.confidence === 'medium' &&
248
+ (!m.ynabTransaction || !autoMatchedYnabIds.has(m.ynabTransaction.id)));
249
+ const unmatchedBankMatches = matches.filter((m) => m.confidence === 'low' || m.confidence === 'none');
250
+ const unmatchedBank = unmatchedBankMatches.map((m) => m.bankTransaction);
251
+ const matchedYnabIds = new Set();
252
+ matches.forEach((m) => {
253
+ if (m.ynabTransaction)
254
+ matchedYnabIds.add(m.ynabTransaction.id);
255
+ });
256
+ const unmatchedYNAB = newYNABTransactions.filter((t) => !matchedYnabIds.has(t.id));
257
+ const balances = calculateBalances(newYNABTransactions, statementBalance, currency);
258
+ const summary = generateSummary(matches.map((m) => m.bankTransaction), newYNABTransactions, autoMatches, suggestedMatches, unmatchedBank, unmatchedYNAB, balances);
542
259
  const nextSteps = generateNextSteps(summary);
543
- const baseInsights = detectInsights(matches, unmatchedBank, summary, balances, config);
544
- const insights = mergeInsights(baseInsights, combinationInsights);
260
+ const insights = detectInsights(unmatchedBank, summary, balances, currency, csvParseErrors, csvParseWarnings);
545
261
  const analysis = {
546
262
  success: true,
547
263
  phase: 'analysis',
548
264
  summary,
549
265
  auto_matches: autoMatches,
550
- suggested_matches: enrichedSuggestedMatches,
266
+ suggested_matches: suggestedMatches,
551
267
  unmatched_bank: unmatchedBank,
552
268
  unmatched_ynab: unmatchedYNAB,
553
269
  balance_info: balances,
@@ -559,7 +275,7 @@ export function analyzeReconciliation(csvContent, csvFilePath, ynabTransactions,
559
275
  account_id: accountId,
560
276
  budget_id: budgetId,
561
277
  analysis,
562
- matching_config: config,
278
+ matching_config: normalizedConfig,
563
279
  });
564
280
  analysis.recommendations = recommendations;
565
281
  }
@@ -0,0 +1,51 @@
1
+ import type { BankTransaction } from '../../types/reconciliation.js';
2
+ export interface CSVParseResult {
3
+ transactions: BankTransaction[];
4
+ errors: ParseError[];
5
+ warnings: ParseWarning[];
6
+ meta: {
7
+ detectedDelimiter: string;
8
+ detectedColumns: string[];
9
+ totalRows: number;
10
+ validRows: number;
11
+ skippedRows: number;
12
+ };
13
+ }
14
+ export interface ParseError {
15
+ row: number;
16
+ field: string;
17
+ message: string;
18
+ rawValue: string;
19
+ }
20
+ export interface ParseWarning {
21
+ row: number;
22
+ message: string;
23
+ }
24
+ export interface BankPreset {
25
+ name: string;
26
+ dateColumn: string | string[];
27
+ amountColumn?: string | string[];
28
+ debitColumn?: string;
29
+ creditColumn?: string;
30
+ descriptionColumn: string | string[];
31
+ amountMultiplier?: number;
32
+ dateFormat?: 'YMD' | 'MDY' | 'DMY';
33
+ header?: boolean;
34
+ }
35
+ export declare const BANK_PRESETS: Record<string, BankPreset>;
36
+ export interface ParseCSVOptions {
37
+ preset?: string;
38
+ invertAmounts?: boolean;
39
+ columns?: {
40
+ date?: string;
41
+ amount?: string;
42
+ debit?: string;
43
+ credit?: string;
44
+ description?: string;
45
+ };
46
+ dateFormat?: 'YMD' | 'MDY' | 'DMY';
47
+ header?: boolean;
48
+ maxRows?: number;
49
+ maxBytes?: number;
50
+ }
51
+ export declare function parseCSV(content: string, options?: ParseCSVOptions): CSVParseResult;