@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,12 +1,11 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { analyzeReconciliation } from '../analyzer.js';
3
3
  import type { Transaction as YNABAPITransaction } from 'ynab';
4
- import * as parser from '../../compareTransactions/parser.js';
4
+ import * as csvParser from '../csvParser.js';
5
5
 
6
6
  // Mock the parser module
7
- vi.mock('../../compareTransactions/parser.js', () => ({
8
- parseBankCSV: vi.fn(),
9
- readCSVFile: vi.fn(),
7
+ vi.mock('../csvParser.js', () => ({
8
+ parseCSV: vi.fn(),
10
9
  }));
11
10
 
12
11
  describe('analyzer', () => {
@@ -17,26 +16,36 @@ describe('analyzer', () => {
17
16
  describe('analyzeReconciliation', () => {
18
17
  it('should perform full analysis and return structured results', () => {
19
18
  // Mock CSV parsing
20
- vi.mocked(parser.parseBankCSV).mockReturnValue({
19
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
21
20
  transactions: [
22
21
  {
22
+ id: 'b1',
23
23
  date: '2025-10-15',
24
- amount: -45.23,
24
+ amount: -45230, // milliunits
25
25
  payee: 'Shell Gas',
26
26
  memo: '',
27
+ sourceRow: 1,
28
+ raw: { date: '10/15/2025', amount: '-45.23', description: 'Shell Gas' },
27
29
  },
28
30
  {
31
+ id: 'b2',
29
32
  date: '2025-10-16',
30
- amount: -100.0,
33
+ amount: -100000, // milliunits
31
34
  payee: 'Netflix',
32
35
  memo: '',
36
+ sourceRow: 2,
37
+ raw: { date: '10/16/2025', amount: '-100.00', description: 'Netflix' },
33
38
  },
34
39
  ],
35
- format_detected: 'standard',
36
- delimiter: ',',
37
- total_rows: 2,
38
- valid_rows: 2,
40
+ meta: {
41
+ detectedDelimiter: ',',
42
+ detectedColumns: ['Date', 'Amount', 'Description'],
43
+ totalRows: 2,
44
+ validRows: 2,
45
+ skippedRows: 0,
46
+ },
39
47
  errors: [],
48
+ warnings: [],
40
49
  });
41
50
 
42
51
  const ynabTxns: YNABAPITransaction[] = [
@@ -76,23 +85,33 @@ describe('analyzer', () => {
76
85
  expect(result.unmatched_ynab).toBeDefined();
77
86
  expect(result.balance_info).toBeDefined();
78
87
  expect(result.next_steps).toBeDefined();
88
+
89
+ // Verify auto-matches (exact matches)
90
+ expect(result.auto_matches.length).toBe(2);
79
91
  });
80
92
 
81
93
  it('should categorize high-confidence matches as auto-matches', () => {
82
- vi.mocked(parser.parseBankCSV).mockReturnValue({
94
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
83
95
  transactions: [
84
96
  {
97
+ id: 'b1',
85
98
  date: '2025-10-15',
86
- amount: -50.0,
99
+ amount: -50000,
87
100
  payee: 'Coffee Shop',
88
101
  memo: '',
102
+ sourceRow: 1,
103
+ raw: {} as any,
89
104
  },
90
105
  ],
91
- format_detected: 'standard',
92
- delimiter: ',',
93
- total_rows: 1,
94
- valid_rows: 1,
106
+ meta: {
107
+ detectedDelimiter: ',',
108
+ detectedColumns: [],
109
+ totalRows: 1,
110
+ validRows: 1,
111
+ skippedRows: 0,
112
+ },
95
113
  errors: [],
114
+ warnings: [],
96
115
  });
97
116
 
98
117
  const ynabTxns: YNABAPITransaction[] = [
@@ -114,28 +133,35 @@ describe('analyzer', () => {
114
133
  });
115
134
 
116
135
  it('should categorize medium-confidence matches as suggested', () => {
117
- vi.mocked(parser.parseBankCSV).mockReturnValue({
136
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
118
137
  transactions: [
119
138
  {
139
+ id: 'b1',
120
140
  date: '2025-10-15',
121
- amount: -50.0,
122
- payee: 'Amazon',
141
+ amount: -50000,
142
+ payee: 'Generic Store',
123
143
  memo: '',
144
+ sourceRow: 1,
145
+ raw: {} as any,
124
146
  },
125
147
  ],
126
- format_detected: 'standard',
127
- delimiter: ',',
128
- total_rows: 1,
129
- valid_rows: 1,
148
+ meta: {
149
+ detectedDelimiter: ',',
150
+ detectedColumns: [],
151
+ totalRows: 1,
152
+ validRows: 1,
153
+ skippedRows: 0,
154
+ },
130
155
  errors: [],
156
+ warnings: [],
131
157
  });
132
158
 
133
159
  const ynabTxns: YNABAPITransaction[] = [
134
160
  {
135
161
  id: 'y1',
136
- date: '2025-10-18', // 3 days difference
162
+ date: '2025-10-18', // 3 days difference - date score drops
137
163
  amount: -50000,
138
- payee_name: 'Amazon Prime',
164
+ payee_name: 'Amazon Prime', // Fuzzy match
139
165
  category_name: 'Shopping',
140
166
  cleared: 'uncleared' as const,
141
167
  approved: true,
@@ -144,25 +170,33 @@ describe('analyzer', () => {
144
170
 
145
171
  const result = analyzeReconciliation('csv', undefined, ynabTxns, -50.0);
146
172
 
147
- // Might be medium or low depending on exact scoring
148
- expect(result.suggested_matches.length + result.unmatched_bank.length).toBeGreaterThan(0);
173
+ // Should be suggested (medium)
174
+ expect(result.suggested_matches.length).toBeGreaterThan(0);
175
+ expect(result.suggested_matches[0].confidence).toBe('medium');
149
176
  });
150
177
 
151
178
  it('should identify unmatched bank transactions', () => {
152
- vi.mocked(parser.parseBankCSV).mockReturnValue({
179
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
153
180
  transactions: [
154
181
  {
182
+ id: 'b1',
155
183
  date: '2025-10-15',
156
- amount: -15.99,
184
+ amount: -15990,
157
185
  payee: 'New Store',
158
186
  memo: '',
187
+ sourceRow: 1,
188
+ raw: {} as any,
159
189
  },
160
190
  ],
161
- format_detected: 'standard',
162
- delimiter: ',',
163
- total_rows: 1,
164
- valid_rows: 1,
191
+ meta: {
192
+ detectedDelimiter: ',',
193
+ detectedColumns: [],
194
+ totalRows: 1,
195
+ validRows: 1,
196
+ skippedRows: 0,
197
+ },
165
198
  errors: [],
199
+ warnings: [],
166
200
  });
167
201
 
168
202
  const ynabTxns: YNABAPITransaction[] = [];
@@ -174,13 +208,17 @@ describe('analyzer', () => {
174
208
  });
175
209
 
176
210
  it('should identify unmatched YNAB transactions', () => {
177
- vi.mocked(parser.parseBankCSV).mockReturnValue({
211
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
178
212
  transactions: [],
179
- format_detected: 'standard',
180
- delimiter: ',',
181
- total_rows: 0,
182
- valid_rows: 0,
213
+ meta: {
214
+ detectedDelimiter: ',',
215
+ detectedColumns: [],
216
+ totalRows: 0,
217
+ validRows: 0,
218
+ skippedRows: 0,
219
+ },
183
220
  errors: [],
221
+ warnings: [],
184
222
  });
185
223
 
186
224
  const ynabTxns: YNABAPITransaction[] = [
@@ -198,77 +236,21 @@ describe('analyzer', () => {
198
236
  const result = analyzeReconciliation('csv', undefined, ynabTxns, 0);
199
237
 
200
238
  expect(result.unmatched_ynab.length).toBe(1);
201
- expect(result.unmatched_ynab[0].payee_name).toBe('Restaurant');
202
- });
203
-
204
- it('should surface combination suggestions and insights when totals align', () => {
205
- vi.mocked(parser.parseBankCSV).mockReturnValue({
206
- transactions: [
207
- {
208
- date: '2025-10-20',
209
- amount: -30.0,
210
- payee: 'Evening Out',
211
- memo: '',
212
- },
213
- ],
214
- format_detected: 'standard',
215
- delimiter: ',',
216
- total_rows: 1,
217
- valid_rows: 1,
218
- errors: [],
219
- });
220
-
221
- const ynabTxns: YNABAPITransaction[] = [
222
- {
223
- id: 'y-combo-1',
224
- date: '2025-10-19',
225
- amount: -20000,
226
- payee_name: 'Dinner',
227
- category_name: 'Dining',
228
- cleared: 'uncleared' as const,
229
- approved: true,
230
- } as YNABAPITransaction,
231
- {
232
- id: 'y-combo-2',
233
- date: '2025-10-20',
234
- amount: -10000,
235
- payee_name: 'Drinks',
236
- category_name: 'Dining',
237
- cleared: 'uncleared' as const,
238
- approved: true,
239
- } as YNABAPITransaction,
240
- {
241
- id: 'y-extra',
242
- date: '2025-10-22',
243
- amount: -5000,
244
- payee_name: 'Snacks',
245
- category_name: 'Dining',
246
- cleared: 'uncleared' as const,
247
- approved: true,
248
- } as YNABAPITransaction,
249
- ];
250
-
251
- const result = analyzeReconciliation('csv', undefined, ynabTxns, -30.0);
252
-
253
- const comboMatch = result.suggested_matches.find(
254
- (match) => match.match_reason === 'combination_match',
255
- );
256
- expect(comboMatch).toBeDefined();
257
- expect(comboMatch?.candidates?.length).toBeGreaterThanOrEqual(2);
258
-
259
- const comboInsight = result.insights.find((insight) => insight.id.startsWith('combination-'));
260
- expect(comboInsight).toBeDefined();
261
- expect(comboInsight?.severity).toBe('info');
239
+ expect(result.unmatched_ynab[0].payee).toBe('Restaurant');
262
240
  });
263
241
 
264
242
  it('should calculate balance information correctly', () => {
265
- vi.mocked(parser.parseBankCSV).mockReturnValue({
243
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
266
244
  transactions: [],
267
- format_detected: 'standard',
268
- delimiter: ',',
269
- total_rows: 0,
270
- valid_rows: 0,
245
+ meta: {
246
+ detectedDelimiter: ',',
247
+ detectedColumns: [],
248
+ totalRows: 0,
249
+ validRows: 0,
250
+ skippedRows: 0,
251
+ },
271
252
  errors: [],
253
+ warnings: [],
272
254
  });
273
255
 
274
256
  const ynabTxns: YNABAPITransaction[] = [
@@ -303,16 +285,36 @@ describe('analyzer', () => {
303
285
  });
304
286
 
305
287
  it('should generate appropriate summary', () => {
306
- vi.mocked(parser.parseBankCSV).mockReturnValue({
288
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
307
289
  transactions: [
308
- { date: '2025-10-15', amount: -50.0, payee: 'Store', memo: '' },
309
- { date: '2025-10-20', amount: -30.0, payee: 'Restaurant', memo: '' },
290
+ {
291
+ id: 'b1',
292
+ date: '2025-10-15',
293
+ amount: -50000,
294
+ payee: 'Store',
295
+ memo: '',
296
+ sourceRow: 1,
297
+ raw: {} as any,
298
+ },
299
+ {
300
+ id: 'b2',
301
+ date: '2025-10-20',
302
+ amount: -30000,
303
+ payee: 'Restaurant',
304
+ memo: '',
305
+ sourceRow: 2,
306
+ raw: {} as any,
307
+ },
310
308
  ],
311
- format_detected: 'standard',
312
- delimiter: ',',
313
- total_rows: 2,
314
- valid_rows: 2,
309
+ meta: {
310
+ detectedDelimiter: ',',
311
+ detectedColumns: [],
312
+ totalRows: 2,
313
+ validRows: 2,
314
+ skippedRows: 0,
315
+ },
315
316
  errors: [],
317
+ warnings: [],
316
318
  });
317
319
 
318
320
  const ynabTxns: YNABAPITransaction[] = [
@@ -334,73 +336,5 @@ describe('analyzer', () => {
334
336
  expect(result.summary.statement_date_range).toContain('2025-10-15');
335
337
  expect(result.summary.statement_date_range).toContain('2025-10-20');
336
338
  });
337
-
338
- it('should generate next steps based on analysis', () => {
339
- vi.mocked(parser.parseBankCSV).mockReturnValue({
340
- transactions: [{ date: '2025-10-15', amount: -50.0, payee: 'Store', memo: '' }],
341
- format_detected: 'standard',
342
- delimiter: ',',
343
- total_rows: 1,
344
- valid_rows: 1,
345
- errors: [],
346
- });
347
-
348
- const ynabTxns: YNABAPITransaction[] = [
349
- {
350
- id: 'y1',
351
- date: '2025-10-15',
352
- amount: -50000,
353
- payee_name: 'Store',
354
- category_name: 'Shopping',
355
- cleared: 'uncleared' as const,
356
- approved: true,
357
- } as YNABAPITransaction,
358
- ];
359
-
360
- const result = analyzeReconciliation('csv', undefined, ynabTxns, -50.0);
361
-
362
- expect(result.next_steps).toBeDefined();
363
- expect(Array.isArray(result.next_steps)).toBe(true);
364
- expect(result.next_steps.length).toBeGreaterThan(0);
365
- });
366
-
367
- it('should use file path when provided', () => {
368
- vi.mocked(parser.readCSVFile).mockReturnValue({
369
- transactions: [{ date: '2025-10-15', amount: -50.0, payee: 'Store', memo: '' }],
370
- format_detected: 'standard',
371
- delimiter: ',',
372
- total_rows: 1,
373
- valid_rows: 1,
374
- errors: [],
375
- });
376
-
377
- const ynabTxns: YNABAPITransaction[] = [];
378
-
379
- const result = analyzeReconciliation('', '/path/to/file.csv', ynabTxns, 0);
380
-
381
- expect(vi.mocked(parser.readCSVFile)).toHaveBeenCalledWith('/path/to/file.csv');
382
- expect(result.success).toBe(true);
383
- });
384
-
385
- it('should assign unique IDs to bank transactions', () => {
386
- vi.mocked(parser.parseBankCSV).mockReturnValue({
387
- transactions: [
388
- { date: '2025-10-15', amount: -50.0, payee: 'Store1', memo: '' },
389
- { date: '2025-10-16', amount: -30.0, payee: 'Store2', memo: '' },
390
- ],
391
- format_detected: 'standard',
392
- delimiter: ',',
393
- total_rows: 2,
394
- valid_rows: 2,
395
- errors: [],
396
- });
397
-
398
- const result = analyzeReconciliation('csv', undefined, [], 0);
399
-
400
- expect(result.unmatched_bank.length).toBe(2);
401
- expect(result.unmatched_bank[0].id).toBeDefined();
402
- expect(result.unmatched_bank[1].id).toBeDefined();
403
- expect(result.unmatched_bank[0].id).not.toBe(result.unmatched_bank[1].id);
404
- });
405
339
  });
406
340
  });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseCSV, ParseCSVOptions } from '../csvParser.js';
3
+
4
+ describe('csvParser', () => {
5
+ describe('Security and Limits', () => {
6
+ it('should throw error if file size exceeds maxBytes', () => {
7
+ const largeContent = 'a'.repeat(1024 * 1024 + 1); // 1MB + 1 byte
8
+ const options: ParseCSVOptions = { maxBytes: 1024 * 1024 }; // 1MB limit
9
+
10
+ expect(() => parseCSV(largeContent, options)).toThrow(/File size exceeds limit/);
11
+ });
12
+
13
+ it('should respect maxRows limit', () => {
14
+ const rows = Array.from(
15
+ { length: 20 },
16
+ (_, i) => `2024-01-${String(i + 1).padStart(2, '0')},Desc ${i},10.00`,
17
+ ).join('\n');
18
+ const content = `Date,Description,Amount\n${rows}`;
19
+ const options: ParseCSVOptions = { maxRows: 10 };
20
+
21
+ const result = parseCSV(content, options);
22
+ expect(result.transactions).toHaveLength(10);
23
+ expect(result.meta.validRows).toBe(10);
24
+ });
25
+
26
+ it('should sanitize control characters from description', () => {
27
+ // ASCII 0x07 (Bell) and 0x1B (Escape) are control characters
28
+ const badDesc = 'Bad\x07Description\x1B';
29
+ const content = `Date,Description,Amount\n2024-01-01,${badDesc},10.00`;
30
+
31
+ const result = parseCSV(content);
32
+ expect(result.transactions[0].payee).toBe('BadDescription');
33
+ });
34
+
35
+ it('should limit description length to 500 characters', () => {
36
+ const longDesc = 'a'.repeat(600);
37
+ const content = `Date,Description,Amount\n2024-01-01,${longDesc},10.00`;
38
+
39
+ const result = parseCSV(content);
40
+ expect(result.transactions[0].payee).toHaveLength(500);
41
+ expect(result.transactions[0].payee).toBe('a'.repeat(500));
42
+ });
43
+ });
44
+
45
+ describe('Timezone Handling', () => {
46
+ it('should parse ISO dates correctly without timezone shift', () => {
47
+ // If parsed as local time in UTC-5 (EST), 2024-01-01 00:00:00 becomes 2023-12-31 19:00:00 UTC
48
+ // But we want 2024-01-01 regardless of local timezone
49
+ const content = `Date,Description,Amount\n2024-01-01,Test,10.00`;
50
+ const result = parseCSV(content);
51
+ expect(result.transactions[0].date).toBe('2024-01-01');
52
+ });
53
+
54
+ it('should parse MDY dates correctly without timezone shift', () => {
55
+ const content = `Date,Description,Amount\n01/01/2024,Test,10.00`;
56
+ const result = parseCSV(content, { dateFormat: 'MDY' });
57
+ expect(result.transactions[0].date).toBe('2024-01-01');
58
+ });
59
+
60
+ it('should parse DMY dates correctly without timezone shift', () => {
61
+ const content = `Date,Description,Amount\n01/01/2024,Test,10.00`;
62
+ const result = parseCSV(content, { dateFormat: 'DMY' });
63
+ expect(result.transactions[0].date).toBe('2024-01-01');
64
+ });
65
+ });
66
+
67
+ describe('Bank Presets', () => {
68
+ it('should detect TD headerless format correctly', () => {
69
+ // TD Pattern: Date, Description, Debit, Credit, Balance
70
+ const content = `
71
+ 01/15/2024,PAYMENT RECEIVED,,100.00,500.00
72
+ 01/16/2024,PURCHASE,50.00,,450.00
73
+ `.trim();
74
+
75
+ // Note: TD pattern detection requires 4+ columns
76
+ // Our parser might need a hint if the csv is very short or malformed
77
+ // But the auto-detect logic checks for date in col 0 and numerics in 2/3
78
+
79
+ const result = parseCSV(content);
80
+ // Should detect TD preset which implies header: false
81
+ expect(result.meta.detectedColumns).toBeDefined();
82
+ expect(result.transactions).toHaveLength(2);
83
+ expect(result.transactions[0].amount).toBe(100000); // Credit is inflow (positive)
84
+ expect(result.transactions[1].amount).toBe(-50000); // Debit is outflow (negative)
85
+ });
86
+ });
87
+ });
@@ -40,7 +40,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
40
40
  await ynabAPI.transactions.deleteTransaction(budgetId, transactionId);
41
41
  });
42
42
  }
43
- });
43
+ }, 60000); // 60 second timeout for cleanup of bulk transactions
44
44
 
45
45
  it(
46
46
  'creates 10 transactions via bulk mode',
@@ -67,6 +67,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
67
67
  this,
68
68
  );
69
69
  if (!result) return;
70
+ if (containsRateLimitFailure(result)) return;
70
71
 
71
72
  trackCreatedTransactions(result);
72
73
  expect(result.summary.transactions_created).toBe(10);
@@ -117,6 +118,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
117
118
  this,
118
119
  );
119
120
  if (!duplicateAttempt) return;
121
+ if (containsRateLimitFailure(duplicateAttempt)) return;
120
122
 
121
123
  const duplicateActions = duplicateAttempt.actions_taken.filter(
122
124
  (action) => action.duplicate === true,
@@ -153,6 +155,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
153
155
  this,
154
156
  );
155
157
  if (!result) return;
158
+ if (containsRateLimitFailure(result)) return;
156
159
  trackCreatedTransactions(result);
157
160
 
158
161
  expect(result.summary.transactions_created).toBe(150);
@@ -187,6 +190,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
187
190
  this,
188
191
  );
189
192
  if (!result) return;
193
+ if (containsRateLimitFailure(result)) return;
190
194
  trackCreatedTransactions(result);
191
195
 
192
196
  const duration = Date.now() - start;
@@ -218,7 +222,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
218
222
  initialAccount: accountSnapshot,
219
223
  currencyCode: 'USD',
220
224
  }),
221
- ).rejects.toThrow();
225
+ ).rejects.toMatchObject({ status: expect.any(Number) });
222
226
  }, this);
223
227
  },
224
228
  60000,
@@ -249,6 +253,17 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
249
253
  }
250
254
  }
251
255
  }
256
+
257
+ function containsRateLimitFailure(result: Awaited<ReturnType<typeof executeReconciliation>>) {
258
+ return result.actions_taken.some((action) => {
259
+ const reason = typeof action.reason === 'string' ? action.reason.toLowerCase() : '';
260
+ return (
261
+ reason.includes('429') ||
262
+ reason.includes('too many requests') ||
263
+ reason.includes('rate limit')
264
+ );
265
+ });
266
+ }
252
267
  });
253
268
 
254
269
  async function resolveDefaultBudgetId(api: ynab.API): Promise<string> {
@@ -291,7 +306,12 @@ function buildIntegrationAnalysis(
291
306
  const clearedDollars = snapshot.cleared_balance / 1000;
292
307
  const totalDelta = transactionAmount * count;
293
308
  const statementBalance = clearedDollars + totalDelta;
294
- const baseDate = Date.parse('2025-12-01');
309
+
310
+ // Choose a base date safely in the past so YNAB accepts the transactions (no future dates),
311
+ // and include a nonce in payee names to avoid duplicate collisions across test runs.
312
+ const dayMs = 24 * 60 * 60 * 1000;
313
+ const baseDate = Date.now() - (count + 1) * dayMs;
314
+ const runNonce = Date.now().toString();
295
315
 
296
316
  return {
297
317
  success: true,
@@ -312,12 +332,12 @@ function buildIntegrationAnalysis(
312
332
  auto_matches: [],
313
333
  suggested_matches: [],
314
334
  unmatched_bank: Array.from({ length: count }, (_, index) => {
315
- const date = new Date(baseDate + index * 24 * 60 * 60 * 1000);
335
+ const date = new Date(baseDate + index * dayMs);
316
336
  return {
317
- id: `integration-bank-${index}`,
337
+ id: `integration-bank-${index}-${runNonce}`,
318
338
  date: date.toISOString().slice(0, 10),
319
339
  amount: transactionAmount,
320
- payee: `Integration Payee ${index}`,
340
+ payee: `Integration Payee ${index}-${runNonce}`,
321
341
  memo: `Integration memo ${index}`,
322
342
  original_csv_row: index + 1,
323
343
  };