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:
- Separation of Concerns: Business logic is isolated from infrastructure
- Dependency Inversion: Core depends on abstractions, not implementations
- Testability: Each layer can be tested independently
- Extensibility: New adapters can be added without changing core logic
- 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.