UNPKG

aetherlight-analyzer

Version:

Code analysis tool to generate ÆtherLight sprint plans from any codebase

389 lines 16.1 kB
"use strict"; /** * ValidationConfigGenerator * * DESIGN DECISION: Auto-generate project-specific validation.toml configuration * WHY: Validators need configuration to be effective, but manual config is error-prone * * REASONING CHAIN: * 1. Analyze project structure (type, packages, dependencies) * 2. Detect potential issues (native deps, runtime npm deps, version mismatches) * 3. Generate intelligent defaults based on analysis * 4. Communicate findings to user * 5. Save configuration after confirmation * 6. Result: Accurate, project-specific validation rules * * Pattern: Pattern-ANALYZER-001 (Auto-Configuration) * Related: VAL-002, VAL-007, SYNC-001 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ValidationConfigGenerator = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); /** * Native dependency patterns (packages that require native compilation) */ const NATIVE_DEP_PATTERNS = [ '@nut-tree-fork/nut-js', 'robotjs', 'node-hid', 'serialport', 'usb', 'ffi-napi', 'ref-napi', 'node-gyp', 'bindings', 'prebuild' ]; /** * Runtime npm dependencies that are problematic for VS Code extensions * (because vsce package --no-dependencies excludes them) */ const RUNTIME_NPM_DEP_PATTERNS = [ 'glob', 'fast-glob', 'lodash', 'underscore', 'moment', 'date-fns', 'axios', 'got', 'chalk', 'colors' ]; /** * Allowed exceptions (packages that ARE allowed as runtime dependencies) */ const ALLOWED_EXCEPTIONS = [ '@iarna/toml', 'node-fetch', 'ws', 'form-data', // Sub-packages are always allowed 'aetherlight-analyzer', 'aetherlight-sdk', 'aetherlight-node' ]; class ValidationConfigGenerator { /** * Detect project type by analyzing package.json and directory structure */ detectProjectType(projectRoot) { const packageJsonPath = path.join(projectRoot, 'package.json'); // Check for monorepo first (packages/ or apps/ directory) const packagesDir = path.join(projectRoot, 'packages'); const appsDir = path.join(projectRoot, 'apps'); if (fs.existsSync(packagesDir) || fs.existsSync(appsDir)) { const hasPackages = fs.existsSync(packagesDir) && fs.readdirSync(packagesDir).some(dir => { const pkgPath = path.join(packagesDir, dir, 'package.json'); return fs.existsSync(pkgPath); }); const hasApps = fs.existsSync(appsDir) && fs.readdirSync(appsDir).some(dir => { const pkgPath = path.join(appsDir, dir, 'package.json'); return fs.existsSync(pkgPath); }); if (hasPackages || hasApps) { return 'monorepo'; } } // Check package.json if (!fs.existsSync(packageJsonPath)) { return 'application'; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); // VS Code extension if (packageJson.engines?.vscode || packageJson.contributes) { return 'vscode-extension'; } // Library (has types and main) if (packageJson.types && packageJson.main) { return 'library'; } return 'application'; } /** * Detect package structure (single package or monorepo with multiple packages) */ detectPackageStructure(projectRoot) { const packages = []; // Check packages/ directory const packagesDir = path.join(projectRoot, 'packages'); if (fs.existsSync(packagesDir)) { const dirs = fs.readdirSync(packagesDir); for (const dir of dirs) { const pkgPath = path.join(packagesDir, dir, 'package.json'); if (fs.existsSync(pkgPath)) { packages.push(`packages/${dir}`); } } } // Check apps/ directory const appsDir = path.join(projectRoot, 'apps'); if (fs.existsSync(appsDir)) { const dirs = fs.readdirSync(appsDir); for (const dir of dirs) { const pkgPath = path.join(appsDir, dir, 'package.json'); if (fs.existsSync(pkgPath)) { packages.push(`apps/${dir}`); } } } // If no packages found, it's a single package if (packages.length === 0) { return { type: 'single', packages: ['.'] }; } return { type: 'monorepo', packages }; } /** * Detect potential issues in the project */ detectPotentialIssues(projectRoot) { const issues = []; const projectType = this.detectProjectType(projectRoot); const packageStructure = this.detectPackageStructure(projectRoot); // Check each package for issues const packagesToCheck = packageStructure.type === 'single' ? [projectRoot] : packageStructure.packages.map(p => path.join(projectRoot, p)); for (const pkgPath of packagesToCheck) { const packageJsonPath = path.join(pkgPath, 'package.json'); if (!fs.existsSync(packageJsonPath)) continue; const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Check for native dependencies const nativeDeps = Object.keys(deps).filter(dep => NATIVE_DEP_PATTERNS.some(pattern => dep.includes(pattern))); if (nativeDeps.length > 0) { issues.push({ type: 'native_dependencies', severity: 'critical', message: `Native dependencies found: ${nativeDeps.join(', ')}`, packages: nativeDeps, suggestion: 'Replace with VS Code APIs or pure JavaScript alternatives' }); } // Check for runtime npm dependencies in VS Code extensions if (projectType === 'vscode-extension') { const runtimeDeps = Object.keys(packageJson.dependencies || {}).filter(dep => RUNTIME_NPM_DEP_PATTERNS.some(pattern => dep.includes(pattern)) && !ALLOWED_EXCEPTIONS.some(allowed => dep.includes(allowed))); if (runtimeDeps.length > 0) { issues.push({ type: 'runtime_npm_dependencies', severity: 'critical', message: `Runtime npm dependencies found: ${runtimeDeps.join(', ')}`, packages: runtimeDeps, suggestion: 'Replace with Node.js built-in APIs (fs, path, etc.)' }); } } // Check for large bundle size (many dependencies) const depCount = Object.keys(deps).length; if (depCount > 30) { issues.push({ type: 'large_bundle', severity: 'warning', message: `Large number of dependencies: ${depCount}`, suggestion: 'Consider reducing dependencies to improve package size' }); } // Check for missing tests const testDirs = ['test', 'tests', '__tests__', 'src/test', 'src/tests']; const hasTests = testDirs.some(dir => fs.existsSync(path.join(pkgPath, dir))); if (!hasTests) { issues.push({ type: 'missing_tests', severity: 'warning', message: 'No test directory found', suggestion: 'Add tests following Pattern-TDD-001' }); } } // Check for version mismatches in monorepo if (packageStructure.type === 'monorepo') { const versions = new Set(); for (const pkg of packageStructure.packages) { const pkgPath = path.join(projectRoot, pkg, 'package.json'); if (fs.existsSync(pkgPath)) { const packageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); versions.add(packageJson.version); } } if (versions.size > 1) { issues.push({ type: 'version_mismatch', severity: 'critical', message: `Version mismatch in monorepo: ${Array.from(versions).join(', ')}`, suggestion: 'All packages should have the same version' }); } } return issues; } /** * Generate validation config based on project analysis */ generateConfig(projectRoot) { const projectType = this.detectProjectType(projectRoot); const packageStructure = this.detectPackageStructure(projectRoot); const packageJsonPath = path.join(projectRoot, 'package.json'); // Get allowed exceptions from existing dependencies const allowedExceptions = [...ALLOWED_EXCEPTIONS]; if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); const deps = Object.keys(packageJson.dependencies || {}); // Add current dependencies to allowed exceptions allowedExceptions.push(...deps.filter(dep => ALLOWED_EXCEPTIONS.some(allowed => dep.includes(allowed)))); } const config = { validation: { enabled: true, dependencies: { allow_native_dependencies: projectType !== 'vscode-extension', allow_runtime_npm_dependencies: projectType !== 'vscode-extension', allowed_exceptions: [...new Set(allowedExceptions)] // Remove duplicates }, test_coverage: { infrastructure_min: 0.90, api_min: 0.85, ui_min: 0.70 } } }; // Add version sync for monorepos if (packageStructure.type === 'monorepo') { config.validation.version_sync = { mode: 'auto-discover', packages: packageStructure.packages, require_exact_match: true }; } // Add package size limits for VS Code extensions if (projectType === 'vscode-extension') { config.validation.package_size = { max_size_mb: 5, warn_size_mb: 2 }; } return config; } /** * Save config to .aetherlight/validation.toml */ async saveConfig(projectRoot, config, options = {}) { const aetherlightDir = path.join(projectRoot, '.aetherlight'); const configPath = path.join(aetherlightDir, 'validation.toml'); // Check if config already exists if (fs.existsSync(configPath) && !options.force) { throw new Error('Config already exists. Use force: true to overwrite.'); } // Create .aetherlight directory if missing if (!fs.existsSync(aetherlightDir)) { fs.mkdirSync(aetherlightDir, { recursive: true }); } // Convert config to TOML format const toml = this.configToToml(config); // Write file fs.writeFileSync(configPath, toml, 'utf-8'); } /** * Convert config object to TOML string */ configToToml(config) { let toml = '# Generated by ÆtherLight Code Analyzer\n'; toml += '# Pattern: Pattern-ANALYZER-001 (Auto-Configuration)\n\n'; toml += '[validation]\n'; toml += `enabled = ${config.validation.enabled}\n\n`; toml += '[validation.dependencies]\n'; toml += `# Detected: ${config.validation.dependencies.allow_native_dependencies ? 'Native deps allowed' : 'Native deps not allowed (VS Code extension)'}\n`; toml += `allow_native_dependencies = ${config.validation.dependencies.allow_native_dependencies}\n`; toml += `allow_runtime_npm_dependencies = ${config.validation.dependencies.allow_runtime_npm_dependencies}\n\n`; toml += '# Auto-whitelisted (detected in use):\n'; toml += `allowed_exceptions = ${JSON.stringify(config.validation.dependencies.allowed_exceptions)}\n\n`; if (config.validation.version_sync) { toml += '[validation.version_sync]\n'; toml += `# Detected: Monorepo with ${config.validation.version_sync.packages.length} packages\n`; toml += `mode = "${config.validation.version_sync.mode}"\n`; toml += `packages = ${JSON.stringify(config.validation.version_sync.packages)}\n`; toml += `require_exact_match = ${config.validation.version_sync.require_exact_match}\n\n`; } if (config.validation.test_coverage) { toml += '[validation.test_coverage]\n'; toml += `infrastructure_min = ${config.validation.test_coverage.infrastructure_min}\n`; toml += `api_min = ${config.validation.test_coverage.api_min}\n`; toml += `ui_min = ${config.validation.test_coverage.ui_min}\n\n`; } if (config.validation.package_size) { toml += '[validation.package_size]\n'; toml += `max_size_mb = ${config.validation.package_size.max_size_mb}\n`; toml += `warn_size_mb = ${config.validation.package_size.warn_size_mb}\n`; } return toml; } /** * Full analysis and config generation workflow */ async analyzeAndGenerateConfig(projectRoot, options = {}) { // 1. ANALYZE const projectType = this.detectProjectType(projectRoot); const packageStructure = this.detectPackageStructure(projectRoot); const issues = this.detectPotentialIssues(projectRoot); // 2. GENERATE const config = this.generateConfig(projectRoot); // 3. SAVE (if autoSave enabled) let configSaved = false; if (options.autoSave) { await this.saveConfig(projectRoot, config, { force: true }); configSaved = true; } return { projectType, packageStructure, issues, config, configSaved }; } } exports.ValidationConfigGenerator = ValidationConfigGenerator; //# sourceMappingURL=ValidationConfigGenerator.js.map