⚠ O ETP Express pode cometer erros. Lembre-se de verificar todas as informações antes de realizar qualquer encaminhamento.
O ETP Express é um sistema wrapper de LLM projetado para auxiliar servidores públicos na elaboração de Estudos Técnicos Preliminares (ETP) conforme Lei 14.133/2021, Art. 18 §1º.
- Transparência: Assume abertamente possibilidade de erros, vieses e imprecisões
- Assistência: Natureza preparatória, não substitui responsabilidade administrativa
- Validação: Requer conferência humana obrigatória antes de uso oficial
- Auditabilidade: Trilha completa de geração, edição e versionamento
┌─────────────────────────────────────────────────────────────┐
│ ETP EXPRESS - FLUXO │
├─────────────────────────────────────────────────────────────┤
│ │
│ Usuário Input → Orquestrador → Subagentes → LLM Wrapper │
│ ↓ ↓ ↓ ↓ │
│ Validação → Busca Web → Refinamento → Normalização │
│ ↓ ↓ ↓ ↓ │
│ Persistência → Versionamento → Export (PDF/JSON/XML) │
│ │
└─────────────────────────────────────────────────────────────┘
| Componente | Tecnologia | Justificativa |
|---|---|---|
| Framework | NestJS | Arquitetura modular, TypeScript nativo, decorators |
| Runtime | Node.js 20 LTS | Performance, ecossistema maduro |
| Database | PostgreSQL 15 | ACID, jsonb, versionamento eficiente |
| ORM | TypeORM | Migrations robustas, relations |
| Validação | class-validator | Validação declarativa, pipes NestJS |
| Auth | Passport + JWT | Padrão industry, extensível |
| Docs API | Swagger/OpenAPI | Auto-documentação, testing |
| Componente | Tecnologia | Justificativa |
|---|---|---|
| Framework | React 18 | Virtual DOM, hooks, ecossistema |
| Language | TypeScript 5 | Type safety, refatoração segura |
| UI Library | Tailwind CSS + shadcn/ui | Componentes acessíveis, customizáveis |
| Forms | React Hook Form + Zod | Performance, validação schema |
| State | Zustand | Simples, performático, DevTools |
| HTTP | Axios | Interceptors, cancelation |
| Build | Vite | Fast HMR, tree-shaking |
- OpenAI API (GPT-4-turbo): Geração de conteúdo, refinamento
- Exa API: Busca web + síntese de contratações similares
- Error Handling: Lança
ServiceUnavailableExceptionquando API falha - Transparência: Sem fallback silencioso - usuário sempre sabe quando busca falha
- Mensagem: "Busca externa temporariamente indisponível. Tente novamente em alguns minutos."
- Government APIs: Fontes oficiais de licitações e preços (PNCP, Compras.gov.br, SINAPI, SICRO)
- PDF Generation: Puppeteer (headless Chrome)
- Analytics: Mixpanel ou PostHog (self-hosted Railway)
Railway Stack:
├── Service: etp-express-backend (NestJS)
├── Service: etp-express-frontend (React/Vite)
├── Database: PostgreSQL 15
├── Redis: Job Queue (BullMQ)
└── Env Variables: API_KEYS, JWT_SECRET, DATABASE_URL, REDIS_URL
O ETP Express utiliza URI Versioning para a API RESTful, garantindo compatibilidade futura e clareza nos endpoints.
// backend/src/main.ts (linhas 89-96)
app.setGlobalPrefix('api');
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});| Componente | Valor |
|---|---|
| Base URL (prod) | https://etp-express-backend-production.up.railway.app |
| Prefixo | /api |
| Versão | /v1 |
| Endpoint exemplo | /auth/login |
| URL Completa | https://etp-express-backend-production.up.railway.app/api/v1/auth/login |
A variável VITE_API_URL DEVE incluir o prefixo de versão:
# Correto - inclui /api/v1
VITE_API_URL=https://etp-express-backend-production.up.railway.app/api/v1
# Incorreto - causará erro 404 em todos os endpoints
VITE_API_URL=https://etp-express-backend-production.up.railway.app/apiValidação em build-time (#915): O frontend valida a presença de /v1 no VITE_API_URL durante o build, prevenindo deploys com configuração incorreta.
Todos os controllers devem estar versionados. O decorator @Controller() com versão explícita é recomendado:
import { Controller, Get, Version } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
@Version('1') // Opcional se defaultVersion for '1'
findAll() {
// GET /api/v1/users
}
@Get()
@Version('2') // Para nova versão do endpoint
findAllV2() {
// GET /api/v2/users
}
}- Issue #913: Hotfix que corrigiu o problema de 404 no login
- Issue #914: Teste E2E de conectividade adicionado
- Issue #915: Validação de VITE_API_URL no build
PostgreSQL Connection Pooling (#108, #343)
Configuração otimizada para Railway Postgres Starter (max 20 connections):
// backend/src/app.module.ts
TypeOrmModule.forRootAsync({
extra: {
max: 20, // Max connections (Railway limit)
min: 5, // Min connections (always warm)
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 5000, // Fail after 5s if pool exhausted
},
maxQueryExecutionTime: 3000, // Log slow queries (>3s)
retryAttempts: 3, // Retry on transient failures
retryDelay: 1000, // Wait 1s between retries
});Environment Variables:
DB_POOL_MAX=20- Maximum pool size (Railway Postgres Starter limit)DB_POOL_MIN=5- Minimum pool size (warm connections)DB_IDLE_TIMEOUT=30000- Idle timeout in msDB_CONNECTION_TIMEOUT=5000- Acquisition timeout in ms
Performance Characteristics:
- Supports 100+ concurrent VUs without connection exhaustion
- Slow query logging (>3s) for performance monitoring
- Automatic retry logic for transient connection failures
- Conservative pool size prevents Railway connection limit errors
Monitoring:
- Slow queries logged automatically by TypeORM
- Connection pool metrics available via health checks
- Railway database metrics dashboard
O ETP Express implementa processamento assíncrono para operações de longa duração usando BullMQ com backend Redis.
Arquitetura:
┌────────────────────────────────────────────────────────────┐
│ ASYNC SECTION GENERATION │
├────────────────────────────────────────────────────────────┤
│ │
│ HTTP Request → Queue Job (jobId) → Return Immediately │
│ ↓ ↓ ↓ │
│ Client Polling → Worker Process → Update Progress │
│ ↓ ↓ ↓ │
│ Poll Status → AI Generation → Save to DB → Notify │
│ │
└────────────────────────────────────────────────────────────┘
Tecnologias:
- BullMQ: Redis-backed job queue com retry e scheduling
- Redis: Railway Redis service para job storage
- @nestjs/bullmq: Integração NestJS com decorators
Características:
- Retry Logic: 3 tentativas com exponential backoff (5s, 10s, 20s)
- Timeout: 5 minutos por job (configurable)
- Progress Tracking: Updates em tempo real (0-100%)
- Job Cleanup: 100 completed jobs retained, 1000 failed jobs
- Escalabilidade: Múltiplos workers podem processar jobs em paralelo
Endpoints:
// Queue a section generation job
POST /sections/etp/:etpId/generate
Response: { jobId: string, status: 'waiting', ... }
// Poll job status
GET /sections/jobs/:jobId
Response: {
jobId: string,
status: 'waiting' | 'active' | 'completed' | 'failed',
progress: number, // 0-100
result?: SectionDTO, // Available when completed
error?: string, // Available when failed
createdAt: Date,
completedAt?: Date
}Fluxo de Geração:
- Client envia
POST /sections/etp/:etpId/generate - Backend cria section entity com status
GENERATING - Job enfileirado no BullMQ com jobId retornado
- Client faz polling de
GET /sections/jobs/:jobIda cada 2-3s - Worker processa job em background:
- 10%: Valida section exists
- 10-90%: OrchestratorService gera conteúdo via LLM
- 90%: Salva conteúdo no database
- 95%: Atualiza ETP completion percentage
- 100%: Job completo
- Client detecta
status: 'completed'e busca section atualizada
Configuração (backend/src/app.module.ts):
BullModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const redisConf = configService.get('redis');
return {
connection: {
host: redisConf.host,
port: redisConf.port,
password: redisConf.password,
db: 0,
maxRetriesPerRequest: null, // Required for BullMQ
enableReadyCheck: false,
},
};
},
}),Redis Configuration (backend/src/config/redis.config.ts):
- Parseia
REDIS_URLdo Railway (formato:redis://:password@host:port) - Fallback para localhost em desenvolvimento
- maxRetriesPerRequest: null (required by BullMQ)
- enableReadyCheck: false (recommended by BullMQ)
Benefits:
- ✅ Evita timeouts HTTP (gerações de 30-60s)
- ✅ Melhor UX (feedback de progresso em tempo real)
- ✅ Retry automático em falhas transientes
- ✅ Escalabilidade horizontal (add more workers)
- ✅ Job monitoring e debugging (retained failed jobs)
O ETP Express implementa dois tipos de health checks para garantir zero-downtime deployment (#181):
GET /api/health (Liveness Probe)
- Propósito: Verifica se a aplicação está viva (não crashou)
- Comportamento: Retorna 200 mesmo durante initialization/migrations
- Uso: Railway utiliza para detectar crashes e reiniciar containers
- Response:
{
"status": "healthy",
"timestamp": "2025-11-29T12:00:00.000Z",
"database": "connected"
}GET /api/health/ready (Readiness Probe)
- Propósito: Verifica se está pronto para receber tráfego
- Comportamento: Retorna 503 durante migrations/initialization
- Uso: deploy.sh utiliza para decisão de switch de tráfego
- Response (ready):
{
"status": "ready",
"timestamp": "2025-11-29T12:00:00.000Z",
"database": "connected",
"migrations": "completed"
}- Response (starting):
{
"status": "starting",
"reason": "migrations_in_progress",
"database": "connected",
"timestamp": "2025-11-29T12:00:00.000Z"
}Distinção Liveness vs Readiness:
- Liveness: "A aplicação está respondendo?" (evita restarts desnecessários durante boot)
- Readiness: "A aplicação pode processar requests?" (evita rotear tráfego antes de pronta)
Migration-Aware Design:
- TypeORM executa migrations síncronas no boot
- Readiness probe detecta migrations pendentes via
DataSource.showMigrations() - Deploy script aguarda status
readyantes de switch de tráfego - Evita falsos-positivos quando DB está conectado mas migrations pendentes
class ETOrchestratorService {
async generateSection(
sectionId: string,
userContext: UserInput,
etpDraft: ETPDraft,
): Promise<GeneratedSection> {
// Chain de subagentes
const chain = [
this.legalAgent, // Valida coerência legal superficial
this.fundamentacaoAgent, // Busca contratações similares
this.clarezaAgent, // Revisa clareza textual
this.simplificacaoAgent, // Simplifica linguagem jurídica
this.antiHallucinationAgent, // Mitiga alucinações
];
let result = await this.llmWrapper.generate(userContext);
for (const agent of chain) {
result = await agent.process(result);
}
return this.normalizer.format(result);
}
}@Injectable()
export class LegalAgent {
async process(draft: string): Promise<ProcessedDraft> {
// Valida menções à Lei 14.133/2021
// Verifica consistência de incisos citados
// Injeta avisos "Confirme se esta interpretação se aplica ao seu órgão"
}
}@Injectable()
export class FundamentacaoAgent {
constructor(private exaService: ExaService) {}
async process(draft: string): Promise<ProcessedDraft> {
// Extrai objeto da contratação
// Busca contratações similares via Exa + Government APIs
// Anexa referências com aviso "verifique a fonte antes de utilizar"
}
}@Injectable()
export class ClarezaAgent {
async process(draft: string): Promise<ProcessedDraft> {
// Identifica jargão excessivo
// Sugere simplificações
// Valida Flesch Reading Ease > 50
}
}@Injectable()
export class AntiHallucinationAgent {
async process(draft: string): Promise<ProcessedDraft> {
// Injeta prompts defensivos
// Valida afirmações factuais contra base de conhecimento
// Marca trechos de baixa confiança para revisão humana
}
}AVISO IMPORTANTE: Esta seção documenta uma decisão arquitetural crítica. Alterações na natureza dos agentes (de determinístico para probabilístico) mudarão drasticamente o comportamento do sistema.
| Agente | Tipo | Método de Validação | Comportamento |
|---|---|---|---|
| AntiHallucinationAgent | DETERMINÍSTICO | Regex + RAG lookup | Sempre retorna o mesmo resultado para a mesma entrada |
| LegalAgent | DETERMINÍSTICO | Keyword matching (includes) |
Sempre retorna o mesmo resultado para a mesma entrada |
| ClarezaAgent | DETERMINÍSTICO | Métricas estatísticas (Flesch-like) | Sempre retorna o mesmo resultado para a mesma entrada |
| FundamentacaoAgent | DETERMINÍSTICO | Keyword matching (includes) |
Sempre retorna o mesmo resultado para a mesma entrada |
| SimplificacaoAgent | DETERMINÍSTICO | Regex patterns + substituição | Sempre retorna o mesmo resultado para a mesma entrada |
A nomenclatura "Agent" segue o padrão arquitetural de subagentes especializados no pipeline de processamento, não implica necessariamente comportamento adaptativo ou uso de IA generativa.
Vantagens da abordagem determinística:
- Previsibilidade: Resultados consistentes e auditáveis
- Performance: Execução em milissegundos (sem chamadas LLM)
- Custo: Sem consumo de tokens OpenAI para validação
- Testabilidade: Testes unitários determinísticos
- Compliance: Facilita auditoria em contexto de licitações públicas
Os agentes NÃO usam LLM para validação. O LLM é usado apenas:
- Geração de conteúdo:
OrchestratorService→ OpenAI API - Prompts de contexto: Agentes fornecem
getSystemPrompt()para instruir o LLM
┌─────────────────────────────────────────────────────────────────┐
│ FLUXO DE GERAÇÃO + VALIDAÇÃO │
├─────────────────────────────────────────────────────────────────┤
│ │
│ UserInput │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ OrchestratorService │ │
│ │ (usa LLM para GERAR conteúdo) │ │
│ └──────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ Pipeline de Validação │ │
│ │ (DETERMINÍSTICO - sem LLM) │ │
│ │ │ │
│ │ 1. LegalAgent (keyword matching) │ │
│ │ 2. FundamentacaoAgent (keyword) │ │
│ │ 3. ClarezaAgent (métricas) │ │
│ │ 4. SimplificacaoAgent (regex) │ │
│ │ 5. AntiHallucinationAgent (regex) │ │
│ └──────────────────────────────────────┘ │
│ ↓ │
│ ValidatedContent + Warnings │
│ │
└─────────────────────────────────────────────────────────────────┘
CUIDADO: Se no futuro decidir substituir validações Regex por modelos de IA:
| Aspecto | Determinístico (atual) | Probabilístico (futuro) |
|---|---|---|
| Consistência | 100% reprodutível | Variável entre execuções |
| Latência | ~1-10ms | ~500-3000ms |
| Custo | $0 | ~$0.001-0.01 por validação |
| Auditoria | Trivial (mesma entrada = mesma saída) | Complexa (precisa logar prompts/respostas) |
| Testes | Unitários simples | Testes probabilísticos ou mocks |
Recomendação: Manter agentes de validação como determinísticos. Usar LLM apenas para geração de conteúdo, não para validação.
-- Usuários e Autenticação
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
orgao VARCHAR(255),
role VARCHAR(50) DEFAULT 'user',
created_at TIMESTAMP DEFAULT NOW()
);
-- ETPs
CREATE TABLE etps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
title VARCHAR(500) NOT NULL,
object TEXT,
status VARCHAR(50) DEFAULT 'draft', -- draft, complete, exported
current_version INT DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Seções do ETP (Incisos da Lei)
CREATE TABLE etp_sections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
etp_id UUID REFERENCES etps(id) ON DELETE CASCADE,
section_code VARCHAR(10) NOT NULL, -- I, II, III, IV... XIII
section_title VARCHAR(255) NOT NULL,
content JSONB, -- { draft, suggestions, references, metadata }
is_mandatory BOOLEAN DEFAULT false,
is_complete BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(etp_id, section_code)
);
-- Versionamento
CREATE TABLE etp_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
etp_id UUID REFERENCES etps(id) ON DELETE CASCADE,
version_number INT NOT NULL,
snapshot JSONB NOT NULL, -- Full ETP snapshot
changed_sections TEXT[], -- ['I', 'IV', 'VIII']
user_id UUID REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(etp_id, version_number)
);
-- Auditoria
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
etp_id UUID REFERENCES etps(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id),
action VARCHAR(100) NOT NULL, -- created, edited_section, exported, etc
section_code VARCHAR(10),
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- Referências de Contratações Similares
CREATE TABLE similar_contracts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
etp_id UUID REFERENCES etps(id) ON DELETE CASCADE,
section_code VARCHAR(10),
url TEXT,
title TEXT,
summary TEXT,
source VARCHAR(100), -- 'exa', 'gov-api', 'manual'
fetched_at TIMESTAMP DEFAULT NOW()
);
-- Telemetria (Analytics UX)
CREATE TABLE analytics_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
etp_id UUID REFERENCES etps(id) ON DELETE SET NULL,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
event_type VARCHAR(100), -- section_opened, llm_suggestion_accepted, etc
section_code VARCHAR(10),
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- Índices para performance
CREATE INDEX idx_etps_user_id ON etps(user_id);
CREATE INDEX idx_etp_sections_etp_id ON etp_sections(etp_id);
CREATE INDEX idx_etp_versions_etp_id ON etp_versions(etp_id);
CREATE INDEX idx_audit_logs_etp_id ON audit_logs(etp_id);
CREATE INDEX idx_analytics_events_created_at ON analytics_events(created_at);| Código | Descrição | Obrigatório |
|---|---|---|
| I | Descrição da necessidade da contratação | ✅ SIM |
| II | Demonstração da previsão de recursos orçamentários | ❌ Não |
| III | Estimativa preliminar de despesa | ❌ Não |
| IV | Justificativa da solução escolhida | ✅ SIM |
| V | Descrição da solução como um todo | ❌ Não |
| VI | Requisitos da contratação | ✅ SIM |
| VII | Estimativas de valor da contratação | ❌ Não |
| VIII | Justificativa do parcelamento ou não da contratação | ✅ SIM |
| IX | Contratações correlatas | ❌ Não |
| X | Demonstração de compatibilidade do orçamento | ❌ Não |
| XI | Descrição dos riscos | ❌ Não |
| XII | Providências a serem adotadas | ❌ Não |
| XIII | Declaração de viabilidade | ✅ SIM |
POST /api/auth/register # Criar conta
POST /api/auth/login # Login (retorna JWT)
POST /api/auth/logout # Logout
GET /api/auth/me # Usuário atual
GET /api/etps # Listar ETPs do usuário
POST /api/etps # Criar novo ETP
GET /api/etps/:id # Obter ETP específico
PATCH /api/etps/:id # Atualizar metadados
DELETE /api/etps/:id # Deletar ETP
GET /api/etps/:id/sections # Listar seções
GET /api/etps/:id/sections/:code # Obter seção específica
POST /api/etps/:id/sections/:code/generate # Gerar conteúdo via LLM
PATCH /api/etps/:id/sections/:code # Atualizar seção
POST /api/etps/:id/sections/:code/refine # Refinar conteúdo existente
POST /api/etps/:id/sections/:code/alternatives # Gerar alternativas
GET /api/etps/:id/versions # Histórico de versões
POST /api/etps/:id/versions # Criar snapshot
GET /api/etps/:id/versions/:version # Obter versão específica
POST /api/etps/:id/versions/:version/restore # Restaurar versão
GET /api/etps/:id/versions/diff/:v1/:v2 # Diff entre versões
POST /api/etps/:id/export/pdf # Gerar PDF (com aviso destacado)
POST /api/etps/:id/export/json # Export estruturado JSON
POST /api/etps/:id/export/xml # Export estruturado XML
GET /api/etps/:id/validate # Validar completude (incisos obrigatórios)
POST /api/search/similar-contracts # Buscar via Exa + Government APIs
GET /api/etps/:id/references # Listar referências anexadas
POST /api/etps/:id/references # Adicionar referência manual
DELETE /api/references/:id # Remover referência
POST /api/analytics/event # Registrar evento telemetria
GET /api/analytics/summary # Resumo de uso (admin)
sequenceDiagram
participant User
participant Frontend
participant Backend
participant Orchestrator
participant OpenAI
participant Exa
participant DB
User->>Frontend: Preenche contexto seção IV
Frontend->>Backend: POST /sections/IV/generate
Backend->>Orchestrator: orchestrate(userInput)
Orchestrator->>OpenAI: Gerar draft inicial
OpenAI-->>Orchestrator: Draft v1
Orchestrator->>Exa: Buscar contratações similares
Exa-->>Orchestrator: [3 referências]
Orchestrator->>OpenAI: Refinar com fundamentação
OpenAI-->>Orchestrator: Draft v2 + referências
Orchestrator->>Orchestrator: LegalAgent.process()
Orchestrator->>Orchestrator: ClarezaAgent.process()
Orchestrator->>Orchestrator: AntiHallucination.process()
Orchestrator->>DB: Salvar seção + referências
Orchestrator-->>Backend: GeneratedSection
Backend-->>Frontend: { content, references, warnings }
Frontend->>User: Exibe sugestão com avisos
const ANTI_HALLUCINATION_PROMPT = `
IMPORTANTE: Você está auxiliando na elaboração de um documento administrativo oficial.
REGRAS OBRIGATÓRIAS:
1. NÃO invente números, valores, datas ou referências legais
2. SE não tiver certeza de uma informação, use placeholder: "[A SER VALIDADO PELO SERVIDOR]"
3. SEMPRE cite a fonte de informações factuais
4. Para interpretações legais, use: "Esta é uma interpretação sugerida, confirme com setor jurídico"
5. Ao sugerir valores estimados, base-se APENAS nas referências de contratações similares fornecidas
Seu objetivo é AUXILIAR, não decidir. Toda afirmação deve ser auditável.
`;class AntiHallucinationAgent {
private readonly FORBIDDEN_PATTERNS = [
/\bgaranto\b/i,
/\bcom certeza\b/i,
/\bsempre\b/i,
/\bnunca\b/i,
/\bdefinitivamente\b/i,
];
async process(draft: string): Promise<ProcessedDraft> {
// Detecta afirmações absolutas
// Injeta disclaimers em citações legais
// Marca valores numéricos sem fonte
// Sugere revisão humana em trechos de baixa confiança
}
}// Componente de aviso persistente
<WarningBanner variant="critical" sticky>
⚠ O ETP Express pode cometer erros. Lembre-se de verificar todas as
informações antes de realizar qualquer encaminhamento.
</WarningBanner>
// Tooltips contextuais em sugestões LLM
<Tooltip>
Esta é uma sugestão gerada por IA. Revise criticamente antes de aceitar.
</Tooltip>
// Badges em referências externas
<Badge variant="warning">
Verifique a fonte antes de utilizar
</Badge>- Visibilidade de Estado: Barra de progresso, status de seções, indicador de salvamento
- Correspondência com Mundo Real: Linguagem simples, tooltips explicativos
- Controle e Liberdade: Desfazer/refazer, múltiplas versões, draft lateral
- Consistência: Design system unificado (shadcn/ui)
- Prevenção de Erros: Validação em tempo real, confirmações antes de ações críticas
- Reconhecimento vs Memória: Sugestões contextuais, templates preenchidos
- Flexibilidade: Atalhos de teclado, modos de visualização
- Design Minimalista: Foco no essencial, progressive disclosure
- Recuperação de Erros: Mensagens claras, sugestões de correção
- Ajuda e Documentação: Tooltips, modal de ajuda, exemplos inline
- Contraste: Mínimo 4.5:1 para texto normal, 3:1 para texto grande
- Navegação por Teclado: Tab order lógico, focus visible
- Screen Readers: ARIA labels, landmarks semânticos
- Responsivo: Suporta zoom até 200%, layout adaptável
- Formulários: Labels explícitos, mensagens de erro associadas
# railway.json (monorepo)
{
'$schema': 'https://railway.app/railway.schema.json',
'build': { 'builder': 'NIXPACKS' },
'deploy':
{
'numReplicas': 1,
'restartPolicyType': 'ON_FAILURE',
'restartPolicyMaxRetries': 10,
},
}# Backend
NODE_ENV=production
DATABASE_URL=${{Postgres.DATABASE_URL}}
JWT_SECRET=${{secrets.JWT_SECRET}}
OPENAI_API_KEY=${{secrets.OPENAI_API_KEY}}
EXA_API_KEY=${{secrets.EXA_API_KEY}}
FRONTEND_URL=https://etp-express-frontend-production.up.railway.app
CORS_ORIGINS=https://etp-express-frontend-production.up.railway.app
# Frontend
VITE_API_URL=https://etp-express-backend-production.up.railway.app
VITE_APP_NAME="ETP Express"// package.json (backend)
{
"scripts": {
"build": "nest build",
"start:prod": "node dist/main",
"migration:run": "typeorm migration:run -d dist/config/typeorm.config.js"
}
}
// package.json (frontend)
{
"scripts": {
"build": "vite build",
"preview": "vite preview --port 3000"
}
}- ✅ Auth básica (JWT)
- ✅ CRUD de ETPs
- ✅ Orquestrador + wrapper OpenAI
- ✅ Formulário guiado (seções I, IV, VI, VIII, XIII)
- ✅ Export PDF com avisos
- ✅ Deploy Railway
- ✅ Integração Exa + Government APIs
- ✅ Agente de fundamentação
- ✅ Painel de referências
- ✅ Sistema de versões
- ✅ Diff textual
- ✅ Trilha de auditoria
- Telemetria analytics
- Tooltips contextuais
- Microinterações
- Loading states elegantes
- Cache de sugestões LLM
- Rate limiting
- Testes E2E
- Documentação completa
- Suporte a modelos on-premise (Llama, Mistral)
- IA híbrida (modelo local + cloud fallback)
- Integração com COMPRASNET
- Templates por órgão/setor
- Modo colaborativo (múltiplos usuários)
- OWASP Top 10:
- ✅ Sanitização de inputs (class-validator)
- ✅ Prepared statements (TypeORM protege contra SQL Injection)
- ✅ CORS configurado
- ✅ Helmet.js (headers de segurança)
- ✅ Rate limiting (express-rate-limit)
- ✅ JWT com expiração
- ✅ Bcrypt para senhas
- ✅ HTTPS obrigatório (Railway)
- Dados sensíveis (API keys) em variáveis de ambiente
- Analytics anonimizado (sem PII)
- Logs sanitizados (sem tokens, senhas)
- LGPD-friendly: usuário pode exportar/deletar dados
Status: Using Railway native secrets management
Approach: Manual rotation with documented procedures
Rationale: See docs/SECRETS_MANAGEMENT_EVALUATION.md
Railway platform provides integrated environment variable management with sealed variables that are not visible in the UI once set. This is sufficient for ETP Express MVP.
┌──────────────────────────────────────────────────────────┐
│ RAILWAY NATIVE SECRETS MANAGEMENT │
├──────────────────────────────────────────────────────────┤
│ │
│ Developer │
│ ↓ │
│ Railway Dashboard / CLI │
│ ↓ │
│ Sealed Environment Variables │
│ ↓ │
│ Application @ Runtime │
│ ↓ │
│ GitHub Issues (Audit Trail) │
│ │
└──────────────────────────────────────────────────────────┘
| Secret | Frequency | Method |
|---|---|---|
| JWT_SECRET | Monthly | Manual rotation + documentation |
| SESSION_SECRET | Monthly | Manual rotation + documentation |
| OPENAI_API_KEY | Quarterly | Manual rotation (provider) |
| EXA_API_KEY | Quarterly | Manual rotation (provider) |
| DATABASE_URL | On-demand | Manual rotation (DB password) |
Simple 4-step process for manual rotation:
- Generate new secret value (openssl rand -base64 32)
- Update in Railway dashboard
- Trigger auto-redeploy
- Create GitHub issue to track rotation
No external platforms. No AWS accounts. No bootstrap credentials.
Track all rotations via:
- GitHub Issues: One per rotation cycle (label:
security) - Railway logs: Auto-captured deployment logs
- Git commits: Signed commits for any automation scripts
Meets LGPD audit trail requirement at MVP scale.
For full strategy: See docs/SECRETS_MANAGEMENT_EVALUATION.md
O ETP Express implementa uma estratégia de testes em múltiplas camadas, garantindo qualidade de código e funcionalidade através de testes automatizados e integração contínua.
╭───────────╮
/ \
/ E2E Tests \ ← Playwright + Puppeteer
/ (11 tests) \ (Fluxos críticos)
╰─────────────────╯
╭─────────────────────╮
/ Integration Tests \ ← Jest + Supertest
/ (150+ tests) \ (Controllers, APIs)
╰─────────────────────────╯
╭─────────────────────────────╮
/ Unit Tests \ ← Jest + Vitest
/ (800+ tests) \ (Services, Components)
╰─────────────────────────────────╯
Cobertura Atual:
- Backend: 78%+ (statements), meta 80%
- Frontend: 58%+ (statements), meta 65%
- Total de Testes: 920+ testes passando
ETP Express/
├── backend/
│ ├── src/
│ │ ├── modules/
│ │ │ ├── auth/
│ │ │ │ ├── auth.service.spec.ts # Unit tests
│ │ │ │ ├── auth.controller.spec.ts # Integration tests
│ │ │ │ └── strategies/
│ │ │ │ ├── jwt.strategy.spec.ts
│ │ │ │ └── local.strategy.spec.ts
│ │ │ ├── etps/
│ │ │ │ ├── etps.service.spec.ts
│ │ │ │ ├── etps.controller.spec.ts
│ │ │ │ └── cascade-delete.spec.ts
│ │ │ ├── orchestrator/
│ │ │ │ ├── orchestrator.service.spec.ts
│ │ │ │ └── agents/
│ │ │ │ ├── legal.agent.spec.ts
│ │ │ │ ├── clareza.agent.spec.ts
│ │ │ │ ├── fundamentacao.agent.spec.ts
│ │ │ │ ├── simplificacao.agent.spec.ts
│ │ │ │ └── anti-hallucination.agent.spec.ts
│ │ │ └── sections/
│ │ │ ├── sections.service.spec.ts
│ │ │ ├── sections.controller.spec.ts
│ │ │ └── sections.processor.spec.ts
│ │ └── common/
│ │ ├── guards/
│ │ │ ├── tenant.guard.spec.ts
│ │ │ └── user-throttler.guard.spec.ts
│ │ └── utils/
│ │ └── retry.spec.ts
│ ├── test/
│ │ └── lgpd-compliance.e2e-spec.ts # E2E Tests (NestJS)
│ └── jest.config.js
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ │ └── **/*.test.tsx # Component tests
│ │ ├── hooks/
│ │ │ └── **/*.test.ts # Hook tests
│ │ ├── services/
│ │ │ └── **/*.test.ts # Service tests
│ │ └── test/
│ │ └── setup.ts # Vitest setup
│ └── vitest.config.ts
├── e2e/
│ └── accessibility.spec.ts # Playwright Accessibility
├── puppeteer-tests/
│ ├── critical-flow.spec.ts # Fluxo crítico completo
│ ├── login.spec.ts
│ └── utils/
│ └── setup.ts
└── playwright.config.ts
Framework: Jest v29 + ts-jest + @nestjs/testing
Configuração (backend/jest.config.js):
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: { '^.+\\.(t|j)s$': 'ts-jest' },
collectCoverageFrom: ['**/*.(t|j)s', '!**/*.module.ts', '!main.ts'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
moduleNameMapper: { '^src/(.*)$': '<rootDir>/$1' },
};Padrões de Teste:
// Exemplo: backend/src/modules/auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
describe('AuthService', () => {
let service: AuthService;
let usersService: UsersService;
const mockUsersService = {
findByEmail: jest.fn(),
create: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{ provide: UsersService, useValue: mockUsersService },
],
}).compile();
service = module.get<AuthService>(AuthService);
jest.clearAllMocks();
});
describe('validateUser', () => {
it('should return user without password when credentials are valid', async () => {
// Arrange
mockUsersService.findByEmail.mockResolvedValue(mockUser);
// Act
const result = await service.validateUser('test@example.com', 'password');
// Assert
expect(result).toBeDefined();
expect(result?.password).toBeUndefined();
});
});
});Framework: Vitest v4 + @testing-library/react + jsdom
Configuração (frontend/vitest.config.ts):
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
css: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test/', '**/*.d.ts'],
},
},
});Padrões de Teste:
// Exemplo: frontend/src/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('should render with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('should call onClick when clicked', () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click</Button>);
fireEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
});Controller Tests:
// Testes de integração validam endpoints HTTP reais
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
describe('AuthController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('/auth/login (POST) should return JWT token', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'test@example.com', password: 'password123' })
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('accessToken');
});
});
});O ETP Express utiliza duas ferramentas de E2E complementares:
Framework: Playwright v1.40+ com @axe-core/playwright
Configuração (playwright.config.ts):
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: process.env.E2E_BASE_URL || 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'cd frontend && npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});Testes de Acessibilidade (WCAG 2.1 AA):
// e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('WCAG 2.1 AA Compliance', () => {
const pages = [
{ path: '/login', name: 'Login' },
{ path: '/register', name: 'Register' },
{ path: '/dashboard', name: 'Dashboard' },
];
for (const page of pages) {
test(`${page.name} should be WCAG 2.1 AA compliant`, async ({
page: p,
}) => {
await p.goto(page.path);
await p.waitForLoadState('networkidle');
const results = await new AxeBuilder({ page: p })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});
}
});Framework: Puppeteer v24 para testes de fluxo crítico com mocking de APIs
Fluxo Crítico Testado:
- Login → Dashboard
- Criar novo ETP (modal)
- Abrir ETP no editor
- Gerar seção com IA (mockado)
- Salvar seção
- Exportar PDF
// puppeteer-tests/critical-flow.spec.ts
describe('Critical Flow E2E', () => {
beforeEach(async () => {
const setup = await setupBrowser();
browser = setup.browser;
page = setup.page;
// Mock API calls (evita custos OpenAI)
await page.setRequestInterception(true);
page.on('request', (request) => {
if (
request.url().includes('/api/sections/') &&
request.url().includes('/generate')
) {
request.respond({
status: 201,
contentType: 'application/json',
body: JSON.stringify({
content: '# Seção Gerada (Mock)',
status: 'completed',
}),
});
} else {
request.continue();
}
});
});
test('deve completar fluxo crítico', async () => {
await login(page, testUser.email, testUser.password);
expect(page.url()).toContain('/dashboard');
// ... resto do fluxo
}, 90000);
});O ETP Express utiliza GitHub Actions para CI/CD com otimizações de cache.
1. ci-tests.yml - Testes Unitários + Cobertura:
name: CI - Tests
on:
push:
branches: [master]
paths:
- 'backend/**/*.ts'
- 'frontend/**/*.{ts,tsx}'
- '.github/workflows/ci-tests.yml'
pull_request:
branches: [master]
jobs:
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:cov
working-directory: backend
- name: Check coverage threshold (70%)
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
if (( $(echo "$COVERAGE < 70" | bc -l) )); then
exit 1
fi
test-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:coverage
working-directory: frontend2. ci-lint.yml - ESLint + TypeScript:
name: CI - Lint
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
lint-backend:
runs-on: ubuntu-latest
steps:
- run: npm run lint
working-directory: backend
lint-frontend:
runs-on: ubuntu-latest
steps:
- run: npm run lint
working-directory: frontend3. playwright.yml - Testes E2E:
name: Playwright Tests
on:
push:
branches: [master]
paths:
- 'e2e/**/*'
- 'playwright.config.ts'
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/| Otimização | Economia |
|---|---|
| Cache NPM | ~60% tempo de build |
| Cache Playwright | ~3-4 min por run |
| Path Filters | Commits docs-only não acionam CI |
| Codecov Integration | Tracking automático de coverage |
cd backend
# Testes unitários
npm test
# Testes com watch mode
npm run test:watch
# Testes com cobertura
npm run test:cov
# Testes E2E (requer banco de dados)
npm run test:e2e
# Debug de testes (Node Inspector)
npm run test:debugcd frontend
# Testes unitários (watch mode por padrão)
npm test
# Testes com UI interativa
npm run test:ui
# Testes com cobertura
npm run test:coverage# Na raiz do projeto
# Instalar browsers (primeira vez)
npx playwright install
# Rodar todos os testes E2E
npx playwright test
# Rodar em um browser específico
npx playwright test --project=chromium
# Modo UI (interativo)
npx playwright test --ui
# Gerar relatório HTML
npx playwright show-reportcd puppeteer-tests
# Rodar testes de fluxo crítico
npm test
# Com debug visual (headful)
PUPPETEER_HEADLESS=false npm test// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Jest Backend",
"program": "${workspaceFolder}/backend/node_modules/.bin/jest",
"args": ["--runInBand", "--watchAll=false"],
"cwd": "${workspaceFolder}/backend",
"console": "integratedTerminal"
},
{
"type": "node",
"request": "launch",
"name": "Debug Vitest Frontend",
"runtimeExecutable": "npx",
"runtimeArgs": ["vitest", "--no-coverage"],
"cwd": "${workspaceFolder}/frontend",
"console": "integratedTerminal"
}
]
}Jest (Backend):
# Rodar apenas um arquivo
npm test -- auth.service.spec.ts
# Rodar apenas um teste específico
npm test -- -t "should return user without password"
# Debug com Node Inspector
node --inspect-brk node_modules/.bin/jest --runInBandVitest (Frontend):
# Rodar apenas um arquivo
npx vitest Button.test.tsx
# Rodar com filtro de nome
npx vitest -t "should render"
# Modo UI para debug visual
npx vitest --uiPlaywright (E2E):
# Debug visual com pause
npx playwright test --debug
# Gerar screenshots em cada step
npx playwright test --trace on
# Codegen para gerar testes
npx playwright codegen localhost:5173| Tipo | Padrão | Exemplo |
|---|---|---|
| Unit (Backend) | *.spec.ts |
auth.service.spec.ts |
| Unit (Frontend) | *.test.tsx |
Button.test.tsx |
| Integration | *.spec.ts |
auth.controller.spec.ts |
| E2E | *.e2e-spec.ts ou *.spec.ts |
critical-flow.spec.ts |
it('should do something', async () => {
// Arrange - Setup inicial
const mockData = { id: 1, name: 'Test' };
mockService.findOne.mockResolvedValue(mockData);
// Act - Executar ação
const result = await service.getById(1);
// Assert - Verificar resultado
expect(result).toEqual(mockData);
expect(mockService.findOne).toHaveBeenCalledWith(1);
});Backend (NestJS):
// Mock de serviço injetado
const mockUsersService = {
findByEmail: jest.fn(),
create: jest.fn(),
};
// Uso no módulo de teste
const module = await Test.createTestingModule({
providers: [
AuthService,
{ provide: UsersService, useValue: mockUsersService },
],
}).compile();Frontend (MSW - Mock Service Worker):
// frontend/src/test/handlers.ts
import { rest } from 'msw';
export const handlers = [
rest.post('/api/auth/login', (req, res, ctx) => {
return res(
ctx.json({
accessToken: 'mock-token',
user: { id: 1, email: 'test@example.com' },
}),
);
}),
];import { Logger } from '@nestjs/common';
this.logger.log({
event: 'etp_section_generated',
etpId: etp.id,
sectionCode: 'IV',
llmProvider: 'openai',
tokensUsed: 1250,
latencyMs: 3400,
});- Latência LLM > 10s
- Taxa de erro > 5%
- Quota OpenAI próxima do limite
- Database connections > 80%
Documento vivo: Este arquivo será atualizado conforme o desenvolvimento avança.
Última atualização: 2025-12-23 Versão: 0.3.1 (API Versioning Documentation)