Michele Polo

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"