agentleFS
rezendefabiorezendefabio/financas-lab/.claude/skills/feature/SKILL.md

feature

Skill0 starsChanged 6 months agoMITView on GitHub

Cria bounded context completo (24 arquivos Java = 16 producao + 8 testes) alinhado a docs/crud-patterns.md baseline. Inclui 5 use cases (Criar/Listar/BuscarPorId/Atualizar/Deletar), controller com auditoria + GET /{id}, userId, repository completo, DTOs e os 4 niveis de teste (domain, 5 useCases com Mockito, RepositoryImpl com Testcontainers, Controller E2E com 7 cenarios). Elimina necessidade de invocar /write-test para o baseline. Recebe nome do contexto em snake_case minusculo.

73 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use bug, bugfix and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • paulocsbSkill

    bug

    Investigation playbook for fixing a bug. Walks through 5 phases: reproduce → isolate → root cause → propose fix → validate. Forces concrete reasoning with code reads, log inspection, and git blame instead of pattern-matching guesses. Use for any non-trivial defect.

    Also in 8 projects that use this

  • team-attentionSkill

    bugfix

    Root cause based one-shot bug fix. debugger diagnosis → requirements.md generation → /execute. /bugfix "error description" Full investigation pipeline: debugger + gap-analyzer + standard verify. QA suggestion after successful fix.

    Also in 7 projects that use this

  • codebycoreySkill

    adopt

    Adopt an existing plan from conversation context into delta-spec format. Use when importing a plan, adopting planning that already happened, capturing plan mode output, or bringing in prior design work.

    Also in 4 projects that use this

  • Google CloudSkill

    fix

    Plan a remediation strategy after a test failure.

    Also in 10 projects that use this

  • marylinSkill

    recover

    Recover work from a crashed or interrupted session. Use when Claude crashed mid-task, after an unexpected exit, when you see 'stale sessions detected', or when you lost context and need to figure out what was happening.

    Also in 3 projects that use this

  • aegiswizardSkill

    context

    Also in 4 projects that use this

The file

We split this skill into the rules it gives an agent

1 rule. 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 67 edited versions we read keep these.

  • 0

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use Write para cada arquivo.

    line 65

“Common” is measured: kept by at least 6 in 10 of the 67 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Voce deve criar o bounded context $ARGUMENTS no projeto financas-lab gerando o baseline completo ja alinhado com docs/crud-patterns.md (secoes 1.1, 1.4, 1.5, 2.1, 2.4-2.7, 3, 4.1-4.4, 5.1, 5.2).

Fonte unica de padrao: docs/crud-patterns.md. Esta skill gera o gabarito canonico inline. O executor depois so adapta nomes/tipos de campos especificos do dominio (Money, enum, FK, M:N, soft-delete, state machine etc -- secoes 1.2, 1.3, 1.6, 1.7, 5.2.1, 10.x de crud-patterns). NAO ler outros bounded contexts como template -- os 15 arquivos abaixo ja sao o canonico.

Definicoes

  • ARG = $ARGUMENTS (ex: lembrete, meta_financeira)
  • NOME = PascalCase de ARG: capitalize a primeira letra de cada segmento separado por underscore e concatene (lembrete -> Lembrete, meta_financeira -> MetaFinanceira)
  • nome = camelCase de ARG (primeira letra minuscula): lembrete, metaFinanceira
  • nomes = plural simples (lembretes, metaFinanceiras) -- executor ajusta plural pt-BR no @RequestMapping se preciso
  • Pacote base: com.laboratorio.financas.ARG

Passo 0 -- Validacoes (ADR-011)

V1 -- formato: ARG deve casar com ^[a-z][a-z0-9_]*$. Se nao casar: "ERRO: argumento invalido -- use letras minusculas, digitos e underscore (ex: /feature lembrete)" e termine.

V2 -- existencia:

Test-Path "src/main/java/com/laboratorio/financas/ARG/"

Se True: "ERRO: bounded context 'ARG' ja existe" e termine.

Passo 1 -- Criar diretorios

[System.Environment]::CurrentDirectory = (Get-Location).Path
New-Item -ItemType Directory -Force `
  -Path "src/main/java/com/laboratorio/financas/ARG/domain", `
         "src/main/java/com/laboratorio/financas/ARG/application", `
         "src/main/java/com/laboratorio/financas/ARG/infrastructure/persistence", `
         "src/main/java/com/laboratorio/financas/ARG/interfaces", `
         "src/main/java/com/laboratorio/financas/ARG/interfaces/dto", `
         "src/test/java/com/laboratorio/financas/ARG/domain", `
         "src/test/java/com/laboratorio/financas/ARG/application", `
         "src/test/java/com/laboratorio/financas/ARG/infrastructure/persistence", `
         "src/test/java/com/laboratorio/financas/ARG/interfaces"

Verifique com Test-Path que os 9 diretorios foram criados. Se algum ausente: reporte qual falhou e termine.

Atencao: interfaces/ (NAO interfaces/rest/). Controller fica em interfaces/, DTOs em interfaces/dto/. Convencao do projeto.

Passo 2 -- Criar os 24 arquivos (16 producao + 8 testes)

Use Write para cada arquivo. Substitua NOME, ARG, nome, nomes pelos valores definidos. Codificacao: UTF-8 sem BOM.

ESTILO CHECKSTYLE OBRIGATORIO (o gate mvn test roda Checkstyle na fase validate -- estilo errado FALHA o build). Os templates abaixo ja estao no estilo correto; ao adaptar, MANTER:

  • Getter em multiplas linhas -- NUNCA public UUID getId() { return id; } numa linha (viola LeftCurly). Sempre:
    public UUID getId() {
        return id;
    }
  • if SEMPRE com chaves -- NUNCA if (x == null) return null; (viola NeedBraces). Sempre:
    if (x == null) {
        return null;
    }
  • Bloco vazio com espaco -- record Comando(...) { } e ValorMonetario(...) { } com espaco entre as chaves, NUNCA {} (viola WhitespaceAround).
  • Logger constante MAIUSCULA -- private static final Logger LOG (nunca log, viola ConstantName).
  • Sem import nao usado (UnusedImports), import na ordem, sem star import.

Arquivo 1: src/main/java/com/laboratorio/financas/ARG/domain/NOME.java

package com.laboratorio.financas.ARG.domain;

import java.time.Instant;
import java.util.Objects;
import java.util.UUID;

public final class NOME {

    private static final int NOME_MAX_LENGTH = 100;

    private final UUID id;
    private final UUID userId;
    private String nome;
    private boolean ativo;
    private final Instant criadoEm;
    private Instant atualizadoEm;

    public NOME(UUID userId, String nome) {
        this(UUID.randomUUID(), userId, nome, true, Instant.now(), Instant.now());
    }

    public NOME(UUID id, UUID userId, String nome,
                boolean ativo, Instant criadoEm, Instant atualizadoEm) {
        Objects.requireNonNull(id, "id nao pode ser nulo");
        Objects.requireNonNull(userId, "userId nao pode ser nulo");
        Objects.requireNonNull(criadoEm, "criadoEm nao pode ser nulo");
        validarNome(nome);

        this.id = id;
        this.userId = userId;
        this.nome = nome.trim();
        this.ativo = ativo;
        this.criadoEm = criadoEm;
        this.atualizadoEm = atualizadoEm;
    }

    private static void validarNome(String nome) {
        Objects.requireNonNull(nome, "nome nao pode ser nulo");
        String trimmed = nome.trim();
        if (trimmed.isEmpty()) {
            throw new IllegalArgumentException("nome nao pode ser vazio");
        }
        if (trimmed.length() > NOME_MAX_LENGTH) {
            throw new IllegalArgumentException(
                "nome nao pode ter mais de " + NOME_MAX_LENGTH + " caracteres");
        }
    }

    public void desativar() {
        this.ativo = false;
        this.atualizadoEm = Instant.now();
    }

    public void atualizar(String novoNome) {
        validarNome(novoNome);
        this.nome = novoNome.trim();
        this.atualizadoEm = Instant.now();
    }

    public UUID getId() {
        return id;
    }

    public UUID getUserId() {
        return userId;
    }

    public String getNome() {
        return nome;
    }

    public boolean isAtivo() {
        return ativo;
    }

    public Instant getCriadoEm() {
        return criadoEm;
    }

    public Instant getAtualizadoEm() {
        return atualizadoEm;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof NOME other)) {
            return false;
        }
        return this.id.equals(other.id);
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }
}

Arquivo 2: src/main/java/com/laboratorio/financas/ARG/domain/NOMERepository.java

package com.laboratorio.financas.ARG.domain;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

public interface NOMERepository {

    NOME salvar(NOME entidade);

    Optional<NOME> buscarPorId(UUID id);

    List<NOME> listarPorUserId(UUID userId);

    NOME atualizar(NOME entidade);

    void deletar(UUID id);
}

Arquivo 3: src/main/java/com/laboratorio/financas/ARG/domain/NOMENaoEncontradoException.java

package com.laboratorio.financas.ARG.domain;

import java.util.UUID;

public class NOMENaoEncontradoException extends RuntimeException {

    private final UUID id;

    public NOMENaoEncontradoException(UUID id) {
        super("NOME nao encontrado: " + id);
        this.id = id;
    }

    public UUID getId() {
        return id;
    }
}

Arquivo 4: src/main/java/com/laboratorio/financas/ARG/application/CriarNOMEUseCase.java

package com.laboratorio.financas.ARG.application;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.UUID;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class CriarNOMEUseCase {

    private final NOMERepository repository;

    public CriarNOMEUseCase(NOMERepository repository) {
        this.repository = repository;
    }

    public record Comando(UUID userId, String nome) { }

    @Transactional
    public NOME executar(Comando comando) {
        NOME entidade = new NOME(comando.userId(), comando.nome());
        return repository.salvar(entidade);
    }
}

Arquivo 5: src/main/java/com/laboratorio/financas/ARG/application/ListarNOMEsUseCase.java

package com.laboratorio.financas.ARG.application;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.List;
import java.util.UUID;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class ListarNOMEsUseCase {

    private final NOMERepository repository;

    public ListarNOMEsUseCase(NOMERepository repository) {
        this.repository = repository;
    }

    @Transactional(readOnly = true)
    public List<NOME> executar(UUID userId) {
        return repository.listarPorUserId(userId);
    }
}

Arquivo 5b: src/main/java/com/laboratorio/financas/ARG/application/BuscarNOMEPorIdUseCase.java

package com.laboratorio.financas.ARG.application;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMENaoEncontradoException;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.UUID;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class BuscarNOMEPorIdUseCase {

    private final NOMERepository repository;

    public BuscarNOMEPorIdUseCase(NOMERepository repository) {
        this.repository = repository;
    }

    @Transactional(readOnly = true)
    public NOME executar(UUID id) {
        return repository.buscarPorId(id)
                .orElseThrow(() -> new NOMENaoEncontradoException(id));
    }
}

Arquivo 6: src/main/java/com/laboratorio/financas/ARG/application/AtualizarNOMEUseCase.java

package com.laboratorio.financas.ARG.application;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMENaoEncontradoException;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.UUID;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class AtualizarNOMEUseCase {

    private final NOMERepository repository;

    public AtualizarNOMEUseCase(NOMERepository repository) {
        this.repository = repository;
    }

    public record Comando(UUID id, String nome) { }

    @Transactional
    public NOME executar(Comando comando) {
        NOME entidade = repository.buscarPorId(comando.id())
                .orElseThrow(() -> new NOMENaoEncontradoException(comando.id()));
        entidade.atualizar(comando.nome());
        return repository.atualizar(entidade);
    }
}

Arquivo 7: src/main/java/com/laboratorio/financas/ARG/application/DeletarNOMEUseCase.java

package com.laboratorio.financas.ARG.application;

import com.laboratorio.financas.ARG.domain.NOMENaoEncontradoException;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.UUID;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class DeletarNOMEUseCase {

    private final NOMERepository repository;

    public DeletarNOMEUseCase(NOMERepository repository) {
        this.repository = repository;
    }

    @Transactional
    public void executar(UUID id) {
        repository.buscarPorId(id)
                .orElseThrow(() -> new NOMENaoEncontradoException(id));
        repository.deletar(id);
    }
}

Arquivo 8: src/main/java/com/laboratorio/financas/ARG/infrastructure/persistence/NOMEEntity.java

package com.laboratorio.financas.ARG.infrastructure.persistence;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
import java.util.UUID;

@Entity
@Table(name = "ARG")
public class NOMEEntity {

    @Id
    @Column(name = "id", columnDefinition = "uuid", nullable = false, updatable = false)
    private UUID id;

    @NotNull
    @Column(name = "user_id", columnDefinition = "uuid", nullable = false)
    private UUID userId;

    @NotNull
    @Column(name = "nome", nullable = false, length = 100)
    private String nome;

    @NotNull
    @Column(name = "ativo", nullable = false)
    private boolean ativo;

    @NotNull
    @Column(name = "criado_em", nullable = false, updatable = false)
    private Instant criadoEm;

    @NotNull
    @Column(name = "atualizado_em", nullable = false)
    private Instant atualizadoEm;

    protected NOMEEntity() {
        // Construtor protected exigido pelo JPA.
    }

    public NOMEEntity(UUID id, UUID userId, String nome,
                      boolean ativo, Instant criadoEm, Instant atualizadoEm) {
        this.id = id;
        this.userId = userId;
        this.nome = nome;
        this.ativo = ativo;
        this.criadoEm = criadoEm;
        this.atualizadoEm = atualizadoEm;
    }

    public UUID getId() {
        return id;
    }

    public UUID getUserId() {
        return userId;
    }

    public String getNome() {
        return nome;
    }

    public boolean isAtivo() {
        return ativo;
    }

    public Instant getCriadoEm() {
        return criadoEm;
    }

    public Instant getAtualizadoEm() {
        return atualizadoEm;
    }
}

Arquivo 9: src/main/java/com/laboratorio/financas/ARG/infrastructure/persistence/NOMEJpaRepository.java

package com.laboratorio.financas.ARG.infrastructure.persistence;

import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;

public interface NOMEJpaRepository extends JpaRepository<NOMEEntity, UUID> {

    List<NOMEEntity> findByUserId(UUID userId);
}

Arquivo 10: src/main/java/com/laboratorio/financas/ARG/infrastructure/persistence/NOMEMapper.java

package com.laboratorio.financas.ARG.infrastructure.persistence;

import com.laboratorio.financas.ARG.domain.NOME;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface NOMEMapper {

    default NOMEEntity toEntity(NOME domain) {
        if (domain == null) {
            return null;
        }
        return new NOMEEntity(
                domain.getId(),
                domain.getUserId(),
                domain.getNome(),
                domain.isAtivo(),
                domain.getCriadoEm(),
                domain.getAtualizadoEm()
        );
    }

    default NOME toDomain(NOMEEntity entity) {
        if (entity == null) {
            return null;
        }
        return new NOME(
                entity.getId(),
                entity.getUserId(),
                entity.getNome(),
                entity.isAtivo(),
                entity.getCriadoEm(),
                entity.getAtualizadoEm()
        );
    }
}

Arquivo 11: src/main/java/com/laboratorio/financas/ARG/infrastructure/persistence/NOMERepositoryImpl.java

package com.laboratorio.financas.ARG.infrastructure.persistence;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.stereotype.Component;

@Component
public class NOMERepositoryImpl implements NOMERepository {

    private final NOMEJpaRepository jpaRepository;
    private final NOMEMapper mapper;

    public NOMERepositoryImpl(NOMEJpaRepository jpaRepository, NOMEMapper mapper) {
        this.jpaRepository = jpaRepository;
        this.mapper = mapper;
    }

    @Override
    public NOME salvar(NOME entidade) {
        return mapper.toDomain(jpaRepository.save(mapper.toEntity(entidade)));
    }

    @Override
    public Optional<NOME> buscarPorId(UUID id) {
        return jpaRepository.findById(id).map(mapper::toDomain);
    }

    @Override
    public List<NOME> listarPorUserId(UUID userId) {
        return jpaRepository.findByUserId(userId).stream()
                .map(mapper::toDomain)
                .toList();
    }

    @Override
    public NOME atualizar(NOME entidade) {
        return mapper.toDomain(jpaRepository.save(mapper.toEntity(entidade)));
    }

    @Override
    public void deletar(UUID id) {
        jpaRepository.deleteById(id);
    }
}

Arquivo 12: src/main/java/com/laboratorio/financas/ARG/interfaces/NOMEController.java

package com.laboratorio.financas.ARG.interfaces;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.laboratorio.financas.auditlog.domain.AuditAction;
import com.laboratorio.financas.auditlog.domain.AuditEvent;
import com.laboratorio.financas.auditlog.infrastructure.AuditPublisher;
import com.laboratorio.financas.ARG.application.AtualizarNOMEUseCase;
import com.laboratorio.financas.ARG.application.BuscarNOMEPorIdUseCase;
import com.laboratorio.financas.ARG.application.CriarNOMEUseCase;
import com.laboratorio.financas.ARG.application.DeletarNOMEUseCase;
import com.laboratorio.financas.ARG.application.ListarNOMEsUseCase;
import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.interfaces.dto.AtualizarNOMERequest;
import com.laboratorio.financas.ARG.interfaces.dto.CriarNOMERequest;
import com.laboratorio.financas.ARG.interfaces.dto.NOMEResponse;
import com.laboratorio.financas.usuario.domain.UsuarioRepository;
import jakarta.validation.Valid;
import java.util.List;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

// TODO: ajustar /api/nomes para plural correto pt-BR se nao for o padrao default.
@RestController
@RequestMapping("/api/nomes")
public class NOMEController {

    private static final Logger LOG = LoggerFactory.getLogger(NOMEController.class);
    private static final String ENTITY_TYPE = "ARG";

    private final CriarNOMEUseCase criarUseCase;
    private final ListarNOMEsUseCase listarUseCase;
    private final BuscarNOMEPorIdUseCase buscarPorIdUseCase;
    private final AtualizarNOMEUseCase atualizarUseCase;
    private final DeletarNOMEUseCase deletarUseCase;
    private final UsuarioRepository usuarioRepository;
    private final AuditPublisher auditPublisher;
    private final ObjectMapper objectMapper;

    public NOMEController(
            CriarNOMEUseCase criarUseCase,
            ListarNOMEsUseCase listarUseCase,
            BuscarNOMEPorIdUseCase buscarPorIdUseCase,
            AtualizarNOMEUseCase atualizarUseCase,
            DeletarNOMEUseCase deletarUseCase,
            UsuarioRepository usuarioRepository,
            AuditPublisher auditPublisher,
            ObjectMapper objectMapper) {
        this.criarUseCase = criarUseCase;
        this.listarUseCase = listarUseCase;
        this.buscarPorIdUseCase = buscarPorIdUseCase;
        this.atualizarUseCase = atualizarUseCase;
        this.deletarUseCase = deletarUseCase;
        this.usuarioRepository = usuarioRepository;
        this.auditPublisher = auditPublisher;
        this.objectMapper = objectMapper;
    }

    @GetMapping
    public List<NOMEResponse> listar(Authentication authentication) {
        UUID userId = resolverUserId(authentication);
        return listarUseCase.executar(userId).stream()
                .map(NOMEResponse::fromDomain)
                .toList();
    }

    @GetMapping("/{id}")
    public NOMEResponse buscar(
            @PathVariable UUID id,
            Authentication authentication) {
        resolverUserId(authentication);
        return NOMEResponse.fromDomain(buscarPorIdUseCase.executar(id));
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public NOMEResponse criar(
            @Valid @RequestBody CriarNOMERequest request,
            Authentication authentication,
            @RequestHeader(value = "X-Screen-Code", required = false) String screenCode) {
        UUID userId = resolverUserId(authentication);
        CriarNOMEUseCase.Comando comando = new CriarNOMEUseCase.Comando(userId, request.nome());
        NOMEResponse response = NOMEResponse.fromDomain(criarUseCase.executar(comando));
        auditPublisher.publish(new AuditEvent(
                ENTITY_TYPE, response.id(), AuditAction.CREATE,
                userEmail(authentication), screenCode, null, toJson(response)));
        return response;
    }

    @PutMapping("/{id}")
    public NOMEResponse atualizar(
            @PathVariable UUID id,
            @Valid @RequestBody AtualizarNOMERequest request,
            Authentication authentication,
            @RequestHeader(value = "X-Screen-Code", required = false) String screenCode) {
        // Chamar sem atribuir -- so valida o usuario autenticado. NAO fazer
        // `UUID userId = resolverUserId(...)` aqui: userId nao e usado em
        // atualizar() (so o id do PathVariable importa) e SpotBugs rejeita
        // como DLS_DEAD_LOCAL_STORE.
        resolverUserId(authentication);
        AtualizarNOMEUseCase.Comando comando = new AtualizarNOMEUseCase.Comando(id, request.nome());
        NOMEResponse response = NOMEResponse.fromDomain(atualizarUseCase.executar(comando));
        auditPublisher.publish(new AuditEvent(
                ENTITY_TYPE, id, AuditAction.UPDATE,
                userEmail(authentication), screenCode, null, toJson(response)));
        return response;
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deletar(
            @PathVariable UUID id,
            Authentication authentication,
            @RequestHeader(value = "X-Screen-Code", required = false) String screenCode) {
        // Mesma regra do atualizar(): chamar SEM atribuir. SpotBugs DLS_DEAD_LOCAL_STORE.
        resolverUserId(authentication);
        deletarUseCase.executar(id);
        auditPublisher.publish(new AuditEvent(
                ENTITY_TYPE, id, AuditAction.DELETE,
                userEmail(authentication), screenCode, null, null));
    }

    private UUID resolverUserId(Authentication authentication) {
        String email = authentication.getName();
        return usuarioRepository.buscarPorEmail(email)
                .orElseThrow(() -> new IllegalStateException(
                        "Usuario autenticado nao encontrado: " + email))
                .getId();
    }

    private String userEmail(Authentication authentication) {
        return (authentication != null) ? authentication.getName() : null;
    }

    private String toJson(Object obj) {
        if (obj == null) {
            return null;
        }
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException ex) {
            LOG.warn("Falha ao serializar payload de audit log para {}", ENTITY_TYPE, ex);
            return null;
        }
    }
}

Arquivo 13: src/main/java/com/laboratorio/financas/ARG/interfaces/dto/CriarNOMERequest.java

package com.laboratorio.financas.ARG.interfaces.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record CriarNOMERequest(
        @NotBlank @Size(max = 100) String nome
) { }

Arquivo 14: src/main/java/com/laboratorio/financas/ARG/interfaces/dto/AtualizarNOMERequest.java

package com.laboratorio.financas.ARG.interfaces.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record AtualizarNOMERequest(
        @NotBlank @Size(max = 100) String nome
) { }

Arquivo 15: src/main/java/com/laboratorio/financas/ARG/interfaces/dto/NOMEResponse.java

package com.laboratorio.financas.ARG.interfaces.dto;

import com.laboratorio.financas.ARG.domain.NOME;
import java.util.UUID;

public record NOMEResponse(
        UUID id,
        UUID userId,
        String nome,
        boolean ativo,
        String criadoEm,
        String atualizadoEm
) {
    public static NOMEResponse fromDomain(NOME domain) {
        return new NOMEResponse(
                domain.getId(),
                domain.getUserId(),
                domain.getNome(),
                domain.isAtivo(),
                domain.getCriadoEm().toString(),
                domain.getAtualizadoEm().toString()
        );
    }
}

Arquivo 16: src/test/java/com/laboratorio/financas/ARG/domain/NOMETest.java

package com.laboratorio.financas.ARG.domain;

import static org.assertj.core.api.Assertions.*;

import java.time.Instant;
import java.util.UUID;
import org.junit.jupiter.api.Test;

class NOMETest {

    private static final UUID USER_ID = UUID.randomUUID();

    @Test
    void construtorCriacaoComArgumentosValidosCriaEntidade() {
        Instant antes = Instant.now();
        NOME entidade = new NOME(USER_ID, "Nome Valido");
        Instant depois = Instant.now();

        assertThat(entidade.getId()).isNotNull();
        assertThat(entidade.getUserId()).isEqualTo(USER_ID);
        assertThat(entidade.getNome()).isEqualTo("Nome Valido");
        assertThat(entidade.isAtivo()).isTrue();
        assertThat(entidade.getCriadoEm()).isBetween(antes, depois);
    }

    @Test
    void construtorCriacaoComUserIdNuloLancaNullPointerException() {
        assertThatNullPointerException()
                .isThrownBy(() -> new NOME(null, "Nome"))
                .withMessageContaining("userId");
    }

    @Test
    void construtorCriacaoComNomeNuloLancaNullPointerException() {
        assertThatNullPointerException()
                .isThrownBy(() -> new NOME(USER_ID, null))
                .withMessageContaining("nome");
    }

    @Test
    void construtorCriacaoComNomeBlankLancaIllegalArgumentException() {
        assertThatIllegalArgumentException()
                .isThrownBy(() -> new NOME(USER_ID, "  "))
                .withMessageContaining("nome");
    }

    @Test
    void desativarMudaAtivoParaFalseEAtualizaTimestamp() {
        NOME entidade = new NOME(USER_ID, "Nome");
        Instant antes = entidade.getAtualizadoEm();

        entidade.desativar();

        assertThat(entidade.isAtivo()).isFalse();
        assertThat(entidade.getAtualizadoEm()).isAfterOrEqualTo(antes);
    }

    @Test
    void atualizarMudaNomeEAtualizaTimestamp() {
        NOME entidade = new NOME(USER_ID, "Nome Antigo");

        entidade.atualizar("Nome Novo");

        assertThat(entidade.getNome()).isEqualTo("Nome Novo");
    }
}

Arquivo 17: src/test/java/com/laboratorio/financas/ARG/application/CriarNOMEUseCaseTest.java

package com.laboratorio.financas.ARG.application;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class CriarNOMEUseCaseTest {

    private static final UUID USER_ID = UUID.fromString("00000000-0000-0000-0000-000000000099");

    private NOMERepository repository;
    private CriarNOMEUseCase useCase;

    @BeforeEach
    void setUp() {
        repository = Mockito.mock(NOMERepository.class);
        useCase = new CriarNOMEUseCase(repository);
    }

    @Test
    void executarCaminhoFelizSalvaERetornaEntidade() {
        NOME salvo = new NOME(USER_ID, "Teste");
        when(repository.salvar(any(NOME.class))).thenReturn(salvo);

        CriarNOMEUseCase.Comando cmd = new CriarNOMEUseCase.Comando(USER_ID, "Teste");
        NOME resultado = useCase.executar(cmd);

        assertThat(resultado).isNotNull();
        assertThat(resultado.getNome()).isEqualTo("Teste");
        verify(repository, times(1)).salvar(any(NOME.class));
    }
}

Arquivo 18: src/test/java/com/laboratorio/financas/ARG/application/ListarNOMEsUseCaseTest.java

package com.laboratorio.financas.ARG.application;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.when;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class ListarNOMEsUseCaseTest {

    private static final UUID USER_ID = UUID.fromString("00000000-0000-0000-0000-000000000099");

    private NOMERepository repository;
    private ListarNOMEsUseCase useCase;

    @BeforeEach
    void setUp() {
        repository = Mockito.mock(NOMERepository.class);
        useCase = new ListarNOMEsUseCase(repository);
    }

    @Test
    void executarRetornaListaDoRepository() {
        when(repository.listarPorUserId(USER_ID))
                .thenReturn(List.of(new NOME(USER_ID, "A"), new NOME(USER_ID, "B")));

        List<NOME> resultado = useCase.executar(USER_ID);

        assertThat(resultado).hasSize(2);
        assertThat(resultado).allMatch(n -> n.getUserId().equals(USER_ID));
    }
}

Arquivo 18b: src/test/java/com/laboratorio/financas/ARG/application/BuscarNOMEPorIdUseCaseTest.java

package com.laboratorio.financas.ARG.application;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.when;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMENaoEncontradoException;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class BuscarNOMEPorIdUseCaseTest {

    private static final UUID USER_ID = UUID.fromString("00000000-0000-0000-0000-000000000099");

    private NOMERepository repository;
    private BuscarNOMEPorIdUseCase useCase;

    @BeforeEach
    void setUp() {
        repository = Mockito.mock(NOMERepository.class);
        useCase = new BuscarNOMEPorIdUseCase(repository);
    }

    @Test
    void executarComIdExistenteRetornaEntidade() {
        NOME existente = new NOME(USER_ID, "Teste");
        when(repository.buscarPorId(existente.getId())).thenReturn(Optional.of(existente));

        NOME resultado = useCase.executar(existente.getId());

        assertThat(resultado).isNotNull();
        assertThat(resultado.getNome()).isEqualTo("Teste");
    }

    @Test
    void executarComIdInexistenteLancaNOMENaoEncontradoException() {
        UUID id = UUID.randomUUID();
        when(repository.buscarPorId(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> useCase.executar(id))
                .isInstanceOf(NOMENaoEncontradoException.class);
    }
}

Arquivo 19: src/test/java/com/laboratorio/financas/ARG/application/AtualizarNOMEUseCaseTest.java

package com.laboratorio.financas.ARG.application;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMENaoEncontradoException;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class AtualizarNOMEUseCaseTest {

    private static final UUID USER_ID = UUID.fromString("00000000-0000-0000-0000-000000000099");

    private NOMERepository repository;
    private AtualizarNOMEUseCase useCase;

    @BeforeEach
    void setUp() {
        repository = Mockito.mock(NOMERepository.class);
        useCase = new AtualizarNOMEUseCase(repository);
    }

    @Test
    void executarComIdExistenteAtualizaNome() {
        NOME existente = new NOME(USER_ID, "Antigo");
        when(repository.buscarPorId(existente.getId())).thenReturn(Optional.of(existente));
        when(repository.atualizar(any(NOME.class))).thenAnswer(inv -> inv.getArgument(0));

        NOME resultado = useCase.executar(new AtualizarNOMEUseCase.Comando(existente.getId(), "Novo"));

        assertThat(resultado.getNome()).isEqualTo("Novo");
        verify(repository).atualizar(any(NOME.class));
    }

    @Test
    void executarComIdInexistenteLancaNOMENaoEncontradoException() {
        UUID id = UUID.randomUUID();
        when(repository.buscarPorId(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> useCase.executar(new AtualizarNOMEUseCase.Comando(id, "X")))
                .isInstanceOf(NOMENaoEncontradoException.class);

        verify(repository, never()).atualizar(any());
    }
}

Arquivo 20: src/test/java/com/laboratorio/financas/ARG/application/DeletarNOMEUseCaseTest.java

package com.laboratorio.financas.ARG.application;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.ARG.domain.NOMENaoEncontradoException;
import com.laboratorio.financas.ARG.domain.NOMERepository;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class DeletarNOMEUseCaseTest {

    private static final UUID USER_ID = UUID.fromString("00000000-0000-0000-0000-000000000099");

    private NOMERepository repository;
    private DeletarNOMEUseCase useCase;

    @BeforeEach
    void setUp() {
        repository = Mockito.mock(NOMERepository.class);
        useCase = new DeletarNOMEUseCase(repository);
    }

    @Test
    void executarComIdExistenteDeleta() {
        UUID id = UUID.randomUUID();
        when(repository.buscarPorId(id)).thenReturn(Optional.of(new NOME(USER_ID, "A")));

        useCase.executar(id);

        verify(repository).deletar(id);
    }

    @Test
    void executarComIdInexistenteLancaExcecaoENaoDeleta() {
        UUID id = UUID.randomUUID();
        when(repository.buscarPorId(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> useCase.executar(id))
                .isInstanceOf(NOMENaoEncontradoException.class);

        verify(repository, never()).deletar(any());
    }
}

Arquivo 21: src/test/java/com/laboratorio/financas/ARG/infrastructure/persistence/NOMERepositoryImplTest.java

package com.laboratorio.financas.ARG.infrastructure.persistence;

import static org.assertj.core.api.Assertions.*;

import com.laboratorio.financas.ARG.domain.NOME;
import com.laboratorio.financas.shared.AbstractIntegrationTest;
import com.laboratorio.financas.usuario.infrastructure.persistence.UsuarioEntity;
import com.laboratorio.financas.usuario.infrastructure.persistence.UsuarioJpaRepository;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

class NOMERepositoryImplTest extends AbstractIntegrationTest {

    @Autowired
    private NOMERepositoryImpl repository;
    @Autowired
    private NOMEJpaRepository jpaRepository;
    @Autowired
    private UsuarioJpaRepository usuarioJpaRepository;

    private UUID userId;

    @BeforeEach
    void setUp() {
        jpaRepository.deleteAll();
        usuarioJpaRepository.deleteAll();
        userId = criarUsuarioPersistido();
    }

    @AfterEach
    void limpar() {
        jpaRepository.deleteAll();
        usuarioJpaRepository.deleteAll();
    }

    private UUID criarUsuarioPersistido() {
        UUID id = UUID.randomUUID();
        usuarioJpaRepository.save(new UsuarioEntity(
                id, "teste+" + id + "@test.com", "hash", true,
                Instant.now(), null, Instant.now()));
        return id;
    }

    @Test
    void salvarEBuscarPorIdRetornaEntidadePersistida() {
        NOME entidade = new NOME(userId, "Teste");
        repository.salvar(entidade);

        Optional<NOME> resultado = repository.buscarPorId(entidade.getId());

        assertThat(resultado).isPresent();
        assertThat(resultado.get().getNome()).isEqualTo("Teste");
    }

    @Test
    void listarPorUserIdRetornaSomenteDoUsuario() {
        repository.salvar(new NOME(userId, "A"));
        repository.salvar(new NOME(userId, "B"));
        UUID outroUserId = criarUsuarioPersistido();
        repository.salvar(new NOME(outroUserId, "Outro"));

        List<NOME> resultado = repository.listarPorUserId(userId);

        assertThat(resultado).hasSize(2);
        assertThat(resultado).allMatch(n -> n.getUserId().equals(userId));
    }

    @Test
    void deletarRemoveEntidade() {
        NOME entidade = new NOME(userId, "Para deletar");
        repository.salvar(entidade);

        repository.deletar(entidade.getId());

        assertThat(repository.buscarPorId(entidade.getId())).isEmpty();
    }
}

Arquivo 22: src/test/java/com/laboratorio/financas/ARG/interfaces/NOMEControllerTest.java

package com.laboratorio.financas.ARG.interfaces;

import static org.assertj.core.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.laboratorio.financas.ARG.infrastructure.persistence.NOMEJpaRepository;
import com.laboratorio.financas.shared.AbstractAuthenticatedIntegrationTest;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;

class NOMEControllerTest extends AbstractAuthenticatedIntegrationTest {

    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private NOMEJpaRepository jpaRepository;

    @BeforeEach
    void setUp() {
        jpaRepository.deleteAll();
    }

    @AfterEach
    void limpar() {
        jpaRepository.deleteAll();
    }

    @Test
    void postValidoRetorna201() throws Exception {
        Map<String, Object> body = Map.of("nome", "Teste");
        mockMvc.perform(comAuth(post("/api/nomes")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(body))))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").isNotEmpty())
                .andExpect(jsonPath("$.nome").value("Teste"));
    }

    @Test
    void postNomeBlankRetorna400() throws Exception {
        Map<String, Object> body = Map.of("nome", "  ");
        mockMvc.perform(comAuth(post("/api/nomes")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(body))))
                .andExpect(status().isBadRequest());
    }

    @Test
    void getListaRetornaArray() throws Exception {
        mockMvc.perform(comAuth(post("/api/nomes")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(Map.of("nome", "A")))));

        mockMvc.perform(comAuth(get("/api/nomes")))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isArray());
    }

    @Test
    void getPorIdExistenteRetorna200() throws Exception {
        MvcResult r = mockMvc.perform(comAuth(post("/api/nomes")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(Map.of("nome", "Buscavel")))))
                .andReturn();
        String id = objectMapper.readTree(r.getResponse().getContentAsString()).get("id").asText();

        mockMvc.perform(comAuth(get("/api/nomes/" + id)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.nome").value("Buscavel"));
    }

    @Test
    void getPorIdInexistenteRetorna404() throws Exception {
        mockMvc.perform(comAuth(get("/api/nomes/" + UUID.randomUUID())))
                .andExpect(status().isNotFound());
    }

    @Test
    void putExistenteRetorna200ComNomeAtualizado() throws Exception {
        MvcResult r = mockMvc.perform(comAuth(post("/api/nomes")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(Map.of("nome", "Antigo")))))
                .andReturn();
        String id = objectMapper.readTree(r.getResponse().getContentAsString()).get("id").asText();

        mockMvc.perform(comAuth(put("/api/nomes/" + id)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(Map.of("nome", "Novo")))))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.nome").value("Novo"));
    }

    @Test
    void putInexistenteRetorna404() throws Exception {
        mockMvc.perform(comAuth(put("/api/nomes/" + UUID.randomUUID())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(Map.of("nome", "X")))))
                .andExpect(status().isNotFound());
    }

    @Test
    void deleteExistenteRetorna204() throws Exception {
        MvcResult r = mockMvc.perform(comAuth(post("/api/nomes")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(Map.of("nome", "Apagar")))))
                .andReturn();
        String id = objectMapper.readTree(r.getResponse().getContentAsString()).get("id").asText();

        mockMvc.perform(comAuth(delete("/api/nomes/" + id)))
                .andExpect(status().isNoContent());
    }

    @Test
    void semAuthRetorna401() throws Exception {
        mockMvc.perform(get("/api/nomes")).andExpect(status().isUnauthorized());
    }
}

Passo 3 -- Verificar criacao

Get-ChildItem -Recurse "src/main/java/com/laboratorio/financas/ARG/" -File | Select-Object FullName
Get-ChildItem -Recurse "src/test/java/com/laboratorio/financas/ARG/" -File | Select-Object FullName

Confirme que os 24 arquivos existem (16 producao + 8 testes). Se algum ausente: reporte qual falta e nao emita o relatorio de sucesso.

Passo 4 -- Guia de adaptacao (para o executor seguir DEPOIS desta skill)

Os 24 arquivos sao gerados com nome: String como campo placeholder. O dominio real (Emprestimo, Lembrete, Investimento, etc.) tem campos especificos diferentes. A adaptacao e trabalho real, mas SEGUIR A ORDEM ABAIXO reduz Updates redundantes (observado nos smokes: cada arquivo "Updated" depois de "Written" custa tool calls; planejar bem evita ate ~30% dessas iteracoes).

Ordem sistematica de adaptacao (top-down, fonte-de-verdade primeiro):

  1. domain/NOME.java -- definir TODOS os campos finais aqui. Validacoes, construtores, getters, equals. ESTA e a fonte de verdade do dominio.
  2. infrastructure/persistence/NOMEEntity.java -- espelhar campos de NOME.java com @Column, @Embedded, @Enumerated. Cada campo do domain tem uma coluna correspondente.
  3. infrastructure/persistence/NOMEMapper.java -- mapear os novos campos em toEntity() e toDomain(). Cada campo novo no domain precisa de uma linha aqui.
  4. interfaces/dto/CriarNOMERequest.java + AtualizarNOMERequest.java -- campos correspondentes aos do domain (sem id/timestamps/userId que vem do contexto), com @NotNull/@Size/@NotBlank conforme regra do dominio.
  5. interfaces/dto/NOMEResponse.java -- campos a expor + fromDomain() mapeia todos.
  6. application/Criar/Atualizar*UseCase.java -- atualizar o record Comando com os novos campos e o executar() para passa-los ao construtor do dominio.
  7. interfaces/NOMEController.java -- passar os novos campos do Request ao Comando.
  8. migration V<N>__cria_tabela_NOME.sql -- colunas espelhando NOMEEntity (precision Money = 19,2; tabelas em portugues singular; FK usuario REFERENCES usuario(id)).
  9. Tests (8 arquivos) -- adaptar assertions para os novos campos. Os tests gerados testam nome; substituir/adicionar testes para cada campo do dominio com validacao especifica (positivo, max, enum).

Regra de ouro: adapte um arquivo COMPLETAMENTE antes de ir para o proximo. NUNCA edite parcialmente e volte. Se descobrir que precisa de um campo novo, volte para o passo 1 (domain) e refaca a cascata. Voltar e re-editar parcialmente e o que causa os Updates redundantes.

Anti-pattern observado nos smokes (NAO fazer):

  • Editar dominio com 2 campos, depois Entity, depois lembrar de 1 campo a mais, voltar para dominio, voltar para Entity = 4 Updates onde poderia haver 2.
  • Editar Mapper antes de fechar Entity = re-edita Mapper quando Entity muda.

Adapter de uma vez todos os arquivos relacionados a um campo conceitual e preferivel a adaptar todos os campos em um arquivo de cada vez. Ou seja, quando adicionar o campo valor (Money), faca dominio + Entity + Mapper + DTOs

  • Response para esse campo em sequencia. Nao misture com outros campos no meio.

Passo 5 -- Relatorio final

Bounded context 'ARG' criado (baseline completo, 24 arquivos = 16 producao + 8 testes).

Producao (16):
  domain/NOME.java
  domain/NOMERepository.java
  domain/NOMENaoEncontradoException.java
  application/CriarNOMEUseCase.java
  application/ListarNOMEsUseCase.java
  application/BuscarNOMEPorIdUseCase.java
  application/AtualizarNOMEUseCase.java
  application/DeletarNOMEUseCase.java
  infrastructure/persistence/NOMEEntity.java
  infrastructure/persistence/NOMEJpaRepository.java
  infrastructure/persistence/NOMEMapper.java
  infrastructure/persistence/NOMERepositoryImpl.java
  interfaces/NOMEController.java       (com auditoria + GET /{id})
  interfaces/dto/CriarNOMERequest.java
  interfaces/dto/AtualizarNOMERequest.java
  interfaces/dto/NOMEResponse.java

Testes (8, cobrindo os 4 niveis -- secao 6 de crud-patterns):
  domain/NOMETest.java                                 (unit, JUnit + AssertJ)
  application/CriarNOMEUseCaseTest.java                (Mockito)
  application/ListarNOMEsUseCaseTest.java              (Mockito)
  application/BuscarNOMEPorIdUseCaseTest.java          (Mockito)
  application/AtualizarNOMEUseCaseTest.java            (Mockito)
  application/DeletarNOMEUseCaseTest.java              (Mockito)
  infrastructure/persistence/NOMERepositoryImplTest.java (Testcontainers)
  interfaces/NOMEControllerTest.java                   (MockMvc E2E, 7 cenarios)

PROXIMOS PASSOS (responsabilidade do executor):
  1. Adaptar campos especificos do dominio (Money, enum, FK, M:N, soft-delete,
     state machine etc) seguindo as secoes correspondentes de docs/crud-patterns.md
     (1.2, 1.3, 1.6, 1.7, 5.2.1, 10.x). NAO ler outros bounded contexts.
     IMPORTANTE: ao renomear/adicionar campos no NOME.java/NOMEEntity.java, ATUALIZAR
     os testes correspondentes (NOMETest, *UseCaseTest, *RepositoryImplTest, *ControllerTest)
     para refletir os novos campos. Os testes gerados ja cobrem o baseline -- expanda;
     nao recrie.
  2. Criar migration Flyway V<N>__cria_tabela_ARG.sql (numero reservado no prompt da task).
     FK para usuario usa `REFERENCES usuario(id)` -- NAO `users(id)`.
  3. Ajustar /api/nomes para plural pt-BR se necessario (ex: /api/lembretes)
  4. Adicionar handler de NOMENaoEncontradoException no GlobalExceptionHandler
     (secao 3 de crud-patterns.md -- ProblemDetail, NAO void)
  5. Registrar ENTITY_TYPE 'ARG' no middleware de auditoria via /add-entity-to-audit
  6. NAO invocar /write-test para os 4 niveis de teste backend -- ja foram criados.
     /write-test so e necessario se a expansao do dominio (passo 1) adicionar um arquivo
     novo de producao que merece teste proprio (ex: um value object com regra propria).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.