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