SDD do zero em Laravel: transformando uma feature real em specification executável
SDD do zero em Laravel: transformando uma feature real em specification executável
Você abre o Claude Code, descreve a feature em três linhas, manda rodar.
Ele entrega 400 linhas. Compila. Os testes (que ele mesmo escreveu) passam. Você dá merge.
Duas semanas depois, suporte abre ticket: o relatório de vendas em PDF está somando os pedidos cancelados, o nome do arquivo não tem a data, e quem é vendedor consegue ver o faturamento de outro vendedor. O agente não inventou — você simplesmente nunca disse pra ele que essas regras existiam.
Essa cena tem nome agora. É o que o pessoal do GitHub chamou de vibe coding quando lançou o Spec Kit em 2025: pedir feature pra IA achando que ela vai adivinhar contexto que está só na sua cabeça. Spec-Driven Development (SDD) é a resposta. A spec deixa de ser comentário em PR e vira input executável: a IA gera código a partir dela, não a partir do seu prompt curto.
Neste post a gente faz o ciclo completo em Laravel, com uma feature que parece trivial de propósito — exportar relatório de vendas em PDF — pra mostrar que SDD aparece exatamente nessas que parecem bobas. Vai ter PRD, spec.md, plan.md, tasks.md, código gerado, teste em Pest, e o ponto onde o humano ainda manda. Stack PHP, que é o nicho que ninguém cobre nesse papo.
TL;DR
- O que é: SDD é escrever a specification antes do código quando se programa com IA. A spec é a fonte da verdade do projeto, não o prompt.
- Stack/Modelos: Laravel 11, Pest 3, dompdf, Claude Code, GitHub Spec Kit (ou variação custom como o Pimzino claude-code-spec-workflow).
- Custo/Acesso: Spec Kit é open source. Claude Code requer assinatura. Tudo o que aparece neste post roda local.
- Repositório/Link útil: github.com/github/spec-kit e o ensaio de referência da Birgitta Böckeler em martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html.
O contexto: por que SDD virou pauta agora
Em 2025 o GitHub publicou o Spec Kit, a AWS lançou o Kiro, a Tessl empurrou a fronteira pra "spec-as-source", e o Thoughtworks publicou o ensaio mais lúcido sobre o assunto. SDD virou padrão emergente.
A tese, do próprio spec-driven.md do GitHub, é direta:
"Specifications don't serve code — code serves specifications. The PRD isn't a guide for implementation; it's the source that generates implementation."
Tradução prática: o documento de requisito deixa de ser PDF morto na pasta /docs e vira input que o agente lê. O ciclo oficial do Spec Kit é Constitution → Specify → Plan → Tasks → Implement, todos comandos /speckit.* no Claude Code, gerando arquivos versionáveis em .specify/specs/[feature]/.
Mas SDD não é silver bullet. A própria Birgitta Böckeler, da Thoughtworks, aponta duas dores que dificilmente vão sumir:
"I'd rather review code than all these markdown files."
E:
"The agent ultimately not follow all the instructions."
Resumo honesto: SDD reduz drasticamente a chance do agente alucinar regra de negócio, mas não elimina o trabalho de revisar nem a não-determinismo do LLM. A gente paga essa dor com markdown. Em troca, ganha rastreabilidade que prompt curto nunca dá.
E aqui é onde o ecossistema PHP fica de fora da conversa. Os exemplos públicos de SDD são quase todos em Python, Next.js ou TypeScript. Laravel raramente aparece. Vamos consertar isso.
Pré-requisitos
- PHP 8.3, Composer, Laravel 11.
- Claude Code instalado (
npm install -g @anthropic-ai/claude-code). - Spec Kit via
uv tool install specify-cli(ou rodar comando-a-comando manualmente, como vou mostrar). - Pest 3 (
composer require pestphp/pest --dev). barryvdh/laravel-dompdfpra geração do PDF.
Toda a estrutura que vai aparecer abaixo é clonável: copia os arquivos dentro de .specify/specs/sales-report-pdf/ num projeto Laravel novo, abre o Claude Code, e o ciclo roda.
A feature trivial que tem mais arestas do que parece
GET /reports/sales/export.pdf. Parece besta. Não é.
O que está implícito num pedido desses, e raramente vem no prompt:
- Filtro de período. Default? "Últimos 30 dias" ou "mês corrente"? Em qual timezone?
- Permissão. Admin enxerga tudo. Vendedor enxerga só os próprios pedidos. Como amarra com o
User? - Status do pedido. Cancelado entra no relatório? Devolvido? Em rascunho?
- Dados sensíveis. CPF do cliente vai mascarado ou cru? E se o time jurídico já bateu sobre LGPD?
- Renderização. Mais de 5.000 linhas — síncrono trava o request. Vai pra fila? Email com link?
- Nome do arquivo.
relatorio.pdfquebra UX.vendas-2026-04-01-a-2026-04-30.pdfé o que o usuário espera. - Cache. O mesmo período pedido duas vezes em dois minutos gera o PDF de novo do zero?
Vibe coding nessa feature normalmente esquece duas ou três dessas. SDD obriga a tropeçar nelas no documento, antes do código.
Mão na massa: o ciclo SDD aplicado
Passo 1: o PRD humano (o que ainda precisa de gente)
Antes do agente entrar, alguém escreve o PRD. Foco em resultado e contexto de negócio, não em técnica. O Spec Kit chama isso de "intent".
# PRD: Exportação de relatório de vendas em PDF
## Problema
Time comercial precisa enviar relatório mensal de vendas para
contabilidade por email. Hoje exportam CSV e formatam manualmente
no Excel. Toma de 30 a 60 minutos por mês, por gerente.
## Outcome desejado
Botão "Exportar PDF" no painel de relatórios devolve o documento
no padrão visual da empresa, pronto pra enviar.
## Restrições conhecidas
- Apenas admin e gerente podem exportar.
- Vendedor só vê os próprios números, não exporta.
- LGPD: CPF do cliente sai mascarado (xxx.xxx.xxx-12).
- Pedidos cancelados entram com badge "cancelado", não somam no total.
- Período default: mês corrente, em timezone America/Sao_Paulo.
## Não-objetivos
- Não é gráfico interativo. É PDF estático.
- Não é exportação programada. É sob demanda.
Esse arquivo é o input cru. Ele não tem decisão técnica — nada de "use dompdf", nada de "endpoint REST". Isso é trabalho do /plan.
Passo 2: /specify — virar specification executável
No Claude Code, dentro do projeto Laravel:
/speckit.specify
Você cola o PRD. O agente devolve um spec.md estruturado em .specify/specs/sales-report-pdf/spec.md. A diferença é que ele explicita ambiguidades que o PRD escondeu:
# Spec: Exportação de relatório de vendas em PDF
## User Stories
US-1: Como gerente, quero exportar PDF do mês corrente em 1 clique.
US-2: Como admin, quero filtrar por intervalo arbitrário antes de exportar.
US-3: Como vendedor, quero exportar apenas os meus pedidos.
## Acceptance Criteria
AC-1: PDF gerado contém: cabeçalho com logo, período, total bruto,
total líquido (sem cancelados), tabela de pedidos.
AC-2: Pedidos cancelados aparecem na tabela com badge visual,
mas não compõem o total líquido.
AC-3: CPF do cliente renderizado no formato xxx.xxx.xxx-NN.
AC-4: Nome do arquivo: "vendas-{YYYY-MM-DD}-a-{YYYY-MM-DD}.pdf".
AC-5: Vendedor que tentar exportar período fora do próprio recebe 403.
## [NEEDS CLARIFICATION]
- NC-1: Acima de quantas linhas a geração vai pra fila?
- NC-2: PDF é cacheado? Por quanto tempo?
- NC-3: Email automático com link após geração assíncrona, ou só download?
Esses [NEEDS CLARIFICATION] são o ouro do SDD. O agente sinaliza o que ele não consegue inferir sozinho. Você responde, atualiza o spec, e segue. Sem isso, ele inventa default.
Passo 3: /plan — escolher stack e contratos
Agora sim entra a técnica. Comando:
/speckit.plan
Saída em .specify/specs/sales-report-pdf/plan.md:
# Plan: Exportação de relatório de vendas em PDF
## Stack
- Renderer: barryvdh/laravel-dompdf (suficiente pro layout simples).
- Action class: App\Actions\Reports\ExportSalesPdf.
- Controller: thin, só valida e chama a action.
- View: resources/views/reports/sales-pdf.blade.php.
- Fila: Laravel Queue. Threshold = 1.000 linhas.
## Contracts
- Endpoint: GET /reports/sales/export.pdf
- Query: ?from=YYYY-MM-DD&to=YYYY-MM-DD
- Resposta síncrona: application/pdf, 200.
- Resposta assíncrona: 202 + { job_id, poll_url }.
## Policies
- SalesReportPolicy::export — verifica role + scope de vendedor.
## Decisões registradas
- dompdf escolhido sobre Snappy: layout não usa CSS3 avançado.
- Cache: 5 minutos por (user_id + from + to).
Repare: o plan.md é o lugar onde decisão técnica vive versionada, não num PR comment que ninguém mais acha.
Passo 4: /tasks — quebrar em tarefas testáveis
/speckit.tasks
Saída em tasks.md:
# Tasks
T1. Criar migration de índice composto (user_id, status, created_at) em orders.
T2. Criar SalesReportPolicy com método export().
T3. Criar Action ExportSalesPdf com __invoke(User $user, CarbonPeriod $range).
T4. Criar SalesReportPdfRequest com validação de from/to.
T5. Criar SalesReportController@export, thin.
T6. Criar view Blade do PDF com mascaramento de CPF.
T7. Criar Job ExportSalesPdfJob para volumes acima do threshold.
T8. Pest: feature test para AC-1, AC-2, AC-3, AC-4, AC-5.
T9. Pest: unit test do mascaramento de CPF.
T10. Documentar no README a rota e os params.
Cada task é pequena, testável, e amarra a um AC. É aqui que o ganho aparece: em vez do agente cuspir 400 linhas de uma vez, ele entrega T1 → review → T2 → review.
Passo 5: /implement (com freio humano)
/speckit.implement T8
Antes de qualquer linha de aplicação, o agente escreve o teste Pest da T8. Pra valer a pena, peça que ele rode o teste antes de implementar — confirma que falha pelo motivo certo:
<?php
use App\Models\{User, Order};
it('exporta PDF com totais corretos ignorando cancelados (AC-2)', function () {
$admin = User::factory()->admin()->create();
Order::factory()->count(3)->for($admin)->create(['status' => 'paid', 'total' => 100]);
Order::factory()->for($admin)->create(['status' => 'canceled', 'total' => 999]);
$response = $this->actingAs($admin)
->get('/reports/sales/export.pdf?from=2026-04-01&to=2026-04-30');
$response->assertOk();
$response->assertHeader('content-type', 'application/pdf');
$pdfText = (new \Smalot\PdfParser\Parser())
->parseContent($response->getContent())->getText();
expect($pdfText)
->toContain('Total líquido: R$ 300,00')
->not->toContain('R$ 999,00');
});
Falhou? Bom. Agora o agente implementa a Action:
<?php
namespace App\Actions\Reports;
use App\Models\{Order, User};
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\CarbonPeriod;
final class ExportSalesPdf
{
public function __invoke(User $user, CarbonPeriod $range): string
{
$orders = Order::query()
->whereBetween('created_at', [$range->start, $range->end])
->when($user->isVendedor(), fn ($q) => $q->where('user_id', $user->id))
->orderBy('created_at')
->get();
$totalLiquido = $orders->reject->isCanceled()->sum('total');
return Pdf::loadView('reports.sales-pdf', [
'orders' => $orders,
'totalLiquido' => $totalLiquido,
'range' => $range,
])->output();
}
}
Roda o Pest de novo: passa. Próxima task. Esse é o loop. Cada task entra no commit com referência ao AC e ao spec — rastreabilidade que PR descrição em texto livre nunca entrega.
Limitações e pontos de atenção (a parte que ninguém posta no thread)
1. Markdown sprawl. Quatro arquivos por feature pequena. Em projeto com 200 features, vira inferno. Solução: SDD não é pra todo CRUD. Aplica em features com regra de negócio — relatório, integração, fluxo de aprovação. Migration de coluna nova não merece spec.md.
2. O agente diverge mesmo com spec rígida. Já vi spec dizendo "máscara CPF xxx.xxx.xxx-NN" e o código sair com "..***-NN". Não é falha do método, é a não-determinismo do LLM. O Pest é a rede de proteção — sem teste, SDD vira só burocracia.
3. Constituição é frágil. O Spec Kit propõe um constitution.md com regras imutáveis ("toda Action é final", "controller é thin"). Funciona em projeto novo. Em legado de 5 anos, a constituição só vale onde você forçar. Não dá pra retroagir.
4. LGPD e dados sensíveis. A spec precisa dizer explicitamente o que sai mascarado. Se você esquecer, o agente expõe. Trate isso como acceptance criteria de primeira classe, não como detalhe de implementação.
5. Não é silver bullet. Feature com 1 query SQL e zero regra de negócio é overhead. SDD ganha onde o custo do erro é alto: relatório financeiro, integração com gateway, fluxo de pagamento, exportação que vai pra contabilidade.
FAQ
Funciona sem o Spec Kit oficial?
Sim. O Spec Kit é só uma convenção de pastas + comandos /speckit.* no Claude Code. Você consegue o mesmo com .claude/commands/specify.md, plan.md e tasks.md custom. O claude-code-spec-workflow do Pimzino é uma alternativa pronta pra colar no projeto. O importante é o ciclo, não o nome do comando.
Preciso refazer o projeto inteiro? Não. SDD é por feature. Legado segue como está. Toda feature nova entra pelo ciclo, e em 6 meses metade do código que importa já está versionada com spec.
Os testes Pest substituem o spec? Não. Spec é "o quê" e "por quê". Teste é "como verifico que funciona". Camadas diferentes. Quando alguém pergunta por que o pedido cancelado não soma, o spec responde. Quando alguém quer saber se ainda funciona depois do refactor, o teste responde.
dompdf aguenta relatório grande?
Pra PDF tabular simples, sim — até uns 2.000-3.000 pedidos render bem. Acima disso, ou se tiver gráfico/CSS3 avançado, troque por Browsershot (Headless Chrome). Mas isso é detalhe que mora no plan.md, não no spec.md.
Conclusão
SDD aplicado a Laravel não é mudança de framework, é mudança de input. O agente sai do modo adivinhador e entra no modo executor: a spec, com acceptance criteria explícitos, é o que ele lê pra gerar código. O markdown extra paga em rastreabilidade, em [NEEDS CLARIFICATION] que pega ambiguidade antes do PR, e em commits que amarram código a regra de negócio. O que sobra pro humano é o que sempre foi: pensar o problema, decidir trade-off, e revisar com critério.
O próximo passo depois de SDD é harness engineering — montar o ferramental ao redor do agente pra ele rodar em produto real, não em chatbot de demo. É exatamente o que vou destrinchar ao vivo no Harness Engineering com Claude Code, 16 e 17 de maio: dois dias construindo do zero um app que recebe link de produto, pesquisa alternativas em e-commerces, compara preço e devolve recomendação estruturada — Claude Code, Laravel, NativePHP, sem prompt mágico e sem chatbot.
Sources:
{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.
Você também pode gostar
O paradoxo da especificação: quando SDD vira overengineering disfarçado de boa prática
Quatro horas escrevendo spec para uma feature de duas horas é o sintoma. SDD virou ortodoxia em 2026 e pouca gente discute o custo: tempo de leitura, revisão dupla, drift entre spec e código, falsa sensação de controle. Aqui vamos ver de onde veio o método, onde entrega de verdade, onde virou cerimônia, e como aplicar spec proporcional ao risco.
5 sinais de que sua especificação virou burocracia (e como voltar à base bem feita)
Spec-driven virou padrão em 2026, e com ele veio o risco do pêndulo: spec gigante, aprovada em comitê, ignorada pelo time e filtrada pelo agente. Cinco sintomas concretos e o ajuste prático para cada um.
Especificação mínima viável: o framework de 1 página que evita construir a Catedral antes da Cabana
Template proprietário de 1 página com objetivo, contexto, restrições, critérios de aceite e anti-escopo. Mostra quando expandir e quando NÃO expandir, e por que esse formato vira o melhor harness pra agente de IA executar sem alucinar feature paralela.
Agente que pesquisa antes de agir: multi-tool + RAG em Laravel com pgvector
Como construir um agente em Laravel que decide quando buscar e quando responder direto. Arquitetura completa com Prism PHP, pgvector e a lógica de orquestração que separa demo de produto.