@resolveio/server-lib 22.3.219 → 22.3.221

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 (745) hide show
  1. package/.nodemon.json +5 -0
  2. package/.vscode/settings.json +21 -0
  3. package/AGENTS.md +195 -0
  4. package/README.md +22 -0
  5. package/build_package.sh +5 -0
  6. package/compileDTS.pl +64 -0
  7. package/docs/ai-assistant-nightly-eval.md +65 -0
  8. package/docs/ai-assistant-preflight-checklist.md +23 -0
  9. package/docs/ai-assistant-report-builder-bridge-playbook.md +115 -0
  10. package/eslint-plugin-custom/index.js +7 -0
  11. package/eslint-plugin-custom/rules/no-filter-zero-index.js +44 -0
  12. package/eslint.config.js +103 -0
  13. package/gulpfile.js +216 -0
  14. package/methodAndPublicationListGenerator.py +375 -0
  15. package/mongodbensurers.js +2 -0
  16. package/mongostop.js +3 -0
  17. package/package.json +1 -1
  18. package/scripts/cleanup-bypassed-callmethod-logs.js +616 -0
  19. package/settings.development.json +25 -0
  20. package/settings.development.redacted.json +25 -0
  21. package/src/.env +12 -0
  22. package/src/ai/assistant-core-heuristics.ts +379 -0
  23. package/src/ai/resolveio-platform-intelligence-memory-corpus.ts +185 -0
  24. package/src/ai/resolveio-platform-intelligence-memory.ts +325 -0
  25. package/{ai/resolveio-platform-intelligence-types.d.ts → src/ai/resolveio-platform-intelligence-types.ts} +20 -15
  26. package/src/ai/resolveio-platform-intelligence.ts +462 -0
  27. package/src/client-server-app.ts +12 -0
  28. package/src/collections/ai-run.collection.ts +117 -0
  29. package/src/collections/ai-terminal-conversation.collection.ts +91 -0
  30. package/src/collections/ai-terminal-issue-report.collection.ts +99 -0
  31. package/src/collections/ai-terminal-message.collection.ts +77 -0
  32. package/src/collections/app-setting.collection.ts +104 -0
  33. package/src/collections/app-status.collection.ts +58 -0
  34. package/src/collections/communication-metric.collection.ts +84 -0
  35. package/src/collections/counter.collection.ts +56 -0
  36. package/src/collections/cron-job-history.collection.ts +94 -0
  37. package/src/collections/cron-job.collection.ts +92 -0
  38. package/src/collections/customer-notification.collection.ts +131 -0
  39. package/src/collections/customer-portal-password.collection.ts +76 -0
  40. package/src/collections/email-history.collection.ts +134 -0
  41. package/src/collections/email-verified.collection.ts +62 -0
  42. package/src/collections/file.collection.ts +74 -0
  43. package/src/collections/flag-update.collection.ts +57 -0
  44. package/src/collections/flag.collection.ts +57 -0
  45. package/src/collections/log-method-latency.collection.ts +77 -0
  46. package/src/collections/log-subscription.collection.ts +80 -0
  47. package/src/collections/log.collection.ts +93 -0
  48. package/src/collections/logged-in-users.collection.ts +67 -0
  49. package/src/collections/monitor-cpu.collection.ts +65 -0
  50. package/src/collections/monitor-function.collection.ts +74 -0
  51. package/src/collections/monitor-memory.collection.ts +77 -0
  52. package/src/collections/monitor-mongo.collection.ts +71 -0
  53. package/src/collections/notification.collection.ts +57 -0
  54. package/src/collections/openai-usage-ledger.collection.ts +131 -0
  55. package/src/collections/report-builder-dashboard-builder.collection.ts +109 -0
  56. package/src/collections/report-builder-library.collection.ts +89 -0
  57. package/src/collections/report-builder-report.collection.ts +184 -0
  58. package/src/collections/user-group.collection.ts +89 -0
  59. package/src/collections/user-guide.collection.ts +57 -0
  60. package/src/collections/user.collection.ts +181 -0
  61. package/src/cron/cron.ts +117 -0
  62. package/src/fixtures/cron-jobs.ts +95 -0
  63. package/src/fixtures/init.ts +35 -0
  64. package/src/http/auth.ts +818 -0
  65. package/src/http/health.ts +7 -0
  66. package/src/http/home.ts +90 -0
  67. package/src/http/slow-query-publication.ts +49 -0
  68. package/src/index.ts +1 -0
  69. package/src/managers/ai-assistant-codex-manager.manager.ts +1131 -0
  70. package/src/managers/ai-run-evidence.manager.ts +264 -0
  71. package/src/managers/communication-metric.manager.ts +82 -0
  72. package/src/managers/cron.manager.ts +333 -0
  73. package/src/managers/customer-notification-content.manager.ts +236 -0
  74. package/src/managers/diagnostic-manager-bootstrap.ts +165 -0
  75. package/src/managers/error-auto-fix.manager.ts +2767 -0
  76. package/src/managers/local-log.manager.ts +113 -0
  77. package/src/managers/method.manager.ts +1857 -0
  78. package/src/managers/mongo.manager.ts +4575 -0
  79. package/src/managers/monitor.manager.ts +507 -0
  80. package/src/managers/openai-usage-ledger.manager.ts +112 -0
  81. package/src/managers/slow-query-verifier.manager.ts +3590 -0
  82. package/src/managers/slow-query.manager.ts +519 -0
  83. package/src/managers/subscription.manager.ts +3128 -0
  84. package/src/managers/websocket.manager.ts +746 -0
  85. package/src/managers/worker-dispatcher.manager.ts +1360 -0
  86. package/src/managers/worker-server.manager.ts +536 -0
  87. package/src/methods/accounts.ts +532 -0
  88. package/src/methods/ai-terminal.ts +29070 -0
  89. package/src/methods/app-settings.ts +114 -0
  90. package/src/methods/aws.ts +649 -0
  91. package/src/methods/collections.ts +641 -0
  92. package/src/methods/counters.ts +69 -0
  93. package/src/methods/cron-jobs.ts +2614 -0
  94. package/src/methods/customer-notifications.ts +458 -0
  95. package/src/methods/diagnostics.ts +616 -0
  96. package/src/methods/flag-updates.ts +7 -0
  97. package/src/methods/flags.ts +7 -0
  98. package/src/methods/logs.ts +657 -0
  99. package/src/methods/mongo-explorer.ts +1880 -0
  100. package/src/methods/monitor.ts +540 -0
  101. package/src/methods/pdf.ts +1236 -0
  102. package/src/methods/publications.ts +129 -0
  103. package/src/methods/report-builder.ts +3300 -0
  104. package/src/methods/support.ts +335 -0
  105. package/src/models/ai-run.model.ts +27 -0
  106. package/src/models/ai-terminal-conversation.model.ts +19 -0
  107. package/src/models/ai-terminal-issue-report.model.ts +21 -0
  108. package/src/models/ai-terminal-message.model.ts +24 -0
  109. package/src/models/app-setting.model.ts +17 -0
  110. package/{models/app-status.model.d.ts → src/models/app-status.model.ts} +3 -2
  111. package/{models/billing-logged-in-users.model.d.ts → src/models/billing-logged-in-users.model.ts} +5 -4
  112. package/src/models/collection-document.model.ts +24 -0
  113. package/src/models/communication-metric.model.ts +23 -0
  114. package/{models/counter.model.d.ts → src/models/counter.model.ts} +4 -3
  115. package/src/models/cron-job-history.model.ts +16 -0
  116. package/src/models/cron-job.model.ts +15 -0
  117. package/src/models/customer-notification.model.ts +28 -0
  118. package/src/models/customer-portal-password.model.ts +12 -0
  119. package/src/models/dialog.model.ts +25 -0
  120. package/{models/email-history.model.js → src/models/email-history.model.ts} +36 -4
  121. package/{models/email-verified.model.d.ts → src/models/email-verified.model.ts} +6 -5
  122. package/{models/file.model.d.ts → src/models/file.model.ts} +8 -7
  123. package/{models/flag-update.model.d.ts → src/models/flag-update.model.ts} +4 -3
  124. package/{models/flag.model.d.ts → src/models/flag.model.ts} +4 -3
  125. package/src/models/log-method-latency.model.ts +11 -0
  126. package/{models/log-subscription.model.d.ts → src/models/log-subscription.model.ts} +11 -9
  127. package/src/models/log.model.ts +19 -0
  128. package/{models/logged-in-users.model.d.ts → src/models/logged-in-users.model.ts} +6 -5
  129. package/{models/method-response.model.d.ts → src/models/method-response.model.ts} +7 -6
  130. package/src/models/method.model.ts +25 -0
  131. package/{models/monitor-cpu.model.d.ts → src/models/monitor-cpu.model.ts} +9 -7
  132. package/src/models/monitor-function.model.ts +16 -0
  133. package/src/models/monitor-memory.model.ts +17 -0
  134. package/src/models/monitor-mongo.model.ts +15 -0
  135. package/{models/notification.model.d.ts → src/models/notification.model.ts} +6 -4
  136. package/src/models/openai-usage-ledger.model.ts +56 -0
  137. package/src/models/pagination.model.ts +35 -0
  138. package/src/models/permission.model.ts +14 -0
  139. package/src/models/report-builder-dashboard-builder.model.ts +29 -0
  140. package/src/models/report-builder-library.model.ts +20 -0
  141. package/src/models/report-builder-report.model.ts +136 -0
  142. package/src/models/report-builder.model.ts +68 -0
  143. package/src/models/select-data-label.model.ts +9 -0
  144. package/src/models/server-message.model.ts +31 -0
  145. package/src/models/slow-query-report.model.ts +23 -0
  146. package/src/models/subscription.model.ts +73 -0
  147. package/src/models/support-ticket.model.ts +104 -0
  148. package/src/models/user-group.model.ts +24 -0
  149. package/{models/user-guide.model.d.ts → src/models/user-guide.model.ts} +5 -4
  150. package/src/models/user.model.ts +96 -0
  151. package/src/private/images/ResolveIO.png +0 -0
  152. package/src/publications/ai-terminal.ts +73 -0
  153. package/src/publications/app-settings.ts +25 -0
  154. package/src/publications/app-status.ts +13 -0
  155. package/src/publications/cron-jobs.ts +40 -0
  156. package/src/publications/customer-notifications.ts +101 -0
  157. package/src/publications/files.ts +33 -0
  158. package/src/publications/flags-update.ts +19 -0
  159. package/src/publications/flags.ts +19 -0
  160. package/src/publications/logs.ts +163 -0
  161. package/src/publications/notifications.ts +13 -0
  162. package/src/publications/report-builder-dashboard-builders.ts +39 -0
  163. package/src/publications/report-builder-libraries.ts +41 -0
  164. package/src/publications/report-builder-reports.ts +47 -0
  165. package/src/publications/super-admin.ts +13 -0
  166. package/src/publications/user-groups.ts +12 -0
  167. package/src/publications/user-guides.ts +12 -0
  168. package/src/resolveio-server-app.ts +617 -0
  169. package/src/server-app.ts +3354 -0
  170. package/src/services/codex-client.ts +1231 -0
  171. package/src/services/openai-client.ts +265 -0
  172. package/src/types/error-report.ts +26 -0
  173. package/src/types/js-tiktoken.d.ts +11 -0
  174. package/src/types/slow-query-report.ts +28 -0
  175. package/src/util/ai-qa-policy.ts +925 -0
  176. package/src/util/ai-run-evidence-adapters.ts +8347 -0
  177. package/src/util/ai-run-evidence-dashboard.ts +323 -0
  178. package/src/util/ai-run-evidence-eval.ts +1057 -0
  179. package/src/util/ai-run-evidence.ts +1430 -0
  180. package/src/util/ai-runner-artifacts.ts +586 -0
  181. package/src/util/ai-runner-manager-autopilot.ts +961 -0
  182. package/src/util/ai-runner-manager-policy.ts +5011 -0
  183. package/src/util/ai-runner-qa-auth.ts +838 -0
  184. package/src/util/ai-runner-qa-tools.ts +3536 -0
  185. package/src/util/aicoder-runner-v6.ts +3121 -0
  186. package/src/util/common.ts +649 -0
  187. package/src/util/customer-portal-password.ts +183 -0
  188. package/src/util/error-reporter.ts +332 -0
  189. package/src/util/error-tracking.ts +79 -0
  190. package/src/util/openai-usage-cost.ts +114 -0
  191. package/src/util/report-builder-unwinds.ts +180 -0
  192. package/src/util/runner-process-janitor.ts +219 -0
  193. package/src/util/schema-report-builder.ts +448 -0
  194. package/src/util/slow-query-reporter.ts +216 -0
  195. package/src/util/subscription-dependency-context.ts +1096 -0
  196. package/src/util/support-runner-v5.ts +10040 -0
  197. package/src/util/tokenizer.ts +38 -0
  198. package/src/workers/codex-runner.worker.ts +142 -0
  199. package/start_server.sh +5 -0
  200. package/tests/ai-assistant-corpus-build.ts +484 -0
  201. package/tests/ai-assistant-corpus-replay-e2e.ts +774 -0
  202. package/tests/ai-assistant-data-parity-e2e.ts +1989 -0
  203. package/tests/ai-assistant-eval-triage.ts +831 -0
  204. package/tests/ai-assistant-openai-e2e.ts +1061 -0
  205. package/tests/ai-assistant-openai-git-e2e.ts +155 -0
  206. package/tests/ai-assistant-preflight-matrix.ts +215 -0
  207. package/tests/ai-assistant-routing-eval.test.ts +585 -0
  208. package/tests/ai-assistant-snf-live-eval.ts +975 -0
  209. package/tests/ai-assistant-utils.test.ts +4834 -0
  210. package/tests/ai-manager-autopilot-snapshot.test.ts +193 -0
  211. package/tests/ai-manager-recovery-checkpoint.test.ts +1383 -0
  212. package/tests/ai-run-eval.test.ts +132 -0
  213. package/tests/ai-run-evidence.test.ts +3773 -0
  214. package/tests/ai-runner-contract.test.ts +515 -0
  215. package/tests/aicoder-runner-v6.test.ts +822 -0
  216. package/tests/error-reporter.test.ts +145 -0
  217. package/tests/method-publication-generator.test.ts +46 -0
  218. package/tests/report-builder-linking.test.ts +79 -0
  219. package/tests/resolveio-platform-intelligence.test.ts +352 -0
  220. package/tests/server-app-cron-owner.test.ts +127 -0
  221. package/tests/subscription-connect-race.test.ts +158 -0
  222. package/tests/subscription-dependency-context.test.ts +324 -0
  223. package/tests/subscription-manager-collection-tracking.test.ts +86 -0
  224. package/tests/subscription-manager-invalidation.test.ts +86 -0
  225. package/tests/support-runner-v5.test.ts +3201 -0
  226. package/tsconfig.json +34 -0
  227. package/ai/assistant-core-heuristics.d.ts +0 -11
  228. package/ai/assistant-core-heuristics.js +0 -356
  229. package/ai/assistant-core-heuristics.js.map +0 -1
  230. package/ai/resolveio-platform-intelligence-memory-corpus.d.ts +0 -3
  231. package/ai/resolveio-platform-intelligence-memory-corpus.js +0 -214
  232. package/ai/resolveio-platform-intelligence-memory-corpus.js.map +0 -1
  233. package/ai/resolveio-platform-intelligence-memory.d.ts +0 -20
  234. package/ai/resolveio-platform-intelligence-memory.js +0 -341
  235. package/ai/resolveio-platform-intelligence-memory.js.map +0 -1
  236. package/ai/resolveio-platform-intelligence-types.js +0 -4
  237. package/ai/resolveio-platform-intelligence-types.js.map +0 -1
  238. package/ai/resolveio-platform-intelligence.d.ts +0 -6
  239. package/ai/resolveio-platform-intelligence.js +0 -463
  240. package/ai/resolveio-platform-intelligence.js.map +0 -1
  241. package/client-server-app.d.ts +0 -1
  242. package/client-server-app.js +0 -68
  243. package/client-server-app.js.map +0 -1
  244. package/collections/ai-run.collection.d.ts +0 -3
  245. package/collections/ai-run.collection.js +0 -170
  246. package/collections/ai-run.collection.js.map +0 -1
  247. package/collections/ai-terminal-conversation.collection.d.ts +0 -2
  248. package/collections/ai-terminal-conversation.collection.js +0 -140
  249. package/collections/ai-terminal-conversation.collection.js.map +0 -1
  250. package/collections/ai-terminal-issue-report.collection.d.ts +0 -2
  251. package/collections/ai-terminal-issue-report.collection.js +0 -148
  252. package/collections/ai-terminal-issue-report.collection.js.map +0 -1
  253. package/collections/ai-terminal-message.collection.d.ts +0 -2
  254. package/collections/ai-terminal-message.collection.js +0 -121
  255. package/collections/ai-terminal-message.collection.js.map +0 -1
  256. package/collections/app-setting.collection.d.ts +0 -3
  257. package/collections/app-setting.collection.js +0 -103
  258. package/collections/app-setting.collection.js.map +0 -1
  259. package/collections/app-status.collection.d.ts +0 -3
  260. package/collections/app-status.collection.js +0 -57
  261. package/collections/app-status.collection.js.map +0 -1
  262. package/collections/communication-metric.collection.d.ts +0 -2
  263. package/collections/communication-metric.collection.js +0 -133
  264. package/collections/communication-metric.collection.js.map +0 -1
  265. package/collections/counter.collection.d.ts +0 -3
  266. package/collections/counter.collection.js +0 -56
  267. package/collections/counter.collection.js.map +0 -1
  268. package/collections/cron-job-history.collection.d.ts +0 -3
  269. package/collections/cron-job-history.collection.js +0 -137
  270. package/collections/cron-job-history.collection.js.map +0 -1
  271. package/collections/cron-job.collection.d.ts +0 -3
  272. package/collections/cron-job.collection.js +0 -92
  273. package/collections/cron-job.collection.js.map +0 -1
  274. package/collections/customer-notification.collection.d.ts +0 -3
  275. package/collections/customer-notification.collection.js +0 -130
  276. package/collections/customer-notification.collection.js.map +0 -1
  277. package/collections/customer-portal-password.collection.d.ts +0 -3
  278. package/collections/customer-portal-password.collection.js +0 -75
  279. package/collections/customer-portal-password.collection.js.map +0 -1
  280. package/collections/email-history.collection.d.ts +0 -3
  281. package/collections/email-history.collection.js +0 -134
  282. package/collections/email-history.collection.js.map +0 -1
  283. package/collections/email-verified.collection.d.ts +0 -3
  284. package/collections/email-verified.collection.js +0 -62
  285. package/collections/email-verified.collection.js.map +0 -1
  286. package/collections/file.collection.d.ts +0 -3
  287. package/collections/file.collection.js +0 -74
  288. package/collections/file.collection.js.map +0 -1
  289. package/collections/flag-update.collection.d.ts +0 -3
  290. package/collections/flag-update.collection.js +0 -57
  291. package/collections/flag-update.collection.js.map +0 -1
  292. package/collections/flag.collection.d.ts +0 -3
  293. package/collections/flag.collection.js +0 -57
  294. package/collections/flag.collection.js.map +0 -1
  295. package/collections/log-method-latency.collection.d.ts +0 -3
  296. package/collections/log-method-latency.collection.js +0 -77
  297. package/collections/log-method-latency.collection.js.map +0 -1
  298. package/collections/log-subscription.collection.d.ts +0 -3
  299. package/collections/log-subscription.collection.js +0 -80
  300. package/collections/log-subscription.collection.js.map +0 -1
  301. package/collections/log.collection.d.ts +0 -3
  302. package/collections/log.collection.js +0 -93
  303. package/collections/log.collection.js.map +0 -1
  304. package/collections/logged-in-users.collection.d.ts +0 -3
  305. package/collections/logged-in-users.collection.js +0 -67
  306. package/collections/logged-in-users.collection.js.map +0 -1
  307. package/collections/monitor-cpu.collection.d.ts +0 -3
  308. package/collections/monitor-cpu.collection.js +0 -65
  309. package/collections/monitor-cpu.collection.js.map +0 -1
  310. package/collections/monitor-function.collection.d.ts +0 -3
  311. package/collections/monitor-function.collection.js +0 -74
  312. package/collections/monitor-function.collection.js.map +0 -1
  313. package/collections/monitor-memory.collection.d.ts +0 -3
  314. package/collections/monitor-memory.collection.js +0 -77
  315. package/collections/monitor-memory.collection.js.map +0 -1
  316. package/collections/monitor-mongo.collection.d.ts +0 -3
  317. package/collections/monitor-mongo.collection.js +0 -71
  318. package/collections/monitor-mongo.collection.js.map +0 -1
  319. package/collections/notification.collection.d.ts +0 -3
  320. package/collections/notification.collection.js +0 -57
  321. package/collections/notification.collection.js.map +0 -1
  322. package/collections/openai-usage-ledger.collection.d.ts +0 -2
  323. package/collections/openai-usage-ledger.collection.js +0 -188
  324. package/collections/openai-usage-ledger.collection.js.map +0 -1
  325. package/collections/report-builder-dashboard-builder.collection.d.ts +0 -3
  326. package/collections/report-builder-dashboard-builder.collection.js +0 -109
  327. package/collections/report-builder-dashboard-builder.collection.js.map +0 -1
  328. package/collections/report-builder-library.collection.d.ts +0 -3
  329. package/collections/report-builder-library.collection.js +0 -87
  330. package/collections/report-builder-library.collection.js.map +0 -1
  331. package/collections/report-builder-report.collection.d.ts +0 -4
  332. package/collections/report-builder-report.collection.js +0 -184
  333. package/collections/report-builder-report.collection.js.map +0 -1
  334. package/collections/user-group.collection.d.ts +0 -4
  335. package/collections/user-group.collection.js +0 -89
  336. package/collections/user-group.collection.js.map +0 -1
  337. package/collections/user-guide.collection.d.ts +0 -3
  338. package/collections/user-guide.collection.js +0 -57
  339. package/collections/user-guide.collection.js.map +0 -1
  340. package/collections/user.collection.d.ts +0 -4
  341. package/collections/user.collection.js +0 -180
  342. package/collections/user.collection.js.map +0 -1
  343. package/cron/cron.d.ts +0 -14
  344. package/cron/cron.js +0 -216
  345. package/cron/cron.js.map +0 -1
  346. package/fixtures/cron-jobs.d.ts +0 -1
  347. package/fixtures/cron-jobs.js +0 -150
  348. package/fixtures/cron-jobs.js.map +0 -1
  349. package/fixtures/init.d.ts +0 -1
  350. package/fixtures/init.js +0 -91
  351. package/fixtures/init.js.map +0 -1
  352. package/http/auth.d.ts +0 -2
  353. package/http/auth.js +0 -951
  354. package/http/auth.js.map +0 -1
  355. package/http/health.d.ts +0 -1
  356. package/http/health.js +0 -11
  357. package/http/health.js.map +0 -1
  358. package/http/home.d.ts +0 -1
  359. package/http/home.js +0 -134
  360. package/http/home.js.map +0 -1
  361. package/http/slow-query-publication.d.ts +0 -2
  362. package/http/slow-query-publication.js +0 -99
  363. package/http/slow-query-publication.js.map +0 -1
  364. package/index.d.ts +0 -1
  365. package/index.js +0 -19
  366. package/index.js.map +0 -1
  367. package/managers/ai-assistant-codex-manager.manager.d.ts +0 -67
  368. package/managers/ai-assistant-codex-manager.manager.js +0 -1113
  369. package/managers/ai-assistant-codex-manager.manager.js.map +0 -1
  370. package/managers/ai-run-evidence.manager.d.ts +0 -36
  371. package/managers/ai-run-evidence.manager.js +0 -377
  372. package/managers/ai-run-evidence.manager.js.map +0 -1
  373. package/managers/communication-metric.manager.d.ts +0 -16
  374. package/managers/communication-metric.manager.js +0 -134
  375. package/managers/communication-metric.manager.js.map +0 -1
  376. package/managers/cron.manager.d.ts +0 -20
  377. package/managers/cron.manager.js +0 -534
  378. package/managers/cron.manager.js.map +0 -1
  379. package/managers/customer-notification-content.manager.d.ts +0 -55
  380. package/managers/customer-notification-content.manager.js +0 -158
  381. package/managers/customer-notification-content.manager.js.map +0 -1
  382. package/managers/diagnostic-manager-bootstrap.d.ts +0 -9
  383. package/managers/diagnostic-manager-bootstrap.js +0 -260
  384. package/managers/diagnostic-manager-bootstrap.js.map +0 -1
  385. package/managers/error-auto-fix.manager.d.ts +0 -149
  386. package/managers/error-auto-fix.manager.js +0 -3064
  387. package/managers/error-auto-fix.manager.js.map +0 -1
  388. package/managers/local-log.manager.d.ts +0 -18
  389. package/managers/local-log.manager.js +0 -88
  390. package/managers/local-log.manager.js.map +0 -1
  391. package/managers/method.manager.d.ts +0 -84
  392. package/managers/method.manager.js +0 -1964
  393. package/managers/method.manager.js.map +0 -1
  394. package/managers/mongo.manager.d.ts +0 -224
  395. package/managers/mongo.manager.js +0 -5000
  396. package/managers/mongo.manager.js.map +0 -1
  397. package/managers/monitor.manager.d.ts +0 -70
  398. package/managers/monitor.manager.js +0 -550
  399. package/managers/monitor.manager.js.map +0 -1
  400. package/managers/openai-usage-ledger.manager.d.ts +0 -30
  401. package/managers/openai-usage-ledger.manager.js +0 -142
  402. package/managers/openai-usage-ledger.manager.js.map +0 -1
  403. package/managers/slow-query-verifier.manager.d.ts +0 -144
  404. package/managers/slow-query-verifier.manager.js +0 -3857
  405. package/managers/slow-query-verifier.manager.js.map +0 -1
  406. package/managers/slow-query.manager.d.ts +0 -28
  407. package/managers/slow-query.manager.js +0 -468
  408. package/managers/slow-query.manager.js.map +0 -1
  409. package/managers/subscription.manager.d.ts +0 -169
  410. package/managers/subscription.manager.js +0 -3434
  411. package/managers/subscription.manager.js.map +0 -1
  412. package/managers/websocket.manager.d.ts +0 -73
  413. package/managers/websocket.manager.js +0 -673
  414. package/managers/websocket.manager.js.map +0 -1
  415. package/managers/worker-dispatcher.manager.d.ts +0 -120
  416. package/managers/worker-dispatcher.manager.js +0 -1266
  417. package/managers/worker-dispatcher.manager.js.map +0 -1
  418. package/managers/worker-server.manager.d.ts +0 -35
  419. package/managers/worker-server.manager.js +0 -582
  420. package/managers/worker-server.manager.js.map +0 -1
  421. package/methods/accounts.d.ts +0 -2
  422. package/methods/accounts.js +0 -624
  423. package/methods/accounts.js.map +0 -1
  424. package/methods/ai-terminal.d.ts +0 -458
  425. package/methods/ai-terminal.js +0 -27991
  426. package/methods/ai-terminal.js.map +0 -1
  427. package/methods/app-settings.d.ts +0 -2
  428. package/methods/app-settings.js +0 -169
  429. package/methods/app-settings.js.map +0 -1
  430. package/methods/aws.d.ts +0 -2
  431. package/methods/aws.js +0 -877
  432. package/methods/aws.js.map +0 -1
  433. package/methods/collections.d.ts +0 -2
  434. package/methods/collections.js +0 -719
  435. package/methods/collections.js.map +0 -1
  436. package/methods/counters.d.ts +0 -2
  437. package/methods/counters.js +0 -113
  438. package/methods/counters.js.map +0 -1
  439. package/methods/cron-jobs.d.ts +0 -2
  440. package/methods/cron-jobs.js +0 -2475
  441. package/methods/cron-jobs.js.map +0 -1
  442. package/methods/customer-notifications.d.ts +0 -2
  443. package/methods/customer-notifications.js +0 -528
  444. package/methods/customer-notifications.js.map +0 -1
  445. package/methods/diagnostics.d.ts +0 -2
  446. package/methods/diagnostics.js +0 -703
  447. package/methods/diagnostics.js.map +0 -1
  448. package/methods/flag-updates.d.ts +0 -2
  449. package/methods/flag-updates.js +0 -8
  450. package/methods/flag-updates.js.map +0 -1
  451. package/methods/flags.d.ts +0 -2
  452. package/methods/flags.js +0 -8
  453. package/methods/flags.js.map +0 -1
  454. package/methods/logs.d.ts +0 -2
  455. package/methods/logs.js +0 -751
  456. package/methods/logs.js.map +0 -1
  457. package/methods/mongo-explorer.d.ts +0 -2
  458. package/methods/mongo-explorer.js +0 -1808
  459. package/methods/mongo-explorer.js.map +0 -1
  460. package/methods/monitor.d.ts +0 -2
  461. package/methods/monitor.js +0 -543
  462. package/methods/monitor.js.map +0 -1
  463. package/methods/pdf.d.ts +0 -2
  464. package/methods/pdf.js +0 -1216
  465. package/methods/pdf.js.map +0 -1
  466. package/methods/publications.d.ts +0 -1
  467. package/methods/publications.js +0 -183
  468. package/methods/publications.js.map +0 -1
  469. package/methods/report-builder.d.ts +0 -2
  470. package/methods/report-builder.js +0 -3094
  471. package/methods/report-builder.js.map +0 -1
  472. package/methods/support.d.ts +0 -2
  473. package/methods/support.js +0 -430
  474. package/methods/support.js.map +0 -1
  475. package/models/ai-run.model.d.ts +0 -19
  476. package/models/ai-run.model.js +0 -4
  477. package/models/ai-run.model.js.map +0 -1
  478. package/models/ai-terminal-conversation.model.d.ts +0 -17
  479. package/models/ai-terminal-conversation.model.js +0 -4
  480. package/models/ai-terminal-conversation.model.js.map +0 -1
  481. package/models/ai-terminal-issue-report.model.d.ts +0 -19
  482. package/models/ai-terminal-issue-report.model.js +0 -4
  483. package/models/ai-terminal-issue-report.model.js.map +0 -1
  484. package/models/ai-terminal-message.model.d.ts +0 -22
  485. package/models/ai-terminal-message.model.js +0 -4
  486. package/models/ai-terminal-message.model.js.map +0 -1
  487. package/models/app-setting.model.d.ts +0 -16
  488. package/models/app-setting.model.js +0 -4
  489. package/models/app-setting.model.js.map +0 -1
  490. package/models/app-status.model.js +0 -4
  491. package/models/app-status.model.js.map +0 -1
  492. package/models/billing-logged-in-users.model.js +0 -4
  493. package/models/billing-logged-in-users.model.js.map +0 -1
  494. package/models/collection-document.model.d.ts +0 -21
  495. package/models/collection-document.model.js +0 -4
  496. package/models/collection-document.model.js.map +0 -1
  497. package/models/communication-metric.model.d.ts +0 -20
  498. package/models/communication-metric.model.js +0 -4
  499. package/models/communication-metric.model.js.map +0 -1
  500. package/models/counter.model.js +0 -4
  501. package/models/counter.model.js.map +0 -1
  502. package/models/cron-job-history.model.d.ts +0 -15
  503. package/models/cron-job-history.model.js +0 -4
  504. package/models/cron-job-history.model.js.map +0 -1
  505. package/models/cron-job.model.d.ts +0 -14
  506. package/models/cron-job.model.js +0 -4
  507. package/models/cron-job.model.js.map +0 -1
  508. package/models/customer-notification.model.d.ts +0 -26
  509. package/models/customer-notification.model.js +0 -4
  510. package/models/customer-notification.model.js.map +0 -1
  511. package/models/customer-portal-password.model.d.ts +0 -11
  512. package/models/customer-portal-password.model.js +0 -4
  513. package/models/customer-portal-password.model.js.map +0 -1
  514. package/models/dialog.model.d.ts +0 -23
  515. package/models/dialog.model.js +0 -4
  516. package/models/dialog.model.js.map +0 -1
  517. package/models/email-history.model.d.ts +0 -32
  518. package/models/email-history.model.js.map +0 -1
  519. package/models/email-verified.model.js +0 -4
  520. package/models/email-verified.model.js.map +0 -1
  521. package/models/file.model.js +0 -4
  522. package/models/file.model.js.map +0 -1
  523. package/models/flag-update.model.js +0 -4
  524. package/models/flag-update.model.js.map +0 -1
  525. package/models/flag.model.js +0 -4
  526. package/models/flag.model.js.map +0 -1
  527. package/models/log-method-latency.model.d.ts +0 -10
  528. package/models/log-method-latency.model.js +0 -4
  529. package/models/log-method-latency.model.js.map +0 -1
  530. package/models/log-subscription.model.js +0 -4
  531. package/models/log-subscription.model.js.map +0 -1
  532. package/models/log.model.d.ts +0 -17
  533. package/models/log.model.js +0 -4
  534. package/models/log.model.js.map +0 -1
  535. package/models/logged-in-users.model.js +0 -4
  536. package/models/logged-in-users.model.js.map +0 -1
  537. package/models/method-response.model.js +0 -4
  538. package/models/method-response.model.js.map +0 -1
  539. package/models/method.model.d.ts +0 -26
  540. package/models/method.model.js +0 -4
  541. package/models/method.model.js.map +0 -1
  542. package/models/monitor-cpu.model.js +0 -4
  543. package/models/monitor-cpu.model.js.map +0 -1
  544. package/models/monitor-function.model.d.ts +0 -14
  545. package/models/monitor-function.model.js +0 -4
  546. package/models/monitor-function.model.js.map +0 -1
  547. package/models/monitor-memory.model.d.ts +0 -15
  548. package/models/monitor-memory.model.js +0 -4
  549. package/models/monitor-memory.model.js.map +0 -1
  550. package/models/monitor-mongo.model.d.ts +0 -13
  551. package/models/monitor-mongo.model.js +0 -4
  552. package/models/monitor-mongo.model.js.map +0 -1
  553. package/models/notification.model.js +0 -4
  554. package/models/notification.model.js.map +0 -1
  555. package/models/openai-usage-ledger.model.d.ts +0 -30
  556. package/models/openai-usage-ledger.model.js +0 -4
  557. package/models/openai-usage-ledger.model.js.map +0 -1
  558. package/models/pagination.model.d.ts +0 -11
  559. package/models/pagination.model.js +0 -28
  560. package/models/pagination.model.js.map +0 -1
  561. package/models/permission.model.d.ts +0 -12
  562. package/models/permission.model.js +0 -4
  563. package/models/permission.model.js.map +0 -1
  564. package/models/report-builder-dashboard-builder.model.d.ts +0 -25
  565. package/models/report-builder-dashboard-builder.model.js +0 -4
  566. package/models/report-builder-dashboard-builder.model.js.map +0 -1
  567. package/models/report-builder-library.model.d.ts +0 -17
  568. package/models/report-builder-library.model.js +0 -4
  569. package/models/report-builder-library.model.js.map +0 -1
  570. package/models/report-builder-report.model.d.ts +0 -121
  571. package/models/report-builder-report.model.js +0 -4
  572. package/models/report-builder-report.model.js.map +0 -1
  573. package/models/report-builder.model.d.ts +0 -61
  574. package/models/report-builder.model.js +0 -4
  575. package/models/report-builder.model.js.map +0 -1
  576. package/models/select-data-label.model.d.ts +0 -9
  577. package/models/select-data-label.model.js +0 -4
  578. package/models/select-data-label.model.js.map +0 -1
  579. package/models/server-message.model.d.ts +0 -32
  580. package/models/server-message.model.js +0 -4
  581. package/models/server-message.model.js.map +0 -1
  582. package/models/slow-query-report.model.d.ts +0 -23
  583. package/models/slow-query-report.model.js +0 -4
  584. package/models/slow-query-report.model.js.map +0 -1
  585. package/models/subscription.model.d.ts +0 -31
  586. package/models/subscription.model.js +0 -4
  587. package/models/subscription.model.js.map +0 -1
  588. package/models/support-ticket.model.d.ts +0 -87
  589. package/models/support-ticket.model.js +0 -4
  590. package/models/support-ticket.model.js.map +0 -1
  591. package/models/user-group.model.d.ts +0 -20
  592. package/models/user-group.model.js +0 -4
  593. package/models/user-group.model.js.map +0 -1
  594. package/models/user-guide.model.js +0 -4
  595. package/models/user-guide.model.js.map +0 -1
  596. package/models/user.model.d.ts +0 -84
  597. package/models/user.model.js +0 -4
  598. package/models/user.model.js.map +0 -1
  599. package/private/images/ResolveIO.png +0 -0
  600. package/public_api.js +0 -127
  601. package/public_api.js.map +0 -1
  602. package/publications/ai-terminal.d.ts +0 -1
  603. package/publications/ai-terminal.js +0 -122
  604. package/publications/ai-terminal.js.map +0 -1
  605. package/publications/app-settings.d.ts +0 -2
  606. package/publications/app-settings.js +0 -28
  607. package/publications/app-settings.js.map +0 -1
  608. package/publications/app-status.d.ts +0 -2
  609. package/publications/app-status.js +0 -16
  610. package/publications/app-status.js.map +0 -1
  611. package/publications/cron-jobs.d.ts +0 -2
  612. package/publications/cron-jobs.js +0 -88
  613. package/publications/cron-jobs.js.map +0 -1
  614. package/publications/customer-notifications.d.ts +0 -2
  615. package/publications/customer-notifications.js +0 -161
  616. package/publications/customer-notifications.js.map +0 -1
  617. package/publications/files.d.ts +0 -2
  618. package/publications/files.js +0 -36
  619. package/publications/files.js.map +0 -1
  620. package/publications/flags-update.d.ts +0 -2
  621. package/publications/flags-update.js +0 -22
  622. package/publications/flags-update.js.map +0 -1
  623. package/publications/flags.d.ts +0 -2
  624. package/publications/flags.js +0 -22
  625. package/publications/flags.js.map +0 -1
  626. package/publications/logs.d.ts +0 -2
  627. package/publications/logs.js +0 -164
  628. package/publications/logs.js.map +0 -1
  629. package/publications/notifications.d.ts +0 -2
  630. package/publications/notifications.js +0 -16
  631. package/publications/notifications.js.map +0 -1
  632. package/publications/report-builder-dashboard-builders.d.ts +0 -2
  633. package/publications/report-builder-dashboard-builders.js +0 -42
  634. package/publications/report-builder-dashboard-builders.js.map +0 -1
  635. package/publications/report-builder-libraries.d.ts +0 -2
  636. package/publications/report-builder-libraries.js +0 -90
  637. package/publications/report-builder-libraries.js.map +0 -1
  638. package/publications/report-builder-reports.d.ts +0 -2
  639. package/publications/report-builder-reports.js +0 -50
  640. package/publications/report-builder-reports.js.map +0 -1
  641. package/publications/super-admin.d.ts +0 -2
  642. package/publications/super-admin.js +0 -16
  643. package/publications/super-admin.js.map +0 -1
  644. package/publications/user-groups.d.ts +0 -1
  645. package/publications/user-groups.js +0 -16
  646. package/publications/user-groups.js.map +0 -1
  647. package/publications/user-guides.d.ts +0 -1
  648. package/publications/user-guides.js +0 -16
  649. package/publications/user-guides.js.map +0 -1
  650. package/resolveio-server-app.d.ts +0 -70
  651. package/resolveio-server-app.js +0 -801
  652. package/resolveio-server-app.js.map +0 -1
  653. package/server-app.d.ts +0 -228
  654. package/server-app.js +0 -3566
  655. package/server-app.js.map +0 -1
  656. package/services/codex-client.d.ts +0 -128
  657. package/services/codex-client.js +0 -1629
  658. package/services/codex-client.js.map +0 -1
  659. package/services/openai-client.d.ts +0 -46
  660. package/services/openai-client.js +0 -318
  661. package/services/openai-client.js.map +0 -1
  662. package/types/error-report.d.ts +0 -25
  663. package/types/error-report.js +0 -4
  664. package/types/error-report.js.map +0 -1
  665. package/types/slow-query-report.d.ts +0 -27
  666. package/types/slow-query-report.js +0 -6
  667. package/types/slow-query-report.js.map +0 -1
  668. package/util/ai-qa-policy.d.ts +0 -124
  669. package/util/ai-qa-policy.js +0 -736
  670. package/util/ai-qa-policy.js.map +0 -1
  671. package/util/ai-run-evidence-adapters.d.ts +0 -109
  672. package/util/ai-run-evidence-adapters.js +0 -7234
  673. package/util/ai-run-evidence-adapters.js.map +0 -1
  674. package/util/ai-run-evidence-dashboard.d.ts +0 -88
  675. package/util/ai-run-evidence-dashboard.js +0 -343
  676. package/util/ai-run-evidence-dashboard.js.map +0 -1
  677. package/util/ai-run-evidence-eval.d.ts +0 -86
  678. package/util/ai-run-evidence-eval.js +0 -1018
  679. package/util/ai-run-evidence-eval.js.map +0 -1
  680. package/util/ai-run-evidence.d.ts +0 -244
  681. package/util/ai-run-evidence.js +0 -1096
  682. package/util/ai-run-evidence.js.map +0 -1
  683. package/util/ai-runner-artifacts.d.ts +0 -82
  684. package/util/ai-runner-artifacts.js +0 -713
  685. package/util/ai-runner-artifacts.js.map +0 -1
  686. package/util/ai-runner-manager-autopilot.d.ts +0 -210
  687. package/util/ai-runner-manager-autopilot.js +0 -642
  688. package/util/ai-runner-manager-autopilot.js.map +0 -1
  689. package/util/ai-runner-manager-policy.d.ts +0 -807
  690. package/util/ai-runner-manager-policy.js +0 -3501
  691. package/util/ai-runner-manager-policy.js.map +0 -1
  692. package/util/ai-runner-qa-auth.d.ts +0 -5
  693. package/util/ai-runner-qa-auth.js +0 -839
  694. package/util/ai-runner-qa-auth.js.map +0 -1
  695. package/util/ai-runner-qa-tools.d.ts +0 -26
  696. package/util/ai-runner-qa-tools.js +0 -3520
  697. package/util/ai-runner-qa-tools.js.map +0 -1
  698. package/util/aicoder-runner-v6.d.ts +0 -426
  699. package/util/aicoder-runner-v6.js +0 -2464
  700. package/util/aicoder-runner-v6.js.map +0 -1
  701. package/util/common.d.ts +0 -31
  702. package/util/common.js +0 -683
  703. package/util/common.js.map +0 -1
  704. package/util/customer-portal-password.d.ts +0 -13
  705. package/util/customer-portal-password.js +0 -209
  706. package/util/customer-portal-password.js.map +0 -1
  707. package/util/error-reporter.d.ts +0 -52
  708. package/util/error-reporter.js +0 -326
  709. package/util/error-reporter.js.map +0 -1
  710. package/util/error-tracking.d.ts +0 -13
  711. package/util/error-tracking.js +0 -120
  712. package/util/error-tracking.js.map +0 -1
  713. package/util/openai-usage-cost.d.ts +0 -6
  714. package/util/openai-usage-cost.js +0 -103
  715. package/util/openai-usage-cost.js.map +0 -1
  716. package/util/report-builder-unwinds.d.ts +0 -15
  717. package/util/report-builder-unwinds.js +0 -156
  718. package/util/report-builder-unwinds.js.map +0 -1
  719. package/util/runner-process-janitor.d.ts +0 -27
  720. package/util/runner-process-janitor.js +0 -208
  721. package/util/runner-process-janitor.js.map +0 -1
  722. package/util/schema-report-builder.d.ts +0 -6
  723. package/util/schema-report-builder.js +0 -481
  724. package/util/schema-report-builder.js.map +0 -1
  725. package/util/slow-query-reporter.d.ts +0 -28
  726. package/util/slow-query-reporter.js +0 -226
  727. package/util/slow-query-reporter.js.map +0 -1
  728. package/util/subscription-dependency-context.d.ts +0 -34
  729. package/util/subscription-dependency-context.js +0 -1283
  730. package/util/subscription-dependency-context.js.map +0 -1
  731. package/util/support-runner-v5.d.ts +0 -1426
  732. package/util/support-runner-v5.js +0 -7624
  733. package/util/support-runner-v5.js.map +0 -1
  734. package/util/tokenizer.d.ts +0 -5
  735. package/util/tokenizer.js +0 -41
  736. package/util/tokenizer.js.map +0 -1
  737. package/workers/codex-runner.worker.d.ts +0 -1
  738. package/workers/codex-runner.worker.js +0 -192
  739. package/workers/codex-runner.worker.js.map +0 -1
  740. /package/{private → src/private}/email-templates/enrollment.html +0 -0
  741. /package/{private → src/private}/email-templates/forgot-password.html +0 -0
  742. /package/{private → src/private}/email-templates/support-ticket-deleted.html +0 -0
  743. /package/{private → src/private}/email-templates/support-ticket-modified.html +0 -0
  744. /package/{private → src/private}/email-templates/support-ticket.html +0 -0
  745. /package/{public_api.d.ts → src/public_api.ts} +0 -0
@@ -0,0 +1,1989 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { ResolveIOServer } from '../src/resolveio-server-app';
4
+ import {
5
+ buildAssistantDatedPivotDisplay,
6
+ buildDisplayTable
7
+ } from '../src/methods/ai-terminal';
8
+
9
+ type ParityCaseExpected = {
10
+ rows: Array<Record<string, any>>;
11
+ meta?: Record<string, any>;
12
+ };
13
+
14
+ type ParityCase = {
15
+ id: string;
16
+ prompt: string;
17
+ runExpected: (context: ParityContext) => Promise<ParityCaseExpected>;
18
+ compare: (params: {
19
+ toolResult: any;
20
+ assistantContent: string;
21
+ expected: ParityCaseExpected;
22
+ context: ParityContext;
23
+ }) => string[];
24
+ };
25
+
26
+ type ParityCaseResult = {
27
+ id: string;
28
+ prompt: string;
29
+ pass: boolean;
30
+ errors: string[];
31
+ conversationId?: string;
32
+ toolSource?: string;
33
+ directive?: {
34
+ type?: string;
35
+ collection?: string;
36
+ source?: string;
37
+ };
38
+ tool?: {
39
+ type?: string;
40
+ collection?: string;
41
+ rowCount?: number;
42
+ total?: number | null;
43
+ columns?: string[];
44
+ };
45
+ replay?: {
46
+ pass: boolean;
47
+ errors: string[];
48
+ };
49
+ expected?: {
50
+ rows: number;
51
+ meta?: Record<string, any>;
52
+ };
53
+ };
54
+
55
+ type ParityContext = {
56
+ db: any;
57
+ now: Date;
58
+ startOfWeek: Date;
59
+ last6MonthsStart: Date;
60
+ last6FullMonthsStart: Date;
61
+ startOfCurrentMonth: Date;
62
+ methodManager: any;
63
+ idUser: string;
64
+ appId: string;
65
+ };
66
+
67
+ type AssistantDirective = {
68
+ type: 'read' | 'aggregate';
69
+ payload: Record<string, any>;
70
+ source: 'metadata.debug.directive' | 'assistant.directive_line';
71
+ };
72
+
73
+ function parseBoolean(value: any): boolean {
74
+ return ['true', '1', 'yes', 'y', 'on'].includes(String(value || '').trim().toLowerCase());
75
+ }
76
+
77
+ function normalizeOptionalString(value: any): string {
78
+ return String(value ?? '').trim();
79
+ }
80
+
81
+ function loadDotEnvFile(filePath: string): Record<string, string> {
82
+ const parsed: Record<string, string> = {};
83
+ if (!fs.existsSync(filePath)) {
84
+ return parsed;
85
+ }
86
+ const content = fs.readFileSync(filePath, 'utf8');
87
+ content.split(/\r?\n/g).forEach((lineRaw) => {
88
+ const line = lineRaw.trim();
89
+ if (!line || line.startsWith('#')) {
90
+ return;
91
+ }
92
+ const jsonLike = line.match(/^"([A-Za-z_][A-Za-z0-9_]*)"\s*:\s*(.+?)\s*,?$/);
93
+ if (jsonLike?.[1]) {
94
+ const key = jsonLike[1];
95
+ let value = String(jsonLike[2] || '').trim();
96
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
97
+ value = value.slice(1, -1);
98
+ }
99
+ if (key) {
100
+ parsed[key] = value;
101
+ }
102
+ return;
103
+ }
104
+ const separator = line.indexOf('=');
105
+ if (separator <= 0) {
106
+ return;
107
+ }
108
+ const key = line.slice(0, separator).trim();
109
+ let value = line.slice(separator + 1).trim();
110
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
111
+ value = value.slice(1, -1);
112
+ }
113
+ if (key) {
114
+ parsed[key] = value;
115
+ }
116
+ });
117
+ return parsed;
118
+ }
119
+
120
+ function applyEnvFromServerDir(serverDir: string): void {
121
+ const envFiles = [
122
+ path.join(serverDir, '.env.codex'),
123
+ path.join(serverDir, '.env.production'),
124
+ path.join(serverDir, '.env')
125
+ ];
126
+ envFiles.forEach((filePath) => {
127
+ const values = loadDotEnvFile(filePath);
128
+ Object.keys(values).forEach((key) => {
129
+ if (!process.env[key]) {
130
+ process.env[key] = values[key];
131
+ }
132
+ });
133
+ });
134
+ }
135
+
136
+ function addDaysUtc(date: Date, days: number): Date {
137
+ return new Date(Date.UTC(
138
+ date.getUTCFullYear(),
139
+ date.getUTCMonth(),
140
+ date.getUTCDate() + days,
141
+ date.getUTCHours(),
142
+ date.getUTCMinutes(),
143
+ date.getUTCSeconds(),
144
+ date.getUTCMilliseconds()
145
+ ));
146
+ }
147
+
148
+ function addMonthsUtc(date: Date, months: number): Date {
149
+ return new Date(Date.UTC(
150
+ date.getUTCFullYear(),
151
+ date.getUTCMonth() + months,
152
+ date.getUTCDate(),
153
+ date.getUTCHours(),
154
+ date.getUTCMinutes(),
155
+ date.getUTCSeconds(),
156
+ date.getUTCMilliseconds()
157
+ ));
158
+ }
159
+
160
+ function startOfDayUtc(date: Date): Date {
161
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0));
162
+ }
163
+
164
+ function startOfWeekUtcMonday(date: Date): Date {
165
+ const day = date.getUTCDay();
166
+ const diff = (day + 6) % 7;
167
+ return startOfDayUtc(addDaysUtc(date, -diff));
168
+ }
169
+
170
+ function startOfMonthUtc(date: Date): Date {
171
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1, 0, 0, 0, 0));
172
+ }
173
+
174
+ function toNormalizedKey(value: string): string {
175
+ return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, '');
176
+ }
177
+
178
+ function parseNumericLoose(value: any): number | null {
179
+ if (typeof value === 'number' && Number.isFinite(value)) {
180
+ return value;
181
+ }
182
+ if (typeof value !== 'string') {
183
+ return null;
184
+ }
185
+ const trimmed = value.trim();
186
+ if (!trimmed) {
187
+ return null;
188
+ }
189
+ const normalized = trimmed
190
+ .replace(/\$/g, '')
191
+ .replace(/,/g, '')
192
+ .replace(/%/g, '')
193
+ .replace(/\s+/g, '');
194
+ if (!normalized || !/^[-+]?\d*\.?\d+$/.test(normalized)) {
195
+ return null;
196
+ }
197
+ const parsed = Number(normalized);
198
+ return Number.isFinite(parsed) ? parsed : null;
199
+ }
200
+
201
+ function normalizeCellValue(value: any): any {
202
+ const numeric = parseNumericLoose(value);
203
+ if (numeric !== null) {
204
+ return Number(numeric.toFixed(6));
205
+ }
206
+ if (value instanceof Date) {
207
+ return value.toISOString();
208
+ }
209
+ if (typeof value === 'string') {
210
+ return value.trim();
211
+ }
212
+ if (value === undefined) {
213
+ return null;
214
+ }
215
+ return value;
216
+ }
217
+
218
+ function normalizeDisplayRows(display: any): Array<Record<string, any>> {
219
+ const rows = Array.isArray(display?.rows) ? display.rows : [];
220
+ return rows.map((row: any) => {
221
+ const next: Record<string, any> = {};
222
+ Object.keys(row || {}).forEach((key) => {
223
+ next[toNormalizedKey(key)] = normalizeCellValue((row as any)[key]);
224
+ });
225
+ return next;
226
+ });
227
+ }
228
+
229
+ function compareDisplayParity(sourceDisplay: any, replayDisplay: any): string[] {
230
+ const sourceRows = normalizeDisplayRows(sourceDisplay);
231
+ const replayRows = normalizeDisplayRows(replayDisplay);
232
+ const sortRows = (rows: Array<Record<string, any>>) => rows
233
+ .map(row => JSON.stringify(row, Object.keys(row).sort()))
234
+ .sort();
235
+
236
+ const sourceSorted = sortRows(sourceRows);
237
+ const replaySorted = sortRows(replayRows);
238
+ if (sourceSorted.length === replaySorted.length) {
239
+ let strictMismatch = false;
240
+ for (let index = 0; index < sourceSorted.length; index += 1) {
241
+ if (sourceSorted[index] !== replaySorted[index]) {
242
+ strictMismatch = true;
243
+ break;
244
+ }
245
+ }
246
+ if (!strictMismatch) {
247
+ return [];
248
+ }
249
+ }
250
+
251
+ // Dated outputs can be pivoted into a different row shape but still contain matching totals.
252
+ const sourceMonthly = parseMonthlyTotalsFromDisplay(sourceDisplay);
253
+ const replayMonthly = parseMonthlyTotalsFromDisplay(replayDisplay);
254
+ if (sourceMonthly && replayMonthly) {
255
+ const monthErrors = compareNumericMaps('Replay monthly totals', sourceMonthly, replayMonthly, 0.05);
256
+ if (!monthErrors.length) {
257
+ return [];
258
+ }
259
+ }
260
+
261
+ const sourceCustomer = parseCustomerMonthTotalsFromDisplay(sourceDisplay);
262
+ const replayCustomer = parseCustomerMonthTotalsFromDisplay(replayDisplay);
263
+ if (sourceCustomer && replayCustomer) {
264
+ const customerErrors = compareNumericMaps('Replay customer monthly totals', sourceCustomer.monthTotals, replayCustomer.monthTotals, 0.05);
265
+ if (!customerErrors.length) {
266
+ return [];
267
+ }
268
+ }
269
+
270
+ return [
271
+ `Replay display mismatch: tool_result rows=${sourceRows.length}, replay rows=${replayRows.length}.`
272
+ ];
273
+ }
274
+
275
+ function findFirstMarkdownTable(content: string): { header: string[]; rows: string[][] } | null {
276
+ const lines = String(content || '').split('\n');
277
+ for (let index = 0; index < lines.length - 1; index += 1) {
278
+ const header = lines[index]?.trim();
279
+ const separator = lines[index + 1]?.trim();
280
+ if (!header || !separator) {
281
+ continue;
282
+ }
283
+ if (!/^\|.+\|$/.test(header)) {
284
+ continue;
285
+ }
286
+ if (!/^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(separator)) {
287
+ continue;
288
+ }
289
+ const headerCells = header.split('|').map(value => value.trim()).filter(Boolean);
290
+ const dataRows: string[][] = [];
291
+ for (let rowIndex = index + 2; rowIndex < lines.length; rowIndex += 1) {
292
+ const line = String(lines[rowIndex] || '');
293
+ if (!line.trim() || !line.includes('|')) {
294
+ break;
295
+ }
296
+ dataRows.push(line.split('|').map(value => value.trim()).filter(Boolean));
297
+ }
298
+ return { header: headerCells, rows: dataRows };
299
+ }
300
+ return null;
301
+ }
302
+
303
+ function collectCommonFormattingErrors(content: string): string[] {
304
+ const errors: string[] = [];
305
+ const table = findFirstMarkdownTable(content);
306
+ if (table && table.rows.length === 0) {
307
+ errors.push('Assistant response contains an empty markdown table header with no data rows.');
308
+ }
309
+ return errors;
310
+ }
311
+
312
+ function collectMonthColumns(columns: string[]): string[] {
313
+ return columns.filter(column => /^\d{4}-\d{2}$/.test(String(column || '').trim()));
314
+ }
315
+
316
+ function parseMonthlyTotalsFromDisplay(display: any): Map<string, number> | null {
317
+ if (!display || !Array.isArray(display.columns) || !Array.isArray(display.rows) || !display.rows.length) {
318
+ return null;
319
+ }
320
+ const columns = display.columns.map((column: any) => String(column || '').trim());
321
+ const monthColumns = collectMonthColumns(columns);
322
+ if (monthColumns.length) {
323
+ const firstColumn = columns[0] || '';
324
+ const normalizedFirst = toNormalizedKey(firstColumn);
325
+ if (normalizedFirst === 'metric') {
326
+ const metricRow = display.rows.find((row: any) => {
327
+ const metricValue = String(row?.[firstColumn] || '').toLowerCase();
328
+ return metricValue.includes('revenue') || metricValue.includes('total');
329
+ }) || display.rows[0];
330
+ const result = new Map<string, number>();
331
+ monthColumns.forEach((month) => {
332
+ const numeric = parseNumericLoose(metricRow?.[month]);
333
+ if (numeric !== null) {
334
+ result.set(month, numeric);
335
+ }
336
+ });
337
+ return result.size ? result : null;
338
+ }
339
+ const result = new Map<string, number>();
340
+ monthColumns.forEach((month) => {
341
+ let sum = 0;
342
+ display.rows.forEach((row: any) => {
343
+ const numeric = parseNumericLoose(row?.[month]);
344
+ if (numeric !== null) {
345
+ sum += numeric;
346
+ }
347
+ });
348
+ result.set(month, sum);
349
+ });
350
+ return result;
351
+ }
352
+ const normalizedColumns = columns.map(toNormalizedKey);
353
+ const monthColumnIndex = normalizedColumns.findIndex((column) => column.includes('month'));
354
+ const totalColumnIndex = normalizedColumns.findIndex((column) => column.includes('revenue') || column.includes('total'));
355
+ if (monthColumnIndex === -1 || totalColumnIndex === -1) {
356
+ return null;
357
+ }
358
+ const monthColumn = columns[monthColumnIndex];
359
+ const totalColumn = columns[totalColumnIndex];
360
+ const result = new Map<string, number>();
361
+ display.rows.forEach((row: any) => {
362
+ const monthValue = String(row?.[monthColumn] || '').trim();
363
+ if (!/^\d{4}-\d{2}$/.test(monthValue)) {
364
+ return;
365
+ }
366
+ const numeric = parseNumericLoose(row?.[totalColumn]);
367
+ if (numeric === null) {
368
+ return;
369
+ }
370
+ result.set(monthValue, (result.get(monthValue) || 0) + numeric);
371
+ });
372
+ return result.size ? result : null;
373
+ }
374
+
375
+ function parseCustomerMonthTotalsFromDisplay(display: any): {
376
+ entries: Array<{ customer: string; month: string; total: number }>;
377
+ nonUnknownCustomers: Set<string>;
378
+ monthTotals: Map<string, number>;
379
+ } | null {
380
+ if (!display || !Array.isArray(display.columns) || !Array.isArray(display.rows) || !display.rows.length) {
381
+ return null;
382
+ }
383
+ const columns = display.columns.map((column: any) => String(column || '').trim());
384
+ const normalizedColumns = columns.map(toNormalizedKey);
385
+ const monthColumns = collectMonthColumns(columns);
386
+ const entries: Array<{ customer: string; month: string; total: number }> = [];
387
+
388
+ if (monthColumns.length && normalizedColumns[0]?.includes('customer')) {
389
+ const customerColumn = columns[0];
390
+ display.rows.forEach((row: any) => {
391
+ const customer = String(row?.[customerColumn] || '').trim() || 'Unknown Customer';
392
+ monthColumns.forEach((month) => {
393
+ const total = parseNumericLoose(row?.[month]);
394
+ if (total === null) {
395
+ return;
396
+ }
397
+ entries.push({ customer, month, total });
398
+ });
399
+ });
400
+ }
401
+ else {
402
+ const customerColumnIndex = normalizedColumns.findIndex((column) => column.includes('customer'));
403
+ const monthColumnIndex = normalizedColumns.findIndex((column) => column.includes('month'));
404
+ const totalColumnIndex = normalizedColumns.findIndex((column) => column.includes('revenue') || column.includes('total'));
405
+ if (customerColumnIndex === -1 || monthColumnIndex === -1 || totalColumnIndex === -1) {
406
+ return null;
407
+ }
408
+ const customerColumn = columns[customerColumnIndex];
409
+ const monthColumn = columns[monthColumnIndex];
410
+ const totalColumn = columns[totalColumnIndex];
411
+ display.rows.forEach((row: any) => {
412
+ const customer = String(row?.[customerColumn] || '').trim() || 'Unknown Customer';
413
+ const month = String(row?.[monthColumn] || '').trim();
414
+ if (!/^\d{4}-\d{2}$/.test(month)) {
415
+ return;
416
+ }
417
+ const total = parseNumericLoose(row?.[totalColumn]);
418
+ if (total === null) {
419
+ return;
420
+ }
421
+ entries.push({ customer, month, total });
422
+ });
423
+ }
424
+ if (!entries.length) {
425
+ return null;
426
+ }
427
+ const nonUnknownCustomers = new Set<string>();
428
+ const monthTotals = new Map<string, number>();
429
+ entries.forEach((entry) => {
430
+ if (!/^unknown customer$/i.test(entry.customer)) {
431
+ nonUnknownCustomers.add(entry.customer.toLowerCase());
432
+ }
433
+ monthTotals.set(entry.month, (monthTotals.get(entry.month) || 0) + entry.total);
434
+ });
435
+ return {
436
+ entries,
437
+ nonUnknownCustomers,
438
+ monthTotals
439
+ };
440
+ }
441
+
442
+ function parseDimensionMonthTotalsFromDisplay(
443
+ display: any,
444
+ dimensionCandidates: string[]
445
+ ): {
446
+ entries: Array<{ dimension: string; month: string; total: number }>;
447
+ nonUnknownDimensions: Set<string>;
448
+ monthTotals: Map<string, number>;
449
+ dimensionTotals: Map<string, number>;
450
+ } | null {
451
+ if (!display || !Array.isArray(display.columns) || !Array.isArray(display.rows) || !display.rows.length) {
452
+ return null;
453
+ }
454
+ const columns = display.columns.map((column: any) => String(column || '').trim());
455
+ const normalizedColumns = columns.map(toNormalizedKey);
456
+ const normalizedDimensionCandidates = (Array.isArray(dimensionCandidates) ? dimensionCandidates : [])
457
+ .map(candidate => toNormalizedKey(candidate))
458
+ .filter(Boolean);
459
+ const isDimensionColumn = (value: string): boolean => {
460
+ return normalizedDimensionCandidates.some((candidate) => value === candidate || value.includes(candidate));
461
+ };
462
+ const monthColumns = collectMonthColumns(columns);
463
+ const entries: Array<{ dimension: string; month: string; total: number }> = [];
464
+
465
+ if (monthColumns.length && isDimensionColumn(normalizedColumns[0] || '')) {
466
+ const dimensionColumn = columns[0];
467
+ display.rows.forEach((row: any) => {
468
+ const dimension = String(row?.[dimensionColumn] || '').trim() || 'Unknown';
469
+ monthColumns.forEach((month) => {
470
+ const total = parseNumericLoose(row?.[month]);
471
+ if (total === null) {
472
+ return;
473
+ }
474
+ entries.push({ dimension, month, total });
475
+ });
476
+ });
477
+ }
478
+ else {
479
+ const dimensionColumnIndex = normalizedColumns.findIndex(column => isDimensionColumn(column));
480
+ const monthColumnIndex = normalizedColumns.findIndex(column => column.includes('month'));
481
+ const totalColumnIndex = normalizedColumns.findIndex((column) => (
482
+ column.includes('revenue')
483
+ || column.includes('total')
484
+ || column.includes('hour')
485
+ || column.includes('amount')
486
+ || column.includes('billable')
487
+ ));
488
+ if (dimensionColumnIndex === -1 || monthColumnIndex === -1 || totalColumnIndex === -1) {
489
+ return null;
490
+ }
491
+ const dimensionColumn = columns[dimensionColumnIndex];
492
+ const monthColumn = columns[monthColumnIndex];
493
+ const totalColumn = columns[totalColumnIndex];
494
+ display.rows.forEach((row: any) => {
495
+ const dimension = String(row?.[dimensionColumn] || '').trim() || 'Unknown';
496
+ const month = String(row?.[monthColumn] || '').trim();
497
+ if (!/^\d{4}-\d{2}$/.test(month)) {
498
+ return;
499
+ }
500
+ const total = parseNumericLoose(row?.[totalColumn]);
501
+ if (total === null) {
502
+ return;
503
+ }
504
+ entries.push({ dimension, month, total });
505
+ });
506
+ }
507
+ if (!entries.length) {
508
+ return null;
509
+ }
510
+ const nonUnknownDimensions = new Set<string>();
511
+ const monthTotals = new Map<string, number>();
512
+ const dimensionTotals = new Map<string, number>();
513
+ entries.forEach((entry) => {
514
+ if (!/^(unknown|unassigned)(\s|$)/i.test(entry.dimension)) {
515
+ nonUnknownDimensions.add(entry.dimension.toLowerCase());
516
+ }
517
+ monthTotals.set(entry.month, (monthTotals.get(entry.month) || 0) + entry.total);
518
+ dimensionTotals.set(entry.dimension, (dimensionTotals.get(entry.dimension) || 0) + entry.total);
519
+ });
520
+ return {
521
+ entries,
522
+ nonUnknownDimensions,
523
+ monthTotals,
524
+ dimensionTotals
525
+ };
526
+ }
527
+
528
+ function mapExpectedStatusCounts(rows: Array<Record<string, any>>): Map<string, number> {
529
+ const map = new Map<string, number>();
530
+ rows.forEach((row) => {
531
+ const status = String(row?.status || row?.state || row?._id || 'Unknown').trim() || 'Unknown';
532
+ const count = Number(row?.work_order_count || row?.count || 0);
533
+ map.set(status, count);
534
+ });
535
+ return map;
536
+ }
537
+
538
+ function mapDisplayStatusCounts(display: any): Map<string, number> | null {
539
+ if (!display || !Array.isArray(display.columns) || !Array.isArray(display.rows)) {
540
+ return null;
541
+ }
542
+ const columns = display.columns.map((column: any) => String(column || '').trim());
543
+ const normalizedColumns = columns.map(toNormalizedKey);
544
+ const statusIndex = normalizedColumns.findIndex((column) => column.includes('status') || column.includes('state') || column === 'group');
545
+ const countIndex = normalizedColumns.findIndex((column) => column.includes('count') || column.includes('total') || column.includes('workorder'));
546
+ if (statusIndex === -1 || countIndex === -1) {
547
+ return null;
548
+ }
549
+ const statusColumn = columns[statusIndex];
550
+ const countColumn = columns[countIndex];
551
+ const map = new Map<string, number>();
552
+ display.rows.forEach((row: any) => {
553
+ const status = String(row?.[statusColumn] || 'Unknown').trim() || 'Unknown';
554
+ const count = parseNumericLoose(row?.[countColumn]);
555
+ if (count === null) {
556
+ return;
557
+ }
558
+ map.set(status, count);
559
+ });
560
+ return map;
561
+ }
562
+
563
+ function resolveFirstExistingField(row: Record<string, any>, candidates: string[]): string | null {
564
+ const keys = Object.keys(row || {});
565
+ const normalizedToRaw = new Map<string, string>();
566
+ keys.forEach((key) => {
567
+ normalizedToRaw.set(toNormalizedKey(key), key);
568
+ });
569
+ for (const candidate of candidates) {
570
+ const raw = normalizedToRaw.get(toNormalizedKey(candidate));
571
+ if (raw) {
572
+ return raw;
573
+ }
574
+ }
575
+ return null;
576
+ }
577
+
578
+ function mapExpectedByDimension(
579
+ rows: Array<Record<string, any>>,
580
+ dimensionCandidates: string[],
581
+ valueCandidates: string[]
582
+ ): Map<string, number> | null {
583
+ const firstRow = rows.find(row => row && typeof row === 'object') || null;
584
+ if (!firstRow) {
585
+ return new Map<string, number>();
586
+ }
587
+ const dimensionField = resolveFirstExistingField(firstRow, dimensionCandidates);
588
+ const valueField = resolveFirstExistingField(firstRow, valueCandidates);
589
+ if (!dimensionField || !valueField) {
590
+ return null;
591
+ }
592
+ const map = new Map<string, number>();
593
+ rows.forEach((row) => {
594
+ const key = normalizeOptionalString(row?.[dimensionField]);
595
+ if (!key) {
596
+ return;
597
+ }
598
+ const numeric = parseNumericLoose(row?.[valueField]);
599
+ if (numeric === null) {
600
+ return;
601
+ }
602
+ map.set(key, (map.get(key) || 0) + numeric);
603
+ });
604
+ return map;
605
+ }
606
+
607
+ function mapDisplayByDimension(
608
+ display: any,
609
+ dimensionCandidates: string[],
610
+ valueCandidates: string[]
611
+ ): Map<string, number> | null {
612
+ if (!display || !Array.isArray(display.columns) || !Array.isArray(display.rows)) {
613
+ return null;
614
+ }
615
+ const columns = display.columns.map((column: any) => String(column || '').trim());
616
+ const normalizedColumns = columns.map(toNormalizedKey);
617
+ const dimensionIndex = dimensionCandidates
618
+ .map(candidate => normalizedColumns.findIndex(column => column === toNormalizedKey(candidate) || column.includes(toNormalizedKey(candidate))))
619
+ .find(index => index !== undefined && index >= 0);
620
+ const valueIndex = valueCandidates
621
+ .map(candidate => normalizedColumns.findIndex(column => column === toNormalizedKey(candidate) || column.includes(toNormalizedKey(candidate))))
622
+ .find(index => index !== undefined && index >= 0);
623
+ if (dimensionIndex === undefined || dimensionIndex < 0 || valueIndex === undefined || valueIndex < 0) {
624
+ return null;
625
+ }
626
+ const dimensionColumn = columns[dimensionIndex];
627
+ const valueColumn = columns[valueIndex];
628
+ const map = new Map<string, number>();
629
+ display.rows.forEach((row: any) => {
630
+ const key = normalizeOptionalString(row?.[dimensionColumn]);
631
+ if (!key) {
632
+ return;
633
+ }
634
+ const numeric = parseNumericLoose(row?.[valueColumn]);
635
+ if (numeric === null) {
636
+ return;
637
+ }
638
+ map.set(key, (map.get(key) || 0) + numeric);
639
+ });
640
+ return map;
641
+ }
642
+
643
+ function parseSingleCountFromDisplay(display: any): number | null {
644
+ if (!display || !Array.isArray(display.columns) || !Array.isArray(display.rows) || !display.rows.length) {
645
+ return null;
646
+ }
647
+ const columns = display.columns.map((column: any) => String(column || '').trim());
648
+ const normalizedColumns = columns.map(toNormalizedKey);
649
+ const countIndex = normalizedColumns.findIndex((column) => (
650
+ column.includes('count')
651
+ || column.includes('total')
652
+ || column.includes('active')
653
+ || column.includes('clients')
654
+ || column.includes('customers')
655
+ ));
656
+ if (countIndex === -1) {
657
+ return null;
658
+ }
659
+ const countColumn = columns[countIndex];
660
+ for (const row of display.rows) {
661
+ const numeric = parseNumericLoose(row?.[countColumn]);
662
+ if (numeric !== null) {
663
+ return numeric;
664
+ }
665
+ }
666
+ return null;
667
+ }
668
+
669
+ function compareNumericMaps(
670
+ label: string,
671
+ expected: Map<string, number>,
672
+ actual: Map<string, number>,
673
+ tolerance = 0.01
674
+ ): string[] {
675
+ const errors: string[] = [];
676
+ const keys = new Set<string>([...Array.from(expected.keys()), ...Array.from(actual.keys())]);
677
+ keys.forEach((key) => {
678
+ const left = expected.get(key);
679
+ const right = actual.get(key);
680
+ if (left === undefined) {
681
+ errors.push(`${label}: unexpected key "${key}" in AI output.`);
682
+ return;
683
+ }
684
+ if (right === undefined) {
685
+ errors.push(`${label}: missing key "${key}" in AI output.`);
686
+ return;
687
+ }
688
+ if (Math.abs(left - right) > tolerance) {
689
+ errors.push(`${label}: mismatch for "${key}" expected=${left} actual=${right}.`);
690
+ }
691
+ });
692
+ return errors;
693
+ }
694
+
695
+ async function sleep(ms: number): Promise<void> {
696
+ return await new Promise(resolve => setTimeout(resolve, ms));
697
+ }
698
+
699
+ async function findSuperAdminUserId(db: any): Promise<string> {
700
+ const user = await db.collection('users').findOne(
701
+ { 'roles.super_admin': true },
702
+ { projection: { _id: 1 }, sort: { updatedAt: -1, createdAt: -1 } }
703
+ );
704
+ if (!user?._id) {
705
+ throw new Error('Could not find super admin user.');
706
+ }
707
+ return String(user._id);
708
+ }
709
+
710
+ async function resolveExistingCollection(db: any, candidates: string[]): Promise<string | null> {
711
+ const rows = await db.listCollections({}, { nameOnly: true }).toArray();
712
+ const names = new Set(rows.map((row: any) => normalizeOptionalString(row?.name)));
713
+ for (const candidate of candidates) {
714
+ if (names.has(candidate)) {
715
+ return candidate;
716
+ }
717
+ }
718
+ return null;
719
+ }
720
+
721
+ async function detectFieldPresence(db: any, collection: string, fields: string[]): Promise<Record<string, boolean>> {
722
+ const projection = fields.reduce((acc: Record<string, number>, field) => {
723
+ acc[field] = 1;
724
+ return acc;
725
+ }, {});
726
+ const docs = await db.collection(collection).find({}, { projection, limit: 200 }).toArray();
727
+ const result: Record<string, boolean> = {};
728
+ fields.forEach((field) => {
729
+ result[field] = docs.some((doc: any) => doc && doc[field] !== undefined && doc[field] !== null && String(doc[field]).trim() !== '');
730
+ });
731
+ return result;
732
+ }
733
+
734
+ async function pollFinalAssistantMessage(db: any, idConversation: string, timeoutMs = 180000): Promise<any> {
735
+ const started = Date.now();
736
+ while (Date.now() - started < timeoutMs) {
737
+ const message = await db.collection('ai-terminal-messages').findOne(
738
+ { id_conversation: idConversation, role: 'assistant' },
739
+ { sort: { createdAt: -1 } }
740
+ );
741
+ if (message && message?.metadata?.pending !== true) {
742
+ return message;
743
+ }
744
+ await sleep(1200);
745
+ }
746
+ throw new Error(`Timed out waiting for final assistant message (${idConversation}).`);
747
+ }
748
+
749
+ function buildMongoCommonContext(methodManager: any, idUser: string): any {
750
+ return Object.assign({}, methodManager, Object.getPrototypeOf(methodManager), {
751
+ id_user: idUser,
752
+ user: 'AI Parity Runner',
753
+ id_ws: 'ai-assistant-data-parity-e2e'
754
+ });
755
+ }
756
+
757
+ async function callMethodAsUser(methodManager: any, idUser: string, method: string, payload: Record<string, any>): Promise<any> {
758
+ const ctx = buildMongoCommonContext(methodManager, idUser);
759
+ return await methodManager.callMethod.call(ctx, method, payload);
760
+ }
761
+
762
+ async function runPrompt(methodManager: any, db: any, idUser: string, prompt: string, appId: string): Promise<{ conversationId: string; message: any }> {
763
+ const model = String(process.env.AI_ASSISTANT_CODEX_MODEL || process.env.OPENAI_MODEL || '').trim();
764
+ const payload = {
765
+ message: prompt,
766
+ id_app: appId,
767
+ max_history: 0,
768
+ config: {
769
+ ...(model ? { model } : {})
770
+ }
771
+ };
772
+ const response = await callMethodAsUser(methodManager, idUser, 'aiCoderTerminalRunCodex', payload);
773
+ const conversationId = String(response?.conversation?._id || response?.conversation?.id_conversation || '').trim();
774
+ if (!conversationId) {
775
+ throw new Error('No conversation id returned from aiCoderTerminalRunCodex.');
776
+ }
777
+ const message = await pollFinalAssistantMessage(db, conversationId);
778
+ return { conversationId, message };
779
+ }
780
+
781
+ function parseDirectiveLine(content: string): AssistantDirective | null {
782
+ const lines = String(content || '').split(/\r?\n/g);
783
+ for (const lineRaw of lines) {
784
+ const line = lineRaw.trim();
785
+ if (!line) {
786
+ continue;
787
+ }
788
+ const readPrefix = 'REPORT_BUILDER_READ:';
789
+ const aggPrefix = 'REPORT_BUILDER_AGG:';
790
+ let type: AssistantDirective['type'] | null = null;
791
+ let jsonText = '';
792
+ if (line.startsWith(readPrefix)) {
793
+ type = 'read';
794
+ jsonText = line.slice(readPrefix.length).trim();
795
+ }
796
+ else if (line.startsWith(aggPrefix)) {
797
+ type = 'aggregate';
798
+ jsonText = line.slice(aggPrefix.length).trim();
799
+ }
800
+ if (!type || !jsonText) {
801
+ continue;
802
+ }
803
+ try {
804
+ const payload = JSON.parse(jsonText);
805
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
806
+ continue;
807
+ }
808
+ return {
809
+ type,
810
+ payload,
811
+ source: 'assistant.directive_line'
812
+ };
813
+ }
814
+ catch {
815
+ continue;
816
+ }
817
+ }
818
+ return null;
819
+ }
820
+
821
+ function extractDirectiveFromMessage(message: any): AssistantDirective | null {
822
+ const debugDirective = message?.metadata?.debug?.directive;
823
+ if (debugDirective && typeof debugDirective === 'object') {
824
+ const normalizedType = normalizeOptionalString(debugDirective?.type).toLowerCase();
825
+ if ((normalizedType === 'read' || normalizedType === 'aggregate')
826
+ && debugDirective.payload
827
+ && typeof debugDirective.payload === 'object'
828
+ && !Array.isArray(debugDirective.payload)) {
829
+ return {
830
+ type: normalizedType as AssistantDirective['type'],
831
+ payload: debugDirective.payload,
832
+ source: 'metadata.debug.directive'
833
+ };
834
+ }
835
+ const rawLineDirective = parseDirectiveLine(normalizeOptionalString(debugDirective?.rawLine));
836
+ if (rawLineDirective) {
837
+ return {
838
+ ...rawLineDirective,
839
+ source: 'metadata.debug.directive'
840
+ };
841
+ }
842
+ }
843
+ return parseDirectiveLine(normalizeOptionalString(message?.content));
844
+ }
845
+
846
+ function buildSyntheticToolResultFromDirective(directive: AssistantDirective, methodResponse: any): any {
847
+ const display = methodResponse?.display && typeof methodResponse.display === 'object'
848
+ ? methodResponse.display
849
+ : {
850
+ columns: [] as string[],
851
+ rows: [] as Array<Record<string, any>>,
852
+ rowCount: 0,
853
+ truncated: false
854
+ };
855
+ const rowCount = Array.isArray(methodResponse?.documents)
856
+ ? methodResponse.documents.length
857
+ : Number(display?.rowCount || 0);
858
+ const total = typeof methodResponse?.total === 'number' ? methodResponse.total : undefined;
859
+ const verification = methodResponse?.verification && typeof methodResponse.verification === 'object'
860
+ ? methodResponse.verification
861
+ : undefined;
862
+ const collection = normalizeOptionalString(methodResponse?.debug?.collectionResolved)
863
+ || normalizeOptionalString(methodResponse?.debug?.collection)
864
+ || normalizeOptionalString(directive.payload?.collection);
865
+ return {
866
+ type: directive.type === 'aggregate' ? 'mongo_agg' : 'mongo_read',
867
+ input: directive.payload,
868
+ output: {
869
+ display,
870
+ total,
871
+ collection: collection || undefined,
872
+ rowCount,
873
+ columns: Array.isArray(display?.columns) ? display.columns : [],
874
+ truncated: display?.truncated === true,
875
+ verification,
876
+ debug: methodResponse?.debug && typeof methodResponse.debug === 'object' ? methodResponse.debug : undefined
877
+ }
878
+ };
879
+ }
880
+
881
+ function resolveParityCases(): ParityCase[] {
882
+ return [
883
+ {
884
+ id: 'active_clients_count',
885
+ prompt: 'How many active clients do I have right now?',
886
+ runExpected: async (context) => {
887
+ const collection = await resolveExistingCollection(context.db, ['clients', 'customers']);
888
+ if (!collection) {
889
+ return { rows: [], meta: { activeCount: 0, collection: null, basis: 'missing_collection' } };
890
+ }
891
+ const collectionFamily = normalizeOptionalString(collection).toLowerCase();
892
+ const activeFields = ['status', 'active', 'is_active', 'isactive', 'enabled', 'is_enabled', 'isenabled'];
893
+ if (!['clients', 'customers'].includes(collectionFamily)) {
894
+ activeFields.splice(1, 0, 'state');
895
+ }
896
+ const fieldPresence = await detectFieldPresence(context.db, collection, activeFields);
897
+ const hasAnyActiveField = activeFields.some(field => fieldPresence[field] === true);
898
+ let query: Record<string, any> = {};
899
+ let basis = 'all_records_default_active';
900
+ if (hasAnyActiveField) {
901
+ const activeBranches: Record<string, any>[] = [
902
+ { status: { $regex: '^active$', $options: 'i' } },
903
+ { active: true },
904
+ { is_active: true },
905
+ { isactive: true },
906
+ { enabled: true },
907
+ { is_enabled: true },
908
+ { isenabled: true }
909
+ ];
910
+ if (!['clients', 'customers'].includes(collectionFamily)) {
911
+ activeBranches.splice(1, 0, { state: { $regex: '^active$', $options: 'i' } });
912
+ }
913
+ query = {
914
+ $or: activeBranches
915
+ };
916
+ basis = 'status_or_active_field';
917
+ }
918
+ const count = await context.db.collection(collection).countDocuments(query);
919
+ const rows = await context.db.collection(collection).find(
920
+ query,
921
+ {
922
+ projection: {
923
+ status: 1,
924
+ state: 1,
925
+ active: 1,
926
+ is_active: 1,
927
+ isactive: 1,
928
+ enabled: 1,
929
+ is_enabled: 1,
930
+ isenabled: 1
931
+ },
932
+ limit: 50
933
+ }
934
+ ).toArray();
935
+ return { rows, meta: { activeCount: count, collection, basis } };
936
+ },
937
+ compare: ({ toolResult, assistantContent, expected }) => {
938
+ const errors: string[] = [];
939
+ const expectedCount = Number(expected?.meta?.activeCount || 0);
940
+ const displayCount = parseSingleCountFromDisplay(toolResult?.output?.display);
941
+ const actualTotal = typeof toolResult?.output?.total === 'number'
942
+ ? toolResult.output.total
943
+ : (displayCount !== null ? displayCount : Number(toolResult?.output?.rowCount || 0));
944
+ if (actualTotal !== expectedCount) {
945
+ errors.push(
946
+ `Active client count mismatch: expected=${expectedCount}, ai=${actualTotal}, basis=${String(expected?.meta?.basis || 'unknown')}, collection=${String(expected?.meta?.collection || 'unknown')}.`
947
+ );
948
+ }
949
+ errors.push(...collectCommonFormattingErrors(assistantContent));
950
+ return errors;
951
+ }
952
+ },
953
+ {
954
+ id: 'wo_last20_this_week_group_status',
955
+ prompt: 'Show me the last 20 work orders created this week, grouped by status.',
956
+ runExpected: async (context) => {
957
+ const rows = await context.db.collection('work-order-dynamics').aggregate([
958
+ {
959
+ $match: {
960
+ date_created: { $gte: context.startOfWeek, $lt: context.now }
961
+ }
962
+ },
963
+ { $sort: { date_created: -1 } },
964
+ { $limit: 20 },
965
+ {
966
+ $group: {
967
+ _id: { $ifNull: ['$status', 'Unknown'] },
968
+ work_order_count: { $sum: 1 }
969
+ }
970
+ },
971
+ { $sort: { work_order_count: -1, _id: 1 } },
972
+ {
973
+ $project: {
974
+ _id: 0,
975
+ status: '$_id',
976
+ work_order_count: 1
977
+ }
978
+ }
979
+ ]).toArray();
980
+ return { rows };
981
+ },
982
+ compare: ({ toolResult, assistantContent, expected }) => {
983
+ const errors: string[] = [];
984
+ const expectedMap = mapExpectedStatusCounts(expected.rows || []);
985
+ const actualRowCount = Number(toolResult?.output?.rowCount || 0);
986
+ if (expectedMap.size === 0 && actualRowCount === 0) {
987
+ // No rows in either source; status grouping table may be absent by design.
988
+ }
989
+ else {
990
+ const actualMap = mapDisplayStatusCounts(toolResult?.output?.display);
991
+ if (!actualMap) {
992
+ errors.push('Could not parse status/count columns from AI display.');
993
+ }
994
+ else {
995
+ errors.push(...compareNumericMaps('Status counts', expectedMap, actualMap, 0));
996
+ }
997
+ }
998
+ errors.push(...collectCommonFormattingErrors(assistantContent));
999
+ return errors;
1000
+ }
1001
+ },
1002
+ {
1003
+ id: 'wo_last20_this_week_by_status_alias',
1004
+ prompt: 'Show me the last 20 work orders created this week by status.',
1005
+ runExpected: async (context) => {
1006
+ const rows = await context.db.collection('work-order-dynamics').aggregate([
1007
+ {
1008
+ $match: {
1009
+ date_created: { $gte: context.startOfWeek, $lt: context.now }
1010
+ }
1011
+ },
1012
+ { $sort: { date_created: -1 } },
1013
+ { $limit: 20 },
1014
+ {
1015
+ $group: {
1016
+ _id: { $ifNull: ['$status', 'Unknown'] },
1017
+ work_order_count: { $sum: 1 }
1018
+ }
1019
+ },
1020
+ { $sort: { work_order_count: -1, _id: 1 } },
1021
+ {
1022
+ $project: {
1023
+ _id: 0,
1024
+ status: '$_id',
1025
+ work_order_count: 1
1026
+ }
1027
+ }
1028
+ ]).toArray();
1029
+ return { rows };
1030
+ },
1031
+ compare: ({ toolResult, assistantContent, expected }) => {
1032
+ const errors: string[] = [];
1033
+ const expectedMap = mapExpectedStatusCounts(expected.rows || []);
1034
+ const actualRowCount = Number(toolResult?.output?.rowCount || 0);
1035
+ if (expectedMap.size === 0 && actualRowCount === 0) {
1036
+ // No rows in either source; status grouping table may be absent by design.
1037
+ }
1038
+ else {
1039
+ const actualMap = mapDisplayStatusCounts(toolResult?.output?.display);
1040
+ if (!actualMap) {
1041
+ errors.push('Could not parse status/count columns from AI display.');
1042
+ }
1043
+ else {
1044
+ errors.push(...compareNumericMaps('Status counts', expectedMap, actualMap, 0));
1045
+ }
1046
+ }
1047
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1048
+ return errors;
1049
+ }
1050
+ },
1051
+ {
1052
+ id: 'wo_completed_per_day_last_30d',
1053
+ prompt: 'For the last 30 days, how many work orders were completed per day?',
1054
+ runExpected: async (context) => {
1055
+ const last30Days = addDaysUtc(context.now, -30);
1056
+ const rows = await context.db.collection('work-order-dynamics').aggregate([
1057
+ {
1058
+ $match: {
1059
+ date_completed: { $gte: last30Days, $lt: context.now }
1060
+ }
1061
+ },
1062
+ {
1063
+ $group: {
1064
+ _id: {
1065
+ $dateToString: { format: '%Y-%m-%d', date: '$date_completed', timezone: 'UTC' }
1066
+ },
1067
+ completed_work_orders: { $sum: 1 }
1068
+ }
1069
+ },
1070
+ { $sort: { _id: 1 } },
1071
+ {
1072
+ $project: {
1073
+ _id: 0,
1074
+ day_utc: '$_id',
1075
+ completed_work_orders: 1
1076
+ }
1077
+ }
1078
+ ]).toArray();
1079
+ return { rows };
1080
+ },
1081
+ compare: ({ toolResult, assistantContent, expected }) => {
1082
+ const errors: string[] = [];
1083
+ const expectedRows = Array.isArray(expected.rows) ? expected.rows.length : 0;
1084
+ const actualRows = Number(toolResult?.output?.rowCount || 0);
1085
+ if (expectedRows === 0 && actualRows === 0) {
1086
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1087
+ return errors;
1088
+ }
1089
+ const expectedMap = mapExpectedByDimension(expected.rows || [], ['day_utc', 'day'], ['completed_work_orders', 'work_order_count', 'count']);
1090
+ const actualMap = mapDisplayByDimension(toolResult?.output?.display, ['day', 'day_utc', 'date'], ['completed', 'count', 'work_order_count', 'total']);
1091
+ if (!expectedMap || !actualMap) {
1092
+ errors.push('Could not parse day/count map for completed work orders.');
1093
+ }
1094
+ else {
1095
+ errors.push(...compareNumericMaps('Completed work orders per day', expectedMap, actualMap, 0));
1096
+ }
1097
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1098
+ return errors;
1099
+ }
1100
+ },
1101
+ {
1102
+ id: 'wo_top_customers_last_6m',
1103
+ prompt: 'List the top 10 customers by number of work orders in the last 6 months.',
1104
+ runExpected: async (context) => {
1105
+ const collectionRows = await context.db.listCollections({}, { nameOnly: true }).toArray();
1106
+ const collectionNames = new Set(collectionRows.map((row: any) => normalizeOptionalString(row?.name)));
1107
+ const collectionCandidates = ['work-order-dynamics', 'maintenance-orders', 'orders']
1108
+ .filter(name => collectionNames.has(name));
1109
+ if (!collectionCandidates.length) {
1110
+ return {
1111
+ rows: [],
1112
+ meta: { collection: null, basis: 'missing_collection' }
1113
+ };
1114
+ }
1115
+ for (const candidate of collectionCandidates) {
1116
+ const rows = await context.db.collection(candidate).aggregate([
1117
+ {
1118
+ $addFields: {
1119
+ effective_date: {
1120
+ $ifNull: [
1121
+ '$date_created',
1122
+ { $ifNull: ['$date_create', '$createdAt'] }
1123
+ ]
1124
+ },
1125
+ customer_label: {
1126
+ $ifNull: [
1127
+ '$customer',
1128
+ {
1129
+ $ifNull: [
1130
+ '$customer_name',
1131
+ {
1132
+ $ifNull: [
1133
+ '$client_name',
1134
+ {
1135
+ $ifNull: [
1136
+ { $toString: '$id_customer' },
1137
+ { $ifNull: ['$qb_ListID_class', 'Unknown'] }
1138
+ ]
1139
+ }
1140
+ ]
1141
+ }
1142
+ ]
1143
+ }
1144
+ ]
1145
+ }
1146
+ }
1147
+ },
1148
+ {
1149
+ $match: {
1150
+ effective_date: { $gte: context.last6MonthsStart, $lt: context.now },
1151
+ 'deleted.date': { $exists: false }
1152
+ }
1153
+ },
1154
+ {
1155
+ $group: {
1156
+ _id: '$customer_label',
1157
+ work_order_count: { $sum: 1 }
1158
+ }
1159
+ },
1160
+ { $sort: { work_order_count: -1, _id: 1 } },
1161
+ { $limit: 10 },
1162
+ {
1163
+ $project: {
1164
+ _id: 0,
1165
+ customer: '$_id',
1166
+ work_order_count: 1
1167
+ }
1168
+ }
1169
+ ]).toArray();
1170
+ if (rows.length) {
1171
+ return { rows, meta: { collection: candidate, basis: 'first_non_empty_candidate' } };
1172
+ }
1173
+ }
1174
+ return { rows: [], meta: { collection: collectionCandidates[0], basis: 'zero_rows_all_candidates' } };
1175
+ },
1176
+ compare: ({ toolResult, assistantContent, expected }) => {
1177
+ const errors: string[] = [];
1178
+ const expectedRows = Array.isArray(expected.rows) ? expected.rows.length : 0;
1179
+ const actualRows = Number(toolResult?.output?.rowCount || 0);
1180
+ if (expectedRows === 0 && actualRows === 0) {
1181
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1182
+ return errors;
1183
+ }
1184
+ if (expectedRows > 0 && actualRows === 0) {
1185
+ errors.push(`Expected top-customer work-order rows (${expectedRows}), but AI returned zero rows.`);
1186
+ }
1187
+ const expectedMap = mapExpectedByDimension(expected.rows || [], ['customer'], ['work_order_count', 'count']);
1188
+ const actualMap = mapDisplayByDimension(toolResult?.output?.display, ['customer', 'client', 'account'], ['work_order_count', 'count', 'total']);
1189
+ if (!expectedMap || !actualMap) {
1190
+ errors.push('Could not parse customer/count map for top work-order customers.');
1191
+ }
1192
+ else {
1193
+ errors.push(...compareNumericMaps('Top customers by work-order count', expectedMap, actualMap, 0));
1194
+ }
1195
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1196
+ return errors;
1197
+ }
1198
+ },
1199
+ {
1200
+ id: 'blend_last10_summary',
1201
+ prompt: 'For blending: summarize the last 10 blend tickets with product, total volume, and created date.',
1202
+ runExpected: async (context) => {
1203
+ const rows = await context.db.collection('chemical-blends').aggregate([
1204
+ { $sort: { date: -1, createdAt: -1 } },
1205
+ { $limit: 10 },
1206
+ {
1207
+ $project: {
1208
+ _id: 1,
1209
+ product: { $ifNull: ['$blend_name', '$chemical'] },
1210
+ total_volume: '$chemical_recipe_quantity',
1211
+ volume_unit: { $cond: [{ $eq: ['$blend_in_pounds', true] }, 'lb', 'gal'] },
1212
+ created_date: { $ifNull: ['$date', '$createdAt'] },
1213
+ batch_number: 1
1214
+ }
1215
+ }
1216
+ ]).toArray();
1217
+ return { rows };
1218
+ },
1219
+ compare: ({ toolResult, assistantContent, expected }) => {
1220
+ const errors: string[] = [];
1221
+ const expectedCount = Array.isArray(expected.rows) ? expected.rows.length : 0;
1222
+ const actualCount = Number(toolResult?.output?.rowCount || 0);
1223
+ if (actualCount !== expectedCount) {
1224
+ errors.push(`Blend last-10 row count mismatch: expected=${expectedCount}, ai=${actualCount}.`);
1225
+ }
1226
+ const normalizedColumns = (toolResult?.output?.columns || []).map((column: string) => toNormalizedKey(column));
1227
+ if (!normalizedColumns.some((column: string) => column.includes('product') || column.includes('blend') || column.includes('chemical'))) {
1228
+ errors.push('Blend summary missing a product/blend column.');
1229
+ }
1230
+ if (!normalizedColumns.some((column: string) => column.includes('volume') || column.includes('quantity'))) {
1231
+ errors.push('Blend summary missing a volume column.');
1232
+ }
1233
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1234
+ return errors;
1235
+ }
1236
+ },
1237
+ {
1238
+ id: 'invoice_top_customers_total_last_6m',
1239
+ prompt: 'Show top 10 customers by invoice total in the last 6 months.',
1240
+ runExpected: async (context) => {
1241
+ const rows = await context.db.collection('invoices').aggregate([
1242
+ {
1243
+ $addFields: {
1244
+ effective_date: {
1245
+ $ifNull: [
1246
+ '$date_paid',
1247
+ { $ifNull: ['$date_invoiced', '$createdAt'] }
1248
+ ]
1249
+ },
1250
+ effective_total: { $ifNull: ['$paid_total', '$grand_total'] },
1251
+ customer_label: {
1252
+ $ifNull: [
1253
+ '$customer_name',
1254
+ {
1255
+ $ifNull: [
1256
+ '$customer.fullname',
1257
+ {
1258
+ $ifNull: [
1259
+ '$customer.name',
1260
+ {
1261
+ $ifNull: [
1262
+ '$customer',
1263
+ { $ifNull: ['$client_name', { $ifNull: ['$client.fullname', { $ifNull: ['$client.name', { $ifNull: ['$client', 'Unknown Customer'] }] }] }] }
1264
+ ]
1265
+ }
1266
+ ]
1267
+ }
1268
+ ]
1269
+ }
1270
+ ]
1271
+ }
1272
+ }
1273
+ },
1274
+ {
1275
+ $match: {
1276
+ effective_date: { $gte: context.last6MonthsStart, $lt: context.now }
1277
+ }
1278
+ },
1279
+ {
1280
+ $group: {
1281
+ _id: '$customer_label',
1282
+ invoice_total: { $sum: { $ifNull: ['$effective_total', 0] } },
1283
+ invoice_count: { $sum: 1 }
1284
+ }
1285
+ },
1286
+ {
1287
+ $project: {
1288
+ _id: 0,
1289
+ customer: '$_id',
1290
+ invoice_total: 1,
1291
+ invoice_count: 1
1292
+ }
1293
+ },
1294
+ { $sort: { invoice_total: -1, invoice_count: -1 } },
1295
+ { $limit: 10 }
1296
+ ]).toArray();
1297
+ return { rows };
1298
+ },
1299
+ compare: ({ toolResult, assistantContent, expected }) => {
1300
+ const errors: string[] = [];
1301
+ const expectedMap = mapExpectedByDimension(expected.rows || [], ['customer'], ['invoice_total', 'total_revenue', 'total']);
1302
+ const actualMap = mapDisplayByDimension(toolResult?.output?.display, ['customer', 'client', 'account'], ['invoice_total', 'total_revenue', 'total', 'revenue']);
1303
+ if (!expectedMap || !actualMap) {
1304
+ errors.push('Could not parse customer/total map for invoice top customers.');
1305
+ }
1306
+ else {
1307
+ errors.push(...compareNumericMaps('Top customers by invoice total', expectedMap, actualMap, 0.05));
1308
+ }
1309
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1310
+ return errors;
1311
+ }
1312
+ },
1313
+ {
1314
+ id: 'deliveries_per_driver_last_month',
1315
+ prompt: 'Break down the number of deliveries per driver last month',
1316
+ runExpected: async (context) => {
1317
+ const thisMonthStart = startOfMonthUtc(context.now);
1318
+ const lastMonthStart = startOfMonthUtc(addMonthsUtc(thisMonthStart, -1));
1319
+ const rows = await context.db.collection('work-order-dynamics').aggregate([
1320
+ {
1321
+ $match: {
1322
+ date_completed: { $gte: lastMonthStart, $lt: thisMonthStart },
1323
+ status: { $in: ['Completed', 'Closed'] }
1324
+ }
1325
+ },
1326
+ { $unwind: { path: '$drivers', preserveNullAndEmptyArrays: true } },
1327
+ {
1328
+ $group: {
1329
+ _id: { $ifNull: ['$drivers.user', 'Unassigned'] },
1330
+ delivery_count: { $sum: 1 }
1331
+ }
1332
+ },
1333
+ { $sort: { delivery_count: -1, _id: 1 } },
1334
+ {
1335
+ $project: {
1336
+ _id: 0,
1337
+ driver: '$_id',
1338
+ delivery_count: 1
1339
+ }
1340
+ }
1341
+ ]).toArray();
1342
+ return { rows };
1343
+ },
1344
+ compare: ({ toolResult, assistantContent, expected }) => {
1345
+ const errors: string[] = [];
1346
+ const expectedMap = mapExpectedByDimension(expected.rows || [], ['driver', 'user'], ['delivery_count', 'count']);
1347
+ const actualMap = mapDisplayByDimension(toolResult?.output?.display, ['driver', 'user', 'name'], ['delivery_count', 'count', 'total']);
1348
+ if (!expectedMap || !actualMap) {
1349
+ errors.push('Could not parse driver/delivery-count map.');
1350
+ }
1351
+ else {
1352
+ errors.push(...compareNumericMaps('Deliveries per driver', expectedMap, actualMap, 0));
1353
+ }
1354
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1355
+ return errors;
1356
+ }
1357
+ },
1358
+ {
1359
+ id: 'invoice_revenue_monthly_grid_last_6m',
1360
+ prompt: 'Break down my total revenue over the last 6 month by month',
1361
+ runExpected: async (context) => {
1362
+ const rows = await context.db.collection('invoices').aggregate([
1363
+ {
1364
+ $addFields: {
1365
+ effective_date: {
1366
+ $ifNull: [
1367
+ '$date_paid',
1368
+ { $ifNull: ['$date_invoiced', '$createdAt'] }
1369
+ ]
1370
+ },
1371
+ effective_total: { $ifNull: ['$paid_total', '$grand_total'] }
1372
+ }
1373
+ },
1374
+ {
1375
+ $match: {
1376
+ $expr: {
1377
+ $and: [
1378
+ { $gte: ['$effective_date', context.last6FullMonthsStart] },
1379
+ { $lt: ['$effective_date', context.startOfCurrentMonth] }
1380
+ ]
1381
+ }
1382
+ }
1383
+ },
1384
+ {
1385
+ $group: {
1386
+ _id: {
1387
+ bucket: {
1388
+ $dateTrunc: {
1389
+ date: '$effective_date',
1390
+ unit: 'month'
1391
+ }
1392
+ }
1393
+ },
1394
+ total_revenue: {
1395
+ $sum: { $ifNull: ['$effective_total', 0] }
1396
+ }
1397
+ }
1398
+ },
1399
+ {
1400
+ $project: {
1401
+ _id: 0,
1402
+ month: { $dateToString: { format: '%Y-%m', date: '$_id.bucket' } },
1403
+ total_revenue: 1
1404
+ }
1405
+ },
1406
+ { $sort: { month: 1 } }
1407
+ ]).toArray();
1408
+ return { rows };
1409
+ },
1410
+ compare: ({ toolResult, assistantContent, expected }) => {
1411
+ const errors: string[] = [];
1412
+ const expectedDisplay = buildDisplayTable(expected.rows || [], { maxColumns: 40, maxRows: 1000 });
1413
+ const expectedPivot = buildAssistantDatedPivotDisplay(expectedDisplay);
1414
+ const expectedTotals = expectedPivot
1415
+ ? parseMonthlyTotalsFromDisplay(expectedPivot)
1416
+ : parseMonthlyTotalsFromDisplay(expectedDisplay);
1417
+ const actualTotals = parseMonthlyTotalsFromDisplay(toolResult?.output?.display);
1418
+ if (!expectedTotals || !actualTotals) {
1419
+ errors.push('Could not parse monthly totals for expected or AI display.');
1420
+ }
1421
+ else {
1422
+ errors.push(...compareNumericMaps('Monthly revenue totals', expectedTotals, actualTotals, 0.05));
1423
+ }
1424
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1425
+ return errors;
1426
+ }
1427
+ },
1428
+ {
1429
+ id: 'invoice_revenue_monthly_customer_grid_last_6m',
1430
+ prompt: 'Break down my total revenue over the last 6 month by month by each customer',
1431
+ runExpected: async (context) => {
1432
+ const rows = await context.db.collection('invoices').aggregate([
1433
+ {
1434
+ $addFields: {
1435
+ effective_date: {
1436
+ $ifNull: [
1437
+ '$date_paid',
1438
+ { $ifNull: ['$date_invoiced', '$createdAt'] }
1439
+ ]
1440
+ },
1441
+ effective_total: { $ifNull: ['$paid_total', '$grand_total'] }
1442
+ }
1443
+ },
1444
+ {
1445
+ $match: {
1446
+ $expr: {
1447
+ $and: [
1448
+ { $gte: ['$effective_date', context.last6FullMonthsStart] },
1449
+ { $lt: ['$effective_date', context.startOfCurrentMonth] }
1450
+ ]
1451
+ }
1452
+ }
1453
+ },
1454
+ {
1455
+ $group: {
1456
+ _id: {
1457
+ customer: {
1458
+ $ifNull: [
1459
+ '$customer_name',
1460
+ {
1461
+ $ifNull: [
1462
+ '$customer.fullname',
1463
+ {
1464
+ $ifNull: [
1465
+ '$customer.name',
1466
+ {
1467
+ $ifNull: [{ $toString: '$id_customer' }, 'Unknown Customer']
1468
+ }
1469
+ ]
1470
+ }
1471
+ ]
1472
+ }
1473
+ ]
1474
+ },
1475
+ bucket: {
1476
+ $dateTrunc: {
1477
+ date: '$effective_date',
1478
+ unit: 'month'
1479
+ }
1480
+ }
1481
+ },
1482
+ total_revenue: { $sum: { $ifNull: ['$effective_total', 0] } }
1483
+ }
1484
+ },
1485
+ {
1486
+ $project: {
1487
+ _id: 0,
1488
+ customer: '$_id.customer',
1489
+ month: { $dateToString: { format: '%Y-%m', date: '$_id.bucket' } },
1490
+ total_revenue: 1
1491
+ }
1492
+ },
1493
+ { $sort: { customer: 1, month: 1 } }
1494
+ ]).toArray();
1495
+ return { rows };
1496
+ },
1497
+ compare: ({ toolResult, assistantContent, expected }) => {
1498
+ const errors: string[] = [];
1499
+ const expectedDisplay = buildDisplayTable(expected.rows || [], { maxColumns: 50, maxRows: 5000 });
1500
+ const expectedPivot = buildAssistantDatedPivotDisplay(expectedDisplay) || expectedDisplay;
1501
+ const expectedParsed = parseCustomerMonthTotalsFromDisplay(expectedPivot);
1502
+ const actualParsed = parseCustomerMonthTotalsFromDisplay(toolResult?.output?.display);
1503
+ if (!expectedParsed || !actualParsed) {
1504
+ errors.push('Could not parse customer-month totals for expected or AI display.');
1505
+ }
1506
+ else {
1507
+ errors.push(...compareNumericMaps('Customer monthly totals (summed by month)', expectedParsed.monthTotals, actualParsed.monthTotals, 0.05));
1508
+ if (expectedParsed.nonUnknownCustomers.size >= 2 && actualParsed.nonUnknownCustomers.size < 2) {
1509
+ errors.push(
1510
+ `Expected multiple named customers (${expectedParsed.nonUnknownCustomers.size}), but AI returned ${actualParsed.nonUnknownCustomers.size}.`
1511
+ );
1512
+ }
1513
+ const expectedCustomerCount = new Set(expectedParsed.entries.map(entry => entry.customer.toLowerCase())).size;
1514
+ const actualCustomerCount = new Set(actualParsed.entries.map(entry => entry.customer.toLowerCase())).size;
1515
+ const outputRowCount = Number(toolResult?.output?.rowCount || 0);
1516
+ const outputDisplayRows = Array.isArray(toolResult?.output?.display?.rows) ? toolResult.output.display.rows.length : 0;
1517
+ const outputTruncated = toolResult?.output?.truncated === true || toolResult?.output?.display?.truncated === true;
1518
+ if (outputTruncated && outputRowCount > outputDisplayRows && expectedCustomerCount > actualCustomerCount) {
1519
+ errors.push(
1520
+ `Customer breakdown display is truncated (${outputDisplayRows} shown of ${outputRowCount} rows); expected ${expectedCustomerCount} customers but output shows ${actualCustomerCount}.`
1521
+ );
1522
+ }
1523
+ }
1524
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1525
+ return errors;
1526
+ }
1527
+ },
1528
+ {
1529
+ id: 'invoice_revenue_monthly_customer_grid_last_6m_per_customer',
1530
+ prompt: 'Break down my total revenue over the last 6 month per month per customer',
1531
+ runExpected: async (context) => {
1532
+ const rows = await context.db.collection('invoices').aggregate([
1533
+ {
1534
+ $addFields: {
1535
+ effective_date: {
1536
+ $ifNull: [
1537
+ '$date_paid',
1538
+ { $ifNull: ['$date_invoiced', '$createdAt'] }
1539
+ ]
1540
+ },
1541
+ effective_total: { $ifNull: ['$paid_total', '$grand_total'] }
1542
+ }
1543
+ },
1544
+ {
1545
+ $match: {
1546
+ $expr: {
1547
+ $and: [
1548
+ { $gte: ['$effective_date', context.last6FullMonthsStart] },
1549
+ { $lt: ['$effective_date', context.startOfCurrentMonth] }
1550
+ ]
1551
+ }
1552
+ }
1553
+ },
1554
+ {
1555
+ $group: {
1556
+ _id: {
1557
+ customer: {
1558
+ $ifNull: [
1559
+ '$customer_name',
1560
+ {
1561
+ $ifNull: [
1562
+ '$customer.fullname',
1563
+ {
1564
+ $ifNull: [
1565
+ '$customer.name',
1566
+ {
1567
+ $ifNull: [{ $toString: '$id_customer' }, 'Unknown Customer']
1568
+ }
1569
+ ]
1570
+ }
1571
+ ]
1572
+ }
1573
+ ]
1574
+ },
1575
+ bucket: {
1576
+ $dateTrunc: {
1577
+ date: '$effective_date',
1578
+ unit: 'month'
1579
+ }
1580
+ }
1581
+ },
1582
+ total_revenue: { $sum: { $ifNull: ['$effective_total', 0] } }
1583
+ }
1584
+ },
1585
+ {
1586
+ $project: {
1587
+ _id: 0,
1588
+ customer: '$_id.customer',
1589
+ month: { $dateToString: { format: '%Y-%m', date: '$_id.bucket' } },
1590
+ total_revenue: 1
1591
+ }
1592
+ },
1593
+ { $sort: { customer: 1, month: 1 } }
1594
+ ]).toArray();
1595
+ return { rows };
1596
+ },
1597
+ compare: ({ toolResult, assistantContent, expected }) => {
1598
+ const errors: string[] = [];
1599
+ const expectedDisplay = buildDisplayTable(expected.rows || [], { maxColumns: 50, maxRows: 5000 });
1600
+ const expectedPivot = buildAssistantDatedPivotDisplay(expectedDisplay) || expectedDisplay;
1601
+ const expectedParsed = parseCustomerMonthTotalsFromDisplay(expectedPivot);
1602
+ const actualParsed = parseCustomerMonthTotalsFromDisplay(toolResult?.output?.display);
1603
+ if (!expectedParsed || !actualParsed) {
1604
+ errors.push('Could not parse customer-month totals for expected or AI display.');
1605
+ }
1606
+ else {
1607
+ errors.push(...compareNumericMaps('Customer monthly totals (summed by month)', expectedParsed.monthTotals, actualParsed.monthTotals, 0.05));
1608
+ if (expectedParsed.nonUnknownCustomers.size >= 2 && actualParsed.nonUnknownCustomers.size < 2) {
1609
+ errors.push(
1610
+ `Expected multiple named customers (${expectedParsed.nonUnknownCustomers.size}), but AI returned ${actualParsed.nonUnknownCustomers.size}.`
1611
+ );
1612
+ }
1613
+ const expectedCustomerCount = new Set(expectedParsed.entries.map(entry => entry.customer.toLowerCase())).size;
1614
+ const actualCustomerCount = new Set(actualParsed.entries.map(entry => entry.customer.toLowerCase())).size;
1615
+ const outputRowCount = Number(toolResult?.output?.rowCount || 0);
1616
+ const outputDisplayRows = Array.isArray(toolResult?.output?.display?.rows) ? toolResult.output.display.rows.length : 0;
1617
+ const outputTruncated = toolResult?.output?.truncated === true || toolResult?.output?.display?.truncated === true;
1618
+ if (outputTruncated && outputRowCount > outputDisplayRows && expectedCustomerCount > actualCustomerCount) {
1619
+ errors.push(
1620
+ `Customer breakdown display is truncated (${outputDisplayRows} shown of ${outputRowCount} rows); expected ${expectedCustomerCount} customers but output shows ${actualCustomerCount}.`
1621
+ );
1622
+ }
1623
+ }
1624
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1625
+ return errors;
1626
+ }
1627
+ },
1628
+ {
1629
+ id: 'support_ticket_billable_hours_per_user_per_month_last_6m',
1630
+ prompt: 'Break up the support tickets last 6 months, give me a total per user per month of the amount of hours they are billing for',
1631
+ runExpected: async (context) => {
1632
+ const collection = await resolveExistingCollection(context.db, ['support-tickets', 'supporttickets', 'support-ticket']);
1633
+ if (!collection) {
1634
+ return {
1635
+ rows: [],
1636
+ meta: { collection: null, basis: 'missing_collection' }
1637
+ };
1638
+ }
1639
+ const rows = await context.db.collection(collection).aggregate([
1640
+ {
1641
+ $addFields: {
1642
+ effective_date: { $ifNull: ['$date_created', '$createdAt'] },
1643
+ effective_hours: { $ifNull: ['$billable_hours', { $ifNull: ['$estimated_billable_hours', 0] }] },
1644
+ assigned_user_first: {
1645
+ $let: {
1646
+ vars: {
1647
+ first_assigned: { $arrayElemAt: [{ $ifNull: ['$users_assigned', []] }, 0] }
1648
+ },
1649
+ in: {
1650
+ $ifNull: ['$$first_assigned.user', '$$first_assigned.id_user']
1651
+ }
1652
+ }
1653
+ }
1654
+ }
1655
+ },
1656
+ {
1657
+ $match: {
1658
+ $expr: {
1659
+ $and: [
1660
+ { $gte: ['$effective_date', context.last6FullMonthsStart] },
1661
+ { $lt: ['$effective_date', context.startOfCurrentMonth] }
1662
+ ]
1663
+ }
1664
+ }
1665
+ },
1666
+ {
1667
+ $group: {
1668
+ _id: {
1669
+ user: {
1670
+ $ifNull: [
1671
+ '$user_created',
1672
+ {
1673
+ $ifNull: [
1674
+ '$client_user.user',
1675
+ {
1676
+ $ifNull: [
1677
+ '$assigned_user_first',
1678
+ { $ifNull: [{ $toString: '$id_user_created' }, 'Unknown User'] }
1679
+ ]
1680
+ }
1681
+ ]
1682
+ }
1683
+ ]
1684
+ },
1685
+ bucket: {
1686
+ $dateTrunc: {
1687
+ date: '$effective_date',
1688
+ unit: 'month'
1689
+ }
1690
+ }
1691
+ },
1692
+ billable_hours: { $sum: { $ifNull: ['$effective_hours', 0] } }
1693
+ }
1694
+ },
1695
+ {
1696
+ $project: {
1697
+ _id: 0,
1698
+ user: '$_id.user',
1699
+ month: { $dateToString: { format: '%Y-%m', date: '$_id.bucket' } },
1700
+ billable_hours: 1
1701
+ }
1702
+ },
1703
+ { $sort: { user: 1, month: 1 } }
1704
+ ]).toArray();
1705
+ return {
1706
+ rows,
1707
+ meta: { collection, basis: 'billable_hours_or_estimated_billable_hours' }
1708
+ };
1709
+ },
1710
+ compare: ({ toolResult, assistantContent, expected }) => {
1711
+ const errors: string[] = [];
1712
+ const collection = normalizeOptionalString(expected?.meta?.collection);
1713
+ if (!collection) {
1714
+ const rowCount = Number(toolResult?.output?.rowCount || 0);
1715
+ if (rowCount > 0) {
1716
+ errors.push(`Expected zero rows because support ticket collection is missing, but AI returned ${rowCount} rows.`);
1717
+ }
1718
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1719
+ return errors;
1720
+ }
1721
+ const expectedRows = Array.isArray(expected.rows) ? expected.rows.length : 0;
1722
+ const actualRowCount = Number(toolResult?.output?.rowCount || 0);
1723
+ if (expectedRows === 0 && actualRowCount === 0) {
1724
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1725
+ return errors;
1726
+ }
1727
+ const expectedDisplay = buildDisplayTable(expected.rows || [], { maxColumns: 60, maxRows: 5000 });
1728
+ const expectedPivot = buildAssistantDatedPivotDisplay(expectedDisplay) || expectedDisplay;
1729
+ const expectedParsed = parseDimensionMonthTotalsFromDisplay(expectedPivot, ['user', 'assignee', 'assigned', 'technician', 'owner']);
1730
+ const actualParsed = parseDimensionMonthTotalsFromDisplay(toolResult?.output?.display, ['user', 'assignee', 'assigned', 'technician', 'owner']);
1731
+ if (!expectedParsed || !actualParsed) {
1732
+ errors.push('Could not parse user-month billable-hours totals for expected or AI display.');
1733
+ }
1734
+ else {
1735
+ errors.push(...compareNumericMaps('Support ticket billable hours by month (summed)', expectedParsed.monthTotals, actualParsed.monthTotals, 0.05));
1736
+ errors.push(...compareNumericMaps('Support ticket billable hours by user (summed)', expectedParsed.dimensionTotals, actualParsed.dimensionTotals, 0.05));
1737
+ if (expectedParsed.nonUnknownDimensions.size >= 2 && actualParsed.nonUnknownDimensions.size < 2) {
1738
+ errors.push(
1739
+ `Expected multiple named users (${expectedParsed.nonUnknownDimensions.size}), but AI returned ${actualParsed.nonUnknownDimensions.size}.`
1740
+ );
1741
+ }
1742
+ }
1743
+ errors.push(...collectCommonFormattingErrors(assistantContent));
1744
+ return errors;
1745
+ }
1746
+ }
1747
+ ];
1748
+ }
1749
+
1750
+ async function runCase(context: ParityContext, parityCase: ParityCase): Promise<ParityCaseResult> {
1751
+ const result: ParityCaseResult = {
1752
+ id: parityCase.id,
1753
+ prompt: parityCase.prompt,
1754
+ pass: false,
1755
+ errors: []
1756
+ };
1757
+ try {
1758
+ const ai = await runPrompt(context.methodManager, context.db, context.idUser, parityCase.prompt, context.appId);
1759
+ result.conversationId = ai.conversationId;
1760
+ const assistantContent = String(ai.message?.content || '');
1761
+ const directive = extractDirectiveFromMessage(ai.message);
1762
+ if (directive) {
1763
+ result.directive = {
1764
+ type: directive.type,
1765
+ collection: normalizeOptionalString(directive.payload?.collection),
1766
+ source: directive.source
1767
+ };
1768
+ }
1769
+ let toolResult = ai.message?.metadata?.tool_result;
1770
+ let toolSource = 'metadata.tool_result';
1771
+ if (!toolResult?.input || !toolResult?.output) {
1772
+ if (!directive) {
1773
+ result.errors.push('No tool_result found in assistant metadata and no directive line was recoverable.');
1774
+ return result;
1775
+ }
1776
+ const replayMethod = directive.type === 'aggregate' ? 'aiAssistantMongoAggregate' : 'aiAssistantMongoRead';
1777
+ try {
1778
+ const directiveReplay = await callMethodAsUser(context.methodManager, context.idUser, replayMethod, directive.payload || {});
1779
+ toolResult = buildSyntheticToolResultFromDirective(directive, directiveReplay);
1780
+ toolSource = `synthetic_from_${directive.source}`;
1781
+ }
1782
+ catch (directiveError: any) {
1783
+ result.errors.push(
1784
+ `No tool_result found; directive replay failed: ${String(directiveError?.message || directiveError || 'unknown error')}`
1785
+ );
1786
+ return result;
1787
+ }
1788
+ }
1789
+ result.toolSource = toolSource;
1790
+ result.tool = {
1791
+ type: normalizeOptionalString(toolResult?.type),
1792
+ collection: normalizeOptionalString(toolResult?.output?.collection) || normalizeOptionalString(toolResult?.input?.collection),
1793
+ rowCount: Number(toolResult?.output?.rowCount || 0),
1794
+ total: typeof toolResult?.output?.total === 'number' ? toolResult.output.total : null,
1795
+ columns: Array.isArray(toolResult?.output?.columns) ? toolResult.output.columns : []
1796
+ };
1797
+
1798
+ if (toolSource === 'metadata.tool_result') {
1799
+ const replayMethod = toolResult.type === 'mongo_agg' ? 'aiAssistantMongoAggregate' : 'aiAssistantMongoRead';
1800
+ const replay = await callMethodAsUser(context.methodManager, context.idUser, replayMethod, toolResult.input || {});
1801
+ const replayErrors = compareDisplayParity(toolResult?.output?.display, replay?.display);
1802
+ const toolRowCount = Number(toolResult?.output?.rowCount || 0);
1803
+ const replayRowCount = Array.isArray(replay?.documents)
1804
+ ? replay.documents.length
1805
+ : Number(replay?.display?.rowCount || 0);
1806
+ if (toolRowCount > 0 && replayRowCount > 0 && toolRowCount !== replayRowCount) {
1807
+ replayErrors.push(`Replay rowCount mismatch: tool_result=${toolRowCount}, replay=${replayRowCount}.`);
1808
+ }
1809
+ result.replay = {
1810
+ pass: replayErrors.length === 0,
1811
+ errors: replayErrors
1812
+ };
1813
+ if (replayErrors.length) {
1814
+ result.errors.push(...replayErrors);
1815
+ }
1816
+ }
1817
+ else {
1818
+ result.replay = {
1819
+ pass: true,
1820
+ errors: []
1821
+ };
1822
+ }
1823
+
1824
+ const expected = await parityCase.runExpected(context);
1825
+ result.expected = {
1826
+ rows: Array.isArray(expected?.rows) ? expected.rows.length : 0,
1827
+ meta: expected?.meta
1828
+ };
1829
+ const caseErrors = parityCase.compare({
1830
+ toolResult,
1831
+ assistantContent,
1832
+ expected,
1833
+ context
1834
+ });
1835
+ if (caseErrors.length) {
1836
+ result.errors.push(...caseErrors);
1837
+ }
1838
+
1839
+ result.pass = result.errors.length === 0;
1840
+ return result;
1841
+ }
1842
+ catch (error: any) {
1843
+ result.errors.push(String(error?.message || error || 'Unknown error'));
1844
+ result.pass = false;
1845
+ return result;
1846
+ }
1847
+ }
1848
+
1849
+ async function run(): Promise<void> {
1850
+ const repoRoot = process.cwd();
1851
+ const defaultServerDir = path.resolve(repoRoot, '../resolveio-all/geochem/server');
1852
+ const serverDir = normalizeOptionalString(process.env.AI_ASSISTANT_PARITY_SERVER_DIR) || defaultServerDir;
1853
+ const settingsPath = path.join(serverDir, normalizeOptionalString(process.env.AI_ASSISTANT_PARITY_SETTINGS) || 'settings.local.json');
1854
+ const appId = normalizeOptionalString(process.env.AI_ASSISTANT_PARITY_APP_ID) || path.basename(path.dirname(serverDir));
1855
+ const dryRun = parseBoolean(process.env.AI_ASSISTANT_PARITY_DRY_RUN);
1856
+
1857
+ if (!fs.existsSync(settingsPath)) {
1858
+ throw new Error(`Settings file not found: ${settingsPath}`);
1859
+ }
1860
+
1861
+ const allCases = resolveParityCases();
1862
+ const caseFilter = normalizeOptionalString(process.env.AI_ASSISTANT_PARITY_CASES)
1863
+ .split(',')
1864
+ .map(value => value.trim())
1865
+ .filter(Boolean);
1866
+ const selectedCases = caseFilter.length
1867
+ ? allCases.filter(parityCase => caseFilter.includes(parityCase.id))
1868
+ : allCases;
1869
+ if (!selectedCases.length) {
1870
+ throw new Error('No parity cases selected.');
1871
+ }
1872
+
1873
+ if (dryRun) {
1874
+ console.log(JSON.stringify({
1875
+ mode: 'dry-run',
1876
+ serverDir,
1877
+ settingsPath,
1878
+ appId,
1879
+ cases: selectedCases.map(parityCase => parityCase.id)
1880
+ }, null, 2));
1881
+ return;
1882
+ }
1883
+
1884
+ applyEnvFromServerDir(serverDir);
1885
+ process.env.AI_ASSISTANT_WORKSPACE_ROOT = process.env.AI_ASSISTANT_WORKSPACE_ROOT || path.resolve(serverDir, '../..');
1886
+ if (!process.env.OPENAI_API_KEY) {
1887
+ throw new Error('OPENAI_API_KEY is required for parity E2E run.');
1888
+ }
1889
+
1890
+ const runtimeTmpDir = path.join(serverDir, 'tmp');
1891
+ const runtimeSrcDir = path.join(serverDir, 'src');
1892
+ const runtimeDir = fs.existsSync(path.join(runtimeTmpDir, 'server-app.js')) ? runtimeTmpDir : runtimeSrcDir;
1893
+ const clientRoutesPath = path.join(runtimeDir, 'client-routes');
1894
+ if (!fs.existsSync(clientRoutesPath) && !fs.existsSync(`${clientRoutesPath}.js`)) {
1895
+ throw new Error(`client-routes module not found under runtime dir: ${runtimeDir}`);
1896
+ }
1897
+ const { CLIENT_ROUTES } = require(clientRoutesPath);
1898
+ const serverConfig = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
1899
+ const envDatabase = normalizeOptionalString(process.env.DATABASE);
1900
+ const envMongoUrl = normalizeOptionalString(process.env.MONGO_URL);
1901
+ const envOplogUrl = normalizeOptionalString(process.env.OPLOG_URL);
1902
+ if (envDatabase) {
1903
+ serverConfig.DATABASE = envDatabase;
1904
+ }
1905
+ if (envMongoUrl) {
1906
+ serverConfig.MONGO_URL = envMongoUrl;
1907
+ }
1908
+ if (envOplogUrl) {
1909
+ serverConfig.OPLOG_URL = envOplogUrl;
1910
+ }
1911
+
1912
+ const codexHome = process.env.CODEX_HOME || path.join(repoRoot, 'tmp', '.codex-ai-parity');
1913
+ const codexSessions = path.join(codexHome, 'sessions');
1914
+ const codexZdotdir = process.env.AI_DASHBOARD_CODEX_ZDOTDIR || path.join(repoRoot, 'tmp', '.codex-zdotdir-parity');
1915
+ fs.mkdirSync(codexSessions, { recursive: true });
1916
+ fs.mkdirSync(codexZdotdir, { recursive: true });
1917
+ process.env.CODEX_HOME = codexHome;
1918
+ process.env.AI_DASHBOARD_CODEX_ZDOTDIR = codexZdotdir;
1919
+ process.env.IS_WORKERS_ENABLED = 'false';
1920
+ process.env.IS_WORKER_INSTANCE = 'false';
1921
+ process.env.WORKER_INDEX = '';
1922
+ process.env.NODE_APP_INSTANCE = process.env.NODE_APP_INSTANCE || '8';
1923
+ process.env.AI_ASSISTANT_WORKER_DEBUG = process.env.AI_ASSISTANT_WORKER_DEBUG || 'false';
1924
+
1925
+ await ResolveIOServer.create(serverConfig, CLIENT_ROUTES(), appId.toUpperCase(), runtimeDir, false, false);
1926
+ const mainServer = ResolveIOServer.getMainServer();
1927
+ const methodManager = mainServer?.getMethodManager();
1928
+ if (!methodManager) {
1929
+ throw new Error('MethodManager unavailable after ResolveIOServer initialization.');
1930
+ }
1931
+ await methodManager.waitUntilReady(120000);
1932
+
1933
+ const dbName = normalizeOptionalString(serverConfig.DATABASE) || 'resolveio';
1934
+ const db = ResolveIOServer.getMongoConnection().db(dbName);
1935
+ const idUser = normalizeOptionalString(process.env.AI_ASSISTANT_PARITY_USER_ID) || await findSuperAdminUserId(db);
1936
+ const now = new Date();
1937
+
1938
+ const context: ParityContext = {
1939
+ db,
1940
+ now,
1941
+ startOfWeek: startOfWeekUtcMonday(now),
1942
+ last6MonthsStart: addMonthsUtc(now, -6),
1943
+ startOfCurrentMonth: startOfMonthUtc(now),
1944
+ last6FullMonthsStart: addMonthsUtc(startOfMonthUtc(now), -6),
1945
+ methodManager,
1946
+ idUser,
1947
+ appId
1948
+ };
1949
+
1950
+ const results: ParityCaseResult[] = [];
1951
+ for (const parityCase of selectedCases) {
1952
+ const caseResult = await runCase(context, parityCase);
1953
+ results.push(caseResult);
1954
+ const status = caseResult.pass ? 'PASS' : 'FAIL';
1955
+ const message = caseResult.errors.length ? `error=${caseResult.errors[0]}` : '';
1956
+ console.log(`${status} ${parityCase.id} ${message}`.trim());
1957
+ }
1958
+
1959
+ const passed = results.filter(result => result.pass).length;
1960
+ const failed = results.length - passed;
1961
+ const output = {
1962
+ now: now.toISOString(),
1963
+ serverDir,
1964
+ settingsPath,
1965
+ appId,
1966
+ database: dbName,
1967
+ total: results.length,
1968
+ passed,
1969
+ failed,
1970
+ results
1971
+ };
1972
+ const outputPath = path.join(repoRoot, 'tmp', 'ai-assistant-data-parity-e2e-output.json');
1973
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1974
+ fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
1975
+ console.log(`ai assistant data parity e2e: ${passed}/${results.length} passed`);
1976
+ console.log(`wrote ${outputPath}`);
1977
+ if (failed > 0) {
1978
+ process.exitCode = 1;
1979
+ }
1980
+ }
1981
+
1982
+ run()
1983
+ .then(() => {
1984
+ process.exit(process.exitCode ?? 0);
1985
+ })
1986
+ .catch((error) => {
1987
+ console.error(error);
1988
+ process.exit(1);
1989
+ });