UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

1,562 lines (1,346 loc) 45 kB
#!/usr/bin/env node /** * AIOS Pattern Extractor * Story 7.3: Extracts and documents code patterns from the codebase * * Analyzes code via AST and regex to detect common patterns, * generating a patterns.md file for reference by agents (especially Spec Writer). * * Usage: * node pattern-extractor.js [command] [options] * * Commands: * extract Extract patterns (default) * json Output as JSON * save Save to .aios/patterns.md * merge Merge with existing patterns * * Options: * --root <path> Project root (default: cwd) * --output <path> Custom output path * --category <c> Extract specific category only * --quiet Suppress output * --help Show this help message * * @module pattern-extractor */ const fs = require('fs').promises; const path = require('path'); // Pattern categories const PATTERN_CATEGORIES = { STATE_MANAGEMENT: 'State Management', API_CALLS: 'API Calls', ERROR_HANDLING: 'Error Handling', COMPONENTS: 'Components', DATA_ACCESS: 'Data Access', TESTING: 'Testing', HOOKS: 'Hooks', UTILITIES: 'Utilities', }; // File extensions to analyze const CODE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs']; // Directories to exclude from scanning const EXCLUDED_DIRS = [ 'node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.nuxt', 'tmp', 'temp', '__pycache__', '.aios/migration-backup', ]; /** * Pattern Extractor Class * Scans codebase and extracts common patterns */ class PatternExtractor { constructor(rootPath, options = {}) { this.rootPath = rootPath || process.cwd(); this.options = options; this.patterns = new Map(); this.fileCache = new Map(); this.detectedPatterns = []; } // ============================================================================= // File Scanning // ============================================================================= /** * Scan files with specific extensions * @param {string[]} extensions - File extensions to scan * @returns {Promise<string[]>} List of file paths */ async scanFiles(extensions = CODE_EXTENSIONS) { const files = []; await this._walkDirectory(this.rootPath, files, extensions); return files; } /** * Walk directory recursively * @private */ async _walkDirectory(dir, files, extensions) { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); // Skip excluded directories if (entry.isDirectory()) { if (EXCLUDED_DIRS.includes(entry.name) || entry.name.startsWith('.')) { continue; } await this._walkDirectory(fullPath, files, extensions); } else if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); if (extensions.includes(ext)) { files.push(fullPath); } } } } catch (error) { // Directory not accessible } } /** * Read and cache file content * @param {string} filePath - Path to file * @returns {Promise<string>} File content */ async readFile(filePath) { if (this.fileCache.has(filePath)) { return this.fileCache.get(filePath); } try { const content = await fs.readFile(filePath, 'utf-8'); this.fileCache.set(filePath, content); return content; } catch (error) { return null; } } // ============================================================================= // Pattern Detection // ============================================================================= /** * Detect all patterns in the codebase * @returns {Promise<Object>} Detected patterns */ async detectPatterns() { const files = await this.scanFiles(); const allPatterns = {}; // Initialize categories for (const category of Object.values(PATTERN_CATEGORIES)) { allPatterns[category] = []; } // Run all detection methods const detectors = [ this.detectStatePatterns.bind(this), this.detectAPIPatterns.bind(this), this.detectErrorHandlingPatterns.bind(this), this.detectComponentPatterns.bind(this), this.detectHookPatterns.bind(this), this.detectDataAccessPatterns.bind(this), this.detectTestingPatterns.bind(this), this.detectUtilityPatterns.bind(this), ]; for (const detector of detectors) { const patterns = await detector(files); for (const pattern of patterns) { if (allPatterns[pattern.category]) { // Check for duplicates const exists = allPatterns[pattern.category].some( (p) => p.name === pattern.name ); if (!exists) { allPatterns[pattern.category].push(pattern); } } } } this.detectedPatterns = allPatterns; return allPatterns; } /** * Detect state management patterns (Zustand, Redux, Context) */ async detectStatePatterns(files) { const patterns = []; const storeFiles = files.filter((f) => f.includes('store') || f.includes('Store')); for (const file of storeFiles) { const content = await this.readFile(file); if (!content) continue; // Zustand with persist middleware if (content.includes('create<') && content.includes('zustand') && content.includes('persist')) { const pattern = this._extractZustandPersistPattern(content, file); if (pattern) patterns.push(pattern); } // Zustand without persist else if (content.includes('create<') && content.includes('zustand')) { const pattern = this._extractZustandPattern(content, file); if (pattern) patterns.push(pattern); } // Redux Toolkit slice if (content.includes('createSlice') && content.includes('@reduxjs/toolkit')) { const pattern = this._extractReduxSlicePattern(content, file); if (pattern) patterns.push(pattern); } // React Context if (content.includes('createContext') && content.includes('useContext')) { const pattern = this._extractContextPattern(content, file); if (pattern) patterns.push(pattern); } } return patterns; } /** * Extract Zustand with persist pattern */ _extractZustandPersistPattern(content, file) { const relativePath = path.relative(this.rootPath, file); const filesUsing = [relativePath]; // Extract interface name const interfaceMatch = content.match(/interface\s+(\w+State)\s*\{/); const interfaceName = interfaceMatch ? interfaceMatch[1] : 'ExampleState'; // Extract store name const storeMatch = content.match(/export\s+const\s+(use\w+Store)\s*=/); const storeName = storeMatch ? storeMatch[1] : 'useExampleStore'; // Extract persist name const persistMatch = content.match(/name:\s*['"`]([^'"`]+)['"`]/); const persistName = persistMatch ? persistMatch[1] : 'example-storage'; return { category: PATTERN_CATEGORIES.STATE_MANAGEMENT, name: 'Zustand Store with Persist', description: 'State management with persistence across browser sessions using Zustand and persist middleware.', whenToUse: 'Any domain state that needs persistence across sessions (settings, preferences, cached data).', example: this._generateZustandPersistExample(interfaceName, storeName, persistName), filesUsing, confidence: 0.95, }; } /** * Generate Zustand persist example */ _generateZustandPersistExample(interfaceName, storeName, persistName) { return `\`\`\`typescript import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface ${interfaceName} { data: Data | null; loading: boolean; error: string | null; fetchData: () => Promise<void>; reset: () => void; } export const ${storeName} = create<${interfaceName}>()( persist( (set, get) => ({ data: null, loading: false, error: null, fetchData: async () => { set({ loading: true, error: null }); try { const data = await api.get('/example'); set({ data, loading: false }); } catch (error) { set({ error: error.message, loading: false }); } }, reset: () => set({ data: null, loading: false, error: null }), }), { name: '${persistName}' } ) ); \`\`\``; } /** * Extract Zustand pattern (without persist) */ _extractZustandPattern(content, file) { const relativePath = path.relative(this.rootPath, file); return { category: PATTERN_CATEGORIES.STATE_MANAGEMENT, name: 'Zustand Store', description: 'Lightweight state management with Zustand.', whenToUse: 'Client-side state that does not need persistence (UI state, temporary data).', example: `\`\`\`typescript import { create } from 'zustand'; interface UIState { isOpen: boolean; toggle: () => void; } export const useUIStore = create<UIState>()((set) => ({ isOpen: false, toggle: () => set((state) => ({ isOpen: !state.isOpen })), })); \`\`\``, filesUsing: [relativePath], confidence: 0.90, }; } /** * Extract Redux slice pattern */ _extractReduxSlicePattern(content, file) { const relativePath = path.relative(this.rootPath, file); return { category: PATTERN_CATEGORIES.STATE_MANAGEMENT, name: 'Redux Toolkit Slice', description: 'Redux state management using Redux Toolkit createSlice.', whenToUse: 'Complex application state with DevTools support and time-travel debugging needs.', example: `\`\`\`typescript import { createSlice, PayloadAction } from '@reduxjs/toolkit'; interface CounterState { value: number; } const initialState: CounterState = { value: 0 }; export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload; }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export default counterSlice.reducer; \`\`\``, filesUsing: [relativePath], confidence: 0.90, }; } /** * Extract React Context pattern */ _extractContextPattern(content, file) { const relativePath = path.relative(this.rootPath, file); return { category: PATTERN_CATEGORIES.STATE_MANAGEMENT, name: 'React Context Pattern', description: 'Native React state management using Context API.', whenToUse: 'Shared state across component tree without external libraries (theme, auth, locale).', example: `\`\`\`typescript import { createContext, useContext, useState, ReactNode } from 'react'; interface ThemeContextType { theme: 'light' | 'dark'; toggleTheme: () => void; } const ThemeContext = createContext<ThemeContextType | undefined>(undefined); export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<'light' | 'dark'>('light'); const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light'); return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } export function useTheme() { const context = useContext(ThemeContext); if (!context) throw new Error('useTheme must be used within ThemeProvider'); return context; } \`\`\``, filesUsing: [relativePath], confidence: 0.85, }; } /** * Detect API call patterns */ async detectAPIPatterns(files) { const patterns = []; const seenPatterns = new Set(); for (const file of files) { const content = await this.readFile(file); if (!content) continue; const relativePath = path.relative(this.rootPath, file); // SWR Pattern if (content.includes('useSWR') && !seenPatterns.has('swr')) { seenPatterns.add('swr'); patterns.push({ category: PATTERN_CATEGORIES.API_CALLS, name: 'SWR Data Fetching', description: 'Data fetching with automatic caching, revalidation, and optimistic updates.', whenToUse: 'Client-side data fetching with automatic cache management and real-time sync.', example: `\`\`\`typescript import useSWR from 'swr'; const fetcher = (url: string) => fetch(url).then(res => res.json()); export function useData(id: string) { const { data, error, isLoading, mutate } = useSWR<DataType>( id ? \`/api/data/\${id}\` : null, fetcher, { refreshInterval: 5000, revalidateOnFocus: true, dedupingInterval: 2000, } ); return { data, isLoading, isError: error, refresh: mutate, }; } \`\`\``, filesUsing: [relativePath], confidence: 0.95, }); } // Fetch with error handling if (content.includes('fetch(') && content.includes('try') && content.includes('catch') && !seenPatterns.has('fetch-error')) { seenPatterns.add('fetch-error'); patterns.push({ category: PATTERN_CATEGORIES.API_CALLS, name: 'Fetch with Error Handling', description: 'Standard fetch API wrapper with proper error handling.', whenToUse: 'Simple API calls without external libraries.', example: `\`\`\`typescript async function fetchData<T>(url: string, options?: RequestInit): Promise<T> { try { const response = await fetch(url, { headers: { 'Content-Type': 'application/json', }, ...options, }); if (!response.ok) { throw new Error(\`HTTP error! status: \${response.status}\`); } return await response.json(); } catch (error) { console.error(\`Error fetching \${url}:\`, error); throw error; } } \`\`\``, filesUsing: [relativePath], confidence: 0.85, }); } // React Query / TanStack Query if ((content.includes('useQuery') || content.includes('@tanstack/react-query')) && !seenPatterns.has('react-query')) { seenPatterns.add('react-query'); patterns.push({ category: PATTERN_CATEGORIES.API_CALLS, name: 'TanStack Query', description: 'Powerful data synchronization with React Query.', whenToUse: 'Complex data fetching with caching, background updates, and mutations.', example: `\`\`\`typescript import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; export function useItems() { return useQuery({ queryKey: ['items'], queryFn: () => fetch('/api/items').then(res => res.json()), staleTime: 5 * 60 * 1000, // 5 minutes }); } export function useCreateItem() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (newItem: NewItem) => fetch('/api/items', { method: 'POST', body: JSON.stringify(newItem), }).then(res => res.json()), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['items'] }); }, }); } \`\`\``, filesUsing: [relativePath], confidence: 0.90, }); } } return patterns; } /** * Detect error handling patterns */ async detectErrorHandlingPatterns(files) { const patterns = []; const seenPatterns = new Set(); for (const file of files) { const content = await this.readFile(file); if (!content) continue; const relativePath = path.relative(this.rootPath, file); // Try-catch with context if (content.match(/catch\s*\(\s*error\s*\)\s*\{[^}]*console\.error\s*\(/)) { if (!seenPatterns.has('try-catch-context')) { seenPatterns.add('try-catch-context'); patterns.push({ category: PATTERN_CATEGORIES.ERROR_HANDLING, name: 'Try-Catch with Context', description: 'Error handling with contextual logging and re-throwing.', whenToUse: 'Any async operation that needs proper error tracking.', example: `\`\`\`typescript async function operation(params: Params): Promise<Result> { try { const result = await performOperation(params); return result; } catch (error) { console.error(\`Error in operation [\${params.id}]:\`, error); throw new Error(\`Failed to perform operation: \${error.message}\`); } } \`\`\``, filesUsing: [relativePath], confidence: 0.90, }); } } // Error boundary pattern (React) if (content.includes('componentDidCatch') || content.includes('ErrorBoundary')) { if (!seenPatterns.has('error-boundary')) { seenPatterns.add('error-boundary'); patterns.push({ category: PATTERN_CATEGORIES.ERROR_HANDLING, name: 'React Error Boundary', description: 'Catch and handle rendering errors in React component tree.', whenToUse: 'Wrap component subtrees to prevent entire app crashes.', example: `\`\`\`typescript 'use client'; import { Component, ReactNode } from 'react'; interface Props { children: ReactNode; fallback?: ReactNode; } interface State { hasError: boolean; error?: Error; } export class ErrorBoundary extends Component<Props, State> { state: State = { hasError: false }; static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; } componentDidCatch(error: Error, info: React.ErrorInfo) { console.error('ErrorBoundary caught:', error, info); } render() { if (this.state.hasError) { return this.props.fallback || <div>Something went wrong.</div>; } return this.props.children; } } \`\`\``, filesUsing: [relativePath], confidence: 0.85, }); } } // Toast notifications for errors if (content.includes('toast.error') || content.includes('toast(') && content.includes('error')) { if (!seenPatterns.has('toast-error')) { seenPatterns.add('toast-error'); patterns.push({ category: PATTERN_CATEGORIES.ERROR_HANDLING, name: 'Toast Error Notifications', description: 'User-friendly error notifications using toast library.', whenToUse: 'Display errors to users in a non-intrusive way.', example: `\`\`\`typescript import { toast } from 'sonner'; // or react-hot-toast async function handleSubmit(data: FormData) { try { await submitForm(data); toast.success('Form submitted successfully!'); } catch (error) { toast.error(\`Failed to submit: \${error.message}\`); } } \`\`\``, filesUsing: [relativePath], confidence: 0.85, }); } } } return patterns; } /** * Detect React component patterns */ async detectComponentPatterns(files) { const patterns = []; const seenPatterns = new Set(); const componentFiles = files.filter((f) => f.endsWith('.tsx') || f.endsWith('.jsx')); for (const file of componentFiles) { const content = await this.readFile(file); if (!content) continue; const relativePath = path.relative(this.rootPath, file); // Memoized component pattern if (content.includes('memo(function') || content.includes('memo(')) { if (!seenPatterns.has('memo-component')) { seenPatterns.add('memo-component'); patterns.push({ category: PATTERN_CATEGORIES.COMPONENTS, name: 'Memoized Component', description: 'Performance-optimized component using React.memo.', whenToUse: 'Components that receive the same props frequently and are expensive to render.', example: `\`\`\`typescript import { memo } from 'react'; interface CardProps { title: string; description: string; onClick?: () => void; } export const Card = memo(function Card({ title, description, onClick }: CardProps) { return ( <div className="card" onClick={onClick}> <h3>{title}</h3> <p>{description}</p> </div> ); }); \`\`\``, filesUsing: [relativePath], confidence: 0.90, }); } } // Compound component pattern if (content.match(/\w+\.\w+\s*=\s*function|\w+\.\w+\s*=\s*\(/)) { if (!seenPatterns.has('compound-component')) { seenPatterns.add('compound-component'); patterns.push({ category: PATTERN_CATEGORIES.COMPONENTS, name: 'Compound Component', description: 'Component with attached sub-components for flexible composition.', whenToUse: 'Complex UI components that need flexible internal composition (Card, Menu, Dialog).', example: `\`\`\`typescript interface CardProps { children: ReactNode; } function Card({ children }: CardProps) { return <div className="card">{children}</div>; } Card.Header = function CardHeader({ children }: { children: ReactNode }) { return <div className="card-header">{children}</div>; }; Card.Body = function CardBody({ children }: { children: ReactNode }) { return <div className="card-body">{children}</div>; }; Card.Footer = function CardFooter({ children }: { children: ReactNode }) { return <div className="card-footer">{children}</div>; }; export { Card }; // Usage: // <Card> // <Card.Header>Title</Card.Header> // <Card.Body>Content</Card.Body> // <Card.Footer>Actions</Card.Footer> // </Card> \`\`\``, filesUsing: [relativePath], confidence: 0.80, }); } } // Conditional rendering with cn/clsx if (content.includes('cn(') || content.includes('clsx(')) { if (!seenPatterns.has('conditional-classnames')) { seenPatterns.add('conditional-classnames'); patterns.push({ category: PATTERN_CATEGORIES.COMPONENTS, name: 'Conditional Class Names', description: 'Utility for conditional class name composition.', whenToUse: 'Dynamic styling based on props or state.', example: `\`\`\`typescript import { cn } from '@/lib/utils'; interface ButtonProps { variant?: 'primary' | 'secondary' | 'danger'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; children: ReactNode; } export function Button({ variant = 'primary', size = 'md', disabled, children }: ButtonProps) { return ( <button disabled={disabled} className={cn( 'rounded font-medium transition-colors', variant === 'primary' && 'bg-blue-500 text-white hover:bg-blue-600', variant === 'secondary' && 'bg-gray-200 text-gray-800 hover:bg-gray-300', variant === 'danger' && 'bg-red-500 text-white hover:bg-red-600', size === 'sm' && 'px-2 py-1 text-sm', size === 'md' && 'px-4 py-2', size === 'lg' && 'px-6 py-3 text-lg', disabled && 'opacity-50 cursor-not-allowed' )} > {children} </button> ); } \`\`\``, filesUsing: [relativePath], confidence: 0.95, }); } } } return patterns; } /** * Detect React hook patterns */ async detectHookPatterns(files) { const patterns = []; const seenPatterns = new Set(); const hookFiles = files.filter((f) => f.includes('/hooks/') || f.includes('use')); for (const file of hookFiles) { const content = await this.readFile(file); if (!content) continue; const relativePath = path.relative(this.rootPath, file); // Custom hook with store if (content.match(/export\s+function\s+use\w+\s*\(/) && content.includes('Store')) { if (!seenPatterns.has('hook-with-store')) { seenPatterns.add('hook-with-store'); patterns.push({ category: PATTERN_CATEGORIES.HOOKS, name: 'Custom Hook with Store', description: 'Custom hook that combines store state with additional logic.', whenToUse: 'Encapsulate store access and related business logic.', example: `\`\`\`typescript import { useAgentStore } from '@/stores/agent-store'; import { useCallback, useMemo } from 'react'; export function useAgents() { const { agents, activeAgentId, setActiveAgent, clearActiveAgent } = useAgentStore(); const activeAgent = useMemo( () => (activeAgentId ? agents[activeAgentId] : null), [agents, activeAgentId] ); const activateAgent = useCallback( (id: string, storyId?: string) => { setActiveAgent(id, storyId); }, [setActiveAgent] ); return { agents: Object.values(agents), activeAgent, activateAgent, deactivateAgent: clearActiveAgent, }; } \`\`\``, filesUsing: [relativePath], confidence: 0.90, }); } } // useEffect with cleanup if (content.includes('useEffect') && content.match(/return\s*\(\)\s*=>/)) { if (!seenPatterns.has('useeffect-cleanup')) { seenPatterns.add('useeffect-cleanup'); patterns.push({ category: PATTERN_CATEGORIES.HOOKS, name: 'useEffect with Cleanup', description: 'Effect hook with proper cleanup function.', whenToUse: 'Effects that create subscriptions, timers, or event listeners.', example: `\`\`\`typescript import { useEffect, useState } from 'react'; export function useWindowSize() { const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { function handleResize() { setSize({ width: window.innerWidth, height: window.innerHeight }); } // Set initial size handleResize(); // Add listener window.addEventListener('resize', handleResize); // Cleanup return () => { window.removeEventListener('resize', handleResize); }; }, []); return size; } \`\`\``, filesUsing: [relativePath], confidence: 0.90, }); } } // useCallback pattern if (content.includes('useCallback') && content.includes('useMemo')) { if (!seenPatterns.has('callback-memo')) { seenPatterns.add('callback-memo'); patterns.push({ category: PATTERN_CATEGORIES.HOOKS, name: 'useCallback + useMemo', description: 'Memoization patterns for performance optimization.', whenToUse: 'Prevent unnecessary re-renders in child components.', example: `\`\`\`typescript import { useCallback, useMemo } from 'react'; export function useFilters(items: Item[]) { const [search, setSearch] = useState(''); const [category, setCategory] = useState<string | null>(null); const filteredItems = useMemo(() => { return items.filter(item => { const matchesSearch = item.name.toLowerCase().includes(search.toLowerCase()); const matchesCategory = !category || item.category === category; return matchesSearch && matchesCategory; }); }, [items, search, category]); const handleSearchChange = useCallback((value: string) => { setSearch(value); }, []); const handleCategoryChange = useCallback((value: string | null) => { setCategory(value); }, []); return { filteredItems, search, category, setSearch: handleSearchChange, setCategory: handleCategoryChange, }; } \`\`\``, filesUsing: [relativePath], confidence: 0.85, }); } } } return patterns; } /** * Detect data access patterns */ async detectDataAccessPatterns(files) { const patterns = []; const seenPatterns = new Set(); for (const file of files) { const content = await this.readFile(file); if (!content) continue; const relativePath = path.relative(this.rootPath, file); // Prisma ORM pattern if (content.includes('prisma') && content.includes('findMany')) { if (!seenPatterns.has('prisma')) { seenPatterns.add('prisma'); patterns.push({ category: PATTERN_CATEGORIES.DATA_ACCESS, name: 'Prisma ORM Queries', description: 'Type-safe database queries using Prisma ORM.', whenToUse: 'Database access in Node.js/Next.js applications.', example: `\`\`\`typescript import { prisma } from '@/lib/prisma'; export async function getUsers(options?: { skip?: number; take?: number; where?: { role?: string }; }) { const { skip = 0, take = 10, where } = options ?? {}; const [users, total] = await Promise.all([ prisma.user.findMany({ skip, take, where, orderBy: { createdAt: 'desc' }, select: { id: true, name: true, email: true, role: true, createdAt: true, }, }), prisma.user.count({ where }), ]); return { users, total }; } \`\`\``, filesUsing: [relativePath], confidence: 0.90, }); } } // File system operations if (content.includes("fs.promises") || content.includes("require('fs').promises")) { if (!seenPatterns.has('fs-async')) { seenPatterns.add('fs-async'); patterns.push({ category: PATTERN_CATEGORIES.DATA_ACCESS, name: 'Async File Operations', description: 'File system operations using fs.promises.', whenToUse: 'Reading/writing files in Node.js scripts.', example: `\`\`\`typescript const fs = require('fs').promises; const path = require('path'); async function readJsonFile<T>(filePath: string): Promise<T | null> { try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } } async function writeJsonFile<T>(filePath: string, data: T): Promise<void> { const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, JSON.stringify(data, null, 2)); } \`\`\``, filesUsing: [relativePath], confidence: 0.85, }); } } } return patterns; } /** * Detect testing patterns */ async detectTestingPatterns(files) { const patterns = []; const seenPatterns = new Set(); const testFiles = files.filter((f) => f.includes('.test.') || f.includes('.spec.')); for (const file of testFiles) { const content = await this.readFile(file); if (!content) continue; const relativePath = path.relative(this.rootPath, file); // Jest describe/it pattern if (content.includes('describe(') && content.includes('it(')) { if (!seenPatterns.has('jest-basic')) { seenPatterns.add('jest-basic'); patterns.push({ category: PATTERN_CATEGORIES.TESTING, name: 'Jest Test Structure', description: 'Standard Jest test file structure with describe/it blocks.', whenToUse: 'Unit and integration tests for JavaScript/TypeScript code.', example: `\`\`\`typescript describe('ComponentName', () => { beforeEach(() => { // Setup before each test }); afterEach(() => { // Cleanup after each test }); describe('method/feature', () => { it('should do something when condition', () => { // Arrange const input = { /* test data */ }; // Act const result = someFunction(input); // Assert expect(result).toEqual(expectedOutput); }); it('should handle edge case', () => { expect(() => someFunction(null)).toThrow('Expected error message'); }); }); }); \`\`\``, filesUsing: [relativePath], confidence: 0.95, }); } } // Mock pattern if (content.includes('jest.mock') || content.includes('vi.mock')) { if (!seenPatterns.has('jest-mock')) { seenPatterns.add('jest-mock'); patterns.push({ category: PATTERN_CATEGORIES.TESTING, name: 'Module Mocking', description: 'Mocking modules and dependencies in tests.', whenToUse: 'Isolate units under test from external dependencies.', example: `\`\`\`typescript // Mock an entire module jest.mock('@/lib/api', () => ({ fetchData: jest.fn(), })); // Mock with implementation jest.mock('fs', () => ({ promises: { readFile: jest.fn().mockResolvedValue('mock content'), writeFile: jest.fn().mockResolvedValue(undefined), }, })); // In test import { fetchData } from '@/lib/api'; describe('MyComponent', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should fetch data', async () => { (fetchData as jest.Mock).mockResolvedValue({ data: 'test' }); const result = await myFunction(); expect(fetchData).toHaveBeenCalledWith('/endpoint'); expect(result).toEqual({ data: 'test' }); }); }); \`\`\``, filesUsing: [relativePath], confidence: 0.90, }); } } } return patterns; } /** * Detect utility patterns */ async detectUtilityPatterns(files) { const patterns = []; const seenPatterns = new Set(); const utilFiles = files.filter((f) => f.includes('/utils/') || f.includes('/lib/') || f.includes('/helpers/')); for (const file of utilFiles) { const content = await this.readFile(file); if (!content) continue; const relativePath = path.relative(this.rootPath, file); // Class-based utility if (content.match(/class\s+\w+\s*\{/) && content.includes('constructor')) { if (!seenPatterns.has('class-utility')) { seenPatterns.add('class-utility'); patterns.push({ category: PATTERN_CATEGORIES.UTILITIES, name: 'Class-Based Utility', description: 'Utility class with encapsulated functionality.', whenToUse: 'Complex utilities with state or multiple related methods.', example: `\`\`\`typescript class TemplateEngine { constructor(options = {}) { this.rootPath = options.rootPath || process.cwd(); this.cache = new Map(); } async loadTemplate(name) { if (this.cache.has(name)) { return this.cache.get(name); } const content = await fs.readFile( path.join(this.rootPath, 'templates', \`\${name}.md\`), 'utf-8' ); this.cache.set(name, content); return content; } render(template, context) { return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => context[key] || ''); } } module.exports = TemplateEngine; \`\`\``, filesUsing: [relativePath], confidence: 0.85, }); } } // Functional utilities with exports if (content.includes('module.exports') && content.match(/function\s+\w+\s*\(/)) { if (!seenPatterns.has('functional-utilities')) { seenPatterns.add('functional-utilities'); patterns.push({ category: PATTERN_CATEGORIES.UTILITIES, name: 'Functional Utilities', description: 'Collection of pure utility functions.', whenToUse: 'Simple, reusable utility functions.', example: `\`\`\`typescript /** * Format bytes to human readable string */ function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]; } /** * Debounce a function */ function debounce(fn, ms) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; } /** * Deep clone an object */ function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } module.exports = { formatBytes, debounce, deepClone }; \`\`\``, filesUsing: [relativePath], confidence: 0.80, }); } } } return patterns; } // ============================================================================= // Output Generation // ============================================================================= /** * Generate markdown output * @returns {string} Markdown content */ generateMarkdown() { const lines = []; const timestamp = new Date().toISOString(); lines.push('# Project Patterns'); lines.push(''); lines.push('> Auto-generated from codebase analysis'); lines.push(`> Last updated: ${timestamp}`); lines.push(''); // Generate table of contents lines.push('## Table of Contents'); lines.push(''); for (const [category, patterns] of Object.entries(this.detectedPatterns)) { if (patterns.length > 0) { const anchor = category.toLowerCase().replace(/\s+/g, '-'); lines.push(`- [${category}](#${anchor})`); } } lines.push(''); // Generate each category section for (const [category, patterns] of Object.entries(this.detectedPatterns)) { if (patterns.length === 0) continue; lines.push(`## ${category}`); lines.push(''); for (const pattern of patterns) { lines.push(`### ${pattern.name}`); lines.push(''); lines.push(pattern.example); lines.push(''); lines.push(`**When to use:** ${pattern.whenToUse}`); lines.push(''); if (pattern.filesUsing && pattern.filesUsing.length > 0) { lines.push(`**Files using this pattern:** ${pattern.filesUsing.join(', ')}`); lines.push(''); } lines.push('---'); lines.push(''); } } return lines.join('\n'); } /** * Save patterns to file * @param {string} outputPath - Output file path */ async savePatterns(outputPath) { const defaultPath = path.join(this.rootPath, '.aios', 'patterns.md'); const targetPath = outputPath || defaultPath; // Ensure directory exists const dir = path.dirname(targetPath); await fs.mkdir(dir, { recursive: true }); const content = this.generateMarkdown(); await fs.writeFile(targetPath, content, 'utf-8'); return targetPath; } /** * Merge with existing patterns file * @param {string} existingPath - Path to existing patterns file */ async mergeWithExisting(existingPath) { const defaultPath = path.join(this.rootPath, '.aios', 'patterns.md'); const targetPath = existingPath || defaultPath; try { const existingContent = await fs.readFile(targetPath, 'utf-8'); // Extract existing patterns const existingPatterns = this._parseExistingPatterns(existingContent); // Merge with new patterns (new patterns take precedence) for (const [category, patterns] of Object.entries(this.detectedPatterns)) { if (!existingPatterns[category]) { existingPatterns[category] = []; } for (const pattern of patterns) { const existingIndex = existingPatterns[category].findIndex( (p) => p.name === pattern.name ); if (existingIndex >= 0) { // Update existing pattern existingPatterns[category][existingIndex] = pattern; } else { // Add new pattern existingPatterns[category].push(pattern); } } } this.detectedPatterns = existingPatterns; } catch (error) { // No existing file, use current patterns } return this.savePatterns(targetPath); } /** * Parse existing patterns file * @private */ _parseExistingPatterns(content) { const patterns = {}; // Initialize categories for (const category of Object.values(PATTERN_CATEGORIES)) { patterns[category] = []; } // Simple regex parsing - in production, use a proper markdown parser const categoryMatches = content.matchAll(/^## (.+)$/gm); for (const match of categoryMatches) { const category = match[1]; if (patterns[category]) { // This is a recognized category - patterns would be parsed here // For simplicity, we'll just preserve the category } } return patterns; } /** * Output patterns as JSON * @returns {Object} JSON representation */ toJSON() { return { generated: new Date().toISOString(), rootPath: this.rootPath, totalPatterns: Object.values(this.detectedPatterns).reduce((sum, p) => sum + p.length, 0), categories: this.detectedPatterns, }; } } // ============================================================================= // CLI Handler // ============================================================================= async function main() { const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { console.log(` AIOS Pattern Extractor Extracts and documents code patterns from the codebase. Usage: node pattern-extractor.js [command] [options] Commands: extract Extract patterns (default) json Output as JSON save Save to .aios/patterns.md merge Merge with existing patterns Options: --root <path> Project root (default: cwd) --output <path> Custom output path --category <c> Extract specific category only --quiet Suppress output --help Show this help message Examples: node pattern-extractor.js node pattern-extractor.js save node pattern-extractor.js json --output patterns.json node pattern-extractor.js --category "State Management" `); return; } // Parse arguments const command = args.find((a) => !a.startsWith('--')) || 'extract'; const quiet = args.includes('--quiet'); const rootIndex = args.indexOf('--root'); const rootPath = rootIndex !== -1 ? args[rootIndex + 1] : process.cwd(); const outputIndex = args.indexOf('--output'); const outputPath = outputIndex !== -1 ? args[outputIndex + 1] : null; const categoryIndex = args.indexOf('--category'); const categoryFilter = categoryIndex !== -1 ? args[categoryIndex + 1] : null; // Find project root (look for package.json or .aios-core) let projectRoot = rootPath; while (projectRoot !== '/') { try { await fs.access(path.join(projectRoot, 'package.json')); break; } catch { projectRoot = path.dirname(projectRoot); } } if (projectRoot === '/') { console.error('Error: Could not find project root. Run from within a project directory.'); process.exit(1); } // Create extractor and detect patterns const extractor = new PatternExtractor(projectRoot); if (!quiet) { console.log(`Scanning patterns in: ${projectRoot}`); } await extractor.detectPatterns(); // Filter by category if specified if (categoryFilter) { const filtered = {}; for (const [category, patterns] of Object.entries(extractor.detectedPatterns)) { if (category.toLowerCase().includes(categoryFilter.toLowerCase())) { filtered[category] = patterns; } } extractor.detectedPatterns = filtered; } // Execute command switch (command) { case 'json': { const json = extractor.toJSON(); if (outputPath) { await fs.writeFile(outputPath, JSON.stringify(json, null, 2)); if (!quiet) console.log(`JSON saved to: ${outputPath}`); } else { console.log(JSON.stringify(json, null, 2)); } break; } case 'save': { const savedPath = await extractor.savePatterns(outputPath); if (!quiet) console.log(`Patterns saved to: ${savedPath}`); break; } case 'merge': { const mergedPath = await extractor.mergeWithExisting(outputPath); if (!quiet) console.log(`Patterns merged and saved to: ${mergedPath}`); break; } case 'extract': default: { const markdown = extractor.generateMarkdown(); if (outputPath) { await fs.writeFile(outputPath, markdown); if (!quiet) console.log(`Patterns saved to: ${outputPath}`); } else { console.log(markdown); } break; } } // Summary if (!quiet) { const totalPatterns = Object.values(extractor.detectedPatterns).reduce( (sum, p) => sum + p.length, 0 ); console.log(`\nTotal patterns detected: ${totalPatterns}`); for (const [category, patterns] of Object.entries(extractor.detectedPatterns)) { if (patterns.length > 0) { console.log(` ${category}: ${patterns.length}`); } } } } // Export for programmatic use module.exports = PatternExtractor; // Run CLI if called directly if (require.main === module) { main().catch((error) => { console.error('Error:', error.message); process.exit(1); }); }