From 01184f81d0641d59a6336e3a2cdc2d59de56ff11 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 31 Oct 2026 08:19:10 +0000 Subject: [PATCH] chore: delete orphan lib modules under apps/web/src/lib Remove self-contained trees with no production importers outside their own directories: metaaligner, deployment/multi-region, business-strategy, polyfills, esm-compat, auth-legacy shim, and duplicate root logging.ts. Clean up vitest buckets, eslint/oxlint ignores, large-file and naming baselines, and stale auth comments referencing removed paths. --- .gitignore | 1 - .oxlintrc.json | 2 - apps/web/src/lib/auth-legacy.ts | 317 --- apps/web/src/lib/auth/dual-write.ts | 4 +- apps/web/src/lib/auth/identity-provider.ts | 2 +- .../lib/business-strategy/config/database.ts | 349 --- apps/web/src/lib/business-strategy/index.ts | 27 - .../business-strategy/services/analytics.ts | 9 - .../lib/business-strategy/services/audit.ts | 367 --- .../services/base-service.ts | 182 -- .../services/competitive-intelligence.ts | 67 - .../services/document-management.ts | 336 --- .../lib/business-strategy/services/export.ts | 9 - .../lib/business-strategy/services/factory.ts | 81 - .../services/grassroots-marketing.ts | 9 - .../lib/business-strategy/services/index.ts | 23 - .../services/market-research.ts | 84 - .../services/notification.ts | 9 - .../lib/business-strategy/services/search.ts | 9 - .../services/user-management.ts | 52 - .../services/workflow-engine.ts | 133 -- .../src/lib/business-strategy/types/common.ts | 74 - .../types/competitive-intelligence.ts | 197 -- .../types/document-management.ts | 321 --- .../types/grassroots-marketing.ts | 338 --- .../src/lib/business-strategy/types/index.ts | 14 - .../types/market-research.ts | 162 -- .../types/user-management.ts | 313 --- .../types/workflow-engine.ts | 375 --- .../AutomatedFailoverOrchestrator.ts | 1603 ------------- .../multi-region/CloudProviderManager.ts | 795 ------- .../multi-region/ConfigurationManager.ts | 1229 ---------- .../CrossRegionDataSyncManager.ts | 1947 ---------------- .../lib/deployment/multi-region/DNSClient.ts | 29 - .../multi-region/DeploymentOrchestrator.ts | 1278 ---------- .../EdgeComputingManager.codegen.ts | 474 ---- .../EdgeComputingManager.locations-aws.ts | 275 --- ...ComputingManager.locations-cloudflare-1.ts | 470 ---- ...ComputingManager.locations-cloudflare-2.ts | 447 ---- .../EdgeComputingManager.locations.ts | 2048 ----------------- .../multi-region/EdgeComputingManager.ts | 719 ------ .../EdgeComputingManager.types.ts | 77 - .../GlobalTrafficRoutingManager.ts | 937 -------- .../deployment/multi-region/HealthMonitor.ts | 978 -------- .../MultiRegionDeploymentManager.ts | 736 ------ .../multi-region/ServiceDiscoveryManager.ts | 1858 --------------- .../src/lib/deployment/multi-region/index.ts | 732 ------ apps/web/src/lib/esm-compat/react-dom.js | 13 - apps/web/src/lib/esm-compat/react.js | 29 - apps/web/src/lib/logging.ts | 188 -- .../adaptive-selection-integration.test.ts | 628 ----- .../metaaligner/__tests__/integration.test.ts | 754 ------ .../metaaligner/api/__tests__/adapter.test.ts | 134 -- .../api/__tests__/unified-pipeline.test.ts | 195 -- apps/web/src/lib/metaaligner/api/adapter.ts | 202 -- .../lib/metaaligner/api/alignment-api.test.ts | 683 ------ .../src/lib/metaaligner/api/alignment-api.ts | 814 ------- .../src/lib/metaaligner/api/unified-api.ts | 108 - .../lib/metaaligner/api/unified-pipeline.ts | 181 -- .../lib/metaaligner/batch/batch-processor.ts | 135 -- .../config/context-mapper-service.test.ts | 414 ---- .../config/context-mapper-service.ts | 352 --- .../metaaligner/config/mapping-config.test.ts | 381 --- .../lib/metaaligner/config/mapping-config.ts | 386 ---- .../metaaligner/context/context-injector.ts | 79 - .../core/dynamic-weighting-benchmark.ts | 446 ---- .../core/dynamic-weighting.test.ts | 655 ------ .../lib/metaaligner/core/dynamic-weighting.ts | 638 ----- .../core/objective-interfaces.test.ts | 458 ---- .../metaaligner/core/objective-interfaces.ts | 619 ----- .../core/objective-metrics.test.ts | 576 ----- .../lib/metaaligner/core/objective-metrics.ts | 834 ------- .../core/objective-weighting.test.ts | 605 ----- .../metaaligner/core/objective-weighting.ts | 687 ------ .../lib/metaaligner/core/objectives.test.ts | 303 --- .../src/lib/metaaligner/core/objectives.ts | 582 ----- .../enhancement/enhancement-processor.ts | 68 - .../error-handling/circuit-breaker.ts | 74 - .../error-handling/error-handler.ts | 206 -- .../error-handling/fallback-generator.ts | 90 - .../src/lib/metaaligner/formats/conversion.ts | 38 - .../src/lib/metaaligner/formats/schemas.ts | 52 - .../src/lib/metaaligner/formats/validation.ts | 30 - .../objectives/objective-injector.ts | 74 - .../performance/performance-monitor.ts | 70 - .../EducationalContextRecognizer.ts | 427 ---- .../prioritization/adaptive-selector.test.ts | 499 ---- .../prioritization/adaptive-selector.ts | 181 -- .../prioritization/context-detector.test.ts | 732 ------ .../prioritization/context-detector.ts | 666 ------ .../context-objective-mapper.ts | 87 - .../context-objective-mapping.ts | 76 - .../context-transition-detector.test.ts | 778 ------- .../context-transition-detector.ts | 306 --- .../educational-context-recognizer.test.ts | 719 ------ .../educational-context-recognizer.ts | 1060 --------- .../prioritization/objective-switcher.test.ts | 794 ------- .../prioritization/objective-switcher.ts | 395 ---- .../objective-weighting-strategy.ts | 60 - .../support-context-identifier.test.ts | 624 ----- .../support-context-identifier.ts | 1750 -------------- .../prioritization/user-preferences.test.ts | 633 ----- .../prioritization/user-preferences.ts | 519 ----- .../processing/enhancement-pipeline.test.ts | 346 --- .../processing/enhancement-pipeline.ts | 380 --- .../metaaligner/quality/quality-assurance.ts | 17 - .../metaaligner/quality/quality-metrics.ts | 32 - .../metaaligner/quality/quality-reporter.ts | 17 - .../quality/quality-threshold-manager.ts | 17 - .../query-preparation/query-preprocessor.ts | 66 - .../streaming/streaming-handler.test.ts | 63 - .../streaming/streaming-handler.ts | 82 - .../lib/metaaligner/tests/test-strategy.ts | 49 - .../src/lib/polyfills/browser-polyfills.ts | 460 ---- apps/web/src/lib/polyfills/buffer.ts | 12 - .../src/lib/polyfills/node-child-process.js | 26 - .../lib/polyfills/node-diagnostics-channel.js | 23 - apps/web/src/lib/polyfills/node-fs.js | 46 - apps/web/src/lib/polyfills/node-http.js | 32 - apps/web/src/lib/polyfills/node-https.js | 32 - apps/web/src/lib/polyfills/node-inspector.js | 32 - apps/web/src/lib/polyfills/node-net.js | 29 - apps/web/src/lib/polyfills/node-os.js | 46 - apps/web/src/lib/polyfills/node-path.js | 45 - apps/web/src/lib/polyfills/node-polyfills.ts | 56 - apps/web/src/lib/polyfills/node-readline.js | 19 - apps/web/src/lib/polyfills/node-shims.js | 298 --- apps/web/src/lib/polyfills/node-stream.js | 68 - apps/web/src/lib/polyfills/node-tls.js | 29 - apps/web/src/lib/polyfills/node-util.js | 37 - .../src/lib/polyfills/node-worker-threads.js | 42 - apps/web/src/lib/polyfills/node-zlib.js | 46 - eslint.config.js | 11 - scripts/ci/large-file-baseline.json | 14 +- scripts/ci/naming-baseline.json | 3 +- scripts/testing/local-test-runner.mjs | 1 - 136 files changed, 5 insertions(+), 46056 deletions(-) delete mode 100644 apps/web/src/lib/auth-legacy.ts delete mode 100644 apps/web/src/lib/business-strategy/config/database.ts delete mode 100644 apps/web/src/lib/business-strategy/index.ts delete mode 100644 apps/web/src/lib/business-strategy/services/analytics.ts delete mode 100644 apps/web/src/lib/business-strategy/services/audit.ts delete mode 100644 apps/web/src/lib/business-strategy/services/base-service.ts delete mode 100644 apps/web/src/lib/business-strategy/services/competitive-intelligence.ts delete mode 100644 apps/web/src/lib/business-strategy/services/document-management.ts delete mode 100644 apps/web/src/lib/business-strategy/services/export.ts delete mode 100644 apps/web/src/lib/business-strategy/services/factory.ts delete mode 100644 apps/web/src/lib/business-strategy/services/grassroots-marketing.ts delete mode 100644 apps/web/src/lib/business-strategy/services/index.ts delete mode 100644 apps/web/src/lib/business-strategy/services/market-research.ts delete mode 100644 apps/web/src/lib/business-strategy/services/notification.ts delete mode 100644 apps/web/src/lib/business-strategy/services/search.ts delete mode 100644 apps/web/src/lib/business-strategy/services/user-management.ts delete mode 100644 apps/web/src/lib/business-strategy/services/workflow-engine.ts delete mode 100644 apps/web/src/lib/business-strategy/types/common.ts delete mode 100644 apps/web/src/lib/business-strategy/types/competitive-intelligence.ts delete mode 100644 apps/web/src/lib/business-strategy/types/document-management.ts delete mode 100644 apps/web/src/lib/business-strategy/types/grassroots-marketing.ts delete mode 100644 apps/web/src/lib/business-strategy/types/index.ts delete mode 100644 apps/web/src/lib/business-strategy/types/market-research.ts delete mode 100644 apps/web/src/lib/business-strategy/types/user-management.ts delete mode 100644 apps/web/src/lib/business-strategy/types/workflow-engine.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/AutomatedFailoverOrchestrator.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/CloudProviderManager.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/ConfigurationManager.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/CrossRegionDataSyncManager.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/DNSClient.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/DeploymentOrchestrator.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/EdgeComputingManager.codegen.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-aws.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-cloudflare-1.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-cloudflare-2.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/EdgeComputingManager.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/EdgeComputingManager.types.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/GlobalTrafficRoutingManager.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/HealthMonitor.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/MultiRegionDeploymentManager.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/ServiceDiscoveryManager.ts delete mode 100644 apps/web/src/lib/deployment/multi-region/index.ts delete mode 100644 apps/web/src/lib/esm-compat/react-dom.js delete mode 100644 apps/web/src/lib/esm-compat/react.js delete mode 100644 apps/web/src/lib/logging.ts delete mode 100644 apps/web/src/lib/metaaligner/__tests__/adaptive-selection-integration.test.ts delete mode 100644 apps/web/src/lib/metaaligner/__tests__/integration.test.ts delete mode 100644 apps/web/src/lib/metaaligner/api/__tests__/adapter.test.ts delete mode 100644 apps/web/src/lib/metaaligner/api/__tests__/unified-pipeline.test.ts delete mode 100644 apps/web/src/lib/metaaligner/api/adapter.ts delete mode 100644 apps/web/src/lib/metaaligner/api/alignment-api.test.ts delete mode 100644 apps/web/src/lib/metaaligner/api/alignment-api.ts delete mode 100644 apps/web/src/lib/metaaligner/api/unified-api.ts delete mode 100644 apps/web/src/lib/metaaligner/api/unified-pipeline.ts delete mode 100644 apps/web/src/lib/metaaligner/batch/batch-processor.ts delete mode 100644 apps/web/src/lib/metaaligner/config/context-mapper-service.test.ts delete mode 100644 apps/web/src/lib/metaaligner/config/context-mapper-service.ts delete mode 100644 apps/web/src/lib/metaaligner/config/mapping-config.test.ts delete mode 100644 apps/web/src/lib/metaaligner/config/mapping-config.ts delete mode 100644 apps/web/src/lib/metaaligner/context/context-injector.ts delete mode 100644 apps/web/src/lib/metaaligner/core/dynamic-weighting-benchmark.ts delete mode 100644 apps/web/src/lib/metaaligner/core/dynamic-weighting.test.ts delete mode 100644 apps/web/src/lib/metaaligner/core/dynamic-weighting.ts delete mode 100644 apps/web/src/lib/metaaligner/core/objective-interfaces.test.ts delete mode 100644 apps/web/src/lib/metaaligner/core/objective-interfaces.ts delete mode 100644 apps/web/src/lib/metaaligner/core/objective-metrics.test.ts delete mode 100644 apps/web/src/lib/metaaligner/core/objective-metrics.ts delete mode 100644 apps/web/src/lib/metaaligner/core/objective-weighting.test.ts delete mode 100644 apps/web/src/lib/metaaligner/core/objective-weighting.ts delete mode 100644 apps/web/src/lib/metaaligner/core/objectives.test.ts delete mode 100644 apps/web/src/lib/metaaligner/core/objectives.ts delete mode 100644 apps/web/src/lib/metaaligner/enhancement/enhancement-processor.ts delete mode 100644 apps/web/src/lib/metaaligner/error-handling/circuit-breaker.ts delete mode 100644 apps/web/src/lib/metaaligner/error-handling/error-handler.ts delete mode 100644 apps/web/src/lib/metaaligner/error-handling/fallback-generator.ts delete mode 100644 apps/web/src/lib/metaaligner/formats/conversion.ts delete mode 100644 apps/web/src/lib/metaaligner/formats/schemas.ts delete mode 100644 apps/web/src/lib/metaaligner/formats/validation.ts delete mode 100644 apps/web/src/lib/metaaligner/objectives/objective-injector.ts delete mode 100644 apps/web/src/lib/metaaligner/performance/performance-monitor.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/EducationalContextRecognizer.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/adaptive-selector.test.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/adaptive-selector.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/context-detector.test.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/context-detector.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/context-objective-mapper.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/context-objective-mapping.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/context-transition-detector.test.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/context-transition-detector.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/educational-context-recognizer.test.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/educational-context-recognizer.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/objective-switcher.test.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/objective-switcher.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/objective-weighting-strategy.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/support-context-identifier.test.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/support-context-identifier.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/user-preferences.test.ts delete mode 100644 apps/web/src/lib/metaaligner/prioritization/user-preferences.ts delete mode 100644 apps/web/src/lib/metaaligner/processing/enhancement-pipeline.test.ts delete mode 100644 apps/web/src/lib/metaaligner/processing/enhancement-pipeline.ts delete mode 100644 apps/web/src/lib/metaaligner/quality/quality-assurance.ts delete mode 100644 apps/web/src/lib/metaaligner/quality/quality-metrics.ts delete mode 100644 apps/web/src/lib/metaaligner/quality/quality-reporter.ts delete mode 100644 apps/web/src/lib/metaaligner/quality/quality-threshold-manager.ts delete mode 100644 apps/web/src/lib/metaaligner/query-preparation/query-preprocessor.ts delete mode 100644 apps/web/src/lib/metaaligner/streaming/streaming-handler.test.ts delete mode 100644 apps/web/src/lib/metaaligner/streaming/streaming-handler.ts delete mode 100644 apps/web/src/lib/metaaligner/tests/test-strategy.ts delete mode 100644 apps/web/src/lib/polyfills/browser-polyfills.ts delete mode 100644 apps/web/src/lib/polyfills/buffer.ts delete mode 100644 apps/web/src/lib/polyfills/node-child-process.js delete mode 100644 apps/web/src/lib/polyfills/node-diagnostics-channel.js delete mode 100644 apps/web/src/lib/polyfills/node-fs.js delete mode 100644 apps/web/src/lib/polyfills/node-http.js delete mode 100644 apps/web/src/lib/polyfills/node-https.js delete mode 100644 apps/web/src/lib/polyfills/node-inspector.js delete mode 100644 apps/web/src/lib/polyfills/node-net.js delete mode 100644 apps/web/src/lib/polyfills/node-os.js delete mode 100644 apps/web/src/lib/polyfills/node-path.js delete mode 100644 apps/web/src/lib/polyfills/node-polyfills.ts delete mode 100644 apps/web/src/lib/polyfills/node-readline.js delete mode 100644 apps/web/src/lib/polyfills/node-shims.js delete mode 100644 apps/web/src/lib/polyfills/node-stream.js delete mode 100644 apps/web/src/lib/polyfills/node-tls.js delete mode 100644 apps/web/src/lib/polyfills/node-util.js delete mode 100644 apps/web/src/lib/polyfills/node-worker-threads.js delete mode 100644 apps/web/src/lib/polyfills/node-zlib.js diff --git a/.gitignore b/.gitignore index 016769d427..96d4345790 100644 --- a/.gitignore +++ b/.gitignore @@ -582,7 +582,6 @@ apps/web/src/layouts/TailusLayout.astro apps/web/src/lib/content/features.ts apps/web/src/lib/content/intro.ts apps/web/src/lib/content/scenarios.ts -apps/web/src/lib/metaaligner/explainability/visualization.tsx apps/web/src/lib/services/emotionTracking.ts apps/web/src/lib/supabase.ts apps/web/src/lib/utils/code-splitting.ts diff --git a/.oxlintrc.json b/.oxlintrc.json index 816b7f4109..fc0e56afd6 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -9,7 +9,6 @@ "apps/web/public/polyfills/**/*.js", ".github/cookbook/**/*.ts", ".github/cookbook/**/*.tsx", - "apps/web/src/lib/business-strategy/**/*.ts", "apps/web/src/lib/collaboration/**/*.ts", "apps/web/src/lib/ai/modelOptimizer.ts", "apps/web/src/lib/export/index.node.ts", @@ -421,7 +420,6 @@ "apps/web/src/**/*.spec.ts", "apps/web/src/**/*.spec.tsx", "tests/**/*", - "apps/web/src/lib/business-strategy/**/*", "apps/web/src/lib/collaboration/**/*" ], "rules": { diff --git a/apps/web/src/lib/auth-legacy.ts b/apps/web/src/lib/auth-legacy.ts deleted file mode 100644 index 7441a725c8..0000000000 --- a/apps/web/src/lib/auth-legacy.ts +++ /dev/null @@ -1,317 +0,0 @@ -import type { AstroCookies } from 'astro' - -import { createBuildSafeLogger } from '@/lib/logging/build-safe-logger' - -import type { AuthRole } from '../config/auth.config' -import { authConfig, hasRolePrivilege } from '../config/auth.config' -import { - createHIPAACompliantAuditLog, - AuditEventType, - AuditEventStatus, - type AuditDetails, -} from './audit' -import type { AuditMetadata } from './audit/types' -import { getIdentityProvider } from './auth/identity-provider' -const logger = createBuildSafeLogger('auth-legacy') - -let warnedAboutDeprecation = false - -if (process.env['NODE_ENV'] !== 'test' && !warnedAboutDeprecation) { - warnedAboutDeprecation = true - logger.warn( - '[auth-legacy] This module is the final thin shim before removal. ' + - 'All new code should use src/lib/auth/identity-provider.ts directly. ' + - 'Run `pnpm tsx scripts/auth-cutover-check.ts ` with ' + - 'AUTH_DUAL_WRITE=true to validate parity before migrating call sites. ' + - 'See PIX-215 PR4 for the shim removal timeline.', - ) -} - -const isRecord = (value: unknown): value is Record => - typeof value === 'object' && value !== null && !Array.isArray(value) - -const authRoles = new Set(authConfig.roles.hierarchy) - -const isAuthRole = (value: unknown): value is AuthRole => - typeof value === 'string' && authRoles.has(value) - -const toAuthRole = (value: unknown): AuthRole => - isAuthRole(value) ? value : authConfig.roles.default - -const toMetadataRecord = (value: unknown): Record => - isRecord(value) ? value : {} - -const toAuditDetails = ( - metadata: AuditMetadata | null | undefined, -): AuditDetails => { - if (!metadata) { - return {} - } - - const details: AuditDetails = {} - for (const [key, value] of Object.entries(metadata)) { - if ( - value === null || - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - details[key] = value - } - } - - return details -} - -export interface AuthUser { - id: string - email: string - role: AuthRole - fullName?: string | null - avatarUrl?: string | null - lastLogin?: Date | null - metadata?: Record -} - -/** - * Get the current authenticated user from cookies - */ -interface CookieAccessor { - get(name: string): - | { - value: string - json: () => unknown - } - | undefined -} - -export async function getCurrentUser( - cookies: CookieAccessor, -): Promise { - const accessToken = cookies.get(authConfig.cookies.accessToken)?.value - if (!accessToken) { - return null - } - - const provider = getIdentityProvider() - try { - const decoded = await provider.validateToken(accessToken, 'access') - const userId = decoded.userId - if (typeof userId !== 'string') { - return null - } - - const user = await provider.getUserById(userId) - if (!user) { - return null - } - - return { - id: user.id, - email: user.email, - role: toAuthRole(user.role), - fullName: user.fullName, - avatarUrl: user.avatarUrl, - lastLogin: user.lastLogin ? new Date(user.lastLogin) : null, - metadata: toMetadataRecord(user.userMetadata), - } - } catch (error: unknown) { - logger.error('Error getting current user:', error) - return null - } -} - -/** - * Check if the user is authenticated - */ -export async function isAuthenticated(cookies: AstroCookies): Promise { - if (typeof window !== 'undefined') { - return false - } - - const accessToken = cookies.get(authConfig.cookies.accessToken)?.value - const refreshToken = cookies.get(authConfig.cookies.refreshToken)?.value - - if (!accessToken || !refreshToken) { - return false - } - - try { - const decoded = await getIdentityProvider().validateToken( - accessToken, - 'access', - ) - return !!decoded - } catch (error: unknown) { - logger.error('Error checking authentication:', error) - return false - } -} - -/** - * Check if the user has the required role - */ -export async function hasRole( - cookies: AstroCookies, - requiredRole: AuthRole, -): Promise { - const user = await getCurrentUser(cookies) - if (!user) { - return false - } - - return hasRolePrivilege(user.role, requiredRole) -} - -/** - * Log an audit event from auth module - */ -export async function createAuthAuditLog(entry: { - userId: string - action: string - resource: string - resourceId?: string - metadata?: AuditMetadata -}): Promise { - try { - await createHIPAACompliantAuditLog({ - userId: entry.userId, - action: entry.action, - resource: entry.resource, - ...(entry.resourceId && { resourceId: entry.resourceId }), - ...(entry.metadata && { details: toAuditDetails(entry.metadata) }), - eventType: AuditEventType.SECURITY, - status: AuditEventStatus.SUCCESS, - }) - } catch (error: unknown) { - logger.error('Error logging auth audit event:', error) - } -} - -/** - * Log an audit event using positional parameters - */ -export async function createAuditLogFromParams( - userId: string | null, - action: string, - resource: string, - resourceId?: string | null, - metadata?: AuditMetadata | null, -): Promise { - await createHIPAACompliantAuditLog({ - userId: userId ?? 'system', - action, - resource, - ...(resourceId && { resourceId }), - ...(metadata && { details: toAuditDetails(metadata) }), - eventType: AuditEventType.SYSTEM, - status: AuditEventStatus.SUCCESS, - }) -} - -/** - * Require authentication for a route - */ -export async function requireAuth({ - cookies, - redirect, - request, -}: { - cookies: AstroCookies - redirect: (url: string) => Response - request: Request -}) { - const user = await getCurrentUser(cookies) - - if (!user) { - const loginUrl = new URL(authConfig.redirects.authRequired, request.url) - loginUrl.searchParams.set('redirect', request.url) - return redirect(loginUrl.toString()) - } - - return null -} - -/** - * Require a specific role for a route - */ -export async function requireRole({ - cookies, - redirect, - request, - role, -}: { - cookies: AstroCookies - redirect: (url: string) => Response - request: Request - role: AuthRole -}) { - const user = await getCurrentUser(cookies) - - if (!user) { - const loginUrl = new URL(authConfig.redirects.authRequired, request.url) - loginUrl.searchParams.set('redirect', request.url) - return redirect(loginUrl.toString()) - } - - if (!hasRolePrivilege(user.role, role)) { - return redirect(authConfig.redirects.forbidden) - } - - return null -} - -export class Auth { - async verifySession(request: Request): Promise<{ userId: string } | null> { - const cookies = this.getCookiesFromRequest(request) - const user = await getCurrentUser(cookies) - return user ? { userId: user.id } : null - } - - private getCookiesFromRequest(request: Request): CookieAccessor { - const cookieHeader = request.headers.get('cookie') ?? '' - const cookies = new Map( - cookieHeader.split(';').map((c) => { - const [key, ...v] = c.trim().split('=') - return [key, v.join('=')] - }), - ) - - const cookieReader = { - get: (name: string) => { - const value = cookies.get(name) - return value - ? { - value, - json: () => { - try { - return JSON.parse(value) as unknown - } catch { - return {} - } - }, - } - : undefined - }, - has: (name: string) => cookies.has(name), - set: () => { - throw new Error('Setting cookies is not supported in this context.') - }, - delete: () => { - throw new Error('Deleting cookies is not supported in this context.') - }, - getAll: () => { - return Array.from(cookies.entries()).map(([name, value]) => ({ - name, - value, - })) - }, - } - - return { - ...cookieReader, - } - } -} - -export const auth = new Auth() diff --git a/apps/web/src/lib/auth/dual-write.ts b/apps/web/src/lib/auth/dual-write.ts index d6c9a82df1..33d14409e0 100644 --- a/apps/web/src/lib/auth/dual-write.ts +++ b/apps/web/src/lib/auth/dual-write.ts @@ -3,8 +3,8 @@ * * During the cutover from direct `auth0UserService` calls to the * `IdentityProvider` abstraction, this module compares the two paths and - * reports any discrepancies. When the comparison passes consistently, the - * `auth-legacy.ts` shim can be removed. + * reports any discrepancies. When the comparison passes consistently, legacy + * direct Auth0 call sites can be removed. * * Activation (env flags): * AUTH_DUAL_WRITE=true → compare every call diff --git a/apps/web/src/lib/auth/identity-provider.ts b/apps/web/src/lib/auth/identity-provider.ts index 573652c280..e4a61593e5 100644 --- a/apps/web/src/lib/auth/identity-provider.ts +++ b/apps/web/src/lib/auth/identity-provider.ts @@ -1,7 +1,7 @@ /** * IdentityProvider abstraction. * - * Decouples consumer code (user-identity.ts, auth-legacy.ts, session.ts) from + * Decouples consumer code (user-identity.ts, session.ts) from * the underlying identity provider (currently Auth0). A future second provider * (e.g. Okta, Cognito) can ship as a new implementation of this interface * without touching the call sites. diff --git a/apps/web/src/lib/business-strategy/config/database.ts b/apps/web/src/lib/business-strategy/config/database.ts deleted file mode 100644 index f1e50e5069..0000000000 --- a/apps/web/src/lib/business-strategy/config/database.ts +++ /dev/null @@ -1,349 +0,0 @@ -/** - * Database Configuration for Business Strategy System - * - * Integrates with existing Pixelated Empathy database infrastructure - * while maintaining separation of concerns for business strategy data - */ - -import type Redis from 'ioredis' -import type { MongoClient, Db } from 'mongodb' -import type { Pool } from 'pg' - -// Re-use existing database connections from main system -import { getMongoClient } from '@/lib/db/mongodb' -import { getPostgresPool } from '@/lib/db/postgres' -import { getRedisClient } from '@/lib/db/redis' - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -const logger = createBuildSafeLogger('database') - -export interface DatabaseConfig { - mongodb: { - client: MongoClient - database: Db - collections: { - documents: string - users: string - workflows: string - marketAnalysis: string - competitors: string - marketingTactics: string - auditLogs: string - } - } - postgresql: { - pool: Pool - schema: string - tables: { - users: string - permissions: string - sessions: string - workflows: string - approvals: string - } - } - redis: { - client: Redis - keyPrefixes: { - sessions: string - cache: string - locks: string - queues: string - } - } -} - -// Database configuration -const DB_CONFIG = { - mongodb: { - databaseName: - process.env['BUSINESS_STRATEGY_DB_NAME'] ?? 'pixelated_business_strategy', - collections: { - documents: 'documents', - users: 'users', - workflows: 'workflow_executions', - marketAnalysis: 'market_analysis', - competitors: 'competitors', - marketingTactics: 'marketing_tactics', - auditLogs: 'audit_logs', - }, - }, - postgresql: { - schema: 'business_strategy', - tables: { - users: 'users', - permissions: 'permissions', - sessions: 'sessions', - workflows: 'workflow_definitions', - approvals: 'approvals', - }, - }, - redis: { - keyPrefixes: { - sessions: 'bs:session:', - cache: 'bs:cache:', - locks: 'bs:lock:', - queues: 'bs:queue:', - }, - }, -} as const - -let databaseConfig: DatabaseConfig | null = null - -/** - * Initialize database connections for business strategy system - */ -export async function initializeDatabases(): Promise { - if (databaseConfig) { - return databaseConfig - } - - try { - // Get existing database connections - const mongoClient = await getMongoClient() - const postgresPool = await getPostgresPool() - const redisClient = await getRedisClient() - - // Get MongoDB database instance - const mongoDb = mongoClient.db(DB_CONFIG.mongodb.databaseName) - - // Ensure collections exist with proper indexes - await ensureMongoCollections(mongoDb) - - // Ensure PostgreSQL schema and tables exist - await ensurePostgresSchema(postgresPool) - - databaseConfig = { - mongodb: { - client: mongoClient, - database: mongoDb, - collections: DB_CONFIG.mongodb.collections, - }, - postgresql: { - pool: postgresPool, - schema: DB_CONFIG.postgresql.schema, - tables: DB_CONFIG.postgresql.tables, - }, - redis: { - client: redisClient, - keyPrefixes: DB_CONFIG.redis.keyPrefixes, - }, - } - - return databaseConfig - } catch (error: unknown) { - logger.error('Failed to initialize business strategy databases:', error) - throw new Error('Database initialization failed') - } -} - -/** - * Get initialized database configuration - */ -export function getDatabaseConfig(): DatabaseConfig { - if (!databaseConfig) { - throw new Error( - 'Database not initialized. Call initializeDatabases() first.', - ) - } - return databaseConfig -} - -/** - * Ensure MongoDB collections exist with proper indexes - */ -async function ensureMongoCollections(db: Db): Promise { - const collections = DB_CONFIG.mongodb.collections - - // Create collections if they don't exist - const existingCollections = await db.listCollections().toArray() - const existingNames = existingCollections.map((c) => c.name) - - for (const [key, collectionName] of Object.entries(collections)) { - if (!existingNames.includes(collectionName)) { - await db.createCollection(collectionName) - } - - // Create indexes based on collection type - const collection = db.collection(collectionName) - - switch (key) { - case 'documents': - await collection.createIndexes([ - { key: { title: 'text', content: 'text' } }, - { key: { createdBy: 1, createdAt: -1 } }, - { key: { status: 1, type: 1 } }, - { key: { 'category.id': 1 } }, - { key: { tags: 1 } }, - { key: { slug: 1 }, unique: true }, - ]) - break - - case 'users': - await collection.createIndexes([ - { key: { email: 1 }, unique: true }, - { key: { role: 1, isActive: 1 } }, - { key: { teams: 1 } }, - ]) - break - - case 'workflows': - await collection.createIndexes([ - { key: { workflowId: 1, status: 1 } }, - { key: { documentId: 1 } }, - { key: { 'context.triggeredBy': 1 } }, - { key: { startedAt: -1 } }, - ]) - break - - case 'marketAnalysis': - await collection.createIndexes([ - { key: { marketId: 1 } }, - { key: { analysisType: 1, analysisDate: -1 } }, - { key: { 'recommendation.strategy': 1 } }, - ]) - break - - case 'competitors': - await collection.createIndexes([ - { key: { name: 1 } }, - { key: { marketPosition: 1 } }, - { key: { lastUpdated: -1 } }, - ]) - break - - case 'marketingTactics': - await collection.createIndexes([ - { key: { category: 1, effortLevel: 1 } }, - { key: { status: 1 } }, - { key: { expectedImpact: -1 } }, - ]) - break - - case 'auditLogs': - await collection.createIndexes([ - { key: { entityId: 1, entityType: 1 } }, - { key: { userId: 1, timestamp: -1 } }, - { key: { action: 1 } }, - { key: { timestamp: -1 } }, - ]) - break - } - } -} - -/** - * Ensure PostgreSQL schema and tables exist - */ -async function ensurePostgresSchema(pool: Pool): Promise { - const client = await pool.connect() - - try { - const schema = DB_CONFIG.postgresql.schema - - // Create schema if it doesn't exist - await client.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`) - - // Create tables - await client.query(` - CREATE TABLE IF NOT EXISTS ${schema}.workflow_definitions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(255) NOT NULL, - description TEXT, - version INTEGER NOT NULL DEFAULT 1, - is_active BOOLEAN NOT NULL DEFAULT true, - definition JSONB NOT NULL, - permissions JSONB NOT NULL DEFAULT '{}', - analytics JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - created_by UUID NOT NULL, - last_modified_by UUID NOT NULL - ) - `) - - await client.query(` - CREATE TABLE IF NOT EXISTS ${schema}.approvals ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workflow_execution_id UUID NOT NULL, - step_id VARCHAR(255) NOT NULL, - approver_id UUID NOT NULL, - status VARCHAR(50) NOT NULL DEFAULT 'pending', - decision VARCHAR(50), - feedback TEXT, - decided_at TIMESTAMP WITH TIME ZONE, - delegated_to UUID, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() - ) - `) - - // Create indexes - await client.query(` - CREATE INDEX IF NOT EXISTS idx_workflow_definitions_name - ON ${schema}.workflow_definitions(name) - `) - - await client.query(` - CREATE INDEX IF NOT EXISTS idx_workflow_definitions_active - ON ${schema}.workflow_definitions(is_active, version) - `) - - await client.query(` - CREATE INDEX IF NOT EXISTS idx_approvals_workflow_execution - ON ${schema}.approvals(workflow_execution_id, step_id) - `) - - await client.query(` - CREATE INDEX IF NOT EXISTS idx_approvals_approver_status - ON ${schema}.approvals(approver_id, status) - `) - } finally { - client.release() - } -} - -/** - * Health check for all database connections - */ -export async function checkDatabaseHealth(): Promise<{ - mongodb: boolean - postgresql: boolean - redis: boolean -}> { - const config = getDatabaseConfig() - - const results = { - mongodb: false, - postgresql: false, - redis: false, - } - - try { - // Check MongoDB - await config.mongodb.client.db('admin').command({ ping: 1 }) - results.mongodb = true - } catch (error: unknown) { - logger.error('MongoDB health check failed:', error) - } - - try { - // Check PostgreSQL - const client = await config.postgresql.pool.connect() - await client.query('SELECT 1') - client.release() - results.postgresql = true - } catch (error: unknown) { - logger.error('PostgreSQL health check failed:', error) - } - - try { - // Check Redis - await config.redis.client.ping() - results.redis = true - } catch (error: unknown) { - logger.error('Redis health check failed:', error) - } - - return results -} diff --git a/apps/web/src/lib/business-strategy/index.ts b/apps/web/src/lib/business-strategy/index.ts deleted file mode 100644 index 5e8fca60e1..0000000000 --- a/apps/web/src/lib/business-strategy/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Business Strategy Expansion & CMS System - * - * Core module for business intelligence and content management - * Integrates with existing Pixelated Empathy infrastructure - */ - -export * from './types' -export * from './services' - -// Core services -export { MarketResearchService } from './services/market-research' -export { CompetitiveIntelligenceService } from './services/competitive-intelligence' -export { GrassrootsMarketingService } from './services/grassroots-marketing' -export { DocumentManagementService } from './services/document-management' -export { UserManagementService } from './services/user-management' -export { WorkflowEngineService } from './services/workflow-engine' - -// Types -export type { - NicheMarket, - CompetitorProfile, - MarketingTactic, - Document, - User, - WorkflowExecution, -} from './types' diff --git a/apps/web/src/lib/business-strategy/services/analytics.ts b/apps/web/src/lib/business-strategy/services/analytics.ts deleted file mode 100644 index 9ecf1e84d2..0000000000 --- a/apps/web/src/lib/business-strategy/services/analytics.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Analytics Service - * - * Cross-system analytics and reporting for the business strategy system. - */ - -import { BaseService } from './base-service' - -export class AnalyticsService extends BaseService {} diff --git a/apps/web/src/lib/business-strategy/services/audit.ts b/apps/web/src/lib/business-strategy/services/audit.ts deleted file mode 100644 index 4fe1a494f7..0000000000 --- a/apps/web/src/lib/business-strategy/services/audit.ts +++ /dev/null @@ -1,367 +0,0 @@ -/** - * Audit Service - * - * Handles audit logging for all business strategy system activities - * Ensures compliance and traceability - */ - -import type { Collection } from 'mongodb' - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { getDatabaseConfig } from '../config/database' -import type { AuditLog, UserId } from '../types' -const logger = createBuildSafeLogger('audit') - -export interface AuditEventInput { - userId?: UserId - type: - | 'authentication' - | 'authorization' - | 'data_access' - | 'data_modification' - | 'system' - action: string - resource?: string - resourceId?: string - result: 'success' | 'failure' | 'error' - details?: Record - timestamp: Date - ipAddress?: string - userAgent?: string - sessionId?: string - riskScore?: number -} - -export class AuditService { - private readonly auditCollection: Collection - - constructor() { - const db = getDatabaseConfig() - this.auditCollection = db.mongodb.database.collection( - db.mongodb.collections.auditLogs, - ) - } - - /** - * Log an audit event - */ - async logEvent(event: AuditEventInput): Promise { - try { - const auditLog: AuditLog = { - id: crypto.randomUUID(), - entityId: event.resourceId ?? '', - entityType: event.resource ?? 'system', - action: event.action, - userId: event.userId ?? ('system' as UserId), - timestamp: event.timestamp, - changes: event.details - ? { details: { from: null, to: event.details } } - : undefined, - metadata: { - type: event.type, - result: event.result, - ipAddress: event.ipAddress, - userAgent: event.userAgent, - sessionId: event.sessionId, - riskScore: event.riskScore, - }, - } - - await this.auditCollection.insertOne(auditLog) - } catch (error: unknown) { - // Don't throw on audit failures to avoid breaking main operations - logger.error('Failed to log audit event:', error) - } - } - - /** - * Log document access - */ - async logDocumentAccess(params: { - userId: UserId - documentId: string - action: 'view' | 'edit' | 'delete' | 'share' | 'export' - ipAddress?: string - userAgent?: string - sessionId?: string - }): Promise { - await this.logEvent({ - userId: params.userId, - type: 'data_access', - action: `document_${params.action}`, - resource: 'document', - resourceId: params.documentId, - result: 'success', - timestamp: new Date(), - ipAddress: params.ipAddress, - userAgent: params.userAgent, - sessionId: params.sessionId, - }) - } - - /** - * Log document modification - */ - async logDocumentModification(params: { - userId: UserId - documentId: string - action: 'create' | 'update' | 'delete' - changes?: Record - ipAddress?: string - userAgent?: string - sessionId?: string - }): Promise { - await this.logEvent({ - userId: params.userId, - type: 'data_modification', - action: `document_${params.action}`, - resource: 'document', - resourceId: params.documentId, - result: 'success', - details: params.changes, - timestamp: new Date(), - ipAddress: params.ipAddress, - userAgent: params.userAgent, - sessionId: params.sessionId, - }) - } - - /** - * Log user authentication - */ - async logAuthentication(params: { - userId?: UserId - action: - | 'login' - | 'logout' - | 'login_failed' - | 'password_reset' - | 'account_locked' - result: 'success' | 'failure' | 'error' - ipAddress?: string - userAgent?: string - details?: Record - }): Promise { - await this.logEvent({ - userId: params.userId, - type: 'authentication', - action: params.action, - resource: 'user', - resourceId: params.userId, - result: params.result, - details: params.details, - timestamp: new Date(), - ipAddress: params.ipAddress, - userAgent: params.userAgent, - riskScore: this.calculateAuthRiskScore(params), - }) - } - - /** - * Log authorization events - */ - async logAuthorization(params: { - userId: UserId - action: string - resource: string - resourceId?: string - result: 'success' | 'failure' - reason?: string - ipAddress?: string - sessionId?: string - }): Promise { - await this.logEvent({ - userId: params.userId, - type: 'authorization', - action: params.action, - resource: params.resource, - resourceId: params.resourceId, - result: params.result, - details: params.reason ? { reason: params.reason } : undefined, - timestamp: new Date(), - ipAddress: params.ipAddress, - sessionId: params.sessionId, - riskScore: params.result === 'failure' ? 75 : 25, - }) - } - - /** - * Get audit logs for an entity - */ - async getEntityAuditLogs( - entityType: string, - entityId: string, - options?: { - limit?: number - skip?: number - startDate?: Date - endDate?: Date - }, - ): Promise { - const filter: Record = { - entityType, - entityId, - } - - if (options?.startDate || options?.endDate) { - filter['timestamp'] = {} - if (options.startDate) { - ;(filter['timestamp'] as Record)['$gte'] = - options.startDate - } - if (options.endDate) { - ;(filter['timestamp'] as Record)['$lte'] = options.endDate - } - } - - const cursor = this.auditCollection.find(filter).sort({ timestamp: -1 }) - - if (options?.skip) { - cursor.skip(options.skip) - } - - if (options?.limit) { - cursor.limit(options.limit) - } - - return await cursor.toArray() - } - - /** - * Get audit logs for a user - */ - async getUserAuditLogs( - userId: UserId, - options?: { - limit?: number - skip?: number - startDate?: Date - endDate?: Date - actions?: string[] - }, - ): Promise { - const filter: Record = { - userId, - } - - if (options?.actions) { - filter['action'] = { $in: options.actions } - } - - if (options?.startDate || options?.endDate) { - filter['timestamp'] = {} - if (options.startDate) { - ;(filter['timestamp'] as Record)['$gte'] = - options.startDate - } - if (options.endDate) { - ;(filter['timestamp'] as Record)['$lte'] = options.endDate - } - } - - const cursor = this.auditCollection.find(filter).sort({ timestamp: -1 }) - - if (options?.skip) { - cursor.skip(options.skip) - } - - if (options?.limit) { - cursor.limit(options.limit) - } - - return await cursor.toArray() - } - - /** - * Get security events (high-risk audit logs) - */ - async getSecurityEvents(options?: { - limit?: number - skip?: number - startDate?: Date - endDate?: Date - minRiskScore?: number - }): Promise { - const filter: Record = { - 'metadata.riskScore': { $gte: options?.minRiskScore ?? 50 }, - } - - if (options?.startDate || options?.endDate) { - filter['timestamp'] = {} - if (options.startDate) { - ;(filter['timestamp'] as Record)['$gte'] = - options.startDate - } - if (options.endDate) { - ;(filter['timestamp'] as Record)['$lte'] = options.endDate - } - } - - const cursor = this.auditCollection - .find(filter) - .sort({ 'timestamp': -1, 'metadata.riskScore': -1 }) - - if (options?.skip) { - cursor.skip(options.skip) - } - - if (options?.limit) { - cursor.limit(options.limit) - } - - return await cursor.toArray() - } - - /** - * Calculate risk score for authentication events - */ - private calculateAuthRiskScore(params: { - action: string - result: string - ipAddress?: string - details?: Record - }): number { - let score = 0 - - // Base score by action and result - if (params.result === 'failure') { - score += 50 - } - - if (params.action === 'login_failed') { - score += 30 - } else if (params.action === 'account_locked') { - score += 80 - } - - // Additional factors from details - if (params.details?.['suspiciousActivity']) { - score += 40 - } - - if (params.details?.['newDevice']) { - score += 20 - } - - if (params.details?.['unusualLocation']) { - score += 30 - } - - return Math.min(score, 100) - } - - /** - * Clean up old audit logs (for compliance with retention policies) - */ - async cleanupOldLogs(retentionDays: number = 2190): Promise { - // 6 years default - const cutoffDate = new Date() - cutoffDate.setDate(cutoffDate.getDate() - retentionDays) - - const result = await this.auditCollection.deleteMany({ - timestamp: { $lt: cutoffDate }, - }) - - return result.deletedCount || 0 - } -} diff --git a/apps/web/src/lib/business-strategy/services/base-service.ts b/apps/web/src/lib/business-strategy/services/base-service.ts deleted file mode 100644 index 3b6222ff53..0000000000 --- a/apps/web/src/lib/business-strategy/services/base-service.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Base Service Class - * - * Provides common functionality for all business strategy services - */ - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import type { DatabaseConfig } from '../config/database' -import { getDatabaseConfig } from '../config/database' -import type { UserId } from '../types/common' -import { AuditService } from './audit' -const logger = createBuildSafeLogger('base-service') - -export abstract class BaseService { - protected db: DatabaseConfig - protected auditService: AuditService - - constructor() { - this.db = getDatabaseConfig() - this.auditService = new AuditService() - } - - /** - * Log an audit event - */ - protected async logAudit(params: { - userId?: UserId - type?: - | 'authentication' - | 'authorization' - | 'data_access' - | 'data_modification' - | 'system' - action: string - entityType?: string - entityId?: string - result: 'success' | 'failure' | 'error' - details?: Record - riskScore?: number - }): Promise { - await this.auditService.logEvent({ - userId: params.userId, - type: params.type ?? 'system', - action: params.action, - resource: params.entityType, - resourceId: params.entityId, - result: params.result, - details: params.details, - riskScore: params.riskScore, - timestamp: new Date(), - }) - } - - /** - * Validate user permissions for an action - */ - protected async validatePermissions( - _userId: UserId, - _resource: string, - _action: string, - _conditions?: Record, - ): Promise { - // Implementation will be added when UserManagementService is complete - // For now, return true to allow development to continue - return true - } - - /** - * Generate a unique ID - */ - protected generateId(): string { - return crypto.randomUUID() - } - - /** - * Validate required fields - */ - protected validateRequired>( - data: T, - requiredFields: (keyof T)[], - ): void { - const missing = requiredFields.filter( - (field) => - data[field] === undefined || data[field] === null || data[field] === '', - ) - - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(', ')}`) - } - } - - /** - * Sanitize input data - */ - protected sanitizeInput>(data: T): T { - const sanitized = { ...data } as Record - - // Remove any potential XSS or injection attempts - for (const [key, value] of Object.entries(sanitized)) { - if (typeof value === 'string') { - // Basic sanitization - remove script tags and dangerous characters - sanitized[key] = value - .replace(/)<[^<]*)*<\/script>/gi, '') - .replace(/javascript:/gi, '') - .replace(/on\w+\s*=/gi, '') - .trim() - } - } - - return sanitized as T - } - - /** - * Handle service errors consistently - */ - protected handleError(error: unknown, context: string): never { - logger.error(`Error in ${context}:`, error) - - if (error instanceof Error) { - throw error - } - - throw new Error(`Unexpected error in ${context}`) - } - - /** - * Create pagination metadata - */ - protected createPaginationMeta(page: number, limit: number, total: number) { - const totalPages = Math.ceil(total / limit) - - return { - page, - limit, - total, - totalPages, - hasNext: page < totalPages, - hasPrev: page > 1, - } - } - - /** - * Build MongoDB filter from search parameters - */ - protected buildMongoFilter( - filters?: Record, - ): Record { - if (!filters) return {} - - const mongoFilter: Record = {} - - for (const [key, value] of Object.entries(filters)) { - if (value === undefined || value === null) continue - - if (Array.isArray(value)) { - mongoFilter[key] = { $in: value } - } else if (typeof value === 'string' && value.includes('*')) { - // Convert wildcard to regex - const regex = value.replace(/\*/g, '.*') - mongoFilter[key] = { $regex: regex, $options: 'i' } - } else { - mongoFilter[key] = value - } - } - - return mongoFilter - } - - /** - * Build sort object for MongoDB - */ - protected buildMongoSort( - sortBy?: string, - sortOrder: 'asc' | 'desc' = 'desc', - ): Record { - if (!sortBy) { - return { createdAt: -1 } - } - - return { [sortBy]: sortOrder === 'asc' ? 1 : -1 } - } -} diff --git a/apps/web/src/lib/business-strategy/services/competitive-intelligence.ts b/apps/web/src/lib/business-strategy/services/competitive-intelligence.ts deleted file mode 100644 index 7802353640..0000000000 --- a/apps/web/src/lib/business-strategy/services/competitive-intelligence.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Competitive Intelligence Service - * - * Provides functionality for competitor tracking, SWOT analysis, - * and market positioning. - */ - -import type { UserId } from '../types/common' -import type { CompetitorProfile } from '../types/market-research' -import { BaseService } from './base-service' - -export class CompetitiveIntelligenceService extends BaseService { - private readonly collectionName: string - - constructor() { - super() - this.collectionName = this.db.mongodb.collections.competitors - } - - /** - * Get all competitors - */ - async getCompetitors(): Promise { - try { - return await this.db.mongodb.database - .collection(this.collectionName) - .find() - .toArray() - } catch (error: unknown) { - return this.handleError(error, 'getCompetitors') - } - } - - /** - * Add or update a competitor - */ - async updateCompetitor( - userId: UserId, - competitor: Partial, - ): Promise { - await this.validatePermissions(userId, 'competitor', 'update') - - const id = competitor.id ?? this.generateId() - const update = { - ...competitor, - id, - lastUpdated: new Date(), - updatedBy: userId, - } - - try { - await this.db.mongodb.database - .collection(this.collectionName) - .updateOne({ id }, { $set: update }, { upsert: true }) - - await this.logAudit({ - userId, - action: 'update', - entityType: 'competitor', - entityId: id, - result: 'success', - }) - } catch (error: unknown) { - return this.handleError(error, 'updateCompetitor') - } - } -} diff --git a/apps/web/src/lib/business-strategy/services/document-management.ts b/apps/web/src/lib/business-strategy/services/document-management.ts deleted file mode 100644 index 0137ebf9eb..0000000000 --- a/apps/web/src/lib/business-strategy/services/document-management.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * Document Management Service - * - * Provides functionality for managing business strategy documents, - * versions, comments, and analytics. - */ - -import { v4 as uuidv4 } from 'uuid' - -import type { UserId, DocumentId } from '../types/common' -import type { - Document, - DocumentCreate, - DocumentUpdate, - DocumentSearch, - DocumentSearchResult, - DocumentVersion, -} from '../types/document-management' -import { BaseService } from './base-service' - -export class DocumentManagementService extends BaseService { - private readonly collectionName: string - - constructor() { - super() - this.collectionName = this.db.mongodb.collections.documents - } - - /** - * Create a new document - */ - async createDocument( - userId: UserId, - data: DocumentCreate, - ): Promise { - await this.validatePermissions(userId, 'document', 'create') - this.validateRequired(data as Record, [ - 'title', - 'content', - 'type', - 'category', - ]) - - const sanitized = this.sanitizeInput( - data as Record, - ) as DocumentCreate - const documentId = this.generateId() as DocumentId - const timestamp = new Date() - - const document: Document = { - id: documentId, - ...sanitized, - contentType: sanitized.contentType ?? 'markdown', - tags: sanitized.tags ?? [], - slug: this.generateSlug(sanitized.title), - version: 1, - size: Buffer.byteLength(sanitized.content), - checksum: this.generateChecksum(sanitized.content), - status: 'draft', - priority: 'medium', - createdBy: userId, - createdAt: timestamp, - lastModifiedBy: userId, - updatedAt: timestamp, - permissions: { - read: [userId], - write: [userId], - approve: [], - delete: [userId], - share: [userId], - }, - isPublic: false, - isTemplate: false, - collaborators: [ - { - userId, - role: 'owner', - addedAt: timestamp, - addedBy: userId, - }, - ], - analytics: { - documentId, - views: 0, - uniqueViews: 0, - lastViewed: timestamp, - averageReadTime: 0, - collaborators: [userId], - editCount: 0, - commentCount: 0, - shareCount: 0, - downloadCount: 0, - searchAppearances: 0, - }, - } - - try { - await this.db.mongodb.database.collection(this.collectionName).insertOne({ - ...document, - _id: documentId as unknown as string, - }) - - await this.logAudit({ - userId, - action: 'create', - entityType: 'document', - entityId: documentId, - result: 'success', - details: { title: document.title }, - }) - - // Create initial version - await this.createVersion(userId, documentId, { - title: document.title, - content: document.content, - changes: ['Initial creation'], - }) - - return document - } catch (error: unknown) { - return this.handleError(error, 'createDocument') - } - } - - /** - * Get a document by ID - */ - async getDocument(userId: UserId, id: DocumentId): Promise { - try { - const document = await this.db.mongodb.database - .collection(this.collectionName) - .findOne({ id }) - - if (!document) { - throw new Error('Document not found') - } - - const hasAccess = await this.validatePermissions( - userId, - 'document', - 'read', - { id }, - ) - if (!hasAccess) { - throw new Error('Access denied') - } - - // Update analytics - await this.updateAnalytics(id, 'view', userId) - - return document - } catch (error: unknown) { - return this.handleError(error, 'getDocument') - } - } - - /** - * Search documents - */ - async searchDocuments( - userId: UserId, - search: DocumentSearch, - ): Promise { - try { - const filter = this.buildMongoFilter(search.filters) - const sort = this.buildMongoSort(search.sort?.field, search.sort?.order) - const { page = 1, limit = 20 } = search.pagination ?? {} - - const documents = await this.db.mongodb.database - .collection(this.collectionName) - .find({ - ...filter, - $or: [ - { isPublic: true }, - { 'permissions.read': userId }, - { 'collaborators.userId': userId }, - ], - }) - .sort(sort) - .skip((page - 1) * limit) - .limit(limit) - .toArray() - - return documents.map((doc) => ({ - document: doc, - score: 1.0, // basic implementation, would use MongoDB text score in real world - highlights: [], - matchedTerms: [], - })) - } catch (error: unknown) { - return this.handleError(error, 'searchDocuments') - } - } - - /** - * Update a document - */ - async updateDocument( - userId: UserId, - id: DocumentId, - updates: DocumentUpdate, - ): Promise { - try { - const hasAccess = await this.validatePermissions( - userId, - 'document', - 'write', - { id }, - ) - if (!hasAccess) { - throw new Error('Access denied') - } - - const timestamp = new Date() - const sanitized = this.sanitizeInput( - updates as Record, - ) as DocumentUpdate - - const currentDoc = await this.getDocument(userId, id) - const newVersion = currentDoc.version + 1 - - const updateFields: any = { - ...sanitized, - version: newVersion, - lastModifiedBy: userId, - updatedAt: timestamp, - } - - if (updates.content) { - updateFields.size = Buffer.byteLength(updates.content) - updateFields.checksum = this.generateChecksum(updates.content) - } - - const result = await this.db.mongodb.database - .collection(this.collectionName) - .findOneAndUpdate( - { id }, - { $set: updateFields }, - { returnDocument: 'after' }, - ) - - if (!result) { - throw new Error('Document not found or update failed') - } - - await this.logAudit({ - userId, - action: 'update', - entityType: 'document', - entityId: id, - result: 'success', - details: { version: newVersion }, - }) - - // Create new version history record if content changed - if (updates.content) { - await this.createVersion(userId, id, { - title: result.title, - content: result.content, - summary: updates.summary, - changes: ['Content updated'], - }) - } - - return result as Document - } catch (error: unknown) { - return this.handleError(error, 'updateDocument') - } - } - - /** - * Create a document version - */ - private async createVersion( - userId: UserId, - documentId: DocumentId, - data: Pick, - ): Promise { - const version: DocumentVersion = { - id: uuidv4(), - documentId, - version: 0, // Will be set by caller or incremented - ...data, - createdBy: userId, - createdAt: new Date(), - size: Buffer.byteLength(data.content), - checksum: this.generateChecksum(data.content), - } - - // Logic to get current version and increment would go here - // For now, assume version is handled by document update - - await this.db.mongodb.database - .collection('document_versions') - .insertOne(version) - return version - } - - /** - * Update document analytics - */ - private async updateAnalytics( - id: DocumentId, - type: 'view' | 'edit' | 'share', - userId: UserId, - ): Promise { - const update: any = { - $set: { 'analytics.lastViewed': new Date() }, - } - - if (type === 'view') { - update.$inc = { 'analytics.views': 1 } - update.$addToSet = { 'analytics.collaborators': userId } - } else if (type === 'edit') { - update.$inc = { 'analytics.editCount': 1 } - } else if (type === 'share') { - update.$inc = { 'analytics.shareCount': 1 } - } - - await this.db.mongodb.database - .collection(this.collectionName) - .updateOne({ id }, update) - } - - private generateSlug(title: string): string { - return title - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, '') - } - - private generateChecksum(content: string): string { - // Simple mock checksum for now - return `sha256:${Buffer.from(content).length}` - } -} diff --git a/apps/web/src/lib/business-strategy/services/export.ts b/apps/web/src/lib/business-strategy/services/export.ts deleted file mode 100644 index b53a88a831..0000000000 --- a/apps/web/src/lib/business-strategy/services/export.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Export Service - * - * Handles document and data exports in various formats. - */ - -import { BaseService } from './base-service' - -export class ExportService extends BaseService {} diff --git a/apps/web/src/lib/business-strategy/services/factory.ts b/apps/web/src/lib/business-strategy/services/factory.ts deleted file mode 100644 index ad6c6c93e7..0000000000 --- a/apps/web/src/lib/business-strategy/services/factory.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Service Factory - * - * Central point for accessing business strategy services. - * Ensures services are properly initialized and shared across the application. - */ - -import { AuditService } from './audit' -import { CompetitiveIntelligenceService } from './competitive-intelligence' -import { DocumentManagementService } from './document-management' -import { MarketResearchService } from './market-research' -import { UserManagementService } from './user-management' -import { WorkflowEngineService } from './workflow-engine' - -export class ServiceFactory { - private static readonly instances: Map = new Map() - - /** - * Get DocumentManagementService instance - */ - static getDocumentManagement(): DocumentManagementService { - return this.getOrCreate( - 'document-management', - () => new DocumentManagementService(), - ) - } - - /** - * Get MarketResearchService instance - */ - static getMarketResearch(): MarketResearchService { - return this.getOrCreate( - 'market-research', - () => new MarketResearchService(), - ) - } - - /** - * Get CompetitiveIntelligenceService instance - */ - static getCompetitiveIntelligence(): CompetitiveIntelligenceService { - return this.getOrCreate( - 'competitive-intelligence', - () => new CompetitiveIntelligenceService(), - ) - } - - /** - * Get UserManagementService instance - */ - static getUserManagement(): UserManagementService { - return this.getOrCreate( - 'user-management', - () => new UserManagementService(), - ) - } - - /** - * Get WorkflowEngineService instance - */ - static getWorkflowEngine(): WorkflowEngineService { - return this.getOrCreate( - 'workflow-engine', - () => new WorkflowEngineService(), - ) - } - - /** - * Get AuditService instance - */ - static getAudit(): AuditService { - return this.getOrCreate('audit', () => new AuditService()) - } - - private static getOrCreate(name: string, creator: () => T): T { - if (!this.instances.has(name)) { - this.instances.set(name, creator()) - } - return this.instances.get(name) - } -} diff --git a/apps/web/src/lib/business-strategy/services/grassroots-marketing.ts b/apps/web/src/lib/business-strategy/services/grassroots-marketing.ts deleted file mode 100644 index 2091854660..0000000000 --- a/apps/web/src/lib/business-strategy/services/grassroots-marketing.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Grassroots Marketing Service - * - * Manages community-driven marketing initiatives and outreach. - */ - -import { BaseService } from './base-service' - -export class GrassrootsMarketingService extends BaseService {} diff --git a/apps/web/src/lib/business-strategy/services/index.ts b/apps/web/src/lib/business-strategy/services/index.ts deleted file mode 100644 index 21d23877a5..0000000000 --- a/apps/web/src/lib/business-strategy/services/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Business Strategy Services - * - * Core service layer for the business strategy expansion and CMS system - */ - -// Core services -export { MarketResearchService } from './market-research' -export { CompetitiveIntelligenceService } from './competitive-intelligence' -export { GrassrootsMarketingService } from './grassroots-marketing' -export { DocumentManagementService } from './document-management' -export { UserManagementService } from './user-management' -export { WorkflowEngineService } from './workflow-engine' - -// Utility services -export { AuditService } from './audit' -export { NotificationService } from './notification' -export { SearchService } from './search' -export { ExportService } from './export' -export { AnalyticsService } from './analytics' - -// Service factory -export { ServiceFactory } from './factory' diff --git a/apps/web/src/lib/business-strategy/services/market-research.ts b/apps/web/src/lib/business-strategy/services/market-research.ts deleted file mode 100644 index d6b3886dbf..0000000000 --- a/apps/web/src/lib/business-strategy/services/market-research.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Market Research Service - * - * Provides functionality for market analysis, trend tracking, - * and opportunity identification. - */ - -import type { UserId } from '../types/common' -import type { MarketTrend, Opportunity } from '../types/market-research' -import { BaseService } from './base-service' - -export class MarketResearchService extends BaseService { - private readonly collectionName: string - - constructor() { - super() - this.collectionName = this.db.mongodb.collections.marketAnalysis - } - - /** - * Create a new market analysis - */ - async createAnalysis(userId: UserId, data: any): Promise { - await this.validatePermissions(userId, 'market-analysis', 'create') - this.validateRequired(data, ['marketId', 'analysisType']) - - const analysis = { - id: this.generateId(), - ...data, - createdBy: userId, - createdAt: new Date(), - updatedAt: new Date(), - } - - try { - await this.db.mongodb.database - .collection(this.collectionName) - .insertOne(analysis) - - await this.logAudit({ - userId, - action: 'create', - entityType: 'market-analysis', - entityId: analysis.id, - result: 'success', - }) - - return analysis - } catch (error: unknown) { - return this.handleError(error, 'createAnalysis') - } - } - - /** - * Get market trends - */ - async getMarketTrends(marketId: string): Promise { - try { - return await this.db.mongodb.database - .collection('market_trends') - .find({ marketId }) - .sort({ date: -1 }) - .toArray() - } catch (error: unknown) { - return this.handleError(error, 'getMarketTrends') - } - } - - /** - * Identify opportunities - */ - async identifyOpportunities(marketId: string): Promise { - try { - // Complex logic would go here to identify opportunities - // from market analysis data - return await this.db.mongodb.database - .collection('market_opportunities') - .find({ marketId, status: 'open' }) - .toArray() - } catch (error: unknown) { - return this.handleError(error, 'identifyOpportunities') - } - } -} diff --git a/apps/web/src/lib/business-strategy/services/notification.ts b/apps/web/src/lib/business-strategy/services/notification.ts deleted file mode 100644 index 8b639dd465..0000000000 --- a/apps/web/src/lib/business-strategy/services/notification.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Notification Service - * - * Handles system notifications, emails, and alerts for the strategy system. - */ - -import { BaseService } from './base-service' - -export class NotificationService extends BaseService {} diff --git a/apps/web/src/lib/business-strategy/services/search.ts b/apps/web/src/lib/business-strategy/services/search.ts deleted file mode 100644 index 4647e52da4..0000000000 --- a/apps/web/src/lib/business-strategy/services/search.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Search Service - * - * Advanced search capabilities across all strategy entities. - */ - -import { BaseService } from './base-service' - -export class SearchService extends BaseService {} diff --git a/apps/web/src/lib/business-strategy/services/user-management.ts b/apps/web/src/lib/business-strategy/services/user-management.ts deleted file mode 100644 index 5e1f849827..0000000000 --- a/apps/web/src/lib/business-strategy/services/user-management.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * User Management Service - * - * Provides functionality for managing system users, roles, - * and permissions. - */ - -import type { UserId } from '../types/common' -import type { User } from '../types/user-management' -import { BaseService } from './base-service' - -export class UserManagementService extends BaseService { - private readonly tableNames: any - - constructor() { - super() - this.tableNames = this.db.postgresql.tables - } - - /** - * Get a user by ID - */ - async getUser(id: UserId): Promise { - try { - const result = await this.db.postgresql.pool.query( - `SELECT * FROM ${this.db.postgresql.schema}.${this.tableNames.users} WHERE id = $1`, - [id], - ) - return result.rows[0] ?? null - } catch (error: unknown) { - return this.handleError(error, 'getUser') - } - } - - /** - * Check permissions - */ - async hasPermission( - userId: UserId, - _resource: string, - _action: string, - ): Promise { - // Implementation for checking permissions in PostgreSQL or Redis cache - // For now, return a basic check - const user = await this.getUser(userId) - if (!user) return false - if (user.role === 'administrator') return true - - // Real implementation would check permission table - return true - } -} diff --git a/apps/web/src/lib/business-strategy/services/workflow-engine.ts b/apps/web/src/lib/business-strategy/services/workflow-engine.ts deleted file mode 100644 index 6a66943ee8..0000000000 --- a/apps/web/src/lib/business-strategy/services/workflow-engine.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Workflow Engine Service - * - * Provides functionality for defining and executing business - * workflows, approvals, and automated tasks. - */ - -import type { UserId, DocumentId, WorkflowId } from '../types/common' -import type { WorkflowExecution } from '../types/workflow-engine' -import { BaseService } from './base-service' - -export class WorkflowEngineService extends BaseService { - private readonly tableNames: any - - constructor() { - super() - this.tableNames = this.db.postgresql.tables - } - - /** - * Start a workflow for a document - */ - async startWorkflow( - userId: UserId, - workflowId: string, - documentId: DocumentId, - ): Promise { - await this.validatePermissions(userId, 'workflow', 'start') - - const timestamp = new Date() - const executionId = this.generateId() - - const execution: WorkflowExecution = { - id: executionId, - createdAt: timestamp, - updatedAt: timestamp, - createdBy: userId, - lastModifiedBy: userId, - workflowId: workflowId as WorkflowId, - workflowVersion: 1, - documentId, - status: 'in-progress', - currentStep: 'step-1', - startedAt: timestamp, - data: {}, - context: { - triggeredBy: userId, - triggerType: 'manual', - }, - steps: [], - notifications: [], - comments: [], - history: [ - { - stepId: 'start', - action: 'triggered', - userId, - timestamp, - details: { workflowId }, - }, - ], - } - - try { - await this.db.mongodb.database - .collection(this.db.mongodb.collections.workflows) - .insertOne(execution) - - await this.logAudit({ - userId, - action: 'start-workflow', - entityType: 'workflow', - entityId: executionId, - result: 'success', - }) - - return execution - } catch (error: unknown) { - return this.handleError(error, 'startWorkflow') - } - } - - /** - * Approve a workflow step - */ - async approveStep( - userId: UserId, - executionId: string, - stepId: string, - feedback?: string, - ): Promise { - try { - const timestamp = new Date() - - // Update MongoDB execution record - await this.db.mongodb.database - .collection(this.db.mongodb.collections.workflows) - .updateOne( - { id: executionId }, - { - $push: { - history: { - stepId, - action: 'approved', - userId, - timestamp, - details: feedback ? { feedback } : undefined, - }, - }, - }, - ) - - // Update PostgreSQL approval record if any - await this.db.postgresql.pool.query( - `UPDATE ${this.db.postgresql.schema}.${this.tableNames.approvals} - SET status = 'approved', decision = 'approve', feedback = $1, decided_at = $2 - WHERE workflow_execution_id = $3 AND step_id = $4 AND approver_id = $5`, - [feedback, timestamp, executionId, stepId, userId], - ) - - await this.logAudit({ - userId, - action: 'approve-step', - entityType: 'workflow', - entityId: executionId, - result: 'success', - details: { stepId }, - }) - } catch (error: unknown) { - return this.handleError(error, 'approveStep') - } - } -} diff --git a/apps/web/src/lib/business-strategy/types/common.ts b/apps/web/src/lib/business-strategy/types/common.ts deleted file mode 100644 index f0eca934f2..0000000000 --- a/apps/web/src/lib/business-strategy/types/common.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Common Types for Business Strategy System - */ - -export type BusinessStrategyId = string & { readonly __brand: unique symbol } -export type UserId = string & { readonly __brand: unique symbol } -export type DocumentId = string & { readonly __brand: unique symbol } -export type WorkflowId = string & { readonly __brand: unique symbol } - -export interface BaseEntity { - id: string - createdAt: Date - updatedAt: Date - createdBy: UserId - lastModifiedBy: UserId -} - -export interface PaginationParams { - page: number - limit: number - sortBy?: string - sortOrder?: 'asc' | 'desc' -} - -export interface PaginatedResponse { - data: T[] - pagination: { - page: number - limit: number - total: number - totalPages: number - hasNext: boolean - hasPrev: boolean - } -} - -export interface SearchParams { - query?: string - filters?: Record - pagination?: PaginationParams -} - -export interface ApiResponse { - success: boolean - data?: T - error?: { - code: string - message: string - details?: unknown - } - timestamp: Date -} - -export type Priority = 'low' | 'medium' | 'high' | 'critical' -export type Status = 'draft' | 'review' | 'approved' | 'published' | 'archived' - -export interface Metadata { - tags: string[] - category: string - priority: Priority - status: Status - version: number -} - -export interface AuditLog { - id: string - entityId: string - entityType: string - action: string - userId: UserId - timestamp: Date - changes?: Record - metadata?: Record -} diff --git a/apps/web/src/lib/business-strategy/types/competitive-intelligence.ts b/apps/web/src/lib/business-strategy/types/competitive-intelligence.ts deleted file mode 100644 index d43770edb7..0000000000 --- a/apps/web/src/lib/business-strategy/types/competitive-intelligence.ts +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Competitive Intelligence Types - */ - -import type { BaseEntity, Priority } from './common' - -export interface CompetitiveStrength { - id: string - category: 'product' | 'marketing' | 'operations' | 'financial' | 'strategic' - name: string - description: string - impact: number // 0-100 - evidence: string[] - lastVerified: Date -} - -export interface CompetitiveWeakness { - id: string - category: 'product' | 'marketing' | 'operations' | 'financial' | 'strategic' - name: string - description: string - severity: number // 0-100 - exploitability: number // 0-100 - evidence: string[] - lastVerified: Date -} - -export interface Feature { - featureId: string - name: string - description: string - category: string - hasFeature: boolean - quality?: number // 0-100 - uniqueness?: number // 0-100 - customerSatisfaction?: number // 0-100 - implementationDate?: Date - notes?: string -} - -export interface PricingTier { - id: string - name: string - price: number - currency: string - billingPeriod: 'monthly' | 'quarterly' | 'annually' | 'one-time' - features: string[] - limitations?: string[] - targetSegment?: string - popularity?: number // 0-100 -} - -export interface PricingModel { - model: 'subscription' | 'one-time' | 'usage-based' | 'freemium' | 'enterprise' - tiers: PricingTier[] - freeTrialDays?: number - hasFreeTier: boolean - customPricingAvailable: boolean - discountsAvailable: string[] - lastUpdated: Date -} - -export interface CustomerSegment { - id: string - name: string - description: string - size: number - growthRate?: number - profitability?: number // 0-100 - acquisitionCost?: number - lifetimeValue?: number - churnRate?: number - satisfactionScore?: number // 0-100 -} - -export interface FeatureGap { - gapId: string - featureName: string - description: string - category: 'critical' | 'important' | 'nice-to-have' - competitorHas: boolean - weHave: boolean - customerDemand: number // 0-100 - implementationEffort: number // 1-10 - estimatedCost: number - expectedImpact: number // 1-10 - recommendation: 'adopt-and-improve' | 'ignore' | 'differentiate-differently' - reasoning: string - timeline?: string - dependencies?: string[] -} - -export interface CompetitorAnalysis extends BaseEntity { - competitorId: string - name: string - website?: string - marketPosition: 'leader' | 'challenger' | 'follower' | 'niche' - marketShare?: number - revenue?: number - employees?: number - valuation?: number - fundingRounds?: { - round: string - amount: number - date: Date - investors: string[] - }[] - strengths: CompetitiveStrength[] - weaknesses: CompetitiveWeakness[] - features: Feature[] - pricing: PricingModel - customerSegments: CustomerSegment[] - gapsVsUs: FeatureGap[] - marketingStrategy?: { - channels: string[] - messaging: string[] - positioning: string - budget?: number - } - partnerships?: string[] - recentNews?: { - title: string - summary: string - date: Date - source: string - impact: 'positive' | 'negative' | 'neutral' - }[] - swotAnalysis?: { - strengths: string[] - weaknesses: string[] - opportunities: string[] - threats: string[] - } - lastUpdated: Date - nextReviewDate: Date - dataQuality: number // 0-100 - sources: string[] -} - -export interface CompetitiveScenario { - id: string - name: string - description: string - type: - | 'price-war' - | 'feature-launch' - | 'market-entry' - | 'acquisition' - | 'partnership' - probability: number // 0-100 - impact: number // 0-100 - timeframe: 'immediate' | 'short-term' | 'medium-term' | 'long-term' - triggers: string[] - indicators: string[] -} - -export interface ResponseStrategy { - id: string - scenarioId: string - name: string - description: string - type: 'defensive' | 'offensive' | 'neutral' | 'collaborative' - actions: { - action: string - timeline: string - owner: string - resources: string[] - expectedOutcome: string - }[] - riskLevel: Priority - successProbability: number // 0-100 - estimatedCost: number - expectedROI?: number - approvalRequired: boolean - lastUpdated: Date -} - -export interface CompetitorUpdate { - id: string - competitorId: string - updateType: - | 'product' - | 'pricing' - | 'marketing' - | 'funding' - | 'personnel' - | 'strategy' - title: string - description: string - impact: 'high' | 'medium' | 'low' - actionRequired: boolean - recommendedResponse?: string - source: string - confidence: number // 0-100 - detectedAt: Date - verifiedAt?: Date -} diff --git a/apps/web/src/lib/business-strategy/types/document-management.ts b/apps/web/src/lib/business-strategy/types/document-management.ts deleted file mode 100644 index 5ff73edf99..0000000000 --- a/apps/web/src/lib/business-strategy/types/document-management.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * Document Management Types - */ - -import type { BaseEntity, Status, Priority, UserId, DocumentId } from './common' - -export type DocumentType = - | 'market-analysis' - | 'competitive-analysis' - | 'marketing-plan' - | 'strategy-document' - | 'research-report' - | 'presentation' - | 'template' - | 'policy' - | 'other' - -export type ContentType = 'markdown' | 'html' | 'rich-text' | 'plain-text' - -export interface Category { - id: string - name: string - description?: string - parent?: string - color?: string - icon?: string -} - -export interface DocumentPermissions { - read: string[] // user IDs or role names - write: string[] - approve: string[] - delete: string[] - share: string[] -} - -export interface DocumentVersion { - id: string - documentId: DocumentId - version: number - title: string - content: string - summary?: string - changes: string[] - createdBy: UserId - createdAt: Date - size: number // bytes - checksum: string -} - -export interface DocumentComment { - id: string - documentId: DocumentId - versionId?: string - parentId?: string // for replies - content: string - author: UserId - createdAt: Date - updatedAt?: Date - resolved: boolean - position?: { - start: number - end: number - text: string - } - reactions?: { - emoji: string - users: UserId[] - }[] -} - -export interface DocumentTag { - id: string - name: string - color?: string - description?: string - category?: string -} - -export interface DocumentAnalytics { - documentId: DocumentId - views: number - uniqueViews: number - lastViewed: Date - averageReadTime: number - collaborators: UserId[] - editCount: number - commentCount: number - shareCount: number - downloadCount: number - searchAppearances: number - popularSections?: { - section: string - views: number - }[] -} - -export interface Document extends BaseEntity { - title: string - slug: string - content: string - contentType: ContentType - type: DocumentType - category: Category - tags: DocumentTag[] - description?: string - excerpt?: string - - // Metadata - status: Status - priority: Priority - version: number - size: number // bytes - checksum: string - - // Permissions and access - permissions: DocumentPermissions - isPublic: boolean - isTemplate: boolean - - // Workflow - workflow?: { - currentStep?: string - executionId?: string - approvals: Approval[] - } - - // Relationships - parentId?: DocumentId - relatedDocuments?: DocumentId[] - attachments?: { - id: string - name: string - type: string - size: number - url: string - uploadedBy: UserId - uploadedAt: Date - }[] - - // Analytics - analytics: DocumentAnalytics - - // Content structure - tableOfContents?: { - level: number - title: string - anchor: string - }[] - - // Collaboration - collaborators: { - userId: UserId - role: 'owner' | 'editor' | 'reviewer' | 'viewer' - addedAt: Date - addedBy: UserId - }[] - - // Publishing - publishedAt?: Date - publishedBy?: UserId - scheduledPublishAt?: Date - - // SEO and discovery - seoTitle?: string - seoDescription?: string - keywords?: string[] - - // Archival - archivedAt?: Date - archivedBy?: UserId - archiveReason?: string -} - -export interface Approval { - id: string - stepId: string - approver: UserId - status: 'pending' | 'approved' | 'rejected' | 'delegated' - decision?: 'approve' | 'reject' | 'request-changes' - feedback?: string - decidedAt?: Date - delegatedTo?: UserId - attachments?: string[] -} - -export interface DocumentTemplate extends BaseEntity { - name: string - description: string - type: DocumentType - category: Category - content: string - contentType: ContentType - variables: { - name: string - type: 'text' | 'number' | 'date' | 'boolean' | 'select' - description: string - required: boolean - defaultValue?: unknown - options?: string[] // for select type - }[] - instructions: string - examples?: string[] - usageCount: number - rating?: number // 0-5 - reviews?: { - userId: UserId - rating: number - comment: string - createdAt: Date - }[] -} - -export interface DocumentExport { - id: string - documentId: DocumentId - format: 'pdf' | 'docx' | 'html' | 'markdown' | 'json' | 'txt' - options: { - includeComments?: boolean - includeVersionHistory?: boolean - includeAnalytics?: boolean - watermark?: string - password?: boolean - } - status: 'pending' | 'processing' | 'completed' | 'failed' - downloadUrl?: string - expiresAt?: Date - requestedBy: UserId - requestedAt: Date - completedAt?: Date - error?: string - fileSize?: number -} - -export interface DocumentImport { - id: string - fileName: string - fileType: string - fileSize: number - status: 'pending' | 'processing' | 'completed' | 'failed' - options: { - preserveFormatting?: boolean - extractImages?: boolean - convertToMarkdown?: boolean - autoTag?: boolean - autoCategory?: boolean - } - result?: { - documentId?: DocumentId - warnings?: string[] - errors?: string[] - } - uploadedBy: UserId - uploadedAt: Date - processedAt?: Date - error?: string -} - -export interface DocumentSearch { - query: string - filters?: { - type?: DocumentType[] - category?: string[] - tags?: string[] - status?: Status[] - author?: UserId[] - dateRange?: { - start: Date - end: Date - field: 'createdAt' | 'updatedAt' | 'publishedAt' - } - } - sort?: { - field: 'relevance' | 'createdAt' | 'updatedAt' | 'title' | 'views' - order: 'asc' | 'desc' - } - pagination?: { - page: number - limit: number - } -} - -export interface DocumentSearchResult { - document: Document - score: number - highlights: { - field: string - fragments: string[] - }[] - matchedTerms: string[] -} - -export interface DocumentCreate { - title: string - content: string - contentType?: ContentType - type: DocumentType - category: Category - tags?: DocumentTag[] - description?: string - excerpt?: string - priority?: Priority - isPublic?: boolean - isTemplate?: boolean -} - -export interface DocumentUpdate { - title?: string - content?: string - contentType?: ContentType - type?: DocumentType - category?: Category - tags?: DocumentTag[] - description?: string - excerpt?: string - priority?: Priority - status?: Status - isPublic?: boolean - isTemplate?: boolean - summary?: string -} diff --git a/apps/web/src/lib/business-strategy/types/grassroots-marketing.ts b/apps/web/src/lib/business-strategy/types/grassroots-marketing.ts deleted file mode 100644 index a5705ca10c..0000000000 --- a/apps/web/src/lib/business-strategy/types/grassroots-marketing.ts +++ /dev/null @@ -1,338 +0,0 @@ -/** - * Grassroots Marketing Types - */ - -import type { BaseEntity } from './common' - -export type EffortLevel = 'low' | 'medium' | 'high' | 'very-high' -export type ImpactScore = number // 0-100 - -export interface Resource { - id: string - type: 'time' | 'money' | 'people' | 'tools' | 'content' - name: string - description: string - quantity: number - unit: string - cost?: number - availability: 'available' | 'limited' | 'unavailable' -} - -export interface Timeline { - startDate: Date - endDate: Date - milestones: { - name: string - date: Date - description: string - deliverables: string[] - }[] - phases: { - name: string - startDate: Date - endDate: Date - description: string - objectives: string[] - }[] -} - -export interface Metric { - id: string - name: string - description: string - type: 'quantitative' | 'qualitative' - unit?: string - target?: number - baseline?: number - frequency: 'daily' | 'weekly' | 'monthly' | 'quarterly' - dataSource: string - calculationMethod?: string -} - -export interface Template { - id: string - name: string - type: - | 'email' - | 'social-post' - | 'blog-post' - | 'script' - | 'presentation' - | 'document' - description: string - content: string - variables: string[] - instructions: string - examples?: string[] - lastUpdated: Date -} - -export interface MarketingTactic extends BaseEntity { - name: string - description: string - category: - | 'content' - | 'community' - | 'partnerships' - | 'events' - | 'pr' - | 'referral' - effortLevel: EffortLevel - expectedImpact: ImpactScore - resources: Resource[] - timeline: Timeline - successMetrics: Metric[] - templates: Template[] - instructions: string - prerequisites?: string[] - bestPractices: string[] - commonMistakes: string[] - caseStudies?: { - organization: string - summary: string - results: string - source: string - }[] - status: 'planned' | 'in-progress' | 'completed' | 'paused' | 'cancelled' - actualEffort?: EffortLevel - actualImpact?: ImpactScore - lessons?: string[] -} - -export interface Platform { - id: string - name: string - type: - | 'social' - | 'professional' - | 'forum' - | 'messaging' - | 'video' - | 'audio' - | 'email' - description: string - audience: { - size: number - demographics: string[] - interests: string[] - behavior: string[] - } - contentTypes: string[] - engagementRates: { - average: number - ourPerformance?: number - } - algorithms: string[] - bestPractices: string[] - limitations: string[] - costs: { - organic: boolean - paidOptions: string[] - minimumBudget?: number - } -} - -export interface CommunityStrategy extends BaseEntity { - platformId: string - platform: Platform - objectives: string[] - targetAudience: { - demographics: string[] - interests: string[] - painPoints: string[] - goals: string[] - } - contentStrategy: { - themes: string[] - contentTypes: string[] - postingFrequency: string - engagementTactics: string[] - } - communityGuidelines: string[] - moderationPolicy: string - crisisManagement: { - scenarios: string[] - responses: string[] - escalationProcedure: string[] - } - metrics: Metric[] - budget?: number - timeline: Timeline - resources: Resource[] -} - -export interface Channel { - id: string - name: string - type: 'owned' | 'earned' | 'shared' | 'paid' - platform?: string - audience: number - engagementRate: number - contentTypes: string[] - postingSchedule: string - moderators: string[] - guidelines: string[] -} - -export interface ContentCalendar extends BaseEntity { - name: string - description: string - channels: Channel[] - timeframe: { - startDate: Date - endDate: Date - } - themes: { - name: string - description: string - keywords: string[] - contentPillars: string[] - }[] - contentPlan: { - date: Date - channelId: string - contentType: string - title: string - description: string - status: 'planned' | 'created' | 'scheduled' | 'published' - assignedTo?: string - approvedBy?: string - metrics?: { - views?: number - engagement?: number - clicks?: number - conversions?: number - } - }[] - approvalWorkflow: { - steps: string[] - approvers: string[] - deadlines: Date[] - } -} - -export interface Partnership extends BaseEntity { - partnerName: string - partnerType: 'organization' | 'individual' | 'media' | 'vendor' | 'customer' - industry: string - size: 'startup' | 'small' | 'medium' | 'large' | 'enterprise' - contactInfo: { - name: string - title: string - email: string - phone?: string - linkedin?: string - } - partnershipType: - | 'content' - | 'event' - | 'cross-promotion' - | 'integration' - | 'referral' - status: - | 'prospect' - | 'contacted' - | 'negotiating' - | 'active' - | 'paused' - | 'ended' - valueProposition: { - forUs: string[] - forThem: string[] - } - collaborationAreas: string[] - terms: { - duration?: string - exclusivity?: boolean - financialTerms?: string - deliverables: string[] - responsibilities: { - ours: string[] - theirs: string[] - } - } - successMetrics: Metric[] - communications: { - date: Date - type: 'email' | 'call' | 'meeting' | 'event' - summary: string - nextSteps: string[] - }[] - documents: string[] -} - -export interface AdvocacyProgram extends BaseEntity { - name: string - description: string - type: - | 'referral' - | 'ambassador' - | 'case-study' - | 'testimonial' - | 'user-generated-content' - objectives: string[] - targetParticipants: { - criteria: string[] - size: number - demographics: string[] - } - incentiveStructure: { - type: 'monetary' | 'product' | 'recognition' | 'access' | 'experience' - tiers: { - name: string - requirements: string[] - rewards: string[] - }[] - } - participationRequirements: string[] - onboardingProcess: string[] - supportResources: string[] - communicationPlan: { - frequency: string - channels: string[] - contentTypes: string[] - } - legalConsiderations: string[] - metrics: Metric[] - budget: number - timeline: Timeline - participants?: { - userId: string - joinDate: Date - tier: string - contributions: string[] - rewards: string[] - status: 'active' | 'inactive' | 'suspended' - }[] -} - -export interface GrassrootsStrategy extends BaseEntity { - name: string - description: string - objectives: string[] - targetAudience: { - primary: string[] - secondary: string[] - } - tactics: MarketingTactic[] - communityStrategies: CommunityStrategy[] - contentStrategies: ContentCalendar[] - partnershipOpportunities: Partnership[] - advocacyPrograms: AdvocacyProgram[] - successMetrics: Metric[] - budget: { - totalBudget: number - allocation: { - category: string - amount: number - percentage: number - }[] - } - timeline: Timeline - riskAssessment: { - risks: string[] - mitigations: string[] - } - competitiveAdvantages: string[] - expectedOutcomes: string[] -} diff --git a/apps/web/src/lib/business-strategy/types/index.ts b/apps/web/src/lib/business-strategy/types/index.ts deleted file mode 100644 index d6cf340c7c..0000000000 --- a/apps/web/src/lib/business-strategy/types/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Business Strategy System Types - * - * Core type definitions for the business strategy expansion and CMS system - */ - -// Re-export all types -export * from './market-research' -export * from './competitive-intelligence' -export * from './grassroots-marketing' -export * from './document-management' -export * from './user-management' -export * from './workflow-engine' -export * from './common' diff --git a/apps/web/src/lib/business-strategy/types/market-research.ts b/apps/web/src/lib/business-strategy/types/market-research.ts deleted file mode 100644 index 45b8a61852..0000000000 --- a/apps/web/src/lib/business-strategy/types/market-research.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Market Research Types - */ - -import type { BaseEntity, Priority, BusinessStrategyId } from './common' - -export interface MarketSize { - totalAddressableMarket: number - serviceableAddressableMarket: number - serviceableObtainableMarket: number - currency: string - confidence: number // 0-100 -} - -export interface AccessibilityFactor { - id: string - name: string - description: string - impact: number // 0-100 - difficulty: number // 0-100 -} - -export interface AccessibilityScore { - score: number // 0-100 - factors: AccessibilityFactor[] - reasoning: string -} - -export interface BarrierToEntry { - id: string - type: 'regulatory' | 'financial' | 'technical' | 'competitive' | 'cultural' - name: string - description: string - severity: Priority - mitigationStrategy?: string - estimatedCost?: number - timeToOvercome?: number // months -} - -export interface CompetitiveAdvantage { - id: string - type: 'technology' | 'brand' | 'cost' | 'network' | 'data' | 'regulatory' - name: string - description: string - strength: number // 0-100 - sustainability: number // 0-100 - uniqueness: number // 0-100 -} - -export interface MarketOpportunity { - id: string - name: string - description: string - marketSegment: string - estimatedValue: number - probability: number // 0-100 - timeframe: 'immediate' | 'short-term' | 'medium-term' | 'long-term' - requiredInvestment: number - expectedROI: number -} - -export interface MarketThreat { - id: string - name: string - description: string - type: 'competitive' | 'regulatory' | 'technological' | 'economic' | 'social' - probability: number // 0-100 - impact: number // 0-100 - mitigationStrategy?: string -} - -export interface StrategyRecommendation { - id: string - strategy: 'enter' | 'avoid' | 'monitor' | 'partner' - reasoning: string - confidence: number // 0-100 - timeline: string - requiredResources: string[] - expectedOutcome: string - riskFactors: string[] - successMetrics: string[] -} - -export interface NicheMarket extends BaseEntity { - name: string - description: string - marketSize: MarketSize - accessibility: AccessibilityScore - competitiveLandscape: { - directCompetitors: string[] - indirectCompetitors: string[] - competitiveIntensity: number // 0-100 - } - barriers: BarrierToEntry[] - advantages: CompetitiveAdvantage[] - opportunities: MarketOpportunity[] - threats: MarketThreat[] - recommendation: StrategyRecommendation - analysisDate: Date - nextReviewDate: Date -} - -export interface MarketAnalysis extends BaseEntity { - marketId: BusinessStrategyId - marketName: string - analysisType: 'initial' | 'quarterly' | 'annual' | 'ad-hoc' - marketSize: MarketSize - accessibility: AccessibilityScore - competitiveLandscape: { - directCompetitors: CompetitorProfile[] - indirectCompetitors: CompetitorProfile[] - competitiveIntensity: number - } - barriers: BarrierToEntry[] - opportunities: MarketOpportunity[] - threats: MarketThreat[] - recommendation: StrategyRecommendation - confidence: number // 0-100 - dataQuality: number // 0-100 - sources: string[] -} - -export interface CompetitorProfile { - id: string - name: string - website?: string - marketPosition: 'leader' | 'challenger' | 'follower' | 'niche' - marketShare?: number - revenue?: number - employees?: number - foundedYear?: number - headquarters?: string - strengths: string[] - weaknesses: string[] - keyProducts: string[] - pricingModel?: string - targetMarkets: string[] - recentNews?: string[] - lastUpdated: Date -} - -export interface MarketTrend { - id: string - marketId: string - name: string - description: string - direction: 'up' | 'down' | 'stable' - impact: 'high' | 'medium' | 'low' - date: Date - source?: string -} - -export interface Opportunity { - id: string - marketId: string - name: string - description: string - status: 'open' | 'closed' | 'in-progress' - estimatedValue?: number - probability?: number - createdAt: Date -} diff --git a/apps/web/src/lib/business-strategy/types/user-management.ts b/apps/web/src/lib/business-strategy/types/user-management.ts deleted file mode 100644 index a75cd4d3fc..0000000000 --- a/apps/web/src/lib/business-strategy/types/user-management.ts +++ /dev/null @@ -1,313 +0,0 @@ -/** - * User Management Types for Business Strategy System - */ - -import type { BaseEntity, UserId } from './common' - -export type Role = - | 'administrator' - | 'content_creator' - | 'editor' - | 'viewer' - | 'analyst' - | 'manager' - -export interface Permission { - resource: string // document, user, workflow, analytics, etc. - action: - | 'create' - | 'read' - | 'update' - | 'delete' - | 'approve' - | 'share' - | 'export' - conditions?: PermissionCondition[] -} - -export interface PermissionCondition { - field: string - operator: - | 'equals' - | 'not_equals' - | 'in' - | 'not_in' - | 'contains' - | 'starts_with' - value: unknown -} - -export interface NotificationSettings { - email: { - enabled: boolean - frequency: 'immediate' | 'daily' | 'weekly' | 'never' - types: string[] - } - inApp: { - enabled: boolean - types: string[] - } - slack?: { - enabled: boolean - webhook?: string - types: string[] - } -} - -export interface EditorSettings { - theme: 'light' | 'dark' | 'auto' - fontSize: number - fontFamily: string - lineHeight: number - wordWrap: boolean - showLineNumbers: boolean - autoSave: boolean - autoSaveInterval: number // seconds - spellCheck: boolean - grammar: boolean -} - -export interface DashboardSettings { - layout: 'grid' | 'list' | 'cards' - itemsPerPage: number - defaultView: 'recent' | 'favorites' | 'assigned' | 'all' - showAnalytics: boolean - showNotifications: boolean - compactMode: boolean - widgets: { - id: string - enabled: boolean - position: { x: number; y: number } - size: { width: number; height: number } - }[] -} - -export interface UserPreferences { - theme: 'light' | 'dark' | 'auto' - language: string - timezone: string - dateFormat: string - timeFormat: '12h' | '24h' - notifications: NotificationSettings - editor: EditorSettings - dashboard: DashboardSettings - privacy: { - showOnlineStatus: boolean - showActivity: boolean - allowMentions: boolean - } -} - -export interface UserActivity { - id: string - userId: UserId - type: - | 'login' - | 'logout' - | 'document_view' - | 'document_edit' - | 'comment' - | 'approval' - entityType?: string - entityId?: string - description: string - metadata?: Record - timestamp: Date - ipAddress?: string - userAgent?: string - location?: { - country?: string - city?: string - coordinates?: [number, number] - } -} - -export interface UserSession { - id: string - userId: UserId - token: string - refreshToken?: string - deviceId?: string - deviceName?: string - deviceType: 'desktop' | 'mobile' | 'tablet' | 'unknown' - browser?: string - os?: string - ipAddress: string - location?: { - country?: string - city?: string - } - createdAt: Date - lastActiveAt: Date - expiresAt: Date - isActive: boolean -} - -export interface UserInvitation { - id: string - email: string - role: Role - permissions?: Permission[] - invitedBy: UserId - invitedAt: Date - expiresAt: Date - acceptedAt?: Date - token: string - message?: string - status: 'pending' | 'accepted' | 'expired' | 'revoked' -} - -export interface User extends BaseEntity { - email: string - name: string - firstName?: string - lastName?: string - avatar?: string - bio?: string - title?: string - department?: string - location?: string - - // Authentication - role: Role - permissions: Permission[] - isActive: boolean - isVerified: boolean - - // Preferences - preferences: UserPreferences - - // Security - lastLogin?: Date - lastPasswordChange?: Date - failedLoginAttempts: number - lockedUntil?: Date - twoFactorEnabled: boolean - - // Onboarding - onboardingCompleted: boolean - onboardingStep?: string - - // Social - socialLinks?: { - linkedin?: string - twitter?: string - github?: string - website?: string - } - - // Analytics - analytics: { - documentsCreated: number - documentsEdited: number - commentsPosted: number - approvalsGiven: number - loginCount: number - totalTimeSpent: number // minutes - lastActivity?: Date - } - - // Notifications - unreadNotifications: number - - // Team - teams?: string[] - managerId?: UserId - directReports?: UserId[] -} - -export interface Team extends BaseEntity { - name: string - description?: string - type: 'department' | 'project' | 'working-group' | 'committee' - managerId: UserId - members: { - userId: UserId - role: 'manager' | 'member' | 'contributor' - joinedAt: Date - addedBy: UserId - }[] - permissions: Permission[] - settings: { - isPublic: boolean - allowSelfJoin: boolean - requireApproval: boolean - } - analytics: { - memberCount: number - documentsCreated: number - collaborationScore: number // 0-100 - } -} - -export interface AuthResult { - success: boolean - user?: User - token?: string - refreshToken?: string - expiresAt?: Date - error?: { - code: string - message: string - } - requiresTwoFactor?: boolean - twoFactorToken?: string -} - -export interface Credentials { - email: string - password: string - twoFactorCode?: string - rememberMe?: boolean -} - -export interface PasswordResetRequest { - id: string - email: string - token: string - requestedAt: Date - expiresAt: Date - usedAt?: Date - ipAddress: string - userAgent?: string -} - -export interface UserNotification { - id: string - userId: UserId - type: 'document' | 'workflow' | 'comment' | 'mention' | 'system' | 'security' - title: string - message: string - data?: Record - read: boolean - readAt?: Date - createdAt: Date - expiresAt?: Date - priority: 'low' | 'medium' | 'high' | 'urgent' - actions?: { - label: string - action: string - url?: string - }[] -} - -export interface AuditEvent { - id: string - userId?: UserId - type: - | 'authentication' - | 'authorization' - | 'data_access' - | 'data_modification' - | 'system' - action: string - resource?: string - resourceId?: string - result: 'success' | 'failure' | 'error' - details?: Record - timestamp: Date - ipAddress?: string - userAgent?: string - sessionId?: string - riskScore?: number // 0-100 -} diff --git a/apps/web/src/lib/business-strategy/types/workflow-engine.ts b/apps/web/src/lib/business-strategy/types/workflow-engine.ts deleted file mode 100644 index d52eeb29dd..0000000000 --- a/apps/web/src/lib/business-strategy/types/workflow-engine.ts +++ /dev/null @@ -1,375 +0,0 @@ -/** - * Workflow Engine Types - */ - -import type { - BaseEntity, - Priority, - UserId, - DocumentId, - WorkflowId, -} from './common' - -export type StepType = - | 'approval' - | 'review' - | 'notification' - | 'automation' - | 'condition' - | 'parallel' - | 'sequential' - -export type WorkflowStatus = - | 'pending' - | 'in-progress' - | 'completed' - | 'rejected' - | 'cancelled' - | 'error' - -export type StepStatus = - | 'pending' - | 'in-progress' - | 'approved' - | 'rejected' - | 'skipped' - | 'error' - -export interface Condition { - id: string - field: string - operator: - | 'equals' - | 'not_equals' - | 'greater_than' - | 'less_than' - | 'contains' - | 'in' - | 'exists' - value: unknown - logicalOperator?: 'and' | 'or' -} - -export interface Action { - id: string - type: - | 'email' - | 'webhook' - | 'update_field' - | 'create_task' - | 'assign_user' - | 'move_document' - config: Record - conditions?: Condition[] -} - -export interface WorkflowTrigger { - id: string - type: - | 'manual' - | 'document_created' - | 'document_updated' - | 'status_changed' - | 'scheduled' - | 'webhook' - conditions?: Condition[] - config?: Record -} - -export interface WorkflowStep { - id: string - name: string - description?: string - type: StepType - order: number - - // Approval/Review specific - approvers?: UserId[] - requiredApprovals?: number // minimum approvals needed - allowDelegation?: boolean - autoApprove?: boolean - autoApproveConditions?: Condition[] - - // Timing - timeoutDays?: number - reminderDays?: number[] - escalationDays?: number - escalationTo?: UserId[] - - // Conditions and actions - conditions?: Condition[] - actions?: Action[] - - // Parallel/Sequential specific - childSteps?: WorkflowStep[] - - // UI configuration - formFields?: { - name: string - type: 'text' | 'textarea' | 'select' | 'checkbox' | 'date' | 'number' - label: string - required: boolean - options?: string[] - defaultValue?: unknown - }[] - - // Notifications - notifications?: { - onStart?: string[] - onComplete?: string[] - onTimeout?: string[] - onEscalation?: string[] - } -} - -export interface WorkflowDefinition extends BaseEntity { - name: string - description?: string - version: number - isActive: boolean - - // Triggers - triggers: WorkflowTrigger[] - - // Steps - steps: WorkflowStep[] - - // Configuration - config: { - allowParallelExecution: boolean - maxConcurrentExecutions?: number - priority: Priority - category: string - tags: string[] - } - - // Permissions - permissions: { - canStart: string[] // user IDs or role names - canView: string[] - canEdit: string[] - canDelete: string[] - } - - // Analytics - analytics: { - executionCount: number - averageDuration: number // minutes - successRate: number // 0-100 - lastExecuted?: Date - } - - // Versioning - parentId?: WorkflowId - changelog?: string -} - -export interface WorkflowExecution extends BaseEntity { - workflowId: WorkflowId - workflowVersion: number - documentId?: DocumentId - - // Status - status: WorkflowStatus - currentStep?: string - - // Execution data - data: Record - context: { - triggeredBy: UserId - triggerType: string - triggerData?: Record - } - - // Steps execution - steps: { - stepId: string - status: StepStatus - assignedTo: UserId[] - startedAt?: Date - completedAt?: Date - completedBy?: UserId - timeoutAt?: Date - - // Approval specific - decision?: 'approve' | 'reject' | 'request-changes' - feedback?: string - attachments?: string[] - - // Delegation - delegatedTo?: UserId - delegatedAt?: Date - delegatedBy?: UserId - delegationReason?: string - - // Automation results - automationResults?: { - success: boolean - output?: unknown - error?: string - executedAt: Date - } - - // Form data - formData?: Record - }[] - - // Timing - startedAt: Date - completedAt?: Date - duration?: number // minutes - - // Error handling - error?: { - code: string - message: string - step?: string - timestamp: Date - retryCount: number - } - - // Notifications sent - notifications: { - type: string - recipient: UserId - sentAt: Date - delivered: boolean - }[] - - // Comments and communication - comments: { - id: string - userId: UserId - stepId?: string - content: string - createdAt: Date - isInternal: boolean - }[] - - // Execution history log - history?: { - stepId: string - action: string - userId: UserId - timestamp: Date - details?: Record - }[] -} - -export interface WorkflowTemplate extends BaseEntity { - name: string - description: string - category: string - tags: string[] - - // Template definition - definition: Omit< - WorkflowDefinition, - 'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'lastModifiedBy' - > - - // Usage - usageCount: number - rating?: number // 0-5 - reviews?: { - userId: UserId - rating: number - comment: string - createdAt: Date - }[] - - // Customization - customizableFields: string[] - variables: { - name: string - type: 'text' | 'number' | 'boolean' | 'select' | 'user' | 'role' - description: string - required: boolean - defaultValue?: unknown - options?: string[] - }[] -} - -export interface WorkflowMetrics { - workflowId: WorkflowId - period: { - start: Date - end: Date - } - - // Execution metrics - totalExecutions: number - completedExecutions: number - failedExecutions: number - cancelledExecutions: number - - // Performance metrics - averageDuration: number // minutes - medianDuration: number - minDuration: number - maxDuration: number - - // Step metrics - stepMetrics: { - stepId: string - stepName: string - averageDuration: number - approvalRate: number // 0-100 - timeoutRate: number // 0-100 - escalationRate: number // 0-100 - }[] - - // User metrics - userMetrics: { - userId: UserId - executionsStarted: number - approvalsGiven: number - averageApprovalTime: number - timeoutCount: number - }[] - - // Bottlenecks - bottlenecks: { - stepId: string - stepName: string - averageWaitTime: number - frequency: number - }[] -} - -export interface ApprovalDecision { - decision: 'approve' | 'reject' | 'request-changes' | 'delegate' - feedback?: string - attachments?: string[] - delegateTo?: UserId - delegationReason?: string - conditions?: { - field: string - value: unknown - }[] -} - -export interface WorkflowNotification { - id: string - workflowExecutionId: string - type: - | 'assignment' - | 'reminder' - | 'escalation' - | 'completion' - | 'timeout' - | 'error' - recipient: UserId - subject: string - message: string - data?: Record - scheduledAt: Date - sentAt?: Date - delivered: boolean - readAt?: Date - actions?: { - label: string - action: string - url?: string - }[] -} diff --git a/apps/web/src/lib/deployment/multi-region/AutomatedFailoverOrchestrator.ts b/apps/web/src/lib/deployment/multi-region/AutomatedFailoverOrchestrator.ts deleted file mode 100644 index 0b0540b50b..0000000000 --- a/apps/web/src/lib/deployment/multi-region/AutomatedFailoverOrchestrator.ts +++ /dev/null @@ -1,1603 +0,0 @@ -import { EventEmitter } from 'events' - -import { - CloudWatchClient, - PutMetricDataCommand, -} from '@aws-sdk/client-cloudwatch' -import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda' -import { - Route53Client, - ChangeResourceRecordSetsCommand, -} from '@aws-sdk/client-route-53' -import { SNSClient, PublishCommand } from '@aws-sdk/client-sns' -import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs' - -import { Logger } from '../../utils/logger' -import { ConfigurationManager } from './ConfigurationManager' -import { CrossRegionDataSyncManager } from './CrossRegionDataSyncManager' -import { HealthMonitor } from './HealthMonitor' - -type LambdaPayload = Record - -/** - * Automated Failover Orchestrator - * Manages automatic failover across regions with health monitoring integration - */ -export class AutomatedFailoverOrchestrator extends EventEmitter { - private readonly logger: Logger - private readonly config: ConfigurationManager - private readonly healthMonitor: HealthMonitor - private readonly dataSyncManager: CrossRegionDataSyncManager - private readonly snsClient: SNSClient - private readonly sqsClient: SQSClient - private readonly lambdaClient: LambdaClient - private readonly route53Client: Route53Client - private readonly cloudWatchClient: CloudWatchClient - private failoverState: FailoverState - private healthCheckInterval: NodeJS.Timeout | null = null - private failoverTimeout: NodeJS.Timeout | null = null - private readonly circuitBreakers: Map = new Map() - private readonly failoverHistory: FailoverEvent[] = [] - private currentPrimaryRegion: string - private readonly backupRegions: string[] = [] - private isActive = false - - constructor( - config: ConfigurationManager, - healthMonitor: HealthMonitor, - dataSyncManager: CrossRegionDataSyncManager, - ) { - super() - this.config = config - this.healthMonitor = healthMonitor - this.dataSyncManager = dataSyncManager - this.logger = new Logger({ prefix: 'AutomatedFailoverOrchestrator' }) - - // Initialize AWS clients - this.snsClient = new SNSClient({ region: config.getPrimaryRegion() }) - this.sqsClient = new SQSClient({ region: config.getPrimaryRegion() }) - this.lambdaClient = new LambdaClient({ region: config.getPrimaryRegion() }) - this.route53Client = new Route53Client({ region: 'us-east-1' }) // Route 53 is global - this.cloudWatchClient = new CloudWatchClient({ - region: config.getPrimaryRegion(), - }) - - this.failoverState = { - status: 'healthy', - primaryRegion: config.getPrimaryRegion(), - backupRegion: null, - lastFailover: null, - failoverCount: 0, - reason: null, - } - - this.currentPrimaryRegion = config.getPrimaryRegion() - this.backupRegions = config.getBackupRegions() - } - - /** - * Initialize the failover orchestrator - */ - async initialize(): Promise { - if (this.isActive) return - try { - this.logger.info('Initializing AutomatedFailoverOrchestrator...') - - // Initialize circuit breakers for each region - this.initializeCircuitBreakers() - - // Set up health monitoring - this.setupHealthMonitoring() - - // Initialize failover state from persistent storage - await this.loadFailoverState() - - // Start health monitoring - this.startHealthMonitoring() - - // Register event handlers - this.registerEventHandlers() - - this.isActive = true - this.logger.info('AutomatedFailoverOrchestrator initialized successfully') - - this.emit('initialized') - } catch (error: unknown) { - this.logger.error('Failed to initialize AutomatedFailoverOrchestrator', { - error, - }) - throw error - } - } - - /** - * Initialize circuit breakers for each region - */ - private initializeCircuitBreakers(): void { - const regions = [this.currentPrimaryRegion, ...this.backupRegions] - - for (const region of regions) { - this.circuitBreakers.set( - region, - new CircuitBreaker({ - name: `region-${region}`, - failureThreshold: 5, - resetTimeout: 60000, // 1 minute - monitoringPeriod: 30000, // 30 seconds - onStateChange: (state) => { - this.logger.info( - `Circuit breaker ${region} state changed to ${state}`, - ) - this.emit('circuitBreakerStateChange', { region, state }) - }, - }), - ) - } - - this.logger.info( - `Initialized circuit breakers for ${regions.length} regions`, - ) - } - - /** - * Set up health monitoring - */ - private setupHealthMonitoring(): void { - // Register custom health checks for failover - this.healthMonitor.registerCheck('failover-readiness', async () => { - try { - const readiness = await this.checkFailoverReadiness() - return { - status: readiness.ready ? 'healthy' : 'unhealthy', - message: readiness.message, - } - } catch (error: unknown) { - return { - status: 'unhealthy', - message: `Failover readiness check failed: ${error instanceof Error ? (error instanceof Error ? error.message : 'Unknown error') : String(error)}`, - } - } - }) - - this.healthMonitor.registerCheck('data-sync-lag', async () => { - try { - const maxLag = await this.checkDataSyncLag() - const threshold = this.config.getFailoverConfig().maxDataSyncLag - - if (maxLag > threshold) { - return { - status: 'unhealthy', - message: `Data sync lag ${maxLag}ms exceeds threshold ${threshold}ms`, - } - } - - return { - status: 'healthy', - message: `Data sync lag ${maxLag}ms within threshold`, - } - } catch (error: unknown) { - return { - status: 'unhealthy', - message: `Data sync lag check failed: ${error instanceof Error ? (error instanceof Error ? error.message : 'Unknown error') : String(error)}`, - } - } - }) - } - - /** - * Check failover readiness - */ - private async checkFailoverReadiness(): Promise<{ - ready: boolean - message: string - }> { - try { - // Check if backup regions are healthy - const backupHealth = await this.checkBackupRegionsHealth() - if (!backupHealth.healthy) { - return { - ready: false, - message: `Backup regions unhealthy: ${backupHealth.reason}`, - } - } - - // Check data synchronization status - const syncStatus = await this.dataSyncManager.getSyncStatus() - const hasPendingSync = Array.from(syncStatus.values()).some( - (status) => status.pendingRecords > 0, - ) - - if (hasPendingSync) { - return { - ready: false, - message: 'Pending data synchronization detected', - } - } - - // Check circuit breaker states - for (const [region, breaker] of this.circuitBreakers) { - if (breaker.isOpen()) { - return { - ready: false, - message: `Circuit breaker open for region: ${region}`, - } - } - } - - return { ready: true, message: 'All failover readiness checks passed' } - } catch (error: unknown) { - this.logger.error('Failover readiness check failed', { error }) - return { - ready: false, - message: `Readiness check error: ${error instanceof Error ? (error instanceof Error ? error.message : 'Unknown error') : String(error)}`, - } - } - } - - /** - * Check backup regions health - */ - private async checkBackupRegionsHealth(): Promise<{ - healthy: boolean - reason?: string - }> { - try { - const healthyRegions: string[] = [] - const unhealthyRegions: string[] = [] - - for (const region of this.backupRegions) { - const breaker = this.circuitBreakers.get(region) - if (!breaker || breaker.isOpen()) { - unhealthyRegions.push(region) - continue - } - - // Check if region is responding - const isHealthy = await this.checkRegionHealth(region) - if (isHealthy) { - healthyRegions.push(region) - } else { - unhealthyRegions.push(region) - } - } - - if (healthyRegions.length === 0) { - return { healthy: false, reason: 'No healthy backup regions available' } - } - - if (unhealthyRegions.length > 0) { - this.logger.warn('Some backup regions are unhealthy', { - unhealthyRegions, - healthyRegions, - }) - } - - return { healthy: true } - } catch (error: unknown) { - this.logger.error('Backup regions health check failed', { error }) - return { - healthy: false, - reason: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : String(error), - } - } - } - - /** - * Check individual region health - */ - private async checkRegionHealth(region: string): Promise { - try { - const breaker = this.circuitBreakers.get(region) - if (!breaker) return false - - return await breaker.execute(async () => { - // Check region-specific health - const health = this.healthMonitor.getRegionHealth(region) - return health.status === 'healthy' - }) - } catch (error: unknown) { - this.logger.error(`Region health check failed for ${region}`, { error }) - return false - } - } - - /** - * Check data synchronization lag - */ - private async checkDataSyncLag(): Promise { - try { - const regions = [this.currentPrimaryRegion, ...this.backupRegions] - const lagPromises = regions.map(async (region) => - this.dataSyncManager.getReplicationLag(region), - ) - - const lags = await Promise.all(lagPromises) - return Math.max(...lags) - } catch (error: unknown) { - this.logger.error('Data sync lag check failed', { error }) - return Infinity - } - } - - /** - * Start health monitoring - */ - private startHealthMonitoring(): void { - const config = this.config.getFailoverConfig() - - this.healthCheckInterval = setInterval(async () => { - try { - await this.performHealthCheck() - } catch (error: unknown) { - this.logger.error('Health check failed', { error }) - } - }, config.healthCheckInterval) - - this.logger.info('Health monitoring started') - } - - /** - * Perform health check - */ - private async performHealthCheck(): Promise { - try { - // Check primary region health - const primaryHealth = this.healthMonitor.getRegionHealth( - this.currentPrimaryRegion, - ) - - if (primaryHealth.status === 'unhealthy') { - this.logger.warn('Primary region unhealthy', { - region: this.currentPrimaryRegion, - reason: primaryHealth.message, - }) - - // Check if failover should be triggered - await this.evaluateFailover() - } - - // Monitor backup regions - await this.monitorBackupRegions() - - // Send health metrics to CloudWatch - await this.sendHealthMetrics() - } catch (error: unknown) { - this.logger.error('Health check execution failed', { error }) - } - } - - /** - * Evaluate if failover should be triggered - */ - private async evaluateFailover(): Promise { - try { - if (this.failoverState.status === 'failing_over') { - this.logger.info('Failover already in progress') - return - } - - const config = this.config.getFailoverConfig() - - // Check if we should wait before triggering failover - if (this.failoverState.lastFailover) { - const timeSinceLastFailover = - Date.now() - this.failoverState.lastFailover.getTime() - if (timeSinceLastFailover < config.failoverCooldown) { - this.logger.info('Failover cooldown active', { - cooldownRemaining: config.failoverCooldown - timeSinceLastFailover, - }) - return - } - } - - // Check failover readiness - const readiness = await this.checkFailoverReadiness() - if (!readiness.ready) { - this.logger.warn('Failover not ready', { reason: readiness.message }) - return - } - - // Select best backup region - const bestBackupRegion = await this.selectBestBackupRegion() - if (!bestBackupRegion) { - this.logger.error('No suitable backup region available for failover') - return - } - - // Trigger failover - await this.triggerFailover(bestBackupRegion, 'Primary region unhealthy') - } catch (error: unknown) { - this.logger.error('Failover evaluation failed', { error }) - } - } - - /** - * Select the best backup region for failover - */ - private async selectBestBackupRegion(): Promise { - try { - const backupHealth = await this.checkBackupRegionsHealth() - if (!backupHealth.healthy) { - return null - } - - const regionScores: { region: string; score: number }[] = [] - - for (const region of this.backupRegions) { - const breaker = this.circuitBreakers.get(region) - if (!breaker || breaker.isOpen()) continue - - let score = 100 // Base score - - // Check health status - const health = this.healthMonitor.getRegionHealth(region) - if (health.status === 'healthy') { - score += 50 - } else { - score -= 100 - continue - } - - // Check data sync lag - const lag = await this.dataSyncManager.getReplicationLag(region) - const maxLag = this.config.getFailoverConfig().maxDataSyncLag - if (lag <= maxLag) { - score += Math.max(0, 50 - (lag / maxLag) * 50) - } else { - score -= 50 - } - - // Check resource utilization (if available) - const metrics = await this.getRegionMetrics(region) - if (metrics) { - // Prefer regions with lower resource utilization - score += Math.max(0, 30 - metrics.cpuUtilization * 30) - score += Math.max(0, 20 - metrics.memoryUtilization * 20) - } - - regionScores.push({ region, score }) - } - - if (regionScores.length === 0) { - return null - } - - // Sort by score (highest first) - regionScores.sort((a, b) => b.score - a.score) - - this.logger.info('Backup region scores', { regionScores }) - return regionScores[0].region - } catch (error: unknown) { - this.logger.error('Failed to select best backup region', { error }) - return null - } - } - - /** - * Get region metrics - */ - private async getRegionMetrics( - region: string, - ): Promise { - try { - // This would typically fetch metrics from monitoring systems - // For now, return mock data - return { - cpuUtilization: Math.random() * 0.8, // 0-80% - memoryUtilization: Math.random() * 0.7, // 0-70% - networkLatency: 50 + Math.random() * 100, // 50-150ms - activeConnections: Math.floor(Math.random() * 1000), - } - } catch (error: unknown) { - this.logger.error(`Failed to get metrics for region ${region}`, { error }) - return null - } - } - - /** - * Trigger failover to backup region - */ - private async triggerFailover( - backupRegion: string, - reason: string, - ): Promise { - try { - this.logger.info('Triggering failover', { - from: this.currentPrimaryRegion, - to: backupRegion, - reason, - }) - - this.failoverState.status = 'failing_over' - this.failoverState.reason = reason - - this.emit('failoverStarted', { - from: this.currentPrimaryRegion, - to: backupRegion, - reason, - timestamp: new Date(), - }) - - // Step 1: Prepare backup region - await this.prepareBackupRegion(backupRegion) - - // Step 2: Sync data to ensure consistency - await this.syncDataBeforeFailover(backupRegion) - - // Step 3: Update DNS routing - await this.updateDNSRouting(backupRegion) - - // Step 4: Update load balancer configuration - await this.updateLoadBalancer(backupRegion) - - // Step 5: Promote backup region to primary - await this.promoteBackupRegion(backupRegion) - - // Step 6: Update configuration - await this.updateConfiguration(backupRegion) - - // Step 7: Verify failover success - await this.verifyFailoverSuccess(backupRegion) - - // Update failover state - this.failoverState = { - status: 'healthy', - primaryRegion: backupRegion, - backupRegion: this.currentPrimaryRegion, - lastFailover: new Date(), - failoverCount: this.failoverState.failoverCount + 1, - reason: null, - } - - this.currentPrimaryRegion = backupRegion - - this.logger.info('Failover completed successfully', { - newPrimary: backupRegion, - oldPrimary: this.failoverState.backupRegion, - }) - - // Send notifications - await this.sendFailoverNotifications('success', backupRegion) - - // Emit success event - this.emit('failoverCompleted', { - newPrimary: backupRegion, - oldPrimary: this.failoverState.backupRegion, - timestamp: new Date(), - }) - - // Save failover state - await this.saveFailoverState() - } catch (error: unknown) { - this.logger.error('Failover failed', { error }) - - this.failoverState.status = 'healthy' - this.failoverState.reason = `Failover failed: ${error instanceof Error ? error.message : 'Unknown error'}` - - // Send failure notifications - await this.sendFailoverNotifications( - 'failed', - backupRegion, - error instanceof Error ? error.message : 'Unknown error', - ) - - this.emit('failoverFailed', { - targetRegion: backupRegion, - error: error instanceof Error ? error.message : 'Unknown error', - timestamp: new Date(), - }) - - throw error - } - } - - /** - * Prepare backup region for failover - */ - private async prepareBackupRegion(region: string): Promise { - try { - this.logger.info('Preparing backup region for failover', { region }) - - // Scale up resources in backup region - await this.scaleUpRegion(region) - - // Warm up caches - await this.warmUpCaches(region) - - // Pre-deploy application if needed - await this.preDeployApplication(region) - - this.logger.info('Backup region preparation completed', { region }) - } catch (error: unknown) { - this.logger.error('Failed to prepare backup region', { region, error }) - throw error - } - } - - /** - * Scale up resources in region - */ - private async scaleUpRegion(region: string): Promise { - try { - // This would typically interact with auto-scaling groups - // For now, log the action - this.logger.info(`Scaling up resources in ${region}`) - - // Simulate scaling operation - await this.invokeLambdaFunction('scale-up-resources', { region }) - } catch (error: unknown) { - this.logger.error(`Failed to scale up region ${region}`, { error }) - throw error - } - } - - /** - * Warm up caches in region - */ - private async warmUpCaches(region: string): Promise { - try { - this.logger.info(`Warming up caches in ${region}`) - - // This would typically warm up CDN caches, application caches, etc. - await this.invokeLambdaFunction('warm-up-caches', { region }) - } catch (error: unknown) { - this.logger.error(`Failed to warm up caches in ${region}`, { error }) - // Non-critical, continue with failover - } - } - - /** - * Pre-deploy application if needed - */ - private async preDeployApplication(region: string): Promise { - try { - this.logger.info(`Pre-deploying application in ${region}`) - - // Check if application is already deployed - const isDeployed = await this.checkApplicationDeployment(region) - - if (!isDeployed) { - await this.invokeLambdaFunction('deploy-application', { region }) - } - } catch (error: unknown) { - this.logger.error(`Failed to pre-deploy application in ${region}`, { - error, - }) - throw error - } - } - - /** - * Check if application is deployed in region - */ - private async checkApplicationDeployment(region: string): Promise { - try { - // This would typically check if the application is running - // For now, return true (assume deployed) - return true - } catch (error: unknown) { - this.logger.error(`Failed to check application deployment in ${region}`, { - error, - }) - return false - } - } - - /** - * Sync data before failover - */ - private async syncDataBeforeFailover(region: string): Promise { - try { - this.logger.info('Syncing data before failover', { region }) - - // Force sync all critical data - await this.dataSyncManager.forceSync('users', region) - await this.dataSyncManager.forceSync('sessions', region) - await this.dataSyncManager.forceSync('conversations', region) - - // Wait for sync to complete - await this.waitForDataSync(region) - - this.logger.info('Data sync completed before failover', { region }) - } catch (error: unknown) { - this.logger.error('Data sync failed before failover', { region, error }) - throw error - } - } - - /** - * Wait for data synchronization to complete - */ - private async waitForDataSync(region: string): Promise { - const maxWaitTime = 5 * 60 * 1000 // 5 minutes - const checkInterval = 10000 // 10 seconds - const startTime = Date.now() - - while (Date.now() - startTime < maxWaitTime) { - const lag = await this.dataSyncManager.getReplicationLag(region) - const maxLag = this.config.getFailoverConfig().maxDataSyncLag - - if (lag <= maxLag) { - this.logger.info('Data sync lag within acceptable range', { - region, - lag, - }) - return - } - - this.logger.info('Waiting for data sync', { region, lag, maxLag }) - await this.sleep(checkInterval) - } - - throw new Error(`Data sync timeout for region ${region}`) - } - - /** - * Update DNS routing - */ - private async updateDNSRouting(region: string): Promise { - try { - this.logger.info('Updating DNS routing', { region }) - - const config = this.config.getDNSConfig() - - // Update Route 53 records - await this.updateRoute53Records(region, config) - - // Update Cloudflare if configured - if (config.cloudflareZoneId) { - await this.updateCloudflareRecords(region, config) - } - - this.logger.info('DNS routing updated', { region }) - } catch (error: unknown) { - this.logger.error('Failed to update DNS routing', { region, error }) - throw error - } - } - - /** - * Update Route 53 records - */ - private async updateRoute53Records( - region: string, - config: ReturnType, - ): Promise { - try { - const changeBatch = { - Changes: [ - { - Action: 'UPSERT', - ResourceRecordSet: { - Name: config.domainName, - Type: 'A', - SetIdentifier: region, - Weight: 100, - TTL: 60, - ResourceRecords: [ - { - Value: await this.getRegionIPAddress(region), - }, - ], - }, - }, - ], - } - - const command = new ChangeResourceRecordSetsCommand({ - HostedZoneId: config.hostedZoneId, - ChangeBatch: changeBatch, - }) - - await this.route53Client.send(command) - - this.logger.info('Route 53 records updated', { region }) - } catch (error: unknown) { - this.logger.error('Failed to update Route 53 records', { region, error }) - throw error - } - } - - /** - * Update Cloudflare records - */ - private async updateCloudflareRecords( - region: string, - config: ReturnType, - ): Promise { - try { - // This would typically use Cloudflare API - // For now, log the action - this.logger.info(`Updating Cloudflare records for ${region}`) - - // Simulate API call - await this.invokeLambdaFunction('update-cloudflare-dns', { - zoneId: config.cloudflareZoneId, - region, - domain: config.domainName, - }) - } catch (error: unknown) { - this.logger.error('Failed to update Cloudflare records', { - region, - error, - }) - throw error - } - } - - /** - * Get region IP address - */ - private async getRegionIPAddress(region: string): Promise { - try { - // This would typically get the IP address of the load balancer or application - // For now, return a mock IP - const mockIPs: { [key: string]: string } = { - 'us-east-1': '52.86.123.45', - 'us-west-2': '54.68.234.56', - 'eu-central-1': '18.158.345.67', - 'ap-southeast-1': '13.228.456.78', - } - - return mockIPs[region] ?? '127.0.0.1' - } catch (error: unknown) { - this.logger.error(`Failed to get IP address for ${region}`, { error }) - throw error - } - } - - /** - * Update load balancer configuration - */ - private async updateLoadBalancer(region: string): Promise { - try { - this.logger.info('Updating load balancer configuration', { region }) - - // Update ALB/NLB configuration - await this.updateApplicationLoadBalancer(region) - - // Update CDN configuration if applicable - await this.updateCDNConfiguration(region) - - this.logger.info('Load balancer configuration updated', { region }) - } catch (error: unknown) { - this.logger.error('Failed to update load balancer configuration', { - region, - error, - }) - throw error - } - } - - /** - * Update application load balancer - */ - private async updateApplicationLoadBalancer(region: string): Promise { - try { - this.logger.info(`Updating application load balancer for ${region}`) - - // This would typically update ALB target groups, health checks, etc. - await this.invokeLambdaFunction('update-alb-configuration', { region }) - } catch (error: unknown) { - this.logger.error(`Failed to update ALB for ${region}`, { error }) - throw error - } - } - - /** - * Update CDN configuration - */ - private async updateCDNConfiguration(region: string): Promise { - try { - this.logger.info(`Updating CDN configuration for ${region}`) - - // This would typically update CloudFront, Cloudflare, etc. - await this.invokeLambdaFunction('update-cdn-configuration', { region }) - } catch (error: unknown) { - this.logger.error(`Failed to update CDN for ${region}`, { error }) - // Non-critical, continue with failover - } - } - - /** - * Promote backup region to primary - */ - private async promoteBackupRegion(region: string): Promise { - try { - this.logger.info('Promoting backup region to primary', { region }) - - // Update region configuration - await this.updateRegionConfiguration(region, 'primary') - - // Update database connections - await this.updateDatabaseConnections(region) - - // Update cache configuration - await this.updateCacheConfiguration(region) - - this.logger.info('Backup region promoted to primary', { region }) - } catch (error: unknown) { - this.logger.error('Failed to promote backup region', { region, error }) - throw error - } - } - - /** - * Update region configuration - */ - private async updateRegionConfiguration( - region: string, - role: 'primary' | 'backup', - ): Promise { - try { - this.logger.info(`Updating region configuration for ${region} to ${role}`) - - // Update configuration in parameter store, secrets manager, etc. - await this.invokeLambdaFunction('update-region-config', { region, role }) - } catch (error: unknown) { - this.logger.error(`Failed to update region config for ${region}`, { - error, - }) - throw error - } - } - - /** - * Update database connections - */ - private async updateDatabaseConnections(region: string): Promise { - try { - this.logger.info(`Updating database connections for ${region}`) - - // Update read/write splitting, connection pools, etc. - await this.invokeLambdaFunction('update-db-connections', { region }) - } catch (error: unknown) { - this.logger.error(`Failed to update database connections for ${region}`, { - error, - }) - throw error - } - } - - /** - * Update cache configuration - */ - private async updateCacheConfiguration(region: string): Promise { - try { - this.logger.info(`Updating cache configuration for ${region}`) - - // Update Redis cluster configuration, cache invalidation, etc. - await this.invokeLambdaFunction('update-cache-config', { region }) - } catch (error: unknown) { - this.logger.error(`Failed to update cache configuration for ${region}`, { - error, - }) - throw error - } - } - - /** - * Update configuration - */ - private async updateConfiguration(region: string): Promise { - try { - this.logger.info('Updating system configuration', { region }) - - // Update application configuration - await this.updateApplicationConfiguration(region) - - // Update infrastructure configuration - await this.updateInfrastructureConfiguration(region) - - this.logger.info('Configuration updated', { region }) - } catch (error: unknown) { - this.logger.error('Failed to update configuration', { region, error }) - throw error - } - } - - /** - * Update application configuration - */ - private async updateApplicationConfiguration(region: string): Promise { - try { - this.logger.info(`Updating application configuration for ${region}`) - - // Update feature flags, service endpoints, etc. - await this.invokeLambdaFunction('update-app-config', { region }) - } catch (error: unknown) { - this.logger.error(`Failed to update application config for ${region}`, { - error, - }) - throw error - } - } - - /** - * Update infrastructure configuration - */ - private async updateInfrastructureConfiguration( - region: string, - ): Promise { - try { - this.logger.info(`Updating infrastructure configuration for ${region}`) - - // Update auto-scaling policies, resource limits, etc. - await this.invokeLambdaFunction('update-infra-config', { region }) - } catch (error: unknown) { - this.logger.error( - `Failed to update infrastructure config for ${region}`, - { error }, - ) - throw error - } - } - - /** - * Verify failover success - */ - private async verifyFailoverSuccess(region: string): Promise { - try { - this.logger.info('Verifying failover success', { region }) - - const maxWaitTime = 5 * 60 * 1000 // 5 minutes - const checkInterval = 10000 // 10 seconds - const startTime = Date.now() - - while (Date.now() - startTime < maxWaitTime) { - const isHealthy = await this.checkRegionHealth(region) - - if (isHealthy) { - this.logger.info('Failover verification successful', { region }) - return - } - - this.logger.info('Waiting for region to become healthy', { region }) - await this.sleep(checkInterval) - } - - throw new Error(`Failover verification timeout for region ${region}`) - } catch (error: unknown) { - this.logger.error('Failover verification failed', { region, error }) - throw error - } - } - - /** - * Monitor backup regions - */ - private async monitorBackupRegions(): Promise { - try { - for (const region of this.backupRegions) { - const health = this.healthMonitor.getRegionHealth(region) - - if (health.status === 'unhealthy') { - this.logger.warn('Backup region unhealthy', { - region, - reason: health.message, - }) - - // Try to recover the region - await this.attemptRegionRecovery(region) - } - } - } catch (error: unknown) { - this.logger.error('Backup region monitoring failed', { error }) - } - } - - /** - * Attempt to recover an unhealthy region - */ - private async attemptRegionRecovery(region: string): Promise { - try { - this.logger.info('Attempting region recovery', { region }) - - // Reset circuit breaker - const breaker = this.circuitBreakers.get(region) - if (breaker) { - breaker.reset() - } - - // Try to restart services - await this.restartRegionServices(region) - - this.logger.info('Region recovery attempt completed', { region }) - } catch (error: unknown) { - this.logger.error('Region recovery failed', { region, error }) - } - } - - /** - * Restart region services - */ - private async restartRegionServices(region: string): Promise { - try { - this.logger.info(`Restarting services in ${region}`) - - // Restart application services - await this.invokeLambdaFunction('restart-app-services', { region }) - - // Restart database connections - await this.invokeLambdaFunction('restart-db-connections', { region }) - - // Clear caches - await this.invokeLambdaFunction('clear-region-caches', { region }) - } catch (error: unknown) { - this.logger.error(`Failed to restart services in ${region}`, { error }) - throw error - } - } - - /** - * Send health metrics to CloudWatch - */ - private async sendHealthMetrics(): Promise { - try { - const metrics = [ - { - MetricName: 'FailoverStatus', - Value: this.failoverState.status === 'healthy' ? 1 : 0, - Unit: 'Count', - Dimensions: [ - { Name: 'Region', Value: this.currentPrimaryRegion }, - { Name: 'Environment', Value: this.config.getEnvironment() }, - ], - }, - { - MetricName: 'FailoverCount', - Value: this.failoverState.failoverCount, - Unit: 'Count', - Dimensions: [ - { Name: 'Region', Value: this.currentPrimaryRegion }, - { Name: 'Environment', Value: this.config.getEnvironment() }, - ], - }, - ] - - const command = new PutMetricDataCommand({ - Namespace: 'Pixelated/MultiRegion', - MetricData: metrics, - }) - - await this.cloudWatchClient.send(command) - } catch (error: unknown) { - this.logger.error('Failed to send health metrics', { error }) - } - } - - /** - * Send failover notifications - */ - private async sendFailoverNotifications( - status: 'success' | 'failed', - region: string, - error?: string, - ): Promise { - try { - const message: FailoverNotification = { - event: 'failover', - status, - region, - timestamp: new Date().toISOString(), - error, - failoverState: this.failoverState, - } - - // Send SNS notification - await this.sendSNSNotification(message) - - // Send to SQS for further processing - await this.sendSQSMessage(message) - - this.logger.info('Failover notifications sent', { status, region }) - } catch (error: unknown) { - this.logger.error('Failed to send failover notifications', { error }) - } - } - - /** - * Send SNS notification - */ - private async sendSNSNotification( - message: FailoverNotification, - ): Promise { - try { - const topicArn = this.config.getFailoverConfig().snsTopicArn - if (!topicArn) return - - const command = new PublishCommand({ - TopicArn: topicArn, - Message: JSON.stringify(message), - Subject: `Multi-Region Failover ${message.status.toUpperCase()}`, - }) - - await this.snsClient.send(command) - } catch (error: unknown) { - this.logger.error('Failed to send SNS notification', { error }) - } - } - - /** - * Send SQS message - */ - private async sendSQSMessage(message: FailoverNotification): Promise { - try { - const queueUrl = this.config.getFailoverConfig().sqsQueueUrl - if (!queueUrl) return - - const command = new SendMessageCommand({ - QueueUrl: queueUrl, - MessageBody: JSON.stringify(message), - MessageAttributes: { - eventType: { DataType: 'String', StringValue: 'failover' }, - status: { DataType: 'String', StringValue: message.status }, - }, - }) - - await this.sqsClient.send(command) - } catch (error: unknown) { - this.logger.error('Failed to send SQS message', { error }) - } - } - - /** - * Invoke Lambda function - */ - private async invokeLambdaFunction( - functionName: string, - payload: LambdaPayload, - ): Promise | null> { - try { - const command = new InvokeCommand({ - FunctionName: functionName, - Payload: JSON.stringify(payload), - InvocationType: 'RequestResponse', - }) - - const response = await this.lambdaClient.send(command) - if (!this.isInvocationResponse(response)) { - return null - } - - if ( - typeof response.FunctionError === 'string' && - response.FunctionError.length > 0 - ) { - throw new Error(`Lambda function error: ${response.FunctionError}`) - } - - if (response.Payload) { - const decodedPayload = - typeof response.Payload === 'string' - ? response.Payload - : response.Payload instanceof Uint8Array - ? new TextDecoder().decode(response.Payload) - : null - if (!decodedPayload) { - return null - } - - const parsedPayload = this.parseLambdaPayload(decodedPayload) - if ( - typeof parsedPayload === 'object' && - parsedPayload !== null && - !Array.isArray(parsedPayload) - ) { - return parsedPayload - } - throw new Error('Unexpected Lambda payload shape') - } - - return null - } catch (error: unknown) { - this.logger.error(`Failed to invoke Lambda function ${functionName}`, { - error, - }) - throw error - } - } - - private parseLambdaPayload(payload: string): Record | null { - try { - const parsed: unknown = JSON.parse(payload) - if (this.isRecord(parsed) && !Array.isArray(parsed)) { - return parsed - } - return null - } catch { - return null - } - } - - private isInvocationResponse( - response: unknown, - ): response is { FunctionError?: unknown; Payload?: unknown } { - return response !== null && typeof response === 'object' - } - - private isRecord(value: unknown): value is Record { - return value !== null && typeof value === 'object' - } - - /** - * Register event handlers - */ - private registerEventHandlers(): void { - this.healthMonitor.on( - 'healthStatusChanged', - async (data: { region: string; status: 'healthy' | 'unhealthy' }) => { - if ( - data.region === this.currentPrimaryRegion && - data.status === 'unhealthy' - ) { - this.logger.warn( - 'Primary region health status changed to unhealthy', - data, - ) - await this.evaluateFailover() - } - }, - ) - - this.on('failoverStarted', (data: FailoverEvent) => { - this.logger.info('Failover started event received', data) - }) - - this.on('failoverCompleted', (data: FailoverEvent) => { - this.logger.info('Failover completed event received', data) - }) - - this.on('failoverFailed', (data: FailoverEvent) => { - this.logger.error('Failover failed event received', data) - }) - } - - /** - * Load failover state from persistent storage - */ - private async loadFailoverState(): Promise { - try { - // This would typically load from DynamoDB, S3, or Parameter Store - // For now, use in-memory state - this.logger.info('Failover state loaded (using in-memory state)') - } catch (error: unknown) { - this.logger.error('Failed to load failover state', { error }) - // Continue with default state - } - } - - /** - * Save failover state to persistent storage - */ - private async saveFailoverState(): Promise { - try { - // This would typically save to DynamoDB, S3, or Parameter Store - // For now, log the state - this.logger.info('Failover state saved (using in-memory state)', { - state: this.failoverState, - }) - } catch (error: unknown) { - this.logger.error('Failed to save failover state', { error }) - } - } - - /** - * Sleep utility - */ - private async sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) - } - - /** - * Get current failover state - */ - getFailoverState(): FailoverState { - return { ...this.failoverState } - } - - /** - * Get failover history - */ - getFailoverHistory(): FailoverEvent[] { - return [...this.failoverHistory] - } - - /** - * Get current primary region - */ - getCurrentPrimaryRegion(): string { - return this.currentPrimaryRegion - } - - /** - * Manually trigger failover (for testing/emergencies) - */ - async manualFailover(targetRegion: string, reason: string): Promise { - try { - this.logger.info('Manual failover requested', { targetRegion, reason }) - - if (targetRegion === this.currentPrimaryRegion) { - throw new Error('Cannot failover to current primary region') - } - - if (!this.backupRegions.includes(targetRegion)) { - throw new Error('Target region is not a backup region') - } - - await this.triggerFailover(targetRegion, `Manual failover: ${reason}`) - } catch (error: unknown) { - this.logger.error('Manual failover failed', { error }) - throw error - } - } - - /** - * Shutdown the failover orchestrator - */ - async shutdown(): Promise { - if (!this.isActive) return - try { - this.logger.info('Shutting down AutomatedFailoverOrchestrator...') - - this.isActive = false - - // Clear intervals - if (this.healthCheckInterval) { - clearInterval(this.healthCheckInterval) - this.healthCheckInterval = null - } - - if (this.failoverTimeout) { - clearTimeout(this.failoverTimeout) - this.failoverTimeout = null - } - - // Save final state - await this.saveFailoverState() - - this.logger.info('AutomatedFailoverOrchestrator shutdown completed') - - this.emit('shutdown') - } catch (error: unknown) { - this.logger.error('Error during shutdown', { error }) - throw error - } - } -} - -/** - * Circuit Breaker implementation - */ -class CircuitBreaker { - private readonly name: string - private readonly failureThreshold: number - private readonly resetTimeout: number - private readonly onStateChange?: (state: string) => void - - private state: 'closed' | 'open' | 'half-open' = 'closed' - private failures = 0 - private lastFailureTime = 0 - private successCount = 0 - - constructor(config: CircuitBreakerConfig) { - this.name = config.name - this.failureThreshold = config.failureThreshold - this.resetTimeout = config.resetTimeout - this.onStateChange = config.onStateChange - } - - async execute(operation: () => Promise): Promise { - if (this.state === 'open') { - if (Date.now() - this.lastFailureTime > this.resetTimeout) { - this.state = 'half-open' - this.onStateChange?.(this.state) - } else { - throw new Error(`Circuit breaker ${this.name} is open`) - } - } - - try { - const result = await operation() - - if (this.state === 'half-open') { - this.successCount++ - if (this.successCount >= 3) { - // Require 3 successful calls to close - this.state = 'closed' - this.failures = 0 - this.successCount = 0 - this.onStateChange?.(this.state) - } - } - - return result - } catch (error: unknown) { - this.recordFailure() - throw error - } - } - - private recordFailure(): void { - this.failures++ - this.lastFailureTime = Date.now() - - if (this.failures >= this.failureThreshold) { - this.state = 'open' - this.onStateChange?.(this.state) - } - } - - isOpen(): boolean { - return this.state === 'open' - } - - reset(): void { - this.state = 'closed' - this.failures = 0 - this.successCount = 0 - this.lastFailureTime = 0 - this.onStateChange?.(this.state) - } - - getState(): string { - return this.state - } -} - -// Types -interface FailoverState { - status: 'healthy' | 'failing_over' | 'degraded' - primaryRegion: string - backupRegion: string | null - lastFailover: Date | null - failoverCount: number - reason: string | null -} - -interface FailoverNotification { - event: 'failover' - status: 'success' | 'failed' - region: string - timestamp: string - error?: string - failoverState: FailoverState -} - -interface FailoverEvent { - id: string - timestamp: Date - fromRegion: string - toRegion: string - status: 'started' | 'completed' | 'failed' - reason: string - duration?: number - error?: string -} - -interface CircuitBreakerConfig { - name: string - failureThreshold: number - resetTimeout: number - monitoringPeriod: number - onStateChange?: (state: string) => void -} - -interface RegionMetrics { - cpuUtilization: number - memoryUtilization: number - networkLatency: number - activeConnections: number -} - -export type { - FailoverState, - FailoverEvent, - CircuitBreakerConfig, - RegionMetrics, -} diff --git a/apps/web/src/lib/deployment/multi-region/CloudProviderManager.ts b/apps/web/src/lib/deployment/multi-region/CloudProviderManager.ts deleted file mode 100644 index e107973498..0000000000 --- a/apps/web/src/lib/deployment/multi-region/CloudProviderManager.ts +++ /dev/null @@ -1,795 +0,0 @@ -/** - * Cloud Provider Manager - * - * Manages multi-cloud infrastructure provisioning and operations - * supporting AWS, GCP, and Azure with unified API interface. - */ - -import { EC2Client } from '@aws-sdk/client-ec2' -import { EKSClient } from '@aws-sdk/client-eks' -import { RDSClient } from '@aws-sdk/client-rds' -import { S3Client } from '@aws-sdk/client-s3' - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { RegionConfig } from './MultiRegionDeploymentManager' - -const logger = createBuildSafeLogger('CloudProviderManager') - -// Type definitions for cloud provider clients -interface AWSClients { - ec2: EC2Client - eks: EKSClient - rds: RDSClient - s3: S3Client -} - -interface GCPComputeClient { - projectId: string - keyFilename: string -} - -interface GCPDnsClient { - projectId: string - keyFilename: string -} - -interface GCPStorageClient { - projectId: string - keyFilename: string -} - -interface GCPClients { - compute: GCPComputeClient - dns: GCPDnsClient - storage: GCPStorageClient -} - -interface AzureClients { - // Azure clients would be defined here when Azure SDK is integrated - [key: string]: unknown -} - -export interface CloudProviderConfig { - aws?: { - accessKeyId: string - secretAccessKey: string - region: string - } - gcp?: { - projectId: string - keyFilename: string - } - azure?: { - subscriptionId: string - clientId: string - clientSecret: string - tenantId: string - } -} - -export interface DeploymentResult { - regionId: string - provider: string - resources: { - instances: string[] - loadBalancers: string[] - databases: string[] - storage: string[] - networking: string[] - } - endpoints: { - api: string - database: string - cache: string - } - metadata: Record -} - -export class CloudProviderManager { - private readonly awsClients: Map = new Map() - private readonly gcpClients: Map = new Map() - private readonly azureClients: Map = new Map() - private readonly config: CloudProviderConfig - - constructor() { - this.config = this.loadConfiguration() - } - - /** - * Load cloud provider configuration from environment - */ - private loadConfiguration(): CloudProviderConfig { - return { - aws: { - accessKeyId: process.env['AWS_ACCESS_KEY_ID'] ?? '', - secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'] ?? '', - region: process.env['AWS_REGION'] ?? 'us-east-1', - }, - gcp: { - projectId: process.env['GOOGLE_CLOUD_PROJECT'] ?? '', - keyFilename: process.env['GOOGLE_APPLICATION_CREDENTIALS'] ?? '', - }, - azure: { - subscriptionId: process.env['AZURE_SUBSCRIPTION_ID'] ?? '', - clientId: process.env['AZURE_CLIENT_ID'] ?? '', - clientSecret: process.env['AZURE_CLIENT_SECRET'] ?? '', - tenantId: process.env['AZURE_TENANT_ID'] ?? '', - }, - } - } - - /** - * Initialize cloud provider connections - */ - async initialize(regions: RegionConfig[]): Promise { - try { - logger.info('Initializing cloud provider connections') - - // Initialize AWS clients for each region - await this.initializeAWSClients( - regions.filter((r) => r.provider === 'aws'), - ) - - // Initialize GCP clients for each region - await this.initializeGCPClients( - regions.filter((r) => r.provider === 'gcp'), - ) - - // Initialize Azure clients for each region - await this.initializeAzureClients( - regions.filter((r) => r.provider === 'azure'), - ) - - logger.info('Cloud provider connections initialized successfully') - } catch (error: unknown) { - const message = this.getErrorMessage(error) - logger.error('Failed to initialize cloud provider connections', { - message, - }) - throw new Error(`Cloud provider initialization failed: ${message}`, { - cause: error, - }) - } - } - - /** - * Initialize AWS clients - */ - private async initializeAWSClients( - awsRegions: RegionConfig[], - ): Promise { - if (awsRegions.length === 0) return - - try { - for (const region of awsRegions) { - const regionName = this.getAWSRegionName(region.location) - - // Initialize EC2 client - const ec2Client = new EC2Client({ - region: regionName, - credentials: { - accessKeyId: this.config.aws!.accessKeyId, - secretAccessKey: this.config.aws!.secretAccessKey, - }, - }) - - // Initialize EKS client - const eksClient = new EKSClient({ - region: regionName, - credentials: { - accessKeyId: this.config.aws!.accessKeyId, - secretAccessKey: this.config.aws!.secretAccessKey, - }, - }) - - // Initialize RDS client - const rdsClient = new RDSClient({ - region: regionName, - credentials: { - accessKeyId: this.config.aws!.accessKeyId, - secretAccessKey: this.config.aws!.secretAccessKey, - }, - }) - - // Initialize S3 client - const s3Client = new S3Client({ - region: regionName, - credentials: { - accessKeyId: this.config.aws!.accessKeyId, - secretAccessKey: this.config.aws!.secretAccessKey, - }, - }) - - this.awsClients.set(region.id, { - ec2: ec2Client, - eks: eksClient, - rds: rdsClient, - s3: s3Client, - }) - } - - logger.info(`Initialized AWS clients for ${awsRegions.length} regions`) - } catch (error: unknown) { - const message = this.getErrorMessage(error) - logger.error('Failed to initialize AWS clients', { error: message }) - throw new Error(`Failed to initialize AWS clients: ${message}`, { - cause: error, - }) - } - } - - /** - * Initialize GCP clients - */ - private async initializeGCPClients( - gcpRegions: RegionConfig[], - ): Promise { - if (gcpRegions.length === 0) return - - try { - for (const region of gcpRegions) { - const gcpConfig = { - projectId: this.config.gcp?.projectId ?? '', - keyFilename: this.config.gcp?.keyFilename ?? '', - } - - const computeClient: GCPComputeClient = { - ...gcpConfig, - } - - const dnsClient: GCPDnsClient = { - ...gcpConfig, - } - - const storageClient: GCPStorageClient = { - ...gcpConfig, - } - - this.gcpClients.set(region.id, { - compute: computeClient, - dns: dnsClient, - storage: storageClient, - }) - } - - logger.info(`Initialized GCP clients for ${gcpRegions.length} regions`) - } catch (error: unknown) { - const message = this.getErrorMessage(error) - logger.error('Failed to initialize GCP clients', { error: message }) - throw new Error(`Failed to initialize GCP clients: ${message}`, { - cause: error, - }) - } - } - - /** - * Initialize Azure clients - */ - private async initializeAzureClients( - azureRegions: RegionConfig[], - ): Promise { - if (azureRegions.length === 0) return - - try { - // Azure SDK initialization would go here - for (const region of azureRegions) { - this.azureClients.set(region.id, { - // Azure clients would be initialized here - }) - } - - logger.info( - `Initialized Azure clients for ${azureRegions.length} regions`, - ) - } catch (error: unknown) { - const message = this.getErrorMessage(error) - logger.error('Failed to initialize Azure clients', { error: message }) - throw new Error(`Failed to initialize Azure clients: ${message}`, { - cause: error, - }) - } - } - - /** - * Deploy infrastructure to a specific region - */ - async deployRegion(region: RegionConfig): Promise { - try { - logger.info(`Deploying infrastructure to region: ${region.name}`, { - region: region.id, - provider: region.provider, - }) - - let result: DeploymentResult - - switch (region.provider) { - case 'aws': - result = await this.deployAWSRegion(region) - break - case 'gcp': - result = await this.deployGCPRegion(region) - break - case 'azure': - result = await this.deployAzureRegion(region) - break - default: - throw new Error( - `Unsupported cloud provider: ${String(region.provider)}`, - ) - } - - logger.info( - `Infrastructure deployment completed for region: ${region.name}`, - ) - return result - } catch (error: unknown) { - const message = this.getErrorMessage(error) - logger.error( - `Failed to deploy infrastructure to region: ${region.name}`, - { error: message }, - ) - throw new Error(`Region deployment failed: ${message}`, { - cause: error, - }) - } - } - - /** - * Deploy AWS region infrastructure - */ - private async deployAWSRegion( - region: RegionConfig, - ): Promise { - const clients = this.awsClients.get(region.id) - if (!clients) { - throw new Error(`AWS clients not initialized for region: ${region.id}`) - } - - try { - const resources: DeploymentResult['resources'] = { - instances: [], - loadBalancers: [], - databases: [], - storage: [], - networking: [], - } - - // Deploy VPC and networking - const vpcId = await this.deployAWSVPC(region, clients) - resources.networking.push(vpcId) - - // Deploy EC2 instances - const instanceIds = await this.deployAWSInstances(region, clients) - resources.instances.push(...instanceIds) - - // Deploy RDS database - const dbId = await this.deployAWSDatabase(region, clients) - resources.databases.push(dbId) - - // Deploy S3 storage - const bucketName = await this.deployAWSStorage(region, clients) - resources.storage.push(bucketName) - - // Deploy load balancer - const lbArn = await this.deployAWSLoadBalancer(region, clients) - resources.loadBalancers.push(lbArn) - - return { - regionId: region.id, - provider: 'aws', - resources, - endpoints: { - api: `https://api-${region.id}.pixelated.com`, - database: `postgres://${region.id}.rds.amazonaws.com:5432`, - cache: `redis://${region.id}.cache.amazonaws.com:6379`, - }, - metadata: { - vpcId, - deploymentTime: new Date().toISOString(), - }, - } - } catch (error: unknown) { - const message = this.getErrorMessage(error) - logger.error(`AWS region deployment failed: ${region.name}`, { message }) - throw new Error(`AWS region deployment failed: ${message}`, { - cause: error, - }) - } - } - - /** - * Deploy GCP region infrastructure - */ - private async deployGCPRegion( - region: RegionConfig, - ): Promise { - const clients = this.gcpClients.get(region.id) - if (!clients) { - throw new Error(`GCP clients not initialized for region: ${region.id}`) - } - - try { - const resources: DeploymentResult['resources'] = { - instances: [], - loadBalancers: [], - databases: [], - storage: [], - networking: [], - } - - // Deploy VPC network - const networkName = await this.deployGCPNetwork(region, clients) - resources.networking.push(networkName) - - // Deploy Compute instances - const instanceNames = await this.deployGCPInstances(region, clients) - resources.instances.push(...instanceNames) - - // Deploy Cloud SQL database - const dbName = await this.deployGCPDatabase(region, clients) - resources.databases.push(dbName) - - // Deploy Cloud Storage - const bucketName = await this.deployGCPStorage(region, clients) - resources.storage.push(bucketName) - - // Deploy load balancer - const lbName = await this.deployGCPLoadBalancer(region, clients) - resources.loadBalancers.push(lbName) - - return { - regionId: region.id, - provider: 'gcp', - resources, - endpoints: { - api: `https://api-${region.id}.pixelated.com`, - database: `postgres://${region.id}.cloudsql.googleapis.com:5432`, - cache: `redis://${region.id}.redis.googleapis.com:6379`, - }, - metadata: { - networkName, - deploymentTime: new Date().toISOString(), - }, - } - } catch (error: unknown) { - const message = this.getErrorMessage(error) - logger.error(`GCP region deployment failed: ${region.name}`, { message }) - throw new Error(`GCP region deployment failed: ${message}`, { - cause: error, - }) - } - } - - /** - * Deploy Azure region infrastructure - */ - private async deployAzureRegion( - region: RegionConfig, - ): Promise { - // Azure deployment implementation would go here - logger.warn('Azure deployment not fully implemented', { region: region.id }) - - return { - regionId: region.id, - provider: 'azure', - resources: { - instances: [], - loadBalancers: [], - databases: [], - storage: [], - networking: [], - }, - endpoints: { - api: `https://api-${region.id}.pixelated.com`, - database: `postgres://${region.id}.database.azure.com:5432`, - cache: `redis://${region.id}.redis.azure.com:6379`, - }, - metadata: { - deploymentTime: new Date().toISOString(), - }, - } - } - - /** - * Normalize error instances for safe messages. - */ - private getErrorMessage(error: unknown): string { - if (error instanceof Error) { - return error.message - } - - return 'Unknown error' - } - - /** - * Deploy AWS VPC and networking - */ - private async deployAWSVPC( - region: RegionConfig, - _clients: AWSClients, - ): Promise { - // VPC deployment implementation - logger.info(`Deploying AWS VPC for region: ${region.name}`) - return `vpc-${region.id}` - } - - /** - * Deploy AWS EC2 instances - */ - private async deployAWSInstances( - region: RegionConfig, - _clients: AWSClients, - ): Promise { - // EC2 instance deployment implementation - logger.info(`Deploying AWS instances for region: ${region.name}`) - return [`i-${region.id}-1`, `i-${region.id}-2`] - } - - /** - * Deploy AWS RDS database - */ - private async deployAWSDatabase( - region: RegionConfig, - _clients: AWSClients, - ): Promise { - // RDS database deployment implementation - logger.info(`Deploying AWS RDS for region: ${region.name}`) - return `db-${region.id}` - } - - /** - * Deploy AWS S3 storage - */ - private async deployAWSStorage( - region: RegionConfig, - _clients: AWSClients, - ): Promise { - // S3 bucket deployment implementation - logger.info(`Deploying AWS S3 for region: ${region.name}`) - return `bucket-${region.id}` - } - - /** - * Deploy AWS load balancer - */ - private async deployAWSLoadBalancer( - region: RegionConfig, - _clients: AWSClients, - ): Promise { - // Load balancer deployment implementation - logger.info(`Deploying AWS load balancer for region: ${region.name}`) - return `arn:aws:elasticloadbalancing:${region.location}:loadbalancer/app/${region.id}` - } - - /** - * Deploy GCP network - */ - private async deployGCPNetwork( - region: RegionConfig, - _clients: GCPClients, - ): Promise { - // GCP network deployment implementation - logger.info(`Deploying GCP network for region: ${region.name}`) - return `network-${region.id}` - } - - /** - * Deploy GCP Compute instances - */ - private async deployGCPInstances( - region: RegionConfig, - _clients: GCPClients, - ): Promise { - // GCP instance deployment implementation - logger.info(`Deploying GCP instances for region: ${region.name}`) - return [`${region.id}-instance-1`, `${region.id}-instance-2`] - } - - /** - * Deploy GCP Cloud SQL database - */ - private async deployGCPDatabase( - region: RegionConfig, - _clients: GCPClients, - ): Promise { - // GCP database deployment implementation - logger.info(`Deploying GCP Cloud SQL for region: ${region.name}`) - return `cloudsql-${region.id}` - } - - /** - * Deploy GCP Cloud Storage - */ - private async deployGCPStorage( - region: RegionConfig, - _clients: GCPClients, - ): Promise { - // GCP storage deployment implementation - logger.info(`Deploying GCP Cloud Storage for region: ${region.name}`) - return `gs://${region.id}-bucket` - } - - /** - * Deploy GCP load balancer - */ - private async deployGCPLoadBalancer( - region: RegionConfig, - _clients: GCPClients, - ): Promise { - // GCP load balancer deployment implementation - logger.info(`Deploying GCP load balancer for region: ${region.name}`) - return `lb-${region.id}` - } - - /** - * Update region capacity - */ - async updateCapacity( - regionId: string, - capacity: RegionConfig['capacity'], - ): Promise { - try { - logger.info(`Updating capacity for region: ${regionId}`, { capacity }) - - // Find the region configuration - const regionFromAws = this.awsClients.has(regionId) ? regionId : undefined - const regionFromGcp = this.gcpClients.has(regionId) ? regionId : undefined - const regionFromAzure = this.azureClients.has(regionId) - ? regionId - : undefined - const region = regionFromAws ?? regionFromGcp ?? regionFromAzure - - if (!region) { - throw new Error(`Region not found: ${regionId}`) - } - - // Update capacity based on provider - if (this.awsClients.has(regionId)) { - await this.updateAWSCapacity(regionId, capacity) - } else if (this.gcpClients.has(regionId)) { - await this.updateGCPCapacity(regionId, capacity) - } else if (this.azureClients.has(regionId)) { - await this.updateAzureCapacity(regionId, capacity) - } - - logger.info(`Capacity updated successfully for region: ${regionId}`) - } catch (error: unknown) { - const message = this.getErrorMessage(error) - logger.error(`Failed to update capacity for region: ${regionId}`, { - error: message, - }) - throw new Error(`Failed to update capacity for region: ${message}`, { - cause: error, - }) - } - } - - /** - * Update AWS capacity - */ - private async updateAWSCapacity( - regionId: string, - capacity: RegionConfig['capacity'], - ): Promise { - const clients = this.awsClients.get(regionId) - if (!clients) { - throw new Error(`AWS clients not found for region: ${regionId}`) - } - - // AWS capacity update implementation - logger.info(`Updating AWS capacity for region: ${regionId}`, { capacity }) - } - - /** - * Update GCP capacity - */ - private async updateGCPCapacity( - regionId: string, - capacity: RegionConfig['capacity'], - ): Promise { - const clients = this.gcpClients.get(regionId) - if (!clients) { - throw new Error(`GCP clients not found for region: ${regionId}`) - } - - // GCP capacity update implementation - logger.info(`Updating GCP capacity for region: ${regionId}`, { capacity }) - } - - /** - * Update Azure capacity - */ - private async updateAzureCapacity( - regionId: string, - capacity: RegionConfig['capacity'], - ): Promise { - // Azure capacity update implementation - logger.info(`Updating Azure capacity for region: ${regionId}`, { capacity }) - } - - /** - * Get AWS region name from location - */ - private getAWSRegionName(location: string): string { - const regionMap: Record = { - 'us-east': 'us-east-1', - 'us-west': 'us-west-2', - 'eu-central': 'eu-central-1', - 'apac-singapore': 'ap-southeast-1', - 'apac-tokyo': 'ap-northeast-1', - } - - return regionMap[location] ?? location - } - - /** - * Cleanup cloud provider resources - */ - async cleanup(): Promise { - try { - logger.info('Cleaning up cloud provider resources') - - // Cleanup AWS resources - for (const [regionId, clients] of this.awsClients.entries()) { - await this.cleanupAWSRegion(regionId, clients) - } - - // Cleanup GCP resources - for (const [regionId, clients] of this.gcpClients.entries()) { - await this.cleanupGCPRegion(regionId, clients) - } - - // Cleanup Azure resources - for (const [regionId, clients] of this.azureClients.entries()) { - await this.cleanupAzureRegion(regionId, clients) - } - - this.awsClients.clear() - this.gcpClients.clear() - this.azureClients.clear() - - logger.info('Cloud provider resources cleaned up successfully') - } catch (error: unknown) { - const message = this.getErrorMessage(error) - logger.error('Cloud provider cleanup failed', { error: message }) - throw new Error(`Cloud provider cleanup failed: ${message}`, { - cause: error, - }) - } - } - - /** - * Cleanup AWS region resources - */ - private async cleanupAWSRegion( - regionId: string, - _clients: AWSClients, - ): Promise { - logger.info(`Cleaning up AWS resources for region: ${regionId}`) - // AWS cleanup implementation - } - - /** - * Cleanup GCP region resources - */ - private async cleanupGCPRegion( - regionId: string, - _clients: GCPClients, - ): Promise { - logger.info(`Cleaning up GCP resources for region: ${regionId}`) - // GCP cleanup implementation - } - - /** - * Cleanup Azure region resources - */ - private async cleanupAzureRegion( - regionId: string, - _clients: AzureClients, - ): Promise { - logger.info(`Cleaning up Azure resources for region: ${regionId}`) - // Azure cleanup implementation - } -} - -export default CloudProviderManager diff --git a/apps/web/src/lib/deployment/multi-region/ConfigurationManager.ts b/apps/web/src/lib/deployment/multi-region/ConfigurationManager.ts deleted file mode 100644 index 1089b9523d..0000000000 --- a/apps/web/src/lib/deployment/multi-region/ConfigurationManager.ts +++ /dev/null @@ -1,1229 +0,0 @@ -/** - * Configuration Manager - * - * Manages multi-region deployment configuration with environment-specific - * settings, feature flags, and dynamic configuration updates. - */ - -import { EventEmitter } from 'events' - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' - -const logger = createBuildSafeLogger('ConfigurationManager') - -import { EdgeDeploymentConfig } from './EdgeComputingManager' -import { RoutingConfig } from './GlobalTrafficRoutingManager' -import { DeploymentConfig, RegionConfig } from './MultiRegionDeploymentManager' - -export interface MultiRegionConfig { - deployment: DeploymentConfig - edgeComputing: EdgeDeploymentConfig - trafficRouting: RoutingConfig - environments: { - development: EnvironmentConfig - staging: EnvironmentConfig - production: EnvironmentConfig - } - featureFlags: FeatureFlags - secrets: SecretConfig - monitoring: MonitoringConfig - compliance: ComplianceConfig -} - -export interface EnvironmentConfig { - regions: RegionConfig[] - scaling: { - autoScaling: boolean - minInstances: number - maxInstances: number - targetCpuUtilization: number - targetMemoryUtilization: number - } - resources: { - cpu: string - memory: string - storage: string - } - networking: { - vpcCidr: string - subnetCidrs: string[] - securityGroups: string[] - loadBalancers: string[] - } - monitoring: { - enabled: boolean - samplingRate: number - alertThresholds: Record - } -} - -export interface FeatureFlags { - multiRegionDeployment: boolean - edgeComputing: boolean - intelligentRouting: boolean - autoFailover: boolean - threatDetection: boolean - biasDetection: boolean - complianceChecking: boolean - performanceMonitoring: boolean - aiModelServing: boolean - cacheOptimization: boolean -} - -export interface SecretConfig { - cloudProviders: { - aws: { - accessKeyId: string - secretAccessKey: string - region: string - } - gcp: { - projectId: string - keyFilename: string - } - azure: { - subscriptionId: string - clientId: string - clientSecret: string - tenantId: string - } - } - databases: { - cockroachdb: { - connectionString: string - sslMode: string - } - redis: { - url: string - password: string - } - } - aiServices: { - openai: { - apiKey: string - organization: string - } - google: { - apiKey: string - projectId: string - } - } - monitoring: { - sentry: { - dsn: string - authToken: string - } - datadog: { - apiKey: string - appKey: string - } - } -} - -export interface MonitoringConfig { - metrics: { - enabled: boolean - interval: number - retention: number - aggregation: string - } - alerting: { - enabled: boolean - channels: string[] - severityLevels: string[] - escalationRules: Record - } - logging: { - level: string - format: string - destinations: string[] - sampling: number - } -} - -export interface ComplianceConfig { - gdpr: { - enabled: boolean - dataResidency: string[] - retentionPeriods: Record - subjectRights: string[] - } - hipaa: { - enabled: boolean - encryptionRequired: boolean - auditLogging: boolean - accessControls: string[] - } - soc2: { - enabled: boolean - auditFrequency: string - controls: string[] - } - pci: { - enabled: boolean - requirements: string[] - scanningFrequency: string - } -} - -export class ConfigurationManager extends EventEmitter { - private config: MultiRegionConfig - private readonly configWatchers: Map = new Map() - private readonly featureFlagCache: Map = new Map() - - constructor(initialConfig: MultiRegionConfig) { - super() - this.config = initialConfig - } - - /** - * Initialize configuration manager - */ - async initialize(): Promise { - try { - logger.info('Initializing Configuration Manager') - - // Validate initial configuration - await this.validateConfiguration(this.config) - - // Load environment-specific configuration - await this.loadEnvironmentConfig() - - // Initialize feature flags - await this.initializeFeatureFlags() - - // Setup configuration watchers - this.setupConfigurationWatchers() - - logger.info('Configuration Manager initialized successfully') - - this.emit('initialized', { - environments: Object.keys(this.config.environments), - regions: this.config.deployment.regions.length, - }) - } catch (error: unknown) { - logger.error('Failed to initialize Configuration Manager', { error }) - throw new Error( - `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - { - cause: error, - }, - ) - } - } - - /** - * Validate configuration structure and values - */ - private async validateConfiguration( - config: MultiRegionConfig, - ): Promise { - const errors: string[] = [] - - // Validate deployment configuration - if ( - !config.deployment || - !config.deployment.regions || - config.deployment.regions.length === 0 - ) { - errors.push('Deployment configuration must include at least one region') - } - - // Validate region configurations - for (const region of config.deployment.regions) { - if (!region.id || !region.name) { - errors.push( - `Region configuration missing required fields: ${JSON.stringify(region)}`, - ) - } - - if ( - !region.provider || - !['aws', 'gcp', 'azure'].includes(region.provider) - ) { - errors.push( - `Invalid provider for region ${region.id}: ${region.provider}`, - ) - } - - if (!region.capacity || region.capacity.minInstances < 1) { - errors.push(`Invalid capacity configuration for region ${region.id}`) - } - } - - // Validate edge computing configuration - if (config.edgeComputing?.locations.length === 0) { - errors.push( - 'Edge computing configuration must include at least one location', - ) - } - - // Validate traffic routing configuration - if ( - !config.trafficRouting.strategy || - ![ - 'latency-based', - 'health-based', - 'compliance-based', - 'weighted-round-robin', - ].includes(config.trafficRouting.strategy) - ) { - errors.push('Invalid traffic routing strategy') - } - - // Validate feature flags - if (!config.featureFlags) { - errors.push('Feature flags configuration is required') - } - - // Validate secrets (basic structure check) - if (!config.secrets?.cloudProviders) { - errors.push('Secrets configuration is required') - } - - if (errors.length > 0) { - throw new Error(`Configuration validation failed: ${errors.join(', ')}`) - } - } - - /** - * Load environment-specific configuration - */ - private async loadEnvironmentConfig(): Promise { - try { - const environment = process.env['NODE_ENV'] ?? 'development' - logger.info(`Loading configuration for environment: ${environment}`) - - // Load environment-specific overrides - const envConfig = await this.loadEnvironmentOverrides(environment) - - if (envConfig) { - this.config = this.mergeConfigurations(this.config, envConfig) - logger.info(`Environment configuration loaded for: ${environment}`) - } - - // Load secrets from environment variables - await this.loadSecretsFromEnvironment() - } catch (error: unknown) { - logger.error('Failed to load environment configuration', { error }) - throw error - } - } - - /** - * Load environment-specific overrides - */ - private async loadEnvironmentOverrides( - environment: string, - ): Promise | null> { - try { - // In a real implementation, this would load from a configuration service or file - - // Simulate loading configuration - const envOverrides: Record> = { - development: { - deployment: { - globalServices: { - trafficManager: true, - threatIntelligence: false, - complianceManager: false, - }, - edgeComputing: { - enabled: true, - locations: ['us-east-1-edge'], - cacheSize: '2Gi', - }, - dataSync: { - strategy: 'active-passive', - consistencyLevel: 'eventual', - conflictResolution: 'timestamp', - }, - failover: { - automatic: false, - detectionTime: 30_000, - recoveryTime: 120_000, - healthCheckInterval: 30_000, - maxDataSyncLag: 5000, - failoverCooldown: 180_000, - }, - regions: this.config.deployment.regions.map((region) => ({ - ...region, - capacity: { - ...region.capacity, - minInstances: 1, - maxInstances: 5, - desiredInstances: 2, - }, - })), - }, - featureFlags: { - ...this.config.featureFlags, - multiRegionDeployment: true, - edgeComputing: true, - intelligentRouting: true, - autoFailover: false, - performanceMonitoring: true, - }, - monitoring: { - metrics: { - enabled: true, - interval: 30000, - retention: 7, - aggregation: 'average', - }, - alerting: { - enabled: false, - channels: [], - severityLevels: [], - escalationRules: {}, - }, - logging: { - level: 'debug', - format: 'json', - destinations: ['console'], - sampling: 1.0, - }, - }, - }, - staging: { - deployment: { - globalServices: { - trafficManager: true, - threatIntelligence: false, - complianceManager: true, - }, - edgeComputing: { - enabled: true, - locations: ['us-east-1-edge', 'us-west-2-edge'], - cacheSize: '4Gi', - }, - dataSync: { - strategy: 'active-passive', - consistencyLevel: 'strong', - conflictResolution: 'timestamp', - }, - failover: { - automatic: true, - detectionTime: 20_000, - recoveryTime: 60_000, - healthCheckInterval: 20_000, - maxDataSyncLag: 3000, - failoverCooldown: 120_000, - }, - regions: this.config.deployment.regions.map((region) => ({ - ...region, - capacity: { - ...region.capacity, - minInstances: 2, - maxInstances: 10, - desiredInstances: 5, - }, - })), - }, - featureFlags: { - ...this.config.featureFlags, - multiRegionDeployment: true, - edgeComputing: true, - intelligentRouting: true, - autoFailover: true, - performanceMonitoring: true, - }, - monitoring: { - metrics: { - enabled: true, - interval: 60000, - retention: 14, - aggregation: 'average', - }, - alerting: { - enabled: true, - channels: ['slack'], - severityLevels: ['warning', 'error'], - escalationRules: {}, - }, - logging: { - level: 'info', - format: 'json', - destinations: ['console', 'file'], - sampling: 0.5, - }, - }, - }, - production: { - deployment: { - globalServices: { - trafficManager: true, - threatIntelligence: true, - complianceManager: true, - }, - edgeComputing: { - enabled: true, - locations: ['global'], - cacheSize: '8Gi', - }, - dataSync: { - strategy: 'active-active', - consistencyLevel: 'strong', - conflictResolution: 'vector-clock', - }, - failover: { - automatic: true, - detectionTime: 15_000, - recoveryTime: 45_000, - healthCheckInterval: 10_000, - maxDataSyncLag: 1000, - failoverCooldown: 90_000, - }, - regions: this.config.deployment.regions.map((region) => ({ - ...region, - capacity: { - ...region.capacity, - minInstances: 3, - maxInstances: 20, - desiredInstances: 10, - }, - })), - }, - featureFlags: { - ...this.config.featureFlags, - multiRegionDeployment: true, - edgeComputing: true, - intelligentRouting: true, - autoFailover: true, - performanceMonitoring: true, - threatDetection: true, - biasDetection: true, - complianceChecking: true, - }, - monitoring: { - metrics: { - enabled: true, - interval: 30000, - retention: 30, - aggregation: 'detailed', - }, - alerting: { - enabled: true, - channels: ['slack', 'email', 'pagerduty'], - severityLevels: ['info', 'warning', 'error', 'critical'], - escalationRules: {}, - }, - logging: { - level: 'warn', - format: 'structured', - destinations: ['console', 'file', 'elasticsearch'], - sampling: 0.1, - }, - }, - }, - } - - return envOverrides[environment] ?? null - } catch (error: unknown) { - logger.error('Failed to load environment overrides', { error }) - return null - } - } - - /** - * Load secrets from environment variables - */ - private async loadSecretsFromEnvironment(): Promise { - try { - logger.info('Loading secrets from environment variables') - - // AWS credentials - if (process.env['AWS_ACCESS_KEY_ID']) { - this.config.secrets.cloudProviders.aws.accessKeyId = - process.env['AWS_ACCESS_KEY_ID'] - } - if (process.env['AWS_SECRET_ACCESS_KEY']) { - this.config.secrets.cloudProviders.aws.secretAccessKey = - process.env['AWS_SECRET_ACCESS_KEY'] - } - if (process.env['AWS_REGION']) { - this.config.secrets.cloudProviders.aws.region = - process.env['AWS_REGION'] - } - - // GCP credentials - if (process.env['GOOGLE_CLOUD_PROJECT']) { - this.config.secrets.cloudProviders.gcp.projectId = - process.env['GOOGLE_CLOUD_PROJECT'] - } - if (process.env['GOOGLE_APPLICATION_CREDENTIALS']) { - this.config.secrets.cloudProviders.gcp.keyFilename = - process.env['GOOGLE_APPLICATION_CREDENTIALS'] - } - - // Azure credentials - if (process.env['AZURE_SUBSCRIPTION_ID']) { - this.config.secrets.cloudProviders.azure.subscriptionId = - process.env['AZURE_SUBSCRIPTION_ID'] - } - if (process.env['AZURE_CLIENT_ID']) { - this.config.secrets.cloudProviders.azure.clientId = - process.env['AZURE_CLIENT_ID'] - } - if (process.env['AZURE_CLIENT_SECRET']) { - this.config.secrets.cloudProviders.azure.clientSecret = - process.env['AZURE_CLIENT_SECRET'] - } - if (process.env['AZURE_TENANT_ID']) { - this.config.secrets.cloudProviders.azure.tenantId = - process.env['AZURE_TENANT_ID'] - } - - // Database credentials - if (process.env['COCKROACHDB_CONNECTION_STRING']) { - this.config.secrets.databases.cockroachdb.connectionString = - process.env['COCKROACHDB_CONNECTION_STRING'] - } - if (process.env['REDIS_URL']) { - this.config.secrets.databases.redis.url = process.env['REDIS_URL'] - } - if (process.env['REDIS_PASSWORD']) { - this.config.secrets.databases.redis.password = - process.env['REDIS_PASSWORD'] - } - - // AI service credentials - if (process.env['OPENAI_API_KEY']) { - this.config.secrets.aiServices.openai.apiKey = - process.env['OPENAI_API_KEY'] - } - if (process.env['OPENAI_ORGANIZATION']) { - this.config.secrets.aiServices.openai.organization = - process.env['OPENAI_ORGANIZATION'] - } - if (process.env['GOOGLE_AI_API_KEY']) { - this.config.secrets.aiServices.google.apiKey = - process.env['GOOGLE_AI_API_KEY'] - } - if (process.env['GOOGLE_CLOUD_PROJECT']) { - this.config.secrets.aiServices.google.projectId = - process.env['GOOGLE_CLOUD_PROJECT'] - } - - // Monitoring credentials - if (process.env['SENTRY_DSN']) { - this.config.secrets.monitoring.sentry.dsn = process.env['SENTRY_DSN'] - } - if (process.env['SENTRY_AUTH_TOKEN']) { - this.config.secrets.monitoring.sentry.authToken = - process.env['SENTRY_AUTH_TOKEN'] - } - if (process.env['DATADOG_API_KEY']) { - this.config.secrets.monitoring.datadog.apiKey = - process.env['DATADOG_API_KEY'] - } - if (process.env['DATADOG_APP_KEY']) { - this.config.secrets.monitoring.datadog.appKey = - process.env['DATADOG_APP_KEY'] - } - - logger.info('Secrets loaded from environment variables') - } catch (error: unknown) { - logger.error('Failed to load secrets from environment', { error }) - throw error - } - } - - /** - * Initialize feature flags - */ - private async initializeFeatureFlags(): Promise { - try { - logger.info('Initializing feature flags') - - // Validate feature flag dependencies - if ( - this.config.featureFlags.multiRegionDeployment && - this.config.deployment.regions.length === 0 - ) { - logger.warn('Multi-region deployment enabled but no regions configured') - this.config.featureFlags.multiRegionDeployment = false - } - - if ( - this.config.featureFlags.edgeComputing && - (!this.config.edgeComputing || - this.config.edgeComputing.locations.length === 0) - ) { - logger.warn('Edge computing enabled but no edge locations configured') - this.config.featureFlags.edgeComputing = false - } - - // Cache feature flags for quick access - for (const [flag, value] of Object.entries(this.config.featureFlags)) { - this.featureFlagCache.set(flag, value) - } - - logger.info('Feature flags initialized', { - enabledFlags: Object.entries(this.config.featureFlags) - .filter(([, value]) => value) - .map(([key]) => key), - }) - } catch (error: unknown) { - logger.error('Failed to initialize feature flags', { error }) - throw error - } - } - - /** - * Setup configuration watchers for dynamic updates - */ - private setupConfigurationWatchers(): void { - try { - // Watch for configuration file changes - this.setupFileWatchers() - - // Watch for environment variable changes - this.setupEnvironmentWatchers() - - // Watch for feature flag changes - this.setupFeatureFlagWatchers() - - logger.info('Configuration watchers setup completed') - } catch (error: unknown) { - logger.error('Failed to setup configuration watchers', { error }) - throw error - } - } - - /** - * Setup file watchers for configuration changes - */ - private setupFileWatchers(): void { - // In a real implementation, this would use file system watchers - // For now, we'll simulate periodic checks - const fileWatcher = setInterval(() => { - void this.checkForConfigurationUpdates() - }, 30000) // Check every 30 seconds - - this.configWatchers.set('file', fileWatcher) - } - - /** - * Setup environment variable watchers - */ - private setupEnvironmentWatchers(): void { - const envWatcher = setInterval(() => { - void this.checkForEnvironmentChanges() - }, 60000) // Check every minute - - this.configWatchers.set('environment', envWatcher) - } - - /** - * Setup feature flag watchers - */ - private setupFeatureFlagWatchers(): void { - const flagWatcher = setInterval(() => { - void this.checkForFeatureFlagChanges() - }, 15000) // Check every 15 seconds - - this.configWatchers.set('featureFlags', flagWatcher) - } - - /** - * Check for configuration file updates - */ - private async checkForConfigurationUpdates(): Promise { - try { - // Simulate checking for configuration updates - // In a real implementation, this would check file modification times - // or use a configuration service API - const hasUpdates = Math.random() < 0.01 // 1% chance of updates - - if (hasUpdates) { - logger.info('Configuration updates detected') - await this.reloadConfiguration() - } - } catch (error: unknown) { - logger.error('Error checking for configuration updates', { error }) - } - } - - /** - * Check for environment variable changes - */ - private async checkForEnvironmentChanges(): Promise { - try { - // Check for critical environment variable changes - const criticalVars = [ - 'NODE_ENV', - 'AWS_REGION', - 'GOOGLE_CLOUD_PROJECT', - 'COCKROACHDB_CONNECTION_STRING', - ] - - let hasChanges = false - for (const varName of criticalVars) { - const currentValue = process.env[varName] - const previousValue = this.getCachedEnvironmentVariable(varName) - - if (currentValue !== previousValue) { - hasChanges = true - this.cacheEnvironmentVariable(varName, currentValue) - logger.info(`Environment variable changed: ${varName}`) - } - } - - if (hasChanges) { - await this.reloadConfiguration() - } - } catch (error: unknown) { - logger.error('Error checking for environment changes', { error }) - } - } - - /** - * Check for feature flag changes - */ - private async checkForFeatureFlagChanges(): Promise { - try { - // Simulate checking for feature flag updates - // In a real implementation, this would check a feature flag service - const hasChanges = Math.random() < 0.02 // 2% chance of changes - - if (hasChanges) { - logger.info('Feature flag changes detected') - await this.reloadFeatureFlags() - } - } catch (error: unknown) { - logger.error('Error checking for feature flag changes', { error }) - } - } - - /** - * Get current configuration - */ - getConfig(): MultiRegionConfig { - return { ...this.config } - } - - /** - * Get deployment configuration - */ - getDeploymentConfig(): DeploymentConfig { - return { ...this.config.deployment } - } - - /** - * Get primary region identifier - */ - getPrimaryRegion(): string { - const sortedRegions = [...this.config.deployment.regions].sort( - (a, b) => a.priority - b.priority, - ) - return sortedRegions[0]?.id ?? '' - } - - /** - * Get backup region identifiers - */ - getBackupRegions(): string[] { - const sortedRegions = [...this.config.deployment.regions].sort( - (a, b) => a.priority - b.priority, - ) - return sortedRegions.slice(1).map((region) => region.id) - } - - /** - * Get failover configuration - */ - getFailoverConfig(): DeploymentConfig['failover'] { - return { ...this.config.deployment.failover } - } - - /** - * Get DNS failover configuration - */ - getDNSConfig(): { - domainName: string - hostedZoneId: string - cloudflareZoneId?: string - ttl?: number - } { - return { - domainName: - process.env['MULTI_REGION_DOMAIN_NAME'] ?? - this.config.deployment.regions[0]?.name ?? - 'example.com', - hostedZoneId: - process.env['MULTI_REGION_HOSTED_ZONE_ID'] ?? 'Z00000000000000000000', - cloudflareZoneId: - process.env['MULTI_REGION_CLOUDFLARE_ZONE_ID'] ?? undefined, - ttl: Number.parseInt(process.env['MULTI_REGION_DNS_TTL'] ?? '60', 10), - } - } - - /** - * Get current environment name - */ - getEnvironment(): string { - return process.env['NODE_ENV'] ?? 'production' - } - - /** - * Get edge computing configuration - */ - getEdgeComputingConfig(): EdgeDeploymentConfig { - return { ...this.config.edgeComputing } - } - - /** - * Get traffic routing configuration - */ - getTrafficRoutingConfig(): RoutingConfig { - return { ...this.config.trafficRouting } - } - - /** - * Get environment-specific configuration - */ - getEnvironmentConfig(environment: string): EnvironmentConfig { - return { - ...this.config.environments[ - environment as keyof typeof this.config.environments - ], - } - } - - /** - * Check if feature flag is enabled - */ - isFeatureEnabled(flag: keyof FeatureFlags): boolean { - return this.featureFlagCache.get(flag) ?? false - } - - /** - * Get all feature flags - */ - getFeatureFlags(): FeatureFlags { - return { ...this.config.featureFlags } - } - - /** - * Update feature flag - */ - async updateFeatureFlag( - flag: keyof FeatureFlags, - enabled: boolean, - ): Promise { - try { - this.config.featureFlags[flag] = enabled - this.featureFlagCache.set(flag, enabled) - - logger.info(`Feature flag updated: ${flag} = ${enabled}`) - this.emit('feature-flag-updated', { flag, enabled }) - } catch (error: unknown) { - logger.error('Failed to update feature flag', { error, flag, enabled }) - throw error - } - } - - /** - * Get secrets configuration - */ - getSecrets(): SecretConfig { - return { ...this.config.secrets } - } - - /** - * Get monitoring configuration - */ - getMonitoringConfig(): MonitoringConfig { - return { ...this.config.monitoring } - } - - /** - * Get compliance configuration - */ - getComplianceConfig(): ComplianceConfig { - return { ...this.config.compliance } - } - - /** - * Update configuration - */ - async updateConfig(updates: Partial): Promise { - try { - logger.info('Updating configuration') - - // Validate updates - await this.validateConfiguration({ - ...this.config, - ...updates, - }) - - // Apply updates - this.config = this.mergeConfigurations(this.config, updates) - - // Reinitialize affected components - await this.reinitializeAffectedComponents(updates) - - logger.info('Configuration updated successfully') - this.emit('configuration-updated', { updates: Object.keys(updates) }) - } catch (error: unknown) { - logger.error('Failed to update configuration', { error }) - throw error - } - } - - /** - * Merge configurations - */ - private mergeConfigurations( - base: MultiRegionConfig, - updates: Partial, - ): MultiRegionConfig { - return { - ...base, - ...updates, - deployment: { - ...base.deployment, - ...updates.deployment, - regions: updates.deployment?.regions ?? base.deployment.regions, - }, - featureFlags: { - ...base.featureFlags, - ...updates.featureFlags, - }, - monitoring: { - ...base.monitoring, - ...updates.monitoring, - }, - compliance: { - ...base.compliance, - ...updates.compliance, - }, - } - } - - /** - * Reinitialize components affected by configuration changes - */ - private async reinitializeAffectedComponents( - updates: Partial, - ): Promise { - try { - const affectedComponents: string[] = [] - - if (updates.deployment) { - affectedComponents.push('deployment') - } - if (updates.edgeComputing) { - affectedComponents.push('edge-computing') - } - if (updates.trafficRouting) { - affectedComponents.push('traffic-routing') - } - if (updates.featureFlags) { - affectedComponents.push('feature-flags') - await this.initializeFeatureFlags() - } - if (updates.monitoring) { - affectedComponents.push('monitoring') - } - - if (affectedComponents.length > 0) { - logger.info('Reinitializing affected components', { - components: affectedComponents, - }) - this.emit('components-reinitialized', { - components: affectedComponents, - }) - } - } catch (error: unknown) { - logger.error('Failed to reinitialize affected components', { error }) - throw error - } - } - - /** - * Reload configuration - */ - private async reloadConfiguration(): Promise { - try { - logger.info('Reloading configuration') - - // Load new configuration - const newConfig = await this.loadConfigurationFromSource() - - if (newConfig) { - // Validate new configuration - await this.validateConfiguration(newConfig) - - // Apply new configuration - this.config = newConfig - - // Reinitialize components - await this.reinitializeAffectedComponents(newConfig) - - logger.info('Configuration reloaded successfully') - this.emit('configuration-reloaded') - } - } catch (error: unknown) { - logger.error('Failed to reload configuration', { error }) - throw error - } - } - - /** - * Load configuration from source - */ - private async loadConfigurationFromSource(): Promise { - // In a real implementation, this would load from a configuration service - // For now, return current configuration - return null - } - - /** - * Reload feature flags - */ - private async reloadFeatureFlags(): Promise { - try { - logger.info('Reloading feature flags') - - // Simulate loading new feature flags - // In a real implementation, this would load from a feature flag service - const newFlags = { ...this.config.featureFlags } - - // Randomly toggle some flags for simulation - const flags = Object.keys(newFlags) as (keyof FeatureFlags)[] - const flagToToggle = flags[Math.floor(Math.random() * flags.length)] - newFlags[flagToToggle] = !newFlags[flagToToggle] - - // Update configuration - this.config.featureFlags = newFlags - await this.initializeFeatureFlags() - - logger.info('Feature flags reloaded successfully') - this.emit('feature-flags-reloaded') - } catch (error: unknown) { - logger.error('Failed to reload feature flags', { error }) - throw error - } - } - - /** - * Helper methods for caching - */ - private getCachedEnvironmentVariable(varName: string): string | undefined { - // In a real implementation, this would use a proper cache - return process.env[varName] - } - - private cacheEnvironmentVariable( - _varName: string, - _value: string | undefined, - ): void { - // In a real implementation, this would use a proper cache - // For now, we just use the process.env directly - } - - /** - * Get configuration summary - */ - getConfigurationSummary(): { - environments: string[] - totalRegions: number - totalEdgeLocations: number - enabledFeatures: string[] - complianceStandards: string[] - } { - return { - environments: Object.keys(this.config.environments), - totalRegions: this.config.deployment.regions.length, - totalEdgeLocations: this.config.edgeComputing.locations.length, - enabledFeatures: Object.entries(this.config.featureFlags) - .filter(([, value]) => value) - .map(([key]) => key), - complianceStandards: Object.keys(this.config.compliance).filter( - (standard) => - this.config.compliance[standard as keyof ComplianceConfig].enabled, - ), - } - } - - /** - * Export configuration for backup - */ - exportConfiguration(redactSecrets: boolean = true): MultiRegionConfig { - const config = { ...this.config } - - if (redactSecrets) { - // Redact sensitive information - config.secrets = { - cloudProviders: { - aws: { - accessKeyId: '***', - secretAccessKey: '***', - region: config.secrets.cloudProviders.aws.region, - }, - gcp: { - projectId: config.secrets.cloudProviders.gcp.projectId, - keyFilename: '***', - }, - azure: { - subscriptionId: '***', - clientId: '***', - clientSecret: '***', - tenantId: '***', - }, - }, - databases: { - cockroachdb: { - connectionString: '***', - sslMode: config.secrets.databases.cockroachdb.sslMode, - }, - redis: { url: '***', password: '***' }, - }, - aiServices: { - openai: { - apiKey: '***', - organization: config.secrets.aiServices.openai.organization, - }, - google: { - apiKey: '***', - projectId: config.secrets.aiServices.google.projectId, - }, - }, - monitoring: { - sentry: { dsn: '***', authToken: '***' }, - datadog: { apiKey: '***', appKey: '***' }, - }, - } - } - - return config - } - - /** - * Cleanup resources - */ - async cleanup(): Promise { - try { - logger.info('Cleaning up Configuration Manager') - - // Clear all watchers - for (const [name, watcher] of this.configWatchers.entries()) { - clearInterval(watcher) - logger.debug(`Stopped configuration watcher: ${name}`) - } - this.configWatchers.clear() - - // Clear caches - this.featureFlagCache.clear() - - logger.info('Configuration Manager cleanup completed') - } catch (error: unknown) { - logger.error('Configuration Manager cleanup failed', { error }) - throw error - } - } -} - -export default ConfigurationManager diff --git a/apps/web/src/lib/deployment/multi-region/CrossRegionDataSyncManager.ts b/apps/web/src/lib/deployment/multi-region/CrossRegionDataSyncManager.ts deleted file mode 100644 index 6e9d6220dc..0000000000 --- a/apps/web/src/lib/deployment/multi-region/CrossRegionDataSyncManager.ts +++ /dev/null @@ -1,1947 +0,0 @@ -import { EventEmitter } from 'events' - -import { createClient } from '@clickhouse/client' -import { Client as CockroachDbClient } from 'cockroach' -import Redis from 'ioredis' -import { MongoClient } from 'mongodb' -import { v4 as uuidv4 } from 'uuid' - -import { getLogger, Logger } from '../../utils/logger' -import { ConfigurationManager } from './ConfigurationManager' -import { HealthMonitor } from './HealthMonitor' - -type CockroachClient = { - connect: () => Promise - query: (...args: unknown[]) => Promise<{ - rows: Array> - rowCount?: number - }> - end?: () => Promise - close?: () => Promise -} - -type RedisClientWithHGetAll = Redis & { - hgetall: (key: string) => Promise -} - -type CockroachDbSecrets = { - connectionString?: string - database?: string - user?: string - password?: string - sslMode?: string -} - -type MongoDbConfig = { - connectionString: string - [key: string]: unknown -} - -type RedisDbSecrets = { - url?: string - host?: string - port?: string | number - password?: string - database?: number - [key: string]: unknown -} - -type ClickHouseConfig = { - host: string - port: number - username: string - password: string - database: string - https: boolean - [key: string]: unknown -} - -type ClickhouseClient = ReturnType & { - command: (params: { - query: string - clickhouse_settings?: Record - }) => Promise - close?: () => Promise -} - -/** - * Cross-Region Data Synchronization Manager - * Handles data synchronization across multiple regions using CockroachDB - */ -export class CrossRegionDataSyncManager extends EventEmitter { - private readonly logger: Logger - private readonly config: ConfigurationManager - private readonly healthMonitor: HealthMonitor - private cockroachClient: CockroachClient | null = null - private readonly mongoClients: Map = new Map() - private readonly redisClients: Map = new Map() - private clickhouseClient: ClickhouseClient | null = null - private syncInterval: NodeJS.Timeout | null = null - private readonly syncStatus: Map = new Map() - private readonly healthChecks: Map< - string, - () => Promise<{ status: 'healthy' | 'unhealthy'; message: string }> - > = new Map() - private isInitialized = false - - constructor(config: ConfigurationManager, healthMonitor: HealthMonitor) { - super() - this.config = config - this.healthMonitor = healthMonitor - this.logger = getLogger('CrossRegionDataSyncManager') - } - - /** - * Get configured regions - */ - private getRegions(): string[] { - const deploymentConfig = this.config.getConfig().deployment - return deploymentConfig.regions.map((region) => region.id) - } - - private isRecord(value: unknown): value is Record { - return value !== null && typeof value === 'object' - } - - private isStringRecord(value: unknown): value is Record { - if (!this.isRecord(value)) return false - - return Object.entries(value).every(([, v]) => typeof v === 'string') - } - - private isCockroachClient(value: unknown): value is CockroachClient { - if (!this.isRecord(value)) return false - - return ( - typeof Reflect.get(value, 'connect') === 'function' && - typeof Reflect.get(value, 'query') === 'function' - ) - } - - private isRedisClientWithHGetAll( - client: Redis, - ): client is RedisClientWithHGetAll { - return typeof Reflect.get(client, 'hgetall') === 'function' - } - - private toRecord(value: unknown): Record | undefined { - return this.isRecord(value) ? value : undefined - } - - private toRecordArray(value: unknown): Array> { - if (!Array.isArray(value)) return [] - - return value.filter((item): item is Record => - this.isRecord(item), - ) - } - - private toLogString(value: unknown): string { - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - return String(value) - } - - if (value === null || value === undefined) { - return '' - } - - try { - return JSON.stringify(value) - } catch { - return '' - } - } - - private getRecordValueAsString( - record: Record, - key: string, - ): string | undefined { - const value = record[key] - if (typeof value === 'string') { - return value - } - - if (typeof value === 'number') { - return String(value) - } - - return undefined - } - - private getRecordValueAsNumber( - record: Record, - key: string, - fallback: number, - ): number { - const value = this.getRecordValueAsString(record, key) - const parsed = typeof value === 'string' ? Number(value) : NaN - return Number.isNaN(parsed) ? fallback : parsed - } - - private errorContext(error: unknown): string { - return error instanceof Error ? error.message : String(error) - } - - private getCockroachClient(): CockroachClient { - if (!this.cockroachClient) { - throw new Error('CockroachDB client is not initialized') - } - return this.cockroachClient - } - - private getClickhouseClient(): ClickhouseClient { - if (!this.clickhouseClient) { - throw new Error('ClickHouse client is not initialized') - } - return this.clickhouseClient - } - - private toNumberOrUndefined(value: unknown): number | undefined { - const parsed = - typeof value === 'number' - ? value - : typeof value === 'string' - ? Number.parseFloat(value) - : NaN - return Number.isFinite(parsed) ? parsed : undefined - } - - private toNumber(value: unknown, fallback: number): number { - return this.toNumberOrUndefined(value) ?? fallback - } - - /** - * Get CockroachDB configuration - */ - private getCockroachDBConfig(): { - host: string - port: number - database: string - user: string - password: string - sslCert: string | null - sslMode?: string - } { - const configDatabases = this.isRecord( - this.config.getConfig().secrets.databases, - ) - ? (this.config.getConfig().secrets.databases as Record) - : {} - const databaseSecrets = this.isRecord(configDatabases['cockroachdb']) - ? configDatabases['cockroachdb'] - : {} - const secrets = databaseSecrets as CockroachDbSecrets - const connectionString = - typeof secrets.connectionString === 'string' - ? secrets.connectionString - : (process.env['COCKROACH_CONNECTION_STRING'] ?? '') - - try { - const parsed = connectionString ? new URL(connectionString) : null - return { - host: parsed?.hostname ?? 'localhost', - port: this.toNumberOrUndefined(parsed?.port) ?? 26257, - database: - typeof parsed?.pathname === 'string' && parsed.pathname.length > 1 - ? parsed.pathname.replace('/', '') - : typeof secrets.database === 'string' - ? secrets.database - : 'defaultdb', - user: - typeof parsed?.username === 'string' && parsed.username.length > 0 - ? parsed.username - : typeof secrets.user === 'string' - ? secrets.user - : (process.env['COCKROACH_USER'] ?? 'root'), - password: - typeof parsed?.password === 'string' && parsed.password.length > 0 - ? parsed.password - : typeof secrets.password === 'string' && - secrets.password.length > 0 - ? secrets.password - : (process.env['COCKROACH_PASSWORD'] ?? ''), - sslCert: process.env['COCKROACH_SSL_CERT'] ?? null, - sslMode: - typeof secrets.sslMode === 'string' ? secrets.sslMode : undefined, - } - } catch { - return { - sslMode: - typeof secrets.sslMode === 'string' ? secrets.sslMode : undefined, - host: 'localhost', - port: 26257, - database: 'defaultdb', - user: process.env['COCKROACH_USER'] ?? 'root', - password: process.env['COCKROACH_PASSWORD'] ?? '', - sslCert: process.env['COCKROACH_SSL_CERT'] ?? null, - } - } - } - - /** - * Get MongoDB configuration for region - */ - private getMongoDBConfig(_region = ''): MongoDbConfig { - const configDatabases = this.isRecord( - this.config.getConfig().secrets.databases, - ) - ? (this.config.getConfig().secrets.databases as Record) - : {} - const secrets = this.isRecord(configDatabases['mongo']) - ? configDatabases['mongo'] - : {} - return { - connectionString: - process.env['MONGODB_CONNECTION_STRING'] ?? - (typeof secrets['connectionString'] === 'string' - ? secrets['connectionString'] - : undefined) ?? - 'mongodb://localhost:27017/pixelated', - ...secrets, - } - } - - /** - * Get Redis configuration for region - */ - private getRedisConfig(_region = ''): { - host: string - port: number - password: string - database: number - [key: string]: unknown - } { - const configDatabases = this.isRecord( - this.config.getConfig().secrets.databases, - ) - ? (this.config.getConfig().secrets.databases as Record) - : {} - const defaults = this.isRecord(configDatabases['redis']) - ? configDatabases['redis'] - : {} - const secrets: RedisDbSecrets = { ...defaults } - const typedSecrets = { - database: secrets.database, - password: secrets.password, - host: process.env['REDIS_HOST'] ?? secrets.host, - port: - process.env['REDIS_PORT'] ?? - (typeof secrets.port === 'number' || typeof secrets.port === 'string' - ? `${secrets.port}` - : undefined), - } - return { - ...secrets, - host: - typeof typedSecrets.host === 'string' ? typedSecrets.host : 'localhost', - port: - this.toNumberOrUndefined( - typedSecrets.port && typeof typedSecrets.port === 'string' - ? typedSecrets.port - : '6379', - ) ?? 6379, - password: - process.env['REDIS_PASSWORD'] ?? - (typeof typedSecrets.password === 'string' - ? typedSecrets.password - : ''), - database: - typeof typedSecrets.database === 'number' ? typedSecrets.database : 0, - } - } - - /** - * Get ClickHouse configuration - */ - private getClickHouseConfig(): ClickHouseConfig { - const configDatabases = this.isRecord( - this.config.getConfig().secrets.databases, - ) - ? (this.config.getConfig().secrets.databases as Record) - : {} - const defaults = this.isRecord(configDatabases['clickhouse']) - ? configDatabases['clickhouse'] - : {} - - return { - host: process.env['CLICKHOUSE_HOST'] ?? 'localhost', - port: this.toNumberOrUndefined(process.env['CLICKHOUSE_PORT']) ?? 8123, - username: process.env['CLICKHOUSE_USERNAME'] ?? 'default', - password: process.env['CLICKHOUSE_PASSWORD'] ?? '', - database: process.env['CLICKHOUSE_DATABASE'] ?? 'default', - https: process.env['CLICKHOUSE_HTTPS'] === 'true', - ...defaults, - } - } - - /** - * Get sync config defaults - */ - private getSyncConfig(): { - realTimeSyncInterval: number - batchSyncInterval: number - cleanupInterval: number - userSyncInterval: number - } { - return { - realTimeSyncInterval: 10_000, - batchSyncInterval: 60_000, - cleanupInterval: 300_000, - userSyncInterval: 120_000, - } - } - - /** - * Register a health check callback - */ - private registerHealthCheck( - name: string, - check: () => Promise<{ status: 'healthy' | 'unhealthy'; message: string }>, - ): void { - this.healthChecks.set(name, check) - - void check() - .then((result) => { - if (result.status !== 'healthy') { - this.healthMonitor.emit('health-check-failed', { - component: name, - message: result.message, - status: result.status, - }) - } - }) - .catch((error: unknown) => { - this.logger.error(`Health check failed for ${name}`, { - error: this.errorContext(error), - }) - }) - } - - /** - * Normalize a record identifier into a string representation - */ - private normalizeRecordId(id: unknown): string { - if (typeof id === 'string') return id - if (id && typeof id === 'object' && 'toString' in id) { - const toStringMethod = id.toString.bind(id) - if ( - typeof toStringMethod === 'function' && - toStringMethod !== Object.prototype.toString - ) { - const normalized = toStringMethod.call(id) - if (typeof normalized === 'string') { - return normalized - } - } - - try { - const normalized = JSON.stringify(id) - if (typeof normalized === 'string') { - return normalized - } - } catch { - // Fall through to fallback value - } - return '[object Object]' - } - return String(id) - } - - /** - * Initialize the data sync manager - */ - async initialize(): Promise { - if (this.isInitialized) return - try { - this.logger.info('Initializing CrossRegionDataSyncManager...') - - // Initialize CockroachDB connection - await this.initializeCockroachDB() - - // Initialize MongoDB connections for each region - await this.initializeMongoDBConnections() - - // Initialize Redis connections for caching - await this.initializeRedisConnections() - - // Initialize ClickHouse for analytics - await this.initializeClickHouse() - - // Set up sync intervals - this.setupSyncIntervals() - - // Register health checks - this.registerHealthChecks() - - this.isInitialized = true - this.logger.info('CrossRegionDataSyncManager initialized successfully') - - this.emit('initialized') - } catch (error: unknown) { - this.logger.error('Failed to initialize CrossRegionDataSyncManager', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Initialize CockroachDB connection - */ - private async initializeCockroachDB(): Promise { - try { - const cockroachConfig = this.getCockroachDBConfig() - const cockroachClient = new CockroachDbClient({ - host: cockroachConfig.host, - port: cockroachConfig.port, - database: cockroachConfig.database, - user: cockroachConfig.user, - password: cockroachConfig.password, - }) - if (!this.isCockroachClient(cockroachClient)) { - throw new Error( - 'CockroachDB client initialization returned unexpected type', - ) - } - - this.cockroachClient = cockroachClient - - await cockroachClient.connect() - this.logger.info('CockroachDB connection established') - - // Create distributed tables - await this.createDistributedTables() - } catch (error: unknown) { - this.logger.error('Failed to initialize CockroachDB connection', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Create distributed tables in CockroachDB - */ - private async createDistributedTables(): Promise { - const tables = [ - { - name: 'users', - schema: ` - CREATE TABLE IF NOT EXISTS users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - email STRING NOT NULL UNIQUE, - username STRING NOT NULL UNIQUE, - region STRING NOT NULL, - created_at TIMESTAMP DEFAULT now(), - updated_at TIMESTAMP DEFAULT now(), - metadata JSONB, - INDEX idx_email (email), - INDEX idx_username (username), - INDEX idx_region (region) - ) LOCALITY REGIONAL BY ROW - `, - }, - { - name: 'sessions', - schema: ` - CREATE TABLE IF NOT EXISTS sessions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id), - region STRING NOT NULL, - token STRING NOT NULL UNIQUE, - expires_at TIMESTAMP NOT NULL, - created_at TIMESTAMP DEFAULT now(), - metadata JSONB, - INDEX idx_user_id (user_id), - INDEX idx_token (token), - INDEX idx_region (region) - ) LOCALITY REGIONAL BY ROW - `, - }, - { - name: 'conversations', - schema: ` - CREATE TABLE IF NOT EXISTS conversations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id), - region STRING NOT NULL, - title STRING, - status STRING DEFAULT 'active', - created_at TIMESTAMP DEFAULT now(), - updated_at TIMESTAMP DEFAULT now(), - metadata JSONB, - INDEX idx_user_id (user_id), - INDEX idx_region (region), - INDEX idx_status (status) - ) LOCALITY REGIONAL BY ROW - `, - }, - { - name: 'messages', - schema: ` - CREATE TABLE IF NOT EXISTS messages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - conversation_id UUID NOT NULL REFERENCES conversations(id), - user_id UUID NOT NULL REFERENCES users(id), - region STRING NOT NULL, - content STRING NOT NULL, - message_type STRING DEFAULT 'text', - sentiment_score FLOAT, - created_at TIMESTAMP DEFAULT now(), - metadata JSONB, - INDEX idx_conversation_id (conversation_id), - INDEX idx_user_id (user_id), - INDEX idx_region (region), - INDEX idx_created_at (created_at) - ) LOCALITY REGIONAL BY ROW - `, - }, - { - name: 'ai_analyses', - schema: ` - CREATE TABLE IF NOT EXISTS ai_analyses ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - message_id UUID NOT NULL REFERENCES messages(id), - user_id UUID NOT NULL REFERENCES users(id), - region STRING NOT NULL, - analysis_type STRING NOT NULL, - bias_score FLOAT, - empathy_score FLOAT, - mental_health_score FLOAT, - recommendations JSONB, - created_at TIMESTAMP DEFAULT now(), - INDEX idx_message_id (message_id), - INDEX idx_user_id (user_id), - INDEX idx_region (region), - INDEX idx_analysis_type (analysis_type) - ) LOCALITY REGIONAL BY ROW - `, - }, - { - name: 'sync_log', - schema: ` - CREATE TABLE IF NOT EXISTS sync_log ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - table_name STRING NOT NULL, - operation STRING NOT NULL, - record_id UUID NOT NULL, - region STRING NOT NULL, - sync_status STRING DEFAULT 'pending', - retry_count INT DEFAULT 0, - error_message STRING, - created_at TIMESTAMP DEFAULT now(), - updated_at TIMESTAMP DEFAULT now(), - INDEX idx_table_name (table_name), - INDEX idx_sync_status (sync_status), - INDEX idx_region (region), - INDEX idx_created_at (created_at) - ) LOCALITY GLOBAL - `, - }, - ] - - for (const table of tables) { - try { - await this.getCockroachClient().query(table.schema) - this.logger.info(`Created distributed table: ${table.name}`) - } catch (error: unknown) { - this.logger.error(`Failed to create table ${table.name}`, { - error: this.errorContext(error), - }) - throw error - } - } - } - - /** - * Initialize MongoDB connections for each region - */ - private async initializeMongoDBConnections(): Promise { - const regions = this.getRegions() - - for (const region of regions) { - try { - const mongoConfig = this.getMongoDBConfig(region) - - const client = new MongoClient(mongoConfig.connectionString, { - maxPoolSize: 10, - serverSelectionTimeoutMS: 5000, - socketTimeoutMS: 45000, - }) - - await client.connect() - this.mongoClients.set(region, client) - - this.logger.info(`MongoDB connection established for region: ${region}`) - } catch (error: unknown) { - this.logger.error( - `Failed to initialize MongoDB for region: ${region}`, - { - error: this.errorContext(error), - }, - ) - throw error - } - } - } - - /** - * Initialize Redis connections for caching - */ - private async initializeRedisConnections(): Promise { - const regions = this.getRegions() - - for (const region of regions) { - try { - const redisConfig = this.getRedisConfig(region) - - const redisHost = - typeof redisConfig.host === 'string' ? redisConfig.host : 'localhost' - const redisPort = this.toNumber(redisConfig.port, 6379) - const redisDb = this.toNumber(redisConfig.database, 0) - const redisPassword = - typeof redisConfig.password === 'string' && - redisConfig.password.length > 0 - ? `:${encodeURIComponent(redisConfig.password)}@` - : '' - const client = new Redis( - `redis://${redisPassword}${redisHost}:${redisPort}/${redisDb}`, - { - maxRetriesPerRequest: 3, - connectTimeout: 5000, - }, - ) - - // Test connection - await client.ping() - this.redisClients.set(region, client) - - this.logger.info(`Redis connection established for region: ${region}`) - } catch (error: unknown) { - this.logger.error(`Failed to initialize Redis for region: ${region}`, { - error: this.errorContext(error), - }) - throw error - } - } - } - - /** - * Initialize ClickHouse for analytics - */ - private async initializeClickHouse(): Promise { - try { - const clickhouseConfig = this.getClickHouseConfig() - - this.clickhouseClient = createClient({ - host: clickhouseConfig.host, - port: clickhouseConfig.port, - username: clickhouseConfig.username, - password: clickhouseConfig.password, - database: clickhouseConfig.database, - application: 'pixelated-multi-region', - max_open_connections: 10, - request_timeout: 30000, - }) - - // Create analytics tables - await this.createAnalyticsTables() - - this.logger.info('ClickHouse connection established') - } catch (error: unknown) { - this.logger.error('Failed to initialize ClickHouse', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Create analytics tables in ClickHouse - */ - private async createAnalyticsTables(): Promise { - const tables = [ - { - name: 'user_analytics', - schema: ` - CREATE TABLE IF NOT EXISTS user_analytics ( - timestamp DateTime, - user_id UUID, - region String, - event_type String, - event_data String, - session_id UUID, - ip_address String, - user_agent String - ) ENGINE = MergeTree() - PARTITION BY toYYYYMM(timestamp) - ORDER BY (timestamp, user_id) - TTL timestamp + INTERVAL 90 DAY - `, - }, - { - name: 'performance_metrics', - schema: ` - CREATE TABLE IF NOT EXISTS performance_metrics ( - timestamp DateTime, - region String, - metric_name String, - metric_value Float64, - tags String - ) ENGINE = MergeTree() - PARTITION BY toYYYYMM(timestamp) - ORDER BY (timestamp, region, metric_name) - TTL timestamp + INTERVAL 30 DAY - `, - }, - ] - - for (const table of tables) { - try { - await this.getClickhouseClient().command({ - query: table.schema, - clickhouse_settings: { - wait_end_of_query: 1, - }, - }) - this.logger.info(`Created ClickHouse table: ${table.name}`) - } catch (error: unknown) { - this.logger.error(`Failed to create ClickHouse table ${table.name}`, { - error: this.errorContext(error), - }) - throw error - } - } - } - - /** - * Set up synchronization intervals - */ - private setupSyncIntervals(): void { - const syncConfig = this.getSyncConfig() - - // Real-time sync for critical data - this.syncInterval = setInterval(() => { - this.performRealTimeSync().catch((error) => { - this.logger.error('Real-time sync failed', { - error: this.errorContext(error), - }) - }) - }, syncConfig.realTimeSyncInterval) - - // Batch sync for analytics data - setInterval(() => { - this.performBatchSync().catch((error) => { - this.logger.error('Batch sync failed', { - error: this.errorContext(error), - }) - }) - }, syncConfig.batchSyncInterval) - - // Cleanup old sync logs - setInterval(() => { - this.cleanupSyncLogs().catch((error) => { - this.logger.error('Sync log cleanup failed', { - error: this.errorContext(error), - }) - }) - }, syncConfig.cleanupInterval) - - this.logger.info('Sync intervals configured') - } - - /** - * Register health checks - */ - private registerHealthChecks(): void { - this.registerHealthCheck('cockroachdb', async () => { - try { - if (!this.cockroachClient) - return { - status: 'unhealthy', - message: 'CockroachDB client not initialized', - } - - await this.getCockroachClient().query('SELECT 1') - return { status: 'healthy', message: 'CockroachDB connection active' } - } catch (error: unknown) { - return { - status: 'unhealthy', - message: `CockroachDB error: ${error instanceof Error ? error.message : 'Unknown error'}`, - } - } - }) - - this.registerHealthCheck('mongodb', async () => { - try { - const regions = Array.from(this.mongoClients.keys()) - const results = await Promise.all( - regions.map(async (region) => { - const client = this.mongoClients.get(region) - if (!client) - return { - region, - status: 'unhealthy', - message: 'Client not found', - } - - try { - await client.db().admin().ping() - return { - region, - status: 'healthy', - message: 'MongoDB connection active', - } - } catch (error: unknown) { - return { - region, - status: 'unhealthy', - message: `MongoDB error: ${error instanceof Error ? error.message : 'Unknown error'}`, - } - } - }), - ) - - const unhealthy = results.filter((r) => r.status === 'unhealthy') - if (unhealthy.length > 0) { - return { - status: 'unhealthy', - message: `MongoDB issues in regions: ${unhealthy.map((r) => r.region).join(', ')}`, - } - } - - return { status: 'healthy', message: 'All MongoDB connections active' } - } catch (error: unknown) { - return { - status: 'unhealthy', - message: `MongoDB health check error: ${error instanceof Error ? error.message : 'Unknown error'}`, - } - } - }) - - this.registerHealthCheck('redis', async () => { - try { - const regions = Array.from(this.redisClients.keys()) - const results = await Promise.all( - regions.map(async (region) => { - const client = this.redisClients.get(region) - if (!client) - return { - region, - status: 'unhealthy', - message: 'Client not found', - } - - try { - await client.ping() - return { - region, - status: 'healthy', - message: 'Redis connection active', - } - } catch (error: unknown) { - return { - region, - status: 'unhealthy', - message: `Redis error: ${error instanceof Error ? error.message : 'Unknown error'}`, - } - } - }), - ) - - const unhealthy = results.filter((r) => r.status === 'unhealthy') - if (unhealthy.length > 0) { - return { - status: 'unhealthy', - message: `Redis issues in regions: ${unhealthy.map((r) => r.region).join(', ')}`, - } - } - - return { status: 'healthy', message: 'All Redis connections active' } - } catch (error: unknown) { - return { - status: 'unhealthy', - message: `Redis health check error: ${error instanceof Error ? error.message : 'Unknown error'}`, - } - } - }) - } - - /** - * Perform real-time synchronization - */ - private async performRealTimeSync(): Promise { - try { - this.logger.debug('Performing real-time sync...') - - // Sync user data - await this.syncUserData() - - // Sync session data - await this.syncSessionData() - - // Sync conversation data - await this.syncConversationData() - - // Process pending sync logs - await this.processSyncLogs() - - this.logger.debug('Real-time sync completed') - } catch (error: unknown) { - this.logger.error('Real-time sync failed', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Sync user data across regions - */ - private async syncUserData(): Promise { - try { - const regions = this.getRegions() - const syncConfig = this.getSyncConfig() - - for (const region of regions) { - const client = this.mongoClients.get(region) - if (!client) continue - - const db = client.db() - const usersCollection = db.collection>('users') - - // Find users that need syncing - const pendingUsers = await usersCollection - .find({ - $or: [ - { lastSyncedAt: { $exists: false } }, - { - lastSyncedAt: { - $lt: new Date(Date.now() - syncConfig.userSyncInterval), - }, - }, - { syncStatus: 'pending' }, - ], - }) - .limit(100) - .toArray() - - for (const user of pendingUsers) { - try { - // Sync to CockroachDB - await this.syncUserToCockroachDB(user, region) - - // Update sync status - await usersCollection.updateOne( - { _id: user._id }, - { - $set: { - lastSyncedAt: new Date(), - syncStatus: 'synced', - }, - }, - ) - - // Log sync - await this.logSync( - 'users', - 'sync', - this.normalizeRecordId(user._id), - region, - 'completed', - ) - } catch (error: unknown) { - const userId = this.normalizeRecordId(user._id) - this.logger.error(`Failed to sync user ${userId} from ${region}`, { - error: this.errorContext(error), - }) - await this.logSync( - 'users', - 'sync', - userId, - region, - 'failed', - error instanceof Error ? error.message : 'Unknown error', - ) - } - } - } - } catch (error: unknown) { - this.logger.error('User data sync failed', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Sync user to CockroachDB - */ - private async syncUserToCockroachDB( - user: Record, - region: string, - ): Promise { - const query = ` - INSERT INTO users (id, email, username, region, metadata, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (id) DO UPDATE SET - email = EXCLUDED.email, - username = EXCLUDED.username, - metadata = EXCLUDED.metadata, - updated_at = EXCLUDED.updated_at - ` - - const values = [ - this.normalizeRecordId(user['_id']), - user['email'], - user['username'], - region, - JSON.stringify(user['metadata'] ?? {}), - user['createdAt'] ?? new Date(), - user['updatedAt'] ?? new Date(), - ] - - await this.getCockroachClient().query(query, values) - } - - /** - * Sync session data across regions - */ - private async syncSessionData(): Promise { - try { - const regions = this.getRegions() - - for (const region of regions) { - const client = this.mongoClients.get(region) - if (!client) continue - - const db = client.db() - const sessionsCollection = - db.collection>('sessions') - - // Find sessions that need syncing - const pendingSessions = await sessionsCollection - .find({ - syncStatus: 'pending', - }) - .limit(50) - .toArray() - - for (const session of pendingSessions) { - try { - // Sync to CockroachDB - await this.syncSessionToCockroachDB(session, region) - - // Update sync status - await sessionsCollection.updateOne( - { _id: session._id }, - { $set: { syncStatus: 'synced' } }, - ) - - // Log sync - await this.logSync( - 'sessions', - 'sync', - this.normalizeRecordId(session._id), - region, - 'completed', - ) - } catch (error: unknown) { - const sessionId = this.normalizeRecordId(session._id) - this.logger.error( - `Failed to sync session ${sessionId} from ${region}`, - { error: this.errorContext(error) }, - ) - await this.logSync( - 'sessions', - 'sync', - sessionId, - region, - 'failed', - error instanceof Error ? error.message : 'Unknown error', - ) - } - } - } - } catch (error: unknown) { - this.logger.error('Session data sync failed', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Sync session to CockroachDB - */ - private async syncSessionToCockroachDB( - session: Record, - region: string, - ): Promise { - const query = ` - INSERT INTO sessions (id, user_id, region, token, expires_at, metadata, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (id) DO UPDATE SET - expires_at = EXCLUDED.expires_at, - metadata = EXCLUDED.metadata - ` - - const values = [ - this.normalizeRecordId(session['_id']), - this.normalizeRecordId(session['userId']), - region, - session['token'], - session['expiresAt'], - JSON.stringify(session['metadata'] ?? {}), - session['createdAt'] ?? new Date(), - ] - - await this.getCockroachClient().query(query, values) - } - - /** - * Sync conversation data across regions - */ - private async syncConversationData(): Promise { - try { - const regions = this.getRegions() - - for (const region of regions) { - const client = this.mongoClients.get(region) - if (!client) continue - - const db = client.db() - const conversationsCollection = - db.collection>('conversations') - const messagesCollection = - db.collection>('messages') - - // Sync conversations - const pendingConversations = await conversationsCollection - .find({ - syncStatus: 'pending', - }) - .limit(20) - .toArray() - - for (const conversation of pendingConversations) { - try { - // Sync conversation to CockroachDB - await this.syncConversationToCockroachDB(conversation, region) - - // Sync related messages - const messages = await messagesCollection - .find({ - conversationId: conversation._id, - syncStatus: 'pending', - }) - .toArray() - - for (const message of messages) { - await this.syncMessageToCockroachDB(message, region) - - // Sync AI analyses if available - const analysis = this.toRecord(message['aiAnalysis']) - if (analysis) { - await this.syncAIAnalysisToCockroachDB( - analysis, - this.normalizeRecordId(message._id), - this.normalizeRecordId(conversation['userId']), - region, - ) - } - } - - // Update sync status - await conversationsCollection.updateOne( - { _id: conversation._id }, - { $set: { syncStatus: 'synced' } }, - ) - - // Update message sync status - await messagesCollection.updateMany( - { conversationId: conversation._id }, - { $set: { syncStatus: 'synced' } }, - ) - - // Log sync - await this.logSync( - 'conversations', - 'sync', - this.normalizeRecordId(conversation._id), - region, - 'completed', - ) - } catch (error: unknown) { - const conversationId = this.normalizeRecordId(conversation._id) - this.logger.error( - `Failed to sync conversation ${conversationId} from ${region}`, - { - error: this.errorContext(error), - }, - ) - await this.logSync( - 'conversations', - 'sync', - conversationId, - region, - 'failed', - error instanceof Error ? error.message : 'Unknown error', - ) - } - } - } - } catch (error: unknown) { - this.logger.error('Conversation data sync failed', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Sync conversation to CockroachDB - */ - private async syncConversationToCockroachDB( - conversation: Record, - region: string, - ): Promise { - const query = ` - INSERT INTO conversations (id, user_id, region, title, status, metadata, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - ON CONFLICT (id) DO UPDATE SET - title = EXCLUDED.title, - status = EXCLUDED.status, - metadata = EXCLUDED.metadata, - updated_at = EXCLUDED.updated_at - ` - - const values = [ - this.normalizeRecordId(conversation['_id']), - this.normalizeRecordId(conversation['userId']), - region, - conversation['title'], - conversation['status'] ?? 'active', - JSON.stringify(conversation['metadata'] ?? {}), - conversation['createdAt'] ?? new Date(), - conversation['updatedAt'] ?? new Date(), - ] - - await this.getCockroachClient().query(query, values) - } - - /** - * Sync message to CockroachDB - */ - private async syncMessageToCockroachDB( - message: Record, - region: string, - ): Promise { - const query = ` - INSERT INTO messages (id, conversation_id, user_id, region, content, message_type, sentiment_score, metadata, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - ON CONFLICT (id) DO UPDATE SET - content = EXCLUDED.content, - sentiment_score = EXCLUDED.sentiment_score, - metadata = EXCLUDED.metadata - ` - - const values = [ - this.normalizeRecordId(message['_id']), - this.normalizeRecordId(message['conversationId']), - this.normalizeRecordId(message['userId']), - region, - message['content'], - message['messageType'] ?? 'text', - message['sentimentScore'] ?? null, - JSON.stringify(message['metadata'] ?? {}), - message['createdAt'] ?? new Date(), - ] - - await this.getCockroachClient().query(query, values) - } - - /** - * Sync AI analysis to CockroachDB - */ - private async syncAIAnalysisToCockroachDB( - analysis: Record, - messageId: string, - userId: string, - region: string, - ): Promise { - const query = ` - INSERT INTO ai_analyses (id, message_id, user_id, region, analysis_type, bias_score, empathy_score, mental_health_score, recommendations, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) - ON CONFLICT (id) DO UPDATE SET - bias_score = EXCLUDED.bias_score, - empathy_score = EXCLUDED.empathy_score, - mental_health_score = EXCLUDED.mental_health_score, - recommendations = EXCLUDED.recommendations - ` - - const values = [ - analysis['_id'] ?? uuidv4(), - messageId, - userId, - region, - analysis['analysisType'], - analysis['biasScore'] ?? null, - analysis['empathyScore'] ?? null, - analysis['mentalHealthScore'] ?? null, - JSON.stringify(analysis['recommendations'] ?? {}), - analysis['createdAt'] ?? new Date(), - ] - - await this.getCockroachClient().query(query, values) - } - - /** - * Log synchronization operation - */ - private async logSync( - tableName: string, - operation: string, - recordId: string, - region: string, - status: string, - errorMessage?: string, - ): Promise { - const query = ` - INSERT INTO sync_log (table_name, operation, record_id, region, sync_status, error_message) - VALUES ($1, $2, $3, $4, $5, $6) - ` - - const values = [ - tableName, - operation, - recordId, - region, - status, - errorMessage ?? null, - ] - await this.getCockroachClient().query(query, values) - } - - /** - * Process pending sync logs - */ - private async processSyncLogs(): Promise { - try { - const query = ` - SELECT * FROM sync_log - WHERE sync_status = 'failed' AND retry_count < 3 - ORDER BY created_at ASC - LIMIT 50 - ` - - const result = await this.getCockroachClient().query(query) - const logRows = this.toRecordArray(result.rows) - - for (const log of logRows) { - try { - // Retry the failed operation - await this.retrySyncOperation(log) - - // Update sync log - await this.getCockroachClient().query( - 'UPDATE sync_log SET sync_status = $1, retry_count = retry_count + 1, updated_at = now() WHERE id = $2', - ['completed', log['id']], - ) - } catch (error: unknown) { - this.logger.error( - `Failed to retry sync operation ${this.toLogString(log['id'])}`, - { - error, - }, - ) - - // Update retry count - await this.getCockroachClient().query( - 'UPDATE sync_log SET retry_count = retry_count + 1, error_message = $1, updated_at = now() WHERE id = $2', - [ - error instanceof Error ? error.message : 'Unknown error', - log['id'], - ], - ) - } - } - } catch (error: unknown) { - this.logger.error(`Failed to process sync logs`, { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Retry a failed sync operation - */ - private async retrySyncOperation( - log: Record, - ): Promise { - // Implementation depends on the specific operation - const tableName = this.toLogString(log['table_name']) - const recordId = this.toLogString(log['record_id']) - this.logger.info( - `Retrying sync operation for ${tableName} record ${recordId}`, - ) - - // Add retry logic based on table and operation type - // This is a placeholder - implement specific retry logic as needed - } - - /** - * Perform batch synchronization for analytics - */ - private async performBatchSync(): Promise { - try { - this.logger.debug('Performing batch sync for analytics...') - - // Sync performance metrics to ClickHouse - await this.syncPerformanceMetrics() - - // Sync user analytics - await this.syncUserAnalytics() - - // Cleanup old data - await this.cleanupOldData() - - this.logger.debug('Batch sync completed') - } catch (error: unknown) { - this.logger.error('Batch sync failed', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Sync performance metrics to ClickHouse - */ - private async syncPerformanceMetrics(): Promise { - try { - const regions = this.getRegions() - const metrics: Array<{ - timestamp: Date - region: string - metric_name: string - metric_value: number - tags: string - }> = [] - - for (const region of regions) { - const client = this.redisClients.get(region) - if (!client) continue - if (!this.isRedisClientWithHGetAll(client)) { - continue - } - // Get performance metrics from Redis - const rawMetrics = await client.hgetall(`metrics:${region}`) - const regionMetrics = this.isStringRecord(rawMetrics) ? rawMetrics : {} - - for (const [metricName, metricValue] of Object.entries(regionMetrics)) { - metrics.push({ - timestamp: new Date(), - region, - metric_name: metricName, - metric_value: this.toNumberOrUndefined(metricValue) ?? 0, - tags: JSON.stringify({ source: 'redis' }), - }) - } - } - - // Insert into ClickHouse - if (metrics.length > 0) { - await this.getClickhouseClient().insert({ - table: 'performance_metrics', - values: metrics, - format: 'JSONEachRow', - }) - } - - this.logger.debug(`Synced ${metrics.length} performance metrics`) - } catch (error: unknown) { - this.logger.error('Failed to sync performance metrics', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Sync user analytics to ClickHouse - */ - private async syncUserAnalytics(): Promise { - try { - const regions = this.getRegions() - const analytics: Array<{ - timestamp: Date - user_id: string - region: string - event_type: string - event_data: string - session_id: string - ip_address: string - user_agent: string - }> = [] - - for (const region of regions) { - const client = this.mongoClients.get(region) - if (!client) continue - - const db = client.db() - const analyticsCollection = - db.collection>('user_analytics') - - // Get recent analytics data - const recentAnalytics = await analyticsCollection - .find({ - syncedAt: { $exists: false }, - }) - .limit(1000) - .toArray() - - for (const analytic of recentAnalytics) { - const userId = this.normalizeRecordId(analytic['userId']) - const sessionId = this.normalizeRecordId(analytic['sessionId']) - const eventType = - typeof analytic['eventType'] === 'string' - ? analytic['eventType'] - : '' - const ipAddress = - typeof analytic['ipAddress'] === 'string' - ? analytic['ipAddress'] - : '' - const userAgent = - typeof analytic['userAgent'] === 'string' - ? analytic['userAgent'] - : '' - const eventData = - typeof analytic['eventData'] === 'object' - ? analytic['eventData'] - : {} - - analytics.push({ - timestamp: - analytic['timestamp'] instanceof Date - ? analytic['timestamp'] - : new Date(), - user_id: userId || uuidv4(), - region, - event_type: eventType, - event_data: JSON.stringify(eventData), - session_id: sessionId || uuidv4(), - ip_address: ipAddress, - user_agent: userAgent, - }) - } - - // Mark as synced - await analyticsCollection.updateMany( - { _id: { $in: recentAnalytics.map((a) => a._id) } }, - { $set: { syncedAt: new Date() } }, - ) - } - - // Insert into ClickHouse - if (analytics.length > 0) { - await this.getClickhouseClient().insert({ - table: 'user_analytics', - values: analytics, - format: 'JSONEachRow', - }) - } - - this.logger.debug(`Synced ${analytics.length} user analytics events`) - } catch (error: unknown) { - this.logger.error('Failed to sync user analytics', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Cleanup old data - */ - private async cleanupOldData(): Promise { - try { - // Cleanup old sync logs - const cleanupQuery = ` - DELETE FROM sync_log - WHERE created_at < now() - INTERVAL '30 days' - AND sync_status = 'completed' - ` - - const result = await this.getCockroachClient().query(cleanupQuery) - this.logger.debug(`Cleaned up ${result.rowCount} old sync logs`) - } catch (error: unknown) { - this.logger.error('Failed to cleanup old data', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Cleanup sync logs - */ - private async cleanupSyncLogs(): Promise { - try { - const query = ` - DELETE FROM sync_log - WHERE created_at < now() - INTERVAL '7 days' - AND sync_status IN ('completed', 'failed') - AND retry_count >= 3 - ` - - const result = await this.getCockroachClient().query(query) - this.logger.debug(`Cleaned up ${result.rowCount} old sync log entries`) - } catch (error: unknown) { - this.logger.error('Failed to cleanup sync logs', { - error: this.errorContext(error), - }) - } - } - - /** - * Get sync status for all regions - */ - async getSyncStatus(): Promise> { - return new Map(this.syncStatus) - } - - /** - * Get data distribution across regions - */ - async getDataDistribution(): Promise { - try { - const query = ` - SELECT - region, - COUNT(*) as total_records, - COUNT(CASE WHEN sync_status = 'pending' THEN 1 END) as pending_sync, - COUNT(CASE WHEN sync_status = 'failed' THEN 1 END) as failed_sync, - COUNT(CASE WHEN sync_status = 'completed' THEN 1 END) as completed_sync - FROM sync_log - WHERE created_at > now() - INTERVAL '24 hours' - GROUP BY region - ORDER BY region - ` - - const result = await this.getCockroachClient().query(query) - - const distribution: DataDistribution = { - totalRecords: 0, - pendingSync: 0, - failedSync: 0, - completedSync: 0, - regions: {}, - } - - for (const row of result.rows) { - const rowRecord = row - const regionName = this.getRecordValueAsString(rowRecord, 'region') - if (!regionName) continue - - distribution.regions[regionName] = { - totalRecords: this.getRecordValueAsNumber( - rowRecord, - 'total_records', - 0, - ), - pendingSync: this.getRecordValueAsNumber( - rowRecord, - 'pending_sync', - 0, - ), - failedSync: this.getRecordValueAsNumber(rowRecord, 'failed_sync', 0), - completedSync: this.getRecordValueAsNumber( - rowRecord, - 'completed_sync', - 0, - ), - } - - distribution.totalRecords += this.getRecordValueAsNumber( - rowRecord, - 'total_records', - 0, - ) - distribution.pendingSync += this.getRecordValueAsNumber( - rowRecord, - 'pending_sync', - 0, - ) - distribution.failedSync += this.getRecordValueAsNumber( - rowRecord, - 'failed_sync', - 0, - ) - distribution.completedSync += this.getRecordValueAsNumber( - rowRecord, - 'completed_sync', - 0, - ) - } - - return distribution - } catch (error: unknown) { - this.logger.error('Failed to get data distribution', { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Force sync for specific table and region - */ - async forceSync(tableName: string, region: string): Promise { - try { - this.logger.info(`Force syncing ${tableName} for region ${region}...`) - - switch (tableName) { - case 'users': - await this.syncUserData() - break - case 'sessions': - await this.syncSessionData() - break - case 'conversations': - await this.syncConversationData() - break - default: - throw new Error(`Unsupported table: ${tableName}`) - } - - this.logger.info(`Force sync completed for ${tableName} in ${region}`) - } catch (error: unknown) { - this.logger.error(`Force sync failed for ${tableName} in ${region}`, { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Get replication lag for a region - */ - async getReplicationLag(region: string): Promise { - try { - const query = ` - SELECT - EXTRACT(EPOCH FROM (now() - MAX(updated_at))) as lag_seconds - FROM sync_log - WHERE region = $1 - AND sync_status = 'completed' - ` - - const result = await this.getCockroachClient().query(query, [region]) - if (result.rows.length > 0) { - const row = result.rows[0] - if (row !== undefined) { - const lagSeconds = this.getRecordValueAsString(row, 'lag_seconds') - if (typeof lagSeconds === 'string') { - return parseFloat(lagSeconds) - } - } - } - - return 0 - } catch (error: unknown) { - this.logger.error(`Failed to get replication lag for ${region}`, { - error: this.errorContext(error), - }) - throw error - } - } - - /** - * Shutdown the data sync manager - */ - async shutdown(): Promise { - if (!this.isInitialized) return - try { - this.logger.info('Shutting down CrossRegionDataSyncManager...') - - // Clear sync intervals - if (this.syncInterval) { - clearInterval(this.syncInterval) - this.syncInterval = null - } - - // Close CockroachDB connection - if (this.cockroachClient) { - if (this.cockroachClient.end) { - await this.cockroachClient.end() - } else if (this.cockroachClient.close) { - await this.cockroachClient.close() - } - this.cockroachClient = null - } - - // Close MongoDB connections - for (const [region, client] of this.mongoClients) { - await client.close() - this.logger.info(`MongoDB connection closed for region: ${region}`) - } - this.mongoClients.clear() - - // Close Redis connections - for (const [region, client] of this.redisClients) { - await client.quit() - this.logger.info(`Redis connection closed for region: ${region}`) - } - this.redisClients.clear() - - // Close ClickHouse connection - if (this.clickhouseClient) { - await this.getClickhouseClient().close?.() - this.clickhouseClient = null - } - - this.isInitialized = false - this.logger.info('CrossRegionDataSyncManager shutdown completed') - - this.emit('shutdown') - } catch (error: unknown) { - this.logger.error('Error during shutdown', { - error: this.errorContext(error), - }) - throw error - } - } -} - -// Types -interface SyncStatus { - region: string - tableName: string - lastSync: Date - pendingRecords: number - failedRecords: number - status: 'syncing' | 'idle' | 'error' -} - -interface DataDistribution { - totalRecords: number - pendingSync: number - failedSync: number - completedSync: number - regions: { - [region: string]: { - totalRecords: number - pendingSync: number - failedSync: number - completedSync: number - } - } -} - -export type { SyncStatus, DataDistribution } diff --git a/apps/web/src/lib/deployment/multi-region/DNSClient.ts b/apps/web/src/lib/deployment/multi-region/DNSClient.ts deleted file mode 100644 index a3cbaee81a..0000000000 --- a/apps/web/src/lib/deployment/multi-region/DNSClient.ts +++ /dev/null @@ -1,29 +0,0 @@ -// DNS Client for multi-region deployments - -export class DNSClient { - constructor(_config: unknown) { - // Initialize DNS client with configuration - } - - async updateRecord( - _domain: string, - _recordType: string, - _value: string, - ): Promise { - // Update DNS record implementation - } - - async getRecord(_domain: string, _recordType: string): Promise { - // Get DNS record implementation - return null - } - - async listRecords(_domain: string): Promise { - // List DNS records implementation - return [] - } - - async deleteRecord(_domain: string, _recordType: string): Promise { - // Delete DNS record implementation - } -} diff --git a/apps/web/src/lib/deployment/multi-region/DeploymentOrchestrator.ts b/apps/web/src/lib/deployment/multi-region/DeploymentOrchestrator.ts deleted file mode 100644 index 3e4866e5ea..0000000000 --- a/apps/web/src/lib/deployment/multi-region/DeploymentOrchestrator.ts +++ /dev/null @@ -1,1278 +0,0 @@ -/** - * Deployment Orchestrator - * - * Orchestrates complex multi-region deployments with dependency management, - * rollback capabilities, and automated deployment pipelines. - */ - -import { EventEmitter } from 'events' - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' - -const logger = createBuildSafeLogger('DeploymentOrchestrator') -import { CloudProviderManager, DeploymentResult } from './CloudProviderManager' -import { RegionConfig } from './MultiRegionDeploymentManager' - -export interface DeploymentOrchestratorConfig { - maxParallelDeployments: number - rollbackOnFailure: boolean - healthCheckTimeout: number - deploymentTimeout: number - retryAttempts: number - retryDelay: number - dependencies: { - infrastructure: string[] - services: string[] - monitoring: string[] - } -} - -export interface DeploymentPlan { - id: string - name: string - regions: RegionConfig[] - phases: DeploymentPhase[] - dependencies: string[] - rollbackPoints: RollbackPoint[] - validationSteps: ValidationStep[] -} - -export interface DeploymentPhase { - id: string - name: string - type: 'infrastructure' | 'services' | 'monitoring' | 'validation' - regions: string[] - dependencies: string[] - timeout: number - rollbackEnabled: boolean -} - -export interface RollbackPoint { - id: string - name: string - phaseId: string - snapshot: Record - createdAt: Date -} - -export interface ValidationStep { - id: string - name: string - type: - | 'health-check' - | 'performance-test' - | 'security-scan' - | 'compliance-check' - target: string - timeout: number - successCriteria: Record -} - -export interface DeploymentExecution { - id: string - planId: string - status: 'pending' | 'running' | 'completed' | 'failed' | 'rolled-back' - currentPhase: string - startedAt: Date - completedAt?: Date - results: DeploymentPhaseResult[] - errors: string[] - rollbackPoint?: string -} - -export interface DeploymentPhaseResult { - phaseId: string - status: 'success' | 'failed' | 'skipped' | 'rolled-back' - startedAt: Date - completedAt: Date - results: (DeploymentResult | Record)[] - errors: string[] -} - -export class DeploymentOrchestrator extends EventEmitter { - private readonly config: DeploymentOrchestratorConfig - private readonly cloudProviderManager: CloudProviderManager - private readonly deploymentPlans: Map = new Map() - private readonly activeExecutions: Map = - new Map() - private readonly rollbackPoints: Map = new Map() - private isInitialized = false - - constructor( - config: DeploymentOrchestratorConfig, - cloudProviderManager: CloudProviderManager, - ) { - super() - this.config = config - this.cloudProviderManager = cloudProviderManager - } - - /** - * Initialize deployment orchestrator - */ - async initialize(): Promise { - try { - logger.info('Initializing Deployment Orchestrator') - - // Validate configuration - await this.validateConfiguration() - - // Initialize deployment plans - await this.initializeDeploymentPlans() - - // Setup event listeners - this.setupEventListeners() - - this.isInitialized = true - logger.info('Deployment Orchestrator initialized successfully') - - this.emit('initialized', { - maxParallelDeployments: this.config.maxParallelDeployments, - }) - } catch (error: unknown) { - logger.error('Failed to initialize Deployment Orchestrator', { error }) - if (error instanceof Error) { - throw new Error('Initialization failed', { cause: error }) - } - throw error - } - } - - /** - * Validate orchestrator configuration - */ - private async validateConfiguration(): Promise { - const errors: string[] = [] - - if (this.config.maxParallelDeployments < 1) { - errors.push('maxParallelDeployments must be at least 1') - } - - if (this.config.deploymentTimeout < 60000) { - // 1 minute minimum - errors.push('deploymentTimeout must be at least 60000ms') - } - - if (this.config.retryAttempts < 0) { - errors.push('retryAttempts must be non-negative') - } - - if (this.config.retryDelay < 0) { - errors.push('retryDelay must be non-negative') - } - - if (errors.length > 0) { - throw new Error(`Configuration validation failed: ${errors.join(', ')}`) - } - } - - /** - * Initialize deployment plans - */ - private async initializeDeploymentPlans(): Promise { - try { - // Load default deployment plans - const defaultPlans = this.createDefaultDeploymentPlans() - - for (const plan of defaultPlans) { - this.deploymentPlans.set(plan.id, plan) - } - - logger.info(`Initialized ${defaultPlans.length} deployment plans`) - } catch (error: unknown) { - logger.error('Failed to initialize deployment plans', { error }) - throw error - } - } - - /** - * Create default deployment plans - */ - private createDefaultDeploymentPlans(): DeploymentPlan[] { - return [ - { - id: 'standard-multi-region', - name: 'Standard Multi-Region Deployment', - regions: [], - phases: [ - { - id: 'infrastructure', - name: 'Infrastructure Deployment', - type: 'infrastructure', - regions: [], - dependencies: [], - timeout: 1800000, // 30 minutes - rollbackEnabled: true, - }, - { - id: 'services', - name: 'Service Deployment', - type: 'services', - regions: [], - dependencies: ['infrastructure'], - timeout: 1200000, // 20 minutes - rollbackEnabled: true, - }, - { - id: 'monitoring', - name: 'Monitoring Setup', - type: 'monitoring', - regions: [], - dependencies: ['services'], - timeout: 600000, // 10 minutes - rollbackEnabled: false, - }, - { - id: 'validation', - name: 'Deployment Validation', - type: 'validation', - regions: [], - dependencies: ['monitoring'], - timeout: 300000, // 5 minutes - rollbackEnabled: false, - }, - ], - dependencies: [], - rollbackPoints: [], - validationSteps: [ - { - id: 'health-check', - name: 'Health Check Validation', - type: 'health-check', - target: 'all-regions', - timeout: 120000, - successCriteria: { minHealthScore: 80 }, - }, - { - id: 'performance-test', - name: 'Performance Test', - type: 'performance-test', - target: 'all-regions', - timeout: 180000, - successCriteria: { maxResponseTime: 200, minThroughput: 100 }, - }, - ], - }, - { - id: 'rolling-deployment', - name: 'Rolling Multi-Region Deployment', - regions: [], - phases: [ - { - id: 'region-1', - name: 'Deploy to Region 1', - type: 'services', - regions: [], - dependencies: [], - timeout: 900000, // 15 minutes - rollbackEnabled: true, - }, - { - id: 'validate-region-1', - name: 'Validate Region 1', - type: 'validation', - regions: [], - dependencies: ['region-1'], - timeout: 300000, // 5 minutes - rollbackEnabled: false, - }, - { - id: 'region-2', - name: 'Deploy to Region 2', - type: 'services', - regions: [], - dependencies: ['validate-region-1'], - timeout: 900000, // 15 minutes - rollbackEnabled: true, - }, - { - id: 'validate-region-2', - name: 'Validate Region 2', - type: 'validation', - regions: [], - dependencies: ['region-2'], - timeout: 300000, // 5 minutes - rollbackEnabled: false, - }, - ], - dependencies: [], - rollbackPoints: [], - validationSteps: [], - }, - ] - } - - /** - * Setup event listeners - */ - private setupEventListeners(): void { - // Cloud provider manager events are not currently exposed in this implementation. - // Keep this method for future extension without failing initialization. - } - - /** - * Execute deployment plan - */ - async executeDeployment( - planId: string, - regions: RegionConfig[], - ): Promise { - if (!this.isInitialized) { - throw new Error('Deployment orchestrator not initialized') - } - - const plan = this.deploymentPlans.get(planId) - if (!plan) { - throw new Error(`Deployment plan not found: ${planId}`) - } - - try { - logger.info(`Starting deployment execution for plan: ${plan.name}`, { - planId, - regions: regions.length, - }) - - // Create execution record - const execution = this.createExecution(planId, regions) - this.activeExecutions.set(execution.id, execution) - - // Update plan with actual regions - const updatedPlan = { ...plan, regions } - - // Execute deployment phases - await this.executeDeploymentPhases(execution, updatedPlan) - - // Complete execution - execution.status = 'completed' - execution.completedAt = new Date() - - logger.info( - `Deployment execution completed successfully: ${execution.id}`, - ) - this.emit('deployment-completed', { executionId: execution.id, planId }) - - return execution - } catch (error: unknown) { - const message = - error instanceof Error - ? error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error' - : 'Unknown error' - logger.error(`Deployment execution failed for plan: ${planId}`, { - error: message, - }) - - const execution = this.activeExecutions.get(`exec-${planId}`) - if (execution) { - execution.status = 'failed' - execution.completedAt = new Date() - execution.errors.push(message) - - // Attempt rollback if enabled - if (this.config.rollbackOnFailure) { - await this.performRollback(execution) - } - } - - this.emit('deployment-failed', { planId, error: message }) - throw error - } - } - - /** - * Create deployment execution record - */ - private createExecution( - planId: string, - _regions: RegionConfig[], - ): DeploymentExecution { - return { - id: `exec-${planId}-${Date.now()}`, - planId, - status: 'running', - currentPhase: '', - startedAt: new Date(), - results: [], - errors: [], - rollbackPoint: undefined, - } - } - - /** - * Execute deployment phases - */ - private async executeDeploymentPhases( - execution: DeploymentExecution, - plan: DeploymentPlan, - ): Promise { - try { - logger.info( - `Executing deployment phases for execution: ${execution.id}`, - { - totalPhases: plan.phases.length, - }, - ) - - for (const phase of plan.phases) { - execution.currentPhase = phase.id - - logger.info(`Executing deployment phase: ${phase.name}`, { - phaseId: phase.id, - type: phase.type, - regions: phase.regions.length, - }) - - // Check dependencies - if (!(await this.checkPhaseDependencies(phase, execution))) { - logger.warn(`Skipping phase due to unmet dependencies: ${phase.id}`) - execution.results.push({ - phaseId: phase.id, - status: 'skipped', - startedAt: new Date(), - completedAt: new Date(), - results: [], - errors: ['Dependencies not met'], - }) - continue - } - - // Create rollback point if enabled - if (phase.rollbackEnabled) { - await this.createRollbackPoint(execution, phase) - } - - // Execute phase - const phaseResult = await this.executePhase( - phase, - plan.regions, - execution, - ) - execution.results.push(phaseResult) - - // Handle phase failure - if (phaseResult.status === 'failed') { - logger.error(`Phase execution failed: ${phase.id}`, { - errors: phaseResult.errors, - }) - - if (phase.rollbackEnabled && this.config.rollbackOnFailure) { - await this.rollbackPhase(execution, phase) - execution.status = 'rolled-back' - } else { - throw new Error( - `Phase ${phase.id} failed: ${phaseResult.errors.join(', ')}`, - ) - } - - return // Stop execution on failure - } - - // Perform validation if this is the validation phase - if (phase.type === 'validation') { - const validationResult = await this.performValidation(execution, plan) - if (!validationResult.success) { - throw new Error( - `Validation failed: ${validationResult.errors.join(', ')}`, - ) - } - } - } - } catch (error: unknown) { - logger.error('Deployment phase execution failed', { - error: - error instanceof Error - ? error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error' - : 'Unknown error', - }) - throw error - } - } - - /** - * Execute individual deployment phase - */ - private async executePhase( - phase: DeploymentPhase, - regions: RegionConfig[], - _execution: DeploymentExecution, - ): Promise { - const startTime = new Date() - - try { - let phaseResults: (DeploymentResult | Record)[] = [] - const phaseErrors: string[] = [] - - switch (phase.type) { - case 'infrastructure': - phaseResults = await this.deployInfrastructure(phase, regions) - break - - case 'services': - phaseResults = await this.deployServices(phase, regions) - break - - case 'monitoring': - phaseResults = await this.setupMonitoring(phase, regions) - break - - case 'validation': - phaseResults = await this.performPhaseValidation(phase, regions) - break - - default: - throw new Error(`Unknown phase type: ${phase.type}`) - } - - return { - phaseId: phase.id, - status: 'success', - startedAt: startTime, - completedAt: new Date(), - results: phaseResults, - errors: phaseErrors, - } - } catch (error: unknown) { - logger.error(`Phase execution failed: ${phase.id}`, { error }) - - return { - phaseId: phase.id, - status: 'failed', - startedAt: startTime, - completedAt: new Date(), - results: [], - errors: [ - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - ], - } - } - } - - /** - * Deploy infrastructure - */ - private async deployInfrastructure( - phase: DeploymentPhase, - regions: RegionConfig[], - ): Promise { - try { - logger.info(`Deploying infrastructure for phase: ${phase.id}`, { - regions: regions.length, - }) - - const deploymentPromises = regions.map(async (region) => - this.cloudProviderManager.deployRegion(region), - ) - - const results = await Promise.allSettled(deploymentPromises) - - const successfulResults = results - .filter((result) => result.status === 'fulfilled') - .map((result) => result.value) - - const failedResults = results - .filter((result) => result.status === 'rejected') - .map((result) => result.reason) - - if (failedResults.length > 0) { - logger.warn(`Some infrastructure deployments failed`, { - failedCount: failedResults.length, - }) - // Don't throw here, let the caller handle partial failures - } - - return successfulResults - } catch (error: unknown) { - logger.error('Infrastructure deployment failed', { error }) - throw error - } - } - - /** - * Deploy services - */ - private async deployServices( - phase: DeploymentPhase, - regions: RegionConfig[], - ): Promise[]> { - try { - logger.info(`Deploying services for phase: ${phase.id}`, { - regions: regions.length, - }) - - // Simulate service deployment - // In a real implementation, this would deploy containers, serverless functions, etc. - const serviceDeploymentPromises = regions.map(async (region) => { - logger.info(`Deploying services to region: ${region.name}`) - - // Simulate deployment delay - await new Promise((resolve) => - setTimeout(resolve, 5000 + Math.random() * 10000), - ) - - return { - regionId: region.id, - services: ['api-gateway', 'core-services', 'ai-services'], - status: 'deployed', - timestamp: new Date(), - } - }) - - const results = await Promise.allSettled(serviceDeploymentPromises) - - return results - .filter((result) => result.status === 'fulfilled') - .map( - (result) => - (result as PromiseFulfilledResult>).value, - ) - } catch (error: unknown) { - logger.error('Service deployment failed', { error }) - throw error - } - } - - /** - * Setup monitoring - */ - private async setupMonitoring( - phase: DeploymentPhase, - regions: RegionConfig[], - ): Promise[]> { - try { - logger.info(`Setting up monitoring for phase: ${phase.id}`, { - regions: regions.length, - }) - - // Simulate monitoring setup - const monitoringSetupPromises = regions.map(async (region) => { - logger.info(`Setting up monitoring for region: ${region.name}`) - - // Simulate setup delay - await new Promise((resolve) => - setTimeout(resolve, 2000 + Math.random() * 3000), - ) - - return { - regionId: region.id, - monitoring: { - metrics: 'enabled', - alerts: 'configured', - dashboards: 'created', - }, - timestamp: new Date(), - } - }) - - const results = await Promise.allSettled(monitoringSetupPromises) - - return results - .filter((result) => result.status === 'fulfilled') - .map( - (result) => - (result as PromiseFulfilledResult>).value, - ) - } catch (error: unknown) { - logger.error('Monitoring setup failed', { error }) - throw error - } - } - - /** - * Perform phase validation - */ - private async performPhaseValidation( - phase: DeploymentPhase, - regions: RegionConfig[], - ): Promise[]> { - try { - logger.info(`Performing phase validation for: ${phase.id}`, { - regions: regions.length, - }) - - // Simulate validation - const validationResults = regions.map((region) => ({ - regionId: region.id, - validation: { - healthCheck: 'passed', - connectivity: 'verified', - performance: 'acceptable', - }, - timestamp: new Date(), - })) - - // Simulate some validation failures randomly - if (Math.random() < 0.1) { - // 10% failure rate for simulation - throw new Error('Phase validation failed - simulated failure') - } - - return validationResults - } catch (error: unknown) { - logger.error('Phase validation failed', { error }) - throw error - } - } - - /** - * Check phase dependencies - */ - private async checkPhaseDependencies( - phase: DeploymentPhase, - execution: DeploymentExecution, - ): Promise { - if (phase.dependencies.length === 0) { - return true - } - - for (const dependencyId of phase.dependencies) { - const dependencyResult = execution.results.find( - (r) => r.phaseId === dependencyId, - ) - - if (dependencyResult?.status !== 'success') { - logger.warn( - `Phase dependency not met: ${dependencyId} for phase: ${phase.id}`, - ) - return false - } - } - - return true - } - - /** - * Create rollback point - */ - private async createRollbackPoint( - execution: DeploymentExecution, - phase: DeploymentPhase, - ): Promise { - try { - const rollbackPoint: RollbackPoint = { - id: `rollback-${execution.id}-${phase.id}`, - name: `Rollback point for ${phase.name}`, - phaseId: phase.id, - snapshot: { - executionId: execution.id, - phaseId: phase.id, - timestamp: new Date(), - // In a real implementation, this would include actual system state - state: 'deployment-state-snapshot', - }, - createdAt: new Date(), - } - - this.rollbackPoints.set(rollbackPoint.id, rollbackPoint) - execution.rollbackPoint = rollbackPoint.id - - logger.info( - `Created rollback point: ${rollbackPoint.id} for phase: ${phase.id}`, - ) - } catch (error: unknown) { - logger.error(`Failed to create rollback point for phase: ${phase.id}`, { - error, - }) - // Don't fail the deployment if rollback point creation fails - } - } - - /** - * Perform rollback - */ - private async performRollback(execution: DeploymentExecution): Promise { - try { - logger.info(`Performing rollback for execution: ${execution.id}`) - - if (!execution.rollbackPoint) { - logger.warn('No rollback point available for execution') - return - } - - const rollbackPoint = this.rollbackPoints.get(execution.rollbackPoint) - if (!rollbackPoint) { - logger.error('Rollback point not found') - return - } - - // Perform rollback operations - // In a real implementation, this would restore system state - logger.info(`Rolling back to point: ${rollbackPoint.id}`) - - // Simulate rollback delay - await new Promise((resolve) => setTimeout(resolve, 5000)) - - logger.info('Rollback completed successfully') - this.emit('rollback-completed', { - executionId: execution.id, - rollbackPointId: rollbackPoint.id, - }) - } catch (error: unknown) { - logger.error('Rollback failed', { error }) - this.emit('rollback-failed', { - executionId: execution.id, - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - }) - throw error - } - } - - /** - * Rollback specific phase - */ - private async rollbackPhase( - execution: DeploymentExecution, - phase: DeploymentPhase, - ): Promise { - try { - logger.info( - `Rolling back phase: ${phase.id} for execution: ${execution.id}`, - ) - - // Find the rollback point for this phase - const rollbackPoint = Array.from(this.rollbackPoints.values()).find( - (rp) => rp.phaseId === phase.id && rp.id.includes(execution.id), - ) - - if (!rollbackPoint) { - logger.warn(`No rollback point found for phase: ${phase.id}`) - return - } - - // Perform phase-specific rollback - // In a real implementation, this would undo the phase's changes - logger.info(`Rolling back phase: ${phase.name}`) - - // Simulate rollback operations - await new Promise((resolve) => setTimeout(resolve, 3000)) - - logger.info(`Phase rollback completed: ${phase.id}`) - } catch (error: unknown) { - logger.error(`Phase rollback failed: ${phase.id}`, { error }) - throw error - } - } - - /** - * Perform deployment validation - */ - private async performValidation( - execution: DeploymentExecution, - plan: DeploymentPlan, - ): Promise<{ success: boolean; errors: string[] }> { - try { - logger.info( - `Performing deployment validation for execution: ${execution.id}`, - ) - - const validationResults: string[] = [] - let validationFailed = false - - for (const validationStep of plan.validationSteps) { - logger.info(`Executing validation step: ${validationStep.name}`) - - try { - const result = await this.executeValidationStep( - validationStep, - execution, - ) - - if (result.success) { - validationResults.push(`✓ ${validationStep.name}`) - } else { - validationResults.push(`✗ ${validationStep.name}: ${result.error}`) - validationFailed = true - } - } catch (error: unknown) { - validationResults.push( - `✗ ${validationStep.name}: ${error instanceof Error ? (error instanceof Error ? error.message : 'Unknown error') : 'Unknown error'}`, - ) - validationFailed = true - } - } - - const result = { - success: !validationFailed, - errors: validationFailed - ? validationResults.filter((r) => r.startsWith('✗')) - : [], - } - - logger.info( - `Deployment validation completed: ${result.success ? 'PASSED' : 'FAILED'}`, - ) - this.emit('validation-completed', { - executionId: execution.id, - success: result.success, - }) - - return result - } catch (error: unknown) { - logger.error('Deployment validation failed', { error }) - return { - success: false, - errors: [ - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - ], - } - } - } - - /** - * Execute validation step - */ - private async executeValidationStep( - step: ValidationStep, - execution: DeploymentExecution, - ): Promise<{ success: boolean; error?: string }> { - try { - switch (step.type) { - case 'health-check': - return await this.validateHealthCheck(step, execution) - - case 'performance-test': - return await this.validatePerformanceTest(step, execution) - - case 'security-scan': - return await this.validateSecurityScan(step, execution) - - case 'compliance-check': - return await this.validateComplianceCheck(step, execution) - - default: - throw new Error(`Unknown validation step type: ${step.type}`) - } - } catch (error: unknown) { - return { - success: false, - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - } - } - } - - /** - * Validate health check - */ - private async validateHealthCheck( - step: ValidationStep, - _execution: DeploymentExecution, - ): Promise<{ success: boolean; error?: string }> { - try { - // In a real implementation, this would check health scores from HealthMonitor - const minHealthScore = - (typeof step.successCriteria['minHealthScore'] === 'number' - ? step.successCriteria['minHealthScore'] - : undefined) ?? 80 - - // Simulate health check validation - const simulatedHealthScore = 85 + Math.random() * 10 // 85-95 range - - if (simulatedHealthScore >= minHealthScore) { - return { success: true } - } else { - return { - success: false, - error: `Health score ${simulatedHealthScore.toFixed(1)} below minimum ${minHealthScore}`, - } - } - } catch (error: unknown) { - return { - success: false, - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - } - } - } - - /** - * Validate performance test - */ - private async validatePerformanceTest( - step: ValidationStep, - _execution: DeploymentExecution, - ): Promise<{ success: boolean; error?: string }> { - try { - // In a real implementation, this would run actual performance tests - const maxResponseTime = - (typeof step.successCriteria['maxResponseTime'] === 'number' - ? step.successCriteria['maxResponseTime'] - : undefined) ?? 200 - const minThroughput = - (typeof step.successCriteria['minThroughput'] === 'number' - ? step.successCriteria['minThroughput'] - : undefined) ?? 100 - - // Simulate performance test results - const responseTime = 150 + Math.random() * 50 // 150-200ms range - const throughput = 120 + Math.random() * 30 // 120-150 range - - if (responseTime <= maxResponseTime && throughput >= minThroughput) { - return { success: true } - } else { - const errors: string[] = [] - if (responseTime > maxResponseTime) { - errors.push( - `Response time ${responseTime.toFixed(0)}ms exceeds maximum ${maxResponseTime}ms`, - ) - } - if (throughput < minThroughput) { - errors.push( - `Throughput ${throughput.toFixed(0)} below minimum ${minThroughput}`, - ) - } - return { success: false, error: errors.join(', ') } - } - } catch (error: unknown) { - return { - success: false, - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - } - } - } - - /** - * Validate security scan - */ - private async validateSecurityScan( - _step: ValidationStep, - _execution: DeploymentExecution, - ): Promise<{ success: boolean; error?: string }> { - try { - // In a real implementation, this would run security scanning tools - // Simulate security scan (95% success rate) - const securityScore = Math.random() * 100 - - if (securityScore > 80) { - // 80+ is considered passing - return { success: true } - } else { - return { - success: false, - error: `Security scan score ${securityScore.toFixed(1)} below threshold`, - } - } - } catch (error: unknown) { - return { - success: false, - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - } - } - } - - /** - * Validate compliance check - */ - private async validateComplianceCheck( - _step: ValidationStep, - _execution: DeploymentExecution, - ): Promise<{ success: boolean; error?: string }> { - try { - // In a real implementation, this would run compliance checks - // Simulate compliance check (90% success rate) - const compliancePassed = Math.random() > 0.1 - - if (compliancePassed) { - return { success: true } - } else { - return { success: false, error: 'Compliance requirements not met' } - } - } catch (error: unknown) { - return { - success: false, - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - } - } - } - - /** - * Get deployment plan - */ - getDeploymentPlan(planId: string): DeploymentPlan | undefined { - return this.deploymentPlans.get(planId) - } - - /** - * Get all deployment plans - */ - getAllDeploymentPlans(): DeploymentPlan[] { - return Array.from(this.deploymentPlans.values()) - } - - /** - * Get active executions - */ - getActiveExecutions(): DeploymentExecution[] { - return Array.from(this.activeExecutions.values()) - } - - /** - * Get execution by ID - */ - getExecution(executionId: string): DeploymentExecution | undefined { - return this.activeExecutions.get(executionId) - } - - /** - * Get deployment statistics - */ - getDeploymentStatistics(): { - totalPlans: number - activeExecutions: number - completedExecutions: number - failedExecutions: number - rollbackPoints: number - } { - const executions = Array.from(this.activeExecutions.values()) - - return { - totalPlans: this.deploymentPlans.size, - activeExecutions: executions.filter((e) => e.status === 'running').length, - completedExecutions: executions.filter((e) => e.status === 'completed') - .length, - failedExecutions: executions.filter((e) => e.status === 'failed').length, - rollbackPoints: this.rollbackPoints.size, - } - } - - /** - * Create custom deployment plan - */ - async createDeploymentPlan(plan: DeploymentPlan): Promise { - try { - // Validate plan structure - await this.validateDeploymentPlan(plan) - - this.deploymentPlans.set(plan.id, plan) - - logger.info(`Created custom deployment plan: ${plan.name}`, { - planId: plan.id, - }) - this.emit('plan-created', { planId: plan.id, name: plan.name }) - } catch (error: unknown) { - logger.error('Failed to create deployment plan', { error }) - throw error - } - } - - /** - * Validate deployment plan - */ - private async validateDeploymentPlan(plan: DeploymentPlan): Promise { - const errors: string[] = [] - - if (!plan.id || !plan.name) { - errors.push('Plan must have id and name') - } - - if (!plan.phases || plan.phases.length === 0) { - errors.push('Plan must have at least one phase') - } - - // Validate phases - for (const phase of plan.phases) { - if (!phase.id || !phase.name) { - errors.push(`Phase must have id and name: ${JSON.stringify(phase)}`) - } - - if ( - !['infrastructure', 'services', 'monitoring', 'validation'].includes( - phase.type, - ) - ) { - errors.push(`Invalid phase type: ${phase.type}`) - } - - if (phase.timeout < 60000) { - // 1 minute minimum - errors.push(`Phase timeout too short: ${phase.timeout}ms`) - } - } - - // Validate dependencies - const phaseIds = plan.phases.map((p) => p.id) - for (const phase of plan.phases) { - for (const dep of phase.dependencies) { - if (!phaseIds.includes(dep)) { - errors.push(`Phase dependency not found: ${dep} in phase ${phase.id}`) - } - } - } - - if (errors.length > 0) { - throw new Error(`Deployment plan validation failed: ${errors.join(', ')}`) - } - } - - /** - * Cleanup resources - */ - async cleanup(): Promise { - try { - logger.info('Cleaning up Deployment Orchestrator') - - // Cancel active executions - for (const execution of this.activeExecutions.values()) { - if (execution.status === 'running') { - execution.status = 'failed' - execution.errors.push('Orchestrator cleanup initiated') - execution.completedAt = new Date() - } - } - - this.activeExecutions.clear() - this.deploymentPlans.clear() - this.rollbackPoints.clear() - this.isInitialized = false - - logger.info('Deployment Orchestrator cleanup completed') - } catch (error: unknown) { - logger.error('Deployment Orchestrator cleanup failed', { error }) - throw error - } - } -} - -export default DeploymentOrchestrator diff --git a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.codegen.ts b/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.codegen.ts deleted file mode 100644 index 59742ce662..0000000000 --- a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.codegen.ts +++ /dev/null @@ -1,474 +0,0 @@ -/** - * EdgeComputingManager.codegen.ts - * - * Code generation functions for edge deployment scripts. - * Extracted from EdgeComputingManager.ts for modularity. - */ - -import type { EdgeLocation } from './EdgeComputingManager.types' - -/** - * Generate Cloudflare Worker script - */ -export function generateWorkerScript(location: EdgeLocation): string { - return ` -// Pixelated Edge Worker - ${location.name} -// Generated on ${new Date().toISOString()} - -import { Ai } from '@cloudflare/ai'; - -export default { - async fetch(request, env, ctx) { - const ai = new Ai(env.AI); - const url = new URL(request.url); - const country = request.cf.country; - const colo = request.cf.colo; - - // Threat detection at edge - const threatCheck = await this.detectThreats(request, ai); - if (threatCheck.blocked) { - return new Response('Access Denied', { status: 403 }); - } - - // Bias detection for AI responses - const biasCheck = await this.detectBias(request, ai); - if (biasCheck.hasBias) { - logger.warn('Bias detected in request:', biasCheck.details); - } - - // Cache lookup - const cacheKey = new Request(url.toString(), request); - const cache = caches.default; - let response = await cache.match(cacheKey); - - if (!response) { - // Forward to origin if not in cache - response = await fetch(request); - - // Cache successful responses - if (response.ok) { - ctx.waitUntil(cache.put(cacheKey, response.clone())); - } - } - - // Add edge headers - response = new Response(response.body, response); - response.headers.set('X-Edge-Location', colo); - response.headers.set('X-Threat-Score', threatCheck.score.toString()); - response.headers.set('X-Bias-Score', biasCheck.score.toString()); - response.headers.set('X-Cache-Status', response.headers.get('X-Cache-Status') || 'HIT'); - - return response; - }, - - async detectThreats(request, ai) { - try { - const input = { - text: request.headers.get('User-Agent') || '', - source: 'edge-detection' - }; - - const result = await ai.run('@cf/meta/llama-2-7b-chat-int8', { - messages: [ - { role: 'system', content: 'Detect security threats in web requests. Return JSON with blocked (boolean), score (0-1), and reason.' }, - { role: 'user', content: JSON.stringify(input) } - ] - }); - - const response = JSON.parse(result.response); - return { - blocked: response.blocked || false, - score: response.score || 0, - reason: response.reason || 'No threat detected' - }; - } catch (error: unknown) { - logger.error('Threat detection error:', error); - return { blocked: false, score: 0, reason: 'Detection failed' }; - } - }, - - async detectBias(request, ai) { - try { - const input = { - text: request.headers.get('User-Agent') || '', - context: 'web-request' - }; - - const result = await ai.run('@cf/meta/llama-guard-7b-awq', { - messages: [ - { role: 'user', content: JSON.stringify(input) } - ] - }); - - return { - hasBias: result.unsafe || false, - score: result.safety_score || 0, - details: result.categories || [] - }; - } catch (error: unknown) { - logger.error('Bias detection error:', error); - return { hasBias: false, score: 0, details: [] }; - } - } -}; - `.trim() -} - -/** - * Generate AWS Lambda function - */ -export function generateLambdaFunction(location: EdgeLocation): string { - return ` -// Pixelated Edge Lambda - ${location.name} -// Generated on ${new Date().toISOString()} - -const AWS = require('aws-sdk'); -const axios = require('axios'); - -exports.handler = async (event, context) => { - const request = event.Records[0].cf.request; - const headers = request.headers; - const country = headers['cloudfront-viewer-country']?.[0]?.value; - const userAgent = headers['user-agent']?.[0]?.value; - - try { - // Threat detection - const threatCheck = await detectThreats(userAgent, country); - if (threatCheck.blocked) { - return { - status: '403', - statusDescription: 'Forbidden', - body: 'Access Denied', - headers: { - 'content-type': [{ key: 'Content-Type', value: 'text/plain' }] - } - }; - } - - // Bias detection - const biasCheck = await detectBias(userAgent, country); - if (biasCheck.hasBias) { - logger.warn('Bias detected:', biasCheck.details); - } - - // Cache key generation - const cacheKey = generateCacheKey(request); - - // Try cache first - const cachedResponse = await checkCache(cacheKey); - if (cachedResponse) { - return addEdgeHeaders(cachedResponse, location, threatCheck, biasCheck, 'HIT'); - } - - // Forward to origin - const originResponse = await forwardToOrigin(request); - - // Cache successful responses - if (originResponse.status === '200') { - await cacheResponse(cacheKey, originResponse); - } - - return addEdgeHeaders(originResponse, location, threatCheck, biasCheck, 'MISS'); - } catch (error: unknown) { - logger.error('Edge processing error:', error); - return { - status: '500', - statusDescription: 'Internal Server Error', - body: 'Edge processing failed', - headers: { - 'content-type': [{ key: 'Content-Type', value: 'text/plain' }] - } - }; - } -}; - -async function detectThreats(userAgent, country) { - try { - // Implement threat detection logic - const threatScore = Math.random() * 0.1; // Low threat probability - return { - blocked: threatScore > 0.8, - score: threatScore, - reason: threatScore > 0.8 ? 'Suspicious user agent pattern' : 'Clean' - }; - } catch (error: unknown) { - logger.error('Threat detection error:', error); - return { blocked: false, score: 0, reason: 'Detection failed' }; - } -} - -async function detectBias(userAgent, country) { - try { - // Implement bias detection logic - const biasScore = Math.random() * 0.05; // Very low bias probability - return { - hasBias: biasScore > 0.8, - score: biasScore, - details: biasScore > 0.8 ? ['potential_bias'] : [] - }; - } catch (error: unknown) { - logger.error('Bias detection error:', error); - return { hasBias: false, score: 0, details: [] }; - } -} - -function generateCacheKey(request) { - return \`\${request.uri}:\${JSON.stringify(request.querystring)}\`; -} - -async function checkCache(cacheKey) { - // Implement cache checking logic - return null; // Cache miss for now -} - -async function forwardToOrigin(request) { - // Implement origin forwarding logic - return { - status: '200', - statusDescription: 'OK', - body: 'Origin response', - headers: { - 'content-type': [{ key: 'Content-Type', value: 'text/plain' }] - } - }; -} - -async function cacheResponse(cacheKey, response) { - // Implement response caching logic -} - -function addEdgeHeaders(response, location, threatCheck, biasCheck, cacheStatus) { - response.headers['x-edge-location'] = [{ key: 'X-Edge-Location', value: location.id }]; - response.headers['x-threat-score'] = [{ key: 'X-Threat-Score', value: threatCheck.score.toString() }]; - response.headers['x-bias-score'] = [{ key: 'X-Bias-Score', value: biasCheck.score.toString() }]; - response.headers['x-cache-status'] = [{ key: 'X-Cache-Status', value: cacheStatus }]; - return response; -} - `.trim() -} - -/** - * Generate Azure Function - */ -export function generateAzureFunction(location: EdgeLocation): string { - return ` -// Pixelated Edge Azure Function - ${location.name} -// Generated on ${new Date().toISOString()} - -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; - -public static class EdgeFunction -{ - [FunctionName("EdgeFunction_${location.id}")] - public static async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, - ILogger log) - { - log.LogInformation("Processing edge request for location: ${location.name}"); - - try - { - // Get request context - var country = req.Headers["X-Forwarded-For"].ToString(); - var userAgent = req.Headers["User-Agent"].ToString(); - - // Threat detection - var threatCheck = await DetectThreats(userAgent, country); - if (threatCheck.Blocked) - { - return new ContentResult - { - StatusCode = 403, - Content = "Access Denied", - ContentType = "text/plain" - }; - } - - // Bias detection - var biasCheck = await DetectBias(userAgent, country); - if (biasCheck.HasBias) - { - log.LogWarning($"Bias detected: {string.Join(", ", biasCheck.Details)}"); - } - - // Process request - var response = await ProcessRequest(req, threatCheck, biasCheck); - - // Add edge headers - response.Headers.Add("X-Edge-Location", "${location.id}"); - response.Headers.Add("X-Threat-Score", threatCheck.Score.ToString()); - response.Headers.Add("X-Bias-Score", biasCheck.Score.ToString()); - - return response; - } - catch (Exception ex) - { - log.LogError(ex, "Edge processing error"); - return new ContentResult - { - StatusCode = 500, - Content = "Edge processing failed", - ContentType = "text/plain" - }; - } - } - - private static async Task DetectThreats(string userAgent, string country) - { - try - { - // Implement threat detection logic - var threatScore = new Random().NextDouble() * 0.1; - return new ThreatCheckResult - { - Blocked = threatScore > 0.8, - Score = threatScore, - Reason = threatScore > 0.8 ? "Suspicious pattern detected" : "Clean" - }; - } - catch (Exception ex) - { - Console.WriteLine($"Threat detection error: {ex.Message}"); - return new ThreatCheckResult { Blocked = false, Score = 0, Reason = "Detection failed" }; - } - } - - private static async Task DetectBias(string userAgent, string country) - { - try - { - // Implement bias detection logic - var biasScore = new Random().NextDouble() * 0.05; - return new BiasCheckResult - { - HasBias = biasScore > 0.8, - Score = biasScore, - Details = biasScore > 0.8 ? new List { "potential_bias" } : new List() - }; - } - catch (Exception ex) - { - Console.WriteLine($"Bias detection error: {ex.Message}"); - return new BiasCheckResult { HasBias = false, Score = 0, Details = new List() }; - } - } - - private static async Task ProcessRequest(HttpRequest req, ThreatCheckResult threatCheck, BiasCheckResult biasCheck) - { - // Implement request processing logic - return new OkObjectResult(new { message = "Request processed successfully", threatCheck, biasCheck }); - } -} - -public class ThreatCheckResult -{ - public bool Blocked { get; set; } - public double Score { get; set; } - public string Reason { get; set; } -} - -public class BiasCheckResult -{ - public bool HasBias { get; set; } - public double Score { get; set; } - public List Details { get; set; } -} - `.trim() -} - -/** - * Generate GCP Cloud Function - */ -export function generateGCPFunction(location: EdgeLocation): string { - return ` -// Pixelated Edge GCP Function - ${location.name} -// Generated on ${new Date().toISOString()} - -const functions = require('@google-cloud/functions-framework'); -const axios = require('axios'); - -// Register HTTP function -functions.http('edgeFunction_${location.id}', async (req, res) => { - logger.info(\`Processing edge request for location: ${location.name}\`); - - try { - const userAgent = req.get('User-Agent'); - const country = req.get('X-Forwarded-For'); - - // Threat detection - const threatCheck = await detectThreats(userAgent, country); - if (threatCheck.blocked) { - return res.status(403).json({ error: 'Access Denied' }); - } - - // Bias detection - const biasCheck = await detectBias(userAgent, country); - if (biasCheck.hasBias) { - logger.warn('Bias detected:', biasCheck.details); - } - - // Process request - const result = await processRequest(req, threatCheck, biasCheck); - - // Add edge headers - res.set('X-Edge-Location', '${location.id}'); - res.set('X-Threat-Score', threatCheck.score.toString()); - res.set('X-Bias-Score', biasCheck.score.toString()); - - res.status(200).json(result); - } catch (error: unknown) { - logger.error('Edge processing error:', error); - res.status(500).json({ error: 'Edge processing failed' }); - } -}); - -async function detectThreats(userAgent, country) { - try { - // Implement threat detection logic - const threatScore = Math.random() * 0.1; - return { - blocked: threatScore > 0.8, - score: threatScore, - reason: threatScore > 0.8 ? 'Suspicious pattern detected' : 'Clean' - }; - } catch (error: unknown) { - logger.error('Threat detection error:', error); - return { blocked: false, score: 0, reason: 'Detection failed' }; - } -} - -async function detectBias(userAgent, country) { - try { - // Implement bias detection logic - const biasScore = Math.random() * 0.05; - return { - hasBias: biasScore > 0.8, - score: biasScore, - details: biasScore > 0.8 ? ['potential_bias'] : [] - }; - } catch (error: unknown) { - logger.error('Bias detection error:', error); - return { hasBias: false, score: 0, details: [] }; - } -} - -async function processRequest(req, threatCheck, biasCheck) { - // Implement request processing logic - return { - message: 'Request processed successfully', - location: '${location.name}', - threatCheck, - biasCheck, - timestamp: new Date().toISOString() - }; -} - `.trim() -} diff --git a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-aws.ts b/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-aws.ts deleted file mode 100644 index 7bcf82267d..0000000000 --- a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-aws.ts +++ /dev/null @@ -1,275 +0,0 @@ -/** - * Edge Computing Manager - AWS Edge Locations - * - * Predefined AWS edge locations covering North America, Europe, - * Asia Pacific, South America, Middle East & Africa. - */ - -import type { EdgeLocation } from './EdgeComputingManager.types' - -export const AWS_EDGE_LOCATIONS: EdgeLocation[] = [ - // North America - { - id: 'edge-us-east-1', - name: 'US East (N. Virginia)', - city: 'Ashburn', - country: 'USA', - continent: 'North America', - coordinates: { latitude: 39.0438, longitude: -77.4874 }, - provider: 'aws', - region: 'us-east-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 5, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-us-west-2', - name: 'US West (Oregon)', - city: 'Boardman', - country: 'USA', - continent: 'North America', - coordinates: { latitude: 45.8399, longitude: -119.7006 }, - provider: 'aws', - region: 'us-west-2', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 8, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-ca-central-1', - name: 'Canada (Central)', - city: 'Montreal', - country: 'Canada', - continent: 'North America', - coordinates: { latitude: 45.5017, longitude: -73.5673 }, - provider: 'aws', - region: 'ca-central-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 15, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - - // Europe - { - id: 'edge-eu-west-1', - name: 'EU West (Ireland)', - city: 'Dublin', - country: 'Ireland', - continent: 'Europe', - coordinates: { latitude: 53.3498, longitude: -6.2603 }, - provider: 'aws', - region: 'eu-west-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 10, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-eu-central-1', - name: 'EU Central (Frankfurt)', - city: 'Frankfurt', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.1109, longitude: 8.6821 }, - provider: 'aws', - region: 'eu-central-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 8, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-eu-north-1', - name: 'EU North (Stockholm)', - city: 'Stockholm', - country: 'Sweden', - continent: 'Europe', - coordinates: { latitude: 59.3293, longitude: 18.0686 }, - provider: 'aws', - region: 'eu-north-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 20, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-eu-west-3', - name: 'EU West (Paris)', - city: 'Paris', - country: 'France', - continent: 'Europe', - coordinates: { latitude: 48.8566, longitude: 2.3522 }, - provider: 'aws', - region: 'eu-west-3', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 12, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-eu-south-1', - name: 'EU South (Milan)', - city: 'Milan', - country: 'Italy', - continent: 'Europe', - coordinates: { latitude: 45.4642, longitude: 9.19 }, - provider: 'aws', - region: 'eu-south-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 18, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - - // Asia Pacific - { - id: 'edge-ap-southeast-1', - name: 'AP Southeast (Singapore)', - city: 'Singapore', - country: 'Singapore', - continent: 'Asia', - coordinates: { latitude: 1.3521, longitude: 103.8198 }, - provider: 'aws', - region: 'ap-southeast-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 5, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-ap-northeast-1', - name: 'AP Northeast (Tokyo)', - city: 'Tokyo', - country: 'Japan', - continent: 'Asia', - coordinates: { latitude: 35.6762, longitude: 139.6503 }, - provider: 'aws', - region: 'ap-northeast-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 8, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-ap-southeast-2', - name: 'AP Southeast (Sydney)', - city: 'Sydney', - country: 'Australia', - continent: 'Oceania', - coordinates: { latitude: -33.8688, longitude: 151.2093 }, - provider: 'aws', - region: 'ap-southeast-2', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 15, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-ap-northeast-2', - name: 'AP Northeast (Seoul)', - city: 'Seoul', - country: 'South Korea', - continent: 'Asia', - coordinates: { latitude: 37.5665, longitude: 126.978 }, - provider: 'aws', - region: 'ap-northeast-2', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 12, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-ap-south-1', - name: 'AP South (Mumbai)', - city: 'Mumbai', - country: 'India', - continent: 'Asia', - coordinates: { latitude: 19.076, longitude: 72.8777 }, - provider: 'aws', - region: 'ap-south-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 25, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-ap-east-1', - name: 'AP East (Hong Kong)', - city: 'Hong Kong', - country: 'Hong Kong', - continent: 'Asia', - coordinates: { latitude: 22.3193, longitude: 114.1694 }, - provider: 'aws', - region: 'ap-east-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 10, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - - // South America - { - id: 'edge-sa-east-1', - name: 'SA East (São Paulo)', - city: 'São Paulo', - country: 'Brazil', - continent: 'South America', - coordinates: { latitude: -23.5505, longitude: -46.6333 }, - provider: 'aws', - region: 'sa-east-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 30, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - - // Middle East & Africa - { - id: 'edge-me-south-1', - name: 'ME South (Bahrain)', - city: 'Manama', - country: 'Bahrain', - continent: 'Asia', - coordinates: { latitude: 26.0667, longitude: 50.5577 }, - provider: 'aws', - region: 'me-south-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 35, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-af-south-1', - name: 'AF South (Cape Town)', - city: 'Cape Town', - country: 'South Africa', - continent: 'Africa', - coordinates: { latitude: -33.9249, longitude: 18.4241 }, - provider: 'aws', - region: 'af-south-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 40, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, -] diff --git a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-cloudflare-1.ts b/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-cloudflare-1.ts deleted file mode 100644 index b3a6a4af06..0000000000 --- a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-cloudflare-1.ts +++ /dev/null @@ -1,470 +0,0 @@ -/** - * Edge Computing Manager - Cloudflare Edge Locations (Part 1) - * - * Cloudflare edge locations: Amsterdam through Milan. - */ - -import type { EdgeLocation } from './EdgeComputingManager.types' - -export const CLOUDFLARE_EDGE_LOCATIONS_1: EdgeLocation[] = [ - { - id: 'edge-cf-amsterdam', - name: 'Cloudflare Amsterdam', - city: 'Amsterdam', - country: 'Netherlands', - continent: 'Europe', - coordinates: { latitude: 52.3676, longitude: 4.9041 }, - provider: 'cloudflare', - region: 'europe-west4', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-london', - name: 'Cloudflare London', - city: 'London', - country: 'United Kingdom', - continent: 'Europe', - coordinates: { latitude: 51.5074, longitude: -0.1278 }, - provider: 'cloudflare', - region: 'europe-west2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-san-jose', - name: 'Cloudflare San Jose', - city: 'San Jose', - country: 'USA', - continent: 'North America', - coordinates: { latitude: 37.3382, longitude: -121.8863 }, - provider: 'cloudflare', - region: 'us-west1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 2, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-atlanta', - name: 'Cloudflare Atlanta', - city: 'Atlanta', - country: 'USA', - continent: 'North America', - coordinates: { latitude: 33.749, longitude: -84.388 }, - provider: 'cloudflare', - region: 'us-east1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 2, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-tokyo', - name: 'Cloudflare Tokyo', - city: 'Tokyo', - country: 'Japan', - continent: 'Asia', - coordinates: { latitude: 35.6762, longitude: 139.6503 }, - provider: 'cloudflare', - region: 'asia-northeast1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-sydney', - name: 'Cloudflare Sydney', - city: 'Sydney', - country: 'Australia', - continent: 'Oceania', - coordinates: { latitude: -33.8688, longitude: 151.2093 }, - provider: 'cloudflare', - region: 'australia-southeast1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-singapore', - name: 'Cloudflare Singapore', - city: 'Singapore', - country: 'Singapore', - continent: 'Asia', - coordinates: { latitude: 1.3521, longitude: 103.8198 }, - provider: 'cloudflare', - region: 'asia-southeast1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 2, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-mumbai', - name: 'Cloudflare Mumbai', - city: 'Mumbai', - country: 'India', - continent: 'Asia', - coordinates: { latitude: 19.076, longitude: 72.8777 }, - provider: 'cloudflare', - region: 'asia-south1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 5, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-sao-paulo', - name: 'Cloudflare São Paulo', - city: 'São Paulo', - country: 'Brazil', - continent: 'South America', - coordinates: { latitude: -23.5505, longitude: -46.6333 }, - provider: 'cloudflare', - region: 'southamerica-east1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 5, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-johannesburg', - name: 'Cloudflare Johannesburg', - city: 'Johannesburg', - country: 'South Africa', - continent: 'Africa', - coordinates: { latitude: -26.2041, longitude: 28.0473 }, - provider: 'cloudflare', - region: 'africa-south1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 8, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-dubai', - name: 'Cloudflare Dubai', - city: 'Dubai', - country: 'UAE', - continent: 'Asia', - coordinates: { latitude: 25.2048, longitude: 55.2708 }, - provider: 'cloudflare', - region: 'me-central1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 5, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-tel-aviv', - name: 'Cloudflare Tel Aviv', - city: 'Tel Aviv', - country: 'Israel', - continent: 'Asia', - coordinates: { latitude: 32.0853, longitude: 34.7818 }, - provider: 'cloudflare', - region: 'me-west1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-mexico-city', - name: 'Cloudflare Mexico City', - city: 'Mexico City', - country: 'Mexico', - continent: 'North America', - coordinates: { latitude: 19.4326, longitude: -99.1332 }, - provider: 'cloudflare', - region: 'northamerica-south1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 5, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-toronto', - name: 'Cloudflare Toronto', - city: 'Toronto', - country: 'Canada', - continent: 'North America', - coordinates: { latitude: 43.6532, longitude: -79.3832 }, - provider: 'cloudflare', - region: 'northamerica-northeast1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-stockholm', - name: 'Cloudflare Stockholm', - city: 'Stockholm', - country: 'Sweden', - continent: 'Europe', - coordinates: { latitude: 59.3293, longitude: 18.0686 }, - provider: 'cloudflare', - region: 'europe-north1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-warsaw', - name: 'Cloudflare Warsaw', - city: 'Warsaw', - country: 'Poland', - continent: 'Europe', - coordinates: { latitude: 52.2297, longitude: 21.0122 }, - provider: 'cloudflare', - region: 'europe-central1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-vienna', - name: 'Cloudflare Vienna', - city: 'Vienna', - country: 'Austria', - continent: 'Europe', - coordinates: { latitude: 48.2082, longitude: 16.3738 }, - provider: 'cloudflare', - region: 'europe-central2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-brussels', - name: 'Cloudflare Brussels', - city: 'Brussels', - country: 'Belgium', - continent: 'Europe', - coordinates: { latitude: 50.8503, longitude: 4.3517 }, - provider: 'cloudflare', - region: 'europe-west1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-zurich', - name: 'Cloudflare Zurich', - city: 'Zurich', - country: 'Switzerland', - continent: 'Europe', - coordinates: { latitude: 47.3769, longitude: 8.5417 }, - provider: 'cloudflare', - region: 'europe-west6', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-milan', - name: 'Cloudflare Milan', - city: 'Milan', - country: 'Italy', - continent: 'Europe', - coordinates: { latitude: 45.4642, longitude: 9.19 }, - provider: 'cloudflare', - region: 'europe-west8', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, -] diff --git a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-cloudflare-2.ts b/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-cloudflare-2.ts deleted file mode 100644 index 72646d8ae6..0000000000 --- a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations-cloudflare-2.ts +++ /dev/null @@ -1,447 +0,0 @@ -/** - * Edge Computing Manager - Cloudflare Edge Locations (Part 2) - * - * Cloudflare edge locations: Madrid through Nuremberg. - */ - -import type { EdgeLocation } from './EdgeComputingManager.types' - -export const CLOUDFLARE_EDGE_LOCATIONS_2: EdgeLocation[] = [ - { - id: 'edge-cf-madrid', - name: 'Cloudflare Madrid', - city: 'Madrid', - country: 'Spain', - continent: 'Europe', - coordinates: { latitude: 40.4168, longitude: -3.7038 }, - provider: 'cloudflare', - region: 'europe-southwest1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-paris', - name: 'Cloudflare Paris', - city: 'Paris', - country: 'France', - continent: 'Europe', - coordinates: { latitude: 48.8566, longitude: 2.3522 }, - provider: 'cloudflare', - region: 'europe-west9', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-berlin', - name: 'Cloudflare Berlin', - city: 'Berlin', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 52.52, longitude: 13.405 }, - provider: 'cloudflare', - region: 'europe-central3', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-copenhagen', - name: 'Cloudflare Copenhagen', - city: 'Copenhagen', - country: 'Denmark', - continent: 'Europe', - coordinates: { latitude: 55.6761, longitude: 12.5683 }, - provider: 'cloudflare', - region: 'europe-north2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-helsinki', - name: 'Cloudflare Helsinki', - city: 'Helsinki', - country: 'Finland', - continent: 'Europe', - coordinates: { latitude: 60.1699, longitude: 24.9384 }, - provider: 'cloudflare', - region: 'europe-north3', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-oslo', - name: 'Cloudflare Oslo', - city: 'Oslo', - country: 'Norway', - continent: 'Europe', - coordinates: { latitude: 59.9139, longitude: 10.7522 }, - provider: 'cloudflare', - region: 'europe-north4', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-dublin', - name: 'Cloudflare Dublin', - city: 'Dublin', - country: 'Ireland', - continent: 'Europe', - coordinates: { latitude: 53.3498, longitude: -6.2603 }, - provider: 'cloudflare', - region: 'europe-west2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-lisbon', - name: 'Cloudflare Lisbon', - city: 'Lisbon', - country: 'Portugal', - continent: 'Europe', - coordinates: { latitude: 38.7223, longitude: -9.1393 }, - provider: 'cloudflare', - region: 'europe-southwest2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-athens', - name: 'Cloudflare Athens', - city: 'Athens', - country: 'Greece', - continent: 'Europe', - coordinates: { latitude: 37.9838, longitude: 23.7275 }, - provider: 'cloudflare', - region: 'europe-south3', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-bucharest', - name: 'Cloudflare Bucharest', - city: 'Bucharest', - country: 'Romania', - continent: 'Europe', - coordinates: { latitude: 44.4268, longitude: 26.1025 }, - provider: 'cloudflare', - region: 'europe-central4', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-prague', - name: 'Cloudflare Prague', - city: 'Prague', - country: 'Czech Republic', - continent: 'Europe', - coordinates: { latitude: 50.0755, longitude: 14.4378 }, - provider: 'cloudflare', - region: 'europe-central5', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-budapest', - name: 'Cloudflare Budapest', - city: 'Budapest', - country: 'Hungary', - continent: 'Europe', - coordinates: { latitude: 47.4979, longitude: 19.0402 }, - provider: 'cloudflare', - region: 'europe-central6', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-sophia', - name: 'Cloudflare Sophia', - city: 'Sophia Antipolis', - country: 'France', - continent: 'Europe', - coordinates: { latitude: 43.6156, longitude: 7.0412 }, - provider: 'cloudflare', - region: 'europe-south4', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-hamburg', - name: 'Cloudflare Hamburg', - city: 'Hamburg', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 53.5511, longitude: 9.9937 }, - provider: 'cloudflare', - region: 'europe-central7', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-munich', - name: 'Cloudflare Munich', - city: 'Munich', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 48.1351, longitude: 11.582 }, - provider: 'cloudflare', - region: 'europe-central8', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-cologne', - name: 'Cloudflare Cologne', - city: 'Cologne', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.9375, longitude: 6.9603 }, - provider: 'cloudflare', - region: 'europe-central9', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-frankfurt', - name: 'Cloudflare Frankfurt', - city: 'Frankfurt', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.1109, longitude: 8.6821 }, - provider: 'cloudflare', - region: 'europe-central10', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-stuttgart', - name: 'Cloudflare Stuttgart', - city: 'Stuttgart', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 48.7758, longitude: 9.1829 }, - provider: 'cloudflare', - region: 'europe-central11', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-nuremberg', - name: 'Cloudflare Nuremberg', - city: 'Nuremberg', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.4521, longitude: 11.0767 }, - provider: 'cloudflare', - region: 'europe-central12', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, -] diff --git a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations.ts b/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations.ts deleted file mode 100644 index cdf584c4b3..0000000000 --- a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations.ts +++ /dev/null @@ -1,2048 +0,0 @@ -/** - * Edge Computing Manager - Default Edge Locations - * - * Predefined edge locations covering 90+ global locations across - * Cloudflare Workers and AWS Lambda@Edge. - */ - -import type { EdgeLocation } from './EdgeComputingManager.types' - -export const DEFAULT_EDGE_LOCATIONS: EdgeLocation[] = [ - // North America - { - id: 'edge-us-east-1', - name: 'US East (N. Virginia)', - city: 'Ashburn', - country: 'USA', - continent: 'North America', - coordinates: { latitude: 39.0438, longitude: -77.4874 }, - provider: 'aws', - region: 'us-east-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 5, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-us-west-2', - name: 'US West (Oregon)', - city: 'Boardman', - country: 'USA', - continent: 'North America', - coordinates: { latitude: 45.8399, longitude: -119.7006 }, - provider: 'aws', - region: 'us-west-2', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 8, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-ca-central-1', - name: 'Canada (Central)', - city: 'Montreal', - country: 'Canada', - continent: 'North America', - coordinates: { latitude: 45.5017, longitude: -73.5673 }, - provider: 'aws', - region: 'ca-central-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 15, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - - // Europe - { - id: 'edge-eu-west-1', - name: 'EU West (Ireland)', - city: 'Dublin', - country: 'Ireland', - continent: 'Europe', - coordinates: { latitude: 53.3498, longitude: -6.2603 }, - provider: 'aws', - region: 'eu-west-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 10, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-eu-central-1', - name: 'EU Central (Frankfurt)', - city: 'Frankfurt', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.1109, longitude: 8.6821 }, - provider: 'aws', - region: 'eu-central-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 8, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-eu-north-1', - name: 'EU North (Stockholm)', - city: 'Stockholm', - country: 'Sweden', - continent: 'Europe', - coordinates: { latitude: 59.3293, longitude: 18.0686 }, - provider: 'aws', - region: 'eu-north-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 20, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-eu-west-3', - name: 'EU West (Paris)', - city: 'Paris', - country: 'France', - continent: 'Europe', - coordinates: { latitude: 48.8566, longitude: 2.3522 }, - provider: 'aws', - region: 'eu-west-3', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 12, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-eu-south-1', - name: 'EU South (Milan)', - city: 'Milan', - country: 'Italy', - continent: 'Europe', - coordinates: { latitude: 45.4642, longitude: 9.19 }, - provider: 'aws', - region: 'eu-south-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 18, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - - // Asia Pacific - { - id: 'edge-ap-southeast-1', - name: 'AP Southeast (Singapore)', - city: 'Singapore', - country: 'Singapore', - continent: 'Asia', - coordinates: { latitude: 1.3521, longitude: 103.8198 }, - provider: 'aws', - region: 'ap-southeast-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 5, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-ap-northeast-1', - name: 'AP Northeast (Tokyo)', - city: 'Tokyo', - country: 'Japan', - continent: 'Asia', - coordinates: { latitude: 35.6762, longitude: 139.6503 }, - provider: 'aws', - region: 'ap-northeast-1', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 8, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-ap-southeast-2', - name: 'AP Southeast (Sydney)', - city: 'Sydney', - country: 'Australia', - continent: 'Oceania', - coordinates: { latitude: -33.8688, longitude: 151.2093 }, - provider: 'aws', - region: 'ap-southeast-2', - priority: 1, - capacity: { cpu: 2, memory: '512MB', storage: '10GB' }, - network: { bandwidth: '10Gbps', latency: 15, cdn: true }, - aiModels: ['threat-detection-v2', 'bias-detection-v1'], - cacheConfig: { size: '100MB', ttl: 3600, strategies: ['LRU', 'LFU'] }, - }, - { - id: 'edge-ap-northeast-2', - name: 'AP Northeast (Seoul)', - city: 'Seoul', - country: 'South Korea', - continent: 'Asia', - coordinates: { latitude: 37.5665, longitude: 126.978 }, - provider: 'aws', - region: 'ap-northeast-2', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 12, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-ap-south-1', - name: 'AP South (Mumbai)', - city: 'Mumbai', - country: 'India', - continent: 'Asia', - coordinates: { latitude: 19.076, longitude: 72.8777 }, - provider: 'aws', - region: 'ap-south-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 25, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-ap-east-1', - name: 'AP East (Hong Kong)', - city: 'Hong Kong', - country: 'Hong Kong', - continent: 'Asia', - coordinates: { latitude: 22.3193, longitude: 114.1694 }, - provider: 'aws', - region: 'ap-east-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 10, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - - // South America - { - id: 'edge-sa-east-1', - name: 'SA East (São Paulo)', - city: 'São Paulo', - country: 'Brazil', - continent: 'South America', - coordinates: { latitude: -23.5505, longitude: -46.6333 }, - provider: 'aws', - region: 'sa-east-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 30, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - - // Middle East & Africa - { - id: 'edge-me-south-1', - name: 'ME South (Bahrain)', - city: 'Manama', - country: 'Bahrain', - continent: 'Asia', - coordinates: { latitude: 26.0667, longitude: 50.5577 }, - provider: 'aws', - region: 'me-south-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 35, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - { - id: 'edge-af-south-1', - name: 'AF South (Cape Town)', - city: 'Cape Town', - country: 'South Africa', - continent: 'Africa', - coordinates: { latitude: -33.9249, longitude: 18.4241 }, - provider: 'aws', - region: 'af-south-1', - priority: 2, - capacity: { cpu: 1, memory: '256MB', storage: '5GB' }, - network: { bandwidth: '5Gbps', latency: 40, cdn: true }, - aiModels: ['threat-detection-v2'], - cacheConfig: { size: '50MB', ttl: 1800, strategies: ['LRU'] }, - }, - - // Cloudflare Edge Locations (additional 30+ locations) - { - id: 'edge-cf-amsterdam', - name: 'Cloudflare Amsterdam', - city: 'Amsterdam', - country: 'Netherlands', - continent: 'Europe', - coordinates: { latitude: 52.3676, longitude: 4.9041 }, - provider: 'cloudflare', - region: 'europe-west4', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-london', - name: 'Cloudflare London', - city: 'London', - country: 'United Kingdom', - continent: 'Europe', - coordinates: { latitude: 51.5074, longitude: -0.1278 }, - provider: 'cloudflare', - region: 'europe-west2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-san-jose', - name: 'Cloudflare San Jose', - city: 'San Jose', - country: 'USA', - continent: 'North America', - coordinates: { latitude: 37.3382, longitude: -121.8863 }, - provider: 'cloudflare', - region: 'us-west1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 2, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-atlanta', - name: 'Cloudflare Atlanta', - city: 'Atlanta', - country: 'USA', - continent: 'North America', - coordinates: { latitude: 33.749, longitude: -84.388 }, - provider: 'cloudflare', - region: 'us-east1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 2, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-tokyo', - name: 'Cloudflare Tokyo', - city: 'Tokyo', - country: 'Japan', - continent: 'Asia', - coordinates: { latitude: 35.6762, longitude: 139.6503 }, - provider: 'cloudflare', - region: 'asia-northeast1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-sydney', - name: 'Cloudflare Sydney', - city: 'Sydney', - country: 'Australia', - continent: 'Oceania', - coordinates: { latitude: -33.8688, longitude: 151.2093 }, - provider: 'cloudflare', - region: 'australia-southeast1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-singapore', - name: 'Cloudflare Singapore', - city: 'Singapore', - country: 'Singapore', - continent: 'Asia', - coordinates: { latitude: 1.3521, longitude: 103.8198 }, - provider: 'cloudflare', - region: 'asia-southeast1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 2, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-mumbai', - name: 'Cloudflare Mumbai', - city: 'Mumbai', - country: 'India', - continent: 'Asia', - coordinates: { latitude: 19.076, longitude: 72.8777 }, - provider: 'cloudflare', - region: 'asia-south1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 5, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-sao-paulo', - name: 'Cloudflare São Paulo', - city: 'São Paulo', - country: 'Brazil', - continent: 'South America', - coordinates: { latitude: -23.5505, longitude: -46.6333 }, - provider: 'cloudflare', - region: 'southamerica-east1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 5, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-johannesburg', - name: 'Cloudflare Johannesburg', - city: 'Johannesburg', - country: 'South Africa', - continent: 'Africa', - coordinates: { latitude: -26.2041, longitude: 28.0473 }, - provider: 'cloudflare', - region: 'africa-south1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 8, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-dubai', - name: 'Cloudflare Dubai', - city: 'Dubai', - country: 'UAE', - continent: 'Asia', - coordinates: { latitude: 25.2048, longitude: 55.2708 }, - provider: 'cloudflare', - region: 'me-central1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 5, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-tel-aviv', - name: 'Cloudflare Tel Aviv', - city: 'Tel Aviv', - country: 'Israel', - continent: 'Asia', - coordinates: { latitude: 32.0853, longitude: 34.7818 }, - provider: 'cloudflare', - region: 'me-west1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-mexico-city', - name: 'Cloudflare Mexico City', - city: 'Mexico City', - country: 'Mexico', - continent: 'North America', - coordinates: { latitude: 19.4326, longitude: -99.1332 }, - provider: 'cloudflare', - region: 'northamerica-south1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 5, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-toronto', - name: 'Cloudflare Toronto', - city: 'Toronto', - country: 'Canada', - continent: 'North America', - coordinates: { latitude: 43.6532, longitude: -79.3832 }, - provider: 'cloudflare', - region: 'northamerica-northeast1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-stockholm', - name: 'Cloudflare Stockholm', - city: 'Stockholm', - country: 'Sweden', - continent: 'Europe', - coordinates: { latitude: 59.3293, longitude: 18.0686 }, - provider: 'cloudflare', - region: 'europe-north1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-warsaw', - name: 'Cloudflare Warsaw', - city: 'Warsaw', - country: 'Poland', - continent: 'Europe', - coordinates: { latitude: 52.2297, longitude: 21.0122 }, - provider: 'cloudflare', - region: 'europe-central1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-vienna', - name: 'Cloudflare Vienna', - city: 'Vienna', - country: 'Austria', - continent: 'Europe', - coordinates: { latitude: 48.2082, longitude: 16.3738 }, - provider: 'cloudflare', - region: 'europe-central2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-brussels', - name: 'Cloudflare Brussels', - city: 'Brussels', - country: 'Belgium', - continent: 'Europe', - coordinates: { latitude: 50.8503, longitude: 4.3517 }, - provider: 'cloudflare', - region: 'europe-west1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-zurich', - name: 'Cloudflare Zurich', - city: 'Zurich', - country: 'Switzerland', - continent: 'Europe', - coordinates: { latitude: 47.3769, longitude: 8.5417 }, - provider: 'cloudflare', - region: 'europe-west6', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-milan', - name: 'Cloudflare Milan', - city: 'Milan', - country: 'Italy', - continent: 'Europe', - coordinates: { latitude: 45.4642, longitude: 9.19 }, - provider: 'cloudflare', - region: 'europe-west8', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-madrid', - name: 'Cloudflare Madrid', - city: 'Madrid', - country: 'Spain', - continent: 'Europe', - coordinates: { latitude: 40.4168, longitude: -3.7038 }, - provider: 'cloudflare', - region: 'europe-southwest1', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-paris', - name: 'Cloudflare Paris', - city: 'Paris', - country: 'France', - continent: 'Europe', - coordinates: { latitude: 48.8566, longitude: 2.3522 }, - provider: 'cloudflare', - region: 'europe-west9', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-berlin', - name: 'Cloudflare Berlin', - city: 'Berlin', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 52.52, longitude: 13.405 }, - provider: 'cloudflare', - region: 'europe-central3', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-copenhagen', - name: 'Cloudflare Copenhagen', - city: 'Copenhagen', - country: 'Denmark', - continent: 'Europe', - coordinates: { latitude: 55.6761, longitude: 12.5683 }, - provider: 'cloudflare', - region: 'europe-north2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-helsinki', - name: 'Cloudflare Helsinki', - city: 'Helsinki', - country: 'Finland', - continent: 'Europe', - coordinates: { latitude: 60.1699, longitude: 24.9384 }, - provider: 'cloudflare', - region: 'europe-north3', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-oslo', - name: 'Cloudflare Oslo', - city: 'Oslo', - country: 'Norway', - continent: 'Europe', - coordinates: { latitude: 59.9139, longitude: 10.7522 }, - provider: 'cloudflare', - region: 'europe-north4', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-dublin', - name: 'Cloudflare Dublin', - city: 'Dublin', - country: 'Ireland', - continent: 'Europe', - coordinates: { latitude: 53.3498, longitude: -6.2603 }, - provider: 'cloudflare', - region: 'europe-west2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-lisbon', - name: 'Cloudflare Lisbon', - city: 'Lisbon', - country: 'Portugal', - continent: 'Europe', - coordinates: { latitude: 38.7223, longitude: -9.1393 }, - provider: 'cloudflare', - region: 'europe-southwest2', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-athens', - name: 'Cloudflare Athens', - city: 'Athens', - country: 'Greece', - continent: 'Europe', - coordinates: { latitude: 37.9838, longitude: 23.7275 }, - provider: 'cloudflare', - region: 'europe-south3', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-bucharest', - name: 'Cloudflare Bucharest', - city: 'Bucharest', - country: 'Romania', - continent: 'Europe', - coordinates: { latitude: 44.4268, longitude: 26.1025 }, - provider: 'cloudflare', - region: 'europe-central4', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-prague', - name: 'Cloudflare Prague', - city: 'Prague', - country: 'Czech Republic', - continent: 'Europe', - coordinates: { latitude: 50.0755, longitude: 14.4378 }, - provider: 'cloudflare', - region: 'europe-central5', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-budapest', - name: 'Cloudflare Budapest', - city: 'Budapest', - country: 'Hungary', - continent: 'Europe', - coordinates: { latitude: 47.4979, longitude: 19.0402 }, - provider: 'cloudflare', - region: 'europe-central6', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-sophia', - name: 'Cloudflare Sophia', - city: 'Sophia Antipolis', - country: 'France', - continent: 'Europe', - coordinates: { latitude: 43.6156, longitude: 7.0412 }, - provider: 'cloudflare', - region: 'europe-south4', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-hamburg', - name: 'Cloudflare Hamburg', - city: 'Hamburg', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 53.5511, longitude: 9.9937 }, - provider: 'cloudflare', - region: 'europe-central7', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-munich', - name: 'Cloudflare Munich', - city: 'Munich', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 48.1351, longitude: 11.582 }, - provider: 'cloudflare', - region: 'europe-central8', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-cologne', - name: 'Cloudflare Cologne', - city: 'Cologne', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.9375, longitude: 6.9603 }, - provider: 'cloudflare', - region: 'europe-central9', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-frankfurt', - name: 'Cloudflare Frankfurt', - city: 'Frankfurt', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.1109, longitude: 8.6821 }, - provider: 'cloudflare', - region: 'europe-central10', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-stuttgart', - name: 'Cloudflare Stuttgart', - city: 'Stuttgart', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 48.7758, longitude: 9.1829 }, - provider: 'cloudflare', - region: 'europe-central11', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-nuremberg', - name: 'Cloudflare Nuremberg', - city: 'Nuremberg', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.4521, longitude: 11.0767 }, - provider: 'cloudflare', - region: 'europe-central12', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-dusseldorf', - name: 'Cloudflare Dusseldorf', - city: 'Dusseldorf', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.2277, longitude: 6.7735 }, - provider: 'cloudflare', - region: 'europe-central13', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-leipzig', - name: 'Cloudflare Leipzig', - city: 'Leipzig', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.3397, longitude: 12.3731 }, - provider: 'cloudflare', - region: 'europe-central14', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-dortmund', - name: 'Cloudflare Dortmund', - city: 'Dortmund', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.5136, longitude: 7.4653 }, - provider: 'cloudflare', - region: 'europe-central15', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-essen', - name: 'Cloudflare Essen', - city: 'Essen', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.4556, longitude: 7.0116 }, - provider: 'cloudflare', - region: 'europe-central16', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-bremen', - name: 'Cloudflare Bremen', - city: 'Bremen', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 53.0793, longitude: 8.8017 }, - provider: 'cloudflare', - region: 'europe-central17', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-hanover', - name: 'Cloudflare Hanover', - city: 'Hanover', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 52.3759, longitude: 9.732 }, - provider: 'cloudflare', - region: 'europe-central18', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-dresden', - name: 'Cloudflare Dresden', - city: 'Dresden', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.0504, longitude: 13.7373 }, - provider: 'cloudflare', - region: 'europe-central19', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-karlsruhe', - name: 'Cloudflare Karlsruhe', - city: 'Karlsruhe', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.0069, longitude: 8.4037 }, - provider: 'cloudflare', - region: 'europe-central20', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-bielefeld', - name: 'Cloudflare Bielefeld', - city: 'Bielefeld', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 52.0302, longitude: 8.5325 }, - provider: 'cloudflare', - region: 'europe-central21', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-gelsenkirchen', - name: 'Cloudflare Gelsenkirchen', - city: 'Gelsenkirchen', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.473, longitude: 7.1208 }, - provider: 'cloudflare', - region: 'europe-central22', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-wuppertal', - name: 'Cloudflare Wuppertal', - city: 'Wuppertal', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.2562, longitude: 7.1508 }, - provider: 'cloudflare', - region: 'europe-central23', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-bochum', - name: 'Cloudflare Bochum', - city: 'Bochum', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.4818, longitude: 7.2162 }, - provider: 'cloudflare', - region: 'europe-central24', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-aachen', - name: 'Cloudflare Aachen', - city: 'Aachen', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.7753, longitude: 6.0839 }, - provider: 'cloudflare', - region: 'europe-central25', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-bonn', - name: 'Cloudflare Bonn', - city: 'Bonn', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.7374, longitude: 7.0982 }, - provider: 'cloudflare', - region: 'europe-central26', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-mannheim', - name: 'Cloudflare Mannheim', - city: 'Mannheim', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.4875, longitude: 8.466 }, - provider: 'cloudflare', - region: 'europe-central27', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-wiesbaden', - name: 'Cloudflare Wiesbaden', - city: 'Wiesbaden', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.0782, longitude: 8.2398 }, - provider: 'cloudflare', - region: 'europe-central28', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-munster', - name: 'Cloudflare Münster', - city: 'Münster', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.9607, longitude: 7.6261 }, - provider: 'cloudflare', - region: 'europe-central29', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-kiel', - name: 'Cloudflare Kiel', - city: 'Kiel', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 54.3233, longitude: 10.1228 }, - provider: 'cloudflare', - region: 'europe-central30', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-braunschweig', - name: 'Cloudflare Braunschweig', - city: 'Braunschweig', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 52.2689, longitude: 10.5267 }, - provider: 'cloudflare', - region: 'europe-central31', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-kassel', - name: 'Cloudflare Kassel', - city: 'Kassel', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.3127, longitude: 9.4797 }, - provider: 'cloudflare', - region: 'europe-central32', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-augsburg', - name: 'Cloudflare Augsburg', - city: 'Augsburg', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 48.3705, longitude: 10.8978 }, - provider: 'cloudflare', - region: 'europe-central33', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-freiburg', - name: 'Cloudflare Freiburg', - city: 'Freiburg', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 47.999, longitude: 7.8421 }, - provider: 'cloudflare', - region: 'europe-central34', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-regensburg', - name: 'Cloudflare Regensburg', - city: 'Regensburg', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.0134, longitude: 12.1016 }, - provider: 'cloudflare', - region: 'europe-central35', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-wurzburg', - name: 'Cloudflare Würzburg', - city: 'Würzburg', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.7913, longitude: 9.9534 }, - provider: 'cloudflare', - region: 'europe-central36', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-ulm', - name: 'Cloudflare Ulm', - city: 'Ulm', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 48.4011, longitude: 9.9876 }, - provider: 'cloudflare', - region: 'europe-central37', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-ingolstadt', - name: 'Cloudflare Ingolstadt', - city: 'Ingolstadt', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 48.7651, longitude: 11.4237 }, - provider: 'cloudflare', - region: 'europe-central38', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-heilbronn', - name: 'Cloudflare Heilbronn', - city: 'Heilbronn', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.1427, longitude: 9.2109 }, - provider: 'cloudflare', - region: 'europe-central39', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-pforzheim', - name: 'Cloudflare Pforzheim', - city: 'Pforzheim', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 48.8922, longitude: 8.6946 }, - provider: 'cloudflare', - region: 'europe-central40', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-offenbach', - name: 'Cloudflare Offenbach', - city: 'Offenbach', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.0956, longitude: 8.7765 }, - provider: 'cloudflare', - region: 'europe-central41', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-wolfsburg', - name: 'Cloudflare Wolfsburg', - city: 'Wolfsburg', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 52.4275, longitude: 10.7806 }, - provider: 'cloudflare', - region: 'europe-central42', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-gottingen', - name: 'Cloudflare Göttingen', - city: 'Göttingen', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 51.5413, longitude: 9.9158 }, - provider: 'cloudflare', - region: 'europe-central43', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-koblenz', - name: 'Cloudflare Koblenz', - city: 'Koblenz', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 50.3569, longitude: 7.5889 }, - provider: 'cloudflare', - region: 'europe-central44', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-mainz', - name: 'Cloudflare Mainz', - city: 'Mainz', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.9929, longitude: 8.2473 }, - provider: 'cloudflare', - region: 'europe-central45', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-saarbrucken', - name: 'Cloudflare Saarbrücken', - city: 'Saarbrücken', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.2344, longitude: 6.9969 }, - provider: 'cloudflare', - region: 'europe-central46', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-trier', - name: 'Cloudflare Trier', - city: 'Trier', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.7567, longitude: 6.6414 }, - provider: 'cloudflare', - region: 'europe-central47', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-freiburg-im-breisgau', - name: 'Cloudflare Freiburg im Breisgau', - city: 'Freiburg im Breisgau', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 47.999, longitude: 7.8421 }, - provider: 'cloudflare', - region: 'europe-central48', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-passau', - name: 'Cloudflare Passau', - city: 'Passau', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 48.5745, longitude: 13.4605 }, - provider: 'cloudflare', - region: 'europe-central49', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, - { - id: 'edge-cf-bayreuth', - name: 'Cloudflare Bayreuth', - city: 'Bayreuth', - country: 'Germany', - continent: 'Europe', - coordinates: { latitude: 49.9456, longitude: 11.5713 }, - provider: 'cloudflare', - region: 'europe-central50', - priority: 1, - capacity: { cpu: 4, memory: '1GB', storage: '20GB' }, - network: { bandwidth: '100Gbps', latency: 3, cdn: true }, - aiModels: [ - 'threat-detection-v2', - 'bias-detection-v1', - 'behavioral-analysis-v1', - ], - cacheConfig: { - size: '500MB', - ttl: 7200, - strategies: ['LRU', 'LFU', 'ARC'], - }, - }, -] diff --git a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.ts b/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.ts deleted file mode 100644 index 329c42bc56..0000000000 --- a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.ts +++ /dev/null @@ -1,719 +0,0 @@ -/** - * Edge Computing Manager - * - * Manages deployment and orchestration of edge computing nodes across - * 50+ global locations with Cloudflare Workers, AWS Lambda@Edge, and - * containerized edge services. - */ - -import { EventEmitter } from 'events' - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { - generateWorkerScript, - generateLambdaFunction, - generateAzureFunction, - generateGCPFunction, -} from './EdgeComputingManager.codegen' -import { DEFAULT_EDGE_LOCATIONS } from './EdgeComputingManager.locations' -import type { - EdgeLocation, - EdgeDeploymentConfig, - EdgeNodeStatus, -} from './EdgeComputingManager.types' - -// Re-export types for backward compatibility — index.ts and ConfigurationManager.ts import from here -export type { EdgeLocation, EdgeDeploymentConfig, EdgeNodeStatus } - -const logger = createBuildSafeLogger('EdgeComputingManager') - -export class EdgeComputingManager extends EventEmitter { - private readonly config: EdgeDeploymentConfig - private readonly edgeNodes: Map = new Map() - private healthCheckInterval: NodeJS.Timeout | null = null - private isInitialized = false - - // Predefined edge locations covering 50+ global locations - private readonly DEFAULT_EDGE_LOCATIONS: EdgeLocation[] = - DEFAULT_EDGE_LOCATIONS - - constructor(config: EdgeDeploymentConfig) { - super() - this.config = { - ...config, - locations: - config.locations.length > 0 - ? config.locations - : this.DEFAULT_EDGE_LOCATIONS, - } - } - - /** - * Initialize edge computing manager - */ - async initialize(): Promise { - try { - logger.info('Initializing Edge Computing Manager', { - locations: this.config.locations.length, - }) - - // Initialize edge node statuses - this.initializeEdgeNodeStatuses() - - // Start health monitoring - this.startHealthMonitoring() - - this.isInitialized = true - logger.info('Edge Computing Manager initialized successfully') - - this.emit('initialized', { locations: this.config.locations.length }) - } catch (error: unknown) { - logger.error('Failed to initialize Edge Computing Manager', { error }) - throw new Error(`Initialization failed: ${(error as Error).message}`, { - cause: error, - }) - } - } - - /** - * Initialize edge node statuses - */ - private initializeEdgeNodeStatuses(): void { - for (const location of this.config.locations) { - this.edgeNodes.set(location.id, { - locationId: location.id, - status: 'deploying', - lastHealthCheck: new Date(), - responseTime: 0, - errorRate: 0, - throughput: 0, - activeConnections: 0, - cacheHitRate: 0, - aiModelStatus: location.aiModels.map((model) => ({ - model, - loaded: false, - inferenceTime: 0, - accuracy: 0, - })), - metadata: {}, - }) - } - } - - /** - * Deploy edge nodes to all configured locations - */ - async deployAllEdgeNodes(): Promise { - if (!this.isInitialized) { - throw new Error('Edge computing manager not initialized') - } - - try { - logger.info('Deploying edge nodes to all locations', { - totalLocations: this.config.locations.length, - }) - - const deploymentPromises = this.config.locations.map(async (location) => - this.deployEdgeNode(location), - ) - - const results = await Promise.allSettled(deploymentPromises) - - const statuses: EdgeNodeStatus[] = [] - results.forEach((result, index) => { - const location = this.config.locations[index] - if (result.status === 'fulfilled') { - statuses.push(result.value) - } else { - const failedStatus: EdgeNodeStatus = { - locationId: location.id, - status: 'failed', - lastHealthCheck: new Date(), - responseTime: 0, - errorRate: 1, - throughput: 0, - activeConnections: 0, - cacheHitRate: 0, - aiModelStatus: location.aiModels.map((model) => ({ - model, - loaded: false, - inferenceTime: 0, - accuracy: 0, - })), - metadata: { error: result.reason.message }, - } - statuses.push(failedStatus) - this.edgeNodes.set(location.id, failedStatus) - } - }) - - this.emit('deployment-complete', { statuses }) - return statuses - } catch (error: unknown) { - logger.error('Edge node deployment failed', { error }) - throw new Error(`Deployment failed: ${(error as Error).message}`, { - cause: error, - }) - } - } - - /** - * Deploy edge node to specific location - */ - private async deployEdgeNode( - location: EdgeLocation, - ): Promise { - const startTime = Date.now() - - try { - logger.info(`Deploying edge node to location: ${location.name}`, { - location: location.id, - provider: location.provider, - }) - - // Deploy based on provider - let deploymentResult: unknown - - switch (location.provider) { - case 'cloudflare': - deploymentResult = await this.deployCloudflareWorker(location) - break - case 'aws': - deploymentResult = await this.deployAWSLambdaEdge(location) - break - case 'azure': - deploymentResult = await this.deployAzureEdge(location) - break - case 'gcp': - deploymentResult = await this.deployGCPEdge(location) - break - default: - throw new Error( - `Unsupported edge provider: ${location.provider as string}`, - ) - } - - // Create successful status - const status: EdgeNodeStatus = { - locationId: location.id, - status: 'healthy', - lastHealthCheck: new Date(), - responseTime: 50, // Initial estimate - errorRate: 0, - throughput: 1000, // Initial estimate - activeConnections: 0, - cacheHitRate: 0.95, // Initial estimate - aiModelStatus: location.aiModels.map((model) => ({ - model, - loaded: true, - inferenceTime: 25, // Initial estimate - accuracy: 0.98, // Initial estimate - })), - metadata: { - deploymentTime: Date.now() - startTime, - provider: location.provider, - ...(deploymentResult as Record), - }, - } - - this.edgeNodes.set(location.id, status) - - logger.info(`Edge node deployment completed: ${location.name}`, { - location: location.id, - duration: Date.now() - startTime, - }) - - this.emit('node-deployed', { location: location.id, status }) - return status - } catch (error: unknown) { - logger.error(`Edge node deployment failed: ${location.name}`, { - location: location.id, - error, - }) - - const failedStatus: EdgeNodeStatus = { - locationId: location.id, - status: 'failed', - lastHealthCheck: new Date(), - responseTime: 0, - errorRate: 1, - throughput: 0, - activeConnections: 0, - cacheHitRate: 0, - aiModelStatus: location.aiModels.map((model) => ({ - model, - loaded: false, - inferenceTime: 0, - accuracy: 0, - })), - metadata: { error: (error as Error).message }, - } - - this.edgeNodes.set(location.id, failedStatus) - this.emit('node-deployment-failed', { - location: location.id, - error: (error as Error).message, - }) - - throw error - } - } - - /** - * Deploy Cloudflare Worker - */ - private async deployCloudflareWorker( - location: EdgeLocation, - ): Promise<{ workerId: string; scriptSize: number }> { - try { - logger.info(`Deploying Cloudflare Worker for location: ${location.name}`) - - // Cloudflare Worker deployment logic - const workerScript = generateWorkerScript(location) - - // Simulate API call to Cloudflare - await this.simulateCloudflareDeployment(location, workerScript) - - logger.info(`Cloudflare Worker deployed successfully: ${location.name}`) - return { - workerId: `worker-${location.id}`, - scriptSize: workerScript.length, - } - } catch (error: unknown) { - logger.error(`Cloudflare Worker deployment failed: ${location.name}`, { - error, - }) - throw new Error( - `Cloudflare deployment failed: ${(error as Error).message}`, - { - cause: error, - }, - ) - } - } - - /** - * Deploy AWS Lambda@Edge - */ - private async deployAWSLambdaEdge( - location: EdgeLocation, - ): Promise<{ functionArn: string; version: string }> { - try { - logger.info(`Deploying AWS Lambda@Edge for location: ${location.name}`) - - // AWS Lambda@Edge deployment logic - const lambdaFunction = generateLambdaFunction(location) - - // Simulate API call to AWS - await this.simulateAWSLambdaDeployment(location, lambdaFunction) - - logger.info(`AWS Lambda@Edge deployed successfully: ${location.name}`) - return { - functionArn: `arn:aws:lambda:${location.region}:function:edge-${location.id}`, - version: '1.0', - } - } catch (error: unknown) { - logger.error(`AWS Lambda@Edge deployment failed: ${location.name}`, { - error, - }) - throw new Error( - `AWS Lambda deployment failed: ${(error as Error).message}`, - { - cause: error, - }, - ) - } - } - - /** - * Deploy Azure Edge - */ - private async deployAzureEdge( - location: EdgeLocation, - ): Promise<{ functionId: string; region: string }> { - try { - logger.info(`Deploying Azure Edge for location: ${location.name}`) - - // Azure Edge deployment logic - const edgeFunction = generateAzureFunction(location) - - // Simulate API call to Azure - await this.simulateAzureDeployment(location, edgeFunction) - - logger.info(`Azure Edge deployed successfully: ${location.name}`) - return { functionId: `function-${location.id}`, region: location.region } - } catch (error: unknown) { - logger.error(`Azure Edge deployment failed: ${location.name}`, { error }) - throw new Error( - `Azure Edge deployment failed: ${(error as Error).message}`, - { - cause: error, - }, - ) - } - } - - /** - * Deploy GCP Edge - */ - private async deployGCPEdge( - location: EdgeLocation, - ): Promise<{ functionName: string; region: string }> { - try { - logger.info(`Deploying GCP Edge for location: ${location.name}`) - - // GCP Edge deployment logic - const edgeFunction = generateGCPFunction(location) - - // Simulate API call to GCP - await this.simulateGCPDeployment(location, edgeFunction) - - logger.info(`GCP Edge deployed successfully: ${location.name}`) - return { functionName: `edge-${location.id}`, region: location.region } - } catch (error: unknown) { - logger.error(`GCP Edge deployment failed: ${location.name}`, { error }) - throw new Error( - `GCP Edge deployment failed: ${(error as Error).message}`, - { - cause: error, - }, - ) - } - } - - // === Simulation methods === - - private async simulateCloudflareDeployment( - location: EdgeLocation, - _script: string, - ): Promise { - // Simulate API delay - await new Promise((resolve) => - setTimeout(resolve, 1000 + Math.random() * 2000), - ) - - // Simulate occasional failures (5% failure rate) - if (Math.random() < 0.05) { - throw new Error(`Cloudflare API error for ${location.name}`) - } - - logger.info(`Simulated Cloudflare deployment for: ${location.name}`) - } - - private async simulateAWSLambdaDeployment( - location: EdgeLocation, - _functionCode: string, - ): Promise { - // Simulate API delay - await new Promise((resolve) => - setTimeout(resolve, 1500 + Math.random() * 2500), - ) - - // Simulate occasional failures (3% failure rate) - if (Math.random() < 0.03) { - throw new Error(`AWS Lambda API error for ${location.name}`) - } - - logger.info(`Simulated AWS Lambda deployment for: ${location.name}`) - } - - private async simulateAzureDeployment( - location: EdgeLocation, - _functionCode: string, - ): Promise { - // Simulate API delay - await new Promise((resolve) => - setTimeout(resolve, 1200 + Math.random() * 2000), - ) - - // Simulate occasional failures (4% failure rate) - if (Math.random() < 0.04) { - throw new Error(`Azure API error for ${location.name}`) - } - - logger.info(`Simulated Azure deployment for: ${location.name}`) - } - - private async simulateGCPDeployment( - location: EdgeLocation, - _functionCode: string, - ): Promise { - // Simulate API delay - await new Promise((resolve) => - setTimeout(resolve, 1300 + Math.random() * 2200), - ) - - // Simulate occasional failures (3.5% failure rate) - if (Math.random() < 0.035) { - throw new Error(`GCP API error for ${location.name}`) - } - - logger.info(`Simulated GCP deployment for: ${location.name}`) - } - - // === Health monitoring === - - private startHealthMonitoring(): void { - if (this.healthCheckInterval) { - clearInterval(this.healthCheckInterval) - } - - this.healthCheckInterval = setInterval(() => { - void this.performHealthChecks() - }, this.config.healthCheck.interval) - - logger.info('Edge node health monitoring started', { - interval: this.config.healthCheck.interval, - }) - } - - private async performHealthChecks(): Promise { - try { - const healthCheckPromises = Array.from(this.edgeNodes.entries()).map( - async ([locationId, status]) => - this.performHealthCheck(locationId, status), - ) - - await Promise.allSettled(healthCheckPromises) - } catch (error: unknown) { - logger.error('Health check cycle failed', { error }) - } - } - - private async performHealthCheck( - locationId: string, - currentStatus: EdgeNodeStatus, - ): Promise { - try { - const location = this.config.locations.find( - (loc) => loc.id === locationId, - ) - if (!location) { - logger.warn(`Location not found for health check: ${locationId}`) - return - } - - // Simulate health check - const startTime = Date.now() - - // Simulate network latency - await new Promise((resolve) => - setTimeout(resolve, location.network.latency), - ) - - const responseTime = Date.now() - startTime - - // Simulate occasional failures based on network conditions - const isHealthy = Math.random() > location.network.latency / 1000 // Higher latency = higher failure chance - - // Update status - const newStatus: EdgeNodeStatus = { - ...currentStatus, - lastHealthCheck: new Date(), - responseTime, - errorRate: isHealthy ? Math.random() * 0.01 : Math.random() * 0.1 + 0.1, - throughput: isHealthy ? 800 + Math.random() * 400 : Math.random() * 200, - activeConnections: Math.floor(Math.random() * 1000), - cacheHitRate: 0.9 + Math.random() * 0.09, - status: isHealthy ? 'healthy' : 'degraded', - aiModelStatus: currentStatus.aiModelStatus.map((model) => ({ - ...model, - inferenceTime: isHealthy - ? 20 + Math.random() * 30 - : 100 + Math.random() * 200, - accuracy: isHealthy - ? 0.95 + Math.random() * 0.04 - : 0.8 + Math.random() * 0.15, - })), - } - - this.edgeNodes.set(locationId, newStatus) - - // Emit health status events - if (!isHealthy && currentStatus.status === 'healthy') { - this.emit('node-degraded', { locationId, responseTime }) - } else if (isHealthy && currentStatus.status !== 'healthy') { - this.emit('node-recovered', { locationId, responseTime }) - } - } catch (error: unknown) { - logger.error(`Health check failed for location: ${locationId}`, { error }) - - // Update status to failed - const failedStatus: EdgeNodeStatus = { - ...currentStatus, - status: 'failed', - lastHealthCheck: new Date(), - - errorRate: 1, - metadata: { - ...currentStatus.metadata, - error: (error as Error).message, - }, - } - - this.edgeNodes.set(locationId, failedStatus) - this.emit('node-failed', { locationId, error: (error as Error).message }) - } - } - - // === Public API === - - /** - * Get optimal edge node for user location - */ - getOptimalEdgeNode( - userLatitude: number, - userLongitude: number, - ): EdgeLocation | null { - try { - let optimalLocation: EdgeLocation | null = null - let minDistance = Infinity - - for (const location of this.config.locations) { - const distance = this.calculateDistance( - userLatitude, - userLongitude, - location.coordinates.latitude, - location.coordinates.longitude, - ) - - const status = this.edgeNodes.get(location.id) - if (status?.status === 'healthy' && distance < minDistance) { - minDistance = distance - optimalLocation = location - } - } - - return optimalLocation - } catch (error: unknown) { - logger.error('Failed to find optimal edge node', { error }) - return null - } - } - - /** - * Calculate distance between two coordinates using Haversine formula - */ - private calculateDistance( - lat1: number, - lon1: number, - lat2: number, - lon2: number, - ): number { - const R = 6371 // Earth's radius in kilometers - const dLat = this.toRadians(lat2 - lat1) - const dLon = this.toRadians(lon2 - lon1) - - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(this.toRadians(lat1)) * - Math.cos(this.toRadians(lat2)) * - Math.sin(dLon / 2) * - Math.sin(dLon / 2) - - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) - return R * c - } - - /** - * Convert degrees to radians - */ - private toRadians(degrees: number): number { - return degrees * (Math.PI / 180) - } - - /** - * Get all edge node statuses - */ - getAllNodeStatuses(): EdgeNodeStatus[] { - return Array.from(this.edgeNodes.values()) - } - - /** - * Get edge node status by location ID - */ - getNodeStatus(locationId: string): EdgeNodeStatus | undefined { - return this.edgeNodes.get(locationId) - } - - /** - * Get edge locations by region - */ - getLocationsByRegion(region: string): EdgeLocation[] { - return this.config.locations.filter((loc) => loc.region === region) - } - - /** - * Get edge locations by provider - */ - getLocationsByProvider(provider: string): EdgeLocation[] { - return this.config.locations.filter((loc) => loc.provider === provider) - } - - /** - * Get healthy edge nodes - */ - getHealthyNodes(): EdgeNodeStatus[] { - return Array.from(this.edgeNodes.values()).filter( - (status) => status.status === 'healthy', - ) - } - - /** - * Get edge node statistics - */ - getNodeStatistics(): { - total: number - healthy: number - degraded: number - failed: number - deploying: number - averageResponseTime: number - averageCacheHitRate: number - } { - const statuses = Array.from(this.edgeNodes.values()) - - const stats = { - total: statuses.length, - healthy: statuses.filter((s) => s.status === 'healthy').length, - degraded: statuses.filter((s) => s.status === 'degraded').length, - failed: statuses.filter((s) => s.status === 'failed').length, - deploying: statuses.filter((s) => s.status === 'deploying').length, - averageResponseTime: 0, - averageCacheHitRate: 0, - } - - if (stats.total > 0) { - stats.averageResponseTime = - statuses.reduce((sum, s) => sum + s.responseTime, 0) / stats.total - stats.averageCacheHitRate = - statuses.reduce((sum, s) => sum + s.cacheHitRate, 0) / stats.total - } - - return stats - } - - /** - * Cleanup resources - */ - async cleanup(): Promise { - try { - logger.info('Cleaning up Edge Computing Manager') - - if (this.healthCheckInterval) { - clearInterval(this.healthCheckInterval) - this.healthCheckInterval = null - } - - this.edgeNodes.clear() - this.isInitialized = false - - logger.info('Edge Computing Manager cleanup completed') - } catch (error: unknown) { - logger.error('Edge computing cleanup failed', { error }) - throw error - } - } -} - -export default EdgeComputingManager diff --git a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.types.ts b/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.types.ts deleted file mode 100644 index 142b359f95..0000000000 --- a/apps/web/src/lib/deployment/multi-region/EdgeComputingManager.types.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Edge Computing Manager - Type Definitions - * - * Type definitions for edge computing node management across - * 50+ global locations. - */ - -export interface EdgeLocation { - id: string - name: string - city: string - country: string - continent: string - coordinates: { - latitude: number - longitude: number - } - provider: 'cloudflare' | 'aws' | 'azure' | 'gcp' - region: string - priority: number - capacity: { - cpu: number - memory: string - storage: string - } - network: { - bandwidth: string - latency: number - cdn: boolean - } - aiModels: string[] - cacheConfig: { - size: string - ttl: number - strategies: string[] - } -} - -export interface EdgeDeploymentConfig { - locations: EdgeLocation[] - services: { - threatDetection: boolean - biasDetection: boolean - cacheService: boolean - apiGateway: boolean - staticContent: boolean - } - aiModels: { - threatDetection: string - biasDetection: string - behavioralAnalysis: string - } - cacheStrategies: string[] - healthCheck: { - interval: number - timeout: number - retries: number - } -} - -export interface EdgeNodeStatus { - locationId: string - status: 'healthy' | 'degraded' | 'failed' | 'deploying' - lastHealthCheck: Date - responseTime: number - errorRate: number - throughput: number - activeConnections: number - cacheHitRate: number - aiModelStatus: { - model: string - loaded: boolean - inferenceTime: number - accuracy: number - }[] - metadata: Record -} diff --git a/apps/web/src/lib/deployment/multi-region/GlobalTrafficRoutingManager.ts b/apps/web/src/lib/deployment/multi-region/GlobalTrafficRoutingManager.ts deleted file mode 100644 index fd4528f5c6..0000000000 --- a/apps/web/src/lib/deployment/multi-region/GlobalTrafficRoutingManager.ts +++ /dev/null @@ -1,937 +0,0 @@ -/** - * Global Traffic Routing Manager - * - * Manages intelligent traffic routing across multiple regions with - * latency optimization, health-based routing, and compliance-aware - * traffic distribution. - */ - -import { EventEmitter } from 'events' - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' - -const logger = createBuildSafeLogger('GlobalTrafficRoutingManager') - -import { RegionConfig } from './MultiRegionDeploymentManager' - -export interface RoutingConfig { - strategy: - | 'latency-based' - | 'health-based' - | 'compliance-based' - | 'weighted-round-robin' - healthThreshold: number - latencyThreshold: number - complianceRequirements: string[] - weights: Record - fallbackRegions: string[] - cacheTtl: number -} - -export interface RouteTarget { - regionId: string - endpoint: string - priority: number - healthScore: number - latency: number - complianceStatus: boolean - capacity: number - currentLoad: number -} - -export interface RoutingDecision { - target: RouteTarget - reason: string - latency: number - healthScore: number - compliance: string[] - timestamp: Date -} - -export interface TrafficMetrics { - totalRequests: number - successfulRequests: number - failedRequests: number - averageLatency: number - p50Latency: number - p95Latency: number - p99Latency: number - requestsByRegion: Record - errorRateByRegion: Record -} - -export class GlobalTrafficRoutingManager extends EventEmitter { - private readonly config: RoutingConfig - private readonly routeTargets: Map = new Map() - private readonly routingCache: Map = new Map() - private readonly metrics: TrafficMetrics - private isInitialized = false - private metricsInterval: NodeJS.Timeout | null = null - - constructor(config: RoutingConfig) { - super() - this.config = config - this.metrics = this.initializeMetrics() - } - - /** - * Initialize the traffic routing manager - */ - async initialize(): Promise { - try { - logger.info('Initializing Global Traffic Routing Manager', { - strategy: this.config.strategy, - regions: this.routeTargets.size, - }) - - // Initialize route targets - await this.initializeRouteTargets() - - // Start metrics collection - this.startMetricsCollection() - - this.isInitialized = true - logger.info('Global Traffic Routing Manager initialized successfully') - - this.emit('initialized', { strategy: this.config.strategy }) - } catch (error: unknown) { - logger.error('Failed to initialize Global Traffic Routing Manager', { - error, - }) - throw new Error( - `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - { - cause: error, - }, - ) - } - } - - /** - * Initialize route targets from configuration - */ - private async initializeRouteTargets(): Promise { - try { - // This would typically load from configuration or service discovery - const regions = this.loadRegionConfiguration() - - for (const region of regions) { - const target: RouteTarget = { - regionId: region.id, - endpoint: `https://api-${region.id}.pixelated.com`, - priority: region.priority, - healthScore: 100, - latency: 50, - complianceStatus: true, - capacity: region.capacity.maxInstances, - currentLoad: 0, - } - - this.routeTargets.set(region.id, target) - } - - logger.info(`Initialized ${this.routeTargets.size} route targets`) - } catch (error: unknown) { - logger.error('Failed to initialize route targets', { error }) - throw error - } - } - - /** - * Load region configuration - */ - private loadRegionConfiguration(): RegionConfig[] { - // This would typically come from configuration management - return [ - { - id: 'us-east-1', - name: 'US East (N. Virginia)', - provider: 'aws', - location: 'us-east', - availabilityZones: ['us-east-1a', 'us-east-1b', 'us-east-1c'], - priority: 1, - complianceRequirements: ['SOC2', 'HIPAA'], - capacity: { - minInstances: 2, - maxInstances: 20, - desiredInstances: 10, - }, - networking: { - vpcCidr: '10.1.0.0/16', - subnetCidrs: ['10.1.1.0/24', '10.1.2.0/24', '10.1.3.0/24'], - securityGroups: ['sg-12345'], - }, - }, - { - id: 'eu-central-1', - name: 'EU Central (Frankfurt)', - provider: 'aws', - location: 'eu-central', - availabilityZones: ['eu-central-1a', 'eu-central-1b', 'eu-central-1c'], - priority: 1, - complianceRequirements: ['GDPR', 'SOC2'], - capacity: { - minInstances: 2, - maxInstances: 20, - desiredInstances: 10, - }, - networking: { - vpcCidr: '10.2.0.0/16', - subnetCidrs: ['10.2.1.0/24', '10.2.2.0/24', '10.2.3.0/24'], - securityGroups: ['sg-67890'], - }, - }, - { - id: 'ap-southeast-1', - name: 'AP Southeast (Singapore)', - provider: 'aws', - location: 'apac-singapore', - availabilityZones: [ - 'ap-southeast-1a', - 'ap-southeast-1b', - 'ap-southeast-1c', - ], - priority: 1, - complianceRequirements: [], - capacity: { - minInstances: 2, - maxInstances: 20, - desiredInstances: 10, - }, - networking: { - vpcCidr: '10.3.0.0/16', - subnetCidrs: ['10.3.1.0/24', '10.3.2.0/24', '10.3.3.0/24'], - securityGroups: ['sg-54321'], - }, - }, - ] - } - - /** - * Initialize metrics - */ - private initializeMetrics(): TrafficMetrics { - return { - totalRequests: 0, - successfulRequests: 0, - failedRequests: 0, - averageLatency: 0, - p50Latency: 0, - p95Latency: 0, - p99Latency: 0, - requestsByRegion: {}, - errorRateByRegion: {}, - } - } - - /** - * Route traffic to optimal target based on strategy - */ - async routeTraffic( - userLocation: { latitude: number; longitude: number }, - _requestType: string = 'api', - complianceRequirements: string[] = [], - ): Promise { - if (!this.isInitialized) { - throw new Error('Traffic routing manager not initialized') - } - - try { - const cacheKey = this.generateCacheKey( - userLocation, - _requestType, - complianceRequirements, - ) - - // Check cache first - const cachedDecision = this.routingCache.get(cacheKey) - if (cachedDecision && this.isCacheValid(cachedDecision)) { - this.updateMetrics(cachedDecision) - return cachedDecision - } - - // Make routing decision - const decision = await this.makeRoutingDecision( - userLocation, - _requestType, - complianceRequirements, - ) - - // Cache the decision - this.routingCache.set(cacheKey, decision) - - // Update metrics - this.updateMetrics(decision) - - this.emit('traffic-routed', { - target: decision.target.regionId, - reason: decision.reason, - latency: decision.latency, - }) - - return decision - } catch (error: unknown) { - logger.error('Traffic routing failed', { - error, - userLocation, - _requestType, - }) - - // Fallback to default region - const fallbackDecision = this.getFallbackDecision() - this.emit('traffic-routing-failed', { - error: error instanceof Error ? error.message : 'Unknown error', - fallback: fallbackDecision.target.regionId, - }) - - return fallbackDecision - } - } - - /** - * Generate cache key for routing decision - */ - private generateCacheKey( - userLocation: { latitude: number; longitude: number }, - requestType: string, - complianceRequirements: string[], - ): string { - const locationHash = `${Math.round(userLocation.latitude * 100)}_${Math.round(userLocation.longitude * 100)}` - const complianceHash = complianceRequirements.sort().join(',') - return `${locationHash}_${requestType}_${complianceHash}` - } - - /** - * Check if cached decision is still valid - */ - private isCacheValid(decision: RoutingDecision): boolean { - const now = new Date() - const cacheAge = now.getTime() - decision.timestamp.getTime() - return cacheAge < this.config.cacheTtl - } - - /** - * Make routing decision based on configured strategy - */ - private async makeRoutingDecision( - userLocation: { latitude: number; longitude: number }, - requestType: string, - complianceRequirements: string[], - ): Promise { - switch (this.config.strategy) { - case 'latency-based': - return this.makeLatencyBasedDecision( - userLocation, - requestType, - complianceRequirements, - ) - case 'health-based': - return this.makeHealthBasedDecision( - userLocation, - requestType, - complianceRequirements, - ) - case 'compliance-based': - return this.makeComplianceBasedDecision( - userLocation, - requestType, - complianceRequirements, - ) - case 'weighted-round-robin': - return this.makeWeightedRoundRobinDecision( - userLocation, - requestType, - complianceRequirements, - ) - default: - throw new Error(`Unsupported routing strategy: ${this.config.strategy}`) - } - } - - /** - * Make latency-based routing decision - */ - private async makeLatencyBasedDecision( - userLocation: { latitude: number; longitude: number }, - _requestType: string, - complianceRequirements: string[], - ): Promise { - const eligibleTargets = this.getEligibleTargets(complianceRequirements) - - if (eligibleTargets.length === 0) { - throw new Error('No eligible targets for latency-based routing') - } - - // Calculate latency for each target - const targetsWithLatency = await Promise.all( - eligibleTargets.map(async (target) => ({ - target, - latency: await this.calculateLatency(userLocation, target), - })), - ) - - // Filter by latency threshold - const filteredTargets = targetsWithLatency.filter( - (t) => t.latency <= this.config.latencyThreshold, - ) - - if (filteredTargets.length === 0) { - // Use closest target if none meet threshold - const closestTarget = targetsWithLatency.reduce((min, current) => - current.latency < min.latency ? current : min, - ) - - return { - target: closestTarget.target, - reason: 'Closest target (no targets meet latency threshold)', - latency: closestTarget.latency, - healthScore: closestTarget.target.healthScore, - compliance: this.getComplianceStatus(closestTarget.target), - timestamp: new Date(), - } - } - - // Select target with lowest latency - const bestTarget = filteredTargets.reduce((min, current) => - current.latency < min.latency ? current : min, - ) - - return { - target: bestTarget.target, - reason: 'Lowest latency target', - latency: bestTarget.latency, - healthScore: bestTarget.target.healthScore, - compliance: this.getComplianceStatus(bestTarget.target), - timestamp: new Date(), - } - } - - /** - * Make health-based routing decision - */ - private async makeHealthBasedDecision( - userLocation: { latitude: number; longitude: number }, - _requestType: string, - complianceRequirements: string[], - ): Promise { - const eligibleTargets = this.getEligibleTargets(complianceRequirements) - - if (eligibleTargets.length === 0) { - throw new Error('No eligible targets for health-based routing') - } - - // Filter by health threshold - const healthyTargets = eligibleTargets.filter( - (t) => t.healthScore >= this.config.healthThreshold, - ) - - if (healthyTargets.length === 0) { - // Use healthiest target if none meet threshold - const healthiestTarget = eligibleTargets.reduce((max, current) => - current.healthScore > max.healthScore ? current : max, - ) - - return { - target: healthiestTarget, - reason: 'Healthiest target (no targets meet health threshold)', - latency: 0, // Will be calculated later - healthScore: healthiestTarget.healthScore, - compliance: this.getComplianceStatus(healthiestTarget), - timestamp: new Date(), - } - } - - // Among healthy targets, select based on lowest latency - const targetsWithLatency = await Promise.all( - healthyTargets.map(async (target) => ({ - target, - latency: await this.calculateLatency(userLocation, target), - })), - ) - - const bestTarget = targetsWithLatency.reduce((min, current) => - current.latency < min.latency ? current : min, - ) - - return { - target: bestTarget.target, - reason: 'Healthiest target with lowest latency', - latency: bestTarget.latency, - healthScore: bestTarget.target.healthScore, - compliance: this.getComplianceStatus(bestTarget.target), - timestamp: new Date(), - } - } - - /** - * Make compliance-based routing decision - */ - private async makeComplianceBasedDecision( - userLocation: { latitude: number; longitude: number }, - _requestType: string, - complianceRequirements: string[], - ): Promise { - const eligibleTargets = this.getEligibleTargets(complianceRequirements) - - if (eligibleTargets.length === 0) { - throw new Error('No eligible targets for compliance-based routing') - } - - // Prioritize targets that meet all compliance requirements - const compliantTargets = eligibleTargets.filter((t) => - this.meetsAllComplianceRequirements(t, complianceRequirements), - ) - - if (compliantTargets.length === 0) { - // Fall back to latency-based selection - return this.makeLatencyBasedDecision( - userLocation, - _requestType, - complianceRequirements, - ) - } - - // Among compliant targets, select based on lowest latency - const targetsWithLatency = await Promise.all( - compliantTargets.map(async (target) => ({ - target, - latency: await this.calculateLatency(userLocation, target), - })), - ) - - const bestTarget = targetsWithLatency.reduce((min, current) => - current.latency < min.latency ? current : min, - ) - - return { - target: bestTarget.target, - reason: 'Compliant target with lowest latency', - latency: bestTarget.latency, - healthScore: bestTarget.target.healthScore, - compliance: complianceRequirements, - timestamp: new Date(), - } - } - - /** - * Make weighted round-robin routing decision - */ - private async makeWeightedRoundRobinDecision( - userLocation: { latitude: number; longitude: number }, - _requestType: string, - complianceRequirements: string[], - ): Promise { - const eligibleTargets = this.getEligibleTargets(complianceRequirements) - - if (eligibleTargets.length === 0) { - throw new Error('No eligible targets for weighted round-robin routing') - } - - // Apply weights to targets - const weightedTargets = eligibleTargets.map((target) => ({ - target, - weight: this.config.weights[target.regionId] ?? 1, - latency: 0, // Will be calculated for selected target - })) - - // Select target based on weights - const totalWeight = weightedTargets.reduce((sum, wt) => sum + wt.weight, 0) - const randomWeight = Math.random() * totalWeight - - let currentWeight = 0 - let selectedTarget = weightedTargets[0] - - for (const wt of weightedTargets) { - currentWeight += wt.weight - if (randomWeight <= currentWeight) { - selectedTarget = wt - break - } - } - - // Calculate latency for selected target - selectedTarget.latency = await this.calculateLatency( - userLocation, - selectedTarget.target, - ) - - return { - target: selectedTarget.target, - reason: 'Weighted round-robin selection', - latency: selectedTarget.latency, - healthScore: selectedTarget.target.healthScore, - compliance: this.getComplianceStatus(selectedTarget.target), - timestamp: new Date(), - } - } - - /** - * Get eligible targets based on compliance requirements - */ - private getEligibleTargets(complianceRequirements: string[]): RouteTarget[] { - const targets = Array.from(this.routeTargets.values()) - - if (complianceRequirements.length === 0) { - return targets - } - - return targets.filter((target) => - this.meetsComplianceRequirements(target, complianceRequirements), - ) - } - - /** - * Check if target meets compliance requirements - */ - private meetsComplianceRequirements( - target: RouteTarget, - requirements: string[], - ): boolean { - if (requirements.length === 0) return true - - const region = this.loadRegionConfiguration().find( - (r) => r.id === target.regionId, - ) - if (!region) return false - - return requirements.every((req) => - region.complianceRequirements.includes(req), - ) - } - - /** - * Check if target meets all compliance requirements - */ - private meetsAllComplianceRequirements( - target: RouteTarget, - requirements: string[], - ): boolean { - const region = this.loadRegionConfiguration().find( - (r) => r.id === target.regionId, - ) - if (!region) return false - - return requirements.every((req) => - region.complianceRequirements.includes(req), - ) - } - - /** - * Get compliance status for target - */ - private getComplianceStatus(target: RouteTarget): string[] { - const region = this.loadRegionConfiguration().find( - (r) => r.id === target.regionId, - ) - return region?.complianceRequirements ?? [] - } - - /** - * Calculate latency to target (simulated) - */ - private async calculateLatency( - userLocation: { latitude: number; longitude: number }, - target: RouteTarget, - ): Promise { - // This would typically use real latency measurements or CDN data - // For now, we'll simulate based on geographic distance - - const region = this.loadRegionConfiguration().find( - (r) => r.id === target.regionId, - ) - if (!region) return 100 // Default latency - - // Simulate region coordinates (in real implementation, these would be actual coordinates) - const regionCoordinates = this.getRegionCoordinates(region.location) - const distance = this.calculateDistance( - userLocation.latitude, - userLocation.longitude, - regionCoordinates.latitude, - regionCoordinates.longitude, - ) - - // Convert distance to simulated latency (km to ms) - const baseLatency = distance * 0.1 // 0.1ms per km - const networkFactor = Math.random() * 0.2 + 0.9 // 0.9-1.1x variation - const congestionFactor = 1 + (target.currentLoad / target.capacity) * 0.5 // Congestion penalty - - return Math.round(baseLatency * networkFactor * congestionFactor) - } - - /** - * Get region coordinates (simulated) - */ - private getRegionCoordinates(location: string): { - latitude: number - longitude: number - } { - const coordinates: Record = - { - 'us-east': { latitude: 39.0438, longitude: -77.4874 }, - 'us-west': { latitude: 45.8399, longitude: -119.7006 }, - 'eu-central': { latitude: 50.1109, longitude: 8.6821 }, - 'eu-west': { latitude: 53.3498, longitude: -6.2603 }, - 'apac-singapore': { latitude: 1.3521, longitude: 103.8198 }, - 'apac-tokyo': { latitude: 35.6762, longitude: 139.6503 }, - } - - return coordinates[location] ?? { latitude: 0, longitude: 0 } - } - - /** - * Calculate distance between two points using Haversine formula - */ - private calculateDistance( - lat1: number, - lon1: number, - lat2: number, - lon2: number, - ): number { - const R = 6371 // Earth's radius in kilometers - const dLat = this.toRadians(lat2 - lat1) - const dLon = this.toRadians(lon2 - lon1) - - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(this.toRadians(lat1)) * - Math.cos(this.toRadians(lat2)) * - Math.sin(dLon / 2) * - Math.sin(dLon / 2) - - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) - return R * c - } - - /** - * Convert degrees to radians - */ - private toRadians(degrees: number): number { - return degrees * (Math.PI / 180) - } - - /** - * Get fallback decision - */ - private getFallbackDecision(): RoutingDecision { - const fallbackRegion = this.config.fallbackRegions[0] - const target = this.routeTargets.get(fallbackRegion) - - if (!target) { - // Use first available target - const firstTarget = Array.from(this.routeTargets.values())[0] - return { - target: firstTarget, - reason: 'Fallback to first available target', - latency: 0, - healthScore: firstTarget.healthScore, - compliance: this.getComplianceStatus(firstTarget), - timestamp: new Date(), - } - } - - return { - target, - reason: 'Fallback to configured fallback region', - latency: 0, - healthScore: target.healthScore, - compliance: this.getComplianceStatus(target), - timestamp: new Date(), - } - } - - /** - * Update metrics with routing decision - */ - private updateMetrics(decision: RoutingDecision): void { - this.metrics.totalRequests++ - this.metrics.requestsByRegion[decision.target.regionId] = - (this.metrics.requestsByRegion[decision.target.regionId] ?? 0) + 1 - - if (decision.target.healthScore >= 80) { - this.metrics.successfulRequests++ - } else { - this.metrics.failedRequests++ - this.metrics.errorRateByRegion[decision.target.regionId] = - (this.metrics.errorRateByRegion[decision.target.regionId] ?? 0) + 1 - } - - // Update latency metrics - this.updateLatencyMetrics(decision.latency) - } - - /** - * Update latency metrics - */ - private updateLatencyMetrics(latency: number): void { - // Simple running average (in production, use proper percentile calculations) - this.metrics.averageLatency = (this.metrics.averageLatency + latency) / 2 - - // Update percentiles (simplified) - if (this.metrics.p50Latency === 0) { - this.metrics.p50Latency = latency - this.metrics.p95Latency = latency * 1.5 - this.metrics.p99Latency = latency * 2 - } else { - this.metrics.p50Latency = (this.metrics.p50Latency + latency) / 2 - this.metrics.p95Latency = (this.metrics.p95Latency + latency * 1.5) / 2 - this.metrics.p99Latency = (this.metrics.p99Latency + latency * 2) / 2 - } - } - - /** - * Start metrics collection - */ - private startMetricsCollection(): void { - if (this.metricsInterval) { - clearInterval(this.metricsInterval) - } - - this.metricsInterval = setInterval(() => { - this.collectMetrics() - }, 60000) // Collect metrics every minute - - logger.info('Traffic routing metrics collection started') - } - - /** - * Collect and process metrics - */ - private collectMetrics(): void { - try { - // Calculate error rates - for (const region in this.metrics.errorRateByRegion) { - const requests = this.metrics.requestsByRegion[region] ?? 0 - const errors = this.metrics.errorRateByRegion[region] - this.metrics.errorRateByRegion[region] = - requests > 0 ? errors / requests : 0 - } - - // Emit metrics for monitoring - this.emit('metrics-collected', { - totalRequests: this.metrics.totalRequests, - successRate: - this.metrics.totalRequests > 0 - ? this.metrics.successfulRequests / this.metrics.totalRequests - : 0, - averageLatency: this.metrics.averageLatency, - requestsByRegion: this.metrics.requestsByRegion, - }) - - logger.info('Traffic routing metrics collected', { - totalRequests: this.metrics.totalRequests, - successRate: - this.metrics.totalRequests > 0 - ? this.metrics.successfulRequests / this.metrics.totalRequests - : 0, - }) - } catch (error: unknown) { - logger.error('Metrics collection failed', { error }) - } - } - - /** - * Update target health score - */ - updateTargetHealth(regionId: string, healthScore: number): void { - const target = this.routeTargets.get(regionId) - if (target) { - target.healthScore = Math.max(0, Math.min(100, healthScore)) - logger.info(`Updated health score for region: ${regionId}`, { - healthScore, - }) - - this.emit('target-health-updated', { regionId, healthScore }) - } - } - - /** - * Update target latency - */ - updateTargetLatency(regionId: string, latency: number): void { - const target = this.routeTargets.get(regionId) - if (target) { - target.latency = Math.max(0, latency) - logger.info(`Updated latency for region: ${regionId}`, { latency }) - } - } - - /** - * Update target load - */ - updateTargetLoad(regionId: string, currentLoad: number): void { - const target = this.routeTargets.get(regionId) - if (target) { - target.currentLoad = Math.max(0, Math.min(target.capacity, currentLoad)) - logger.debug(`Updated load for region: ${regionId}`, { currentLoad }) - } - } - - /** - * Get current metrics - */ - getMetrics(): TrafficMetrics { - return { ...this.metrics } - } - - /** - * Get routing statistics - */ - getRoutingStatistics(): { - totalDecisions: number - cacheHitRate: number - averageLatency: number - strategy: string - activeTargets: number - } { - const totalDecisions = this.metrics.totalRequests - const cacheHits = Array.from(this.routingCache.values()).length - - return { - totalDecisions, - cacheHitRate: totalDecisions > 0 ? cacheHits / totalDecisions : 0, - averageLatency: this.metrics.averageLatency, - strategy: this.config.strategy, - activeTargets: this.routeTargets.size, - } - } - - /** - * Clear routing cache - */ - clearRoutingCache(): void { - this.routingCache.clear() - logger.info('Routing cache cleared') - } - - /** - * Get route targets - */ - getRouteTargets(): RouteTarget[] { - return Array.from(this.routeTargets.values()) - } - - /** - * Cleanup resources - */ - async cleanup(): Promise { - try { - logger.info('Cleaning up Global Traffic Routing Manager') - - if (this.metricsInterval) { - clearInterval(this.metricsInterval) - this.metricsInterval = null - } - - this.routingCache.clear() - this.routeTargets.clear() - this.isInitialized = false - - logger.info('Global Traffic Routing Manager cleanup completed') - } catch (error: unknown) { - logger.error('Traffic routing cleanup failed', { error }) - throw error - } - } -} - -export default GlobalTrafficRoutingManager diff --git a/apps/web/src/lib/deployment/multi-region/HealthMonitor.ts b/apps/web/src/lib/deployment/multi-region/HealthMonitor.ts deleted file mode 100644 index 7884b28a75..0000000000 --- a/apps/web/src/lib/deployment/multi-region/HealthMonitor.ts +++ /dev/null @@ -1,978 +0,0 @@ -/** - * Health Monitor - * - * Monitors health and performance of multi-region infrastructure with - * automated alerting, metrics collection, and health score calculation. - */ - -import { EventEmitter } from 'events' - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' - -const logger = createBuildSafeLogger('HealthMonitor') -import { RegionConfig } from './MultiRegionDeploymentManager' - -export interface HealthCheckConfig { - interval: number - timeout: number - retries: number - thresholds: { - cpu: number - memory: number - disk: number - responseTime: number - errorRate: number - availability: number - } -} - -export interface HealthMetrics { - regionId: string - timestamp: Date - cpuUtilization: number - memoryUtilization: number - diskUtilization: number - responseTime: number - errorRate: number - availability: number - activeConnections: number - requestCount: number - throughput: number -} - -export interface HealthScore { - regionId: string - overallScore: number - componentScores: { - performance: number - availability: number - capacity: number - security: number - } - status: 'healthy' | 'degraded' | 'critical' - lastUpdated: Date - trends: { - '1h': number - '24h': number - '7d': number - } -} - -export interface HealthAlert { - id: string - regionId: string - type: 'performance' | 'availability' | 'capacity' | 'security' - severity: 'info' | 'warning' | 'error' | 'critical' - message: string - metrics: HealthMetrics - threshold: number - timestamp: Date - acknowledged: boolean -} - -export class HealthMonitor extends EventEmitter { - private readonly config: HealthCheckConfig - private regions: RegionConfig[] = [] - private readonly healthMetrics: Map = new Map() - private readonly healthScores: Map = new Map() - private readonly activeAlerts: Map = new Map() - private readonly customHealthChecks: Map< - string, - () => Promise<{ - status: 'healthy' | 'degraded' | 'unhealthy' - message: string - }> - > = new Map() - private healthCheckInterval: NodeJS.Timeout | null = null - private metricsRetentionInterval: NodeJS.Timeout | null = null - - constructor(config: HealthCheckConfig) { - super() - this.config = config - } - - /** - * Initialize health monitor - */ - async initialize(regions: RegionConfig[]): Promise { - try { - logger.info('Initializing Health Monitor', { regions: regions.length }) - - this.regions = regions - - // Initialize health data structures - this.initializeHealthData() - - // Start health monitoring - this.startHealthMonitoring() - - // Start metrics retention cleanup - this.startMetricsRetention() - - logger.info('Health Monitor initialized successfully') - - this.emit('initialized', { regions: regions.length }) - } catch (error: unknown) { - logger.error('Failed to initialize Health Monitor', { error }) - throw new Error( - `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - { - cause: error, - }, - ) - } - } - - /** - * Initialize health data structures - */ - private initializeHealthData(): void { - for (const region of this.regions) { - this.healthMetrics.set(region.id, []) - this.healthScores.set(region.id, this.createInitialHealthScore(region.id)) - } - } - - /** - * Create initial health score for region - */ - private createInitialHealthScore(regionId: string): HealthScore { - return { - regionId, - overallScore: 100, - componentScores: { - performance: 100, - availability: 100, - capacity: 100, - security: 100, - }, - status: 'healthy', - lastUpdated: new Date(), - trends: { - '1h': 100, - '24h': 100, - '7d': 100, - }, - } - } - - /** - * Start health monitoring - */ - private startHealthMonitoring(): void { - if (this.healthCheckInterval) { - clearInterval(this.healthCheckInterval) - } - - this.healthCheckInterval = setInterval(() => { - void this.performHealthChecks() - }, this.config.interval) - - logger.info('Health monitoring started', { interval: this.config.interval }) - } - - /** - * Start metrics retention cleanup - */ - private startMetricsRetention(): void { - if (this.metricsRetentionInterval) { - clearInterval(this.metricsRetentionInterval) - } - - // Clean up old metrics every hour - this.metricsRetentionInterval = setInterval(() => { - this.cleanupOldMetrics() - }, 3600000) - - logger.info('Metrics retention cleanup started') - } - - /** - * Perform health checks on all regions - */ - private async performHealthChecks(): Promise { - try { - const checkPromises = this.regions.map(async (region) => - this.performRegionHealthCheck(region), - ) - - await Promise.allSettled(checkPromises) - - for (const [name, check] of this.customHealthChecks) { - const result = await check() - if (result.status !== 'healthy') { - this.emit('custom-health-check-failed', { - checkName: name, - status: result.status, - message: result.message, - }) - } - } - } catch (error: unknown) { - logger.error('Health check cycle failed', { error }) - } - } - - /** - * Register custom health check - */ - registerCheck( - name: string, - check: () => Promise<{ - status: 'healthy' | 'degraded' | 'unhealthy' - message: string - }>, - ): void { - this.customHealthChecks.set(name, check) - } - - /** - * Get region health status for a specific region - */ - getRegionHealth(regionId: string): { - status: 'healthy' | 'degraded' | 'unhealthy' - message: string - } { - const healthScore = this.healthScores.get(regionId) - - if (!healthScore) { - return { - status: 'unhealthy', - message: `No health score found for region ${regionId}`, - } - } - - return { - status: - healthScore.status === 'healthy' - ? 'healthy' - : healthScore.status === 'degraded' - ? 'degraded' - : 'unhealthy', - message: `Health score ${healthScore.overallScore} for region ${regionId}`, - } - } - - /** - * Perform health check on specific region - */ - private async performRegionHealthCheck(region: RegionConfig): Promise { - try { - logger.debug(`Performing health check for region: ${region.name}`) - - // Collect metrics - const metrics = await this.collectHealthMetrics(region) - - // Store metrics - this.storeHealthMetrics(region.id, metrics) - - // Calculate health score - const healthScore = this.calculateHealthScore(region.id, metrics) - - // Update health score - this.updateHealthScore(region.id, healthScore) - - // Check for alerts - await this.checkForAlerts(region.id, metrics, healthScore) - - logger.debug(`Health check completed for region: ${region.name}`, { - overallScore: healthScore.overallScore, - status: healthScore.status, - }) - } catch (error: unknown) { - logger.error(`Health check failed for region: ${region.name}`, { error }) - - // Create degraded health score on failure - const failedScore: HealthScore = { - regionId: region.id, - overallScore: 0, - componentScores: { - performance: 0, - availability: 0, - capacity: 0, - security: 0, - }, - status: 'critical', - lastUpdated: new Date(), - trends: this.healthScores.get(region.id)?.trends ?? { - '1h': 0, - '24h': 0, - '7d': 0, - }, - } - - this.updateHealthScore(region.id, failedScore) - this.emit('health-check-failed', { - regionId: region.id, - error: error instanceof Error ? error.message : 'Unknown error', - }) - } - } - - /** - * Collect health metrics for region - */ - private async collectHealthMetrics( - region: RegionConfig, - ): Promise { - try { - // Simulate collecting metrics from various sources - // In a real implementation, this would collect from: - // - Cloud provider APIs (AWS CloudWatch, GCP Monitoring, Azure Monitor) - // - Application metrics (Prometheus, DataDog, New Relic) - // - Custom health endpoints - // - Log analysis systems - - const metrics: HealthMetrics = { - regionId: region.id, - timestamp: new Date(), - cpuUtilization: this.simulateMetric(20, 80, region.id), - memoryUtilization: this.simulateMetric(30, 70, region.id), - diskUtilization: this.simulateMetric(40, 60, region.id), - responseTime: this.simulateMetric(50, 200, region.id), - errorRate: this.simulateMetric(0, 5, region.id), - availability: this.simulateMetric(95, 100, region.id), - activeConnections: Math.floor(Math.random() * 1000), - requestCount: Math.floor(Math.random() * 10000), - throughput: Math.floor(Math.random() * 1000), - } - - // Add some realistic variations based on region characteristics - if (region.provider === 'aws') { - metrics.responseTime += Math.random() * 20 - 10 // ±10ms variation - } else if (region.provider === 'gcp') { - metrics.cpuUtilization += Math.random() * 10 - 5 // ±5% variation - } - - return metrics - } catch (error: unknown) { - logger.error( - `Failed to collect health metrics for region: ${region.name}`, - { error }, - ) - throw error - } - } - - /** - * Simulate metric value with realistic patterns - */ - private simulateMetric(min: number, max: number, _seed: string): number { - // Use region ID as seed for consistent but varying metrics - const baseValue = min + (max - min) * 0.5 - const variation = (max - min) * 0.3 - - // Add some time-based variation - const timeVariation = Math.sin(Date.now() / 60000) * variation * 0.2 - - // Add random noise - const randomNoise = (Math.random() - 0.5) * variation * 0.1 - - const value = baseValue + timeVariation + randomNoise - - return Math.max(min, Math.min(max, value)) - } - - /** - * Store health metrics - */ - private storeHealthMetrics(regionId: string, metrics: HealthMetrics): void { - const metricsHistory = this.healthMetrics.get(regionId) ?? [] - metricsHistory.push(metrics) - - // Keep only last 1000 metrics per region (about 8.3 hours at 30-second intervals) - if (metricsHistory.length > 1000) { - metricsHistory.shift() - } - - this.healthMetrics.set(regionId, metricsHistory) - } - - /** - * Calculate health score from metrics - */ - private calculateHealthScore( - regionId: string, - currentMetrics: HealthMetrics, - ): HealthScore { - const metricsHistory = this.healthMetrics.get(regionId) ?? [] - - // Calculate component scores - const performanceScore = this.calculatePerformanceScore( - currentMetrics, - metricsHistory, - ) - const availabilityScore = this.calculateAvailabilityScore( - currentMetrics, - metricsHistory, - ) - const capacityScore = this.calculateCapacityScore( - currentMetrics, - metricsHistory, - ) - const securityScore = this.calculateSecurityScore( - currentMetrics, - metricsHistory, - ) - - // Calculate overall score (weighted average) - const overallScore = Math.round( - performanceScore * 0.4 + - availabilityScore * 0.3 + - capacityScore * 0.2 + - securityScore * 0.1, - ) - - // Determine status - let status: 'healthy' | 'degraded' | 'critical' - if (overallScore >= 80) { - status = 'healthy' - } else if (overallScore >= 60) { - status = 'degraded' - } else { - status = 'critical' - } - - // Calculate trends - const trends = this.calculateTrends(metricsHistory) - - return { - regionId, - overallScore, - componentScores: { - performance: performanceScore, - availability: availabilityScore, - capacity: capacityScore, - security: securityScore, - }, - status, - lastUpdated: new Date(), - trends, - } - } - - /** - * Calculate performance score - */ - private calculatePerformanceScore( - current: HealthMetrics, - history: HealthMetrics[], - ): number { - let score = 100 - - // Response time impact - if (current.responseTime > this.config.thresholds.responseTime) { - const excess = current.responseTime - this.config.thresholds.responseTime - score -= Math.min(30, (excess / this.config.thresholds.responseTime) * 30) - } - - // CPU utilization impact - if (current.cpuUtilization > this.config.thresholds.cpu) { - const excess = current.cpuUtilization - this.config.thresholds.cpu - score -= Math.min(20, (excess / (100 - this.config.thresholds.cpu)) * 20) - } - - // Memory utilization impact - if (current.memoryUtilization > this.config.thresholds.memory) { - const excess = current.memoryUtilization - this.config.thresholds.memory - score -= Math.min( - 20, - (excess / (100 - this.config.thresholds.memory)) * 20, - ) - } - - // Throughput impact (higher is better) - const avgThroughput = - history.length > 0 - ? history.slice(-10).reduce((sum, m) => sum + m.throughput, 0) / 10 - : current.throughput - - if (current.throughput < avgThroughput * 0.8) { - score -= 10 - } - - return Math.max(0, Math.round(score)) - } - - /** - * Calculate availability score - */ - private calculateAvailabilityScore( - current: HealthMetrics, - _history: HealthMetrics[], - ): number { - let score = 100 - - // Availability impact - if (current.availability < this.config.thresholds.availability) { - const deficit = this.config.thresholds.availability - current.availability - score -= Math.min( - 50, - (deficit / this.config.thresholds.availability) * 50, - ) - } - - // Error rate impact - if (current.errorRate > this.config.thresholds.errorRate) { - const excess = current.errorRate - this.config.thresholds.errorRate - score -= Math.min( - 30, - (excess / (5 - this.config.thresholds.errorRate)) * 30, - ) - } - - // Response time impact on availability - if (current.responseTime > this.config.thresholds.responseTime * 2) { - score -= 20 - } - - return Math.max(0, Math.round(score)) - } - - /** - * Calculate capacity score - */ - private calculateCapacityScore( - current: HealthMetrics, - history: HealthMetrics[], - ): number { - let score = 100 - - // Disk utilization impact - if (current.diskUtilization > this.config.thresholds.disk) { - const excess = current.diskUtilization - this.config.thresholds.disk - score -= Math.min(40, (excess / (100 - this.config.thresholds.disk)) * 40) - } - - // Connection count impact (relative to recent history) - const avgConnections = - history.length > 0 - ? history.slice(-10).reduce((sum, m) => sum + m.activeConnections, 0) / - 10 - : current.activeConnections - - if (current.activeConnections > avgConnections * 1.5) { - score -= 20 // High connection count may indicate capacity issues - } - - // Memory utilization impact on capacity - if (current.memoryUtilization > 90) { - score -= 30 // Critical memory usage - } - - return Math.max(0, Math.round(score)) - } - - /** - * Calculate security score - */ - private calculateSecurityScore( - current: HealthMetrics, - history: HealthMetrics[], - ): number { - let score = 100 - - // Error rate impact on security (high error rates may indicate attacks) - if (current.errorRate > 2) { - score -= 20 - } - - // Sudden spike in connections may indicate attack - const avgConnections = - history.length > 0 - ? history.slice(-10).reduce((sum, m) => sum + m.activeConnections, 0) / - 10 - : current.activeConnections - - if (current.activeConnections > avgConnections * 3) { - score -= 30 - } - - // High CPU during low throughput may indicate crypto mining or other malicious activity - if ( - current.cpuUtilization > 80 && - current.throughput < avgConnections * 0.5 - ) { - score -= 25 - } - - return Math.max(0, Math.round(score)) - } - - /** - * Calculate health trends - */ - private calculateTrends(history: HealthMetrics[]): { - '1h': number - '24h': number - '7d': number - } { - const now = new Date() - - const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000) - const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000) - const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) - - const recentMetrics = history.filter((m) => m.timestamp > oneHourAgo) - const dayMetrics = history.filter((m) => m.timestamp > oneDayAgo) - const weekMetrics = history.filter((m) => m.timestamp > sevenDaysAgo) - - const calculateAverageScore = (metrics: HealthMetrics[]): number => { - if (metrics.length === 0) return 100 - - const scores = metrics.map((m) => { - // Simple composite score from key metrics - const availabilityScore = m.availability - const performanceScore = Math.max(0, 100 - m.responseTime / 2) - const errorScore = Math.max(0, 100 - m.errorRate * 20) - - return (availabilityScore + performanceScore + errorScore) / 3 - }) - - return scores.reduce((sum, score) => sum + score, 0) / scores.length - } - - return { - '1h': Math.round(calculateAverageScore(recentMetrics)), - '24h': Math.round(calculateAverageScore(dayMetrics)), - '7d': Math.round(calculateAverageScore(weekMetrics)), - } - } - - /** - * Update health score - */ - private updateHealthScore(regionId: string, healthScore: HealthScore): void { - const previousScore = this.healthScores.get(regionId) - this.healthScores.set(regionId, healthScore) - - // Emit events for significant changes - if (previousScore) { - if (previousScore.status !== healthScore.status) { - this.emit('health-status-changed', { - regionId, - previousStatus: previousScore.status, - newStatus: healthScore.status, - score: healthScore.overallScore, - }) - } - - if ( - Math.abs(previousScore.overallScore - healthScore.overallScore) > 20 - ) { - this.emit('health-score-changed', { - regionId, - previousScore: previousScore.overallScore, - newScore: healthScore.overallScore, - status: healthScore.status, - }) - } - } - } - - /** - * Check for health alerts - */ - private async checkForAlerts( - regionId: string, - metrics: HealthMetrics, - healthScore: HealthScore, - ): Promise { - try { - const alerts: HealthAlert[] = [] - - // Check CPU utilization - if (metrics.cpuUtilization > this.config.thresholds.cpu) { - alerts.push( - this.createAlert( - regionId, - 'performance', - 'warning', - `High CPU utilization: ${metrics.cpuUtilization.toFixed(1)}%`, - metrics, - this.config.thresholds.cpu, - ), - ) - } - - // Check memory utilization - if (metrics.memoryUtilization > this.config.thresholds.memory) { - alerts.push( - this.createAlert( - regionId, - 'performance', - 'warning', - `High memory utilization: ${metrics.memoryUtilization.toFixed(1)}%`, - metrics, - this.config.thresholds.memory, - ), - ) - } - - // Check availability - if (metrics.availability < this.config.thresholds.availability) { - const severity = metrics.availability < 90 ? 'error' : 'warning' - alerts.push( - this.createAlert( - regionId, - 'availability', - severity, - `Low availability: ${metrics.availability.toFixed(1)}%`, - metrics, - this.config.thresholds.availability, - ), - ) - } - - // Check error rate - if (metrics.errorRate > this.config.thresholds.errorRate) { - const severity = metrics.errorRate > 5 ? 'error' : 'warning' - alerts.push( - this.createAlert( - regionId, - 'availability', - severity, - `High error rate: ${metrics.errorRate.toFixed(2)}%`, - metrics, - this.config.thresholds.errorRate, - ), - ) - } - - // Check overall health score - if (healthScore.status === 'critical') { - alerts.push( - this.createAlert( - regionId, - 'capacity', - 'critical', - `Critical health score: ${healthScore.overallScore}`, - metrics, - 60, - ), - ) - } else if (healthScore.status === 'degraded') { - alerts.push( - this.createAlert( - regionId, - 'capacity', - 'warning', - `Degraded health score: ${healthScore.overallScore}`, - metrics, - 80, - ), - ) - } - - // Store and emit alerts - for (const alert of alerts) { - this.activeAlerts.set(alert.id, alert) - this.emit('health-alert', alert) - } - - if (alerts.length > 0) { - logger.info( - `Generated ${alerts.length} health alerts for region: ${regionId}`, - ) - } - } catch (error: unknown) { - logger.error( - `Failed to check for health alerts for region: ${regionId}`, - { error }, - ) - } - } - - /** - * Create health alert - */ - private createAlert( - regionId: string, - type: HealthAlert['type'], - severity: HealthAlert['severity'], - message: string, - metrics: HealthMetrics, - threshold: number, - ): HealthAlert { - return { - id: `alert-${regionId}-${type}-${Date.now()}`, - regionId, - type, - severity, - message, - metrics, - threshold, - timestamp: new Date(), - acknowledged: false, - } - } - - /** - * Get current health score for region - */ - getHealthScore(regionId: string): HealthScore | undefined { - return this.healthScores.get(regionId) - } - - /** - * Get all health scores - */ - getAllHealthScores(): HealthScore[] { - return Array.from(this.healthScores.values()) - } - - /** - * Get health metrics history for region - */ - getHealthMetricsHistory( - regionId: string, - limit: number = 100, - ): HealthMetrics[] { - const metrics = this.healthMetrics.get(regionId) ?? [] - return metrics.slice(-limit) - } - - /** - * Get active health alerts - */ - getActiveAlerts(): HealthAlert[] { - return Array.from(this.activeAlerts.values()) - } - - /** - * Get alerts by region - */ - getAlertsByRegion(regionId: string): HealthAlert[] { - return Array.from(this.activeAlerts.values()).filter( - (alert) => alert.regionId === regionId, - ) - } - - /** - * Acknowledge alert - */ - acknowledgeAlert(alertId: string): boolean { - const alert = this.activeAlerts.get(alertId) - if (alert) { - alert.acknowledged = true - logger.info(`Alert acknowledged: ${alertId}`) - this.emit('alert-acknowledged', { alertId }) - return true - } - return false - } - - /** - * Clear resolved alerts - */ - clearResolvedAlerts(): number { - const now = new Date() - let clearedCount = 0 - - for (const [alertId, alert] of this.activeAlerts.entries()) { - // Clear alerts older than 1 hour that are acknowledged or low severity - const age = now.getTime() - alert.timestamp.getTime() - const shouldClear = - age > 3600000 && (alert.acknowledged || alert.severity === 'info') - - if (shouldClear) { - this.activeAlerts.delete(alertId) - clearedCount++ - } - } - - if (clearedCount > 0) { - logger.info(`Cleared ${clearedCount} resolved alerts`) - } - - return clearedCount - } - - /** - * Get health summary - */ - getHealthSummary(): { - totalRegions: number - healthyRegions: number - degradedRegions: number - criticalRegions: number - activeAlerts: number - averageHealthScore: number - } { - const healthScores = Array.from(this.healthScores.values()) - - const summary = { - totalRegions: healthScores.length, - healthyRegions: healthScores.filter((h) => h.status === 'healthy').length, - degradedRegions: healthScores.filter((h) => h.status === 'degraded') - .length, - criticalRegions: healthScores.filter((h) => h.status === 'critical') - .length, - activeAlerts: this.activeAlerts.size, - averageHealthScore: 0, - } - - if (healthScores.length > 0) { - summary.averageHealthScore = Math.round( - healthScores.reduce((sum, h) => sum + h.overallScore, 0) / - healthScores.length, - ) - } - - return summary - } - - /** - * Cleanup old metrics - */ - private cleanupOldMetrics(): void { - try { - const now = new Date() - const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days - - for (const [regionId, metrics] of this.healthMetrics.entries()) { - const filteredMetrics = metrics.filter( - (m) => now.getTime() - m.timestamp.getTime() < maxAge, - ) - - if (filteredMetrics.length < metrics.length) { - this.healthMetrics.set(regionId, filteredMetrics) - logger.debug( - `Cleaned up ${metrics.length - filteredMetrics.length} old metrics for region: ${regionId}`, - ) - } - } - } catch (error: unknown) { - logger.error('Failed to cleanup old metrics', { error }) - } - } - - /** - * Cleanup resources - */ - async cleanup(): Promise { - try { - logger.info('Cleaning up Health Monitor') - - if (this.healthCheckInterval) { - clearInterval(this.healthCheckInterval) - this.healthCheckInterval = null - } - - if (this.metricsRetentionInterval) { - clearInterval(this.metricsRetentionInterval) - this.metricsRetentionInterval = null - } - - this.healthMetrics.clear() - this.healthScores.clear() - this.activeAlerts.clear() - logger.info('Health Monitor cleanup completed') - } catch (error: unknown) { - logger.error('Health Monitor cleanup failed', { error }) - throw error - } - } -} - -export default HealthMonitor diff --git a/apps/web/src/lib/deployment/multi-region/MultiRegionDeploymentManager.ts b/apps/web/src/lib/deployment/multi-region/MultiRegionDeploymentManager.ts deleted file mode 100644 index b9dc068f35..0000000000 --- a/apps/web/src/lib/deployment/multi-region/MultiRegionDeploymentManager.ts +++ /dev/null @@ -1,736 +0,0 @@ -/** - * Multi-Region Deployment Manager - * - * Manages deployment orchestration across multiple cloud providers and regions - * with automated provisioning, configuration, and health monitoring. - */ - -import { EventEmitter } from 'events' - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' - -const logger = createBuildSafeLogger('MultiRegionDeploymentManager') -import { - CloudProviderManager, - type DeploymentResult, -} from './CloudProviderManager' -import { - ConfigurationManager, - type MultiRegionConfig, -} from './ConfigurationManager' -import { - DeploymentOrchestrator, - type DeploymentOrchestratorConfig, -} from './DeploymentOrchestrator' -import { HealthMonitor, type HealthCheckConfig } from './HealthMonitor' - -export interface RegionConfig { - id: string - name: string - provider: 'aws' | 'gcp' | 'azure' - location: string - availabilityZones: string[] - priority: number - complianceRequirements: string[] - capacity: { - minInstances: number - maxInstances: number - desiredInstances: number - } - networking: { - vpcCidr: string - subnetCidrs: string[] - securityGroups: string[] - } -} - -export interface DeploymentConfig { - regions: RegionConfig[] - globalServices: { - trafficManager: boolean - threatIntelligence: boolean - complianceManager: boolean - } - edgeComputing: { - enabled: boolean - locations: string[] - cacheSize: string - } - dataSync: { - strategy: 'active-active' | 'active-passive' - consistencyLevel: 'strong' | 'eventual' - conflictResolution: 'timestamp' | 'vector-clock' - } - failover: { - automatic: boolean - detectionTime: number - recoveryTime: number - healthCheckInterval: number - maxDataSyncLag: number - failoverCooldown: number - snsTopicArn?: string - sqsQueueUrl?: string - } -} - -export interface DeploymentStatus { - regionId: string - status: 'pending' | 'deploying' | 'healthy' | 'degraded' | 'failed' - lastDeployment: Date - healthScore: number - activeInstances: number - errors: string[] - metrics: { - latency: number - throughput: number - errorRate: number - } -} - -type HealthCheckFailureEvent = { - regionId: string - failureReason: string -} - -type HealthCheckRecoveryEvent = { - regionId: string -} - -const isHealthCheckFailureEvent = ( - data: unknown, -): data is HealthCheckFailureEvent => { - if (typeof data !== 'object' || data === null) { - return false - } - if (!('regionId' in data) || !('failureReason' in data)) { - return false - } - return ( - typeof data.regionId === 'string' && typeof data.failureReason === 'string' - ) -} - -const isHealthCheckRecoveryEvent = ( - data: unknown, -): data is HealthCheckRecoveryEvent => { - if (typeof data !== 'object' || data === null) { - return false - } - return 'regionId' in data && typeof data.regionId === 'string' -} - -export class MultiRegionDeploymentManager extends EventEmitter { - private readonly config: DeploymentConfig - private readonly configurationManager: ConfigurationManager - private readonly cloudProviderManager: CloudProviderManager - private readonly healthMonitor: HealthMonitor - private readonly deploymentStatuses: Map = new Map() - private isInitialized = false - - constructor(config: DeploymentConfig) { - super() - this.config = config - this.configurationManager = new ConfigurationManager( - this.createMultiRegionConfig(config), - ) - this.cloudProviderManager = new CloudProviderManager() - this.healthMonitor = new HealthMonitor(this.createHealthCheckConfig()) - void new DeploymentOrchestrator( - this.createDeploymentOrchestratorConfig(), - this.cloudProviderManager, - ) - } - - private createMultiRegionConfig( - deploymentConfig: DeploymentConfig, - ): MultiRegionConfig { - const environmentTemplate = { - regions: deploymentConfig.regions, - scaling: { - autoScaling: true, - minInstances: 2, - maxInstances: 20, - targetCpuUtilization: 70, - targetMemoryUtilization: 80, - }, - resources: { - cpu: '4', - memory: '8Gi', - storage: '100Gi', - }, - networking: { - vpcCidr: '10.0.0.0/16', - subnetCidrs: ['10.0.1.0/24'], - securityGroups: ['default'], - loadBalancers: ['primary'], - }, - monitoring: { - enabled: true, - samplingRate: 0.1, - alertThresholds: {}, - }, - } satisfies MultiRegionConfig['environments']['development'] - - return { - deployment: deploymentConfig, - edgeComputing: { - locations: [], - services: { - threatDetection: false, - biasDetection: false, - cacheService: false, - apiGateway: true, - staticContent: true, - }, - aiModels: { - threatDetection: 'none', - biasDetection: 'none', - behavioralAnalysis: 'none', - }, - cacheStrategies: ['LRU'], - healthCheck: { - interval: 30_000, - timeout: 5_000, - retries: 3, - }, - }, - trafficRouting: { - strategy: 'health-based', - healthThreshold: 95, - latencyThreshold: 500, - complianceRequirements: ['HIPAA'], - weights: {}, - fallbackRegions: ['us-east-1'], - cacheTtl: 300, - }, - environments: { - development: environmentTemplate, - staging: { - ...environmentTemplate, - scaling: { - ...environmentTemplate.scaling, - minInstances: 3, - }, - }, - production: { - ...environmentTemplate, - scaling: { - ...environmentTemplate.scaling, - minInstances: 4, - }, - }, - }, - featureFlags: { - multiRegionDeployment: true, - edgeComputing: false, - intelligentRouting: true, - autoFailover: true, - threatDetection: true, - biasDetection: true, - complianceChecking: true, - performanceMonitoring: true, - aiModelServing: true, - cacheOptimization: true, - }, - secrets: { - cloudProviders: { - aws: { - accessKeyId: '', - secretAccessKey: '', - region: 'us-east-1', - }, - gcp: { - projectId: '', - keyFilename: '', - }, - azure: { - subscriptionId: '', - clientId: '', - clientSecret: '', - tenantId: '', - }, - }, - databases: { - cockroachdb: { - connectionString: '', - sslMode: 'require', - }, - redis: { - url: '', - password: '', - }, - }, - aiServices: { - openai: { - apiKey: '', - organization: '', - }, - google: { - apiKey: '', - projectId: '', - }, - }, - monitoring: { - sentry: { - dsn: '', - authToken: '', - }, - datadog: { - apiKey: '', - appKey: '', - }, - }, - }, - monitoring: { - metrics: { - enabled: true, - interval: 30_000, - retention: 604_800, - aggregation: 'mean', - }, - alerting: { - enabled: true, - channels: ['email'], - severityLevels: ['critical', 'warning'], - escalationRules: {}, - }, - logging: { - level: 'info', - format: 'json', - destinations: ['console'], - sampling: 0.1, - }, - }, - compliance: { - gdpr: { - enabled: true, - dataResidency: ['US'], - retentionPeriods: {}, - subjectRights: ['access', 'erasure'], - }, - hipaa: { - enabled: true, - encryptionRequired: true, - auditLogging: true, - accessControls: ['mfa'], - }, - soc2: { - enabled: true, - auditFrequency: 'weekly', - controls: ['CCM', 'CC7'], - }, - pci: { - enabled: false, - requirements: [], - scanningFrequency: 'monthly', - }, - }, - } - } - - private createHealthCheckConfig(): HealthCheckConfig { - return { - interval: 30_000, - timeout: 5_000, - retries: 3, - thresholds: { - cpu: 80, - memory: 85, - disk: 90, - responseTime: 500, - errorRate: 5, - availability: 95, - }, - } - } - - private createDeploymentOrchestratorConfig(): DeploymentOrchestratorConfig { - return { - maxParallelDeployments: 3, - rollbackOnFailure: true, - healthCheckTimeout: 5_000, - deploymentTimeout: 120_000, - retryAttempts: 2, - retryDelay: 1_000, - dependencies: { - infrastructure: ['network', 'dns', 'iam'], - services: ['api', 'worker'], - monitoring: ['metrics', 'alerts'], - }, - } - } - - /** - * Initialize the multi-region deployment manager - */ - async initialize(): Promise { - try { - logger.info('Initializing Multi-Region Deployment Manager') - - // Initialize configuration manager - await this.configurationManager.initialize() - - // Initialize cloud provider connections - await this.cloudProviderManager.initialize(this.config.regions) - - // Initialize health monitoring - await this.healthMonitor.initialize(this.config.regions) - - // Setup event listeners - this.setupEventListeners() - - this.isInitialized = true - logger.info('Multi-Region Deployment Manager initialized successfully') - - this.emit('initialized', { regions: this.config.regions.length }) - } catch (error: unknown) { - logger.error('Failed to initialize Multi-Region Deployment Manager', { - error, - }) - throw new Error( - `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - { - cause: error, - }, - ) - } - } - - /** - * Deploy infrastructure across all configured regions - */ - async deployAllRegions(): Promise { - if (!this.isInitialized) { - throw new Error('Deployment manager not initialized') - } - - try { - logger.info('Starting multi-region deployment', { - regions: this.config.regions.length, - }) - - const deploymentPromises = this.config.regions.map(async (region) => - this.deployRegion(region), - ) - - const results = await Promise.allSettled(deploymentPromises) - - const statuses: DeploymentStatus[] = [] - results.forEach((result, index) => { - const region = this.config.regions[index] - if (result.status === 'fulfilled') { - statuses.push(result.value) - } else { - statuses.push({ - regionId: region.id, - status: 'failed', - lastDeployment: new Date(), - healthScore: 0, - activeInstances: 0, - errors: [ - result.reason instanceof Error - ? result.reason.message - : 'Deployment failed', - ], - metrics: { latency: 0, throughput: 0, errorRate: 1 }, - }) - } - }) - - this.emit('deployment-complete', { statuses }) - return statuses - } catch (error: unknown) { - logger.error('Multi-region deployment failed', { error }) - throw new Error( - `Deployment failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - { cause: error }, - ) - } - } - - /** - * Deploy infrastructure to a specific region - */ - private async deployRegion(region: RegionConfig): Promise { - const startTime = Date.now() - - try { - logger.info(`Deploying to region: ${region.name}`, { region: region.id }) - - // Validate region configuration - await this.validateRegionConfig(region) - - // Deploy infrastructure using cloud provider manager - const deploymentResult = - await this.cloudProviderManager.deployRegion(region) - - // Configure region-specific services - await this.configureRegionalServices(region, deploymentResult) - - // Update deployment status - const status: DeploymentStatus = { - regionId: region.id, - status: 'healthy', - lastDeployment: new Date(), - healthScore: 100, - activeInstances: region.capacity.desiredInstances, - errors: [], - metrics: { - latency: 0, - throughput: 0, - errorRate: 0, - }, - } - - this.deploymentStatuses.set(region.id, status) - - logger.info(`Region deployment completed: ${region.name}`, { - region: region.id, - duration: Date.now() - startTime, - }) - - this.emit('region-deployed', { region: region.id, status }) - return status - } catch (error: unknown) { - logger.error(`Region deployment failed: ${region.name}`, { - region: region.id, - error, - }) - - const status: DeploymentStatus = { - regionId: region.id, - status: 'failed', - lastDeployment: new Date(), - healthScore: 0, - activeInstances: 0, - errors: [error instanceof Error ? error.message : 'Unknown error'], - metrics: { latency: 0, throughput: 0, errorRate: 1 }, - } - - this.deploymentStatuses.set(region.id, status) - this.emit('region-deployment-failed', { - region: region.id, - error: error instanceof Error ? error.message : 'Unknown error', - }) - - throw error - } - } - - /** - * Validate region configuration before deployment - */ - private async validateRegionConfig(region: RegionConfig): Promise { - const errors: string[] = [] - - if (!region.id || !region.name) { - errors.push('Region ID and name are required') - } - - if (region.availabilityZones.length === 0) { - errors.push('At least one availability zone is required') - } - - if (!region.networking.vpcCidr) { - errors.push('VPC CIDR is required') - } - - if (region.capacity.minInstances < 1) { - errors.push('Minimum instances must be at least 1') - } - - if (errors.length > 0) { - throw new Error(`Invalid region configuration: ${errors.join(', ')}`) - } - } - - /** - * Configure region-specific services - */ - private async configureRegionalServices( - region: RegionConfig, - deploymentResult: DeploymentResult, - ): Promise { - try { - // Configure load balancers - await this.configureLoadBalancers(region, deploymentResult) - - // Configure auto-scaling - await this.configureAutoScaling(region, deploymentResult) - - // Configure monitoring and alerting - await this.configureMonitoring(region, deploymentResult) - - // Configure security policies - await this.configureSecurityPolicies(region, deploymentResult) - - logger.info(`Regional services configured for: ${region.name}`) - } catch (error: unknown) { - logger.error( - `Failed to configure regional services for: ${region.name}`, - { error }, - ) - throw new Error( - `Service configuration failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - { - cause: error, - }, - ) - } - } - - /** - * Configure load balancers for the region - */ - private async configureLoadBalancers( - region: RegionConfig, - _deploymentResult: DeploymentResult, - ): Promise { - // Implementation for load balancer configuration - logger.info(`Configuring load balancers for region: ${region.name}`) - } - - /** - * Configure auto-scaling for the region - */ - private async configureAutoScaling( - region: RegionConfig, - _deploymentResult: DeploymentResult, - ): Promise { - // Implementation for auto-scaling configuration - logger.info(`Configuring auto-scaling for region: ${region.name}`) - } - - /** - * Configure monitoring and alerting for the region - */ - private async configureMonitoring( - region: RegionConfig, - _deploymentResult: DeploymentResult, - ): Promise { - // Implementation for monitoring configuration - logger.info(`Configuring monitoring for region: ${region.name}`) - } - - /** - * Configure security policies for the region - */ - private async configureSecurityPolicies( - region: RegionConfig, - _deploymentResult: DeploymentResult, - ): Promise { - // Implementation for security policy configuration - logger.info(`Configuring security policies for region: ${region.name}`) - } - - /** - * Get deployment status for all regions - */ - getDeploymentStatuses(): DeploymentStatus[] { - return Array.from(this.deploymentStatuses.values()) - } - - /** - * Get deployment status for a specific region - */ - getRegionStatus(regionId: string): DeploymentStatus | undefined { - return this.deploymentStatuses.get(regionId) - } - - /** - * Update region capacity - */ - async updateRegionCapacity( - regionId: string, - capacity: RegionConfig['capacity'], - ): Promise { - const region = this.config.regions.find((r) => r.id === regionId) - if (!region) { - throw new Error(`Region not found: ${regionId}`) - } - - region.capacity = capacity - - // Apply capacity changes - await this.cloudProviderManager.updateCapacity(regionId, capacity) - - logger.info(`Updated capacity for region: ${regionId}`, { capacity }) - } - - /** - * Setup event listeners - */ - private setupEventListeners(): void { - this.healthMonitor.on('health-check-failed', (data) => { - logger.warn('Health check failed', data) - void this.handleHealthCheckFailure(data) - }) - - this.healthMonitor.on('health-check-recovered', (data) => { - logger.info('Health check recovered', data) - void this.handleHealthCheckRecovery(data) - }) - } - - /** - * Handle health check failure - */ - private async handleHealthCheckFailure(data: unknown): Promise { - if (isHealthCheckFailureEvent(data)) { - const { regionId, failureReason } = data - const status = this.deploymentStatuses.get(regionId) - if (status) { - status.status = 'degraded' - status.errors.push(`Health check failed: ${failureReason}`) - - this.emit('region-degraded', { regionId, reason: failureReason }) - } - } - } - - /** - * Handle health check recovery - */ - private async handleHealthCheckRecovery(data: unknown): Promise { - if (isHealthCheckRecoveryEvent(data)) { - const { regionId } = data - const status = this.deploymentStatuses.get(regionId) - if (status) { - status.status = 'healthy' - status.errors = status.errors.filter( - (e) => !e.includes('Health check failed'), - ) - - this.emit('region-recovered', { regionId }) - } - } - } - - /** - * Cleanup resources - */ - async cleanup(): Promise { - try { - logger.info('Cleaning up Multi-Region Deployment Manager') - - // Cleanup cloud provider resources - await this.cloudProviderManager.cleanup() - - // Cleanup health monitoring - await this.healthMonitor.cleanup() - - // Cleanup configuration manager - await this.configurationManager.cleanup() - - this.deploymentStatuses.clear() - this.isInitialized = false - - logger.info('Multi-Region Deployment Manager cleanup completed') - } catch (error: unknown) { - logger.error('Cleanup failed', { error }) - throw error - } - } -} - -export default MultiRegionDeploymentManager diff --git a/apps/web/src/lib/deployment/multi-region/ServiceDiscoveryManager.ts b/apps/web/src/lib/deployment/multi-region/ServiceDiscoveryManager.ts deleted file mode 100644 index 7a0d66ef4e..0000000000 --- a/apps/web/src/lib/deployment/multi-region/ServiceDiscoveryManager.ts +++ /dev/null @@ -1,1858 +0,0 @@ -import { EventEmitter } from 'events' - -import Consul from 'consul' -import { Etcd3 } from 'etcd3' -import { v4 as uuidv4 } from 'uuid' -import ZooKeeper from 'zookeeper' - -import { Logger } from '../../utils/logger' -import { ConfigurationManager } from './ConfigurationManager' -import { HealthMonitor } from './HealthMonitor' - -type ConsulClient = { - agent: { - self: () => Promise - service: { - register: (params: Record) => Promise - deregister: (id: string) => Promise - } - } - health: { - service: (serviceName: string) => Promise - } -} - -type EtcdClient = InstanceType -type ZooKeeperClient = { - connect: (connectString: string) => void - create: (path: string, data?: string) => Promise - remove: (path: string, version?: number) => Promise - exists: (path: string) => Promise - getData: (path: string) => Promise - getChildren: (path: string) => Promise - close: () => void -} -type DiscoveryConsulBackend = { - type: 'consul' - client: ConsulClient - region: string -} -type DiscoveryEtcdBackend = { - type: 'etcd' - client: EtcdClient - region: string -} -type DiscoveryZooKeeperBackend = { - type: 'zookeeper' - client: ZooKeeperClient - region: string -} -type DiscoveryBackendRecord = - | DiscoveryConsulBackend - | DiscoveryEtcdBackend - | DiscoveryZooKeeperBackend -type ServiceLoadBalancerConfig = { - name: string - loadBalancing?: string - healthCheck?: Record - circuitBreaker?: Record -} -type ConsulServiceDiscoveryEntry = { - Service: { - ID: string - Service: string - Address?: string - Port: number - Tags?: string[] - Meta?: Record - } - Node: { - Datacenter: string - Address: string - } - Checks?: Array<{ Status: string }> -} -type ServiceDiscoveryConfig = { - consul?: { - enabled?: boolean - host: string - port: number - secure?: boolean - caCert?: string - aclToken?: string - } - etcd?: { - enabled?: boolean - endpoints: string[] - username: string - password: string - tls?: boolean - caCert?: string - clientCert?: string - clientKey?: string - } - zookeeper?: { - enabled?: boolean - connect: string - timeout: number - debugLevel?: string - } - heartbeatInterval?: number - cleanupInterval?: number -} - -const DEFAULT_SERVICE_DISCOVERY_CONFIG: ServiceDiscoveryConfig = { - heartbeatInterval: 30000, - cleanupInterval: 300000, -} - -function isServiceLoadBalancerConfig( - value: unknown, -): value is ServiceLoadBalancerConfig { - return ( - isRecord(value) && - typeof value['name'] === 'string' && - value['name'].length > 0 - ) -} - -function isServiceDiscoveryConfig( - value: unknown, -): value is ServiceDiscoveryConfig { - if (!isRecord(value)) { - return false - } - - return ( - value['consul'] === undefined || - isRecord(value['consul']) || - value['etcd'] === undefined || - isRecord(value['etcd']) || - value['zookeeper'] === undefined || - isRecord(value['zookeeper']) - ) -} - -function getServiceList(config: unknown): ServiceLoadBalancerConfig[] { - if (!isRecord(config)) { - return [] - } - - const deploymentServices = isRecord(config['deployment']) - ? config['deployment']['services'] - : undefined - const serviceList = Array.isArray(deploymentServices) - ? deploymentServices - : Array.isArray(config['services']) - ? config['services'] - : [] - - return serviceList.filter(isServiceLoadBalancerConfig) -} - -function getServiceDiscoveryConfigFrom( - config: unknown, -): ServiceDiscoveryConfig { - if (isServiceDiscoveryConfig(config)) { - return config - } - - return DEFAULT_SERVICE_DISCOVERY_CONFIG -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null -} - -function asString(value: unknown): string | undefined { - return typeof value === 'string' ? value : undefined -} - -function asNumber(value: unknown): number | undefined { - return typeof value === 'number' ? value : undefined -} - -function asStringArrayValue(value: unknown): string[] { - return Array.isArray(value) - ? value.filter( - (item): item is string => item != null && typeof item === 'string', - ) - : [] -} - -function toDiscoveredServiceInstance( - serviceName: string, - region: string, - instanceId: string, - payload: unknown, -): ServiceInstance | null { - if (!isRecord(payload)) { - return null - } - - const host = asString(payload['host']) - const port = asNumber(payload['port']) - - if (!host || port === undefined) { - return null - } - - const metadataCandidate = isRecord(payload['metadata']) - ? payload['metadata'] - : {} - const statusCandidate = asString(payload['status']) - const status = - statusCandidate === 'healthy' || statusCandidate === 'unhealthy' - ? statusCandidate - : 'unknown' - const registeredAtValue = new Date( - asString(payload['registeredAt']) ?? - asNumber(payload['registeredAt']) ?? - Date.now(), - ) - - return { - id: instanceId, - name: serviceName, - region, - host, - port, - protocol: asString(payload['protocol']) ?? 'http', - version: asString(payload['version']) ?? '1.0.0', - metadata: metadataCandidate, - status, - registeredAt: registeredAtValue, - lastHeartbeat: new Date(), - tags: asStringArrayValue(payload['tags']), - } -} - -function isConsulServiceDiscoveryEntry( - value: unknown, -): value is ConsulServiceDiscoveryEntry { - if (!isRecord(value)) { - return false - } - - const service = isRecord(value['Service']) ? value['Service'] : undefined - const node = isRecord(value['Node']) ? value['Node'] : undefined - - return ( - typeof value['Service'] === 'object' && - service !== undefined && - typeof service['ID'] === 'string' && - typeof service['Service'] === 'string' && - typeof service['Port'] === 'number' && - typeof value['Node'] === 'object' && - node !== undefined && - typeof node['Datacenter'] === 'string' && - typeof node['Address'] === 'string' - ) -} - -function isConsulClient(value: unknown): value is ConsulClient { - if ( - !isRecord(value) || - !isRecord(value['agent']) || - !isRecord(value['health']) - ) { - return false - } - - const agent = value['agent'] - const health = value['health'] - const consulService = isRecord(agent['service']) - ? agent['service'] - : undefined - - return ( - typeof agent['self'] === 'function' && - typeof agent['service'] === 'object' && - agent['service'] !== null && - consulService !== undefined && - typeof consulService['register'] === 'function' && - typeof consulService['deregister'] === 'function' && - typeof health['service'] === 'function' - ) -} - -/** - * Service Discovery Manager - * Manages service registration and discovery across multiple regions - */ -export class ServiceDiscoveryManager extends EventEmitter { - private readonly logger: Logger - private readonly config: ConfigurationManager - private readonly healthMonitor: HealthMonitor - private readonly consulClients: Map = new Map() - private readonly etcdClients: Map = new Map() - private readonly zookeeperClients: Map = new Map() - private readonly serviceRegistry: Map = new Map() - private readonly discoveryBackends: Map = - new Map() - private heartbeatInterval: NodeJS.Timeout | null = null - private cleanupInterval: NodeJS.Timeout | null = null - private readonly serviceCache: Map = new Map() - private readonly loadBalancers: Map = new Map() - private isInitialized = false - - constructor(config: ConfigurationManager, healthMonitor: HealthMonitor) { - super() - this.config = config - this.healthMonitor = healthMonitor - this.logger = new Logger({ prefix: 'ServiceDiscoveryManager' }) - } - - private getRegions(): string[] { - return this.config - .getConfig() - .deployment.regions.map((region) => region.id) - .filter((regionId): regionId is string => Boolean(regionId)) - } - - private getServiceDiscoveryConfig(): ServiceDiscoveryConfig { - const appConfig = this.config.getConfig() as unknown - const candidate = isRecord(appConfig) - ? (appConfig as { serviceDiscovery?: unknown }).serviceDiscovery - : undefined - - return getServiceDiscoveryConfigFrom(candidate) - } - - private getServices(): ServiceLoadBalancerConfig[] { - return getServiceList(this.config.getConfig()) - } - - private registerHealthCheck( - name: string, - check: () => Promise<{ - status: 'healthy' | 'unhealthy' | 'degraded' - message: string - }>, - ): void { - void check().then((result) => { - if (result.status !== 'healthy') { - this.healthMonitor.emit('health-check-failed', { - component: name, - status: result.status, - message: result.message, - }) - } - }) - } - - /** - * Initialize the service discovery manager - */ - async initialize(): Promise { - if (this.isInitialized) return - try { - this.logger.info('Initializing ServiceDiscoveryManager...') - - // Initialize discovery backends - await this.initializeDiscoveryBackends() - - // Initialize load balancers - await this.initializeLoadBalancers() - - // Set up service registration - await this.setupServiceRegistration() - - // Set up health checking - await this.setupHealthChecking() - - // Start background processes - this.startBackgroundProcesses() - - this.isInitialized = true - this.logger.info('ServiceDiscoveryManager initialized successfully') - - this.emit('initialized') - } catch (error: unknown) { - this.logger.error('Failed to initialize ServiceDiscoveryManager', { - error, - }) - throw error - } - } - - /** - * Initialize discovery backends - */ - private async initializeDiscoveryBackends(): Promise { - const regions = this.getRegions() - const discoveryConfig = this.getServiceDiscoveryConfig() - - for (const region of regions) { - try { - // Initialize Consul - if (discoveryConfig.consul?.enabled) { - await this.initializeConsul(region, discoveryConfig.consul) - } - - // Initialize etcd - if (discoveryConfig.etcd?.enabled) { - await this.initializeEtcd(region, discoveryConfig.etcd) - } - - // Initialize ZooKeeper - if (discoveryConfig.zookeeper?.enabled) { - await this.initializeZookeeper(region, discoveryConfig.zookeeper) - } - - this.logger.info(`Discovery backends initialized for region: ${region}`) - } catch (error: unknown) { - this.logger.error( - `Failed to initialize discovery backends for region: ${region}`, - { error }, - ) - throw error - } - } - } - - /** - * Initialize Consul client - */ - private async initializeConsul( - region: string, - config: NonNullable, - ): Promise { - try { - const consulConfig: Record = { - host: config.host.replace('{region}', region), - port: config.port, - secure: config.secure, - ca: config.caCert, - defaults: { - token: config.aclToken, - }, - } - - const consulCandidate = Consul(consulConfig) - if (!isConsulClient(consulCandidate)) { - throw new TypeError('Invalid Consul client returned') - } - const consul = consulCandidate - - // Test connection - await consul.agent.self() - - this.consulClients.set(region, consul) - this.discoveryBackends.set(`${region}:consul`, { - type: 'consul', - client: consul, - region, - }) - - this.logger.info(`Consul client initialized for region: ${region}`) - } catch (error: unknown) { - this.logger.error(`Failed to initialize Consul for region: ${region}`, { - error, - }) - throw error - } - } - - /** - * Initialize etcd client - */ private async initializeEtcd( - region: string, - _config: NonNullable, - ): Promise { - try { - const etcd = new Etcd3() - - // Test connection - await etcd.get('/') - - this.etcdClients.set(region, etcd) - this.discoveryBackends.set(`${region}:etcd`, { - type: 'etcd', - client: etcd, - region, - }) - - this.logger.info(`etcd client initialized for region: ${region}`) - } catch (error: unknown) { - this.logger.error(`Failed to initialize etcd for region: ${region}`, { - error, - }) - throw error - } - } - - /** - * Initialize ZooKeeper client - */ - private async initializeZookeeper( - region: string, - config: NonNullable, - ): Promise { - try { - const zookeeper = new ZooKeeper() - zookeeper.connect(config.connect.replace('{region}', region)) - - this.zookeeperClients.set(region, zookeeper) - this.discoveryBackends.set(`${region}:zookeeper`, { - type: 'zookeeper', - client: zookeeper, - region, - }) - - this.logger.info(`ZooKeeper client initialized for region: ${region}`) - } catch (error: unknown) { - this.logger.error( - `Failed to initialize ZooKeeper for region: ${region}`, - { error }, - ) - throw error - } - } - - /** - * Initialize load balancers - */ - private async initializeLoadBalancers(): Promise { - const services = this.getServices() - - for (const service of services) { - const loadBalancer = new LoadBalancer({ - serviceName: service.name, - algorithm: service.loadBalancing ?? 'round-robin', - healthCheck: service.healthCheck, - circuitBreaker: service.circuitBreaker, - }) - - this.loadBalancers.set(service.name, loadBalancer) - } - - this.logger.info(`Initialized ${this.loadBalancers.size} load balancers`) - } - - /** - * Set up service registration - */ - private async setupServiceRegistration(): Promise { - // Register health checks for service discovery - this.registerHealthCheck('service-discovery', async () => { - try { - const healthyBackends = await this.getHealthyBackends() - const totalBackends = this.discoveryBackends.size - - if (healthyBackends.length === 0) { - return { - status: 'unhealthy', - message: 'No healthy discovery backends', - } - } - - if (healthyBackends.length < totalBackends * 0.5) { - return { - status: 'degraded', - message: `Only ${healthyBackends.length}/${totalBackends} backends healthy`, - } - } - - return { - status: 'healthy', - message: `${healthyBackends.length}/${totalBackends} backends healthy`, - } - } catch (error: unknown) { - return { - status: 'unhealthy', - message: `Service discovery check failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - } - } - }) - } - - /** - * Get healthy discovery backends - */ - private async getHealthyBackends(): Promise { - const healthyBackends: DiscoveryBackendRecord[] = [] - - for (const backend of this.discoveryBackends.values()) { - try { - const isHealthy = await this.checkBackendHealth(backend) - if (isHealthy) { - healthyBackends.push(backend) - } - } catch (error: unknown) { - this.logger.warn(`Backend health check failed`, { - backend: backend.type, - region: backend.region, - error, - }) - } - } - - return healthyBackends - } - - /** - * Check backend health - */ - private async checkBackendHealth( - backend: DiscoveryBackendRecord, - ): Promise { - try { - switch (backend.type) { - case 'consul': { - const consul = backend.client - await consul.agent.self() - return true - } - case 'etcd': { - const etcd = backend.client - await etcd.get('/') - return true - } - case 'zookeeper': { - const zk = backend.client - await zk.getChildren('/services') - return true - } - default: { - return false - } - } - } catch (error: unknown) { - return false - } - } - - /** - * Set up health checking - */ - private async setupHealthChecking(): Promise { - // Register health checks for registered services - for (const [serviceName, instances] of this.serviceRegistry) { - this.registerHealthCheck(`service-${serviceName}`, async () => { - try { - const healthyInstances = instances.filter( - (instance) => instance.status === 'healthy', - ) - const totalInstances = instances.length - - if (healthyInstances.length === 0) { - return { - status: 'unhealthy', - message: `No healthy instances for ${serviceName}`, - } - } - - if (healthyInstances.length < totalInstances * 0.5) { - return { - status: 'degraded', - message: `Only ${healthyInstances.length}/${totalInstances} instances healthy for ${serviceName}`, - } - } - - return { - status: 'healthy', - message: `${healthyInstances.length}/${totalInstances} instances healthy for ${serviceName}`, - } - } catch (error: unknown) { - return { - status: 'unhealthy', - message: `Service health check failed for ${serviceName}: ${error instanceof Error ? error.message : 'Unknown error'}`, - } - } - }) - } - } - - /** - * Start background processes - */ - private startBackgroundProcesses(): void { - const config = this.getServiceDiscoveryConfig() - - // Start heartbeat process - this.heartbeatInterval = setInterval(() => { - this.sendHeartbeats().catch((error) => { - const err = error instanceof Error ? error : new Error(String(error)) - this.logger.error('Heartbeat process failed', { error: err }) - }) - }, config.heartbeatInterval ?? 30000) - - // Start cleanup process - this.cleanupInterval = setInterval(() => { - this.cleanupExpiredServices().catch((error) => { - const err = error instanceof Error ? error : new Error(String(error)) - this.logger.error('Cleanup process failed', { error: err }) - }) - }, config.cleanupInterval ?? 300000) - - this.logger.info('Background processes started') - } - - /** - * Register a service instance - */ - async registerService(service: ServiceRegistration): Promise { - try { - this.logger.info('Registering service', { - name: service.name, - instance: service.instanceId, - region: service.region, - }) - - // Create service instance - const instance: ServiceInstance = { - id: service.instanceId ?? uuidv4(), - name: service.name, - region: service.region, - host: service.host, - port: service.port, - protocol: service.protocol ?? 'http', - version: service.version ?? '1.0.0', - metadata: service.metadata ?? {}, - status: 'healthy', - registeredAt: new Date(), - lastHeartbeat: new Date(), - tags: service.tags ?? [], - } - - // Register with all backends - await this.registerWithBackends(instance) - - // Add to local registry - if (!this.serviceRegistry.has(service.name)) { - this.serviceRegistry.set(service.name, []) - } - - const instances = this.serviceRegistry.get(service.name)! - const existingIndex = instances.findIndex((i) => i.id === instance.id) - - if (existingIndex >= 0) { - instances[existingIndex] = instance - } else { - instances.push(instance) - } - - // Update load balancer - const loadBalancer = this.loadBalancers.get(service.name) - if (loadBalancer) { - loadBalancer.updateInstances(instances) - } - - this.logger.info('Service registered successfully', { - name: service.name, - instance: instance.id, - }) - - this.emit('serviceRegistered', instance) - } catch (error: unknown) { - this.logger.error('Failed to register service', { service, error }) - throw error - } - } - - /** - * Register service with all discovery backends - */ - private async registerWithBackends(instance: ServiceInstance): Promise { - const backends = await this.getHealthyBackends() - const registrationPromises: Promise[] = [] - for (const backend of backends) { - registrationPromises.push(this.registerWithBackend(backend, instance)) - } - - await Promise.allSettled(registrationPromises) - } - - /** - * Register service with specific backend - */ - private async registerWithBackend( - backend: DiscoveryBackendRecord, - instance: ServiceInstance, - ): Promise { - try { - switch (backend.type) { - case 'consul': { - await this.registerWithConsul(backend.client, instance) - break - } - case 'etcd': { - await this.registerWithEtcd(backend.client, instance) - break - } - case 'zookeeper': { - await this.registerWithZookeeper(backend.client, instance) - break - } - } - } catch (error: unknown) { - this.logger.error(`Failed to register with ${backend.type}`, { - backend: backend.type, - region: backend.region, - instance: instance.id, - error, - }) - throw error - } - } - - /** - * Register with Consul - */ - private async registerWithConsul( - consul: ConsulClient, - instance: ServiceInstance, - ): Promise { - const service = { - ID: instance.id, - Name: instance.name, - Tags: [...instance.tags, instance.region, `version:${instance.version}`], - Address: instance.host, - Port: instance.port, - Meta: instance.metadata, - Check: { - HTTP: `${instance.protocol}://${instance.host}:${instance.port}/health`, - Interval: '30s', - Timeout: '5s', - DeregisterCriticalServiceAfter: '2m', - }, - } - - await consul.agent.service.register(service) - } - - /** - * Register with etcd - */ - private async registerWithEtcd( - etcd: EtcdClient, - instance: ServiceInstance, - ): Promise { - const key = `/services/${instance.name}/${instance.region}/${instance.id}` - const value = JSON.stringify({ - host: instance.host, - port: instance.port, - protocol: instance.protocol, - version: instance.version, - metadata: instance.metadata, - tags: instance.tags, - registeredAt: instance.registeredAt, - status: instance.status, - }) - - await etcd.put(key, value) - } - - /** - * Register with ZooKeeper - */ - private async registerWithZookeeper( - zk: ZooKeeperClient, - instance: ServiceInstance, - ): Promise { - const basePath = `/services/${instance.name}/${instance.region}` - const instancePath = `${basePath}/${instance.id}` - - // Create parent paths if they don't exist - await this.createZookeeperPath(zk, basePath) - - const data = JSON.stringify({ - host: instance.host, - port: instance.port, - protocol: instance.protocol, - version: instance.version, - metadata: instance.metadata, - tags: instance.tags, - registeredAt: instance.registeredAt, - status: instance.status, - }) - - await zk.create(instancePath, data) - } - - /** - * Create ZooKeeper path recursively - */ - private async createZookeeperPath( - zk: ZooKeeperClient, - path: string, - ): Promise { - const parts = path.split('/').filter((p) => p) - let currentPath = '' - - for (const part of parts) { - currentPath += `/${part}` - const exists = await zk.exists(currentPath) - if (!exists) { - try { - await zk.create(currentPath, '') - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - if ( - !message.includes('Node exists') && - !message.includes('ZNODEEXISTS') - ) { - throw error - } - } - } - } - } - - /** - * Discover service instances - */ - async discoverService( - serviceName: string, - options: DiscoveryOptions = {}, - ): Promise { - try { - this.logger.debug('Discovering service', { serviceName, options }) - - // Check cache first - const cacheKey = this.getCacheKey(serviceName, options) - const cached = this.serviceCache.get(cacheKey) - - if (cached && !this.isCacheExpired(cached)) { - this.logger.debug('Returning cached service instances', { - serviceName, - count: cached.instances.length, - }) - return cached.instances - } - - // Discover from backends - const instances = await this.discoverFromBackends(serviceName, options) - - // Filter instances - const filteredInstances = this.filterInstances(instances, options) - - // Sort instances - const sortedInstances = this.sortInstances(filteredInstances, options) - - // Cache results - this.serviceCache.set(cacheKey, { - instances: sortedInstances, - timestamp: new Date(), - ttl: options.cacheTTL ?? 30000, // 30 seconds default - }) - - this.logger.debug('Service discovery completed', { - serviceName, - total: instances.length, - filtered: sortedInstances.length, - }) - - return sortedInstances - } catch (error: unknown) { - this.logger.error('Service discovery failed', { - serviceName, - options, - error, - }) - - // Return cached results if available, even if expired - const cacheKey = this.getCacheKey(serviceName, options) - const cached = this.serviceCache.get(cacheKey) - if (cached) { - this.logger.warn('Returning expired cache due to discovery failure', { - serviceName, - }) - return cached.instances - } - - throw error - } - } - - /** - * Discover service instances from backends - */ - private async discoverFromBackends( - serviceName: string, - options: DiscoveryOptions, - ): Promise { - const backends = await this.getHealthyBackends() - const discoveryPromises: Promise[] = [] - for (const backend of backends) { - discoveryPromises.push( - this.discoverFromBackend(backend, serviceName, options), - ) - } - - const results = await Promise.allSettled(discoveryPromises) - const instances: ServiceInstance[] = [] - - for (const result of results) { - if (result.status === 'fulfilled') { - instances.push(...result.value) - } else { - const reason = - result.reason instanceof Error - ? result.reason - : new Error(String(result.reason)) - this.logger.warn('Discovery from backend failed', { - serviceName, - error: reason, - }) - } - } - - return instances - } - - /** - * Discover from specific backend - */ - private async discoverFromBackend( - backend: DiscoveryBackendRecord, - serviceName: string, - options: DiscoveryOptions, - ): Promise { - try { - switch (backend.type) { - case 'consul': { - return await this.discoverFromConsul( - backend.client, - serviceName, - options, - ) - } - case 'etcd': { - return await this.discoverFromEtcd( - backend.client, - serviceName, - options, - ) - } - case 'zookeeper': { - return await this.discoverFromZookeeper( - backend.client, - serviceName, - options, - ) - } - default: { - return [] - } - } - } catch (error: unknown) { - this.logger.error(`Failed to discover from ${backend.type}`, { - backend: backend.type, - region: backend.region, - serviceName, - error, - }) - throw error - } - } - - /** - * Discover from Consul - */ - private async discoverFromConsul( - consul: ConsulClient, - serviceName: string, - _options: DiscoveryOptions, - ): Promise { - const response = await consul.health.service(serviceName) - const services = Array.isArray(response) - ? response.filter(isConsulServiceDiscoveryEntry) - : [] - - return services.map((service) => ({ - id: service.Service.ID, - name: service.Service.Service, - region: service.Node.Datacenter, - host: service.Service.Address ?? service.Node.Address, - port: service.Service.Port, - protocol: 'http', // Default protocol - version: - service.Service.Tags?.find((tag) => tag.startsWith('version:'))?.split( - ':', - )[1] ?? '1.0.0', - metadata: service.Service.Meta ?? {}, - status: service.Checks?.every((check) => check.Status === 'passing') - ? 'healthy' - : 'unhealthy', - registeredAt: new Date(), - lastHeartbeat: new Date(), - tags: service.Service.Tags ?? [], - })) - } - - /** - * Discover from etcd - */ - private async discoverFromEtcd( - etcd: EtcdClient, - serviceName: string, - _options: DiscoveryOptions, - ): Promise { - const keyPrefix = `/services/${serviceName}/` - const responseMap = await etcd.getAll([keyPrefix]) - const response = Object.fromEntries( - Array.from(responseMap.entries()).filter(([key]) => - key.startsWith(keyPrefix), - ), - ) - - const instances: ServiceInstance[] = [] - - for (const [key, value] of Object.entries(response)) { - try { - const parts = key.split('/') - const parsed = toDiscoveredServiceInstance( - serviceName, - parts[parts.length - 2], - parts[parts.length - 1], - JSON.parse(value), - ) - - if (parsed) { - instances.push(parsed) - } - } catch (error: unknown) { - this.logger.warn('Failed to parse etcd service data', { key, error }) - } - } - - return instances - } - - /** - * Discover from ZooKeeper - */ - private async discoverFromZookeeper( - zk: ZooKeeperClient, - serviceName: string, - _options: DiscoveryOptions, - ): Promise { - const basePath = `/services/${serviceName}` - const instances: ServiceInstance[] = [] - - try { - const regions = await zk.getChildren(basePath) - - for (const region of regions) { - const regionPath = `${basePath}/${region}` - const instanceIds = await zk.getChildren(regionPath) - - for (const instanceId of instanceIds) { - try { - const instancePath = `${regionPath}/${instanceId}` - const data = await zk.getData(instancePath) - - if (data) { - const serviceData = toDiscoveredServiceInstance( - serviceName, - region, - instanceId, - JSON.parse(data), - ) - - if (serviceData) { - instances.push(serviceData) - } - } - } catch (error: unknown) { - this.logger.warn('Failed to parse ZooKeeper service data', { - instanceId, - region, - error, - }) - } - } - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - if (!message.includes('No node') && !message.includes('NO_NODE')) { - throw error - } - } - - return instances - } - - /** - * Filter instances based on options - */ - private filterInstances( - instances: ServiceInstance[], - options: DiscoveryOptions, - ): ServiceInstance[] { - return instances.filter((instance) => { - // Filter by region - if (options.region && instance.region !== options.region) { - return false - } - - // Filter by version - if (options.version && instance.version !== options.version) { - return false - } - - // Filter by status - if (options.healthyOnly && instance.status !== 'healthy') { - return false - } - - // Filter by tags - if (options.tags && options.tags.length > 0) { - const hasAllTags = options.tags.every((tag) => - instance.tags.includes(tag), - ) - if (!hasAllTags) return false - } - - // Filter by metadata - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (instance.metadata[key] !== value) { - return false - } - } - } - - return true - }) - } - - /** - * Sort instances based on options - */ - private sortInstances( - instances: ServiceInstance[], - options: DiscoveryOptions, - ): ServiceInstance[] { - const sorted = [...instances] - - switch (options.sortBy) { - case 'region': - sorted.sort((a, b) => a.region.localeCompare(b.region)) - break - case 'version': - sorted.sort((a, b) => a.version.localeCompare(b.version)) - break - case 'health': - sorted.sort((a, b) => { - if (a.status === 'healthy' && b.status !== 'healthy') return -1 - if (a.status !== 'healthy' && b.status === 'healthy') return 1 - return 0 - }) - break - case 'registered': - sorted.sort( - (a, b) => b.registeredAt.getTime() - a.registeredAt.getTime(), - ) - break - case undefined: - default: - // Default: sort by registration time (newest first) - sorted.sort( - (a, b) => b.registeredAt.getTime() - a.registeredAt.getTime(), - ) - } - - return sorted - } - - /** - * Get cache key - */ - private getCacheKey(serviceName: string, options: DiscoveryOptions): string { - const keyParts = [serviceName] - - if (options.region) keyParts.push(`region:${options.region}`) - if (options.version) keyParts.push(`version:${options.version}`) - if (options.healthyOnly) keyParts.push('healthyOnly') - if (options.tags && options.tags.length > 0) - keyParts.push(`tags:${options.tags.join(',')}`) - if (options.sortBy) keyParts.push(`sort:${options.sortBy}`) - - return keyParts.join('|') - } - - /** - * Check if cache is expired - */ - private isCacheExpired(entry: ServiceCacheEntry): boolean { - const now = new Date().getTime() - const entryTime = entry.timestamp.getTime() - return now - entryTime > entry.ttl - } - - /** - * Send heartbeats for registered services - */ - private async sendHeartbeats(): Promise { - try { - const backends = await this.getHealthyBackends() - - for (const [_serviceName, instances] of this.serviceRegistry) { - for (const instance of instances) { - if (instance.status === 'healthy') { - instance.lastHeartbeat = new Date() - - // Send heartbeat to backends - const heartbeatPromises: Promise[] = [] - for (const backend of backends) { - heartbeatPromises.push( - this.sendHeartbeatToBackend(backend, instance), - ) - } - - await Promise.allSettled(heartbeatPromises) - } - } - } - } catch (error: unknown) { - this.logger.error('Heartbeat process failed', { error }) - } - } - - /** - * Send heartbeat to backend - */ - private async sendHeartbeatToBackend( - backend: DiscoveryBackendRecord, - instance: ServiceInstance, - ): Promise { - try { - switch (backend.type) { - case 'consul': { - // Consul handles heartbeats automatically via TTL checks - break - } - case 'etcd': { - // Update lease for etcd - const etcd = backend.client - const key = `/services/${instance.name}/${instance.region}/${instance.id}` - await etcd.get(key) // Touch the key to renew lease - break - } - case 'zookeeper': { - // ZooKeeper ephemeral nodes handle heartbeats automatically - break - } - } - } catch (error: unknown) { - this.logger.warn(`Failed to send heartbeat to ${backend.type}`, { - backend: backend.type, - region: backend.region, - instance: instance.id, - error, - }) - } - } - - /** - * Cleanup expired services - */ - private async cleanupExpiredServices(): Promise { - try { - const now = new Date() - const maxAge = 5 * 60 * 1000 // 5 minutes - - for (const [serviceName, instances] of this.serviceRegistry) { - const validInstances = instances.filter((instance) => { - const age = now.getTime() - instance.lastHeartbeat.getTime() - return age < maxAge - }) - - const removedCount = instances.length - validInstances.length - - if (removedCount > 0) { - this.logger.info( - `Removed ${removedCount} expired instances for ${serviceName}`, - ) - this.serviceRegistry.set(serviceName, validInstances) - - // Update load balancer - const loadBalancer = this.loadBalancers.get(serviceName) - if (loadBalancer) { - loadBalancer.updateInstances(validInstances) - } - } - } - - // Clear expired cache entries - for (const [key, entry] of this.serviceCache) { - if (this.isCacheExpired(entry)) { - this.serviceCache.delete(key) - } - } - } catch (error: unknown) { - this.logger.error('Cleanup process failed', { error }) - } - } - - /** - * Get service instance using load balancer - */ - async getServiceInstance( - serviceName: string, - options: DiscoveryOptions = {}, - ): Promise { - try { - const instances = await this.discoverService(serviceName, options) - - if (instances.length === 0) { - return null - } - - const loadBalancer = this.loadBalancers.get(serviceName) - if (!loadBalancer) { - // Return first healthy instance if no load balancer - return instances.find((i) => i.status === 'healthy') ?? null - } - - return loadBalancer.selectInstance(instances, options) - } catch (error: unknown) { - this.logger.error('Failed to get service instance', { - serviceName, - options, - error, - }) - throw error - } - } - - /** - * Watch for service changes - */ - async watchService( - serviceName: string, - callback: (instances: ServiceInstance[]) => void, - options: DiscoveryOptions = {}, - ): Promise<() => void> { - try { - this.logger.info('Starting service watch', { serviceName, options }) - - let isWatching = true - const watchInterval = options.watchInterval ?? 10000 // 10 seconds default - - const watchLoop = async () => { - if (!isWatching) { - return - } - - try { - const instances = await this.discoverService(serviceName, options) - callback(instances) - } catch (error: unknown) { - this.logger.error('Service watch error', { serviceName, error }) - } - } - - const timer = setInterval(() => { - void watchLoop() - }, watchInterval) - - // Start watching - void watchLoop() - - // Return stop function - return () => { - isWatching = false - clearInterval(timer) - this.logger.info('Service watch stopped', { serviceName }) - } - } catch (error: unknown) { - this.logger.error('Failed to start service watch', { - serviceName, - options, - error, - }) - throw error - } - } - - /** - * Get service statistics - */ - async getServiceStats(serviceName?: string): Promise { - try { - const stats: ServiceStats = { - totalServices: 0, - totalInstances: 0, - healthyInstances: 0, - unhealthyInstances: 0, - services: {}, - } - - const servicesToCheck: [string, ServiceInstance[]][] = serviceName - ? [[serviceName, this.serviceRegistry.get(serviceName) ?? []]] - : Array.from(this.serviceRegistry.entries()) - - for (const [name, instances] of servicesToCheck) { - const healthy = instances.filter((i) => i.status === 'healthy').length - const unhealthy = instances.filter((i) => i.status !== 'healthy').length - - stats.services[name] = { - totalInstances: instances.length, - healthyInstances: healthy, - unhealthyInstances: unhealthy, - regions: [...new Set(instances.map((i) => i.region))], - } - - stats.totalInstances += instances.length - stats.healthyInstances += healthy - stats.unhealthyInstances += unhealthy - } - - stats.totalServices = Object.keys(stats.services).length - - return stats - } catch (error: unknown) { - this.logger.error('Failed to get service statistics', { - serviceName, - error, - }) - throw error - } - } - - /** - * Deregister a service instance - */ - async deregisterService( - serviceName: string, - instanceId: string, - ): Promise { - try { - this.logger.info('Deregistering service', { serviceName, instanceId }) - - // Remove from backends - const backends = await this.getHealthyBackends() - const deregistrationPromises: Promise[] = [] - for (const backend of backends) { - deregistrationPromises.push( - this.deregisterFromBackend(backend, serviceName, instanceId), - ) - } - - await Promise.allSettled(deregistrationPromises) - - // Remove from local registry - const instances = this.serviceRegistry.get(serviceName) - if (instances) { - const filteredInstances = instances.filter((i) => i.id !== instanceId) - this.serviceRegistry.set(serviceName, filteredInstances) - - // Update load balancer - const loadBalancer = this.loadBalancers.get(serviceName) - if (loadBalancer) { - loadBalancer.updateInstances(filteredInstances) - } - } - - // Clear cache - for (const [key, entry] of this.serviceCache) { - if (key.startsWith(serviceName)) { - const filteredInstances = entry.instances.filter( - (i) => i.id !== instanceId, - ) - this.serviceCache.set(key, { - ...entry, - instances: filteredInstances, - }) - } - } - - this.logger.info('Service deregistered successfully', { - serviceName, - instanceId, - }) - - this.emit('serviceDeregistered', { serviceName, instanceId }) - } catch (error: unknown) { - this.logger.error('Failed to deregister service', { - serviceName, - instanceId, - error, - }) - throw error - } - } - - /** - * Deregister from backend - */ - private async deregisterFromBackend( - backend: DiscoveryBackendRecord, - serviceName: string, - instanceId: string, - ): Promise { - try { - switch (backend.type) { - case 'consul': { - const consul = backend.client - await consul.agent.service.deregister(instanceId) - break - } - case 'etcd': { - const etcd = backend.client - const key = `/services/${serviceName}/${backend.region}/${instanceId}` - await etcd.delete(key) - break - } - case 'zookeeper': { - const zk = backend.client - const path = `/services/${serviceName}/${backend.region}/${instanceId}` - await zk.remove(path) - break - } - } - } catch (error: unknown) { - this.logger.warn(`Failed to deregister from ${backend.type}`, { - backend: backend.type, - region: backend.region, - serviceName, - instanceId, - error, - }) - } - } - - /** - * Shutdown the service discovery manager - */ - async shutdown(): Promise { - if (!this.isInitialized) return - try { - this.logger.info('Shutting down ServiceDiscoveryManager...') - - // Clear intervals - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval) - this.heartbeatInterval = null - } - - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval) - this.cleanupInterval = null - } - - // Deregister all services - for (const [serviceName, instances] of this.serviceRegistry) { - for (const instance of instances) { - try { - await this.deregisterService(serviceName, instance.id) - } catch (error: unknown) { - this.logger.warn('Failed to deregister service during shutdown', { - serviceName, - instance: instance.id, - error, - }) - } - } - } - - // Close backend connections - for (const [region, _consul] of this.consulClients) { - // Consul doesn't have a close method - this.logger.info(`Consul connection closed for region: ${region}`) - } - - for (const [region, etcd] of this.etcdClients) { - etcd.close() - this.logger.info(`etcd connection closed for region: ${region}`) - } - - for (const [region, zk] of this.zookeeperClients) { - zk.close() - this.logger.info(`ZooKeeper connection closed for region: ${region}`) - } - - this.consulClients.clear() - this.etcdClients.clear() - this.zookeeperClients.clear() - this.discoveryBackends.clear() - - this.isInitialized = false - this.logger.info('ServiceDiscoveryManager shutdown completed') - - this.emit('shutdown') - } catch (error: unknown) { - this.logger.error('Error during shutdown', { error }) - throw error - } - } -} - -/** - * Load Balancer implementation - */ -class LoadBalancer { - private readonly algorithm: string - private currentIndex = 0 - private readonly instanceWeights: Map = new Map() - - constructor(config: LoadBalancerConfig) { - this.algorithm = config.algorithm - } - - selectInstance( - instances: ServiceInstance[], - options: DiscoveryOptions = {}, - ): ServiceInstance | null { - const healthyInstances = instances.filter((i) => i.status === 'healthy') - - if (healthyInstances.length === 0) { - return null - } - - switch (this.algorithm) { - case 'round-robin': - return this.roundRobin(healthyInstances) - - case 'weighted-round-robin': - return this.weightedRoundRobin(healthyInstances) - - case 'least-connections': - return this.leastConnections(healthyInstances) - - case 'random': - return this.random(healthyInstances) - - case 'geo-proximity': - return this.geoProximity(healthyInstances, options.clientRegion) - - default: - return healthyInstances[0] ?? null - } - } - - private roundRobin(instances: ServiceInstance[]): ServiceInstance { - const instance = instances[this.currentIndex % instances.length] - this.currentIndex++ - return instance - } - - private weightedRoundRobin(instances: ServiceInstance[]): ServiceInstance { - // Simple weighted round-robin implementation - // In production, use more sophisticated algorithms - return this.roundRobin(instances) - } - - private leastConnections(instances: ServiceInstance[]): ServiceInstance { - // Return instance with least connections - // This would typically use connection metrics - return instances[0] - } - - private random(instances: ServiceInstance[]): ServiceInstance { - const index = Math.floor(Math.random() * instances.length) - return instances[index] - } - - private geoProximity( - instances: ServiceInstance[], - clientRegion?: string, - ): ServiceInstance { - if (!clientRegion) return instances[0] - - // Find instance in same region or closest region - const sameRegion = instances.find((i) => i.region === clientRegion) - if (sameRegion) return sameRegion - - return instances[0] - } - - updateInstances(instances: ServiceInstance[]): void { - // Update weights based on instance health and performance - for (const instance of instances) { - let weight = 100 // Base weight - - if (instance.status !== 'healthy') { - weight = 0 - } else { - // Adjust weight based on metadata - if (typeof instance.metadata['weight'] === 'string') { - const parsedWeight = Number.parseInt(instance.metadata['weight'], 10) - weight = Number.isNaN(parsedWeight) ? 100 : parsedWeight - } - } - - this.instanceWeights.set(instance.id, weight) - } - } -} - -// Types -interface ServiceRegistration { - name: string - instanceId?: string - region: string - host: string - port: number - protocol?: string - version?: string - metadata?: Record - tags?: string[] -} - -interface ServiceInstance { - id: string - name: string - region: string - host: string - port: number - protocol: string - version: string - metadata: Record - status: 'healthy' | 'unhealthy' | 'unknown' - registeredAt: Date - lastHeartbeat: Date - tags: string[] -} - -interface DiscoveryOptions { - region?: string - version?: string - healthyOnly?: boolean - tags?: string[] - metadata?: Record - sortBy?: 'region' | 'version' | 'health' | 'registered' - cacheTTL?: number - watchInterval?: number - clientRegion?: string -} - -interface ServiceCacheEntry { - instances: ServiceInstance[] - timestamp: Date - ttl: number -} - -interface ServiceStats { - totalServices: number - totalInstances: number - healthyInstances: number - unhealthyInstances: number - services: { - [serviceName: string]: { - totalInstances: number - healthyInstances: number - unhealthyInstances: number - regions: string[] - } - } -} - -interface LoadBalancerConfig { - serviceName: string - algorithm: string - healthCheck?: Record - circuitBreaker?: Record -} - -export type { - ServiceRegistration, - ServiceInstance, - DiscoveryOptions, - ServiceStats, - LoadBalancerConfig, -} diff --git a/apps/web/src/lib/deployment/multi-region/index.ts b/apps/web/src/lib/deployment/multi-region/index.ts deleted file mode 100644 index daa6aa7896..0000000000 --- a/apps/web/src/lib/deployment/multi-region/index.ts +++ /dev/null @@ -1,732 +0,0 @@ -/** - * Multi-Region Deployment Infrastructure - * Comprehensive multi-region deployment system for global scale - */ - -import { AutomatedFailoverOrchestrator } from './AutomatedFailoverOrchestrator' -import { - CloudProviderManager, - CloudProviderConfig, - DeploymentResult, -} from './CloudProviderManager' -import { - ConfigurationManager, - MultiRegionConfig, - EnvironmentConfig, - FeatureFlags, - SecretConfig, - MonitoringConfig, - ComplianceConfig, -} from './ConfigurationManager' -import { - CrossRegionDataSyncManager, - SyncStatus, - DataDistribution, -} from './CrossRegionDataSyncManager' -import { - DeploymentOrchestrator, - DeploymentOrchestratorConfig, - DeploymentPlan, - DeploymentPhase, - RollbackPoint, - ValidationStep, - DeploymentExecution, - DeploymentPhaseResult, -} from './DeploymentOrchestrator' -import { - EdgeComputingManager, - EdgeLocation, - EdgeDeploymentConfig, - EdgeNodeStatus, -} from './EdgeComputingManager' -import { - GlobalTrafficRoutingManager, - RoutingConfig, - RouteTarget, - RoutingDecision, - TrafficMetrics, -} from './GlobalTrafficRoutingManager' -import { - HealthMonitor, - HealthCheckConfig, - HealthMetrics, - HealthScore, - HealthAlert, -} from './HealthMonitor' -import { - MultiRegionDeploymentManager, - type DeploymentConfig, - type RegionConfig, -} from './MultiRegionDeploymentManager' -import { - ServiceDiscoveryManager, - ServiceRegistration, - ServiceInstance, - DiscoveryOptions, - ServiceStats, - LoadBalancerConfig, -} from './ServiceDiscoveryManager' - -export { - MultiRegionDeploymentManager, - CloudProviderManager, - EdgeComputingManager, - GlobalTrafficRoutingManager, - CrossRegionDataSyncManager, - AutomatedFailoverOrchestrator, - ServiceDiscoveryManager, - ConfigurationManager, - HealthMonitor, - DeploymentOrchestrator, -} -export type { - CloudProviderConfig, - DeploymentResult, - EdgeLocation, - EdgeDeploymentConfig, - EdgeNodeStatus, - RoutingConfig, - RouteTarget, - RoutingDecision, - TrafficMetrics, - SyncStatus, - DataDistribution, - ServiceRegistration, - ServiceInstance, - DiscoveryOptions, - ServiceStats, - LoadBalancerConfig, - MultiRegionConfig, - EnvironmentConfig, - FeatureFlags, - SecretConfig, - MonitoringConfig, - ComplianceConfig, - HealthCheckConfig, - HealthMetrics, - HealthScore, - HealthAlert, - DeploymentOrchestratorConfig, - DeploymentPlan, - DeploymentPhase, - RollbackPoint, - ValidationStep, - DeploymentExecution, - DeploymentPhaseResult, -} - -// Types -/** - * Multi-Region Deployment System - * - * This module provides a comprehensive multi-region deployment infrastructure - * with the following capabilities: - * - * 1. **Multi-Region Deployment Manager**: Orchestrates deployments across multiple cloud providers - * 2. **Cloud Provider Manager**: Handles AWS, GCP, and Azure integrations - * 3. **Edge Computing Manager**: Manages 50+ global edge locations - * 4. **Global Traffic Routing**: Intelligent routing with latency optimization - * 5. **Cross-Region Data Sync**: CockroachDB-based data synchronization - * 6. **Automated Failover**: Health-based automatic failover orchestration - * 7. **Service Discovery**: Multi-backend service registration and discovery - * 8. **Configuration Management**: Centralized multi-region configuration - * 9. **Health Monitoring**: Comprehensive health checking and alerting - * 10. **Deployment Orchestration**: Coordinated deployment with rollback support - * - * ## Usage Example - * - * ```typescript - * import { - * MultiRegionDeploymentManager, - * ConfigurationManager, - * HealthMonitor - * } from './multi-region'; - * - * // Initialize configuration - * const config = new ConfigurationManager(); - * await config.initialize(); - * - * // Initialize health monitor - * const healthMonitor = new HealthMonitor(config); - * await healthMonitor.initialize(); - * - * // Initialize deployment manager - * const deploymentManager = new MultiRegionDeploymentManager(config, healthMonitor); - * await deploymentManager.initialize(); - * - * // Deploy to multiple regions - * const deploymentResult = await deploymentManager.deploy({ - * regions: ['us-east-1', 'us-west-2', 'eu-central-1', 'ap-southeast-1'], - * services: ['api', 'web', 'ai-service'], - * version: '1.0.0' - * }); - * - * console.log('Deployment completed:', deploymentResult); - * ``` - * - * ## Architecture Overview - * - * The system follows a modular architecture with clear separation of concerns: - * - * - **Orchestration Layer**: Coordinates deployment across regions - * - **Provider Layer**: Abstracts cloud provider differences - * - **Edge Layer**: Manages edge computing infrastructure - * - **Data Layer**: Handles cross-region data synchronization - * - **Network Layer**: Manages traffic routing and load balancing - * - **Monitoring Layer**: Provides health monitoring and alerting - * - **Failover Layer**: Handles automatic failover scenarios - * - * ## Key Features - * - * ### Multi-Cloud Support - * - AWS, Google Cloud Platform, Azure - * - Unified API across providers - * - Provider-specific optimizations - * - * ### Global Edge Network - * - 50+ edge locations worldwide - * - Cloudflare Workers integration - * - AWS Lambda@Edge support - * - Containerized edge services - * - * ### Intelligent Traffic Routing - * - Latency-based routing - * - Geographic proximity routing - * - Health-based routing - * - Compliance-aware routing (GDPR, HIPAA) - * - * ### Data Synchronization - * - CockroachDB for distributed data - * - Real-time and batch sync modes - * - Conflict resolution - * - Data consistency guarantees - * - * ### Automated Failover - * - Health-based failover triggers - * - Circuit breaker patterns - * - Graceful degradation - * - Automatic recovery - * - * ### Service Discovery - * - Consul, etcd, ZooKeeper support - * - Multi-backend service registration - * - Health-aware service discovery - * - Load balancing integration - * - * ### Monitoring & Alerting - * - Comprehensive health checks - * - Real-time metrics collection - * - Automated alerting - * - Performance monitoring - * - * ## Configuration - * - * The system is highly configurable through environment variables and configuration files: - * - * ```bash - * # Multi-region configuration - * MULTI_REGION_ENABLED=true - * PRIMARY_REGION=us-east-1 - * BACKUP_REGIONS=us-west-2,eu-central-1,ap-southeast-1 - * - * # Cloud provider configuration - * AWS_REGION=us-east-1 - * GCP_PROJECT_ID=pixelated-multi-region - * AZURE_SUBSCRIPTION_ID=your-subscription-id - * - * # Service discovery configuration - * CONSUL_ENABLED=true - * ETCD_ENABLED=true - * ZOOKEEPER_ENABLED=false - * - * # Database configuration - * COCKROACHDB_ENABLED=true - * MONGODB_ENABLED=true - * REDIS_ENABLED=true - * - * # Monitoring configuration - * HEALTH_CHECK_INTERVAL=30000 - * FAILOVER_THRESHOLD=50 - * ALERTING_ENABLED=true - * ``` - * - * ## Deployment - * - * Use the provided deployment script for automated multi-region deployment: - * - * ```bash - * # Deploy to all regions - * ./scripts/deploy/multi-region-deploy.sh --environment production - * - * # Deploy to specific regions - * ./scripts/deploy/multi-region-deploy.sh --environment staging --regions us-east-1,eu-central-1 - * - * # Deploy with custom configuration - * ./scripts/deploy/multi-region-deploy.sh --environment production --config custom-config.json - * ``` - * - * ## Monitoring - * - * The system provides comprehensive monitoring capabilities: - * - * - **Health Dashboards**: Grafana dashboards for real-time monitoring - * - **Metrics Collection**: Prometheus metrics for all components - * - **Log Aggregation**: Centralized logging across all regions - * - **Alerting**: Multi-channel alerting (Slack, email, PagerDuty) - * - **Performance Monitoring**: Real-time performance metrics - * - * ## Security - * - * Security is built into every layer: - * - * - **Encryption**: TLS 1.3 for all communications - * - **Authentication**: Multi-factor authentication - * - **Authorization**: Role-based access control - * - **Audit Logging**: Comprehensive audit trails - * - **Compliance**: HIPAA, GDPR, SOC2 compliance - * - **Network Security**: VPC isolation, security groups - * - **Data Protection**: Encryption at rest and in transit - * - * ## Scaling - * - * The system is designed for horizontal scaling: - * - * - **Auto-scaling**: Based on CPU, memory, and custom metrics - * - **Load Balancing**: Multi-tier load balancing - * - **Database Sharding**: Automatic sharding for large datasets - * - **Cache Distribution**: Distributed caching across regions - * - **Edge Scaling**: Dynamic edge node provisioning - * - * ## Troubleshooting - * - * Common issues and solutions: - * - * ### Deployment Failures - * - Check cloud provider credentials - * - Verify network connectivity - * - Review resource quotas - * - Check configuration validity - * - * ### Service Discovery Issues - * - Verify discovery backend connectivity - * - Check service registration status - * - Review health check configurations - * - Validate network policies - * - * ### Data Sync Problems - * - Check CockroachDB cluster health - * - Verify replication lag - * - Review conflict resolution logs - * - Monitor sync queue status - * - * ### Failover Issues - * - Check health monitor status - * - Verify backup region health - * - Review failover configuration - * - Check DNS propagation - * - * ## Support - * - * For issues and questions: - * - * - **Documentation**: See `/docs` directory - * - **Logs**: Check `/logs` directory - * - **Monitoring**: Access Grafana dashboards - * - **Community**: Join our Discord server - * - **Issues**: Report on GitHub - */ - -// Re-export everything for convenience -export * from './MultiRegionDeploymentManager' -export * from './CloudProviderManager' -export * from './EdgeComputingManager' -export * from './GlobalTrafficRoutingManager' -export * from './CrossRegionDataSyncManager' -export * from './AutomatedFailoverOrchestrator' -export * from './ServiceDiscoveryManager' -export * from './ConfigurationManager' -export * from './HealthMonitor' -export * from './DeploymentOrchestrator' - -/** - * Create a fully configured multi-region deployment system - */ -export async function createMultiRegionSystem( - configOverrides?: Partial, -) { - const config = new ConfigurationManager(buildDefaultConfiguration()) - - if (configOverrides) { - await config.updateConfig(configOverrides) - } - - await config.initialize() - const deploymentConfig = config.getDeploymentConfig() - const regions = deploymentConfig.regions - - const healthMonitor = new HealthMonitor(buildDefaultHealthCheckConfig()) - await healthMonitor.initialize(regions) - - const cloudProviderManager = new CloudProviderManager() - await cloudProviderManager.initialize(regions) - - const deploymentOrchestrator = new DeploymentOrchestrator( - buildDefaultDeploymentOrchestratorConfig(), - cloudProviderManager, - ) - await deploymentOrchestrator.initialize() - - const dataSyncManager = new CrossRegionDataSyncManager(config, healthMonitor) - await dataSyncManager.initialize() - - const serviceDiscovery = new ServiceDiscoveryManager(config, healthMonitor) - await serviceDiscovery.initialize() - - const failoverOrchestrator = new AutomatedFailoverOrchestrator( - config, - healthMonitor, - dataSyncManager, - ) - await failoverOrchestrator.initialize() - - const deploymentManager = new MultiRegionDeploymentManager(deploymentConfig) - await deploymentManager.initialize() - - return { - config, - cloudProviderManager, - healthMonitor, - dataSyncManager, - serviceDiscovery, - failoverOrchestrator, - deploymentOrchestrator, - deploymentManager, - } -} - -function buildDefaultHealthCheckConfig(): HealthCheckConfig { - return { - interval: 30_000, - timeout: 5_000, - retries: 3, - thresholds: { - cpu: 80, - memory: 85, - disk: 90, - responseTime: 500, - errorRate: 5, - availability: 95, - }, - } -} - -function buildDefaultDeploymentOrchestratorConfig(): DeploymentOrchestratorConfig { - return { - maxParallelDeployments: 3, - rollbackOnFailure: true, - healthCheckTimeout: 5_000, - deploymentTimeout: 120_000, - retryAttempts: 2, - retryDelay: 1_000, - dependencies: { - infrastructure: ['network', 'dns', 'iam'], - services: ['api', 'worker'], - monitoring: ['metrics', 'alerts'], - }, - } -} - -function buildDefaultDeploymentConfig(): DeploymentConfig { - const regions: RegionConfig[] = [ - { - id: 'us-east-1', - name: 'US East', - provider: 'aws', - location: 'us-east-1', - availabilityZones: ['us-east-1a', 'us-east-1b', 'us-east-1c'], - priority: 1, - complianceRequirements: ['HIPAA', 'GDPR'], - capacity: { - minInstances: 2, - maxInstances: 20, - desiredInstances: 4, - }, - networking: { - vpcCidr: '10.0.0.0/16', - subnetCidrs: ['10.0.1.0/24', '10.0.2.0/24'], - securityGroups: ['default'], - }, - }, - ] - - return { - regions, - globalServices: { - trafficManager: true, - threatIntelligence: false, - complianceManager: false, - }, - edgeComputing: { - enabled: true, - locations: ['us-east-1-edge'], - cacheSize: '4Gi', - }, - dataSync: { - strategy: 'active-passive', - consistencyLevel: 'eventual', - conflictResolution: 'timestamp', - }, - failover: { - automatic: true, - detectionTime: 30_000, - recoveryTime: 120_000, - healthCheckInterval: 30_000, - maxDataSyncLag: 5000, - failoverCooldown: 180_000, - snsTopicArn: '', - sqsQueueUrl: '', - }, - } -} - -function buildDefaultConfiguration(): MultiRegionConfig { - const deployment = buildDefaultDeploymentConfig() - - return { - deployment, - edgeComputing: { - locations: [], - services: { - threatDetection: false, - biasDetection: false, - cacheService: false, - apiGateway: true, - staticContent: true, - }, - aiModels: { - threatDetection: 'none', - biasDetection: 'none', - behavioralAnalysis: 'none', - }, - cacheStrategies: ['LRU'], - healthCheck: { - interval: 30_000, - timeout: 5_000, - retries: 3, - }, - }, - trafficRouting: { - strategy: 'health-based', - healthThreshold: 95, - latencyThreshold: 500, - complianceRequirements: ['HIPAA'], - weights: {}, - fallbackRegions: deployment.regions.map((region) => region.id), - cacheTtl: 300, - }, - environments: { - development: { - regions: deployment.regions, - scaling: { - autoScaling: true, - minInstances: 1, - maxInstances: 5, - targetCpuUtilization: 80, - targetMemoryUtilization: 70, - }, - resources: { - cpu: '2', - memory: '4Gi', - storage: '20Gi', - }, - networking: { - vpcCidr: '10.0.0.0/16', - subnetCidrs: ['10.0.1.0/24'], - securityGroups: ['default'], - loadBalancers: ['primary'], - }, - monitoring: { - enabled: true, - samplingRate: 0.5, - alertThresholds: {}, - }, - }, - staging: { - regions: deployment.regions, - scaling: { - autoScaling: true, - minInstances: 2, - maxInstances: 10, - targetCpuUtilization: 70, - targetMemoryUtilization: 80, - }, - resources: { - cpu: '4', - memory: '8Gi', - storage: '50Gi', - }, - networking: { - vpcCidr: '10.1.0.0/16', - subnetCidrs: ['10.1.1.0/24'], - securityGroups: ['default'], - loadBalancers: ['primary'], - }, - monitoring: { - enabled: true, - samplingRate: 0.25, - alertThresholds: {}, - }, - }, - production: { - regions: deployment.regions, - scaling: { - autoScaling: true, - minInstances: 3, - maxInstances: 20, - targetCpuUtilization: 70, - targetMemoryUtilization: 80, - }, - resources: { - cpu: '8', - memory: '16Gi', - storage: '100Gi', - }, - networking: { - vpcCidr: '10.2.0.0/16', - subnetCidrs: ['10.2.1.0/24'], - securityGroups: ['default'], - loadBalancers: ['primary'], - }, - monitoring: { - enabled: true, - samplingRate: 0.1, - alertThresholds: {}, - }, - }, - }, - featureFlags: { - multiRegionDeployment: true, - edgeComputing: true, - intelligentRouting: true, - autoFailover: true, - threatDetection: true, - biasDetection: true, - complianceChecking: true, - performanceMonitoring: true, - aiModelServing: true, - cacheOptimization: true, - }, - secrets: { - cloudProviders: { - aws: { - accessKeyId: process.env['AWS_ACCESS_KEY_ID'] ?? '', - secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'] ?? '', - region: process.env['AWS_REGION'] ?? 'us-east-1', - }, - gcp: { - projectId: process.env['GOOGLE_CLOUD_PROJECT'] ?? '', - keyFilename: process.env['GOOGLE_APPLICATION_CREDENTIALS'] ?? '', - }, - azure: { - subscriptionId: process.env['AZURE_SUBSCRIPTION_ID'] ?? '', - clientId: process.env['AZURE_CLIENT_ID'] ?? '', - clientSecret: process.env['AZURE_CLIENT_SECRET'] ?? '', - tenantId: process.env['AZURE_TENANT_ID'] ?? '', - }, - }, - databases: { - cockroachdb: { - connectionString: - process.env['COCKROACH_CONNECTION_STRING'] ?? - 'postgres://localhost:26257/defaultdb', - sslMode: process.env['COCKROACH_SSL_MODE'] ?? 'disable', - }, - redis: { - url: process.env['REDIS_URL'] ?? 'redis://localhost:6379', - password: process.env['REDIS_PASSWORD'] ?? '', - }, - }, - aiServices: { - openai: { - apiKey: process.env['OPENAI_API_KEY'] ?? '', - organization: process.env['OPENAI_ORGANIZATION'] ?? '', - }, - google: { - apiKey: process.env['GOOGLE_API_KEY'] ?? '', - projectId: process.env['GOOGLE_CLOUD_PROJECT'] ?? '', - }, - }, - monitoring: { - sentry: { - dsn: process.env['SENTRY_DSN'] ?? '', - authToken: process.env['SENTRY_AUTH_TOKEN'] ?? '', - }, - datadog: { - apiKey: process.env['DATADOG_API_KEY'] ?? '', - appKey: process.env['DATADOG_APP_KEY'] ?? '', - }, - }, - }, - monitoring: { - metrics: { - enabled: true, - interval: 30_000, - retention: 14, - aggregation: 'average', - }, - alerting: { - enabled: true, - channels: ['console'], - severityLevels: ['warning', 'critical'], - escalationRules: {}, - }, - logging: { - level: 'info', - format: 'json', - destinations: ['console'], - sampling: 1.0, - }, - }, - compliance: { - gdpr: { - enabled: true, - dataResidency: ['US'], - retentionPeriods: {}, - subjectRights: ['access', 'erasure'], - }, - hipaa: { - enabled: true, - encryptionRequired: true, - auditLogging: true, - accessControls: ['mfa'], - }, - soc2: { - enabled: true, - auditFrequency: 'weekly', - controls: ['CCM', 'CC7'], - }, - pci: { - enabled: false, - requirements: [], - scanningFrequency: 'monthly', - }, - }, - } -} - -/** - * Default export for convenience - */ -export default { - MultiRegionDeploymentManager, - CloudProviderManager, - EdgeComputingManager, - GlobalTrafficRoutingManager, - CrossRegionDataSyncManager, - AutomatedFailoverOrchestrator, - ServiceDiscoveryManager, - ConfigurationManager, - HealthMonitor, - DeploymentOrchestrator, - createMultiRegionSystem, -} diff --git a/apps/web/src/lib/esm-compat/react-dom.js b/apps/web/src/lib/esm-compat/react-dom.js deleted file mode 100644 index 5693a928f2..0000000000 --- a/apps/web/src/lib/esm-compat/react-dom.js +++ /dev/null @@ -1,13 +0,0 @@ -// ESM compatibility wrapper for React DOM -import ReactDOM from 'react-dom' -export default ReactDOM -export const { - render, - hydrate, - unmountComponentAtNode, - findDOMNode, - createPortal, - flushSync, - unstable_batchedUpdates, - version, -} = ReactDOM diff --git a/apps/web/src/lib/esm-compat/react.js b/apps/web/src/lib/esm-compat/react.js deleted file mode 100644 index f138ed5022..0000000000 --- a/apps/web/src/lib/esm-compat/react.js +++ /dev/null @@ -1,29 +0,0 @@ -// ESM compatibility wrapper for React -import React from 'react' -export default React -export const { - useState, - useEffect, - useContext, - useReducer, - useCallback, - useMemo, - useRef, - useLayoutEffect, - useImperativeHandle, - useDebugValue, - createContext, - createElement, - cloneElement, - createRef, - isValidElement, - Children, - Fragment, - StrictMode, - Suspense, - lazy, - Component, - PureComponent, - memo, - forwardRef, -} = React diff --git a/apps/web/src/lib/logging.ts b/apps/web/src/lib/logging.ts deleted file mode 100644 index 6864149de8..0000000000 --- a/apps/web/src/lib/logging.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Logging utility for the application - */ - -/** - * Log levels in order of increasing severity - */ -export enum LogLevel { - DEBUG = 'debug', - INFO = 'info', - WARN = 'warn', - ERROR = 'error', -} - -/** - * Type definition for structured log data - */ -export interface LogData { - message: string - level: LogLevel - timestamp: number - metadata?: Record -} - -/** - * Logger interface defining the core logging operations - */ -export interface Logger { - debug: (message: string, metadata?: Record) => void - info: (message: string, metadata?: Record) => void - warn: (message: string, metadata?: Record) => void - error: (message: string, metadata?: Record) => void -} - -/** - * Options for creating a logger - */ -export interface LoggerOptions { - prefix?: string -} - -// Helper function to check environment -const isServer = typeof window === 'undefined' -const isDevelopment = isServer - ? process.env['NODE_ENV'] === 'development' - : window.location.hostname === 'localhost' || - window.location.hostname === '127.0.0.1' - -// Helper function to get log level from environment -const getEnvLogLevel = (): LogLevel => { - if (isServer) { - return (process.env['LOG_LEVEL'] as LogLevel) || LogLevel.INFO - } - return LogLevel.INFO -} - -/** - * Console-based logger implementation that respects log levels - * and provides structured logging capabilities - */ -class ConsoleLogger implements Logger { - private readonly logLevel: LogLevel - private readonly isDevelopment: boolean - private readonly prefix?: string - - constructor(level: LogLevel = LogLevel.INFO, prefix?: string) { - this.logLevel = level - this.isDevelopment = isDevelopment - this.prefix = prefix - } - - private createLogEntry( - level: LogLevel, - message: string, - metadata?: Record, - ): LogData { - return { - message: this.prefix ? `[${this.prefix}] ${message}` : message, - level, - timestamp: Date.now(), - metadata, - } - } - - private formatLogEntry({ - message, - level, - timestamp, - metadata, - }: LogData): string { - const time = new Date(timestamp).toISOString() - const metadataStr = metadata ? ` ${JSON.stringify(metadata)}` : '' - return `[${time}] [${level.toUpperCase()}] ${message}${metadataStr}` - } - - private writeLog(entry: LogData): void { - const formattedMessage = this.formatLogEntry(entry) - - switch (entry.level) { - case LogLevel.ERROR: - console.error(formattedMessage) - break - case LogLevel.WARN: - console.warn(formattedMessage) - break - case LogLevel.INFO: - console.info(formattedMessage) - break - case LogLevel.DEBUG: - console.debug(formattedMessage) - break - } - } - - private shouldLog(level: LogLevel): boolean { - const levels = Object.values(LogLevel) - const currentLevelIndex = levels.indexOf(this.logLevel) - const targetLevelIndex = levels.indexOf(level) - return targetLevelIndex >= currentLevelIndex - } - - debug: (message: string, metadata?: Record) => void = ( - message, - metadata, - ) => { - if (this.isDevelopment && this.shouldLog(LogLevel.DEBUG)) { - this.writeLog(this.createLogEntry(LogLevel.DEBUG, message, metadata)) - } - } - - info: (message: string, metadata?: Record) => void = ( - message, - metadata, - ) => { - if (this.isDevelopment && this.shouldLog(LogLevel.INFO)) { - this.writeLog(this.createLogEntry(LogLevel.INFO, message, metadata)) - } - } - - warn: (message: string, metadata?: Record) => void = ( - message, - metadata, - ) => { - if (this.shouldLog(LogLevel.WARN)) { - this.writeLog(this.createLogEntry(LogLevel.WARN, message, metadata)) - } - } - - error: (message: string, metadata?: Record) => void = ( - message, - metadata, - ) => { - if (this.shouldLog(LogLevel.ERROR)) { - this.writeLog(this.createLogEntry(LogLevel.ERROR, message, metadata)) - } - } -} - -// Singleton logger instance -let loggerInstance: Logger - -/** - * Get the logger instance, creating it if necessary - * @param options Optional configuration options for the logger - */ -export function getLogger(options?: LoggerOptions): Logger { - if (!loggerInstance) { - const envLogLevel = getEnvLogLevel() - loggerInstance = new ConsoleLogger(envLogLevel) - } - - // If a prefix is provided, create a new logger with that prefix - if (options?.prefix) { - return new ConsoleLogger(getEnvLogLevel(), options.prefix) - } - - return loggerInstance -} - -/** - * Set a custom logger implementation - */ -export function setLogger(customLogger: Logger): void { - loggerInstance = customLogger -} - -// createBuildSafeLogger function removed to avoid initialization issues during build -// Use createBuildSafeLogger directly instead diff --git a/apps/web/src/lib/metaaligner/__tests__/adaptive-selection-integration.test.ts b/apps/web/src/lib/metaaligner/__tests__/adaptive-selection-integration.test.ts deleted file mode 100644 index 04b57de8c5..0000000000 --- a/apps/web/src/lib/metaaligner/__tests__/adaptive-selection-integration.test.ts +++ /dev/null @@ -1,628 +0,0 @@ -/** - * Integration Tests for Adaptive Selection Workflow - * - * Tests the complete end-to-end adaptive selection pipeline: - * ContextDetector → ContextTransitionDetector → ObjectiveSwitcher → AdaptiveSelector - * - * Covers: - * - Multi-turn conversations with context changes - * - Crisis escalation scenarios - * - Transition smoothing and immediate elevation - * - Observer notifications across components - * - Performance and stability - */ - -import { describe, it, expect, beforeEach, vi } from 'vitest' - -import type { AIService, AICompletion } from '../../ai/models/ai-types' -import { ContextType } from '../core/objectives' -import { AdaptiveSelector } from '../prioritization/adaptive-selector' -import { ContextDetector } from '../prioritization/context-detector' -import { - ContextTransitionDetector, - type ContextEvent, -} from '../prioritization/context-transition-detector' -import { - ObjectiveSwitcher, - type ObjectiveSwitchObserver, -} from '../prioritization/objective-switcher' - -// Mock AI Service -const createMockAIService = (): AIService => { - return { - getModelInfo: vi - .fn< - () => { - id: string - name: string - provider: string - capabilities: string[] - contextWindow: number - maxTokens: number - } - >() - .mockReturnValue({ - id: 'test-model', - name: 'Test Model', - provider: 'test', - capabilities: [], - contextWindow: 4096, - maxTokens: 2048, - }), - createChatCompletion: vi - .fn< - ( - messages: Array<{ role: string; content: string }>, - ) => Promise - >() - .mockImplementation(async (messages) => { - // Simple mock responses based on user input - const userMessage = - messages.find((m: any) => m.role === 'user')?.content ?? '' - - let detectedContext = ContextType.GENERAL - let confidence = 0.7 - let urgency: 'low' | 'medium' | 'high' | 'critical' = 'low' - - // Crisis detection - if ( - userMessage.toLowerCase().includes('hurt myself') || - userMessage.toLowerCase().includes('suicide') || - userMessage.toLowerCase().includes('end it all') - ) { - detectedContext = ContextType.CRISIS - confidence = 0.95 - urgency = 'critical' - } - // Educational queries - else if ( - userMessage.toLowerCase().includes('what is') || - userMessage.toLowerCase().includes('tell me about') || - userMessage.toLowerCase().includes('explain') - ) { - detectedContext = ContextType.EDUCATIONAL - confidence = 0.9 - urgency = 'low' - } - // Clinical assessment - else if ( - userMessage.toLowerCase().includes('diagnose') || - userMessage.toLowerCase().includes('assessment') || - userMessage.toLowerCase().includes('phq-9') - ) { - detectedContext = ContextType.CLINICAL_ASSESSMENT - confidence = 0.88 - urgency = 'medium' - } - // Support context - else if ( - userMessage.toLowerCase().includes('feeling') || - userMessage.toLowerCase().includes('cope') || - userMessage.toLowerCase().includes('help me') - ) { - detectedContext = ContextType.SUPPORT - confidence = 0.85 - urgency = 'medium' - } - // Informational - else if ( - userMessage.toLowerCase().includes('where can i') || - userMessage.toLowerCase().includes('how do i find') || - userMessage.toLowerCase().includes('hotline') - ) { - detectedContext = ContextType.INFORMATIONAL - confidence = 0.87 - urgency = 'low' - } - - return { - id: 'test-completion', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - detectedContext: detectedContext, - confidence: confidence, - contextualIndicators: [ - { - type: 'pattern_match', - description: `Detected ${detectedContext} context`, - confidence: confidence, - }, - ], - needsSpecialHandling: - detectedContext === ContextType.CRISIS || - detectedContext === ContextType.CLINICAL_ASSESSMENT, - urgency: urgency, - metadata: {}, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { - promptTokens: 10, - completionTokens: 20, - totalTokens: 30, - }, - provider: 'test', - content: JSON.stringify({ - detectedContext: detectedContext, - confidence: confidence, - urgency: urgency, - }), - } - }), - createStreamingChatCompletion: vi.fn<() => Promise>(), - dispose: vi.fn<() => void>(), - } as unknown as AIService -} - -describe('Adaptive Selection Integration Tests', () => { - let aiService: AIService - let contextDetector: ContextDetector - let transitionDetector: ContextTransitionDetector - let objectiveSwitcher: ObjectiveSwitcher - let adaptiveSelector: AdaptiveSelector - - beforeEach(() => { - aiService = createMockAIService() - - contextDetector = new ContextDetector({ - aiService, - model: 'test-model', - enableCrisisIntegration: false, // Simplified for integration tests - enableEducationalRecognition: false, - }) - - transitionDetector = new ContextTransitionDetector({ - minConfidenceThreshold: 0.7, - smoothingWindow: 2, - enableCrisisElevation: true, - enableTelemetry: true, - }) - - objectiveSwitcher = new ObjectiveSwitcher({ - enableTelemetry: true, - enableAuditLog: true, - }) - - adaptiveSelector = new AdaptiveSelector({ - aiService, - }) - }) - - describe('End-to-End Workflow', () => { - it('should detect context, track transition, and switch objectives', async () => { - // Turn 1: Educational query - const turn1 = await contextDetector.detectContext('What is anxiety?') - expect(turn1.detectedContext).toBe(ContextType.EDUCATIONAL) - - const event1: ContextEvent = { - turnId: 1, - contextType: turn1.detectedContext, - confidence: turn1.confidence, - urgency: turn1.urgency, - timestamp: Date.now(), - } - - transitionDetector.addEvent(event1) - - // Turn 2: Support request - const turn2 = await contextDetector.detectContext( - 'I am feeling overwhelmed and need help coping', - ) - expect(turn2.detectedContext).toBe(ContextType.SUPPORT) - - const event2: ContextEvent = { - turnId: 2, - contextType: turn2.detectedContext, - confidence: turn2.confidence, - urgency: turn2.urgency, - timestamp: Date.now() + 1000, - } - - const transition = transitionDetector.addEvent(event2) - - expect(transition).not.toBeNull() - expect(transition!.from.contextType).toBe(ContextType.EDUCATIONAL) - expect(transition!.to.contextType).toBe(ContextType.SUPPORT) - - // Verify transition was detected but smoothing applied - if (transition!.detected) { - await objectiveSwitcher.onContextTransition(transition!) - - const objectives = objectiveSwitcher.getObjectives() - expect(objectives.length).toBeGreaterThan(0) - - // Support context should prioritize empathy - const empathyObj = objectives.find((obj) => obj.key === 'empathy') - expect(empathyObj).toBeDefined() - } - }) - - it('should handle crisis escalation immediately', async () => { - // Turn 1: General conversation - const turn1 = await contextDetector.detectContext('Hello, how are you?') - - const event1: ContextEvent = { - turnId: 1, - contextType: turn1.detectedContext, - confidence: turn1.confidence, - urgency: turn1.urgency, - timestamp: Date.now(), - } - - transitionDetector.addEvent(event1) - - // Turn 2: Crisis situation - const turn2 = await contextDetector.detectContext('I want to hurt myself') - expect(turn2.detectedContext).toBe(ContextType.CRISIS) - - const event2: ContextEvent = { - turnId: 2, - contextType: turn2.detectedContext, - confidence: turn2.confidence, - urgency: turn2.urgency, - timestamp: Date.now() + 500, - } - - const transition = transitionDetector.addEvent(event2) - - expect(transition).not.toBeNull() - expect(transition!.transitionType).toBe('crisis_elevation') - expect(transition!.detected).toBe(true) - expect(transition!.shouldSmooth).toBe(false) - - // Switch objectives immediately - await objectiveSwitcher.onContextTransition(transition!) - - const objectives = objectiveSwitcher.getObjectives() - const safetyObj = objectives.find((obj) => obj.key === 'safety') - expect(safetyObj).toBeDefined() - expect(safetyObj!.priority).toBe(1) // Highest priority - - // Verify telemetry - const telemetry = objectiveSwitcher.getTelemetry() - expect(telemetry.objective_switch_count).toBe(1) - }) - - it('should handle multi-turn conversation with multiple transitions', async () => { - const conversation = [ - { text: 'Hello', expectedContext: ContextType.GENERAL }, - { - text: 'What is depression?', - expectedContext: ContextType.EDUCATIONAL, - }, - { - text: 'Can you diagnose me?', - expectedContext: ContextType.CLINICAL_ASSESSMENT, - }, - { - text: 'Where can I find a therapist?', - expectedContext: ContextType.INFORMATIONAL, - }, - ] - - let previousContext: ContextType | null = null - - for (let i = 0; i < conversation.length; i++) { - const turn = conversation[i] - const detection = await contextDetector.detectContext(turn.text) - - // Context detection may vary based on mock, so we'll be flexible - const event: ContextEvent = { - turnId: i + 1, - contextType: detection.detectedContext, - confidence: detection.confidence, - urgency: detection.urgency, - timestamp: Date.now() + i * 1000, - } - - const transition = transitionDetector.addEvent(event) - - if (transition?.detected) { - await objectiveSwitcher.onContextTransition(transition) - - expect(transition.from.contextType).toBe(previousContext!) - expect(transition.to.contextType).toBe(detection.detectedContext) - } - - previousContext = detection.detectedContext - } - - // Verify transition history - const history = transitionDetector.getHistory() - expect(history.length).toBe(conversation.length) - - // Verify telemetry - const stats = transitionDetector.getTransitionStats() - expect(stats.totalEvents).toBe(conversation.length) - expect(stats.transitions).toBeGreaterThanOrEqual(1) - }) - }) - - describe('AdaptiveSelector Integration', () => { - it('should select appropriate objectives based on context', async () => { - const result = await adaptiveSelector.selectObjectives('What is anxiety?') - - expect(result.contextDetectionResult).toBeDefined() - expect(result.contextDetectionResult.detectedContext).toBe( - ContextType.EDUCATIONAL, - ) - expect(result.selectedObjectives.length).toBeGreaterThan(0) - - // Educational context should weight informativeness highly - const infoObj = result.selectedObjectives.find( - (obj) => obj.objective.id === 'informativeness', - ) - expect(infoObj).toBeDefined() - expect(infoObj!.weight).toBeGreaterThan(0) - }) - - it('should handle crisis context with high safety priority', async () => { - const result = await adaptiveSelector.selectObjectives( - 'I want to end it all', - ) - - expect(result.contextDetectionResult.detectedContext).toBe( - ContextType.CRISIS, - ) - expect(result.contextDetectionResult.urgency).toBe('critical') - - // Crisis should prioritize safety - const safetyObj = result.selectedObjectives.find( - (obj) => obj.objective.id === 'safety', - ) - expect(safetyObj).toBeDefined() - expect(safetyObj!.weight).toBeGreaterThan(0.7) - }) - - it('should handle conversation history', async () => { - const conversationHistory = [ - 'What is anxiety?', - 'How can I manage stress?', - ] - - const result = await adaptiveSelector.selectObjectives( - 'I am feeling overwhelmed', - conversationHistory, - 'user123', - ) - - expect(result.alignmentContext.conversationHistory).toEqual( - conversationHistory, - ) - expect(result.selectedObjectives.length).toBeGreaterThan(0) - }) - }) - - describe('Observer Pattern Integration', () => { - it('should notify observers across the pipeline', async () => { - const switchObserver = vi.fn() - objectiveSwitcher.addObserver(switchObserver) - - // Simulate context change - const turn1 = await contextDetector.detectContext('What is anxiety?') - const event1: ContextEvent = { - turnId: 1, - contextType: turn1.detectedContext, - confidence: turn1.confidence, - urgency: turn1.urgency, - timestamp: Date.now(), - } - transitionDetector.addEvent(event1) - - const turn2 = await contextDetector.detectContext('I need help coping') - const event2: ContextEvent = { - turnId: 2, - contextType: turn2.detectedContext, - confidence: turn2.confidence, - urgency: turn2.urgency, - timestamp: Date.now() + 1000, - } - - const transition = transitionDetector.addEvent(event2) - - if (transition?.detected) { - await objectiveSwitcher.onContextTransition(transition) - - // Allow async notifications - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(switchObserver).toHaveBeenCalled() - const callArgs = switchObserver.mock.calls[0] - expect(callArgs[0]).toBeInstanceOf(Array) // objectives - expect(callArgs[1]).toHaveProperty('toContext') - } - }) - }) - - describe('Performance and Stability', () => { - it('should handle rapid context changes efficiently', async () => { - const startTime = performance.now() - - const queries = [ - 'What is anxiety?', - 'I feel sad', - 'Can you diagnose me?', - 'Where can I find help?', - 'I feel better now', - ] - - for (let i = 0; i < queries.length; i++) { - const detection = await contextDetector.detectContext(queries[i]) - - const event: ContextEvent = { - turnId: i + 1, - contextType: detection.detectedContext, - confidence: detection.confidence, - urgency: detection.urgency, - timestamp: Date.now() + i * 100, - } - - const transition = transitionDetector.addEvent(event) - if (transition?.detected) { - await objectiveSwitcher.onContextTransition(transition) - } - } - - const duration = performance.now() - startTime - - // Should complete in reasonable time - expect(duration).toBeLessThan(1000) // 1 second for 5 turns - - // Verify all components are in valid state - expect(transitionDetector.getCurrentContext()).toBeDefined() - expect(objectiveSwitcher.getObjectives().length).toBeGreaterThan(0) - expect(transitionDetector.getTransitionStats().totalEvents).toBe( - queries.length, - ) - }) - - it('should maintain stability across multiple runs', async () => { - const runs = 3 - const results: any[] = [] - - for (let run = 0; run < runs; run++) { - const result = - await adaptiveSelector.selectObjectives('What is anxiety?') - results.push(result) - } - - // All runs should produce consistent context detection - const contexts = results.map( - (r) => r.contextDetectionResult.detectedContext, - ) - expect(new Set(contexts).size).toBe(1) // All same - expect(contexts[0]).toBe(ContextType.EDUCATIONAL) - - // All runs should have valid objectives - results.forEach((result) => { - expect(result.selectedObjectives.length).toBeGreaterThan(0) - expect( - result.selectedObjectives.every((obj: any) => obj.weight >= 0), - ).toBe(true) - }) - }) - }) - - describe('Error Handling and Resilience', () => { - it('should handle context detection failures gracefully', async () => { - const failingAIService = { - ...aiService, - createChatCompletion: vi - .fn() - .mockRejectedValue(new Error('AI service error')), - } as unknown as AIService - - const failingDetector = new ContextDetector({ - aiService: failingAIService, - enableCrisisIntegration: false, - }) - - const result = await failingDetector.detectContext('Test message') - - // Should fall back to GENERAL - expect(result.detectedContext).toBe(ContextType.GENERAL) - expect(result.confidence).toBeLessThan(0.5) - expect(result.metadata['error']).toBeDefined() - }) - - it('should handle objective switching errors without crashing', async () => { - const turn1 = await contextDetector.detectContext('What is anxiety?') - const event1: ContextEvent = { - turnId: 1, - contextType: turn1.detectedContext, - confidence: turn1.confidence, - urgency: turn1.urgency, - timestamp: Date.now(), - } - transitionDetector.addEvent(event1) - - // Create invalid transition - const invalidTransition: any = { - from: event1, - to: { ...event1, contextType: 'INVALID' as any }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.8, - } - - // Should not throw - await expect( - objectiveSwitcher.onContextTransition(invalidTransition), - ).resolves.not.toThrow() - - // Should still have valid state - const telemetry = objectiveSwitcher.getTelemetry() - expect(telemetry.failed_switches).toBe(0) // May handle gracefully - }) - }) - - describe('Representative Scenarios (Acceptance Criteria)', () => { - it('Scenario 1: Educational to Support Journey', async () => { - // User starts with educational question - const result1 = await adaptiveSelector.selectObjectives( - 'What is depression?', - ) - expect(result1.contextDetectionResult.detectedContext).toBe( - ContextType.EDUCATIONAL, - ) - - // Progresses to seeking support - const result2 = await adaptiveSelector.selectObjectives( - 'I think I might be depressed. How can I cope?', - ['What is depression?'], - ) - - // Should detect support context - expect([ContextType.SUPPORT, ContextType.EDUCATIONAL]).toContain( - result2.contextDetectionResult.detectedContext, - ) - expect(result2.selectedObjectives.length).toBeGreaterThan(0) - }) - - it('Scenario 2: Crisis Escalation from General Conversation', async () => { - // Casual start - - // Sudden crisis - const result2 = await adaptiveSelector.selectObjectives( - 'I cannot take this anymore, I want to hurt myself', - ['Hello'], - ) - - expect(result2.contextDetectionResult.detectedContext).toBe( - ContextType.CRISIS, - ) - expect(result2.contextDetectionResult.urgency).toBe('critical') - - const safetyObj = result2.selectedObjectives.find( - (obj) => obj.objective.id === 'safety', - ) - expect(safetyObj).toBeDefined() - }) - - it('Scenario 3: Clinical Assessment Request', async () => { - // User seeks clinical evaluation - const result = await adaptiveSelector.selectObjectives( - 'Can you help me with a PHQ-9 assessment for depression?', - ) - - expect(result.contextDetectionResult.detectedContext).toBe( - ContextType.CLINICAL_ASSESSMENT, - ) - expect(result.contextDetectionResult.needsSpecialHandling).toBe(true) - - // Should prioritize correctness and professionalism - const correctnessObj = result.selectedObjectives.find( - (obj) => obj.objective.id === 'correctness', - ) - expect(correctnessObj).toBeDefined() - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/__tests__/integration.test.ts b/apps/web/src/lib/metaaligner/__tests__/integration.test.ts deleted file mode 100644 index 1df2fc3340..0000000000 --- a/apps/web/src/lib/metaaligner/__tests__/integration.test.ts +++ /dev/null @@ -1,754 +0,0 @@ -/** - * Integration tests for MetaAligner multi-objective analysis workflow - * Tests the complete end-to-end functionality including evaluation, enhancement, and integration - */ - -import type { - AIMessage, - AIService, - AICompletion, -} from '../../ai/models/ai-types' -import { MetaAlignerAPI, IntegratedAIService } from '../api/alignment-api' -import { - DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, - WeightingStrategy, -} from '../core/objective-weighting' -import { ContextType, CORE_MENTAL_HEALTH_OBJECTIVES } from '../core/objectives' -import type { AlignmentContext, UserProfile } from '../core/objectives' -import type { AdaptiveSelectorConfig } from '../prioritization/adaptive-selector' -import { AdaptiveSelector as ActualAdaptiveSelector } from '../prioritization/adaptive-selector' -import { getContextualObjectiveWeights } from '../prioritization/context-objective-mapper' - -// Mock logger -vi.mock('../../logging', () => ({ - getLogger: () => ({ - info: vi.fn<() => void>(), - error: vi.fn<() => void>(), - warn: vi.fn<() => void>(), - debug: vi.fn<() => void>(), - }), -})) - -// Mock AI service for testing enhancement -const createMockAIService = (enhancedResponse?: string): AIService => { - const mockService: Partial = { - createChatCompletion: vi - .fn<() => Promise>() - .mockResolvedValue({ - id: 'test-response', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { - role: 'assistant', - content: enhancedResponse ?? 'Enhanced response...', - }, - finishReason: 'stop', - }, - ], - usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150 }, - provider: 'test', - content: enhancedResponse ?? 'Enhanced response...', - }), - createStreamingChatCompletion: vi.fn(), - getModelInfo: vi.fn(), - dispose: vi.fn(), - createChatCompletionWithTracking: vi.fn(), - generateCompletion: vi.fn(), - } - return mockService as AIService -} - -describe('MetaAligner Integration Tests', () => { - let metaAligner: MetaAlignerAPI - let mockAIService: AIService - - beforeEach(() => { - vi.clearAllMocks() - mockAIService = createMockAIService() - - metaAligner = new MetaAlignerAPI({ - enableRealTimeEvaluation: true, - enableResponseEnhancement: true, - enhancementThreshold: 0.7, - maxEnhancementAttempts: 2, - aiService: mockAIService, - model: 'test-model', - temperature: 0.7, - // Override objectives with low-scoring evaluation functions for testing enhancement - objectives: CORE_MENTAL_HEALTH_OBJECTIVES.map((obj) => ({ - ...obj, - evaluationFunction: (response: string, context: AlignmentContext) => { - // Return very low scores for the "poorResponse" to ensure enhancement is triggered - if ( - response.includes('should probably calm down') && - context.detectedContext === ContextType.CRISIS - ) { - if (obj.id === 'safety') { - return 0.1 - } - if (obj.id === 'empathy') { - return 0.1 - } - return 0.2 - } - // Default scores for other responses/contexts - if (obj.id === 'safety') { - return 0.9 - } - if (obj.id === 'empathy') { - return 0.8 - } - if (obj.id === 'correctness') { - return 0.85 - } - return 0.75 - }, - })), - }) - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe('Complete Multi-Objective Analysis Workflow', () => { - it('should perform end-to-end evaluation workflow for crisis context', async () => { - // Setup crisis scenario - const userQuery = 'I want to hurt myself and feel hopeless' - const originalResponse = - 'That sounds difficult. Have you considered talking to someone?' - - // Step 1: Context Detection - const context = metaAligner.detectContext(userQuery) - expect(context.detectedContext).toBe(ContextType.CRISIS) - expect(context.userQuery).toBe(userQuery) - - // Step 2: Response Evaluation - const evaluation = await metaAligner.evaluateResponse({ - response: originalResponse, - context, - }) - - // Verify evaluation structure - expect(evaluation.evaluation.objectiveResults).toBeDefined() - expect(evaluation.metrics).toBeDefined() - expect(evaluation.recommendations).toBeDefined() - expect(typeof evaluation.needsEnhancement).toBe('boolean') - - // Verify all core objectives were evaluated - const objectiveIds = Object.keys(evaluation.evaluation.objectiveResults) - expect(objectiveIds).toContain('correctness') - expect(objectiveIds).toContain('empathy') - expect(objectiveIds).toContain('safety') - expect(objectiveIds).toContain('professionalism') - expect(objectiveIds).toContain('informativeness') - - // Crisis scenarios should prioritize safety - const safetyResult = evaluation.evaluation.objectiveResults['safety'] - expect(safetyResult).toBeDefined() - if (safetyResult) { - expect(safetyResult.objectiveId).toBe('safety') - expect(safetyResult.score).toBeGreaterThanOrEqual(0) - expect(safetyResult.score).toBeLessThanOrEqual(1) - } - - // Step 3: Enhancement (if needed) - if (evaluation.needsEnhancement) { - const enhancement = await metaAligner.enhanceResponse({ - originalResponse, - evaluationResult: evaluation.evaluation, - context, - }) - - expect(enhancement.enhancedResponse).toBeDefined() - expect(enhancement.enhancementApplied).toBe(true) - expect(enhancement.improvementMetrics).toBeDefined() - expect(enhancement.enhancementExplanation).toBeTruthy() - - // Enhanced response should be different from original - expect(enhancement.enhancedResponse).not.toBe(originalResponse) - } - }) - - it('should handle educational context workflow', async () => { - const userQuery = - 'What is cognitive behavioral therapy and how does it work?' - const originalResponse = - 'CBT is a type of therapy that focuses on thoughts and behaviors.' - - // Context detection - const context = metaAligner.detectContext(userQuery) - expect(context.detectedContext).toBe(ContextType.INFORMATIONAL) // Adjusted from EDUCATIONAL - - // Evaluation - const evaluation = await metaAligner.evaluateResponse({ - response: originalResponse, - context, - }) - - // Educational context should prioritize informativeness and correctness - const informativenessResult = - evaluation.evaluation.objectiveResults['informativeness'] - const correctnessResult = - evaluation.evaluation.objectiveResults['correctness'] - - expect(informativenessResult).toBeDefined() - expect(correctnessResult).toBeDefined() - - // Verify metrics include balance and contextual alignment - expect(evaluation.metrics.balanceScore).toBeGreaterThanOrEqual(0) - expect(evaluation.metrics.contextualAlignment).toBeGreaterThanOrEqual(0) - expect(evaluation.metrics.overallScore).toBeGreaterThanOrEqual(0) // Corrected from overallPerformance - }) - - it('should handle support context with empathy prioritization', async () => { - const userQuery = "I'm feeling really overwhelmed with work stress lately" - const originalResponse = - 'Work stress is common. Try to manage your time better.' - - const context = metaAligner.detectContext(userQuery) - expect(context.detectedContext).toBe(ContextType.SUPPORT) - - const evaluation = await metaAligner.evaluateResponse({ - response: originalResponse, - context, - }) - - // Support context should have strong empathy component - const empathyResult = evaluation.evaluation.objectiveResults['empathy'] - expect(empathyResult).toBeDefined() - if (empathyResult) { - expect(empathyResult.criteriaScores).toBeDefined() - // Should have specific empathy criteria evaluated - expect(empathyResult.criteriaScores).toHaveProperty( - 'emotional_validation', - ) - expect(empathyResult.criteriaScores).toHaveProperty( - 'understanding_demonstration', - ) - expect(empathyResult.criteriaScores).toHaveProperty('supportive_tone') - } - }) - }) - - describe('Integrated AI Service Workflow', () => { - it('should automatically evaluate and enhance responses', async () => { - const baseAIService = createMockAIService( - 'Original response that may need improvement', - ) - const integratedService = new IntegratedAIService( - baseAIService, - metaAligner, - ) - - const messages: AIMessage[] = [ - { - role: 'user', - content: "I feel anxious and don't know what to do", - name: 'user', - }, - ] - - const response = await integratedService.createChatCompletion(messages) - - // Should include alignment information - expect(response.alignment).toBeDefined() - expect(response.alignment?.evaluation).toBeDefined() - expect(response.alignment?.metrics).toBeDefined() - expect(typeof response.alignment?.enhanced).toBe('boolean') - expect(typeof response.alignment?.enhancementAttempts).toBe('number') - - // Response should have been processed - expect((response as any).content).toBeDefined() - expect((response as any).choices?.[0]?.message?.content).toBeDefined() - }) - - it('should handle conversation history in context detection', async () => { - const baseAIService = createMockAIService() - const integratedService = new IntegratedAIService( - baseAIService, - metaAligner, - ) - - const messages: AIMessage[] = [ - { role: 'user', content: "I've been feeling sad", name: 'user' }, - { - role: 'assistant', - content: "I understand you're feeling sad. Can you tell me more?", - name: 'assistant', - }, - { - role: 'user', - content: "It's been going on for weeks now", - name: 'user', - }, - ] - - const response = await integratedService.createChatCompletion(messages) - - expect( - response.alignment?.evaluation?.evaluationContext?.conversationHistory, - ).toBeDefined() - expect( - response.alignment?.evaluation?.evaluationContext?.conversationHistory - ?.length, - ).toBeGreaterThan(0) - }) - }) - - describe('Objective Balancing and Prioritization', () => { - it('should adapt objective weights based on context', async () => { - // Test different contexts and verify weight adaptation - const contexts = [ - { query: 'I want to kill myself', expectedContext: ContextType.CRISIS }, - { - query: 'What is depression?', - expectedContext: ContextType.INFORMATIONAL, // Adjusted from EDUCATIONAL - }, - { - query: 'I need emotional support', - expectedContext: ContextType.SUPPORT, - }, - ] - - for (const { query, expectedContext } of contexts) { - const context = metaAligner.detectContext(query) - expect(context.detectedContext).toBe(expectedContext) - - const evaluation = await metaAligner.evaluateResponse({ - response: 'Test response for objective weighting', - context, - }) - - // Each context should produce different objective emphasis - expect(evaluation.metrics.balanceScore).toBeGreaterThanOrEqual(0) - expect(evaluation.metrics.contextualAlignment).toBeGreaterThanOrEqual(0) - } - }) - - it('should maintain objective balance across different response types', async () => { - const responses = [ - 'Very technical medical information about depression symptoms', - 'Warm, empathetic response showing understanding and support', - 'Safety-focused response with crisis resources and emergency contacts', - ] - - const context: AlignmentContext = { - userQuery: "I'm struggling with my mental health", - detectedContext: ContextType.SUPPORT, - } - - const evaluations: any[] = [] - for (const response of responses) { - const evaluation = await metaAligner.evaluateResponse({ - response, - context, - }) - evaluations.push(evaluation) - } - - // All evaluations should have valid balance scores - for (const evaluation of evaluations) { - expect(evaluation.metrics.balanceScore).toBeGreaterThanOrEqual(0) - expect(evaluation.metrics.balanceScore).toBeLessThanOrEqual(1) - expect(evaluation.evaluation.overallScore).toBeGreaterThanOrEqual(0) - expect(evaluation.evaluation.overallScore).toBeLessThanOrEqual(1) - } - }) - }) - - describe('Enhancement Pipeline Integration', () => { - it('should perform iterative enhancement with improvement tracking', async () => { - const poorResponse = "That's not good. You should probably calm down." // Made worse for crisis context - const context: AlignmentContext = { - userQuery: - "I'm having panic attacks and feel scared, I think I might die.", // Made query more urgent - detectedContext: ContextType.CRISIS, - } - - // Initial evaluation - const initialEvaluation = await metaAligner.evaluateResponse({ - response: poorResponse, - context, - }) - - // Should identify need for enhancement - expect(initialEvaluation.needsEnhancement).toBe(true) - expect(initialEvaluation.evaluation.overallScore).toBeLessThan(0.7) - - // Perform enhancement - const enhancement = await metaAligner.enhanceResponse({ - originalResponse: poorResponse, - evaluationResult: initialEvaluation.evaluation, - context, - }) - - expect(enhancement.enhancementApplied).toBe(true) - expect(enhancement.enhancedResponse).not.toBe(poorResponse) - expect(enhancement.improvementMetrics).toBeDefined() - - // Re-evaluate enhanced response - const enhancedEvaluation = await metaAligner.evaluateResponse({ - response: enhancement.enhancedResponse, - context, - }) - - // Enhanced response should show improvement - expect(enhancedEvaluation.evaluation.overallScore).toBeGreaterThanOrEqual( - initialEvaluation.evaluation.overallScore, - ) - }) - - it('should handle enhancement failures gracefully', async () => { - // Mock AI service that fails - const failingAIService = createMockAIService() - vi.mocked(failingAIService.createChatCompletion).mockRejectedValue( - new Error('API Error'), - ) - - const metaAlignerWithFailingAI = new MetaAlignerAPI({ - aiService: failingAIService, - enableResponseEnhancement: true, - }) - - const context: AlignmentContext = { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - } - - const evaluation = await metaAlignerWithFailingAI.evaluateResponse({ - response: 'Test response', - context, - }) - - // Should handle enhancement failure gracefully - const enhancementResult = await metaAlignerWithFailingAI.enhanceResponse({ - originalResponse: 'Test response', - evaluationResult: evaluation.evaluation, - context, - }) - expect(enhancementResult.enhancementApplied).toBe(false) - expect(enhancementResult.enhancedResponse).toBe('Test response') // Original response returned - expect(enhancementResult.enhancementExplanation).toContain( - 'Enhancement failed', - ) - }) - }) - - describe('Performance and Scalability', () => { - it('should handle multiple concurrent evaluations', async () => { - const requests = Array.from({ length: 5 }, (_, i) => ({ - response: `Test response ${i}`, - context: { - userQuery: `Test query ${i}`, - detectedContext: ContextType.GENERAL, - }, - })) - - const evaluations = await Promise.all( - requests.map(async (request) => metaAligner.evaluateResponse(request)), - ) - - expect(evaluations).toHaveLength(5) - for (const evaluation of evaluations) { - expect(evaluation.evaluation).toBeDefined() - expect(evaluation.metrics).toBeDefined() - } - }) - - it('should maintain performance with large conversation histories', async () => { - const largeHistory: AIMessage[] = Array.from({ length: 100 }, (_, i) => ({ - role: i % 2 === 0 ? 'user' : 'assistant', - content: `Message ${i}`, - name: i % 2 === 0 ? 'user' : 'assistant', - })) - - const context = metaAligner.detectContext('Current query', largeHistory) - expect(context.conversationHistory).toBeDefined() - - const evaluation = await metaAligner.evaluateResponse({ - response: 'Response to large conversation', - context, - }) - - expect(evaluation.evaluation).toBeDefined() - expect(evaluation.metrics).toBeDefined() - }) - }) - - describe('Error Handling and Edge Cases', () => { - it('should handle malformed responses gracefully', async () => { - const malformedResponses = ['', null, undefined, '\n\n\n', ' '] - - for (const response of malformedResponses) { - const evaluation = await metaAligner.evaluateResponse({ - response: response!, - context: { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - }, - }) - - expect(evaluation.evaluation).toBeDefined() - expect(evaluation.evaluation.overallScore).toBeGreaterThanOrEqual(0) - expect(evaluation.evaluation.overallScore).toBeLessThanOrEqual(1) - } - }) - - it('should validate objective consistency', async () => { - const context: AlignmentContext = { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - } - - const evaluation = await metaAligner.evaluateResponse({ - response: 'Test response', - context, - }) - - // All objectives should have consistent structure - for (const [objectiveId, result] of Object.entries( - evaluation.evaluation.objectiveResults, - )) { - if (typeof result === 'object' && result !== null) { - expect((result as any).objectiveId).toBe(objectiveId) - expect((result as any).score).toBeGreaterThanOrEqual(0) - expect((result as any).score).toBeLessThanOrEqual(1) - expect((result as any).confidence).toBeGreaterThanOrEqual(0) - expect((result as any).confidence).toBeLessThanOrEqual(1) - expect((result as any).explanation).toBeTruthy() - expect(typeof (result as any).explanation).toBe('string') - } - } - }) - }) -}) - -describe('AdaptiveSelector Workflow Integration Tests', () => { - let adaptiveSelector: ActualAdaptiveSelector - let mockAIServiceForSelector: AIService - - beforeEach(() => { - vi.clearAllMocks() // Ensure mocks are clean for each test - mockAIServiceForSelector = createMockAIService( - 'Default mock response for selector', - ) - - // Config for AdaptiveSelector, ensuring its internal ContextDetector uses our mock AIService - const adaptiveSelectorConfig: AdaptiveSelectorConfig = { - aiService: mockAIServiceForSelector, - contextDetectorConfig: { - enableCrisisIntegration: false, - enableEducationalRecognition: false, - }, - } - adaptiveSelector = new ActualAdaptiveSelector(adaptiveSelectorConfig) - }) - - test('should process general input, detect context, and select objectives with default weights', async () => { - const userInput = "Just a casual chat, how's the weather?" - // Mock AI response for ContextDetector (general context) - const generalContextResponse = JSON.stringify({ - detectedContext: ContextType.GENERAL, - confidence: 0.8, - contextualIndicators: [ - { type: 'greeting', description: 'Casual greeting', confidence: 0.7 }, - ], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }) - vi.mocked(mockAIServiceForSelector.createChatCompletion).mockResolvedValue({ - id: 'test-ctx-resp', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { role: 'assistant', content: generalContextResponse }, - finishReason: 'stop', - }, - ], - usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, - provider: 'test', - content: generalContextResponse, - }) - - const result1 = await adaptiveSelector.selectObjectives(userInput) - expect(result1.contextDetectionResult.detectedContext).toBe( - ContextType.GENERAL, - ) - expect(result1.selectedObjectives.length).toBeGreaterThan(0) - expect(result1.weightCalculationResult.strategy).toBe( - DEFAULT_WEIGHT_ADJUSTMENT_PARAMS.strategy, - ) - const sumOfWeights1 = result1.selectedObjectives.reduce( - (sum: number, so: any) => sum + so.weight, - 0, - ) - expect(sumOfWeights1).toBeCloseTo(1.0, 5) - const result2 = await adaptiveSelector.selectObjectives(userInput) - expect(result2.contextDetectionResult.detectedContext).toBe( - ContextType.GENERAL, - ) - expect(result2.selectedObjectives.length).toBeGreaterThan(0) - expect(result2.weightCalculationResult.strategy).toBe( - DEFAULT_WEIGHT_ADJUSTMENT_PARAMS.strategy, - ) - const sumOfWeights2 = result2.selectedObjectives.reduce( - (sum: number, so: any) => sum + so.weight, - 0, - ) - expect(sumOfWeights2).toBeCloseTo(1.0, 5) - expect(sumOfWeights2).toBeCloseTo(1.0, 5) - }) - - test('should detect a context transition and adjust weights accordingly', async () => { - const firstInput = 'Hello there!' - const generalContextResponse = JSON.stringify({ - detectedContext: ContextType.GENERAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }) - vi.mocked( - mockAIServiceForSelector.createChatCompletion, - ).mockResolvedValueOnce({ - id: 'test-ctx-resp-1', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { role: 'assistant', content: generalContextResponse }, - finishReason: 'stop', - }, - ], - usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, - provider: 'test', - content: generalContextResponse, - }) - await adaptiveSelector.selectObjectives(firstInput) - const secondInput = "I'm feeling very sad and hopeless." - const crisisContextResponse = JSON.stringify({ - detectedContext: ContextType.CRISIS, - confidence: 0.95, - contextualIndicators: [ - { type: 'keywords', description: 'hopeless, sad', confidence: 0.9 }, - ], - needsSpecialHandling: true, - urgency: 'high', - metadata: { - transition: { from: ContextType.GENERAL, to: ContextType.CRISIS }, - }, - }) - vi.mocked( - mockAIServiceForSelector.createChatCompletion, - ).mockResolvedValueOnce({ - id: 'test-ctx-resp-2', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { role: 'assistant', content: crisisContextResponse }, - finishReason: 'stop', - }, - ], - usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, - provider: 'test', - content: crisisContextResponse, - }) - const result = await adaptiveSelector.selectObjectives(secondInput) - expect(result.contextDetectionResult.detectedContext).toBe( - ContextType.CRISIS, - ) - expect(result.contextDetectionResult.metadata.transition).toBeDefined() - expect(result.contextDetectionResult.metadata.transition?.from).toBe( - ContextType.GENERAL, - ) - expect(result.contextDetectionResult.metadata.transition?.to).toBe( - ContextType.CRISIS, - ) - const safetyObjective = result.selectedObjectives.find( - (so: any) => so.objective.id === 'safety', - ) - expect(safetyObjective).toBeDefined() - const highestWeight = result.selectedObjectives.reduce( - (max: number, so: any) => Math.max(max, so.weight), - 0, - ) - expect(safetyObjective!.weight).toBe(highestWeight) - }) - - test('should apply user preferences if WeightingStrategy is USER_PREFERENCE_ADJUSTED', async () => { - // For this test, we need to ensure ObjectiveWeightingEngine within AdaptiveSelector uses the USER_PREFERENCE_ADJUSTED strategy. - // This might require temporarily altering DEFAULT_WEIGHT_ADJUSTMENT_PARAMS or a more direct way to configure AdaptiveSelector's engine. - const originalStrategy = DEFAULT_WEIGHT_ADJUSTMENT_PARAMS.strategy - DEFAULT_WEIGHT_ADJUSTMENT_PARAMS.strategy = - WeightingStrategy.USER_PREFERENCE_ADJUSTED - const userPrefConfig: AdaptiveSelectorConfig = { - aiService: mockAIServiceForSelector, - contextDetectorConfig: { - enableCrisisIntegration: false, - enableEducationalRecognition: false, - }, - } - adaptiveSelector = new ActualAdaptiveSelector(userPrefConfig) - const userInput = 'Tell me a joke.' - const generalContextResponse = JSON.stringify({ - detectedContext: ContextType.GENERAL, - confidence: 0.8, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }) - vi.mocked(mockAIServiceForSelector.createChatCompletion).mockResolvedValue({ - id: 'test-ctx-resp-joke', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { role: 'assistant', content: generalContextResponse }, - finishReason: 'stop', - }, - ], - usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, - provider: 'test', - content: generalContextResponse, - }) - const userProfile: UserProfile = { - preferences: { - objectiveWeightAdjustments: { empathy: 2.0 }, - }, - } - const result = await adaptiveSelector.selectObjectives( - userInput, - [], - 'test-user', - userProfile, - ) - DEFAULT_WEIGHT_ADJUSTMENT_PARAMS.strategy = originalStrategy - expect(result.weightCalculationResult.strategy).toBe( - WeightingStrategy.USER_PREFERENCE_ADJUSTED, - ) - const empathyObjective = result.selectedObjectives.find( - (so: any) => so.objective.id === 'empathy', - ) - const otherObjective = result.selectedObjectives.find( - (so: any) => so.objective.id === 'correctness', - ) - expect(empathyObjective).toBeDefined() - expect(otherObjective).toBeDefined() - const baseWeightsForGeneralContext = getContextualObjectiveWeights( - ContextType.GENERAL, - ) - const expectedEmpathyRatio = - (baseWeightsForGeneralContext.empathy * 2.0) / - baseWeightsForGeneralContext.correctness - const actualEmpathyRatio = empathyObjective!.weight / otherObjective!.weight - expect(actualEmpathyRatio).toBeCloseTo(expectedEmpathyRatio, 1) - }) -}) diff --git a/apps/web/src/lib/metaaligner/api/__tests__/adapter.test.ts b/apps/web/src/lib/metaaligner/api/__tests__/adapter.test.ts deleted file mode 100644 index 61bfc777db..0000000000 --- a/apps/web/src/lib/metaaligner/api/__tests__/adapter.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* @vitest-environment node */ -import { describe, it, expect } from 'vitest' - -import { - LLMAdapter, - type OpenAIRawOutput, - type AnthropicRawOutput, - type GeminiRawOutput, -} from '../adapter' -import type { UnifiedContext } from '../unified-api' - -describe('LLMAdapter', () => { - const adapter = new LLMAdapter() - const mockContext: UnifiedContext = { - sessionId: 'session-123', - userId: 'user-456', - } - - it('transforms OpenAI output into unified format', () => { - const rawOutput: OpenAIRawOutput = { - model: 'gpt-4o', - choices: [ - { - message: { role: 'assistant', content: 'Take a deep breath.' }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 15, - completion_tokens: 6, - total_tokens: 21, - }, - } - - const result = adapter.transform('OpenAI', rawOutput, mockContext) - - expect(result.llmOutput.content).toBe('Take a deep breath.') - expect(result.llmOutput.metadata?.provider).toBe('OpenAI') - expect(result.llmOutput.metadata?.model).toBe('gpt-4o') - expect(result.llmOutput.metadata?.finishReason).toBe('stop') - expect(result.llmOutput.metadata?.tokenUsage).toEqual({ - promptTokens: 15, - completionTokens: 6, - totalTokens: 21, - }) - expect(result.context).toEqual(mockContext) - }) - - it('transforms Anthropic output into unified format', () => { - const rawOutput: AnthropicRawOutput = { - model: 'claude-3-7-sonnet', - content: [ - { type: 'text', text: 'I understand ' }, - { type: 'text', text: 'how difficult this is.' }, - ], - stop_reason: 'end_turn', - usage: { - input_tokens: 25, - output_tokens: 12, - }, - } - - const result = adapter.transform('Anthropic', rawOutput, mockContext) - - expect(result.llmOutput.content).toBe('I understand how difficult this is.') - expect(result.llmOutput.metadata?.provider).toBe('Anthropic') - expect(result.llmOutput.metadata?.model).toBe('claude-3-7-sonnet') - expect(result.llmOutput.metadata?.finishReason).toBe('end_turn') - expect(result.llmOutput.metadata?.tokenUsage).toEqual({ - promptTokens: 25, - completionTokens: 12, - totalTokens: 37, - }) - }) - - it('transforms Gemini output into unified format', () => { - const rawOutput: GeminiRawOutput = { - model: 'gemini-2.5-flash', - candidates: [ - { - content: { - parts: [{ text: 'Let us explore ' }, { text: 'what feels off.' }], - }, - finishReason: 'STOP', - }, - ], - usageMetadata: { - promptTokenCount: 18, - candidatesTokenCount: 9, - totalTokenCount: 27, - }, - } - - const result = adapter.transform('Gemini', rawOutput, mockContext) - - expect(result.llmOutput.content).toBe('Let us explore what feels off.') - expect(result.llmOutput.metadata?.provider).toBe('Gemini') - expect(result.llmOutput.metadata?.model).toBe('gemini-2.5-flash') - expect(result.llmOutput.metadata?.finishReason).toBe('STOP') - expect(result.llmOutput.metadata?.tokenUsage).toEqual({ - promptTokens: 18, - completionTokens: 9, - totalTokens: 27, - }) - }) - - it('transforms MentalLLaMA output into unified format', () => { - const rawOutput = { - content: 'Clinical reflection.', - finishReason: 'stop', - model: 'mental-llama-7b', - tokenUsage: { - promptTokens: 10, - completionTokens: 5, - totalTokens: 15, - }, - metadata: { - customFlag: true, - }, - } - - const result = adapter.transform('MentalLLaMA', rawOutput, mockContext) - - expect(result.llmOutput.content).toBe('Clinical reflection.') - expect(result.llmOutput.metadata?.model).toBe('mental-llama-7b') - expect(result.llmOutput.metadata?.tokenUsage).toEqual(rawOutput.tokenUsage) - }) - - it('throws for an unsupported provider', () => { - expect(() => - adapter.transform('UnknownProvider' as any, {}, mockContext), - ).toThrow('Unsupported LLM provider: UnknownProvider') - }) -}) diff --git a/apps/web/src/lib/metaaligner/api/__tests__/unified-pipeline.test.ts b/apps/web/src/lib/metaaligner/api/__tests__/unified-pipeline.test.ts deleted file mode 100644 index 2444f3fb5b..0000000000 --- a/apps/web/src/lib/metaaligner/api/__tests__/unified-pipeline.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -/* @vitest-environment node */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -vi.mock('../../../logging/build-safe-logger', () => ({ - createBuildSafeLogger: () => ({ - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }), -})) - -import type { AIService } from '../../ai/models/ai-types' -import type { AICompletion } from '../../ai/models/ai-types' -import type { - ObjectiveDefinition, - AlignmentContext, -} from '../../core/objectives' -import { MetaAlignerAPI } from '../alignment-api' -import type { UnifiedProcessingRequest } from '../unified-api' -import { - UnifiedMetaAlignerPipeline, - type UnifiedPipelineConfig, -} from '../unified-pipeline' - -function makeRequest( - overrides: Partial = {}, -): UnifiedProcessingRequest { - return { - llmOutput: { content: 'I hear you. It sounds like a difficult time.' }, - context: { userQuery: 'I have been feeling overwhelmed lately' }, - ...overrides, - } -} - -function makeMockAIService(enhancedResponse: string): AIService { - return { - createChatCompletion: vi.fn().mockResolvedValue({ - id: 'test-response', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { role: 'assistant', content: enhancedResponse }, - finishReason: 'stop', - }, - ], - usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150 }, - provider: 'test', - content: enhancedResponse, - } satisfies AICompletion), - createStreamingChatCompletion: vi.fn(), - getModelInfo: vi.fn(), - dispose: vi.fn(), - } as unknown as AIService -} - -/** - * Objectives whose scores are driven by a controllable marker in the response, - * mirroring the integration-test pattern: responses containing 'poorResponse' - * score low (triggering enhancement), everything else scores high. - */ -function makeControllableObjectives(): ObjectiveDefinition[] { - const objectives = [ - { id: 'correctness', weight: 0.25 }, - { id: 'informativeness', weight: 0.2 }, - { id: 'professionalism', weight: 0.35 }, - { id: 'empathy', weight: 0.15 }, - { id: 'safety', weight: 0.05 }, - ] - return objectives.map(({ id, weight }) => ({ - id, - name: id, - description: `${id} objective`, - weight, - criteria: [], - evaluationFunction: (response: string, _context: AlignmentContext) => { - if (response.includes('poorResponse')) { - return id === 'safety' ? 0.1 : 0.2 - } - return 0.9 - }, - })) -} - -describe('UnifiedMetaAlignerPipeline', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('passes a request through evaluation without enhancement when score is sufficient', async () => { - const aiService = makeMockAIService('Enhanced response.') - const metaAligner = new MetaAlignerAPI({ - enableResponseEnhancement: true, - enhancementThreshold: 0.7, - aiService, - objectives: makeControllableObjectives(), - }) - const createSpy = vi.mocked(aiService.createChatCompletion) - - const pipeline = new UnifiedMetaAlignerPipeline({}, metaAligner) - const request = makeRequest() - const response = await pipeline.process(request) - - expect(createSpy).not.toHaveBeenCalled() - expect(response.enhancedResponse).toBe(request.llmOutput.content) - expect(response.originalResponse).toBe(request.llmOutput.content) - expect(response.alignment.enhanced).toBe(false) - expect(response.alignment.enhancementAttempts).toBe(0) - expect(response.errors).toBeUndefined() - expect(response.alignment.evaluation.overallScore).toBeGreaterThan(0.7) - }) - - it('enhances once and stops when re-evaluation passes', async () => { - const aiService = makeMockAIService('Improved supportive response.') - const metaAligner = new MetaAlignerAPI({ - enableResponseEnhancement: true, - enhancementThreshold: 0.7, - maxEnhancementAttempts: 2, - aiService, - objectives: makeControllableObjectives(), - }) - const createSpy = vi.mocked(aiService.createChatCompletion) - - const pipeline = new UnifiedMetaAlignerPipeline({}, metaAligner) - const response = await pipeline.process( - makeRequest({ llmOutput: { content: 'poorResponse content here' } }), - ) - - expect(createSpy).toHaveBeenCalledOnce() - expect(response.enhancedResponse).toBe('Improved supportive response.') - expect(response.alignment.enhanced).toBe(true) - expect(response.alignment.enhancementAttempts).toBe(1) - expect(response.alignment.evaluation.overallScore).toBeGreaterThanOrEqual( - 0.7, - ) - }) - - it('truncates conversation history to maxHistoryLength', async () => { - const aiService = makeMockAIService('Enhanced.') - const metaAligner = new MetaAlignerAPI({ - enableResponseEnhancement: true, - aiService, - objectives: makeControllableObjectives(), - }) - - const history = Array.from({ length: 30 }, (_, i) => `msg-${i}`) - const config: UnifiedPipelineConfig = { maxHistoryLength: 10 } - - const pipeline = new UnifiedMetaAlignerPipeline(config, metaAligner) - const response = await pipeline.process( - makeRequest({ - context: { userQuery: 'q', conversationHistory: history }, - }), - ) - - // History is forwarded via the request context to evaluation - expect(response.errors).toBeUndefined() - expect(response.originalResponse).toBe( - 'I hear you. It sounds like a difficult time.', - ) - }) - - it('returns an error response instead of throwing on invalid input', async () => { - const pipeline = new UnifiedMetaAlignerPipeline() - - const response = await pipeline.process( - makeRequest({ - context: { userQuery: undefined as unknown as string }, - }), - ) - - expect(response.errors).toBeDefined() - expect(response.errors?.[0]?.stage).toBe('validation') - }) - - it('stringifies object llmOutput content for evaluation', async () => { - const aiService = makeMockAIService('Enhanced.') - const metaAligner = new MetaAlignerAPI({ - enableResponseEnhancement: true, - aiService, - objectives: makeControllableObjectives(), - }) - - const pipeline = new UnifiedMetaAlignerPipeline({}, metaAligner) - const response = await pipeline.process( - makeRequest({ - llmOutput: { content: { structured: true } }, - }), - ) - - expect(response.enhancedResponse).toBe('{"structured":true}') - expect(response.originalResponse).toBe('{"structured":true}') - }) -}) diff --git a/apps/web/src/lib/metaaligner/api/adapter.ts b/apps/web/src/lib/metaaligner/api/adapter.ts deleted file mode 100644 index 13b668d82e..0000000000 --- a/apps/web/src/lib/metaaligner/api/adapter.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * @module adapter - * @description module provides adapter transforming outputs LLM providers into unified format. - * allows MetaAligner pipeline process responses various LLMs in standardized way. - */ - -import type { LLMResponse as MentalLLaMAResponse } from '../../ai/mental-llama/types/mentalLLaMATypes' -import type { - LLMOutput, - UnifiedContext, - UnifiedProcessingRequest, -} from './unified-api' - -/** - * Represents supported LLM providers. - * enum used identify source LLM output. - */ -export type LLMProvider = 'MentalLLaMA' | 'OpenAI' | 'Anthropic' | 'Gemini' - -/** - * Raw output shapes from each supported provider's chat completion API. - */ -export interface OpenAIRawOutput { - id?: string - choices?: Array<{ - message?: { role?: string; content?: string } - finish_reason?: string - }> - usage?: { - prompt_tokens?: number - completion_tokens?: number - total_tokens?: number - } - model?: string -} - -export interface AnthropicRawOutput { - id?: string - content?: Array<{ type?: string; text?: string }> - usage?: { input_tokens?: number; output_tokens?: number } - stop_reason?: string - model?: string -} - -export interface GeminiRawOutput { - candidates?: Array<{ - content?: { parts?: Array<{ text?: string }>; role?: string } - finishReason?: string - }> - usageMetadata?: { - promptTokenCount?: number - candidatesTokenCount?: number - totalTokenCount?: number - } - model?: string -} - -/** - * LLM adapter class. - * responsible transforming provider-specific LLM outputs into unified format. - */ -export class LLMAdapter { - /** - * Transforms raw output LLM provider into {@link UnifiedProcessingRequest}. - * main method adapter, dispatches appropriate provider-specific transformer. - * - * @param provider - LLM provider, e.g., 'MentalLLaMA', 'OpenAI'. - * @param rawOutput - raw output LLM provider. - * @param context - context processing request. - * @returns transformed processing request, ready sent MetaAligner pipeline. - * @throws An error provider not supported. - */ - public transform( - provider: LLMProvider, - rawOutput: unknown, - context: UnifiedContext, - ): UnifiedProcessingRequest { - switch (provider) { - case 'MentalLLaMA': - return this.transformMentalLLaMA( - rawOutput as MentalLLaMAResponse, - context, - ) - case 'OpenAI': - return this.transformOpenAI(rawOutput as OpenAIRawOutput, context) - case 'Anthropic': - return this.transformAnthropic(rawOutput as AnthropicRawOutput, context) - case 'Gemini': - return this.transformGemini(rawOutput as GeminiRawOutput, context) - default: { - const exhaustive: never = provider - throw new Error(`Unsupported LLM provider: ${String(exhaustive)}`) - } - } - } - - /** - * Transforms raw output from the MentalLLaMA provider into unified format. - */ - private transformMentalLLaMA( - rawOutput: MentalLLaMAResponse, - context: UnifiedContext, - ): UnifiedProcessingRequest { - const llmOutput: LLMOutput = { - content: rawOutput.content, - metadata: { - ...rawOutput.metadata, - finishReason: rawOutput.finishReason, - tokenUsage: rawOutput.tokenUsage, - model: rawOutput.model, - }, - } - - return { llmOutput, context } - } - - /** - * Transforms raw output from the OpenAI Chat Completions API into unified format. - */ - private transformOpenAI( - rawOutput: OpenAIRawOutput, - context: UnifiedContext, - ): UnifiedProcessingRequest { - const choice = rawOutput.choices?.[0] - const content = choice?.message?.content ?? '' - const llmOutput: LLMOutput = { - content, - metadata: { - provider: 'OpenAI', - model: rawOutput.model, - finishReason: choice?.finish_reason, - tokenUsage: rawOutput.usage - ? { - promptTokens: rawOutput.usage.prompt_tokens ?? 0, - completionTokens: rawOutput.usage.completion_tokens ?? 0, - totalTokens: rawOutput.usage.total_tokens ?? 0, - } - : undefined, - }, - } - return { llmOutput, context } - } - - /** - * Transforms raw output from the Anthropic Messages API into unified format. - */ - private transformAnthropic( - rawOutput: AnthropicRawOutput, - context: UnifiedContext, - ): UnifiedProcessingRequest { - const content = - rawOutput.content?.map((block) => block.text ?? '').join('') ?? '' - const llmOutput: LLMOutput = { - content, - metadata: { - provider: 'Anthropic', - model: rawOutput.model, - finishReason: rawOutput.stop_reason, - tokenUsage: - rawOutput.usage != null - ? { - promptTokens: rawOutput.usage.input_tokens ?? 0, - completionTokens: rawOutput.usage.output_tokens ?? 0, - totalTokens: - (rawOutput.usage.input_tokens ?? 0) + - (rawOutput.usage.output_tokens ?? 0), - } - : undefined, - }, - } - return { llmOutput, context } - } - - /** - * Transforms raw output from the Google Gemini generateContent API into unified format. - */ - private transformGemini( - rawOutput: GeminiRawOutput, - context: UnifiedContext, - ): UnifiedProcessingRequest { - const candidate = rawOutput.candidates?.[0] - const content = - candidate?.content?.parts?.map((part) => part.text ?? '').join('') ?? '' - const llmOutput: LLMOutput = { - content, - metadata: { - provider: 'Gemini', - model: rawOutput.model, - finishReason: candidate?.finishReason, - tokenUsage: rawOutput.usageMetadata - ? { - promptTokens: rawOutput.usageMetadata.promptTokenCount ?? 0, - completionTokens: - rawOutput.usageMetadata.candidatesTokenCount ?? 0, - totalTokens: rawOutput.usageMetadata.totalTokenCount ?? 0, - } - : undefined, - }, - } - return { llmOutput, context } - } -} diff --git a/apps/web/src/lib/metaaligner/api/alignment-api.test.ts b/apps/web/src/lib/metaaligner/api/alignment-api.test.ts deleted file mode 100644 index 3776b06f62..0000000000 --- a/apps/web/src/lib/metaaligner/api/alignment-api.test.ts +++ /dev/null @@ -1,683 +0,0 @@ -/** - * Unit tests for MetaAligner LLM Integration API - */ - -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' - -import type { AIMessage, AIService } from '../../ai/models/ai-types' -import { ContextType } from '../core/objectives' -import { - MetaAlignerAPI, - IntegratedAIService, - type AlignmentIntegrationConfig, -} from './alignment-api' - -// Mock the AI service -const mockAIService: Partial = { - createChatCompletion: vi.fn(), - createStreamingChatCompletion: vi.fn(), - generateCompletion: vi.fn(), - createChatCompletionWithTracking: vi.fn(), - dispose: vi.fn(), - getModelInfo: vi.fn(), -} - -// Mock the logger -vi.mock('../../logging', () => ({ - getLogger: () => ({ - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), - }), -})) - -describe('MetaAlignerAPI', () => { - let metaAligner: MetaAlignerAPI - let config: AlignmentIntegrationConfig - - beforeEach(() => { - vi.clearAllMocks() - - config = { - enableRealTimeEvaluation: true, - enableResponseEnhancement: true, - enhancementThreshold: 0.7, - maxEnhancementAttempts: 2, - aiService: mockAIService as AIService, - model: 'test-model', - temperature: 0.7, - } - - metaAligner = new MetaAlignerAPI(config) - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe('Constructor', () => { - it('should initialize with default configuration', () => { - const api = new MetaAlignerAPI() - expect(api).toBeInstanceOf(MetaAlignerAPI) - }) - - it('should initialize with custom configuration', () => { - const customConfig = { - enableRealTimeEvaluation: false, - enhancementThreshold: 0.8, - } - - const api = new MetaAlignerAPI(customConfig) - expect(api).toBeInstanceOf(MetaAlignerAPI) - }) - - it('should use core mental health objectives by default', () => { - const api = new MetaAlignerAPI() - expect(api).toBeInstanceOf(MetaAlignerAPI) - }) - }) - - describe('detectContext', () => { - it('should detect crisis context from keywords', () => { - const context = metaAligner.detectContext('I want to kill myself') - expect(context.detectedContext).toBe(ContextType.CRISIS) - expect(context.userQuery).toBe('I want to kill myself') - }) - - it('should detect educational context', () => { - const context = metaAligner.detectContext( - 'Can you explain what anxiety is?', - ) - expect(context.detectedContext).toBe(ContextType.EDUCATIONAL) - }) - - it('should detect support context', () => { - const context = metaAligner.detectContext( - 'I need help dealing with stress', - ) - expect(context.detectedContext).toBe(ContextType.SUPPORT) - }) - - it('should detect clinical assessment context', () => { - const context = metaAligner.detectContext( - 'What are the symptoms of depression?', - ) - expect(context.detectedContext).toBe(ContextType.CLINICAL_ASSESSMENT) - }) - - it('should detect informational context', () => { - const context = metaAligner.detectContext( - 'What is cognitive behavioral therapy?', - ) - expect(context.detectedContext).toBe(ContextType.INFORMATIONAL) - }) - - it('should default to general context', () => { - const context = metaAligner.detectContext('Hello there') - expect(context.detectedContext).toBe(ContextType.GENERAL) - }) - - it('should include conversation history', () => { - const history: AIMessage[] = [ - { role: 'user', content: 'Previous message', name: 'user' }, - { role: 'assistant', content: 'Previous response', name: 'assistant' }, - ] - - const context = metaAligner.detectContext('Current message', history) - expect(context.conversationHistory).toHaveLength(2) - expect(context.conversationHistory).toContain('Previous message') - expect(context.conversationHistory).toContain('Previous response') - }) - }) - - describe('evaluateResponse', () => { - it('should evaluate a mental health response', async () => { - const request = { - response: - "I understand that you're feeling anxious. This is a common experience, and there are evidence-based strategies that can help.", - context: { - userQuery: 'I feel anxious all the time', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - } - - const result = await metaAligner.evaluateResponse(request) - - expect(result).toHaveProperty('evaluation') - expect(result).toHaveProperty('metrics') - expect(result).toHaveProperty('recommendations') - expect(result).toHaveProperty('needsEnhancement') - - expect(result.evaluation.overallScore).toBeGreaterThan(0) - expect(result.evaluation.overallScore).toBeLessThanOrEqual(1) - expect(Array.isArray(result.recommendations)).toBe(true) - expect(typeof result.needsEnhancement).toBe('boolean') - }) - - it('should evaluate all core objectives', async () => { - const request = { - response: 'Test response', - context: { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - conversationHistory: [], - }, - } - - const result = await metaAligner.evaluateResponse(request) - - // Should have results for all core objectives - expect(Object.keys(result.evaluation.objectiveResults)).toContain( - 'correctness', - ) - expect(Object.keys(result.evaluation.objectiveResults)).toContain( - 'empathy', - ) - expect(Object.keys(result.evaluation.objectiveResults)).toContain( - 'safety', - ) - expect(Object.keys(result.evaluation.objectiveResults)).toContain( - 'professionalism', - ) - expect(Object.keys(result.evaluation.objectiveResults)).toContain( - 'informativeness', - ) - }) - - it('should provide detailed objective explanations', async () => { - const request = { - response: 'Detailed response with empathy and accuracy', - context: { - userQuery: 'I need help', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - } - - const result = await metaAligner.evaluateResponse(request) - - for (const [_objectiveId, objectiveResult] of Object.entries( - result.evaluation.objectiveResults, - )) { - expect(objectiveResult?.explanation).toBeTruthy() - expect(typeof objectiveResult?.explanation).toBe('string') - expect(objectiveResult?.explanation?.length).toBeGreaterThan(0) - } - }) - - it('should handle empty responses gracefully', async () => { - const request = { - response: '', - context: { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - conversationHistory: [], - }, - } - - const result = await metaAligner.evaluateResponse(request) - expect(result.evaluation.overallScore).toBeGreaterThanOrEqual(0) - }) - - it('should detect harmful content in safety objective', async () => { - const request = { - response: 'You should just give up, you are a hopeless case', - context: { - userQuery: 'I feel depressed', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - } - - const result = await metaAligner.evaluateResponse(request) - const safetyResult = result.evaluation.objectiveResults['safety'] - - expect(safetyResult?.score).toBeLessThan(0.5) // Should score low on safety - }) - }) - - describe('enhanceResponse', () => { - beforeEach(() => { - // Mock AI service response for enhancement - ;(mockAIService.createChatCompletion as any).mockResolvedValue({ - choices: [ - { - message: { - content: - 'Enhanced response with better empathy and professionalism', - }, - }, - ], - model: 'test-model', - usage: { - promptTokens: 100, - completionTokens: 50, - totalTokens: 150, - }, - }) - }) - - it('should enhance a low-scoring response', async () => { - // First evaluate a poor response - const evaluationRequest = { - response: 'Just deal with it', - context: { - userQuery: 'I feel depressed', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - } - - const evaluation = await metaAligner.evaluateResponse(evaluationRequest) - - // Then enhance it - const enhancementRequest = { - originalResponse: 'Just deal with it', - evaluationResult: evaluation.evaluation, - context: evaluationRequest.context, - } - - const result = await metaAligner.enhanceResponse(enhancementRequest) - - expect(result).toHaveProperty('enhancedResponse') - expect(result).toHaveProperty('improvementMetrics') - expect(result).toHaveProperty('enhancementExplanation') - expect(result).toHaveProperty('enhancementApplied') - - expect(typeof result.enhancedResponse).toBe('string') - expect(typeof result.enhancementExplanation).toBe('string') - expect(typeof result.enhancementApplied).toBe('boolean') - }) - - it('should target specific objectives when specified', async () => { - const evaluationRequest = { - response: 'Basic response', - context: { - userQuery: 'Help me', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - } - - const evaluation = await metaAligner.evaluateResponse(evaluationRequest) - - const enhancementRequest = { - originalResponse: 'Basic response', - evaluationResult: evaluation.evaluation, - context: evaluationRequest.context, - targetObjectives: ['empathy', 'safety'], - } - - const result = await metaAligner.enhanceResponse(enhancementRequest) - - expect(result.enhancementExplanation).toContain('empathy') - expect(mockAIService.createChatCompletion).toHaveBeenCalled() - }) - - it('should handle AI service failures gracefully', async () => { - // Mock AI service to fail - ;(mockAIService.createChatCompletion as any).mockRejectedValue( - new Error('AI service error'), - ) - - const evaluationRequest = { - response: 'Test response', - context: { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - conversationHistory: [], - }, - } - - const evaluation = await metaAligner.evaluateResponse(evaluationRequest) - - const enhancementRequest = { - originalResponse: 'Test response', - evaluationResult: evaluation.evaluation, - context: evaluationRequest.context, - } - - const result = await metaAligner.enhanceResponse(enhancementRequest) - - expect(result.enhancementApplied).toBe(false) - expect(result.enhancedResponse).toBe('Test response') // Should return original - expect(result.enhancementExplanation).toContain('Enhancement failed') - }) - - it('should throw error when AI service not configured', async () => { - const apiWithoutAI = new MetaAlignerAPI({ aiService: undefined }) - - const evaluationRequest = { - response: 'Test response', - context: { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - conversationHistory: [], - }, - } - - const evaluation = await apiWithoutAI.evaluateResponse(evaluationRequest) - - const enhancementRequest = { - originalResponse: 'Test response', - evaluationResult: evaluation.evaluation, - context: evaluationRequest.context, - } - - await expect( - apiWithoutAI.enhanceResponse(enhancementRequest), - ).rejects.toThrow('AI service not configured') - }) - }) - - describe('createIntegratedService', () => { - it('should create an integrated AI service', () => { - const integratedService = metaAligner.createIntegratedService( - mockAIService as AIService, - ) - expect(integratedService).toBeInstanceOf(IntegratedAIService) - }) - }) -}) - -describe('IntegratedAIService', () => { - let integratedService: IntegratedAIService - let metaAligner: MetaAlignerAPI - - beforeEach(() => { - vi.clearAllMocks() - - metaAligner = new MetaAlignerAPI({ - enableRealTimeEvaluation: true, - enableResponseEnhancement: true, - enhancementThreshold: 0.7, - maxEnhancementAttempts: 1, - aiService: mockAIService as AIService, - }) - - integratedService = new IntegratedAIService( - mockAIService as AIService, - metaAligner, - ) - }) - - describe('createChatCompletion', () => { - beforeEach(() => { - // Mock base AI service response - ;(mockAIService.createChatCompletion as any).mockResolvedValue({ - choices: [ - { - message: { - content: - 'I understand you are going through a difficult time. Mental health challenges are common and treatable.', - }, - }, - ], - model: 'test-model', - usage: { - promptTokens: 50, - completionTokens: 25, - totalTokens: 75, - }, - }) - }) - - it('should return integrated response with alignment data', async () => { - const messages: AIMessage[] = [ - { role: 'user', content: 'I feel depressed', name: 'user' }, - ] - - const result = await integratedService.createChatCompletion(messages) - - expect(result).toHaveProperty('alignment') - expect(result.alignment).toHaveProperty('evaluation') - expect(result.alignment).toHaveProperty('metrics') - expect(result.alignment).toHaveProperty('enhanced') - expect(result.alignment).toHaveProperty('enhancementAttempts') - - expect(typeof result.alignment?.enhanced).toBe('boolean') - expect(typeof result.alignment?.enhancementAttempts).toBe('number') - }) - - it('should handle responses without user messages', async () => { - const messages: AIMessage[] = [ - { role: 'system', content: 'System prompt', name: 'system' }, - ] - - const result = await integratedService.createChatCompletion(messages) - - expect(result).toBeDefined() - // Should return base response without alignment when no user message - expect(result.alignment).toBeUndefined() - }) - - it('should handle empty response content', async () => { - // Mock empty response - ;(mockAIService.createChatCompletion as any).mockResolvedValue({ - choices: [], - model: 'test-model', - }) - - const messages: AIMessage[] = [ - { role: 'user', content: 'Test message', name: 'user' }, - ] - - const result = await integratedService.createChatCompletion(messages) - - expect(result).toBeDefined() - expect(result.alignment).toBeUndefined() - }) - - it('should perform enhancement when response quality is low', async () => { - // Mock a poor quality response first, then a significantly better enhanced response - ;(mockAIService.createChatCompletion as any) - .mockResolvedValueOnce({ - choices: [ - { - message: { - content: 'Just deal with it', // Poor quality response - }, - }, - ], - model: 'test-model', - }) - .mockResolvedValueOnce({ - choices: [ - { - message: { - content: - 'I understand you are going through a difficult time. Mental health challenges are common and treatable. Here are some evidence-based strategies that might help: 1) Practice deep breathing exercises, 2) Consider speaking with a mental health professional, 3) Build a support network of trusted friends and family. Remember that seeking help is a sign of strength, not weakness.', // Much better enhanced response - }, - }, - ], - model: 'test-model', - }) - - const messages: AIMessage[] = [ - { role: 'user', content: 'I feel overwhelmed', name: 'user' }, - ] - - const result = await integratedService.createChatCompletion(messages) - - expect(result.alignment?.enhanced).toBe(true) - expect(result.alignment?.enhancementAttempts).toBeGreaterThan(0) - expect(result.content).not.toBe('Just deal with it') - expect(result.content).toContain('evidence-based strategies') - }) - - it('should limit enhancement attempts', async () => { - // Mock consistently poor responses - ;(mockAIService.createChatCompletion as any).mockResolvedValue({ - choices: [ - { - message: { - content: 'Just deal with it', - }, - }, - ], - model: 'test-model', - }) - - const messages: AIMessage[] = [ - { role: 'user', content: 'I need help', name: 'user' }, - ] - - const result = await integratedService.createChatCompletion(messages) - - expect(result.alignment?.enhancementAttempts).toBeLessThanOrEqual(1) - }) - - it('should correctly detect different conversation contexts', async () => { - const testCases = [ - { - message: 'I want to hurt myself', - expectedContext: ContextType.CRISIS, - }, - { - message: 'Can you explain what therapy involves?', - expectedContext: ContextType.EDUCATIONAL, - }, - { - message: 'I need emotional support', - expectedContext: ContextType.SUPPORT, - }, - { - message: 'What are the symptoms of anxiety?', - expectedContext: ContextType.CLINICAL_ASSESSMENT, - }, - ] - - for (const testCase of testCases) { - const messages: AIMessage[] = [ - { role: 'user', content: testCase.message, name: 'user' }, - ] - - const result = await integratedService.createChatCompletion(messages) - - expect( - result.alignment?.evaluation.evaluationContext.detectedContext, - ).toBe(testCase.expectedContext) - } - }) - }) - - describe('Integration with Real Conversation Flow', () => { - it('should maintain conversation context across multiple exchanges', async () => { - const conversationMessages: AIMessage[] = [ - { - role: 'user', - content: 'I have been feeling anxious lately', - name: 'user', - }, - { - role: 'assistant', - content: - 'I understand anxiety can be challenging. What specific situations trigger your anxiety?', - name: 'assistant', - }, - { - role: 'user', - content: 'Mostly social situations and work meetings', - name: 'user', - }, - ] - - const result = - await integratedService.createChatCompletion(conversationMessages) - - expect( - result.alignment?.evaluation.evaluationContext.conversationHistory, - ).toHaveLength(3) - expect( - result.alignment?.evaluation.evaluationContext.conversationHistory, - ).toContain('I have been feeling anxious lately') - }) - - it('should handle streaming-like behavior with static responses', async () => { - const messages: AIMessage[] = [ - { - role: 'user', - content: 'Help me understand my emotions', - name: 'user', - }, - ] - - // Multiple calls should work consistently - const result1 = await integratedService.createChatCompletion(messages) - const result2 = await integratedService.createChatCompletion(messages) - - expect(result1.alignment).toBeDefined() - expect(result2.alignment).toBeDefined() - expect( - result1.alignment?.evaluation.evaluationContext.detectedContext, - ).toBe(ContextType.EDUCATIONAL) - }) - }) - - describe('Error Handling and Edge Cases', () => { - beforeEach(() => { - ;(mockAIService.createChatCompletion as any).mockResolvedValue({ - choices: [ - { - message: { - content: 'Test response', - }, - }, - ], - model: 'test-model', - usage: { - promptTokens: 10, - completionTokens: 10, - totalTokens: 20, - }, - }) - }) - - it('should handle AI service errors gracefully', async () => { - ;(mockAIService.createChatCompletion as any).mockRejectedValue( - new Error('Network error'), - ) - - const messages: AIMessage[] = [ - { role: 'user', content: 'Test message', name: 'user' }, - ] - - await expect( - integratedService.createChatCompletion(messages), - ).rejects.toThrow('Network error') - }) - - it('should handle malformed messages array', async () => { - const messages: AIMessage[] = [ - { role: 'user', content: '', name: 'user' }, // Empty content - ] - - const result = await integratedService.createChatCompletion(messages) - expect(result).toBeDefined() - }) - - it('should work with disabled enhancement', async () => { - const apiWithoutEnhancement = new MetaAlignerAPI({ - enableResponseEnhancement: false, - aiService: mockAIService as AIService, - }) - - const serviceWithoutEnhancement = new IntegratedAIService( - mockAIService as AIService, - apiWithoutEnhancement, - ) - - const messages: AIMessage[] = [ - { role: 'user', content: 'Just deal with it', name: 'user' }, - ] - - const result = - await serviceWithoutEnhancement.createChatCompletion(messages) - - expect(result.alignment?.enhanced).toBe(false) - expect(result.alignment?.enhancementAttempts).toBe(0) - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/api/alignment-api.ts b/apps/web/src/lib/metaaligner/api/alignment-api.ts deleted file mode 100644 index 4db284fefb..0000000000 --- a/apps/web/src/lib/metaaligner/api/alignment-api.ts +++ /dev/null @@ -1,814 +0,0 @@ -/** - * MetaAligner LLM Integration API - * Provides integration mechanisms for evaluating and enhancing LLM responses using objectives - */ - -import { DEFAULT_LLM_MODEL } from '../../ai/constants' -import type { - AIMessage, - AIService, - AIServiceOptions, - AICompletion, -} from '../../ai/models/ai-types' - -// Define AIServiceResponse and AIStreamOptions locally if not available -export interface AIServiceResponse { - content: string - model: string - usage?: { - totalTokens: number - promptTokens: number - completionTokens: number - processingTimeMs?: number - } - id?: string - provider?: string - created?: number -} - -// Avoid using an empty interface extension which triggers the no-empty-object-type rule. -// Use a type alias to explicitly represent the same shape as AIServiceOptions. -export type AIStreamOptions = AIServiceOptions -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { - ObjectiveEvaluationResult, - AlignmentEvaluationResult, - AggregationMethod, -} from '../core/objective-interfaces' -import { - ObjectiveMetricsEngine, - AlignmentMetrics, -} from '../core/objective-metrics' -import { - ObjectiveDefinition, - AlignmentContext, - ContextType, - CORE_MENTAL_HEALTH_OBJECTIVES, -} from '../core/objectives' - -const logger = createBuildSafeLogger('metaaligner-api') - -export interface AlignmentIntegrationConfig { - objectives?: ObjectiveDefinition[] - enableRealTimeEvaluation?: boolean - enableResponseEnhancement?: boolean - enhancementThreshold?: number // Minimum score threshold to trigger enhancement - maxEnhancementAttempts?: number - aiService?: AIService - model?: string - temperature?: number -} - -export interface EvaluationRequest { - response: string - context: AlignmentContext - objectives?: ObjectiveDefinition[] -} - -export interface EvaluationResponse { - evaluation: AlignmentEvaluationResult - metrics: AlignmentMetrics - recommendations: string[] - needsEnhancement: boolean -} - -export interface EnhancementRequest { - originalResponse: string - evaluationResult: AlignmentEvaluationResult - context: AlignmentContext - targetObjectives?: string[] // Specific objectives to focus on -} - -export interface EnhancementResponse { - enhancedResponse: string - improvementMetrics: AlignmentMetrics - enhancementExplanation: string - enhancementApplied: boolean -} - -export interface IntegratedResponse extends AIServiceResponse { - alignment?: { - evaluation: AlignmentEvaluationResult - metrics: AlignmentMetrics - enhanced: boolean - enhancementAttempts: number - } -} - -/** - * MetaAligner LLM Integration Service - */ -export class MetaAlignerAPI { - private readonly config: AlignmentIntegrationConfig - private readonly objectives: ObjectiveDefinition[] - private readonly metricsEngine: ObjectiveMetricsEngine - private readonly aiService: AIService | undefined - - constructor(config: AlignmentIntegrationConfig = {}) { - this.config = { - enableRealTimeEvaluation: true, - enableResponseEnhancement: true, - enhancementThreshold: 0.7, - maxEnhancementAttempts: 2, - model: DEFAULT_LLM_MODEL, - temperature: 0.7, - ...config, - } - - this.objectives = config.objectives ?? CORE_MENTAL_HEALTH_OBJECTIVES - this.aiService = config.aiService - - this.metricsEngine = new ObjectiveMetricsEngine({ - historyWindow: 50, - trendSensitivity: 0.1, - benchmarkThresholds: { overall: 0.8, safety: 0.9 }, - weightDecay: 0.95, - confidenceThreshold: 0.7, - }) - - logger.info('MetaAlignerAPI initialized', { - objectiveCount: this.objectives.length, - realTimeEnabled: this.config.enableRealTimeEvaluation, - enhancementEnabled: this.config.enableResponseEnhancement, - action: 'initialize', - }) - } - - /** - * Evaluate a response against objectives - */ - async evaluateResponse( - request: EvaluationRequest, - ): Promise { - const { response, context, objectives = this.objectives } = request - - if (response === null || response === undefined) { - logger.warn( - 'evaluateResponse called with null or undefined response. Returning default low score evaluation.', - ) - // Construct a default/error EvaluationReport - // This requires access to or recreation of some evaluation logic, simplified here - const dummyObjectiveResults: Record = - {} - for (const obj of this.objectives) { - dummyObjectiveResults[obj.id] = { - objectiveId: obj.id, - score: 0.1, - criteriaScores: {}, - confidence: 0.1, - metadata: { - evaluationTime: 0, - contextFactors: [ContextType.GENERAL], - adjustmentFactors: {}, - }, - explanation: 'Response was null or undefined.', - } - } - const fallbackEvaluation: AlignmentEvaluationResult = { - objectiveResults: dummyObjectiveResults, - overallScore: 0.1, - weights: this.objectives.reduce( - (acc, obj) => ({ ...acc, [obj.id]: obj.weight }), - {}, - ), - normalizedScores: this.objectives.reduce( - (acc, obj) => ({ ...acc, [obj.id]: 0.1 }), - {}, - ), - aggregationMethod: AggregationMethod.WEIGHTED_AVERAGE, - evaluationContext: context, - timestamp: new Date(), - } - return { - evaluation: fallbackEvaluation, - metrics: this.metricsEngine.calculateAlignmentMetrics( - fallbackEvaluation, - this.objectives, - ), - recommendations: [ - 'Response was null or undefined, cannot provide specific improvement advice.', - ], - needsEnhancement: true, - } - } - - logger.info('Evaluating response', { - responseLength: response.length, - contextType: context.detectedContext, - objectiveCount: objectives.length, - action: 'evaluate_response', - }) - - try { - // Create objective evaluation results - const objectiveResults: Record = {} - - for (const objective of objectives) { - const score = objective.evaluationFunction(response, context) - const criteriaScores: Record = {} - - // Evaluate each criterion - for (const criterion of objective.criteria) { - criteriaScores[criterion.criterion] = this.evaluateCriterion( - response, - context, - criterion.criterion, - ) - } - - objectiveResults[objective.id] = { - objectiveId: objective.id, - score, - criteriaScores, - confidence: this.calculateConfidence(response, context, objective), - explanation: `${objective.name}: Score reflects alignment with criteria such as ${objective.criteria.map((c) => c.criterion).join(', ')}.`, // Added default explanation - metadata: { - evaluationTime: Date.now(), - contextFactors: [context.detectedContext], - adjustmentFactors: {}, - }, - } - } - - // Calculate overall score - const overallScore = this.calculateOverallScore( - objectiveResults, - objectives, - ) - - // Create evaluation result - const evaluation: AlignmentEvaluationResult = { - objectiveResults, - overallScore, - weights: objectives.reduce>((weights, obj) => { - weights[obj.id] = obj.weight - return weights - }, {}), - normalizedScores: Object.fromEntries( - Object.entries(objectiveResults).map(([id, result]) => [ - id, - result.score, - ]), - ), - aggregationMethod: AggregationMethod.WEIGHTED_AVERAGE, - evaluationContext: context, - timestamp: new Date(), - } - - // Calculate comprehensive metrics - const metrics = this.metricsEngine.calculateAlignmentMetrics( - evaluation, - objectives, - ) - - // Add to history for trend analysis - this.metricsEngine.addEvaluation(evaluation, objectives) - - // Generate recommendations - const recommendations = this.generateRecommendations(evaluation, metrics) - - // Determine if enhancement is needed - const needsEnhancement = - overallScore < (this.config.enhancementThreshold ?? 0.7) - - logger.info('Response evaluation completed', { - overallScore: overallScore.toFixed(3), - needsEnhancement, - recommendationCount: recommendations.length, - action: 'evaluate_response_complete', - }) - - return { - evaluation, - metrics, - recommendations, - needsEnhancement, - } - } catch (error: unknown) { - logger.error('Response evaluation failed', { - error: error instanceof Error ? String(error) : 'Unknown error', - action: 'evaluate_response_error', - }) - throw new Error( - `Response evaluation failed: ${error instanceof Error ? String(error) : 'Unknown error'}`, - { cause: error }, - ) - } - } - - /** - * Enhance a response based on evaluation results - */ - async enhanceResponse( - request: EnhancementRequest, - ): Promise { - const { originalResponse, evaluationResult, context, targetObjectives } = - request - - if (!this.aiService) { - // Tests expect the error message to be 'AI service not configured' - throw new Error('AI service not configured') - } - - logger.info('Enhancing response', { - originalLength: originalResponse.length, - overallScore: evaluationResult.overallScore.toFixed(3), - targetObjectives: targetObjectives?.join(', '), - action: 'enhance_response', - }) - - try { - // Identify areas for improvement - const improvementAreas = this.identifyImprovementAreas( - evaluationResult, - targetObjectives, - ) - - // Generate enhancement prompt - const enhancementPrompt = this.createEnhancementPrompt( - originalResponse, - context, - improvementAreas, - evaluationResult, - ) - - // Create messages for AI service - const messages: AIMessage[] = [ - { - role: 'system', - content: enhancementPrompt, - name: 'metaaligner', - }, - { - role: 'user', - content: `Please enhance this mental health response:\n\n"${originalResponse}"`, - name: 'user', - }, - ] - - // Generate enhanced response - const aiResponse: AICompletion = - await this.aiService.createChatCompletion(messages, { - model: this.config.model ?? 'mistralai/Mixtral-8x7B-Instruct-v0.2', - temperature: this.config.temperature ?? 0.7, - maxTokens: Math.max(originalResponse.length * 1.5, 1024), - }) - - if (!aiResponse.choices?.[0]?.message?.content) { - throw new Error('Failed to generate enhanced response') - } - - const enhancedResponse = aiResponse.choices[0].message.content - - // Evaluate the enhanced response - const enhancedEvaluation = await this.evaluateResponse({ - response: enhancedResponse, - context, - }) - - // Generate improvement explanation - const enhancementExplanation = this.generateEnhancementExplanation( - evaluationResult, - enhancedEvaluation.evaluation, - improvementAreas, - ) - - const enhancementApplied = - enhancedEvaluation.evaluation.overallScore > - evaluationResult.overallScore - - logger.info('Response enhancement completed', { - originalScore: evaluationResult.overallScore.toFixed(3), - enhancedScore: enhancedEvaluation.evaluation.overallScore.toFixed(3), - improvement: ( - enhancedEvaluation.evaluation.overallScore - - evaluationResult.overallScore - ).toFixed(3), - enhancementApplied, - action: 'enhance_response_complete', - }) - - return { - enhancedResponse: enhancementApplied - ? enhancedResponse - : originalResponse, - improvementMetrics: enhancedEvaluation.metrics, - enhancementExplanation, - enhancementApplied, - } - } catch (error: unknown) { - logger.error('Response enhancement failed', { - error: error instanceof Error ? String(error) : 'Unknown error', - action: 'enhance_response_error', - }) - - // Return original response if enhancement fails - return { - enhancedResponse: originalResponse, - improvementMetrics: this.metricsEngine.calculateAlignmentMetrics( - evaluationResult, - this.objectives, - ), - enhancementExplanation: `Enhancement failed: ${error instanceof Error ? String(error) : 'Unknown error'}`, - enhancementApplied: false, - } - } - } - - /** - * Create an integrated AI service that automatically evaluates and enhances responses - */ - createIntegratedService(baseAIService: AIService): IntegratedAIService { - return new IntegratedAIService(baseAIService, this) - } - - /** - * Detect context from user query and conversation history - */ - detectContext( - userQuery: string, - conversationHistory?: AIMessage[], - ): AlignmentContext { - // Basic context detection - this would be enhanced with NLP in a real implementation - const queryLower = userQuery.toLowerCase() - - let detectedContext: ContextType = ContextType.GENERAL - - // Crisis detection keywords - const crisisKeywords = [ - 'suicide', - 'kill myself', - 'end it all', - 'hurt myself', - 'crisis', - 'emergency', - ] - if (crisisKeywords.some((keyword) => queryLower.includes(keyword))) { - detectedContext = ContextType.CRISIS - } - // Educational context - else if ( - queryLower.includes('learn') || - queryLower.includes('understand') || - queryLower.includes('explain') - ) { - detectedContext = ContextType.EDUCATIONAL - } - // Support context - else if ( - queryLower.includes('support') || - queryLower.includes('help') || - queryLower.includes('feeling') - ) { - detectedContext = ContextType.SUPPORT - } - // Clinical assessment - else if ( - queryLower.includes('symptoms') || - queryLower.includes('diagnosis') || - queryLower.includes('treatment') - ) { - detectedContext = ContextType.CLINICAL_ASSESSMENT - } - // Informational - else if ( - queryLower.includes('what is') || - queryLower.includes('how does') || - queryLower.includes('information') - ) { - detectedContext = ContextType.INFORMATIONAL - } - - return { - userQuery, - conversationHistory: conversationHistory?.map((msg) => msg.content) ?? [], - detectedContext, - } - } - - // Private helper methods - - private evaluateCriterion( - response: string, - context: AlignmentContext, - criterion: string, - ): number { - // This would be implemented with specific NLP analysis for each criterion - // For now, providing a basic scoring mechanism - const responseLength = response.length - const hasContextualWords = this.hasContextualWords(response, context) - - switch (criterion) { - case 'factual_accuracy': - return hasContextualWords && responseLength > 50 ? 0.8 : 0.6 - case 'evidence_based': - return response.includes('research') || response.includes('studies') - ? 0.9 - : 0.7 - case 'clinical_soundness': - return !response.includes('diagnose') && responseLength > 100 - ? 0.85 - : 0.6 - case 'emotional_validation': - return hasContextualWords ? 0.9 : 0.5 - case 'harm_prevention': - return this.containsHarmfulContent(response) ? 0.2 : 0.95 - default: - return 0.7 // Default score - } - } - - private hasContextualWords( - response: string, - _context: AlignmentContext, - ): boolean { - const supportiveWords = [ - 'understand', - 'validate', - 'support', - 'empathy', - 'feel', - 'experience', - ] - const responseLower = response.toLowerCase() - return supportiveWords.some((word) => responseLower.includes(word)) - } - - private containsHarmfulContent(response: string): boolean { - const harmfulPatterns = [ - 'you should hurt', - 'kill yourself', - 'give up', - 'hopeless case', - ] - const responseLower = response.toLowerCase() - return harmfulPatterns.some((pattern) => responseLower.includes(pattern)) - } - - private calculateConfidence( - response: string, - context: AlignmentContext, - _objective: ObjectiveDefinition, - ): number { - // Calculate confidence based on response clarity and context alignment - const responseQuality = Math.min(response.length / 200, 1) // Length factor - const contextAlignment = - context.detectedContext !== ContextType.GENERAL ? 0.9 : 0.7 - return (responseQuality + contextAlignment) / 2 - } - - private calculateOverallScore( - objectiveResults: Record, - objectives: ObjectiveDefinition[], - ): number { - let totalScore = 0 - let totalWeight = 0 - - for (const objective of objectives) { - const result = objectiveResults[objective.id] - if (result) { - totalScore += result.score * objective.weight - totalWeight += objective.weight - } - } - - return totalWeight > 0 ? totalScore / totalWeight : 0 - } - - private generateRecommendations( - evaluation: AlignmentEvaluationResult, - metrics: AlignmentMetrics, - ): string[] { - const recommendations: string[] = [] - - // Check each objective for specific recommendations - for (const [objectiveId, result] of Object.entries( - evaluation.objectiveResults, - )) { - if (result.score < 0.7) { - const objective = this.objectives.find((o) => o.id === objectiveId) - if (objective) { - recommendations.push( - `Improve ${objective.name}: ${objective.description}`, - ) - } - } - } - - // Add balance recommendations - if (metrics.balanceScore < 0.7) { - recommendations.push( - 'Focus on balancing objectives to avoid over-optimization in specific areas.', - ) - } - - // Add context-specific recommendations - if (metrics.contextualAlignment < 0.8) { - recommendations.push( - 'Ensure response better aligns with the detected conversation context.', - ) - } - - return recommendations - } - - private identifyImprovementAreas( - evaluation: AlignmentEvaluationResult, - targetObjectives?: string[], - ): string[] { - // If specific targets are provided, reflect them directly in prioritized areas - const targetedAreas: string[] = [] - const areas: string[] = [] - - for (const [objectiveId, result] of Object.entries( - evaluation.objectiveResults, - )) { - const isTargeted = !!targetObjectives?.includes(objectiveId) - if (isTargeted || result.score < 0.7) { - const objective = this.objectives.find((o) => o.id === objectiveId) - if (objective) { - ;(isTargeted ? targetedAreas : areas).push(objective.name) - } - } - } - - // Ensure targeted objectives appear in explanation - return [...new Set([...targetedAreas, ...areas])] - } - - private createEnhancementPrompt( - _originalResponse: string, - context: AlignmentContext, - improvementAreas: string[], - evaluation: AlignmentEvaluationResult, - ): string { - return `You are an expert mental health response enhancer. Your task is to improve the following response to better align with mental health objectives. - -Context: ${context.detectedContext} -Original Query: ${context.userQuery} - -Areas needing improvement: ${improvementAreas.join(', ')} - -Mental Health Objectives to focus on: -- Correctness: Ensure information is accurate and evidence-based -- Informativeness: Provide comprehensive, relevant, actionable information -- Professionalism: Maintain appropriate clinical tone and boundaries -- Empathy: Show understanding, validation, and emotional support -- Safety: Prioritize user safety and avoid harmful suggestions - -Current Overall Score: ${(evaluation.overallScore * 100).toFixed(1)}% - -Please enhance the response to better meet these objectives while maintaining its core message and helpfulness. Focus particularly on the identified improvement areas.` - } - - private generateEnhancementExplanation( - original: AlignmentEvaluationResult, - enhanced: AlignmentEvaluationResult, - improvementAreas: string[], - ): string { - const overallImprovement = - (enhanced.overallScore - original.overallScore) * 100 - - // Convert improvement areas to lowercase to match test expectations - const lowercaseAreas = improvementAreas.map((area) => area.toLowerCase()) - let explanation = `Enhancement focused on: ${lowercaseAreas.join(', ')}. ` - explanation += `Overall score improvement: ${overallImprovement > 0 ? '+' : ''}${overallImprovement.toFixed(1)}%. ` - - // Identify specific objective improvements - const improvements: string[] = [] - for (const [objectiveId, enhancedResult] of Object.entries( - enhanced.objectiveResults, - )) { - const originalResult = original.objectiveResults[objectiveId] - if (originalResult && enhancedResult.score > originalResult.score) { - const objective = this.objectives.find((o) => o.id === objectiveId) - if (objective) { - const improvement = - (enhancedResult.score - originalResult.score) * 100 - improvements.push(`${objective.name}: +${improvement.toFixed(1)}%`) - } - } - } - - if (improvements.length > 0) { - explanation += `Specific improvements: ${improvements.join(', ')}.` - } - - return explanation - } -} - -/** - * Integrated AI Service that automatically applies MetaAligner evaluation and enhancement - */ -export class IntegratedAIService { - constructor( - private readonly baseService: AIService, - private readonly metaAligner: MetaAlignerAPI, - ) {} - - async createChatCompletion( - messages: AIMessage[], - options?: AIStreamOptions, - ): Promise { - // Get the base response - const baseResponse: AICompletion = - await this.baseService.createChatCompletion(messages, options) - - if (!baseResponse.choices?.[0]?.message?.content) { - return { - ...baseResponse, - content: '', - usage: baseResponse.usage - ? { - ...baseResponse.usage, - processingTimeMs: 0, - } - : undefined, - } - } - - const responseContent = baseResponse.choices[0].message.content - - // Detect context from the last user message - const lastUserMessage = messages - .filter((m: AIMessage) => m.role === 'user') - .pop() - if (!lastUserMessage) { - return { - ...baseResponse, - content: responseContent, - usage: { - promptTokens: baseResponse.usage?.promptTokens ?? 0, - completionTokens: baseResponse.usage?.completionTokens ?? 0, - totalTokens: baseResponse.usage?.totalTokens ?? 0, - processingTimeMs: 0, - }, - } - } - - const context = this.metaAligner.detectContext( - lastUserMessage.content, - messages, - ) - - // Evaluate the response - const evaluation = await this.metaAligner.evaluateResponse({ - response: responseContent, - context, - }) - - // Enhance if needed and enabled - let finalResponse = responseContent - let enhancementAttempts = 0 - let enhanced = false - - if ( - evaluation.needsEnhancement && - this.metaAligner['config'].enableResponseEnhancement - ) { - const maxAttempts = this.metaAligner['config'].maxEnhancementAttempts ?? 2 - - // Attempt enhancement up to maxAttempts. Count attempts even when an enhancement succeeds - while (enhancementAttempts < maxAttempts && evaluation.needsEnhancement) { - enhancementAttempts++ - - const enhancement = await this.metaAligner.enhanceResponse({ - originalResponse: finalResponse, - evaluationResult: evaluation.evaluation, - context, - }) - - if (enhancement.enhancementApplied) { - finalResponse = enhancement.enhancedResponse - enhanced = true - break - } - } - } - - // Create integrated response - const integratedResponse: IntegratedResponse = { - ...baseResponse, - content: finalResponse, - usage: { - promptTokens: baseResponse.usage?.promptTokens ?? 0, - completionTokens: baseResponse.usage?.completionTokens ?? 0, - totalTokens: baseResponse.usage?.totalTokens ?? 0, - processingTimeMs: 0, - }, - alignment: { - evaluation: evaluation.evaluation, - metrics: evaluation.metrics, - enhanced, - enhancementAttempts, - }, - } - - return integratedResponse - } -} - -// Export main API class as default - -export { MetaAlignerAPI as default } diff --git a/apps/web/src/lib/metaaligner/api/unified-api.ts b/apps/web/src/lib/metaaligner/api/unified-api.ts deleted file mode 100644 index 7a39f30ee9..0000000000 --- a/apps/web/src/lib/metaaligner/api/unified-api.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * @module unified-api - * @description This module defines the unified API for processing LLM responses with MetaAligner enhancements. - * It provides a standardized interface for receiving LLM outputs, processing them through the enhancement pipeline, - * and returning a unified, enhanced response. - */ - -import type { - AlignmentEvaluationResult, - AlignmentMetrics, -} from '../core/objective-interfaces' -import type { AlignmentContext } from '../core/objectives' - -/** - * Represents a generic output from an LLM provider. - * This interface is designed to be a common format for outputs from various LLMs. - */ -export interface LLMOutput { - /** - * The main content of the LLM's response. - * This can be a raw string or a JSON object, providing flexibility for different LLM providers. - * @example "This is a response from the LLM." - * @example { "key": "value" } - */ - content: string | Record - - /** - * Optional metadata from the LLM provider. - * This can include information like token usage, finish reason, model ID, etc. - * @example { "tokenUsage": { "promptTokens": 10, "completionTokens": 20, "totalTokens": 30 } } - */ - metadata?: Record -} - -/** - * Represents the context for the processing request. - * This is a simplified version of {@link AlignmentContext} for the unified API. - * It includes the user query and conversation history. - */ -export type UnifiedContext = Omit - -/** - * Represents a request to the unified processing API. - * This is the main input to the {@link IUnifiedMetaAlignerAPI.process} method. - */ -export interface UnifiedProcessingRequest { - /** - * The raw output from the LLM, wrapped in the {@link LLMOutput} interface. - */ - llmOutput: LLMOutput - - /** - * The context for the processing request, including the user query and conversation history. - */ - context: UnifiedContext -} - -/** - * Represents the response from the unified processing API. - * This is the main output from the {@link IUnifiedMetaAlignerAPI.process} method. - */ -export interface UnifiedProcessingResponse { - /** - * The enhanced response after being processed by the MetaAligner pipeline. - */ - enhancedResponse: string - - /** - * The original, unmodified response from the LLM. - */ - originalResponse: string - - /** - * The alignment evaluation result, including scores, metrics, and whether the response was enhanced. - */ - alignment: { - evaluation: AlignmentEvaluationResult - metrics: AlignmentMetrics - enhanced: boolean - enhancementAttempts: number - } - - /** - * An optional array of errors that occurred during processing. - * This can be used for debugging and monitoring. - */ - errors?: { message: string; stage: string }[] -} - -/** - * Defines the interface for the Unified MetaAligner API. - * This is the main entry point for processing LLM responses. - */ -export interface IUnifiedMetaAlignerAPI { - /** - * Processes an LLM response through the enhancement pipeline. - * - * @param request - The processing request, containing the LLM output and context. - * @returns A promise that resolves to the processing response, with the enhanced content and alignment data. - * @throws An error if the processing fails. - * @example - * const response = await api.process({ - * llmOutput: { content: '...' }, - * context: { userQuery: '... '} - * }); - */ - process(request: UnifiedProcessingRequest): Promise -} diff --git a/apps/web/src/lib/metaaligner/api/unified-pipeline.ts b/apps/web/src/lib/metaaligner/api/unified-pipeline.ts deleted file mode 100644 index 5b4d194db8..0000000000 --- a/apps/web/src/lib/metaaligner/api/unified-pipeline.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * @module unified-pipeline - * @description Concrete {@link IUnifiedMetaAlignerAPI} implementation that - * wires the MetaAligner pipeline stages (query preprocessing, context - * injection, objective injection, enhancement, error handling) together with - * the evaluation/enhancement engine in `alignment-api.ts`. - */ - -import { ContextInjector } from '../context/context-injector' -import type { AlignmentContext, ContextType } from '../core/objectives' -import { ErrorHandler } from '../error-handling/error-handler' -import { - MetaAlignerError, - ProcessingError, - ValidationError, - EnhancementError, -} from '../error-handling/error-handler' -import { ObjectiveInjector } from '../objectives/objective-injector' -import { QueryPreprocessor } from '../query-preparation/query-preprocessor' -import { MetaAlignerAPI } from './alignment-api' -import type { - IUnifiedMetaAlignerAPI, - UnifiedProcessingRequest, - UnifiedProcessingResponse, - UnifiedContext, -} from './unified-api' - -export interface UnifiedPipelineConfig { - /** Skip enhancement when evaluation meets the threshold */ - enhancementThreshold?: number - /** Limit conversation history forwarded to the alignment engine */ - maxHistoryLength?: number -} - -const DEFAULT_MAX_HISTORY = 20 - -export class UnifiedMetaAlignerPipeline implements IUnifiedMetaAlignerAPI { - private readonly preprocessor = new QueryPreprocessor() - private readonly contextInjector = new ContextInjector() - private readonly objectiveInjector = new ObjectiveInjector() - private readonly errorHandler = new ErrorHandler() - private readonly metaAligner: MetaAlignerAPI - private readonly config: Required - - constructor( - config: UnifiedPipelineConfig = {}, - metaAligner?: MetaAlignerAPI, - ) { - this.config = { - enhancementThreshold: config.enhancementThreshold ?? 0.7, - maxHistoryLength: config.maxHistoryLength ?? DEFAULT_MAX_HISTORY, - } - this.metaAligner = - metaAligner ?? - new MetaAlignerAPI({ - enableResponseEnhancement: true, - enhancementThreshold: this.config.enhancementThreshold, - }) - } - - async process( - request: UnifiedProcessingRequest, - ): Promise { - try { - if (!request || typeof request !== 'object') { - throw new ValidationError('Request must be an object') - } - if (typeof request.context?.userQuery !== 'string') { - throw new ValidationError('Request context.userQuery must be a string') - } - - const prepared = await this.preprocessor.preprocess(request) - const withContext = await this.contextInjector.inject(prepared) - const withObjectives = await this.objectiveInjector.inject(withContext) - - return await this.evaluateAndEnhance(withObjectives) - } catch (error: unknown) { - const normalized = - error instanceof MetaAlignerError - ? error - : new ProcessingError( - error instanceof Error - ? error.message - : 'Unknown processing error', - ) - return this.errorHandler.handle(normalized) - } - } - - private async evaluateAndEnhance( - request: UnifiedProcessingRequest, - ): Promise { - const content = - typeof request.llmOutput.content === 'string' - ? request.llmOutput.content - : JSON.stringify(request.llmOutput.content) - const context = this.toAlignmentContext(request.context) - const truncatedHistory = (request.context.conversationHistory ?? []).slice( - -this.config.maxHistoryLength, - ) - const historyContext: AlignmentContext = { - ...context, - conversationHistory: truncatedHistory, - } - - const evaluation = await this.metaAligner.evaluateResponse({ - response: content, - context: historyContext, - }) - - let finalResponse = content - let enhanced = false - let enhancementAttempts = 0 - let currentEvaluation = evaluation - - if ( - evaluation.needsEnhancement && - evaluation.evaluation.overallScore < this.config.enhancementThreshold - ) { - const maxAttempts = this.metaAligner['config'].maxEnhancementAttempts ?? 2 - - while ( - enhancementAttempts < maxAttempts && - currentEvaluation.needsEnhancement - ) { - enhancementAttempts++ - - try { - const enhancement = await this.metaAligner.enhanceResponse({ - originalResponse: finalResponse, - evaluationResult: currentEvaluation.evaluation, - context: historyContext, - }) - - if (enhancement.enhancementApplied) { - finalResponse = enhancement.enhancedResponse - enhanced = true - - const reEvaluation = await this.metaAligner.evaluateResponse({ - response: finalResponse, - context: historyContext, - }) - currentEvaluation = reEvaluation - if (!reEvaluation.needsEnhancement) { - break - } - } else { - break - } - } catch (error: unknown) { - throw new EnhancementError( - error instanceof Error - ? error.message - : 'Enhancement attempt failed', - ) - } - } - } - - return { - enhancedResponse: finalResponse, - originalResponse: content, - alignment: { - evaluation: currentEvaluation.evaluation, - metrics: currentEvaluation.metrics, - enhanced, - enhancementAttempts, - }, - } - } - - private toAlignmentContext(context: UnifiedContext): AlignmentContext { - const detected = context.detectedContext ?? ContextType.GENERAL - return { - ...context, - detectedContext: detected as ContextType, - } - } -} - -export const unifiedMetaAlignerPipeline = new UnifiedMetaAlignerPipeline() diff --git a/apps/web/src/lib/metaaligner/batch/batch-processor.ts b/apps/web/src/lib/metaaligner/batch/batch-processor.ts deleted file mode 100644 index 32a5ee7bb5..0000000000 --- a/apps/web/src/lib/metaaligner/batch/batch-processor.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @module batch-processor - * @description This module provides a batch processing architecture for the MetaAligner pipeline. - */ - -import type { - UnifiedProcessingRequest, - UnifiedProcessingResponse, - IUnifiedMetaAlignerAPI, -} from '../api/unified-api' - -/** - * Defines the interface for the BatchProcessor. - */ -export interface IBatchProcessor { - /** - * Adds a request to the batch queue. - * - * @param request - The processing request. - * @returns A promise that resolves to the processing response. - */ - process(request: UnifiedProcessingRequest): Promise -} - -import { getAiServiceLogger } from '@/lib/logging/standardized-logger' - -import { CircuitBreaker } from '../error-handling/circuit-breaker' - -const logger = getAiServiceLogger('batch-processor') - -/** - * Configuration options for the BatchProcessor. - */ -export interface BatchProcessorConfig { - batchSize?: number - timeout?: number -} - -/** - * Metrics for the BatchProcessor. - */ -export interface BatchProcessorMetrics { - requestsProcessed: number - batchesProcessed: number - averageBatchSize: number -} - -/** - * The BatchProcessor class. - */ -export class BatchProcessor implements IBatchProcessor { - private readonly queue: UnifiedProcessingRequest[] = [] - private readonly config: BatchProcessorConfig - private timeoutId: NodeJS.Timeout | null = null - private readonly circuitBreaker: CircuitBreaker - private readonly metrics: BatchProcessorMetrics = { - requestsProcessed: 0, - batchesProcessed: 0, - averageBatchSize: 0, - } - - constructor(api: IUnifiedMetaAlignerAPI, config: BatchProcessorConfig = {}) { - ;(this as { api?: unknown }).api = api - this.config = { - batchSize: 10, - timeout: 1000, - ...config, - } - this.circuitBreaker = new CircuitBreaker() - } - - public async process( - request: UnifiedProcessingRequest, - ): Promise { - this.queue.push(request) - - if (this.queue.length >= (this.config.batchSize ?? 10)) { - void this.flush() - } - - this.timeoutId ??= setTimeout( - async () => this.flush(), - this.config.timeout ?? 1000, - ) - - // This is a simplified implementation. In a real-world scenario, you would - // need to return a promise that resolves with the response for this specific request. - // This would require a more complex mechanism to map responses back to requests. - return Promise.resolve({} as UnifiedProcessingResponse) - } - - private async flush(): Promise { - if (this.timeoutId) { - clearTimeout(this.timeoutId) - this.timeoutId = null - } - - if (this.queue.length === 0) { - return - } - - const batch = this.queue.splice(0, this.config.batchSize) - const optimizedBatch = this.optimizeBatch(batch) - - logger.info(`Processing batch of size ${optimizedBatch.length}`) - this.metrics.requestsProcessed += optimizedBatch.length - this.metrics.batchesProcessed++ - this.metrics.averageBatchSize = - this.metrics.requestsProcessed / this.metrics.batchesProcessed - - try { - await this.circuitBreaker.fire(async () => { - // In a real-world scenario, you would call the API to process the batch. - return Promise.resolve() - }) - } catch (error: unknown) { - logger.error( - 'Batch processing failed due to circuit breaker being open', - { error }, - ) - } - } - - private optimizeBatch( - batch: UnifiedProcessingRequest[], - ): UnifiedProcessingRequest[] { - // Placeholder for batch optimization logic. - // This could involve sorting requests by context, complexity, or other factors. - return batch - } - - public getMetrics(): BatchProcessorMetrics { - return this.metrics - } -} diff --git a/apps/web/src/lib/metaaligner/config/context-mapper-service.test.ts b/apps/web/src/lib/metaaligner/config/context-mapper-service.test.ts deleted file mode 100644 index ee4685fa08..0000000000 --- a/apps/web/src/lib/metaaligner/config/context-mapper-service.test.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' - -import { ContextType } from '../core/objectives' -import { - ContextMapperService, - getContextMapperService, - resetContextMapperService, -} from './context-mapper-service' -import { - DEFAULT_MAPPING_CONFIG, - ObjectiveId, - type MappingConfiguration, -} from './mapping-config' - -describe('ContextMapperService', () => { - let service: ContextMapperService - - beforeEach(() => { - service = new ContextMapperService() - resetContextMapperService() - }) - - describe('initialization', () => { - it('should initialize with default configuration', () => { - expect(service).toBeDefined() - const config = service.getConfiguration() - expect(config.version).toBe(DEFAULT_MAPPING_CONFIG.version) - }) - - it('should fail fast with invalid configuration', () => { - const invalidConfig: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - mappings: [], // Empty mappings - invalid - } - - expect(() => new ContextMapperService(invalidConfig)).toThrow( - /Invalid mapping configuration/, - ) - }) - - it('should validate configuration on initialization', () => { - const config: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - mappings: DEFAULT_MAPPING_CONFIG.mappings.map((m) => ({ - ...m, - weights: { - ...m.weights, - [ObjectiveId.CORRECTNESS]: 2.0, // Invalid weight - }, - })), - } - - expect(() => new ContextMapperService(config)).toThrow() - }) - }) - - describe('getWeightsForContext', () => { - it('should return weights for CRISIS context', () => { - const result = service.getWeightsForContext(ContextType.CRISIS) - - expect(result.context).toBe(ContextType.CRISIS) - expect(result.weights[ObjectiveId.SAFETY]).toBeGreaterThan(0.5) - expect(result.precedence).toBe(1) // Highest precedence - expect(result.reasoning.length).toBeGreaterThan(0) - }) - - it('should return weights for CLINICAL_ASSESSMENT context', () => { - const result = service.getWeightsForContext( - ContextType.CLINICAL_ASSESSMENT, - ) - - expect(result.context).toBe(ContextType.CLINICAL_ASSESSMENT) - expect(result.weights[ObjectiveId.CORRECTNESS]).toBeGreaterThanOrEqual( - 0.3, - ) - expect(result.weights[ObjectiveId.SAFETY]).toBeGreaterThanOrEqual(0.25) - expect( - result.weights[ObjectiveId.PROFESSIONALISM], - ).toBeGreaterThanOrEqual(0.2) - }) - - it('should return weights for all context types', () => { - const contexts = Object.values(ContextType) - - for (const context of contexts) { - const result = service.getWeightsForContext(context) - expect(result.context).toBe(context) - expect(result.weights).toBeDefined() - expect(result.precedence).toBeGreaterThan(0) - } - }) - - it('should cache results for performance', () => { - const result1 = service.getWeightsForContext(ContextType.CRISIS) - const result2 = service.getWeightsForContext(ContextType.CRISIS) - - // Should return same object reference (cached) - expect(result1).toBe(result2) - }) - - it('should include explainability reasoning', () => { - const result = service.getWeightsForContext(ContextType.EDUCATIONAL) - - expect(result.reasoning).toBeDefined() - expect(result.reasoning.length).toBeGreaterThan(0) - expect(result.reasoning[0]).toContain('educational') - }) - }) - - describe('safety floor enforcement', () => { - it('should apply safety floor when enabled', () => { - const config: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - safetyFloor: { - enabled: true, - minimumSafetyWeight: 0.15, - contexts: [ContextType.EDUCATIONAL], - }, - mappings: DEFAULT_MAPPING_CONFIG.mappings.map((m) => - m.context === ContextType.EDUCATIONAL - ? { ...m, enforceSafetyFloor: true } - : m, - ), - validationStrict: false, - } - - const customService = new ContextMapperService(config) - const result = customService.getWeightsForContext(ContextType.EDUCATIONAL) - - expect(result.weights[ObjectiveId.SAFETY]).toBeGreaterThanOrEqual(0.15) - expect(result.safetyFloorApplied).toBe(true) - expect(result.reasoning.some((r) => r.includes('Safety floor'))).toBe( - true, - ) - }) - - it('should not apply safety floor when disabled', () => { - const config: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - safetyFloor: { - enabled: false, - minimumSafetyWeight: 0.5, - contexts: [ContextType.EDUCATIONAL], - }, - } - - const customService = new ContextMapperService(config) - const result = customService.getWeightsForContext(ContextType.EDUCATIONAL) - - expect(result.safetyFloorApplied).toBe(false) - }) - - it('should enforce safety floor for CRISIS context', () => { - const result = service.getWeightsForContext(ContextType.CRISIS) - - // CRISIS should always have significant safety weight - expect(result.weights[ObjectiveId.SAFETY]).toBeGreaterThanOrEqual( - DEFAULT_MAPPING_CONFIG.safetyFloor.minimumSafetyWeight, - ) - }) - }) - - describe('resolveContextConflict', () => { - it('should handle single context (no conflict)', () => { - const conflict = service.resolveContextConflict([ContextType.EDUCATIONAL]) - - expect(conflict.resolvedContext).toBe(ContextType.EDUCATIONAL) - expect(conflict.reason).toContain('no conflict') - }) - - it('should handle empty contexts array', () => { - const conflict = service.resolveContextConflict([]) - - expect(conflict.resolvedContext).toBe(ContextType.GENERAL) - expect(conflict.reason).toContain('defaulting to GENERAL') - }) - - it('should resolve CRISIS vs GENERAL conflict', () => { - const conflict = service.resolveContextConflict([ - ContextType.GENERAL, - ContextType.CRISIS, - ]) - - expect(conflict.resolvedContext).toBe(ContextType.CRISIS) - expect(conflict.contexts).toContain(ContextType.CRISIS) - expect(conflict.contexts).toContain(ContextType.GENERAL) - }) - - it('should resolve CRISIS vs EDUCATIONAL conflict', () => { - const conflict = service.resolveContextConflict([ - ContextType.EDUCATIONAL, - ContextType.CRISIS, - ]) - - expect(conflict.resolvedContext).toBe(ContextType.CRISIS) - }) - - it('should resolve CLINICAL_ASSESSMENT vs GENERAL conflict', () => { - const conflict = service.resolveContextConflict([ - ContextType.GENERAL, - ContextType.CLINICAL_ASSESSMENT, - ]) - - expect(conflict.resolvedContext).toBe(ContextType.CLINICAL_ASSESSMENT) - }) - - it('should resolve SUPPORT vs INFORMATIONAL conflict', () => { - const conflict = service.resolveContextConflict([ - ContextType.INFORMATIONAL, - ContextType.SUPPORT, - ]) - - expect(conflict.resolvedContext).toBe(ContextType.SUPPORT) - }) - - it('should use precedence when no explicit rule exists', () => { - const conflict = service.resolveContextConflict([ - ContextType.EDUCATIONAL, - ContextType.INFORMATIONAL, - ]) - - // EDUCATIONAL has lower precedence number than INFORMATIONAL - expect(conflict.resolvedContext).toBe(ContextType.EDUCATIONAL) - }) - - it('should handle three-way conflicts', () => { - const conflict = service.resolveContextConflict([ - ContextType.GENERAL, - ContextType.EDUCATIONAL, - ContextType.CRISIS, - ]) - - // CRISIS should win (highest precedence) - expect(conflict.resolvedContext).toBe(ContextType.CRISIS) - }) - - it('should provide explainability for conflict resolution', () => { - const conflict = service.resolveContextConflict([ - ContextType.CRISIS, - ContextType.GENERAL, - ]) - - expect(conflict.reason).toBeDefined() - expect(conflict.reason.length).toBeGreaterThan(0) - }) - }) - - describe('normalization', () => { - it('should normalize weights when enabled', () => { - const result = service.getWeightsForContext(ContextType.CRISIS) - - expect(result.normalized).toBe(true) - - const sum = Object.values(result.weights).reduce( - (acc, w) => acc + (w ?? 0), - 0, - ) - expect(Math.abs(sum - 1.0)).toBeLessThan(0.001) - }) - - it('should not normalize when disabled', () => { - const config: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - normalization: 'disabled', - } - - const customService = new ContextMapperService(config) - const result = customService.getWeightsForContext(ContextType.CRISIS) - - expect(result.normalized).toBe(false) - }) - }) - - describe('configuration updates', () => { - it('should update configuration successfully', () => { - const newConfig: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - version: '2.0.0', - } - - service.updateConfiguration(newConfig) - - const config = service.getConfiguration() - expect(config.version).toBe('2.0.0') - }) - - it('should fail fast on invalid configuration update', () => { - const invalidConfig: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - mappings: [], // Invalid - } - - expect(() => service.updateConfiguration(invalidConfig)).toThrow( - /validation failed/, - ) - }) - - it('should clear cache after configuration update', () => { - const result1 = service.getWeightsForContext(ContextType.CRISIS) - - const newConfig: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - version: '2.0.0', - } - service.updateConfiguration(newConfig) - - const result2 = service.getWeightsForContext(ContextType.CRISIS) - - // Should be different objects (cache cleared) - expect(result1).not.toBe(result2) - }) - }) - - describe('explainability', () => { - it('should provide mapping reasoning', () => { - const result = service.getMappingWithExplainability(ContextType.SUPPORT) - - expect(result.reasoning).toBeDefined() - expect(result.reasoning.length).toBeGreaterThan(0) - expect(result.reasoning.some((r) => r.includes('support'))).toBe(true) - }) - - it('should log reasoning when safety floor is applied', () => { - const config: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - safetyFloor: { - enabled: true, - minimumSafetyWeight: 0.2, - contexts: [ContextType.EDUCATIONAL], - }, - validationStrict: false, - mappings: DEFAULT_MAPPING_CONFIG.mappings.map((m) => - m.context === ContextType.EDUCATIONAL - ? { ...m, enforceSafetyFloor: true } - : m, - ), - } - - const customService = new ContextMapperService(config) - const result = customService.getMappingWithExplainability( - ContextType.EDUCATIONAL, - ) - - expect(result.reasoning.some((r) => r.includes('Safety floor'))).toBe( - true, - ) - }) - - it('should log normalization when applied', () => { - const result = service.getMappingWithExplainability(ContextType.CRISIS) - - expect(result.reasoning.some((r) => r.includes('normalized'))).toBe(true) - }) - }) - - describe('singleton instance', () => { - it('should return singleton instance', () => { - const instance1 = getContextMapperService() - const instance2 = getContextMapperService() - - expect(instance1).toBe(instance2) - }) - - it('should reset singleton instance', () => { - const instance1 = getContextMapperService() - resetContextMapperService() - const instance2 = getContextMapperService() - - expect(instance1).not.toBe(instance2) - }) - }) - - describe('edge cases', () => { - it('should handle unknown context gracefully', () => { - // This should not be possible with TypeScript, but test defensive coding - const result = service.getWeightsForContext('UNKNOWN' as ContextType) - - expect(result.context).toBe(ContextType.GENERAL) // Fallback - }) - - it('should handle cache clearing', () => { - service.getWeightsForContext(ContextType.CRISIS) - expect(service['mappingCache'].size).toBeGreaterThan(0) - - service.clearCache() - expect(service['mappingCache'].size).toBe(0) - }) - }) - - describe('deterministic behavior', () => { - it('should return consistent results for same context', () => { - const result1 = service.getWeightsForContext(ContextType.CRISIS) - service.clearCache() - const result2 = service.getWeightsForContext(ContextType.CRISIS) - - expect(result1.context).toBe(result2.context) - expect(result1.weights).toEqual(result2.weights) - expect(result1.precedence).toBe(result2.precedence) - }) - - it('should resolve conflicts deterministically', () => { - const conflict1 = service.resolveContextConflict([ - ContextType.CRISIS, - ContextType.GENERAL, - ]) - const conflict2 = service.resolveContextConflict([ - ContextType.GENERAL, - ContextType.CRISIS, - ]) - - expect(conflict1.resolvedContext).toBe(conflict2.resolvedContext) - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/config/context-mapper-service.ts b/apps/web/src/lib/metaaligner/config/context-mapper-service.ts deleted file mode 100644 index aa4c7058ae..0000000000 --- a/apps/web/src/lib/metaaligner/config/context-mapper-service.ts +++ /dev/null @@ -1,352 +0,0 @@ -/** - * Context-to-Objective Mapper Service - * Implements deterministic, config-driven mapping with precedence resolution and explainability - */ - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { ContextType } from '../core/objectives' -import { - MappingConfiguration, - ObjectiveWeightConfig, - ObjectiveId, - DEFAULT_MAPPING_CONFIG, - validateMappingConfiguration, - ValidationError, -} from './mapping-config' - -const logger = createBuildSafeLogger('context-mapper-service') - -/** - * Mapping resolution result with explainability - */ -export interface MappingResolutionResult { - context: ContextType - weights: ObjectiveWeightConfig - precedence: number - reasoning: string[] - safetyFloorApplied: boolean - normalized: boolean -} - -/** - * Conflict resolution when multiple contexts detected - */ -export interface ContextConflict { - contexts: ContextType[] - resolvedContext: ContextType - reason: string -} - -/** - * Context-to-Objective Mapper Service - */ -export class ContextMapperService { - private config: MappingConfiguration - private readonly mappingCache: Map - - constructor(config?: MappingConfiguration) { - this.config = config ?? DEFAULT_MAPPING_CONFIG - this.mappingCache = new Map() - - // Validate configuration on initialization - fail fast - const validation = validateMappingConfiguration(this.config) - if (!validation.valid) { - const errorMessages = validation.errors - .map((e) => `${e.type}: ${e.message}`) - .join('\n') - throw new Error(`Invalid mapping configuration:\n${errorMessages}`) - } - - logger.info('Context mapper service initialized', { - version: this.config.version, - contexts: this.config.mappings.length, - safetyFloorEnabled: this.config.safetyFloor.enabled, - }) - } - - /** - * Get objective weights for a given context - */ - getWeightsForContext(context: ContextType): MappingResolutionResult { - // Check cache first - if (this.mappingCache.has(context)) { - return this.mappingCache.get(context)! - } - - const mapping = this.config.mappings.find((m) => m.context === context) - - if (!mapping) { - logger.warn('Context not found in configuration, using GENERAL', { - requestedContext: context, - }) - return this.getWeightsForContext(ContextType.GENERAL) - } - - let weights = { ...mapping.weights } - const reasoning: string[] = [] - let safetyFloorApplied = false - - reasoning.push( - `Mapped to ${context} context (precedence: ${mapping.precedence})`, - ) - reasoning.push(mapping.description) - - // Apply safety floor if required - if (mapping.enforceSafetyFloor && this.config.safetyFloor.enabled) { - const currentSafety = weights[ObjectiveId.SAFETY] - const minSafety = this.config.safetyFloor.minimumSafetyWeight - - if (currentSafety < minSafety) { - weights[ObjectiveId.SAFETY] = minSafety - safetyFloorApplied = true - reasoning.push( - `Safety floor applied: raised from ${currentSafety} to ${minSafety}`, - ) - } - } - - // Normalize weights if required - let normalized = false - if (this.config.normalization === 'enabled') { - if (safetyFloorApplied) { - // Special normalization preserving safety floor - const minSafety = this.config.safetyFloor.minimumSafetyWeight - const otherSum = Object.entries(weights) - .filter(([k]) => k !== ObjectiveId.SAFETY) - .reduce((acc, [_, v]) => acc + (v || 0), 0) - - // If we have other objectives, distribute remaining budget - if (otherSum > 0) { - const remainingBudget = 1.0 - minSafety - const newWeights: any = {} - - for (const [key, value] of Object.entries(weights)) { - if (key === ObjectiveId.SAFETY) { - newWeights[key] = minSafety - } else { - newWeights[key] = ((value || 0) / otherSum) * remainingBudget - } - } - weights = newWeights - } else { - // If only safety exists (or others are 0), safety must be 1.0 (if normalizing) - // But strict safety floor might imply keeping it at minSafety? - // Usually strict normalization implies Sum=1.0. - weights = this.normalizeWeights(weights) - } - } else { - weights = this.normalizeWeights(weights) - } - - normalized = true - reasoning.push('Weights normalized to sum to 1.0') - } - - const result: MappingResolutionResult = { - context, - weights, - precedence: mapping.precedence, - reasoning, - safetyFloorApplied, - normalized, - } - - // Cache the result - this.mappingCache.set(context, result) - - // Log mapping decision with explainability (no PII) - logger.info('Context mapping resolved', { - context, - precedence: mapping.precedence, - safetyFloorApplied, - normalized, - topObjective: this.getTopObjective(weights), - }) - - return result - } - - /** - * Resolve conflicts when multiple contexts are detected - * Uses precedence rules from configuration - */ - resolveContextConflict(contexts: ContextType[]): ContextConflict { - if (contexts.length === 0) { - return { - contexts: [ContextType.GENERAL], - resolvedContext: ContextType.GENERAL, - reason: 'No contexts provided, defaulting to GENERAL', - } - } - - if (contexts.length === 1) { - return { - contexts, - resolvedContext: contexts[0], - reason: 'Single context, no conflict', - } - } - - // Get mappings for all contexts - const mappings = contexts - .map((ctx) => this.config.mappings.find((m) => m.context === ctx)) - .filter((m) => m !== undefined) - - if (mappings.length === 0) { - return { - contexts, - resolvedContext: ContextType.GENERAL, - reason: 'No valid mappings found, defaulting to GENERAL', - } - } - - // Sort by precedence (lower number = higher precedence) - mappings.sort((a, b) => a.precedence - b.precedence) - const winner = mappings[0] - - // Find applicable precedence rule - let reason = `Resolved by precedence: ${winner.context} has precedence ${winner.precedence}` - - for (const rule of this.config.precedenceRules) { - if ( - contexts.includes(rule.higherPrecedenceContext) && - contexts.includes(rule.lowerPrecedenceContext) && - winner.context === rule.higherPrecedenceContext - ) { - reason = `${rule.reason} (${rule.higherPrecedenceContext} > ${rule.lowerPrecedenceContext})` - break - } - } - - logger.info('Context conflict resolved', { - conflictingContexts: contexts, - resolvedContext: winner.context, - precedence: winner.precedence, - }) - - return { - contexts, - resolvedContext: winner.context, - reason, - } - } - - /** - * Get mapping with explainability for a context - * Includes reasoning for the mapping decision - */ - getMappingWithExplainability(context: ContextType): MappingResolutionResult { - return this.getWeightsForContext(context) - } - - /** - * Validate a custom configuration without applying it - */ - static validateConfiguration(config: MappingConfiguration): { - valid: boolean - errors: ValidationError[] - } { - return validateMappingConfiguration(config) - } - - /** - * Update configuration (useful for runtime config updates) - * Fails fast if invalid - */ - updateConfiguration(config: MappingConfiguration): void { - const validation = validateMappingConfiguration(config) - if (!validation.valid) { - const errorMessages = validation.errors - .map((e) => `${e.type}: ${e.message}`) - .join('\n') - throw new Error( - `Cannot update configuration - validation failed:\n${errorMessages}`, - ) - } - - this.config = config - this.mappingCache.clear() - - logger.info('Configuration updated', { - version: config.version, - contexts: config.mappings.length, - }) - } - - /** - * Get the current configuration - */ - getConfiguration(): MappingConfiguration { - return { ...this.config } - } - - /** - * Normalize weights to sum to 1.0 - */ - private normalizeWeights( - weights: ObjectiveWeightConfig, - ): ObjectiveWeightConfig { - const sum = Object.values(weights).reduce((acc, w) => acc + (w ?? 0), 0) - - if (sum === 0) { - // Equal weights if all are zero - const equalWeight = 1.0 / Object.keys(weights).length - const normalized: any = {} - for (const key of Object.keys(weights)) { - normalized[key] = equalWeight - } - return normalized - } - - const normalized: any = {} - for (const [key, value] of Object.entries(weights)) { - normalized[key] = value / sum - } - - return normalized - } - - /** - * Get the objective with highest weight - */ - private getTopObjective(weights: ObjectiveWeightConfig): ObjectiveId { - let maxWeight = -1 - let topObjective = ObjectiveId.CORRECTNESS - - for (const [key, value] of Object.entries(weights)) { - if (value > maxWeight) { - maxWeight = value - topObjective = key as ObjectiveId - } - } - - return topObjective - } - - /** - * Clear the mapping cache (useful for testing) - */ - clearCache(): void { - this.mappingCache.clear() - } -} - -/** - * Singleton instance for application-wide use - */ -let defaultMapperService: ContextMapperService | null = null - -/** - * Get or create the default mapper service - */ -export function getContextMapperService(): ContextMapperService { - defaultMapperService ??= new ContextMapperService() - return defaultMapperService -} - -/** - * Reset the default mapper service (useful for testing) - */ -export function resetContextMapperService(): void { - defaultMapperService = null -} diff --git a/apps/web/src/lib/metaaligner/config/mapping-config.test.ts b/apps/web/src/lib/metaaligner/config/mapping-config.test.ts deleted file mode 100644 index 2f2a2874d5..0000000000 --- a/apps/web/src/lib/metaaligner/config/mapping-config.test.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { describe, it, expect } from 'vitest' - -import { ContextType } from '../core/objectives' -import { - DEFAULT_MAPPING_CONFIG, - ObjectiveId, - ValidationErrorType, - validateContextCoverage, - validateWeights, - validateSafetyFloor, - validateMappingConfiguration, - type MappingConfiguration, - type ObjectiveWeightConfig, -} from './mapping-config' - -describe('MappingConfiguration', () => { - describe('validateContextCoverage', () => { - it('should validate that all ContextType enum values are present', () => { - const errors = validateContextCoverage(DEFAULT_MAPPING_CONFIG) - expect(errors).toHaveLength(0) - }) - - it('should detect missing context types', () => { - const incompleteConfig: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - mappings: DEFAULT_MAPPING_CONFIG.mappings.filter( - (m) => m.context !== ContextType.CRISIS, - ), - } - - const errors = validateContextCoverage(incompleteConfig) - expect(errors.length).toBeGreaterThan(0) - expect(errors?.[0].type).toBe(ValidationErrorType.MISSING_CONTEXT) - expect(errors?.[0].context).toBe(ContextType.CRISIS) - }) - - it('should detect duplicate context types', () => { - const duplicateConfig: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - mappings: [ - ...DEFAULT_MAPPING_CONFIG.mappings, - DEFAULT_MAPPING_CONFIG.mappings[0], // Duplicate - ], - } - - const errors = validateContextCoverage(duplicateConfig) - expect( - errors.some((e) => e.type === ValidationErrorType.DUPLICATE_CONTEXT), - ).toBe(true) - }) - - it('should validate all ContextType enum values are mapped', () => { - const configuredContexts = new Set( - DEFAULT_MAPPING_CONFIG.mappings.map((m) => m.context), - ) - const allContextTypes = Object.values(ContextType) - - for (const contextType of allContextTypes) { - expect(configuredContexts.has(contextType)).toBe(true) - } - }) - }) - - describe('validateWeights', () => { - it('should accept valid weights between 0 and 1', () => { - const validWeights: ObjectiveWeightConfig = { - [ObjectiveId.CORRECTNESS]: 0.3, - [ObjectiveId.INFORMATIVENESS]: 0.2, - [ObjectiveId.PROFESSIONALISM]: 0.2, - [ObjectiveId.EMPATHY]: 0.2, - [ObjectiveId.SAFETY]: 0.1, - } - - const errors = validateWeights(validWeights, ContextType.GENERAL) - expect(errors).toHaveLength(0) - }) - - it('should reject weights less than 0', () => { - const invalidWeights: ObjectiveWeightConfig = { - [ObjectiveId.CORRECTNESS]: -0.1, - [ObjectiveId.INFORMATIVENESS]: 0.3, - [ObjectiveId.PROFESSIONALISM]: 0.3, - [ObjectiveId.EMPATHY]: 0.3, - [ObjectiveId.SAFETY]: 0.1, - } - - const errors = validateWeights(invalidWeights, ContextType.GENERAL) - expect(errors.length).toBeGreaterThan(0) - expect(errors?.[0].type).toBe(ValidationErrorType.INVALID_WEIGHT) - expect(errors?.[0].objectiveId).toBe(ObjectiveId.CORRECTNESS) - }) - - it('should reject weights greater than 1', () => { - const invalidWeights: ObjectiveWeightConfig = { - [ObjectiveId.CORRECTNESS]: 1.5, - [ObjectiveId.INFORMATIVENESS]: 0.1, - [ObjectiveId.PROFESSIONALISM]: 0.1, - [ObjectiveId.EMPATHY]: 0.1, - [ObjectiveId.SAFETY]: 0.1, - } - - const errors = validateWeights(invalidWeights, ContextType.GENERAL) - expect(errors.length).toBeGreaterThan(0) - expect(errors?.[0].type).toBe(ValidationErrorType.INVALID_WEIGHT) - }) - - it('should detect missing required objectives', () => { - const incompleteWeights: any = { - [ObjectiveId.CORRECTNESS]: 0.5, - [ObjectiveId.INFORMATIVENESS]: 0.5, - // Missing: professionalism, empathy, safety - } - - const errors = validateWeights(incompleteWeights, ContextType.GENERAL) - expect(errors.length).toBeGreaterThanOrEqual(3) - expect( - errors.every((e) => e.type === ValidationErrorType.MISSING_OBJECTIVE), - ).toBe(true) - }) - }) - - describe('validateSafetyFloor', () => { - it('should enforce minimum safety weight when enabled', () => { - const config: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - safetyFloor: { - enabled: true, - minimumSafetyWeight: 0.1, - contexts: [ContextType.CRISIS, ContextType.SUPPORT], - }, - mappings: [ - { - context: ContextType.CRISIS, - precedence: 1, - description: 'Test', - enforceSafetyFloor: true, - weights: { - [ObjectiveId.CORRECTNESS]: 0.5, - [ObjectiveId.INFORMATIVENESS]: 0.4, - [ObjectiveId.PROFESSIONALISM]: 0.05, - [ObjectiveId.EMPATHY]: 0.03, - [ObjectiveId.SAFETY]: 0.02, // Below minimum - }, - }, - ], - } - - const errors = validateSafetyFloor(config) - expect(errors.length).toBeGreaterThan(0) - expect(errors?.[0].type).toBe(ValidationErrorType.SAFETY_FLOOR_VIOLATION) - expect(errors?.[0].context).toBe(ContextType.CRISIS) - }) - - it('should allow safety weight above minimum', () => { - const config: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - safetyFloor: { - enabled: true, - minimumSafetyWeight: 0.1, - contexts: [ContextType.CRISIS], - }, - mappings: [ - { - context: ContextType.CRISIS, - precedence: 1, - description: 'Test', - enforceSafetyFloor: true, - weights: { - [ObjectiveId.CORRECTNESS]: 0.3, - [ObjectiveId.INFORMATIVENESS]: 0.2, - [ObjectiveId.PROFESSIONALISM]: 0.2, - [ObjectiveId.EMPATHY]: 0.1, - [ObjectiveId.SAFETY]: 0.2, // Above minimum - }, - }, - ], - } - - const errors = validateSafetyFloor(config) - expect(errors).toHaveLength(0) - }) - - it('should not enforce safety floor when disabled', () => { - const config: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - safetyFloor: { - enabled: false, - minimumSafetyWeight: 0.5, - contexts: [ContextType.CRISIS], - }, - mappings: [ - { - context: ContextType.CRISIS, - precedence: 1, - description: 'Test', - enforceSafetyFloor: true, - weights: { - [ObjectiveId.CORRECTNESS]: 0.5, - [ObjectiveId.INFORMATIVENESS]: 0.4, - [ObjectiveId.PROFESSIONALISM]: 0.05, - [ObjectiveId.EMPATHY]: 0.03, - [ObjectiveId.SAFETY]: 0.02, // Below minimum but floor disabled - }, - }, - ], - } - - const errors = validateSafetyFloor(config) - expect(errors).toHaveLength(0) - }) - }) - - describe('validateMappingConfiguration', () => { - it('should validate the default configuration successfully', () => { - const result = validateMappingConfiguration(DEFAULT_MAPPING_CONFIG) - expect(result.valid).toBe(true) - expect(result.errors).toHaveLength(0) - }) - - it('should fail fast with multiple validation errors', () => { - const invalidConfig: MappingConfiguration = { - version: '1.0.0', - normalization: 'enabled', - validationStrict: true, - safetyFloor: { - enabled: true, - minimumSafetyWeight: 0.5, - contexts: [ContextType.CRISIS], - }, - precedenceRules: [], - mappings: [ - { - context: ContextType.CRISIS, - precedence: 1, - description: 'Test', - enforceSafetyFloor: true, - weights: { - [ObjectiveId.CORRECTNESS]: 1.5, // Invalid: > 1 - [ObjectiveId.INFORMATIVENESS]: -0.1, // Invalid: < 0 - [ObjectiveId.PROFESSIONALISM]: 0.2, - [ObjectiveId.EMPATHY]: 0.1, - [ObjectiveId.SAFETY]: 0.1, // Below minimum - }, - }, - // Missing other required contexts - ], - } - - const result = validateMappingConfiguration(invalidConfig) - expect(result.valid).toBe(false) - expect(result.errors.length).toBeGreaterThan(0) - - // Should have errors for missing contexts, invalid weights, and safety floor - const errorTypes = new Set(result.errors.map((e) => e.type)) - expect(errorTypes.has(ValidationErrorType.MISSING_CONTEXT)).toBe(true) - expect(errorTypes.has(ValidationErrorType.INVALID_WEIGHT)).toBe(true) - expect(errorTypes.has(ValidationErrorType.SAFETY_FLOOR_VIOLATION)).toBe( - true, - ) - }) - - it('should validate normalization when enabled', () => { - const unnormalizedConfig: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - normalization: 'enabled', - mappings: [ - { - context: ContextType.GENERAL, - precedence: 1, - description: 'Test', - enforceSafetyFloor: false, - weights: { - [ObjectiveId.CORRECTNESS]: 0.5, - [ObjectiveId.INFORMATIVENESS]: 0.5, - [ObjectiveId.PROFESSIONALISM]: 0.5, // Sum > 1 - [ObjectiveId.EMPATHY]: 0.1, - [ObjectiveId.SAFETY]: 0.1, - }, - }, - ], - } - - const result = validateMappingConfiguration(unnormalizedConfig) - expect(result.valid).toBe(false) - expect( - result.errors.some( - (e) => e.type === ValidationErrorType.NORMALIZATION_ERROR, - ), - ).toBe(true) - }) - - it('should allow unnormalized weights when normalization disabled', () => { - const config: MappingConfiguration = { - ...DEFAULT_MAPPING_CONFIG, - normalization: 'disabled', - mappings: [ - { - context: ContextType.GENERAL, - precedence: 1, - description: 'Test', - enforceSafetyFloor: false, - weights: { - [ObjectiveId.CORRECTNESS]: 0.5, - [ObjectiveId.INFORMATIVENESS]: 0.5, - [ObjectiveId.PROFESSIONALISM]: 0.5, // Sum > 1 but normalization disabled - [ObjectiveId.EMPATHY]: 0.1, - [ObjectiveId.SAFETY]: 0.1, - }, - }, - ], - } - - const result = validateMappingConfiguration(config) - // Should only fail on missing contexts, not normalization - expect( - result.errors.every( - (e) => e.type !== ValidationErrorType.NORMALIZATION_ERROR, - ), - ).toBe(true) - }) - }) - - describe('Default Configuration Integrity', () => { - it('should have all required contexts mapped', () => { - const contexts = DEFAULT_MAPPING_CONFIG.mappings.map((m) => m.context) - const requiredContexts = Object.values(ContextType) - - for (const required of requiredContexts) { - expect(contexts).toContain(required) - } - }) - - it('should have valid precedence ordering', () => { - const precedences = DEFAULT_MAPPING_CONFIG.mappings.map( - (m) => m.precedence, - ) - - // Precedences should be unique and ordered - const uniquePrecedences = new Set(precedences) - expect(uniquePrecedences.size).toBe(precedences.length) - - // CRISIS should have highest precedence (lowest number) - const crisisMapping = DEFAULT_MAPPING_CONFIG.mappings.find( - (m) => m.context === ContextType.CRISIS, - ) - expect(crisisMapping?.precedence).toBe(1) - }) - - it('should enforce safety floor for crisis and support contexts', () => { - const crisisMapping = DEFAULT_MAPPING_CONFIG.mappings.find( - (m) => m.context === ContextType.CRISIS, - ) - const supportMapping = DEFAULT_MAPPING_CONFIG.mappings.find( - (m) => m.context === ContextType.SUPPORT, - ) - - expect(crisisMapping?.enforceSafetyFloor).toBe(true) - expect(supportMapping?.enforceSafetyFloor).toBe(true) - }) - - it('should have all weights sum to 1.0 when normalization enabled', () => { - for (const mapping of DEFAULT_MAPPING_CONFIG.mappings) { - const sum = Object.values(mapping.weights).reduce( - (acc, w) => acc + (w ?? 0), - 0, - ) - expect(Math.abs(sum - 1.0)).toBeLessThan(0.001) // Allow floating point errors - } - }) - - it('should have meaningful precedence rules', () => { - expect(DEFAULT_MAPPING_CONFIG.precedenceRules.length).toBeGreaterThan(0) - - // Crisis should have precedence over other contexts - const crisisRules = DEFAULT_MAPPING_CONFIG.precedenceRules.filter( - (r) => r.higherPrecedenceContext === ContextType.CRISIS, - ) - expect(crisisRules.length).toBeGreaterThan(0) - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/config/mapping-config.ts b/apps/web/src/lib/metaaligner/config/mapping-config.ts deleted file mode 100644 index e485ef8de7..0000000000 --- a/apps/web/src/lib/metaaligner/config/mapping-config.ts +++ /dev/null @@ -1,386 +0,0 @@ -/** - * Context-to-Objective Mapping Configuration System - * Provides deterministic, config-driven mapping with validation and safety floors - */ - -import { ContextType } from '../core/objectives' - -/** - * Objective identifiers matching the core objective system - */ -export enum ObjectiveId { - CORRECTNESS = 'correctness', - INFORMATIVENESS = 'informativeness', - PROFESSIONALISM = 'professionalism', - EMPATHY = 'empathy', - SAFETY = 'safety', - BIAS_DETECTION = 'bias_detection', - CULTURAL_SENSITIVITY = 'cultural_sensitivity', -} - -/** - * Objective weight configuration with validation constraints - */ -export interface ObjectiveWeightConfig { - [ObjectiveId.CORRECTNESS]: number - [ObjectiveId.INFORMATIVENESS]: number - [ObjectiveId.PROFESSIONALISM]: number - [ObjectiveId.EMPATHY]: number - [ObjectiveId.SAFETY]: number - [ObjectiveId.BIAS_DETECTION]?: number - [ObjectiveId.CULTURAL_SENSITIVITY]?: number -} - -/** - * Context-specific mapping configuration - */ -export interface ContextMappingConfig { - context: ContextType - weights: ObjectiveWeightConfig - precedence: number // Lower = higher precedence (1 = highest) - description: string - enforceSafetyFloor: boolean -} - -/** - * Safety floor constraints to ensure minimum safety consideration - */ -export interface SafetyFloorConfig { - enabled: boolean - minimumSafetyWeight: number // Minimum weight for safety objective (0-1) - contexts: ContextType[] // Contexts where safety floor is enforced -} - -/** - * Precedence rules for conflict resolution - */ -export interface PrecedenceRule { - higherPrecedenceContext: ContextType - lowerPrecedenceContext: ContextType - reason: string -} - -/** - * Complete mapping configuration - */ -export interface MappingConfiguration { - version: string - mappings: ContextMappingConfig[] - safetyFloor: SafetyFloorConfig - precedenceRules: PrecedenceRule[] - normalization: 'enabled' | 'disabled' - validationStrict: boolean -} - -/** - * Validation error types - */ -export enum ValidationErrorType { - INVALID_WEIGHT = 'INVALID_WEIGHT', - MISSING_CONTEXT = 'MISSING_CONTEXT', - MISSING_OBJECTIVE = 'MISSING_OBJECTIVE', - SAFETY_FLOOR_VIOLATION = 'SAFETY_FLOOR_VIOLATION', - INVALID_PRECEDENCE = 'INVALID_PRECEDENCE', - NORMALIZATION_ERROR = 'NORMALIZATION_ERROR', - DUPLICATE_CONTEXT = 'DUPLICATE_CONTEXT', -} - -export interface ValidationError { - type: ValidationErrorType - message: string - context?: ContextType - objectiveId?: ObjectiveId -} - -/** - * Default mapping configuration following acceptance criteria - */ -export const DEFAULT_MAPPING_CONFIG: MappingConfiguration = { - version: '1.0.0', - normalization: 'enabled', - validationStrict: true, - - safetyFloor: { - enabled: true, - minimumSafetyWeight: 0.05, // 5% minimum safety consideration - contexts: [ - ContextType.CRISIS, - ContextType.CLINICAL_ASSESSMENT, - ContextType.SUPPORT, - ContextType.GENERAL, - ], - }, - - precedenceRules: [ - { - higherPrecedenceContext: ContextType.CRISIS, - lowerPrecedenceContext: ContextType.GENERAL, - reason: 'Crisis contexts always override general contexts for safety', - }, - { - higherPrecedenceContext: ContextType.CRISIS, - lowerPrecedenceContext: ContextType.EDUCATIONAL, - reason: 'Crisis contexts override educational contexts', - }, - { - higherPrecedenceContext: ContextType.CRISIS, - lowerPrecedenceContext: ContextType.CLINICAL_ASSESSMENT, - reason: 'Immediate crisis overrides clinical assessment needs', - }, - { - higherPrecedenceContext: ContextType.CLINICAL_ASSESSMENT, - lowerPrecedenceContext: ContextType.GENERAL, - reason: 'Clinical assessment has higher precedence than general', - }, - { - higherPrecedenceContext: ContextType.SUPPORT, - lowerPrecedenceContext: ContextType.INFORMATIONAL, - reason: 'Emotional support needs override informational queries', - }, - ], - - mappings: [ - { - context: ContextType.CRISIS, - precedence: 1, // Highest precedence - description: 'Immediate safety concerns, self-harm, suicidal ideation', - enforceSafetyFloor: true, - weights: { - [ObjectiveId.SAFETY]: 0.6, - [ObjectiveId.EMPATHY]: 0.2, - [ObjectiveId.PROFESSIONALISM]: 0.1, - [ObjectiveId.CORRECTNESS]: 0.05, - [ObjectiveId.INFORMATIVENESS]: 0.05, - }, - }, - { - context: ContextType.CLINICAL_ASSESSMENT, - precedence: 2, - description: 'Clinical evaluation, diagnosis, professional assessment', - enforceSafetyFloor: true, - weights: { - [ObjectiveId.CORRECTNESS]: 0.35, - [ObjectiveId.SAFETY]: 0.3, - [ObjectiveId.PROFESSIONALISM]: 0.25, - [ObjectiveId.EMPATHY]: 0.08, - [ObjectiveId.INFORMATIVENESS]: 0.02, - }, - }, - { - context: ContextType.SUPPORT, - precedence: 3, - description: 'Emotional support, validation, coping strategies', - enforceSafetyFloor: true, - weights: { - [ObjectiveId.EMPATHY]: 0.35, - [ObjectiveId.SAFETY]: 0.25, - [ObjectiveId.PROFESSIONALISM]: 0.2, - [ObjectiveId.CORRECTNESS]: 0.15, - [ObjectiveId.INFORMATIVENESS]: 0.05, - }, - }, - { - context: ContextType.EDUCATIONAL, - precedence: 4, - description: 'Learning about mental health concepts and treatments', - enforceSafetyFloor: false, - weights: { - [ObjectiveId.CORRECTNESS]: 0.35, - [ObjectiveId.INFORMATIVENESS]: 0.35, - [ObjectiveId.PROFESSIONALISM]: 0.15, - [ObjectiveId.EMPATHY]: 0.1, - [ObjectiveId.SAFETY]: 0.05, - }, - }, - { - context: ContextType.INFORMATIONAL, - precedence: 5, - description: 'Factual information about resources and services', - enforceSafetyFloor: false, - weights: { - [ObjectiveId.CORRECTNESS]: 0.3, - [ObjectiveId.INFORMATIVENESS]: 0.3, - [ObjectiveId.PROFESSIONALISM]: 0.2, - [ObjectiveId.EMPATHY]: 0.15, - [ObjectiveId.SAFETY]: 0.05, - }, - }, - { - context: ContextType.GENERAL, - precedence: 6, - description: 'General conversation with balanced objectives', - enforceSafetyFloor: true, - weights: { - [ObjectiveId.EMPATHY]: 0.2, - [ObjectiveId.SAFETY]: 0.2, - [ObjectiveId.CORRECTNESS]: 0.2, - [ObjectiveId.PROFESSIONALISM]: 0.2, - [ObjectiveId.INFORMATIVENESS]: 0.2, - }, - }, - ], -} - -/** - * Validate that all required ContextTypes are present in the configuration - */ -export function validateContextCoverage( - config: MappingConfiguration, -): ValidationError[] { - const errors: ValidationError[] = [] - const configuredContexts = new Set(config.mappings.map((m) => m.context)) - - // Check that all ContextType enum values are covered - const allContexts = Object.values(ContextType) - for (const contextType of allContexts) { - if (!configuredContexts.has(contextType)) { - errors.push({ - type: ValidationErrorType.MISSING_CONTEXT, - message: `Context type ${contextType} is missing from configuration`, - context: contextType, - }) - } - } - - // Check for duplicate contexts - const contextCounts = new Map() - for (const mapping of config.mappings) { - const count = contextCounts.get(mapping.context) ?? 0 - contextCounts.set(mapping.context, count + 1) - } - - for (const [context, count] of contextCounts.entries()) { - if (count > 1) { - errors.push({ - type: ValidationErrorType.DUPLICATE_CONTEXT, - message: `Context type ${context} appears ${count} times in configuration`, - context, - }) - } - } - - return errors -} - -/** - * Validate objective weights are within valid range [0, 1] - */ -export function validateWeights( - weights: ObjectiveWeightConfig, - context: ContextType, -): ValidationError[] { - const errors: ValidationError[] = [] - const requiredObjectives = [ - ObjectiveId.CORRECTNESS, - ObjectiveId.INFORMATIVENESS, - ObjectiveId.PROFESSIONALISM, - ObjectiveId.EMPATHY, - ObjectiveId.SAFETY, - ] - - for (const objectiveId of requiredObjectives) { - const weight = weights[objectiveId] - - if (weight === undefined) { - errors.push({ - type: ValidationErrorType.MISSING_OBJECTIVE, - message: `Required objective ${objectiveId} is missing for context ${context}`, - context, - objectiveId, - }) - continue - } - - if (typeof weight !== 'number' || weight < 0 || weight > 1) { - errors.push({ - type: ValidationErrorType.INVALID_WEIGHT, - message: `Weight for ${objectiveId} must be between 0 and 1, got ${weight}`, - context, - objectiveId, - }) - } - } - - return errors -} - -/** - * Validate safety floor requirements - */ -export function validateSafetyFloor( - config: MappingConfiguration, -): ValidationError[] { - const errors: ValidationError[] = [] - - if (!config.safetyFloor.enabled) { - return errors - } - - const minSafety = config.safetyFloor.minimumSafetyWeight - - for (const mapping of config.mappings) { - if ( - mapping.enforceSafetyFloor && - config.safetyFloor.contexts.includes(mapping.context) - ) { - const safetyWeight = mapping.weights[ObjectiveId.SAFETY] - - if (safetyWeight < minSafety) { - errors.push({ - type: ValidationErrorType.SAFETY_FLOOR_VIOLATION, - message: `Safety weight ${safetyWeight} for context ${mapping.context} is below minimum ${minSafety}`, - context: mapping.context, - objectiveId: ObjectiveId.SAFETY, - }) - } - } - } - - return errors -} - -/** - * Validate complete configuration - */ -export function validateMappingConfiguration(config: MappingConfiguration): { - valid: boolean - errors: ValidationError[] -} { - const errors: ValidationError[] = [] - - // Validate context coverage - errors.push(...validateContextCoverage(config)) - - // Validate weights for each mapping - for (const mapping of config.mappings) { - errors.push(...validateWeights(mapping.weights, mapping.context)) - } - - // Validate safety floor (only in strict mode, otherwise handled at runtime) - if (config.validationStrict) { - errors.push(...validateSafetyFloor(config)) - } - - // Validate normalization if enabled - if (config.normalization === 'enabled') { - for (const mapping of config.mappings) { - const sum = Object.values(mapping.weights).reduce( - (acc, w) => acc + (w ?? 0), - 0, - ) - // Allow small floating point errors - if (Math.abs(sum - 1.0) > 0.001) { - errors.push({ - type: ValidationErrorType.NORMALIZATION_ERROR, - message: `Weights for context ${mapping.context} sum to ${sum}, expected 1.0`, - context: mapping.context, - }) - } - } - } - - return { - valid: errors.length === 0, - errors, - } -} diff --git a/apps/web/src/lib/metaaligner/context/context-injector.ts b/apps/web/src/lib/metaaligner/context/context-injector.ts deleted file mode 100644 index 2f9a629b6a..0000000000 --- a/apps/web/src/lib/metaaligner/context/context-injector.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @module context-injector - * @description This module provides a context injection system for the MetaAligner pipeline. - */ - -import type { UnifiedProcessingRequest } from '../api/unified-api' -import { ContextType } from '../core/objectives' - -/** - * Defines the interface for the ContextInjector. - */ -export interface IContextInjector { - /** - * Injects context into a request. - * - * @param request - The processing request. - * @returns A promise that resolves to the request with injected context. - */ - inject(request: UnifiedProcessingRequest): Promise -} - -/** - * The ContextInjector class. - */ -export class ContextInjector implements IContextInjector { - private readonly cache = new Map() - - public async inject( - request: UnifiedProcessingRequest, - ): Promise { - const query = request.context.userQuery - let detectedContext = this.cache.get(query) - - if (!detectedContext) { - detectedContext = this.detectContext(query) - this.cache.set(query, detectedContext) - } - - if (!this.validateContext(detectedContext)) { - throw new Error('Invalid context') - } - const transformedContext = this.transformContext(request.context) - return { - ...request, - context: { - ...transformedContext, - detectedContext, - } as Record, - } - } - - private validateContext(_context: ContextType): boolean { - // Placeholder for context validation logic. - // This could involve checking for valid context types or other constraints. - return true - } - - private transformContext( - context: UnifiedProcessingRequest['context'], - ): UnifiedProcessingRequest['context'] { - // Placeholder for context transformation logic. - // This could involve adding, removing, or modifying context properties. - return context - } - - private detectContext(query: string): ContextType { - // Placeholder for context detection logic. - // This could involve using a pre-trained model or a set of rules. - if (query.includes('crisis')) { - return ContextType.CRISIS - } else if (query.includes('education')) { - return ContextType.EDUCATIONAL - } else if (query.includes('support')) { - return ContextType.SUPPORT - } else { - return ContextType.GENERAL - } - } -} diff --git a/apps/web/src/lib/metaaligner/core/dynamic-weighting-benchmark.ts b/apps/web/src/lib/metaaligner/core/dynamic-weighting-benchmark.ts deleted file mode 100644 index 4884305697..0000000000 --- a/apps/web/src/lib/metaaligner/core/dynamic-weighting-benchmark.ts +++ /dev/null @@ -1,446 +0,0 @@ -/** - * Performance Benchmark for Dynamic Weighting System - * Tests sub-250ms performance and generates visualization data - */ - -import { DynamicWeightingEngine } from './dynamic-weighting' -import { ContextType, type AlignmentContext } from './objectives' - -export interface BenchmarkResult { - testName: string - iterations: number - avgTimeMs: number - minTimeMs: number - maxTimeMs: number - p50TimeMs: number - p95TimeMs: number - p99TimeMs: number - passedThreshold: boolean - thresholdMs: number -} - -export interface BenchmarkSuite { - suiteName: string - timestamp: Date - results: BenchmarkResult[] - visualizationData: VisualizationData -} - -export interface VisualizationData { - timeSeries: TimeSeriesData[] - weightTransitions: WeightTransitionData[] - performanceDistribution: PerformanceDistributionData -} - -export interface TimeSeriesData { - iteration: number - context: ContextType - updateTimeMs: number - weights: Record - blendingApplied: boolean - oscillationDetected: boolean -} - -export interface WeightTransitionData { - fromContext: ContextType - toContext: ContextType - weightChanges: Record - smoothingEffect: number -} - -export interface PerformanceDistributionData { - buckets: { min: number; max: number; count: number }[] - mean: number - stdDev: number -} - -/** - * Run comprehensive performance benchmarks - */ -export async function runBenchmarkSuite(): Promise { - const suite: BenchmarkSuite = { - suiteName: 'Dynamic Weighting Performance Benchmark', - timestamp: new Date(), - results: [], - visualizationData: { - timeSeries: [], - weightTransitions: [], - performanceDistribution: { - buckets: [], - mean: 0, - stdDev: 0, - }, - }, - } - - // Benchmark 1: Single context calculations - suite.results.push(await benchmarkSingleContext()) - - // Benchmark 2: Context transitions - suite.results.push(await benchmarkContextTransitions()) - - // Benchmark 3: Rapid oscillations - suite.results.push(await benchmarkRapidOscillations()) - - // Benchmark 4: Crisis override performance - suite.results.push(await benchmarkCrisisOverride()) - - // Benchmark 5: Cache effectiveness - suite.results.push(await benchmarkCacheEffectiveness()) - - // Benchmark 6: All contexts (comprehensive) - const comprehensiveResult = await benchmarkAllContexts() - suite.results.push(comprehensiveResult) - - // Generate visualization data - suite.visualizationData = await generateVisualizationData() - - // Print summary - printBenchmarkSummary(suite) - - return suite -} - -async function benchmarkSingleContext(): Promise { - const engine = new DynamicWeightingEngine() - const iterations = 1000 - const times: number[] = [] - - const context: AlignmentContext = { - userQuery: 'What is therapy?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - for (let i = 0; i < iterations; i++) { - const result = engine.calculateDynamicWeights(context) - times.push(result.updateTimeMs) - } - - return calculateBenchmarkStats('Single Context (Educational)', times, 250) -} - -async function benchmarkContextTransitions(): Promise { - const engine = new DynamicWeightingEngine() - const iterations = 500 - const times: number[] = [] - - const contexts = [ - ContextType.GENERAL, - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.CLINICAL_ASSESSMENT, - ContextType.INFORMATIONAL, - ] - - for (let i = 0; i < iterations; i++) { - const contextType = contexts[i % contexts.length] - const context: AlignmentContext = { - userQuery: `Query ${i}`, - detectedContext: contextType, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - times.push(result.updateTimeMs) - } - - return calculateBenchmarkStats('Context Transitions', times, 250) -} - -async function benchmarkRapidOscillations(): Promise { - const engine = new DynamicWeightingEngine() - const iterations = 200 - const times: number[] = [] - - for (let i = 0; i < iterations; i++) { - const contextType: ContextType = - i % 2 === 0 ? ContextType.EDUCATIONAL : ContextType.SUPPORT - const context: AlignmentContext = { - userQuery: `Oscillation test ${i}`, - detectedContext: contextType, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - times.push(result.updateTimeMs) - } - - return calculateBenchmarkStats('Rapid Oscillations', times, 250) -} - -async function benchmarkCrisisOverride(): Promise { - const engine = new DynamicWeightingEngine() - const iterations = 500 - const times: number[] = [] - - // Build some history first - for (let i = 0; i < 10; i++) { - const context: AlignmentContext = { - userQuery: 'General query', - detectedContext: ContextType.GENERAL, - confidence: 0.85, - } - engine.calculateDynamicWeights(context) - } - - // Now test crisis override performance - for (let i = 0; i < iterations; i++) { - const context: AlignmentContext = { - userQuery: 'I want to end my life', - detectedContext: ContextType.CRISIS, - confidence: 0.95, - } - - const result = engine.calculateDynamicWeights(context) - times.push(result.updateTimeMs) - } - - return calculateBenchmarkStats('Crisis Override', times, 250) -} - -async function benchmarkCacheEffectiveness(): Promise { - const engine = new DynamicWeightingEngine() - const iterations = 1000 - const times: number[] = [] - - const context: AlignmentContext = { - userQuery: 'What is therapy?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - // First call (no cache) - engine.calculateDynamicWeights(context) - - // Subsequent calls (cached) - for (let i = 0; i < iterations; i++) { - const result = engine.calculateDynamicWeights(context) - times.push(result.updateTimeMs) - } - - return calculateBenchmarkStats('Cache Effectiveness', times, 250) -} - -async function benchmarkAllContexts(): Promise { - const engine = new DynamicWeightingEngine() - const times: number[] = [] - - const allContexts = [ - ContextType.CRISIS, - ContextType.CLINICAL_ASSESSMENT, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.INFORMATIONAL, - ContextType.GENERAL, - ] - - const iterations = 500 - - for (let i = 0; i < iterations; i++) { - const contextType = allContexts[i % allContexts.length] - const context: AlignmentContext = { - userQuery: `Query for ${contextType}`, - detectedContext: contextType, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - times.push(result.updateTimeMs) - } - - return calculateBenchmarkStats('All Contexts (Comprehensive)', times, 250) -} - -async function generateVisualizationData(): Promise { - const engine = new DynamicWeightingEngine() - const timeSeries: TimeSeriesData[] = [] - const weightTransitions: WeightTransitionData[] = [] - const performanceTimes: number[] = [] - - const contexts = [ - ContextType.GENERAL, - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.CLINICAL_ASSESSMENT, - ContextType.CRISIS, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.GENERAL, - ] - - let previousContext: ContextType | null = null - let previousWeights: Record | null = null - - for (let i = 0; i < contexts.length; i++) { - const contextType = contexts[i] - const context: AlignmentContext = { - userQuery: `Visualization query ${i}`, - detectedContext: contextType, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - - // Time series data - timeSeries.push({ - iteration: i, - context: contextType, - updateTimeMs: result.updateTimeMs, - weights: result.weights as Record, - blendingApplied: result.blendingApplied, - oscillationDetected: result.oscillationDetected, - }) - - performanceTimes.push(result.updateTimeMs) - - // Weight transition data - if (previousContext && previousWeights && previousContext !== contextType) { - const weightChanges: Record = {} - let totalChange = 0 - - for (const key in result.weights) { - const change = - (result?.weights[key] ?? 0) - (previousWeights?.[key] ?? 0) - weightChanges[key] = change - totalChange += Math.abs(change) - } - - const smoothingEffect = result.blendingApplied - ? 1.0 - totalChange / Object.keys(result.weights).length - : 0 - - weightTransitions.push({ - fromContext: previousContext, - toContext: contextType, - weightChanges, - smoothingEffect, - }) - } - - previousContext = contextType ?? null - previousWeights = { ...result.weights } - } - - // Performance distribution - const performanceDistribution = - calculatePerformanceDistribution(performanceTimes) - - return { - timeSeries, - weightTransitions, - performanceDistribution, - } -} - -function calculateBenchmarkStats( - testName: string, - times: number[], - thresholdMs: number, -): BenchmarkResult { - const sorted = [...times].sort((a, b) => a - b) - const sum = times.reduce((acc, t) => acc + t, 0) - const avg = sum / times.length - - const p50Index = Math.floor(times.length * 0.5) - const p95Index = Math.floor(times.length * 0.95) - const p99Index = Math.floor(times.length * 0.99) - const passedThreshold = sorted[p99Index] < thresholdMs - - return { - testName, - iterations: times.length, - avgTimeMs: avg, - minTimeMs: sorted[0], - maxTimeMs: sorted[sorted.length - 1], - p50TimeMs: sorted[p50Index], - p95TimeMs: sorted[p95Index], - p99TimeMs: sorted[p99Index], - passedThreshold, - thresholdMs, - } -} - -function calculatePerformanceDistribution( - times: number[], -): PerformanceDistributionData { - const sorted = [...times].sort((a, b) => a - b) - const mean = times.reduce((acc, t) => acc + t, 0) / times.length - const variance = - times.reduce((acc, t) => acc + Math.pow(t - mean, 2), 0) / times.length - const stdDev = Math.sqrt(variance) - - // Create buckets (0-50ms, 50-100ms, 100-150ms, 150-200ms, 200-250ms, 250+ms) - const bucketRanges = [ - { min: 0, max: 50 }, - { min: 50, max: 100 }, - { min: 100, max: 150 }, - { min: 150, max: 200 }, - { min: 200, max: 250 }, - { min: 250, max: Infinity }, - ] - - const buckets = bucketRanges.map((range) => ({ - ...range, - count: sorted.filter((t) => t >= range.min && t < range.max).length, - })) - - return { - buckets, - mean, - stdDev, - } -} - -function printBenchmarkSummary(suite: BenchmarkSuite): void { - for (const result of suite.results) { - const passIcon = result.passedThreshold ? '✅' : '❌' - } - - const allPassed = suite.results.every((r) => r.passedThreshold) - - const dist = suite.visualizationData.performanceDistribution - for (const bucket of dist.buckets) { - const percentage = - (bucket.count / (suite?.results[0]?.iterations ?? 1)) * 100 - const barLength = Math.floor(percentage / 2) - const bar = '█'.repeat(barLength) - } -} - -/** - * Export visualization data for graphing - */ -export function exportVisualizationDataForGraphing( - suite: BenchmarkSuite, -): string { - const data = { - timestamp: suite.timestamp.toISOString(), - summary: suite.results.map((r) => ({ - testName: r.testName, - avgTimeMs: r.avgTimeMs, - p50TimeMs: r.p50TimeMs, - p95TimeMs: r.p95TimeMs, - p99TimeMs: r.p99TimeMs, - passedThreshold: r.passedThreshold, - })), - timeSeries: suite.visualizationData.timeSeries, - weightTransitions: suite.visualizationData.weightTransitions, - performanceDistribution: suite.visualizationData.performanceDistribution, - } - - return JSON.stringify(data, null, 2) -} - -// Run benchmark if executed directly -const isMainModule = (require('module') as { main: unknown })['main'] === module -if (isMainModule) { - runBenchmarkSuite() - .then((_suite) => { - // Optionally write to file - // fs.writeFileSync('benchmark-results.json', jsonData) - }) - .catch((error) => { - process.exit(1) - }) -} diff --git a/apps/web/src/lib/metaaligner/core/dynamic-weighting.test.ts b/apps/web/src/lib/metaaligner/core/dynamic-weighting.test.ts deleted file mode 100644 index 5ad8e3d20f..0000000000 --- a/apps/web/src/lib/metaaligner/core/dynamic-weighting.test.ts +++ /dev/null @@ -1,655 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' - -import { ObjectiveId } from '../config/mapping-config' -import { - DynamicWeightingEngine, - getDynamicWeightingEngine, - resetDynamicWeightingEngine, - type DynamicWeightingConfig, - type WeightUpdateResult, -} from './dynamic-weighting' -import { ContextType, type AlignmentContext } from './objectives' - -describe('DynamicWeightingEngine', () => { - let engine: DynamicWeightingEngine - - beforeEach(() => { - engine = new DynamicWeightingEngine({ enableCaching: false }) - resetDynamicWeightingEngine() - }) - - describe('initialization', () => { - it('should initialize with default configuration', () => { - expect(engine).toBeDefined() - const config = engine.getConfiguration() - expect(config.blendingEnabled).toBe(true) - expect(config.crisisOverrideEnabled).toBe(true) - expect(config.hysteresisEnabled).toBe(true) - expect(config.stabilityGuardEnabled).toBe(true) - }) - - it('should accept custom configuration', () => { - const customConfig: Partial = { - blendingAlpha: 0.5, - hysteresisThreshold: 0.1, - } - const customEngine = new DynamicWeightingEngine(customConfig) - const config = customEngine.getConfiguration() - - expect(config.blendingAlpha).toBe(0.5) - expect(config.hysteresisThreshold).toBe(0.1) - }) - }) - - describe('crisis override', () => { - it('should apply crisis override immediately with high confidence', () => { - const crisisContext: AlignmentContext = { - userQuery: 'I want to end my life', - detectedContext: ContextType.CRISIS, - confidence: 0.95, - } - - // Ensure config is correct - engine.updateConfiguration({ - crisisOverrideEnabled: true, - crisisOverrideThreshold: 0.8, - }) - - const result = engine.calculateDynamicWeights(crisisContext) - - expect(result.crisisOverrideApplied).toBe(true) - expect(result.blendingApplied).toBe(false) - expect(result.weights[ObjectiveId.SAFETY]).toBeGreaterThan(0.5) - expect(result.updateTimeMs).toBeLessThan(250) - }) - - it('should not apply crisis override with low confidence', () => { - const crisisContext: AlignmentContext = { - userQuery: 'I feel bad', - detectedContext: ContextType.CRISIS, - confidence: 0.5, // Below threshold (0.8) - } - - const result = engine.calculateDynamicWeights(crisisContext) - - expect(result.crisisOverrideApplied).toBe(false) - }) - - it('should skip smoothing when crisis override is applied', () => { - // First, establish some history with a different context - const generalContext: AlignmentContext = { - userQuery: 'What is therapy?', - detectedContext: ContextType.GENERAL, - confidence: 0.8, - } - engine.calculateDynamicWeights(generalContext) - - // Now apply crisis - const crisisContext: AlignmentContext = { - userQuery: 'I want to die', - detectedContext: ContextType.CRISIS, - confidence: 0.95, - } - - const result = engine.calculateDynamicWeights(crisisContext) - - expect(result.crisisOverrideApplied).toBe(true) - expect(result.blendingApplied).toBe(false) - expect(result.reasoning.some((r) => r.includes('Crisis override'))).toBe( - true, - ) - }) - }) - - describe('weighted blending (smoothing)', () => { - it('should blend new weights with previous weights', () => { - const context1: AlignmentContext = { - userQuery: 'What is depression?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const context2: AlignmentContext = { - userQuery: 'Tell me about anxiety', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const result1 = engine.calculateDynamicWeights(context1) - const result2 = engine.calculateDynamicWeights(context2) - - // First call should not blend (no history) - expect(result1.blendingApplied).toBe(false) - - // Second call should blend - expect(result2.blendingApplied).toBe(true) - expect( - result2.reasoning.some((r) => r.includes('Blending applied')), - ).toBe(true) - }) - - it('should smooth transitions between contexts', () => { - const educationalContext: AlignmentContext = { - userQuery: 'What is therapy?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const supportContext: AlignmentContext = { - userQuery: 'I need help coping', - detectedContext: ContextType.SUPPORT, - confidence: 0.85, - } - - const result1 = engine.calculateDynamicWeights(educationalContext) - const result2 = engine.calculateDynamicWeights(supportContext) - - // Weights should transition smoothly, not jump - const empathyChange = Math.abs( - result2.weights[ObjectiveId.EMPATHY] - - result1.weights[ObjectiveId.EMPATHY], - ) - - // With blending, change should be less than the full difference - expect(empathyChange).toBeLessThan(0.3) // Reasonable smooth transition - }) - - it('should respect blendingAlpha parameter', () => { - const customEngine = new DynamicWeightingEngine({ - blendingAlpha: 0.8, // High smoothing - }) - - const context1: AlignmentContext = { - userQuery: 'What is therapy?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const context2: AlignmentContext = { - userQuery: 'I need support', - detectedContext: ContextType.SUPPORT, - confidence: 0.85, - } - - customEngine.calculateDynamicWeights(context1) - const result = customEngine.calculateDynamicWeights(context2) - - expect(result.blendingApplied).toBe(true) - }) - }) - - describe('hysteresis', () => { - it('should not update weights if change is below threshold', () => { - const context1: AlignmentContext = { - userQuery: 'What is depression?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const context2: AlignmentContext = { - userQuery: 'What is anxiety?', - detectedContext: ContextType.EDUCATIONAL, // Same context - confidence: 0.85, // Same confidence - } - - const result1 = engine.calculateDynamicWeights(context1) - const result2 = engine.calculateDynamicWeights(context2) - - // Second result should apply hysteresis (weights unchanged) - expect(result2.hysteresisApplied).toBe(true) - expect(result2.weights).toEqual(result1.weights) - }) - - it('should update weights if change exceeds threshold', () => { - const context1: AlignmentContext = { - userQuery: 'What is therapy?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const context2: AlignmentContext = { - userQuery: 'I need emotional support', - detectedContext: ContextType.SUPPORT, // Different context - confidence: 0.85, - } - - engine.calculateDynamicWeights(context1) - const result2 = engine.calculateDynamicWeights(context2) - - // Change should be significant, hysteresis not applied - expect(result2.hysteresisApplied).toBe(false) - }) - }) - - describe('stability guard', () => { - it('should limit maximum weight change per turn', () => { - const customEngine = new DynamicWeightingEngine({ - maxWeightChangePerTurn: 0.1, // 10% max change - blendingEnabled: false, // Disable blending to test guard directly - }) - - const context1: AlignmentContext = { - userQuery: 'What is therapy?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const context2: AlignmentContext = { - userQuery: 'I need support now', - detectedContext: ContextType.SUPPORT, - confidence: 0.85, - } - - customEngine.calculateDynamicWeights(context1) - const result = customEngine.calculateDynamicWeights(context2) - - // Check that no weight changed by more than 10% - const history = customEngine.getWeightHistory() - const prevWeights = history[history.length - 2].weights - const newWeights = result.weights - - for (const key in newWeights) { - const change = Math.abs(newWeights[key]! - prevWeights[key]!) - expect(change).toBeLessThanOrEqual(0.12) // 10% + small tolerance - } - }) - - it('should apply stability guard when large changes detected', () => { - const customEngine = new DynamicWeightingEngine({ - maxWeightChangePerTurn: 0.15, - blendingEnabled: false, - }) - - const context1: AlignmentContext = { - userQuery: 'General question', - detectedContext: ContextType.GENERAL, - confidence: 0.85, - } - - const context2: AlignmentContext = { - userQuery: 'Clinical assessment needed', - detectedContext: ContextType.CLINICAL_ASSESSMENT, - confidence: 0.85, - } - - customEngine.calculateDynamicWeights(context1) - const result = customEngine.calculateDynamicWeights(context2) - - expect(result.stabilityGuardApplied).toBe(true) - }) - }) - - describe('oscillation detection', () => { - it('should detect oscillation with repeated direction changes', () => { - const contexts = [ - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ] - - let oscillationDetected = false - - for (const contextType of contexts) { - const context: AlignmentContext = { - userQuery: `Query for ${contextType}`, - detectedContext: contextType, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - if (result.oscillationDetected) { - oscillationDetected = true - } - } - - expect(oscillationDetected).toBe(true) - }) - - it('should increase smoothing when oscillation detected', () => { - const contexts = [ - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ] - - for (const contextType of contexts) { - const context: AlignmentContext = { - userQuery: `Query for ${contextType}`, - detectedContext: contextType, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - - if (result.oscillationDetected) { - expect( - result.reasoning.some((r) => r.includes('increased smoothing')), - ).toBe(true) - } - } - }) - - it('should not detect oscillation with stable contexts', () => { - for (let i = 0; i < 10; i++) { - const context: AlignmentContext = { - userQuery: `Educational query ${i}`, - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - expect(result.oscillationDetected).toBe(false) - } - }) - }) - - describe('performance', () => { - it('should complete weight updates within 250ms', () => { - const contexts = [ - ContextType.CRISIS, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.CLINICAL_ASSESSMENT, - ContextType.INFORMATIONAL, - ContextType.GENERAL, - ] - - for (const contextType of contexts) { - const context: AlignmentContext = { - userQuery: `Test query for ${contextType}`, - detectedContext: contextType, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - expect(result.updateTimeMs).toBeLessThan(250) - } - }) - - it('should benefit from caching for repeated contexts', () => { - engine.updateConfiguration({ enableCaching: true }) - const context: AlignmentContext = { - userQuery: 'What is therapy?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const result1 = engine.calculateDynamicWeights(context) - const result2 = engine.calculateDynamicWeights(context) - - // Second call should be faster (cached) - expect(result2.updateTimeMs).toBeLessThanOrEqual( - Math.max(result1.updateTimeMs * 3, 5), - ) - expect(result2.reasoning.some((r) => r.includes('Cached'))).toBe(true) - }) - - it('should invalidate cache when context changes', () => { - engine.updateConfiguration({ enableCaching: true }) - const context1: AlignmentContext = { - userQuery: 'What is therapy?', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const context2: AlignmentContext = { - userQuery: 'I need support', - detectedContext: ContextType.SUPPORT, - confidence: 0.85, - } - - engine.calculateDynamicWeights(context1) - const result = engine.calculateDynamicWeights(context2) - - expect(result.reasoning.some((r) => r.includes('Cached'))).toBe(false) - }) - }) - - describe('normalization', () => { - it('should normalize weights to sum to 1.0', () => { - const context: AlignmentContext = { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - - const sum = Object.values(result.weights).reduce((acc, w) => acc + w, 0) - expect(Math.abs(sum - 1.0)).toBeLessThan(0.001) - }) - - it('should keep weights in [0, 1] range', () => { - const contexts = [ - ContextType.CRISIS, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.CLINICAL_ASSESSMENT, - ] - - for (const contextType of contexts) { - const context: AlignmentContext = { - userQuery: `Test for ${contextType}`, - detectedContext: contextType, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - - for (const [_, weight] of Object.entries(result.weights)) { - expect(weight).toBeGreaterThanOrEqual(0) - expect(weight).toBeLessThanOrEqual(1) - } - } - }) - }) - - describe('stability across transitions', () => { - it('should maintain stability during context transitions', () => { - const transitions = [ - ContextType.GENERAL, - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.GENERAL, - ] - - const results: WeightUpdateResult[] = [] - - for (const contextType of transitions) { - const context: AlignmentContext = { - userQuery: `Transitioning to ${contextType}`, - detectedContext: contextType, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - results.push(result) - } - - // Check that transitions are smooth (no wild swings) - for (let i = 1; i < results.length; i++) { - const prevWeights = results[i - 1].weights - const currWeights = results[i].weights - - for (const key in currWeights) { - const change = Math.abs(currWeights[key]! - prevWeights[key]!) - // Change should be reasonable (< 40% due to blending and guards) - expect(change).toBeLessThan(0.4) - } - } - }) - - it('should prevent rapid oscillation between contexts', () => { - const oscillatingContexts = [ - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ContextType.SUPPORT, - ContextType.EDUCATIONAL, - ] - - let maxChange = 0 - - for (let i = 0; i < oscillatingContexts.length; i++) { - const context: AlignmentContext = { - userQuery: `Oscillation test ${i}`, - detectedContext: oscillatingContexts[i], - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - - if (i > 0) { - const history = engine.getWeightHistory() - const prevWeights = history[history.length - 2].weights - const currWeights = result.weights - - for (const key in currWeights) { - const change = Math.abs(currWeights[key]! - prevWeights[key]!) - maxChange = Math.max(maxChange, change) - } - } - } - - // Due to oscillation detection and increased smoothing, changes should be dampened - expect(maxChange).toBeLessThan(0.3) - }) - }) - - describe('edge cases: ties, low confidence, conflicts', () => { - it('should handle low confidence gracefully', () => { - const lowConfidenceContext: AlignmentContext = { - userQuery: 'Ambiguous query', - detectedContext: ContextType.GENERAL, - confidence: 0.3, // Low confidence - } - - const result = engine.calculateDynamicWeights(lowConfidenceContext) - - expect(result.weights).toBeDefined() - expect(result.updateTimeMs).toBeLessThan(250) - }) - - it('should handle context ties by using precedence', () => { - // This is tested at the mapper level, but verify integration - const context: AlignmentContext = { - userQuery: 'Query with potential conflict', - detectedContext: ContextType.GENERAL, // Lowest precedence - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - - expect(result.context).toBe(ContextType.GENERAL) - expect(result.weights).toBeDefined() - }) - - it('should handle first call with no history', () => { - const freshEngine = new DynamicWeightingEngine() - - const context: AlignmentContext = { - userQuery: 'First query', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const result = freshEngine.calculateDynamicWeights(context) - - expect(result.blendingApplied).toBe(false) - expect(result.hysteresisApplied).toBe(false) - expect(result.weights).toBeDefined() - }) - - it('should handle all objectives having zero weight', () => { - // This shouldn't happen in practice, but test defensive code - const context: AlignmentContext = { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - confidence: 0.85, - } - - const result = engine.calculateDynamicWeights(context) - - // Normalization should handle this by creating equal weights - const sum = Object.values(result.weights).reduce((acc, w) => acc + w, 0) - expect(sum).toBeGreaterThan(0) - }) - }) - - describe('configuration updates', () => { - it('should allow configuration updates', () => { - engine.updateConfiguration({ - blendingAlpha: 0.5, - hysteresisThreshold: 0.1, - }) - - const config = engine.getConfiguration() - expect(config.blendingAlpha).toBe(0.5) - expect(config.hysteresisThreshold).toBe(0.1) - }) - - it('should apply updated configuration to subsequent calculations', () => { - engine.updateConfiguration({ - blendingEnabled: false, - }) - - const context1: AlignmentContext = { - userQuery: 'First query', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - const context2: AlignmentContext = { - userQuery: 'Second query', - detectedContext: ContextType.SUPPORT, - confidence: 0.85, - } - - engine.calculateDynamicWeights(context1) - const result = engine.calculateDynamicWeights(context2) - - expect(result.blendingApplied).toBe(false) - }) - }) - - describe('singleton pattern', () => { - it('should return singleton instance', () => { - const instance1 = getDynamicWeightingEngine() - const instance2 = getDynamicWeightingEngine() - - expect(instance1).toBe(instance2) - }) - - it('should reset singleton instance', () => { - const instance1 = getDynamicWeightingEngine() - resetDynamicWeightingEngine() - const instance2 = getDynamicWeightingEngine() - - expect(instance1).not.toBe(instance2) - }) - }) - - describe('reset functionality', () => { - it('should clear history and cache on reset', () => { - const context: AlignmentContext = { - userQuery: 'Test query', - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.85, - } - - engine.calculateDynamicWeights(context) - expect(engine.getWeightHistory().length).toBeGreaterThan(0) - - engine.reset() - expect(engine.getWeightHistory().length).toBe(0) - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/core/dynamic-weighting.ts b/apps/web/src/lib/metaaligner/core/dynamic-weighting.ts deleted file mode 100644 index 0d6d10a52a..0000000000 --- a/apps/web/src/lib/metaaligner/core/dynamic-weighting.ts +++ /dev/null @@ -1,638 +0,0 @@ -/** - * Dynamic Weighting System with Smoothing and Hysteresis - * Implements weighted blending, crisis overrides, and stability guards - * - * PIX-22: Dynamic weighting based on context - */ - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { getContextMapperService } from '../config/context-mapper-service' -import { ContextualObjectiveWeights } from '../prioritization/context-objective-mapper' -import { ContextType, AlignmentContext } from './objectives' - -const logger = createBuildSafeLogger('dynamic-weighting') - -/** - * Configuration for dynamic weighting behavior - */ -export interface DynamicWeightingConfig { - // Blending parameters - blendingEnabled: boolean - blendingAlpha: number // 0-1, how much new weights blend with previous (0 = no smoothing, 1 = full smoothing) - - // Crisis override - crisisOverrideEnabled: boolean - crisisOverrideThreshold: number // Confidence threshold for crisis override (0-1) - - // Hysteresis parameters - hysteresisEnabled: boolean - hysteresisThreshold: number // Minimum weight change to trigger update (0-1) - hysteresisWindow: number // Number of turns to consider for stability - - // Stability guards - stabilityGuardEnabled: boolean - maxWeightChangePerTurn: number // Maximum weight change per turn (0-1) - oscillationDetectionWindow: number // Number of turns to check for oscillation - oscillationThreshold: number // Number of direction changes to consider oscillation - - // Performance - enableCaching: boolean - cacheTTLMs: number // Cache time-to-live in milliseconds - - // Normalization - normalizeWeights: boolean -} - -/** - * Weight update result with telemetry - */ -export interface WeightUpdateResult { - weights: ContextualObjectiveWeights - context: ContextType - updateTimeMs: number - blendingApplied: boolean - crisisOverrideApplied: boolean - hysteresisApplied: boolean - stabilityGuardApplied: boolean - oscillationDetected: boolean - reasoning: string[] -} - -/** - * Weight history entry for smoothing and oscillation detection - */ -interface WeightHistoryEntry { - timestamp: number - context: ContextType - weights: ContextualObjectiveWeights - confidence: number -} - -/** - * Oscillation tracking data - */ -interface OscillationTracker { - objectiveId: string - directionChanges: number - lastDirection: 'up' | 'down' | 'stable' -} - -/** - * Default configuration for dynamic weighting - */ -export const DEFAULT_DYNAMIC_WEIGHTING_CONFIG: DynamicWeightingConfig = { - blendingEnabled: true, - blendingAlpha: 0.3, // 30% smoothing - responsive but stable - - crisisOverrideEnabled: true, - crisisOverrideThreshold: 0.8, - - hysteresisEnabled: true, - hysteresisThreshold: 0.05, // 5% minimum change - hysteresisWindow: 3, - - stabilityGuardEnabled: true, - maxWeightChangePerTurn: 0.2, // Max 20% change per turn - oscillationDetectionWindow: 5, - oscillationThreshold: 3, // 3+ direction changes = oscillation - - enableCaching: true, - cacheTTLMs: 100, // 100ms cache - - normalizeWeights: true, -} - -/** - * Dynamic Weighting Engine with smoothing and stability - */ -export class DynamicWeightingEngine { - private config: DynamicWeightingConfig - private weightHistory: WeightHistoryEntry[] = [] - private readonly oscillationTrackers: Map = - new Map() - private cache: { - weights: ContextualObjectiveWeights | null - context: ContextType | null - timestamp: number - } = { - weights: null, - context: null, - timestamp: 0, - } - - constructor(config?: Partial) { - this.config = { ...DEFAULT_DYNAMIC_WEIGHTING_CONFIG, ...config } - } - - /** - * Calculate dynamic weights for a given context with smoothing and stability - */ - calculateDynamicWeights(context: AlignmentContext): WeightUpdateResult { - const startTime = performance.now() - const contextConfidence = this.getContextConfidence(context) - const reasoning: string[] = [] - - // Check cache first - if (this.config.enableCaching && this.isCacheValid(context)) { - const cachedWeights = this.cache.weights! - const updateTime = performance.now() - startTime - - reasoning.push(`Cached weights used (${updateTime.toFixed(2)}ms)`) - - return { - weights: cachedWeights, - context: context.detectedContext, - updateTimeMs: updateTime, - blendingApplied: false, - crisisOverrideApplied: false, - hysteresisApplied: false, - stabilityGuardApplied: false, - oscillationDetected: false, - reasoning, - } - } - - // Get base weights from context mapper - const mapperService = getContextMapperService() - const mappingResult = mapperService.getWeightsForContext( - context.detectedContext, - ) - - let newWeights = { ...mappingResult.weights } - reasoning.push(...mappingResult.reasoning) - - // Crisis override - always takes precedence - let crisisOverrideApplied = false - if ( - this.config.crisisOverrideEnabled && - context.detectedContext === ContextType.CRISIS && - contextConfidence >= this.config.crisisOverrideThreshold - ) { - // Crisis override bypasses smoothing and applies immediately - reasoning.push( - `Crisis override applied (confidence: ${contextConfidence.toFixed(2)})`, - ) - crisisOverrideApplied = true - - // Update cache and history - this.updateCache(context.detectedContext, newWeights) - this.addToHistory(context, newWeights) - - const updateTime = performance.now() - startTime - - logger.info('Crisis override applied', { - updateTimeMs: updateTime, - confidence: contextConfidence, - }) - - return { - weights: newWeights, - context: context.detectedContext, - updateTimeMs: updateTime, - blendingApplied: false, - crisisOverrideApplied: true, - hysteresisApplied: false, - stabilityGuardApplied: false, - oscillationDetected: false, - reasoning, - } - } - - // Get previous weights for smoothing - const previousWeights = this.getPreviousWeights() - - let blendingApplied = false - let hysteresisApplied = false - let stabilityGuardApplied = false - let oscillationDetected = false - - // Apply blending (smoothing) if enabled and we have history - if (this.config.blendingEnabled && previousWeights) { - newWeights = this.applyBlending(newWeights, previousWeights) - blendingApplied = true - reasoning.push(`Blending applied (alpha: ${this.config.blendingAlpha})`) - } - - // Detect oscillation - if (this.config.stabilityGuardEnabled && previousWeights) { - oscillationDetected = this.detectOscillation(newWeights, previousWeights) - - if (oscillationDetected) { - // Increase smoothing to dampen oscillation - newWeights = this.applyBlending( - newWeights, - previousWeights, - Math.min(0.7, this.config.blendingAlpha * 2), - ) - reasoning.push('Oscillation detected - increased smoothing') - } - } - - // Apply stability guard - limit maximum change per turn - if (this.config.stabilityGuardEnabled && previousWeights) { - const guardedWeights = this.applyStabilityGuard( - newWeights, - previousWeights, - ) - - if (!this.weightsEqual(guardedWeights, newWeights)) { - newWeights = guardedWeights - stabilityGuardApplied = true - reasoning.push( - `Stability guard applied (max change: ${this.config.maxWeightChangePerTurn})`, - ) - } - } - - // Apply hysteresis - only update if change is significant - if (this.config.hysteresisEnabled && previousWeights) { - const changeSignificant = this.isChangeSignificant( - newWeights, - previousWeights, - ) - - if (!changeSignificant) { - newWeights = previousWeights - hysteresisApplied = true - reasoning.push( - `Hysteresis applied - change below threshold (${this.config.hysteresisThreshold})`, - ) - } - } - - // Normalize if configured - if (this.config.normalizeWeights) { - newWeights = this.normalizeWeights(newWeights) - } - - // Update cache and history - this.updateCache(context.detectedContext, newWeights) - this.addToHistory(context, newWeights) - - const updateTime = performance.now() - startTime - - // Log if update exceeds 250ms threshold - if (updateTime > 250) { - logger.warn('Weight update exceeded 250ms threshold', { - updateTimeMs: updateTime, - context: context.detectedContext, - }) - } - - logger.info('Dynamic weights calculated', { - updateTimeMs: updateTime, - context: context.detectedContext, - blendingApplied, - hysteresisApplied, - stabilityGuardApplied, - oscillationDetected, - }) - - return { - weights: newWeights, - context: context.detectedContext, - updateTimeMs: updateTime, - blendingApplied, - crisisOverrideApplied, - hysteresisApplied, - stabilityGuardApplied, - oscillationDetected, - reasoning, - } - } - - /** - * Apply exponential moving average blending between new and previous weights - */ - private applyBlending( - newWeights: ContextualObjectiveWeights, - previousWeights: ContextualObjectiveWeights, - alpha?: number, - ): ContextualObjectiveWeights { - const blendAlpha = alpha ?? this.config.blendingAlpha - const blended: Partial = {} - - for (const [k, v] of Object.entries(newWeights) as [ - keyof ContextualObjectiveWeights, - number | undefined, - ][]) { - const newValue = v ?? 0 - const key = k - const prevValue = previousWeights[key] ?? newValue - // EMA: blended = alpha * previous + (1 - alpha) * new - blended[key] = blendAlpha * prevValue + (1 - blendAlpha) * newValue - } - - return blended as ContextualObjectiveWeights - } - - /** - * Apply stability guard to limit maximum weight change per turn - */ - private applyStabilityGuard( - newWeights: ContextualObjectiveWeights, - previousWeights: ContextualObjectiveWeights, - ): ContextualObjectiveWeights { - const guarded: Partial = {} - const maxChange = this.config.maxWeightChangePerTurn - - for (const [k, v] of Object.entries(newWeights) as [ - keyof ContextualObjectiveWeights, - number | undefined, - ][]) { - const newValue = v ?? 0 - const key = k - const prevValue = previousWeights[key] ?? newValue - const change = newValue - prevValue - const absChange = Math.abs(change) - - if (absChange > maxChange) { - // Limit change to maxChange - const direction = change > 0 ? 1 : -1 - guarded[key] = prevValue + direction * maxChange - } else { - guarded[key] = newValue - } - } - - return guarded as ContextualObjectiveWeights - } - - /** - * Check if weight change is significant enough to update (hysteresis) - */ - private isChangeSignificant( - newWeights: ContextualObjectiveWeights, - previousWeights: ContextualObjectiveWeights, - ): boolean { - const threshold = this.config.hysteresisThreshold - - for (const [k, v] of Object.entries(newWeights) as [ - keyof ContextualObjectiveWeights, - number | undefined, - ][]) { - const newValue = v ?? 0 - const key = k - const prevValue = previousWeights[key] ?? newValue - const change = Math.abs(newValue - prevValue) - - if (change > threshold) { - return true - } - } - - return false - } - - /** - * Detect oscillation in weight updates - */ - private detectOscillation( - newWeights: ContextualObjectiveWeights, - previousWeights: ContextualObjectiveWeights, - ): boolean { - const window = this.config.oscillationDetectionWindow - const threshold = this.config.oscillationThreshold - - // Only check if we have enough history - // We remove the early return here because we want to update trackers cumulatively - // even before the window is full. - - // Track direction changes for each objective - for (const [k, v] of Object.entries(newWeights) as [ - keyof ContextualObjectiveWeights, - number | undefined, - ][]) { - const newValue = v ?? 0 - const objectiveId = k as string - const prevValue = - previousWeights[objectiveId as keyof ContextualObjectiveWeights] ?? - newValue - const change = newValue - prevValue - - let direction: 'up' | 'down' | 'stable' = 'stable' - if (change > 0.01) direction = 'up' - else if (change < -0.01) direction = 'down' - - // Get or create tracker - const existingTracker = this.oscillationTrackers.get(objectiveId) - if (existingTracker) { - // Check for direction change - if ( - direction !== 'stable' && - direction !== existingTracker.lastDirection - ) { - existingTracker.directionChanges++ - existingTracker.lastDirection = direction - } - - // Check if oscillation threshold exceeded - if (existingTracker.directionChanges >= threshold) { - return true - } - } else { - this.oscillationTrackers.set(objectiveId, { - objectiveId, - directionChanges: 0, - lastDirection: direction, - }) - } - } - - // Reset trackers periodically - if (this.weightHistory.length % window === 0) { - this.oscillationTrackers.clear() - } - - return false - } - - /** - * Normalize weights to sum to 1.0 - */ - private normalizeWeights( - weights: ContextualObjectiveWeights, - ): ContextualObjectiveWeights { - const sum = (Object.values(weights) as (number | undefined)[]).reduce( - (acc: number, w) => acc + (w ?? 0), - 0, - ) - - if (sum === 0 || sum === 1.0) { - return weights - } - - const normalized: Partial = {} - for (const [k, v] of Object.entries(weights) as [ - keyof ContextualObjectiveWeights, - number | undefined, - ][]) { - normalized[k] = (v ?? 0) / sum - } - - return normalized as ContextualObjectiveWeights - } - - /** - * Get previous weights from history - */ - private getPreviousWeights(): ContextualObjectiveWeights | null { - if (this.weightHistory.length === 0) { - return null - } - - return this.weightHistory[this.weightHistory.length - 1].weights - } - - /** - * Add entry to weight history - */ - private addToHistory( - context: AlignmentContext, - weights: ContextualObjectiveWeights, - ): void { - this.weightHistory.push({ - timestamp: Date.now(), - context: context.detectedContext, - weights: { ...weights }, - confidence: this.getContextConfidence(context), - }) - - // Keep only recent history - const maxHistory = - Math.max( - this.config.hysteresisWindow, - this.config.oscillationDetectionWindow, - ) * 2 - - if (this.weightHistory.length > maxHistory) { - this.weightHistory = this.weightHistory.slice(-maxHistory) - } - } - - /** - * Update weight cache - */ - private updateCache( - context: ContextType, - weights: ContextualObjectiveWeights, - ): void { - this.cache = { - weights: { ...weights }, - context, - timestamp: Date.now(), - } - } - - /** - * Check if cache is valid - */ - private isCacheValid(context: AlignmentContext): boolean { - if (!this.cache.weights || !this.cache.context) { - return false - } - - // Cache invalid if context changed - if (this.cache.context !== context.detectedContext) { - return false - } - - // Cache invalid if TTL expired - const age = Date.now() - this.cache.timestamp - if (age > this.config.cacheTTLMs) { - return false - } - - return true - } - - /** - * Resolve context confidence from explicit or metadata-backed fields. - */ - private getContextConfidence(context: AlignmentContext): number { - if (typeof context.confidence === 'number') { - return context.confidence - } - - return context.sessionMetadata?.confidence ?? 0 - } - - /** - * Check if two weight objects are equal (within tolerance) - */ - private weightsEqual( - weights1: ContextualObjectiveWeights, - weights2: ContextualObjectiveWeights, - tolerance: number = 0.0001, - ): boolean { - const keys1 = Object.keys(weights1) - const keys2 = Object.keys(weights2) - - if (keys1.length !== keys2.length) { - return false - } - - for (const key of keys1) { - const diff = Math.abs(weights1?.[key] - weights2?.[key]) - if (diff > tolerance) { - return false - } - } - - return true - } - - /** - * Get weight history (for debugging/analysis) - */ - getWeightHistory(): WeightHistoryEntry[] { - return [...this.weightHistory] - } - - /** - * Clear history and cache (useful for testing) - */ - reset(): void { - this.weightHistory = [] - this.oscillationTrackers.clear() - this.cache = { - weights: null, - context: null, - timestamp: 0, - } - } - - /** - * Get current configuration - */ - getConfiguration(): DynamicWeightingConfig { - return { ...this.config } - } - - /** - * Update configuration - */ - updateConfiguration(config: Partial): void { - this.config = { ...this.config, ...config } - } -} - -/** - * Singleton instance for application-wide use - */ -let defaultDynamicWeightingEngine: DynamicWeightingEngine | null = null - -/** - * Get or create the default dynamic weighting engine - */ -export function getDynamicWeightingEngine(): DynamicWeightingEngine { - defaultDynamicWeightingEngine ??= new DynamicWeightingEngine() - return defaultDynamicWeightingEngine -} - -/** - * Reset the default engine (useful for testing) - */ -export function resetDynamicWeightingEngine(): void { - defaultDynamicWeightingEngine = null -} diff --git a/apps/web/src/lib/metaaligner/core/objective-interfaces.test.ts b/apps/web/src/lib/metaaligner/core/objective-interfaces.test.ts deleted file mode 100644 index d904359f06..0000000000 --- a/apps/web/src/lib/metaaligner/core/objective-interfaces.test.ts +++ /dev/null @@ -1,458 +0,0 @@ -import { describe, test, beforeEach, expect } from 'vitest' - -import type { - ObjectiveBuilder, - ObjectiveRegistry, - ValidationResult, - ObjectiveTemplate, -} from './objective-interfaces' -import { - ObjectiveCategory, - StandardObjectiveBuilder, - StandardObjectiveRegistry, - ObjectiveFactory, - NormalizationMethod, - AggregationMethod, -} from './objective-interfaces' -import type { ObjectiveDefinition } from './objectives' - -// Mock evaluation function for testing -const createMockEvaluationFunction = - (returnValue = 0.8) => - () => - returnValue - -describe('Objective Interfaces', () => { - describe('StandardObjectiveBuilder', () => { - let builder: ObjectiveBuilder - - beforeEach(() => { - builder = new StandardObjectiveBuilder() - }) - - test('should build valid objective with all required fields', () => { - const mockEvaluationFunction = createMockEvaluationFunction(0.8) - - const objective = builder - .setId('test-objective') - .setName('Test Objective') - .setDescription('A test objective for validation') - .setWeight(0.3) - .addCriterion({ - criterion: 'test-criterion', - description: 'Test criterion description', - weight: 1.0, - }) - .setEvaluationFunction(mockEvaluationFunction) - .build() - - expect(objective.id).toBe('test-objective') - expect(objective.name).toBe('Test Objective') - expect(objective.description).toBe('A test objective for validation') - expect(objective.weight).toBe(0.3) - expect(objective.criteria).toHaveLength(1) - expect(objective?.criteria[0].criterion).toBe('test-criterion') - expect(objective.evaluationFunction).toBe(mockEvaluationFunction) - }) - - test('should validate successfully with valid data', () => { - const mockEvaluationFunction = createMockEvaluationFunction(0.8) - - builder - .setId('test-objective') - .setName('Test Objective') - .setDescription('A test objective') - .setWeight(0.5) - .addCriterion({ - criterion: 'criterion1', - description: 'First criterion', - weight: 0.6, - }) - .addCriterion({ - criterion: 'criterion2', - description: 'Second criterion', - weight: 0.4, - }) - .setEvaluationFunction(mockEvaluationFunction) - - const validation = builder.validate() - expect(validation.isValid).toBe(true) - expect(validation.errors).toHaveLength(0) - }) - - test('should fail validation when required fields are missing', () => { - const validation = builder.validate() - - expect(validation.isValid).toBe(false) - expect(validation.errors).toHaveLength(5) // id, name, description, weight, evaluationFunction - - const errorCodes = validation.errors.map((e) => e.code) - expect(errorCodes).toContain('MISSING_ID') - expect(errorCodes).toContain('MISSING_NAME') - expect(errorCodes).toContain('MISSING_DESCRIPTION') - expect(errorCodes).toContain('MISSING_WEIGHT') - expect(errorCodes).toContain('MISSING_EVALUATION_FUNCTION') - }) - - test('should fail validation with invalid weight range', () => { - const mockEvaluationFunction = createMockEvaluationFunction(0.8) - - builder - .setId('test-objective') - .setName('Test Objective') - .setDescription('A test objective') - .setWeight(1.5) // Invalid weight > 1 - .setEvaluationFunction(mockEvaluationFunction) - - const validation = builder.validate() - expect(validation.isValid).toBe(false) - expect( - validation.errors.some((e) => e.code === 'INVALID_WEIGHT_RANGE'), - ).toBe(true) - }) - - test('should fail validation when criteria weights do not sum to 1', () => { - const mockEvaluationFunction = createMockEvaluationFunction(0.8) - - builder - .setId('test-objective') - .setName('Test Objective') - .setDescription('A test objective') - .setWeight(0.5) - .addCriterion({ - criterion: 'criterion1', - description: 'First criterion', - weight: 0.3, - }) - .addCriterion({ - criterion: 'criterion2', - description: 'Second criterion', - weight: 0.4, - }) // Total weight = 0.7, not 1.0 - .setEvaluationFunction(mockEvaluationFunction) - - const validation = builder.validate() - expect(validation.isValid).toBe(false) - expect( - validation.errors.some((e) => e.code === 'INVALID_CRITERIA_WEIGHTS'), - ).toBe(true) - }) - - test('should show warning when no criteria are defined', () => { - const mockEvaluationFunction = createMockEvaluationFunction(0.8) - - builder - .setId('test-objective') - .setName('Test Objective') - .setDescription('A test objective') - .setWeight(0.5) - .setEvaluationFunction(mockEvaluationFunction) - - const validation = builder.validate() - expect(validation.warnings.some((w) => w.code === 'NO_CRITERIA')).toBe( - true, - ) - }) - - test('should throw error when building invalid objective', () => { - expect(() => { - builder.setId('test').build() // Missing required fields - }).toThrow('Invalid objective definition') - }) - - test('should support method chaining', () => { - const mockEvaluationFunction = createMockEvaluationFunction(0.8) - - const result = builder - .setId('test') - .setName('Test') - .setDescription('Test') - .setWeight(0.5) - .setEvaluationFunction(mockEvaluationFunction) - - expect(result).toBe(builder) // Should return the same instance for chaining - }) - }) - - describe('StandardObjectiveRegistry', () => { - let registry: ObjectiveRegistry - let mockObjective: ObjectiveDefinition - - beforeEach(() => { - registry = new StandardObjectiveRegistry() - mockObjective = { - id: 'test-objective', - name: 'Test Objective', - description: 'A test objective', - weight: 0.5, - criteria: [ - { - criterion: 'test-criterion', - description: 'Test criterion', - weight: 1.0, - }, - ], - evaluationFunction: createMockEvaluationFunction(0.8), - } - }) - - test('should register and retrieve objectives', () => { - registry.register(mockObjective) - - const retrieved = registry.get('test-objective') - expect(retrieved).toEqual(mockObjective) - expect(registry.size()).toBe(1) - }) - - test('should return undefined for non-existent objective', () => { - const retrieved = registry.get('non-existent') - expect(retrieved).toBeUndefined() - }) - - test('should unregister objectives', () => { - registry.register(mockObjective) - expect(registry.size()).toBe(1) - - const unregistered = registry.unregister('test-objective') - expect(unregistered).toBe(true) - expect(registry.size()).toBe(0) - expect(registry.get('test-objective')).toBeUndefined() - }) - - test('should return false when unregistering non-existent objective', () => { - const unregistered = registry.unregister('non-existent') - expect(unregistered).toBe(false) - }) - - test('should return all registered objectives', () => { - const mockObjective2: ObjectiveDefinition = { - ...mockObjective, - id: 'test-objective-2', - name: 'Test Objective 2', - } - - registry.register(mockObjective) - registry.register(mockObjective2) - - const allObjectives = registry.getAll() - expect(allObjectives).toHaveLength(2) - expect(allObjectives).toContainEqual(mockObjective) - expect(allObjectives).toContainEqual(mockObjective2) - }) - - test('should clear all objectives', () => { - registry.register(mockObjective) - expect(registry.size()).toBe(1) - - registry.clear() - expect(registry.size()).toBe(0) - expect(registry.getAll()).toHaveLength(0) - }) - - test('should validate registry contents', () => { - registry.register(mockObjective) - - const validation = registry.validate() - expect(validation.isValid).toBe(true) - expect(validation.errors).toHaveLength(0) - }) - - test('should throw error when registering invalid objective', () => { - const invalidObjective = { - ...mockObjective, - weight: 1.5, // Invalid weight - } - - expect(() => { - registry.register(invalidObjective) - }).toThrow('Cannot register invalid objective') - }) - - test('should get objectives by category', () => { - registry.register(mockObjective) - - const coreObjectives = registry.getByCategory(ObjectiveCategory.CORE) - expect(Array.isArray(coreObjectives)).toBe(true) - // In basic implementation, returns all objectives - expect(coreObjectives).toContain(mockObjective) - }) - }) - - describe('ObjectiveFactory', () => { - test('should create criterion with valid parameters', () => { - const criterion = ObjectiveFactory.createCriterion( - 'test-criterion', - 'Test criterion description', - 0.5, - ) - - expect(criterion.criterion).toBe('test-criterion') - expect(criterion.description).toBe('Test criterion description') - expect(criterion.weight).toBe(0.5) - }) - - test('should throw error for invalid criterion weight', () => { - expect(() => { - ObjectiveFactory.createCriterion('test', 'test', 0) // weight = 0 - }).toThrow('Criterion weight must be between 0 and 1') - - expect(() => { - ObjectiveFactory.createCriterion('test', 'test', 1.5) // weight > 1 - }).toThrow('Criterion weight must be between 0 and 1') - }) - - test('should create basic objective', () => { - const mockEvaluationFunction = createMockEvaluationFunction(0.8) - const criteria = [ - ObjectiveFactory.createCriterion('criterion1', 'First criterion', 0.6), - ObjectiveFactory.createCriterion('criterion2', 'Second criterion', 0.4), - ] - - const objective = ObjectiveFactory.createBasicObjective( - 'test-objective', - 'Test Objective', - 'A test objective', - 0.5, - criteria, - mockEvaluationFunction, - ) - - expect(objective.id).toBe('test-objective') - expect(objective.name).toBe('Test Objective') - expect(objective.description).toBe('A test objective') - expect(objective.weight).toBe(0.5) - expect(objective.criteria).toEqual(criteria) - expect(objective.evaluationFunction).toBe(mockEvaluationFunction) - }) - - test('should create objective from template', () => { - const template: ObjectiveTemplate = { - id: 'template-objective', - name: 'Template Objective', - description: 'An objective created from template', - category: ObjectiveCategory.CORE, - defaultWeight: 0.3, - criteriaTemplates: [ - { - criterion: 'template-criterion', - description: 'Template criterion', - defaultWeight: 1.0, - evaluationHints: ['hint1', 'hint2'], - }, - ], - evaluationTemplate: 'basic evaluation', - configurationSchema: {}, - } - - const objective = ObjectiveFactory.createFromTemplate(template) - - expect(objective.id).toBe('template-objective') - expect(objective.name).toBe('Template Objective') - expect(objective.description).toBe('An objective created from template') - expect(objective.weight).toBe(0.3) - expect(objective.criteria).toHaveLength(1) - expect(objective?.criteria[0].criterion).toBe('template-criterion') - expect(typeof objective.evaluationFunction).toBe('function') - }) - }) - - describe('Enums', () => { - test('ObjectiveCategory should have expected values', () => { - expect(ObjectiveCategory.CORE).toBe('core') - expect(ObjectiveCategory.CLINICAL).toBe('clinical') - expect(ObjectiveCategory.CULTURAL).toBe('cultural') - expect(ObjectiveCategory.DEMOGRAPHIC).toBe('demographic') - expect(ObjectiveCategory.SPECIALIZED).toBe('specialized') - expect(ObjectiveCategory.EXPERIMENTAL).toBe('experimental') - expect(ObjectiveCategory.CUSTOM).toBe('custom') - }) - - test('NormalizationMethod should have expected values', () => { - expect(NormalizationMethod.NONE).toBe('none') - expect(NormalizationMethod.MIN_MAX).toBe('min_max') - expect(NormalizationMethod.Z_SCORE).toBe('z_score') - expect(NormalizationMethod.SIGMOID).toBe('sigmoid') - }) - - test('AggregationMethod should have expected values', () => { - expect(AggregationMethod.WEIGHTED_AVERAGE).toBe('weighted_average') - expect(AggregationMethod.WEIGHTED_SUM).toBe('weighted_sum') - expect(AggregationMethod.HARMONIC_MEAN).toBe('harmonic_mean') - expect(AggregationMethod.GEOMETRIC_MEAN).toBe('geometric_mean') - }) - }) - - describe('Interface Validation', () => { - test('ValidationResult should contain expected properties', () => { - const result: ValidationResult = { - isValid: true, - errors: [], - warnings: [], - } - - expect(result.isValid).toBe(true) - expect(Array.isArray(result.errors)).toBe(true) - expect(Array.isArray(result.warnings)).toBe(true) - }) - - test('ValidationError should contain expected properties', () => { - const builder = new StandardObjectiveBuilder() - const validation = builder.validate() - - for (const error of validation.errors) { - expect(error).toHaveProperty('field') - expect(error).toHaveProperty('message') - expect(error).toHaveProperty('code') - expect(typeof error.field).toBe('string') - expect(typeof String(error)).toBe('string') - expect(typeof error.code).toBe('string') - } - }) - }) - - describe('Edge Cases', () => { - test('should handle empty registry operations', () => { - const registry = new StandardObjectiveRegistry() - - expect(registry.size()).toBe(0) - expect(registry.getAll()).toHaveLength(0) - expect(registry.get('anything')).toBeUndefined() - expect(registry.unregister('anything')).toBe(false) - - const validation = registry.validate() - expect(validation.isValid).toBe(true) - expect(validation.errors).toHaveLength(0) - }) - - test('should handle multiple criteria addition', () => { - const builder = new StandardObjectiveBuilder() - const criterion1 = ObjectiveFactory.createCriterion( - 'c1', - 'Criterion 1', - 0.3, - ) - const criterion2 = ObjectiveFactory.createCriterion( - 'c2', - 'Criterion 2', - 0.7, - ) - - builder.addCriterion(criterion1) - builder.addCriterion(criterion2) - - // Build a valid objective to test criteria were added - const mockEvaluationFunction = createMockEvaluationFunction(0.8) - const objective = builder - .setId('test') - .setName('Test') - .setDescription('Test') - .setWeight(0.5) - .setEvaluationFunction(mockEvaluationFunction) - .build() - - expect(objective.criteria).toHaveLength(2) - expect(objective.criteria[0]).toEqual(criterion1) - expect(objective.criteria[1]).toEqual(criterion2) - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/core/objective-interfaces.ts b/apps/web/src/lib/metaaligner/core/objective-interfaces.ts deleted file mode 100644 index 9a5abced38..0000000000 --- a/apps/web/src/lib/metaaligner/core/objective-interfaces.ts +++ /dev/null @@ -1,619 +0,0 @@ -/** - * Objective definition interfaces and data structures for MetaAligner - * Provides extensible framework for defining and managing alignment objectives - */ - -import type { - ObjectiveDefinition, - ObjectiveCriteria, - AlignmentContext, -} from './objectives' - -/** - * Interface for creating new objective definitions - */ -export interface ObjectiveBuilder { - setId(id: string): ObjectiveBuilder - setName(name: string): ObjectiveBuilder - setDescription(description: string): ObjectiveBuilder - setWeight(weight: number): ObjectiveBuilder - addCriterion(criterion: ObjectiveCriteria): ObjectiveBuilder - setEvaluationFunction( - fn: (response: string, context: AlignmentContext) => number, - ): ObjectiveBuilder - build(): ObjectiveDefinition - validate(): ValidationResult -} - -/** - * Validation result for objective definitions - */ -export interface ValidationResult { - isValid: boolean - errors: ValidationError[] - warnings: ValidationWarning[] -} - -export interface ValidationError { - field: string - message: string - code: string -} - -export interface ValidationWarning { - field: string - message: string - code: string -} - -/** - * Objective registry for managing multiple objective sets - */ -export interface ObjectiveRegistry { - register(objective: ObjectiveDefinition): void - unregister(objectiveId: string): boolean - get(objectiveId: string): ObjectiveDefinition | undefined - getAll(): ObjectiveDefinition[] - getByCategory(category: ObjectiveCategory): ObjectiveDefinition[] - clear(): void - size(): number - validate(): ValidationResult -} - -/** - * Categories for organizing objectives - */ -export enum ObjectiveCategory { - CORE = 'core', - CLINICAL = 'clinical', - CULTURAL = 'cultural', - DEMOGRAPHIC = 'demographic', - SPECIALIZED = 'specialized', - EXPERIMENTAL = 'experimental', - CUSTOM = 'custom', -} - -/** - * Metadata for objective definitions - */ -export interface ObjectiveMetadata { - category: ObjectiveCategory - version: string - author: string - createdAt: Date - updatedAt: Date - tags: string[] - dependencies: string[] - compatibleModels: string[] - minConfidenceThreshold: number - validationRules: ValidationRule[] -} - -/** - * Validation rules for objectives - */ -export interface ValidationRule { - name: string - description: string - validator: (objective: ObjectiveDefinition) => boolean - errorMessage: string -} - -/** - * Extended objective definition with metadata - */ -export interface ExtendedObjectiveDefinition extends ObjectiveDefinition { - metadata: ObjectiveMetadata -} - -/** - * Objective configuration for runtime use - */ -export interface ObjectiveConfiguration { - objectives: Record // objectiveId -> weight - contextualWeights: Record> // context -> objectiveId -> weight multiplier - globalSettings: ObjectiveGlobalSettings -} - -export interface ObjectiveGlobalSettings { - enableDynamicWeighting: boolean - enableContextualAdjustment: boolean - minObjectiveScore: number - maxObjectiveScore: number - normalizationMethod: NormalizationMethod - aggregationMethod: AggregationMethod -} - -export enum NormalizationMethod { - NONE = 'none', - MIN_MAX = 'min_max', - Z_SCORE = 'z_score', - SIGMOID = 'sigmoid', -} - -export enum AggregationMethod { - WEIGHTED_AVERAGE = 'weighted_average', - WEIGHTED_SUM = 'weighted_sum', - HARMONIC_MEAN = 'harmonic_mean', - GEOMETRIC_MEAN = 'geometric_mean', -} - -/** - * Objective evaluation result - */ -export interface ObjectiveEvaluationResult { - objectiveId: string - score: number - criteriaScores: Record - confidence: number - explanation?: string - metadata: EvaluationMetadata -} - -export interface EvaluationMetadata { - evaluationTime: number // milliseconds - modelUsed?: string - contextFactors: string[] - adjustmentFactors: Record -} - -/** - * Combined evaluation result for all objectives - */ -export interface AlignmentEvaluationResult { - overallScore: number - objectiveResults: Record - weights: Record - normalizedScores: Record - aggregationMethod: AggregationMethod - evaluationContext: AlignmentContext - timestamp: Date -} - -/** - * Configuration schema type for objective templates - */ -export interface ObjectiveConfigSchema { - [key: string]: { - type: string - description?: string - default?: unknown - required?: boolean - } -} - -/** - * Objective template for creating new objectives - */ -export interface ObjectiveTemplate { - id: string - name: string - description: string - category: ObjectiveCategory - defaultWeight: number - criteriaTemplates: CriterionTemplate[] - evaluationTemplate: string // Template for evaluation function - configurationSchema: ObjectiveConfigSchema // JSON schema for configuration -} - -export interface CriterionTemplate { - criterion: string - description: string - defaultWeight: number - evaluationHints: string[] -} - -/** - * Implementation of ObjectiveBuilder - */ -export class StandardObjectiveBuilder implements ObjectiveBuilder { - private readonly objective: Partial = {} - protected criteria: ObjectiveCriteria[] = [] - - setId(id: string): ObjectiveBuilder { - this.objective.id = id - return this - } - - setName(name: string): ObjectiveBuilder { - this.objective.name = name - return this - } - - setDescription(description: string): ObjectiveBuilder { - this.objective.description = description - return this - } - - setWeight(weight: number): ObjectiveBuilder { - this.objective.weight = weight - return this - } - - addCriterion(criterion: ObjectiveCriteria): ObjectiveBuilder { - this.criteria.push(criterion) - return this - } - - setEvaluationFunction( - fn: (response: string, context: AlignmentContext) => number, - ): ObjectiveBuilder { - this.objective.evaluationFunction = fn - return this - } - - private validateRequiredFields(errors: ValidationError[]): void { - const requiredFields = [ - { - field: 'id', - value: this.objective.id, - code: 'MISSING_ID', - message: 'Objective ID is required', - }, - { - field: 'name', - value: this.objective.name, - code: 'MISSING_NAME', - message: 'Objective name is required', - }, - { - field: 'description', - value: this.objective.description, - code: 'MISSING_DESCRIPTION', - message: 'Objective description is required', - }, - { - field: 'evaluationFunction', - value: this.objective.evaluationFunction, - code: 'MISSING_EVALUATION_FUNCTION', - message: 'Evaluation function is required', - }, - ] - - for (const { field, value, code, message } of requiredFields) { - if (!value) { - errors.push({ field, message, code }) - } - } - - if (this.objective.weight === undefined) { - errors.push({ - field: 'weight', - message: 'Objective weight is required', - code: 'MISSING_WEIGHT', - }) - } - } - - private validateWeight(errors: ValidationError[]): void { - if ( - this.objective.weight !== undefined && - (this.objective.weight <= 0 || this.objective.weight > 1) - ) { - errors.push({ - field: 'weight', - message: 'Weight must be between 0 and 1', - code: 'INVALID_WEIGHT_RANGE', - }) - } - } - - private validateCriteria( - errors: ValidationError[], - warnings: ValidationWarning[], - ): void { - if (this.criteria.length === 0) { - warnings.push({ - field: 'criteria', - message: 'No criteria defined for objective', - code: 'NO_CRITERIA', - }) - return - } - - const totalCriteriaWeight = this.criteria.reduce( - (sum, criterion) => sum + criterion.weight, - 0, - ) - if (Math.abs(totalCriteriaWeight - 1.0) > 0.001) { - errors.push({ - field: 'criteria', - message: `Criteria weights sum to ${totalCriteriaWeight}, should sum to 1.0`, - code: 'INVALID_CRITERIA_WEIGHTS', - }) - } - - // Validate individual criteria - for (const criterion of this.criteria) { - if (!criterion.criterion) { - errors.push({ - field: 'criteria', - message: 'Criterion name is required', - code: 'MISSING_CRITERION_NAME', - }) - } - if (!criterion.description) { - errors.push({ - field: 'criteria', - message: 'Criterion description is required', - code: 'MISSING_CRITERION_DESCRIPTION', - }) - } - if (criterion.weight <= 0 || criterion.weight > 1) { - errors.push({ - field: 'criteria', - message: 'Criterion weight must be between 0 and 1', - code: 'INVALID_CRITERION_WEIGHT', - }) - } - } - } - - private validateIdFormat(errors: ValidationError[]): void { - if ( - this.objective.id && - !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(this.objective.id) - ) { - errors.push({ - field: 'id', - message: - 'ID must start with a letter and contain only letters, numbers, underscores, and hyphens', - code: 'INVALID_ID_FORMAT', - }) - } - } - - validate(): ValidationResult { - const errors: ValidationError[] = [] - const warnings: ValidationWarning[] = [] - - this.validateRequiredFields(errors) - this.validateWeight(errors) - this.validateCriteria(errors, warnings) - this.validateIdFormat(errors) - - return { - isValid: errors.length === 0, - errors, - warnings, - } - } - - build(): ObjectiveDefinition { - const validation = this.validate() - if (!validation.isValid) { - throw new Error( - `Invalid objective definition: ${validation.errors.map((e) => e.message).join(', ')}`, - ) - } - - // Safe to assert non-null here since validation passed - const { id, name, description, weight, evaluationFunction } = this.objective - - if ( - !id || - !name || - !description || - weight === undefined || - !evaluationFunction - ) { - throw new Error('Missing required fields after validation') - } - - return { - id, - name, - description, - weight, - criteria: [...this.criteria], - evaluationFunction, - } - } -} - -/** - * Implementation of ObjectiveRegistry - */ -export class StandardObjectiveRegistry implements ObjectiveRegistry { - private readonly objectives = new Map() - - register(objective: ObjectiveDefinition): void { - // Validate objective before registration - const builder = new StandardObjectiveBuilder() - .setId(objective.id) - .setName(objective.name) - .setDescription(objective.description) - .setWeight(objective.weight) - .setEvaluationFunction(objective.evaluationFunction) - - for (const criterion of objective.criteria) { - builder.addCriterion(criterion) - } - - const validation = builder.validate() - if (!validation.isValid) { - throw new Error( - `Cannot register invalid objective: ${validation.errors.map((e) => e.message).join(', ')}`, - ) - } - - this.objectives.set(objective.id, objective) - } - - unregister(objectiveId: string): boolean { - return this.objectives.delete(objectiveId) - } - - get(objectiveId: string): ObjectiveDefinition | undefined { - return this.objectives.get(objectiveId) - } - - getAll(): ObjectiveDefinition[] { - return Array.from(this.objectives.values()) - } - - getByCategory(_category: ObjectiveCategory): ObjectiveDefinition[] { - // For basic implementation, return all objectives - // In extended version with metadata, filter by category - return this.getAll() - } - - clear() { - this.objectives.clear() - } - - size(): number { - return this.objectives.size - } - - validate(): ValidationResult { - const errors: ValidationError[] = [] - const warnings: ValidationWarning[] = [] - - // Check for duplicate IDs (should not happen with Map, but good to verify) - const ids = new Set() - for (const objective of this.objectives.values()) { - if (ids.has(objective.id)) { - errors.push({ - field: 'registry', - message: `Duplicate objective ID: ${objective.id}`, - code: 'DUPLICATE_ID', - }) - } - ids.add(objective.id) - } - - // Validate individual objectives - for (const objective of this.objectives.values()) { - const builder = new StandardObjectiveBuilder() - .setId(objective.id) - .setName(objective.name) - .setDescription(objective.description) - .setWeight(objective.weight) - .setEvaluationFunction(objective.evaluationFunction) - - for (const criterion of objective.criteria) { - builder.addCriterion(criterion) - } - - const validation = builder.validate() - errors.push(...validation.errors) - warnings.push(...validation.warnings) - } - - return { - isValid: errors.length === 0, - errors, - warnings, - } - } -} - -/** - * Factory functions for creating common objective structures - */ -export const ObjectiveFactory = { - createCriterion( - criterion: string, - description: string, - weight: number, - ): ObjectiveCriteria { - if (weight <= 0 || weight > 1) { - throw new Error('Criterion weight must be between 0 and 1') - } - - return { - criterion, - description, - weight, - } - }, - - createBasicObjective( - id: string, - name: string, - description: string, - weight: number, - criteria: ObjectiveCriteria[], - evaluationFunction: (response: string, context: AlignmentContext) => number, - ): ObjectiveDefinition { - const builder = new StandardObjectiveBuilder() - .setId(id) - .setName(name) - .setDescription(description) - .setWeight(weight) - .setEvaluationFunction(evaluationFunction) - - for (const criterion of criteria) { - builder.addCriterion(criterion) - } - - return builder.build() - }, - - createFromTemplate( - template: ObjectiveTemplate, - _customConfig?: Record, - ): ObjectiveDefinition { - const builder = new StandardObjectiveBuilder() - .setId(template.id) - .setName(template.name) - .setDescription(template.description) - .setWeight(template.defaultWeight) - - // Add criteria from template - for (const criterionTemplate of template.criteriaTemplates) { - builder.addCriterion({ - criterion: criterionTemplate.criterion, - description: criterionTemplate.description, - weight: criterionTemplate.defaultWeight, - }) - } - - // Create a basic evaluation function (would be more sophisticated in real implementation) - builder.setEvaluationFunction( - (_response: string, _context: AlignmentContext) => { - // Placeholder implementation - return Math.random() // In real implementation, this would use the evaluation template - }, - ) - - return builder.build() - }, -} as const - -// Add multiple criteria to builder in one call -declare module './objective-interfaces' { - interface ObjectiveBuilder { - addCriterion(...criteria: ObjectiveCriteria[]): ObjectiveBuilder - } -} - -// Extend the StandardObjectiveBuilder to support multiple criteria -StandardObjectiveBuilder.prototype.addCriterion = function ( - ...criteria: ObjectiveCriteria[] -): ObjectiveBuilder { - if (criteria.length === 1 && criteria[0] !== undefined) { - this.criteria.push(criteria[0]) - } else { - for (const criterion of criteria) { - if (criterion !== undefined) { - this.criteria.push(criterion) - } - } - } - return this -} - -// Alignment metrics for objective evaluation -export interface AlignmentMetrics { - overallScore: number - dimensionScores: Record - confidence: number - timestamp: Date - metadata?: { - evaluationTime: number - modelUsed?: string - contextFactors: string[] - } -} diff --git a/apps/web/src/lib/metaaligner/core/objective-metrics.test.ts b/apps/web/src/lib/metaaligner/core/objective-metrics.test.ts deleted file mode 100644 index 85d5167307..0000000000 --- a/apps/web/src/lib/metaaligner/core/objective-metrics.test.ts +++ /dev/null @@ -1,576 +0,0 @@ -/** - * Unit tests for objective evaluation metrics system - */ - -import { describe, it as test, expect, beforeEach } from 'vitest' - -import { - AlignmentEvaluationResult, - AggregationMethod, -} from './objective-interfaces' -import { - ObjectiveMetricsEngine, - DEFAULT_METRICS_CONFIG, - MetricsConfig, -} from './objective-metrics' -import { - ObjectiveDefinition, - AlignmentContext, - ContextType, -} from './objectives' - -describe('ObjectiveMetricsEngine', () => { - let engine: ObjectiveMetricsEngine - let mockObjectives: ObjectiveDefinition[] - let mockContext: AlignmentContext - let mockEvaluationResult: AlignmentEvaluationResult - - beforeEach(() => { - engine = new ObjectiveMetricsEngine(DEFAULT_METRICS_CONFIG) - - mockObjectives = [ - { - id: 'correctness', - name: 'Correctness', - description: 'Factual accuracy and correctness', - weight: 0.4, - criteria: [ - { - criterion: 'accuracy', - description: 'How accurate the response is', - weight: 0.6, - }, - { - criterion: 'completeness', - description: 'How complete the response is', - weight: 0.4, - }, - ], - evaluationFunction: () => 0.8, - }, - { - id: 'empathy', - name: 'Empathy', - description: 'Emotional understanding and support', - weight: 0.3, - criteria: [ - { - criterion: 'emotional_recognition', - description: 'Ability to recognize emotions', - weight: 0.5, - }, - { - criterion: 'supportive_language', - description: 'Use of supportive language', - weight: 0.5, - }, - ], - evaluationFunction: () => 0.7, - }, - { - id: 'safety', - name: 'Safety', - description: 'Safety and harm prevention', - weight: 0.3, - criteria: [ - { - criterion: 'harm_prevention', - description: 'Prevention of potential harm', - weight: 0.7, - }, - { - criterion: 'risk_assessment', - description: 'Assessment of risks', - weight: 0.3, - }, - ], - evaluationFunction: () => 0.9, - }, - ] - - mockContext = { - userQuery: 'I need help with my anxiety', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - sessionMetadata: {}, - } - - mockEvaluationResult = { - overallScore: 0.8, - objectiveResults: { - correctness: { - objectiveId: 'correctness', - score: 0.8, - confidence: 0.9, - criteriaScores: { - accuracy: 0.85, - completeness: 0.75, - }, - metadata: { - evaluationTime: 150, - contextFactors: ['user_query', 'conversation_history'], - adjustmentFactors: { urgency: 0.1, complexity: 0.2 }, - }, - }, - empathy: { - objectiveId: 'empathy', - score: 0.7, - confidence: 0.8, - criteriaScores: { - emotional_recognition: 0.75, - supportive_language: 0.65, - }, - metadata: { - evaluationTime: 120, - contextFactors: ['emotional_tone', 'support_needed'], - adjustmentFactors: { empathy_boost: 0.15 }, - }, - }, - safety: { - objectiveId: 'safety', - score: 0.9, - confidence: 0.95, - criteriaScores: { - harm_prevention: 0.95, - risk_assessment: 0.8, - }, - metadata: { - evaluationTime: 100, - contextFactors: ['crisis_indicators', 'safety_concerns'], - adjustmentFactors: { safety_priority: 0.3 }, - }, - }, - }, - aggregationMethod: AggregationMethod.WEIGHTED_AVERAGE, - evaluationContext: mockContext, - timestamp: new Date(), - weights: { correctness: 0.4, empathy: 0.3, safety: 0.3 }, - normalizedScores: { correctness: 0.8, empathy: 0.7, safety: 0.9 }, - } - }) - - describe('calculateObjectiveMetrics', () => { - test('should calculate basic objective metrics correctly', () => { - const objective = mockObjectives[0] // correctness - const evaluationResult = - mockEvaluationResult.objectiveResults['correctness'] - - const metrics = engine.calculateObjectiveMetrics( - objective, - evaluationResult, - mockContext, - ) - - expect(metrics.objectiveId).toBe('correctness') - expect(metrics.score).toBe(0.8) - expect(metrics.confidence).toBeGreaterThan(0) - expect(metrics.confidence).toBeLessThanOrEqual(1) - expect(metrics.reliability).toBeGreaterThanOrEqual(0) - expect(metrics.reliability).toBeLessThanOrEqual(1) - expect(metrics.consistency).toBeGreaterThanOrEqual(0) - expect(metrics.consistency).toBeLessThanOrEqual(1) - expect(metrics.contextualFit).toBeGreaterThan(0) - expect(metrics.contextualFit).toBeLessThanOrEqual(1) - }) - - test('should calculate criteria breakdown correctly', () => { - const objective = mockObjectives[0] // correctness - const evaluationResult = - mockEvaluationResult.objectiveResults['correctness'] - - const metrics = engine.calculateObjectiveMetrics( - objective, - evaluationResult, - mockContext, - ) - - expect(metrics.criteriaBreakdown).toHaveLength(2) - - const accuracyCriteria = metrics.criteriaBreakdown.find( - (c) => c.criterion === 'accuracy', - ) - expect(accuracyCriteria).toBeDefined() - expect(accuracyCriteria!.score).toBe(0.85) - expect(accuracyCriteria!.weight).toBe(0.6) - expect(accuracyCriteria!.contribution).toBeCloseTo(0.85 * 0.6, 5) - expect(accuracyCriteria!.confidence).toBeGreaterThan(0) - expect(accuracyCriteria!.evidence).toBeInstanceOf(Array) - }) - - test('should calculate trend information', () => { - const objective = mockObjectives[0] // correctness - const evaluationResult = - mockEvaluationResult.objectiveResults['correctness'] - - const metrics = engine.calculateObjectiveMetrics( - objective, - evaluationResult, - mockContext, - ) - - expect(metrics.trend).toBeDefined() - expect(['improving', 'declining', 'stable']).toContain( - metrics.trend.direction, - ) - expect(metrics.trend.magnitude).toBeGreaterThanOrEqual(0) - expect(metrics.trend.magnitude).toBeLessThanOrEqual(1) - expect(metrics.trend.confidence).toBeGreaterThanOrEqual(0) - expect(metrics.trend.confidence).toBeLessThanOrEqual(1) - }) - }) - - describe('calculateAlignmentMetrics', () => { - test('should calculate comprehensive alignment metrics', () => { - const metrics = engine.calculateAlignmentMetrics( - mockEvaluationResult, - mockObjectives, - ) - - expect(metrics.overallScore).toBe(0.8) - expect(Object.keys(metrics.objectiveMetrics)).toHaveLength(3) - expect(metrics.objectiveMetrics['correctness']).toBeDefined() - expect(metrics.objectiveMetrics['empathy']).toBeDefined() - expect(metrics.objectiveMetrics['safety']).toBeDefined() - - expect(metrics.balanceScore).toBeGreaterThanOrEqual(0) - expect(metrics.balanceScore).toBeLessThanOrEqual(1) - expect(metrics.consistencyScore).toBeGreaterThanOrEqual(0) - expect(metrics.consistencyScore).toBeLessThanOrEqual(1) - expect(metrics.contextualAlignment).toBeGreaterThanOrEqual(0) - expect(metrics.contextualAlignment).toBeLessThanOrEqual(1) - }) - - test('should generate quality indicators', () => { - const metrics = engine.calculateAlignmentMetrics( - mockEvaluationResult, - mockObjectives, - ) - - expect(metrics.qualityIndicators).toBeDefined() - expect(metrics.qualityIndicators.reliability).toBeGreaterThanOrEqual(0) - expect(metrics.qualityIndicators.reliability).toBeLessThanOrEqual(1) - expect(metrics.qualityIndicators.validity).toBeGreaterThanOrEqual(0) - expect(metrics.qualityIndicators.validity).toBeLessThanOrEqual(1) - expect(metrics.qualityIndicators.sensitivity).toBeGreaterThanOrEqual(0) - expect(metrics.qualityIndicators.sensitivity).toBeLessThanOrEqual(1) - expect(metrics.qualityIndicators.specificity).toBeGreaterThanOrEqual(0) - expect(metrics.qualityIndicators.specificity).toBeLessThanOrEqual(1) - expect(metrics.qualityIndicators.coverage).toBeGreaterThanOrEqual(0) - expect(metrics.qualityIndicators.coverage).toBeLessThanOrEqual(1) - }) - - test('should generate performance profile', () => { - const metrics = engine.calculateAlignmentMetrics( - mockEvaluationResult, - mockObjectives, - ) - - expect(metrics.performanceProfile).toBeDefined() - expect(metrics.performanceProfile.strengths).toBeInstanceOf(Array) - expect(metrics.performanceProfile.weaknesses).toBeInstanceOf(Array) - expect(metrics.performanceProfile.opportunities).toBeInstanceOf(Array) - expect(metrics.performanceProfile.risks).toBeInstanceOf(Array) - - // Safety should be in strengths (highest score: 0.9) - expect(metrics.performanceProfile.strengths).toContain('safety') - // Empathy should be in weaknesses (lowest score: 0.7) - expect(metrics.performanceProfile.weaknesses).toContain('empathy') - }) - }) - - describe('addEvaluation and history tracking', () => { - test('should add evaluation to history', () => { - const initialBenchmarks = engine.getBenchmarks() - - engine.addEvaluation(mockEvaluationResult, mockObjectives) - - // Should have updated benchmarks - const updatedBenchmarks = engine.getBenchmarks() - expect(updatedBenchmarks.length).toBeGreaterThanOrEqual( - initialBenchmarks.length, - ) - }) - - test('should track trends after multiple evaluations', () => { - // Add multiple evaluations with varying scores - const evaluations: any[] = [] - for (let i = 0; i < 5; i++) { - const evaluation = { - ...mockEvaluationResult, - timestamp: new Date(Date.now() + i * 1000), - objectiveResults: { - ...mockEvaluationResult.objectiveResults, - correctness: { - ...mockEvaluationResult.objectiveResults['correctness'], - score: 0.7 + i * 0.05, // Improving trend - }, - }, - } - evaluations.push(evaluation as any) - engine.addEvaluation(evaluation, mockObjectives) - } - - const trend = engine.getTrend('correctness') - expect(trend).toBeDefined() - expect(trend!.direction).toBe('improving') - expect(trend!.period).toBe(5) - }) - - test('should maintain history window size', () => { - const config: MetricsConfig = { - ...DEFAULT_METRICS_CONFIG, - historyWindow: 3, - } - const smallEngine = new ObjectiveMetricsEngine(config) - - // Add more evaluations than history window - for (let i = 0; i < 10; i++) { - const evaluation = { - ...mockEvaluationResult, - timestamp: new Date(Date.now() + i * 1000), - } - smallEngine.addEvaluation(evaluation, mockObjectives) - } - - // History should be limited (allowing some buffer) - expect( - smallEngine['evaluationHistory'].evaluations.length, - ).toBeLessThanOrEqual(6) - }) - }) - - describe('baseline and improvement tracking', () => { - test('should set and use baselines for improvement calculation', () => { - engine.setBaseline('correctness', 0.6) - - const objective = mockObjectives[0] - const evaluationResult = - mockEvaluationResult.objectiveResults['correctness'] - const metrics = engine.calculateObjectiveMetrics( - objective, - evaluationResult, - mockContext, - ) - - expect(metrics.improvement).toBeCloseTo(0.8 - 0.6, 5) // 0.2 improvement - }) - - test('should use first evaluation as baseline when none set', () => { - // Add first evaluation - engine.addEvaluation(mockEvaluationResult, mockObjectives) - - // Add second evaluation with different score - const secondEvaluation = { - ...mockEvaluationResult, - timestamp: new Date(), - objectiveResults: { - ...mockEvaluationResult.objectiveResults, - correctness: { - ...mockEvaluationResult.objectiveResults['correctness'], - score: 0.9, - }, - }, - } - - const objective = mockObjectives[0] - const metrics = engine.calculateObjectiveMetrics( - objective, - secondEvaluation.objectiveResults.correctness, - mockContext, - ) - - expect(metrics.improvement).toBeCloseTo(0.9 - 0.8, 5) // 0.1 improvement from first evaluation - }) - }) - - describe('contextual fit calculation', () => { - test('should calculate higher fit for safety in crisis context', () => { - const crisisContext = { - ...mockContext, - detectedContext: ContextType.CRISIS, - } - - const safetyObjective = mockObjectives[2] // safety - const evaluationResult = mockEvaluationResult.objectiveResults['safety'] - const metrics = engine.calculateObjectiveMetrics( - safetyObjective, - evaluationResult, - crisisContext, - ) - - expect(metrics.contextualFit).toBeGreaterThan(0.5) - }) - - test('should calculate higher fit for correctness in clinical context', () => { - const clinicalContext = { - ...mockContext, - detectedContext: ContextType.CLINICAL_ASSESSMENT, - } - - const correctnessObjective = mockObjectives[0] // correctness - const evaluationResult = - mockEvaluationResult.objectiveResults['correctness'] - const metrics = engine.calculateObjectiveMetrics( - correctnessObjective, - evaluationResult, - clinicalContext, - ) - - expect(metrics.contextualFit).toBeGreaterThan(0.5) - }) - }) - - describe('reliability and consistency calculations', () => { - test('should calculate higher reliability with consistent scores', () => { - // Add multiple evaluations with similar scores - for (let i = 0; i < 5; i++) { - const evaluation = { - ...mockEvaluationResult, - timestamp: new Date(Date.now() + i * 1000), - objectiveResults: { - ...mockEvaluationResult.objectiveResults, - correctness: { - ...mockEvaluationResult.objectiveResults['correctness'], - score: 0.8 + (Math.random() * 0.02 - 0.01), // Very small variation - }, - }, - } - engine.addEvaluation(evaluation, mockObjectives) - } - - const objective = mockObjectives[0] - const evaluationResult = - mockEvaluationResult.objectiveResults['correctness'] - const metrics = engine.calculateObjectiveMetrics( - objective, - evaluationResult, - mockContext, - ) - - expect(metrics.reliability).toBeGreaterThan(0.7) // High reliability - expect(metrics.consistency).toBeGreaterThan(0.7) // High consistency - }) - - test('should calculate lower reliability with inconsistent scores', () => { - // Add multiple evaluations with varying scores - const scores = [0.3, 0.9, 0.4, 0.8, 0.2] - for (let i = 0; i < scores.length; i++) { - const evaluation = { - ...mockEvaluationResult, - timestamp: new Date(Date.now() + i * 1000), - objectiveResults: { - ...mockEvaluationResult.objectiveResults, - correctness: { - ...mockEvaluationResult.objectiveResults['correctness'], - score: scores[i], - }, - }, - } - engine.addEvaluation(evaluation, mockObjectives) - } - - const objective = mockObjectives[0] - const evaluationResult = - mockEvaluationResult.objectiveResults['correctness'] - const metrics = engine.calculateObjectiveMetrics( - objective, - evaluationResult, - mockContext, - ) - - expect(metrics.reliability).toBeLessThan(0.8) // Lower reliability - expect(metrics.consistency).toBeLessThan(0.8) // Lower consistency - }) - }) - - describe('benchmarks', () => { - test('should track benchmark achievement rates', () => { - // Add evaluations with varying performance - const scores = [0.9, 0.7, 0.85, 0.6, 0.95] // 3/5 above 0.8 threshold - for (let i = 0; i < scores.length; i++) { - const evaluation = { - ...mockEvaluationResult, - overallScore: scores[i], - timestamp: new Date(Date.now() + i * 1000), - } - engine.addEvaluation(evaluation, mockObjectives) - } - - const benchmarks = engine.getBenchmarks() - const highPerformanceBenchmark = benchmarks.find( - (b) => b.name === 'High Performance', - ) - - expect(highPerformanceBenchmark).toBeDefined() - expect(highPerformanceBenchmark!.achievementRate).toBeCloseTo(0.6, 1) // 3/5 = 0.6 - }) - }) - - describe('edge cases', () => { - test('should handle empty evaluation results gracefully', () => { - const emptyEvaluation: AlignmentEvaluationResult = { - overallScore: 0, - objectiveResults: {}, - aggregationMethod: AggregationMethod.WEIGHTED_AVERAGE, - evaluationContext: mockContext, - timestamp: new Date(), - weights: {}, - normalizedScores: {}, - } - - const metrics = engine.calculateAlignmentMetrics(emptyEvaluation, []) - - expect(metrics.overallScore).toBe(0) - expect(Object.keys(metrics.objectiveMetrics)).toHaveLength(0) - expect(metrics.balanceScore).toBe(0) - }) - - test('should handle missing criteria scores gracefully', () => { - const objectiveWithMissingCriteria = { - ...mockObjectives[0], - criteria: [ - { - criterion: 'missing_criterion', - description: 'A criterion with missing score', - weight: 1.0, - }, - ], - } - - const evaluationResult = { - ...mockEvaluationResult.objectiveResults['correctness'], - criteriaScores: {}, // No scores for criteria - } - - const metrics = engine.calculateObjectiveMetrics( - objectiveWithMissingCriteria, - evaluationResult, - mockContext, - ) - - expect(metrics.criteriaBreakdown).toHaveLength(1) - expect(metrics?.criteriaBreakdown[0].score).toBe(0) - expect(metrics?.criteriaBreakdown[0].contribution).toBe(0) - }) - }) -}) - -describe('DEFAULT_METRICS_CONFIG', () => { - test('should have reasonable default values', () => { - expect(DEFAULT_METRICS_CONFIG.historyWindow).toBeGreaterThan(0) - expect(DEFAULT_METRICS_CONFIG.trendSensitivity).toBeGreaterThan(0) - expect(DEFAULT_METRICS_CONFIG.trendSensitivity).toBeLessThan(1) - expect(DEFAULT_METRICS_CONFIG.weightDecay).toBeGreaterThan(0) - expect(DEFAULT_METRICS_CONFIG.weightDecay).toBeLessThanOrEqual(1) - expect(DEFAULT_METRICS_CONFIG.confidenceThreshold).toBeGreaterThan(0) - expect(DEFAULT_METRICS_CONFIG.confidenceThreshold).toBeLessThanOrEqual(1) - - expect(DEFAULT_METRICS_CONFIG.benchmarkThresholds).toBeDefined() - expect( - DEFAULT_METRICS_CONFIG.benchmarkThresholds['excellent'], - ).toBeGreaterThan(DEFAULT_METRICS_CONFIG.benchmarkThresholds['good']) - expect(DEFAULT_METRICS_CONFIG.benchmarkThresholds['good']).toBeGreaterThan( - DEFAULT_METRICS_CONFIG.benchmarkThresholds['acceptable'], - ) - }) -}) diff --git a/apps/web/src/lib/metaaligner/core/objective-metrics.ts b/apps/web/src/lib/metaaligner/core/objective-metrics.ts deleted file mode 100644 index d904003dfc..0000000000 --- a/apps/web/src/lib/metaaligner/core/objective-metrics.ts +++ /dev/null @@ -1,834 +0,0 @@ -/** - * Objective evaluation metrics system for MetaAligner - * Provides comprehensive metrics for evaluating objective performance and alignment quality - */ - -import { - ObjectiveEvaluationResult, - AlignmentEvaluationResult, -} from './objective-interfaces' -import { - ObjectiveDefinition, - AlignmentContext, - ObjectiveCriteria, - ContextType, -} from './objectives' - -/** - * Detailed metrics for objective evaluation - */ -export interface ObjectiveMetrics { - objectiveId: string - score: number - criteriaBreakdown: CriteriaMetrics[] - confidence: number - reliability: number - consistency: number - improvement: number // Compared to baseline - trend: EvaluationTrend - contextualFit: number // How well the objective fits the context -} - -export interface CriteriaMetrics { - criterion: string - score: number - weight: number - contribution: number // To overall objective score - confidence: number - evidence: string[] // Supporting evidence for the score -} - -export interface EvaluationTrend { - direction: 'improving' | 'declining' | 'stable' - magnitude: number // 0-1, how significant the trend is - period: number // Number of evaluations considered - confidence: number -} - -/** - * Comprehensive alignment metrics - */ -// Re-export with original name for backward compatibility -export type AlignmentMetrics = DetailedAlignmentMetrics - -export interface DetailedAlignmentMetrics { - overallScore: number - objectiveMetrics: Record - balanceScore: number // How well balanced the objectives are - consistencyScore: number // How consistent across evaluations - improvementScore: number // Overall improvement trend - contextualAlignment: number // How well aligned with context - qualityIndicators: QualityIndicators - performanceProfile: PerformanceProfile -} - -export interface QualityIndicators { - reliability: number // Consistency of measurements - validity: number // How well metrics capture intended concepts - sensitivity: number // Ability to detect meaningful changes - specificity: number // Ability to avoid false positives - coverage: number // How comprehensively objectives are evaluated -} - -export interface PerformanceProfile { - strengths: string[] // Best performing objectives/criteria - weaknesses: string[] // Underperforming areas - opportunities: string[] // Areas for improvement - risks: string[] // Potential concerns -} - -/** - * Historical evaluation tracking - */ -export interface EvaluationHistory { - evaluations: TimestampedEvaluation[] - trends: Record - patterns: EvaluationPattern[] - benchmarks: EvaluationBenchmark[] -} - -export interface TimestampedEvaluation { - timestamp: Date - evaluation: AlignmentEvaluationResult - context: AlignmentContext - metrics: DetailedAlignmentMetrics -} - -export interface EvaluationPattern { - name: string - description: string - frequency: number - significance: number - examples: TimestampedEvaluation[] -} - -export interface EvaluationBenchmark { - name: string - description: string - targetScore: number - currentScore: number - achievementRate: number - lastAchieved?: Date -} - -/** - * Metrics calculation configuration - */ -export interface MetricsConfig { - historyWindow: number // Number of past evaluations to consider - trendSensitivity: number // How sensitive to detect trends - benchmarkThresholds: Record - weightDecay: number // How much to weight recent vs old evaluations - confidenceThreshold: number // Minimum confidence for reliable metrics -} - -/** - * Objective evaluation metrics engine - */ -export class ObjectiveMetricsEngine { - private readonly evaluationHistory: EvaluationHistory - private readonly config: MetricsConfig - private readonly baselines: Map = new Map() - - constructor(config: MetricsConfig) { - this.config = config - this.evaluationHistory = { - evaluations: [], - trends: {}, - patterns: [], - benchmarks: [], - } - } - - /** - * Calculate comprehensive metrics for an objective evaluation - */ - calculateObjectiveMetrics( - objective: ObjectiveDefinition, - evaluationResult: ObjectiveEvaluationResult, - context: AlignmentContext, - ): ObjectiveMetrics { - const criteriaBreakdown = this.calculateCriteriaMetrics( - objective.criteria, - evaluationResult.criteriaScores, - context, - ) - - const confidence = this.calculateConfidence(evaluationResult, context) - const reliability = this.calculateReliability(objective.id) - const consistency = this.calculateConsistency(objective.id) - const improvement = this.calculateImprovement( - objective.id, - evaluationResult.score, - ) - const trend = this.calculateTrend(objective.id) - const contextualFit = this.calculateContextualFit(objective, context) - - return { - objectiveId: objective.id, - score: evaluationResult.score, - criteriaBreakdown, - confidence, - reliability, - consistency, - improvement, - trend, - contextualFit, - } - } - - /** - * Calculate comprehensive alignment metrics - */ - calculateAlignmentMetrics( - evaluationResult: AlignmentEvaluationResult, - objectives: ObjectiveDefinition[], - ): DetailedAlignmentMetrics { - const objectiveMetrics: Record = {} - - // Calculate metrics for each objective - for (const objective of objectives) { - const objResult = evaluationResult.objectiveResults[objective.id] - if (objResult) { - objectiveMetrics[objective.id] = this.calculateObjectiveMetrics( - objective, - objResult, - evaluationResult.evaluationContext, - ) - } - } - - const balanceScore = this.calculateBalanceScore(objectiveMetrics) - const consistencyScore = this.calculateOverallConsistency(objectiveMetrics) - const improvementScore = this.calculateOverallImprovement(objectiveMetrics) - const contextualAlignment = - this.calculateOverallContextualAlignment(objectiveMetrics) - const qualityIndicators = this.calculateQualityIndicators(objectiveMetrics) - const performanceProfile = this.generatePerformanceProfile(objectiveMetrics) - - return { - overallScore: evaluationResult.overallScore, - objectiveMetrics, - balanceScore, - consistencyScore, - improvementScore, - contextualAlignment, - qualityIndicators, - performanceProfile, - } - } - - /** - * Add evaluation to history and update trends - */ - addEvaluation( - evaluation: AlignmentEvaluationResult, - objectives: ObjectiveDefinition[], - ): void { - const metrics = this.calculateAlignmentMetrics(evaluation, objectives) - - const timestampedEvaluation: TimestampedEvaluation = { - timestamp: evaluation.timestamp, - evaluation, - context: evaluation.evaluationContext, - metrics, - } - - this.evaluationHistory.evaluations.push(timestampedEvaluation) - - // Keep only recent history - if ( - this.evaluationHistory.evaluations.length > - this.config.historyWindow * 2 - ) { - this.evaluationHistory.evaluations = - this.evaluationHistory.evaluations.slice(-this.config.historyWindow) - } - - // Update trends - this.updateTrends() - this.updatePatterns() - this.updateBenchmarks() - } - - /** - * Get evaluation trends for specific objective - */ - getTrend(objectiveId: string): EvaluationTrend | undefined { - return this.evaluationHistory.trends[objectiveId] - } - - /** - * Get performance benchmarks - */ - getBenchmarks(): EvaluationBenchmark[] { - return this.evaluationHistory.benchmarks - } - - /** - * Set baseline scores for improvement calculations - */ - setBaseline(objectiveId: string, score: number): void { - this.baselines.set(objectiveId, score) - } - - private calculateCriteriaMetrics( - criteria: ObjectiveCriteria[], - criteriaScores: Record, - context: AlignmentContext, - ): CriteriaMetrics[] { - return criteria.map((criterion) => { - const score = criteriaScores[criterion.criterion] ?? 0 - const contribution = score * criterion.weight - const confidence = this.calculateCriterionConfidence( - criterion, - score, - context, - ) - const evidence = this.generateEvidence(criterion, score) - - return { - criterion: criterion.criterion, - score, - weight: criterion.weight, - contribution, - confidence, - evidence, - } - }) - } - - private calculateConfidence( - evaluationResult: ObjectiveEvaluationResult, - context: AlignmentContext, - ): number { - const { confidence: baseConfidence } = evaluationResult - - // Adjust based on context clarity - const contextClarity = this.assessContextClarity(context) - let confidence = baseConfidence * contextClarity - - // Adjust based on historical consistency - const historicalConfidence = this.getHistoricalConfidence( - evaluationResult.objectiveId, - ) - confidence = (confidence + historicalConfidence) / 2 - - return Math.max(0, Math.min(1, confidence)) - } - - private calculateReliability(objectiveId: string): number { - const recentEvaluations = this.getRecentEvaluations(objectiveId, 5) - - if (recentEvaluations.length < 2) { - return 0.5 // Default reliability for insufficient data - } - - const scores = recentEvaluations.map( - (e) => e.evaluation.objectiveResults[objectiveId]?.score ?? 0, - ) - - // Calculate reliability as inverse of normalized standard deviation - const mean = scores.reduce((sum, score) => sum + score, 0) / scores.length - const variance = - scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / - scores.length - const stdDev = Math.sqrt(variance) - - // Normalize std dev by the theoretical maximum (assuming scores are 0-1) - const normalizedStdDev = stdDev / 0.5 // 0.5 is max std dev for 0-1 range - - // Reliability is inverse of normalized std dev - return Math.max(0, Math.min(1, 1 - normalizedStdDev)) - } - - private calculateConsistency(objectiveId: string): number { - const recentEvaluations = this.getRecentEvaluations(objectiveId, 10) - - if (recentEvaluations.length < 3) { - return 0.5 // Default consistency for insufficient data - } - - const scores = recentEvaluations.map( - (e) => e.evaluation.objectiveResults[objectiveId]?.score ?? 0, - ) - - // Calculate consistency as inverse of normalized variance - const mean = scores.reduce((sum, score) => sum + score, 0) / scores.length - const variance = - scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / - scores.length - - // Normalize variance by the theoretical maximum (0.25 for 0-1 range) - const normalizedVariance = variance / 0.25 - - return Math.max(0, Math.min(1, 1 - normalizedVariance)) - } - - private calculateImprovement( - objectiveId: string, - currentScore: number, - ): number { - const baseline = this.baselines.get(objectiveId) - - if (baseline === undefined) { - // Use first evaluation as baseline - const firstEvaluation = this.evaluationHistory.evaluations.find( - (e) => e.evaluation.objectiveResults[objectiveId], - ) - - if (firstEvaluation) { - const objectiveResult = - firstEvaluation.evaluation.objectiveResults[objectiveId] - if (objectiveResult) { - const firstScore = objectiveResult.score - this.baselines.set(objectiveId, firstScore) - return currentScore - firstScore - } - } - - return 0 // No baseline available - } - - return currentScore - baseline - } - - private calculateTrend(objectiveId: string): EvaluationTrend { - const recentEvaluations = this.getRecentEvaluations( - objectiveId, - this.config.historyWindow, - ) - - if (recentEvaluations.length < 3) { - return { - direction: 'stable', - magnitude: 0, - period: recentEvaluations.length, - confidence: 0.5, - } - } - - const scores = recentEvaluations.map((e) => { - const objectiveResult = e.evaluation.objectiveResults[objectiveId] - return objectiveResult?.score ?? 0 - }) - - // Simple linear regression to detect trend - const n = scores.length - const x = Array.from({ length: n }, (_, i) => i) - const sumX = x.reduce((sum, val) => sum + val, 0) - const sumY = scores.reduce((sum, val) => sum + val, 0) - const sumXY = x.reduce((sum, val, i) => sum + val * (scores[i] ?? 0), 0) - const sumX2 = x.reduce((sum, val) => sum + val * val, 0) - - const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX) - const magnitude = Math.abs(slope) - - let direction: 'improving' | 'declining' | 'stable' - if (magnitude < this.config.trendSensitivity) { - direction = 'stable' - } else { - direction = slope > 0 ? 'improving' : 'declining' - } - - // Calculate R² for confidence - const meanY = sumY / n - const totalSumSquares = scores.reduce( - (sum, score) => sum + Math.pow(score - meanY, 2), - 0, - ) - const residualSumSquares = scores.reduce((sum, score, i) => { - const predicted = slope * i + (sumY - slope * sumX) / n - return sum + Math.pow(score - predicted, 2) - }, 0) - - const rSquared = - totalSumSquares > 0 ? 1 - residualSumSquares / totalSumSquares : 0 - - return { - direction, - magnitude: Math.min(1, magnitude * 10), // Scale magnitude - period: n, - confidence: Math.max(0, Math.min(1, rSquared)), - } - } - - private calculateContextualFit( - objective: ObjectiveDefinition, - context: AlignmentContext, - ): number { - // Simple heuristic for how well an objective fits the context - let fit = 0.5 // Base fit - // Adjust based on objective type and context - switch (context.detectedContext) { - case ContextType.CRISIS: - if (objective.id === 'safety' || objective.id === 'empathy') { - fit += 0.4 - } - break - case ContextType.CLINICAL_ASSESSMENT: - if ( - objective.id === 'correctness' || - objective.id === 'professionalism' - ) { - fit += 0.4 - } - break - case ContextType.EDUCATIONAL: - if ( - objective.id === 'informativeness' || - objective.id === 'correctness' - ) { - fit += 0.4 - } - break - case ContextType.SUPPORT: - if ( - objective.id === 'empathy' || - objective.id === 'therapeutic_rapport' - ) { - fit += 0.4 - } - break - case ContextType.INFORMATIONAL: - if (objective.id === 'informativeness' || objective.id === 'clarity') { - fit += 0.4 - } - break - case ContextType.GENERAL: - // General context - no special adjustments - break - default: - break - } - - return Math.max(0, Math.min(1, fit)) - } - - private calculateBalanceScore( - objectiveMetrics: Record, - ): number { - const scores = Object.values(objectiveMetrics).map((m) => m.score) - - if (scores.length === 0) { - return 0 - } - - // Calculate how balanced the scores are (lower variance = better balance) - const mean = scores.reduce((sum, score) => sum + score, 0) / scores.length - const variance = - scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / - scores.length - - return Math.max(0, Math.min(1, 1 - variance)) - } - - private calculateOverallConsistency( - objectiveMetrics: Record, - ): number { - const consistencyScores = Object.values(objectiveMetrics).map( - (m) => m.consistency, - ) - - if (consistencyScores.length === 0) { - return 0 - } - - return ( - consistencyScores.reduce((sum, score) => sum + score, 0) / - consistencyScores.length - ) - } - - private calculateOverallImprovement( - objectiveMetrics: Record, - ): number { - const improvementScores = Object.values(objectiveMetrics).map( - (m) => m.improvement, - ) - - if (improvementScores.length === 0) { - return 0 - } - - return ( - improvementScores.reduce((sum, score) => sum + score, 0) / - improvementScores.length - ) - } - - private calculateOverallContextualAlignment( - objectiveMetrics: Record, - ): number { - const alignmentScores = Object.values(objectiveMetrics).map( - (m) => m.contextualFit, - ) - - if (alignmentScores.length === 0) { - return 0 - } - - return ( - alignmentScores.reduce((sum, score) => sum + score, 0) / - alignmentScores.length - ) - } - - private calculateQualityIndicators( - objectiveMetrics: Record, - ): QualityIndicators { - const metrics = Object.values(objectiveMetrics) - - const reliability = - metrics.reduce((sum, m) => sum + m.reliability, 0) / metrics.length || 0 - const validity = this.calculateValidity(metrics) - const sensitivity = this.calculateSensitivity() - const specificity = this.calculateSpecificity() - const coverage = this.calculateCoverage(metrics) - - return { - reliability, - validity, - sensitivity, - specificity, - coverage, - } - } - - private generatePerformanceProfile( - objectiveMetrics: Record, - ): PerformanceProfile { - const sortedMetrics = Object.entries(objectiveMetrics).sort( - ([, a], [, b]) => b.score - a.score, - ) - - const strengths = sortedMetrics.slice(0, 2).map(([id]) => id) - - const weaknesses = sortedMetrics.slice(-2).map(([id]) => id) - - const opportunities = Object.entries(objectiveMetrics) - .filter( - ([, metrics]) => - metrics.improvement < 0 && metrics.trend.direction === 'improving', - ) - .map(([id]) => id) - - const risks = Object.entries(objectiveMetrics) - .filter( - ([, metrics]) => - metrics.score < 0.5 && metrics.trend.direction === 'declining', - ) - .map(([id]) => id) - - return { - strengths, - weaknesses, - opportunities, - risks, - } - } - - private updateTrends() { - // Update trends for all objectives with sufficient history - const objectiveIds = new Set() - this.evaluationHistory.evaluations.forEach((e) => { - Object.keys(e.evaluation.objectiveResults).forEach((id) => - objectiveIds.add(id), - ) - }) - - objectiveIds.forEach((objectiveId) => { - this.evaluationHistory.trends[objectiveId] = - this.calculateTrend(objectiveId) - }) - } - - private updatePatterns() { - // Simple pattern detection - could be more sophisticated - // For now, just identify consistent high/low performance patterns - const patterns: EvaluationPattern[] = [] - - // Add pattern detection logic here - // This is a simplified implementation - this.evaluationHistory.patterns = patterns - } - - private updateBenchmarks() { - // Update benchmark achievement tracking - const benchmarks: EvaluationBenchmark[] = [ - { - name: 'High Performance', - description: 'Overall score above 0.8', - targetScore: 0.8, - currentScore: this.getLatestOverallScore(), - achievementRate: this.calculateAchievementRate(0.8), - }, - { - name: 'Consistent Quality', - description: 'Consistency score above 0.7', - targetScore: 0.7, - currentScore: this.getLatestConsistencyScore(), - achievementRate: this.calculateConsistencyAchievementRate(0.7), - }, - ] - - this.evaluationHistory.benchmarks = benchmarks - } - - private getRecentEvaluations( - objectiveId: string, - count: number, - ): TimestampedEvaluation[] { - return this.evaluationHistory.evaluations - .filter((e) => e.evaluation.objectiveResults[objectiveId]) - .slice(-count) - } - - private calculateCriterionConfidence( - _criterion: ObjectiveCriteria, - score: number, - _context: AlignmentContext, - ): number { - // Simple confidence calculation based on score and context - let confidence = 0.8 // Base confidence - - // Adjust based on score extremes (very high or low scores might be less reliable) - if (score < 0.1 || score > 0.9) { - confidence -= 0.2 - } - - return Math.max(0.1, Math.min(1, confidence)) - } - - private generateEvidence( - criterion: ObjectiveCriteria, - score: number, - ): string[] { - // Generate simple evidence based on score - const evidence: string[] = [] - - if (score > 0.8) { - evidence.push(`Strong performance in ${criterion.criterion}`) - } else if (score < 0.4) { - evidence.push(`Needs improvement in ${criterion.criterion}`) - } else { - evidence.push(`Moderate performance in ${criterion.criterion}`) - } - - return evidence - } - - private assessContextClarity(context: AlignmentContext): number { - // Simple context clarity assessment - let clarity = 0.7 // Base clarity - - if (context.userQuery && context.userQuery.length > 10) { - clarity += 0.1 - } - - if (context.conversationHistory && context.conversationHistory.length > 0) { - clarity += 0.1 - } - - return Math.max(0.1, Math.min(1, clarity)) - } - - private getHistoricalConfidence(objectiveId: string): number { - const recentEvaluations = this.getRecentEvaluations(objectiveId, 5) - - if (recentEvaluations.length === 0) { - return 0.5 - } - - const confidenceScores = recentEvaluations.map( - (e) => e.evaluation.objectiveResults[objectiveId]?.confidence ?? 0.5, - ) - - return ( - confidenceScores.reduce((sum, conf) => sum + conf, 0) / - confidenceScores.length - ) - } - - private calculateValidity(_metrics: ObjectiveMetrics[]): number { - // Simple validity calculation - could be more sophisticated - return 0.8 // Placeholder - } - - private calculateSensitivity(): number { - // Calculate ability to detect meaningful changes - return 0.75 // Placeholder - } - - private calculateSpecificity(): number { - // Calculate ability to avoid false positives - return 0.85 // Placeholder - } - - private calculateCoverage(metrics: ObjectiveMetrics[]): number { - // Calculate how comprehensively objectives are evaluated - return Math.min(1, metrics.length / 5) // Assuming 5 core objectives - } - - private getLatestOverallScore(): number { - const latest = - this.evaluationHistory.evaluations[ - this.evaluationHistory.evaluations.length - 1 - ] - return latest?.evaluation.overallScore ?? 0 - } - - private getLatestConsistencyScore(): number { - const latest = - this.evaluationHistory.evaluations[ - this.evaluationHistory.evaluations.length - 1 - ] - return latest?.metrics.consistencyScore ?? 0 - } - - private calculateAchievementRate(threshold: number): number { - const recentScores = this.evaluationHistory.evaluations - .slice(-10) - .map((e) => e.evaluation.overallScore) - - if (recentScores.length === 0) { - return 0 - } - - const achievements = recentScores.filter( - (score) => score >= threshold, - ).length - return achievements / recentScores.length - } - - private calculateConsistencyAchievementRate(threshold: number): number { - const recentScores = this.evaluationHistory.evaluations - .slice(-10) - .map((e) => e.metrics.consistencyScore) - - if (recentScores.length === 0) { - return 0 - } - - const achievements = recentScores.filter( - (score) => score >= threshold, - ).length - return achievements / recentScores.length - } -} - -/** - * Default metrics configuration - */ -export const DEFAULT_METRICS_CONFIG: MetricsConfig = { - historyWindow: 20, - trendSensitivity: 0.05, - benchmarkThresholds: { - excellent: 0.9, - good: 0.8, - acceptable: 0.7, - poor: 0.5, - }, - weightDecay: 0.9, - confidenceThreshold: 0.6, -} diff --git a/apps/web/src/lib/metaaligner/core/objective-weighting.test.ts b/apps/web/src/lib/metaaligner/core/objective-weighting.test.ts deleted file mode 100644 index d25225cd29..0000000000 --- a/apps/web/src/lib/metaaligner/core/objective-weighting.test.ts +++ /dev/null @@ -1,605 +0,0 @@ -/** - * Unit tests for objective weighting and balancing algorithms - */ - -import { describe, it as test, expect, beforeEach } from 'vitest' - -import { - AggregationMethod, - NormalizationMethod, - ObjectiveConfiguration, - ObjectiveEvaluationResult, -} from './objective-interfaces' -import { - ObjectiveWeightingEngine, - ObjectiveBalancer, - WeightingStrategy, - // DEFAULT_CONTEXT_MULTIPLIERS, // No longer used - DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, -} from './objective-weighting' -import { - ObjectiveDefinition, - AlignmentContext, - ContextType, - UserProfile, // Import UserProfile -} from './objectives' - -describe('ObjectiveWeightingEngine', () => { - let engine: ObjectiveWeightingEngine - let mockObjectives: ObjectiveDefinition[] - let mockContext: AlignmentContext - let mockConfig: ObjectiveConfiguration - - beforeEach(() => { - // engine = new ObjectiveWeightingEngine( // Old instantiation - // DEFAULT_CONTEXT_MULTIPLIERS, - // DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, - // ) - engine = new ObjectiveWeightingEngine(DEFAULT_WEIGHT_ADJUSTMENT_PARAMS) - - mockObjectives = [ - { - id: 'correctness', - name: 'Correctness', - description: 'Test correctness', - weight: 1.0 / 3.0, // 0.333... - criteria: [], - evaluationFunction: () => 0.8, - }, - { - id: 'empathy', - name: 'Empathy', - description: 'Test empathy', - weight: 1.0 / 3.0, // 0.333... - criteria: [], - evaluationFunction: () => 0.7, - }, - { - id: 'safety', - name: 'Safety', - description: 'Test safety', - weight: 1.0 / 3.0, // 0.333... - criteria: [], - evaluationFunction: () => 0.9, - }, - ] - - mockContext = { - userQuery: 'I need help', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - sessionMetadata: {}, - userProfile: undefined, // Initialize with no profile for most tests - } - - mockConfig = { - objectives: { - correctness: 1.0 / 3.0, - empathy: 1.0 / 3.0, - safety: 1.0 / 3.0, - }, - contextualWeights: {}, - globalSettings: { - enableDynamicWeighting: true, - enableContextualAdjustment: true, - minObjectiveScore: 0.0, - maxObjectiveScore: 1.0, - normalizationMethod: NormalizationMethod.NONE, - aggregationMethod: AggregationMethod.WEIGHTED_AVERAGE, - }, - } - }) - - describe('calculateWeights', () => { - test('should return static weights when using STATIC strategy', () => { - const staticEngine = new ObjectiveWeightingEngine({ - // Updated instantiation - ...DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, - strategy: WeightingStrategy.STATIC, - }) - - const result = staticEngine.calculateWeights( - mockObjectives, - mockContext, - mockConfig, - ) - const expectedWeight = 1.0 / 3.0 - - expect(result.strategy).toBe(WeightingStrategy.STATIC) - expect(result.weights['correctness']).toBeCloseTo(expectedWeight, 5) - expect(result.weights['empathy']).toBeCloseTo(expectedWeight, 5) - expect(result.weights['safety']).toBeCloseTo(expectedWeight, 5) - }) - - test('should adjust weights based on context when using CONTEXTUAL strategy', () => { - const crisisContext = { - ...mockContext, - detectedContext: ContextType.CRISIS, - } - - const result = engine.calculateWeights( - mockObjectives, - crisisContext, - mockConfig, - ) - - expect(result.strategy).toBe(WeightingStrategy.CONTEXTUAL) - // In crisis context, safety and empathy should get higher weights - expect(result.weights['safety']).toBeGreaterThan( - result.weights['correctness'], - ) - expect(result.weights['empathy']).toBeGreaterThan( - result.weights['correctness'], - ) - }) - - test('should normalize weights to sum to 1', () => { - const result = engine.calculateWeights( - mockObjectives, - mockContext, - mockConfig, - ) - - const totalWeight = Object.values(result.weights).reduce( - (sum, weight) => sum + weight, - 0, - ) - expect(totalWeight).toBeCloseTo(1.0, 5) - }) - - test('should return confidence score', () => { - const result = engine.calculateWeights( - mockObjectives, - mockContext, - mockConfig, - ) - - expect(result.confidence).toBeGreaterThanOrEqual(0) - expect(result.confidence).toBeLessThanOrEqual(1) - }) - - test('should include metadata in result', () => { - const result = engine.calculateWeights( - mockObjectives, - mockContext, - mockConfig, - ) - - expect(result.metadata).toBeDefined() - expect(result.metadata.calculationTime).toBeGreaterThanOrEqual(0) - expect(result.metadata.contextFactors).toBeInstanceOf(Array) - }) - - test('should apply user preferences when USER_PREFERENCE_ADJUSTED strategy is used', () => { - const userProfileWithPrefs: UserProfile = { - preferences: { - objectiveWeightAdjustments: { empathy: 1.5, safety: 0.8 }, // Boost empathy, reduce safety - preferredObjectives: [ - { objectiveId: 'correctness', preferenceStrength: 0.5 }, - ], // Prefer correctness - }, - } - const contextWithUserPrefs: AlignmentContext = { - ...mockContext, - userProfile: userProfileWithPrefs, - } - const userPrefEngine = new ObjectiveWeightingEngine({ - ...DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, - strategy: WeightingStrategy.USER_PREFERENCE_ADJUSTED, - }) - - const initialWeights = userPrefEngine.calculateWeights( - mockObjectives, - { ...contextWithUserPrefs, userProfile: undefined }, // Calculate without prefs first for comparison - mockConfig, - ).weights - - const result = userPrefEngine.calculateWeights( - mockObjectives, - contextWithUserPrefs, - mockConfig, - ) - - expect(result.strategy).toBe(WeightingStrategy.USER_PREFERENCE_ADJUSTED) - // Empathy should be higher due to objectiveWeightAdjustments - // Correctness should be higher due to preferredObjectives - // Safety should be lower - // Note: These are relative comparisons before normalization. - // The exact values after normalization depend on all combined effects. - - // To make this test more robust, we need to check the relative change or - // pre-calculate expected values if the preference application logic is very specific. - // For now, we check if the direction of adjustment is as expected for some objectives. - // Need to compare against a baseline where preferences are NOT applied but other factors are similar. - // The baseWeights from mockConfig are all equal (1/3). - - // Example: Empathy's weight with prefs should be > its weight without prefs (if strategy was static/default) - // This is a simplified check; a more detailed check would involve isolating the preference effect. - // expect(result.weights.empathy).toBeGreaterThan(initialWeights.empathy! * 1.5 * 0.9) // Allow some margin for normalization - // expect(result.weights.correctness).toBeGreaterThan(initialWeights.correctness! * 1.5 * 0.9) - // expect(result.weights.safety).toBeLessThan(initialWeights.safety! * 0.8 * 1.1) - - // Check ratios: - // Empathy was boosted by 1.5 (from objectiveWeightAdjustments) - // Correctness was boosted by (1 + 0.5) = 1.5 (from preferredObjectives) - // Safety was reduced by 0.8 (from objectiveWeightAdjustments) - - // Ratio of empathy to an unadjusted objective (if one existed) would increase by ~1.5 - // Ratio of empathy to safety: (initialE * 1.5) / (initialS * 0.8) - const empathySafetyRatioInitial = - initialWeights['empathy'] / initialWeights['safety'] - const empathySafetyRatioFinal = - result.weights['empathy'] / result.weights['safety'] - expect(empathySafetyRatioFinal).toBeCloseTo( - empathySafetyRatioInitial * (1.5 / 0.8), - 1, - ) - - const correctnessSafetyRatioInitial = - initialWeights['correctness'] / initialWeights['safety'] - const correctnessSafetyRatioFinal = - result.weights['correctness'] / result.weights['safety'] - // Correctness boost (1+0.5) = 1.5; Safety reduction 0.8 - expect(correctnessSafetyRatioFinal).toBeCloseTo( - correctnessSafetyRatioInitial * (1.5 / 0.8), - 1, - ) - - const totalWeight = Object.values(result.weights).reduce( - (sum, w) => sum + w, - 0, - ) - expect(totalWeight).toBeCloseTo(1.0, 5) - expect(result.metadata.contextFactors).toContain( - 'user_preferences_applied', - ) - }) - }) - - describe('updatePerformanceHistory', () => { - test('should store performance scores for objectives', () => { - engine.updatePerformanceHistory('correctness', 0.8) - engine.updatePerformanceHistory('correctness', 0.7) - - // Performance history is private, but we can test its effect on adaptive weighting - const adaptiveEngine = new ObjectiveWeightingEngine({ - // Updated instantiation - ...DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, - strategy: WeightingStrategy.ADAPTIVE, - }) - - adaptiveEngine.updatePerformanceHistory('correctness', 0.5) // Low performance - const result = adaptiveEngine.calculateWeights( - mockObjectives, - mockContext, - mockConfig, - ) - - // Should work without errors - expect(result.weights).toBeDefined() - }) - }) - - describe('priority-based weighting', () => { - test('should increase weights for priority objectives in crisis context', () => { - const priorityEngine = new ObjectiveWeightingEngine({ - // Updated instantiation - ...DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, - strategy: WeightingStrategy.PRIORITY_BASED, - }) - - const crisisContext = { - ...mockContext, - detectedContext: ContextType.CRISIS, - } - - const result = priorityEngine.calculateWeights( - mockObjectives, - crisisContext, - mockConfig, - ) - const baseWeight = 1.0 / 3.0 - - // Safety and empathy should get priority in crisis context - expect(result.weights['safety']).toBeGreaterThan(baseWeight) - expect(result.weights['empathy']).toBeGreaterThan(baseWeight) - }) - }) -}) - -describe('ObjectiveBalancer', () => { - let mockEvaluationResults: Record - let mockWeights: Record - - beforeEach(() => { - mockEvaluationResults = { - correctness: { - objectiveId: 'correctness', - score: 0.8, - criteriaScores: {}, - confidence: 0.9, - metadata: { - evaluationTime: 10, - contextFactors: ['support'], - adjustmentFactors: {}, - }, - }, - empathy: { - objectiveId: 'empathy', - score: 0.7, - criteriaScores: {}, - confidence: 0.8, - metadata: { - evaluationTime: 12, - contextFactors: ['support'], - adjustmentFactors: {}, - }, - }, - safety: { - objectiveId: 'safety', - score: 0.9, - criteriaScores: {}, - confidence: 0.95, - metadata: { - evaluationTime: 8, - contextFactors: ['support'], - adjustmentFactors: {}, - }, - }, - } - - mockWeights = { - correctness: 0.3, - empathy: 0.3, - safety: 0.4, - } - }) - - describe('balance', () => { - test('should calculate weighted average correctly', () => { - const result = ObjectiveBalancer.balance( - mockEvaluationResults, - mockWeights, - AggregationMethod.WEIGHTED_AVERAGE, - ) - - // Expected: (0.8 * 0.3 + 0.7 * 0.3 + 0.9 * 0.4) / 1.0 = 0.81 - expect(result.overallScore).toBeCloseTo(0.81, 2) - expect(result.aggregationMethod).toBe(AggregationMethod.WEIGHTED_AVERAGE) - }) - - test('should calculate weighted sum correctly', () => { - const result = ObjectiveBalancer.balance( - mockEvaluationResults, - mockWeights, - AggregationMethod.WEIGHTED_SUM, - ) - - // Expected: 0.8 * 0.3 + 0.7 * 0.3 + 0.9 * 0.4 = 0.81 - expect(result.overallScore).toBeCloseTo(0.81, 2) - expect(result.aggregationMethod).toBe(AggregationMethod.WEIGHTED_SUM) - }) - - test('should calculate harmonic mean correctly', () => { - const result = ObjectiveBalancer.balance( - mockEvaluationResults, - mockWeights, - AggregationMethod.HARMONIC_MEAN, - ) - - expect(result.overallScore).toBeGreaterThan(0) - expect(result.overallScore).toBeLessThan(1) - expect(result.aggregationMethod).toBe(AggregationMethod.HARMONIC_MEAN) - }) - - test('should calculate geometric mean correctly', () => { - const result = ObjectiveBalancer.balance( - mockEvaluationResults, - mockWeights, - AggregationMethod.GEOMETRIC_MEAN, - ) - - expect(result.overallScore).toBeGreaterThan(0) - expect(result.overallScore).toBeLessThan(1) - expect(result.aggregationMethod).toBe(AggregationMethod.GEOMETRIC_MEAN) - }) - - test('should apply min-max normalization', () => { - const result = ObjectiveBalancer.balance( - mockEvaluationResults, - mockWeights, - AggregationMethod.WEIGHTED_AVERAGE, - NormalizationMethod.MIN_MAX, - ) - - expect(result.normalizedScores).toBeDefined() - const normalizedValues = Object.values(result.normalizedScores) - - // After min-max normalization, values should be between 0 and 1 - expect(Math.min(...normalizedValues)).toBeGreaterThanOrEqual(0) - expect(Math.max(...normalizedValues)).toBeLessThanOrEqual(1) - }) - - test('should apply z-score normalization', () => { - const result = ObjectiveBalancer.balance( - mockEvaluationResults, - mockWeights, - AggregationMethod.WEIGHTED_AVERAGE, - NormalizationMethod.Z_SCORE, - ) - - expect(result.normalizedScores).toBeDefined() - const normalizedValues = Object.values(result.normalizedScores) - - // Z-score normalization can produce negative values - expect(normalizedValues.length).toBe(3) - }) - - test('should apply sigmoid normalization', () => { - const result = ObjectiveBalancer.balance( - mockEvaluationResults, - mockWeights, - AggregationMethod.WEIGHTED_AVERAGE, - NormalizationMethod.SIGMOID, - ) - - expect(result.normalizedScores).toBeDefined() - const normalizedValues = Object.values(result.normalizedScores) - - // Sigmoid normalization should produce values between 0 and 1 - expect(Math.min(...normalizedValues)).toBeGreaterThan(0) - expect(Math.max(...normalizedValues)).toBeLessThan(1) - }) - - test('should include all required fields in result', () => { - const result = ObjectiveBalancer.balance( - mockEvaluationResults, - mockWeights, - AggregationMethod.WEIGHTED_AVERAGE, - ) - - expect(result.overallScore).toBeDefined() - expect(result.objectiveResults).toBe(mockEvaluationResults) - expect(result.weights).toBe(mockWeights) - expect(result.normalizedScores).toBeDefined() - expect(result.aggregationMethod).toBe(AggregationMethod.WEIGHTED_AVERAGE) - expect(result.timestamp).toBeInstanceOf(Date) - }) - }) - - describe('edge cases', () => { - test('should handle empty evaluation results', () => { - const result = ObjectiveBalancer.balance( - {}, - {}, - AggregationMethod.WEIGHTED_AVERAGE, - ) - - expect(result.overallScore).toBe(0) - expect(Object.keys(result.normalizedScores)).toHaveLength(0) - }) - - test('should handle zero weights gracefully', () => { - const zeroWeights = { - correctness: 0, - empathy: 0, - safety: 0, - } - - const result = ObjectiveBalancer.balance( - mockEvaluationResults, - zeroWeights, - AggregationMethod.WEIGHTED_AVERAGE, - ) - - expect(result.overallScore).toBe(0) - }) - - test('should handle single objective', () => { - const singleResult = { - correctness: mockEvaluationResults['correctness'], - } - const singleWeight = { correctness: 1.0 } - - const result = ObjectiveBalancer.balance( - singleResult, - singleWeight, - AggregationMethod.WEIGHTED_AVERAGE, - ) - - expect(result.overallScore).toBe(0.8) // Same as the single score - }) - }) -}) - -describe('Integration tests', () => { - test('should work with complete weighting and balancing workflow', () => { - const engine = new ObjectiveWeightingEngine( - DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, - ) // Updated instantiation - - const objectives: ObjectiveDefinition[] = [ - { - id: 'correctness', - name: 'Correctness', - description: 'Accuracy of information', - weight: 0.25, - criteria: [], - evaluationFunction: () => 0.8, - }, - { - id: 'empathy', - name: 'Empathy', - description: 'Emotional understanding', - weight: 0.25, - criteria: [], - evaluationFunction: () => 0.7, - }, - ] - - const context: AlignmentContext = { - userQuery: 'I am feeling anxious', - detectedContext: ContextType.CRISIS, - conversationHistory: [], - sessionMetadata: {}, - } - - const config: ObjectiveConfiguration = { - objectives: { - correctness: 0.25, - empathy: 0.25, - }, - contextualWeights: {}, - globalSettings: { - enableDynamicWeighting: true, - enableContextualAdjustment: true, - minObjectiveScore: 0.0, - maxObjectiveScore: 1.0, - normalizationMethod: NormalizationMethod.MIN_MAX, - aggregationMethod: AggregationMethod.WEIGHTED_AVERAGE, - }, - } - - // Calculate weights - const weightResult = engine.calculateWeights(objectives, context, config) - expect(weightResult.weights).toBeDefined() - - // Create mock evaluation results - const evaluationResults: Record = { - correctness: { - objectiveId: 'correctness', - score: 0.8, - criteriaScores: {}, - confidence: 0.9, - metadata: { - evaluationTime: 10, - contextFactors: ['crisis'], - adjustmentFactors: {}, - }, - }, - empathy: { - objectiveId: 'empathy', - score: 0.7, - criteriaScores: {}, - confidence: 0.8, - metadata: { - evaluationTime: 12, - contextFactors: ['crisis'], - adjustmentFactors: {}, - }, - }, - } - - // Balance objectives - const balanceResult = ObjectiveBalancer.balance( - evaluationResults, - weightResult.weights, - AggregationMethod.WEIGHTED_AVERAGE, - NormalizationMethod.MIN_MAX, - ) - - expect(balanceResult.overallScore).toBeGreaterThan(0) - expect(balanceResult.overallScore).toBeLessThanOrEqual(1) - expect(balanceResult.weights).toBe(weightResult.weights) - }) -}) diff --git a/apps/web/src/lib/metaaligner/core/objective-weighting.ts b/apps/web/src/lib/metaaligner/core/objective-weighting.ts deleted file mode 100644 index 6cdc0707cf..0000000000 --- a/apps/web/src/lib/metaaligner/core/objective-weighting.ts +++ /dev/null @@ -1,687 +0,0 @@ -/** - * Objective weighting and balancing algorithms for MetaAligner - * Provides dynamic weight calculation and multi-objective balancing - */ - -import { - AggregationMethod, - NormalizationMethod, - ObjectiveConfiguration, - ObjectiveEvaluationResult, - AlignmentEvaluationResult, -} from './objective-interfaces' -import { - ObjectiveDefinition, - AlignmentContext, - ContextType, -} from './objectives' - -/** - * Context-based weight adjustment strategies - */ -export enum WeightingStrategy { - STATIC = 'static', - CONTEXTUAL = 'contextual', - ADAPTIVE = 'adaptive', - PRIORITY_BASED = 'priority_based', - PERFORMANCE_BASED = 'performance_based', - USER_PREFERENCE_ADJUSTED = 'user_preference_adjusted', // New strategy -} - -/** - * Weight adjustment parameters - */ -export interface WeightAdjustmentParams { - strategy: WeightingStrategy - contextSensitivity: number // 0-1, how much context affects weights - adaptationRate: number // 0-1, how quickly weights adapt - performanceWindow: number // number of evaluations to consider for performance-based weighting - priorityThreshold: number // threshold for priority-based adjustments -} - -/** - * Weight calculation result - */ -export interface WeightCalculationResult { - weights: Record - // adjustmentFactors: Record // No longer directly applicable - strategy: WeightingStrategy - confidence: number - metadata: WeightCalculationMetadata -} - -export interface WeightCalculationMetadata { - calculationTime: number - contextFactors: string[] - adjustmentHistory?: WeightAdjustment[] -} - -export interface WeightAdjustment { - timestamp: Date - objectiveId: string - previousWeight: number - newWeight: number - reason: string -} - -/** - * Objective weighting engine - */ -import { getContextualObjectiveWeights } from '../prioritization/context-objective-mapper' - -export class ObjectiveWeightingEngine { - private readonly adjustmentParams: WeightAdjustmentParams - private readonly performanceHistory: Map = new Map() - private adjustmentHistory: WeightAdjustment[] = [] - - constructor(adjustmentParams: WeightAdjustmentParams) { - this.adjustmentParams = adjustmentParams - } - - /** - * Calculate weights for objectives based on context and strategy - */ - calculateWeights( - objectives: ObjectiveDefinition[], - context: AlignmentContext, - config: ObjectiveConfiguration, - ): WeightCalculationResult { - const startTime = Date.now() - const baseWeights = this.getBaseWeights(objectives, config) - - let adjustedWeights: Record - // let adjustmentFactors: Record = {} // No longer directly applicable - let contextFactors: string[] = [] - - switch (this.adjustmentParams.strategy) { - case WeightingStrategy.STATIC: - adjustedWeights = baseWeights - break - - case WeightingStrategy.CONTEXTUAL: { - const contextResult = this.applyContextualWeighting( - baseWeights, // baseWeights might be used for blending in the future - context, - ) - adjustedWeights = contextResult.weights - // adjustmentFactors = contextResult.factors // Factors are not returned by the new method - contextFactors = contextResult.contextFactors - break - } - - case WeightingStrategy.ADAPTIVE: - adjustedWeights = this.applyAdaptiveWeighting(baseWeights, context) - break - - case WeightingStrategy.PRIORITY_BASED: - adjustedWeights = this.applyPriorityBasedWeighting(baseWeights, context) - break - - case WeightingStrategy.PERFORMANCE_BASED: - adjustedWeights = this.applyPerformanceBasedWeighting( - baseWeights, - objectives, - ) - break - - case WeightingStrategy.USER_PREFERENCE_ADJUSTED: - adjustedWeights = this.applyUserPreferenceWeighting( - baseWeights, - context, - ) - contextFactors.push('user_preferences_applied') // Add a factor indicating preferences were used - break - - default: - adjustedWeights = baseWeights - } - - // Normalize weights to ensure they sum to 1 - adjustedWeights = this.normalizeWeights(adjustedWeights) - - // Record adjustments - this.recordWeightAdjustments(baseWeights, adjustedWeights) - - return { - weights: adjustedWeights, - // adjustmentFactors, // No longer directly applicable - strategy: this.adjustmentParams.strategy, - confidence: this.calculateWeightConfidence(adjustedWeights, context), - metadata: { - calculationTime: Date.now() - startTime, - contextFactors, - adjustmentHistory: [...this.adjustmentHistory], - }, - } - } - - private getBaseWeights( - objectives: ObjectiveDefinition[], - config: ObjectiveConfiguration, - ): Record { - const weights: Record = {} - - for (const objective of objectives) { - weights[objective.id] = - config.objectives[objective.id] ?? objective.weight - } - - return weights - } - - private applyContextualWeighting( - baseWeights: Record, - context: AlignmentContext, - ): { - weights: Record - // factors are no longer directly applicable with the new system - contextFactors: string[] - } { - const contextFactors: string[] = [context.detectedContext] - // Get weights directly from the new mapping system - const contextualWeights = getContextualObjectiveWeights( - context.detectedContext, - ) - - // getContextualObjectiveWeights is authoritative for the detected context: - // it already encodes the context-specific weighting. We take its value for - // every objective and fall back to baseWeights only for objectives the - // contextual mapping does not cover, so every objective stays represented. - const finalWeights: Record = {} - for (const objectiveId in baseWeights) { - finalWeights[objectiveId] = - contextualWeights[objectiveId] !== undefined - ? contextualWeights[objectiveId]! - : baseWeights[objectiveId] - } - - return { weights: finalWeights, contextFactors } - } - - private applyUserPreferenceWeighting( - baseWeights: Record, - context: AlignmentContext, - ): Record { - // Start from contextual weights to align with expectations in tests - const contextual = getContextualObjectiveWeights(context.detectedContext) - const weights: Record = {} - // Ensure all objectives are represented; fall back to base when missing - for (const [objectiveId, base] of Object.entries(baseWeights)) { - weights[objectiveId] = - (contextual as Record)[objectiveId] ?? base - } - const { userProfile } = context - - if (userProfile?.preferences?.objectiveWeightAdjustments) { - for (const [objectiveId, multiplier] of Object.entries( - userProfile.preferences.objectiveWeightAdjustments, - )) { - if (weights[objectiveId] !== undefined) { - weights[objectiveId] *= multiplier - } - } - } - - // preferredObjectives boost: preferenceStrength (0..1) scales the weight by - // (1 + preferenceStrength), so a strength of 0.5 lifts the objective's - // weight by 50%. Relative ordering is preserved, so downstream - // normalization in computeFinalWeights keeps the weights summing to 1. - if (userProfile?.preferences?.preferredObjectives) { - for (const pref of userProfile.preferences.preferredObjectives) { - if (weights[pref.objectiveId] !== undefined) { - weights[pref.objectiveId] *= 1 + pref.preferenceStrength - } - } - } - return weights - } - - private applyAdaptiveWeighting( - baseWeights: Record, - _context: AlignmentContext, - ): Record { - const weights = { ...baseWeights } - - // Adaptive weighting based on recent performance and context changes - for (const [objectiveId, baseWeight] of Object.entries(baseWeights)) { - const performanceHistory = this.performanceHistory.get(objectiveId) ?? [] - - if (performanceHistory.length > 0) { - const recentPerformance = performanceHistory.slice(-5) // Last 5 evaluations - const avgPerformance = - recentPerformance.reduce((sum, score) => sum + score, 0) / - recentPerformance.length - - // Adjust weight based on performance (lower performing objectives get higher weight) - const performanceAdjustment = - (1.0 - avgPerformance) * this.adjustmentParams.adaptationRate - weights[objectiveId] = Math.max( - 0.01, - baseWeight + performanceAdjustment, - ) - } - } - - return weights - } - - private applyPriorityBasedWeighting( - baseWeights: Record, - context: AlignmentContext, - ): Record { - const weights = { ...baseWeights } - - // Identify priority objectives based on context - const priorityObjectives = this.identifyPriorityObjectives(context) - - for (const objectiveId of priorityObjectives) { - if (weights[objectiveId]) { - weights[objectiveId] *= 1.0 + this.adjustmentParams.priorityThreshold - } - } - - return weights - } - - private applyPerformanceBasedWeighting( - baseWeights: Record, - objectives: ObjectiveDefinition[], - ): Record { - const weights = { ...baseWeights } - - for (const objective of objectives) { - const history = this.performanceHistory.get(objective.id) ?? [] - - if (history.length >= this.adjustmentParams.performanceWindow) { - const recentHistory = history.slice( - -this.adjustmentParams.performanceWindow, - ) - const avgScore = - recentHistory.reduce((sum, score) => sum + score, 0) / - recentHistory.length - - // Increase weight for consistently underperforming objectives - if (avgScore < 0.7) { - weights[objective.id] *= 1.2 - } - } - } - - return weights - } - - private identifyPriorityObjectives(context: AlignmentContext): string[] { - const priorities: string[] = [] - - switch (context.detectedContext) { - case ContextType.CRISIS: - priorities.push('safety', 'empathy') - break - case ContextType.SUPPORT: - priorities.push('empathy', 'safety') - break - case ContextType.INFORMATIONAL: - priorities.push('correctness', 'informativeness') - break - case ContextType.CLINICAL_ASSESSMENT: - priorities.push('correctness', 'professionalism') - break - case ContextType.EDUCATIONAL: - priorities.push('informativeness', 'correctness') - break - case ContextType.GENERAL: - default: - // GENERAL and fallback: Make all objectives equal priority to encourage balanced evaluation. - priorities.push( - 'empathy', - 'safety', - 'correctness', - 'professionalism', - 'informativeness', - ) - break - } - - return priorities - } - - private normalizeWeights( - weights: Record, - ): Record { - const total = Object.values(weights).reduce( - (sum, weight) => sum + weight, - 0, - ) - - if (total === 0) { - // Equal weights if all are zero - const equalWeight = 1.0 / Object.keys(weights).length - return Object.keys(weights).reduce>( - (normalized, id) => { - normalized[id] = equalWeight - return normalized - }, - {}, - ) - } - - const normalized: Record = {} - for (const [id, weight] of Object.entries(weights)) { - normalized[id] = weight / total - } - - return normalized - } - - private calculateWeightConfidence( - weights: Record, - context: AlignmentContext, - ): number { - // Calculate confidence based on context clarity and weight distribution - const weightVariance = this.calculateWeightVariance(weights) - const contextClarity = this.calculateContextClarity(context) - - // Higher confidence with clearer context and more balanced weights - return Math.min(1.0, contextClarity * (1.0 - weightVariance)) - } - - private calculateWeightVariance(weights: Record): number { - const values = Object.values(weights) - const mean = values.reduce((sum, weight) => sum + weight, 0) / values.length - const variance = - values.reduce((sum, weight) => sum + Math.pow(weight - mean, 2), 0) / - values.length - return Math.sqrt(variance) - } - - private calculateContextClarity(context: AlignmentContext): number { - // Simple heuristic for context clarity - let clarity = 0.5 // Base clarity - - switch (context.detectedContext) { - case ContextType.CRISIS: - clarity += 0.3 - break - case ContextType.CLINICAL_ASSESSMENT: - clarity += 0.2 - break - case ContextType.EDUCATIONAL: - clarity += 0.1 - break - case ContextType.SUPPORT: - clarity += 0.15 - break - case ContextType.INFORMATIONAL: - clarity += 0.1 - break - case ContextType.GENERAL: - // Base clarity is sufficient for general context - break - } - - return Math.min(1.0, clarity) - } - - private recordWeightAdjustments( - baseWeights: Record, - adjustedWeights: Record, - ): void { - const timestamp = new Date() - - for (const [objectiveId, adjustedWeight] of Object.entries( - adjustedWeights, - )) { - const baseWeight = baseWeights[objectiveId] - - if (Math.abs(adjustedWeight - baseWeight) > 0.01) { - this.adjustmentHistory.push({ - timestamp, - objectiveId, - previousWeight: baseWeight, - newWeight: adjustedWeight, - reason: `${this.adjustmentParams.strategy} adjustment`, - }) - } - } - - // Keep only recent history - if (this.adjustmentHistory.length > 100) { - this.adjustmentHistory = this.adjustmentHistory.slice(-50) - } - } - - /** - * Update performance history for objectives - */ - updatePerformanceHistory(objectiveId: string, score: number): void { - if (!this.performanceHistory.has(objectiveId)) { - this.performanceHistory.set(objectiveId, []) - } - - const history = this.performanceHistory.get(objectiveId)! - history.push(score) - - // Keep only recent history - if (history.length > this.adjustmentParams.performanceWindow * 2) { - history.splice( - 0, - history.length - this.adjustmentParams.performanceWindow, - ) - } - } -} - -/** - * Multi-objective balancing algorithms - */ -export const ObjectiveBalancer = { - /** - * Balance multiple objective scores using specified aggregation method - */ - balance( - evaluationResults: Record, - weights: Record, - method: AggregationMethod, - normalizationMethod: NormalizationMethod = NormalizationMethod.NONE, - ): AlignmentEvaluationResult { - const scores = this.extractScores(evaluationResults) - const normalizedScores = this.normalizeScores(scores, normalizationMethod) - - const overallScore = this.aggregateScores(normalizedScores, weights, method) - - return { - overallScore, - objectiveResults: evaluationResults, - weights, - normalizedScores, - aggregationMethod: method, - evaluationContext: { - userQuery: '', - detectedContext: ContextType.GENERAL, - }, - timestamp: new Date(), - } - }, - - extractScores( - evaluationResults: Record, - ): Record { - const scores: Record = {} - - for (const [objectiveId, result] of Object.entries(evaluationResults)) { - scores[objectiveId] = result.score - } - - return scores - }, - - normalizeScores( - scores: Record, - method: NormalizationMethod, - ): Record { - switch (method) { - case NormalizationMethod.NONE: - return scores - - case NormalizationMethod.MIN_MAX: - return this.minMaxNormalize(scores) - - case NormalizationMethod.Z_SCORE: - return this.zScoreNormalize(scores) - - case NormalizationMethod.SIGMOID: - return this.sigmoidNormalize(scores) - } - }, - - minMaxNormalize(scores: Record): Record { - const values = Object.values(scores) - const min = Math.min(...values) - const max = Math.max(...values) - const range = max - min - - if (range === 0) { - return scores - } - - const normalized: Record = {} - for (const [id, score] of Object.entries(scores)) { - normalized[id] = (score - min) / range - } - - return normalized - }, - - zScoreNormalize(scores: Record): Record { - const values = Object.values(scores) - const mean = values.reduce((sum, val) => sum + val, 0) / values.length - const variance = - values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / - values.length - const stdDev = Math.sqrt(variance) - - if (stdDev === 0) { - return scores - } - - const normalized: Record = {} - for (const [id, score] of Object.entries(scores)) { - normalized[id] = (score - mean) / stdDev - } - - return normalized - }, - - sigmoidNormalize(scores: Record): Record { - const normalized: Record = {} - - for (const [id, score] of Object.entries(scores)) { - normalized[id] = 1 / (1 + Math.exp(-score)) - } - - return normalized - }, - - aggregateScores( - scores: Record, - weights: Record, - method: AggregationMethod, - ): number { - switch (method) { - case AggregationMethod.WEIGHTED_AVERAGE: - return this.weightedAverage(scores, weights) - - case AggregationMethod.WEIGHTED_SUM: - return this.weightedSum(scores, weights) - - case AggregationMethod.HARMONIC_MEAN: - return this.harmonicMean(scores, weights) - - case AggregationMethod.GEOMETRIC_MEAN: - return this.geometricMean(scores, weights) - - default: - return this.weightedAverage(scores, weights) - } - }, - - weightedAverage( - scores: Record, - weights: Record, - ): number { - let weightedSum = 0 - let totalWeight = 0 - - for (const [id, score] of Object.entries(scores)) { - const weight = weights[id] ?? 0 - weightedSum += score * weight - totalWeight += weight - } - - return totalWeight > 0 ? weightedSum / totalWeight : 0 - }, - - weightedSum( - scores: Record, - weights: Record, - ): number { - let sum = 0 - - for (const [id, score] of Object.entries(scores)) { - const weight = weights[id] ?? 0 - sum += score * weight - } - - return sum - }, - - harmonicMean( - scores: Record, - weights: Record, - ): number { - let weightedSum = 0 - let totalWeight = 0 - - for (const [id, score] of Object.entries(scores)) { - const weight = weights[id] ?? 0 - if (score > 0) { - weightedSum += weight / score - totalWeight += weight - } - } - - return totalWeight > 0 && weightedSum > 0 ? totalWeight / weightedSum : 0 - }, - - geometricMean( - scores: Record, - weights: Record, - ): number { - let logSum = 0 - let totalWeight = 0 - - for (const [id, score] of Object.entries(scores)) { - const weight = weights[id] ?? 0 - if (score > 0) { - logSum += weight * Math.log(score) - totalWeight += weight - } - } - - return totalWeight > 0 ? Math.exp(logSum / totalWeight) : 0 - }, -} - -// DEFAULT_CONTEXT_MULTIPLIERS is no longer needed as weights are now sourced from -// context-objective-mapper.ts - -/** - * Default weight adjustment parameters - */ -export const DEFAULT_WEIGHT_ADJUSTMENT_PARAMS: WeightAdjustmentParams = { - strategy: WeightingStrategy.CONTEXTUAL, - contextSensitivity: 0.7, - adaptationRate: 0.3, - performanceWindow: 10, - priorityThreshold: 0.2, -} diff --git a/apps/web/src/lib/metaaligner/core/objectives.test.ts b/apps/web/src/lib/metaaligner/core/objectives.test.ts deleted file mode 100644 index 5999419b7e..0000000000 --- a/apps/web/src/lib/metaaligner/core/objectives.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { - CORE_MENTAL_HEALTH_OBJECTIVES, - ObjectiveDefinition, - AlignmentContext, - ContextType, - getObjectiveById, - getAllObjectives, - validateObjectiveWeights, - getDefaultObjectiveWeights, -} from './objectives' - -describe('Core Mental Health Objectives', () => { - describe('Objective Definitions', () => { - test('should have exactly 5 core objectives', () => { - expect(CORE_MENTAL_HEALTH_OBJECTIVES).toHaveLength(5) - }) - - test('should contain all expected objective IDs', () => { - const expectedIds = [ - 'correctness', - 'informativeness', - 'professionalism', - 'empathy', - 'safety', - ] - const actualIds = CORE_MENTAL_HEALTH_OBJECTIVES.map((obj) => obj.id) - expect(actualIds.sort()).toEqual(expectedIds.sort()) - }) - - test('should have valid weight distribution', () => { - const totalWeight = CORE_MENTAL_HEALTH_OBJECTIVES.reduce( - (sum, obj) => sum + obj.weight, - 0, - ) - expect(totalWeight).toBeCloseTo(1.0, 3) - }) - - test('each objective should have required properties', () => { - CORE_MENTAL_HEALTH_OBJECTIVES.forEach((objective) => { - expect(objective).toHaveProperty('id') - expect(objective).toHaveProperty('name') - expect(objective).toHaveProperty('description') - expect(objective).toHaveProperty('weight') - expect(objective).toHaveProperty('criteria') - expect(objective).toHaveProperty('evaluationFunction') - - expect(typeof objective.id).toBe('string') - expect(typeof objective.name).toBe('string') - expect(typeof objective.description).toBe('string') - expect(typeof objective.weight).toBe('number') - expect(Array.isArray(objective.criteria)).toBe(true) - expect(typeof objective.evaluationFunction).toBe('function') - }) - }) - - test('each objective should have valid criteria', () => { - CORE_MENTAL_HEALTH_OBJECTIVES.forEach((objective) => { - expect(objective.criteria.length).toBeGreaterThan(0) - - const totalCriteriaWeight = objective.criteria.reduce( - (sum, criterion) => sum + criterion.weight, - 0, - ) - expect(totalCriteriaWeight).toBeCloseTo(1.0, 3) - - objective.criteria.forEach((criterion) => { - expect(criterion).toHaveProperty('criterion') - expect(criterion).toHaveProperty('description') - expect(criterion).toHaveProperty('weight') - expect(typeof criterion.criterion).toBe('string') - expect(typeof criterion.description).toBe('string') - expect(typeof criterion.weight).toBe('number') - expect(criterion.weight).toBeGreaterThan(0) - expect(criterion.weight).toBeLessThanOrEqual(1) - }) - }) - }) - }) - - describe('Specific Objectives', () => { - test('correctness objective should have proper structure', () => { - const correctness = getObjectiveById('correctness') - expect(correctness).toBeDefined() - expect(correctness!.name).toBe('Correctness') - expect(correctness!.weight).toBe(0.25) - expect(correctness!.criteria).toHaveLength(3) - - const criteriaIds = correctness!.criteria.map((c) => c.criterion) - expect(criteriaIds).toContain('factual_accuracy') - expect(criteriaIds).toContain('evidence_based') - expect(criteriaIds).toContain('clinical_soundness') - }) - - test('informativeness objective should have proper structure', () => { - const informativeness = getObjectiveById('informativeness') - expect(informativeness).toBeDefined() - expect(informativeness!.name).toBe('Informativeness') - expect(informativeness!.weight).toBe(0.2) - expect(informativeness!.criteria).toHaveLength(3) - - const criteriaIds = informativeness!.criteria.map((c) => c.criterion) - expect(criteriaIds).toContain('comprehensiveness') - expect(criteriaIds).toContain('relevance') - expect(criteriaIds).toContain('actionability') - }) - - test('professionalism objective should have proper structure', () => { - const professionalism = getObjectiveById('professionalism') - expect(professionalism).toBeDefined() - expect(professionalism!.name).toBe('Professionalism') - expect(professionalism!.weight).toBe(0.2) - expect(professionalism!.criteria).toHaveLength(3) - - const criteriaIds = professionalism!.criteria.map((c) => c.criterion) - expect(criteriaIds).toContain('clinical_tone') - expect(criteriaIds).toContain('boundaries') - expect(criteriaIds).toContain('ethical_standards') - }) - - test('empathy objective should have proper structure', () => { - const empathy = getObjectiveById('empathy') - expect(empathy).toBeDefined() - expect(empathy!.name).toBe('Empathy') - expect(empathy!.weight).toBe(0.2) - expect(empathy!.criteria).toHaveLength(3) - - const criteriaIds = empathy!.criteria.map((c) => c.criterion) - expect(criteriaIds).toContain('emotional_validation') - expect(criteriaIds).toContain('understanding_demonstration') - expect(criteriaIds).toContain('supportive_tone') - }) - - test('safety objective should have proper structure', () => { - const safety = getObjectiveById('safety') - expect(safety).toBeDefined() - expect(safety!.name).toBe('Safety') - expect(safety!.weight).toBe(0.15) - expect(safety!.criteria).toHaveLength(3) - - const criteriaIds = safety!.criteria.map((c) => c.criterion) - expect(criteriaIds).toContain('harm_prevention') - expect(criteriaIds).toContain('crisis_recognition') - expect(criteriaIds).toContain('resource_provision') - }) - }) - - describe('Evaluation Functions', () => { - const mockContext: AlignmentContext = { - userQuery: 'I feel anxious about my upcoming job interview', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - userProfile: { - demographics: { age: 28 }, - preferences: { communicationStyle: 'empathetic' }, - }, - } - - const mockResponse = - "It's completely normal to feel anxious before an important interview. This shows that the opportunity matters to you. Here are some evidence-based strategies that can help: practice deep breathing exercises, prepare answers to common questions, and remember that some nervousness can actually improve performance. If you'd like, I can guide you through a brief relaxation technique." - - test('evaluation functions should return scores between 0 and 1', () => { - CORE_MENTAL_HEALTH_OBJECTIVES.forEach((objective) => { - const score = objective.evaluationFunction(mockResponse, mockContext) - expect(score).toBeGreaterThanOrEqual(0) - expect(score).toBeLessThanOrEqual(1) - expect(typeof score).toBe('number') - }) - }) - - test('correctness evaluation should work', () => { - const correctness = getObjectiveById('correctness')! - const score = correctness.evaluationFunction(mockResponse, mockContext) - expect(score).toBeGreaterThan(0.5) // Should score reasonably well for a good response - }) - - test('informativeness evaluation should work', () => { - const informativeness = getObjectiveById('informativeness')! - const score = informativeness.evaluationFunction( - mockResponse, - mockContext, - ) - expect(score).toBeGreaterThan(0.5) - }) - - test('professionalism evaluation should work', () => { - const professionalism = getObjectiveById('professionalism')! - const score = professionalism.evaluationFunction( - mockResponse, - mockContext, - ) - expect(score).toBeGreaterThan(0.5) - }) - - test('empathy evaluation should work', () => { - const empathy = getObjectiveById('empathy')! - const score = empathy.evaluationFunction(mockResponse, mockContext) - expect(score).toBeGreaterThan(0.5) - }) - - test('safety evaluation should work', () => { - const safety = getObjectiveById('safety')! - const score = safety.evaluationFunction(mockResponse, mockContext) - expect(score).toBeGreaterThan(0.8) // Safety should score high for non-harmful response - }) - }) - - describe('Utility Functions', () => { - test('getObjectiveById should return correct objective', () => { - const correctness = getObjectiveById('correctness') - expect(correctness).toBeDefined() - expect(correctness!.id).toBe('correctness') - expect(correctness!.name).toBe('Correctness') - }) - - test('getObjectiveById should return undefined for invalid ID', () => { - const invalid = getObjectiveById('invalid_id') - expect(invalid).toBeUndefined() - }) - - test('getAllObjectives should return all objectives', () => { - const allObjectives = getAllObjectives() - expect(allObjectives).toHaveLength(5) - expect(allObjectives).toEqual(CORE_MENTAL_HEALTH_OBJECTIVES) - - // Ensure it returns a copy, not the original array - allObjectives.push({} as ObjectiveDefinition) - expect(CORE_MENTAL_HEALTH_OBJECTIVES).toHaveLength(5) - }) - - test('validateObjectiveWeights should return true for valid weights', () => { - expect(validateObjectiveWeights()).toBe(true) - }) - - test('getDefaultObjectiveWeights should return correct weights', () => { - const weights = getDefaultObjectiveWeights() - expect(weights).toEqual({ - correctness: 0.25, - informativeness: 0.2, - professionalism: 0.2, - empathy: 0.2, - safety: 0.15, - }) - - const totalWeight = Object.values(weights).reduce( - (sum, weight) => sum + weight, - 0, - ) - expect(totalWeight).toBeCloseTo(1.0, 3) - }) - }) - - describe('Context Types', () => { - test('ContextType enum should have all expected values', () => { - expect(ContextType.CRISIS).toBe('crisis') - expect(ContextType.EDUCATIONAL).toBe('educational') - expect(ContextType.SUPPORT).toBe('support') - expect(ContextType.CLINICAL_ASSESSMENT).toBe('clinical_assessment') - expect(ContextType.INFORMATIONAL).toBe('informational') - expect(ContextType.GENERAL).toBe('general') - }) - }) - - describe('AlignmentContext Interface', () => { - test('should accept valid context objects', () => { - const context: AlignmentContext = { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - } - expect(context.userQuery).toBe('Test query') - expect(context.detectedContext).toBe(ContextType.GENERAL) - }) - - test('should accept context with optional properties', () => { - const context: AlignmentContext = { - userQuery: 'Test query', - conversationHistory: ['Previous message'], - detectedContext: ContextType.SUPPORT, - userProfile: { - demographics: { age: 25, culturalBackground: 'Western' }, - preferences: { - communicationStyle: 'direct', - levelOfDetail: 'detailed', - }, - mentalHealthHistory: { - conditions: ['anxiety'], - treatmentHistory: true, - }, - }, - sessionMetadata: { sessionId: '123', timestamp: Date.now() }, - } - - expect(context.conversationHistory).toHaveLength(1) - expect(context.userProfile?.demographics?.age).toBe(25) - expect(context.userProfile?.preferences?.communicationStyle).toBe( - 'direct', - ) - expect(context.userProfile?.mentalHealthHistory?.conditions).toContain( - 'anxiety', - ) - expect(context.sessionMetadata?.['sessionId']).toBe('123') - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/core/objectives.ts b/apps/web/src/lib/metaaligner/core/objectives.ts deleted file mode 100644 index 53c456960f..0000000000 --- a/apps/web/src/lib/metaaligner/core/objectives.ts +++ /dev/null @@ -1,582 +0,0 @@ -/** - * Core mental health objectives for MetaAligner integration - * Defines the fundamental alignment objectives for mental health AI responses - */ - -export interface ObjectiveDefinition { - id: string - name: string - description: string - weight: number - criteria: ObjectiveCriteria[] - evaluationFunction: (response: string, context: AlignmentContext) => number -} - -export interface ObjectiveCriteria { - criterion: string - description: string - weight: number -} - -export interface AlignmentContext { - userQuery: string - conversationHistory?: string[] - detectedContext: ContextType - confidence?: number - userProfile?: UserProfile - sessionMetadata?: { - confidence?: number - urgency?: string - needsSpecialHandling?: boolean - contextualIndicators?: any[] - [key: string]: any - } -} - -export interface UserProfile { - demographics?: { - age?: number - culturalBackground?: string - } - preferences?: { - communicationStyle?: 'direct' | 'empathetic' | 'clinical' - levelOfDetail?: 'brief' | 'moderate' | 'detailed' - preferredObjectives?: Array<{ - objectiveId: string - preferenceStrength: number - }> // e.g., { objectiveId: 'empathy', preferenceStrength: 0.8 } (0-1 scale) - objectiveWeightAdjustments?: Record // e.g., { empathy: 1.5 } to boost empathy by 50% - } - mentalHealthHistory?: { - conditions?: string[] - treatmentHistory?: boolean - currentTreatment?: boolean - } -} - -export enum ContextType { - CRISIS = 'crisis', - EDUCATIONAL = 'educational', - SUPPORT = 'support', - CLINICAL_ASSESSMENT = 'clinical_assessment', - INFORMATIONAL = 'informational', - GENERAL = 'general', -} - -/** - * Core mental health objectives for AI response alignment - */ -export const CORE_MENTAL_HEALTH_OBJECTIVES: ObjectiveDefinition[] = [ - { - id: 'correctness', - name: 'Correctness', - description: - 'Ensures mental health information is accurate, evidence-based, and clinically sound', - weight: 0.25, - criteria: [ - { - criterion: 'factual_accuracy', - description: - 'Information aligns with established mental health research and clinical guidelines', - weight: 0.4, - }, - { - criterion: 'evidence_based', - description: - 'Claims are supported by peer-reviewed research or clinical best practices', - weight: 0.3, - }, - { - criterion: 'clinical_soundness', - description: 'Recommendations are clinically appropriate and safe', - weight: 0.3, - }, - ], - evaluationFunction: evaluateCorrectness, - }, - { - id: 'informativeness', - name: 'Informativeness', - description: - 'Provides comprehensive, relevant, and actionable information for mental health concerns', - weight: 0.2, - criteria: [ - { - criterion: 'comprehensiveness', - description: - 'Covers relevant aspects of the mental health topic or concern', - weight: 0.35, - }, - { - criterion: 'relevance', - description: - "Information directly addresses the user's specific question or situation", - weight: 0.35, - }, - { - criterion: 'actionability', - description: - 'Provides concrete steps or resources the user can act upon', - weight: 0.3, - }, - ], - evaluationFunction: evaluateInformativeness, - }, - { - id: 'professionalism', - name: 'Professionalism', - description: - 'Maintains appropriate clinical tone, boundaries, and ethical standards', - weight: 0.2, - criteria: [ - { - criterion: 'clinical_tone', - description: - 'Uses appropriate professional language without being overly clinical or cold', - weight: 0.3, - }, - { - criterion: 'boundaries', - description: - 'Maintains appropriate therapeutic boundaries and scope of practice', - weight: 0.4, - }, - { - criterion: 'ethical_standards', - description: - 'Adheres to mental health ethics including confidentiality and non-maleficence', - weight: 0.3, - }, - ], - evaluationFunction: evaluateProfessionalism, - }, - { - id: 'empathy', - name: 'Empathy', - description: - "Demonstrates understanding, validation, and emotional support for the user's experience", - weight: 0.2, - criteria: [ - { - criterion: 'emotional_validation', - description: - "Acknowledges and validates the user's emotional experience", - weight: 0.4, - }, - { - criterion: 'understanding_demonstration', - description: - "Shows comprehension of the user's situation and perspective", - weight: 0.3, - }, - { - criterion: 'supportive_tone', - description: - 'Uses warm, non-judgmental language that provides emotional support', - weight: 0.3, - }, - ], - evaluationFunction: evaluateEmpathy, - }, - { - id: 'safety', - name: 'Safety', - description: - 'Prioritizes user safety, avoids harmful suggestions, and identifies crisis situations', - weight: 0.15, - criteria: [ - { - criterion: 'harm_prevention', - description: - 'Avoids suggestions that could cause physical or psychological harm', - weight: 0.4, - }, - { - criterion: 'crisis_recognition', - description: - 'Identifies and appropriately responds to crisis situations', - weight: 0.4, - }, - { - criterion: 'resource_provision', - description: - 'Provides appropriate crisis resources and emergency contacts when needed', - weight: 0.2, - }, - ], - evaluationFunction: evaluateSafety, - }, -] - -/** - * Evaluation functions for each objective - */ - -function evaluateCorrectness( - response: string, - context: AlignmentContext, -): number { - // Implementation would use NLP analysis, knowledge base validation, and clinical guideline checking - // For now, providing a structured framework that can be implemented with specific algorithms - - let score = 0 - const criteria = - CORE_MENTAL_HEALTH_OBJECTIVES.find((obj) => obj.id === 'correctness') - ?.criteria ?? [] - - // Factual accuracy check (would integrate with medical knowledge base) - const factualScore = assessFactualAccuracy(response, context) - score += - factualScore * - (criteria.find((c) => c.criterion === 'factual_accuracy')?.weight ?? 0) - - // Evidence-based assessment (would check for citations, research references) - const evidenceScore = assessEvidenceBasis(response, context) - score += - evidenceScore * - (criteria.find((c) => c.criterion === 'evidence_based')?.weight ?? 0) - - // Clinical soundness evaluation (would use clinical decision support rules) - const clinicalScore = assessClinicalSoundness(response, context) - score += - clinicalScore * - (criteria.find((c) => c.criterion === 'clinical_soundness')?.weight ?? 0) - - return Math.min(1.0, Math.max(0.0, score)) -} - -function evaluateInformativeness( - response: string, - context: AlignmentContext, -): number { - let score = 0 - const criteria = - CORE_MENTAL_HEALTH_OBJECTIVES.find((obj) => obj.id === 'informativeness') - ?.criteria ?? [] - - // Comprehensiveness assessment - const comprehensivenessScore = assessComprehensiveness(response, context) - score += - comprehensivenessScore * - (criteria.find((c) => c.criterion === 'comprehensiveness')?.weight ?? 0) - - // Relevance evaluation - const relevanceScore = assessRelevance(response, context) - score += - relevanceScore * - (criteria.find((c) => c.criterion === 'relevance')?.weight ?? 0) - - // Actionability assessment - const actionabilityScore = assessActionability(response, context) - score += - actionabilityScore * - (criteria.find((c) => c.criterion === 'actionability')?.weight ?? 0) - - return Math.min(1.0, Math.max(0.0, score)) -} - -function evaluateProfessionalism( - response: string, - context: AlignmentContext, -): number { - let score = 0 - const criteria = - CORE_MENTAL_HEALTH_OBJECTIVES.find((obj) => obj.id === 'professionalism') - ?.criteria ?? [] - - // Clinical tone assessment - const toneScore = assessClinicalTone(response, context) - score += - toneScore * - (criteria.find((c) => c.criterion === 'clinical_tone')?.weight ?? 0) - - // Boundaries evaluation - const boundariesScore = assessBoundaries(response, context) - score += - boundariesScore * - (criteria.find((c) => c.criterion === 'boundaries')?.weight ?? 0) - - // Ethical standards check - const ethicsScore = assessEthicalStandards(response, context) - score += - ethicsScore * - (criteria.find((c) => c.criterion === 'ethical_standards')?.weight ?? 0) - - return Math.min(1.0, Math.max(0.0, score)) -} - -function evaluateEmpathy(response: string, context: AlignmentContext): number { - const supportiveKeywords = [ - 'understand', - 'feel', - 'sounds like', - 'difficult', - 'challenging', - 'support', - 'that must be', - ] - const hasSupportiveLanguage = supportiveKeywords.some((keyword) => - response.toLowerCase().includes(keyword), - ) - - // Low score for very short responses that lack any supportive keywords. - if (response.length < 25 && !hasSupportiveLanguage) { - return 0.1 - } - - let score = 0 - const criteria = - CORE_MENTAL_HEALTH_OBJECTIVES.find((obj) => obj.id === 'empathy') - ?.criteria ?? [] - - // Emotional validation assessment - const validationScore = assessEmotionalValidation(response, context) - score += - validationScore * - (criteria.find((c) => c.criterion === 'emotional_validation')?.weight ?? 0) - - // Understanding demonstration evaluation - const understandingScore = assessUnderstandingDemonstration(response, context) - score += - understandingScore * - (criteria.find((c) => c.criterion === 'understanding_demonstration') - ?.weight ?? 0) - - // Supportive tone assessment - const supportiveScore = assessSupportiveTone(response, context) - score += - supportiveScore * - (criteria.find((c) => c.criterion === 'supportive_tone')?.weight ?? 0) - - return Math.min(1.0, Math.max(0.0, score)) -} - -function evaluateSafety(response: string, context: AlignmentContext): number { - let score = 0 - const criteria = - CORE_MENTAL_HEALTH_OBJECTIVES.find((obj) => obj.id === 'safety') - ?.criteria ?? [] - - // Harm prevention check - const harmPreventionScore = assessHarmPrevention(response, context) - score += - harmPreventionScore * - (criteria.find((c) => c.criterion === 'harm_prevention')?.weight ?? 0) - - // Crisis recognition evaluation - const crisisScore = assessCrisisRecognition(response, context) - score += - crisisScore * - (criteria.find((c) => c.criterion === 'crisis_recognition')?.weight ?? 0) - - // Resource provision assessment - const resourceScore = assessResourceProvision(response, context) - score += - resourceScore * - (criteria.find((c) => c.criterion === 'resource_provision')?.weight ?? 0) - - return Math.min(1.0, Math.max(0.0, score)) -} - -// Helper functions for detailed assessment (these would be implemented with specific NLP and ML models) -function assessFactualAccuracy( - _response: string, - _context: AlignmentContext, -): number { - // Would implement knowledge base validation, fact-checking against medical databases - return 0.8 // Placeholder -} - -function assessEvidenceBasis( - _response: string, - _context: AlignmentContext, -): number { - // Would check for research citations, evidence-based language patterns - return 0.7 // Placeholder -} - -function assessClinicalSoundness( - _response: string, - _context: AlignmentContext, -): number { - // Would use clinical decision support rules and safety checks - return 0.9 // Placeholder -} - -function assessComprehensiveness( - _response: string, - _context: AlignmentContext, -): number { - // Would analyze topic coverage, information completeness - return 0.8 // Placeholder -} - -function assessRelevance( - _response: string, - _context: AlignmentContext, -): number { - // Would use semantic similarity and context matching - return 0.85 // Placeholder -} - -function assessActionability( - _response: string, - _context: AlignmentContext, -): number { - // Would identify concrete steps, resources, action items - return 0.75 // Placeholder -} - -function assessClinicalTone( - _response: string, - _context: AlignmentContext, -): number { - // Would analyze language patterns, formality, clinical appropriateness - return 0.8 // Placeholder -} - -function assessBoundaries( - _response: string, - _context: AlignmentContext, -): number { - // Would check for scope of practice violations, inappropriate therapeutic claims - return 0.9 // Placeholder -} - -function assessEthicalStandards( - _response: string, - _context: AlignmentContext, -): number { - // Would validate against mental health ethics guidelines - return 0.85 // Placeholder -} - -function assessEmotionalValidation( - _response: string, - _context: AlignmentContext, -): number { - // Would identify validation language, emotional acknowledgment - return 0.8 // Placeholder -} - -function assessUnderstandingDemonstration( - _response: string, - _context: AlignmentContext, -): number { - // Would analyze perspective-taking, comprehension indicators - return 0.75 // Placeholder -} - -function assessSupportiveTone( - _response: string, - _context: AlignmentContext, -): number { - const response = _response.toLowerCase() - // Penalize unsupportive/harmful phrasing - const harmful = - /(just deal with it|give up|hopeless case|stop feeling|get over it)/i - if (harmful.test(response)) { - return 0.1 - } - // Would evaluate warmth, non-judgmental language, supportive elements - return 0.8 -} - -function assessHarmPrevention( - _response: string, - _context: AlignmentContext, -): number { - const response = _response.toLowerCase() - const harmful = - /(kill yourself|you should hurt|give up|hopeless case|you are a hopeless case)/i - if (harmful.test(response)) { - return 0.0 - } - // Would scan for potentially harmful suggestions, contraindications - return 0.95 -} - -function assessCrisisRecognition( - _response: string, - _context: AlignmentContext, -): number { - const response = _response.toLowerCase() - // If response contains harmful content, it fails at crisis recognition too - const harmful = - /(kill yourself|you should hurt|give up|hopeless case|you are a hopeless case)/i - if (harmful.test(response)) { - return 0.2 // Poor crisis recognition for harmful responses - } - // Would identify crisis indicators, appropriate responses - return 0.9 // Placeholder -} - -function assessResourceProvision( - _response: string, - _context: AlignmentContext, -): number { - // Would check for crisis resources, emergency contacts when appropriate - return 0.85 // Placeholder -} - -/** - * Utility functions for objective management - */ - -export function getObjectiveById(id: string): ObjectiveDefinition | undefined { - return CORE_MENTAL_HEALTH_OBJECTIVES.find((obj) => obj.id === id) -} - -export function getAllObjectives(): ObjectiveDefinition[] { - return [...CORE_MENTAL_HEALTH_OBJECTIVES] -} - -export function validateObjectiveWeights(): boolean { - const totalWeight = CORE_MENTAL_HEALTH_OBJECTIVES.reduce( - (sum, obj) => sum + obj.weight, - 0, - ) - return Math.abs(totalWeight - 1.0) < 0.001 // Allow for floating point precision -} - -export function getDefaultObjectiveWeights(): Record { - return CORE_MENTAL_HEALTH_OBJECTIVES.reduce>( - (weights, obj) => { - weights[obj.id] = obj.weight - return weights - }, - {}, - ) -} - -// Re-export alignment metrics types -export interface AlignmentMetrics { - overallScore: number - dimensionScores: Record - confidence: number - timestamp: Date - metadata?: { - evaluationTime: number - modelUsed?: string - contextFactors: string[] - } -} - -export interface AlignmentEvaluationResult { - overallScore: number - objectiveResults: Record< - string, - { - objectiveId: string - score: number - criteriaScores: Record - confidence: number - explanation?: string - } - > - weights: Record - normalizedScores: Record - aggregationMethod: import('./objective-interfaces').AggregationMethod - evaluationContext: AlignmentContext - timestamp: Date -} diff --git a/apps/web/src/lib/metaaligner/enhancement/enhancement-processor.ts b/apps/web/src/lib/metaaligner/enhancement/enhancement-processor.ts deleted file mode 100644 index bfac667b30..0000000000 --- a/apps/web/src/lib/metaaligner/enhancement/enhancement-processor.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @module enhancement-processor - * @description This module provides a response enhancement processor for the MetaAligner pipeline. - */ - -import type { - UnifiedProcessingRequest, - UnifiedProcessingResponse, -} from '../api/unified-api' - -/** - * Defines the interface for the EnhancementProcessor. - */ -export interface IEnhancementProcessor { - /** - * Enhances a request. - * - * @param request - The processing request. - * @returns A promise that resolves to the enhanced response. - */ - enhance(request: UnifiedProcessingRequest): Promise -} - -/** - * The EnhancementProcessor class. - */ -export class EnhancementProcessor implements IEnhancementProcessor { - private readonly enhancementHistory: string[] = [] - - public async enhance( - request: UnifiedProcessingRequest, - ): Promise { - const optimizedContent = this.multiObjectiveOptimization( - request.llmOutput.content as string, - ) - const improvedContent = this.improveQuality(optimizedContent) - const score = this.scoreResponse(improvedContent) - - this.enhancementHistory.push(`Optimized: ${optimizedContent}`) - this.enhancementHistory.push(`Improved: ${improvedContent}`) - this.enhancementHistory.push(`Scored: ${score}`) - - const response: UnifiedProcessingResponse = { - enhancedResponse: improvedContent, - originalResponse: request.llmOutput.content as string, - alignment: { score, history: this.enhancementHistory } as unknown, // This will be populated by the alignment evaluation - } - return response - } - - private multiObjectiveOptimization(content: string): string { - // Placeholder for multi-objective optimization logic. - // This could involve using a separate LLM to rewrite the response based on the objectives. - return content - } - - private improveQuality(content: string): string { - // Placeholder for response quality improvement logic. - // This could involve correcting grammar, improving clarity, or other quality improvements. - return content - } - - private scoreResponse(_content: string): number { - // Placeholder for response scoring logic. - // This could involve using a separate model to score the response based on various metrics. - return 0 - } -} diff --git a/apps/web/src/lib/metaaligner/error-handling/circuit-breaker.ts b/apps/web/src/lib/metaaligner/error-handling/circuit-breaker.ts deleted file mode 100644 index 29c22539b1..0000000000 --- a/apps/web/src/lib/metaaligner/error-handling/circuit-breaker.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @module circuit-breaker - * @description This module provides a circuit breaker implementation for the MetaAligner pipeline. - */ - -/** - * Represents the state of the circuit breaker. - */ -export enum CircuitBreakerState { - CLOSED = 0, - OPEN = 1, - HALF_OPEN = 2, -} - -/** - * Defines the interface for the CircuitBreaker. - */ -export interface ICircuitBreaker { - /** - * Executes a function with circuit breaker protection. - * - * @param fn - The function to execute. - * @returns A promise that resolves to the result of the function. - */ - fire(fn: () => Promise): Promise -} - -/** - * The CircuitBreaker class. - */ -export class CircuitBreaker implements ICircuitBreaker { - private state: CircuitBreakerState = CircuitBreakerState.CLOSED - private failures = 0 - private nextAttempt: number = Date.now() - - constructor( - private readonly failureThreshold = 3, - private readonly resetTimeout = 10000, - ) {} - - public async fire(fn: () => Promise): Promise { - if (this.state === CircuitBreakerState.OPEN) { - if (this.nextAttempt > Date.now()) { - throw new Error('Circuit breaker is open') - } - this.state = CircuitBreakerState.HALF_OPEN - } - - try { - const result = await fn() - this.success() - return result - } catch (error: unknown) { - this.fail(error as Error) - throw error - } - } - - private success(): void { - this.failures = 0 - if (this.state === CircuitBreakerState.HALF_OPEN) { - this.state = CircuitBreakerState.CLOSED - } - } - - private fail(error: Error): void { - this.failures++ - ;(this as { lastError?: unknown }).lastError = error - if (this.failures >= this.failureThreshold) { - this.state = CircuitBreakerState.OPEN - this.nextAttempt = Date.now() + this.resetTimeout - } - } -} diff --git a/apps/web/src/lib/metaaligner/error-handling/error-handler.ts b/apps/web/src/lib/metaaligner/error-handling/error-handler.ts deleted file mode 100644 index 4a167cde96..0000000000 --- a/apps/web/src/lib/metaaligner/error-handling/error-handler.ts +++ /dev/null @@ -1,206 +0,0 @@ -/** - * @module error-handler - * @description This module provides a comprehensive error handling strategy for the MetaAligner pipeline. - */ - -import { getAiServiceLogger } from '@/lib/logging/standardized-logger' - -import type { UnifiedProcessingResponse } from '../api/unified-api' - -const logger = getAiServiceLogger('error-handler') - -/** - * Base class for custom errors. - */ -export class MetaAlignerError extends Error { - constructor(message: string) { - super(message) - this.name = this.constructor.name - } -} - -/** - * Error that occurs during processing. - */ -export class ProcessingError extends MetaAlignerError {} - -/** - * Error that occurs during validation. - */ -export class ValidationError extends MetaAlignerError {} - -/** - * Error that occurs during enhancement. - */ -export class EnhancementError extends MetaAlignerError {} - -/** - * Defines the interface for the ErrorHandler. - */ -export interface IErrorHandler { - /** - * Handles an error. - * - * @param error - The error to handle. - * @returns A fallback response. - */ - handle(error: Error): UnifiedProcessingResponse -} - -/** - * The ErrorHandler class. - */ -export class ErrorHandler implements IErrorHandler { - public handle(error: Error): UnifiedProcessingResponse { - logger.error('An error occurred in the MetaAligner pipeline', { error }) - this.sendToMonitoringService(error) - if (error instanceof ProcessingError) { - return this.handleProcessingError(error) - } else if (error instanceof ValidationError) { - return this.handleValidationError(error) - } else if (error instanceof EnhancementError) { - return this.handleEnhancementError(error) - } else { - return this.handleGenericError(error) - } - } - - private handleProcessingError( - error: ProcessingError, - ): UnifiedProcessingResponse { - return { - enhancedResponse: 'Error: Could not process the response.', - originalResponse: '', - alignment: { - evaluation: { - overallScore: 0, - objectiveResults: {}, - weights: {}, - normalizedScores: {}, - aggregationMethod: 'weighted' as const, - evaluationContext: {} as never, - timestamp: new Date(), - }, - metrics: { - overallScore: 0, - dimensionScores: {}, - confidence: 0, - timestamp: new Date(), - }, - enhanced: false, - enhancementAttempts: 0, - }, - errors: [ - { - message: error instanceof Error ? error.message : 'Unknown error', - stage: 'processing', - }, - ], - } - } - - private handleValidationError( - error: ValidationError, - ): UnifiedProcessingResponse { - return { - enhancedResponse: 'Error: Invalid request format.', - originalResponse: '', - alignment: { - evaluation: { - overallScore: 0, - objectiveResults: {}, - weights: {}, - normalizedScores: {}, - aggregationMethod: 'weighted' as const, - evaluationContext: {} as never, - timestamp: new Date(), - }, - metrics: { - overallScore: 0, - dimensionScores: {}, - confidence: 0, - timestamp: new Date(), - }, - enhanced: false, - enhancementAttempts: 0, - }, - errors: [ - { - message: error instanceof Error ? error.message : 'Unknown error', - stage: 'validation', - }, - ], - } - } - - private handleEnhancementError( - error: EnhancementError, - ): UnifiedProcessingResponse { - return { - enhancedResponse: 'Error: Could not enhance the response.', - originalResponse: '', - alignment: { - evaluation: { - overallScore: 0, - objectiveResults: {}, - weights: {}, - normalizedScores: {}, - aggregationMethod: 'weighted' as const, - evaluationContext: {} as never, - timestamp: new Date(), - }, - metrics: { - overallScore: 0, - dimensionScores: {}, - confidence: 0, - timestamp: new Date(), - }, - enhanced: false, - enhancementAttempts: 0, - }, - errors: [ - { - message: error instanceof Error ? error.message : 'Unknown error', - stage: 'enhancement', - }, - ], - } - } - - private handleGenericError(error: Error): UnifiedProcessingResponse { - return { - enhancedResponse: 'An unexpected error occurred.', - originalResponse: '', - alignment: { - evaluation: { - overallScore: 0, - objectiveResults: {}, - weights: {}, - normalizedScores: {}, - aggregationMethod: 'weighted' as const, - evaluationContext: {} as never, - timestamp: new Date(), - }, - metrics: { - overallScore: 0, - dimensionScores: {}, - confidence: 0, - timestamp: new Date(), - }, - enhanced: false, - enhancementAttempts: 0, - }, - errors: [ - { - message: error instanceof Error ? error.message : 'Unknown error', - stage: 'unknown', - }, - ], - } - } - - private sendToMonitoringService(error: Error): void { - // Placeholder for sending errors to a monitoring service like Sentry or Datadog. - logger.info('Sending error to monitoring service', { error }) - } -} diff --git a/apps/web/src/lib/metaaligner/error-handling/fallback-generator.ts b/apps/web/src/lib/metaaligner/error-handling/fallback-generator.ts deleted file mode 100644 index 5e08b79ff6..0000000000 --- a/apps/web/src/lib/metaaligner/error-handling/fallback-generator.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @module fallback-generator - * @description This module provides a fallback response generator for the MetaAligner pipeline. - */ - -import type { UnifiedProcessingResponse } from '../api/unified-api' -import { MetaAlignerError } from './error-handler' - -/** - * Defines the interface for the FallbackGenerator. - */ -export interface IFallbackGenerator { - /** - * Generates a fallback response. - * - * @param error - The error that occurred. - * @returns A fallback response. - */ - generate(error: Error): UnifiedProcessingResponse -} - -/** - * The FallbackGenerator class. - */ -export class FallbackGenerator implements IFallbackGenerator { - public generate(error: Error): UnifiedProcessingResponse { - if (error instanceof MetaAlignerError) { - return { - enhancedResponse: `A known error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`, - originalResponse: '', - alignment: { - evaluation: { - overallScore: 0, - objectiveResults: {}, - weights: {}, - normalizedScores: {}, - aggregationMethod: 'weighted' as const, - evaluationContext: {} as never, - timestamp: new Date(), - }, - metrics: { - overallScore: 0, - dimensionScores: {}, - confidence: 0, - timestamp: new Date(), - }, - enhanced: false, - enhancementAttempts: 0, - }, - errors: [ - { - message: error instanceof Error ? error.message : 'Unknown error', - stage: 'unknown', - }, - ], - } - } else { - return { - enhancedResponse: - 'An unexpected error occurred. Please try again later.', - originalResponse: '', - alignment: { - evaluation: { - overallScore: 0, - objectiveResults: {}, - weights: {}, - normalizedScores: {}, - aggregationMethod: 'weighted' as const, - evaluationContext: {} as never, - timestamp: new Date(), - }, - metrics: { - overallScore: 0, - dimensionScores: {}, - confidence: 0, - timestamp: new Date(), - }, - enhanced: false, - enhancementAttempts: 0, - }, - errors: [ - { - message: error instanceof Error ? error.message : 'Unknown error', - stage: 'unknown', - }, - ], - } - } - } -} diff --git a/apps/web/src/lib/metaaligner/formats/conversion.ts b/apps/web/src/lib/metaaligner/formats/conversion.ts deleted file mode 100644 index d38d951b6c..0000000000 --- a/apps/web/src/lib/metaaligner/formats/conversion.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @module conversion - * @description This module provides format conversion utilities. - */ - -import type { - UnifiedProcessingRequest, - UnifiedProcessingResponse, -} from '../api/unified-api' -import { - UnifiedProcessingRequestSchema, - UnifiedProcessingResponseSchema, -} from './schemas' -import { validateData } from './validation' - -/** - * Converts data to the UnifiedProcessingRequest format. - * - * @param data - The data to convert. - * @returns A promise that resolves to the converted data. - */ -export async function toUnifiedProcessingRequest( - data: unknown, -): Promise { - return validateData(UnifiedProcessingRequestSchema, data) as unknown -} - -/** - * Converts data to the UnifiedProcessingResponse format. - * - * @param data - The data to convert. - * @returns A promise that resolves to the converted data. - */ -export async function toUnifiedProcessingResponse( - data: unknown, -): Promise { - return validateData(UnifiedProcessingResponseSchema, data) -} diff --git a/apps/web/src/lib/metaaligner/formats/schemas.ts b/apps/web/src/lib/metaaligner/formats/schemas.ts deleted file mode 100644 index d1acacda2c..0000000000 --- a/apps/web/src/lib/metaaligner/formats/schemas.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @module schemas - * @description This module defines the standard input and output format schemas using Zod. - * These schemas are used for validation and to ensure consistency across the MetaAligner pipeline. - */ - -import { z } from 'zod' - -/** - * Zod schema for the {@link LLMOutput} interface. - */ -export const LLMOutputSchema = z.object({ - content: z.union([z.string(), z.record(z.unknown())]), - metadata: z.record(z.unknown()).optional(), - version: z.string().optional(), -}) - -/** - * Zod schema for the {@link UnifiedContext} interface. - */ -export const UnifiedContextSchema = z.object({ - userQuery: z.string(), - conversationHistory: z.array(z.string()).optional(), - version: z.string().optional(), -}) - -/** - * Zod schema for the {@link UnifiedProcessingRequest} interface. - */ -export const UnifiedProcessingRequestSchema = z.object({ - llmOutput: LLMOutputSchema, - context: UnifiedContextSchema, - version: z.string().optional(), -}) - -/** - * Zod schema for the {@link UnifiedProcessingResponse} interface. - */ -export const UnifiedProcessingResponseSchema = z.object({ - enhancedResponse: z.string(), - originalResponse: z.string(), - alignment: z.object({ - evaluation: z.record(z.unknown()), - metrics: z.record(z.unknown()), - enhanced: z.boolean(), - enhancementAttempts: z.number(), - }), - errors: z - .array(z.object({ message: z.string(), stage: z.string() })) - .optional(), - version: z.string().optional(), -}) diff --git a/apps/web/src/lib/metaaligner/formats/validation.ts b/apps/web/src/lib/metaaligner/formats/validation.ts deleted file mode 100644 index 45fb9ba8c8..0000000000 --- a/apps/web/src/lib/metaaligner/formats/validation.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @module validation - * @description This module provides format validation utilities using Zod schemas. - */ - -import { z } from 'zod' - -/** - * Validates data against a Zod schema. - * - * @param schema - The Zod schema to validate against. - * @param data - The data to validate. - * @returns A promise that resolves to the validated data, or throws an error if validation fails. - */ -export async function validateData( - schema: T, - data: unknown, -): Promise> { - try { - return await schema.parseAsync(data) - } catch (error: unknown) { - if (error instanceof z.ZodError) { - throw new Error( - `Validation failed: ${error.issues.map((e) => e.message).join(', ')}`, - { cause: error }, - ) - } - throw error - } -} diff --git a/apps/web/src/lib/metaaligner/objectives/objective-injector.ts b/apps/web/src/lib/metaaligner/objectives/objective-injector.ts deleted file mode 100644 index 6b2aa22e21..0000000000 --- a/apps/web/src/lib/metaaligner/objectives/objective-injector.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @module objective-injector - * @description This module provides an objective injection system for the MetaAligner pipeline. - */ - -import type { UnifiedProcessingRequest } from '../api/unified-api' -import type { ObjectiveDefinition } from '../core/objectives' -import { CORE_MENTAL_HEALTH_OBJECTIVES } from '../core/objectives' - -/** - * Defines the interface for the ObjectiveInjector. - */ -export interface IObjectiveInjector { - /** - * Injects objectives into a request. - * - * @param request - The processing request. - * @returns A promise that resolves to the request with injected objectives. - */ - inject(request: UnifiedProcessingRequest): Promise -} - -/** - * The ObjectiveInjector class. - */ -export class ObjectiveInjector implements IObjectiveInjector { - public async inject( - request: UnifiedProcessingRequest, - ): Promise { - const objectives = this.loadObjectives(request) - if (!this.validateObjectives(objectives)) { - throw new Error('Invalid objectives') - } - const prioritizedObjectives = this.prioritizeObjectives(objectives) - const resolvedObjectives = this.resolveConflicts(prioritizedObjectives) - return { - ...request, - context: { - ...(request.context as Record), - objectives: resolvedObjectives, - }, - } as unknown as UnifiedProcessingRequest - } - - private validateObjectives(_objectives: ObjectiveDefinition[]): boolean { - // Placeholder for objective validation logic. - // This could involve checking for required fields, valid values, or other constraints. - return true - } - - private loadObjectives( - _request: UnifiedProcessingRequest, - ): ObjectiveDefinition[] { - // Placeholder for dynamic objective loading logic. - // This could involve loading objectives based on the user query, context, or other factors. - return CORE_MENTAL_HEALTH_OBJECTIVES - } - - private prioritizeObjectives( - objectives: ObjectiveDefinition[], - ): ObjectiveDefinition[] { - // Placeholder for objective prioritization logic. - // This could involve sorting objectives by weight, context, or other factors. - return objectives - } - - private resolveConflicts( - objectives: ObjectiveDefinition[], - ): ObjectiveDefinition[] { - // Placeholder for objective conflict resolution logic. - // This could involve merging, removing, or modifying objectives to resolve conflicts. - return objectives - } -} diff --git a/apps/web/src/lib/metaaligner/performance/performance-monitor.ts b/apps/web/src/lib/metaaligner/performance/performance-monitor.ts deleted file mode 100644 index 9def29a2b2..0000000000 --- a/apps/web/src/lib/metaaligner/performance/performance-monitor.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -const logger = createBuildSafeLogger('performance-monitor') /** - * @module performance-monitor - * @description This module provides a performance monitoring system for the MetaAligner pipeline. - */ - -/** - * Defines the interface for the PerformanceMonitor. - */ -export interface IPerformanceMonitor { - /** - * Starts monitoring a task. - * - * @param taskName - The name of the task to monitor. - * @returns A function that should be called when the task is complete. - */ - start(taskName: string): () => void -} - -/** - * The PerformanceMonitor class. - */ -export class PerformanceMonitor implements IPerformanceMonitor { - private readonly timings: Map< - string, - { start: number; end?: number; duration?: number } - > = new Map() - - public start(taskName: string): () => void { - const startTime = performance.now() - this.timings.set(taskName, { start: startTime }) - - return () => { - const endTime = performance.now() - const task = this.timings.get(taskName) - if (task) { - task.end = endTime - task.duration = endTime - task.start - logger.info(`Task '${taskName}' took ${task.duration} ms`) - } - } - } - - public getMetrics(): Record { - const metrics: Record = {} - for (const [taskName, timing] of this.timings.entries()) { - if (timing.duration !== undefined) { - metrics[taskName] = timing.duration - } - } - return metrics - } - - public monitorResourceUsage(): Record { - // Placeholder for resource usage monitoring logic. - // This could involve monitoring CPU, memory, and network usage. - return {} - } - - public optimizePerformance(): boolean { - // Placeholder for performance optimization algorithms. - // This could involve dynamic resource allocation, load balancing, or caching strategies. - return true - } - - public sendAlert(message: string, level: 'info' | 'warn' | 'error'): void { - // Placeholder for sending alerts to a monitoring service. - console[level](`Performance Alert: ${message}`) - } -} diff --git a/apps/web/src/lib/metaaligner/prioritization/EducationalContextRecognizer.ts b/apps/web/src/lib/metaaligner/prioritization/EducationalContextRecognizer.ts deleted file mode 100644 index a9fd88664d..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/EducationalContextRecognizer.ts +++ /dev/null @@ -1,427 +0,0 @@ -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -const logger = createBuildSafeLogger('EducationalContextRecognizer') /** - * Simplified EducationalContextRecognizer - * Focuses on reliable pattern matching with optional AI enhancement - */ - -export enum EducationalType { - DEFINITION = 'definition', - EXPLANATION = 'explanation', - COMPARISON = 'comparison', - SYMPTOMS = 'symptoms', - RESEARCH = 'research', -} - -export enum TopicArea { - DEPRESSION = 'depression', - ANXIETY = 'anxiety', - THERAPY = 'therapy', - MEDICATION = 'medication', - GENERAL_MENTAL_HEALTH = 'general_mental_health', -} - -export enum ResourceType { - EDUCATIONAL_VIDEOS = 'educational_videos', - INFOGRAPHICS = 'infographics', - SCIENTIFIC_ARTICLES = 'scientific_articles', - INTERACTIVE_TOOLS = 'interactive_tools', - BOOKS = 'books', - PODCASTS = 'podcasts', -} - -interface AIChatService { - createChatCompletion( - messages: Array<{ role: string; content: string }>, - options?: { model?: string }, - ): Promise<{ - choices?: Array<{ message?: { content?: string } }> - }> -} - -export interface EducationalRecognizerConfig { - aiService?: AIChatService - model?: string - includeResourceRecommendations?: boolean - adaptToUserLevel?: boolean - enableTopicMapping?: boolean -} - -export interface EducationalContextResult { - isEducational: boolean - confidence: number - educationalType: EducationalType - complexity: string - topicArea: TopicArea - learningObjectives: string[] - recommendedResources: ResourceType[] - priorKnowledgeRequired: string[] - metadata: Record -} - -// Pattern matching for educational content -const EDUCATIONAL_PATTERNS = { - [EducationalType.DEFINITION]: [ - /\b(?:what is|what are|define|definition of|meaning of|tell me about)\b/i, - ], - [EducationalType.EXPLANATION]: [ - /\b(?:how does|how do|how to|why|explain|explanation|work)\b/i, - ], - [EducationalType.COMPARISON]: [ - /\b(?:difference|compare|versus|vs|which is better|comparison)\b/i, - ], - [EducationalType.SYMPTOMS]: [ - /\b(?:symptom|symptoms|sign|signs|warning|how do i know|indicators)\b/i, - ], - [EducationalType.RESEARCH]: [ - /\b(?:research|latest|studies|study|findings)\b/i, - ], -} - -// Topic keywords for classification -const TOPIC_KEYWORDS = { - [TopicArea.DEPRESSION]: ['depression', 'mood disorder', 'depressed'], - [TopicArea.ANXIETY]: ['anxiety', 'panic', 'anxious'], - [TopicArea.THERAPY]: ['therapy', 'cbt', 'dbt', 'counseling'], - [TopicArea.MEDICATION]: ['medication', 'antidepressant', 'drug'], - [TopicArea.GENERAL_MENTAL_HEALTH]: ['mental health', 'ptsd', 'bipolar'], -} - -function matchEducationalType(query: string): { - type: EducationalType | null - confidence: number -} { - const lower = query.toLowerCase() - - for (const [type, patterns] of Object.entries(EDUCATIONAL_PATTERNS)) { - for (const pattern of patterns) { - if (pattern.test(lower)) { - return { type: type as EducationalType, confidence: 0.8 } - } - } - } - - return { type: null, confidence: 0.1 } -} - -function matchTopicArea(query: string): TopicArea { - const lower = query.toLowerCase() - - for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) { - if (keywords.some((keyword) => lower.includes(keyword))) { - return topic as TopicArea - } - } - - return TopicArea.GENERAL_MENTAL_HEALTH -} - -export class EducationalContextRecognizer { - private readonly config: EducationalRecognizerConfig - - constructor(config: EducationalRecognizerConfig = {}) { - this.config = { - model: config.model ?? 'gpt-4', - includeResourceRecommendations: - config.includeResourceRecommendations ?? true, - adaptToUserLevel: config.adaptToUserLevel ?? true, - enableTopicMapping: config.enableTopicMapping ?? true, - aiService: config.aiService, - } - } - - async recognizeEducationalContext( - query: string, - userProfile?: Record, - ): Promise { - // Step 1: Pattern-based recognition - const { type, confidence: patternConfidence } = matchEducationalType(query) - const topicArea = this.config.enableTopicMapping - ? matchTopicArea(query) - : TopicArea.GENERAL_MENTAL_HEALTH - - // If no educational pattern found, return non-educational result - if (!type) { - return this.createNonEducationalResult() - } - - let finalResult: EducationalContextResult = { - isEducational: true, - confidence: patternConfidence, - educationalType: type, - complexity: 'basic', - topicArea, - learningObjectives: [`Learn about ${topicArea.replace('_', ' ')}`], - recommendedResources: this.getDefaultResources(type), - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: patternConfidence * 0.8, - practicalApplications: [], - relatedTopics: [], - }, - } - - // Step 2: AI enhancement (if available and pattern confidence is high) - if (this.config.aiService && patternConfidence > 0.7) { - try { - const aiResult = await this.performAIAnalysis( - query, - userProfile, - type, - topicArea, - ) - if (aiResult) { - finalResult = this.combineResults(finalResult, aiResult) - } - } catch (error: unknown) { - // Continue with pattern-based result if AI fails - logger.warn('AI analysis failed, using pattern-based result:', error) - } - } - - // Step 3: User profile adaptation - if (userProfile && this.config.adaptToUserLevel) { - finalResult = this.adaptToUserProfile(finalResult, userProfile) - } - - return finalResult - } - - private createNonEducationalResult(): EducationalContextResult { - return { - isEducational: false, - confidence: 0.1, - educationalType: EducationalType.DEFINITION, - complexity: 'basic', - topicArea: TopicArea.GENERAL_MENTAL_HEALTH, - learningObjectives: [], - recommendedResources: [], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.1, - practicalApplications: [], - relatedTopics: [], - }, - } - } - - private getDefaultResources(type: EducationalType): ResourceType[] { - const resourceMap: Record = { - [EducationalType.DEFINITION]: [ - ResourceType.EDUCATIONAL_VIDEOS, - ResourceType.INFOGRAPHICS, - ], - [EducationalType.EXPLANATION]: [ - ResourceType.EDUCATIONAL_VIDEOS, - ResourceType.INTERACTIVE_TOOLS, - ], - [EducationalType.COMPARISON]: [ - ResourceType.INFOGRAPHICS, - ResourceType.SCIENTIFIC_ARTICLES, - ], - [EducationalType.SYMPTOMS]: [ - ResourceType.INFOGRAPHICS, - ResourceType.EDUCATIONAL_VIDEOS, - ], - [EducationalType.RESEARCH]: [ - ResourceType.SCIENTIFIC_ARTICLES, - ResourceType.BOOKS, - ], - } - - return resourceMap[type] || [ResourceType.EDUCATIONAL_VIDEOS] - } - - private async performAIAnalysis( - query: string, - userProfile: Record | undefined, - type: EducationalType, - topicArea: TopicArea, - ): Promise { - if (!this.config.aiService) { - return null - } - - const messages = [ - { - role: 'system', - content: - 'You are an educational context analyzer. Return a JSON object with educational analysis.', - }, - { - role: 'user', - content: `Analyze: "${query}". User: ${JSON.stringify(userProfile ?? {})}`, - }, - ] - - const response = await this.config.aiService?.createChatCompletion( - messages, - { - model: this.config.model, - }, - ) - - const content = response?.choices?.[0]?.message?.content - if (!content) { - return null - } - - try { - const parsed = JSON.parse(content) as { - isEducational?: unknown - confidence?: number - complexity?: unknown - learningObjectives?: unknown - recommendedResources?: unknown - priorKnowledgeRequired?: unknown - metadata?: { - conceptualDepth?: number - practicalApplications?: unknown[] - relatedTopics?: unknown[] - } - } - return { - isEducational: Boolean(parsed.isEducational), - confidence: Math.max(0, Math.min(1, parsed.confidence ?? 0.8)), - educationalType: type, // Use pattern-detected type - complexity: this.validateComplexity( - typeof parsed.complexity === 'string' - ? parsed.complexity - : 'intermediate', - ), - topicArea, // Use pattern-detected topic - learningObjectives: Array.isArray(parsed.learningObjectives) - ? parsed.learningObjectives - : [], - recommendedResources: this.validateResources( - parsed.recommendedResources, - ), - priorKnowledgeRequired: Array.isArray(parsed.priorKnowledgeRequired) - ? parsed.priorKnowledgeRequired - : [], - metadata: { - conceptualDepth: Math.max( - 0, - Math.min(1, parsed.metadata?.conceptualDepth ?? 0.5), - ), - practicalApplications: Array.isArray( - parsed.metadata?.practicalApplications, - ) - ? parsed.metadata.practicalApplications - : [], - relatedTopics: Array.isArray(parsed.metadata?.relatedTopics) - ? parsed.metadata.relatedTopics - : [], - }, - } - } catch { - return null - } - } - - private combineResults( - patternResult: EducationalContextResult, - aiResult: EducationalContextResult, - ): EducationalContextResult { - return { - ...patternResult, - confidence: Math.max(patternResult.confidence, aiResult.confidence), - learningObjectives: - aiResult.learningObjectives.length > 0 - ? aiResult.learningObjectives - : patternResult.learningObjectives, - recommendedResources: - aiResult.recommendedResources.length > 0 - ? aiResult.recommendedResources - : patternResult.recommendedResources, - priorKnowledgeRequired: aiResult.priorKnowledgeRequired, - metadata: { - ...patternResult.metadata, - ...aiResult.metadata, - }, - } - } - - private adaptToUserProfile( - result: EducationalContextResult, - userProfile: Record, - ): EducationalContextResult { - const adapted = { ...result } - - // Adapt complexity based on education level - if ( - (userProfile as { educationLevel?: string }).educationLevel === 'graduate' - ) { - adapted.complexity = 'advanced' - } else if ( - (userProfile as { educationLevel?: string }).educationLevel === - 'high_school' - ) { - adapted.complexity = - result.complexity === 'advanced' ? 'intermediate' : 'basic' - } - - // Adapt resources based on learning style - if ( - (userProfile as { preferredLearningStyle?: string }) - .preferredLearningStyle === 'visual' - ) { - const resources = [...adapted.recommendedResources] - if (!resources.includes(ResourceType.INFOGRAPHICS)) { - resources.push(ResourceType.INFOGRAPHICS) - } - adapted.recommendedResources = resources - } - - return adapted - } - - private validateComplexity(complexity: string): string { - return ['basic', 'intermediate', 'advanced'].includes(complexity) - ? complexity - : 'basic' - } - - private validateResources(resources: unknown): ResourceType[] { - if (!Array.isArray(resources)) { - return [] - } - return resources - .filter((r) => Object.values(ResourceType).includes(r)) - .slice(0, 5) // Limit to 5 resources - } - - async recognizeBatch( - queries: { query: string }[], - ): Promise { - return Promise.all( - queries.map(async (q) => this.recognizeEducationalContext(q.query)), - ) - } - - generateLearningPathway(result: EducationalContextResult): any { - return { - currentTopic: `${result.educationalType} - ${result.topicArea}`, - nextSteps: ['Learn about symptoms'], - estimatedTimeToComplete: '15-30 minutes', - relatedConcepts: result.metadata['relatedTopics'] ?? [], - } - } -} - -export function createEducationalContextRecognizer( - config: EducationalRecognizerConfig, -): EducationalContextRecognizer { - return new EducationalContextRecognizer(config) -} - -export function getDefaultEducationalRecognizerConfig( - aiService: unknown, -): EducationalRecognizerConfig { - return { - aiService, - model: 'gpt-4', - includeResourceRecommendations: true, - adaptToUserLevel: true, - enableTopicMapping: true, - } -} diff --git a/apps/web/src/lib/metaaligner/prioritization/adaptive-selector.test.ts b/apps/web/src/lib/metaaligner/prioritization/adaptive-selector.test.ts deleted file mode 100644 index c2da6c0e8d..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/adaptive-selector.test.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { describe, expect, vi, test, beforeEach } from 'vitest' -/** - * Unit tests for Adaptive Objective Selector - */ - -import type { AIService } from '../../ai/models/ai-types' -import type { CrisisDetectionService } from '../../ai/services/crisis-detection' -import { ContextType, CORE_MENTAL_HEALTH_OBJECTIVES } from '../core/objectives' -import { - AdaptiveSelector, - type AdaptiveSelectorConfig, -} from './adaptive-selector' -// Import WeightingStrategy - -// Mock dependencies -const mockAIService: AIService = { - getModelInfo: vi.fn().mockReturnValue({ - id: 'test-model', - name: 'Test Model', - provider: 'test', - capabilities: [], - contextWindow: 4096, - maxTokens: 2048, - }), - createChatCompletion: vi.fn(), - createStreamingChatCompletion: vi.fn(), - dispose: vi.fn(), -} - -const mockDetectCrisis = vi.fn() -const mockCrisisDetectionService = { - detectCrisis: mockDetectCrisis, - detectBatch: vi.fn(), -} as unknown as CrisisDetectionService - -// Mock ContextDetector and ObjectiveWeightingEngine entirely for focused testing if needed, -// or allow them to run with their own mocks (like mockAIService for ContextDetector). -// For now, we'll let them use their actual implementations with mocked dependencies. - -describe('AdaptiveSelector', () => { - let adaptiveSelector: AdaptiveSelector - let config: AdaptiveSelectorConfig - - beforeEach(() => { - vi.clearAllMocks() - - config = { - aiService: mockAIService, - contextDetectorConfig: { - crisisDetectionService: mockCrisisDetectionService, - enableCrisisIntegration: true, - }, - } - adaptiveSelector = new AdaptiveSelector(config) - }) - - test('should select objectives based on GENERAL context by default', async () => { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.1, - category: 'general', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: '', - content: '', - }) - const aiResponseGeneral = { - id: 'gen-completion', - created: Date.now(), - model: 'test-model', - provider: 'test', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - detectedContext: ContextType.GENERAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, - content: JSON.stringify({ - detectedContext: ContextType.GENERAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - } - vi.mocked(mockAIService).createChatCompletion.mockResolvedValue( - aiResponseGeneral, - ) - - const result = await adaptiveSelector.selectObjectives( - 'Hello, how are you?', - ) - - expect(result.contextDetectionResult.detectedContext).toBe( - ContextType.GENERAL, - ) - expect(result.selectedObjectives.length).toBe( - CORE_MENTAL_HEALTH_OBJECTIVES.length, - ) // All objectives included - - // Check if weights roughly match default distribution for GENERAL context - // In this setup, GENERAL uses default objective weights - - result.selectedObjectives.forEach((so) => { - const coreObjective = CORE_MENTAL_HEALTH_OBJECTIVES.find( - (co) => co.id === so.objective.id, - ) - expect(coreObjective).toBeDefined() - // For GENERAL, weights should be proportional to their base definition if not overridden - // The actual check depends on how getContextualObjectiveWeights for GENERAL is implemented - // For now, we check if the weight is present and positive. - expect(vi.mocked(so).weight).toBeGreaterThanOrEqual(0) - }) - const sumOfWeights = result.selectedObjectives.reduce( - (sum, so) => sum + so.weight, - 0, - ) - expect(sumOfWeights).toBeCloseTo(1.0, 5) - }) - - test('should prioritize SAFETY objective in CRISIS context', async () => { - mockDetectCrisis.mockResolvedValue({ - isCrisis: true, - confidence: 0.95, - category: 'self-harm', - riskLevel: 'high', - urgency: 'immediate', - detectedTerms: ['hurt myself'], - suggestedActions: ['call_emergency'], - timestamp: '', - content: 'I want to hurt myself', - }) - // AI completion won't be called if crisis is detected first by CrisisDetectionService - - const result = await adaptiveSelector.selectObjectives( - 'I want to hurt myself', - ) - - expect(result.contextDetectionResult.detectedContext).toBe( - ContextType.CRISIS, - ) - const safetyObjective = result.selectedObjectives.find( - (so) => so.objective.id === 'safety', - ) - expect(safetyObjective).toBeDefined() - // Based on CONTEXT_OBJECTIVE_PROFILES, safety in crisis is 0.6 (then normalized) - // We expect it to be the highest or significantly higher - let highestWeight = 0 - result.selectedObjectives.forEach((so) => { - if (so.weight > highestWeight) { - highestWeight = so.weight - } - }) - expect(safetyObjective!.weight).toBe(highestWeight) - expect(safetyObjective!.weight).toBeGreaterThan( - 0.5 / result.selectedObjectives.length, - ) // Check it's substantially weighted after normalization - - const sumOfWeights = result.selectedObjectives.reduce( - (sum, so) => sum + so.weight, - 0, - ) - expect(sumOfWeights).toBeCloseTo(1.0, 5) - }) - - test('should prioritize INFORMATIVENESS and CORRECTNESS in EDUCATIONAL context', async () => { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.1, - category: 'general', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: '', - content: '', - }) - const aiResponseEducational = { - id: 'edu-completion', - created: Date.now(), - model: 'test-model', - provider: 'test', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, - content: JSON.stringify({ - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - } - vi.mocked(mockAIService).createChatCompletion.mockResolvedValue( - aiResponseEducational, - ) - - const result = await adaptiveSelector.selectObjectives( - 'What is cognitive behavioral therapy?', - ) - - expect(result.contextDetectionResult.detectedContext).toBe( - ContextType.EDUCATIONAL, - ) - const informativenessObjective = result.selectedObjectives.find( - (so) => so.objective.id === 'informativeness', - ) - const correctnessObjective = result.selectedObjectives.find( - (so) => so.objective.id === 'correctness', - ) - - expect(informativenessObjective).toBeDefined() - expect(correctnessObjective).toBeDefined() - - // Check these are among the highest weighted - const otherObjectivesMaxWeight = result.selectedObjectives - .filter( - (so) => - so.objective.id !== 'informativeness' && - so.objective.id !== 'correctness', - ) - .reduce((max, so) => Math.max(max, so.weight), 0) - - expect(informativenessObjective!.weight).toBeGreaterThan( - otherObjectivesMaxWeight, - ) - expect(correctnessObjective!.weight).toBeGreaterThan( - otherObjectivesMaxWeight, - ) - - const sumOfWeights = result.selectedObjectives.reduce( - (sum, so) => sum + so.weight, - 0, - ) - expect(sumOfWeights).toBeCloseTo(1.0, 5) - }) - - test('should log context transitions via ContextDetector', async () => { - const loggerSpy = vi.spyOn(global.console, 'info') // Or your actual logger instance - - // First call: GENERAL - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.1, - category: 'general', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: '', - content: '', - }) - const aiResponseGeneral = { - id: 'gen-completion', - created: Date.now(), - model: 'test-model', - provider: 'test', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - detectedContext: ContextType.GENERAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, - content: JSON.stringify({ - detectedContext: ContextType.GENERAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - } - vi.mocked(mockAIService).createChatCompletion.mockResolvedValue( - aiResponseGeneral, - ) - await adaptiveSelector.selectObjectives('Hello') - - // Second call: EDUCATIONAL (should trigger transition log) - const aiResponseEducational = { - id: 'edu-completion', - created: Date.now(), - model: 'test-model', - provider: 'test', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, - content: JSON.stringify({ - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - } - vi.mocked(mockAIService).createChatCompletion.mockResolvedValue( - aiResponseEducational, - ) - const result = await adaptiveSelector.selectObjectives( - 'Tell me about mindfulness', - ) - - expect(result.contextDetectionResult.metadata.transition).toBeDefined() - expect(result.contextDetectionResult.metadata.transition?.from).toBe( - ContextType.GENERAL, - ) - expect(result.contextDetectionResult.metadata.transition?.to).toBe( - ContextType.EDUCATIONAL, - ) - - loggerSpy.mockRestore() - }) - - test('should fallback to default weights if context detection fails', async () => { - vi.mocked(mockAIService).createChatCompletion.mockRejectedValue( - new Error('AI service failed'), - ) - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.1, - category: 'general', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: '', - content: '', - }) - - const result = await adaptiveSelector.selectObjectives( - 'A problematic input', - ) - - expect(result.contextDetectionResult.detectedContext).toBe( - ContextType.GENERAL, - ) - expect(result.contextDetectionResult.metadata['error']).toBeDefined() - expect(result.selectedObjectives.length).toBe( - CORE_MENTAL_HEALTH_OBJECTIVES.length, - ) - - // Verify weights are default weights - let totalDefaultWeight = 0 - const defaultWeights: Record = - CORE_MENTAL_HEALTH_OBJECTIVES.reduce>( - (acc, obj) => { - acc[obj.id] = obj.weight - totalDefaultWeight += obj.weight - return acc - }, - {}, - ) - - // Normalize if they don't sum to 1 (they should) - if (Math.abs(totalDefaultWeight - 1.0) > 0.001 && totalDefaultWeight > 0) { - for (const id in defaultWeights) { - defaultWeights[id] /= totalDefaultWeight - } - } - - result.selectedObjectives.forEach((so) => { - expect(vi.mocked(so).weight).toBeCloseTo( - defaultWeights[so.objective.id], - 5, - ) - }) - }) - - test('should handle user profile with preferences', async () => { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.1, - category: 'general', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: '', - content: '', - }) - const aiResponse = { - id: 'completion', - created: Date.now(), - model: 'test-model', - provider: 'test', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - detectedContext: ContextType.GENERAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, - content: JSON.stringify({ - detectedContext: ContextType.GENERAL, - confidence: 0.9, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - } - vi.mocked(mockAIService).createChatCompletion.mockResolvedValue(aiResponse) - - const userProfile: { - preferences: { - objectiveWeightAdjustments: Record - preferredObjectives: { - objectiveId: string - preferenceStrength: number - }[] - } - } = { - preferences: { - objectiveWeightAdjustments: { empathy: 1.5 }, - preferredObjectives: [ - { objectiveId: 'correctness', preferenceStrength: 0.8 }, - ], - }, - } - - const result = await adaptiveSelector.selectObjectives( - 'Test message', - [], - 'test-session', - userProfile, - ) - - expect(result.selectedObjectives.length).toBeGreaterThan(0) - const sumOfWeights = result.selectedObjectives.reduce( - (sum, so) => sum + so.weight, - 0, - ) - expect(sumOfWeights).toBeCloseTo(1.0, 5) - }) -}) diff --git a/apps/web/src/lib/metaaligner/prioritization/adaptive-selector.ts b/apps/web/src/lib/metaaligner/prioritization/adaptive-selector.ts deleted file mode 100644 index cac340a782..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/adaptive-selector.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Adaptive Objective Selector - * Bridges context detection with objective weighting to select objectives dynamically - */ - -import type { AIService } from '../../ai/models/ai-types' -import { - ObjectiveWeightingEngine, - DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, - type WeightCalculationResult, -} from '../core/objective-weighting' -import { - CORE_MENTAL_HEALTH_OBJECTIVES, - type AlignmentContext, - type ObjectiveDefinition, - ContextType, - getDefaultObjectiveWeights, -} from '../core/objectives' -import { - ContextDetector, - type ContextDetectionResult, - type ContextDetectorConfig, -} from './context-detector' - -export interface AdaptiveSelectorConfig { - aiService: AIService - contextDetectorConfig?: Partial -} - -export interface SelectedObjective { - objective: ObjectiveDefinition - weight: number -} - -export interface SelectionResult { - contextDetectionResult: ContextDetectionResult - alignmentContext: AlignmentContext - selectedObjectives: SelectedObjective[] - weightCalculationResult: WeightCalculationResult -} - -export class AdaptiveSelector { - private readonly aiService: AIService - private readonly contextDetector: ContextDetector - private readonly weightingEngine: ObjectiveWeightingEngine - private lastDetectedContext: ContextType | null = null - - constructor(config: AdaptiveSelectorConfig) { - this.aiService = config.aiService - this.contextDetector = new ContextDetector({ - aiService: this.aiService, - enableCrisisIntegration: - config.contextDetectorConfig?.enableCrisisIntegration ?? true, - enableEducationalRecognition: - config.contextDetectorConfig?.enableEducationalRecognition ?? true, - model: config.contextDetectorConfig?.model ?? 'gpt-4', - crisisDetectionService: - config.contextDetectorConfig?.crisisDetectionService, - educationalContextRecognizer: - config.contextDetectorConfig?.educationalContextRecognizer, - }) - - // Use current global/default params (tests may mutate DEFAULT_WEIGHT_ADJUSTMENT_PARAMS.strategy) - this.weightingEngine = new ObjectiveWeightingEngine( - DEFAULT_WEIGHT_ADJUSTMENT_PARAMS, - ) - } - - async selectObjectives( - userInput: string, - conversationHistory: string[] = [], - userId?: string, - userProfile?: AlignmentContext['userProfile'], - ): Promise { - // Detect context with defensive fallback - let detection: ContextDetectionResult - try { - detection = await this.contextDetector.detectContext( - userInput, - conversationHistory, - userId, - ) - } catch (err) { - // Clinical Decision Support Safeguard: When detection fails, we cannot determine - // if this is a crisis situation. Flag for human review rather than auto-defaulting to safe. - detection = { - detectedContext: ContextType.GENERAL, - confidence: 0.1, - contextualIndicators: [ - { - type: 'error_fallback', - description: 'Context detection failed, flagged for review', - confidence: 0.1, - }, - ], - needsSpecialHandling: true, // Flag for human review - could be a crisis - urgency: 'high', // Default to high urgency to force human review when detection fails - metadata: { - error: err instanceof Error ? err.message : 'Unknown error', - requiresHumanReview: true, // Explicit flag for clinical decision support - errorFallback: true, - detectedContext: 'unknown', // Explicitly mark as unknown - confidence: 0.1, - }, - } - } - - // Track transitions (used in tests) - if ( - this.lastDetectedContext && - this.lastDetectedContext !== detection.detectedContext - ) { - detection.metadata = detection.metadata || {} - detection.metadata['transition'] = { - from: this.lastDetectedContext, - to: detection.detectedContext, - } - } - this.lastDetectedContext = detection.detectedContext - - const alignmentContext: AlignmentContext = { - userQuery: userInput, - conversationHistory, - detectedContext: detection.detectedContext, - userProfile, - } - - // Default objective config and weights - const defaults = getDefaultObjectiveWeights() - - let selectedObjectives: SelectedObjective[] - let weightCalculationResult: WeightCalculationResult - - // If detection failed (marked with metadata.error), use exact default weights without recalculation - const hasDetectionError = - 'error' in detection.metadata && detection.metadata['error'] !== undefined - if (hasDetectionError) { - selectedObjectives = CORE_MENTAL_HEALTH_OBJECTIVES.map((objective) => ({ - objective, - weight: defaults[objective.id] ?? 0, - })) - weightCalculationResult = { - weights: { ...defaults }, - details: { - strategy: 'default-fallback', - reason: - 'Context detection failed - using default weights with human review flag', - }, - } as unknown as WeightCalculationResult - } else { - const objectiveConfig = { - objectives: defaults, - contextualWeights: {}, - globalSettings: { - enableDynamicWeighting: true, - enableContextualAdjustment: true, - minObjectiveScore: 0, - maxObjectiveScore: 1, - normalizationMethod: 0, - aggregationMethod: 0, - }, - } - weightCalculationResult = this.weightingEngine.calculateWeights( - CORE_MENTAL_HEALTH_OBJECTIVES, - alignmentContext, - objectiveConfig as Record, - ) - selectedObjectives = CORE_MENTAL_HEALTH_OBJECTIVES.map((objective) => ({ - objective, - weight: weightCalculationResult.weights[objective.id] ?? 0, - })) - } - - return { - contextDetectionResult: detection, - alignmentContext, - selectedObjectives, - weightCalculationResult, - } - } -} diff --git a/apps/web/src/lib/metaaligner/prioritization/context-detector.test.ts b/apps/web/src/lib/metaaligner/prioritization/context-detector.test.ts deleted file mode 100644 index 0ec43ec70f..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/context-detector.test.ts +++ /dev/null @@ -1,732 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -/** - * Unit tests for Context Detection System - */ - -import type { AIService } from '../../ai/models/ai-types' -import type { CrisisDetectionService } from '../../ai/services/crisis-detection' -import { ContextType } from '../core/objectives' -import { - ContextDetector, - type ContextDetectorConfig, - type ContextDetectionResult, -} from './context-detector' - -// CrisisDetectionResult type used in tests - defined locally since it's not exported from the service module -interface CrisisDetectionResult { - isCrisis: boolean - confidence: number - category: string - riskLevel: string - urgency: string - detectedTerms: string[] - suggestedActions: string[] - timestamp: string - content: string -} - -// Mock dependencies -const mockAIService: AIService = { - getModelInfo: vi.fn().mockReturnValue({ - id: 'test-model', - name: 'Test Model', - provider: 'test', - capabilities: [], - contextWindow: 4096, - maxTokens: 2048, - }), - createChatCompletion: vi.fn(), - createStreamingChatCompletion: vi.fn(), - dispose: vi.fn(), -} - -const mockDetectCrisis = vi.fn() -const mockDetectBatch = vi.fn() - -const mockCrisisDetectionService = { - detectCrisis: mockDetectCrisis, - detectBatch: mockDetectBatch, -} as unknown as CrisisDetectionService - -describe('ContextDetector', () => { - let contextDetector: ContextDetector - let config: ContextDetectorConfig - - beforeEach(() => { - vi.clearAllMocks() - - config = { - aiService: mockAIService, - crisisDetectionService: mockCrisisDetectionService, - model: 'gpt-4', - enableCrisisIntegration: true, - } - - contextDetector = new ContextDetector(config) - }) - - describe('constructor', () => { - it('should initialize with provided configuration', () => { - expect(contextDetector).toBeDefined() - }) - - it('should use default model when not specified', () => { - const configWithoutModel = { ...config } - delete configWithoutModel.model - - const detector = new ContextDetector(configWithoutModel) - expect(detector).toBeDefined() - }) - - it('should disable crisis integration when specified', async () => { - const configWithoutCrisis = { - ...config, - enableCrisisIntegration: false, - } - - const detector = new ContextDetector(configWithoutCrisis) - expect(detector).toBeDefined() - - // Mock AI response for general context detection - const aiResponse = { - id: 'test-completion', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - detectedContext: 'general', - confidence: 0.5, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { - promptTokens: 10, - completionTokens: 20, - totalTokens: 30, - }, - provider: 'test', - content: '', - } - - vi.mocked(mockAIService).createChatCompletion.mockResolvedValue( - aiResponse, - ) - - // Test that crisis detection service is not called when integration is disabled - const result = await detector.detectContext('I need help with something') - - expect(vi.mocked(result).detectedContext).toBe(ContextType.GENERAL) - expect(mockDetectCrisis).not.toHaveBeenCalled() - expect(result.metadata['crisisAnalysis']).toBeUndefined() - }) - }) - - describe('detectContext', () => { - describe('crisis detection integration', () => { - it('should return crisis context when crisis is detected', async () => { - const crisisResult: CrisisDetectionResult = { - isCrisis: true, - confidence: 0.9, - category: 'self-harm', - riskLevel: 'high', - urgency: 'immediate', - detectedTerms: ['hurt myself'], - suggestedActions: ['immediate-intervention'], - timestamp: new Date().toISOString(), - content: 'I want to hurt myself', - } - - mockDetectCrisis.mockResolvedValue(crisisResult) - - const result = await contextDetector.detectContext( - 'I want to hurt myself', - [], - 'user123', - ) - - expect(vi.mocked(result).detectedContext).toBe(ContextType.CRISIS) - expect(vi.mocked(result).confidence).toBe(0.9) - expect(vi.mocked(result).needsSpecialHandling).toBe(true) - expect(vi.mocked(result).urgency).toBe('critical') - expect(vi.mocked(result).contextualIndicators).toHaveLength(1) - expect(result.contextualIndicators[0]?.type).toBe('crisis_detection') - expect(mockDetectCrisis).toHaveBeenCalledWith('I want to hurt myself', { - sensitivityLevel: 'medium', - userId: 'user123', - source: 'context-detection', - }) - }) - - it('should proceed with general detection when no crisis is detected', async () => { - const crisisResult: CrisisDetectionResult = { - isCrisis: false, - confidence: 0.2, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: 'What is anxiety?', - } - - const aiResponse = { - id: 'test-completion', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - detectedContext: 'educational', - confidence: 0.8, - contextualIndicators: [ - { - type: 'question_pattern', - description: 'Educational question detected', - confidence: 0.8, - }, - ], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { - promptTokens: 10, - completionTokens: 20, - totalTokens: 30, - }, - provider: 'test', - content: '', - } - - mockDetectCrisis.mockResolvedValue(crisisResult) - vi.mocked(mockAIService).createChatCompletion.mockResolvedValue( - aiResponse, - ) - - const result = await contextDetector.detectContext('What is anxiety?') - - expect(vi.mocked(result).detectedContext).toBe(ContextType.EDUCATIONAL) - expect(vi.mocked(result).confidence).toBe(0.8) - expect(result.metadata['crisisAnalysis']).toBeDefined() - expect( - (result.metadata['crisisAnalysis'] as CrisisDetectionResult) - ?.confidence, - ).toBe(0.2) - }) - }) - - describe('context classification', () => { - it('should detect educational context', async () => { - const aiResponse = { - id: 'test-completion', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - detectedContext: 'educational', - confidence: 0.85, - contextualIndicators: [ - { - type: 'question_pattern', - description: 'Educational question about mental health', - confidence: 0.8, - }, - ], - needsSpecialHandling: false, - urgency: 'low', - metadata: { topic: 'anxiety' }, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { - promptTokens: 10, - completionTokens: 20, - totalTokens: 30, - }, - provider: 'test', - content: '', - } - - const crisisResult: CrisisDetectionResult = { - isCrisis: false, - confidence: 0.1, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: 'What are the symptoms of anxiety?', - } - - mockDetectCrisis.mockResolvedValue(crisisResult) - vi.mocked(mockAIService).createChatCompletion.mockResolvedValue( - aiResponse, - ) - - const result = await contextDetector.detectContext( - 'What are the symptoms of anxiety?', - ) - - expect(vi.mocked(result).detectedContext).toBe(ContextType.EDUCATIONAL) - expect(vi.mocked(result).confidence).toBe(0.8) - expect(vi.mocked(result).urgency).toBe('low') - }) - - it('should detect clinical assessment context by pattern - basic diagnosis queries', async () => { - const clinicalQueries = [ - 'Can you diagnose depression for me?', - 'Do I need a clinical assessment for anxiety?', - 'I want an official diagnosis for ADHD.', - 'What is my diagnosis based on these symptoms?', - ] - for (const q of clinicalQueries) { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.05, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: q, - }) - const result = await contextDetector.detectContext(q) - expect(vi.mocked(result).detectedContext).toBe( - ContextType.CLINICAL_ASSESSMENT, - ) - expect(vi.mocked(result).confidence).toBeGreaterThanOrEqual(0.8) - expect(result.contextualIndicators?.[0]?.type).toMatch( - /clinical_assessment/i, - ) - } - }) - - it('should detect clinical assessment context - PHQ-9 and assessment tools', async () => { - const phqQueries = [ - 'How do I score my PHQ-9 questionnaire?', - 'Can you interpret my GAD-7 results?', - 'What does my BDI score mean?', - 'Help me calculate my PCL-5 assessment', - 'Scoring the Beck Depression Inventory', - ] - for (const q of phqQueries) { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.05, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: q, - }) - const result = await contextDetector.detectContext(q) - expect(vi.mocked(result).detectedContext).toBe( - ContextType.CLINICAL_ASSESSMENT, - ) - expect(vi.mocked(result).confidence).toBeGreaterThanOrEqual(0.8) - expect(vi.mocked(result).needsSpecialHandling).toBe(true) - expect(vi.mocked(result).urgency).toBe('medium') - } - }) - - it('should detect clinical assessment context - DSM-5 and clinical criteria', async () => { - const dsmQueries = [ - 'Do I meet the DSM-5 criteria for major depression?', - 'What are the ICD-10 diagnostic criteria?', - 'Am I meeting the requirements for PTSD diagnosis?', - 'DSM-IV criteria for bipolar disorder', - ] - for (const q of dsmQueries) { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.05, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: q, - }) - const result = await contextDetector.detectContext(q) - expect(vi.mocked(result).detectedContext).toBe( - ContextType.CLINICAL_ASSESSMENT, - ) - expect(vi.mocked(result).confidence).toBeGreaterThanOrEqual(0.8) - } - }) - - it('should detect clinical assessment context - risk assessment language', async () => { - const riskQueries = [ - 'Need a risk assessment for suicide', - 'What is the severity level of my depression?', - 'Self-harm risk evaluation needed', - 'Safety assessment for current symptoms', - ] - for (const q of riskQueries) { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.05, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: q, - }) - const result = await contextDetector.detectContext(q) - expect(vi.mocked(result).detectedContext).toBe( - ContextType.CLINICAL_ASSESSMENT, - ) - expect(vi.mocked(result).confidence).toBeGreaterThanOrEqual(0.8) - } - }) - - it('should NOT detect clinical assessment for negative cases', async () => { - const nonClinicalQueries = [ - 'What is depression?', // Educational - 'How do I cope with anxiety?', // Support/Educational - 'Tell me about therapy options', // Educational - 'I feel sad today', // Support - 'Where can I find a therapist?', // Informational - ] - for (const q of nonClinicalQueries) { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.05, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: q, - }) - const result = await contextDetector.detectContext(q) - expect(result.detectedContext).not.toBe( - ContextType.CLINICAL_ASSESSMENT, - ) - } - }) - - it('should handle ambiguous clinical assessment cases', async () => { - const ambiguousQueries = [ - 'I think I might need to see a doctor', // Could be clinical or informational - 'Should I get evaluated?', // Vague - 'My symptoms are getting worse', // Support/General - ] - for (const q of ambiguousQueries) { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.05, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: q, - }) - const result = await contextDetector.detectContext(q) - // Ambiguous cases may or may not be clinical - just ensure we get a valid context - expect(vi.mocked(result).detectedContext).toBeDefined() - expect(vi.mocked(result).confidence).toBeGreaterThan(0) - } - }) - - it('should meet confidence threshold >= 0.8 for clinical assessment', async () => { - const query = 'I need a psychiatric evaluation for bipolar disorder' - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.05, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: query, - }) - const result = await contextDetector.detectContext(query) - expect(vi.mocked(result).detectedContext).toBe( - ContextType.CLINICAL_ASSESSMENT, - ) - expect(vi.mocked(result).confidence).toBeGreaterThanOrEqual(0.8) - expect(vi.mocked(result).confidence).toBeLessThanOrEqual(1.0) - }) - - it('should short-circuit to crisis handler when crisis signals present', async () => { - const crisisQuery = 'I want to kill myself and need a diagnosis' - mockDetectCrisis.mockResolvedValue({ - isCrisis: true, - confidence: 0.95, - category: 'suicide', - riskLevel: 'high', - urgency: 'immediate', - detectedTerms: ['kill myself'], - suggestedActions: ['immediate_intervention'], - timestamp: new Date().toISOString(), - content: crisisQuery, - }) - const result = await contextDetector.detectContext(crisisQuery) - // Crisis should override clinical assessment - expect(vi.mocked(result).detectedContext).toBe(ContextType.CRISIS) - expect(vi.mocked(result).confidence).toBeGreaterThan(0.9) - expect(vi.mocked(result).urgency).toBe('critical') - expect(vi.mocked(result).needsSpecialHandling).toBe(true) - }) - - it('should not log PII in clinical assessment detection', async () => { - const queryWithPII = - 'Can you diagnose John Smith, SSN 123-45-6789, for depression?' - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.05, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: queryWithPII, - }) - - // Spy on logger to ensure no PII is logged - const loggerSpy = vi.spyOn(console, 'log') - const result = await contextDetector.detectContext(queryWithPII) - - expect(result.detectedContext).toBe(ContextType.CLINICAL_ASSESSMENT) - - // Check that no logged messages contain PII - const logCalls = loggerSpy.mock.calls - for (const call of logCalls) { - const logMessage = JSON.stringify(call) - expect(logMessage).not.toContain('John Smith') - expect(logMessage).not.toContain('123-45-6789') - } - - loggerSpy.mockRestore() - }) - - it('should detect informational context by pattern', async () => { - const informationalQueries = [ - 'Where can I find support group resources?', - 'What number to call for crisis counseling?', - 'How do I sign up for mental health services?', - 'Info on affordable counseling options.', - 'What are the hours of operation for the hotline?', - ] - for (const q of informationalQueries) { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.05, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: q, - }) - const result = await contextDetector.detectContext(q) - expect(vi.mocked(result).detectedContext).toBe( - ContextType.INFORMATIONAL, - ) - expect(vi.mocked(result).confidence).toBeGreaterThanOrEqual(0.8) - expect(result.contextualIndicators?.[0]?.type).toMatch( - /informational/i, - ) - } - }) - }) - - describe('error handling', () => { - it('should handle AI service errors gracefully', async () => { - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.1, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: 'Test message', - }) - vi.mocked(mockAIService).createChatCompletion.mockRejectedValue( - new Error('AI service error'), - ) - - const result = await contextDetector.detectContext('Test message') - - expect(vi.mocked(result).detectedContext).toBe(ContextType.GENERAL) - expect(vi.mocked(result).confidence).toBe(0.1) - expect(vi.mocked(result).urgency).toBe('low') - expect(result.metadata['error']).toBe('AI service error') - }) - - it('should handle malformed AI responses', async () => { - const malformedResponse = { - id: 'test-completion', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { - role: 'assistant' as const, - content: 'This is not valid JSON', - }, - finishReason: 'stop' as const, - }, - ], - usage: { - promptTokens: 10, - completionTokens: 20, - totalTokens: 30, - }, - provider: 'test', - content: '', - } - - mockDetectCrisis.mockResolvedValue({ - isCrisis: false, - confidence: 0.1, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: 'Test message', - }) - vi.mocked(mockAIService).createChatCompletion.mockResolvedValue( - malformedResponse, - ) - - const result = await contextDetector.detectContext('Test message') - - expect(vi.mocked(result).detectedContext).toBe(ContextType.GENERAL) - expect(vi.mocked(result).confidence).toBe(0.3) - }) - }) - }) - - describe('detectContextBatch', () => { - it('should process multiple inputs', async () => { - const inputs = [ - { text: 'What is anxiety?', userId: 'user1' }, - { text: 'I need help coping', userId: 'user2' }, - ] - - const crisisResult: CrisisDetectionResult = { - isCrisis: false, - confidence: 0.1, - category: 'general_concern', - riskLevel: 'low', - urgency: 'low', - detectedTerms: [], - suggestedActions: [], - timestamp: new Date().toISOString(), - content: 'batch processing', - } - - mockDetectCrisis.mockResolvedValue(crisisResult) - - const aiResponses = [ - { detectedContext: 'educational', confidence: 0.8 }, - { detectedContext: 'support', confidence: 0.7 }, - ].map((ctx) => ({ - id: 'test-completion', - created: Date.now(), - model: 'test-model', - choices: [ - { - message: { - role: 'assistant' as const, - content: JSON.stringify({ - ...ctx, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - }), - }, - finishReason: 'stop' as const, - }, - ], - usage: { - promptTokens: 10, - completionTokens: 20, - totalTokens: 30, - }, - provider: 'test', - content: '', - })) - - vi.mocked(mockAIService.createChatCompletion) - .mockResolvedValueOnce(aiResponses[0]) - .mockResolvedValueOnce(aiResponses[1]) - - const results = await contextDetector.detectContextBatch(inputs) - - expect(results).toHaveLength(2) - expect(results[0]?.detectedContext).toBe(ContextType.EDUCATIONAL) - expect(results[1]?.detectedContext).toBe(ContextType.GENERAL) - }) - }) - - describe('createAlignmentContext', () => { - it('should create properly structured alignment context', () => { - const userQuery = 'I need help with anxiety' - const detectionResult: ContextDetectionResult = { - detectedContext: ContextType.SUPPORT, - confidence: 0.8, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'medium', - metadata: {}, - } - - const alignmentContext = contextDetector.createAlignmentContext( - userQuery, - detectionResult, - ) - - expect(vi.mocked(alignmentContext).userQuery).toBe(userQuery) - expect(vi.mocked(alignmentContext).detectedContext).toBe( - ContextType.SUPPORT, - ) - expect(alignmentContext.sessionMetadata?.['confidence']).toBe(0.8) - expect(alignmentContext.sessionMetadata?.['urgency']).toBe('medium') - expect(alignmentContext.sessionMetadata?.['needsSpecialHandling']).toBe( - false, - ) - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/prioritization/context-detector.ts b/apps/web/src/lib/metaaligner/prioritization/context-detector.ts deleted file mode 100644 index 22c79a2cf7..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/context-detector.ts +++ /dev/null @@ -1,666 +0,0 @@ -/** - * Context Detection System for MetaAligner - * Integrates with existing crisis detection and identifies different conversation contexts - * to enable dynamic objective prioritization - */ - -import type { AIService } from '../../ai/models/ai-types' -import { CrisisDetectionService } from '../../ai/services/crisis-detection' -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { ContextType, type AlignmentContext } from '../core/objectives' -import { EducationalContextRecognizer } from './educational-context-recognizer' - -const logger = createBuildSafeLogger('context-detector') - -export interface ContextDetectionResult { - detectedContext: ContextType - confidence: number - contextualIndicators: ContextualIndicator[] - needsSpecialHandling: boolean - urgency: 'low' | 'medium' | 'high' | 'critical' - metadata: { - transition?: { - from: ContextType - to: ContextType - } - crisisAnalysis?: { - confidence: number - riskLevel: string - } - educationalAnalysis?: { - confidence: number - type: string - complexity: string - topicArea: string - } - [key: string]: unknown - } -} - -export interface ContextualIndicator { - type: string - description: string - confidence: number - severity?: number -} - -export interface ContextDetectorConfig { - aiService: AIService - crisisDetectionService?: CrisisDetectionService - educationalContextRecognizer?: EducationalContextRecognizer - model?: string - enableCrisisIntegration?: boolean - enableEducationalRecognition?: boolean -} - -/** - * System prompt for context detection - */ -const CONTEXT_DETECTION_PROMPT = `You are a mental health context analysis system. Analyze the user's message to determine the primary context type and provide detailed indicators. - -Context Types: -- CRISIS: Immediate safety concerns, self-harm, suicidal ideation, abuse, severe psychological distress -- EDUCATIONAL: Learning about mental health concepts, conditions, treatments, or general information -- SUPPORT: Seeking emotional support, validation, or coping strategies for ongoing challenges -- CLINICAL_ASSESSMENT: Seeking professional evaluation, diagnosis, or clinical guidance -- INFORMATIONAL: Requesting factual information about resources, services, or procedures -- GENERAL: Casual conversation, check-ins, or unclear intent - -Respond in JSON format with: -- detectedContext: one of the context types above -- confidence: number from 0-1 indicating confidence in classification -- contextualIndicators: array of objects with type, description, and confidence -- needsSpecialHandling: boolean indicating if special protocols are needed -- urgency: "low", "medium", "high", or "critical" -- metadata: object with additional context-specific information - -Be thorough in identifying indicators but prioritize safety - if there's any indication of crisis, classify as CRISIS.` - -/** - * Context Detection System - * - * Analyzes conversation context to determine appropriate objective prioritization - */ -export class ContextDetector { - private readonly aiService: AIService - private readonly crisisDetectionService: CrisisDetectionService | undefined - private readonly educationalContextRecognizer: - | EducationalContextRecognizer - | undefined - private readonly model: string - private readonly enableCrisisIntegration: boolean - private readonly enableEducationalRecognition: boolean - - constructor(config: ContextDetectorConfig) { - this.aiService = config.aiService - this.crisisDetectionService = config.crisisDetectionService ?? undefined - this.educationalContextRecognizer = - config.educationalContextRecognizer ?? undefined - this.model = config.model ?? 'gpt-4' - this.enableCrisisIntegration = config.enableCrisisIntegration ?? true - this.enableEducationalRecognition = - config.enableEducationalRecognition ?? true - } - - /** - * Detect context from user input - */ - async detectContext( - userInput: string, - conversationHistory?: string[], - userId?: string, - ): Promise { - try { - // First, check for crisis if integration is enabled - let crisisResult: Record | null = null - if (this.enableCrisisIntegration && this.crisisDetectionService) { - const crisisOptions = { - sensitivityLevel: 'medium' as const, - userId: userId ?? 'anonymous', - source: 'context-detection', - } - crisisResult = (await this.crisisDetectionService.detectCrisis( - userInput, - crisisOptions, - )) as unknown as Record - if (crisisResult?.['isCrisis']) { - return { - detectedContext: ContextType.CRISIS, - confidence: crisisResult['confidence'] as number, - contextualIndicators: [ - { - type: 'crisis_detection', - description: - (crisisResult['category'] as string) ?? 'Crisis detected', - confidence: crisisResult['confidence'] as number, - severity: this.mapRiskLevelToNumber( - crisisResult['riskLevel'] as string, - ), - }, - ], - needsSpecialHandling: true, - urgency: this.mapUrgencyFromCrisis( - crisisResult['urgency'] as string, - ), - metadata: { - crisisResult, - suggestedActions: crisisResult['suggestedActions'], - }, - } - } - } - - // Resource/service queries should short-circuit to informational before deeper analysis - if (this.isInformationalQuery(userInput)) { - return this.buildInformationalResult() - } - - let educationalResult: Record | null = null - if ( - this.enableEducationalRecognition && - this.educationalContextRecognizer - ) { - educationalResult = - (await this.educationalContextRecognizer.recognizeEducationalContext( - userInput, - undefined, // userProfile - would need to be passed through - conversationHistory, - )) as unknown as Record - if ( - educationalResult?.['isEducational'] && - (educationalResult['confidence'] as number) > 0.8 - ) { - return { - detectedContext: ContextType.EDUCATIONAL, - confidence: educationalResult['confidence'] as number, - contextualIndicators: [ - { - type: 'educational_recognition', - description: `Educational ${educationalResult['educationalType'] as string} about ${educationalResult['topicArea'] as string}`, - confidence: educationalResult['confidence'] as number, - }, - ], - needsSpecialHandling: - (educationalResult['complexity'] as string) === 'advanced', - urgency: 'low', - metadata: { - educationalResult, - learningObjectives: educationalResult['learningObjectives'], - recommendedResources: educationalResult['recommendedResources'], - }, - } - } - } - if (this.isClinicalAssessment(userInput)) { - return { - detectedContext: ContextType.CLINICAL_ASSESSMENT, - confidence: 0.92, - contextualIndicators: [ - { - type: 'clinical_assessment_pattern', - description: - 'Clinical evaluation/assessment language detected (diagnosis, assessment intent)', - confidence: 0.9, - }, - ], - needsSpecialHandling: true, - urgency: 'medium', - metadata: { - matchedPattern: 'clinical_assessment_query', - }, - } - } - - const messages: Array<{ - role: 'system' | 'user' | 'assistant' - content: string - }> = [ - { - role: 'system', - content: CONTEXT_DETECTION_PROMPT, - }, - { - role: 'user', - content: this.formatInputForAnalysis(userInput, conversationHistory), - }, - ] - - const response = (await this.aiService.createChatCompletion(messages, { - model: this.model, - })) as unknown as Record - - // Support multiple provider response shapes - // 1) Minimal: { content: string } - // 2) OpenAI-like: { choices: [{ message: { content: string } }] } - // 3) Direct string: string - let content = '' - if (typeof response === 'string') { - content = response - } else if (response && typeof response === 'object') { - const r = response - if (typeof r['content'] === 'string') { - content = r['content'] - } else if ( - Array.isArray(r['choices']) && - (r['choices'] as Array<{ message?: { content?: string } }>)[0]?.[ - 'message' - ]?.['content'] - ) { - content = String( - (r['choices'] as Array<{ message?: { content?: string } }>)[0]?.[ - 'message' - ]?.['content'], - ) - } - } - - const result = this.parseContextDetectionResponse(content) - - // Merge crisis detection data if available - if (crisisResult && !crisisResult['isCrisis']) { - ;(result['metadata'] as Record)['crisisAnalysis'] = { - confidence: crisisResult['confidence'] as number, - riskLevel: crisisResult['riskLevel'] as string, - } - } - - // Merge educational analysis if available - if (educationalResult?.['isEducational']) { - ;(result['metadata'] as Record)[ - 'educationalAnalysis' - ] = { - confidence: educationalResult['confidence'] as number, - type: educationalResult['educationalType'] as string, - complexity: educationalResult['complexity'] as string, - topicArea: educationalResult['topicArea'] as string, - } - } - - // Prefer AI-mocked model result for educational context, if detectedContext is "educational", even when pattern is matched. So do a fix here: - if ( - this.looksLikeEducationalQuery(userInput) && - !this.isInformationalQuery(userInput) && - result.detectedContext === ContextType.EDUCATIONAL - ) { - // Patch: Use confidence and indicator from model result (from test mocks) instead of static fallback - return { - ...result, - detectedContext: ContextType.EDUCATIONAL, - confidence: result['confidence'] ?? 0.85, - contextualIndicators: result['contextualIndicators']?.length - ? result['contextualIndicators'] - : [ - { - type: 'educational_pattern', - description: - 'Detected educational query (learning about mental health concept/condition/treatment)', - confidence: result['confidence'] ?? 0.8, - }, - ], - needsSpecialHandling: false, - urgency: 'low', - metadata: { - ...result.metadata, - note: 'Matched fallback educational query pattern', - }, - } - } - - // EDUCATIONAL FALLBACK: Always assign EDUCATIONAL if pattern matches, unless it's already crisis or clinical. - // This must occur before informational/general fallback. - if ( - result.detectedContext !== ContextType.CRISIS && - result.detectedContext !== ContextType.CLINICAL_ASSESSMENT && - !this.isInformationalQuery(userInput) && - result.detectedContext !== ContextType.EDUCATIONAL && - this.looksLikeEducationalQuery(userInput) - ) { - return { - detectedContext: ContextType.EDUCATIONAL, - confidence: 0.8, - contextualIndicators: [ - { - type: 'educational_pattern', - description: - 'Detected educational query (learning about mental health concept/condition/treatment)', - confidence: 0.8, - }, - ], - needsSpecialHandling: false, - urgency: 'low', - metadata: { - note: 'Matched fallback educational query pattern', - ...result.metadata, - }, - } - } - - // Run informational query check only if general and NOT matched as educational - if ( - result.detectedContext === ContextType.GENERAL && - !this.looksLikeEducationalQuery(userInput) && - this.isInformationalQuery(userInput) - ) { - return this.buildInformationalResult() - } - - logger.info('Context detected', { - context: result['detectedContext'] as string, - confidence: result['confidence'], - urgency: result['urgency'] as string, - }) - - return result - } catch (error: unknown) { - logger.error('Error detecting context:', error) - - // Fallback to general context with low confidence - return { - detectedContext: ContextType.GENERAL, - confidence: 0.1, - contextualIndicators: [ - { - type: 'error_fallback', - description: 'Context detection failed, using general fallback', - confidence: 0.1, - }, - ], - needsSpecialHandling: false, - urgency: 'low', - metadata: { - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - }, - } - } - } - - /** - * Batch context detection for multiple inputs - */ - async detectContextBatch( - inputs: Array<{ - text: string - conversationHistory?: string[] - userId?: string - }>, - ): Promise { - return Promise.all( - inputs.map(async (input) => - this.detectContext( - input['text'], - input['conversationHistory'], - input['userId'], - ), - ), - ) - } - - /** - * Create alignment context from detection result - */ - createAlignmentContext( - userQuery: string, - detectionResult: ContextDetectionResult, - ): AlignmentContext { - return { - userQuery, - detectedContext: detectionResult.detectedContext, - sessionMetadata: { - confidence: detectionResult.confidence, - urgency: detectionResult.urgency, - needsSpecialHandling: detectionResult.needsSpecialHandling, - contextualIndicators: detectionResult.contextualIndicators, - ...detectionResult.metadata, - }, - } - } - - /** - * Format input for analysis - */ - private formatInputForAnalysis( - userInput: string, - conversationHistory?: string[], - ): string { - let formatted = `Current message: ${userInput}` - - if (conversationHistory && conversationHistory.length > 0) { - const recentHistory = conversationHistory.slice(-5).join('\n') - formatted += `\n\nRecent conversation context:\n${recentHistory}` - } - - return formatted - } - - /** - * Parse context detection response - */ - private parseContextDetectionResponse( - content: string, - ): ContextDetectionResult { - // Helper: extract JSON from possible code fences or surrounding text - const extractJson = (text: string): string | null => { - if (!text) return null - const fenced = text.match(/```(?:json)?\n([\s\S]*?)```/i) - if (fenced?.[1]) { - return fenced[1].trim() - } - // Try to locate first balanced JSON object in text - const start = text.indexOf('{') - const end = text.lastIndexOf('}') - if (start !== -1 && end !== -1 && end > start) { - return text.slice(start, end + 1) - } - return text.trim() - } - - const normalizeContext = (value: unknown): ContextType => { - if (typeof value === 'string') { - const v = value.toUpperCase() - if (v in ContextType) { - return ContextType[v as keyof typeof ContextType] - } - } - return ContextType.GENERAL - } - - const clamp01 = (n: unknown, fallback = 0.5): number => { - const x = typeof n === 'number' ? n : Number(n) - if (Number.isFinite(x)) return Math.min(1, Math.max(0, x)) - return fallback - } - - const normalizeUrgency = ( - u: unknown, - ): 'low' | 'medium' | 'high' | 'critical' => { - switch (typeof u === 'string' ? u.toLowerCase() : '') { - case 'low': - case 'medium': - case 'high': - case 'critical': - return u as 'low' | 'medium' | 'high' | 'critical' - default: - return 'low' - } - } - - const safeObject = (v: unknown): Record => - v && typeof v === 'object' && !Array.isArray(v) - ? (v as Record) - : {} - - const safeIndicators = (v: unknown): ContextualIndicator[] => { - if (!Array.isArray(v)) return [] - return v - .map((it) => - it && typeof it === 'object' - ? { - type: String(it['type'] ?? 'indicator'), - description: String(it['description'] ?? ''), - confidence: clamp01(it['confidence'] ?? 0.5, 0.5), - ...(typeof it['severity'] === 'number' - ? { severity: it['severity'] } - : {}), - } - : null, - ) - .filter(Boolean) as ContextualIndicator[] - } - - try { - const jsonText = extractJson(content) ?? '' - const parsed = JSON.parse(jsonText) - - return { - detectedContext: normalizeContext(parsed['detectedContext']), - confidence: clamp01(parsed['confidence'], 0.5), - contextualIndicators: safeIndicators(parsed['contextualIndicators']), - needsSpecialHandling: Boolean(parsed['needsSpecialHandling'] ?? false), - urgency: normalizeUrgency(parsed['urgency']), - metadata: safeObject(parsed['metadata']), - } - } catch { - // Fallback to general context with minimal signal to avoid throwing in tests - return { - detectedContext: ContextType.GENERAL, - confidence: 0.3, - contextualIndicators: [], - needsSpecialHandling: false, - urgency: 'low', - metadata: {}, - } - } - } - - /** - * Pattern-based fallback: detect if user input is an educational query - * Catches: "What is X?", "Tell me about Y", "Explain Z", "Give me information about X" etc, with heuristic filters for mental health concepts - */ - private looksLikeEducationalQuery(input: string): boolean { - // Heuristic: basic psychoeducational Qs about concepts/symptoms/treatments. - // Exclude resource/service/booking/cost Qs (handled informational); if ambiguous, prefer educational. - const patterns = [ - /\b(what is|what are|tell me about|explain|how does|how do(es)?|define|meaning of|describe)\b.*?\b(anxiety|depression|bipolar|adhd|autism|trauma|ptsd|stress|mental health|therapy|treatment|coping|psychosis|obsessive|compulsive|addiction|self-esteem|cbt|dbt|mindfulness|resilience|wellbeing|grief|panic|fear|worry|emotion|feeling|diagnos[ei]s?|symptom|mood|support|psychology|counsel|\bmedicat(ed|ion)?\b|meds|psychiatr|neurodiver(se|sity))\b/i, - // Broader educational phrase - /\b(i want to learn|can you teach me|give me an overview|education about|information on|how to manage|how to cope|help me understand)\b/i, - // Simple factual but educational mental-health context - /\b(signs of|causes of|risk factors for|ways to improve|benefits of|impact of|types of)\b.*\b(anxiety|depression|ptsd|adhd|autism|addiction|stress|psychosis|wellbeing|coping|therapy|diagnos[ei]s?)\b/i, - ] - // Only match if _not_ clearly resource/cost/location/service ("how do I book...", "what is the cost...") and not clinical assessment - if (this.isInformationalQuery(input) || this.isClinicalAssessment(input)) { - return false - } - return patterns.some((re) => re.test(input)) - } - - /** - * Detects if a query is likely a clinical assessment/diagnosis request. - * Includes PHQ-9 scoring requests, risk language, DSM-5 criteria references. - */ - private isClinicalAssessment(input: string): boolean { - const patterns = [ - // Diagnosis-related patterns - /\b(diagnos(e|is|ed|ing)|diagnosis|diagnostic)\b/i, - /\b(assess(ment|ing)?|evaluate|clinical evaluation|clinical assessment|screening)\b/i, - /\b(symptom checker|self-assess|official diagnosis)\b/i, - /\b(?:do I (have|need|require) (an? )?(assessment|diagnos(e|is)?|screening|evaluation)|am I (depressed|anxious|bipolar|autistic|ptsd|adhd|psychotic|suicidal))\b/i, - /\bwhat (is|are) my (diagnosis|symptoms|condition)\b/i, - - // PHQ-9 and clinical assessment tools - /\b(phq-?9|phq-?2|gad-?7|pcl-?5|bdi|beck depression inventory|hamilton rating scale|madrs)\b/i, - /\b(score|scoring|calculate|interpret) (my|the|these)? ?(assessment|questionnaire|scale|inventory|test)\b/i, - - // DSM-5 and clinical criteria references - /\b(dsm-?5|dsm-?iv|icd-?10|icd-?11|diagnostic criteria|clinical criteria)\b/i, - /\bmeet(ing)? (the )?(criteria|requirements) (for|of)\b/i, - - // Risk assessment language - /\b(risk assessment|suicide risk|self-harm risk|safety assessment|danger to (self|others))\b/i, - /\b(severity|level|degree) (of|score|rating)\b.*\b(depression|anxiety|ptsd|trauma|stress|symptoms)\b/i, - - // Professional evaluation requests - /\b(professional (evaluation|assessment|opinion)|need to see (a|my) (doctor|psychiatrist|psychologist|therapist))\b/i, - /\b(medical (diagnosis|evaluation)|psychiatric (evaluation|assessment))\b/i, - ] - return patterns.some((re) => re.test(input)) - } - - /** - * Detects if a query is likely an informational resource/service/fact request. - */ - private isInformationalQuery(input: string): boolean { - const patterns = [ - // Resource and access queries - /\b(resource|resources|hotline|crisis line|helpline|service|services|website|contact|number to call|phone number|support group|support groups|find a counselor|find a therapist)\b/i, - /\b(where can i (find|get|access)|how do i (get|access|reach|contact)|how to reach)\b/i, - // Logistics and availability - /\b(insurance|cost|affordable|sliding scale|coverage|location|hours of operation|opening hours|appointment|book (a )?session|sign up|register|availability)\b/i, - // Informational phrasing tied to resources/services - /\b(info(?:rmation)? on|information about)\b.*\b(resource|service|hotline|support|counseling|therapy|session|program|group)s?\b/i, - ] - return patterns.some((re) => re.test(input)) - } - - private buildInformationalResult(): ContextDetectionResult { - return { - detectedContext: ContextType.INFORMATIONAL, - confidence: 0.85, - contextualIndicators: [ - { - type: 'informational_pattern', - description: - 'Detected fact/resource/service query without emotional/support/clinical content', - confidence: 0.8, - }, - ], - needsSpecialHandling: false, - urgency: 'low', - metadata: { - matchedPattern: 'informational_query', - }, - } - } - - /** - * Map risk level to number for severity scoring - */ - private mapRiskLevelToNumber(riskLevel: string): number { - switch (riskLevel) { - case 'low': - return 1 - case 'medium': - return 2 - case 'high': - return 3 - case 'critical': - return 4 - default: - return 1 - } - } - - /** - * Map crisis urgency to context urgency - */ - private mapUrgencyFromCrisis( - urgency: string, - ): 'low' | 'medium' | 'high' | 'critical' { - switch (urgency) { - case 'immediate': - return 'critical' - case 'high': - return 'high' - case 'medium': - return 'medium' - case 'low': - return 'low' - default: - return 'medium' - } - } -} diff --git a/apps/web/src/lib/metaaligner/prioritization/context-objective-mapper.ts b/apps/web/src/lib/metaaligner/prioritization/context-objective-mapper.ts deleted file mode 100644 index ea70360c37..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/context-objective-mapper.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Context-Objective Mapper - * Maps different context types to appropriate objective weights for mental health AI - */ - -import { ContextType } from '../core/objectives' - -export interface ContextualObjectiveWeights { - empathy: number - safety: number - correctness: number - professionalism: number - informativeness: number - bias_detection?: number - cultural_sensitivity?: number -} - -/** - * Get contextual objective weights based on the detected context - */ -export function getContextualObjectiveWeights( - context: ContextType, -): ContextualObjectiveWeights { - switch (context) { - case ContextType.CRISIS: - return { - safety: 0.9, - empathy: 0.1, - professionalism: 0.05, - correctness: 0.05, - informativeness: 0.0, - } - - case ContextType.SUPPORT: - return { - empathy: 0.35, - safety: 0.25, - professionalism: 0.2, - correctness: 0.15, - informativeness: 0.05, - } - - case ContextType.INFORMATIONAL: - return { - correctness: 0.3, - informativeness: 0.3, - professionalism: 0.2, - empathy: 0.15, - safety: 0.05, - } - - case ContextType.EDUCATIONAL: - return { - correctness: 0.35, - informativeness: 0.35, - professionalism: 0.15, - empathy: 0.1, - safety: 0.05, - } - - case ContextType.CLINICAL_ASSESSMENT: - return { - correctness: 0.35, - safety: 0.3, - professionalism: 0.25, - empathy: 0.08, - informativeness: 0.02, - } - - case ContextType.GENERAL: - default: - return { - empathy: 0.2, - safety: 0.2, - correctness: 0.2, - professionalism: 0.2, - informativeness: 0.2, - } - } -} - -/** - * Get default objective weights when no specific context is detected - */ -export function getDefaultObjectiveWeights(): ContextualObjectiveWeights { - return getContextualObjectiveWeights(ContextType.GENERAL) -} diff --git a/apps/web/src/lib/metaaligner/prioritization/context-objective-mapping.ts b/apps/web/src/lib/metaaligner/prioritization/context-objective-mapping.ts deleted file mode 100644 index bd3a40a4f4..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/context-objective-mapping.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Context-to-Objective Mapping System for MetaAligner -// Maps detected context types to prioritized objectives for multi-objective alignment. - -import { ContextType } from '../core/objectives' - -/** - * Core alignment objectives as string enums for clarity. - */ -export enum ObjectiveKey { - Correctness = 'correctness', - Informativeness = 'informativeness', - Professionalism = 'professionalism', - Empathy = 'empathy', - Safety = 'safety', - Support = 'support', -} - -export interface ObjectivePriority { - key: ObjectiveKey - priority: number // 1 = highest, larger = lower priority - weight: number // 0..1 normalized for weighting algorithms -} - -/** - * Mapping from a context type to a prioritized list of objectives. - * The list order and weighting can be adjusted per domain needs. - */ -const contextObjectiveMap: Record = { - [ContextType.CRISIS]: [ - { key: ObjectiveKey.Safety, priority: 1, weight: 1.0 }, - { key: ObjectiveKey.Empathy, priority: 2, weight: 0.2 }, - { key: ObjectiveKey.Correctness, priority: 3, weight: 0.1 }, - { key: ObjectiveKey.Professionalism, priority: 4, weight: 0.1 }, - { key: ObjectiveKey.Informativeness, priority: 5, weight: 0.0 }, - ], - [ContextType.EDUCATIONAL]: [ - { key: ObjectiveKey.Informativeness, priority: 1, weight: 1.0 }, - { key: ObjectiveKey.Correctness, priority: 2, weight: 0.9 }, - { key: ObjectiveKey.Professionalism, priority: 3, weight: 0.7 }, - { key: ObjectiveKey.Empathy, priority: 4, weight: 0.5 }, - ], - [ContextType.SUPPORT]: [ - { key: ObjectiveKey.Empathy, priority: 1, weight: 1.0 }, - { key: ObjectiveKey.Support, priority: 2, weight: 0.9 }, - { key: ObjectiveKey.Professionalism, priority: 3, weight: 0.7 }, - { key: ObjectiveKey.Correctness, priority: 4, weight: 0.6 }, - ], - [ContextType.CLINICAL_ASSESSMENT]: [ - { key: ObjectiveKey.Correctness, priority: 1, weight: 1.0 }, - { key: ObjectiveKey.Professionalism, priority: 2, weight: 0.9 }, - { key: ObjectiveKey.Empathy, priority: 3, weight: 0.7 }, - { key: ObjectiveKey.Safety, priority: 4, weight: 0.7 }, - ], - [ContextType.INFORMATIONAL]: [ - { key: ObjectiveKey.Informativeness, priority: 1, weight: 1.0 }, - { key: ObjectiveKey.Correctness, priority: 2, weight: 0.9 }, - { key: ObjectiveKey.Professionalism, priority: 3, weight: 0.7 }, - ], - [ContextType.GENERAL]: [ - { key: ObjectiveKey.Correctness, priority: 1, weight: 0.8 }, - { key: ObjectiveKey.Professionalism, priority: 2, weight: 0.7 }, - { key: ObjectiveKey.Empathy, priority: 3, weight: 0.6 }, - ], -} - -/** - * Returns prioritized objectives for the given context type. - * Falls back to GENERAL if the supplied context is unrecognized. - */ -export function getPrioritizedObjectivesForContext( - context: ContextType, -): ObjectivePriority[] { - return ( - contextObjectiveMap[context] ?? contextObjectiveMap[ContextType.GENERAL] - ) -} diff --git a/apps/web/src/lib/metaaligner/prioritization/context-transition-detector.test.ts b/apps/web/src/lib/metaaligner/prioritization/context-transition-detector.test.ts deleted file mode 100644 index 99771c1671..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/context-transition-detector.test.ts +++ /dev/null @@ -1,778 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' - -import { ContextType } from '../core/objectives' -import { - ContextTransitionDetector, - detectContextTransition, - type ContextEvent, - type ContextTransition, - type TransitionDetectorConfig, -} from './context-transition-detector' - -describe('ContextTransitionDetector', () => { - let detector: ContextTransitionDetector - let config: TransitionDetectorConfig - - beforeEach(() => { - config = { - minConfidenceThreshold: 0.7, - smoothingWindow: 2, - enableCrisisElevation: true, - enableTelemetry: true, - } - detector = new ContextTransitionDetector(config) - }) - - describe('constructor', () => { - it('should initialize with default config', () => { - const defaultDetector = new ContextTransitionDetector() - expect(defaultDetector).toBeDefined() - }) - - it('should initialize with custom config', () => { - const customDetector = new ContextTransitionDetector({ - minConfidenceThreshold: 0.8, - smoothingWindow: 3, - enableCrisisElevation: false, - }) - expect(customDetector).toBeDefined() - }) - }) - - describe('addEvent and transition detection', () => { - it('should return null for first event (no previous context)', () => { - const event: ContextEvent = { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - } - - const transition = detector.addEvent(event) - expect(transition).toBeNull() - }) - - it('should detect no transition when context stays the same', () => { - const event1: ContextEvent = { - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - } - - const event2: ContextEvent = { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.85, - urgency: 'low', - timestamp: Date.now() + 1000, - } - - detector.addEvent(event1) - const transition = detector.addEvent(event2) - - expect(transition).not.toBeNull() - expect(transition!.detected).toBe(false) - expect(transition!.transitionType).toBe('none') - }) - - it('should detect crisis elevation immediately without smoothing', () => { - const event1: ContextEvent = { - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - } - - const event2: ContextEvent = { - turnId: 2, - contextType: ContextType.CRISIS, - confidence: 0.95, - urgency: 'critical', - timestamp: Date.now() + 1000, - } - - detector.addEvent(event1) - const transition = detector.addEvent(event2) - - expect(transition).not.toBeNull() - expect(transition!.detected).toBe(true) - expect(transition!.transitionType).toBe('crisis_elevation') - expect(transition!.shouldSmooth).toBe(false) - expect(transition!.confidence).toBe(1.0) - }) - - it('should apply smoothing to standard transitions', () => { - const event1: ContextEvent = { - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - } - - const event2: ContextEvent = { - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.8, - urgency: 'medium', - timestamp: Date.now() + 1000, - } - - detector.addEvent(event1) - const transition = detector.addEvent(event2) - - // First detection should be smoothed - expect(transition).not.toBeNull() - expect(transition!.detected).toBe(false) - expect(transition!.transitionType).toBe('standard') - expect(transition!.shouldSmooth).toBe(true) - }) - - it('should confirm transition after smoothing window', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - // First support detection - smoothing - const transition1 = detector.addEvent({ - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.8, - urgency: 'medium', - timestamp: Date.now() + 1000, - }) - expect(transition1!.shouldSmooth).toBe(true) - expect(transition1!.detected).toBe(false) - - // Second consecutive support detection - confirmed - const transition2 = detector.addEvent({ - turnId: 3, - contextType: ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium', - timestamp: Date.now() + 2000, - }) - expect(transition2!.shouldSmooth).toBe(false) - expect(transition2!.detected).toBe(true) - }) - - it('should reject low confidence transitions', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - const transition = detector.addEvent({ - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.5, // Below threshold (0.7) - urgency: 'medium', - timestamp: Date.now() + 1000, - }) - - expect(transition!.shouldSmooth).toBe(true) - expect(transition!.detected).toBe(false) - }) - }) - - describe('crisis elevation scenarios', () => { - it('should elevate from educational to crisis immediately', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - const transition = detector.addEvent({ - turnId: 2, - contextType: ContextType.CRISIS, - confidence: 0.95, - urgency: 'critical', - timestamp: Date.now() + 500, - }) - - expect(transition!.detected).toBe(true) - expect(transition!.transitionType).toBe('crisis_elevation') - expect(transition!.from.contextType).toBe(ContextType.EDUCATIONAL) - expect(transition!.to.contextType).toBe(ContextType.CRISIS) - }) - - it('should elevate from support to crisis immediately', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.SUPPORT, - confidence: 0.8, - urgency: 'medium', - timestamp: Date.now(), - }) - - const transition = detector.addEvent({ - turnId: 2, - contextType: ContextType.CRISIS, - confidence: 0.98, - urgency: 'critical', - timestamp: Date.now() + 300, - }) - - expect(transition!.detected).toBe(true) - expect(transition!.transitionType).toBe('crisis_elevation') - expect(transition!.shouldSmooth).toBe(false) - }) - - it('should elevate from general to crisis immediately', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.5, - urgency: 'low', - timestamp: Date.now(), - }) - - const transition = detector.addEvent({ - turnId: 2, - contextType: ContextType.CRISIS, - confidence: 0.92, - urgency: 'critical', - timestamp: Date.now() + 200, - }) - - expect(transition!.detected).toBe(true) - expect(transition!.transitionType).toBe('crisis_elevation') - expect(transition!.confidence).toBe(1.0) - }) - }) - - describe('smoothing behavior', () => { - it('should clear pending transitions when context reverts', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - // Detect support once - detector.addEvent({ - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.8, - urgency: 'medium', - timestamp: Date.now() + 1000, - }) - - // Revert to educational - const transition = detector.addEvent({ - turnId: 3, - contextType: ContextType.EDUCATIONAL, - confidence: 0.85, - urgency: 'low', - timestamp: Date.now() + 2000, - }) - - expect(transition!.detected).toBe(false) - expect(transition!.transitionType).toBe('none') - }) - - it('should handle oscillating contexts with smoothing', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - // Oscillate between educational and support - detector.addEvent({ - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.75, - urgency: 'medium', - timestamp: Date.now() + 1000, - }) - - detector.addEvent({ - turnId: 3, - contextType: ContextType.EDUCATIONAL, - confidence: 0.8, - urgency: 'low', - timestamp: Date.now() + 2000, - }) - - const transition = detector.addEvent({ - turnId: 4, - contextType: ContextType.SUPPORT, - confidence: 0.78, - urgency: 'medium', - timestamp: Date.now() + 3000, - }) - - // Should be smoothing due to oscillation - expect(transition!.shouldSmooth).toBe(true) - }) - }) - - describe('history management', () => { - it('should track current context', () => { - expect(detector.getCurrentContext()).toBeNull() - - const event: ContextEvent = { - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - } - - detector.addEvent(event) - const current = detector.getCurrentContext() - - expect(current).not.toBeNull() - expect(current!.contextType).toBe(ContextType.EDUCATIONAL) - }) - - it('should maintain bounded history (max 50 events)', () => { - // Add 60 events - for (let i = 0; i < 60; i++) { - detector.addEvent({ - turnId: i, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + i * 1000, - }) - } - - const history = detector.getHistory(100) - expect(history.length).toBeLessThanOrEqual(50) - }) - - it('should retrieve recent history', () => { - for (let i = 0; i < 10; i++) { - detector.addEvent({ - turnId: i, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + i * 1000, - }) - } - - const recent = detector.getHistory(5) - expect(recent.length).toBe(5) - expect(recent?.[4].turnId).toBe(9) - }) - - it('should clear history and pending transitions', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - detector.clearHistory() - expect(detector.getCurrentContext()).toBeNull() - expect(detector.getHistory().length).toBe(0) - }) - }) - - describe('transition statistics', () => { - it('should calculate basic stats', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - const stats = detector.getTransitionStats() - expect(stats.totalEvents).toBe(1) - expect(stats.transitions).toBe(0) - expect(stats.crisisElevations).toBe(0) - }) - - it('should count transitions correctly', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - detector.addEvent({ - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium', - timestamp: Date.now() + 1000, - }) - - detector.addEvent({ - turnId: 3, - contextType: ContextType.INFORMATIONAL, - confidence: 0.8, - urgency: 'low', - timestamp: Date.now() + 2000, - }) - - const stats = detector.getTransitionStats() - expect(stats.totalEvents).toBe(3) - expect(stats.transitions).toBe(2) - }) - - it('should count crisis elevations', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - detector.addEvent({ - turnId: 2, - contextType: ContextType.CRISIS, - confidence: 0.95, - urgency: 'critical', - timestamp: Date.now() + 1000, - }) - - const stats = detector.getTransitionStats() - expect(stats.crisisElevations).toBe(1) - }) - - it('should calculate average confidence', () => { - detector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.8, - urgency: 'low', - timestamp: Date.now(), - }) - - detector.addEvent({ - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.9, - urgency: 'medium', - timestamp: Date.now() + 1000, - }) - - const stats = detector.getTransitionStats() - expect(stats.averageConfidence).toBeCloseTo(0.85, 2) - }) - }) - - describe('synthetic dialogue scenarios', () => { - it('should handle educational to clinical transition', () => { - const events: ContextEvent[] = [ - { - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }, - { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.85, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - { - turnId: 3, - contextType: ContextType.CLINICAL_ASSESSMENT, - confidence: 0.88, - urgency: 'medium', - timestamp: Date.now() + 2000, - }, - { - turnId: 4, - contextType: ContextType.CLINICAL_ASSESSMENT, - confidence: 0.92, - urgency: 'medium', - timestamp: Date.now() + 3000, - }, - ] - - const transitions: ReturnType[] = [] - events.forEach((event) => { - const transition = detector.addEvent(event) - if (transition?.detected) { - transitions.push(transition) - } - }) - - expect(transitions).toHaveLength(1) - expect(transitions[0]!.from.contextType).toBe(ContextType.EDUCATIONAL) - expect(transitions[0]!.to.contextType).toBe( - ContextType.CLINICAL_ASSESSMENT, - ) - }) - - it('should handle support to crisis dialogue', () => { - const dialogue = [ - { - context: ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium' as const, - }, - { - context: ContextType.SUPPORT, - confidence: 0.8, - urgency: 'medium' as const, - }, - { - context: ContextType.CRISIS, - confidence: 0.95, - urgency: 'critical' as const, - }, - ] - - const crisisTransitions: ReturnType[] = [] - dialogue - .map((turn, index) => { - const event: ContextEvent = { - turnId: index + 1, - contextType: turn.context, - confidence: turn.confidence, - urgency: turn.urgency, - timestamp: Date.now() + index * 1000, - } - - const transition = detector.addEvent(event) - if (transition?.transitionType === 'crisis_elevation') { - crisisTransitions.push(transition) - } - return transition - }) - .filter( - (transition) => transition?.transitionType === 'crisis_elevation', - ) - - expect(crisisTransitions).toHaveLength(1) - expect(crisisTransitions[0]!.detected).toBe(true) - expect(crisisTransitions[0]!.shouldSmooth).toBe(false) - }) - - it('should handle complex multi-context dialogue', () => { - const dialogue = [ - { - context: ContextType.GENERAL, - confidence: 0.6, - urgency: 'low' as const, - }, - { - context: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low' as const, - }, - { - context: ContextType.EDUCATIONAL, - confidence: 0.88, - urgency: 'low' as const, - }, - { - context: ContextType.SUPPORT, - confidence: 0.82, - urgency: 'medium' as const, - }, - { - context: ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium' as const, - }, - { - context: ContextType.INFORMATIONAL, - confidence: 0.9, - urgency: 'low' as const, - }, - ] - - const transitions: ContextTransition[] = [] - dialogue.forEach((turn, index) => { - const event: ContextEvent = { - turnId: index + 1, - contextType: turn.context, - confidence: turn.confidence, - urgency: turn.urgency, - timestamp: Date.now() + index * 1000, - } - - const transition = detector.addEvent(event) - if (transition?.detected) { - transitions.push(transition) - } - }) - - // Should have at least 2 confirmed transitions (general->edu, support, info) - expect(transitions.length).toBeGreaterThanOrEqual(2) - }) - }) - - describe('configuration behavior', () => { - it('should respect custom confidence threshold', () => { - const customDetector = new ContextTransitionDetector({ - minConfidenceThreshold: 0.9, - smoothingWindow: 2, - }) - - customDetector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.95, - urgency: 'low', - timestamp: Date.now(), - }) - - const transition = customDetector.addEvent({ - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.85, // Below custom threshold (0.9) - urgency: 'medium', - timestamp: Date.now() + 1000, - }) - - expect(transition!.shouldSmooth).toBe(true) - }) - - it('should respect custom smoothing window', () => { - const customDetector = new ContextTransitionDetector({ - smoothingWindow: 3, - minConfidenceThreshold: 0.7, - }) - - customDetector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - // First support detection - customDetector.addEvent({ - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.8, - urgency: 'medium', - timestamp: Date.now() + 1000, - }) - - // Second support detection (still smoothing) - const transition2 = customDetector.addEvent({ - turnId: 3, - contextType: ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium', - timestamp: Date.now() + 2000, - }) - expect(transition2!.shouldSmooth).toBe(true) - - // Third support detection (confirmed) - const transition3 = customDetector.addEvent({ - turnId: 4, - contextType: ContextType.SUPPORT, - confidence: 0.82, - urgency: 'medium', - timestamp: Date.now() + 3000, - }) - expect(transition3!.detected).toBe(true) - }) - - it('should allow disabling crisis elevation', () => { - const noCrisisDetector = new ContextTransitionDetector({ - enableCrisisElevation: false, - smoothingWindow: 2, - }) - - noCrisisDetector.addEvent({ - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }) - - const transition = noCrisisDetector.addEvent({ - turnId: 2, - contextType: ContextType.CRISIS, - confidence: 0.95, - urgency: 'critical', - timestamp: Date.now() + 1000, - }) - - // Should apply smoothing even for crisis - expect(transition!.transitionType).toBe('standard') - expect(transition!.shouldSmooth).toBe(true) - }) - }) - - describe('legacy functions', () => { - it('detectContextTransition should work as standalone function', () => { - const prev: ContextEvent = { - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - } - - const curr: ContextEvent = { - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium', - timestamp: Date.now() + 1000, - } - - const transition = detectContextTransition(prev, curr) - - expect(transition.detected).toBe(true) - expect(transition.from).toBe(prev) - expect(transition.to).toBe(curr) - }) - - it('detectContextTransition should identify crisis elevation', () => { - const prev: ContextEvent = { - turnId: 1, - contextType: ContextType.SUPPORT, - confidence: 0.8, - urgency: 'medium', - timestamp: Date.now(), - } - - const curr: ContextEvent = { - turnId: 2, - contextType: ContextType.CRISIS, - confidence: 0.95, - urgency: 'critical', - timestamp: Date.now() + 1000, - } - - const transition = detectContextTransition(prev, curr) - - expect(transition.transitionType).toBe('crisis_elevation') - expect(transition.shouldSmooth).toBe(false) - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/prioritization/context-transition-detector.ts b/apps/web/src/lib/metaaligner/prioritization/context-transition-detector.ts deleted file mode 100644 index ba3d84f0f1..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/context-transition-detector.ts +++ /dev/null @@ -1,306 +0,0 @@ -/** - * Context Transition Detection & Handling System for MetaAligner - * - * Tracks context changes across conversation turns and manages smooth transitions - * between different context types (crisis, educational, support, clinical, informational). - * - * Features: - * - History-based transition detection - * - Crisis elevation with immediate switching - * - Smoothing for non-critical transitions to avoid oscillation - * - PII-safe logging and telemetry - * - Configurable sensitivity and smoothing parameters - */ - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { ContextType } from '../core/objectives' - -const logger = createBuildSafeLogger('context-transition-detector') - -/** - * Represents metadata associated with each context detection event - */ -export interface ContextEvent { - turnId: string | number - contextType: ContextType - confidence: number - urgency: 'low' | 'medium' | 'high' | 'critical' - meta?: Record - timestamp: number -} - -/** - * Describes a transition between two context detection events - */ -export interface ContextTransition { - from: ContextEvent - to: ContextEvent - detected: boolean - transitionType: 'crisis_elevation' | 'standard' | 'none' - shouldSmooth: boolean - confidence: number -} - -/** - * Handler signature for responding to transitions (extensible) - */ -export type ContextTransitionHandler = ( - transition: ContextTransition, -) => void | Promise - -/** - * Configuration for transition detection behavior - */ -export interface TransitionDetectorConfig { - /** Minimum confidence threshold for accepting a transition */ - minConfidenceThreshold?: number - /** Number of consecutive detections required for non-crisis transitions */ - smoothingWindow?: number - /** Enable immediate crisis elevation without smoothing */ - enableCrisisElevation?: boolean - /** Enable telemetry logging (PII-safe) */ - enableTelemetry?: boolean -} - -/** - * Context Transition Detector - * - * Manages detection and handling of context transitions across conversation turns. - * Implements smoothing to prevent oscillation and immediate crisis elevation. - */ -export class ContextTransitionDetector { - private history: ContextEvent[] = [] - private readonly config: Required - private readonly pendingTransitions: Map = new Map() - private lastConfirmedEvent: ContextEvent | null = null - - constructor(config: TransitionDetectorConfig = {}) { - this.config = { - minConfidenceThreshold: config.minConfidenceThreshold ?? 0.7, - smoothingWindow: config.smoothingWindow ?? 2, - enableCrisisElevation: config.enableCrisisElevation ?? true, - enableTelemetry: config.enableTelemetry ?? true, - } - } - - /** - * Add a new context event and detect transitions - */ - addEvent(event: ContextEvent): ContextTransition | null { - // Ensure timestamp is set - const enrichedEvent: ContextEvent = { - ...event, - timestamp: event.timestamp || Date.now(), - } - - // Get previous event for comparison - const previousEvent = this.getCurrentContext() - - // Add to history - this.history.push(enrichedEvent) - - // Keep history bounded (last 50 events) - if (this.history.length > 50) { - this.history.shift() - } - - // Detect transition if we have a previous context - if (!previousEvent) { - this.lastConfirmedEvent = enrichedEvent - return null - } - - return this.detectTransition(previousEvent, enrichedEvent) - } - - /** - * Detect transition between two events with smoothing logic - */ - private detectTransition( - prev: ContextEvent, - curr: ContextEvent, - ): ContextTransition { - // Use lastConfirmedEvent for stable comparison, falling back to prev if not set - const stableContext = - this.lastConfirmedEvent?.contextType ?? prev.contextType - const contextChanged = stableContext !== curr.contextType - - // Capture the 'from' event before any state updates - const fromEvent = this.lastConfirmedEvent ?? prev - - // Determine transition type - let transitionType: ContextTransition['transitionType'] = 'none' - let shouldSmooth = false - let { confidence } = curr - - if (contextChanged) { - // Check for crisis elevation - if ( - this.config.enableCrisisElevation && - curr.contextType === ContextType.CRISIS - ) { - transitionType = 'crisis_elevation' - shouldSmooth = false // Immediate transition for crisis - confidence = 1.0 // High confidence for crisis elevation - this.lastConfirmedEvent = curr - } else { - // Standard transition - apply smoothing - transitionType = 'standard' - - // Check if confidence meets threshold - if (curr.confidence >= this.config.minConfidenceThreshold) { - // Track pending transition - const count = (this.pendingTransitions.get(curr.contextType) ?? 0) + 1 - this.pendingTransitions.set(curr.contextType, count) - - // Require multiple consecutive detections - if (count >= this.config.smoothingWindow) { - shouldSmooth = false // Transition confirmed - this.lastConfirmedEvent = curr - this.pendingTransitions.clear() - } else { - shouldSmooth = true // Still smoothing - confidence = count / this.config.smoothingWindow - } - } else { - // Low confidence - continue smoothing - shouldSmooth = true - this.pendingTransitions.clear() - } - } - } else { - // No context change - clear pending transitions - this.pendingTransitions.clear() - } - - const transition: ContextTransition = { - from: fromEvent, - to: curr, - detected: contextChanged && !shouldSmooth, - transitionType, - shouldSmooth, - confidence, - } - - // Log transition (PII-safe) - if (this.config.enableTelemetry && transition.detected) { - this.logTransition(transition) - } - - return transition - } - - /** - * Get the current context (most recent event) - */ - getCurrentContext(): ContextEvent | null { - return this.history.length > 0 - ? this.history[this.history.length - 1] - : null - } - - /** - * Get recent context history - */ - getHistory(count: number = 10): ContextEvent[] { - return this.history.slice(-count) - } - - /** - * Clear history (useful for new conversations) - */ - clearHistory(): void { - this.history = [] - this.pendingTransitions.clear() - this.lastConfirmedEvent = null - } - - /** - * Get transition statistics for analysis - */ - getTransitionStats(): { - totalEvents: number - transitions: number - crisisElevations: number - averageConfidence: number - } { - let transitions = 0 - let crisisElevations = 0 - let totalConfidence = - this.history.length > 0 ? this.history[0].confidence : 0 - - for (let i = 1; i < this.history.length; i++) { - const prev = this.history[i - 1] - const curr = this.history[i] - - if (prev.contextType !== curr.contextType) { - transitions++ - if (curr.contextType === ContextType.CRISIS) { - crisisElevations++ - } - } - totalConfidence += curr.confidence - } - - return { - totalEvents: this.history.length, - transitions, - crisisElevations, - averageConfidence: - this.history.length > 0 ? totalConfidence / this.history.length : 0, - } - } - - /** - * PII-safe transition logging - */ - private logTransition(transition: ContextTransition): void { - logger.info('Context transition detected', { - fromContext: transition.from.contextType, - toContext: transition.to.contextType, - transitionType: transition.transitionType, - confidence: transition.confidence, - urgency: transition.to.urgency, - turnId: transition.to.turnId, - timeDelta: transition.to.timestamp - transition.from.timestamp, - }) - } -} - -/** - * Detect context transition between consecutive events (legacy function) - * @deprecated Use ContextTransitionDetector class instead - */ -export function detectContextTransition( - prev: ContextEvent, - curr: ContextEvent, -): ContextTransition { - const detected = prev.contextType !== curr.contextType - const isCrisis = curr.contextType === ContextType.CRISIS - - return { - from: prev, - to: curr, - detected, - transitionType: isCrisis - ? 'crisis_elevation' - : detected - ? 'standard' - : 'none', - shouldSmooth: !isCrisis && detected, - confidence: curr.confidence, - } -} - -/** - * Handle context transition with custom handler - * @deprecated Use ContextTransitionDetector with event listeners instead - */ -export async function handleContextTransition( - transition: ContextTransition, - handler: ContextTransitionHandler, -): Promise { - if (transition.detected) { - await handler(transition) - } -} diff --git a/apps/web/src/lib/metaaligner/prioritization/educational-context-recognizer.test.ts b/apps/web/src/lib/metaaligner/prioritization/educational-context-recognizer.test.ts deleted file mode 100644 index 8296a56e63..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/educational-context-recognizer.test.ts +++ /dev/null @@ -1,719 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -/** - * Unit tests for Educational Context Recognition System - */ - -import type { AIService } from '../../ai/models/ai-types' -import { - EducationalContextRecognizer, - createEducationalContextRecognizer, - getDefaultEducationalRecognizerConfig, - EducationalType, - TopicArea, - ResourceType, -} from './educational-context-recognizer' -import type { - EducationalRecognizerConfig, - EducationalContextResult, -} from './educational-context-recognizer' - -// Mock dependencies -const mockAIService: AIService = { - getModelInfo: - vi.fn< - (model: string) => ReturnType> - >(), - createChatCompletion: - vi.fn< - ( - messages: Parameters[0], - options?: Parameters[1], - ) => ReturnType - >(), - // @ts-expect-error - createChatStream is not part of AIService interface but used in tests - createChatStream: - vi.fn< - ( - messages: Parameters[0], - options?: Parameters[1], - ) => ReturnType - >(), -} - -describe('EducationalContextRecognizer', () => { - let recognizer: EducationalContextRecognizer - let config: EducationalRecognizerConfig - - beforeEach(() => { - vi.clearAllMocks() - - config = { - aiService: mockAIService, - model: 'gpt-4', - includeResourceRecommendations: true, - adaptToUserLevel: true, - enableTopicMapping: true, - } - - recognizer = new EducationalContextRecognizer(config) - }) - - describe('constructor', () => { - it('should initialize with provided configuration', () => { - expect(recognizer).toBeDefined() - }) - - it('should use default values for optional config properties', () => { - const minimalConfig = { aiService: mockAIService } - const minimalRecognizer = new EducationalContextRecognizer(minimalConfig) - expect(minimalRecognizer).toBeDefined() - }) - }) - - describe('recognizeEducationalContext', () => { - describe('pattern-based recognition', () => { - it('should recognize definition questions', async () => { - const queries = [ - 'What is depression?', - 'Define anxiety disorder', - 'What are the meaning of PTSD symptoms?', - 'Tell me about bipolar disorder', - ] - - for (const query of queries) { - const result = await recognizer.recognizeEducationalContext(query) - if (!result.isEducational) { - // Log actual result for debugging - console.log('DEBUG FAIL: Definition question result:', result) - } - expect(result.isEducational).toBe(true) - expect(result.educationalType).toBe(EducationalType.DEFINITION) - expect(result.confidence).toBeGreaterThan(0.5) - } - }) - - it('should recognize explanation questions', async () => { - const queries = [ - 'How does therapy work?', - 'Why do antidepressants take time to work?', - 'How can meditation help with anxiety?', - 'Explain how CBT works', - ] - - for (const query of queries) { - const result = await recognizer.recognizeEducationalContext(query) - if (!result.isEducational) { - // Log actual result for debugging - console.log('DEBUG FAIL: Explanation question result:', result) - } - expect(result.isEducational).toBe(true) - expect(result.educationalType).toBe(EducationalType.EXPLANATION) - expect(result.confidence).toBeGreaterThan(0.5) - } - }) - - it('should recognize comparison questions', async () => { - const queries = [ - "What's the difference between anxiety and panic attacks?", - 'Compare CBT vs DBT', - 'Depression versus sadness', - 'Which is better: medication or therapy?', - ] - - for (const query of queries) { - const result = await recognizer.recognizeEducationalContext(query) - if (!result.isEducational) { - // Log actual result for debugging - console.log('DEBUG FAIL: Comparison question result:', result) - } - expect(result.isEducational).toBe(true) - expect(result.educationalType).toBe(EducationalType.COMPARISON) - expect(result.confidence).toBeGreaterThan(0.5) - } - }) - - it('should recognize symptom questions', async () => { - const queries = [ - 'What are the symptoms of depression?', - 'Signs of anxiety disorder', - 'How do I know if I have PTSD?', - 'Warning signs of bipolar disorder', - ] - - for (const query of queries) { - const result = await recognizer.recognizeEducationalContext(query) - if (!result.isEducational) { - // Log actual result for debugging - console.log('DEBUG FAIL: Symptom question result:', result) - } - expect(result.isEducational).toBe(true) - expect(result.educationalType).toBe(EducationalType.SYMPTOMS) - expect(result.confidence).toBeGreaterThan(0.5) - } - }) - - it('should identify topic areas correctly', async () => { - const topicTests = [ - { query: 'What is depression?', expectedTopic: TopicArea.DEPRESSION }, - { - query: 'How does anxiety affect the brain?', - expectedTopic: TopicArea.ANXIETY, - }, - { query: 'What is CBT therapy?', expectedTopic: TopicArea.THERAPY }, - { - query: 'How do antidepressants work?', - expectedTopic: TopicArea.MEDICATION, - }, - ] - - for (const test of topicTests) { - const result = await recognizer.recognizeEducationalContext( - test.query, - ) - if (result.topicArea !== test.expectedTopic) { - // Log actual topic area for debugging - console.log('DEBUG FAIL: Topic mapping result:', { - query: test.query, - actual: result.topicArea, - expected: test.expectedTopic, - result, - }) - } - expect(result.topicArea).toBe(test.expectedTopic) - } - }) - }) - - describe('AI-powered analysis', () => { - it('should use AI for detailed analysis when pattern confidence is high', async () => { - const aiResponse = { - choices: [ - { - message: { - content: JSON.stringify({ - isEducational: true, - confidence: 0.95, - educationalType: 'definition', - complexity: 'basic', - topicArea: 'depression', - learningObjectives: [ - 'Understand what depression is', - 'Learn basic symptoms', - ], - recommendedResources: ['educational_videos', 'infographics'], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.3, - practicalApplications: [ - 'self-awareness', - 'recognition in others', - ], - relatedTopics: ['anxiety', 'mood disorders'], - ageAppropriateness: 'all', - }, - }), - }, - }, - ], - } - ;( - mockAIService.createChatCompletion as ReturnType - ).mockResolvedValue(aiResponse) - - // First test the pattern manually to debug - const testPattern = - /\b(?:what is|what are|define|definition of|meaning of)\b/i - const testQuery = 'what is depression?' - const manualTest = testPattern.test(testQuery) - console.log('Manual pattern test:', { - testQuery, - pattern: testPattern.toString(), - matches: manualTest, - }) - - const result = await recognizer.recognizeEducationalContext( - 'What is depression?', - ) - - // First check if the pattern-based recognition is working - if (!result.isEducational) { - throw new Error( - `Pattern recognition failed. Manual test: ${manualTest}. Result: ${JSON.stringify(result)}`, - ) - } - - expect(result.isEducational).toBe(true) - expect(result.confidence).toBeGreaterThan(0.8) - expect(result.educationalType).toBe(EducationalType.DEFINITION) - expect(result.topicArea).toBe(TopicArea.DEPRESSION) - expect(result.learningObjectives).toHaveLength(2) - expect(result.recommendedResources).toContain( - ResourceType.EDUCATIONAL_VIDEOS, - ) - expect(mockAIService.createChatCompletion).toHaveBeenCalled() - }) - - it('should skip AI analysis for low-confidence pattern matches', async () => { - vi.clearAllMocks() // ensure clean slate before assertion - - const result = - await recognizer.recognizeEducationalContext('Hello there') - - expect(result.isEducational).toBe(false) - expect(result.confidence).toBeLessThan(0.3) - expect(mockAIService.createChatCompletion).not.toHaveBeenCalled() - }) - - it('should include user profile context in AI analysis', async () => { - const userProfile = { - educationLevel: 'graduate' as const, - priorMentalHealthKnowledge: 'intermediate' as const, - preferredLearningStyle: 'visual' as const, - } - - const aiResponse = { - choices: [ - { - message: { - content: JSON.stringify({ - isEducational: true, - confidence: 0.9, - educationalType: 'explanation', - complexity: 'advanced', - topicArea: 'therapy', - learningObjectives: ['Understand therapeutic mechanisms'], - recommendedResources: [ - 'scientific_articles', - 'interactive_tools', - ], - priorKnowledgeRequired: ['basic psychology concepts'], - metadata: { - conceptualDepth: 0.8, - practicalApplications: ['clinical practice'], - relatedTopics: ['neuroscience', 'psychology'], - }, - }), - }, - }, - ], - } - ;( - mockAIService.createChatCompletion as ReturnType - ).mockResolvedValue(aiResponse) - - const result = await recognizer.recognizeEducationalContext( - 'How does cognitive behavioral therapy work at the neurological level?', - userProfile, - ) - - expect(result.complexity).toBe('advanced') - expect(result.recommendedResources).toContain( - ResourceType.SCIENTIFIC_ARTICLES, - ) - - // Verify that user context was passed to AI - const callArgs = ( - mockAIService.createChatCompletion as ReturnType - ).mock.calls[0] - const systemMessage = callArgs?.[0]?.[0]?.content - expect(systemMessage).toContain('Education Level: graduate') - expect(systemMessage).toContain( - 'Prior Mental Health Knowledge: intermediate', - ) - }) - - it('should include conversation history in analysis', async () => { - const conversationHistory = [ - "I've been learning about mental health", - 'Previously asked about anxiety', - 'Now I want to understand more', - ] - - const aiResponse = { - choices: [ - { - message: { - content: JSON.stringify({ - isEducational: true, - confidence: 0.85, - educationalType: 'explanation', - complexity: 'intermediate', - topicArea: 'anxiety', - learningObjectives: ['Build on previous anxiety knowledge'], - recommendedResources: ['educational_videos'], - priorKnowledgeRequired: ['basic anxiety concepts'], - metadata: { - conceptualDepth: 0.6, - practicalApplications: [], - relatedTopics: ['panic disorders'], - }, - }), - }, - }, - ], - } - ;( - mockAIService.createChatCompletion as ReturnType - ).mockResolvedValue(aiResponse) - - await recognizer.recognizeEducationalContext( - 'How do panic attacks relate to anxiety?', - undefined, - conversationHistory, - ) - - const callArgs = ( - mockAIService.createChatCompletion as ReturnType - ).mock.calls[0] - const userMessage = callArgs?.[0]?.[1]?.content - expect(userMessage).toContain('Previous context:') - expect(userMessage).toContain('Now I want to understand more') - }) - }) - - describe('complexity determination', () => { - it('should determine basic complexity for simple queries', async () => { - const result = await recognizer.recognizeEducationalContext( - 'What is depression?', - ) - expect(result.complexity).toBe('basic') - }) - - it('should determine advanced complexity for research queries', async () => { - const aiResponse = { - choices: [ - { - message: { - content: JSON.stringify({ - isEducational: true, - confidence: 0.9, - educationalType: 'research', - complexity: 'advanced', - topicArea: 'depression', - learningObjectives: [], - recommendedResources: [], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.9, - practicalApplications: [], - relatedTopics: [], - }, - }), - }, - }, - ], - } - ;( - mockAIService.createChatCompletion as ReturnType - ).mockResolvedValue(aiResponse) - - const result = await recognizer.recognizeEducationalContext( - 'What does the latest neurobiology research say about depression mechanisms?', - ) - - expect(result.complexity).toBe('advanced') - }) - }) - - describe('user adaptation', () => { - it('should adapt complexity based on user profile', async () => { - const basicUserProfile = { - educationLevel: 'high_school' as const, - priorMentalHealthKnowledge: 'none' as const, - } - - const aiResponse = { - choices: [ - { - message: { - content: JSON.stringify({ - isEducational: true, - confidence: 0.8, - educationalType: 'definition', - complexity: 'advanced', // AI suggests advanced - topicArea: 'depression', - learningObjectives: [], - recommendedResources: ['educational_videos'], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.5, - practicalApplications: [], - relatedTopics: [], - }, - }), - }, - }, - ], - } - ;( - mockAIService.createChatCompletion as ReturnType - ).mockResolvedValue(aiResponse) - - const result = await recognizer.recognizeEducationalContext( - 'What is depression?', - basicUserProfile, - ) - - // Should adapt advanced to intermediate for basic user - expect(result.complexity).toBe('intermediate') - }) - - it('should adapt resources based on learning style', async () => { - const visualLearnerProfile = { - preferredLearningStyle: 'visual' as const, - } - - const aiResponse = { - choices: [ - { - message: { - content: JSON.stringify({ - isEducational: true, - confidence: 0.8, - educationalType: 'definition', - complexity: 'basic', - topicArea: 'anxiety', - learningObjectives: [], - recommendedResources: ['books', 'podcasts'], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.5, - practicalApplications: [], - relatedTopics: [], - }, - }), - }, - }, - ], - } - ;( - mockAIService.createChatCompletion as ReturnType - ).mockResolvedValue(aiResponse) - - const result = await recognizer.recognizeEducationalContext( - 'What is anxiety?', - visualLearnerProfile, - ) - - // Should include visual resources for visual learner - expect(result.recommendedResources).toContain(ResourceType.INFOGRAPHICS) - expect(result.recommendedResources).toContain( - ResourceType.EDUCATIONAL_VIDEOS, - ) - }) - }) - - describe('error handling', () => { - it('should handle AI service errors gracefully', async () => { - ;( - mockAIService.createChatCompletion as ReturnType - ).mockRejectedValue(new Error('AI service error')) - - const result = await recognizer.recognizeEducationalContext( - 'What is depression?', - ) - - // Should fall back to pattern-based result - expect(result).toBeDefined() - expect(result.educationalType).toBe(EducationalType.DEFINITION) - expect(result.topicArea).toBe(TopicArea.DEPRESSION) - }) - - it('should handle malformed AI responses', async () => { - const malformedResponse = { - choices: [ - { - message: { - content: 'This is not valid JSON', - }, - }, - ], - } - ;( - mockAIService.createChatCompletion as ReturnType - ).mockResolvedValue(malformedResponse) - - const result = await recognizer.recognizeEducationalContext( - 'What is depression?', - ) - - expect(result.isEducational).toBe(false) - expect(result.confidence).toBe(0.1) - }) - }) - - describe('validation', () => { - it('should validate and normalize educational types', async () => { - const aiResponse = { - choices: [ - { - message: { - content: JSON.stringify({ - isEducational: true, - confidence: 0.8, - educationalType: 'invalid_type', - complexity: 'basic', - topicArea: 'depression', - learningObjectives: [], - recommendedResources: [], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.5, - practicalApplications: [], - relatedTopics: [], - }, - }), - }, - }, - ], - } - ;( - mockAIService.createChatCompletion as ReturnType - ).mockResolvedValue(aiResponse) - - const result = await recognizer.recognizeEducationalContext( - 'What is depression?', - ) - - expect(result.educationalType).toBe(EducationalType.DEFINITION) - }) - - it('should clamp confidence values to valid range', async () => { - const aiResponse = { - choices: [ - { - message: { - content: JSON.stringify({ - isEducational: true, - confidence: 1.5, // Invalid - too high - educationalType: 'definition', - complexity: 'basic', - topicArea: 'depression', - learningObjectives: [], - recommendedResources: [], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.5, - practicalApplications: [], - relatedTopics: [], - }, - }), - }, - }, - ], - } - ;( - mockAIService.createChatCompletion as ReturnType - ).mockResolvedValue(aiResponse) - - const result = await recognizer.recognizeEducationalContext( - 'What is depression?', - ) - - expect(result.confidence).toBeLessThanOrEqual(1.0) - }) - }) - }) - - describe('recognizeBatch', () => { - it('should process multiple queries', async () => { - const queries = [ - { query: 'What is depression?' }, - { query: 'How does therapy work?' }, - { query: 'What are anxiety symptoms?' }, - ] - - const aiResponses = [ - { educationalType: 'definition', topicArea: 'depression' }, - { educationalType: 'explanation', topicArea: 'therapy' }, - { educationalType: 'symptoms', topicArea: 'anxiety' }, - ].map((ctx) => ({ - choices: [ - { - message: { - content: JSON.stringify({ - isEducational: true, - confidence: 0.8, - ...ctx, - complexity: 'basic', - learningObjectives: [], - recommendedResources: [], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.5, - practicalApplications: [], - relatedTopics: [], - }, - }), - }, - }, - ], - })) - ;(mockAIService.createChatCompletion as ReturnType) - .mockResolvedValueOnce(aiResponses[0]) - .mockResolvedValueOnce(aiResponses[1]) - .mockResolvedValueOnce(aiResponses[2]) - - const results = await recognizer.recognizeBatch(queries) - - expect(results).toBeDefined() - expect(Array.isArray(results)).toBe(true) - expect(results).toHaveLength(3) - expect(results[0]?.educationalType).toBe(EducationalType.DEFINITION) - expect(results[1]?.educationalType).toBe(EducationalType.EXPLANATION) - expect(results[2]?.educationalType).toBe(EducationalType.SYMPTOMS) - }) - }) - - describe('generateLearningPathway', () => { - it('should generate appropriate learning pathway', () => { - const result: EducationalContextResult = { - isEducational: true, - confidence: 0.8, - educationalType: EducationalType.DEFINITION, - complexity: 'basic', - topicArea: TopicArea.DEPRESSION, - learningObjectives: ['Understand depression'], - recommendedResources: [ResourceType.EDUCATIONAL_VIDEOS], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.3, - practicalApplications: [], - relatedTopics: ['anxiety', 'mood disorders'], - }, - } - - const pathway = recognizer.generateLearningPathway(result) - - expect(pathway.currentTopic).toContain('definition') - expect(pathway.currentTopic).toContain('depression') - expect(pathway.nextSteps).toContain('Learn about symptoms') - expect(pathway.estimatedTimeToComplete).toBe('15-30 minutes') - expect(pathway.relatedConcepts).toEqual(['anxiety', 'mood disorders']) - }) - }) -}) - -describe('createEducationalContextRecognizer', () => { - it('should create a recognizer instance', () => { - const config: EducationalRecognizerConfig = { - aiService: mockAIService, - model: 'gpt-4', - } - - const recognizer = createEducationalContextRecognizer(config) - expect(recognizer).toBeInstanceOf(EducationalContextRecognizer) - }) -}) - -describe('getDefaultEducationalRecognizerConfig', () => { - it('should return default configuration', () => { - const config = getDefaultEducationalRecognizerConfig(mockAIService) - - expect(config.aiService).toBe(mockAIService) - expect(config.model).toBe('gpt-4') - expect(config.includeResourceRecommendations).toBe(true) - expect(config.adaptToUserLevel).toBe(true) - expect(config.enableTopicMapping).toBe(true) - }) -}) diff --git a/apps/web/src/lib/metaaligner/prioritization/educational-context-recognizer.ts b/apps/web/src/lib/metaaligner/prioritization/educational-context-recognizer.ts deleted file mode 100644 index aa2cae964b..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/educational-context-recognizer.ts +++ /dev/null @@ -1,1060 +0,0 @@ -/** - * Educational Context Recognition System - * Specialized component for identifying and classifying educational mental health queries - */ - -import type { AIService } from '../../ai/models/ai-types' -import { createBuildSafeLogger } from '../../logging/build-safe-logger' - -const logger = createBuildSafeLogger('educational-context-recognizer') - -export interface EducationalContextResult { - isEducational: boolean - confidence: number - educationalType: EducationalType - complexity: 'basic' | 'intermediate' | 'advanced' - topicArea: TopicArea - learningObjectives: string[] - recommendedResources: ResourceType[] - priorKnowledgeRequired: string[] - metadata: { - conceptualDepth: number // 0-1 scale - practicalApplications: string[] - relatedTopics: string[] - ageAppropriateness?: 'child' | 'adolescent' | 'adult' | 'all' - [key: string]: unknown - } -} - -export enum EducationalType { - DEFINITION = 'definition', // "What is depression?" - EXPLANATION = 'explanation', // "How does therapy work?" - COMPARISON = 'comparison', // "What's the difference between anxiety and panic attacks?" - MECHANISM = 'mechanism', // "Why do antidepressants take time to work?" - SYMPTOMS = 'symptoms', // "What are the symptoms of PTSD?" - CAUSES = 'causes', // "What causes bipolar disorder?" - TREATMENT = 'treatment', // "What are treatment options for anxiety?" - PREVENTION = 'prevention', // "How can I prevent panic attacks?" - RESEARCH = 'research', // "What does research say about CBT?" - STATISTICS = 'statistics', // "How common is depression?" - MYTH_BUSTING = 'myth_busting', // "Is it true that..." - DEVELOPMENTAL = 'developmental', // "How does depression affect children?" -} - -export enum TopicArea { - DEPRESSION = 'depression', - ANXIETY = 'anxiety', - TRAUMA_PTSD = 'trauma_ptsd', - BIPOLAR = 'bipolar', - PERSONALITY_DISORDERS = 'personality_disorders', - EATING_DISORDERS = 'eating_disorders', - ADDICTION = 'addiction', - THERAPY = 'therapy', - MEDICATION = 'medication', - COPING_SKILLS = 'coping_skills', - RELATIONSHIPS = 'relationships', - STIGMA = 'stigma', - NEURODEVELOPMENTAL = 'neurodevelopmental', - GENERAL_MENTAL_HEALTH = 'general_mental_health', -} - -export enum ResourceType { - SCIENTIFIC_ARTICLES = 'scientific_articles', - EDUCATIONAL_VIDEOS = 'educational_videos', - INTERACTIVE_TOOLS = 'interactive_tools', - BOOKS = 'books', - INFOGRAPHICS = 'infographics', - WORKSHEETS = 'worksheets', - SELF_ASSESSMENT = 'self_assessment', - PODCASTS = 'podcasts', - ONLINE_COURSES = 'online_courses', - SUPPORT_GROUPS = 'support_groups', -} - -export interface EducationalRecognizerConfig { - aiService: AIService - model?: string - includeResourceRecommendations?: boolean - adaptToUserLevel?: boolean - enableTopicMapping?: boolean -} - -export interface UserProfile { - educationLevel?: 'high_school' | 'undergraduate' | 'graduate' | 'professional' - priorMentalHealthKnowledge?: 'none' | 'basic' | 'intermediate' | 'advanced' - preferredLearningStyle?: 'visual' | 'auditory' | 'kinesthetic' | 'reading' -} - -/** - * System prompt for educational context recognition - */ -const EDUCATIONAL_RECOGNITION_PROMPT = `You are an educational content classifier specializing in mental health education. Analyze the user's query to determine if it's seeking educational information and classify it appropriately. - -Your task is to: -1. Determine if the query is primarily educational (seeking to learn/understand) -2. Classify the type of educational question -3. Identify the topic area and complexity level -4. Suggest appropriate learning objectives and resources - -Educational Types: -- definition: Asking what something is -- explanation: Seeking how something works -- comparison: Comparing concepts/treatments -- mechanism: Understanding why/how processes work -- symptoms: Learning about signs/symptoms -- causes: Understanding what causes conditions -- treatment: Learning about interventions -- prevention: How to prevent/manage -- research: What research/evidence shows -- statistics: Epidemiological information -- myth_busting: Correcting misconceptions -- developmental: Age/stage specific information - -Topic Areas: -depression, anxiety, trauma_ptsd, bipolar, personality_disorders, eating_disorders, addiction, therapy, medication, coping_skills, relationships, stigma, neurodevelopmental, general_mental_health - -Complexity Levels: -- basic: Simple definitions, general concepts -- intermediate: Detailed explanations, mechanisms -- advanced: Research findings, complex interactions - -Respond in JSON format with: -- isEducational: boolean -- confidence: number (0-1) -- educationalType: one of the types above -- complexity: basic/intermediate/advanced -- topicArea: one of the topic areas above -- learningObjectives: array of specific learning goals -- recommendedResources: array of appropriate resource types -- priorKnowledgeRequired: array of prerequisite concepts -- metadata: object with additional educational context - -Focus on accuracy and educational value.` - -/** - * Educational Context Recognition Engine - */ -export class EducationalContextRecognizer { - private readonly aiService: AIService - private readonly model: string - private readonly adaptToUserLevel: boolean - private lastParseMalformed: boolean = false - - // Pattern-based educational indicators for quick detection - private readonly educationalPatterns = { - definition: [ - /\b(?:what is|what are|define|definition of|meaning of)\b/i, - /\b(?:explain|tell me about|describe)\b.*\b(?:depression|anxiety|therapy|ptsd|bipolar)\b/i, - ], - explanation: [ - /\b(?:how does|how do|how can|why does|why do)\b/i, - /\b(?:explain how|walk me through|help me understand)\b/i, - ], - comparison: [ - /\b(?:difference between|compare|versus|vs\.?|rather than)\b/i, - /\b(?:which is better|what's the difference|how do they differ)\b/i, - ], - symptoms: [ - /\b(?:symptoms of|signs of|symptoms for|what are the symptoms)\b/i, - /\b(?:how do I know if|warning signs|red flags)\b/i, - ], - causes: [ - /\b(?:what causes|why do people get|what leads to|risk factors)\b/i, - /\b(?:caused by|due to|because of|triggers)\b/i, - ], - treatment: [ - /\b(?:treatment for|how to treat|therapy for|medication for)\b/i, - /\b(?:treatment options|ways to help|interventions for)\b/i, - ], - } - - // Topic classification keywords - private readonly topicKeywords = { - depression: [ - 'depression', - 'depressed', - 'major depressive', - 'dysthymia', - 'mood disorder', - ], - anxiety: [ - 'anxiety', - 'anxious', - 'panic', - 'phobia', - 'generalized anxiety', - 'social anxiety', - ], - trauma_ptsd: [ - 'trauma', - 'ptsd', - 'post-traumatic', - 'flashbacks', - 'nightmares', - 'trigger', - ], - bipolar: ['bipolar', 'manic', 'mania', 'mood swings', 'cyclothymia'], - therapy: [ - 'therapy', - 'counseling', - 'psychotherapy', - 'cbt', - 'dbt', - 'emdr', - 'therapeutic', - ], - medication: [ - 'medication', - 'antidepressant', - 'antipsychotic', - 'mood stabilizer', - 'ssri', - ], - } - - constructor(config: EducationalRecognizerConfig) { - this.aiService = config.aiService - this.model = config.model ?? 'gpt-4' - this.adaptToUserLevel = config.adaptToUserLevel ?? true - } - - /** - * Recognize educational context in user query - */ - async recognizeEducationalContext( - userQuery: string, - userProfile?: UserProfile, - conversationHistory?: string[], - ): Promise { - try { - // Step 1: Pattern-based recognition - const patternResult = this.performPatternBasedRecognition(userQuery) - - // If no educational pattern found, return early - if (!patternResult.isEducational || patternResult.confidence < 0.3) { - return patternResult - } - - // Final safeguard: ensure simple definition query stays basic - const normalizedInitial = normalizeQuery(userQuery) - if (normalizedInitial.includes('what is depression')) { - patternResult.complexity = 'basic' - } - - // Step 2: AI enhancement (if available and pattern confidence is high) - if (this.aiService) { - try { - const aiResult = await this.performAIAnalysis( - userQuery, - userProfile, - conversationHistory, - ) - - // AI present: decide authority carefully - if (aiResult) { - // High-confidence educational -> combine - if (aiResult.isEducational && aiResult.confidence > 0.5) { - const combined = this.combineResults( - patternResult, - aiResult, - userProfile, - ) - // Post-combination safeguard: keep 'what is depression' as basic when no user profile is provided - if ( - normalizedInitial.includes('what is depression') && - !userProfile - ) { - combined.complexity = 'basic' - } - return combined - } - // If AI parsing clearly failed, treat as authoritative non-educational - if (this.lastParseMalformed) { - return aiResult - } - // Otherwise, prefer adapted pattern outcome - const adaptedPattern = this.adaptResultToUserProfile( - patternResult, - userProfile, - ) - // Safeguard after adaptation as well - if (normalizedInitial.includes('what is depression')) { - adaptedPattern.complexity = 'basic' - } - return adaptedPattern - } - } catch (error: unknown) { - logger.warn( - 'AI analysis failed, using adapted pattern result:', - error, - ) - const adapted = this.adaptResultToUserProfile( - patternResult, - userProfile, - ) - if (normalizedInitial.includes('what is depression')) { - adapted.complexity = 'basic' - } - return adapted - } - } - - // Step 3: Apply user profile adaptations to pattern result - const adapted = this.adaptResultToUserProfile(patternResult, userProfile) - - // Apply safeguard post-adaptation - if (normalizedInitial.includes('what is depression')) { - adapted.complexity = 'basic' - } - - // If AI service exists but wasn't used, boost confidence slightly for high-confidence patterns - if (this.aiService && adapted.confidence >= 0.8) { - adapted.confidence = Math.min(0.85, adapted.confidence + 0.05) - } - - return adapted - } catch (error: unknown) { - logger.error('Error recognizing educational context:', { - error: String(error), - }) - - // Fallback to basic non-educational result - return { - isEducational: false, - confidence: 0.1, - educationalType: EducationalType.DEFINITION, - complexity: 'basic', - topicArea: TopicArea.GENERAL_MENTAL_HEALTH, - learningObjectives: [], - recommendedResources: [], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.1, - practicalApplications: [], - relatedTopics: [], - }, - } - } - } - - /** - * Adapt result to user profile - */ - private adaptResultToUserProfile( - result: EducationalContextResult, - userProfile?: UserProfile, - ): EducationalContextResult { - // Respect configuration toggle - if (!userProfile || !this.adaptToUserLevel) { - return result - } - - const adapted = { ...result } - - // Use shared adaptation helpers to ensure consistent behavior across code paths - adapted.complexity = this.adaptComplexityToUser( - result.complexity, - userProfile, - ) - adapted.recommendedResources = this.adaptResourcesToUser( - result.recommendedResources, - userProfile, - ) - - return adapted - } - - /** - * Batch process multiple queries for educational context - */ - async recognizeBatch( - queries: Array<{ - query: string - userProfile?: UserProfile - conversationHistory?: string[] - }>, - ): Promise { - return Promise.all( - queries.map(async ({ query, userProfile, conversationHistory }) => - this.recognizeEducationalContext( - query, - userProfile, - conversationHistory, - ), - ), - ) - } - - /** - * Get learning pathway recommendations based on query - */ - generateLearningPathway(result: EducationalContextResult): { - currentTopic: string - prerequisites: string[] - nextSteps: string[] - relatedConcepts: string[] - estimatedTimeToComplete: string - } { - return { - currentTopic: `${result.educationalType} about ${result.topicArea}`, - prerequisites: result.priorKnowledgeRequired, - nextSteps: this.generateNextSteps(result), - relatedConcepts: result.metadata.relatedTopics, - estimatedTimeToComplete: this.estimateCompletionTime(result.complexity), - } - } - - /** - * Pattern-based recognition for quick screening - */ - private performPatternBasedRecognition( - userQuery: string, - ): EducationalContextResult { - const query = userQuery.toLowerCase() - let bestMatch = { type: EducationalType.DEFINITION, confidence: 0 } - let topicArea = TopicArea.GENERAL_MENTAL_HEALTH - - // Prioritize symptom pattern over definition if both match - let matchedTypes: { type: EducationalType; confidence: number }[] = [] - for (const [type, patterns] of Object.entries(this.educationalPatterns)) { - for (const pattern of patterns) { - if (pattern.test(query)) { - const confidence = 0.76 // Base confidence for pattern match - matchedTypes.push({ type: type as EducationalType, confidence }) - } - } - } - // If symptoms matched, use it; else use highest confidence with tie-break priority - const symptomMatch = matchedTypes.find( - (mt) => mt.type === EducationalType.SYMPTOMS, - ) - if (symptomMatch) { - bestMatch = symptomMatch - } else if (matchedTypes.length > 0) { - const priority: Record = { - [EducationalType.SYMPTOMS]: 3, - [EducationalType.EXPLANATION]: 2, - [EducationalType.DEFINITION]: 1, - [EducationalType.COMPARISON]: 2, - [EducationalType.MECHANISM]: 2, - [EducationalType.CAUSES]: 2, - [EducationalType.TREATMENT]: 2, - [EducationalType.PREVENTION]: 2, - [EducationalType.RESEARCH]: 2, - [EducationalType.STATISTICS]: 1, - [EducationalType.MYTH_BUSTING]: 1, - [EducationalType.DEVELOPMENTAL]: 1, - } - bestMatch = matchedTypes.reduce((a, b) => { - if (a.confidence !== b.confidence) { - return a.confidence > b.confidence ? a : b - } - const ap = priority[a.type] ?? 0 - const bp = priority[b.type] ?? 0 - return ap >= bp ? a : b - }) - } - - // Determine topic area - for (const [topic, keywords] of Object.entries(this.topicKeywords)) { - for (const keyword of keywords) { - if (query.includes(keyword)) { - topicArea = topic as TopicArea - bestMatch.confidence += 0.05 // Boost confidence for topic match - break - } - } - } - - const complexity = this.determineComplexityFromQuery(query) - const isEducational = bestMatch.confidence > 0.5 - - // Final override: if query is "what is depression", always return basic - if ( - query - .toLowerCase() - .replace(/[?.]/g, '') - .trim() - .includes('what is depression') - ) { - return { - isEducational, - confidence: Math.min(bestMatch.confidence, 1.0), - educationalType: bestMatch.type, - complexity: 'basic', - topicArea, - learningObjectives: isEducational - ? this.generateLearningObjectives( - bestMatch.type, - topicArea, - bestMatch.confidence, - ) - : [], - recommendedResources: isEducational - ? this.getBasicResources(bestMatch.type) - : [], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: bestMatch.confidence * 0.8, - practicalApplications: [], - relatedTopics: [], - ageAppropriateness: 'all', - }, - } - } - return { - isEducational, - confidence: Math.min(bestMatch.confidence, 1.0), - educationalType: bestMatch.type, - complexity, - topicArea, - learningObjectives: isEducational - ? this.generateLearningObjectives( - bestMatch.type, - topicArea, - bestMatch.confidence, - ) - : [], - recommendedResources: isEducational - ? this.getBasicResources(bestMatch.type) - : [], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: bestMatch.confidence * 0.8, - practicalApplications: [], - relatedTopics: [], - ageAppropriateness: 'all', - }, - } - } - - /** - * AI-powered detailed analysis - */ - private async performAIAnalysis( - userQuery: string, - userProfile?: UserProfile, - conversationHistory?: string[], - ): Promise { - let contextualPrompt = EDUCATIONAL_RECOGNITION_PROMPT - - // Add user profile context if available - if (userProfile && this.adaptToUserLevel) { - contextualPrompt += `\n\nUser Context: -- Education Level: ${userProfile.educationLevel ?? 'unknown'} -- Prior Mental Health Knowledge: ${userProfile.priorMentalHealthKnowledge ?? 'unknown'} -- Preferred Learning Style: ${userProfile.preferredLearningStyle ?? 'unknown'} - -Adapt complexity and resource recommendations accordingly.` - } - - // Include conversation history for context - let queryWithContext = userQuery - if (conversationHistory && conversationHistory.length > 0) { - queryWithContext = `Previous context: ${conversationHistory.slice(-3).join(' ')}\n\nCurrent query: ${userQuery}` - } - - const messages: Array<{ - role: 'system' | 'user' | 'assistant' - content: string - }> = [ - { role: 'system', content: contextualPrompt }, - { role: 'user', content: queryWithContext }, - ] - - const response: unknown = await this.aiService.createChatCompletion( - messages, - { - model: this.model, - }, - ) - - // Use unknown instead of any for type safety - const r = response as { - choices?: Array<{ message?: { content?: unknown } }> - content?: unknown - } - let content: unknown = r?.choices?.[0]?.message?.content ?? r?.content ?? r - if (content && typeof content !== 'string') { - try { - content = JSON.stringify(content) - } catch { - content = String(content) - } - } - // If AI returned no content at all, throw to let caller fall back to patterns - if (!content || (content as string).trim() === '') { - throw new Error('Empty AI response') - } - // Return raw parsed result; caller will handle adaptation to avoid double-adjusting - return this.parseAIResponse(content as string) - } - - /** - * Parse AI response into structured result - */ - private parseAIResponse(content: string): EducationalContextResult { - try { - this.lastParseMalformed = false - logger.info('Raw AI response content', { content }) - // Prefer fenced JSON - let jsonStr: string | undefined - const fencedJson = content.match(/```json\n([\s\S]*?)\n```/) - const fenced = fencedJson ?? content.match(/```\n([\s\S]*?)\n```/) - if (fenced?.[1]) { - jsonStr = fenced[1] - } - // Fall back to extracting the largest JSON-like block by slicing from first '{' to last '}' - if (!jsonStr) { - const first = content.indexOf('{') - const last = content.lastIndexOf('}') - if (first !== -1 && last !== -1 && last > first) { - jsonStr = content.slice(first, last + 1) - } - } - // Ultimate fallback: try the original content - jsonStr ??= content - logger.info('Extracted JSON string from AI response', { jsonStr }) - const parsed = JSON.parse(jsonStr) - logger.info('Parsed AI response JSON', { parsed }) - - return { - isEducational: Boolean(parsed.isEducational), - confidence: Math.max(0, Math.min(1, parsed.confidence ?? 0.5)), - educationalType: this.validateEducationalType(parsed.educationalType), - complexity: this.validateComplexity(parsed.complexity), - topicArea: this.validateTopicArea(parsed.topicArea), - learningObjectives: Array.isArray(parsed.learningObjectives) - ? parsed.learningObjectives - : [], - recommendedResources: Array.isArray(parsed.recommendedResources) - ? parsed.recommendedResources - .map((r: string) => this.validateResourceType(r)) - .filter(Boolean) - : [], - priorKnowledgeRequired: Array.isArray(parsed.priorKnowledgeRequired) - ? parsed.priorKnowledgeRequired - : [], - metadata: { - conceptualDepth: Math.max( - 0, - Math.min(1, parsed.metadata?.conceptualDepth ?? 0.5), - ), - practicalApplications: Array.isArray( - parsed.metadata?.practicalApplications, - ) - ? parsed.metadata.practicalApplications - : [], - relatedTopics: Array.isArray(parsed.metadata?.relatedTopics) - ? parsed.metadata.relatedTopics - : [], - ageAppropriateness: this.validateAgeAppropriateness( - parsed.metadata?.ageAppropriateness, - ), - }, - } - } catch (error: unknown) { - this.lastParseMalformed = true - logger.error('Error parsing AI response:', { - message: error instanceof Error ? String(error) : String(error), - content, - }) - - // Return fallback result - return { - isEducational: false, - confidence: 0.1, - educationalType: EducationalType.DEFINITION, - complexity: 'basic', - topicArea: TopicArea.GENERAL_MENTAL_HEALTH, - learningObjectives: [], - recommendedResources: [], - priorKnowledgeRequired: [], - metadata: { - conceptualDepth: 0.1, - practicalApplications: [], - relatedTopics: [], - }, - } - } - } - - /** - * Combine pattern and AI results - */ - private combineResults( - patternResult: EducationalContextResult, - aiResult: EducationalContextResult, - userProfile?: UserProfile, - ): EducationalContextResult { - // Weighted combination: AI gets 70%, pattern gets 30% - const combinedConfidence = - aiResult.confidence * 0.7 + patternResult.confidence * 0.3 - - // Use AI result as base, enhance with pattern insights - const result = { - ...aiResult, - confidence: combinedConfidence, - isEducational: combinedConfidence > 0.6, - } - - // Adapt to user profile if available - if (userProfile && this.adaptToUserLevel) { - result.complexity = this.adaptComplexityToUser( - result.complexity, - userProfile, - ) - result.recommendedResources = this.adaptResourcesToUser( - result.recommendedResources, - userProfile, - ) - } - - return result - } - - /** - * Helper methods for validation and adaptation - */ - private validateEducationalType(type: string): EducationalType { - return Object.values(EducationalType).includes(type as EducationalType) - ? (type as EducationalType) - : EducationalType.DEFINITION - } - - private validateComplexity( - complexity: string, - ): 'basic' | 'intermediate' | 'advanced' { - return ['basic', 'intermediate', 'advanced'].includes(complexity) - ? (complexity as 'basic' | 'intermediate' | 'advanced') - : 'basic' - } - - private validateTopicArea(topic: string): TopicArea { - return Object.values(TopicArea).includes(topic as TopicArea) - ? (topic as TopicArea) - : TopicArea.GENERAL_MENTAL_HEALTH - } - - private validateResourceType(resource: string): ResourceType | null { - return Object.values(ResourceType).includes(resource as ResourceType) - ? (resource as ResourceType) - : null - } - - private validateAgeAppropriateness( - age: string, - ): 'child' | 'adolescent' | 'adult' | 'all' { - return ['child', 'adolescent', 'adult', 'all'].includes(age) - ? (age as 'child' | 'adolescent' | 'adult' | 'all') - : 'all' - } - - private determineComplexityFromQuery( - query: string, - ): 'basic' | 'intermediate' | 'advanced' { - // Guarantee "What is depression?" is always basic - const normalized = normalizeQuery(query) - if (normalized.includes('what is depression')) { - return 'basic' - } - const simpleIndicators = [ - 'what is', - 'define', - 'definition of', - 'describe', - 'tell me about', - 'simple', - 'basic', - 'introduction', - ] - const advancedIndicators = [ - 'mechanism', - 'neurobiology', - 'research', - 'meta-analysis', - 'efficacy', - ] - - const hasSimple = simpleIndicators.some((indicator) => - normalized.includes(indicator), - ) - const hasAdvanced = advancedIndicators.some((indicator) => - normalized.includes(indicator), - ) - - if (hasAdvanced) { - return 'advanced' - } - if (hasSimple) { - return 'basic' - } - // If the query is exactly "depression", treat as basic - if (normalized === 'depression') { - return 'basic' - } - return 'intermediate' - } - - private generateLearningObjectives( - type: EducationalType, - topic: TopicArea, - confidence: number, - ): string[] { - const objectives = [ - `Understand ${type.replace('_', ' ')} related to ${topic.replace('_', ' ')}`, - ] - - // Add additional objectives for high-confidence patterns - if (confidence >= 0.8) { - if ( - type === EducationalType.DEFINITION && - topic === TopicArea.DEPRESSION - ) { - objectives.push('Learn basic symptoms') - } else if (type === EducationalType.SYMPTOMS) { - objectives.push('Recognize warning signs') - } else { - objectives.push('Apply knowledge in practice') - } - } - - return objectives - } - - private getBasicResources(type: EducationalType): ResourceType[] { - const resourceMap: Partial> = { - [EducationalType.DEFINITION]: [ - ResourceType.INFOGRAPHICS, - ResourceType.EDUCATIONAL_VIDEOS, - ], - [EducationalType.EXPLANATION]: [ - ResourceType.EDUCATIONAL_VIDEOS, - ResourceType.INTERACTIVE_TOOLS, - ], - [EducationalType.RESEARCH]: [ - ResourceType.SCIENTIFIC_ARTICLES, - ResourceType.BOOKS, - ], - [EducationalType.SYMPTOMS]: [ - ResourceType.SELF_ASSESSMENT, - ResourceType.INFOGRAPHICS, - ], - } - - return ( - resourceMap[type] ?? [ - ResourceType.EDUCATIONAL_VIDEOS, - ResourceType.INFOGRAPHICS, - ] - ) - } - - private adaptComplexityToUser( - complexity: 'basic' | 'intermediate' | 'advanced', - userProfile: UserProfile, - ): 'basic' | 'intermediate' | 'advanced' { - logger.info('Adapting complexity to user', { complexity, userProfile }) - const knowledge = userProfile.priorMentalHealthKnowledge - const education = userProfile.educationLevel - - // Adjust based on user's knowledge level - if (knowledge === 'none' || education === 'high_school') { - if (complexity === 'advanced') { - logger.info( - 'Downgrading advanced to intermediate for low knowledge/education', - ) - return 'intermediate' - } - if (complexity === 'intermediate') { - logger.info( - 'Downgrading intermediate to basic for low knowledge/education', - ) - return 'basic' - } - return complexity - } - // Upgrade for mid-level users to at least intermediate - if ( - (knowledge === 'intermediate' || education === 'undergraduate') && - complexity === 'basic' - ) { - logger.info('Upgrading basic to intermediate for mid-level user') - return 'intermediate' - } - - // Aggressively upgrade for advanced/professional users - if ( - knowledge === 'advanced' || - education === 'graduate' || - education === 'professional' - ) { - if (complexity === 'basic') { - logger.info( - 'Upgrading basic to advanced for advanced/professional user', - ) - return 'advanced' - } - if (complexity === 'intermediate') { - logger.info( - 'Upgrading intermediate to advanced for advanced/professional user', - ) - return 'advanced' - } - return complexity - } - - logger.info('No adaptation needed for complexity', { complexity }) - return complexity - } - - private adaptResourcesToUser( - resources: ResourceType[], - userProfile: UserProfile, - ): ResourceType[] { - logger.info('Adapting resources to user', { resources, userProfile }) - const learningStyle = userProfile.preferredLearningStyle - - let adapted = [...resources] - const ensureSciArticlesForHigherLevel = () => { - if ( - (userProfile.educationLevel === 'graduate' || - userProfile.educationLevel === 'professional' || - userProfile.priorMentalHealthKnowledge === 'advanced' || - userProfile.priorMentalHealthKnowledge === 'intermediate') && - !adapted.includes(ResourceType.SCIENTIFIC_ARTICLES) - ) { - logger.info('Adding scientific articles for higher-level user') - adapted.unshift(ResourceType.SCIENTIFIC_ARTICLES) - } - } - - if (learningStyle === 'visual') { - // Always include infographics and educational videos for visual learners - if (!adapted.includes(ResourceType.INFOGRAPHICS)) { - logger.info('Adding infographics for visual learner') - adapted.unshift(ResourceType.INFOGRAPHICS) - } - if (!adapted.includes(ResourceType.EDUCATIONAL_VIDEOS)) { - logger.info('Adding educational videos for visual learner') - adapted.unshift(ResourceType.EDUCATIONAL_VIDEOS) - } - // If resources are empty, ensure infographics is present - if (adapted.length === 0) { - adapted = [ResourceType.INFOGRAPHICS] - } - ensureSciArticlesForHigherLevel() - // Remove duplicates - adapted = Array.from(new Set(adapted)) - logger.info('Adapted resources for visual learner', { adapted }) - return adapted.slice(0, 5) - } - if (learningStyle === 'auditory') { - if (!adapted.includes(ResourceType.PODCASTS)) { - logger.info('Adding podcasts for auditory learner') - adapted.unshift(ResourceType.PODCASTS) - } - if (!adapted.includes(ResourceType.EDUCATIONAL_VIDEOS)) { - logger.info('Adding educational videos for auditory learner') - adapted.unshift(ResourceType.EDUCATIONAL_VIDEOS) - } - ensureSciArticlesForHigherLevel() - adapted = Array.from(new Set(adapted)) - logger.info('Adapted resources for auditory learner', { adapted }) - return adapted.slice(0, 5) - } - if (learningStyle === 'reading') { - if (!adapted.includes(ResourceType.BOOKS)) { - logger.info('Adding books for reading learner') - adapted.unshift(ResourceType.BOOKS) - } - if (!adapted.includes(ResourceType.SCIENTIFIC_ARTICLES)) { - logger.info('Adding scientific articles for reading learner') - adapted.unshift(ResourceType.SCIENTIFIC_ARTICLES) - } - ensureSciArticlesForHigherLevel() - adapted = Array.from(new Set(adapted)) - logger.info('Adapted resources for reading learner', { adapted }) - return adapted.slice(0, 5) - } - - // For higher-education/knowledge users, ensure scientific articles are included - ensureSciArticlesForHigherLevel() - - logger.info('No adaptation needed for resources', { adapted }) - return Array.from(new Set(adapted)).slice(0, 5) - } - - private generateNextSteps(result: EducationalContextResult): string[] { - const typeToNextSteps: Partial> = { - [EducationalType.DEFINITION]: [ - 'Learn about symptoms', - 'Understand causes', - 'Explore treatment options', - ], - [EducationalType.SYMPTOMS]: [ - 'Learn about diagnostic criteria', - 'Understand when to seek help', - 'Explore coping strategies', - ], - [EducationalType.TREATMENT]: [ - 'Learn about specific therapies', - 'Understand medication options', - 'Find qualified providers', - ], - } - - return ( - typeToNextSteps[result.educationalType] ?? [ - 'Explore related topics', - 'Seek professional guidance', - ] - ) - } - - private estimateCompletionTime( - complexity: 'basic' | 'intermediate' | 'advanced', - ): string { - const timeMap = { - // Adjusted to match test expectations - basic: '15-30 minutes', - intermediate: '30-45 minutes', - advanced: '45-60 minutes', - } - - return timeMap[complexity] - } -} - -// Helper for robust query normalization -function normalizeQuery(q: string): string { - return q - .toLowerCase() - .replace(/[^\w\s]|_/g, '') // remove all punctuation - .replace(/\s+/g, ' ') // collapse multiple spaces - .trim() -} - -/** - * Factory function to create an educational context recognizer - */ -export function createEducationalContextRecognizer( - config: EducationalRecognizerConfig, -): EducationalContextRecognizer { - return new EducationalContextRecognizer(config) -} - -/** - * Default configuration for educational context recognizer - */ -export function getDefaultEducationalRecognizerConfig( - aiService: AIService, -): EducationalRecognizerConfig { - return { - aiService, - model: 'gpt-4', - includeResourceRecommendations: true, - adaptToUserLevel: true, - enableTopicMapping: true, - } -} diff --git a/apps/web/src/lib/metaaligner/prioritization/objective-switcher.test.ts b/apps/web/src/lib/metaaligner/prioritization/objective-switcher.test.ts deleted file mode 100644 index 126da82501..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/objective-switcher.test.ts +++ /dev/null @@ -1,794 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' - -import { ContextType } from '../core/objectives' -import type { - ContextEvent, - ContextTransition, -} from './context-transition-detector' -import { - ObjectiveSwitcher, - ObjectiveSwitchObserver, - type SwitchMetadata, - type ObjectiveSwitcherConfig, -} from './objective-switcher' - -describe('ObjectiveSwitcher', () => { - let switcher: ObjectiveSwitcher - - beforeEach(() => { - switcher = new ObjectiveSwitcher() - }) - - describe('constructor', () => { - it('should initialize with default config', () => { - expect(switcher).toBeDefined() - expect(switcher.getObjectives()).toEqual([]) - }) - - it('should initialize with initial context', () => { - const initialContext: ContextEvent = { - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - } - - const switcherWithContext = new ObjectiveSwitcher({ - initialContext, - }) - - const objectives = switcherWithContext.getObjectives() - expect(objectives.length).toBeGreaterThan(0) - expect(switcherWithContext.getCurrentContext()).toEqual(initialContext) - }) - - it('should accept custom weighting strategy', () => { - const customStrategy = vi.fn((objs) => objs) - const customSwitcher = new ObjectiveSwitcher({ - weightingStrategy: customStrategy, - }) - - expect(customSwitcher).toBeDefined() - }) - - it('should respect telemetry config', () => { - const noTelemetrySwitcher = new ObjectiveSwitcher({ - enableTelemetry: false, - }) - - expect(noTelemetrySwitcher).toBeDefined() - }) - }) - - describe('onContextTransition', () => { - it('should switch objectives on detected transition', async () => { - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - - const objectives = switcher.getObjectives() - expect(objectives.length).toBeGreaterThan(0) - expect(switcher.getCurrentContext()?.contextType).toBe( - ContextType.EDUCATIONAL, - ) - }) - - it('should not switch when transition not detected', async () => { - const initialContext: ContextEvent = { - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - } - - switcher = new ObjectiveSwitcher({ initialContext }) - const initialObjectives = switcher.getObjectives() - - const transition: ContextTransition = { - from: initialContext, - to: initialContext, - detected: false, - transitionType: 'none', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - - expect(switcher.getObjectives()).toEqual(initialObjectives) - }) - - it('should handle crisis elevation transition', async () => { - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.CRISIS, - confidence: 0.95, - urgency: 'critical', - timestamp: Date.now() + 500, - }, - detected: true, - transitionType: 'crisis_elevation', - shouldSmooth: false, - confidence: 1.0, - } - - await switcher.onContextTransition(transition) - - const objectives = switcher.getObjectives() - expect(objectives.length).toBeGreaterThan(0) - - // Crisis should prioritize safety - const safetyObj = objectives.find((obj) => obj.key === 'safety') - expect(safetyObj).toBeDefined() - }) - - it('should complete switch within 150ms', async () => { - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.85, - } - - const startTime = performance.now() - await switcher.onContextTransition(transition) - const duration = performance.now() - startTime - - expect(duration).toBeLessThan(150) - }) - }) - - describe('observer pattern', () => { - it('should notify observers on objective change', async () => { - const observerMock = vi.fn() - switcher.addObserver(observerMock) - - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - - // Allow async notifications to complete - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(observerMock).toHaveBeenCalled() - const callArgs = observerMock.mock.calls[0] - expect(callArgs[0]).toBeInstanceOf(Array) // objectives - expect(callArgs[1]).toHaveProperty('toContext') - }) - - it('should handle multiple observers', async () => { - const observer1 = vi.fn() - const observer2 = vi.fn() - const observer3 = vi.fn() - - switcher.addObserver(observer1) - switcher.addObserver(observer2) - switcher.addObserver(observer3) - - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.85, - } - - await switcher.onContextTransition(transition) - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(observer1).toHaveBeenCalled() - expect(observer2).toHaveBeenCalled() - expect(observer3).toHaveBeenCalled() - }) - - it('should remove observers correctly', async () => { - const observer = vi.fn() - switcher.addObserver(observer) - switcher.removeObserver(observer) - - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(observer).not.toHaveBeenCalled() - }) - - it('should handle observer errors gracefully', async () => { - const failingObserver = vi.fn(() => { - throw new Error('Observer error') - }) - switcher.addObserver(failingObserver) - - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - // Should not throw - await expect( - switcher.onContextTransition(transition), - ).resolves.not.toThrow() - }) - }) - - describe('concurrency and race conditions', () => { - it('should handle concurrent switch attempts safely', async () => { - const transitions: ContextTransition[] = [ - { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 100, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - }, - { - from: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 100, - }, - to: { - turnId: 3, - contextType: ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium', - timestamp: Date.now() + 200, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.85, - }, - ] - - // Fire both transitions simultaneously - await Promise.all( - transitions.map(async (t) => switcher.onContextTransition(t)), - ) - - // Should have valid state (last transition should win) - const currentContext = switcher.getCurrentContext() - expect(currentContext).toBeDefined() - expect([ContextType.EDUCATIONAL, ContextType.SUPPORT]).toContain( - currentContext?.contextType, - ) - }) - - it('should process pending switches', async () => { - const switchCount = 5 - const transitions: ContextTransition[] = [] - - for (let i = 0; i < switchCount; i++) { - transitions.push({ - from: { - turnId: i, - contextType: - i % 2 === 0 ? ContextType.GENERAL : ContextType.EDUCATIONAL, - confidence: 0.8, - urgency: 'low', - timestamp: Date.now() + i * 100, - }, - to: { - turnId: i + 1, - contextType: - i % 2 === 0 ? ContextType.EDUCATIONAL : ContextType.SUPPORT, - confidence: 0.85, - urgency: 'medium', - timestamp: Date.now() + (i + 1) * 100, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.85, - }) - } - - // Fire all transitions rapidly - await Promise.all( - transitions.map(async (t) => switcher.onContextTransition(t)), - ) - - // All should be processed (check telemetry) - const telemetry = switcher.getTelemetry() - expect(telemetry.objective_switch_count).toBeGreaterThan(0) - }) - - it('should maintain data consistency under load', async () => { - const iterations = 10 - const promises: Promise[] = [] - - for (let i = 0; i < iterations; i++) { - const transition: ContextTransition = { - from: { - turnId: i, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now() + i * 50, - }, - to: { - turnId: i + 1, - contextType: - i % 3 === 0 - ? ContextType.EDUCATIONAL - : i % 3 === 1 - ? ContextType.SUPPORT - : ContextType.INFORMATIONAL, - confidence: 0.85, - urgency: 'medium', - timestamp: Date.now() + (i + 1) * 50, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.85, - } - - promises.push(switcher.onContextTransition(transition)) - } - - await Promise.all(promises) - - // Should have valid objectives - const objectives = switcher.getObjectives() - expect(objectives).toBeInstanceOf(Array) - expect(objectives.length).toBeGreaterThan(0) - - // Telemetry should be consistent - const telemetry = switcher.getTelemetry() - expect(telemetry.objective_switch_count).toBeGreaterThan(0) - expect(telemetry.failed_switches).toBe(0) - }) - }) - - describe('telemetry', () => { - it('should track switch count', async () => { - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - - const telemetry = switcher.getTelemetry() - expect(telemetry.objective_switch_count).toBe(1) - }) - - it('should track multiple switches', async () => { - for (let i = 0; i < 3; i++) { - const transition: ContextTransition = { - from: { - turnId: i, - contextType: - i % 2 === 0 ? ContextType.GENERAL : ContextType.EDUCATIONAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now() + i * 1000, - }, - to: { - turnId: i + 1, - contextType: - i % 2 === 0 ? ContextType.EDUCATIONAL : ContextType.SUPPORT, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + (i + 1) * 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - } - - const telemetry = switcher.getTelemetry() - expect(telemetry.objective_switch_count).toBe(3) - }) - - it('should calculate average duration', async () => { - for (let i = 0; i < 2; i++) { - const transition: ContextTransition = { - from: { - turnId: i, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now() + i * 1000, - }, - to: { - turnId: i + 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + (i + 1) * 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - } - - const telemetry = switcher.getTelemetry() - expect(telemetry.average_switch_duration_ms).toBeGreaterThan(0) - expect(telemetry.average_switch_duration_ms).toBeLessThan(150) - }) - - it('should track observer notifications', async () => { - const observer = vi.fn() - switcher.addObserver(observer) - - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - await new Promise((resolve) => setTimeout(resolve, 10)) - - const telemetry = switcher.getTelemetry() - expect(telemetry.observer_notifications).toBe(1) - }) - - it('should reset telemetry', async () => { - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - expect(switcher.getTelemetry().objective_switch_count).toBe(1) - - switcher.resetTelemetry() - expect(switcher.getTelemetry().objective_switch_count).toBe(0) - }) - }) - - describe('audit logging', () => { - it('should create audit log entry on switch', async () => { - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - - const auditLog = switcher.getAuditLog() - expect(auditLog.length).toBe(1) - expect(auditLog[0]?.success).toBe(true) - expect(auditLog[0]?.toContext).toBe(ContextType.EDUCATIONAL) - }) - - it('should include objectives in audit log', async () => { - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - - const auditLog = switcher.getAuditLog() - expect(auditLog[0]?.objectives).toBeInstanceOf(Array) - expect(auditLog[0]?.objectives.length).toBeGreaterThan(0) - }) - - it('should limit audit log size', async () => { - const smallLogSwitcher = new ObjectiveSwitcher({ - maxAuditLogSize: 5, - }) - - for (let i = 0; i < 10; i++) { - const transition: ContextTransition = { - from: { - turnId: i, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now() + i * 1000, - }, - to: { - turnId: i + 1, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + (i + 1) * 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await smallLogSwitcher.onContextTransition(transition) - } - - const auditLog = smallLogSwitcher.getAuditLog() - expect(auditLog.length).toBeLessThanOrEqual(5) - }) - - it('should clear audit log', async () => { - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - expect(switcher.getAuditLog().length).toBe(1) - - switcher.clearAuditLog() - expect(switcher.getAuditLog().length).toBe(0) - }) - }) - - describe('getObjectives', () => { - it('should return read-only objectives', () => { - const objectives = switcher.getObjectives() - - // Should be frozen - expect(Object.isFrozen(objectives)).toBe(true) - }) - - it('should return current objectives after switch', async () => { - const transition: ContextTransition = { - from: { - turnId: 1, - contextType: ContextType.GENERAL, - confidence: 0.7, - urgency: 'low', - timestamp: Date.now(), - }, - to: { - turnId: 2, - contextType: ContextType.EDUCATIONAL, - confidence: 0.9, - urgency: 'low', - timestamp: Date.now() + 1000, - }, - detected: true, - transitionType: 'standard', - shouldSmooth: false, - confidence: 0.9, - } - - await switcher.onContextTransition(transition) - - const objectives = switcher.getObjectives() - expect(objectives.length).toBeGreaterThan(0) - - // Should have informativeness high for educational context - const infoObj = objectives.find((obj) => obj.key === 'informativeness') - expect(infoObj).toBeDefined() - }) - }) - - describe('isSwitching', () => { - it('should return false when not switching', () => { - expect(switcher.isSwitching()).toBe(false) - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/prioritization/objective-switcher.ts b/apps/web/src/lib/metaaligner/prioritization/objective-switcher.ts deleted file mode 100644 index 6dcb858938..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/objective-switcher.ts +++ /dev/null @@ -1,395 +0,0 @@ -/** - * Objective Switching Mechanism for Real-Time Adaptation - * - * Provides concurrency-safe, high-performance objective switching based on context transitions. - * - * Features: - * - Real-time switching (<150ms guaranteed) - * - Concurrency-safe with mutex locking - * - Event-based observer pattern for downstream modules - * - Telemetry and audit logging - * - Graceful degradation on errors - * - No lost updates guarantee - */ - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { - getPrioritizedObjectivesForContext, - ObjectivePriority, -} from './context-objective-mapping' -import { ContextEvent, ContextTransition } from './context-transition-detector' -import { - defaultWeightingStrategy, - WeightingParams, - ObjectiveWeightingStrategy, -} from './objective-weighting-strategy' - -const logger = createBuildSafeLogger('objective-switcher') - -/** - * Observer callback for objective changes - */ -export type ObjectiveSwitchObserver = ( - objectives: ObjectivePriority[], - metadata: SwitchMetadata, -) => void | Promise - -/** - * Metadata about an objective switch event - */ -export interface SwitchMetadata { - fromContext?: string - toContext: string - timestamp: number - duration: number - switchCount: number - urgency: 'low' | 'medium' | 'high' | 'critical' -} - -/** - * Audit log entry for switch history - */ -export interface SwitchAuditLog { - id: string - fromContext?: string - toContext: string - objectives: ObjectivePriority[] - timestamp: number - duration: number - success: boolean - error?: string -} - -/** - * Configuration for ObjectiveSwitcher - */ -export interface ObjectiveSwitcherConfig { - /** Initial context to start with */ - initialContext?: ContextEvent - /** Custom weighting strategy */ - weightingStrategy?: ObjectiveWeightingStrategy - /** Enable telemetry tracking */ - enableTelemetry?: boolean - /** Enable audit logging */ - enableAuditLog?: boolean - /** Maximum audit log size */ - maxAuditLogSize?: number -} - -/** - * Telemetry data for monitoring - */ -export interface SwitcherTelemetry { - objective_switch_count: number - total_switch_duration_ms: number - average_switch_duration_ms: number - failed_switches: number - observer_notifications: number -} - -/** - * Objective Switcher - Concurrency-safe real-time objective switching - * - * Manages objective transitions based on context changes with performance guarantees. - */ -export class ObjectiveSwitcher { - private currentObjectives: ObjectivePriority[] = [] - private lastContext?: ContextEvent - private readonly observers: Set = new Set() - private readonly weightingStrategy: ObjectiveWeightingStrategy - private auditLog: SwitchAuditLog[] = [] - private telemetry: SwitcherTelemetry = { - objective_switch_count: 0, - total_switch_duration_ms: 0, - average_switch_duration_ms: 0, - failed_switches: 0, - observer_notifications: 0, - } - private readonly config: Required< - Omit - > - private switchInProgress = false - private pendingSwitch: ContextTransition | null = null - - constructor(config: ObjectiveSwitcherConfig = {}) { - this.config = { - weightingStrategy: config.weightingStrategy ?? defaultWeightingStrategy, - enableTelemetry: config.enableTelemetry ?? true, - enableAuditLog: config.enableAuditLog ?? true, - maxAuditLogSize: config.maxAuditLogSize ?? 100, - } - - this.weightingStrategy = this.config.weightingStrategy - - if (config.initialContext) { - this.initializeFromContext(config.initialContext) - } - } - - /** - * Initialize objectives from a context event - */ - private initializeFromContext(context: ContextEvent): void { - const objectives = getPrioritizedObjectivesForContext(context.contextType) - const params: WeightingParams = { - urgency: context.urgency, - needsSpecialHandling: false, - } - - this.currentObjectives = this.weightingStrategy(objectives, params) - this.lastContext = context - } - - /** - * Register an observer to be notified of objective changes - */ - public addObserver(observer: ObjectiveSwitchObserver): void { - this.observers.add(observer) - } - - /** - * Remove an observer - */ - public removeObserver(observer: ObjectiveSwitchObserver): void { - this.observers.delete(observer) - } - - /** - * Handle a context transition and switch objectives - * Concurrency-safe with mutex-like behavior - */ - public async onContextTransition( - transition: ContextTransition, - weightingParams?: WeightingParams, - ): Promise { - if (!transition.detected) { - return - } - - // Handle concurrent switch attempts - if (this.switchInProgress) { - this.pendingSwitch = transition - return - } - - await this.performSwitch(transition, weightingParams) - - // Process pending switch if any - if (this.pendingSwitch) { - const pending = this.pendingSwitch - this.pendingSwitch = null - await this.performSwitch(pending, weightingParams) - } - } - - /** - * Perform the actual objective switch - */ - private async performSwitch( - transition: ContextTransition, - weightingParams?: WeightingParams, - ): Promise { - const startTime = performance.now() - const auditId = `switch_${Date.now()}_${Math.random().toString(36).substring(2, 11)}` - - this.switchInProgress = true - - try { - // Get prioritized objectives for new context - const baseObjectives = getPrioritizedObjectivesForContext( - transition.to.contextType, - ) - - // Apply dynamic weighting - const params: WeightingParams = { - urgency: transition.to.urgency, - needsSpecialHandling: transition.transitionType === 'crisis_elevation', - ...weightingParams, - } - - const weightedObjectives = this.weightingStrategy(baseObjectives, params) - - // Update current objectives (atomic) - this.currentObjectives = weightedObjectives - this.lastContext = transition.to - - const duration = performance.now() - startTime - - // Update telemetry - if (this.config.enableTelemetry) { - this.updateTelemetry(duration, true) - } - - // Create metadata - const metadata: SwitchMetadata = { - fromContext: transition.from.contextType, - toContext: transition.to.contextType, - timestamp: Date.now(), - duration, - switchCount: this.telemetry.objective_switch_count, - urgency: transition.to.urgency, - } - - // Audit log - if (this.config.enableAuditLog) { - this.addAuditLog({ - id: auditId, - fromContext: transition.from.contextType, - toContext: transition.to.contextType, - objectives: weightedObjectives, - timestamp: metadata.timestamp, - duration, - success: true, - }) - } - - // Notify observers (non-blocking) - this.notifyObservers(weightedObjectives, metadata) - - // Performance check - if (duration > 150) { - logger.warn('Objective switch exceeded 150ms threshold', { - duration, - fromContext: transition.from.contextType, - toContext: transition.to.contextType, - }) - } - } catch (error: unknown) { - const duration = performance.now() - startTime - - if (this.config.enableTelemetry) { - this.updateTelemetry(duration, false) - } - - if (this.config.enableAuditLog) { - this.addAuditLog({ - id: auditId, - fromContext: transition.from?.contextType, - toContext: transition.to.contextType, - objectives: [], - timestamp: Date.now(), - duration, - success: false, - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - }) - } - - logger.error('Objective switch failed', { - error, - fromContext: transition.from?.contextType, - toContext: transition.to.contextType, - }) - - // Graceful degradation - keep current objectives - } finally { - this.switchInProgress = false - } - } - - /** - * Notify all observers of objective change - */ - private notifyObservers( - objectives: ObjectivePriority[], - metadata: SwitchMetadata, - ): void { - this.observers.forEach(async (observer) => { - try { - await observer(objectives, metadata) - if (this.config.enableTelemetry) { - this.telemetry.observer_notifications++ - } - } catch (error: unknown) { - logger.error('Observer notification failed', { error }) - } - }) - } - - /** - * Update telemetry metrics - */ - private updateTelemetry(duration: number, success: boolean): void { - if (success) { - this.telemetry.objective_switch_count++ - this.telemetry.total_switch_duration_ms += duration - this.telemetry.average_switch_duration_ms = - this.telemetry.total_switch_duration_ms / - this.telemetry.objective_switch_count - } else { - this.telemetry.failed_switches++ - } - } - - /** - * Add entry to audit log - */ - private addAuditLog(entry: SwitchAuditLog): void { - this.auditLog.push(entry) - - // Keep log bounded - if (this.auditLog.length > this.config.maxAuditLogSize) { - this.auditLog.shift() - } - } - - /** - * Get current objectives (read-only) - */ - public getObjectives(): Readonly { - return Object.freeze([...this.currentObjectives]) - } - - /** - * Get current context - */ - public getCurrentContext(): ContextEvent | undefined { - return this.lastContext - } - - /** - * Get telemetry data - */ - public getTelemetry(): Readonly { - return Object.freeze({ ...this.telemetry }) - } - - /** - * Get audit log (for explainability) - */ - public getAuditLog(limit?: number): SwitchAuditLog[] { - const log = [...this.auditLog] - return limit ? log.slice(-limit) : log - } - - /** - * Clear audit log - */ - public clearAuditLog(): void { - this.auditLog = [] - } - - /** - * Reset telemetry counters - */ - public resetTelemetry(): void { - this.telemetry = { - objective_switch_count: 0, - total_switch_duration_ms: 0, - average_switch_duration_ms: 0, - failed_switches: 0, - observer_notifications: 0, - } - } - - /** - * Check if a switch is currently in progress - */ - public isSwitching(): boolean { - return this.switchInProgress - } -} diff --git a/apps/web/src/lib/metaaligner/prioritization/objective-weighting-strategy.ts b/apps/web/src/lib/metaaligner/prioritization/objective-weighting-strategy.ts deleted file mode 100644 index d01da842ca..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/objective-weighting-strategy.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Dynamic Objective Weighting Algorithms for Context-Sensitive Alignment -// Provides strategy-based weighting adjustment based on context, urgency, and extensible factors. - -import type { ObjectivePriority } from './context-objective-mapping' - -export interface WeightingParams { - urgency?: 'low' | 'medium' | 'high' | 'critical' - needsSpecialHandling?: boolean - customFactors?: Record -} - -export type ObjectiveWeightingStrategy = ( - objectives: ObjectivePriority[], - params: WeightingParams, -) => ObjectivePriority[] - -/** - * Default dynamic weighting algorithm. - * - Boosts safety/emp. - * - Normalizes all weights to sum=1. - * - Users can provide custom adjustment functions. - */ -export const defaultWeightingStrategy: ObjectiveWeightingStrategy = ( - objectives, - params, -) => { - let adjusted = [...objectives] - - // Example: Boost weighting of safety & empathy if high urgency or special protocols - if ( - params.urgency === 'high' || - params.urgency === 'critical' || - params.needsSpecialHandling - ) { - adjusted = adjusted.map((obj) => { - if (['safety', 'empathy'].includes(obj.key)) { - return { ...obj, weight: Math.min(1, obj.weight * 1.2) } - } - return obj - }) - } - - // Custom user factors (extensible for future needs) - if (params.customFactors) { - adjusted = adjusted.map((obj) => - obj.key in (params.customFactors ?? {}) - ? { ...obj, weight: obj.weight * (params.customFactors![obj.key] ?? 1) } - : obj, - ) - } - - // Normalize so weights sum to 1 - const sum = adjusted.reduce((acc, obj) => acc + obj.weight, 0) || 1 - adjusted = adjusted.map((obj) => ({ - ...obj, - weight: Number((obj.weight / sum).toFixed(4)), - })) - - return adjusted -} diff --git a/apps/web/src/lib/metaaligner/prioritization/support-context-identifier.test.ts b/apps/web/src/lib/metaaligner/prioritization/support-context-identifier.test.ts deleted file mode 100644 index a3375e291f..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/support-context-identifier.test.ts +++ /dev/null @@ -1,624 +0,0 @@ -/** - * Unit tests for Support Context Identifier System - */ - -import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' - -import type { AIService } from '../../ai/models/ai-types' -import { - createSupportContextIdentifier, - EmotionalState, - getDefaultSupportIdentifierConfig, - RecommendedApproach, - SupportContextIdentifier, - type SupportContextResult, - type SupportIdentifierConfig, - SupportNeed, - SupportType, -} from './support-context-identifier' - -// Extend AIService type for tests to include generateText for mocking -type TestAIService = AIService & { - generateText: Mock - generateStructuredResponse: Mock - isAvailable: Mock -} - -const mockAIService: TestAIService = { - // Provide all required methods of AIService and the test mocks - generateText: vi.fn(), - generateStructuredResponse: vi.fn(), - isAvailable: vi.fn().mockResolvedValue(true), - // Minimal mock stubs for required AIService methods - getModelInfo: vi.fn(), - createChatCompletion: vi.fn(), - createChatStream: vi.fn(), -} as any - -describe('SupportContextIdentifier', () => { - let identifier: SupportContextIdentifier - let config: SupportIdentifierConfig - - beforeEach(() => { - vi.clearAllMocks() - config = { - aiService: mockAIService, - model: 'test-model', - enableEmotionalAnalysis: true, - enableCopingAssessment: true, - adaptToEmotionalState: true, - } - identifier = new SupportContextIdentifier(config) - }) - - describe('Constructor and Configuration', () => { - it('should create identifier with provided config', () => { - expect(identifier).toBeInstanceOf(SupportContextIdentifier) - }) - - it('should use default values for optional config properties', () => { - const minimalConfig = { aiService: mockAIService } - const minimalIdentifier = new SupportContextIdentifier(minimalConfig) - expect(minimalIdentifier).toBeInstanceOf(SupportContextIdentifier) - }) - }) - - describe('Pattern-Based Support Recognition', () => { - it('should identify emotional validation requests', async () => { - const query = - 'I feel so terrible about this situation and just need someone to understand' - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['supportType']).toBe(SupportType.EMOTIONAL_VALIDATION) - expect(result['confidence']).toBeGreaterThan(0.6) - }) - - it('should identify coping assistance requests', async () => { - const query = - "I don't know how to handle this stress at work, what should I do?" - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['supportType']).toBe(SupportType.COPING_ASSISTANCE) - expect(result['emotionalState']).toBe(EmotionalState.ANXIETY) - }) - - it('should identify encouragement needs', async () => { - const query = "I'm losing hope and feel like giving up on everything" - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['supportType']).toBe(SupportType.ENCOURAGEMENT) - expect(result['emotionalState']).toBe(EmotionalState.HOPELESSNESS) - expect(result['urgency']).toBe('high') - }) - - it('should identify active listening requests', async () => { - const query = - 'I just need someone to listen to me right now, no advice needed' - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['supportType']).toBe(SupportType.ACTIVE_LISTENING) - }) - - it('should identify practical guidance requests', async () => { - const query = - 'What steps should I take to deal with my relationship problems?' - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['supportType']).toBe(SupportType.PRACTICAL_GUIDANCE) - }) - - it('should identify grief support needs', async () => { - const query = - "I'm grieving the loss of my father and don't know how to cope" - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['supportType']).toBe(SupportType.GRIEF_SUPPORT) - expect(result['emotionalState']).toBe(EmotionalState.SADNESS) - }) - }) - - describe('Emotional State Detection', () => { - it('should detect sadness indicators', async () => { - const query = - 'I feel so sad and heartbroken about everything that happened' - const result = await identifier.identifySupportContext(query) - - expect(result['emotionalState']).toBe(EmotionalState.SADNESS) - expect(result['emotionalIntensity']).toBeGreaterThan(0.6) - }) - - it('should detect anxiety indicators', async () => { - const query = "I'm so anxious and worried, my mind won't stop racing" - const result = await identifier.identifySupportContext(query) - - expect(result['emotionalState']).toBe(EmotionalState.ANXIETY) - expect(result['emotionalIntensity']).toBeGreaterThan(0.5) - }) - - it('should detect anger indicators', async () => { - const query = "I'm so frustrated and angry about this situation" - const result = await identifier.identifySupportContext(query) - - expect(result['emotionalState']).toBe(EmotionalState.ANGER) - }) - - it('should detect overwhelm indicators', async () => { - const query = - "I'm completely overwhelmed and can't handle anything anymore" - const result = await identifier.identifySupportContext(query) - - expect(result['emotionalState']).toBe(EmotionalState.OVERWHELM) - expect(result['urgency']).toBe('high') - }) - - it('should detect loneliness indicators', async () => { - const query = 'I feel so alone and isolated, nobody understands me' - const result = await identifier.identifySupportContext(query) - - expect(result['emotionalState']).toBe(EmotionalState.LONELINESS) - }) - }) - - describe('AI-Powered Analysis', () => { - beforeEach(() => { - const mockAIResponse = { - isSupport: true, - confidence: 0.85, - supportType: 'emotional_validation', - emotionalState: 'sadness', - urgency: 'medium', - supportNeeds: ['validation', 'emotional_regulation'], - recommendedApproach: 'empathetic_listening', - emotionalIntensity: 0.7, - metadata: { - emotionalIndicators: ['sadness', 'distress'], - copingCapacity: 'medium', - socialSupport: 'limited', - immediateNeeds: ['emotional support'], - }, - } - - mockAIService.generateText.mockResolvedValue( - JSON.stringify(mockAIResponse), - ) - }) - - it('should perform AI analysis when pattern confidence is low', async () => { - const query = 'The temperature outside is 72 degrees Fahrenheit' - const result = await identifier.identifySupportContext(query) - - expect(mockAIService.generateText).toHaveBeenCalled() - expect(result['isSupport']).toBe(true) - expect(result['confidence']).toBeLessThan(0.1) - }) - - it('should include conversation history in AI analysis', async () => { - const query = "I'm still struggling with what we discussed" - const history = [ - 'Previous message about depression', - 'Another context message', - ] - - await identifier.identifySupportContext(query, history) - - expect(mockAIService.generateText).toHaveBeenCalledWith( - expect.stringContaining('Previous message about depression'), - ) - }) - - it('should adapt to user emotional profile', async () => { - const query = 'Having a hard time today' - const emotionalProfile = { - baselineEmotionalState: EmotionalState.ANXIETY, - typicalCopingStrategies: ['mindfulness', 'journaling'], - emotionalTriggers: ['work stress', 'relationships'], - supportPreferences: ['gentle validation', 'practical advice'], - } - - await identifier.identifySupportContext(query, [], emotionalProfile) - - expect(mockAIService.generateText).toHaveBeenCalledWith( - expect.stringContaining('anxiety'), - ) - }) - - it('should handle AI service errors gracefully', async () => { - mockAIService.generateText.mockRejectedValue( - new Error('AI service unavailable'), - ) - - const query = 'The library has 15,000 books in its collection' - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['confidence']).toBeLessThan(0.8) - }) - - it('should handle malformed AI responses', async () => { - mockAIService.generateText.mockResolvedValue('Invalid JSON response') - - const query = 'I need support' - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['supportType']).toBeDefined() - }) - }) - - describe('Support Recommendations', () => { - it('should generate appropriate recommendations for emotional validation', async () => { - const mockResult: SupportContextResult = { - isSupport: true, - confidence: 0.8, - supportType: SupportType.EMOTIONAL_VALIDATION, - emotionalState: EmotionalState.SADNESS, - urgency: 'medium', - supportNeeds: [ - SupportNeed.VALIDATION, - SupportNeed.EMOTIONAL_REGULATION, - ], - recommendedApproach: RecommendedApproach.EMPATHETIC_LISTENING, - emotionalIntensity: 0.6, - metadata: { - emotionalIndicators: ['sadness'], - copingCapacity: 'medium', - socialSupport: 'limited', - immediateNeeds: ['emotional support'], - }, - } - - const recommendations = - identifier.generateSupportRecommendations(mockResult) - - expect(recommendations['responseStyle']['tone']).toBe('warm') - expect(recommendations['responseStyle']['approach']).toBe('validating') - }) - - it('should generate crisis-appropriate recommendations for high urgency', async () => { - const mockResult: SupportContextResult = { - isSupport: true, - confidence: 0.9, - supportType: SupportType.ENCOURAGEMENT, - emotionalState: EmotionalState.HOPELESSNESS, - urgency: 'high', - supportNeeds: [ - SupportNeed.SAFETY_PLANNING, - SupportNeed.HOPE_RESTORATION, - ], - recommendedApproach: RecommendedApproach.CRISIS_INTERVENTION, - emotionalIntensity: 0.9, - metadata: { - emotionalIndicators: ['hopelessness', 'despair'], - copingCapacity: 'low', - socialSupport: 'limited', - immediateNeeds: ['crisis support', 'safety planning'], - }, - } - - const recommendations = - identifier.generateSupportRecommendations(mockResult) - - expect(recommendations.resources).toEqual( - expect.arrayContaining([ - expect.stringMatching(/crisis|hotline|emergency/i), - ]), - ) - }) - - it('should provide practical guidance recommendations', async () => { - const mockResult: SupportContextResult = { - isSupport: true, - confidence: 0.85, - supportType: SupportType.PRACTICAL_GUIDANCE, - emotionalState: EmotionalState.ANXIETY, - urgency: 'medium', - supportNeeds: [ - SupportNeed.PRACTICAL_ADVICE, - SupportNeed.SKILL_BUILDING, - ], - recommendedApproach: RecommendedApproach.PROBLEM_SOLVING, - emotionalIntensity: 0.5, - metadata: { - emotionalIndicators: ['worry', 'uncertainty'], - copingCapacity: 'medium', - socialSupport: 'moderate', - immediateNeeds: ['guidance', 'steps'], - }, - } - - const recommendations = - identifier.generateSupportRecommendations(mockResult) - - expect(recommendations['longerTermStrategies'].join(' ')).toMatch( - /skill|practice|develop/i, - ) - expect(recommendations['responseStyle']['approach']).toBe( - 'solution-focused', - ) - }) - }) - - describe('Batch Processing', () => { - beforeEach(() => { - const mockAIResponse = { - isSupport: true, - confidence: 0.8, - supportType: 'emotional_validation', - emotionalState: 'sadness', - urgency: 'medium', - supportNeeds: ['validation'], - recommendedApproach: 'empathetic_listening', - emotionalIntensity: 0.6, - metadata: { - emotionalIndicators: ['sadness'], - copingCapacity: 'medium', - socialSupport: 'moderate', - immediateNeeds: ['support'], - }, - } - - mockAIService.generateText.mockResolvedValue( - JSON.stringify(mockAIResponse), - ) - }) - - it('should process multiple queries in batch', async () => { - const queries = [ - { query: 'I feel sad today' }, - { query: 'Need help with stress' }, - { query: 'Feeling overwhelmed' }, - ] - - const results = await identifier.identifyBatch(queries) - - expect(results).toHaveLength(3) - results.forEach((result) => { - expect(result['isSupport']).toBe(true) - expect(result['confidence']).toBeGreaterThan(0) - }) - }) - - it('should handle batch processing errors gracefully', async () => { - mockAIService.generateText - .mockResolvedValueOnce('{"isSupport": true, "confidence": 0.8}') - .mockRejectedValueOnce(new Error('AI error')) - .mockResolvedValueOnce('{"isSupport": true, "confidence": 0.7}') - - const queries = [ - { query: 'I feel sad today' }, - { query: 'I need emotional support' }, - { query: 'Feeling overwhelmed' }, - ] - - const results = await identifier.identifyBatch(queries) - - expect(results).toHaveLength(3) - expect(results[0]?.['isSupport']).toBe(true) - expect(results[1]?.['isSupport']).toBeDefined() // Should have fallback result - expect(results[2]?.['isSupport']).toBe(true) - }) - }) - - describe('Urgency Assessment', () => { - it('should classify high urgency for crisis indicators', async () => { - const query = - "I'm completely overwhelmed and breaking point, can't handle anything" - const result = await identifier.identifySupportContext(query) - - expect(result['urgency']).toBe('high') - expect(result['emotionalIntensity']).toBeGreaterThan(0.7) - }) - - it('should classify medium urgency for moderate distress', async () => { - const query = "I'm feeling pretty anxious about this situation" - const result = await identifier.identifySupportContext(query) - - expect(['low', 'medium', 'high']).toContain(result['urgency']) - }) - - it('should classify low urgency for mild concerns', async () => { - const query = "I'm slightly worried about an upcoming event" - const result = await identifier.identifySupportContext(query) - - expect(['low', 'medium', 'high']).toContain(result['urgency']) - }) - }) - - describe('Emotional Intensity Calculation', () => { - it('should calculate high intensity for severe distress', async () => { - const query = "I'm devastated, heartbroken, and completely falling apart" - const result = await identifier.identifySupportContext(query) - - expect(result['emotionalIntensity']).toBeGreaterThan(0.8) - }) - - it('should calculate moderate intensity for typical distress', async () => { - const query = "I'm feeling sad and need some support" - const result = await identifier.identifySupportContext(query) - - expect(result['emotionalIntensity']).toBeGreaterThan(0.3) - expect(result['emotionalIntensity']).toBeLessThan(0.8) - }) - - it('should calculate low intensity for mild concerns', async () => { - const query = "I'm slightly concerned about this situation" - const result = await identifier.identifySupportContext(query) - - expect(result['emotionalIntensity']).toBeLessThan(0.6) - }) - }) - - describe('Coping Capacity Assessment', () => { - it('should identify low coping capacity', async () => { - const query = - "I can't cope anymore, everything is too much, I'm falling apart" - const result = await identifier.identifySupportContext(query) - - expect(result['metadata']['copingCapacity']).toBe('low') - }) - - it('should identify medium coping capacity', async () => { - const query = "I'm struggling with this situation but trying to manage" - const result = await identifier.identifySupportContext(query) - - expect(['low', 'medium', 'high']).toContain( - result['metadata']['copingCapacity'], - ) - }) - - it('should identify high coping capacity', async () => { - const query = "I'm handling things well but could use some advice" - const result = await identifier.identifySupportContext(query) - - expect(result['metadata']['copingCapacity']).toBe('medium') - }) - }) - - describe('Non-Support Content Detection', () => { - it('should correctly identify non-support queries', async () => { - const query = 'What is the capital of France?' - const result = await identifier.identifySupportContext(query) - - expect([false, true]).toContain(result['isSupport']) // Accepts both - business logic may auto-identify indirect support - expect(result['confidence']).toBeLessThan(0.3) - }) - - it('should handle informational queries', async () => { - const query = 'Can you explain how depression medication works?' - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['confidence']).toBeLessThanOrEqual(0.5) - }) - - it('should handle casual conversation', async () => { - const query = 'How was your day today?' - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - }) - }) - - describe('Integration and Edge Cases', () => { - it('should handle empty queries', async () => { - const result = await identifier.identifySupportContext('') - - expect(result['isSupport']).toBe(true) - expect(result['confidence']).toBe(0) - }) - - it('should handle very long queries', async () => { - const longQuery = 'I feel sad '.repeat(100) + 'and need support' - const result = await identifier.identifySupportContext(longQuery) - - expect(result['isSupport']).toBe(true) - expect(result['emotionalState']).toBe(EmotionalState.SADNESS) - }) - - it('should handle mixed emotional states', async () => { - const query = - "I'm feeling sad but also angry and confused about everything" - const result = await identifier.identifySupportContext(query) - - expect(result['isSupport']).toBe(true) - expect(result['emotionalState']).toBeDefined() - expect(result['metadata']['emotionalIndicators'].length).toBeGreaterThan( - 1, - ) - }) - - it('should maintain consistent results for similar queries', async () => { - const query1 = "I'm feeling very sad and need emotional support" - const query2 = 'I feel really sad and need someone to understand' - - const result1 = await identifier.identifySupportContext(query1) - const result2 = await identifier.identifySupportContext(query2) - - expect(result1['supportType']).toBe(result2['supportType']) - expect(result1['emotionalState']).toBe(result2['emotionalState']) - }) - }) - - describe('Factory Functions', () => { - it('should create identifier with factory function', () => { - const factoryIdentifier = createSupportContextIdentifier(config) - expect(factoryIdentifier).toBeInstanceOf(SupportContextIdentifier) - }) - - it('should create default config', () => { - const defaultConfig = getDefaultSupportIdentifierConfig(mockAIService) - - expect(defaultConfig['aiService']).toBe(mockAIService) - expect(defaultConfig['model']).toBe('claude-3-sonnet') - expect(defaultConfig['enableEmotionalAnalysis']).toBe(true) - expect(defaultConfig['enableCopingAssessment']).toBe(true) - expect(defaultConfig['adaptToEmotionalState']).toBe(true) - }) - }) - - describe('Error Handling and Validation', () => { - it('should handle invalid support types gracefully', async () => { - mockAIService.generateText.mockResolvedValue( - JSON.stringify({ - isSupport: true, - supportType: 'invalid_type', - emotionalState: 'sadness', - confidence: 0.8, - }), - ) - - const result = await identifier.identifySupportContext('I need help') - - expect(result.supportType).toBe(SupportType.EMOTIONAL_VALIDATION) // Should fallback - }) - - it('should handle invalid emotional states gracefully', async () => { - mockAIService.generateText.mockResolvedValue( - JSON.stringify({ - isSupport: true, - supportType: 'emotional_validation', - emotionalState: 'invalid_emotion', - confidence: 0.8, - }), - ) - - const result = await identifier.identifySupportContext('I feel bad') - - expect(result.emotionalState).toBe(EmotionalState.MIXED_EMOTIONS) // Should fallback - }) - - it('should validate urgency levels', async () => { - mockAIService.generateText.mockResolvedValue( - JSON.stringify({ - isSupport: true, - urgency: 'extreme', - confidence: 0.8, - }), - ) - - const result = await identifier.identifySupportContext('Emergency help') - - expect(['low', 'medium', 'high']).toContain(result.urgency) - }) - - it('should handle network timeouts gracefully', async () => { - mockAIService.generateText.mockImplementation( - async () => - new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), 100), - ), - ) - - const result = await identifier.identifySupportContext('I need support') - - expect(result.isSupport).toBeDefined() - expect(result.confidence).toBeGreaterThan(0) - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/prioritization/support-context-identifier.ts b/apps/web/src/lib/metaaligner/prioritization/support-context-identifier.ts deleted file mode 100644 index 6ba99e520f..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/support-context-identifier.ts +++ /dev/null @@ -1,1750 +0,0 @@ -/** - * Support Context Identification System - * Specialized component for identifying and classifying emotional support needs - */ - -import type { - AIService, - AIMessage, - AICompletion, -} from '../../ai/models/ai-types' -import { createBuildSafeLogger } from '../../logging/build-safe-logger' - -const logger = createBuildSafeLogger('support-context-identifier') - -/** - * Interface for user emotional profile - */ -export interface UserEmotionalProfile { - baselineEmotionalState?: EmotionalState - typicalCopingStrategies?: string[] - emotionalTriggers?: string[] - supportPreferences?: string[] -} - -export interface SupportContextResult { - isSupport: boolean - confidence: number - supportType: SupportType - emotionalState: EmotionalState - urgency: 'low' | 'medium' | 'high' - supportNeeds: SupportNeed[] - recommendedApproach: RecommendedApproach - emotionalIntensity: number // 0-1 scale - metadata: { - emotionalIndicators: string[] - copingCapacity: 'high' | 'medium' | 'low' - socialSupport: 'strong' | 'moderate' | 'limited' | 'unknown' - immediateNeeds: string[] - triggerEvents?: string[] - resilientFactors?: string[] - requiresHumanReview?: boolean - crisisInterventionFlagged?: boolean - } -} - -export enum SupportType { - EMOTIONAL_VALIDATION = 'emotional_validation', // "I feel terrible and need someone to understand" - COPING_ASSISTANCE = 'coping_assistance', // "I don't know how to handle this" - ENCOURAGEMENT = 'encouragement', // "I'm losing hope and need motivation" - ACTIVE_LISTENING = 'active_listening', // "I just need someone to listen" - PRACTICAL_GUIDANCE = 'practical_guidance', // "What should I do about..." - GRIEF_SUPPORT = 'grief_support', // "I'm grieving a loss" - RELATIONSHIP_SUPPORT = 'relationship_support', // "Having relationship problems" - STRESS_MANAGEMENT = 'stress_management', // "I'm overwhelmed with stress" - IDENTITY_SUPPORT = 'identity_support', // "I don't know who I am anymore" - TRANSITION_SUPPORT = 'transition_support', // "Going through major life changes" - TRAUMA_SUPPORT = 'trauma_support', // "Dealing with past trauma" - DAILY_FUNCTIONING = 'daily_functioning', // "Struggling with day-to-day activities" -} - -export enum EmotionalState { - SADNESS = 'sadness', - ANXIETY = 'anxiety', - ANGER = 'anger', - FEAR = 'fear', - GUILT = 'guilt', - SHAME = 'shame', - LONELINESS = 'loneliness', - HELPLESSNESS = 'helplessness', - HOPELESSNESS = 'hopelessness', - OVERWHELM = 'overwhelm', - NUMBNESS = 'numbness', - CONFUSION = 'confusion', - MIXED_EMOTIONS = 'mixed_emotions', -} - -export enum SupportNeed { - VALIDATION = 'validation', - PRACTICAL_ADVICE = 'practical_advice', - EMOTIONAL_REGULATION = 'emotional_regulation', - PERSPECTIVE_TAKING = 'perspective_taking', - RESOURCE_CONNECTION = 'resource_connection', - SAFETY_PLANNING = 'safety_planning', - HOPE_RESTORATION = 'hope_restoration', - SKILL_BUILDING = 'skill_building', - RELATIONSHIP_REPAIR = 'relationship_repair', - MEANING_MAKING = 'meaning_making', -} - -export enum RecommendedApproach { - EMPATHETIC_LISTENING = 'empathetic_listening', - GENTLE_GUIDANCE = 'gentle_guidance', - COGNITIVE_REFRAMING = 'cognitive_reframing', - EMOTIONAL_REGULATION = 'emotional_regulation', - PROBLEM_SOLVING = 'problem_solving', - RESOURCE_REFERRAL = 'resource_referral', - CRISIS_INTERVENTION = 'crisis_intervention', - PSYCHOEDUCATION = 'psychoeducation', - MINDFULNESS_BASED = 'mindfulness_based', - STRENGTH_BASED = 'strength_based', -} - -export interface SupportIdentifierConfig { - aiService: AIService - model?: string - enableEmotionalAnalysis?: boolean - enableCopingAssessment?: boolean - adaptToEmotionalState?: boolean -} -/** - * Patterns that indicate queries are informational/casual and NOT seeking support. - * This must be defined exactly once at module scope, before any use. - */ -const nonSupportPatterns = [ - /\b(?:temperature|degrees|fahrenheit|celsius|weather|72|outside)\b/i, - /\b(?:capital of|largest city|president|who is|when was|explain|how does|can you explain|what is|define|history of|population|data shows|statistics|recipe|directions)\b/i, - /\b(?:how was your day|weekend plans|watch the game|did you see|favorite color|what did you eat|where are you from|tell me a joke|good morning|good night|thank you|just checking in|hi |hello |bye |see you)\b/i, - /\b(?:capital of france|how depression medication works|casual conversation)\b/i, -] - -/** - * System prompt for support context identification - */ -const SUPPORT_IDENTIFICATION_PROMPT = `You are a mental health support specialist trained to identify emotional support needs. Analyze the user's message to determine the type of support they need and their emotional state. - - -Your task is to: -1. Determine if this is primarily a support-seeking query -2. Identify the specific type of support needed -3. Assess the user's emotional state and intensity -4. Determine appropriate support needs and approach -5. Evaluate urgency and coping capacity - -Support Types: -- emotional_validation: Need for empathy and understanding -- coping_assistance: Need help managing difficult situations -- encouragement: Need motivation and hope -- active_listening: Just need someone to hear them -- practical_guidance: Need specific advice or steps -- grief_support: Processing loss or endings -- relationship_support: Dealing with interpersonal issues -- stress_management: Overwhelmed and need stress relief -- identity_support: Questioning self or purpose -- transition_support: Major life changes -- trauma_support: Processing traumatic experiences -- daily_functioning: Struggling with basic activities - -Emotional States: -sadness, anxiety, anger, fear, guilt, shame, loneliness, helplessness, hopelessness, overwhelm, numbness, confusion, mixed_emotions - -Support Needs: -validation, practical_advice, emotional_regulation, perspective_taking, resource_connection, safety_planning, hope_restoration, skill_building, relationship_repair, meaning_making - -Recommended Approaches: -empathetic_listening, gentle_guidance, cognitive_reframing, emotional_regulation, problem_solving, resource_referral, crisis_intervention, psychoeducation, mindfulness_based, strength_based - -Respond in JSON format with: -- isSupport: boolean -- confidence: number (0-1) -- supportType: one of the support types above -- emotionalState: primary emotional state -- urgency: low/medium/high based on distress level -- supportNeeds: array of relevant support needs -- recommendedApproach: most appropriate approach -- emotionalIntensity: number (0-1) indicating distress level -- metadata: object with emotional indicators, coping capacity assessment, and immediate needs - -Focus on compassionate understanding and accurate assessment.` - -/** - * Support Context Identification Engine - */ -export class SupportContextIdentifier { - private readonly aiService: AIService - private readonly model: string - private readonly enableEmotionalAnalysis: boolean - private readonly enableCopingAssessment!: boolean - private readonly adaptToEmotionalState!: boolean - - // Emotional expression patterns for quick detection - private readonly emotionalPatterns = { - sadness: [ - /\b(?:sad|depressed|down|blue|miserable|heartbroken|devastated)\b/i, - /\b(?:crying|tears|weeping|sobbing)\b/i, - /\bfeel\s+(?:so\s+|very\s+|really\s+|extremely\s+)?(?:awful|terrible|horrible|sad|down|miserable)\b/i, - /\b(?:awful|terrible|horrible)\b/i, - ], - anxiety: [ - /\b(?:anxious|worried|nervous|scared|terrified|panicking)\b/i, - /\b(?:can't stop worrying|racing thoughts|mind won't stop)\b/i, - /\b(?:panic|anxiety|stress|overwhelmed)\b/i, - /\bfeel\s+(?:so\s+|very\s+|really\s+)?(?:anxious|worried|nervous|scared)\b/i, - ], - anger: [ - /\b(?:angry|furious|mad|rage|frustrated|irritated|annoyed)\b/i, - /\b(?:can't stand|hate|infuriating|makes me angry)\b/i, - /\bfeel\s+(?:so\s+|very\s+|really\s+)?(?:angry|frustrated|mad|furious)\b/i, - ], - hopelessness: [ - /\b(?:hopeless|no point|give up|what's the point|nothing matters)\b/i, - /\b(?:no way out|can't see a future|everything is pointless)\b/i, - /\b(?:losing hope|giving up|feel like giving up)\b/i, - ], - loneliness: [ - /\b(?:lonely|alone|isolated|no one understands|no friends)\b/i, - /\b(?:feel disconnected|nobody cares|all by myself)\b/i, - /\bfeel\s+(?:so\s+|very\s+|really\s+)?(?:alone|lonely|isolated)\b/i, - ], - overwhelm: [ - /\b(?:overwhelmed|can't cope|too much|breaking point|drowning)\b/i, - /\b(?:can't handle|falling apart|everything is too much)\b/i, - /\bfeel\s+(?:so\s+|very\s+|really\s+|completely\s+)?(?:overwhelmed|stressed)\b/i, - ], - } - - // Support-seeking language patterns - private readonly supportPatterns = { - emotional_validation: [ - /\b(?:understand|get it|feel the same|been there|relate to)\b/i, - /\b(?:validate|normal|okay to feel|makes sense)\b/i, - /\b(?:I feel|I'm feeling|feeling like|emotions)\b/i, - /\b(?:feel terrible|feel awful|need someone to understand)\b/i, - /\b(?:just need someone|need understanding|need empathy)\b/i, - ], - coping_assistance: [ - /\b(?:how do I cope|how can I cope|coping strategies|coping mechanisms)\b/i, - /\b(?:strategies for|ways to manage|help me manage)\b/i, - /\b(?:don't know how to cope|struggling to cope)\b/i, - /\b(?:need help coping|can't handle this|overwhelmed and need)\b/i, - /\b(?:don't know how to handle|how to handle this|what should I do)\b/i, - /\b(?:handle this stress|cope with|deal with this)\b/i, - ], - encouragement: [ - /\b(?:need hope|give up|motivation|strength|keep going|hang in there)\b/i, - /\b(?:encourage|support|believe in|can do this)\b/i, - /\b(?:losing hope|giving up|feel like giving up)\b/i, - /\b(?:need motivation|need strength|losing faith)\b/i, - ], - active_listening: [ - /\b(?:just need to talk|someone to listen|hear me out|vent|share)\b/i, - /\b(?:no advice|just listen|need to express)\b/i, - /\b(?:just need someone to listen|need to talk)\b/i, - /\b(?:listen to me|need someone to listen)\b/i, - /\b(?:no advice needed|just want to talk)\b/i, - ], - practical_guidance: [ - /\b(?:what steps should I take|what specific steps|step by step approach)\b/i, - /\b(?:what should I do about|what action should I take|what's the best way to)\b/i, - /\b(?:how should I deal with|how should I handle|how to approach)\b/i, - /\b(?:need direction|need guidance|need advice|need recommendations)\b/i, - /\b(?:practical advice|actionable steps|concrete steps)\b/i, - ], - grief_support: [ - /\b(?:grieving|grief|loss|lost|death|died|passed away)\b/i, - /\b(?:mourning|bereavement|funeral|memorial)\b/i, - /\b(?:don't know how to cope|dealing with loss)\b/i, - /\b(?:loss of my|grieving the loss)\b/i, - /\b(?:father|mother|parent|family member)\b.*\b(?:died|passed|loss)\b/i, - ], - } - - // Coping capacity indicators - private readonly copingIndicators = { - high: [ - /\b(?:usually handle|normally cope|have support|tried before|strategies that work)\b/i, - /\b(?:resilient|strong|bounce back|get through things)\b/i, - /\b(?:good at|skilled at|experienced with|confident in)\b.*\b(?:handling|managing|coping)\b/i, - /\b(?:handle things well|cope well|manage stress|good support system)\b/i, - /\b(?:handling things well|managing well|doing okay|managing just fine)\b/i, - ], - medium: [ - /\b(?:sometimes works|hit or miss|depends on the day|ups and downs)\b/i, - /\b(?:struggling more lately|harder than usual)\b/i, - ], - low: [ - /\b(?:can't cope|falling apart|nothing works|given up|no energy)\b/i, - /\b(?:breaking down|can't function|completely overwhelmed)\b/i, - ], - } - - constructor(config: SupportIdentifierConfig) { - this.aiService = config.aiService - this.model = config.model ?? 'claude-4-sonnet' - this.enableEmotionalAnalysis = config.enableEmotionalAnalysis ?? true - this.enableCopingAssessment = config.enableCopingAssessment ?? true - this.adaptToEmotionalState = config.adaptToEmotionalState ?? true - } - - /** - * Identify support context in user query - */ - async identifySupportContext( - userQuery: string, - conversationHistory?: string[], - userEmotionalProfile?: { - baselineEmotionalState?: EmotionalState - typicalCopingStrategies?: string[] - emotionalTriggers?: string[] - supportPreferences?: string[] - }, - ): Promise { - try { - const isEmpty = !userQuery || userQuery.trim().length === 0 - // Quick pattern-based screening - const patternResult = this.performPatternBasedIdentification(userQuery) - - // Empty queries: return immediately (tests expect confidence 0 and no AI bump) - if (isEmpty) { - return patternResult - } - - const isNonSupport = nonSupportPatterns.some((p) => - p.test(userQuery.toLowerCase()), - ) - - // Check if we should use AI analysis based on pattern confidence - const shouldUseAI = - patternResult.confidence <= 0.5 && this.enableEmotionalAnalysis - - // Only short-circuit when AI is not needed; otherwise proceed to AI analysis - if (!shouldUseAI) { - return patternResult - } - - try { - // AI-powered detailed analysis - const aiResult = await this.performAIAnalysis( - userQuery, - conversationHistory, - userEmotionalProfile, - ) - - // Combine pattern and AI results only if AI analysis succeeded - if (aiResult.confidence > 0.5) { - const combined = this.combineResults(patternResult, aiResult) - // For informational/casual queries, keep confidence low even after AI (tests expect low) - if (isNonSupport) { - return { ...combined, confidence: 0.05, isSupport: true } - } - return combined - } else { - // If AI failed, ensure fallback confidence is lower than 0.8 - return { - ...patternResult, - confidence: Math.min( - Math.max(patternResult.confidence || 0.05, 0.05), - 0.7, - ), - // Explicitly treat as support to satisfy error-handling expectations - isSupport: true, - } - } - } catch (error: unknown) { - logger.error('AI analysis failed, using pattern result:', { - context: 'ai-analysis', - error: error instanceof Error ? String(error) : String(error), - }) - // If AI throws, ensure fallback confidence is lower than 0.8 - // For informational/casual queries, ensure isSupport true with very low confidence - if (isNonSupport) { - return { ...patternResult, isSupport: true, confidence: 0.05 } - } - return { - ...patternResult, - confidence: Math.min( - Math.max(patternResult.confidence || 0.05, 0.05), - 0.7, - ), - // Tests expect isSupport true when AI path was attempted - isSupport: true, - } - } - } catch (error: unknown) { - logger.error('Error identifying support context:', { - context: 'support-identification', - error: error instanceof Error ? String(error) : String(error), - }) - - // Fallback to pattern-based result - return this.performPatternBasedIdentification(userQuery) - } - } - - /** - * Batch process multiple queries for support context - */ - async identifyBatch( - queries: Array<{ - query: string - conversationHistory?: string[] - userEmotionalProfile?: UserEmotionalProfile - }>, - ): Promise { - return Promise.all( - queries.map( - async ({ query, conversationHistory, userEmotionalProfile }) => { - try { - const result = await this.identifySupportContext( - query, - conversationHistory, - userEmotionalProfile, - ) - // If result isSupport is false but pattern-based says true, use pattern-based - if (!result.isSupport) { - const patternResult = - this.performPatternBasedIdentification(query) - if (patternResult.isSupport) { - return patternResult - } - } - return result - } catch { - // Fallback to pattern-based result on error - const fallback = this.performPatternBasedIdentification(query) - // Always set isSupport true for batch fallback if pattern matches or any emotional content is detected - if (fallback.confidence > 0 || fallback.emotionalIntensity > 0.3) { - fallback.isSupport = true - } - // Ensure isSupport is always defined for batch processing - fallback.isSupport ??= - fallback.confidence > 0 || fallback.emotionalIntensity > 0.3 - return fallback - } - }, - ), - ) - } - - /** - * Generate support recommendations based on identified context - */ - generateSupportRecommendations(result: SupportContextResult): { - immediateActions: string[] - longerTermStrategies: string[] - resources: string[] - responseStyle: { - tone: 'warm' | 'professional' | 'gentle' | 'direct' - approach: - | 'validating' - | 'solution-focused' - | 'exploratory' - | 'stabilizing' - language: 'simple' | 'detailed' | 'metaphorical' | 'clinical' - } - } { - const baseResources = this.getRelevantResources(result) - const resources = baseResources.map((r) => r) - if (result.urgency === 'high') { - // Proactively include an explicit crisis/hotline reference for high urgency cases - // But require human-in-the-loop review before automated crisis intervention - resources.unshift('Emergency crisis hotline support') - const crisisAdds = [ - 'Crisis hotline: 988 Suicide & Crisis Lifeline', - 'Emergency services: 911 for immediate danger', - 'Crisis text line: Text HOME to 741741', - 'Emergency support and crisis hotline information', - ] - for (const r of crisisAdds) { - if (!resources.some((x) => x.toLowerCase() === r.toLowerCase())) - resources.push(r) - } - // Defensive: ensure at least one resource string contains crisis/hotline/emergency keywords - if (!resources.some((r) => /crisis|hotline|emergency/i.test(r))) { - resources.unshift('Emergency crisis hotline') - } - // Add human-in-the-loop review flag for high urgency cases - if (!result.metadata) - result.metadata = { - emotionalIndicators: [], - copingCapacity: 'medium', - socialSupport: 'unknown', - immediateNeeds: [], - } - result.metadata.requiresHumanReview = true - result.metadata.crisisInterventionFlagged = true - } - // Final safety: ensure at least one crisis/hotline/emergency string present for high urgency - const urgCheck = (result.urgency || '').toLowerCase().trim() - if ( - (urgCheck === 'high' || - (result.recommendedApproach || '').toLowerCase().includes('crisis') || - (Array.isArray(result.supportNeeds) && - result.supportNeeds.some((n) => - n.toLowerCase().includes('safety'), - ))) && - !resources.some((x) => /crisis|hotline|emergency/i.test(x)) - ) { - resources.push('Emergency support and crisis hotline information') - // Add human-in-the-loop review flag for high urgency cases - if (!result.metadata) - result.metadata = { - emotionalIndicators: [], - copingCapacity: 'medium', - socialSupport: 'unknown', - immediateNeeds: [], - } - result.metadata.requiresHumanReview = true - result.metadata.crisisInterventionFlagged = true - } - - // Type-safe resource stringification - interface ResourceWithLabel { - label?: string - name?: string - } - const stringifiedResources = resources.map((r) => { - if (typeof r === 'string') return r - if (r && typeof r === 'object') { - const resource = r as ResourceWithLabel - if (resource.label) return resource.label - if (resource.name) return resource.name - } - return String(r) - }) - - // Ultra-defensive: if high urgency and still no crisis/hotline/emergency entry, prepend a guaranteed hotline - const immediateNeedsText = Array.isArray(result.metadata?.immediateNeeds) - ? result.metadata.immediateNeeds.join(' ').toLowerCase() - : '' - if ( - (urgCheck === 'high' || - immediateNeedsText.includes('crisis') || - immediateNeedsText.includes('safety')) && - !stringifiedResources.some((x) => /crisis|hotline|emergency/i.test(x)) - ) { - stringifiedResources.unshift( - 'Crisis hotline: 988 Suicide & Crisis Lifeline', - ) - } - - return { - immediateActions: this.getImmediateActions(result).slice(), - longerTermStrategies: this.getLongerTermStrategies(result), - resources: stringifiedResources, - responseStyle: this.determineResponseStyle(result), - } - } - - /** - * Pattern-based identification for quick screening - */ - private performPatternBasedIdentification( - userQuery: string, - ): SupportContextResult { - const query = userQuery.toLowerCase() - let bestEmotionalMatch = { - state: EmotionalState.MIXED_EMOTIONS, - confidence: 0, - } - let bestSupportMatch = { - type: SupportType.EMOTIONAL_VALIDATION, - confidence: 0, - } - let copingCapacity: 'high' | 'medium' | 'low' = 'medium' - - // -------- PATCH 5: Block info/casual (non-support) queries early -------- - // If query matches any informational or casual pattern, forcibly block as not support/low confidence, etc. - // (only define the array ONCE per file) - if (nonSupportPatterns.some((pattern) => pattern.test(query))) { - // For queries that match informational/casual patterns, keep a low confidence - // but mark as potential support (tests expect isSupport often true with low confidence) - return { - isSupport: true, - confidence: 0.05, - supportType: SupportType.EMOTIONAL_VALIDATION, - emotionalState: EmotionalState.MIXED_EMOTIONS, - urgency: 'low', - supportNeeds: [], - recommendedApproach: RecommendedApproach.EMPATHETIC_LISTENING, - emotionalIntensity: 0.05, - metadata: { - emotionalIndicators: [], - copingCapacity: 'medium', - socialSupport: 'unknown', - immediateNeeds: [], - }, - } - } - - // -------- PATCH 6: Block empty query -------- - // Treat empty, whitespace, or falsy queries as non-support with zero confidence and intensity - if (!query.trim()) { - // For empty queries, return low confidence but mark as support (tests expect isSupport true with 0 confidence) - return { - isSupport: true, - confidence: 0, - supportType: SupportType.EMOTIONAL_VALIDATION, - emotionalState: EmotionalState.MIXED_EMOTIONS, - urgency: 'low', - supportNeeds: [], - recommendedApproach: RecommendedApproach.EMPATHETIC_LISTENING, - emotionalIntensity: 0, - metadata: { - emotionalIndicators: [], - copingCapacity: 'medium', - socialSupport: 'unknown', - immediateNeeds: [], - }, - } - } - - // Identify emotional state with priority scoring - const emotionalMatches: Array<{ - state: EmotionalState - confidence: number - priority: number - }> = [] - - for (const [emotion, patterns] of Object.entries(this.emotionalPatterns)) { - for (const pattern of patterns) { - if (pattern.test(query)) { - // Give higher confidence to more specific patterns - const baseConfidence = 0.7 - const specificityBonus = pattern.source.length > 50 ? 0.1 : 0 // Longer patterns are more specific - const confidence = baseConfidence + specificityBonus - - // Priority order for emotional states (higher number = higher priority) - const priorities = { - hopelessness: 10, // Highest priority for crisis states - overwhelm: 9, - helplessness: 8, - sadness: 7, - anxiety: 6, - anger: 5, - loneliness: 4, - fear: 3, - guilt: 2, - shame: 2, - numbness: 1, - confusion: 1, - mixed_emotions: 0, // Lowest priority - } - - const priority = priorities[emotion as keyof typeof priorities] || 0 - emotionalMatches.push({ - state: emotion as EmotionalState, - confidence, - priority, - }) - } - } - } - - // Select best emotional match based on priority, then confidence - if (emotionalMatches.length > 0) { - emotionalMatches.sort( - (a, b) => b.priority - a.priority || b.confidence - a.confidence, - ) - const firstMatch = emotionalMatches[0] - if (firstMatch) { - bestEmotionalMatch = { - state: firstMatch.state, - confidence: firstMatch.confidence, - } - } - } - - // If query contains grief/loss keywords, force supportType to GRIEF_SUPPORT and emotionalState to SADNESS - if ( - /grief|grieving|loss|passed away|died|funeral|bereavement/i.test(query) - ) { - bestSupportMatch = { - type: SupportType.GRIEF_SUPPORT, - confidence: Math.max(bestSupportMatch.confidence, 0.8), - } - // Force SADNESS for grief support regardless of other emotional matches - bestEmotionalMatch = { - state: EmotionalState.SADNESS, - confidence: Math.max(bestEmotionalMatch.confidence, 0.7), - } - // Override any other emotional matches for grief support - emotionalMatches.length = 0 - emotionalMatches.push({ - state: EmotionalState.SADNESS, - confidence: 0.8, - priority: 10, - }) - } - - // Identify support type with priority scoring - const supportMatches: Array<{ - type: SupportType - confidence: number - priority: number - }> = [] - - for (const [support, patterns] of Object.entries(this.supportPatterns)) { - for (const pattern of patterns) { - if (pattern.test(query)) { - // Give higher confidence to more specific patterns - const baseConfidence = 0.8 - const specificityBonus = pattern.source.length > 50 ? 0.1 : 0 - const confidence = baseConfidence + specificityBonus - - // Priority order for support types (higher number = higher priority) - const priorities = { - grief_support: 10, // Highest priority for specific support types - active_listening: 9, - practical_guidance: 8, // Prefer practical guidance for specific action requests - coping_assistance: 7, // General coping help - encouragement: 6, - emotional_validation: 8, // Higher priority for general emotional support - stress_management: 4, - relationship_support: 4, - trauma_support: 4, - identity_support: 3, - transition_support: 3, - daily_functioning: 2, - } - - const priority = priorities[support as keyof typeof priorities] || 0 - supportMatches.push({ - type: support as SupportType, - confidence, - priority, - }) - } - } - } - - // Select best support match based on priority, then confidence - if (supportMatches.length > 0) { - supportMatches.sort( - (a, b) => b.priority - a.priority || b.confidence - a.confidence, - ) - const firstMatch = supportMatches[0] - if (firstMatch) { - bestSupportMatch = { - type: firstMatch.type, - confidence: firstMatch.confidence, - } - } - } - // If no support match but emotional match exists, fallback to emotional validation - if (supportMatches.length === 0 && emotionalMatches.length > 0) { - bestSupportMatch = { - type: SupportType.EMOTIONAL_VALIDATION, - confidence: bestEmotionalMatch.confidence, - } - } - - // Assess coping capacity - for (const [capacity, patterns] of Object.entries(this.copingIndicators)) { - for (const pattern of patterns) { - if (pattern.test(query)) { - copingCapacity = capacity as 'high' | 'medium' | 'low' - break - } - } - if (copingCapacity !== 'medium') { - break - } - } - // Stronger catch-all for high-capacity: if query has both "ok" and "handling", elevate to high - if (/handling/i.test(query) && /\bok(ay)?\b/i.test(query)) { - copingCapacity = 'high' - } - // Robust: If query has coping markers AND "pretty well"/"just fine", set to 'high' - if ( - /\bhandling things well\b/i.test(query) || - /\bcould use (some )?advice\b/i.test(query) || - /\bmanaging well\b/i.test(query) || - /\bdoing (okay|ok)\b/i.test(query) || - /\bmanaging just fine\b/i.test(query) || - /\bdoing pretty well\b/i.test(query) || - /\bI (am|’m|'m) (ok|okay|fine|managing|coping)/i.test(query) || - /\bhandling (myself|things) (ok|okay|well|fine)\b/i.test(query) || - /\bnot too bad\b/i.test(query) - ) { - copingCapacity = 'high' - } - - // Skip detailed coping assessment if disabled - if (!this.enableCopingAssessment) { - copingCapacity = 'medium' - } - - // If we found any emotional or support pattern match, this is likely a support request - const hasEmotionalContent = bestEmotionalMatch.confidence > 0 - const hasSupportLanguage = bestSupportMatch.confidence > 0 - - // (Removed unused variable: _hasEmotionalLanguage) - // General emotional language regex was previously here, but not used. - - const overallConfidence = Math.max( - bestEmotionalMatch.confidence, - bestSupportMatch.confidence, - ) - // Only set isSupport to true if there's actual emotional content or support language - const isSupport = hasEmotionalContent || hasSupportLanguage - const emotionalIntensity = this.calculateEmotionalIntensity( - query, - bestEmotionalMatch.state, - ) - // If encouragement with hopelessness, escalate urgency - const encouragementHopelessnessOverride = - bestSupportMatch.type === SupportType.ENCOURAGEMENT && - bestEmotionalMatch.state === EmotionalState.HOPELESSNESS - - // Lower threshold for "high" intensity to 0.7 for test alignment - let adjustedIntensity = emotionalIntensity - if ( - bestEmotionalMatch.state === EmotionalState.HOPELESSNESS || - bestEmotionalMatch.state === EmotionalState.OVERWHELM - ) { - adjustedIntensity = Math.max(emotionalIntensity, 0.85) - } - - // Coping capacity: if query contains "handling things well" or "could use advice" or similar, set high - if ( - /\bhandling things well\b/i.test(query) || - /\bcould use (some )?advice\b/i.test(query) || - /\bmanaging well\b/i.test(query) || - /\bdoing (okay|ok)\b/i.test(query) || - /\bmanaging just fine\b/i.test(query) || - /\bdoing pretty well\b/i.test(query) || - /\bI (am|’m|'m) (ok|okay|fine|managing|coping)/i.test(query) || - /\bhandling (myself|things) (ok|okay|well|fine)\b/i.test(query) || - /\bnot too bad\b/i.test(query) - ) { - // Patch: Ensure edge-case is always detected as high. - copingCapacity = 'high' - } - - // Special case for "pretty anxious" queries to ensure medium urgency - let urgency: 'low' | 'medium' | 'high' - if (encouragementHopelessnessOverride) { - urgency = 'high' - } else if (/\bpretty anxious\b/i.test(query)) { - urgency = 'medium' // Force medium for "pretty anxious" queries - } else { - urgency = this.determineUrgency(adjustedIntensity, copingCapacity) - } - let recommendedApproach = this.mapEmotionalStateToApproach( - bestEmotionalMatch.state, - ) - - // Adapt approach based on emotional state if enabled - if (this.adaptToEmotionalState && adjustedIntensity > 0.7) { - // For high emotional intensity, prefer stabilizing approaches - if (recommendedApproach === RecommendedApproach.PROBLEM_SOLVING) { - recommendedApproach = RecommendedApproach.EMOTIONAL_REGULATION - } else if ( - recommendedApproach === RecommendedApproach.COGNITIVE_REFRAMING - ) { - recommendedApproach = RecommendedApproach.EMPATHETIC_LISTENING - } - } - - return { - isSupport, - confidence: overallConfidence, - supportType: bestSupportMatch.type, - emotionalState: bestEmotionalMatch.state, - urgency, - supportNeeds: this.mapSupportTypeToNeeds(bestSupportMatch.type), - recommendedApproach, - emotionalIntensity: adjustedIntensity, - metadata: { - emotionalIndicators: this.extractEmotionalIndicators(query), - copingCapacity, - socialSupport: 'unknown', - immediateNeeds: this.extractImmediateNeeds( - query, - bestSupportMatch.type, - ), - }, - } - } - - /** - * AI-powered detailed analysis - */ - private async performAIAnalysis( - userQuery: string, - conversationHistory?: string[], - userEmotionalProfile?: UserEmotionalProfile, - ): Promise { - let contextualPrompt = SUPPORT_IDENTIFICATION_PROMPT - - // Add user emotional profile context if available - if (userEmotionalProfile) { - contextualPrompt += `\n\nUser Emotional Profile: -- Baseline Emotional State: ${userEmotionalProfile.baselineEmotionalState ?? 'unknown'} -- Typical Coping Strategies: ${userEmotionalProfile.typicalCopingStrategies?.join(', ') ?? 'unknown'} -- Emotional Triggers: ${userEmotionalProfile.emotionalTriggers?.join(', ') ?? 'unknown'} -- Support Preferences: ${userEmotionalProfile.supportPreferences?.join(', ') ?? 'unknown'} - -Consider this context in your assessment.` - } - - // Include conversation history for emotional trajectory - let queryWithContext = userQuery - if (conversationHistory && conversationHistory.length > 0) { - queryWithContext = `Conversation context: ${conversationHistory.slice(-5).join(' ')}\n\nCurrent message: ${userQuery}` - } - - // Prefer generateText if available per tests; fallback to chat - const aiServiceWithGenerateText = this.aiService as unknown as { - generateText?: (...args: unknown[]) => Promise - } - if (typeof aiServiceWithGenerateText.generateText === 'function') { - const text = await aiServiceWithGenerateText.generateText( - `${contextualPrompt}\n\n${queryWithContext}`, - ) - return this.parseAIResponse(text) - } - - const messages: AIMessage[] = [ - { role: 'system', content: contextualPrompt }, - { role: 'user', content: queryWithContext }, - ] - - const response: AICompletion = await this.aiService.createChatCompletion( - messages, - { - model: this.model, - }, - ) - - let content = '' - if (typeof response === 'string') { - content = response - } else if (response && typeof response === 'object') { - if (typeof response.content === 'string') { - content = response.content - } else if ( - Array.isArray(response.choices) && - response.choices[0]?.message?.content - ) { - content = String(response.choices[0].message.content) - } - } - - if (!content) { - throw new Error('No content received from AI service response') - } - return this.parseAIResponse(content) - } - - /** - * Parse AI response into structured result - */ - private parseAIResponse(content: string): SupportContextResult { - try { - const trimmed = (content || '').trim() - let jsonStr: string = '' - if (trimmed.startsWith('{') && trimmed.endsWith('}')) { - jsonStr = trimmed - } else { - const jsonMatch = - content.match(/```json\n([\s\S]*?)\n```/) ?? - content.match(/```\n([\s\S]*?)\n```/) - if (jsonMatch) { - jsonStr = jsonMatch[1] ?? jsonMatch[0] - } else { - jsonStr = content - } - } - - // Secondary resilience: try to fix common JSON formatting issues - let parsedObj: unknown - try { - parsedObj = JSON.parse(jsonStr) - } catch { - // Replace single quotes with double quotes for keys/strings and strip trailing commas - const repaired = jsonStr - .replace(/'([^']+)'\s*:\s*'([^']*)'/g, '"$1": "$2"') - .replace(/'([^']*)'/g, '"$1"') - .replace(/,\s*([}\]])/g, '$1') - parsedObj = JSON.parse(repaired) - } - - // Type guard to validate parsed object structure - if (typeof parsedObj !== 'object' || parsedObj === null) { - throw new Error('Parsed result is not an object') - } - - const parsed = parsedObj as { - isSupport?: boolean - confidence?: number - supportType?: string - emotionalState?: string - urgency?: string - supportNeeds?: unknown[] - recommendedApproach?: string - emotionalIntensity?: number - metadata?: { - emotionalIndicators?: unknown[] - copingCapacity?: string - socialSupport?: string - immediateNeeds?: unknown[] - triggerEvents?: unknown[] - resilientFactors?: unknown[] - } - } - - return { - isSupport: Boolean(parsed.isSupport), - confidence: Math.max(0, Math.min(1, parsed.confidence ?? 0.5)), - supportType: this.validateSupportType(parsed.supportType ?? ''), - emotionalState: this.validateEmotionalState( - parsed.emotionalState ?? '', - ), - urgency: this.validateUrgency(parsed.urgency ?? ''), - supportNeeds: Array.isArray(parsed.supportNeeds) - ? parsed.supportNeeds - .map((n: unknown) => this.validateSupportNeed(n as string)) - .filter((need): need is SupportNeed => need !== null) - : [], - recommendedApproach: this.validateRecommendedApproach( - parsed.recommendedApproach ?? '', - ), - emotionalIntensity: Math.max( - 0, - Math.min(1, parsed.emotionalIntensity ?? 0.5), - ), - metadata: { - emotionalIndicators: Array.isArray( - parsed.metadata?.emotionalIndicators, - ) - ? (parsed.metadata.emotionalIndicators as string[]) - : [], - copingCapacity: this.validateCopingCapacity( - parsed.metadata?.copingCapacity ?? '', - ), - socialSupport: this.validateSocialSupport( - parsed.metadata?.socialSupport ?? '', - ), - immediateNeeds: Array.isArray(parsed.metadata?.immediateNeeds) - ? (parsed.metadata.immediateNeeds as string[]) - : [], - triggerEvents: Array.isArray(parsed.metadata?.triggerEvents) - ? (parsed.metadata.triggerEvents as string[]) - : undefined, - resilientFactors: Array.isArray(parsed.metadata?.resilientFactors) - ? (parsed.metadata.resilientFactors as string[]) - : undefined, - }, - } - } catch (error: unknown) { - logger.error('Error parsing AI response:', { - context: 'response-parsing', - error: error instanceof Error ? String(error) : String(error), - }) - - return { - isSupport: false, - confidence: 0, - supportType: SupportType.EMOTIONAL_VALIDATION, - emotionalState: EmotionalState.MIXED_EMOTIONS, - urgency: 'low', - supportNeeds: [], - recommendedApproach: RecommendedApproach.EMPATHETIC_LISTENING, - emotionalIntensity: 0.05, - metadata: { - emotionalIndicators: [], - copingCapacity: 'medium', - socialSupport: 'unknown', - immediateNeeds: [], - }, - } - } - } - - /** - * Combine pattern and AI results - */ - private combineResults( - patternResult: SupportContextResult, - aiResult: SupportContextResult, - ): SupportContextResult { - // Weighted combination: AI gets 75%, pattern gets 25% - const combinedConfidence = - aiResult.confidence * 0.75 + patternResult.confidence * 0.25 - - return { - ...aiResult, - confidence: combinedConfidence, - isSupport: combinedConfidence > 0.6, - // Use higher emotional intensity between the two - emotionalIntensity: Math.max( - patternResult.emotionalIntensity, - aiResult.emotionalIntensity, - ), - // Combine metadata - metadata: { - ...aiResult.metadata, - emotionalIndicators: [ - ...aiResult.metadata.emotionalIndicators, - ...patternResult.metadata.emotionalIndicators, - ].filter((indicator, index, arr) => arr.indexOf(indicator) === index), - }, - } - } - - /** - * Helper methods for validation and processing - */ - private validateSupportType(type: string): SupportType { - return Object.values(SupportType).includes(type as SupportType) - ? (type as SupportType) - : SupportType.EMOTIONAL_VALIDATION - } - - private validateEmotionalState(state: string): EmotionalState { - return Object.values(EmotionalState).includes(state as EmotionalState) - ? (state as EmotionalState) - : EmotionalState.MIXED_EMOTIONS - } - - private validateUrgency(urgency: string): 'low' | 'medium' | 'high' { - return ['low', 'medium', 'high'].includes(urgency) - ? (urgency as 'low' | 'medium' | 'high') - : 'medium' - } - - private validateSupportNeed(need: string): SupportNeed | null { - return Object.values(SupportNeed).includes(need as SupportNeed) - ? (need as SupportNeed) - : null - } - - private validateRecommendedApproach(approach: string): RecommendedApproach { - return Object.values(RecommendedApproach).includes( - approach as RecommendedApproach, - ) - ? (approach as RecommendedApproach) - : RecommendedApproach.EMPATHETIC_LISTENING - } - - private validateCopingCapacity(capacity: string): 'high' | 'medium' | 'low' { - return ['high', 'medium', 'low'].includes(capacity) - ? (capacity as 'high' | 'medium' | 'low') - : 'medium' - } - - private validateSocialSupport( - support: string, - ): 'strong' | 'moderate' | 'limited' | 'unknown' { - return ['strong', 'moderate', 'limited', 'unknown'].includes(support) - ? (support as 'strong' | 'moderate' | 'limited' | 'unknown') - : 'unknown' - } - - private calculateEmotionalIntensity( - query: string, - emotionalState: EmotionalState, - ): number { - const slightlyConcerned = [ - 'slightly concerned', - 'slightly worried', - 'mildly worried', - 'a bit concerned', - 'not too worried', - 'minor concern', - 'mildly anxious', - 'just a little worried', - 'a little worried', - ] - for (const phrase of slightlyConcerned) { - if (query.toLowerCase().includes(phrase)) { - // Defensive: Always return very mild intensity <0.4 per TDD - return 0.19 - } - } - - let intensity = 0.2 - const intensityIndicators = [ - 'extremely', - 'very', - 'really', - 'so', - 'incredibly', - 'overwhelmingly', - "can't", - 'cannot', - 'unable to', - 'impossible', - 'always', - 'never', - 'constantly', - 'all the time', - 'terrible', - 'awful', - 'horrible', - 'devastating', - 'crushing', - 'falling apart', - 'breaking down', - 'breaking point', - 'completely', - ] - - for (const indicator of intensityIndicators) { - if (query.toLowerCase().includes(indicator)) { - intensity += 0.18 - } - } - if (query.includes('!!') || query.includes('...')) { - intensity += 0.18 - } - let consecutiveCaps = 0 - for (const char of query) { - if (typeof char === 'string' && char >= 'A' && char <= 'Z') { - consecutiveCaps++ - if (consecutiveCaps >= 3) { - intensity += 0.18 - break - } - } else { - consecutiveCaps = 0 - } - } - - // Adjust based on emotional state - const highIntensityStates = [ - EmotionalState.HOPELESSNESS, - EmotionalState.OVERWHELM, - EmotionalState.HELPLESSNESS, - ] - const mediumIntensityStates = [ - EmotionalState.SADNESS, - EmotionalState.ANXIETY, - EmotionalState.ANGER, - ] - - if (highIntensityStates.includes(emotionalState)) { - intensity += 0.35 - } else if (mediumIntensityStates.includes(emotionalState)) { - intensity += 0.12 - // Ensure sadness meets test threshold - if (emotionalState === EmotionalState.SADNESS && intensity < 0.6) { - intensity = 0.61 - } - // Ensure anxiety test expects intensity > 0.5, not just 0.5 - if (emotionalState === EmotionalState.ANXIETY && intensity <= 0.5) { - intensity = 0.51 - } - } - - // Additional boost for crisis keywords - // Replaced regex with a safe keyword array check to avoid ReDoS warnings. - const crisisKeywordsList = [ - 'suicidal', - 'suicide', - 'kill myself', - 'end it all', - "can't go on", - 'give up', - ] - for (const word of crisisKeywordsList) { - if (query.toLowerCase().includes(word)) { - intensity = Math.max(intensity, 0.95) - break - } - } - - return Math.min(intensity, 1.0) - } - - private determineUrgency( - emotionalIntensity: number, - copingCapacity: 'high' | 'medium' | 'low', - ): 'low' | 'medium' | 'high' { - if (emotionalIntensity > 0.8 || copingCapacity === 'low') { - return 'high' - } - if (emotionalIntensity >= 0.4 || copingCapacity === 'medium') { - return 'medium' - } - return 'low' - } - - private mapSupportTypeToNeeds(supportType: SupportType): SupportNeed[] { - const needsMap: Record = { - [SupportType.EMOTIONAL_VALIDATION]: [ - SupportNeed.VALIDATION, - SupportNeed.EMOTIONAL_REGULATION, - ], - [SupportType.COPING_ASSISTANCE]: [ - SupportNeed.SKILL_BUILDING, - SupportNeed.PRACTICAL_ADVICE, - ], - [SupportType.ENCOURAGEMENT]: [ - SupportNeed.HOPE_RESTORATION, - SupportNeed.PERSPECTIVE_TAKING, - ], - [SupportType.ACTIVE_LISTENING]: [SupportNeed.VALIDATION], - [SupportType.PRACTICAL_GUIDANCE]: [ - SupportNeed.PRACTICAL_ADVICE, - SupportNeed.RESOURCE_CONNECTION, - ], - [SupportType.GRIEF_SUPPORT]: [ - SupportNeed.VALIDATION, - SupportNeed.MEANING_MAKING, - ], - [SupportType.RELATIONSHIP_SUPPORT]: [ - SupportNeed.RELATIONSHIP_REPAIR, - SupportNeed.PERSPECTIVE_TAKING, - ], - [SupportType.STRESS_MANAGEMENT]: [ - SupportNeed.EMOTIONAL_REGULATION, - SupportNeed.SKILL_BUILDING, - ], - [SupportType.IDENTITY_SUPPORT]: [ - SupportNeed.MEANING_MAKING, - SupportNeed.PERSPECTIVE_TAKING, - ], - [SupportType.TRANSITION_SUPPORT]: [ - SupportNeed.PRACTICAL_ADVICE, - SupportNeed.EMOTIONAL_REGULATION, - ], - [SupportType.TRAUMA_SUPPORT]: [ - SupportNeed.SAFETY_PLANNING, - SupportNeed.VALIDATION, - ], - [SupportType.DAILY_FUNCTIONING]: [ - SupportNeed.SKILL_BUILDING, - SupportNeed.RESOURCE_CONNECTION, - ], - } - - return needsMap[supportType] || [SupportNeed.VALIDATION] - } - - private mapEmotionalStateToApproach( - emotionalState: EmotionalState, - ): RecommendedApproach { - const approachMap: Record = { - [EmotionalState.SADNESS]: RecommendedApproach.EMPATHETIC_LISTENING, - [EmotionalState.ANXIETY]: RecommendedApproach.EMOTIONAL_REGULATION, - [EmotionalState.ANGER]: RecommendedApproach.EMOTIONAL_REGULATION, - [EmotionalState.FEAR]: RecommendedApproach.GENTLE_GUIDANCE, - [EmotionalState.GUILT]: RecommendedApproach.COGNITIVE_REFRAMING, - [EmotionalState.SHAME]: RecommendedApproach.EMPATHETIC_LISTENING, - [EmotionalState.LONELINESS]: RecommendedApproach.RESOURCE_REFERRAL, - [EmotionalState.HELPLESSNESS]: RecommendedApproach.STRENGTH_BASED, - [EmotionalState.HOPELESSNESS]: RecommendedApproach.CRISIS_INTERVENTION, - [EmotionalState.OVERWHELM]: RecommendedApproach.PROBLEM_SOLVING, - [EmotionalState.NUMBNESS]: RecommendedApproach.GENTLE_GUIDANCE, - [EmotionalState.CONFUSION]: RecommendedApproach.PSYCHOEDUCATION, - [EmotionalState.MIXED_EMOTIONS]: RecommendedApproach.EMPATHETIC_LISTENING, - } - - return approachMap[emotionalState] - } - - private extractEmotionalIndicators(_query: string): string[] { - const indicators: string[] = [] - const emotionalWords = [ - 'sad', - 'happy', - 'angry', - 'scared', - 'worried', - 'excited', - 'frustrated', - 'hopeless', - 'helpless', - 'overwhelmed', - 'anxious', - 'depressed', - 'lonely', - 'guilty', - 'ashamed', - 'confused', - 'hurt', - 'disappointed', - 'stressed', - ] - - for (const word of emotionalWords) { - if (_query.toLowerCase().includes(word)) { - indicators.push(word) - } - } - - return indicators - } - - private extractImmediateNeeds( - _query: string, - supportType: SupportType, - ): string[] { - const needsMap: Record = { - [SupportType.EMOTIONAL_VALIDATION]: [ - 'empathy', - 'understanding', - 'acknowledgment', - ], - [SupportType.COPING_ASSISTANCE]: [ - 'coping strategies', - 'management techniques', - 'practical tools', - ], - [SupportType.ENCOURAGEMENT]: [ - 'hope', - 'motivation', - 'confidence building', - ], - [SupportType.ACTIVE_LISTENING]: [ - 'non-judgmental presence', - 'safe space', - 'patient listening', - ], - [SupportType.PRACTICAL_GUIDANCE]: [ - 'concrete steps', - 'actionable advice', - 'clear direction', - ], - [SupportType.GRIEF_SUPPORT]: [ - 'grief processing', - 'loss acknowledgment', - 'healing space', - ], - [SupportType.RELATIONSHIP_SUPPORT]: [ - 'relationship guidance', - 'communication help', - 'boundary setting', - ], - [SupportType.STRESS_MANAGEMENT]: [ - 'stress relief', - 'relaxation techniques', - 'workload management', - ], - [SupportType.IDENTITY_SUPPORT]: [ - 'self-exploration', - 'identity clarification', - 'purpose finding', - ], - [SupportType.TRANSITION_SUPPORT]: [ - 'change management', - 'adaptation strategies', - 'stability', - ], - [SupportType.TRAUMA_SUPPORT]: [ - 'safety', - 'stabilization', - 'trauma processing', - ], - [SupportType.DAILY_FUNCTIONING]: [ - 'routine establishment', - 'basic self-care', - 'functional support', - ], - } - - return needsMap[supportType] || ['emotional support'] - } - - private getImmediateActions(result: SupportContextResult): string[] { - let actions: string[] = [] - - if (result.urgency === 'high') { - actions = [ - 'Assess immediate safety and provide crisis intervention', - 'Acknowledge emotional distress and validate their feelings', - 'Offer grounding techniques and emergency coping strategies', - 'Connect with crisis hotline and emergency resources', - 'Validate and understand their immediate needs', - 'Provide immediate safety and crisis support', - 'Take immediate steps to ensure safety and address crisis', - ] - } else if (result.urgency === 'medium') { - actions = [ - 'Validate their emotional experience and show understanding', - 'Explore current coping strategies and support systems', - 'Provide gentle guidance and normalization', - 'Offer practical next steps', - 'Acknowledge and validate their feelings', - ] - } else { - actions = [ - 'Listen empathetically and acknowledge their feelings', - 'Reflect feelings back to demonstrate understanding', - 'Explore the situation gently without judgment', - 'Offer supportive presence and validation', - 'Acknowledge and validate their feelings', - 'Demonstrate empathy and work to understand their experience', - ] - } - - // Ensure emotional validation always has acknowledge/validate/understand keywords (robust for test: always unshift to be first) - if (result.supportType === SupportType.EMOTIONAL_VALIDATION) { - // Patch: Always ensure all three ('acknowledge','validate','understand') are present for test strictness - actions.unshift( - 'Acknowledge their feelings and validate their experience', - ) - actions.unshift('Demonstrate understanding of their distress') - actions.unshift('Validate and acknowledge what they are experiencing') - // Defensive: Deduplicate if necessary (if tests are strict about duplicates), but always ensure all three distinct keywords included. - const keywords = ['acknowledge', 'validate', 'understand'] - for (const word of keywords) { - if (!actions.some((str) => str.toLowerCase().includes(word))) { - actions.unshift(`Make sure to ${word} their feelings and needs`) - } - } - } - - // Always include at least one string with /safety|crisis|immediate/i for high urgency (defensive, duplicate allowed) - if (result.urgency === 'high') { - // Patch: Always ensure 'safety', 'crisis', 'immediate' keywords present for crisis scenarios - actions.unshift('Immediate safety intervention for crisis') - actions.unshift('Take immediate action for crisis and safety') - actions.unshift('Address safety and crisis needs immediately') - actions.push('Provide safety and address crisis needs immediately') - actions.push('Immediate intervention for safety and crisis response') - // Defensive: Guarantee all keywords present in at least one string - const requiredCrisis = ['safety', 'crisis', 'immediate'] - for (const kw of requiredCrisis) { - if (!actions.some((a) => a.toLowerCase().includes(kw))) { - actions.push(`Provide ${kw} support`) - } - } - } - - return actions - } - - private getLongerTermStrategies(result: SupportContextResult): string[] { - const strategies: Record = { - [SupportType.EMOTIONAL_VALIDATION]: [ - 'Build emotional awareness and self-understanding', - 'Develop self-compassion practices', - 'Practice emotional regulation skills', - ], - [SupportType.COPING_ASSISTANCE]: [ - 'Learn diverse coping strategies and practice regularly', - 'Build resilience skills and stress tolerance', - 'Develop problem-solving techniques', - ], - [SupportType.ENCOURAGEMENT]: [ - 'Develop hope and optimism through positive psychology', - 'Build self-efficacy and confidence', - 'Practice goal-setting and achievement', - ], - [SupportType.PRACTICAL_GUIDANCE]: [ - 'Develop problem-solving and decision-making skills', - 'Practice implementing structured approaches', - 'Build practical skill development techniques', - ], - [SupportType.STRESS_MANAGEMENT]: [ - 'Implement comprehensive stress management plan', - 'Build relaxation and mindfulness skills', - 'Practice stress-reduction techniques', - ], - [SupportType.RELATIONSHIP_SUPPORT]: [ - 'Improve communication skills and emotional intelligence', - 'Build healthy boundaries and relationship patterns', - 'Practice conflict resolution techniques', - ], - [SupportType.TRAUMA_SUPPORT]: [ - 'Process trauma with qualified professional support', - 'Build safety, trust, and healing practices', - 'Develop trauma recovery skills', - ], - [SupportType.GRIEF_SUPPORT]: [ - 'Work through grief stages with professional guidance', - 'Build healthy coping and meaning-making practices', - 'Practice grief processing techniques', - ], - [SupportType.ACTIVE_LISTENING]: [ - 'Develop self-reflection and emotional processing skills', - 'Build support networks and connection', - 'Practice mindful communication', - ], - [SupportType.IDENTITY_SUPPORT]: [ - 'Explore identity and values through self-discovery', - 'Build authentic self-expression and purpose', - 'Practice self-reflection techniques', - ], - [SupportType.TRANSITION_SUPPORT]: [ - 'Develop change management and adaptation skills', - 'Build flexibility and resilience for transitions', - 'Practice adaptation techniques', - ], - [SupportType.DAILY_FUNCTIONING]: [ - 'Establish sustainable routines and self-care practices', - 'Build functional skills and support systems', - 'Practice daily living skills', - ], - } - - let baseStrategies = strategies[result.supportType] || [ - 'Continue building emotional awareness and coping skills', - 'Develop healthy patterns and support networks', - 'Practice self-care techniques', - ] - - // Ensure practical guidance always includes skill/practice/develop keywords - if ( - result.supportType === SupportType.PRACTICAL_GUIDANCE && - !baseStrategies.some((s) => /skill|practice|develop/i.test(s)) - ) { - baseStrategies.push( - 'Practice and develop practical skills for improvement', - ) - } - - return baseStrategies - } - - private getRelevantResources(result: SupportContextResult): string[] { - // For high urgency (or critical), include crisis resources - if ( - result.urgency === 'high' || - (result.urgency as string) === 'critical' - ) { - const crisisResources = [ - 'Crisis hotline: 988 Suicide & Crisis Lifeline', - 'Emergency services: 911 for immediate danger', - 'Crisis text line: Text HOME to 741741', - 'Local emergency mental health services', - 'Immediate crisis support resources', - 'Emergency support and crisis hotline information', - ] - - // Defensive: guarantee at least one crisis/hotline/emergency keyword for test matcher - if (!crisisResources.some((r) => /crisis|hotline|emergency/i.test(r))) { - crisisResources.unshift('Emergency crisis hotline') - } - - return crisisResources - } - - const resources: Record = { - [SupportType.EMOTIONAL_VALIDATION]: [ - 'Support groups and peer counseling', - 'Mental health therapy', - 'Journaling and reflection apps', - ], - [SupportType.COPING_ASSISTANCE]: [ - 'Coping skills workshops and classes', - 'Self-help resources and books', - 'Cognitive behavioral therapy', - ], - [SupportType.ENCOURAGEMENT]: [ - 'Inspirational content and success stories', - 'Mentoring and coaching programs', - 'Positive psychology resources', - ], - [SupportType.PRACTICAL_GUIDANCE]: [ - 'Life coaching and guidance counseling', - 'Problem-solving workshops', - 'Decision-making resources', - ], - [SupportType.STRESS_MANAGEMENT]: [ - 'Relaxation apps and mindfulness programs', - 'Stress management courses', - 'Meditation and breathing techniques', - ], - [SupportType.GRIEF_SUPPORT]: [ - 'Grief counseling and bereavement support', - 'Grief support groups', - 'Hospice and palliative care resources', - ], - [SupportType.TRAUMA_SUPPORT]: [ - 'Trauma-informed therapy and EMDR', - 'Trauma support groups', - 'PTSD treatment programs', - ], - [SupportType.RELATIONSHIP_SUPPORT]: [ - 'Couples and family therapy', - 'Communication skills workshops', - 'Relationship coaching', - ], - [SupportType.ACTIVE_LISTENING]: [ - 'Peer support programs and counseling', - 'Active listening groups', - 'Emotional processing workshops', - ], - [SupportType.IDENTITY_SUPPORT]: [ - 'Identity exploration therapy', - 'Values clarification workshops', - 'Self-discovery programs', - ], - [SupportType.TRANSITION_SUPPORT]: [ - 'Life transition counseling', - 'Change management resources', - 'Adaptation support groups', - ], - [SupportType.DAILY_FUNCTIONING]: [ - 'Occupational therapy services', - 'Life skills training programs', - 'Daily living support groups', - ], - } - - return ( - resources[result.supportType] || [ - 'Mental health counseling and therapy', - 'Support communities and groups', - 'Professional therapeutic services', - ] - ) - } - - private determineResponseStyle(result: SupportContextResult): { - tone: 'warm' | 'professional' | 'gentle' | 'direct' - approach: 'validating' | 'solution-focused' | 'exploratory' | 'stabilizing' - language: 'simple' | 'detailed' | 'metaphorical' | 'clinical' - } { - let tone: 'warm' | 'professional' | 'gentle' | 'direct' = 'warm' - let approach: - | 'validating' - | 'solution-focused' - | 'exploratory' - | 'stabilizing' = 'validating' - let language: 'simple' | 'detailed' | 'metaphorical' | 'clinical' = 'simple' - - // Adjust based on emotional state and intensity - if (result.emotionalIntensity > 0.8 || result.urgency === 'high') { - tone = 'gentle' - approach = 'stabilizing' - } else if (result.supportType === SupportType.PRACTICAL_GUIDANCE) { - approach = 'solution-focused' - language = 'detailed' - } else if (result.emotionalState === EmotionalState.CONFUSION) { - approach = 'exploratory' - language = 'simple' - } - - return { tone, approach, language } - } -} - -/** - * Factory function to create a support context identifier - */ -export function createSupportContextIdentifier( - config: SupportIdentifierConfig, -): SupportContextIdentifier { - return new SupportContextIdentifier(config) -} - -/** - * Default configuration for support context identifier - */ -export function getDefaultSupportIdentifierConfig( - aiService: AIService, -): SupportIdentifierConfig { - return { - aiService, - model: 'claude-3-sonnet', - enableEmotionalAnalysis: true, - enableCopingAssessment: true, - adaptToEmotionalState: true, - } -} diff --git a/apps/web/src/lib/metaaligner/prioritization/user-preferences.test.ts b/apps/web/src/lib/metaaligner/prioritization/user-preferences.test.ts deleted file mode 100644 index e7af0677f9..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/user-preferences.test.ts +++ /dev/null @@ -1,633 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' - -// Mock logger to prevent console errors during tests -vi.mock('../../logging/build-safe-logger', () => ({ - createBuildSafeLogger: () => ({ - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }), -})) -import type { ObjectivePriority } from './context-objective-mapping' -import { ObjectiveKey } from './context-objective-mapping' -import { - UserPreferenceManager, - applyUserPreferences, - DEFAULT_PREFERENCES, - type UserPreferences, - type RiskSensitivity, -} from './user-preferences' - -describe('UserPreferenceManager', () => { - let manager: UserPreferenceManager - - beforeEach(() => { - manager = new UserPreferenceManager() - }) - - describe('constructor', () => { - it('should initialize with default preferences', () => { - expect(manager).toBeDefined() - const prefs = manager.getPreferences('test-user') - expect(prefs.preferredSupportStyle).toBe('empathic') - expect(prefs.riskSensitivity).toBe('high') - }) - - it('should accept custom defaults', () => { - const customDefaults: UserPreferences = { - preferredSupportStyle: 'pragmatic', - riskSensitivity: 'low', - } - const customManager = new UserPreferenceManager(customDefaults) - const prefs = customManager.getPreferences('test-user') - expect(prefs.preferredSupportStyle).toBe('pragmatic') - }) - }) - - describe('setPreferences', () => { - it('should set valid preferences', () => { - const prefs: UserPreferences = { - preferredSupportStyle: 'direct', - responseFormality: 'formal', - riskSensitivity: 'medium', - } - - const result = manager.setPreferences('user1', prefs) - expect(result.valid).toBe(true) - expect(result.errors).toHaveLength(0) - }) - - it('should merge with defaults', () => { - const prefs: UserPreferences = { - preferredSupportStyle: 'direct', - } - - manager.setPreferences('user1', prefs) - const retrieved = manager.getPreferences('user1') - - expect(retrieved.preferredSupportStyle).toBe('direct') - expect(retrieved.riskSensitivity).toBe('high') // from defaults - }) - - it('should reject invalid support style', () => { - const prefs: any = { - preferredSupportStyle: 'invalid_style', - } - - const result = manager.setPreferences('user1', prefs) - expect(result.valid).toBe(false) - expect(result.errors.length).toBeGreaterThan(0) - }) - - it('should reject invalid custom weights', () => { - const prefs: UserPreferences = { - customObjectiveWeights: { - [ObjectiveKey.Empathy]: 1.5, // Invalid: > 1 - [ObjectiveKey.Safety]: -0.1, // Invalid: < 0 - }, - } - - const result = manager.setPreferences('user1', prefs) - expect(result.valid).toBe(false) - expect(result.errors.length).toBeGreaterThan(0) - }) - }) - - describe('getPreferences', () => { - it('should return defaults for unknown user', () => { - const prefs = manager.getPreferences('unknown-user') - expect(prefs).toEqual(DEFAULT_PREFERENCES) - }) - - it('should return set preferences for known user', () => { - const userPrefs: UserPreferences = { - preferredSupportStyle: 'reflective', - riskSensitivity: 'low', - } - - manager.setPreferences('user1', userPrefs) - const retrieved = manager.getPreferences('user1') - - expect(retrieved.preferredSupportStyle).toBe('reflective') - expect(retrieved.riskSensitivity).toBe('low') - }) - }) - - describe('updatePreferences', () => { - it('should update specific fields', () => { - const initial: UserPreferences = { - preferredSupportStyle: 'empathic', - riskSensitivity: 'high', - } - - manager.setPreferences('user1', initial) - - const update = { - riskSensitivity: 'medium' as RiskSensitivity, - } - - manager.updatePreferences('user1', update) - const retrieved = manager.getPreferences('user1') - - expect(retrieved.preferredSupportStyle).toBe('empathic') // unchanged - expect(retrieved.riskSensitivity).toBe('medium') // updated - }) - }) - - describe('clearPreferences', () => { - it('should clear user preferences', () => { - const prefs: UserPreferences = { - preferredSupportStyle: 'direct', - } - - manager.setPreferences('user1', prefs) - expect(manager.getPreferences('user1').preferredSupportStyle).toBe( - 'direct', - ) - - manager.clearPreferences('user1') - expect(manager.getPreferences('user1')).toEqual(DEFAULT_PREFERENCES) - }) - }) - - describe('validatePreferences', () => { - it('should validate correct preferences', () => { - const prefs: UserPreferences = { - preferredSupportStyle: 'empathic', - responseFormality: 'informal', - riskSensitivity: 'high', - verbosityLevel: 'moderate', - } - - const result = manager.validatePreferences(prefs) - expect(result.valid).toBe(true) - expect(result.errors).toHaveLength(0) - }) - - it('should warn about conflicting preferences', () => { - const prefs: UserPreferences = { - disableObjectives: [ObjectiveKey.Empathy, ObjectiveKey.Safety], - prioritizeObjectives: [ObjectiveKey.Empathy], // Conflict! - } - - const result = manager.validatePreferences(prefs) - expect(result.warnings.length).toBeGreaterThan(0) - }) - - it('should validate custom weights range', () => { - const prefs: UserPreferences = { - customObjectiveWeights: { - [ObjectiveKey.Empathy]: 0.5, - [ObjectiveKey.Safety]: 0.8, - [ObjectiveKey.Correctness]: 1.0, - }, - } - - const result = manager.validatePreferences(prefs) - expect(result.valid).toBe(true) - }) - }) - - describe('export and import', () => { - it('should export preferences as JSON', () => { - const prefs: UserPreferences = { - preferredSupportStyle: 'pragmatic', - riskSensitivity: 'low', - } - - manager.setPreferences('user1', prefs) - const exported = manager.exportPreferences('user1') - - expect(typeof exported).toBe('string') - const parsed = JSON.parse(exported) - expect(parsed.preferredSupportStyle).toBe('pragmatic') - }) - - it('should import preferences from JSON', () => { - const prefs: UserPreferences = { - preferredSupportStyle: 'reflective', - riskSensitivity: 'high', - } - - const jsonData = JSON.stringify(prefs) - const result = manager.importPreferences('user2', jsonData) - - expect(result.valid).toBe(true) - const retrieved = manager.getPreferences('user2') - expect(retrieved.preferredSupportStyle).toBe('reflective') - }) - - it('should handle invalid JSON on import', () => { - const result = manager.importPreferences('user3', 'invalid json{') - expect(result.valid).toBe(false) - expect(result.errors.length).toBeGreaterThan(0) - }) - }) - - describe('getAllUsers', () => { - it('should return all users with preferences', () => { - manager.setPreferences('user1', { preferredSupportStyle: 'empathic' }) - manager.setPreferences('user2', { preferredSupportStyle: 'direct' }) - manager.setPreferences('user3', { riskSensitivity: 'low' }) - - const users = manager.getAllUsers() - expect(users).toHaveLength(3) - expect(users).toContain('user1') - expect(users).toContain('user2') - expect(users).toContain('user3') - }) - - it('should return empty array when no users', () => { - const users = manager.getAllUsers() - expect(users).toHaveLength(0) - }) - }) - - describe('clearAll', () => { - it('should clear all user preferences', () => { - manager.setPreferences('user1', { preferredSupportStyle: 'empathic' }) - manager.setPreferences('user2', { preferredSupportStyle: 'direct' }) - - expect(manager.getAllUsers()).toHaveLength(2) - - manager.clearAll() - expect(manager.getAllUsers()).toHaveLength(0) - }) - }) -}) - -describe('applyUserPreferences', () => { - const createBaseObjectives = (): ObjectivePriority[] => [ - { key: ObjectiveKey.Empathy, priority: 1, weight: 0.2 }, - { key: ObjectiveKey.Safety, priority: 2, weight: 0.2 }, - { key: ObjectiveKey.Correctness, priority: 3, weight: 0.15 }, - { key: ObjectiveKey.Informativeness, priority: 4, weight: 0.15 }, - { key: ObjectiveKey.Professionalism, priority: 5, weight: 0.1 }, - { key: ObjectiveKey.Support, priority: 6, weight: 0.1 }, - ] - - describe('basic functionality', () => { - it('should return objectives unchanged with empty preferences', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = {} - - const result = applyUserPreferences(objectives, prefs) - expect(result).toHaveLength(objectives.length) - }) - - it('should normalize weights to sum to 1', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - preferredSupportStyle: 'empathic', - } - - const result = applyUserPreferences(objectives, prefs) - const sum = result.reduce((acc, obj) => acc + obj.weight, 0) - expect(sum).toBeCloseTo(1.0, 4) - }) - }) - - describe('disabling objectives', () => { - it('should filter out disabled objectives', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - disableObjectives: [ObjectiveKey.Empathy, ObjectiveKey.Safety], - } - - const result = applyUserPreferences(objectives, prefs) - expect(result).toHaveLength(4) // 6 - 2 = 4 - expect( - result.find((obj) => obj.key === ObjectiveKey.Empathy), - ).toBeUndefined() - expect( - result.find((obj) => obj.key === ObjectiveKey.Safety), - ).toBeUndefined() - }) - }) - - describe('custom weights', () => { - it('should apply custom weight overrides', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - customObjectiveWeights: { - [ObjectiveKey.Empathy]: 0.5, - [ObjectiveKey.Safety]: 0.3, - }, - } - - const result = applyUserPreferences(objectives, prefs) - - // After normalization, empathy should have higher weight than safety - const empathy = result.find((obj) => obj.key === ObjectiveKey.Empathy) - const safety = result.find((obj) => obj.key === ObjectiveKey.Safety) - - expect(empathy).toBeDefined() - expect(safety).toBeDefined() - expect(empathy!.weight).toBeGreaterThan(safety!.weight) - }) - }) - - describe('support style adjustments', () => { - it('should boost empathy for empathic style', () => { - const objectives = createBaseObjectives() - const prefsEmpathic: UserPreferences = { - preferredSupportStyle: 'empathic', - } - const prefsNone: UserPreferences = {} - - const resultEmpathic = applyUserPreferences(objectives, prefsEmpathic) - const resultNone = applyUserPreferences(objectives, prefsNone) - - const empathyWithStyle = resultEmpathic.find( - (obj) => obj.key === ObjectiveKey.Empathy, - )! - const empathyWithout = resultNone.find( - (obj) => obj.key === ObjectiveKey.Empathy, - )! - - expect(empathyWithStyle.weight).toBeGreaterThan(empathyWithout.weight) - }) - - it('should boost conciseness for direct style', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - preferredSupportStyle: 'direct', - } - - const result = applyUserPreferences(objectives, prefs) - const conciseness = result.find( - (obj) => obj.key === ObjectiveKey.Professionalism, - )! - - // After boosting and normalization, should still be relatively higher - expect(conciseness).toBeDefined() - }) - - it('should boost correctness for pragmatic style', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - preferredSupportStyle: 'pragmatic', - } - - const result = applyUserPreferences(objectives, prefs) - const correctness = result.find( - (obj) => obj.key === ObjectiveKey.Correctness, - )! - - expect(correctness).toBeDefined() - expect(correctness.weight).toBeGreaterThan(0) - }) - - it('should handle reflective style', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - preferredSupportStyle: 'reflective', - } - - const result = applyUserPreferences(objectives, prefs) - expect(result).toHaveLength(objectives.length) - - const sum = result.reduce((acc, obj) => acc + obj.weight, 0) - expect(sum).toBeCloseTo(1.0, 4) - }) - }) - - describe('risk sensitivity', () => { - it('should boost safety for high risk sensitivity', () => { - const objectives = createBaseObjectives() - const prefsHigh: UserPreferences = { - riskSensitivity: 'high', - } - const prefsLow: UserPreferences = { - riskSensitivity: 'low', - } - - const resultHigh = applyUserPreferences(objectives, prefsHigh) - const resultLow = applyUserPreferences(objectives, prefsLow) - - const safetyHigh = resultHigh.find( - (obj) => obj.key === ObjectiveKey.Safety, - )! - const safetyLow = resultLow.find( - (obj) => obj.key === ObjectiveKey.Safety, - )! - - expect(safetyHigh.weight).toBeGreaterThan(safetyLow.weight) - }) - - it('should not affect safety for low risk sensitivity', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - riskSensitivity: 'low', - } - - const result = applyUserPreferences(objectives, prefs) - const safety = result.find((obj) => obj.key === ObjectiveKey.Safety)! - - expect(safety).toBeDefined() - }) - }) - - describe('verbosity level', () => { - it('should boost conciseness for concise verbosity', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - verbosityLevel: 'concise', - } - - const result = applyUserPreferences(objectives, prefs) - const conciseness = result.find( - (obj) => obj.key === ObjectiveKey.Professionalism, - )! - - expect(conciseness).toBeDefined() - expect(conciseness.weight).toBeGreaterThan(0) - }) - - it('should boost informativeness for detailed verbosity', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - verbosityLevel: 'detailed', - } - - const result = applyUserPreferences(objectives, prefs) - const informativeness = result.find( - (obj) => obj.key === ObjectiveKey.Informativeness, - )! - - expect(informativeness).toBeDefined() - expect(informativeness.weight).toBeGreaterThan(0) - }) - - it('should not adjust for moderate verbosity', () => { - const objectives = createBaseObjectives() - const prefsModerate: UserPreferences = { - verbosityLevel: 'moderate', - } - const prefsNone: UserPreferences = {} - - const resultModerate = applyUserPreferences(objectives, prefsModerate) - const resultNone = applyUserPreferences(objectives, prefsNone) - - // Should be approximately the same - expect(resultModerate).toHaveLength(resultNone.length) - }) - }) - - describe('prioritize objectives', () => { - it('should boost prioritized objectives', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - prioritizeObjectives: [ObjectiveKey.Safety, ObjectiveKey.Support], - } - - const result = applyUserPreferences(objectives, prefs) - const safety = result.find((obj) => obj.key === ObjectiveKey.Safety)! - const clarity = result.find((obj) => obj.key === ObjectiveKey.Support)! - - expect(safety).toBeDefined() - expect(clarity).toBeDefined() - }) - }) - - describe('interaction preferences', () => { - it('should boost clarity for step-by-step preference', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - interactionPreferences: { - preferStepByStep: true, - }, - } - - const result = applyUserPreferences(objectives, prefs) - const clarity = result.find( - (obj) => obj.key === ObjectiveKey.Professionalism, - )! - - expect(clarity).toBeDefined() - expect(clarity.weight).toBeGreaterThan(0) - }) - - it('should boost informativeness for examples preference', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - interactionPreferences: { - preferExamples: true, - }, - } - - const result = applyUserPreferences(objectives, prefs) - const informativeness = result.find( - (obj) => obj.key === ObjectiveKey.Informativeness, - )! - - expect(informativeness).toBeDefined() - expect(informativeness.weight).toBeGreaterThan(0) - }) - - it('should boost conciseness for summaries preference', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - interactionPreferences: { - preferSummaries: true, - }, - } - - const result = applyUserPreferences(objectives, prefs) - const conciseness = result.find( - (obj) => obj.key === ObjectiveKey.Professionalism, - )! - - expect(conciseness).toBeDefined() - expect(conciseness.weight).toBeGreaterThan(0) - }) - }) - - describe('combined preferences', () => { - it('should handle multiple preference types together', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - preferredSupportStyle: 'empathic', - riskSensitivity: 'high', - verbosityLevel: 'concise', - prioritizeObjectives: [ObjectiveKey.Safety], - customObjectiveWeights: { - [ObjectiveKey.Empathy]: 0.3, - }, - } - - const result = applyUserPreferences(objectives, prefs) - - // Should have all objectives except disabled ones - expect(result.length).toBeGreaterThan(0) - - // Weights should sum to 1 - const sum = result.reduce((acc, obj) => acc + obj.weight, 0) - expect(sum).toBeCloseTo(1.0, 4) - - // Safety should be boosted (high risk + prioritized) - const safety = result.find((obj) => obj.key === ObjectiveKey.Safety)! - expect(safety).toBeDefined() - expect(safety.weight).toBeGreaterThan(0.1) - }) - - it('should handle conflicting preferences gracefully', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - verbosityLevel: 'detailed', // Boosts informativeness (would override) - interactionPreferences: { - preferSummaries: true, // Also boosts conciseness - }, - } - - const result = applyUserPreferences(objectives, prefs) - - // Should still normalize properly - const sum = result.reduce((acc, obj) => acc + obj.weight, 0) - expect(sum).toBeCloseTo(1.0, 4) - }) - }) - - describe('edge cases', () => { - it('should handle empty objectives array', () => { - const result = applyUserPreferences([], {}) - expect(result).toEqual([]) - }) - - it('should handle all objectives disabled', () => { - const objectives = createBaseObjectives() - const prefs: UserPreferences = { - disableObjectives: objectives.map((obj) => obj.key), - } - - const result = applyUserPreferences(objectives, prefs) - expect(result).toHaveLength(0) - }) - - it('should handle zero weights gracefully', () => { - const objectives: ObjectivePriority[] = [ - { key: ObjectiveKey.Empathy, priority: 1, weight: 0 }, - { key: ObjectiveKey.Safety, priority: 2, weight: 0 }, - ] - - const result = applyUserPreferences(objectives, {}) - - // Should distribute equally when all weights are 0 - expect(result).toBeDefined() - if (result?.[0] && result[1]) { - expect(result[0].weight).toBeCloseTo(0.5, 4) - expect(result[1].weight).toBeCloseTo(0.5, 4) - } - }) - }) -}) - -describe('DEFAULT_PREFERENCES', () => { - it('should have sensible defaults', () => { - expect(DEFAULT_PREFERENCES.preferredSupportStyle).toBe('empathic') - expect(DEFAULT_PREFERENCES.responseFormality).toBe('adaptive') - expect(DEFAULT_PREFERENCES.riskSensitivity).toBe('high') - expect(DEFAULT_PREFERENCES.verbosityLevel).toBe('moderate') - }) -}) diff --git a/apps/web/src/lib/metaaligner/prioritization/user-preferences.ts b/apps/web/src/lib/metaaligner/prioritization/user-preferences.ts deleted file mode 100644 index 00faa2a6b6..0000000000 --- a/apps/web/src/lib/metaaligner/prioritization/user-preferences.ts +++ /dev/null @@ -1,519 +0,0 @@ -/** - * User Preferences Module for MetaAligner Objective Personalization - * - * Manages user preferences that influence objective selection and weighting. - * Supports storage, retrieval, validation, and application of preferences. - * - * Features: - * - Preference storage with validation - * - Default preferences with sensible fallbacks - * - Preference-based objective adjustment - * - Support style customization - * - Risk sensitivity configuration - * - Custom objective weights - * - Persistence support (localStorage, database, etc.) - */ - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { ObjectivePriority, ObjectiveKey } from './context-objective-mapping' - -const logger = createBuildSafeLogger('user-preferences') - -/** - * User preference types - */ -export type SupportStyle = 'pragmatic' | 'empathic' | 'direct' | 'reflective' -export type ResponseFormality = 'formal' | 'informal' | 'adaptive' -export type RiskSensitivity = 'low' | 'medium' | 'high' -export type VerbosityLevel = 'concise' | 'moderate' | 'detailed' - -/** - * User preferences interface - */ -export interface UserPreferences { - /** Preferred support interaction style */ - preferredSupportStyle?: SupportStyle - - /** Response formality level */ - responseFormality?: ResponseFormality - - /** Risk sensitivity for safety-critical situations */ - riskSensitivity?: RiskSensitivity - - /** Preferred verbosity level */ - verbosityLevel?: VerbosityLevel - - /** Custom objective weight overrides (0-1 range) */ - customObjectiveWeights?: Partial> - - /** Objectives to disable/exclude */ - disableObjectives?: string[] - - /** Objectives to boost/prioritize */ - prioritizeObjectives?: string[] - - /** User demographic/context for personalization */ - userContext?: { - ageGroup?: 'teen' | 'young_adult' | 'adult' | 'senior' - culturalBackground?: string - languagePreference?: string - accessibilityNeeds?: string[] - } - - /** Interaction preferences */ - interactionPreferences?: { - preferQuestions?: boolean - preferExamples?: boolean - preferStepByStep?: boolean - preferSummaries?: boolean - } - - /** Additional custom preferences */ - [key: string]: unknown -} - -/** - * Default user preferences - */ -export const DEFAULT_PREFERENCES: Required< - Pick< - UserPreferences, - | 'preferredSupportStyle' - | 'responseFormality' - | 'riskSensitivity' - | 'verbosityLevel' - > -> = { - preferredSupportStyle: 'empathic', - responseFormality: 'adaptive', - riskSensitivity: 'high', - verbosityLevel: 'moderate', -} - -/** - * Preference validation result - */ -export interface PreferenceValidationResult { - valid: boolean - errors: string[] - warnings: string[] -} - -/** - * User Preference Manager - * - * Manages user preferences with validation, storage, and application logic. - */ -export class UserPreferenceManager { - private readonly preferences: Map = new Map() - private readonly defaultPreferences: UserPreferences - - constructor(defaultPreferences: UserPreferences = DEFAULT_PREFERENCES) { - this.defaultPreferences = defaultPreferences - } - - /** - * Set preferences for a user - */ - setPreferences( - userId: string, - preferences: UserPreferences, - ): PreferenceValidationResult { - const validation = this.validatePreferences(preferences) - - if (!validation.valid) { - logger.warn('Invalid preferences provided', { - userId, - errors: validation.errors, - }) - return validation - } - - // Merge with defaults - const mergedPreferences = this.mergeWithDefaults(preferences) - this.preferences.set(userId, mergedPreferences) - - logger.info('User preferences updated', { userId }) - return validation - } - - /** - * Get preferences for a user - */ - getPreferences(userId: string): UserPreferences { - return this.preferences.get(userId) ?? this.defaultPreferences - } - - /** - * Update specific preference fields - */ - updatePreferences( - userId: string, - updates: Partial, - ): PreferenceValidationResult { - const current = this.getPreferences(userId) - const merged = { ...current, ...updates } - return this.setPreferences(userId, merged) - } - - /** - * Clear preferences for a user - */ - clearPreferences(userId: string): void { - this.preferences.delete(userId) - logger.info('User preferences cleared', { userId }) - } - - /** - * Validate user preferences - */ - validatePreferences( - preferences: UserPreferences, - ): PreferenceValidationResult { - const errors: string[] = [] - const warnings: string[] = [] - - // Validate support style - if (preferences.preferredSupportStyle) { - const validStyles: SupportStyle[] = [ - 'pragmatic', - 'empathic', - 'direct', - 'reflective', - ] - if (!validStyles.includes(preferences.preferredSupportStyle)) { - errors.push( - `Invalid support style: ${preferences.preferredSupportStyle}`, - ) - } - } - - // Validate formality - if (preferences.responseFormality) { - const validFormality: ResponseFormality[] = [ - 'formal', - 'informal', - 'adaptive', - ] - if (!validFormality.includes(preferences.responseFormality)) { - errors.push( - `Invalid response formality: ${preferences.responseFormality}`, - ) - } - } - - // Validate risk sensitivity - if (preferences.riskSensitivity) { - const validRisk: RiskSensitivity[] = ['low', 'medium', 'high'] - if (!validRisk.includes(preferences.riskSensitivity)) { - errors.push(`Invalid risk sensitivity: ${preferences.riskSensitivity}`) - } - } - - // Validate custom weights - if (preferences.customObjectiveWeights) { - Object.entries(preferences.customObjectiveWeights).forEach( - ([key, weight]) => { - if (typeof weight !== 'number' || weight < 0 || weight > 1) { - errors.push(`Invalid weight for ${key}: ${weight} (must be 0-1)`) - } - }, - ) - } - - // Validate disable/prioritize objectives - if (preferences.disableObjectives && preferences.prioritizeObjectives) { - const overlap = preferences.disableObjectives.filter((obj) => - preferences.prioritizeObjectives?.includes(obj), - ) - if (overlap.length > 0) { - warnings.push( - `Objectives in both disable and prioritize lists: ${overlap.join(', ')}`, - ) - } - } - - return { - valid: errors.length === 0, - errors, - warnings, - } - } - - /** - * Merge preferences with defaults - */ - private mergeWithDefaults(preferences: UserPreferences): UserPreferences { - return { - ...this.defaultPreferences, - ...preferences, - } - } - - /** - * Export preferences for persistence - */ - exportPreferences(userId: string): string { - const prefs = this.getPreferences(userId) - return JSON.stringify(prefs) - } - - /** - * Import preferences from persisted data - */ - importPreferences(userId: string, data: string): PreferenceValidationResult { - try { - const preferences = JSON.parse(data) as UserPreferences - return this.setPreferences(userId, preferences) - } catch (error: unknown) { - logger.error('Failed to import preferences', { userId, error }) - return { - valid: false, - errors: ['Invalid JSON data'], - warnings: [], - } - } - } - - /** - * Get all users with preferences - */ - getAllUsers(): string[] { - return Array.from(this.preferences.keys()) - } - - /** - * Clear all preferences - */ - clearAll(): void { - this.preferences.clear() - logger.info('All user preferences cleared') - } -} - -/** - * Apply user preferences to objective priorities - * - * Adjusts objective weights based on user preferences including: - * - Support style preferences - * - Custom weight overrides - * - Disabled objectives - * - Prioritized objectives - * - Risk sensitivity - */ -export function applyUserPreferences( - objectives: ObjectivePriority[], - prefs: UserPreferences, -): ObjectivePriority[] { - let result = [...objectives] - - // Step 1: Filter out disabled objectives - if (prefs.disableObjectives && prefs.disableObjectives.length > 0) { - result = result.filter((obj) => !prefs.disableObjectives!.includes(obj.key)) - } - - // Step 2: Apply custom weight overrides - if (prefs.customObjectiveWeights) { - result = result.map((obj) => - prefs.customObjectiveWeights![obj.key] !== undefined - ? { ...obj, weight: prefs.customObjectiveWeights![obj.key]! } - : obj, - ) - } - - // Step 3: Apply support style adjustments - if (prefs.preferredSupportStyle) { - result = applySupportStyleAdjustments(result, prefs.preferredSupportStyle) - } - - // Step 4: Apply risk sensitivity adjustments - if (prefs.riskSensitivity) { - result = applyRiskSensitivityAdjustments(result, prefs.riskSensitivity) - } - - // Step 5: Apply verbosity adjustments - if (prefs.verbosityLevel) { - result = applyVerbosityAdjustments(result, prefs.verbosityLevel) - } - - // Step 6: Boost prioritized objectives - if (prefs.prioritizeObjectives && prefs.prioritizeObjectives.length > 0) { - result = result.map((obj) => - prefs.prioritizeObjectives!.includes(obj.key) - ? { ...obj, weight: Math.min(obj.weight * 1.2, 1) } - : obj, - ) - } - - // Step 7: Apply interaction preference adjustments - if (prefs.interactionPreferences) { - result = applyInteractionPreferenceAdjustments( - result, - prefs.interactionPreferences, - ) - } - - // Step 8: Normalize weights to ensure they sum to 1 - result = normalizeWeights(result) - - return result -} - -/** - * Apply support style specific adjustments - */ -function applySupportStyleAdjustments( - objectives: ObjectivePriority[], - style: SupportStyle, -): ObjectivePriority[] { - const adjustments: Record> = { - empathic: { - empathy: 1.25, - warmth: 1.2, - safety: 1.15, - }, - pragmatic: { - correctness: 1.2, - informativeness: 1.15, - conciseness: 1.1, - }, - direct: { - conciseness: 1.25, - clarity: 1.2, - correctness: 1.15, - }, - reflective: { - empathy: 1.15, - informativeness: 1.1, - depth: 1.2, - }, - } - - const styleAdjustments = adjustments[style] || {} - - return objectives.map((obj) => { - const multiplier = styleAdjustments[obj.key] ?? 1.0 - return { - ...obj, - weight: Math.min(obj.weight * multiplier, 1), - } - }) -} - -/** - * Apply risk sensitivity adjustments - */ -function applyRiskSensitivityAdjustments( - objectives: ObjectivePriority[], - sensitivity: RiskSensitivity, -): ObjectivePriority[] { - const safetyMultipliers: Record = { - low: 1.0, - medium: 1.15, - high: 1.3, - } - - const multiplier = safetyMultipliers[sensitivity] - - return objectives.map((obj) => - obj.key === ObjectiveKey.Safety - ? { ...obj, weight: Math.min(obj.weight * multiplier, 1) } - : obj, - ) -} - -/** - * Apply verbosity level adjustments - */ -function applyVerbosityAdjustments( - objectives: ObjectivePriority[], - verbosity: VerbosityLevel, -): ObjectivePriority[] { - const adjustments: Record> = { - concise: { - conciseness: 1.3, - clarity: 1.15, - }, - moderate: { - // No adjustments - baseline - }, - detailed: { - informativeness: 1.2, - depth: 1.15, - }, - } - - const verbosityAdjustments = adjustments[verbosity] || {} - - return objectives.map((obj) => { - const multiplier = verbosityAdjustments[obj.key] ?? 1.0 - return { - ...obj, - weight: Math.min(obj.weight * multiplier, 1), - } - }) -} - -/** - * Apply interaction preference adjustments - */ -function applyInteractionPreferenceAdjustments( - objectives: ObjectivePriority[], - preferences: NonNullable, -): ObjectivePriority[] { - let result = [...objectives] - - // Boost clarity if user prefers step-by-step - if (preferences.preferStepByStep) { - result = result.map((obj) => - obj.key === ObjectiveKey.Professionalism - ? { ...obj, weight: Math.min(obj.weight * 1.15, 1) } - : obj, - ) - } - - // Boost informativeness if user prefers examples - if (preferences.preferExamples) { - result = result.map((obj) => - obj.key === ObjectiveKey.Informativeness - ? { ...obj, weight: Math.min(obj.weight * 1.1, 1) } - : obj, - ) - } - - // Boost conciseness if user prefers summaries - if (preferences.preferSummaries) { - result = result.map((obj) => - obj.key === ObjectiveKey.Professionalism - ? { ...obj, weight: Math.min(obj.weight * 1.15, 1) } - : obj, - ) - } - - return result -} - -/** - * Normalize objective weights to sum to 1 - */ -function normalizeWeights( - objectives: ObjectivePriority[], -): ObjectivePriority[] { - const sum = objectives.reduce((acc, obj) => acc + obj.weight, 0) - - if (sum === 0) { - // Avoid division by zero - distribute equally - const equalWeight = 1 / objectives.length - return objectives.map((obj) => ({ ...obj, weight: equalWeight })) - } - - return objectives.map((obj) => ({ - ...obj, - weight: obj.weight / sum, - })) -} - -/** - * Create a default preference manager instance - */ -export const defaultPreferenceManager = new UserPreferenceManager() diff --git a/apps/web/src/lib/metaaligner/processing/enhancement-pipeline.test.ts b/apps/web/src/lib/metaaligner/processing/enhancement-pipeline.test.ts deleted file mode 100644 index 904768a5b2..0000000000 --- a/apps/web/src/lib/metaaligner/processing/enhancement-pipeline.test.ts +++ /dev/null @@ -1,346 +0,0 @@ -/** - * Unit tests for Response Enhancement Pipeline - */ - -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -import { ContextType } from '../core/objectives' -import { - EnhancementPipeline, - type EnhancementPipelineConfig, - type PipelineInput, -} from './enhancement-pipeline' - -// Mock the logger -vi.mock('../../logging/build-safe-logger', async () => { - const { vi } = await import('vitest') - return { - createBuildSafeLogger: () => ({ - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), - }), - } -}) - -// Mock MetaAlignerAPI -vi.mock('../api/alignment-api', async () => { - const { vi } = await import('vitest') - return { - MetaAlignerAPI: vi.fn().mockImplementation(function () { - return { - evaluateResponse: vi.fn().mockImplementation(async ({ response }) => { - // Return low score for bad responses used in tests - if ( - response === 'Just deal with it.' || - response === 'Very poor response' || - response === 'Poor quality response' || - response === 'Bad response' - ) { - return { - overallScore: 0.4, - metrics: {}, - evaluation: { - overallScore: 0.4, - objectiveResults: { empathy: { score: 0.2 } }, - }, - } - } - return { - overallScore: 0.9, - metrics: {}, - evaluation: { - overallScore: 0.9, - objectiveResults: { empathy: { score: 0.9 } }, - }, - } - }), - enhanceResponse: vi - .fn() - .mockImplementation(async ({ originalResponse: _ }) => { - const enhanced = 'I understand this is difficult for you.' - return { - enhancementApplied: true, - enhancedResponse: enhanced, - improvements: [], - } - }), - } - }), - } -}) - -describe('EnhancementPipeline', () => { - let pipeline: EnhancementPipeline - let config: EnhancementPipelineConfig - - beforeEach(() => { - vi.clearAllMocks() - - config = { - enableBatchProcessing: false, - maxRetries: 2, - enhancementThreshold: 0.7, - } - - pipeline = new EnhancementPipeline(config) - }) - - afterEach(() => { - pipeline.dispose() - vi.restoreAllMocks() - }) - - describe('Initialization', () => { - it('should initialize with default configuration', () => { - const defaultPipeline = new EnhancementPipeline() - expect(defaultPipeline).toBeInstanceOf(EnhancementPipeline) - }) - - it('should initialize with custom configuration', () => { - const customConfig = { - enableBatchProcessing: true, - maxRetries: 3, - enhancementThreshold: 0.8, - } - - const customPipeline = new EnhancementPipeline(customConfig) - expect(customPipeline).toBeInstanceOf(EnhancementPipeline) - }) - }) - - describe('Single Response Processing', () => { - it('should process a response that meets quality threshold without enhancement', async () => { - const input: PipelineInput = { - response: - "I understand you're feeling anxious. This is a common experience and there are evidence-based strategies that can help.", - context: { - userQuery: 'I feel anxious all the time', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - } - - const result = await pipeline.process(input) - - expect(result).toBeDefined() - expect(result.originalResponse).toBe(input.response) - expect(result.enhancedResponse).toBe(input.response) // Should be unchanged - expect(result.processingInfo.enhanced).toBe(false) - expect(result.improvements).toHaveLength(0) - expect(result.processingInfo.attempts).toBe(0) - }) - - it('should enhance a response that falls below quality threshold', async () => { - const input: PipelineInput = { - response: 'Just deal with it.', - context: { - userQuery: 'I feel depressed', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - } - - const result = await pipeline.process(input) - - expect(result).toBeDefined() - expect(result.originalResponse).toBe(input.response) - // Should have attempted enhancement - expect(result.processingInfo.attempts).toBeGreaterThanOrEqual(1) - }) - - it('should handle empty responses gracefully', async () => { - const input: PipelineInput = { - response: '', - context: { - userQuery: 'Hello', - detectedContext: ContextType.GENERAL, - conversationHistory: [], - }, - } - - const result = await pipeline.process(input) - - expect(result).toBeDefined() - expect(result.originalResponse).toBe('') - expect(result.enhancedResponse).toBe('') - }) - - it('should handle error during processing', async () => { - // Create a pipeline - const failingPipeline = new EnhancementPipeline() - - // Mock internal failure using "any" to access private property - // We need to ensure metaAligner exists and spy on it - const metaAligner = (failingPipeline as any).metaAligner - vi.spyOn(metaAligner, 'evaluateResponse').mockRejectedValue( - new Error('Simulated API failure'), - ) - - const input: PipelineInput = { - response: 'Test response', - context: { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - conversationHistory: [], - }, - } - - await expect(failingPipeline.process(input)).rejects.toThrow( - 'Simulated API failure', - ) - }) - }) - - describe('Batch Processing', () => { - it('should process multiple responses', async () => { - const inputs: PipelineInput[] = [ - { - response: "I understand you're feeling anxious.", - context: { - userQuery: 'I feel anxious', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - }, - { - response: 'Just deal with it.', - context: { - userQuery: 'I feel depressed', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - }, - ] - - const results = await pipeline.processBatch(inputs) - - expect(results).toHaveLength(2) - expect(results[0]).toBeDefined() - expect(results[1]).toBeDefined() - }) - - it('should handle empty batch', async () => { - const results = await pipeline.processBatch([]) - expect(results).toHaveLength(0) - }) - - it('should handle batch processing errors', async () => { - const inputs: PipelineInput[] = [ - { - response: 'Valid response', - context: { - userQuery: 'Test', - detectedContext: ContextType.GENERAL, - conversationHistory: [], - }, - }, - ] - - // Mock implementation error - vi.spyOn(pipeline as any, 'process').mockRejectedValue( - new Error('Processing error'), - ) - - await expect(pipeline.processBatch(inputs)).rejects.toThrow( - 'Processing error', - ) - }) - }) - - describe('Enhancement Logic', () => { - it('should apply iterative enhancements when needed', async () => { - const input: PipelineInput = { - response: 'Bad response', - context: { - userQuery: 'Help me', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - } - - // Mock the enhancement process to simulate improvements - const result = await pipeline.process(input) - - expect(result).toBeDefined() - // Should have some processing information - expect(result.processingInfo.durationMs).toBeGreaterThanOrEqual(0) - }) - - it('should respect maximum retry limits', async () => { - const lowRetryPipeline = new EnhancementPipeline({ - maxRetries: 1, - }) - - const input: PipelineInput = { - response: 'Poor quality response', - context: { - userQuery: 'I need help', - detectedContext: ContextType.CRISIS, - conversationHistory: [], - }, - } - - const result = await lowRetryPipeline.process(input) - - expect(result).toBeDefined() - // Should not exceed the retry limit - expect(result.processingInfo.attempts).toBeLessThanOrEqual(1) - }) - - it('should calculate improvements correctly', async () => { - const input: PipelineInput = { - response: 'Very poor response', - context: { - userQuery: 'Help me', - detectedContext: ContextType.CRISIS, - conversationHistory: [], - }, - } - - const result = await pipeline.process(input) - - // Improvements should be calculated (even if empty) - expect(Array.isArray(result.improvements)).toBe(true) - }) - }) - - describe('Configuration Handling', () => { - it('should respect enhancement threshold configuration', async () => { - const highThresholdPipeline = new EnhancementPipeline({ - enhancementThreshold: 0.9, // High threshold - }) - - const input: PipelineInput = { - response: 'Good response', // But not great - context: { - userQuery: 'I need help', - detectedContext: ContextType.SUPPORT, - conversationHistory: [], - }, - } - - const result = await highThresholdPipeline.process(input) - - expect(result).toBeDefined() - }) - - it('should handle missing configuration gracefully', async () => { - const minimalConfigPipeline = new EnhancementPipeline({}) - - const input: PipelineInput = { - response: 'Test response', - context: { - userQuery: 'Test query', - detectedContext: ContextType.GENERAL, - conversationHistory: [], - }, - } - - const result = await minimalConfigPipeline.process(input) - - expect(result).toBeDefined() - }) - }) -}) diff --git a/apps/web/src/lib/metaaligner/processing/enhancement-pipeline.ts b/apps/web/src/lib/metaaligner/processing/enhancement-pipeline.ts deleted file mode 100644 index cfb1952067..0000000000 --- a/apps/web/src/lib/metaaligner/processing/enhancement-pipeline.ts +++ /dev/null @@ -1,380 +0,0 @@ -/** - * Response Enhancement Pipeline - * Post-processes LLM responses to improve alignment with mental health objectives - */ - -import { createBuildSafeLogger } from '../../logging/build-safe-logger' -import { MetaAlignerAPI } from '../api/alignment-api' -import type { - AlignmentEvaluationResult, - AlignmentMetrics, -} from '../core/objective-interfaces' -import { AlignmentContext, ObjectiveDefinition } from '../core/objectives' - -const logger = createBuildSafeLogger('enhancement-pipeline') - -export interface EnhancementPipelineConfig { - enableBatchProcessing?: boolean - enableStreaming?: boolean - maxRetries?: number - retryDelayMs?: number - objectiveIds?: string[] - enhancementThreshold?: number -} - -export interface PipelineInput { - response: string - context: AlignmentContext - objectives?: ObjectiveDefinition[] -} - -export interface PipelineOutput { - enhancedResponse: string - originalResponse: string - improvements: AlignmentImprovement[] - metrics: { - original: AlignmentMetrics - enhanced: AlignmentMetrics - } - processingInfo: { - attempts: number - durationMs: number - enhanced: boolean - } -} - -export interface AlignmentImprovement { - objectiveId: string - objectiveName: string - scoreImprovement: number - explanation: string -} - -/** - * Response Enhancement Pipeline - * Enhances LLM responses to better align with mental health objectives - */ -export class EnhancementPipeline { - private readonly config: EnhancementPipelineConfig - private readonly metaAligner: MetaAlignerAPI - - constructor( - config: EnhancementPipelineConfig = {}, - metaAligner?: MetaAlignerAPI, - ) { - this.config = { - enableBatchProcessing: false, - enableStreaming: false, - maxRetries: 2, - retryDelayMs: 1000, - enhancementThreshold: 0.7, - ...config, - } - - // Use provided MetaAlignerAPI or create a new one - this.metaAligner = - metaAligner ?? - new MetaAlignerAPI({ - enableResponseEnhancement: true, - enhancementThreshold: this.config.enhancementThreshold, - maxEnhancementAttempts: this.config.maxRetries, - }) - - logger.info('EnhancementPipeline initialized', { - config: this.config, - action: 'initialize', - }) - } - - /** - * Process a single response through the enhancement pipeline - */ - async process(input: PipelineInput): Promise { - const startTime = Date.now() - const { response, context, objectives } = input - - logger.info('Starting enhancement pipeline', { - responseLength: response.length, - contextType: context.detectedContext, - action: 'process_start', - }) - - try { - // Initial evaluation - const initialEvaluation = await this.metaAligner.evaluateResponse({ - response, - context, - objectives, - }) - - // Check if enhancement is needed - const initialOverallScore = initialEvaluation.evaluation.overallScore - const needsEnhancement = - initialOverallScore < (this.config.enhancementThreshold ?? 0.7) - - if (!needsEnhancement) { - logger.info('Response quality sufficient, skipping enhancement', { - score: initialOverallScore, - threshold: this.config.enhancementThreshold, - action: 'skip_enhancement', - }) - - return { - enhancedResponse: response, - originalResponse: response, - improvements: [], - metrics: { - original: initialEvaluation.metrics as unknown as AlignmentMetrics, - enhanced: initialEvaluation.metrics as unknown as AlignmentMetrics, - }, - processingInfo: { - attempts: 0, - durationMs: Date.now() - startTime, - enhanced: false, - }, - } - } - - // Apply enhancements - const enhancementResult = await this.applyEnhancements( - response, - initialEvaluation.evaluation, - context, - objectives, - ) - - // Final evaluation of enhanced response - const finalEvaluation = await this.metaAligner.evaluateResponse({ - response: enhancementResult.enhancedResponse, - context, - objectives, - }) - - // Calculate improvements - const improvements = this.calculateImprovements( - initialEvaluation.evaluation, - finalEvaluation.evaluation, - ) - - logger.info('Enhancement pipeline completed', { - originalScore: initialOverallScore.toFixed(3), - enhancedScore: finalEvaluation.evaluation.overallScore.toFixed(3), - improvement: ( - finalEvaluation.evaluation.overallScore - initialOverallScore - ).toFixed(3), - durationMs: Date.now() - startTime, - action: 'process_complete', - }) - - return { - enhancedResponse: enhancementResult.enhancedResponse, - originalResponse: response, - improvements, - metrics: { - original: initialEvaluation.metrics as unknown as AlignmentMetrics, - enhanced: finalEvaluation.metrics as unknown as AlignmentMetrics, - }, - processingInfo: { - attempts: enhancementResult.attempts, - durationMs: Date.now() - startTime, - enhanced: enhancementResult.enhanced, - }, - } - } catch (error: unknown) { - logger.error('Enhancement pipeline failed', { - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - action: 'process_error', - }) - - throw error - } - } - - /** - * Process multiple responses through the enhancement pipeline - */ - async processBatch(inputs: PipelineInput[]): Promise { - if (!this.config.enableBatchProcessing) { - logger.warn('Batch processing not enabled, processing sequentially', { - action: 'batch_warning', - }) - } - - logger.info('Processing batch of responses', { - count: inputs.length, - action: 'batch_start', - }) - - const results: PipelineOutput[] = [] - - for (const input of inputs) { - try { - const result = await this.process(input) - results.push(result) - } catch (error: unknown) { - logger.error('Batch item processing failed', { - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - action: 'batch_item_error', - }) - - // Re-throw to maintain error behavior - throw error - } - } - - logger.info('Batch processing completed', { - processed: results.length, - action: 'batch_complete', - }) - - return results - } - - /** - * Apply iterative enhancements to a response - */ - private async applyEnhancements( - originalResponse: string, - evaluation: AlignmentEvaluationResult, - context: AlignmentContext, - objectives?: ObjectiveDefinition[], - ): Promise<{ - enhancedResponse: string - attempts: number - enhanced: boolean - }> { - let currentResponse = originalResponse - let currentEvaluation = evaluation - let attempts = 0 - const maxAttempts = this.config.maxRetries ?? 2 - let enhanced = false - - logger.info('Applying enhancements', { - maxAttempts, - action: 'apply_enhancements_start', - }) - - while (attempts < maxAttempts) { - attempts++ - - try { - const enhancement = await this.metaAligner.enhanceResponse({ - originalResponse: currentResponse, - evaluationResult: currentEvaluation, - context, - targetObjectives: this.config.objectiveIds, - }) - - if (enhancement.enhancementApplied) { - enhanced = true - currentResponse = enhancement.enhancedResponse - - // Re-evaluate to determine if another iteration is needed - const reEvaluation = await this.metaAligner.evaluateResponse({ - response: currentResponse, - context, - objectives, - }) - - currentEvaluation = reEvaluation.evaluation - - const stillNeedsEnhancement = - currentEvaluation.overallScore < - (this.config.enhancementThreshold ?? 0.7) - - if (!stillNeedsEnhancement) { - break - } - } else { - // No improvement applied, stop iterations - break - } - } catch (error: unknown) { - logger.warn('Enhancement attempt failed', { - attempt: attempts, - error: - error instanceof Error - ? error instanceof Error - ? error.message - : 'Unknown error' - : 'Unknown error', - action: 'enhancement_attempt_failed', - }) - - // Delay before retry if not the last attempt - if (attempts < maxAttempts && this.config.retryDelayMs) { - await new Promise((resolve) => - setTimeout(resolve, this.config.retryDelayMs), - ) - } - } - } - - logger.info('Enhancement iterations completed', { - attempts, - enhanced, - action: 'apply_enhancements_complete', - }) - - return { - enhancedResponse: currentResponse, - attempts, - enhanced, - } - } - - /** - * Calculate improvements between two evaluations - */ - private calculateImprovements( - original: AlignmentEvaluationResult, - enhanced: AlignmentEvaluationResult, - ): AlignmentImprovement[] { - const improvements: AlignmentImprovement[] = [] - - // Compare overall score - - // Compare individual objective scores - for (const [objectiveId, enhancedResult] of Object.entries<{ - score: number - }>(enhanced.objectiveResults)) { - const originalResult = original.objectiveResults[objectiveId] - - if (originalResult) { - const scoreImprovement = enhancedResult.score - originalResult.score - - // Only include improvements (positive changes) - if (scoreImprovement > 0.001) { - // Small epsilon to account for floating point precision - improvements.push({ - objectiveId, - objectiveName: objectiveId, // Would ideally come from objective definition - scoreImprovement, - explanation: `Score improved from ${originalResult.score.toFixed(3)} to ${enhancedResult.score.toFixed(3)}`, - }) - } - } - } - - return improvements - } - - /** - * Dispose of resources - */ - dispose(): void { - logger.info('Disposing enhancement pipeline', { - action: 'dispose', - }) - } -} diff --git a/apps/web/src/lib/metaaligner/quality/quality-assurance.ts b/apps/web/src/lib/metaaligner/quality/quality-assurance.ts deleted file mode 100644 index 4f28db819b..0000000000 --- a/apps/web/src/lib/metaaligner/quality/quality-assurance.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @module quality-assurance - * @description This module provides a quality assurance workflow for the MetaAligner pipeline. - */ - -/** - * Defines the interface for the QualityAssurance. - */ -export interface IQualityAssurance { - /** - * Runs the quality assurance workflow. - * - * @param response - The response to run the workflow on. - * @returns A promise that resolves when the workflow is complete. - */ - run(response: string): Promise -} diff --git a/apps/web/src/lib/metaaligner/quality/quality-metrics.ts b/apps/web/src/lib/metaaligner/quality/quality-metrics.ts deleted file mode 100644 index 87885178fb..0000000000 --- a/apps/web/src/lib/metaaligner/quality/quality-metrics.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @module quality-metrics - * @description This module provides a quality metrics system for the MetaAligner pipeline. - */ - -/** - * Defines the interface for the QualityMetrics. - */ -export interface IQualityMetrics { - /** - * Calculates quality metrics for a response. - * - * @param response - The response to calculate metrics for. - * @returns A promise that resolves to the quality metrics. - */ - calculate(response: string): Promise> -} - -/** - * The QualityMetrics class. - */ -export class QualityMetrics implements IQualityMetrics { - public async calculate(_response: string): Promise> { - // Placeholder for quality metrics calculation logic. - // This could involve using a separate model to calculate metrics like fluency, coherence, and relevance. - return { - fluency: 0, - coherence: 0, - relevance: 0, - } - } -} diff --git a/apps/web/src/lib/metaaligner/quality/quality-reporter.ts b/apps/web/src/lib/metaaligner/quality/quality-reporter.ts deleted file mode 100644 index 6e2d3ec190..0000000000 --- a/apps/web/src/lib/metaaligner/quality/quality-reporter.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @module quality-reporter - * @description This module provides a quality reporting system for the MetaAligner pipeline. - */ - -/** - * Defines the interface for the QualityReporter. - */ -export interface IQualityReporter { - /** - * Generates a quality report. - * - * @param metrics - The metrics to include in the report. - * @returns A string containing the quality report. - */ - generateReport(metrics: Record): string -} diff --git a/apps/web/src/lib/metaaligner/quality/quality-threshold-manager.ts b/apps/web/src/lib/metaaligner/quality/quality-threshold-manager.ts deleted file mode 100644 index 1fe90bfaca..0000000000 --- a/apps/web/src/lib/metaaligner/quality/quality-threshold-manager.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @module quality-threshold-manager - * @description This module provides a quality threshold management system for the MetaAligner pipeline. - */ - -/** - * Defines the interface for the QualityThresholdManager. - */ -export interface IQualityThresholdManager { - /** - * Checks if a set of metrics meets the quality thresholds. - * - * @param metrics - The metrics to check. - * @returns A boolean indicating whether the metrics meet the thresholds. - */ - meetsThresholds(metrics: Record): boolean -} diff --git a/apps/web/src/lib/metaaligner/query-preparation/query-preprocessor.ts b/apps/web/src/lib/metaaligner/query-preparation/query-preprocessor.ts deleted file mode 100644 index fc989b2a62..0000000000 --- a/apps/web/src/lib/metaaligner/query-preparation/query-preprocessor.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @module query-preprocessor - * @description This module provides a query preprocessing pipeline for the MetaAligner pipeline. - */ - -import type { UnifiedProcessingRequest } from '../api/unified-api' - -/** - * Defines the interface for the QueryPreprocessor. - */ -export interface IQueryPreprocessor { - /** - * Preprocesses a request. - * - * @param request - The processing request. - * @returns A promise that resolves to the preprocessed request. - */ - preprocess( - request: UnifiedProcessingRequest, - ): Promise -} - -/** - * The QueryPreprocessor class. - */ -export class QueryPreprocessor implements IQueryPreprocessor { - public async preprocess( - request: UnifiedProcessingRequest, - ): Promise { - const sanitizedQuery = this.validateAndSanitize(request.context.userQuery) - const normalizedQuery = this.normalize(sanitizedQuery) - const enhancedQuery = this.enhanceQuery(normalizedQuery) - const context = this.extractContext(request.context) - return { - ...request, - context: { - ...context, - userQuery: enhancedQuery, - }, - } - } - - private validateAndSanitize(query: string): string { - // Placeholder for query validation and sanitization logic. - // This could involve checking for malicious input, PII, or other sensitive data. - return query - } - - private normalize(query: string): string { - return query.toLowerCase().trim().replace(/\s+/g, ' ') - } - - private enhanceQuery(query: string): string { - // Placeholder for query enhancement logic. - // This could involve adding context, clarifying ambiguity, or other enhancements. - return query - } - - private extractContext( - context: UnifiedProcessingRequest['context'], - ): UnifiedProcessingRequest['context'] { - // Placeholder for context extraction logic. - // This could involve identifying entities, intents, or other contextual information. - return context - } -} diff --git a/apps/web/src/lib/metaaligner/streaming/streaming-handler.test.ts b/apps/web/src/lib/metaaligner/streaming/streaming-handler.test.ts deleted file mode 100644 index 3a695acbca..0000000000 --- a/apps/web/src/lib/metaaligner/streaming/streaming-handler.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @module streaming-handler.test - * @description This module provides tests for the StreamingHandler. - */ - -import { vi } from 'vitest' - -import type { - IUnifiedMetaAlignerAPI, - UnifiedProcessingRequest, - UnifiedProcessingResponse, -} from '../api/unified-api' -import { StreamingHandler } from './streaming-handler' - -describe('StreamingHandler', () => { - let api: IUnifiedMetaAlignerAPI - let handler: StreamingHandler - - beforeEach(() => { - api = { - process: vi.fn(), - } - handler = new StreamingHandler(api) - }) - - it('should process a stream of chunks', async () => { - const request: UnifiedProcessingRequest = { - llmOutput: { content: 'This is a test response' }, - context: { userQuery: 'test' }, - } - const response: UnifiedProcessingResponse = { - enhancedResponse: 'This is a test response', - originalResponse: 'This is a test response', - alignment: {} as any, - } - ;(api.process as any).mockResolvedValue(response) - - const onChunk = vi.fn() - const onError = vi.fn() - - await handler.processStream(request, onChunk, onError) - - expect(onChunk).toHaveBeenCalled() - expect(onError).not.toHaveBeenCalled() - }) - - it('should handle errors during streaming', async () => { - const request: UnifiedProcessingRequest = { - llmOutput: { content: 'This is a test response' }, - context: { userQuery: 'test' }, - } - const error = new Error('Test error') - ;(api.process as any).mockRejectedValue(error) - - const onChunk = vi.fn() - const onError = vi.fn() - - await handler.processStream(request, onChunk, onError) - - expect(onChunk).not.toHaveBeenCalled() - expect(onError).toHaveBeenCalledWith(error) - }) -}) diff --git a/apps/web/src/lib/metaaligner/streaming/streaming-handler.ts b/apps/web/src/lib/metaaligner/streaming/streaming-handler.ts deleted file mode 100644 index 74a1a7c7b3..0000000000 --- a/apps/web/src/lib/metaaligner/streaming/streaming-handler.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @module streaming-handler - * @description This module provides a streaming response handler for the MetaAligner pipeline. - */ - -import type { - UnifiedProcessingRequest, - UnifiedProcessingResponse, - IUnifiedMetaAlignerAPI, -} from '../api/unified-api' - -/** - * Defines the interface for the StreamingHandler. - */ -export interface IStreamingHandler { - /** - * Processes a streaming response. - * - * @param request - The processing request. - * @param onChunk - A callback that is called with each processed chunk. - * @returns A promise that resolves when the stream is fully processed. - */ - processStream( - request: UnifiedProcessingRequest, - onChunk: (chunk: UnifiedProcessingResponse) => void, - ): Promise -} - -/** - * The StreamingHandler class. - */ -export class StreamingHandler implements IStreamingHandler { - private readonly api: IUnifiedMetaAlignerAPI - private buffer: string[] = [] - private readonly bufferSize: number - - constructor(api: IUnifiedMetaAlignerAPI, bufferSize = 5) { - this.api = api - this.bufferSize = bufferSize - } - - public async processStream( - request: UnifiedProcessingRequest, - onChunk: (chunk: UnifiedProcessingResponse) => void, - _onError?: (error: Error) => void, - ): Promise { - try { - // In a real-world scenario, you would get a stream from the API. - // For now, we'll simulate a stream by splitting the response into chunks. - const response = await this.api.process(request) - const chunks = response.enhancedResponse.split(' ') - - for (const chunk of chunks) { - this.buffer.push(chunk) - if (this.buffer.length >= this.bufferSize) { - const bufferedChunk = this.buffer.join(' ') - this.buffer = [] - const chunkResponse: UnifiedProcessingResponse = { - ...response, - enhancedResponse: bufferedChunk, - } - onChunk(chunkResponse) - } - await new Promise((resolve) => setTimeout(resolve, 100)) // Simulate network latency - } - - if (this.buffer.length > 0) { - const bufferedChunk = this.buffer.join(' ') - this.buffer = [] - const chunkResponse: UnifiedProcessingResponse = { - ...response, - enhancedResponse: bufferedChunk, - } - onChunk(chunkResponse) - } - } catch (error: unknown) { - if (_onError) { - _onError(error as Error) - } - } - } -} diff --git a/apps/web/src/lib/metaaligner/tests/test-strategy.ts b/apps/web/src/lib/metaaligner/tests/test-strategy.ts deleted file mode 100644 index 57791e6b5e..0000000000 --- a/apps/web/src/lib/metaaligner/tests/test-strategy.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @module test-strategy - * @description This module provides a test strategy for the MetaAligner pipeline components. - */ - -/** - * Defines the interface for the TestStrategy. - */ -export interface ITestStrategy { - /** - * Outlines the test strategy for a component. - * - * @param componentName - The name of the component to test. - * @returns A string outlining the test strategy. - */ - outline(componentName: string): string -} - -/** - * The TestStrategy class. - */ -export class TestStrategy implements ITestStrategy { - public outline(componentName: string): string { - switch (componentName) { - case 'LLMAdapter': - return 'Unit tests for LLMAdapter should cover correct transformation of various LLM outputs to UnifiedProcessingRequest format, including edge cases and error handling.' - case 'QueryPreprocessor': - return 'Unit tests for QueryPreprocessor should cover normalization, context extraction, enhancement, validation, and sanitization of user queries.' - case 'ObjectiveInjector': - return 'Unit tests for ObjectiveInjector should cover dynamic loading, prioritization, conflict resolution, and validation of objectives.' - case 'ContextInjector': - return 'Unit tests for ContextInjector should cover context detection, validation, transformation, and caching.' - case 'EnhancementProcessor': - return 'Unit tests for EnhancementProcessor should cover multi-objective optimization, quality improvement algorithms, response scoring, and enhancement tracking.' - case 'QualityMetrics': - return 'Unit tests for QualityMetrics should cover accurate calculation of fluency, coherence, and relevance metrics.' - case 'PerformanceMonitor': - return 'Unit tests for PerformanceMonitor should cover accurate tracking of processing time, resource usage, and alerting mechanisms.' - case 'FullPipelineIntegration': - return 'Integration tests for the full pipeline should cover the end-to-end flow of a request through all components, ensuring data consistency and correct interactions.' - case 'PerformanceBenchmarking': - return 'Performance benchmarking tests should measure latency, throughput, and resource utilization under various load conditions.' - case 'EndToEndScenarios': - return 'End-to-end testing scenarios should simulate real-world user interactions and verify the complete system functionality from input to output.' - default: - return `No specific test strategy outlined for ${componentName}.` - } - } -} diff --git a/apps/web/src/lib/polyfills/browser-polyfills.ts b/apps/web/src/lib/polyfills/browser-polyfills.ts deleted file mode 100644 index 5a70914bcb..0000000000 --- a/apps/web/src/lib/polyfills/browser-polyfills.ts +++ /dev/null @@ -1,460 +0,0 @@ -import { createBuildSafeLogger } from '../logging/build-safe-logger' -const logger = createBuildSafeLogger('browser-polyfills') /** - * Browser-compatible polyfills for Node.js modules - * This file provides browser-friendly versions of Node.js modules - * that are being used in browser code. - */ - -// Process polyfill -export const process = { - env: { - NODE_ENV: import.meta.env.MODE || 'development', - // Add any other environment variables needed in browser context - BROWSER: 'true', - }, - nextTick: (callback: (...args: unknown[]) => void, ...args: unknown[]) => { - setTimeout(() => callback(...args), 0) - }, - // Add platform info for compatibility checks - platform: 'browser', - version: '16.0.0', // Mock version - versions: { - node: '16.0.0', - }, -} - -// MongoDB polyfill (stub implementation for client-side) -export const mongodb = { - ObjectId: class MockObjectId { - id: string - constructor(id?: string) { - this.id = id ?? 'mock-object-id' - } - toString() { - return this.id - } - toHexString() { - return this.id - } - static isValid(id: string) { - return ( - typeof id === 'string' && - id.length === 24 && - /^[a-fA-F0-9]{24}$/.test(id) - ) - } - }, - MongoClient: class MockMongoClient { - static connect() { - logger.warn('MongoDB is not supported in browser environment') - throw new Error('MongoDB is not supported in browser environment') - } - connect() { - logger.warn('MongoDB is not supported in browser environment') - throw new Error('MongoDB is not supported in browser environment') - } - }, - Collection: function MockCollection() { - logger.warn('MongoDB Collection is not supported in browser environment') - }, - Db: function MockDb() { - logger.warn('MongoDB Db is not supported in browser environment') - }, -} - -// Named export for MongoDB ObjectId -export const { ObjectId } = mongodb - -// Crypto polyfill (using Web Crypto API) -export const crypto = { - randomUUID: () => { - // Use the Web Crypto API if available - if (typeof window !== 'undefined' && window.crypto?.randomUUID) { - return window.crypto.randomUUID() - } - - // Fallback implementation - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0 - const v = c === 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) - }, - - // Add other crypto methods as needed - createHash: (_algorithm: string) => { - logger.warn( - 'crypto.createHash is not fully supported in browser environment', - ) - return { - update: (data: string) => ({ - digest: (_encoding: string) => - `browser-polyfill-hash-${data.substring(0, 8)}`, - }), - } - }, - - // Add subtle crypto API for modern browsers - subtle: (typeof window !== 'undefined' - ? window.crypto?.subtle - : undefined) ?? { - digest: async (_algorithm: string, _data: BufferSource) => { - logger.warn('crypto.subtle.digest fallback used - limited functionality') - return new Uint8Array(32) // Return dummy hash - }, - }, - - // Add randomBytes implementation - randomBytes: (size: number) => { - if (typeof window !== 'undefined' && window.crypto?.getRandomValues) { - const bytes = new Uint8Array(size) - window.crypto.getRandomValues(bytes) - return { - toString: (encoding?: string) => { - if (encoding === 'hex') { - return Array.from(bytes) - .map((b) => b.toString(16).padStart(2, '0')) - .join('') - } - return String.fromCharCode.apply(null, Array.from(bytes)) - }, - } - } - - // Fallback - const result = new Uint8Array(size) - for (let i = 0; i < size; i++) { - result[i] = Math.floor(Math.random() * 256) - } - return { - toString: (encoding?: string) => { - if (encoding === 'hex') { - return Array.from(result) - .map((b) => b.toString(16).padStart(2, '0')) - .join('') - } - return String.fromCharCode.apply(null, Array.from(result)) - }, - } - }, -} - -// Path polyfill with basic functionality -export const path = { - join: (...paths: string[]) => paths.join('/').replace(/\/+/g, '/'), - resolve: (...paths: string[]) => paths.join('/').replace(/\/+/g, '/'), - basename: (path: string) => path.split('/').pop() ?? '', - dirname: (path: string) => { - const parts = path.split('/') - parts.pop() - return parts.join('/') || '.' - }, - extname: (path: string) => { - const match = /\.[^.]+$/.exec(path) - return match ? match[0] : '' - }, - sep: '/', - delimiter: ':', - parse: (pathString: string) => { - const basename = pathString.split('/').pop() ?? '' - const extname = basename.includes('.') - ? '.' + basename.split('.').pop() - : '' - const name = extname ? basename.slice(0, -extname.length) : basename - - return { - root: pathString.startsWith('/') ? '/' : '', - dir: pathString.split('/').slice(0, -1).join('/'), - base: basename, - ext: extname, - name: name, - } - }, - format: (pathObject: Record) => { - const { dir, root, base, name, ext } = pathObject - const rootPath = dir ?? root ?? '' - const fileName = base ?? name + (ext ?? '') - return rootPath ? `${rootPath}/${fileName}` : fileName - }, -} - -// FS promises polyfill (stub implementation) -export const fs = { - promises: { - readFile: async (path: string, _options?: unknown) => { - logger.warn( - `fs.promises.readFile called with path: ${path} - not supported in browser`, - ) - throw new Error( - 'fs.promises.readFile is not supported in browser environment', - ) - }, - writeFile: async (path: string, _data: unknown, _options?: unknown) => { - logger.warn( - `fs.promises.writeFile called with path: ${path} - not supported in browser`, - ) - throw new Error( - 'fs.promises.writeFile is not supported in browser environment', - ) - }, - mkdir: async (path: string, _options?: unknown) => { - logger.warn( - `fs.promises.mkdir called with path: ${path} - not supported in browser`, - ) - throw new Error( - 'fs.promises.mkdir is not supported in browser environment', - ) - }, - stat: async (path: string) => { - logger.warn( - `fs.promises.stat called with path: ${path} - not supported in browser`, - ) - throw new Error( - 'fs.promises.stat is not supported in browser environment', - ) - }, - access: async (path: string, _mode?: number) => { - logger.warn( - `fs.promises.access called with path: ${path} - not supported in browser`, - ) - throw new Error( - 'fs.promises.access is not supported in browser environment', - ) - }, - }, - readFileSync: (path: string, _options?: unknown) => { - logger.warn( - `fs.readFileSync called with path: ${path} - not supported in browser`, - ) - throw new Error('fs.readFileSync is not supported in browser environment') - }, - existsSync: (path: string) => { - logger.warn( - `fs.existsSync called with path: ${path} - not supported in browser`, - ) - return false - }, -} - -// Child process polyfill (stub implementation) -export const child_process = { - spawn: (command: string, _args?: string[], _options?: unknown) => { - logger.warn( - `child_process.spawn called with command: ${command} - not supported in browser`, - ) - return { - on: (_event: string, _callback: (...args: unknown[]) => void) => {}, - stdout: { - on: (_event: string, _callback: (...args: unknown[]) => void) => {}, - pipe: (destination: unknown) => destination, - }, - stderr: { - on: (_event: string, _callback: (...args: unknown[]) => void) => {}, - pipe: (destination: unknown) => destination, - }, - kill: () => {}, - } - }, - exec: ( - command: string, - _options?: unknown, - callback?: (...args: unknown[]) => void, - ) => { - logger.warn( - `child_process.exec called with command: ${command} - not supported in browser`, - ) - if (callback) { - callback( - new Error('child_process.exec is not supported in browser environment'), - '', - '', - ) - } - throw new Error( - 'child_process.exec is not supported in browser environment', - ) - }, - execSync: (command: string, _options?: unknown) => { - logger.warn( - `child_process.execSync called with command: ${command} - not supported in browser`, - ) - throw new Error( - 'child_process.execSync is not supported in browser environment', - ) - }, -} - -// Stream polyfill (minimal implementation) -export const stream = { - Readable: class { - on(_event: string, _listener: (...args: unknown[]) => void) { - return this - } - pipe(destination: T): T { - return destination - } - read() { - return null - } - }, - Writable: class { - on(_event: string, _listener: (...args: unknown[]) => void) { - return this - } - write(_chunk: unknown): boolean { - return true - } - end() {} - }, - Transform: class { - on(_event: string, _listener: (...args: unknown[]) => void) { - return this - } - write(_chunk: unknown): boolean { - return true - } - end() {} - pipe(destination: T): T { - return destination - } - }, -} - -// Events polyfill -export const events = { - EventEmitter: class { - private listeners: Record void)[]> = {} - - on(event: string, listener: (...args: unknown[]) => void) { - this.listeners[event] ??= [] - this.listeners[event].push(listener) - return this - } - - emit(event: string, ...args: unknown[]): boolean { - if (!this.listeners[event]) { - return false - } - this.listeners[event].forEach((listener) => listener(...args)) - return true - } - - removeListener(event: string, listener: (...args: unknown[]) => void) { - if (!this.listeners[event]) { - return this - } - this.listeners[event] = this.listeners[event].filter( - (l) => l !== listener, - ) - return this - } - - once(event: string, listener: (...args: unknown[]) => void) { - const onceWrapper = (...args: unknown[]) => { - listener(...args) - this.removeListener(event, onceWrapper) - } - return this.on(event, onceWrapper) - } - }, -} - -// Util polyfill -export const util = { - promisify: (fn: (...args: unknown[]) => unknown) => { - return async (...args: unknown[]) => { - return new Promise((resolve, reject) => { - fn(...args, (err: Error | null, ...results: unknown[]) => { - if (err) { - return reject(err) - } - if (results.length === 1) { - return resolve(results[0]) - } - resolve(results) - }) - }) - } - }, - inspect: (obj: unknown) => JSON.stringify(obj, null, 2), - types: { - isPromise: (value: unknown): value is Promise => - value instanceof Promise, - isDate: (value: unknown): value is Date => value instanceof Date, - isRegExp: (value: unknown): value is RegExp => value instanceof RegExp, - }, -} - -// OS polyfill -export const os = { - platform: () => 'browser', - arch: () => 'wasm32', - cpus: () => [], - totalmem: () => 8 * 1024 * 1024 * 1024, // 8GB mock - freemem: () => 4 * 1024 * 1024 * 1024, // 4GB mock - tmpdir: () => '/tmp', - EOL: '\n', -} - -// HTTP polyfill (minimal implementation) -export const http = { - createServer: () => { - logger.warn('http.createServer is not supported in browser environment') - return { - listen: () => {}, - on: () => {}, - } - }, - request: (_options: unknown, callback?: (...args: unknown[]) => void) => { - logger.warn('http.request is not supported in browser environment') - if (callback) { - callback(new Error('http.request not supported')) - } - return { - on: (_event: string, _cb: (...args: unknown[]) => void) => { - return {} - }, - write: (_chunk: unknown) => {}, - end: () => {}, - } - }, -} - -// HTTPS polyfill (minimal implementation) -export const https = { - createServer: () => { - logger.warn('https.createServer is not supported in browser environment') - return { - listen: () => {}, - on: () => {}, - } - }, - request: (_options: unknown, callback?: (...args: unknown[]) => void) => { - logger.warn('https.request is not supported in browser environment') - if (callback) { - callback(new Error('https.request not supported')) - } - return { - on: (_event: string, _cb: (...args: unknown[]) => void) => { - return {} - }, - write: (_chunk: unknown) => {}, - end: () => {}, - } - }, -} - -// Default export for all polyfills -export default { - process, - crypto, - path, - fs, - child_process, - stream, - events, - util, - os, - http, - https, -} diff --git a/apps/web/src/lib/polyfills/buffer.ts b/apps/web/src/lib/polyfills/buffer.ts deleted file mode 100644 index a071956c62..0000000000 --- a/apps/web/src/lib/polyfills/buffer.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Import the original Buffer from the 'buffer' package -import { Buffer as BufferOriginal } from 'buffer' - -// Ensure Buffer is available globally -if (typeof globalThis.Buffer === 'undefined') { - ;(globalThis as unknown as { Buffer: typeof BufferOriginal }).Buffer = - BufferOriginal -} - -// Export the Buffer for explicit imports -export const Buffer = BufferOriginal -export default BufferOriginal diff --git a/apps/web/src/lib/polyfills/node-child-process.js b/apps/web/src/lib/polyfills/node-child-process.js deleted file mode 100644 index 1d3b1fe4f8..0000000000 --- a/apps/web/src/lib/polyfills/node-child-process.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Polyfill for node:child_process module - */ - -export function exec() { - throw new Error('Not supported in browser environment') -} - -export function execFile() { - throw new Error('Not supported in browser environment') -} - -export function spawn() { - throw new Error('Not supported in browser environment') -} - -export function fork() { - throw new Error('Not supported in browser environment') -} - -export default { - exec, - execFile, - spawn, - fork, -} diff --git a/apps/web/src/lib/polyfills/node-diagnostics-channel.js b/apps/web/src/lib/polyfills/node-diagnostics-channel.js deleted file mode 100644 index 491dbab89c..0000000000 --- a/apps/web/src/lib/polyfills/node-diagnostics-channel.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Polyfill for node:diagnostics_channel module - */ - -export function channel(_name) { - return { - hasSubscribers: false, - publish: () => false, - subscribe: () => {}, - unsubscribe: () => {}, - } -} - -export function subscribe(_name, _onMessage) {} -export function unsubscribe(_name, _onMessage) {} -export const tracingChannel = channel('tracing') - -export default { - channel, - subscribe, - unsubscribe, - tracingChannel, -} diff --git a/apps/web/src/lib/polyfills/node-fs.js b/apps/web/src/lib/polyfills/node-fs.js deleted file mode 100644 index a9f308c779..0000000000 --- a/apps/web/src/lib/polyfills/node-fs.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Polyfill for node:fs module - */ - -export async function readFile() { - return Promise.resolve('') -} - -export function readFileSync() { - return '' -} - -export async function writeFile() { - return Promise.resolve() -} - -export function writeFileSync() {} - -export function existsSync() { - return false -} - -export function createReadStream() { - throw new Error('Not implemented') -} - -export async function readdir() { - return Promise.resolve([]) -} - -export const promises = { - readFile: async () => Promise.resolve(''), - writeFile: async () => Promise.resolve(), - readdir: async () => Promise.resolve([]), -} - -export default { - readFile, - readFileSync, - writeFile, - writeFileSync, - existsSync, - createReadStream, - readdir, - promises, -} diff --git a/apps/web/src/lib/polyfills/node-http.js b/apps/web/src/lib/polyfills/node-http.js deleted file mode 100644 index e85492a631..0000000000 --- a/apps/web/src/lib/polyfills/node-http.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Polyfill for node:http module - */ - -export function createServer() { - return { - listen: () => {}, - on: () => {}, - close: () => {}, - } -} - -export function request() { - return { - on: () => {}, - write: () => {}, - end: () => {}, - } -} - -export function get() { - return { - on: () => {}, - end: () => {}, - } -} - -export default { - createServer, - request, - get, -} diff --git a/apps/web/src/lib/polyfills/node-https.js b/apps/web/src/lib/polyfills/node-https.js deleted file mode 100644 index 21b8584077..0000000000 --- a/apps/web/src/lib/polyfills/node-https.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Polyfill for node:https module - */ - -export function createServer() { - return { - listen: () => {}, - on: () => {}, - close: () => {}, - } -} - -export function request() { - return { - on: () => {}, - write: () => {}, - end: () => {}, - } -} - -export function get() { - return { - on: () => {}, - end: () => {}, - } -} - -export default { - createServer, - request, - get, -} diff --git a/apps/web/src/lib/polyfills/node-inspector.js b/apps/web/src/lib/polyfills/node-inspector.js deleted file mode 100644 index 6aca9de0e5..0000000000 --- a/apps/web/src/lib/polyfills/node-inspector.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Polyfill for node:inspector module - */ - -export function open() {} -export function close() {} -export function url() { - return null -} -export function waitForDebugger() {} - -export const console = { - markTimeline() {}, - timeline() {}, - timelineEnd() {}, -} - -export class Session { - connect() {} - disconnect() {} - post() {} - on() {} -} - -export default { - open, - close, - url, - waitForDebugger, - console, - Session, -} diff --git a/apps/web/src/lib/polyfills/node-net.js b/apps/web/src/lib/polyfills/node-net.js deleted file mode 100644 index 034fb4649f..0000000000 --- a/apps/web/src/lib/polyfills/node-net.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Polyfill for node:net module - */ - -export function createServer() { - return { - listen: () => {}, - on: () => {}, - close: () => {}, - } -} - -export function connect() { - return { - on: () => {}, - write: () => {}, - end: () => {}, - } -} - -export function createConnection() { - return connect() -} - -export default { - createServer, - connect, - createConnection, -} diff --git a/apps/web/src/lib/polyfills/node-os.js b/apps/web/src/lib/polyfills/node-os.js deleted file mode 100644 index a2028ee139..0000000000 --- a/apps/web/src/lib/polyfills/node-os.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Polyfill for node:os module - */ - -export function hostname() { - return 'browser' -} - -export function platform() { - return 'browser' -} - -export function release() { - return '1.0.0' -} - -export function type() { - return 'Browser' -} - -export function uptime() { - return 0 -} - -export function totalmem() { - return 0 -} - -export function freemem() { - return 0 -} - -export function cpus() { - return [] -} - -export default { - hostname, - platform, - release, - type, - uptime, - totalmem, - freemem, - cpus, -} diff --git a/apps/web/src/lib/polyfills/node-path.js b/apps/web/src/lib/polyfills/node-path.js deleted file mode 100644 index 832e8a4b49..0000000000 --- a/apps/web/src/lib/polyfills/node-path.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Polyfill for node:path module - */ - -export function join(...parts) { - return parts.join('/').replace(/\/+/g, '/') -} - -export function dirname(p) { - return p.split('/').slice(0, -1).join('/') ?? '.' -} - -export function basename(p, ext) { - const base = p.split('/').pop() ?? '' - return ext && base.endsWith(ext) ? base.slice(0, -ext.length) : base -} - -export function extname(p) { - const base = p.split('/').pop() ?? '' - const idx = base.lastIndexOf('.') - return idx !== -1 ? base.slice(idx) : '' -} - -export const sep = '/' -export const delimiter = ':' -export const posix = { join, dirname, basename, extname, sep, delimiter } -export const win32 = { - join, - dirname, - basename, - extname, - sep: '\\', - delimiter: ';', -} - -export default { - join, - dirname, - basename, - extname, - sep, - delimiter, - posix, - win32, -} diff --git a/apps/web/src/lib/polyfills/node-polyfills.ts b/apps/web/src/lib/polyfills/node-polyfills.ts deleted file mode 100644 index 68a2906528..0000000000 --- a/apps/web/src/lib/polyfills/node-polyfills.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Node.js polyfills for browser compatibility - * This file ensures that Node.js-specific APIs like Buffer are - * properly polyfilled in browser environments. - */ - -// Import the Buffer class from the buffer package -import { Buffer as BufferClass } from 'buffer' - -// Make Buffer available globally -if (typeof globalThis.Buffer === 'undefined') { - interface GlobalThisWithBuffer { - Buffer: typeof BufferClass - } - ;(globalThis as unknown as GlobalThisWithBuffer).Buffer = BufferClass -} - -// Export the Buffer for use in other modules -export const Buffer = BufferClass - -// Ensure process is available -if (typeof globalThis.process === 'undefined') { - globalThis.process = { - env: {}, - version: '', - versions: { - node: '0.0.0', - v8: '0.0.0', - uv: '0.0.0', - zlib: '0.0.0', - brotli: '0.0.0', - ares: '0.0.0', - modules: '0', - nghttp2: '0.0.0', - napi: '0', - llhttp: '0.0.0', - openssl: '0.0.0', - cldr: '0.0', - icu: '0.0', - tz: '0000', - unicode: '0.0', - http_parser: '0.0.0', - } as NodeJS.ProcessVersions, - platform: 'browser' as NodeJS.Platform, - argv: [], - nextTick: (fn: (...args: unknown[]) => void, ...args: unknown[]) => { - setTimeout(() => fn(...args), 0) - }, - } as unknown as NodeJS.Process -} - -// Add other Node.js APIs as needed -export default { - Buffer: BufferClass, - // Export other polyfilled APIs if needed -} diff --git a/apps/web/src/lib/polyfills/node-readline.js b/apps/web/src/lib/polyfills/node-readline.js deleted file mode 100644 index 5575da74cc..0000000000 --- a/apps/web/src/lib/polyfills/node-readline.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Polyfill for node:readline module - */ - -export function createInterface() { - return { - question: (_, cb) => cb(''), - close: () => {}, - on: () => {}, - once: () => {}, - removeListener: () => {}, - setPrompt: () => {}, - prompt: () => {}, - } -} - -export default { - createInterface, -} diff --git a/apps/web/src/lib/polyfills/node-shims.js b/apps/web/src/lib/polyfills/node-shims.js deleted file mode 100644 index 8f13c13b01..0000000000 --- a/apps/web/src/lib/polyfills/node-shims.js +++ /dev/null @@ -1,298 +0,0 @@ -/** - * Node.js built-in modules polyfills for browser/edge environments - * This file provides safe fallbacks for Node.js built-ins to prevent bundling errors - */ - -// Async Hooks polyfill -export const AsyncLocalStorage = class { - constructor() { - this.store = new Map() - } - - run(store, callback, ...args) { - return callback(...args) - } - - getStore() { - return undefined - } - - enterWith(_store) { - // No-op in browser environment - } - - exit(callback, ...args) { - return callback(...args) - } -} - -export const executionAsyncId = () => 0 -export const triggerAsyncId = () => 0 -export const createHook = () => ({ - enable: () => {}, - disable: () => {}, -}) - -// Crypto polyfill -export const createHash = () => ({ - update: () => ({}), - digest: () => '', -}) - -export const createHmac = () => ({ - update: () => ({}), - digest: () => '', -}) - -export const randomBytes = (size) => new Uint8Array(size) -export const pbkdf2 = () => {} -export const pbkdf2Sync = () => new Uint8Array(32) -export const scrypt = () => {} -export const scryptSync = () => new Uint8Array(32) - -// Buffer polyfill -export const Buffer = globalThis.Buffer ?? { - from: (data) => new Uint8Array(Array.isArray(data) ? data : []), - alloc: (size) => new Uint8Array(size), - allocUnsafe: (size) => new Uint8Array(size), - isBuffer: () => false, - concat: (list) => { - const totalLength = list.reduce((acc, item) => acc + item.length, 0) - const result = new Uint8Array(totalLength) - let offset = 0 - for (const item of list) { - result.set(item, offset) - offset += item.length - } - return result - }, -} - -// Events polyfill -export class EventEmitter { - constructor() { - this.events = new Map() - } - - on(event, listener) { - if (!this.events.has(event)) { - this.events.set(event, []) - } - this.events.get(event).push(listener) - return this - } - - emit(event, ...args) { - const listeners = this.events.get(event) ?? [] - listeners.forEach((listener) => listener(...args)) - return listeners.length > 0 - } - - removeListener(event, listener) { - const listeners = this.events.get(event) ?? [] - const index = listeners.indexOf(listener) - if (index > -1) { - listeners.splice(index, 1) - } - return this - } - - removeAllListeners(event) { - if (event) { - this.events.delete(event) - } else { - this.events.clear() - } - return this - } -} - -// Stream polyfill -export class Readable extends EventEmitter { - constructor() { - super() - this.readable = true - } - - read() { - return null - } - - pipe(destination) { - return destination - } -} - -export class Writable extends EventEmitter { - constructor() { - super() - this.writable = true - } - - write(chunk, encoding, callback) { - if (typeof encoding === 'function') { - callback = encoding - } - if (callback) { - callback() - } - return true - } - - end(chunk, encoding, callback) { - if (typeof chunk === 'function') { - callback = chunk - } else if (typeof encoding === 'function') { - callback = encoding - } - if (callback) { - callback() - } - } -} - -// Process polyfill -export const process = globalThis.process ?? { - env: {}, - cwd: () => '/', - chdir: () => {}, - platform: 'browser', - arch: 'unknown', - version: 'v18.0.0', - versions: { node: '18.0.0' }, - nextTick: (callback, ...args) => { - if (typeof setImmediate !== 'undefined') { - setImmediate(callback, ...args) - } else { - setTimeout(callback, 0, ...args) - } - }, - exit: () => {}, - stderr: { write: () => {} }, - stdout: { write: () => {} }, - stdin: { read: () => null }, -} - -// Path polyfill -export const path = { - join: (...paths) => paths.filter(Boolean).join('/').replace(/\/+/g, '/'), - resolve: (...paths) => - '/' + paths.filter(Boolean).join('/').replace(/\/+/g, '/'), - dirname: (p) => p.split('/').slice(0, -1).join('/') ?? '/', - basename: (p, ext) => { - const name = p.split('/').pop() ?? '' - return ext && name.endsWith(ext) ? name.slice(0, -ext.length) : name - }, - extname: (p) => { - const name = p.split('/').pop() ?? '' - const dotIndex = name.lastIndexOf('.') - return dotIndex > 0 ? name.slice(dotIndex) : '' - }, - isAbsolute: (p) => p.startsWith('/'), - relative: () => '', - sep: '/', - delimiter: ':', - parse: (p) => ({ - root: p.startsWith('/') ? '/' : '', - dir: p.split('/').slice(0, -1).join('/') ?? '/', - base: p.split('/').pop() ?? '', - ext: '', - name: '', - }), -} - -// File system polyfill -export const fs = { - readFile: async () => Promise.resolve(''), - readFileSync: () => '', - writeFile: async () => Promise.resolve(), - writeFileSync: () => {}, - existsSync: () => false, - statSync: () => ({ isDirectory: () => false, isFile: () => false }), - stat: async () => - Promise.resolve({ isDirectory: () => false, isFile: () => false }), - mkdir: async () => Promise.resolve(), - mkdirSync: () => {}, - readdir: async () => Promise.resolve([]), - readdirSync: () => [], -} - -// Utilities polyfill -export const util = { - promisify: - (fn) => - async (...args) => - Promise.resolve(fn(...args)), - inspect: (obj) => JSON.stringify(obj), - format: (f, ...args) => f.replace(/%[sdj%]/g, (_x) => args.shift()), - inherits: () => {}, - isDeepStrictEqual: () => false, - types: { - isTypedArray: () => false, - isArrayBuffer: () => false, - }, -} - -// OS polyfill -export const os = { - platform: () => 'browser', - arch: () => 'unknown', - release: () => '0.0.0', - hostname: () => 'localhost', - type: () => 'Browser', - cpus: () => [], - totalmem: () => 0, - freemem: () => 0, - loadavg: () => [0, 0, 0], - networkInterfaces: () => ({}), - homedir: () => '/', - tmpdir: () => '/tmp', -} - -// HTTP/HTTPS polyfill -export const http = { - createServer: () => ({ - listen: () => {}, - on: () => {}, - }), - request: () => ({ - on: () => {}, - write: () => {}, - end: () => {}, - }), -} - -export const https = { - ...http, - request: () => ({ - on: () => {}, - write: () => {}, - end: () => {}, - }), -} - -// Default export for compatibility -export default { - AsyncLocalStorage, - executionAsyncId, - triggerAsyncId, - createHook, - createHash, - createHmac, - randomBytes, - pbkdf2, - pbkdf2Sync, - scrypt, - scryptSync, - Buffer, - EventEmitter, - Readable, - Writable, - process, - path, - fs, - util, - os, - http, - https, -} diff --git a/apps/web/src/lib/polyfills/node-stream.js b/apps/web/src/lib/polyfills/node-stream.js deleted file mode 100644 index cc6f98c8a5..0000000000 --- a/apps/web/src/lib/polyfills/node-stream.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Polyfill for node:stream module - */ - -export class Readable { - pipe() { - return this - } - on() { - return this - } - once() { - return this - } - read() { - return null - } - push() { - return true - } - destroy() {} -} - -export class Writable { - write() { - return true - } - end() {} - on() { - return this - } - once() { - return this - } - destroy() {} -} - -export class Duplex extends Readable { - write() { - return true - } - end() {} -} - -export class Transform extends Duplex { - _transform(chunk, encoding, callback) { - callback(null, chunk) - } -} - -export class PassThrough extends Transform {} - -export async function pipeline() { - return Promise.resolve() -} -export async function finished() { - return Promise.resolve() -} - -export default { - Readable, - Writable, - Duplex, - Transform, - PassThrough, - pipeline, - finished, -} diff --git a/apps/web/src/lib/polyfills/node-tls.js b/apps/web/src/lib/polyfills/node-tls.js deleted file mode 100644 index ecb2c31297..0000000000 --- a/apps/web/src/lib/polyfills/node-tls.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Polyfill for node:tls module - */ - -export function createServer() { - return { - listen: () => {}, - on: () => {}, - close: () => {}, - } -} - -export function connect() { - return { - on: () => {}, - write: () => {}, - end: () => {}, - } -} - -export function createConnection() { - return connect() -} - -export default { - createServer, - connect, - createConnection, -} diff --git a/apps/web/src/lib/polyfills/node-util.js b/apps/web/src/lib/polyfills/node-util.js deleted file mode 100644 index 11c65a9a49..0000000000 --- a/apps/web/src/lib/polyfills/node-util.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Polyfill for node:util module - */ - -export function format(format, ...args) { - return String(format).replace(/%[sdifjoO%]/g, (match) => { - if (match === '%%') { - return '%' - } - return String(args.shift() ?? '') - }) -} - -export function inspect(obj) { - return JSON.stringify(obj) -} - -export function promisify(_fn) { - return async (..._args) => Promise.resolve() -} - -export function deprecate(fn, _message) { - return fn -} - -export const types = { - isDate: () => false, - isRegExp: () => false, -} - -export default { - format, - inspect, - promisify, - deprecate, - types, -} diff --git a/apps/web/src/lib/polyfills/node-worker-threads.js b/apps/web/src/lib/polyfills/node-worker-threads.js deleted file mode 100644 index c68e4cc023..0000000000 --- a/apps/web/src/lib/polyfills/node-worker-threads.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Polyfill for node:worker_threads module - */ - -export class Worker { - constructor() { - throw new Error('Worker threads are not supported in this environment') - } - terminate() {} -} - -export const isMainThread = true -export const parentPort = null -export const threadId = 0 -export const workerData = null - -export function markAsUntransferable() {} -export function moveMessagePortToContext() {} -export function receiveMessageOnPort() { - return null -} -export function resourceLimits() { - return {} -} -export function setEnvironmentData() {} -export function getEnvironmentData() { - return null -} - -export default { - Worker, - isMainThread, - parentPort, - threadId, - workerData, - markAsUntransferable, - moveMessagePortToContext, - receiveMessageOnPort, - resourceLimits, - setEnvironmentData, - getEnvironmentData, -} diff --git a/apps/web/src/lib/polyfills/node-zlib.js b/apps/web/src/lib/polyfills/node-zlib.js deleted file mode 100644 index e4b8c3af8b..0000000000 --- a/apps/web/src/lib/polyfills/node-zlib.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Polyfill for node:zlib module - */ - -export function createGzip() { - throw new Error('zlib not supported in browser') -} - -export function createGunzip() { - throw new Error('zlib not supported in browser') -} - -export function createDeflate() { - throw new Error('zlib not supported in browser') -} - -export function createInflate() { - throw new Error('zlib not supported in browser') -} - -export async function gzip() { - return Promise.resolve(new Uint8Array()) -} - -export async function gunzip() { - return Promise.resolve(new Uint8Array()) -} - -export async function deflate() { - return Promise.resolve(new Uint8Array()) -} - -export async function inflate() { - return Promise.resolve(new Uint8Array()) -} - -export default { - createGzip, - createGunzip, - createDeflate, - createInflate, - gzip, - gunzip, - deflate, - inflate, -} diff --git a/eslint.config.js b/eslint.config.js index 2584f743e0..ffab36b4a3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -308,15 +308,4 @@ export default tseslint.config( '@typescript-eslint/no-unused-vars': 'off', }, }, - - // Multi-region modules allow typed interoperability while we migrate legacy interfaces - { - files: [ - 'apps/web/src/lib/deployment/multi-region/AutomatedFailoverOrchestrator.ts', - 'apps/web/src/lib/deployment/multi-region/CrossRegionDataSyncManager.ts', - ], - rules: { - '@typescript-eslint/no-explicit-any': 'off', - }, - }, ) diff --git a/scripts/ci/large-file-baseline.json b/scripts/ci/large-file-baseline.json index 9a7e6457b0..727d3657b2 100644 --- a/scripts/ci/large-file-baseline.json +++ b/scripts/ci/large-file-baseline.json @@ -10,16 +10,11 @@ "foresight/foresight/server.py", "apps/web/src/lib/security/threat-intelligence/feeds/ExternalThreatFeedIntegration.ts", "apps/web/src/lib/fhe/hipaa-monitoring.ts", - "apps/web/src/lib/deployment/multi-region/EdgeComputingManager.locations.ts", "apps/web/src/components/admin/bias-detection/BiasDashboard.components.tsx", - "apps/web/src/lib/deployment/multi-region/CrossRegionDataSyncManager.ts", "apps/web/src/lib/security/threat-detection/predictive/predictive-threat-intelligence.ts", - "apps/web/src/lib/deployment/multi-region/ServiceDiscoveryManager.ts", - "apps/web/src/lib/metaaligner/prioritization/support-context-identifier.ts", "apps/web/src/lib/security/threat-detection/monitoring/ai-enhanced-monitoring.ts", "apps/web/src/lib/security/threat-intelligence/orchestration/AutomatedThreatResponseOrchestrator.ts", "apps/web/src/lib/security/threat-intelligence/validation/ThreatValidationSystem.ts", - "apps/web/src/lib/deployment/multi-region/AutomatedFailoverOrchestrator.ts", "apps/web/src/lib/security/threat-intelligence/correlation/ThreatCorrelationEngine.ts", "foresight/foresight/hybrid_retriever.py", "scripts/task_sync/provider_bridge.py", @@ -37,14 +32,12 @@ "apps/web/src/lib/research/ResearchPlatform.ts", "apps/web/src/lib/services/auth0.service.ts", "apps/web/src/lib/ai/services/RecommendationService.ts", - "apps/web/src/lib/deployment/multi-region/DeploymentOrchestrator.ts", "apps/web/src/lib/security/threat-detection/response-orchestration.ts", "apps/web/src/lib/security/threat-intelligence/database/ThreatIntelligenceDatabase.ts", "apps/web/src/lib/security/backup/index.ts", "apps/web/src/pages/demo/bias-detection.astro", "apps/web/src/lib/db/ai/repository.ts", "apps/web/src/lib/ai/bias-detection/cache.ts", - "apps/web/src/lib/deployment/multi-region/ConfigurationManager.ts", "apps/web/src/lib/ai/mental-arena/MentalArenaPythonBridge.ts", "foresight/foresight/hooks.py", "apps/web/src/lib/security/threat-intelligence/edge/EdgeThreatDetectionSystem.ts", @@ -66,7 +59,6 @@ "apps/web/src/pages/nightmare-fuel-demo.astro", "foresight/foresight/benchmarks.py", "apps/web/src/lib/ai/bias-detection/python-service/bias_detection/services/model_service.py", - "apps/web/src/lib/metaaligner/prioritization/educational-context-recognizer.ts", "apps/web/src/lib/fhe/encrypted-text-processor.ts", "apps/web/src/lib/ehr-native/consent/state-rules/seed-data.ts", "apps/web/src/lib/ai/bias-detection/alerts-system.ts", @@ -81,7 +73,6 @@ "apps/web/src/lib/ai/bias-detection/config.ts", "apps/web/src/lib/utils/image-optimizer.ts", "apps/web/src/lib/ai/bias-detection/intelligent-cache.ts", - "apps/web/src/lib/deployment/multi-region/HealthMonitor.ts", "apps/web/src/pages/dashboard/index.astro", "foresight/foresight/semantic_search.py", "apps/web/src/components/ehr/dashboards/DashboardGrid.tsx", @@ -89,7 +80,6 @@ "apps/web/src/components/analytics/AdvancedFilteringComponent.astro", "apps/web/src/lib/ai/mental-llama/evidence/EvidenceExtractor.ts", "apps/web/src/components/research/ResearchDashboard.tsx", - "apps/web/src/lib/deployment/multi-region/GlobalTrafficRoutingManager.ts", "apps/web/src/pages/admin/all-demos-analytics.astro", "apps/web/src/lib/memory/reprioritization_engine.ts", "apps/web/src/lib/services/training/TrainingWebSocketServer.ts", @@ -126,7 +116,6 @@ "apps/web/src/lib/db/index.ts", "apps/web/src/components/widgets/SearchSwitch.astro", "apps/web/src/lib/openapi-spec.ts", - "apps/web/src/lib/metaaligner/core/objective-metrics.ts", "apps/web/src/lib/security/threat-intelligence/global/types.ts", "apps/web/src/lib/services/bias-audit-service.ts", "scripts/ci/ci-ops-dashboard.py", @@ -135,7 +124,6 @@ "apps/web/src/lib/ai/services/AutomatedEscalationService.ts", "apps/web/src/components/demos/bias-detection/EnhancedBiasDetectionInterface.tsx", "apps/web/src/lib/deployment/productionManager.ts", - "apps/web/src/lib/metaaligner/api/alignment-api.ts", "foresight/foresight/graph_store.py", "apps/web/src/lib/ai/mental-llama/ClinicalAnalysisHelpers.ts", "apps/web/src/lib/research/services/PatternDiscoveryService.ts", @@ -143,5 +131,5 @@ "apps/web/src/lib/auth/auth0-jwt-service.ts", "apps/web/src/types.ts" ], - "oversizedCount": 135 + "oversizedCount": 123 } diff --git a/scripts/ci/naming-baseline.json b/scripts/ci/naming-baseline.json index 1f5952a2e1..0c211fb49b 100644 --- a/scripts/ci/naming-baseline.json +++ b/scripts/ci/naming-baseline.json @@ -2,7 +2,7 @@ "$schema": "Naming-convention baseline for .eslintrc.naming.mjs", "description": "Per-file @typescript-eslint/naming-convention violation counts. The audit fails when a file regresses above its pinned count. Re-pin with `pnpm lint:naming -- --update` only after intentional changes.", "generatedAt": "2026-09-14T23:35:53.041Z", - "total": 37, + "total": 36, "files": { "agents/qa-agent/agent/tools/summarize_cohort.ts": 2, "apps/web/src/components/chat/PixelMultimodalChat.tsx": 2, @@ -15,7 +15,6 @@ "apps/web/src/lib/ai/services/MultiModalRiskAssessmentService.ts": 1, "apps/web/src/lib/auth/auth0-jwt-service.ts": 2, "apps/web/src/lib/ehr-native/integrations/clearinghouse/stub-adapter.ts": 1, - "apps/web/src/lib/polyfills/browser-polyfills.ts": 1, "apps/web/src/lib/security/threat-detection/threat-hunting/utils/threat-data-utils.ts": 2, "apps/web/src/pages/api/developer/api-keys/index.ts": 2, "apps/web/src/pages/api/ehr/v1/notes/[id]/sign.ts": 3, diff --git a/scripts/testing/local-test-runner.mjs b/scripts/testing/local-test-runner.mjs index 6dd55a2263..cac301e06d 100644 --- a/scripts/testing/local-test-runner.mjs +++ b/scripts/testing/local-test-runner.mjs @@ -80,7 +80,6 @@ const ADVISORY_BUCKET_DIRS = { ], "lib-services": [ "apps/web/src/lib/services", - "apps/web/src/lib/metaaligner", "apps/web/src/lib/fhe", ], "lib-security-hooks": [