Arquitetura
O raro-reviewer segue o padrão Ports & Adapters (Hexagonal Architecture), separando a lógica de negócio das dependências externas.
Diagrama de Componentes
┌─────────────────────────────────────────────────────────────────┐
│ Entrypoints │
│ ┌──────────────┐ ┌───────────────────────┐ │
│ │ app.py │ (Container) │ lambda_handler.py │ │
│ │ uvicorn │ │ Mangum + Lambda │ │
│ └──────┬───────┘ └──────────┬────────────┘ │
│ │ │ │
│ └───────────────┬───────────────────────┘ │
│ │ │
│ ┌────▼────┐ │
│ │webapp.py│ create_app() factory │
│ └────┬────┘ │
│ │ │
│ ┌────▼────────┐ │
│ │webhook/ │ Token validation + routing │
│ │router.py │ │
│ └────┬────────┘ │
│ │ │
│ ┌────▼────────┐ │
│ │ Dispatcher │ Fire-and-forget │
│ └────┬────────┘ │
│ │ │
├─────────────────────────┼───────────────────────────────────────┤
│ Core (Pure Python) │
│ ┌────▼────────────┐ │
│ │ReviewPipeline │ Orchestration │
│ └────┬────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌─────▼─────┐ ┌────▼──────┐ │
│ │ Diff │ │ Position │ │ Finding │ │
│ │ Parser │ │ Mapper │ │ Models │ │
│ └─────────┘ └───────────┘ └───────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ Ports (Interfaces) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ GitlabClient │ │ LlmClient │ │ Dispatcher │ │
│ │ (Protocol) │ │ (Protocol) │ │ (Protocol) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
├───────────┼────────────────┼────────────────┼───────────────────┤
│ Adapters (Implementations) │
│ ┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼───────┐ │
│ │GitlabRest │ │ NvidiaNim │ │ Background │ │
│ │Client │ │ Client │ │ TaskDispatch │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐│
│ │LambdaSelf ││
│ │InvokeDispatch││
│ └──────────────┘│
└─────────────────────────────────────────────────────────────────┘
Camadas
Core (src/core/)
Lógica de negócio pura, sem dependências de frameworks externos.
| Módulo | Responsabilidade |
|---|---|
models.py | Dataclasses: MrEvent, FileDiff, DiffLine, Finding |
diff_parser.py | Parse de diffs unificados do GitLab |
position_mapper.py | Mapeamento de findings para posições inline no GitLab |
review_pipeline.py | Orquestração: fetch diff → LLM review → post comments |
Ports (src/ports/)
Interfaces (Protocol classes) que definem os contratos entre core e adapters.
| Porta | Método Principal |
|---|---|
GitlabClient | fetch_diff(), post_inline_comment() |
LlmClient | review_file() |
Dispatcher | dispatch() |
Adapters (src/adapters/)
Implementações concretas das ports.
| Adapter | Implementa |
|---|---|
gitlab_rest_client.py | GitlabClient via REST API |
nvidia_nim_client.py | LlmClient via OpenAI SDK |
background_tasks.py | Dispatcher via asyncio (container) |
lambda_self_invoke.py | Dispatcher via Lambda self-invoke |
Entrypoints
| Arquivo | Ambiente | Descrição |
|---|---|---|
app.py | Container | uvicorn app:app — lê env vars diretamente |
lambda_handler.py | AWS Lambda | Busca secrets do Secrets Manager no cold start |
Fluxo de um Webhook
1. GitLab POST /webhook
│
2. webhook/router.py valida X-Gitlab-Token
│
3. parse_mr_event() extrai MrEvent do payload
│
4. dispatcher.dispatch(event) → fire-and-forget
│
5. ReviewPipeline.run(event)
│
├── action == "open" → gitlab.fetch_diff()
└── action == "update" → gitlab.fetch_compare(oldrev, newrev)
│
6. Para cada FileDiff:
│
├── Tamanho > 800 linhas? → skip
│
└── llm.review_file(event, file_diff)
│
├── LlmParseError → skip arquivo
└── findings → para cada finding:
│
├── position_mapper → linha existe no diff?
│ ├── sim → post_inline_comment()
│ └── não → post_fallback_comment()
│
└── erro no post → log, não aborta
Princípios de Design
- Core puro:
src/corenunca importaboto3,fastapi,openai - Protocol-based: Contratos definidos com
typing.Protocol - Fail-isolated: Erro em um arquivo não afeta outros
- Stateless: Sem banco de dados, sem cache entre requests
- Dual-target: Mesmo código, dois entrypoints (container e Lambda)