Files
sar/CODING-RULES.md
julian 17c08e6392 chore: initial monorepo scaffold + WDS Phase 1+2 artifacts
- Nx 22.7 monorepo (pnpm 11.1, TypeScript 5.9, Node 24)
- apps/api: NestJS 11 (CJS conforme CODING-RULES.md PGD-DB-004)
- apps/web: React 19 + Vite 8 (ESM)
- libs/shared/api-interface: Zod contract base
- Docker Compose dev: Postgres 18, Valkey 8, MinIO, Mailpit
- WDS artifacts:
  - design-artifacts/A-Product-Brief/ (5 docs canônicos + 16 dialogs)
  - design-artifacts/B-Trigger-Map/ (hub + 4 personas + feature impact)
- Stack canon: STACK.md v2.2 + CODING-RULES.md v2.0 + brand.md
- AGENTS.md + README.md como entrada para devs/agentes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:34:20 +00:00

136 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODING-RULES.md — Regras de Geração de Código
> Define **como escrever o código**: invariantes obrigatórias e armadilhas críticas. **Leia antes de gerar ou alterar qualquer código.**
> Companion de [`STACK.md`](STACK.md) (tecnologias e versões). Documentação completa em [`docs/`](docs/) (capítulos `00``23`).
<!--
FONTE DA VERDADE: stack-jcs/CODING-RULES.md (sincronizado com docs/).
Distribuído por cópia para repos de apps consumidoras. Não editar fora deste repo.
Versão: 2.0 — última revisão: 2026-05-24.
Major: migração de AWS sa-east-1 para Proxmox on-prem — ver docs/adr/0004-migracao-aws-proxmox.md.
-->
## 1. Invariantes (constituição — não mudam sem RFC)
**Tipos & contrato**
- Schema **Zod compartilhado** entre Nest e React (DTO via `createZodDto`, nunca duplicado). Schemas em `*.schema.ts`; DTOs em `*.dto.ts`; ambos em `libs/shared/api-interface`. → [§06](docs/06-validacao.md)
- Erros HTTP em **RFC 9457 Problem Details**; Zod → **422** (nunca 400). 403 vira **404** quando o cliente não tem acesso (não vazar existência). → [§05](docs/05-api-design.md)
- `tsconfig` base: `noUncheckedIndexedAccess`, `module/moduleResolution: NodeNext`, `experimentalDecorators` + `emitDecoratorMetadata`, `target: ES2023`. → [§01](docs/01-runtime-tooling.md)
- `openapi.json` em `libs/shared/api-interface/`; CI bloqueia PR sem bump de versão na URL. → [§05](docs/05-api-design.md)
**Boot, config & secrets**
- `import './tracing'` como **primeiro import** de `main.ts` (OTel monkey-patch antes do Nest). → [§09](docs/09-observabilidade.md)
- Config Nest validada com **Zod** (`validate: env => EnvSchema.parse(env)`) — fail-fast no boot. → [§08](docs/08-secrets-config.md)
- Secrets via **HashiCorp Vault** (KV v2); runtime via Vault Agent injetando env no entrypoint do container. Nunca env hardcoded ou embutido no bundle. → [§08](docs/08-secrets-config.md)
**Dados, jobs & side-effects**
- Mudanças em DB via **migration Prisma** (expand/contract, backward-compatible, sem `db push` em prod); rodam **antes** do rolling deploy via URL direta (5432, sem PgBouncer). → [§03](docs/03-backend.md), [§18](docs/18-cicd-containers.md)
- Trabalho assíncrono (email, webhook, processamento, agendado) **via BullMQ**; `jobId` determinístico; `queue.add` **depois** do `prisma.$transaction`. → [§11](docs/11-filas-jobs.md)
- POST financeiro/crítico (pagamentos, pedidos) exige header **`Idempotency-Key`** memoizado por 24h. → [§05](docs/05-api-design.md)
**Auth & rate limit**
- Refresh em cookie `httpOnly; Secure; SameSite=Lax; Path=/api/v1/auth/refresh`; access token em memória (nunca `localStorage`). → [§07](docs/07-autenticacao.md)
- Senha mínimo 12 chars + `zxcvbn`/`haveibeenpwned`; hash **argon2id** `m=65536, t=3, p=4` (defaults da lib `argon2@0.44.0`, centralizado no master-login). Refresh token com family revocation e hash no DB (32 bytes base64url, single-use, TTL 15-30 min). → [§07](docs/07-autenticacao.md)
- Rate limit em `/login`, `/signup`, `/forgot-password`: **5/min/IP + 10/h/email** via `@nestjs/throttler` (Valkey). → [§07](docs/07-autenticacao.md), [§20](docs/20-praticas-nest-universais.md)
**Frontend & UX**
- Estado de servidor em **TanStack Query**; estado de formulário em **react-hook-form**; cliente em **Zustand**. Não misturar. → [§04](docs/04-frontend.md)
- Upload via **MinIO presigned POST policy** (S3-compat) com `content-length-range` + `starts-with $Content-Type`; chave `users/<userId>/`; nunca proxy upload/download pelo backend. → [§14](docs/14-uploads.md)
- Frontend Vite servido por **Nginx em VM Proxmox + Cloudflare**; `Cache-Control: immutable` em assets versionados, `no-cache` + purge Cloudflare em `index.html`. → [§18](docs/18-cicd-containers.md)
**Monorepo & flags**
- Toda lib nova declara as **três tags** `scope:api|web|shared` + `type:app|domain|feature|data-access|ui|util` + `domain:<nome>|shared` ([ADR 0002](docs/adr/0002-tags-nx-canonicas.md)). Sem `layer:*` (deprecado). CI roda `pnpm nx affected -t lint` para validar boundaries. → [§02](docs/02-monorepo-nx.md)
- Dependências compartilhadas (`zod`, `react`, `typescript`) via pnpm catalogs (`"zod": "catalog:"`). Named exports em libs; `index.ts` expõe a API pública. → [§01](docs/01-runtime-tooling.md), [§02](docs/02-monorepo-nx.md)
- Feature flags em `libs/shared/feature-flags` com `owner` + `expiresAt` obrigatórios; CI falha em flag expirada, sem owner ou chave duplicada. Prefixos `rel-` / `exp-` / `ops-` / `perm-`. → [§10](docs/10-feature-flags.md)
**Observabilidade & resiliência**
- Logs com **Pino estruturado** + `redact` para `*.cpf`/`*.cardNumber`/`*.password`/`authorization`/`cookie`; sem `console.log`. → [§09](docs/09-observabilidade.md)
- OTel sampling head-based 10% (`OTEL_TRACES_SAMPLER=parentbased_traceidratio`, `ARG=0.1`). Sentry React com `maskAllText: true`, `blockAllMedia: true`, `tracePropagationTargets` (W3C). → [§09](docs/09-observabilidade.md)
- Métricas mínimas: p50/p95/p99, error rate, queue depth, DB pool, heap/RSS, cache hit ratio. Alertas derivados dos SLOs default por tier ([ADR 0001](docs/adr/0001-slo-defaults.md)): CRUD p99 >800ms/5min, agregação p99 >2s/5min, error >1%/5min, queue >1000/10min, DB pool >80%/5min, bounce rate do provedor de email >5% (Resend). → [§09](docs/09-observabilidade.md)
- Health: `/health/live` (só `memory.checkHeap(350MB)`) + `/health/ready` (master-login + **amostra LRU** dos K pools quentes do `WorkspacePrismaPool` + Valkey + BullMQ — nunca pingar os N workspaces); dreno 15s no shutdown, `stop_grace_period: 30s` no docker-compose, HAProxy drain de 30s antes do `compose up -d`. → [§20](docs/20-praticas-nest-universais.md), [§18](docs/18-cicd-containers.md)
**CI/CD & testes**
- Mesma image digest promovida dev→staging→prod; tag por commit SHA, **nunca `latest`**. → [§18](docs/18-cicd-containers.md)
- Actions GitHub pinadas por **commit SHA** (não tag); Trivy gate (`severity: CRITICAL,HIGH`, `exit-code: 1`); workflows com `fetch-depth: 0` + `nrwl/nx-set-shas@v4`. → [§18](docs/18-cicd-containers.md)
- Pirâmide ~70% unit / ~25% integração / ~5% E2E; integração com **Testcontainers** (Postgres real `postgres:18-alpine`, Valkey quando exercitado); HTTP via `createTestClient` (`libs/api/util-testing`), nunca import direto de supertest. → [§16](docs/16-testes.md)
- **Conventional Commits** (commitlint enforça): `feat, fix, chore, docs, refactor, test, perf, style, ci, build`. → [§19](docs/19-padroes-projeto.md)
- Sem `console.log` / `any` / `@ts-ignore` sem justificativa. → [§17](docs/17-qualidade-codigo.md)
## 2. Pegadinhas 🔥 críticas (quebram produção / vazam dados / violam LGPD)
Catálogo curto — apenas o que quebra prod, vaza dados ou viola LGPD. Pegadinhas de menor severidade (⚠️ inconsistência funcional, 💡 estilo) vivem nos capítulos canônicos em `docs/`.
> **Para adicionar/editar uma pegadinha:** leia primeiro [§19b — Guia de estilo editorial](docs/19b-guia-estilo-docs.md) (template canônico, IDs `PGD-<DOMÍNIO>-<NNN>`, severidades 🔥/⚠️/💡, fluxo de contribuição).
**Secrets, infra & imagem**
-`PGD-SEC-001` Long-lived credentials em GitHub Secrets (`*_ACCESS_KEY`, senhas, tokens sem expiração) — use OIDC GitHub → Vault JWT auth → SSH cert efêmero (TTL 5min) ou tokens de curtíssima duração emitidos por role com `bound_claims` confinado a repo+branch. → [§18.3](docs/18-cicd-containers.md#oidc-github--vault--ssh-cert), [§08.6](docs/08-secrets-config.md#cicd-oidc-github--vault--ssh-proxmox)
-`PGD-SEC-002` Secret runtime embutido em build/bundle Docker — runtime via **Vault Agent** no entrypoint do container; CI scaneia `(AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{30,}|re_[A-Za-z0-9]{20,}|hvs\.[A-Za-z0-9]+)` no `dist/`. → [§08.6](docs/08-secrets-config.md#cicd-oidc-github--vault--ssh-proxmox)
-`PGD-SEC-003` `VITE_*_SECRET` / server key / SMTP / token-de-escrita em `VITE_*` — bundle é público. Permitido só `VITE_API_URL`, public keys, Sentry DSN. → [§08.8](docs/08-secrets-config.md#frontend-vite)
-`PGD-CICD-001` Imagem Alpine sem `apk add --no-cache openssl` — Prisma e sharp crasham. → [§18.5](docs/18-cicd-containers.md#dockerfile--backend-nestjs-multi-stage)
**Backend / Prisma**
-`PGD-DB-001` `prisma migrate` via PgBouncer transaction pooling — Schema Engine quebra; use `MIGRATION_DATABASE_URL` direta (5432). → [§03.3](docs/03-backend.md#migrações)
-`PGD-DB-002` Modelo de multi-tenancy errado (row-level `tenantId`, `SET search_path`, schema-per-tenant) — o stack adota **BD-por-workspace** ([ADR 0006](docs/adr/0006-multi-tenancy-bd-por-workspace.md)): cada workspace tem cluster PG próprio, resolvido via `get_workspace_connection` no master-login; modelos de domínio **não** declaram `workspaceId`/`tenantId`. → [§03.4](docs/03-backend.md#multi-tenancy)
-`PGD-DB-003` `prisma` em `devDependencies` — quebra após `pnpm deploy --prod`; vai em `dependencies`. → [§03.5](docs/03-backend.md#riscos-críticos)
-`PGD-DB-004` `moduleFormat = "esm"` no generator Prisma — NestJS é CJS; use `"cjs"`. → [§03.2](docs/03-backend.md#prisma)
-`PGD-DB-005` `ADD COLUMN NOT NULL DEFAULT` em tabela grande — full table scan sob `AccessExclusiveLock`; use 3 deploys (nullable → backfill em job → `CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL`). → [§03.3](docs/03-backend.md#1-coluna-not-null-em-3-deploys)
-`PGD-DB-006` `DROP COLUMN` no mesmo PR que remove o uso — migrate roda antes do rolling swap das VMs; app v1 quebra com `P2022` durante a janela em que VM antiga ainda recebe tráfego. Faça em 2 deploys (remover uso primeiro, drop depois). → [§03.3](docs/03-backend.md#2-drop-de-coluna-em-2-deploys)
-`PGD-DB-007` `RENAME COLUMN`/`RENAME TABLE` direto — app v1 ainda em tráfego usa o nome antigo. Padrão add → dual-write + backfill → drop em 3 deploys. → [§03.3](docs/03-backend.md#3-rename-em-3-deploys-add--dual-write--remove)
-`PGD-DB-008` `CREATE INDEX` sem `CONCURRENTLY` em tabela quente — `ShareLock` segura todas as escritas até terminar; use `CREATE INDEX CONCURRENTLY` em migration fora de transação. → [§03.3](docs/03-backend.md#4-índice-em-tabela-quente-create-index-concurrently)
-`PGD-DB-009` `import { prisma }` singleton ou `new PrismaClient()` direto em service de domínio — sob BD-por-workspace ([ADR 0006](docs/adr/0006-multi-tenancy-bd-por-workspace.md)), `PrismaClient` é resolvido por request via `cls.get('prisma')`; singleton acerta o BD errado (master-login/default) e quebra o isolamento. Lint em `libs/api/util-eslint-authz` proíbe fora de `util-prisma-workspace` e dos nomes `auditPrisma`/`billingPrisma`. → [§03.4](docs/03-backend.md#multi-tenancy)
-`PGD-HTTP-001` Retornar entidades Prisma cruas — sempre mapear via Zod schema em `libs/shared/api-interface`. → [§03.3](docs/03-backend.md#type-safety-nunca-retorne-entidades-prisma-cruas)
**Auth**
-`PGD-AUTH-001` JWT em `localStorage`; refresh sem family revocation; `client_secret` OAuth no frontend; token de reset/refresh cru no DB. → [§07.3](docs/07-autenticacao.md#storage-no-frontend)
-`PGD-AUTH-002` Cookie de refresh sem `Path=/api/v1/auth/refresh` exato — `Path` largo (`/`, `/api`) vaza credencial de 30d em logs/proxies; `Path` estreito (sem o prefixo global `/api/v1`) faz o navegador não enviar o cookie e o refresh quebra em prod (401 silencioso). → [§07.3](docs/07-autenticacao.md#refresh-rotation)
-`PGD-AUTH-005` `JWT_SECRET` HS256 compartilhada entre apps cliente do master-login — uma app comprometida forja tokens válidos para qualquer outra do workspace (impersonação cross-app). Distribuir só via Vault Agent enquanto RS256 não fecha. → [§07](docs/07-autenticacao.md#guards-sem-passport)
-`PGD-AUTH-007` App cliente mantendo `users.passwordHash` próprio em vez de delegar ao master-login — credenciais divergem na 1ª troca de senha + vazamento da app expõe hashes que deveriam viver só no IdP. → [§07](docs/07-autenticacao.md#password-hashing)
**Autorização**
-`PGD-AUTHZ-001` `findUnique({ where: { id } })` em service de domínio usando `PrismaClient` singleton — query acerta o BD errado (master-login/default) e quebra isolamento cross-workspace; sempre injetar via `cls.get('prisma')`. Defense-in-depth intra-workspace: `assertOwnership(resource, user)`. → [§23.4](docs/23-autorizacao.md#234-isolamento-por-workspace)
-`PGD-AUTHZ-002` `workspace_id`/`ownerId` vindo do body/param/query do cliente — sempre do JWT (`req.user.workspace_id`, `req.user.sub`); administração cross-workspace é função do master-login, não das apps cliente. → [§23.3](docs/23-autorizacao.md#233-claims-no-jwt)
-`PGD-AUTHZ-003` Decisão de autorização só no frontend — backend re-decide em **todas** as ações; UI só esconde. → [§23.8](docs/23-autorizacao.md#238-frontend-esconde-não-decide)
-`PGD-AUTHZ-005` `$queryRaw`/`$executeRaw` em `PrismaClient` singleton ou em `auditPrisma`/`billingPrisma` dentro de service de domínio — atravessa workspaces sem aviso. Sempre rodar em `cls.get('prisma')`; cross-workspace deliberado usa nome explícito em `data-access-audit`/`data-access-billing`. → [§23.4](docs/23-autorizacao.md#234-isolamento-por-workspace)
-`PGD-AUTHZ-006` Job BullMQ sem `workspaceId` no payload — worker não tem `req`; sem repopular o CLS (`cls.runWith({ workspaceId, prisma: pool.getOrCreate(...) }, handler)`), o handler crasha ou (com singleton acidental) escreve no BD errado. Producer sempre inclui `workspaceId`; worker sempre recria o CLS. → [§23.4](docs/23-autorizacao.md#234-isolamento-por-workspace), [§11](docs/11-filas-jobs.md)
**Filas / jobs**
-`PGD-FILA-001` Valkey com `maxmemory-policy` diferente de `noeviction` (LRU/LFU/random) — descarta jobs em pressão de memória. Configure `noeviction` no `valkey.conf` + alerta de memória ≥80%. → [§11.2](docs/11-filas-jobs.md#pegadinhas-críticas-leia-antes)
-`PGD-FILA-002` `@nestjs/schedule` em multi-instância (>1 VM/container rodando o backend ou worker) — dispara em todas. Use BullMQ Job Schedulers (com `timeZone: 'America/Sao_Paulo'`). → [§11.2](docs/11-filas-jobs.md#pegadinhas-críticas-leia-antes)
-`PGD-FILA-003` Job sem `jobId` determinístico — sem idempotência em retries / webhooks at-least-once. → [§11.9](docs/11-filas-jobs.md#idempotência)
-`PGD-FILA-004` Email/webhook síncrono em handler HTTP — sempre via BullMQ. → [§11.4](docs/11-filas-jobs.md#producer), [§15.3](docs/15-email-notificacoes.md#resend--setup)
-`PGD-FILA-005` `queue.add` antes do commit Prisma — `queue.add` confirma no Valkey mesmo se a transação reverter, deixando o job órfão. → [§11.4](docs/11-filas-jobs.md#producer), [§20.8](docs/20-praticas-nest-universais.md#208-use-transações-prisma-para-operações-multi-step)
-`PGD-FILA-006` `@nestjs/event-emitter` para evento de domínio cross-instância — não atravessa containers/VMs, sem retry. Use BullMQ. → [§21.10](docs/21-filosofia-arquitetural.md#eventos-de-domínio-via-bullmq)
**Cache & CDN**
-`PGD-CACHE-001` TTL em **segundos** com cache-manager 6 — agora é **milissegundos** (breaking vs v5). → [§12.4](docs/12-cache.md#setup)
-`PGD-CACHE-002` `CacheInterceptor` genérico em rotas autenticadas — chave é a URL, vaza dados entre usuários/workspaces. Use chave manual `user:<id>:...`. → [§12.6](docs/12-cache.md#uso-manual-chave-por-usuárioworkspace)
-`PGD-CACHE-003` Cachear endpoints autenticados na borda (Cloudflare) / esquecer `Vary: Authorization` em cache compartilhado. Cloudflare deve bypassar `/api/*` por default. → [§12.9](docs/12-cache.md#vary--crítico)
**Real-time & uploads**
-`PGD-RT-001` Token em query string para auth WebSocket — vaza em access logs. Use ticket single-use 30-60s emitido por endpoint REST. → [§13.5](docs/13-realtime.md#auth-no-handshake--ticket-de-curta-duração)
-`PGD-RT-002` Handler `@SubscribeMessage` lendo BD sem reconstruir CLS — o upgrade WS roda fora do pipeline HTTP, então `WorkspaceContextMiddleware` nunca executou e `cls.get('prisma')` retorna `undefined` (crash) ou, pior, importar `PrismaClient` singleton acerta o BD do master-login (`PGD-DB-009`). Padrão: para **escrita**, enfileirar BullMQ com `workspaceId` no payload (`PGD-AUTHZ-006`); para **leitura síncrona** no handler, `cls.runWith({ workspaceId: client.data.workspaceId, prisma: pool.getOrCreate(dbConfig) }, handler)` usando o `WorkspacePrismaPool` da app. → [§13.4](docs/13-realtime.md#leitura-síncrona-no-handler--clsrunwith)
-`PGD-UPL-001` Presigned **PUT** sem POST policy — não trava `content-length`/`content-type`; use `createPresignedPost` com `Conditions`. → [§14.5](docs/14-uploads.md#nestjs--presigned-post-recomendado)
-`PGD-UPL-002` CORS do bucket MinIO esquecido — PUT/POST quebra silenciosamente; configure via `mc` na criação do bucket e teste em CI. → [§14.12](docs/14-uploads.md#cors-do-bucket-exemplo-minio)
**Observabilidade & LGPD**
-`PGD-OBS-001` `import './tracing'` depois do Nest — `tracing.ts` (NodeSDK) deve ser **primeiro import** de `main.ts`. → [§09.3](docs/09-observabilidade.md#setup-mínimo)
-`PGD-OBS-002` `redact` Pino sem `*.cpf`, `*.cardNumber`, `*.password`, `req.headers.authorization`, `req.headers.cookie` — LGPD exige redact agressivo. → [§09.3](docs/09-observabilidade.md#setup-mínimo)
-`PGD-OBS-003` `/health/ready` pingando todos os workspaces ativos (`for (const ws of workspaces) ping(ws)`) — escala O(N) e marca o container `unhealthy` quando **um** workspace dormente está lento, retirando o nó inteiro do HAProxy por culpa de um tenant que ninguém estava usando. Sob [BD-por-workspace (ADR 0006)](docs/adr/0006-multi-tenancy-bd-por-workspace.md), readiness pinga **master-login** (obrigatório) + **amostra LRU** dos K pools mais quentes do `WorkspacePrismaPool` (K=3 default); workspaces individuais com problema vivem em `db.connections{workspace}` (alerta), não em readiness. → [§20.12](docs/20-praticas-nest-universais.md#2012-health-checks-com-nestjsterminus)
**Email**
-`PGD-EMAIL-001` Sem webhook do provedor (Resend) + alarme Grafana (bounce >5% / complaint >0.1%) — provedor suspende o domínio. → [§15.5](docs/15-email-notificacoes.md#bounce--complaint-handling)
-`PGD-EMAIL-002` Domínio sem DKIM+SPF+DMARC publicado (Gmail/Yahoo bloqueiam desde 2024) — vale qualquer provedor. Resend gera os CNAMEs prontos. → [§15.6](docs/15-email-notificacoes.md#dkim--spf--dmarc)
**Feature flags**
-`PGD-FLAG-001` Decisão de negócio (preço, permissão, cobrança) avaliada só no client — sempre re-avaliar no backend; frontend só decide UI visual. → [§10.10](docs/10-feature-flags.md#backend-vs-frontend-evaluation)
-`PGD-FLAG-002` Email, CPF ou telefone como atributo de targeting — hashear `userId` (SHA-256 + salt). → [§10.8](docs/10-feature-flags.md#lgpd-em-targeting)
**CI/CD**
-`PGD-CICD-002` Actions GitHub por tag (use SHA); tag `latest` em prod; workflow sem `fetch-depth: 0` (`nx affected` quebra). → [§18.4](docs/18-cicd-containers.md#pin-de-actions-por-commit-sha)
-`PGD-CICD-003` Containerizar o frontend Vite — sirva estático via Nginx em VM + Cloudflare. → [§18.8](docs/18-cicd-containers.md#frontend-vite--não-containerizar)
> Ao atualizar uma 🔥 aqui, atualize a seção §NN.x correspondente no mesmo PR.
---
**Regra de ouro para o agente:** ao divergir das regras acima, **pergunte antes** e cite o capítulo de `docs/` que justifica a alternativa. Não introduza tecnologia fora de [`STACK.md`](STACK.md) sem aprovação explícita.