import { type SourceFile, type ClassDeclaration, type MethodDeclaration, Scope, } from 'ts-morph'; /** * TypeScript error representation */ export interface TypeScriptError { code: string; message: string; file: string; line: number; column: number; } /** * Comprehensive transformation metrics for E2E validation */ export interface TransformationMetrics { before: { services: number; methods: number; responsibilities: Set; properties: number; tsErrors: TypeScriptError[]; eslintErrors: number; eslintWarnings: number; compilationSuccess: boolean; }; after: { focusedServices: number; stateServices: number; propertiesExtracted: number; importsAdded: number; crossServiceDependencies: number; tsErrors: TypeScriptError[]; eslintErrors: number; eslintWarnings: number; compilationSuccess: boolean; }; transformation: { duration: number; filesGenerated: number; filesModified: number; }; validation: { typeScriptPassed: boolean; eslintPassed: boolean; buildPassed: boolean; rollbackTriggered: boolean; }; } /** * Collects comprehensive metrics before/after transformation for E2E validation */ export class TransformationMetricsCollector { /** * Collect metrics from original service before transformation */ async collectBeforeMetrics( sourceFile: SourceFile, eslintResults?: { errors: number; warnings: number }, ): Promise { const classDecl = this.findServiceClass(sourceFile); if (!classDecl) { throw new Error('No service class found in source file'); } const methods = classDecl.getMethods().filter((m) => { const scope = m.getScope(); return scope !== Scope.Private; }); const properties = classDecl.getProperties(); const responsibilities = this.detectResponsibilities(classDecl, methods); const tsErrors = this.detectTypeScriptErrors(sourceFile); return { services: 1, methods: methods.length, responsibilities, properties: properties.length, tsErrors, eslintErrors: eslintResults?.errors ?? 0, eslintWarnings: eslintResults?.warnings ?? 0, compilationSuccess: tsErrors.length === 0, }; } /** * Collect metrics from generated services after transformation */ async collectAfterMetrics( _originalFile: SourceFile, generatedFiles: SourceFile[], eslintResults?: { errors: number; warnings: number }, ): Promise { const focusedServices = generatedFiles.filter( (f) => !f.getFilePath().includes('state.service.ts'), ).length; const stateServices = generatedFiles.filter((f) => f.getFilePath().includes('state.service.ts'), ).length; const propertiesExtracted = this.countExtractedProperties(generatedFiles); const importsAdded = this.countAddedImports(generatedFiles); const crossServiceDependencies = this.detectCrossServiceDependencies(generatedFiles); // Collect TS errors from all generated files const tsErrors: TypeScriptError[] = []; for (const file of generatedFiles) { tsErrors.push(...this.detectTypeScriptErrors(file)); } return { focusedServices, stateServices, propertiesExtracted, importsAdded, crossServiceDependencies, tsErrors, eslintErrors: eslintResults?.errors ?? 0, eslintWarnings: eslintResults?.warnings ?? 0, compilationSuccess: tsErrors.length === 0, }; } /** * Calculate improvement metrics */ calculateImprovement(metrics: TransformationMetrics): { errorReduction: string; eslintImprovement: string; zeroManualFixes: boolean; productionReady: boolean; } { const totalBeforeErrors = metrics.before.tsErrors.length + metrics.before.eslintErrors; const totalAfterErrors = metrics.after.tsErrors.length + metrics.after.eslintErrors; const errorReduction = totalBeforeErrors === 0 ? '0%' : `${((1 - totalAfterErrors / totalBeforeErrors) * 100).toFixed(1)}%`; const eslintImprovement = metrics.before.eslintErrors === 0 ? '0%' : `${((1 - metrics.after.eslintErrors / metrics.before.eslintErrors) * 100).toFixed(1)}%`; const zeroManualFixes = metrics.after.tsErrors.length === 0 && metrics.after.eslintErrors === 0; const fullValidationPassed = metrics.validation.typeScriptPassed && metrics.validation.eslintPassed && metrics.validation.buildPassed; const productionReady = metrics.after.compilationSuccess && zeroManualFixes && fullValidationPassed; return { errorReduction, eslintImprovement, zeroManualFixes, productionReady, }; } /** * Find the service class in source file */ private findServiceClass( sourceFile: SourceFile, ): ClassDeclaration | undefined { const classes = sourceFile.getClasses(); return ( classes.find((c) => { const decorators = c.getDecorators(); return decorators.some((d) => d.getName() === 'Injectable'); }) ?? classes[0] ); } /** * Detect responsibilities in service class */ private detectResponsibilities( classDecl: ClassDeclaration, methods: MethodDeclaration[], ): Set { const responsibilities = new Set(); // Pattern-based detection const patterns = { 'event-handling': [ /^on/i, /^handle/i, /^emit/i, /^trigger/i, /^notify/i, /^broadcast/i, /clicked$/i, /^toggle/i, /Saved$/i, /Changed$/i, ], 'data-retrieval': [ /^get/i, /^fetch/i, /^load/i, /^read/i, /^query/i, /^find/i, ], 'data-mutation': [ /^save/i, /^update/i, /^delete/i, /^write/i, /^create/i, /^remove/i, /^set/i, /^modify/i, ], validation: [/^validate/i, /^check/i, /^verify/i, /^ensure/i, /^is/i], transformation: [ /^transform/i, /^convert/i, /^map/i, /^parse/i, /^decode/i, /AsBoolean$/i, /AsNumber$/i, ], 'cache-management': [ /cache/i, /^clear/i, /^reset/i, /^invalidate/i, /^cleanup/i, ], }; for (const method of methods) { const methodName = method.getName(); for (const [responsibility, regexes] of Object.entries(patterns)) { if (regexes.some((regex) => regex.test(methodName))) { responsibilities.add(responsibility); } } } // Property-based detection (RxJS) const properties = classDecl.getProperties(); let rxjsPropertyCount = 0; for (const prop of properties) { const type = prop.getType().getText(); if ( type.includes('Subject') || type.includes('Observable') || type.includes('EventEmitter') ) { rxjsPropertyCount++; } } if (rxjsPropertyCount >= 3) { responsibilities.add('event-handling'); } return responsibilities; } /** * Detect TypeScript errors in source file */ private detectTypeScriptErrors(sourceFile: SourceFile): TypeScriptError[] { const errors: TypeScriptError[] = []; const diagnostics = sourceFile.getPreEmitDiagnostics(); for (const diagnostic of diagnostics) { const message = diagnostic.getMessageText(); const messageStr = typeof message === 'string' ? message : message.getMessageText(); const sourceFile = diagnostic.getSourceFile(); const start = diagnostic.getStart(); if (sourceFile && start !== undefined) { const { line, column } = sourceFile.getLineAndColumnAtPos(start); errors.push({ code: `TS${diagnostic.getCode()}`, message: messageStr, file: sourceFile.getFilePath(), line, column, }); } } return errors; } /** * Count properties extracted to focused services */ private countExtractedProperties(generatedFiles: SourceFile[]): number { let count = 0; for (const file of generatedFiles) { const classDecl = this.findServiceClass(file); if (classDecl) { count += classDecl.getProperties().length; } } return count; } /** * Count imports added to generated services */ private countAddedImports(generatedFiles: SourceFile[]): number { let count = 0; for (const file of generatedFiles) { const imports = file.getImportDeclarations(); count += imports.length; } return count; } /** * Detect cross-service dependencies in generated files */ private detectCrossServiceDependencies(generatedFiles: SourceFile[]): number { let count = 0; for (const file of generatedFiles) { const classDecl = this.findServiceClass(file); if (!classDecl) { continue; } const constructor = classDecl.getConstructors()[0]; if (!constructor) { continue; } const params = constructor.getParameters(); for (const param of params) { const type = param.getType().getText(); // Check if parameter is another generated service if ( generatedFiles.some((f) => { const otherClass = this.findServiceClass(f); return otherClass && type.includes(otherClass.getName() ?? ''); }) ) { count++; } } } return count; } /** * Format metrics as human-readable report */ formatMetricsReport(metrics: TransformationMetrics): string { const improvement = this.calculateImprovement(metrics); const lines: string[] = []; lines.push('='.repeat(80)); lines.push('TRANSFORMATION METRICS REPORT'); lines.push('='.repeat(80)); lines.push(''); lines.push('BEFORE TRANSFORMATION:'); lines.push(` Services analyzed: ${metrics.before.services}`); lines.push(` Total methods: ${metrics.before.methods}`); lines.push(` Responsibilities: ${metrics.before.responsibilities.size}`); lines.push(` Properties: ${metrics.before.properties}`); lines.push(` TypeScript errors: ${metrics.before.tsErrors.length}`); lines.push(` ESLint errors: ${metrics.before.eslintErrors}`); lines.push(` ESLint warnings: ${metrics.before.eslintWarnings}`); lines.push( ` Compilation success: ${metrics.before.compilationSuccess ? '✅' : '❌'}`, ); lines.push(''); lines.push('AFTER TRANSFORMATION:'); lines.push(` Focused services created: ${metrics.after.focusedServices}`); lines.push(` State services created: ${metrics.after.stateServices}`); lines.push(` Properties extracted: ${metrics.after.propertiesExtracted}`); lines.push(` Imports added: ${metrics.after.importsAdded}`); lines.push( ` Cross-service dependencies: ${metrics.after.crossServiceDependencies}`, ); lines.push(` TypeScript errors: ${metrics.after.tsErrors.length}`); lines.push(` ESLint errors: ${metrics.after.eslintErrors}`); lines.push(` ESLint warnings: ${metrics.after.eslintWarnings}`); lines.push( ` Compilation success: ${metrics.after.compilationSuccess ? '✅' : '❌'}`, ); lines.push(''); lines.push('TRANSFORMATION:'); lines.push(` Duration: ${metrics.transformation.duration}ms`); lines.push(` Files generated: ${metrics.transformation.filesGenerated}`); lines.push(` Files modified: ${metrics.transformation.filesModified}`); lines.push(''); lines.push('VALIDATION:'); lines.push( ` TypeScript passed: ${metrics.validation.typeScriptPassed ? '✅' : '❌'}`, ); lines.push( ` ESLint passed: ${metrics.validation.eslintPassed ? '✅' : '❌'}`, ); lines.push( ` Build passed: ${metrics.validation.buildPassed ? '✅' : '❌'}`, ); lines.push( ` Rollback triggered: ${metrics.validation.rollbackTriggered ? '❌' : '✅'}`, ); lines.push(''); lines.push('IMPROVEMENT:'); lines.push(` Total error reduction: ${improvement.errorReduction}`); lines.push(` ESLint improvement: ${improvement.eslintImprovement}`); lines.push( ` Zero manual fixes: ${improvement.zeroManualFixes ? '✅' : '❌'}`, ); lines.push( ` Production ready: ${improvement.productionReady ? '✅' : '❌'}`, ); lines.push(''); lines.push('='.repeat(80)); if (improvement.productionReady) { lines.push('✅ SUCCESS: Transformation achieves zero manual fixes!'); } else { lines.push( '❌ FAILURE: Manual fixes required or compilation errors detected', ); } lines.push('='.repeat(80)); return lines.join('\n'); } }