optivise
Version:
Optivise - The Ultimate Optimizely Development Assistant with AI-powered features, zero-config setup, and comprehensive development support
383 lines • 18.6 kB
JavaScript
/**
* 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