Core Concepts

Architecture & Extensibility

Learn about AI Developer Assistant's architecture and extensibility features

Architecture & Extensibility

AI Developer Assistant is built with a clean, modular architecture that follows Hexagonal Architecture (Ports & Adapters) principles. This design ensures maintainability, testability, and extensibility while keeping the core business logic separate from external dependencies.

Hexagonal Architecture

Core Principles

AI Developer Assistant follows these architectural principles:

  1. Separation of Concerns: Business logic is isolated from infrastructure
  2. Dependency Inversion: Core depends on abstractions, not implementations
  3. Testability: Each layer can be tested independently
  4. Extensibility: New adapters can be added without changing core logic
  5. Technology Agnostic: Core logic doesn't depend on specific technologies

Architecture Layers

┌─────────────────────┐
│   CLI / IDE / API   │  ← Driving Adapters (Inbound)
│  (Inbound Ports)    │
└─────────┬───────────┘
          │
┌─────────▼───────────┐
│  Application Core   │  ← Domain Entities + Use Cases
│ (Domain Layer)      │
└─────────┬───────────┘
          │
┌─────────▼───────────┐
│ Infrastructure      │  ← Git, LLM, GitHub, Output
│   Adapters          │    (Outbound Adapters)
└─────────────────────┘

Domain Layer

The domain layer contains the core business logic and is independent of external systems.

Domain Entities

// src/domain/entities/Diff.ts
export interface Diff {
  files: DiffFile[];
  baseCommit: string;
  headCommit: string;
  timestamp: Date;
}

export interface DiffFile {
  path: string;
  changes: Change[];
  language: string;
  framework: string;
}

export interface Change {
  type: 'added' | 'modified' | 'deleted';
  line: number;
  content: string;
  context: string;
}
// src/domain/entities/ReviewReport.ts
export interface ReviewReport {
  summary: ReviewSummary;
  issues: ReviewIssue[];
  recommendations: Recommendation[];
  metadata: ReportMetadata;
}

export interface ReviewIssue {
  file: string;
  line: number;
  severity: 'low' | 'medium' | 'high' | 'critical';
  category: string;
  message: string;
  suggestion: string;
  code: string;
}

Domain Ports (Interfaces)

// src/domain/ports/LLMPort.ts
export interface LLMProvider {
  generateResponse(messages: LLMMessage[]): Promise<LLMResponse>;
  validateConfiguration(): Promise<boolean>;
}

export interface LLMMessage {
  role: 'system' | 'user' | 'assistant';
  content: string;
}

export interface LLMResponse {
  content: string;
  usage: TokenUsage;
  model: string;
  provider: string;
}
// src/domain/ports/GitPort.ts
export interface GitAdapter {
  getDiff(base: string, head: string): Promise<Diff>;
  getStagedChanges(): Promise<Diff>;
  getUnstagedChanges(): Promise<Diff>;
  getCommitHistory(limit: number): Promise<Commit[]>;
}

Use Cases

// src/domain/usecases/ReviewCodeUseCase.ts
export class ReviewCodeUseCase {
  constructor(
    private gitAdapter: GitAdapter,
    private llmAdapter: LLMAdapter,
    private outputAdapter: OutputAdapter
  ) {}

  async execute(options: ReviewOptions): Promise<ReviewReport> {
    // 1. Get code changes
    const diff = await this.getCodeChanges(options);
    
    // 2. Analyze code with LLM
    const analysis = await this.analyzeCode(diff);
    
    // 3. Generate report
    const report = this.generateReport(analysis);
    
    // 4. Output results
    await this.outputAdapter.displayReviewReport(report);
    
    return report;
  }

  private async getCodeChanges(options: ReviewOptions): Promise<Diff> {
    if (options.staged) {
      return await this.gitAdapter.getStagedChanges();
    } else if (options.unstaged) {
      return await this.gitAdapter.getUnstagedChanges();
    } else {
      return await this.gitAdapter.getDiff(options.base, options.head);
    }
  }

  private async analyzeCode(diff: Diff): Promise<CodeAnalysis> {
    const messages = this.buildAnalysisMessages(diff);
    const response = await this.llmAdapter.generateResponse(messages);
    return this.parseAnalysisResponse(response);
  }
}

Infrastructure Layer

The infrastructure layer implements the domain ports and handles external system interactions.

Inbound Adapters

CLI Adapter

// src/adapters/inbound/CLI/CLIAdapter.ts
export class CLIAdapter {
  constructor(
    private reviewUseCase: ReviewCodeUseCase,
    private explainUseCase: ExplainCodeUseCase,
    private testUseCase: GenerateTestsUseCase
  ) {}

  registerCommands(program: Command): void {
    program
      .command('review')
      .description('Review code changes')
      .option('-s, --staged', 'Review staged changes')
      .option('-u, --unstaged', 'Review unstaged changes')
      .option('-b, --base <ref>', 'Base reference')
      .option('-h, --head <ref>', 'Head reference')
      .action(async (options) => {
        try {
          await this.reviewUseCase.execute(options);
        } catch (error) {
          console.error('Review failed:', error);
          process.exit(1);
        }
      });
  }
}

IDE Adapter (Future)

// src/adapters/inbound/IDE/IDEAdapter.ts
export class IDEAdapter {
  constructor(private reviewUseCase: ReviewCodeUseCase) {}

  async reviewCurrentFile(filePath: string): Promise<ReviewReport> {
    const options: ReviewOptions = {
      filePatterns: [filePath],
      verbose: true
    };
    
    return await this.reviewUseCase.execute(options);
  }

  async reviewSelection(filePath: string, selection: TextSelection): Promise<ReviewReport> {
    // Implementation for reviewing selected code
  }
}

Outbound Adapters

Git Adapter

// src/adapters/outbound/Git/GitAdapter.ts
export class GitAdapter implements GitPort {
  constructor(private git: SimpleGit) {}

  async getDiff(base: string, head: string): Promise<Diff> {
    const diffOutput = await this.git.diff([base, head]);
    return this.parseDiffOutput(diffOutput);
  }

  async getStagedChanges(): Promise<Diff> {
    const diffOutput = await this.git.diff(['--staged']);
    return this.parseDiffOutput(diffOutput);
  }

  private parseDiffOutput(output: string): Diff {
    // Parse git diff output into structured format
    const files = this.parseDiffFiles(output);
    return {
      files,
      baseCommit: 'HEAD~1',
      headCommit: 'HEAD',
      timestamp: new Date()
    };
  }
}

LLM Adapter

// src/adapters/outbound/LLM/LLMAdapter.ts
export class LLMAdapter implements LLMPort {
  private providers: Map<string, LLMProvider> = new Map();

  constructor() {
    this.registerDefaultProviders();
  }

  registerProvider(name: string, provider: LLMProvider): void {
    this.providers.set(name, provider);
  }

  async generateResponse(messages: LLMMessage[]): Promise<LLMResponse> {
    const provider = this.getCurrentProvider();
    return await provider.generateResponse(messages);
  }

  private getCurrentProvider(): LLMProvider {
    const providerName = this.getProviderName();
    const provider = this.providers.get(providerName);
    
    if (!provider) {
      throw new Error(`Provider ${providerName} not found`);
    }
    
    return provider;
  }

  private registerDefaultProviders(): void {
    this.registerProvider('openai', new OpenAIProvider());
    this.registerProvider('gemini', new GeminiProvider());
    this.registerProvider('ollama', new OllamaProvider());
  }
}

Output Adapter

// src/adapters/outbound/Output/OutputAdapter.ts
export class OutputAdapter implements OutputPort {
  private formats: Map<string, OutputFormat> = new Map();

  constructor() {
    this.registerDefaultFormats();
  }

  registerFormat(name: string, format: OutputFormat): void {
    this.formats.set(name, format);
  }

  async displayReviewReport(report: ReviewReport): Promise<void> {
    const format = this.getCurrentFormat();
    await format.display(report);
  }

  private registerDefaultFormats(): void {
    this.registerFormat('console', new ConsoleFormat());
    this.registerFormat('markdown', new MarkdownFormat());
    this.registerFormat('json', new JSONFormat());
    this.registerFormat('html', new HTMLFormat());
  }
}

Extensibility Features

Plugin System

// src/core/PluginSystem.ts
export interface Plugin {
  name: string;
  version: string;
  initialize(context: PluginContext): Promise<void>;
  execute(input: any): Promise<any>;
  cleanup(): Promise<void>;
}

export class PluginManager {
  private plugins: Map<string, Plugin> = new Map();

  async registerPlugin(plugin: Plugin): Promise<void> {
    await plugin.initialize(this.createPluginContext());
    this.plugins.set(plugin.name, plugin);
  }

  async executePlugin(name: string, input: any): Promise<any> {
    const plugin = this.plugins.get(name);
    if (!plugin) {
      throw new Error(`Plugin ${name} not found`);
    }
    
    return await plugin.execute(input);
  }

  async executeAllPlugins(input: any): Promise<any[]> {
    const results: any[] = [];
    
    for (const plugin of this.plugins.values()) {
      try {
        const result = await plugin.execute(input);
        results.push(result);
      } catch (error) {
        console.error(`Plugin ${plugin.name} failed:`, error);
      }
    }
    
    return results;
  }
}

Custom Adapter Registration

// src/core/AdapterRegistry.ts
export class AdapterRegistry {
  private llmProviders: Map<string, LLMProvider> = new Map();
  private outputFormats: Map<string, OutputFormat> = new Map();
  private gitAdapters: Map<string, GitAdapter> = new Map();

  registerLLMProvider(name: string, provider: LLMProvider): void {
    this.llmProviders.set(name, provider);
  }

  registerOutputFormat(name: string, format: OutputFormat): void {
    this.outputFormats.set(name, format);
  }

  registerGitAdapter(name: string, adapter: GitAdapter): void {
    this.gitAdapters.set(name, adapter);
  }

  getLLMProvider(name: string): LLMProvider {
    const provider = this.llmProviders.get(name);
    if (!provider) {
      throw new Error(`LLM provider ${name} not found`);
    }
    return provider;
  }
}

Configuration System

// src/config/ConfigurationManager.ts
export class ConfigurationManager {
  private config: Configuration;
  private validators: ConfigValidator[] = [];

  constructor() {
    this.loadConfiguration();
    this.registerDefaultValidators();
  }

  private loadConfiguration(): void {
    // Load from files, environment variables, CLI options
    this.config = this.mergeConfigurations([
      this.loadDefaultConfig(),
      this.loadFileConfig(),
      this.loadEnvironmentConfig(),
      this.loadCLIConfig()
    ]);
  }

  private registerDefaultValidators(): void {
    this.validators.push(new LLMConfigValidator());
    this.validators.push(new GitConfigValidator());
    this.validators.push(new OutputConfigValidator());
  }

  validateConfiguration(): ValidationResult {
    const errors: string[] = [];
    
    for (const validator of this.validators) {
      const result = validator.validate(this.config);
      if (!result.valid) {
        errors.push(...result.errors);
      }
    }
    
    return {
      valid: errors.length === 0,
      errors: errors
    };
  }
}

Custom Extensions

Creating Custom LLM Providers

// extensions/CustomLLMProvider.ts
export class CustomLLMProvider implements LLMProvider {
  constructor(private config: CustomLLMConfig) {}

  async generateResponse(messages: LLMMessage[]): Promise<LLMResponse> {
    // Your custom implementation
    const response = await this.callCustomAPI(messages);
    
    return {
      content: response.text,
      usage: {
        promptTokens: response.promptTokens,
        completionTokens: response.completionTokens,
        totalTokens: response.totalTokens
      },
      model: this.config.model,
      provider: 'custom'
    };
  }

  async validateConfiguration(): Promise<boolean> {
    try {
      await this.callCustomAPI([{ role: 'user', content: 'test' }]);
      return true;
    } catch (error) {
      return false;
    }
  }
}

Creating Custom Output Formats

// extensions/CustomOutputFormat.ts
export class CustomOutputFormat implements OutputFormat {
  constructor(private config: CustomOutputConfig) {}

  async display(report: ReviewReport): Promise<void> {
    const formattedOutput = this.formatReport(report);
    
    if (this.config.outputPath) {
      await fs.writeFile(this.config.outputPath, formattedOutput);
    } else {
      console.log(formattedOutput);
    }
  }

  private formatReport(report: ReviewReport): string {
    // Your custom formatting logic
    return `
      🎯 CUSTOM REVIEW REPORT
      ======================
      
      Files: ${report.summary.filesReviewed}
      Issues: ${report.summary.issuesFound}
      
      ${report.issues.map(issue => 
        `📍 ${issue.file}:${issue.line} - ${issue.message}`
      ).join('\n')}
    `;
  }
}

Creating Custom Plugins

// plugins/SecurityAnalysisPlugin.ts
export class SecurityAnalysisPlugin implements Plugin {
  name = 'security-analysis';
  version = '1.0.0';

  async initialize(context: PluginContext): Promise<void> {
    // Initialize plugin
  }

  async execute(input: CodeAnalysisInput): Promise<SecurityAnalysisResult> {
    const securityIssues = await this.analyzeSecurity(input.code);
    
    return {
      plugin: this.name,
      issues: securityIssues,
      metadata: {
        analysisType: 'security',
        timestamp: new Date()
      }
    };
  }

  async cleanup(): Promise<void> {
    // Cleanup resources
  }

  private async analyzeSecurity(code: string): Promise<SecurityIssue[]> {
    // Security analysis logic
  }
}

Testing Architecture

Unit Testing

// tests/domain/usecases/ReviewCodeUseCase.test.ts
describe('ReviewCodeUseCase', () => {
  let useCase: ReviewCodeUseCase;
  let mockGitAdapter: jest.Mocked<GitAdapter>;
  let mockLLMAdapter: jest.Mocked<LLMAdapter>;
  let mockOutputAdapter: jest.Mocked<OutputAdapter>;

  beforeEach(() => {
    mockGitAdapter = createMockGitAdapter();
    mockLLMAdapter = createMockLLMAdapter();
    mockOutputAdapter = createMockOutputAdapter();
    
    useCase = new ReviewCodeUseCase(
      mockGitAdapter,
      mockLLMAdapter,
      mockOutputAdapter
    );
  });

  it('should review staged changes', async () => {
    // Arrange
    const mockDiff = createMockDiff();
    mockGitAdapter.getStagedChanges.mockResolvedValue(mockDiff);
    
    // Act
    const result = await useCase.execute({ staged: true });
    
    // Assert
    expect(mockGitAdapter.getStagedChanges).toHaveBeenCalled();
    expect(mockLLMAdapter.generateResponse).toHaveBeenCalled();
    expect(mockOutputAdapter.displayReviewReport).toHaveBeenCalled();
  });
});

Integration Testing

// tests/integration/ReviewIntegration.test.ts
describe('Review Integration', () => {
  it('should review real code changes', async () => {
    // Setup real adapters
    const gitAdapter = new GitAdapter(createRealGit());
    const llmAdapter = new LLMAdapter();
    const outputAdapter = new OutputAdapter();
    
    const useCase = new ReviewCodeUseCase(gitAdapter, llmAdapter, outputAdapter);
    
    // Execute with real data
    const result = await useCase.execute({
      filePatterns: ['src/**/*.ts']
    });
    
    expect(result.summary.filesReviewed).toBeGreaterThan(0);
  });
});

Best Practices

1. Follow Hexagonal Architecture

  • Keep domain logic pure - no external dependencies
  • Use ports and adapters - define interfaces for external systems
  • Dependency inversion - depend on abstractions, not concretions

2. Design for Extensibility

  • Plugin system for adding new functionality
  • Adapter registry for custom implementations
  • Configuration system for flexible setup

3. Ensure Testability

  • Mock external dependencies in unit tests
  • Test each layer independently
  • Use dependency injection for easy mocking

4. Maintain Clean Code

  • Single responsibility for each class
  • Clear interfaces between layers
  • Consistent error handling across the system
The modular architecture makes it easy to extend AI Developer Assistant with new features. Start with simple customizations and gradually build more complex extensions as your needs grow.