- 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>
136 lines
20 KiB
Markdown
136 lines
20 KiB
Markdown
# 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.
|