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