br.dev.jsilveira.coresync/agents.md

338 lines
12 KiB
Markdown

# 🤖 CoreSync — Agent Guidelines
Este documento define a arquitetura, as regras de negócio e os padrões de código obrigatórios para o desenvolvimento do backend do **CoreSync**. Todo código gerado ou modificado deve obedecer estritamente às diretrizes abaixo.
---
## 🛠 Stack Tecnológico
| Camada | Tecnologia |
|---|---|
| Linguagem | Groovy (Closures, Records, sintaxe enxuta) |
| Runtime | Java 26 (toolchain configurado no Gradle) |
| Framework | Spring Boot (Web, Security, Data MongoDB) |
| Banco de Dados | MongoDB |
| Cliente | React Native — comunicação exclusiva via JSON/REST + JWT stateless |
---
## 🏛 Arquitetura: Hexagonal (Ports & Adapters) + DDD
O sistema é um **Monolito Modular** construído sobre **Arquitetura Hexagonal**. O objetivo é o isolamento absoluto da Regra de Negócio.
### Regra de Ouro
> As camadas `domain` e `application` são **100% agnósticas de framework**. Elas não conhecem Spring Boot, MongoDB nem HTTP. Tudo que for infraestrutura ou framework vive exclusivamente na camada `adapter`.
### 📂 Estrutura de Pacotes Obrigatória
Os pacotes são organizados por **módulo de negócio** (ex: `user`, `workout`, `session`). Dentro de cada módulo:
```
src/main/groovy/br/dev/jsilveira/coresync/[modulo]/
├── domain/
│ ├── model/ # Records Groovy — Agregados, Entidades e Value Objects
│ └── exception/ # Exceções de negócio do módulo
├── application/
│ ├── port/
│ │ ├── in/ # Interfaces de entrada (Casos de Uso chamados pela API)
│ │ └── out/ # Interfaces de saída (o que o domínio precisa do mundo externo)
│ └── service/ # Implementa ports/in; consome ports/out
├── adapter/
│ ├── in/web/ # Controllers REST + DTOs de entrada/saída
│ └── out/persistence/ # Spring Data Repositories, Entities MongoDB, Mappers
└── config/ # Classes @Configuration com definições de @Bean
```
---
## 📜 Regras de Código
### 1. Modelagem com Records (Imutabilidade)
- Todas as Entidades, Agregados e Value Objects do domínio **devem ser `record` do Groovy**.
- O estado **nunca é mutado diretamente**. Toda transição de estado produz uma nova instância do record (padrão *copy-on-write*).
### 2. Validação no Construtor Compacto
- Toda validação de invariantes de negócio ocorre **no construtor compacto do record**.
- É **proibido** instanciar um objeto de domínio em estado inválido (IDs nulos, strings vazias, datas inconsistentes, dependências ausentes).
- Violations devem lançar a **exceção de negócio específica do módulo** (nunca `IllegalArgumentException` genérica).
### 3. Identificadores (UUID)
- O banco de dados **não gera IDs**. Cada entidade possui seu próprio `UUID id`.
- A geração do ID ocorre na camada **`application/service`** via `UUID.randomUUID()`, antes do envio ao adaptador de persistência.
- Referências entre Aggregate Roots são feitas **exclusivamente por ID** — nunca aninhe objetos raiz dentro de outros.
### 4. Zero Lombok
- Lombok é **proibido**. Use os recursos nativos do Groovy:
- Construtores de mapa (`new Foo(bar: "x")`)
- Propriedades automáticas (`@groovy.transform.CompileStatic`)
- `@Slf4j` nativo do Groovy para logging
- `record` para imutabilidade
### 5. Domínio Rico (Rich Domain)
- O domínio **não deve ser anêmico**. Lógicas de cálculo, transições de estado, formatações e validações pertinentes à entidade ficam **dentro do próprio record**.
- O `Service` orquestra; ele **não executa regras de negócio**.
### 6. Services são POJOs
- Classes de `application/service` **não recebem** `@Service` nem qualquer anotação do Spring.
- São classes Groovy puras instanciadas como `@Bean` via uma classe `@Configuration` no módulo.
- Dependências injetadas **exclusivamente pelo construtor**.
### 7. Nomenclatura
| Artefato | Convenção |
|---|---|
| Porta de entrada | `[Ação][Entidade]UseCase` (ex: `FinishSessionUseCase`) |
| Porta de saída — leitura | `Load[Entidade]Port` |
| Porta de saída — escrita | `Save[Entidade]Port` |
| Porta de saída — remoção | `Delete[Entidade]Port` |
| Serviço | `[Ação][Entidade]Service` |
| Controller | `[Entidade]Controller` |
| Entity (Mongo) | `[Entidade]Entity` |
| DTO de entrada | `[Entidade]Request` |
| DTO de saída | `[Entidade]Response` |
### 8. Mapeamento entre Camadas
- Conversão entre `DomainModel ↔ Entity` e `DomainModel ↔ DTO` ocorre em classes **Mapper dedicadas** em `adapter/out/persistence/` e `adapter/in/web/`, respectivamente.
- Nunca exponha uma `Entity` fora do adaptador de persistência.
- Nunca exponha um objeto de domínio diretamente em uma resposta HTTP.
### 9. Tratamento de Erros
- Exceções de negócio definidas em `domain/exception/` são capturadas por um `@ControllerAdvice` global no adaptador web.
- O `@ControllerAdvice` traduz exceções para respostas HTTP adequadas (ex: `SessionException``400 Bad Request`).
- O domínio **nunca conhece** códigos HTTP.
### 10. Segurança e Autenticação
- Toda autenticação é **stateless via JWT**.
- O token é validado no adaptador (`adapter/in/web/`) antes de qualquer chamada ao caso de uso.
- O `userId` extraído do token deve ser passado explicitamente como parâmetro ao caso de uso — nunca acessado diretamente de um contexto de segurança dentro da camada `application`.
---
## 💻 Exemplos de Código
### Modelo de Domínio — `record` com Construtor Compacto
**Local:** `session/domain/model/UserWorkoutSession.groovy`
```groovy
package br.dev.jsilveira.coresync.session.domain.model
import br.dev.jsilveira.coresync.session.domain.exception.SessionException
import java.time.LocalDateTime
import java.util.UUID
record UserWorkoutSession(
UUID id,
UUID userId,
UUID workoutDayId,
LocalDateTime startedAt,
LocalDateTime completedAt
) {
// Construtor compacto: validação de invariantes de negócio
public UserWorkoutSession {
if (!id) throw new SessionException("O ID não pode ser nulo")
if (!userId) throw new SessionException("O ID do usuário é obrigatório")
if (!workoutDayId) throw new SessionException("O ID do treino é obrigatório")
if (!startedAt) throw new SessionException("A data de início é obrigatória")
if (completedAt != null && completedAt.isBefore(startedAt)) {
throw new SessionException("A data de conclusão não pode ser anterior à data de início")
}
}
// Domínio Rico: transição de estado gera uma nova instância imutável
UserWorkoutSession finish(LocalDateTime time = LocalDateTime.now()) {
if (completedAt != null) throw new SessionException("A sessão já foi finalizada")
return new UserWorkoutSession(id, userId, workoutDayId, startedAt, time)
}
boolean isFinished() {
return completedAt != null
}
}
```
---
### Porta de Entrada (API)
**Local:** `session/application/port/in/FinishSessionUseCase.groovy`
```groovy
package br.dev.jsilveira.coresync.session.application.port.in
import java.util.UUID
interface FinishSessionUseCase {
void execute(UUID sessionId, UUID requestingUserId)
}
```
---
### Porta de Saída (SPI)
**Local:** `session/application/port/out/SaveSessionPort.groovy`
```groovy
package br.dev.jsilveira.coresync.session.application.port.out
import br.dev.jsilveira.coresync.session.domain.model.UserWorkoutSession
// O domínio declara o contrato; a implementação vive no adaptador
interface SaveSessionPort {
void save(UserWorkoutSession session)
}
```
**Local:** `session/application/port/out/LoadSessionPort.groovy`
```groovy
package br.dev.jsilveira.coresync.session.application.port.out
import br.dev.jsilveira.coresync.session.domain.model.UserWorkoutSession
import java.util.UUID
interface LoadSessionPort {
UserWorkoutSession load(UUID sessionId)
}
```
---
### Serviço (Caso de Uso) — POJO Groovy
**Local:** `session/application/service/FinishWorkoutSessionService.groovy`
```groovy
package br.dev.jsilveira.coresync.session.application.service
import br.dev.jsilveira.coresync.session.application.port.in.FinishSessionUseCase
import br.dev.jsilveira.coresync.session.application.port.out.LoadSessionPort
import br.dev.jsilveira.coresync.session.application.port.out.SaveSessionPort
import br.dev.jsilveira.coresync.session.domain.exception.SessionException
import java.util.UUID
// Sem @Service — instanciado via @Bean em SessionConfig
class FinishWorkoutSessionService implements FinishSessionUseCase {
private final LoadSessionPort loadPort
private final SaveSessionPort savePort
FinishWorkoutSessionService(LoadSessionPort loadPort, SaveSessionPort savePort) {
this.loadPort = loadPort
this.savePort = savePort
}
@Override
void execute(UUID sessionId, UUID requestingUserId) {
def session = loadPort.load(sessionId)
// Autorização de negócio: o usuário só pode finalizar a própria sessão
if (session.userId() != requestingUserId) {
throw new SessionException("Usuário não autorizado a finalizar esta sessão")
}
// Regra de negócio encapsulada no domínio
def finishedSession = session.finish()
savePort.save(finishedSession)
}
}
```
---
### Entity de Banco (Adaptador de Persistência)
**Local:** `session/adapter/out/persistence/entity/SessionEntity.groovy`
```groovy
package br.dev.jsilveira.coresync.session.adapter.out.persistence.entity
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
import java.time.LocalDateTime
import java.util.UUID
// Anotações de framework são permitidas apenas na camada adapter
@Document(collection = "workout_sessions")
record SessionEntity(
@Id UUID id,
UUID userId,
UUID workoutDayId,
LocalDateTime startedAt,
LocalDateTime completedAt
) {}
```
---
### Configuração do Módulo (Bean Wiring)
**Local:** `session/config/SessionConfig.groovy`
```groovy
package br.dev.jsilveira.coresync.session.config
import br.dev.jsilveira.coresync.session.application.port.out.LoadSessionPort
import br.dev.jsilveira.coresync.session.application.port.out.SaveSessionPort
import br.dev.jsilveira.coresync.session.application.service.FinishWorkoutSessionService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class SessionConfig {
@Bean
FinishWorkoutSessionService finishWorkoutSessionService(
LoadSessionPort loadPort,
SaveSessionPort savePort
) {
return new FinishWorkoutSessionService(loadPort, savePort)
}
}
```
---
## ✅ Checklist por Artefato
Antes de submeter qualquer código, verifique:
**Domain Model (record)**
- [ ] Validações completas no construtor compacto
- [ ] Sem anotações de Spring ou Mongo
- [ ] Transições de estado retornam nova instância (imutabilidade)
- [ ] Lógica de negócio pertinente está no record, não no service
**Application Service**
- [ ] Sem `@Service` ou qualquer anotação do Spring
- [ ] Injeção de dependência apenas por construtor
- [ ] Consome somente `ports/in` e `ports/out`
- [ ] Não contém regras de negócio (delega ao domínio)
**Adapter (Controller / Repository)**
- [ ] Controller recebe e retorna apenas DTOs (`*Request` / `*Response`)
- [ ] Mapeamento domínio ↔ DTO em Mapper dedicado
- [ ] Repository implementa a interface `port/out` correspondente
- [ ] Mapeamento domínio ↔ Entity em Mapper dedicado
**Geral**
- [ ] Nenhum import do Lombok em qualquer camada
- [ ] IDs gerados na camada `application`, nunca no banco
- [ ] Referências entre Aggregate Roots feitas apenas por UUID
- [ ] Exceções de negócio definidas em `domain/exception/`