Dall'Idea al Codice: Analisi, Progettazione e Architettura Software
L'architettura è la sapiente, rigorosa e magnifica combinazione di volumi nella luce
— Le Corbusier
Architettura come Arte e Scienza
Quando Le Corbusier definiva l'architettura come "sapiente, rigorosa e magnifica combinazione di volumi nella luce", parlava di edifici. Ma la sua definizione si applica perfettamente anche al software. Perché l'architettura software, come quella fisica, è un equilibrio delicato tra arte e scienza, tra rigore e bellezza, tra struttura e flessibilità.
Un'architettura ben progettata è invisibile quando funziona, ma diventa drammaticamente evidente quando manca. È la differenza tra un sistema che evolve con grazia e uno che collassa sotto il peso della propria complessità. È la differenza tra codice che invita alla modifica e codice che terrorizza al solo pensiero di toccarlo.
In questo post esplorerò il viaggio architetturale di CodeIntel System: come sono passato dall'idea astratta alla struttura concreta, dalle visioni ai layer, dai principi alle implementazioni. Condividerò le decisioni prese, i trade-off accettati, gli errori evitati.
Perché l'architettura non è qualcosa che emerge spontaneamente dal codice. È una scelta consapevole, un atto di design, una dichiarazione di intenti.
Il Problema: L'Entropia Architetturale
Tutti i sistemi software soffrono di una forma di "seconda legge della termodinamica": tendono naturalmente verso il chaos.
Inizia sempre bene. Il primo modulo è pulito, ben strutturato, testato. Poi arriva il secondo modulo, che ha bisogno di qualcosa dal primo. "Solo una piccola dipendenza", pensiamo. Poi il terzo modulo dipende dal secondo e dal primo. Poi il quarto crea una dipendenza circolare perché "è più comodo così". Poi qualcuno, di fretta, bypassa l'interfaccia e accede direttamente al database. Poi un altro aggiunge logica business nel controller HTTP "tanto è solo una validazione veloce".
E improvvisamente, senza che nessuno se ne accorga, hai un big ball of mud: un sistema dove tutto dipende da tutto, dove ogni modifica rischia di rompere qualcosa in un'area apparentemente non correlata, dove testare è impossibile perché non riesci a isolare nulla.
L'architettura esiste per prevenire questo decay.
La Soluzione: Clean Architecture e Dependency Rule
Per CodeIntel System, ho scelto un'architettura basata su Clean Architecture (Uncle Bob Martin) e Hexagonal Architecture (Alistair Cockburn), con una regola ferrea:
The Dependency Rule
┌──────────────┐
│ AGENTS │ Business logic & orchestration
└──────┬───────┘
│ ↓ Dependencies flow inward
┌──────▼───────┐
│ CORE │ Domain logic & interfaces
└──────┬───────┘
│ ↓ Never outward
┌──────▼───────┐
│ INFRA │ Implementation details
└──────────────┘
LA REGOLA: Le dipendenze fluiscono sempre verso l'interno. Mai verso l'esterno.
Sembra semplice, ma è rivoluzionario. Questo significa:
✅ ALLOWED:
- Agent → Core Service
- Core Service → Infrastructure
- CLI/API → Agent
❌ FORBIDDEN:
- Core → Agent
- Infrastructure → Core
- Infrastructure → Agent
Perché Questa Regola Cambia Tutto
Immagina questo scenario: devo cambiare database vettoriale, da ChromaDB a Qdrant.
Con Dependency Rule:
// Solo change: src/core/vector-store/src/QdrantClient.ts
// Agents unchanged ✅
// Core interface unchanged ✅
// Tests di business logic unchanged ✅
Senza Dependency Rule:
// Must change:
// - 6 different agents ❌
// - All agent tests ❌
// - API endpoints ❌
// - CLI commands ❌
// - 2-3 settimane di lavoro ❌
Questa è la magia della dipendenza controllata: cambiamenti implementativi non propagano al business logic. L'architettura ti protegge dal caos.
I Quattro Layer dell'Architettura
CodeIntel è organizzato in 4 layer concentrici, ognuno con responsabilità precise:
Layer 1: Infrastructure (Implementazione Tecnica)
Responsabilità: Fornire capacità tecniche (database, LLM, filesystem)
Componenti:
- Ollama (LLM runtime locale)
- ChromaDB (vector database)
- File System
- Docker containers
Regole:
- ✅ Implementa dettagli tecnici
- ✅ Usa librerie third-party
- ❌ NON SA NULLA di Core
- ❌ NON SA NULLA di Agents
- ❌ NON CONTIENE business logic
Esempio pratico:
// ✅ CORRECT - Pure infrastructure
export class ChromaDBClient {
async connect(): Promise<void> {
this.client = new ChromaClient({ url: this.config.url });
await this.client.heartbeat(); // Health check
}
async query(embedding: number[]): Promise<Result[]> {
const results = await this.collection.query({
queryEmbeddings: [embedding],
nResults: 10,
});
return this.transformResults(results);
}
}
// ❌ WRONG - Business logic in infrastructure
export class ChromaDBClient {
async indexCodebase(files: File[]): Promise<void> {
// ❌ This is business logic!
// Infrastructure should NOT know about "codebase" or "files"
}
}
Layer 2: Core Services (Logica di Dominio)
Responsabilità: Fornire capacità di dominio (parsing, embeddings, search)
Componenti:
- LanguageDetector: Multi-strategy detection (extension + content analysis)
- ASTParser: Tree-sitter parsing per 7 linguaggi
- EmbeddingService: Vector embeddings con caching intelligente
- VectorStoreService: Semantic search, batch operations
- LLMService: Ollama client con streaming e retry logic
Regole:
- ✅ Espone interfacce
- ✅ Implementa logica di dominio
- ✅ Può dipendere da Infrastructure
- ✅ Fornisce caching, validazione, error handling
- ❌ NON SA NULLA di Agents
- ❌ NON SA NULLA di CLI/API
Esempio pratico:
// ✅ CORRECT - Core service orchestration
export class VectorStoreService {
constructor(private client: IVectorDBClient) {}
async upsert(doc: VectorDocument): Promise<void> {
// Validation (domain concern)
if (!doc.embedding || doc.embedding.length === 0) {
throw new Error('Document must have valid embedding');
}
// Caching (domain optimization)
if (this.cache.has(doc.id)) {
this.logger.debug(`Cache hit for ${doc.id}`);
return;
}
// Delegation to infrastructure
await this.client.upsert(doc);
this.cache.set(doc.id, doc);
}
async semanticSearch(query: string, limit: number): Promise<SearchResult[]> {
// Generate embedding (domain logic)
const embedding = await this.embedder.embed(query);
// Query vector store (infrastructure)
const results = await this.client.query(embedding, limit);
// Score normalization (domain logic)
return this.normalizeScores(results);
}
}
// ❌ WRONG - Agent logic in Core
export class VectorStoreService {
async indexProject(path: string): Promise<void> {
// ❌ This is agent-level orchestration!
// Core should NOT know about "projects" or file paths
}
}
Perché le interfacce sono critiche:
// Interface = Contract
export interface IVectorStore {
upsert(doc: VectorDocument): Promise<void>;
query(embedding: number[], limit: number): Promise<SearchResult[]>;
delete(ids: string[]): Promise<void>;
}
// Implementation 1: ChromaDB
export class ChromaVectorStore implements IVectorStore {
// ChromaDB-specific implementation
}
// Implementation 2: Qdrant (future)
export class QdrantVectorStore implements IVectorStore {
// Qdrant-specific implementation
}
// Agent uses interface, not implementation
export class CodebaseIndexAgent {
constructor(private vectorStore: IVectorStore) {
// Doesn't care if it's Chroma or Qdrant!
}
}
Questo è Dependency Inversion Principle (SOLID) in azione.
Layer 3: Agents (Orchestrazione e Workflow)
Responsabilità: Orchestrare servizi Core, implementare use cases
Componenti:
- CodebaseIndexAgent: Indicizzazione con semantic chunking
- CodeUnderstandingAgent: Q&A contestuale con RAG
- DocumentationAgent: Generazione docs (JSDoc, Markdown, OpenAPI, Mermaid)
- ArchitectureAnalysisAgent: Dependency analysis, pattern detection, metrics
- CodeGenerationAgent: Code generation da specs
- RefactoringAgent: Code smell detection, refactoring suggestions
Regole:
- ✅ Orchestra servizi Core
- ✅ Implementa business workflows
- ✅ Produce evidenze (facts, metrics, results)
- ✅ Logging con traceId
- ❌ NON IMPLEMENTA parsing/embedding/storage
- ❌ NON IMPORTA Infrastructure direttamente
Esempio pratico - CodebaseIndexAgent:
// ✅ CORRECT - Agent as Orchestrator
export class CodebaseIndexAgent extends BaseAgent {
constructor(
private languageDetector: ILanguageDetector,
private astParser: IASTParser,
private embedder: IEmbeddings,
private vectorStore: IVectorStore,
logger: ILogger
) {
super(logger);
}
async index(files: File[]): Promise<IndexResult> {
const traceId = this.generateTraceId();
this.logger.info(`[${traceId}] IndexAgent start`, {
count: files.length
});
const results: FileResult[] = [];
// Parallel processing for performance
const chunks = await Promise.all(
files.map(async (file) => {
// STEP 1: Detect language (Core service)
const lang = await this.languageDetector.detect(file.path);
// STEP 2: Parse AST (Core service)
const ast = await this.astParser.parse(file.content, lang);
// STEP 3: Semantic chunking (Agent logic)
const chunks = this.chunkSemanticaly(ast, file);
return chunks;
})
);
// STEP 4: Generate embeddings (Core service, batched)
const allChunks = chunks.flat();
const embeddings = await this.embedder.embedBatch(
allChunks.map(c => c.content)
);
// STEP 5: Upsert to vector store (Core service, batched)
const documents = allChunks.map((chunk, i) => ({
id: chunk.id,
embedding: embeddings[i],
metadata: chunk.metadata,
}));
await this.vectorStore.upsertBatch(documents);
this.logger.info(`[${traceId}] IndexAgent end`, {
indexed: allChunks.length,
performance: '10-100x faster through batching'
});
return {
filesIndexed: files.length,
chunksCreated: allChunks.length,
traceId,
};
}
// Agent-specific logic (not in Core!)
private chunkSemanticaly(ast: AST, file: File): Chunk[] {
// Strategy: functions > classes > imports > fallback
// This is BUSINESS LOGIC, belongs in Agent
}
}
Contrasto con anti-pattern:
// ❌ WRONG - Agent implements parsing
class BadIndexAgent {
async index(files: File[]): Promise<void> {
for (const file of files) {
// ❌ Implementing AST parsing (should be Core)
const tokens = this.tokenize(file.content);
const ast = this.buildAST(tokens);
// ❌ Implementing embedding logic (should be Core)
const vector = file.content
.split('')
.map((c) => c.charCodeAt(0));
// ❌ Direct infrastructure access (should be Core)
const chroma = new ChromaDB();
await chroma.insert(vector);
}
}
// ❌ 200 lines of parsing logic in Agent
private buildAST(tokens: Token[]): AST {
// This should be in ASTParser service!
}
}
Questa violazione della Dependency Rule porta a:
- Duplicazione: Ogni agent reimplementa parsing
- Fragilità: Cambiare parser richiede modificare tutti gli agents
- Inestensibilità: Impossibile testare agent senza infrastruttura completa
Layer 4: Interface (API/CLI)
Responsabilità: Esporre capacità agli utenti
Componenti:
- REST API (Express + Zod validation)
- CLI (Commander, future)
- Future: IDE plugins, VS Code extension
Regole:
- ✅ Chiama Agents
- ✅ Gestisce HTTP/CLI concerns
- ✅ Validation, auth, rate limiting
- ❌ NON CHIAMA Core direttamente (deve passare per Agents)
- ❌ NON CONTIENE business logic
Esempio pratico - API Endpoint:
// ✅ CORRECT - API as thin wrapper
app.post('/api/index',
helmet(), // Security
rateLimit({ max: 100 }), // Rate limiting
async (req, res) => {
// Validation (interface concern)
const schema = z.object({
path: z.string().min(1),
options: z.object({
languages: z.array(z.string()).optional(),
}).optional(),
});
const parsed = schema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: 'Invalid request',
details: parsed.error
});
}
// Delegation to Agent (business logic)
const result = await codebaseIndexAgent.index(
parsed.data.path,
parsed.data.options
);
// HTTP response (interface concern)
res.json({
success: true,
data: result,
timestamp: new Date().toISOString(),
});
}
);
// ❌ WRONG - Business logic in API
app.post('/api/index', async (req, res) => {
// ❌ File system access (should be Agent)
const files = await fs.readdir(req.body.path);
// ❌ Language detection (should be Core)
const lang = detectLanguage(files[0]);
// ❌ Parsing (should be Core)
const ast = parse(files[0]);
// ❌ Direct vector store access (should be Agent → Core)
await vectorStore.upsert(ast);
res.json({ success: true });
});
Pattern Architetturali Applicati
Oltre ai layer, CodeIntel implementa diversi pattern architetturali consolidati:
1. Hexagonal Architecture (Ports & Adapters)
┌─────────────────┐
│ Application │
│ (Agents) │
└────────┬────────┘
│
┌────────┴────────┐
│ Ports │ ← Interfaces
│ (IVectorStore) │
└────────┬────────┘
│
┌─────────────┴─────────────┐
│ │
┌───▼────┐ ┌────▼────┐
│Adapter │ │ Adapter │
│Chroma │ │ Qdrant │
└────────┘ └─────────┘
Beneficio: Posso sostituire ChromaDB con Qdrant senza toccare gli Agents.
2. Dependency Injection
// Dependencies injected via constructor
export class CodebaseIndexAgent {
constructor(
private languageDetector: ILanguageDetector,
private astParser: IASTParser,
private embedder: IEmbeddings,
private vectorStore: IVectorStore,
logger: ILogger
) {
super(logger);
}
}
// Wired in main.ts or IoC container
const agent = new CodebaseIndexAgent(
languageDetectorService,
astParserService,
embeddingService,
vectorStoreService,
logger
);
Beneficio: Facile testare agent con mock services.
3. Repository Pattern
// Interface defines contract
export interface IVectorStore {
upsert(doc: VectorDocument): Promise<void>;
query(embedding: number[], limit: number): Promise<Result[]>;
delete(ids: string[]): Promise<void>;
}
// Implementation hides ChromaDB details
export class ChromaVectorStoreRepository implements IVectorStore {
// ChromaDB-specific implementation
// Agents don't know it's ChromaDB!
}
Beneficio: Astrazione del persistence layer.
4. Hierarchical Agent Orchestration
Pattern avanzato usato per agent complessi come ArchitectureAnalysisAgent:
┌──────────────────────────────┐
│ ArchitectureAnalysisAgent │ ← Parent Agent
│ (Orchestrates child agents) │
└──────────┬───────────────────┘
│
┌──────┴──────┬──────────────────┐
│ │ │
┌───▼────┐ ┌─────▼─────┐ ┌────────▼────────┐
│Dependency│ │ Pattern │ │ Metrics │ ← Child Agents
│Analyzer │ │ Detector │ │ Calculator │
└──────────┘ └───────────┘ └─────────────────┘
Implementazione:
export class ArchitectureAnalysisAgent extends BaseAgent {
constructor(
private dependencyAnalyzer: DependencyAnalyzer,
private patternDetector: PatternDetector,
private metricsCalculator: MetricsCalculator,
logger: ILogger
) {
super(logger);
}
async analyze(projectPath: string): Promise<ArchitectureReport> {
const traceId = this.generateTraceId();
// Parallel execution of child agents
const [dependencies, patterns, metrics] = await Promise.all([
this.dependencyAnalyzer.analyze(projectPath),
this.patternDetector.detect(projectPath),
this.metricsCalculator.calculate(projectPath),
]);
// Parent agent synthesizes results
return {
dependencies,
patterns,
metrics,
health: this.calculateHealth(metrics),
recommendations: this.generateRecommendations(patterns, metrics),
traceId,
};
}
}
Benefici:
- Separazione delle responsabilità: Ogni child agent ha dominio chiaro
- Testabilità: Ogni child agent testabile indipendentemente
- Parallelismo: Child agents eseguibili in parallelo
- Riusabilità: Child agents riusabili in altri contesti
Anti-Pattern Evitati
❌ God Object Agent
Il problema: Agent da 2000 righe che fa tutto.
La soluzione: Separazione in servizi Core + agent orchestrator sottile (~200 righe).
❌ Mixing Analysis and Generation
Il problema: LLM genera tutto, incluso "fatti" che dovrebbero essere misurabili.
La soluzione: Separare analisi (facts) da generazione (synthesis).
// ✅ CORRECT - Facts + Generation separated
async suggestRefactoring(code: string): Promise<Suggestion> {
// PHASE 1: Analysis (measurable facts)
const metrics = await this.metricsCalculator.analyze(code);
const smells = await this.smellDetector.detect(code);
const facts = { metrics, smells }; // Verifiable
// PHASE 2: Generation (LLM with facts)
const prompt = this.buildPrompt(facts);
const suggestion = await this.llm.generate(prompt);
return {
facts, // Can be unit tested
suggestion, // LLM output
confidence: this.calculateConfidence(facts),
};
}
❌ Direct Infrastructure Access
Il problema: Agent che istanzia direttamente ChromaDB o Ollama.
La soluzione: Sempre passare per interfacce iniettate.
Conclusione: Architettura come Investimento
Progettare un'architettura solida richiede tempo. CodeIntel ha dedicato Sprint 0 completo a pianificazione e design. Poi Sprint 3 Task 6 (3 story points) per documentare l'Agent Framework in dettaglio (1,576 righe di AGENT_FRAMEWORK.md).
Vale la pena? Assolutamente sì.
Perché ora:
- Posso cambiare vector database in 1 file
- Posso testare agents in isolamento con mock
- Posso aggiungere nuovi linguaggi estendendo 1 service
- Posso parallelizzare child agents senza race conditions
- Posso onboarding nuovi sviluppatori con architettura chiara
L'architettura non è overhead. È investimento. È quella "sapiente, rigorosa e magnifica combinazione" che rende il software un'opera d'arte, non solo un mucchio di codice funzionante.
Nel prossimo post esploreremo come l'Intelligenza Artificiale è integrata in CodeIntel sia come strumento di sviluppo (Claude Code, Gemini, Perplexity) che come capability del sistema stesso (RAG, LLM, embeddings).
Nel prossimo post: L'Intelligenza Artificiale come Co-Sviluppatore - Una Partnership Simbiotica
Serie "CodeIntel System: Dal Concetto al Codice"
- Post 1: La Visione del Progetto
- Post 2: SDLC e Metodologia Agile
- Post 3: Analisi, Progettazione e Architettura ← Sei qui
- Post 4: L'IA come Co-Sviluppatore
- Post 5: AST, Tree-sitter e Parsing del Codice
- Post 6: CI/CD e Quality Gates
- Post 7: Agent Framework e Orchestrazione
- Post 8: Documentazione come Filosofia