@dizzlkheinz/ynab-mcpb 0.12.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 (435) hide show
  1. package/.chunkhound.json +11 -0
  2. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +8 -0
  3. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +3 -0
  4. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +3 -0
  5. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +1 -0
  6. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +12 -0
  7. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +1 -0
  8. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +1 -0
  9. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +5 -0
  10. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +1453 -0
  11. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +8 -0
  12. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +9 -0
  13. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +1 -0
  14. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +5 -0
  15. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +81 -0
  16. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +610 -0
  17. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +1 -0
  18. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +1 -0
  19. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +1 -0
  20. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +3 -0
  21. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +8 -0
  22. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +5 -0
  23. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +1 -0
  24. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +9 -0
  25. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +1 -0
  26. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +13 -0
  27. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +14 -0
  28. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +5 -0
  29. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +5 -0
  30. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +5 -0
  31. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +3 -0
  32. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +1 -0
  33. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +1 -0
  34. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +5 -0
  35. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +5 -0
  36. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +8 -0
  37. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +8 -0
  38. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +5 -0
  39. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +1 -0
  40. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +1 -0
  41. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +1 -0
  42. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +1 -0
  43. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +1 -0
  44. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +5 -0
  45. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +1 -0
  46. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +1 -0
  47. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +1 -0
  48. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +3 -0
  49. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +8 -0
  50. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +1 -0
  51. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +1 -0
  52. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +8 -0
  53. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +3 -0
  54. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +5 -0
  55. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +1 -0
  56. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +1 -0
  57. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +5 -0
  58. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +1 -0
  59. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +14 -0
  60. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +1 -0
  61. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +5 -0
  62. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +5 -0
  63. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +11 -0
  64. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +1 -0
  65. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +8 -0
  66. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +1 -0
  67. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +3 -0
  68. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +3 -0
  69. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +8 -0
  70. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +1 -0
  71. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +5 -0
  72. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +1 -0
  73. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +1 -0
  74. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +1 -0
  75. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +1 -0
  76. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +3 -0
  77. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +8 -0
  78. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +1 -0
  79. package/.dxtignore +57 -0
  80. package/.env.example +44 -0
  81. package/.gemini/settings.json +8 -0
  82. package/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
  83. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  84. package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  85. package/.github/ISSUE_TEMPLATE/release_checklist.md +31 -0
  86. package/.github/pull_request_template.md +41 -0
  87. package/.github/workflows/ci-tests.yml +41 -0
  88. package/.github/workflows/claude-code-review.yml +57 -0
  89. package/.github/workflows/claude.yml +50 -0
  90. package/.github/workflows/full-integration.yml +22 -0
  91. package/.github/workflows/pr-description-check.yml +88 -0
  92. package/.github/workflows/publish.yml +33 -0
  93. package/.github/workflows/release.yml +89 -0
  94. package/.mcpbignore +58 -0
  95. package/.prettierignore +10 -0
  96. package/.prettierrc.json +10 -0
  97. package/ADOS-2-Module-1-Complete-Manual.md +757 -0
  98. package/AGENTS.md +36 -0
  99. package/CHANGELOG.md +187 -0
  100. package/CLAUDE.md +414 -0
  101. package/CODEREVIEW_RESPONSE.md +128 -0
  102. package/LICENSE +17 -0
  103. package/NUL +1 -0
  104. package/README.md +222 -0
  105. package/SCHEMA_IMPROVEMENT_SUMMARY.md +120 -0
  106. package/TESTING_NOTES.md +217 -0
  107. package/WARP.md +245 -0
  108. package/accountactivity-merged.csv +149 -0
  109. package/bin/ynab-mcp-server.cjs +4 -0
  110. package/bin/ynab-mcp-server.js +8 -0
  111. package/bundle-analysis.html +13110 -0
  112. package/dist/bundle/index.cjs +124 -0
  113. package/dist/index.d.ts +2 -0
  114. package/dist/index.js +85 -0
  115. package/dist/server/YNABMCPServer.d.ts +264 -0
  116. package/dist/server/YNABMCPServer.js +845 -0
  117. package/dist/server/budgetResolver.d.ts +15 -0
  118. package/dist/server/budgetResolver.js +99 -0
  119. package/dist/server/cacheManager.d.ts +74 -0
  120. package/dist/server/cacheManager.js +306 -0
  121. package/dist/server/config.d.ts +3 -0
  122. package/dist/server/config.js +19 -0
  123. package/dist/server/deltaCache.d.ts +61 -0
  124. package/dist/server/deltaCache.js +206 -0
  125. package/dist/server/deltaCache.merge.d.ts +9 -0
  126. package/dist/server/deltaCache.merge.js +111 -0
  127. package/dist/server/diagnostics.d.ts +90 -0
  128. package/dist/server/diagnostics.js +163 -0
  129. package/dist/server/errorHandler.d.ts +69 -0
  130. package/dist/server/errorHandler.js +524 -0
  131. package/dist/server/prompts.d.ts +31 -0
  132. package/dist/server/prompts.js +205 -0
  133. package/dist/server/rateLimiter.d.ts +27 -0
  134. package/dist/server/rateLimiter.js +82 -0
  135. package/dist/server/requestLogger.d.ts +62 -0
  136. package/dist/server/requestLogger.js +190 -0
  137. package/dist/server/resources.d.ts +39 -0
  138. package/dist/server/resources.js +85 -0
  139. package/dist/server/responseFormatter.d.ts +14 -0
  140. package/dist/server/responseFormatter.js +42 -0
  141. package/dist/server/securityMiddleware.d.ts +87 -0
  142. package/dist/server/securityMiddleware.js +117 -0
  143. package/dist/server/serverKnowledgeStore.d.ts +11 -0
  144. package/dist/server/serverKnowledgeStore.js +42 -0
  145. package/dist/server/toolRegistry.d.ts +85 -0
  146. package/dist/server/toolRegistry.js +272 -0
  147. package/dist/tools/__tests__/deltaTestUtils.d.ts +18 -0
  148. package/dist/tools/__tests__/deltaTestUtils.js +26 -0
  149. package/dist/tools/accountTools.d.ts +37 -0
  150. package/dist/tools/accountTools.js +175 -0
  151. package/dist/tools/budgetTools.d.ts +10 -0
  152. package/dist/tools/budgetTools.js +68 -0
  153. package/dist/tools/categoryTools.d.ts +27 -0
  154. package/dist/tools/categoryTools.js +232 -0
  155. package/dist/tools/compareTransactions/formatter.d.ts +71 -0
  156. package/dist/tools/compareTransactions/formatter.js +97 -0
  157. package/dist/tools/compareTransactions/index.d.ts +30 -0
  158. package/dist/tools/compareTransactions/index.js +160 -0
  159. package/dist/tools/compareTransactions/matcher.d.ts +12 -0
  160. package/dist/tools/compareTransactions/matcher.js +140 -0
  161. package/dist/tools/compareTransactions/parser.d.ts +14 -0
  162. package/dist/tools/compareTransactions/parser.js +430 -0
  163. package/dist/tools/compareTransactions/types.d.ts +27 -0
  164. package/dist/tools/compareTransactions/types.js +1 -0
  165. package/dist/tools/compareTransactions.d.ts +1 -0
  166. package/dist/tools/compareTransactions.js +1 -0
  167. package/dist/tools/deltaFetcher.d.ts +22 -0
  168. package/dist/tools/deltaFetcher.js +137 -0
  169. package/dist/tools/deltaSupport.d.ts +20 -0
  170. package/dist/tools/deltaSupport.js +176 -0
  171. package/dist/tools/exportTransactions.d.ts +17 -0
  172. package/dist/tools/exportTransactions.js +191 -0
  173. package/dist/tools/monthTools.d.ts +16 -0
  174. package/dist/tools/monthTools.js +107 -0
  175. package/dist/tools/payeeTools.d.ts +17 -0
  176. package/dist/tools/payeeTools.js +82 -0
  177. package/dist/tools/reconcileAdapter.d.ts +25 -0
  178. package/dist/tools/reconcileAdapter.js +167 -0
  179. package/dist/tools/reconciliation/analyzer.d.ts +3 -0
  180. package/dist/tools/reconciliation/analyzer.js +567 -0
  181. package/dist/tools/reconciliation/executor.d.ts +94 -0
  182. package/dist/tools/reconciliation/executor.js +611 -0
  183. package/dist/tools/reconciliation/index.d.ts +54 -0
  184. package/dist/tools/reconciliation/index.js +249 -0
  185. package/dist/tools/reconciliation/matcher.d.ts +3 -0
  186. package/dist/tools/reconciliation/matcher.js +160 -0
  187. package/dist/tools/reconciliation/payeeNormalizer.d.ts +6 -0
  188. package/dist/tools/reconciliation/payeeNormalizer.js +77 -0
  189. package/dist/tools/reconciliation/recommendationEngine.d.ts +2 -0
  190. package/dist/tools/reconciliation/recommendationEngine.js +273 -0
  191. package/dist/tools/reconciliation/reportFormatter.d.ts +13 -0
  192. package/dist/tools/reconciliation/reportFormatter.js +214 -0
  193. package/dist/tools/reconciliation/types.d.ts +172 -0
  194. package/dist/tools/reconciliation/types.js +7 -0
  195. package/dist/tools/schemas/outputs/accountOutputs.d.ts +58 -0
  196. package/dist/tools/schemas/outputs/accountOutputs.js +24 -0
  197. package/dist/tools/schemas/outputs/budgetOutputs.d.ts +48 -0
  198. package/dist/tools/schemas/outputs/budgetOutputs.js +15 -0
  199. package/dist/tools/schemas/outputs/categoryOutputs.d.ts +93 -0
  200. package/dist/tools/schemas/outputs/categoryOutputs.js +37 -0
  201. package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +269 -0
  202. package/dist/tools/schemas/outputs/comparisonOutputs.js +181 -0
  203. package/dist/tools/schemas/outputs/index.d.ts +14 -0
  204. package/dist/tools/schemas/outputs/index.js +14 -0
  205. package/dist/tools/schemas/outputs/monthOutputs.d.ts +122 -0
  206. package/dist/tools/schemas/outputs/monthOutputs.js +51 -0
  207. package/dist/tools/schemas/outputs/payeeOutputs.d.ts +34 -0
  208. package/dist/tools/schemas/outputs/payeeOutputs.js +16 -0
  209. package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +1275 -0
  210. package/dist/tools/schemas/outputs/reconciliationOutputs.js +377 -0
  211. package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +717 -0
  212. package/dist/tools/schemas/outputs/transactionMutationOutputs.js +260 -0
  213. package/dist/tools/schemas/outputs/transactionOutputs.d.ts +98 -0
  214. package/dist/tools/schemas/outputs/transactionOutputs.js +49 -0
  215. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +219 -0
  216. package/dist/tools/schemas/outputs/utilityOutputs.js +120 -0
  217. package/dist/tools/schemas/shared/commonOutputs.d.ts +24 -0
  218. package/dist/tools/schemas/shared/commonOutputs.js +27 -0
  219. package/dist/tools/toolCategories.d.ts +32 -0
  220. package/dist/tools/toolCategories.js +32 -0
  221. package/dist/tools/transactionTools.d.ts +315 -0
  222. package/dist/tools/transactionTools.js +1722 -0
  223. package/dist/tools/utilityTools.d.ts +10 -0
  224. package/dist/tools/utilityTools.js +56 -0
  225. package/dist/types/index.d.ts +20 -0
  226. package/dist/types/index.js +16 -0
  227. package/dist/types/toolAnnotations.d.ts +7 -0
  228. package/dist/types/toolAnnotations.js +1 -0
  229. package/dist/utils/amountUtils.d.ts +3 -0
  230. package/dist/utils/amountUtils.js +10 -0
  231. package/dist/utils/dateUtils.d.ts +9 -0
  232. package/dist/utils/dateUtils.js +43 -0
  233. package/dist/utils/money.d.ts +21 -0
  234. package/dist/utils/money.js +51 -0
  235. package/docs/README.md +72 -0
  236. package/docs/assets/examples/reconciliation-with-recommendations.json +68 -0
  237. package/docs/assets/schemas/reconciliation-v2.json +338 -0
  238. package/docs/getting-started/CONFIGURATION.md +175 -0
  239. package/docs/getting-started/INSTALLATION.md +333 -0
  240. package/docs/getting-started/QUICKSTART.md +282 -0
  241. package/docs/guides/ARCHITECTURE.md +650 -0
  242. package/docs/guides/DEPLOYMENT.md +189 -0
  243. package/docs/guides/INTEGRATION_TESTING.md +730 -0
  244. package/docs/guides/TESTING.md +591 -0
  245. package/docs/reconciliation-flow.md +83 -0
  246. package/docs/reference/API.md +1450 -0
  247. package/docs/reference/EXAMPLES.md +946 -0
  248. package/docs/reference/TOOLS.md +348 -0
  249. package/docs/reference/TROUBLESHOOTING.md +481 -0
  250. package/esbuild.config.mjs +68 -0
  251. package/eslint.config.js +49 -0
  252. package/fix-types.sh +17 -0
  253. package/meta.json +12550 -0
  254. package/package.json +105 -0
  255. package/package.json.tmp +105 -0
  256. package/scripts/analyze-bundle.mjs +41 -0
  257. package/scripts/create-pr-description.js +203 -0
  258. package/scripts/generate-mcpb.ps1 +96 -0
  259. package/scripts/run-domain-integration-tests.js +33 -0
  260. package/scripts/run-generate-mcpb.js +29 -0
  261. package/scripts/run-throttled-integration-tests.js +116 -0
  262. package/scripts/test-delta-params.mjs +140 -0
  263. package/scripts/test-recommendations.ts +53 -0
  264. package/scripts/tmpTransaction.ts +48 -0
  265. package/scripts/validate-env.js +122 -0
  266. package/scripts/verify-build.js +105 -0
  267. package/scripts/watch-and-restart.ps1 +50 -0
  268. package/src/__tests__/comprehensive.integration.test.ts +1196 -0
  269. package/src/__tests__/delta.performance.test.ts +80 -0
  270. package/src/__tests__/performance.test.ts +725 -0
  271. package/src/__tests__/setup.ts +449 -0
  272. package/src/__tests__/testRunner.ts +444 -0
  273. package/src/__tests__/testUtils.ts +563 -0
  274. package/src/__tests__/workflows.e2e.test.ts +1675 -0
  275. package/src/index.ts +124 -0
  276. package/src/server/.gitkeep +1 -0
  277. package/src/server/YNABMCPServer.ts +1188 -0
  278. package/src/server/__tests__/YNABMCPServer.integration.test.ts +903 -0
  279. package/src/server/__tests__/YNABMCPServer.test.ts +894 -0
  280. package/src/server/__tests__/budgetResolver.test.ts +425 -0
  281. package/src/server/__tests__/cacheManager.test.ts +880 -0
  282. package/src/server/__tests__/config.test.ts +166 -0
  283. package/src/server/__tests__/deltaCache.merge.test.ts +724 -0
  284. package/src/server/__tests__/deltaCache.swr.test.ts +168 -0
  285. package/src/server/__tests__/deltaCache.test.ts +774 -0
  286. package/src/server/__tests__/diagnostics.test.ts +823 -0
  287. package/src/server/__tests__/errorHandler.integration.test.ts +466 -0
  288. package/src/server/__tests__/errorHandler.test.ts +416 -0
  289. package/src/server/__tests__/prompts.test.ts +354 -0
  290. package/src/server/__tests__/rateLimiter.test.ts +314 -0
  291. package/src/server/__tests__/requestLogger.test.ts +408 -0
  292. package/src/server/__tests__/resources.test.ts +299 -0
  293. package/src/server/__tests__/security.integration.test.ts +426 -0
  294. package/src/server/__tests__/securityMiddleware.test.ts +449 -0
  295. package/src/server/__tests__/server-startup.integration.test.ts +477 -0
  296. package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -0
  297. package/src/server/__tests__/toolRegistry.test.ts +855 -0
  298. package/src/server/budgetResolver.ts +235 -0
  299. package/src/server/cacheManager.ts +503 -0
  300. package/src/server/config.ts +41 -0
  301. package/src/server/deltaCache.merge.ts +149 -0
  302. package/src/server/deltaCache.ts +341 -0
  303. package/src/server/diagnostics.ts +338 -0
  304. package/src/server/errorHandler.ts +756 -0
  305. package/src/server/prompts.ts +291 -0
  306. package/src/server/rateLimiter.ts +156 -0
  307. package/src/server/requestLogger.ts +344 -0
  308. package/src/server/resources.ts +168 -0
  309. package/src/server/responseFormatter.ts +51 -0
  310. package/src/server/securityMiddleware.ts +236 -0
  311. package/src/server/serverKnowledgeStore.ts +91 -0
  312. package/src/server/toolRegistry.ts +489 -0
  313. package/src/tools/.gitkeep +1 -0
  314. package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -0
  315. package/src/tools/__tests__/accountTools.integration.test.ts +117 -0
  316. package/src/tools/__tests__/accountTools.test.ts +653 -0
  317. package/src/tools/__tests__/budgetTools.delta.integration.test.ts +90 -0
  318. package/src/tools/__tests__/budgetTools.integration.test.ts +134 -0
  319. package/src/tools/__tests__/budgetTools.test.ts +423 -0
  320. package/src/tools/__tests__/categoryTools.delta.integration.test.ts +80 -0
  321. package/src/tools/__tests__/categoryTools.integration.test.ts +295 -0
  322. package/src/tools/__tests__/categoryTools.test.ts +622 -0
  323. package/src/tools/__tests__/compareTransactions/formatter.test.ts +486 -0
  324. package/src/tools/__tests__/compareTransactions/index.test.ts +383 -0
  325. package/src/tools/__tests__/compareTransactions/matcher.test.ts +410 -0
  326. package/src/tools/__tests__/compareTransactions/parser.test.ts +764 -0
  327. package/src/tools/__tests__/compareTransactions.test.ts +342 -0
  328. package/src/tools/__tests__/compareTransactions.window.test.ts +147 -0
  329. package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +76 -0
  330. package/src/tools/__tests__/deltaFetcher.test.ts +270 -0
  331. package/src/tools/__tests__/deltaSupport.test.ts +188 -0
  332. package/src/tools/__tests__/deltaTestUtils.ts +46 -0
  333. package/src/tools/__tests__/exportTransactions.test.ts +213 -0
  334. package/src/tools/__tests__/monthTools.delta.integration.test.ts +80 -0
  335. package/src/tools/__tests__/monthTools.integration.test.ts +174 -0
  336. package/src/tools/__tests__/monthTools.test.ts +523 -0
  337. package/src/tools/__tests__/payeeTools.delta.integration.test.ts +80 -0
  338. package/src/tools/__tests__/payeeTools.integration.test.ts +150 -0
  339. package/src/tools/__tests__/payeeTools.test.ts +445 -0
  340. package/src/tools/__tests__/transactionTools.integration.test.ts +762 -0
  341. package/src/tools/__tests__/transactionTools.test.ts +3521 -0
  342. package/src/tools/__tests__/utilityTools.integration.test.ts +128 -0
  343. package/src/tools/__tests__/utilityTools.test.ts +205 -0
  344. package/src/tools/accountTools.ts +283 -0
  345. package/src/tools/budgetTools.ts +112 -0
  346. package/src/tools/categoryTools.ts +366 -0
  347. package/src/tools/compareTransactions/formatter.ts +163 -0
  348. package/src/tools/compareTransactions/index.ts +228 -0
  349. package/src/tools/compareTransactions/matcher.ts +240 -0
  350. package/src/tools/compareTransactions/parser.ts +557 -0
  351. package/src/tools/compareTransactions/types.ts +60 -0
  352. package/src/tools/compareTransactions.ts +3 -0
  353. package/src/tools/deltaFetcher.ts +278 -0
  354. package/src/tools/deltaSupport.ts +293 -0
  355. package/src/tools/exportTransactions.ts +273 -0
  356. package/src/tools/monthTools.ts +164 -0
  357. package/src/tools/payeeTools.ts +140 -0
  358. package/src/tools/reconcileAdapter.ts +312 -0
  359. package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +122 -0
  360. package/src/tools/reconciliation/__tests__/adapter.test.ts +234 -0
  361. package/src/tools/reconciliation/__tests__/analyzer.test.ts +406 -0
  362. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +366 -0
  363. package/src/tools/reconciliation/__tests__/executor.test.ts +779 -0
  364. package/src/tools/reconciliation/__tests__/matcher.test.ts +650 -0
  365. package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +278 -0
  366. package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +658 -0
  367. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1000 -0
  368. package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +151 -0
  369. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +573 -0
  370. package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +78 -0
  371. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +47 -0
  372. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +61 -0
  373. package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +49 -0
  374. package/src/tools/reconciliation/analyzer.ts +824 -0
  375. package/src/tools/reconciliation/executor.ts +880 -0
  376. package/src/tools/reconciliation/index.ts +400 -0
  377. package/src/tools/reconciliation/matcher.ts +269 -0
  378. package/src/tools/reconciliation/payeeNormalizer.ts +167 -0
  379. package/src/tools/reconciliation/recommendationEngine.ts +506 -0
  380. package/src/tools/reconciliation/reportFormatter.ts +363 -0
  381. package/src/tools/reconciliation/types.ts +314 -0
  382. package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +424 -0
  383. package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +310 -0
  384. package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +448 -0
  385. package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +519 -0
  386. package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +155 -0
  387. package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +288 -0
  388. package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +478 -0
  389. package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +370 -0
  390. package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +401 -0
  391. package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +213 -0
  392. package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +474 -0
  393. package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +333 -0
  394. package/src/tools/schemas/outputs/accountOutputs.ts +137 -0
  395. package/src/tools/schemas/outputs/budgetOutputs.ts +86 -0
  396. package/src/tools/schemas/outputs/categoryOutputs.ts +194 -0
  397. package/src/tools/schemas/outputs/comparisonOutputs.ts +600 -0
  398. package/src/tools/schemas/outputs/index.ts +270 -0
  399. package/src/tools/schemas/outputs/monthOutputs.ts +243 -0
  400. package/src/tools/schemas/outputs/payeeOutputs.ts +105 -0
  401. package/src/tools/schemas/outputs/reconciliationOutputs.ts +796 -0
  402. package/src/tools/schemas/outputs/transactionMutationOutputs.ts +758 -0
  403. package/src/tools/schemas/outputs/transactionOutputs.ts +243 -0
  404. package/src/tools/schemas/outputs/utilityOutputs.ts +411 -0
  405. package/src/tools/schemas/shared/commonOutputs.ts +140 -0
  406. package/src/tools/toolCategories.ts +140 -0
  407. package/src/tools/transactionTools.ts +2509 -0
  408. package/src/tools/utilityTools.ts +90 -0
  409. package/src/types/.gitkeep +1 -0
  410. package/src/types/__tests__/index.test.ts +52 -0
  411. package/src/types/index.ts +67 -0
  412. package/src/types/integration-tests.d.ts +35 -0
  413. package/src/types/toolAnnotations.ts +44 -0
  414. package/src/utils/__tests__/dateUtils.test.ts +170 -0
  415. package/src/utils/__tests__/money.test.ts +189 -0
  416. package/src/utils/amountUtils.ts +32 -0
  417. package/src/utils/dateUtils.ts +108 -0
  418. package/src/utils/money.ts +123 -0
  419. package/test-csv-sample.csv +28 -0
  420. package/test-exports/sample_bank_statement.csv +7 -0
  421. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +23 -0
  422. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +23 -0
  423. package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +44 -0
  424. package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +58 -0
  425. package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +115 -0
  426. package/test-reconcile-autodetect.js +40 -0
  427. package/test-reconcile-tool.js +152 -0
  428. package/test-reconcile-with-csv.cjs +89 -0
  429. package/test-statement.csv +8 -0
  430. package/test_debug.js +47 -0
  431. package/test_simple.mjs +16 -0
  432. package/tsconfig.json +31 -0
  433. package/tsconfig.prod.json +18 -0
  434. package/vitest-reporters/split-json-reporter.ts +211 -0
  435. package/vitest.config.ts +96 -0
@@ -0,0 +1,567 @@
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';
6
+ 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,
17
+ };
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
+ }
258
+ }
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
+ }
277
+ }
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
+ }
286
+ }
287
+ return ynabTransactions.filter((txn) => !matchedIds.has(txn.id));
288
+ }
289
+ function calculateBalances(ynabTransactions, statementBalance, currency) {
290
+ let clearedBalance = 0;
291
+ let unclearedBalance = 0;
292
+ for (const txn of ynabTransactions) {
293
+ const amount = txn.amount / 1000;
294
+ if (txn.cleared === 'cleared' || txn.cleared === 'reconciled') {
295
+ clearedBalance += amount;
296
+ }
297
+ else {
298
+ unclearedBalance += amount;
299
+ }
300
+ }
301
+ const totalBalance = clearedBalance + unclearedBalance;
302
+ const discrepancy = clearedBalance - statementBalance;
303
+ 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,
310
+ };
311
+ }
312
+ function generateSummary(bankTransactions, ynabTransactions, autoMatches, suggestedMatches, unmatchedBank, unmatchedYNAB, balances) {
313
+ const dates = bankTransactions.map((t) => t.date).sort();
314
+ const dateRange = dates.length > 0 ? `${dates[0]} to ${dates[dates.length - 1]}` : 'Unknown';
315
+ let discrepancyExplanation = '';
316
+ if (balances.on_track) {
317
+ discrepancyExplanation = 'Cleared balance matches statement';
318
+ }
319
+ else {
320
+ const actionsNeeded = [];
321
+ if (autoMatches.length > 0) {
322
+ actionsNeeded.push(`clear ${autoMatches.length} transactions`);
323
+ }
324
+ if (unmatchedBank.length > 0) {
325
+ actionsNeeded.push(`add ${unmatchedBank.length} missing`);
326
+ }
327
+ if (unmatchedYNAB.length > 0) {
328
+ actionsNeeded.push(`review ${unmatchedYNAB.length} unmatched YNAB`);
329
+ }
330
+ discrepancyExplanation =
331
+ actionsNeeded.length > 0 ? `Need to ${actionsNeeded.join(', ')}` : 'Manual review required';
332
+ }
333
+ return {
334
+ statement_date_range: dateRange,
335
+ bank_transactions_count: bankTransactions.length,
336
+ ynab_transactions_count: ynabTransactions.length,
337
+ auto_matched: autoMatches.length,
338
+ suggested_matches: suggestedMatches.length,
339
+ unmatched_bank: unmatchedBank.length,
340
+ unmatched_ynab: unmatchedYNAB.length,
341
+ current_cleared_balance: balances.current_cleared,
342
+ target_statement_balance: balances.target_statement,
343
+ discrepancy: balances.discrepancy,
344
+ discrepancy_explanation: discrepancyExplanation,
345
+ };
346
+ }
347
+ function generateNextSteps(summary) {
348
+ const steps = [];
349
+ if (summary.auto_matched > 0) {
350
+ steps.push(`Review ${summary.auto_matched} auto-matched transactions for approval`);
351
+ }
352
+ if (summary.suggested_matches > 0) {
353
+ steps.push(`Review ${summary.suggested_matches} suggested matches and choose best match`);
354
+ }
355
+ if (summary.unmatched_bank > 0) {
356
+ steps.push(`Decide whether to add ${summary.unmatched_bank} missing bank transactions to YNAB`);
357
+ }
358
+ if (summary.unmatched_ynab > 0) {
359
+ steps.push(`Decide what to do with ${summary.unmatched_ynab} unmatched YNAB transactions (unclear/delete/ignore)`);
360
+ }
361
+ if (steps.length === 0) {
362
+ steps.push('All transactions matched! Review and approve to complete reconciliation');
363
+ }
364
+ return steps;
365
+ }
366
+ function formatCurrency(amount) {
367
+ const formatter = new Intl.NumberFormat('en-US', {
368
+ style: 'currency',
369
+ currency: 'USD',
370
+ minimumFractionDigits: 2,
371
+ maximumFractionDigits: 2,
372
+ });
373
+ return formatter.format(amount);
374
+ }
375
+ function repeatAmountInsights(unmatchedBank) {
376
+ const insights = [];
377
+ if (unmatchedBank.length === 0) {
378
+ return insights;
379
+ }
380
+ const frequency = new Map();
381
+ for (const txn of unmatchedBank) {
382
+ const key = txn.amount.toFixed(2);
383
+ const entry = frequency.get(key) ?? { amount: txn.amount, txns: [] };
384
+ entry.txns.push(txn);
385
+ frequency.set(key, entry);
386
+ }
387
+ const repeated = Array.from(frequency.values())
388
+ .filter((entry) => entry.txns.length >= 2)
389
+ .sort((a, b) => b.txns.length - a.txns.length);
390
+ if (repeated.length === 0) {
391
+ return insights;
392
+ }
393
+ const top = repeated[0];
394
+ insights.push({
395
+ id: `repeat-${top.amount.toFixed(2)}`,
396
+ type: 'repeat_amount',
397
+ 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)}. ` +
400
+ 'Repeated amounts are usually the quickest wins — reconcile these first.',
401
+ evidence: {
402
+ amount: top.amount,
403
+ occurrences: top.txns.length,
404
+ dates: top.txns.map((txn) => txn.date),
405
+ csv_rows: top.txns.map((txn) => txn.original_csv_row),
406
+ },
407
+ });
408
+ return insights;
409
+ }
410
+ function nearMatchInsights(matches, config) {
411
+ 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) {
457
+ insights.push({
458
+ id: 'balance-gap',
459
+ type: 'anomaly',
460
+ severity: discrepancyAbs >= 100 ? 'critical' : 'warning',
461
+ title: `Cleared balance off by ${balances.discrepancy.value_display}`,
462
+ description: `YNAB cleared balance is ${balances.current_cleared.value_display} but the statement expects ` +
463
+ `${balances.target_statement.value_display}. Focus on closing this gap.`,
464
+ evidence: {
465
+ cleared_balance: balances.current_cleared,
466
+ statement_balance: balances.target_statement,
467
+ discrepancy: balances.discrepancy,
468
+ },
469
+ });
470
+ }
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
+ return insights;
485
+ }
486
+ function detectInsights(matches, unmatchedBank, summary, balances, config) {
487
+ const insights = [];
488
+ const seen = new Set();
489
+ const addUnique = (insight) => {
490
+ if (seen.has(insight.id))
491
+ return;
492
+ seen.add(insight.id);
493
+ insights.push(insight);
494
+ };
495
+ for (const insight of repeatAmountInsights(unmatchedBank)) {
496
+ addUnique(insight);
497
+ }
498
+ for (const insight of nearMatchInsights(matches, config)) {
499
+ addUnique(insight);
500
+ }
501
+ for (const insight of anomalyInsights(summary, balances)) {
502
+ addUnique(insight);
503
+ }
504
+ return insights.slice(0, 5);
505
+ }
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;
538
+ }
539
+ const enrichedSuggestedMatches = [...suggestedMatches, ...combinationMatches];
540
+ const balances = calculateBalances(convertedYNABTxns, statementBalance, currency);
541
+ const summary = generateSummary(bankTransactions, convertedYNABTxns, autoMatches, enrichedSuggestedMatches, unmatchedBank, unmatchedYNAB, balances);
542
+ const nextSteps = generateNextSteps(summary);
543
+ const baseInsights = detectInsights(matches, unmatchedBank, summary, balances, config);
544
+ const insights = mergeInsights(baseInsights, combinationInsights);
545
+ const analysis = {
546
+ success: true,
547
+ phase: 'analysis',
548
+ summary,
549
+ auto_matches: autoMatches,
550
+ suggested_matches: enrichedSuggestedMatches,
551
+ unmatched_bank: unmatchedBank,
552
+ unmatched_ynab: unmatchedYNAB,
553
+ balance_info: balances,
554
+ next_steps: nextSteps,
555
+ insights,
556
+ };
557
+ if (accountId && budgetId) {
558
+ const recommendations = generateRecommendations({
559
+ account_id: accountId,
560
+ budget_id: budgetId,
561
+ analysis,
562
+ matching_config: config,
563
+ });
564
+ analysis.recommendations = recommendations;
565
+ }
566
+ return analysis;
567
+ }
@@ -0,0 +1,94 @@
1
+ import type * as ynab from 'ynab';
2
+ import type { ReconciliationAnalysis } from './types.js';
3
+ import type { ReconcileAccountRequest } from './index.js';
4
+ export interface AccountSnapshot {
5
+ balance: number;
6
+ cleared_balance: number;
7
+ uncleared_balance: number;
8
+ }
9
+ export interface ExecutionOptions {
10
+ ynabAPI: ynab.API;
11
+ analysis: ReconciliationAnalysis;
12
+ params: ReconcileAccountRequest;
13
+ budgetId: string;
14
+ accountId: string;
15
+ initialAccount: AccountSnapshot;
16
+ currencyCode: string;
17
+ }
18
+ export interface ExecutionActionRecord {
19
+ type: string;
20
+ transaction: Record<string, unknown> | null;
21
+ reason: string;
22
+ bulk_chunk_index?: number;
23
+ correlation_key?: string;
24
+ duplicate?: boolean;
25
+ }
26
+ export interface ExecutionSummary {
27
+ bank_transactions_count: number;
28
+ ynab_transactions_count: number;
29
+ matches_found: number;
30
+ missing_in_ynab: number;
31
+ missing_in_bank: number;
32
+ transactions_created: number;
33
+ transactions_updated: number;
34
+ dates_adjusted: number;
35
+ dry_run: boolean;
36
+ }
37
+ export interface BulkOperationDetails {
38
+ chunks_processed: number;
39
+ bulk_successes: number;
40
+ sequential_fallbacks: number;
41
+ duplicates_detected: number;
42
+ failed_transactions: number;
43
+ bulk_chunk_failures: number;
44
+ transaction_failures: number;
45
+ sequential_attempts?: number;
46
+ }
47
+ export interface ExecutionResult {
48
+ summary: ExecutionSummary;
49
+ account_balance: {
50
+ before: AccountSnapshot;
51
+ after: AccountSnapshot;
52
+ };
53
+ actions_taken: ExecutionActionRecord[];
54
+ recommendations: string[];
55
+ balance_reconciliation?: Awaited<ReturnType<typeof buildBalanceReconciliation>>;
56
+ bulk_operation_details?: BulkOperationDetails;
57
+ }
58
+ export declare function executeReconciliation(options: ExecutionOptions): Promise<ExecutionResult>;
59
+ declare function buildBalanceReconciliation(args: {
60
+ ynabAPI: ynab.API;
61
+ budgetId: string;
62
+ accountId: string;
63
+ statementDate: string;
64
+ statementBalance: number;
65
+ analysis: ReconciliationAnalysis;
66
+ }): Promise<{
67
+ status: string;
68
+ precision_calculations: {
69
+ bank_statement_balance_milliunits: number;
70
+ ynab_calculated_balance_milliunits: number;
71
+ discrepancy_milliunits: number;
72
+ discrepancy_dollars: number;
73
+ };
74
+ discrepancy_analysis?: ReturnType<typeof buildLikelyCauses>;
75
+ final_verification: {
76
+ balance_matches_exactly: boolean;
77
+ all_transactions_accounted: boolean;
78
+ audit_trail_complete: boolean;
79
+ reconciliation_complete: boolean;
80
+ };
81
+ }>;
82
+ declare function buildLikelyCauses(discrepancyMilli: number): {
83
+ confidence_level: number;
84
+ likely_causes: {
85
+ cause_type: string;
86
+ description: string;
87
+ confidence: number;
88
+ amount_milliunits: number;
89
+ suggested_resolution: string;
90
+ evidence: unknown[];
91
+ }[];
92
+ risk_assessment: string;
93
+ } | undefined;
94
+ export type { ExecutionResult as LegacyReconciliationResult };