UNPKG

optivise

Version:

Optivise - The Ultimate Optimizely Development Assistant with AI-powered features, zero-config setup, and comprehensive development support

383 lines 18.6 kB
/** * Product Detection Service * Detects Optimizely products from project context and prompts */ import { promises as fs } from 'fs'; import * as path from 'path'; export class ProductDetectionService { logger; isInitialized = false; // Detection patterns for different products detectionPatterns = { 'configured-commerce': { files: ['*Handler.cs', '*Pipeline.cs', '*.Blueprint.tsx', 'Startup.cs', 'ISC.*', 'InsiteCommerce.Web.*', '*.ascx', '*.aspx'], directories: ['Extensions/', 'FrontEnd/', 'src/Extensions/', 'InsiteCommerce.Web/', 'modules/blueprints/', 'Themes/', 'wwwroot/', 'Content/', 'Scripts/'], dependencies: ['insite*', 'InsiteCommerce*', '@insite/*', 'optimizely.commerce*'], content: ['HandlerChainManager', 'IPipelineAssemblyOptions', 'IHandlerFactory', 'insite', 'commerce', 'blueprint', 'configured commerce', 'ISC', 'InsiteCommerce', 'HandlerFactory', 'PipelineAssembly'] }, 'commerce-connect': { files: ['*Connector.cs', '*Sync.cs', 'Commerce.Connect.*'], directories: ['Connectors/', 'Commerce.Connect/'], dependencies: ['Optimizely.Commerce.Connect*'], content: ['ICommerceConnector', 'SynchronizationService'] }, 'cms-paas': { files: ['*.ascx', '*Controller.cs', 'Startup.cs', 'web.config'], directories: ['modules/', 'App_Data/', 'Views/', 'Controllers/'], dependencies: ['episerver*', 'optimizely*cms*', 'EPiServer*'], content: ['ContentType', 'PropertyFor', 'EPiServer', 'IContentRepository'] }, 'cms-saas': { files: ['*.json', 'next.config.js', 'gatsby-config.js'], directories: ['components/', 'src/components/', 'pages/'], dependencies: ['@optimizely/cms*', '@episerver/*', 'optimizely-graph*'], content: ['OptimizelyGraph', 'ContentDelivery', 'headless'] }, 'cmp': { files: ['campaign-config.*', 'marketing-*'], directories: ['campaigns/', 'marketing/'], dependencies: ['@optimizely/marketing*', 'optimizely-campaign*'], content: ['CampaignManager', 'MarketingAutomation'] }, 'dxp': { files: ['personalization.*', 'visitor-groups.*'], directories: ['Personalization/', 'VisitorGroups/'], dependencies: ['optimizely-dxp*', 'episerver-dxp*'], content: ['VisitorGroup', 'PersonalizationProvider', 'RecommendationService'] }, 'web-experimentation': { files: ['optimizely.js', 'experiment-*', '*.experiment.*'], directories: ['experiments/', 'src/experiments/'], dependencies: ['@optimizely/optimizely-sdk', 'optimizely-client-sdk'], content: ['optimizely.createInstance', 'activate', 'track', 'isFeatureEnabled'] }, 'feature-experimentation': { files: ['feature-flags.*', 'rollout.*', 'datafile.*'], directories: ['features/', 'flags/', 'rollouts/'], dependencies: ['@optimizely/sdk', '@optimizely/feature-experimentation'], content: ['createUserContext', 'decide', 'trackEvent', 'OptimizelyUserContext'] }, 'data-platform': { files: ['data-platform.*', 'analytics.*', 'tracking.*'], directories: ['analytics/', 'tracking/', 'data/'], dependencies: ['@optimizely/data-platform*', 'optimizely-analytics*'], content: ['DataPlatform', 'EventTracker', 'CustomerData'] }, 'connect-platform': { files: ['connect.*', 'integration.*', 'webhook.*'], directories: ['integrations/', 'webhooks/', 'connect/'], dependencies: ['@optimizely/connect*', 'optimizely-integration*'], content: ['ConnectPlatform', 'IntegrationService', 'WebhookHandler'] }, 'recommendations': { files: ['recommendations.*', 'recs.*'], directories: ['recommendations/', 'recs/'], dependencies: ['@optimizely/recommendations*', 'optimizely-recs*'], content: ['RecommendationEngine', 'ProductRecommendations'] } }; constructor(logger) { this.logger = logger; } async initialize() { if (this.isInitialized) { return; } this.logger.debug('Initializing Product Detection Service'); // No async initialization needed for Phase 1 // In future phases, this might load ML models or enhanced patterns this.isInitialized = true; this.logger.info('Product Detection Service initialized'); } async detectFromProject(projectPath) { const startTime = Date.now(); try { this.logger.debug('Starting project-based product detection', { projectPath }); const evidence = []; const productScores = new Map(); // Initialize scores for (const product of Object.keys(this.detectionPatterns)) { productScores.set(product, 0); } // Check if project path exists try { await fs.access(projectPath); } catch { throw new Error(`Project path does not exist: ${projectPath}`); } // Analyze project structure await this.analyzeProjectStructure(projectPath, evidence, productScores); // Analyze JS/TS package dependencies await this.analyzePackageDependencies(projectPath, evidence, productScores); // Analyze .NET project files for Optimizely/EPiServer/Commerce signals await this.analyzeDotnetDependencies(projectPath, evidence, productScores); // Calculate results const sortedProducts = Array.from(productScores.entries()) .filter(([, score]) => score > 0) .sort(([, a], [, b]) => b - a); const detectedProducts = sortedProducts.slice(0, 3).map(([product]) => product); const confidence = sortedProducts.length > 0 ? Math.min((sortedProducts[0]?.[1] ?? 0) / 10, 1.0) : 0; // Normalize to 0-1 const result = { products: detectedProducts, confidence: Math.min(confidence, 1.0), context: 'ide', evidence, suggestedActions: this.generateSuggestedActions(detectedProducts), timestamp: new Date() }; this.logger.info('Project-based detection completed', { detectedProducts, confidence: result.confidence, evidenceCount: evidence.length, processingTime: Date.now() - startTime }); return result; } catch (error) { this.logger.error('Project-based product detection failed', error, { projectPath, processingTime: Date.now() - startTime }); throw error; } } async detectFromPrompt(prompt, productHints = []) { const startTime = Date.now(); try { this.logger.debug('Starting prompt-based product detection', { promptLength: prompt.length, hintsCount: productHints.length }); const evidence = []; const productScores = new Map(); // Initialize scores with hints for (const product of Object.keys(this.detectionPatterns)) { productScores.set(product, productHints.includes(product) ? 5 : 0); } // Analyze prompt content this.analyzePromptContent(prompt, evidence, productScores); // Calculate results const sortedProducts = Array.from(productScores.entries()) .filter(([, score]) => score > 0) .sort(([, a], [, b]) => b - a); const detectedProducts = sortedProducts.slice(0, 2).map(([product]) => product); const confidence = sortedProducts.length > 0 ? Math.min((sortedProducts[0]?.[1] ?? 0) / 10, 1.0) : 0; const result = { products: detectedProducts, confidence, context: 'prompt', evidence, suggestedActions: this.generateSuggestedActions(detectedProducts), timestamp: new Date() }; this.logger.info('Prompt-based detection completed', { detectedProducts, confidence: result.confidence, evidenceCount: evidence.length, processingTime: Date.now() - startTime }); return result; } catch (error) { this.logger.error('Prompt-based product detection failed', error, { promptLength: prompt.length, processingTime: Date.now() - startTime }); throw error; } } async analyzeProjectStructure(projectPath, evidence, productScores) { try { const items = await fs.readdir(projectPath, { withFileTypes: true }); for (const item of items.slice(0, 50)) { // Limit for performance if (!item) continue; const itemPath = path.join(projectPath, item.name); if (item.isDirectory()) { await this.checkDirectoryPatterns(itemPath, item.name, evidence, productScores); } else { await this.checkFilePatterns(itemPath, item.name, evidence, productScores); } } } catch (error) { this.logger.warn('Error analyzing project structure'); } } async checkDirectoryPatterns(dirPath, dirName, evidence, productScores) { for (const [product, patterns] of Object.entries(this.detectionPatterns)) { for (const pattern of patterns.directories) { if (this.matchesPattern(dirName, pattern)) { evidence.push({ type: 'directory', path: dirPath, pattern, confidence: 0.7, weight: 3, description: `Directory matches ${product} pattern: ${pattern}` }); const currentScore = productScores.get(product) ?? 0; productScores.set(product, currentScore + 3); } } } } async checkFilePatterns(filePath, fileName, evidence, productScores) { for (const [product, patterns] of Object.entries(this.detectionPatterns)) { for (const pattern of patterns.files) { if (this.matchesPattern(fileName, pattern)) { evidence.push({ type: 'file', path: filePath, pattern, confidence: 0.8, weight: 4, description: `File matches ${product} pattern: ${pattern}` }); const currentScore = productScores.get(product) ?? 0; productScores.set(product, currentScore + 4); } } } } async analyzePackageDependencies(projectPath, evidence, productScores) { const packageJsonPath = path.join(projectPath, 'package.json'); try { const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies, ...packageJson.peerDependencies }; for (const [product, patterns] of Object.entries(this.detectionPatterns)) { for (const pattern of patterns.dependencies) { for (const [depName, depVersion] of Object.entries(dependencies)) { if (this.matchesPattern(depName, pattern)) { const versionInfo = typeof depVersion === 'string' ? depVersion : ''; const versionBoost = /\b(12|13)\b/.test(versionInfo) && product.includes('cms') ? 1 : 0; evidence.push({ type: 'dependency', path: packageJsonPath, pattern, confidence: 0.9, weight: 5 + versionBoost, description: `Dependency matches ${product} pattern: ${pattern} ${versionInfo ? `(version ${versionInfo})` : ''}` }); const currentScore = productScores.get(product) ?? 0; // Slightly boost if version present suggests major train (e.g., cms 12) productScores.set(product, currentScore + 5 + versionBoost); } } } } } catch { // No package.json or parsing error - not necessarily an issue this.logger.debug('No package.json found or parsing failed', { projectPath }); } } async analyzeDotnetDependencies(projectPath, evidence, productScores) { const filesToCheck = []; // Shallow recursive walk limited to improve perf const walk = async (dir, depth = 0) => { if (depth > 3) return; try { const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { const full = path.join(dir, item.name); if (item.isDirectory()) { await walk(full, depth + 1); } else if (/\.csproj$/i.test(item.name) || /Directory\.Packages\.props$/i.test(item.name) || /packages\.config$/i.test(item.name)) { filesToCheck.push(full); } } } catch (err) { /* noop */ } }; await walk(projectPath, 0); for (const file of filesToCheck) { try { const raw = await fs.readFile(file, 'utf-8'); const content = raw.toLowerCase(); const dotnetSignals = [ { product: 'cms-paas', pattern: /(episerver|optimizely)[\w.-]*cms/i, weight: 5, desc: 'Found CMS dependency', versionRegex: /Version\s*=\s*"([0-9][^"]+)"/i }, { product: 'configured-commerce', pattern: /(insite|optimizely)[\w.-]*commerce/i, weight: 5, desc: 'Found Commerce dependency', versionRegex: /Version\s*=\s*"([0-9][^"]+)"/i }, { product: 'dxp', pattern: /(optimizely|episerver).*dxp/i, weight: 3, desc: 'Found DXP hint', versionRegex: /Version\s*=\s*"([0-9][^"]+)"/i } ]; for (const signal of dotnetSignals) { if (signal.pattern.test(content)) { let versionTag = ''; const m = raw.match(signal.versionRegex || /Version\s*=\s*"([0-9][^"]+)"/i); if (m && m[1]) versionTag = m[1]; const versionBoost = versionTag ? 1 : 0; evidence.push({ type: 'dependency', path: file, pattern: signal.pattern.source, confidence: 0.9, weight: signal.weight + versionBoost, description: `${signal.desc} in ${path.basename(file)}${versionTag ? ` (version ${versionTag})` : ''}` }); const currentScore = productScores.get(signal.product) ?? 0; productScores.set(signal.product, currentScore + signal.weight + versionBoost); } } } catch (err) { /* noop */ } } } analyzePromptContent(prompt, evidence, productScores) { const lowerPrompt = prompt.toLowerCase(); for (const [product, patterns] of Object.entries(this.detectionPatterns)) { for (const term of patterns.content) { if (lowerPrompt.includes(term.toLowerCase())) { evidence.push({ type: 'content', path: 'prompt', pattern: term, confidence: 0.6, weight: 2, description: `Prompt contains ${product} term: ${term}` }); const currentScore = productScores.get(product) ?? 0; productScores.set(product, currentScore + 2); } } } } matchesPattern(text, pattern) { // Simple glob pattern matching const regexPattern = pattern .replace(/\*/g, '.*') .replace(/\?/g, '.'); return new RegExp(`^${regexPattern}$`, 'i').test(text); } generateSuggestedActions(detectedProducts) { const actions = []; if (detectedProducts.length === 0) { actions.push('Consider providing more specific context about your Optimizely implementation'); actions.push('Check if you are working with a supported Optimizely product'); return actions; } actions.push(`Focus on ${detectedProducts.map(p => p.replace('-', ' ')).join(', ')} specific documentation and patterns`); if (detectedProducts.includes('configured-commerce')) { actions.push('Review handler chain patterns and extension development guidelines'); } if (detectedProducts.includes('cms-paas') || detectedProducts.includes('cms-saas')) { actions.push('Check content type definitions and MVC implementation patterns'); } if (detectedProducts.includes('web-experimentation') || detectedProducts.includes('feature-experimentation')) { actions.push('Verify SDK implementation and event tracking setup'); } return actions; } isEnabled() { return this.isInitialized; } } //# sourceMappingURL=product-detection-service.js.map