Fundamentos de Qualidade
~40 min Avançado Parte 6

Fundamentos de Qualidade de Software

O glossário de referência para todos os workflows. Clean Code, SOLID, arquitetura hexagonal, TDD, pirâmide de testes, OWASP Top 10 e disciplina de janela de contexto.

Clean Code + SOLID

SOLID é um conjunto de cinco princípios de design orientado a objetos formulados por Robert C. Martin. Cada princípio resolve uma categoria específica de problema de manutenibilidade. Violações custam caro: código difícil de testar, de estender e de entender. Quando você instrui o Claude a seguir SOLID explicitamente no CLAUDE.md, o modelo aplica esses critérios sem necessidade de reforço por sessão.

S — Single Responsibility Principle

Uma classe ou função deve ter uma única razão para mudar. O sinal prático: se você precisa de "e" para descrever o que a função faz, ela viola SRP.

// Ruim — valida E persiste E envia email
async function processUserRegistration(data: unknown) {
  if (!data || typeof data !== 'object') throw new Error('invalid');
  const user = await db.users.create(data as any);
  await mailer.send({ to: user.email, subject: 'Welcome' });
  return user;
}

// Bom — cada função tem uma responsabilidade
function validateRegistrationPayload(data: unknown): RegistrationDTO {
  const parsed = RegistrationSchema.safeParse(data);
  if (!parsed.success) throw new ValidationError(parsed.error);
  return parsed.data;
}

async function createUser(dto: RegistrationDTO): Promise<User> {
  return db.users.create(dto);
}

async function sendWelcomeEmail(user: User): Promise<void> {
  await mailer.send({ to: user.email, subject: 'Welcome' });
}

// Orquestrador — apenas sequencia, não conhece detalhes
async function registerUser(data: unknown): Promise<User> {
  const dto = validateRegistrationPayload(data);
  const user = await createUser(dto);
  await sendWelcomeEmail(user);
  return user;
}

O — Open/Closed Principle

Entidades devem estar abertas para extensão e fechadas para modificação. Em TypeScript, estratégia e polimorfismo são os mecanismos centrais.

// Ruim — cada novo método de pagamento exige modificar a função
function processPayment(method: string, amount: number) {
  if (method === 'credit') { /* ... */ }
  else if (method === 'pix') { /* ... */ }
  else if (method === 'boleto') { /* ... */ } // nova linha a cada método
}

// Bom — extensão via implementação de interface, zero modificação
interface PaymentStrategy {
  process(amount: number): Promise<PaymentResult>;
}

class CreditCardStrategy implements PaymentStrategy {
  async process(amount: number): Promise<PaymentResult> { /* ... */ }
}

class PixStrategy implements PaymentStrategy {
  async process(amount: number): Promise<PaymentResult> { /* ... */ }
}

class PaymentProcessor {
  constructor(private strategy: PaymentStrategy) {}
  execute(amount: number) { return this.strategy.process(amount); }
}

L — Liskov Substitution Principle

Subtipos devem ser substituíveis por seus tipos base sem alterar a corretude do programa. O violação clássica: sobrescrever um método lançando uma exceção que a classe base não lança.

// Ruim — ReadOnlyRepository não suporta o contrato completo de Repository
class Repository<T> {
  async save(entity: T): Promise<T> { /* persiste */ }
  async findById(id: string): Promise<T | null> { /* busca */ }
}

class ReadOnlyRepository<T> extends Repository<T> {
  async save(_entity: T): Promise<T> {
    throw new Error('Read-only!'); // viola LSP — quem usa Repository espera que save funcione
  }
}

// Bom — hierarquia honesta, contratos separados
interface ReadRepository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
}

interface WriteRepository<T> extends ReadRepository<T> {
  save(entity: T): Promise<T>;
  delete(id: string): Promise<void>;
}

I — Interface Segregation Principle

Clientes não devem ser forçados a depender de interfaces que não utilizam. Interfaces gordas geram acoplamento desnecessário e dificultam mocks em testes.

// Ruim — interface monolítica, implementadores carregam métodos que não usam
interface UserService {
  createUser(dto: CreateUserDTO): Promise<User>;
  deleteUser(id: string): Promise<void>;
  sendNotification(userId: string, msg: string): Promise<void>;
  generateReport(userId: string): Promise<Report>;
}

// Bom — interfaces coesas e focadas
interface UserWriter {
  createUser(dto: CreateUserDTO): Promise<User>;
  deleteUser(id: string): Promise<void>;
}

interface UserNotifier {
  sendNotification(userId: string, message: string): Promise<void>;
}

interface UserReporter {
  generateReport(userId: string): Promise<Report>;
}

D — Dependency Inversion Principle

Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Em prática: injete interfaces, nunca instancie dependências dentro das classes de negócio.

// Ruim — OrderService conhece e instancia a implementação concreta
class OrderService {
  private repo = new PrismaOrderRepository(); // acoplamento direto à infra
  async placeOrder(dto: OrderDTO) {
    return this.repo.save(dto);
  }
}

// Bom — OrderService depende da abstração, não da concreção
interface OrderRepository {
  save(dto: OrderDTO): Promise<Order>;
  findById(id: string): Promise<Order | null>;
}

class OrderService {
  constructor(private readonly repo: OrderRepository) {} // injeção via construtor
  async placeOrder(dto: OrderDTO) {
    return this.repo.save(dto);
  }
}

// Wiring no ponto de entrada da aplicação
const orderService = new OrderService(new PrismaOrderRepository());

Diretivas SOLID no CLAUDE.md

Adicione o seguinte bloco ao seu CLAUDE.md para que o Claude aplique esses critérios automaticamente em todo o código gerado:

# CLAUDE.md — diretivas de qualidade

- Siga SOLID rigorosamente. SRP em particular: uma função = uma responsabilidade
- Nunca use nomes como data, info, result, temp, x, y
- Funções com mais de 20 linhas provavelmente violam SRP
- Se vai copiar código, prefira extrair uma função
- Interfaces sempre antes de implementações concretas
- Injeção de dependência via construtor, nunca instancie dentro da classe

Arquitetura Hexagonal

A arquitetura hexagonal (Ports & Adapters, Alistair Cockburn, 2005) isola o domínio de negócio de toda infraestrutura: banco de dados, HTTP, filas, SDKs externos. O resultado: domínio testável sem mocks de infra, substituição de adaptadores sem tocar regras de negócio, e deploys que variam de provedor sem reescritas.

Regra de ouro: O domain core nunca importa nada de infra. Se você ver um import de Prisma, Axios, AWS SDK ou qualquer biblioteca de infraestrutura dentro de src/domain/, a arquitetura foi violada.
graph TD subgraph Adapters["Adapters (Infra)"] HTTP["HTTP Controller\n(Express / Fastify)"] DB["DB Adapter\n(Prisma / TypeORM)"] EXT["External Services\n(Stripe, SendGrid)"] CLI["CLI Adapter"] end subgraph Ports["Ports (Interfaces)"] IP["Inbound Ports\n(Use Case interfaces)"] OP["Outbound Ports\n(Repository interfaces)"] end subgraph Domain["Domain Core"] UC["Use Cases"] ENT["Entities"] VO["Value Objects"] end HTTP --> IP CLI --> IP IP --> UC UC --> ENT UC --> VO UC --> OP OP --> DB OP --> EXT

Arquitetura hexagonal: domain core no centro, adapters na periferia, ports como contratos

Estrutura de pastas

src/
  domain/          <-- entidades, use cases, value objects
    entities/
      user.ts
      order.ts
    use-cases/
      create-user.ts
      place-order.ts
    value-objects/
      email.ts
      money.ts

  ports/           <-- interfaces (contratos)
    repositories/
      user-repository.ts
      order-repository.ts
    services/
      payment-service.ts
      notification-service.ts

  adapters/        <-- implementações concretas
    http/
      user-controller.ts
      order-controller.ts
    database/
      prisma-user-repository.ts
      prisma-order-repository.ts
    external/
      stripe-payment-service.ts
      sendgrid-notification-service.ts

Sequência de implementação

Sempre implemente nesta ordem — nunca comece pelo adapter:

  1. Domain: Entidades e value objects com suas invariantes. Zero dependências externas.
  2. Ports: Interfaces que o domínio precisa (outbound) e que o domínio expõe (inbound). Apenas TypeScript puro.
  3. Use Cases: Orquestram entidades e chamam ports outbound. Testáveis com mocks das interfaces.
  4. Adapters: Implementam os ports. Aqui entram Prisma, Axios, AWS SDK etc.

Estratégia de testes por camada

// Testes de domínio: sem mocks, sem infra — apenas lógica pura
describe('Order entity', () => {
  it('should not allow negative quantity', () => {
    expect(() => new Order({ quantity: -1 })).toThrow(InvalidQuantityError);
  });
});

// Testes de use case: mocks apenas das interfaces (ports)
describe('PlaceOrderUseCase', () => {
  it('should persist order and notify customer', async () => {
    const repo = mock<OrderRepository>();
    const notifier = mock<NotificationService>();
    repo.save.mockResolvedValue(buildOrder());

    const useCase = new PlaceOrderUseCase(repo, notifier);
    await useCase.execute(buildOrderDTO());

    expect(repo.save).toHaveBeenCalledOnce();
    expect(notifier.notify).toHaveBeenCalledOnce();
  });
});

// Testes de adapter: infra real via testcontainers
describe('PrismaOrderRepository (integration)', () => {
  let pg: StartedPostgreSqlContainer;

  beforeAll(async () => {
    pg = await new PostgreSqlContainer().start();
    process.env.DATABASE_URL = pg.getConnectionUri();
    await runMigrations();
  });

  afterAll(() => pg.stop());

  it('should persist and retrieve order by id', async () => {
    const repo = new PrismaOrderRepository(prisma);
    const saved = await repo.save(buildOrderDTO());
    const found = await repo.findById(saved.id);
    expect(found).toEqual(saved);
  });
});

TDD — Red, Green, Refactor

Test-Driven Development é uma disciplina de design, não de cobertura. O ciclo de três etapas força o engenheiro a pensar na interface e no comportamento antes de qualquer linha de implementação. Isso resulta em APIs mais limpas, menos acoplamento e código que já nasce testável.

TDD nao e sobre coverage. E sobre design. O teste forca voce a pensar na interface antes da implementacao. Coverage e um subproduto — nao o objetivo.
stateDiagram-v2 [*] --> Red: Escreva o teste\n(falha esperada) Red --> Green: Implemente o mínimo\npara passar Green --> Refactor: Melhore o código\nsem quebrar testes Refactor --> Red: Próxima\nfuncionalidade

Ciclo TDD: Red (teste falha) → Green (implementacao minima) → Refactor (qualidade)

Exemplo completo em TypeScript

Implementando um serviço de cálculo de frete do zero com TDD:

Passo 1 — Red: escreva o teste (ele falha, a classe nem existe)

// shipping.spec.ts
import { ShippingCalculator } from './shipping-calculator';

describe('ShippingCalculator', () => {
  describe('calculate', () => {
    it('should return free shipping for orders above R$ 299', () => {
      const calc = new ShippingCalculator();
      expect(calc.calculate({ subtotal: 300, weightKg: 1 })).toBe(0);
    });

    it('should charge R$ 15 per kg for orders below threshold', () => {
      const calc = new ShippingCalculator();
      expect(calc.calculate({ subtotal: 100, weightKg: 2 })).toBe(30);
    });

    it('should throw for negative weight', () => {
      const calc = new ShippingCalculator();
      expect(() => calc.calculate({ subtotal: 100, weightKg: -1 }))
        .toThrow('Weight must be positive');
    });
  });
});

Passo 2 — Green: implementação mínima para os testes passarem

// shipping-calculator.ts
const FREE_SHIPPING_THRESHOLD = 299;
const PRICE_PER_KG = 15;

interface ShippingInput {
  subtotal: number;
  weightKg: number;
}

export class ShippingCalculator {
  calculate({ subtotal, weightKg }: ShippingInput): number {
    if (weightKg < 0) throw new Error('Weight must be positive');
    if (subtotal > FREE_SHIPPING_THRESHOLD) return 0;
    return weightKg * PRICE_PER_KG;
  }
}

Passo 3 — Refactor: melhore sem quebrar nenhum teste

// shipping-calculator.ts (após refactor)
export class ShippingCalculator {
  private static readonly FREE_THRESHOLD = 299;
  private static readonly RATE_PER_KG = 15;

  calculate(input: ShippingInput): number {
    this.validate(input);
    return this.isFreeShipping(input.subtotal)
      ? 0
      : this.computeShippingCost(input.weightKg);
  }

  private validate({ weightKg }: ShippingInput): void {
    if (weightKg < 0) throw new Error('Weight must be positive');
  }

  private isFreeShipping(subtotal: number): boolean {
    return subtotal > ShippingCalculator.FREE_THRESHOLD;
  }

  private computeShippingCost(weightKg: number): number {
    return weightKg * ShippingCalculator.RATE_PER_KG;
  }
}

Configuração Vitest

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
      },
      include: ['src/domain/**', 'src/use-cases/**'],
    },
  },
});

Como instruir o Claude para TDD

Implemente ShippingCalculator seguindo TDD estrito:
1. Escreva os testes PRIMEIRO em src/domain/shipping-calculator.spec.ts
2. PARE e mostre os testes — aguarde minha aprovação antes de implementar
3. Implemente o mínimo em src/domain/shipping-calculator.ts para os testes passarem
4. Refatore mantendo todos os testes verdes
5. Nunca escreva código de produção sem um teste que o justifique

Pirâmide de Testes

A pirâmide de testes (Mike Cohn) define a proporção ideal entre tipos de teste: muitos testes unitários rápidos na base, menos testes de integração no meio, poucos testes E2E lentos no topo. Inverter essa pirâmide é um dos erros mais caros em projetos de software — suítes lentas, frágeis e difíceis de diagnosticar.

graph BT A["Unit Tests\n(Vitest)\nRápidos, isolados, muitos\n~70% da suíte"] B["Integration Tests\n(Testcontainers)\nInfra real, moderados\n~20% da suíte"] C["E2E Tests\n(Cypress)\nJornadas críticas, lentos\n~8% da suíte"] D["Load / Stress\n(k6)\nPré-produção\n~2% da suíte"] A --> B B --> C C --> D

Pirâmide de testes: base larga de unitários, topo restrito de E2E e carga

Unit Tests com Vitest

npm install -D vitest @vitest/coverage-v8 @vitest/ui
npx vitest run              # execucao CI
npx vitest                  # watch mode desenvolvimento
npx vitest --coverage       # com relatorio de cobertura
npx vitest --ui             # interface visual

Cobertura mínima recomendada: 80% em todo o domain core, 100% nos use cases críticos (checkout, pagamento, autenticação).

Integration Tests com Testcontainers

npm install -D @testcontainers/postgresql @testcontainers/core
// user-repository.integration.spec.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { PrismaClient } from '@prisma/client';

describe('UserRepository (integration)', () => {
  let container: StartedPostgreSqlContainer;
  let prisma: PrismaClient;

  beforeAll(async () => {
    container = await new PostgreSqlContainer('postgres:16').start();
    process.env.DATABASE_URL = container.getConnectionUri();
    prisma = new PrismaClient();
    await prisma.$executeRaw`CREATE TABLE users (id uuid PRIMARY KEY, email text NOT NULL)`;
  }, 60_000);

  afterAll(async () => {
    await prisma.$disconnect();
    await container.stop();
  });

  it('should persist and retrieve user', async () => {
    const repo = new PrismaUserRepository(prisma);
    const user = await repo.create({ email: 'dev@example.com' });
    const found = await repo.findById(user.id);
    expect(found?.email).toBe('dev@example.com');
  });
});
Nunca mocke o banco em integration tests. O objetivo e testar o adapter com infra real. Mocks enganam — voce aprende com falhas reais (constraint violations, index misses, transaction isolation).

E2E com Cypress

npm install -D cypress
npx cypress open            # interface interativa
npx cypress run --headless  # CI (sem GUI)
// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.ts',
    video: false,
    screenshotOnRunFailure: true,
    viewportWidth: 1280,
    viewportHeight: 720,
  },
});

// cypress/e2e/checkout.cy.ts — foco em jornadas críticas
describe('Checkout flow', () => {
  it('should complete purchase as authenticated user', () => {
    cy.login('user@example.com', 'password');
    cy.visit('/products/notebook-pro');
    cy.get('[data-testid="add-to-cart"]').click();
    cy.get('[data-testid="checkout-btn"]').click();
    cy.get('[data-testid="card-number"]').type('4111111111111111');
    cy.get('[data-testid="submit-payment"]').click();
    cy.get('[data-testid="order-confirmation"]').should('be.visible');
  });
});

Load Tests com k6

brew install k6
k6 run k6/load.js           # load test padrao
k6 run k6/stress.js         # stress test
// k6/load.js — load test padrao (baseline de producao)
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 50 },   // ramp up
    { duration: '3m', target: 50 },   // steady state
    { duration: '1m', target: 0 },    // ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // p95 abaixo de 500ms
    http_req_failed: ['rate<0.01'],   // error rate abaixo de 1%
  },
};

export default function () {
  const res = http.get('https://api.example.com/products');
  check(res, { 'status 200': (r) => r.status === 200 });
  sleep(1);
}

// k6/stress.js — stress test (encontrar ponto de ruptura)
export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '5m', target: 200 },
    { duration: '2m', target: 300 },
    { duration: '5m', target: 300 },
    { duration: '2m', target: 0 },
  ],
};

Quando usar cada nivel

Nivel Ferramenta Quando usar Velocidade Custo de manutencao
Unit Vitest Logica de dominio, value objects, utilitarios Milissegundos Baixo
Integration Testcontainers Adapters de banco, HTTP clients, queues Segundos Medio
E2E Cypress Jornadas criticas do usuario (checkout, login) Minutos Alto
Load k6 Pre-release em producao simulada Minutos Baixo
Stress k6 Encontrar ponto de ruptura, validar auto-scaling Decadas de minutos Baixo

OWASP Top 10

O OWASP Top 10 é o padrão de referência para riscos de segurança em aplicações web. Cada categoria representa uma classe de vulnerabilidade com impacto mensurável em produção. Um engenheiro sênior usa esta lista como checklist de design — não apenas como referência pós-auditoria.

ID Vulnerabilidade Como mitigar Gate no workflow
A01 Broken Access Control Checar permissoes em todo endpoint; deny-by-default; testes de autorizacao por role Code review obrigatorio + testes de autorizacao
A02 Cryptographic Failures TLS 1.3+; AES-256 para dados em repouso; nunca MD5/SHA1 para senhas (use bcrypt/argon2) Config review + secrets scanner no CI
A03 Injection (SQL, NoSQL, OS) Parameterized queries sempre; ORM com prepared statements; validacao na borda com Zod/Joi Linting com regras anti-concatenacao + SAST
A04 Insecure Design Threat modeling antes de implementar; arquitetura hexagonal com limites claros Design review na fase de planejamento
A05 Security Misconfiguration Headers de seguranca (CSP, HSTS, X-Frame-Options); remover endpoints de debug em producao Helmet.js + auditoria de headers pre-deploy
A06 Vulnerable Components npm audit no CI; atualizacoes automaticas via Dependabot; lock de versoes npm audit --audit-level=high como gate obrigatorio
A07 Auth & Session Failures Rate limiting em login; MFA para admins; tokens de sessao com expiracao e rotacao Testes de brute force + revisao de configuracao JWT
A08 Software & Data Integrity Failures Verificar assinaturas em dependencias; CI/CD pipeline protegido; SRI para assets externos Assinatura de commits + revisao de pipeline
A09 Security Logging & Monitoring Logar tentativas de autenticacao; alertas em anomalias; retencao de logs por 90 dias Validacao de observabilidade pre-launch
A10 Server-Side Request Forgery (SSRF) Validar e restringir URLs fornecidas pelo usuario; allowlist de dominios externos; bloquear IPs internos Code review em toda funcionalidade que faz HTTP outbound
npm audit deve ser gate obrigatorio em todo CI. Um npm audit --audit-level=high que falha deve bloquear o merge. Nunca deixe vulnerabilidades high/critical em dependencias de producao.

Diretivas de seguranca no CLAUDE.md

# CLAUDE.md — seguranca

- Sempre use parameterized queries. Nunca concatene SQL.
- Nunca commite secrets. Use variaveis de ambiente.
- Valide TODA entrada do usuario na borda (sem trusts implicitos).
- npm audit deve passar sem vulnerabilidades high/critical.
- Headers de seguranca: Helmet.js em todo servidor Express/Fastify.
- Autenticacao: nunca implemente JWT do zero — use uma lib auditada (jose, jsonwebtoken).
- Senhas: bcrypt com cost factor >= 12 ou argon2id.

Validacao de entrada com Zod (A03)

import { z } from 'zod';

// Schema de validacao — define o contrato na borda
const CreateUserSchema = z.object({
  email: z.string().email().max(255),
  password: z.string().min(12).max(128),
  name: z.string().min(1).max(100).regex(/^[a-zA-Z\s]+$/),
  role: z.enum(['user', 'admin']).default('user'),
});

type CreateUserDTO = z.infer<typeof CreateUserSchema>;

// Controller — valida antes de tocar o dominio
app.post('/users', async (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() });
  }
  const user = await userService.create(result.data); // dados ja validados e tipados
  return res.status(201).json(user);
});

Context Window Discipline

A janela de contexto do Claude Code tem limite finito — e esse limite e compartilhado entre instrucoes do sistema, historico da conversa, conteudo de arquivos lidos e output gerado. Um contexto mal gerido degrada a qualidade das respostas, aumenta custo e pode causar perda silenciosa de instrucoes importantes. Discipline de contexto e uma skill operacional de primeiro nivel.

graph LR A["Sessao inicia\n~5K tokens"] --> B["Codigo gerado\n+30K tokens"] B --> C["Arquivos lidos\n+20K tokens"] C --> D{70K tokens?} D -->|Nao| E["Continua\ntrabalhando"] E --> C D -->|Sim| F["/compact\nresume contexto"] F --> G["Continua com\ncontexto comprimido"] G --> C

Ciclo de crescimento de contexto e quando acionar /compact

Regras de ouro

Limites recomendados por agente:
  Orquestrador:  max 80K tokens de contexto ativo
  Subagente:     contexto inicial max 60K tokens
  /compact:      obrigatorio ao cruzar 70K tokens

Handoffs entre agentes:
  - Sempre via arquivos no sistema de arquivos
  - Nunca via variaveis in-memory ou estado de sessao
  - Formato recomendado: HANDOFF.md com estado atual + proximos passos

Monitoramento:
  /usage    — tokens consumidos na sessao atual
  /compact  — comprime historico mantendo contexto essencial
  /clear    — limpa totalmente (use apenas para nova tarefa)

Calculadora rapida de tokens

Regra de estimativa rapida:
  1K tokens  ~  750 palavras  ~  50 linhas de codigo

Exemplos praticos:
  CLAUDE.md bem escrito:       ~2K tokens
  Arquivo TypeScript tipico:   ~1-3K tokens
  Schema Prisma completo:      ~3-5K tokens
  Output de npm ls --all:      ~10-30K tokens (nao leia isso!)

Janela total disponivel:
  claude-opus-4:     200K tokens
  claude-sonnet-4:   200K tokens

Orçamento util por sessao de desenvolvimento:
  Reserva para instrucoes + historico: ~20K tokens
  Disponivel para codigo e contexto:   ~150K tokens
  Zona de alerta (/compact):           acima de 70K usados
Handoffs via arquivos, nunca in-memory. Quando um orquestrador delega trabalho a um subagente, o estado deve ser serializado em um arquivo (ex: TASK.md, HANDOFF.md). Se o subagente falhar, o trabalho nao se perde — o proximo agente le o arquivo e retoma.

Estrategia de contexto por tipo de tarefa

Tarefa pequena (bug fix, util function):
  - Uma sessao, contexto direto, sem /compact necessario

Tarefa media (nova feature, refactor):
  - Sessao focada na feature
  - /compact ao atingir 50% da janela
  - Commit antes de /compact para nao perder estado

Tarefa grande (novo modulo, migracao):
  - Divida em sub-tarefas com handoffs via arquivo
  - Orquestrador gera PLAN.md com fases
  - Cada subagente recebe contexto minimo necessario
  - Estado persistido em arquivos entre fases

Frontend Design Standards

Para projetos com UI, um conjunto coerente de decisoes de design elimina bikeshedding e garante consistencia visual sem esforco. As tres pecas fundamentais para stacks Next.js/React modernas: shadcn/ui para componentes, Lucide para iconografia e Geist para tipografia.

shadcn/ui

shadcn/ui nao e uma biblioteca instalada como dependencia — e um conjunto de componentes copiados diretamente para o projeto, buildados sobre Radix UI e estilizados com Tailwind CSS. Voce e dono do codigo; atualizacoes sao explicitas, nao silenciosas.

# Inicializacao
npx shadcn@latest init

# Adicionar componentes individualmente
npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add data-table

# Componentes ficam em src/components/ui/
# Personalize diretamente — voce e o dono do codigo
// Uso tipico com shadcn/ui
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';

export function CreateUserDialog({ onSubmit }: { onSubmit: (data: FormData) => void }) {
  return (
    <Dialog>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Novo usuario</DialogTitle>
        </DialogHeader>
        <form onSubmit={onSubmit}>
          <Input name="email" type="email" placeholder="email@exemplo.com" />
          <Button type="submit">Criar</Button>
        </form>
      </DialogContent>
    </Dialog>
  );
}

Lucide Icons (outlined)

Lucide e a biblioteca de icones padrao para stacks com shadcn/ui. Sempre use a variante outlined (padrao). Consistencia e inegociavel: misturar Lucide com heroicons ou FontAwesome no mesmo projeto e um antipadrao visual.

npm install lucide-react
// Import pattern correto — sempre named imports, nunca default
import { User, Mail, Settings, AlertTriangle, CheckCircle2 } from 'lucide-react';

// Tamanhos padrao
<User size={16} />        // inline com texto
<User size={20} />        // botoes e inputs
<User size={24} />        // headings (padrao Lucide)
<User size={32} />        // icones standalone

// Com className para Tailwind
<AlertTriangle className="h-4 w-4 text-yellow-500" />
<CheckCircle2 className="h-5 w-5 text-green-500" />

Geist Fonts

Geist e a familia tipografica criada pela Vercel para interfaces de software. Geist Sans para UI geral; Geist Mono obrigatorio para blocos de codigo, terminais e qualquer dado tecnico (IDs, hashes, endpoints).

// app/layout.tsx (Next.js App Router)
import { GeistSans } from 'geist/font/sans';
import { GeistMono } from 'geist/font/mono';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR" className={`${GeistSans.variable} ${GeistMono.variable}`}>
      <body className="font-sans">
        {children}
      </body>
    </html>
  );
}

// tailwind.config.ts
export default {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-geist-sans)'],
        mono: ['var(--font-geist-mono)'],
      },
    },
  },
};

Dark mode por padrao

Para aplicacoes voltadas a desenvolvedores, dark mode deve ser o padrao — nao uma opcao opcional. Configure no Tailwind com a classe dark no elemento html e implemente a troca via next-themes.

npm install next-themes
// app/layout.tsx
import { ThemeProvider } from 'next-themes';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="dark"
          enableSystem={false}
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Quando invocar o skill frontend-design

Antes de implementar qualquer nova UI, invoque skill: frontend-design para gerar um contrato visual (UI-SPEC.md) com paleta, componentes, espacamento e comportamentos definidos. Implementar sem spec visual e a principal causa de inconsistencia em projetos de longa duracao.
# Como invocar no Claude Code
skill: frontend-design

# O skill gera:
# - UI-SPEC.md com todas as decisoes de design
# - Paleta de cores com variaveis CSS
# - Mapa de componentes necessarios
# - Regras de espacamento e tipografia
# - Comportamentos de estado (hover, focus, disabled, loading)

Hooks como Quality Gates

O conceito central

Hooks nao substituem o CLAUDE.md — eles o completam. O CLAUDE.md opera no plano das instrucoes: influencia o comportamento do modelo de forma probabilistica, com eficacia em torno de 90%. Hooks operam no plano do output: interceptam o resultado e bloqueiam qualquer saida que nao atenda ao criterio definido, com eficacia de 100%.

  • CLAUDE.md = influencia o comportamento (probabilistico ~90%)
  • Hooks = bloqueiam output invalido (deterministico 100%)
CLAUDE.md ensina como trabalhar. Hooks garantem o resultado. Juntos voce sai do territorio probabilistico para o deterministico nos criterios que importam.

Tabela de hooks por categoria

Hook Trigger Verifica Deterministico?
PostToolUse Write/Edit arquivos .ts/.js/.tsx ESLint + tsc --noEmit Sim
PostToolUse Write/Edit arquivos .ts fonte vitest run (testes devem passar) Sim
PostToolUse Write/Edit package.json npm audit --audit-level high (OWASP A06) Sim
PostToolUse Write/Edit src/domain/** import boundaries — domain nao importa adapters Sim
PostToolUse Write/Edit qualquer arquivo regex de secrets (OWASP A02) Sim
PreToolUse Bash qualquer comando bloqueia comandos destrutivos Sim
PostToolUse Write/Edit arquivos .ts/.js vitest coverage ≥ 80% domain core Sim

Configuracao base — settings.json

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{"type": "command", "command": "node scripts/quality-gate.mjs \"$CLAUDE_TOOL_INPUT_FILE_PATH\""}]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{"type": "command", "command": "node scripts/bash-guard.mjs"}]
      }
    ]
  }
}

Script quality-gate.mjs

#!/usr/bin/env node
// quality-gate.mjs — gate de qualidade pos-escrita
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { extname } from 'node:path';

const file = process.argv[2];
if (!file) process.exit(0);

const ext = extname(file);

// OWASP A02 — Secret detection em qualquer arquivo
try {
  const content = readFileSync(file, 'utf8');
  const secretPattern = /(?:api_key|secret_key|password|private_key|token)\s*[:=]\s*['"][^'"]{8,}/i;
  if (secretPattern.test(content)) {
    console.error(`[quality-gate] BLOQUEADO: possivel secret detectado em ${file}`);
    process.exit(1);
  }
} catch {}

// TypeScript + ESLint — Clean Code gate
if (['.ts', '.tsx', '.js', '.jsx'].includes(ext) && !file.includes('.test.') && !file.includes('.spec.')) {
  try {
    execSync('npx tsc --noEmit', { stdio: 'inherit', cwd: process.cwd() });
  } catch {
    console.error('[quality-gate] BLOQUEADO: TypeScript errors encontrados');
    process.exit(1);
  }
  try {
    execSync(`npx eslint --max-warnings 0 "${file}"`, { stdio: 'inherit' });
  } catch {
    console.error('[quality-gate] BLOQUEADO: ESLint warnings/errors encontrados');
    process.exit(1);
  }
  // TDD gate — testes devem passar apos qualquer mudanca em source
  try {
    execSync('npx vitest run --reporter=dot --passWithNoTests', { stdio: 'inherit' });
  } catch {
    console.error('[quality-gate] BLOQUEADO: testes falhando');
    process.exit(1);
  }
}

// OWASP A06 — Vulnerable Components
if (file.endsWith('package.json') && !file.includes('node_modules')) {
  try {
    execSync('npm audit --audit-level high', { stdio: 'inherit' });
  } catch {
    console.error('[quality-gate] BLOQUEADO: vulnerabilidades high/critical em dependencias');
    process.exit(1);
  }
}

console.log(`[quality-gate] OK: ${file}`);

Script bash-guard.mjs

#!/usr/bin/env node
// bash-guard.mjs — bloqueia comandos bash destrutivos
const cmd = process.env.CLAUDE_TOOL_INPUT_COMMAND ?? '';

const blocked = [
  { pattern: /DROP\s+TABLE/i, reason: 'DROP TABLE detectado' },
  { pattern: /rm\s+-rf\s+\/(?!tmp)/, reason: 'rm -rf em diretorio raiz' },
  { pattern: /git\s+push\s+--force\s+(?:origin\s+)?main/i, reason: 'force push em main' },
  { pattern: /git\s+reset\s+--hard\s+HEAD/i, reason: 'git reset --hard HEAD' },
  { pattern: /TRUNCATE\s+TABLE/i, reason: 'TRUNCATE TABLE detectado' },
];

for (const { pattern, reason } of blocked) {
  if (pattern.test(cmd)) {
    console.error(`[bash-guard] BLOQUEADO: ${reason}`);
    console.error(`[bash-guard] Comando: ${cmd}`);
    process.exit(1);
  }
}

Check de import boundaries (arquitetura hexagonal)

#!/usr/bin/env node
// check-boundaries.mjs — enforca arquitetura hexagonal
import { readFileSync } from 'node:fs';

const file = process.argv[2];
if (!file || !file.includes('/domain/')) process.exit(0);

const content = readFileSync(file, 'utf8');

// Domain core nao pode importar de adapters ou infrastructure
const violations = [
  { pattern: /from\s+['"].*\/adapters\//, label: 'adapters' },
  { pattern: /from\s+['"].*\/infrastructure\//, label: 'infrastructure' },
  { pattern: /from\s+['"].*\/database\//, label: 'database' },
  { pattern: /from\s+['"].*\/http\//, label: 'http' },
];

for (const { pattern, label } of violations) {
  if (pattern.test(content)) {
    console.error(`[boundaries] BLOQUEADO: domain/ importa de ${label} em ${file}`);
    console.error('[boundaries] Domain core nao pode depender de adapters/infrastructure.');
    process.exit(1);
  }
}
Este hook e a garantia mecanica da arquitetura hexagonal. Sem ele, a regra de ouro ("domain core nao importa infra") existe apenas no CLAUDE.md — probabilistica. Com o hook, e inviolavel.

O que NAO pode ser enforcado por Hooks

Hooks sao ferramentas de verificacao de output — eles inspecionam o resultado, nao o processo. Existe uma categoria de criterios de qualidade que permanece fora do alcance deterministico dos hooks:

  • TDD ORDER (testes escritos antes da impl) — hooks apenas verificam que testes existem e passam, nao a ordem de criacao
  • SOLID semanticamente — linter so pega sintaxe (complexidade ciclomatica, max-lines); responsabilidade conceitual exige revisao humana
  • OWASP A01 (Broken Access Control) — requer analise semantica de logica de negocio, que hooks de arquivo nao conseguem realizar
Hooks sao guards do output, nao teachers do processo. O processo e responsabilidade do CLAUDE.md + disciplina do dev.
0%