Saltar a contenido

Arquitectura de Capas - Hexagonal Architecture

Descripción

Diagrama de capas siguiendo Clean Architecture / Hexagonal Architecture (Ports & Adapters). Muestra la separación entre capas de dominio, aplicación, infraestructura e interfaces.

Las dependencias fluyen hacia adentro: Infrastructure → Application → Domain.

Diagrama

graph TB
    subgraph InterfacesLayer["Interfaces Layer (UI + API)"]
        GUI[Streamlit GUI<br/>app.py]
        CLI[CLI Interface<br/>cli.py<br/>✅ 20 comandos]
        API[FastAPI REST<br/>api/main.py<br/>✅ 22 endpoints<br/>JWT + Swagger]
    end

    subgraph ApplicationLayer["Application Layer"]
        UC[Use Cases<br/>ProcessDocumentUseCase<br/>ExportExcelUseCase<br/>ImportExcelUseCase]
        Services[Services S23<br/>ValidationService<br/>SearchService<br/>StatisticsService<br/>ManualEntryService<br/>DedupConfigService]
        DTOs[DTOs frozen<br/>ProcessDocument Req/Resp<br/>Statistics Resp<br/>SearchDocuments Req/Resp<br/>ValidateDocument Req/Resp<br/>CreateManualDocument Req/Resp<br/>RecurringCheck Req/Resp<br/>DedupConfig Resp/UpdateReq]
        Container[Service Container<br/>14 properties lazy-loaded<br/>Dependency Injection<br/>Thread-safe RLock]
    end

    subgraph DomainLayer["Domain Layer (Core)"]
        Entities[Entities<br/>Document<br/>DocumentType<br/>DocumentStatus]
        ValueObjects[Value Objects<br/>EntityResult<br/>OCRResult<br/>DuplicateCandidate<br/>NERFields<br/>CSJMetadata<br/>ProvisionalInfo<br/>SectionsInfo]
        Errors[core/errors.py<br/>AppError + ErrorCode<br/>Tipado de errores S27]
    end

    subgraph InfrastructureLayer["Infrastructure Layer (Adapters)"]
        subgraph "OCR Adapters"
            OCRRouter[OCR Router<br/>Confidence-based]
            Tesseract[Tesseract Adapter]
            Paddle[PaddleOCR Adapter]
            QAnalyzer[Quality Analyzer]
        end

        subgraph "NER Adapters"
            SpaCy[SpaCy Adapter<br/>es_core_news_lg]
            CtxScorer[ContextScorer ✅]
            EntLinker[EntityLinker ✅]
            RadVal[RadicadoValidator ✅]
            StructAnalyzer[DocumentStructureAnalyzer ✅]
            NERensemble[EntityExtractorEnsemble ✅]
            ActiveLearn[ActiveLearning ✅<br/>Infraestructura]
            ActaReparto[ActaRepartoExtractor ✅]
            SectionsDet[SectionsDetector ✅]
            ProvMeasure[ProvisionalMeasureDetector ✅]
        end

        subgraph "Duplicate Detection"
            DedupEnsemble[Hybrid Ensemble<br/>Detector]
            ExactHash[Exact Matcher<br/>SHA-256]
            MinHash[MinHash LSH<br/>Matcher]
            TFIDF[TF-IDF Similarity<br/>Matcher]
            EntityMatch[Entity Matcher<br/>Legal Parties]
        end

        subgraph "Persistence"
            Repo[SQLite Repository<br/>FTS5 Support<br/>3 Mixins: Connection,<br/>Correction, Metrics<br/>Transaction Support]
            ISP[ISP Protocols:<br/>DocumentReader,<br/>DocumentWriter + Txn,<br/>DocumentSearcher,<br/>CorrectionRepository,<br/>MetricsRepository]
        end

        subgraph "Logging"
            Logger[Logger Service<br/>File + DB handlers]
            DBHandler[Database Handler<br/>Audit Trail]
        end
    end

    subgraph PortsLayer["Ports (Protocols/Interfaces)"]
        OCRPort[OCRPort Protocol]
        NERPort[NER Port Protocol]
        DedupPort[DuplicateDetectorPort]
        RepoPort[DocumentRepository<br/>Protocol ISP:<br/>Reader, Writer, Searcher,<br/>Correction, Metrics]
    end

    GUI --> Container
    GUI --> Services
    CLI --> Container
    API --> Container
    API --> Services
    Container --> UC
    Container --> Services
    UC --> DTOs
    UC --> OCRPort
    UC --> NERPort
    UC --> DedupPort
    UC --> RepoPort

    Services --> RepoPort
    Services --> DTOs
    DTOs --> Entities
    DTOs --> ValueObjects
    UC --> Errors

    OCRPort -.implements.-> OCRRouter
    OCRRouter --> Tesseract
    OCRRouter --> Paddle
    OCRRouter --> QAnalyzer

    NERPort -.implements.-> SpaCy
    SpaCy --> CtxScorer
    SpaCy --> EntLinker
    SpaCy --> RadVal
    SpaCy --> StructAnalyzer
    NERPort -.implements.-> NERensemble
    SpaCy --> ActiveLearn
    SpaCy --> ActaReparto
    SpaCy --> SectionsDet
    SpaCy --> ProvMeasure

    DedupPort -.implements.-> DedupEnsemble
    DedupEnsemble --> ExactHash
    DedupEnsemble --> MinHash
    DedupEnsemble --> TFIDF
    DedupEnsemble --> EntityMatch

    RepoPort -.implements.-> ISP
    ISP -.implements.-> Repo

    OCRRouter -.logs.-> Logger
    SpaCy -.logs.-> Logger
    DedupEnsemble -.logs.-> Logger
    Logger --> DBHandler
    DBHandler --> Repo

    style DomainLayer fill:#d4f1d4
    style ApplicationLayer fill:#d4e4f7
    style InfrastructureLayer fill:#fff4d4
    style InterfacesLayer fill:#f7d4e4
    style PortsLayer fill:#e4d4f7

Descripción de Capas

1. Domain Layer (Núcleo)

Responsabilidad: Lógica de negocio pura, sin dependencias externas.

  • Entities: Document, DocumentType, DocumentStatus
  • Value Objects: EntityResult, OCRResult, DuplicateCandidate, NERFields, CSJMetadata, ProvisionalInfo, SectionsInfo (Sprint 23: 4 nuevos frozen VOs)
  • Errors (Sprint 27): core/errors.pyAppError dataclass + ErrorCode enum con mapeo a HTTP status codes. Base del flujo de errores tipados Result[T, AppError]
  • Reglas de negocio: Validaciones de estado, invariantes del dominio

Característica clave: Cero dependencias a frameworks o bibliotecas externas.

2. Application Layer

Responsabilidad: Orquestación de casos de uso, coordinación entre puertos.

  • Use Cases: ProcessDocumentUseCase (pipeline OCR -> NER -> Dedup -> Persist), ExportExcelUseCase, ImportExcelUseCase
  • Services (Sprint 23): ValidationService, SearchService, StatisticsService, ManualEntryService, DedupConfigService — lógica extraída de GUI, sin dependencia Streamlit
  • DTOs: Objetos inmutables para transferencia entre capas
  • Service Container: Inyección de dependencias, 14 properties lazy-loaded, thread-safe con RLock

Pattern: Railway-Oriented Programming con returns.Result para manejo de errores.

3. Infrastructure Layer (Adapters)

Responsabilidad: Implementaciones concretas de puertos, integraciones externas.

  • OCR Adapters: Tesseract, PaddleOCR con routing inteligente
  • NER Adapters: SpaCy con modelo es_core_news_lg + Validators (ContextScorer, EntityLinker, RadicadoValidator, DocumentStructureAnalyzer) + Extractores especializados (ActaRepartoExtractor, SectionsDetector, ProvisionalMeasureDetector)
  • Duplicate Detection: 4 niveles (Exact, MinHash, TF-IDF, Entity)
  • Persistence: SQLite con FTS5 para búsqueda full-text, refactorizado en 3 mixins (Connection, Correction, Metrics)
  • Logging: Sistema centralizado con handlers de archivo (rotación diaria) y base de datos (auditoría)
  • Config Persistence: EnsembleConfig con save/load JSON (data/config/dedup_config.json)

Pattern: Adapter Pattern, cada implementación cumple un Port (protocolo ABC).

4. Interfaces Layer

Responsabilidad: Interacción con usuarios (GUI, CLI, API REST).

  • Streamlit GUI ✅: Interfaz web para operadores (9 páginas implementadas)
  • CLI ✅: Interfaz de línea de comandos con 20 comandos (procesamiento batch, búsqueda, exportación, debugging) en interfaces/cli.py
  • API REST ✅: FastAPI con 22 endpoints, JWT auth (HS256, 8h expiry, max 5 refreshes), Swagger UI (/docs), async OCR via TaskManager (ThreadPoolExecutor), exception handler global. Score auditoría 9.2/10. Componentes: main.py (app factory), config.py (ApiSettings), auth.py (JWT), dependencies.py (DI), result_mapper.py (Result→JSON), tasks.py (async), 11 routers en routes/, 8 schemas Pydantic en schemas/

5. Ports (Protocolos)

Responsabilidad: Contratos (interfaces) que define Application, implementa Infrastructure.

  • OCRPort: Contrato para motores OCR
  • NERPort: Contrato para extractores de entidades
  • DuplicateDetectorPort: Contrato para detectores de duplicados
  • DocumentRepository: Contrato para persistencia, segregado en ISP protocols: DocumentReader, DocumentWriter, DocumentSearcher, CorrectionRepository, MetricsRepository

Flujo de Dependencias

GUI/CLI/API → Application → Ports ← Infrastructure
                Domain (Core)

Regla de Oro: Las capas internas nunca conocen las externas. Infrastructure depende de Application, pero no al revés.

Nota: Los 16 bypasses legacy GUI→Repository fueron eliminados en Sprint 23.1 (Fase 1.5). La API REST no tiene ningún bypass — todas las operaciones pasan por Services/UseCases del Application Layer.

Flujo de Errores Tipados (Sprint 27)

AppError con ErrorCode

A partir del Sprint 27, el sistema utiliza un flujo de errores tipado basado en AppError con ErrorCode que se propaga desde la capa de persistencia hasta la capa API de forma uniforme:

Persistence Layer           Application Layer           API Layer
─────────────────          ───────────────────         ──────────────
SQLiteRepository            Services / UseCases         result_mapper.py
     │                           │                           │
     ├─ Result[T, AppError] ───→ ├─ bind/map (ROP) ───────→ ├─ map_result_to_response()
     │                           │                           │
     │  AppError(                │  Propaga AppError         │  AppError.code.http_status
     │    code=ErrorCode.XXX,    │  sin transformar o        │  → HTTP status code directo
     │    message="...",         │  enriquece con contexto   │
     │    details={...}          │                           │  Ej: NOT_FOUND → 404
     │  )                        │                           │      VALIDATION_ERROR → 422
     │                           │                           │      DUPLICATE → 409
                                                             │      INTERNAL_ERROR → 500

Propagación por capas

  1. Persistence → Application: Los métodos del repositorio (delete(), update_status(), update_document_field(), save_processing_metrics()) retornan Result[T, AppError]. La capa de aplicación usa bind() y map() del patrón ROP para encadenar operaciones sin try/except.

  2. Application → API: Los Services y UseCases retornan Result[T, AppError] a los routers. No se transforman las excepciones — el error fluye como valor.

  3. API → HTTP Response: result_mapper.py convierte el resultado:

  4. AppError (preferido): Lee AppError.code.http_status para obtener el código HTTP directamente. El ErrorCode enum define el mapeo (ej: ErrorCode.NOT_FOUND → 404, ErrorCode.VALIDATION_ERROR → 422).
  5. String errors (legacy): Para compatibilidad con código anterior que retorna Failure("mensaje string"), el mapper aplica heurísticas de clasificación (busca patrones como "not found", "invalid", etc.) para determinar el HTTP status code.

Ejemplo de flujo completo

# Persistence (retorna Result)
def delete(self, doc_id: str) -> Result[None, AppError]:
    ...
    return Failure(AppError(code=ErrorCode.NOT_FOUND, message=f"Documento {doc_id} no encontrado"))

# Application (propaga Result)
def delete_document(self, doc_id: str) -> Result[None, AppError]:
    return self.repository.delete(doc_id)

# API Router (usa result_mapper)
@router.delete("/{doc_id}")
def delete_document(...):
    result = container.repository.delete(doc_id)
    return map_result_to_response(result)  # → 404 {"error": "Documento X no encontrado"}

Principios Aplicados

  • Dependency Inversion Principle (DIP): Application depende de abstracciones (Ports), no implementaciones.
  • Single Responsibility Principle (SRP): Cada adapter tiene una responsabilidad única.
  • Open/Closed Principle (OCP): Extender con nuevos adapters sin modificar Application.
  • Interface Segregation Principle (ISP): Ports mínimos, específicos por caso de uso.

Leyenda de Estados

Símbolo Significado
Implementado y funcional
Planificado para fase futura
🔬 En investigación

Última actualización: 2026-03-27 (Sprint 28: flujo errores tipados AppError+ErrorCode, Result[T, AppError] en persistencia, 1707 tests, 87.77% coverage gate 88%, tests/integration/ 16 tests cross-layer)