# 04 — Plano de aplicação

Ordem em três ondas. A regra é: **primeiro o que reduz risco e não muda comportamento**, depois o
que muda topologia, por último o que exige medição/decisão. Cada onda termina com um conjunto
observável — não com “o arquivo foi editado”.

## Onda 0 — risco atual (P0)

Objetivo: fechar a superfície óbvia, ligar a observabilidade que falta e impedir colisão entre
instâncias. Nenhuma mudança de arquitetura.

| Ordem | Item | Depende de | Por que agora |
|---|---|---|---|
| 1 | U-07 (nome do projeto obrigatório) | — | Um `down` no CMS pode derrubar o V7 hoje, com o fallback `concordia` nos dois. |
| 2 | U-01 (`.env.example` + preflight) | U-07 | Um clone novo falha por variável ausente sem mensagem clara. |
| 3 | S-09 (pinagem PECL + checksum BLAKE3) | — | Fecha a cadeia de suprimentos do único artefato de runtime. |
| 4 | P-02 + S-05 (pool e healthcheck por ping) | P-02 antes de S-05 | Hoje o FPM pode estar travado e o Compose dizer `healthy`. |
| 5 | S-01 (`no-new-privileges` + `cap_drop`) | — | Testar por serviço; documentar exceções do Postgres/Valkey. |
| 6 | R-02 (`stop_grace_period`) | — | Evita matar job no deploy; barato e sem dependência. |
| 7 | R-06 (healthcheck do Valkey) | S-04 se a senha mudar de canal | Probe atual vaza senha e quebra se `requirepass` for ao config. |
| 8 | P-05 (Postgres shm + GUCs) | — | Evita falha de paralelismo e torna contenção visível. |
| 9 | R-05 (poda do WAL) | backup base existente | `archive_timeout=300` grava ~4,6 GB/dia sem poda. |
| 10 | O-01 (access log do Caddy) | — | Sem isso não há SLO nem correlação com o log do Laravel. |
| 11 | O-03 (coletor de traces em produção) | O-01 não é pré-requisito | Hoje `TELEMETRY_ENABLED=true` e endpoint inexistente. |

**Aceite da onda 0**

- `docker compose config -q` e `docker compose up -d` verdes; `docker compose ps` sem
  `unhealthy`.
- `curl -fsS http://127.0.0.1:${WEB_PORT}/up` = 200.
- `docker inspect` mostra `no-new-privileges` e `CapDrop` esperados em cada serviço.
- `docker compose logs web --tail=1` é JSON com `status`, `duration` e `request_id`.
- `curl -s http://127.0.0.1:16686/api/services` contém o serviço de telemetria **ou** o endpoint
  externo configurado responde; nenhum erro de exportação no stderr.
- `du -sh $DATA_PATH/pgdata/pgdata/wal_archive` estabiliza depois da poda.
- `./up_prod.sh --dry-run` com `.env` incompleto falha **antes** do lock.

## Onda 1 — ganho relevante (P1)

Objetivo: uma imagem, limites, rede segmentada, rootfs imutável e coletor completo.

| Ordem | Item | Depende de | Observação |
|---|---|---|---|
| 1 | P-01 (imagem única) | — | Base para R-04 e S-08. |
| 2 | P-03 (contexto mínimo) | P-01 | Atualizar README de build no mesmo commit. |
| 3 | P-04 (cache + purga) | P-03 | Validar `php -m` após purgar o toolchain. |
| 4 | P-06 (Valkey) | — | `io-threads`/`activedefrag` só com medição. |
| 5 | P-07 (Caddy timeouts) | P-02 | `read_timeout` > `request_terminate_timeout`. |
| 6 | P-08 (limites) | P-05/P-06 | Dimensionar com `docker stats`; monitorar OOM. |
| 7 | S-02 (rootfs read-only) | S-01 | Testar upload de 110 MB e socket do PG. |
| 8 | S-03 (redes) | S-02 | Testar egress e resolução; `data` como `internal`. |
| 9 | S-06 (deny no Caddy) | O-01 | Testar `security.txt`. |
| 10 | S-10 (`X-Request-ID`) | O-01 | Mudança em `src/` → `make check` obrigatório. |
| 11 | R-01 (`init`) | R-02 | Validar sinais do FPM. |
| 12 | R-03 (healthchecks restantes) | P-01 | `procps` no lugar certo. |
| 13 | R-04 (`up_prod` preflight) | P-01, U-01, U-06 | Falhar antes do lock. |
| 14 | O-02 (Vector/Loki/Grafana) | O-01, O-03, P-08 | Profile `observability`; retenção e PII. |
| 15 | O-04 (retenção local) | O-02 | Definir política com o dono. |
| 16 | U-02 (portas) | U-01 | Tabela de portas no README. |
| 17 | U-03 (`make doctor`) | O-01/O-03 | Não é gate; diagnóstico. |
| 18 | U-06 (Compose de prod morto) | U-05 | Remover o `if`. |

**Aceite da onda 1**

- `docker compose config --images` lista **uma** imagem PHP; `docker compose build` constrói uma.
- `docker stats --no-stream` dentro dos limites; nenhum `OOMKilled` em 24 h de carga de teste.
- `docker compose exec postgres ...` não alcança a internet; `php-fpm` alcança `postgres`,
  `valkey`, `storage` e o OTLP.
- `docker compose exec php-fpm sh -c 'touch /x'` falha (`read_only`); upload real passa.
- `docker compose logs web` e o Loki mostram a mesma request pelo `X-Request-ID`.
- `make doctor` verde; com `valkey` parado, aponta o serviço e o sintoma.
- `make check` verde duas vezes (ADR-001); `make check E2E=1` verde porque headers/CSP do Caddy
  mudaram (spec 16/`tools/security-check.sh` leem o Caddyfile).

## Onda 2 — estrutural/condicionado (P2)

| Ordem | Item | Condição |
|---|---|---|
| 1 | S-04 (segredos de infra) | Só se `POSTGRES_PASSWORD_FILE`/ACL do Valkey forem validados na versão em uso. |
| 2 | S-07 (`USER` padrão) | Depois de P-03/P-04; testar uso fora do Compose. |
| 3 | P-09 (preload) | Só com ganho medido em p50/p95 e memória; rollback por diretiva. |
| 4 | U-04 (âncoras YAML) | Fazer junto com S-01/P-08 para não repetir a mudança. |

## Matriz de verificação por eixo

| Eixo | Comando/observação | Critério |
|---|---|---|
| Config válida | `docker compose --profile debug --profile telemetry config -q` | exit 0 |
| Saúde | `docker compose ps --format json` | todos `healthy`/`running` |
| Readiness | `curl -fsS /up` | 200 e JSON do listener |
| Segurança de container | `docker inspect -f '{{.HostConfig.SecurityOpt}} {{.HostConfig.CapDrop}} {{.HostConfig.ReadonlyRootfs}}'` | conforme S-01/S-02 |
| Rede | egress do `postgres`; acesso do `web` ao banco | ambos negados |
| Logs | `docker compose logs web --tail=1` (pipeline com `jq`) | JSON com status/duração/ID |
| Telemetria | `jaeger/api/services` ou endpoint externo | serviço presente |
| Dados | `psql -c 'show shared_buffers'`; `valkey-cli INFO memory` | valores esperados |
| WAL | `du -sh .../wal_archive` em 24 h | estável dentro da política |
| Código | `make check` (2×) e `make check E2E=1` quando borda/UI | verde |

## Rollback

- **Compose/config**: reverter o commit (humano) e `docker compose up -d`; a imagem anterior
  continua tagueada (P-01 facilita: uma tag).
- **Caddy**: `caddy validate` antes do restart (já no `up_prod.sh`); manter o Caddyfile anterior no
  commit.
- **WAL**: poda só remove segmentos anteriores ao backup base mais recente; nunca apagar
  `pg_wal`. Guardar o `pg_dump -Fc` do deploy.
- **Rootfs read-only/redes**: mudanças reversíveis por serviço; aplicar uma por vez e observar 24 h.
- **Telemetria**: o exportador é fail-open; desligar `TELEMETRY_ENABLED` não afeta request.
- **Preload**: remover a diretiva e `kill -USR2 1`.

## Critérios de aceite transversais (“logs ativados” em produção)

1. `APP_ENV=production` mantém `LOG_STACK=json,daily` (nunca `null`); `LOG_LEVEL=info`.
2. Caddy emite access log JSON em stdout; Docker rotaciona (`10m`×`3`) e o coletor retém.
3. Postgres e Valkey emitem log estruturado (GUCs e `logfmt`).
4. Traces têm destino real (coletor local no profile ou endpoint externo), sem circuito aberto
   permanente.
5. PII é mascarada **antes** de sair do host (O-02), e `Cookie`/`Authorization` não entram no
   access log.
6. RustFS emite log JSON em stdout (nível `info`) e o healthcheck usa `/health/ready` +
   `/minio/health/cluster`; o `tmpfs /logs` não é tratado como retenção.
7. `make check` não regride; `make check E2E=1` cobre borda/headers quando o Caddy mudar.


## Revisão das skills — encaixe nas ondas

| Onda | Itens | Observação |
|---|---|---|
| 0 | SK-01, SK-05 | Timeouts de upload/download e access log sem PII; não dependem de mudança de topologia. |
| 1 | SK-03, SK-04, SK-06, SK-07, SK-09, SK-10, SK-11 | Dependem de SK-01/P-02 (orçamento de tempo), P-07/P-08 (borda) e R-02 (queue). |
| 2 | SK-02, SK-08, SK-12, SK-13 | Exigem medição (cookies, I/O do autovacuum) ou decisão de backup. |

A ordem dentro da onda 1 é: SK-01 → SK-09 → SK-04 (orçamento de tempo); SK-03 → SK-04
(encerramento/retry); SK-06/SK-07 (php.ini/pool) por último, com `make check`.

## Storage (RustFS) — onda própria

A revisão da skill `rustfs` está em `08-rustfs-storage.md`. Ela não reusa a numeração P/S/O/R/U;
os itens são `RS-xx` para não colidir com o catálogo original. A ordem abaixo é independente das
ondas 0–2, mas compartilha dependências com P-08, S-03, S-04 e O-02.

| Etapa | Item | Depende de | Por que agora |
|---|---|---|---|
| S0.1 | RS-06 (backup/DR da mídia) | — | O bucket é single-drive `EC:0` e só o banco tem backup; perda do host perde a mídia. |
| S0.2 | RS-01 (1.0.2-glibc) | RS-06 (backup antes) | Correções de stdout, multipart e quórum; re-pinar o digest. |
| S0.3 | RS-02 (healthcheck readiness) | RS-01 | `/health` é liveness; `php-fpm`/`queue` sobem antes de o S3 aceitar escrita. |
| S0.4 | RS-03 (stdout/`info`) | RS-01 (fix de stdout) | Hoje o log do storage fica em tmpfs e só registra `error`. |
| S0.5 | RS-05 (`strict`/`fail`) | — | Documenta fsync e falha cedo em filesystem não suportado. |
| S0.6 | RS-04 (stop/segurança; `mem_limit` fica com P-08) | S-01 | Encerramento gracioso e sandbox; o limite de memória entra com P-08/RS-09. |
| S1.1 | RS-08 (bucket versioning/lifecycle/PAB) | RS-01 | Bootstrap só cria o bucket; falta o resto. |
| S1.2 | RS-07 (root × workload) | RS-08 | O `ensure-bucket` atual usa a chave do app; mover o bootstrap para root. |
| S1.3 | RS-10 (decisão single-drive) | RS-06 | Sem backup, `EC:0` não é aceitável por acidente. |
| S2.1 | RS-09 (tuning/limites medidos) | P-08, RS-10 | 16 workers/32 MiB de EC não devem ser copiados sem medição. |
| S2.2 | RS-11 (rede/TLS) | S-03 | Storage só em `data`; TLS interno depois, com CA no cliente. |
| S2.3 | RS-13 (OTLP/métricas) | O-02 | Jaeger não é coletor de métricas; não apontar `RUSTFS_OBS_ENDPOINT` para ele. |
| S2.4 | RS-14 (`_FILE`) | RS-01 | `RUSTFS_ACCESS_KEY_FILE`/`_SECRET_KEY_FILE` confirmados na skill. |
| S2.5 | RS-12 (SSE/KMS) | RS-11 + classificação | Só com custódia de chave; SSE-S3 local não tem rotação. |

**Aceite de storage**

- `docker compose exec -T storage curl -fsS http://127.0.0.1:9000/health/ready` e
  `/minio/health/cluster` = 200;
  `docker compose ps` mostra `storage`/`php-fpm`/`queue` saudáveis e o app não recebe 503 no
  primeiro upload após restart.
- `docker compose logs storage --tail=50` é JSON (linha a linha) e contém o evento de
  durabilidade; não depende de `/logs`.
- `docker inspect` mostra `no-new-privileges`, `CapDrop`, `PidsLimit` e `StopTimeout=45`.
- `docker compose exec storage rustfs info --json` mostra 1.0.2 e `durability=strict`.
- A mídia tem cópia offsite restaurável: `mc mirror` do bucket (exceto `staging/`) para o target, contagem
  de objetos/tamanho estável e um objeto restaurado com SHA-256 igual.
- `mc version info`, `mc ilm rule ls` e o PAB confirmam versioning/lifecycle/privacidade; o app
  continua servindo `GET /midia/...`.
- A chave do app não executa `mc admin` e não acessa outro bucket.
- `docker compose exec web getent hosts storage` falha após S-03; `php-fpm`/`queue` alcançam.
- `docker stats` durante upload multipart sem OOM; `make check` 2×; E2E se borda/headers mudarem.

**Rollback de storage**

- Imagem: voltar ao digest 1.0.1; o `FormatV3` não muda na 1.0.2, mas mantenha o backup da mídia
  e valide leitura antes de reverter.
- Config: reverter o commit do Compose; não “consertar” topologia apagando `format.json`/`pool.bin`.
- Bucket: versioning/PAB/lifecycle são reversíveis via API; não desabilitar versioning com
  noncurrent pendente sem entender o custo.
- Backup: nunca apontar o `mc mirror` de volta para o bucket de produção; restore vai para um
  bucket de scratch e é validado antes.
