@resolveio/server-lib 22.3.125 → 22.3.126

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 (735) 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 +77 -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 +57 -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 +23497 -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 +16 -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 +521 -0
  177. package/src/util/ai-run-evidence-dashboard.ts +271 -0
  178. package/src/util/ai-run-evidence-eval.ts +885 -0
  179. package/src/util/ai-run-evidence.ts +964 -0
  180. package/src/util/ai-runner-artifacts.ts +586 -0
  181. package/src/util/ai-runner-qa-auth.ts +821 -0
  182. package/src/util/ai-runner-qa-tools.ts +3045 -0
  183. package/src/util/aicoder-runner-v6.ts +526 -0
  184. package/src/util/common.ts +649 -0
  185. package/src/util/customer-portal-password.ts +183 -0
  186. package/src/util/error-reporter.ts +332 -0
  187. package/src/util/error-tracking.ts +79 -0
  188. package/src/util/openai-usage-cost.ts +114 -0
  189. package/src/util/report-builder-unwinds.ts +180 -0
  190. package/src/util/runner-process-janitor.ts +219 -0
  191. package/src/util/schema-report-builder.ts +448 -0
  192. package/src/util/slow-query-reporter.ts +216 -0
  193. package/src/util/subscription-dependency-context.ts +1096 -0
  194. package/src/util/support-runner-v5.ts +897 -0
  195. package/src/util/tokenizer.ts +38 -0
  196. package/src/workers/codex-runner.worker.ts +142 -0
  197. package/start_server.sh +5 -0
  198. package/tests/ai-assistant-corpus-build.ts +484 -0
  199. package/tests/ai-assistant-corpus-replay-e2e.ts +774 -0
  200. package/tests/ai-assistant-data-parity-e2e.ts +1989 -0
  201. package/tests/ai-assistant-eval-triage.ts +831 -0
  202. package/tests/ai-assistant-openai-e2e.ts +1061 -0
  203. package/tests/ai-assistant-openai-git-e2e.ts +155 -0
  204. package/tests/ai-assistant-preflight-matrix.ts +215 -0
  205. package/tests/ai-assistant-routing-eval.test.ts +560 -0
  206. package/tests/ai-assistant-snf-live-eval.ts +975 -0
  207. package/tests/ai-assistant-utils.test.ts +2968 -0
  208. package/tests/ai-run-eval.test.ts +88 -0
  209. package/tests/ai-run-evidence.test.ts +232 -0
  210. package/tests/ai-runner-contract.test.ts +488 -0
  211. package/tests/aicoder-runner-v6.test.ts +92 -0
  212. package/tests/error-reporter.test.ts +145 -0
  213. package/tests/method-publication-generator.test.ts +46 -0
  214. package/tests/report-builder-linking.test.ts +79 -0
  215. package/tests/resolveio-platform-intelligence.test.ts +352 -0
  216. package/tests/server-app-cron-owner.test.ts +127 -0
  217. package/tests/subscription-connect-race.test.ts +158 -0
  218. package/tests/subscription-dependency-context.test.ts +324 -0
  219. package/tests/subscription-manager-collection-tracking.test.ts +86 -0
  220. package/tests/subscription-manager-invalidation.test.ts +86 -0
  221. package/tests/support-runner-v5.test.ts +191 -0
  222. package/tsconfig.json +34 -0
  223. package/ai/assistant-core-heuristics.d.ts +0 -11
  224. package/ai/assistant-core-heuristics.js +0 -356
  225. package/ai/assistant-core-heuristics.js.map +0 -1
  226. package/ai/resolveio-platform-intelligence-memory-corpus.d.ts +0 -3
  227. package/ai/resolveio-platform-intelligence-memory-corpus.js +0 -214
  228. package/ai/resolveio-platform-intelligence-memory-corpus.js.map +0 -1
  229. package/ai/resolveio-platform-intelligence-memory.d.ts +0 -20
  230. package/ai/resolveio-platform-intelligence-memory.js +0 -341
  231. package/ai/resolveio-platform-intelligence-memory.js.map +0 -1
  232. package/ai/resolveio-platform-intelligence-types.js +0 -4
  233. package/ai/resolveio-platform-intelligence-types.js.map +0 -1
  234. package/ai/resolveio-platform-intelligence.d.ts +0 -6
  235. package/ai/resolveio-platform-intelligence.js +0 -463
  236. package/ai/resolveio-platform-intelligence.js.map +0 -1
  237. package/client-server-app.d.ts +0 -1
  238. package/client-server-app.js +0 -68
  239. package/client-server-app.js.map +0 -1
  240. package/collections/ai-run.collection.d.ts +0 -3
  241. package/collections/ai-run.collection.js +0 -170
  242. package/collections/ai-run.collection.js.map +0 -1
  243. package/collections/ai-terminal-conversation.collection.d.ts +0 -2
  244. package/collections/ai-terminal-conversation.collection.js +0 -140
  245. package/collections/ai-terminal-conversation.collection.js.map +0 -1
  246. package/collections/ai-terminal-issue-report.collection.d.ts +0 -2
  247. package/collections/ai-terminal-issue-report.collection.js +0 -148
  248. package/collections/ai-terminal-issue-report.collection.js.map +0 -1
  249. package/collections/ai-terminal-message.collection.d.ts +0 -2
  250. package/collections/ai-terminal-message.collection.js +0 -121
  251. package/collections/ai-terminal-message.collection.js.map +0 -1
  252. package/collections/app-setting.collection.d.ts +0 -3
  253. package/collections/app-setting.collection.js +0 -103
  254. package/collections/app-setting.collection.js.map +0 -1
  255. package/collections/app-status.collection.d.ts +0 -3
  256. package/collections/app-status.collection.js +0 -57
  257. package/collections/app-status.collection.js.map +0 -1
  258. package/collections/communication-metric.collection.d.ts +0 -2
  259. package/collections/communication-metric.collection.js +0 -133
  260. package/collections/communication-metric.collection.js.map +0 -1
  261. package/collections/counter.collection.d.ts +0 -3
  262. package/collections/counter.collection.js +0 -56
  263. package/collections/counter.collection.js.map +0 -1
  264. package/collections/cron-job-history.collection.d.ts +0 -3
  265. package/collections/cron-job-history.collection.js +0 -137
  266. package/collections/cron-job-history.collection.js.map +0 -1
  267. package/collections/cron-job.collection.d.ts +0 -3
  268. package/collections/cron-job.collection.js +0 -92
  269. package/collections/cron-job.collection.js.map +0 -1
  270. package/collections/customer-notification.collection.d.ts +0 -3
  271. package/collections/customer-notification.collection.js +0 -130
  272. package/collections/customer-notification.collection.js.map +0 -1
  273. package/collections/customer-portal-password.collection.d.ts +0 -3
  274. package/collections/customer-portal-password.collection.js +0 -75
  275. package/collections/customer-portal-password.collection.js.map +0 -1
  276. package/collections/email-history.collection.d.ts +0 -3
  277. package/collections/email-history.collection.js +0 -134
  278. package/collections/email-history.collection.js.map +0 -1
  279. package/collections/email-verified.collection.d.ts +0 -3
  280. package/collections/email-verified.collection.js +0 -62
  281. package/collections/email-verified.collection.js.map +0 -1
  282. package/collections/file.collection.d.ts +0 -3
  283. package/collections/file.collection.js +0 -74
  284. package/collections/file.collection.js.map +0 -1
  285. package/collections/flag-update.collection.d.ts +0 -3
  286. package/collections/flag-update.collection.js +0 -57
  287. package/collections/flag-update.collection.js.map +0 -1
  288. package/collections/flag.collection.d.ts +0 -3
  289. package/collections/flag.collection.js +0 -57
  290. package/collections/flag.collection.js.map +0 -1
  291. package/collections/log-method-latency.collection.d.ts +0 -3
  292. package/collections/log-method-latency.collection.js +0 -77
  293. package/collections/log-method-latency.collection.js.map +0 -1
  294. package/collections/log-subscription.collection.d.ts +0 -3
  295. package/collections/log-subscription.collection.js +0 -80
  296. package/collections/log-subscription.collection.js.map +0 -1
  297. package/collections/log.collection.d.ts +0 -3
  298. package/collections/log.collection.js +0 -93
  299. package/collections/log.collection.js.map +0 -1
  300. package/collections/logged-in-users.collection.d.ts +0 -3
  301. package/collections/logged-in-users.collection.js +0 -67
  302. package/collections/logged-in-users.collection.js.map +0 -1
  303. package/collections/monitor-cpu.collection.d.ts +0 -3
  304. package/collections/monitor-cpu.collection.js +0 -65
  305. package/collections/monitor-cpu.collection.js.map +0 -1
  306. package/collections/monitor-function.collection.d.ts +0 -3
  307. package/collections/monitor-function.collection.js +0 -74
  308. package/collections/monitor-function.collection.js.map +0 -1
  309. package/collections/monitor-memory.collection.d.ts +0 -3
  310. package/collections/monitor-memory.collection.js +0 -77
  311. package/collections/monitor-memory.collection.js.map +0 -1
  312. package/collections/monitor-mongo.collection.d.ts +0 -3
  313. package/collections/monitor-mongo.collection.js +0 -71
  314. package/collections/monitor-mongo.collection.js.map +0 -1
  315. package/collections/notification.collection.d.ts +0 -3
  316. package/collections/notification.collection.js +0 -57
  317. package/collections/notification.collection.js.map +0 -1
  318. package/collections/openai-usage-ledger.collection.d.ts +0 -2
  319. package/collections/openai-usage-ledger.collection.js +0 -124
  320. package/collections/openai-usage-ledger.collection.js.map +0 -1
  321. package/collections/report-builder-dashboard-builder.collection.d.ts +0 -3
  322. package/collections/report-builder-dashboard-builder.collection.js +0 -109
  323. package/collections/report-builder-dashboard-builder.collection.js.map +0 -1
  324. package/collections/report-builder-library.collection.d.ts +0 -3
  325. package/collections/report-builder-library.collection.js +0 -87
  326. package/collections/report-builder-library.collection.js.map +0 -1
  327. package/collections/report-builder-report.collection.d.ts +0 -4
  328. package/collections/report-builder-report.collection.js +0 -184
  329. package/collections/report-builder-report.collection.js.map +0 -1
  330. package/collections/user-group.collection.d.ts +0 -4
  331. package/collections/user-group.collection.js +0 -89
  332. package/collections/user-group.collection.js.map +0 -1
  333. package/collections/user-guide.collection.d.ts +0 -3
  334. package/collections/user-guide.collection.js +0 -57
  335. package/collections/user-guide.collection.js.map +0 -1
  336. package/collections/user.collection.d.ts +0 -4
  337. package/collections/user.collection.js +0 -180
  338. package/collections/user.collection.js.map +0 -1
  339. package/cron/cron.d.ts +0 -14
  340. package/cron/cron.js +0 -216
  341. package/cron/cron.js.map +0 -1
  342. package/fixtures/cron-jobs.d.ts +0 -1
  343. package/fixtures/cron-jobs.js +0 -150
  344. package/fixtures/cron-jobs.js.map +0 -1
  345. package/fixtures/init.d.ts +0 -1
  346. package/fixtures/init.js +0 -91
  347. package/fixtures/init.js.map +0 -1
  348. package/http/auth.d.ts +0 -2
  349. package/http/auth.js +0 -951
  350. package/http/auth.js.map +0 -1
  351. package/http/health.d.ts +0 -1
  352. package/http/health.js +0 -11
  353. package/http/health.js.map +0 -1
  354. package/http/home.d.ts +0 -1
  355. package/http/home.js +0 -134
  356. package/http/home.js.map +0 -1
  357. package/http/slow-query-publication.d.ts +0 -2
  358. package/http/slow-query-publication.js +0 -99
  359. package/http/slow-query-publication.js.map +0 -1
  360. package/index.d.ts +0 -1
  361. package/index.js +0 -19
  362. package/index.js.map +0 -1
  363. package/managers/ai-assistant-codex-manager.manager.d.ts +0 -67
  364. package/managers/ai-assistant-codex-manager.manager.js +0 -1113
  365. package/managers/ai-assistant-codex-manager.manager.js.map +0 -1
  366. package/managers/ai-run-evidence.manager.d.ts +0 -36
  367. package/managers/ai-run-evidence.manager.js +0 -377
  368. package/managers/ai-run-evidence.manager.js.map +0 -1
  369. package/managers/communication-metric.manager.d.ts +0 -16
  370. package/managers/communication-metric.manager.js +0 -134
  371. package/managers/communication-metric.manager.js.map +0 -1
  372. package/managers/cron.manager.d.ts +0 -20
  373. package/managers/cron.manager.js +0 -534
  374. package/managers/cron.manager.js.map +0 -1
  375. package/managers/customer-notification-content.manager.d.ts +0 -55
  376. package/managers/customer-notification-content.manager.js +0 -158
  377. package/managers/customer-notification-content.manager.js.map +0 -1
  378. package/managers/diagnostic-manager-bootstrap.d.ts +0 -9
  379. package/managers/diagnostic-manager-bootstrap.js +0 -260
  380. package/managers/diagnostic-manager-bootstrap.js.map +0 -1
  381. package/managers/error-auto-fix.manager.d.ts +0 -149
  382. package/managers/error-auto-fix.manager.js +0 -3064
  383. package/managers/error-auto-fix.manager.js.map +0 -1
  384. package/managers/local-log.manager.d.ts +0 -18
  385. package/managers/local-log.manager.js +0 -88
  386. package/managers/local-log.manager.js.map +0 -1
  387. package/managers/method.manager.d.ts +0 -84
  388. package/managers/method.manager.js +0 -1964
  389. package/managers/method.manager.js.map +0 -1
  390. package/managers/mongo.manager.d.ts +0 -224
  391. package/managers/mongo.manager.js +0 -5000
  392. package/managers/mongo.manager.js.map +0 -1
  393. package/managers/monitor.manager.d.ts +0 -70
  394. package/managers/monitor.manager.js +0 -550
  395. package/managers/monitor.manager.js.map +0 -1
  396. package/managers/openai-usage-ledger.manager.d.ts +0 -16
  397. package/managers/openai-usage-ledger.manager.js +0 -93
  398. package/managers/openai-usage-ledger.manager.js.map +0 -1
  399. package/managers/slow-query-verifier.manager.d.ts +0 -144
  400. package/managers/slow-query-verifier.manager.js +0 -3857
  401. package/managers/slow-query-verifier.manager.js.map +0 -1
  402. package/managers/slow-query.manager.d.ts +0 -28
  403. package/managers/slow-query.manager.js +0 -468
  404. package/managers/slow-query.manager.js.map +0 -1
  405. package/managers/subscription.manager.d.ts +0 -169
  406. package/managers/subscription.manager.js +0 -3434
  407. package/managers/subscription.manager.js.map +0 -1
  408. package/managers/websocket.manager.d.ts +0 -73
  409. package/managers/websocket.manager.js +0 -673
  410. package/managers/websocket.manager.js.map +0 -1
  411. package/managers/worker-dispatcher.manager.d.ts +0 -120
  412. package/managers/worker-dispatcher.manager.js +0 -1266
  413. package/managers/worker-dispatcher.manager.js.map +0 -1
  414. package/managers/worker-server.manager.d.ts +0 -35
  415. package/managers/worker-server.manager.js +0 -582
  416. package/managers/worker-server.manager.js.map +0 -1
  417. package/methods/accounts.d.ts +0 -2
  418. package/methods/accounts.js +0 -624
  419. package/methods/accounts.js.map +0 -1
  420. package/methods/ai-terminal.d.ts +0 -337
  421. package/methods/ai-terminal.js +0 -23166
  422. package/methods/ai-terminal.js.map +0 -1
  423. package/methods/app-settings.d.ts +0 -2
  424. package/methods/app-settings.js +0 -169
  425. package/methods/app-settings.js.map +0 -1
  426. package/methods/aws.d.ts +0 -2
  427. package/methods/aws.js +0 -877
  428. package/methods/aws.js.map +0 -1
  429. package/methods/collections.d.ts +0 -2
  430. package/methods/collections.js +0 -719
  431. package/methods/collections.js.map +0 -1
  432. package/methods/counters.d.ts +0 -2
  433. package/methods/counters.js +0 -113
  434. package/methods/counters.js.map +0 -1
  435. package/methods/cron-jobs.d.ts +0 -2
  436. package/methods/cron-jobs.js +0 -2475
  437. package/methods/cron-jobs.js.map +0 -1
  438. package/methods/customer-notifications.d.ts +0 -2
  439. package/methods/customer-notifications.js +0 -528
  440. package/methods/customer-notifications.js.map +0 -1
  441. package/methods/diagnostics.d.ts +0 -2
  442. package/methods/diagnostics.js +0 -703
  443. package/methods/diagnostics.js.map +0 -1
  444. package/methods/flag-updates.d.ts +0 -2
  445. package/methods/flag-updates.js +0 -8
  446. package/methods/flag-updates.js.map +0 -1
  447. package/methods/flags.d.ts +0 -2
  448. package/methods/flags.js +0 -8
  449. package/methods/flags.js.map +0 -1
  450. package/methods/logs.d.ts +0 -2
  451. package/methods/logs.js +0 -751
  452. package/methods/logs.js.map +0 -1
  453. package/methods/mongo-explorer.d.ts +0 -2
  454. package/methods/mongo-explorer.js +0 -1808
  455. package/methods/mongo-explorer.js.map +0 -1
  456. package/methods/monitor.d.ts +0 -2
  457. package/methods/monitor.js +0 -543
  458. package/methods/monitor.js.map +0 -1
  459. package/methods/pdf.d.ts +0 -2
  460. package/methods/pdf.js +0 -1216
  461. package/methods/pdf.js.map +0 -1
  462. package/methods/publications.d.ts +0 -1
  463. package/methods/publications.js +0 -183
  464. package/methods/publications.js.map +0 -1
  465. package/methods/report-builder.d.ts +0 -2
  466. package/methods/report-builder.js +0 -3094
  467. package/methods/report-builder.js.map +0 -1
  468. package/methods/support.d.ts +0 -2
  469. package/methods/support.js +0 -430
  470. package/methods/support.js.map +0 -1
  471. package/models/ai-run.model.d.ts +0 -19
  472. package/models/ai-run.model.js +0 -4
  473. package/models/ai-run.model.js.map +0 -1
  474. package/models/ai-terminal-conversation.model.d.ts +0 -17
  475. package/models/ai-terminal-conversation.model.js +0 -4
  476. package/models/ai-terminal-conversation.model.js.map +0 -1
  477. package/models/ai-terminal-issue-report.model.d.ts +0 -19
  478. package/models/ai-terminal-issue-report.model.js +0 -4
  479. package/models/ai-terminal-issue-report.model.js.map +0 -1
  480. package/models/ai-terminal-message.model.d.ts +0 -22
  481. package/models/ai-terminal-message.model.js +0 -4
  482. package/models/ai-terminal-message.model.js.map +0 -1
  483. package/models/app-setting.model.d.ts +0 -16
  484. package/models/app-setting.model.js +0 -4
  485. package/models/app-setting.model.js.map +0 -1
  486. package/models/app-status.model.js +0 -4
  487. package/models/app-status.model.js.map +0 -1
  488. package/models/billing-logged-in-users.model.js +0 -4
  489. package/models/billing-logged-in-users.model.js.map +0 -1
  490. package/models/collection-document.model.d.ts +0 -21
  491. package/models/collection-document.model.js +0 -4
  492. package/models/collection-document.model.js.map +0 -1
  493. package/models/communication-metric.model.d.ts +0 -20
  494. package/models/communication-metric.model.js +0 -4
  495. package/models/communication-metric.model.js.map +0 -1
  496. package/models/counter.model.js +0 -4
  497. package/models/counter.model.js.map +0 -1
  498. package/models/cron-job-history.model.d.ts +0 -15
  499. package/models/cron-job-history.model.js +0 -4
  500. package/models/cron-job-history.model.js.map +0 -1
  501. package/models/cron-job.model.d.ts +0 -14
  502. package/models/cron-job.model.js +0 -4
  503. package/models/cron-job.model.js.map +0 -1
  504. package/models/customer-notification.model.d.ts +0 -26
  505. package/models/customer-notification.model.js +0 -4
  506. package/models/customer-notification.model.js.map +0 -1
  507. package/models/customer-portal-password.model.d.ts +0 -11
  508. package/models/customer-portal-password.model.js +0 -4
  509. package/models/customer-portal-password.model.js.map +0 -1
  510. package/models/dialog.model.d.ts +0 -23
  511. package/models/dialog.model.js +0 -4
  512. package/models/dialog.model.js.map +0 -1
  513. package/models/email-history.model.d.ts +0 -32
  514. package/models/email-history.model.js.map +0 -1
  515. package/models/email-verified.model.js +0 -4
  516. package/models/email-verified.model.js.map +0 -1
  517. package/models/file.model.js +0 -4
  518. package/models/file.model.js.map +0 -1
  519. package/models/flag-update.model.js +0 -4
  520. package/models/flag-update.model.js.map +0 -1
  521. package/models/flag.model.js +0 -4
  522. package/models/flag.model.js.map +0 -1
  523. package/models/log-method-latency.model.d.ts +0 -10
  524. package/models/log-method-latency.model.js +0 -4
  525. package/models/log-method-latency.model.js.map +0 -1
  526. package/models/log-subscription.model.js +0 -4
  527. package/models/log-subscription.model.js.map +0 -1
  528. package/models/log.model.d.ts +0 -17
  529. package/models/log.model.js +0 -4
  530. package/models/log.model.js.map +0 -1
  531. package/models/logged-in-users.model.js +0 -4
  532. package/models/logged-in-users.model.js.map +0 -1
  533. package/models/method-response.model.js +0 -4
  534. package/models/method-response.model.js.map +0 -1
  535. package/models/method.model.d.ts +0 -26
  536. package/models/method.model.js +0 -4
  537. package/models/method.model.js.map +0 -1
  538. package/models/monitor-cpu.model.js +0 -4
  539. package/models/monitor-cpu.model.js.map +0 -1
  540. package/models/monitor-function.model.d.ts +0 -14
  541. package/models/monitor-function.model.js +0 -4
  542. package/models/monitor-function.model.js.map +0 -1
  543. package/models/monitor-memory.model.d.ts +0 -15
  544. package/models/monitor-memory.model.js +0 -4
  545. package/models/monitor-memory.model.js.map +0 -1
  546. package/models/monitor-mongo.model.d.ts +0 -13
  547. package/models/monitor-mongo.model.js +0 -4
  548. package/models/monitor-mongo.model.js.map +0 -1
  549. package/models/notification.model.js +0 -4
  550. package/models/notification.model.js.map +0 -1
  551. package/models/openai-usage-ledger.model.d.ts +0 -15
  552. package/models/openai-usage-ledger.model.js +0 -4
  553. package/models/openai-usage-ledger.model.js.map +0 -1
  554. package/models/pagination.model.d.ts +0 -11
  555. package/models/pagination.model.js +0 -28
  556. package/models/pagination.model.js.map +0 -1
  557. package/models/permission.model.d.ts +0 -12
  558. package/models/permission.model.js +0 -4
  559. package/models/permission.model.js.map +0 -1
  560. package/models/report-builder-dashboard-builder.model.d.ts +0 -25
  561. package/models/report-builder-dashboard-builder.model.js +0 -4
  562. package/models/report-builder-dashboard-builder.model.js.map +0 -1
  563. package/models/report-builder-library.model.d.ts +0 -17
  564. package/models/report-builder-library.model.js +0 -4
  565. package/models/report-builder-library.model.js.map +0 -1
  566. package/models/report-builder-report.model.d.ts +0 -121
  567. package/models/report-builder-report.model.js +0 -4
  568. package/models/report-builder-report.model.js.map +0 -1
  569. package/models/report-builder.model.d.ts +0 -61
  570. package/models/report-builder.model.js +0 -4
  571. package/models/report-builder.model.js.map +0 -1
  572. package/models/select-data-label.model.d.ts +0 -9
  573. package/models/select-data-label.model.js +0 -4
  574. package/models/select-data-label.model.js.map +0 -1
  575. package/models/server-message.model.d.ts +0 -32
  576. package/models/server-message.model.js +0 -4
  577. package/models/server-message.model.js.map +0 -1
  578. package/models/slow-query-report.model.d.ts +0 -23
  579. package/models/slow-query-report.model.js +0 -4
  580. package/models/slow-query-report.model.js.map +0 -1
  581. package/models/subscription.model.d.ts +0 -31
  582. package/models/subscription.model.js +0 -4
  583. package/models/subscription.model.js.map +0 -1
  584. package/models/support-ticket.model.d.ts +0 -87
  585. package/models/support-ticket.model.js +0 -4
  586. package/models/support-ticket.model.js.map +0 -1
  587. package/models/user-group.model.d.ts +0 -20
  588. package/models/user-group.model.js +0 -4
  589. package/models/user-group.model.js.map +0 -1
  590. package/models/user-guide.model.js +0 -4
  591. package/models/user-guide.model.js.map +0 -1
  592. package/models/user.model.d.ts +0 -84
  593. package/models/user.model.js +0 -4
  594. package/models/user.model.js.map +0 -1
  595. package/private/images/ResolveIO.png +0 -0
  596. package/public_api.js +0 -125
  597. package/public_api.js.map +0 -1
  598. package/publications/ai-terminal.d.ts +0 -1
  599. package/publications/ai-terminal.js +0 -122
  600. package/publications/ai-terminal.js.map +0 -1
  601. package/publications/app-settings.d.ts +0 -2
  602. package/publications/app-settings.js +0 -28
  603. package/publications/app-settings.js.map +0 -1
  604. package/publications/app-status.d.ts +0 -2
  605. package/publications/app-status.js +0 -16
  606. package/publications/app-status.js.map +0 -1
  607. package/publications/cron-jobs.d.ts +0 -2
  608. package/publications/cron-jobs.js +0 -88
  609. package/publications/cron-jobs.js.map +0 -1
  610. package/publications/customer-notifications.d.ts +0 -2
  611. package/publications/customer-notifications.js +0 -161
  612. package/publications/customer-notifications.js.map +0 -1
  613. package/publications/files.d.ts +0 -2
  614. package/publications/files.js +0 -36
  615. package/publications/files.js.map +0 -1
  616. package/publications/flags-update.d.ts +0 -2
  617. package/publications/flags-update.js +0 -22
  618. package/publications/flags-update.js.map +0 -1
  619. package/publications/flags.d.ts +0 -2
  620. package/publications/flags.js +0 -22
  621. package/publications/flags.js.map +0 -1
  622. package/publications/logs.d.ts +0 -2
  623. package/publications/logs.js +0 -164
  624. package/publications/logs.js.map +0 -1
  625. package/publications/notifications.d.ts +0 -2
  626. package/publications/notifications.js +0 -16
  627. package/publications/notifications.js.map +0 -1
  628. package/publications/report-builder-dashboard-builders.d.ts +0 -2
  629. package/publications/report-builder-dashboard-builders.js +0 -42
  630. package/publications/report-builder-dashboard-builders.js.map +0 -1
  631. package/publications/report-builder-libraries.d.ts +0 -2
  632. package/publications/report-builder-libraries.js +0 -90
  633. package/publications/report-builder-libraries.js.map +0 -1
  634. package/publications/report-builder-reports.d.ts +0 -2
  635. package/publications/report-builder-reports.js +0 -50
  636. package/publications/report-builder-reports.js.map +0 -1
  637. package/publications/super-admin.d.ts +0 -2
  638. package/publications/super-admin.js +0 -16
  639. package/publications/super-admin.js.map +0 -1
  640. package/publications/user-groups.d.ts +0 -1
  641. package/publications/user-groups.js +0 -16
  642. package/publications/user-groups.js.map +0 -1
  643. package/publications/user-guides.d.ts +0 -1
  644. package/publications/user-guides.js +0 -16
  645. package/publications/user-guides.js.map +0 -1
  646. package/resolveio-server-app.d.ts +0 -70
  647. package/resolveio-server-app.js +0 -801
  648. package/resolveio-server-app.js.map +0 -1
  649. package/server-app.d.ts +0 -228
  650. package/server-app.js +0 -3566
  651. package/server-app.js.map +0 -1
  652. package/services/codex-client.d.ts +0 -128
  653. package/services/codex-client.js +0 -1629
  654. package/services/codex-client.js.map +0 -1
  655. package/services/openai-client.d.ts +0 -46
  656. package/services/openai-client.js +0 -318
  657. package/services/openai-client.js.map +0 -1
  658. package/types/error-report.d.ts +0 -25
  659. package/types/error-report.js +0 -4
  660. package/types/error-report.js.map +0 -1
  661. package/types/slow-query-report.d.ts +0 -27
  662. package/types/slow-query-report.js +0 -6
  663. package/types/slow-query-report.js.map +0 -1
  664. package/util/ai-qa-policy.d.ts +0 -124
  665. package/util/ai-qa-policy.js +0 -736
  666. package/util/ai-qa-policy.js.map +0 -1
  667. package/util/ai-run-evidence-adapters.d.ts +0 -33
  668. package/util/ai-run-evidence-adapters.js +0 -660
  669. package/util/ai-run-evidence-adapters.js.map +0 -1
  670. package/util/ai-run-evidence-dashboard.d.ts +0 -67
  671. package/util/ai-run-evidence-dashboard.js +0 -309
  672. package/util/ai-run-evidence-dashboard.js.map +0 -1
  673. package/util/ai-run-evidence-eval.d.ts +0 -86
  674. package/util/ai-run-evidence-eval.js +0 -854
  675. package/util/ai-run-evidence-eval.js.map +0 -1
  676. package/util/ai-run-evidence.d.ts +0 -212
  677. package/util/ai-run-evidence.js +0 -649
  678. package/util/ai-run-evidence.js.map +0 -1
  679. package/util/ai-runner-artifacts.d.ts +0 -82
  680. package/util/ai-runner-artifacts.js +0 -713
  681. package/util/ai-runner-artifacts.js.map +0 -1
  682. package/util/ai-runner-qa-auth.d.ts +0 -5
  683. package/util/ai-runner-qa-auth.js +0 -822
  684. package/util/ai-runner-qa-auth.js.map +0 -1
  685. package/util/ai-runner-qa-tools.d.ts +0 -26
  686. package/util/ai-runner-qa-tools.js +0 -3029
  687. package/util/ai-runner-qa-tools.js.map +0 -1
  688. package/util/aicoder-runner-v6.d.ts +0 -168
  689. package/util/aicoder-runner-v6.js +0 -347
  690. package/util/aicoder-runner-v6.js.map +0 -1
  691. package/util/common.d.ts +0 -31
  692. package/util/common.js +0 -683
  693. package/util/common.js.map +0 -1
  694. package/util/customer-portal-password.d.ts +0 -13
  695. package/util/customer-portal-password.js +0 -209
  696. package/util/customer-portal-password.js.map +0 -1
  697. package/util/error-reporter.d.ts +0 -52
  698. package/util/error-reporter.js +0 -326
  699. package/util/error-reporter.js.map +0 -1
  700. package/util/error-tracking.d.ts +0 -13
  701. package/util/error-tracking.js +0 -120
  702. package/util/error-tracking.js.map +0 -1
  703. package/util/openai-usage-cost.d.ts +0 -6
  704. package/util/openai-usage-cost.js +0 -92
  705. package/util/openai-usage-cost.js.map +0 -1
  706. package/util/report-builder-unwinds.d.ts +0 -15
  707. package/util/report-builder-unwinds.js +0 -156
  708. package/util/report-builder-unwinds.js.map +0 -1
  709. package/util/runner-process-janitor.d.ts +0 -27
  710. package/util/runner-process-janitor.js +0 -208
  711. package/util/runner-process-janitor.js.map +0 -1
  712. package/util/schema-report-builder.d.ts +0 -6
  713. package/util/schema-report-builder.js +0 -481
  714. package/util/schema-report-builder.js.map +0 -1
  715. package/util/slow-query-reporter.d.ts +0 -28
  716. package/util/slow-query-reporter.js +0 -226
  717. package/util/slow-query-reporter.js.map +0 -1
  718. package/util/subscription-dependency-context.d.ts +0 -34
  719. package/util/subscription-dependency-context.js +0 -1283
  720. package/util/subscription-dependency-context.js.map +0 -1
  721. package/util/support-runner-v5.d.ts +0 -250
  722. package/util/support-runner-v5.js +0 -634
  723. package/util/support-runner-v5.js.map +0 -1
  724. package/util/tokenizer.d.ts +0 -5
  725. package/util/tokenizer.js +0 -41
  726. package/util/tokenizer.js.map +0 -1
  727. package/workers/codex-runner.worker.d.ts +0 -1
  728. package/workers/codex-runner.worker.js +0 -192
  729. package/workers/codex-runner.worker.js.map +0 -1
  730. /package/{private → src/private}/email-templates/enrollment.html +0 -0
  731. /package/{private → src/private}/email-templates/forgot-password.html +0 -0
  732. /package/{private → src/private}/email-templates/support-ticket-deleted.html +0 -0
  733. /package/{private → src/private}/email-templates/support-ticket-modified.html +0 -0
  734. /package/{private → src/private}/email-templates/support-ticket.html +0 -0
  735. /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
+ });