# 09 — Veredito: o que do `update_docker/` entra no Public V7

> **Objeto:** os 63 itens propostos (`03-melhorias.md` = 36, `07-skills-oportunidades.md` = 13,
> `08-rustfs-storage.md` = 14) contra os ADRs vigentes e o repo real.
> **Método:** cada alegação do catálogo foi conferida no repositório e, quando tocava runtime,
> **medida** (containers, logs, `caddy validate`, `docker compose config`, endpoints reais).
> **Resultado:** 1 defeito de produção confirmado, 4 correções ao próprio catálogo e 1 P0 que o
> catálogo classificou como "backlog".
> **Nada foi implementado.** Este documento é o julgamento; a aplicação é uma decisão separada.

---

## 1. Resumo executivo

**Um achado domina os outros:** o limite de upload de 110 MiB que o projeto documenta e impõe em
três camadas **é inalcançável**. O `read_body 10s` do Caddy corta o corpo do cliente em 10,0 s —
medido, não suposto. Um PDF de 100 MiB a 2 MB/s (50 s) morre com **HTTP 504** antes de chegar ao
PHP. A cadeia de timeouts (Caddy → Apache → PHP → FPM) nunca foi verificada como um todo, e cada
camada a corta mais cedo que a anterior.

| # | Achado | Evidência | Veredito |
|---|---|---|---|
| 1 | **Upload >~10 s ⇒ 504.** `read_body 10s` (Caddy) mata o corpo antes do PHP | `curl --limit-rate 1M -F file=@20MiB` → `HTTP=504 t=10.00`; log do Caddy: `read tcp …: i/o timeout`, `err_trace: reverseproxy.statusError` | **Corrigir agora** (Onda A) |
| 1b | **Segundo corte, descoberto pelo controle-positivo: `write 30s` é um prazo *total*, não *idle*.** Ele consome o upload lento e mata a resposta — e por isso também corta download de PDF grande | Com `read_body 300s` e `write 30s`: 50 MiB a 2 MB/s (32,5 s) → `HTTP=100` e log `writing: write tcp …: i/o timeout`. Com `read_body 300s` **e** `write 300s`: o mesmo upload **chega ao app** (`HTTP=405` = o Laravel respondendo `/up`) | **Corrigir agora** (Onda A) |
| 2 | **`RUSTFS_OBS_USE_STDOUT=true` _silencia_ o stdout no 1.0.1** — o catálogo manda ligá-lo | A/B: `info` sem a var → **60–63** linhas JSON no stdout; `info` **com** a var → **0** linhas | **Corrigir o catálogo** |
| 3 | **A cadeia de timeout do catálogo se contradiz**: P-02 diz 35 s, P-07 diz 40 s, SK-09 diz 120 s — e os três cortam um upload de 110 MiB | ADR-026 admite 110 MiB; `LimitRequestBody 115343360`, `request_body max_size 110MiB`, `upload_max_filesize=110M` | **Substituir por uma cadeia derivada** (§3) |
| 4 | **`cap_drop: ALL` no Postgres quebra o container** (o entrypoint faz `chown`/`chmod` como root) | `chmod: … '/var/run/postgresql': Operation not permitted` / `chown: … Operation not permitted` | **Aplicar por serviço, não global** |
| 5 | **A mídia não tem backup nenhum** — single-drive, EC:0, sem versioning, sem cópia fora do host. O `08` classificou como "P0" e o ADR-030 §9 pôs no backlog | ADR-030 §9 ("Backup/DR do bucket entra no backlog"); RS-06 | **P0 de verdade** (Onda C1) |
| 6 | **Telemetria de produção aponta para `http://jaeger:4318`**, que só existe no profile `telemetry` (dev-only). Toda request tenta exportar para um nome inexistente | `config/observability.php:34`; `jaeger` tem `profiles: ["telemetry"]`; `up_prod.sh` não sobe profile nenhum | **Corrigir** (Onda A3) |

**Sobre Vector + Loki + Grafana (O-02/O-03) — a pergunta direta: que ganho eu ganho?**
**Nenhum ganho funcional, e a licença viola o ADR-002.** Grafana, Loki e Tempo são **AGPL-3.0**
(relicensing da Grafana Labs, 2021; `LICENSING.md` do Tempo/Loki: *"The default license for this
project is AGPL-3.0-only"*). O ADR-002 permite **apenas** MIT/ISC/Apache-2.0/BSD/OFL e bloqueia
copyleft forte "no código e nas imagens" — o Vector (MPL-2.0) também não está na allowlist. O
custo seria 4 containers, um pipeline de máscara de PII, uma superfície de autenticação e RAM de
uma box WHM — parabuiltinear o que o projeto **já tem**: log JSON no stderr (ADR-024), trilha de
auditoria imutável no Postgres (ADR-035), `docker compose logs` com rotação `json-file` 10 MB×3.
Detalhe em §6.

---

## 2. Como este veredito julga

Três filtros, nesta ordem:

1. **O ADR diz?** O Constitution e os ADRs 001/002/005/007/020/021/024/030/031/032 são a lei.
   Item que contradiz um ADR não entra sem decisão registrada.
2. **Está medido?** Alegação de runtime sem medição no ambiente é hipótese, não entrada. Onde o
   catálogo mediu, conferi; onde não mediu, medi.
3. **Custa menos que o que entrega?** (ADR-001: simplicidade acima de tudo.) Item que exige
   container novo para um problema que o runtime já resolve fica de fora.

Vereditos: **ADOTAR** (entra, com a correção indicada) · **ADOTAR PARCIAL** (entra só a parte
citada) · **ADIAR** (certo, mas depende de medição/decisão) · **REJEITAR** (não entra).

---

## 3. A cadeia de timeout (o achado n.º 1) — a recomendação central

Hoje, para um request de upload, os quatro tempos possíveis de corte são:

| Camada | Valor atual | Efeito |
|---|---|---|
| Caddy `read_body` | **10s** | medido: **504 em 10,00 s** — o corte que acontece |
| Caddy `write` | **30s** | medido: **prazo *total* do request**, não idle — o upload lento o consome e a resposta morre (`HTTP=100` + `writing: … i/o timeout`) |
| Apache `ProxyPass timeout` | 60 s | corta o request inteiro depois de 60 s |
| PHP `max_input_time` | 30 s | corta a leitura do corpo |
| FPM `request_terminate_timeout` | **ausente** | worker preso em I/O não é recycled |

> **Controle-positivo (medido, stack real, config restaurada byte a byte depois).** Subindo só
> `read_body` para 300 s, o upload de 50 MiB a 2 MB/s deixou de dar 504 — mas continuou falhando:
> `HTTP=100` e `writing: write tcp …: i/o timeout`, porque **`write 30s` é um prazo total do
> request** (o Caddy conta desde o início dele), não um tempo de inatividade. Subindo **os dois**
> para 300 s, o mesmo upload **chegou ao aplicativo** (`HTTP=405` — o Laravel respondendo que
> `/up` não aceita POST). Ou seja: **são dois cortes independentes, e só o segundo só aparece
> depois que o primeiro é resolvido.** É por isso que a recommendation precisa ser a cadeia
> inteira, e não um ajuste isolado.

O tamanho máximo que o projeto **declara** é 110 MiB. O Apache já impõe a taxa mínima:
`RequestReadTimeout body=20,MinRate=500` — mata quem envia menos de 500 B/s. Logo, o cliente mais
lento que o projeto aceita é 500 B/s ⇒ 110 MiB ÷ 500 B/s ≈ **231 s**. A janela de tempo precisa
ser pelo menos essa; 300 s dá a folga. É um número **derivado dos limites do próprio projeto**, não
uma escolha.

Cadeia proposta (a ordem é uma invariante, e vira gate):

```
apache ProxyPass timeout  ≥  caddy read_body  ≥  php max_input_time
caddy fastcgi read_timeout  ≥  fpm request_terminate_timeout  ≥  php max_input_time + php max_execution_time
```

| Parâmetro | Hoje | Proposto | Porquê |
|---|---|---|---|
| Caddy `read_body` | 10s | **300s** | 110 MiB ÷ 500 B/s ≈ 231 s + folga |
| Caddy `read_body_idle` | — | **30s** | **a defesa real contra slowloris**: corta quem *para*, não quem é lento |
| Caddy `write` / `write_idle` | 30s / — | **300s / 30s** | `write` é prazo **total** do request (medido): precisa cobrir upload **e** download de 100 MiB; `write_idle` é o teto de inatividade |
| Caddy `grace_period` | — | **10s** | deploy não derruba request em andamento (validado) |
| Caddy `php_fastcgi` `read_timeout`/`write_timeout` | — | **360s / 360s** | ≥ `request_terminate_timeout` |
| Apache `ProxyPass timeout` | 60 s | **300s** | ≥ `read_body` |
| Apache `RequestReadTimeout` | `body=20,MinRate=500` | **manter** | é a defesa por **taxa**, não por tempo absoluto |
| PHP `max_input_time` | 30 s | **300s** | precisa cobrir a transferência |
| PHP `max_execution_time` | 30 s | **manter** | orçamento de CPU, não de rede |
| FPM `request_terminate_timeout` | ausente | **360s** | > `max_input_time` + `max_execution_time` = 330 s |
| FPM `ping.path` / `ping.response` | ausente | `/ping` / `pong` | healthcheck real (S-05) |

**A segurança não regride**, porque o slowloris não éOGRADO pelo timeout absoluto, e sim por
**taxa mínima** (Apache `MinRate=500`) **e** por **idle** (Caddy `read_body_idle 30s`). Ambos
permanecem. O que muda é que um cliente **constante e lento** (que é o caso legítimo de quem
envia 100 MiB numa conexão de juxtaposto banda larga) deixa de ser cortado por estar "devagar".

**A invariante acima é testável** — e o projeto já tem o lugar para isso: `tools/security-check.sh`
faz `require_text` no Caddyfile e no `app.conf`. A cadeia deve ser uma asserção do gate, não uma
convenção. Isso é ADR-038/ADR-001 aplicados a runtime.

**Verificação (a ser executada na aplicação):**
`curl --limit-rate 2M -F file=@100MiB …` → 200/2xx e **não** 504; `docker compose logs web` sem
`i/o timeout`; `caddy validate` verde; `make check` 2×.

---

## 4. Veredito item a item

### 4.1 `03-melhorias.md` — 36 itens

| ID | Veredito | Motivo (com evidência) |
|---|---|---|
| **P-01** | **ADOTAR** | ADR-031 §32: *"`queue` reusa a mesma imagem do `php-fpm`"*. Realidade: `docker compose config --images` lista `concordia-php-fpm` **e** `concordia-queue`; há **6 imagens do projeto ≈ 5 GB** no host (57 no total). Fix: `image:` + `pull_policy: never` no `queue`, `build:` só no `php-fpm` — snippet testado (sem `pull_policy`, o Compose falha com *"image not found"*). |
| **P-02** | **ADOTAR — com 360 s, não 35 s** | `request_terminate_timeout` ausente é o defeito (worker preso em I/O não é recycled). Mas 35 s mata upload de 110 MiB (§3). Ver §3. `pm.status_path` não deve ser exposto no Caddy. |
| **P-03** | **REJEITAR** | O `.dockerignore` já exclui `vendor`, `node_modules`, `wiki`, `.git`, `s3-local`; o Dockerfile copia **só** o `entrypoint.sh`. O contexto já é mínimo. Ganho ≈ 0. |
| **P-04** | **ADOTAR PARCIAL** | Purgar `/tmp/pear` + cache do `pecl` **e** o `docker-php-source` **encaminha a imagem de 878 MB**; cache de build é ganho de CI. Versão do PECL é S-09 (exige build). |
| **P-05** | **ADOTAR PARCIAL** | `shm_size: 256mb` (hoje `/dev/shm` = **64 MB** no Postgres, contra `shared_buffers=256MB`) e `log_lock_waits`/`log_temp_files`/`log_autovacuum_min_duration` (observabilidade, ADR-024). `log_connections`/`log_disconnections` = ruído em produção. |
| **P-06** | **ADOTAR** | A premissa do catálogo está errada: `maxmemory 256mb` e `maxmemory-policy volatile-lru` **já existem** (`docker/valkey/valkey.conf:19-21`). Falta: `log-format logfmt`, `hide-user-data-from-log` (PII — ADR-019), `lazyfree-lazy-*` (DEL grande não bloqueia o event loop), `latency-monitor-threshold`. **Conjunto validado**: as diretivas carregam sem erro no Valkey 9.1.2 ("Configuration loaded", exit 0). |
| **P-07** | **ADOTAR — com 360 s, não 40 s** | `read_timeout 40s` cortaria upload de 110 MiB. `dial_timeout 3s` está certo. `grace_period 10s` **validado** com `caddy validate`. Atenção: o catálogo trata aqui só o timeout de **upstream**; o corte real que matava o upload era o `read_body`/`write` do **cliente** (§3) — P-07 sozinho **não** corrige o defeito. |
| **P-08** | **ADIAR (parcial)** | `pids_limit` + `ulimits.nofile` agora (barato, sem risco de OOM mal dimensionado). `mem_limit`/`cpus` **só após medir no host WHM** — os números do catálogo são chutes e um limite errado = `OOMKilled` em loop (ADR-001: medir antes de otimizar). |
| **P-09** | **REJEITAR** | ADR-008 exige medição; o projeto **já mediu** e desligou o JIT (2,4× pior em tracing). Preload sem medição é palpite, e preload obsoleto após deploy é risco de determinismo. |
| **S-01** | **ADOTAR PARCIAL** | `no-new-privileges: true` em todos os serviços. `cap_drop: ALL` **não** no Postgres: **medido** — `chown: changing ownership of '/var/lib/postgresql/18/docker': Operation not permitted` e `chmod: … '/var/run/postgresql': Operation not permitted` ⇒ o container não sobe. Aplicar `cap_drop` seletivo (web/valkey/storage) e documentar a exceção do Postgres. |
| **S-02** | **ADIAR** | `read_only` + Postgres (que precisa de `chown` como root) + `/tmp` + uploads de 110 MiB é um emaranhado de exceções, e **isola menos** que a segmentação de rede (S-03), que já entrega o isolamento sem risco de boot. |
| **S-03** | **ADOTAR** | Ganho real e **medido**: hoje `web → storage:9000` é **ALCANÇÁVEL** (e `web → postgres/valkey` também) — o container internet-facing alcança o banco e o bucket. Topologia de **2 redes** (não 4) validada: `edge` (web+app) e `data` (`internal: true`, app+db). Matriz medida: `web→db` **bloqueado**, `app→db` **ALCANÇA**, `db→internet` **bloqueado**, `app→internet` **ALCANÇA** (o `composer install` do deploy continua funcionando). |
| **S-04** | **REJEITAR** | `POSTGRES_PASSWORD_FILE` / `_FILE` em deployment single-host com `.env` 600 no host tem efeito equivalente; o ganho não paga a complexidade de segredos no Compose. |
| **S-05** | **ADOTAR (com build)** | `ping.path`/`ping.response` + `cgi-fcgi` no healthcheck. **Requer** `libfcgi-bin` na imagem ⇒ rebuild (que já ocorre por P-01/S-09) **e** entrada na allowlist de binários de `tools/assets-licenses-check.php` (licença permissiva). |
| **S-06** | **REJEITAR** | **Já coberto.** Testei 6 caminhos: `/.env`, `/.env.example`, `/foo/.env`, `/storage/logs/laravel.log`, `/composer.json`, `/app/.env` → **todos 404** (regra `@forbidden path *.ini *.log error_log .*` + root `public/`). |
| **S-07** | **ADIAR** | O Compose já força `user: "${UID}:${GID}"` (ADR-031). Falta verificar se o `entrypoint.sh` escreve em `/usr/local/etc/php` (se escreve, `USER` na imagem quebra). Ganho marginal. |
| **S-08** | **ADIAR (CI)** | Gate de CI, fora do `make check` (rede + minutos). `make sbom` já cobre as dependências PHP (ADR-020). Trivy pinado por digest; `.trivyignore` com prazo. |
| **S-09** | **ADOTAR (com build)** | `pecl install apcu redis imagick pcov` **sem versão** e `curl … php-blake3-ext.tar.gz` **sem checksum** violam ADR-001 (build reprodutível) e ADR-031 (supply chain). Exige build completo + `make check` — é o item que mais exige pipeline, e por isso não é "B". |
| **S-10** | **ADIAR** | `resolveRequestId` preserva qualquer string 8–128 do cliente ⇒ divergência real com o ADR-024 (X-Request-ID = UUIDv7). Impacto baixo (correlação de log, não autorização) e **quebra um teste existente** ⇒ mudança em `src/` ⇒ ciclo completo. Registrar a divergência. |
| **O-01** | **ADOTAR** | O `Caddyfile` **não tem** `log` ⇒ **nenhuma** linha de acesso é emitida. Config **validada** com `caddy validate` ("Valid configuration"), incluindo a redação: `format filter { wrap json fields { >request>headers>Cookie delete … } }`. É o substrato de qualquer coletor futuro — e o único item de observabilidade que vale. |
| **O-02** | **REJEITAR** | §6. AGPL + ganho ≈ 0 + 4 containers. |
| **O-03** | **ADOTAR (correção)** | "Nunca ligado com endpoint inexistente" é o critério certo — mas a opção (a) do catálogo ("subir o profile telemetry em produção") viola ADR-031 ("profiles dev-only"). O fix: **endpoint vazio em produção ⇒ `NullTelemetry`**, com `OTEL_EXPORTER_OTLP_ENDPOINT` documentado no `.env.example` para quem tiver coletor. `fail-open` preservado. |
| **O-04** | **ADIAR (P2)** | `LOG_DAILY_DAYS=730` de log **técnico** na cota do WHM é exagero — a trilha de auditoria tem retenção própria (ADR-035 + `audit:expunge`), o log técnico não precisa de 2 anos. Verificar `config/logging.php` antes de mexer. |
| **R-01** | **ADOTAR** | `init: true` em `queue` (o `queue:work` é PID 1 e jobs com `Symfony Process` deixam zumbis) e em `php-fpm`. Uma linha, sem custo. |
| **R-02** | **ADOTAR** | `stop_grace_period`: `queue` **150 s** (> `--timeout 120`), `php-fpm` **30 s**. Evita matar job no deploy. |
| **R-03** | **ADOTAR (só `queue`)** | `queue` é o único serviço de produção sem healthcheck. `mailpit`/`jaeger` são dev-only — não mexer. |
| **R-04** | **ADOTAR PARCIAL** | Build único (vem com P-01) + preflight de variáveis obrigatórias (vem com U-01). `--remove-orphans` é perigoso enquanto o nome do projeto colidir (U-07). |
| **R-05** | **ADOTAR** | `tools/wal-retention.sh` **existe e não é chamado por nada** (grep em `up_prod.sh`, `Makefile`, `tools/check.sh` → vazio). Estado real: **151 arquivos / 2,4 GB** no `wal_archive`; `pg_stat_archiver` = `archived_count=151, failed_count=0` ⇒ arquivar funciona, **podar não**. Crescimento ilimitado na cota do WHM. |
| **R-06** | **ADOTAR PARCIAL** | O healthcheck usa `valkey-cli -a "$REDIS_PASSWORD"` ⇒ senha em `argv` (visível em `docker top`). Usar `VALKEYCLI_AUTH`/`REDISCLI_AUTH` por env. O `--requirepass` em `command` fica para um item separado. |
| **U-01** | **ADOTAR** | **7 variáveis** usadas pelo Compose e ausentes do `.env.example`: `AWS_DEFAULT_REGION`, `COMPOSE_PROJECT_NAME`, `DATA_PATH`, `GID`, `STORAGE_PATH`, `UID`, `WEB_PORT`. `UID`/`GID`/`DATA_PATH`/`STORAGE_PATH` afetam **propriedade de arquivo** dos bind mounts. |
| **U-02** | **REJEITAR** | Só `WEB_PORT` é publicado (o resto é interno). Uma tabela de portas para um host é over-engineering. |
| **U-03** | **REJEITAR (reusar)** | Já existem `tools/whm-canary.sh`, `tools/go-live-check.sh` e `tools/security-check.sh`. Um quarto tool duplica. Se o comando "diagnosticar o host" for necessário, fazer um `make doctor` **fino** que encadeia os três. |
| **U-04** | **REJEITAR** | Cosmético; o Compose já usa âncora (`x-logging`). Unir `php-fpm`/`queue` esconde uma diferença real (o `command` do worker). |
| **U-05** | **ADOTAR (doc)** | Barato e resolve a confusão entre "artefato único" e "logs ligados em produção" (ADR-032 + ADR-024). |
| **U-06** | **ADOTAR** | `up_prod.sh:155` procura `docker-compose.prod.yml`, que **não existe** ⇒ código morto no caminho de deploy, e contradiz o axioma A0 (artefato único). |
| **U-07** | **ADIAR (decisão de produto)** | Colisão **real**: o `name:` do Compose cai em `concordia`, o resolver deriva de `APP_NAME=Concordia` ⇒ slug `concordia` — **igual ao do repositório `cms` irmão**. Um `down --remove-orphans` em um derruba o outro. Ver §5. |

### 4.2 `07-skills-oportunidades.md` — 13 itens

| ID | Veredito | Motivo |
|---|---|---|
| **SK-01** | **ADOTAR** | É o achado n.º 1 (Caddy × 110 MiB). Coberto por §3. |
| **SK-02** | **ADIAR** | Default real da Caddy 2.11 é **16 KiB**, não 1 MB (a skill está errada). Medir o tamanho real de `Cookie`+headers antes de fixar 64 KiB; por ora, documentar o default. |
| **SK-03** | **ADOTAR** | `grace_period 10s` — **validado** com `caddy validate`. |
| **SK-04** | **ADOTAR (360 s)** | Timeouts de upstream cobertos em §3. `retry` do upstream: **não** adotar retry automático em POST (não-idempotente ⇒ upload duplicado). **Atenção:** SK-04 e P-07 tratam só do **upstream**; o corte que matava o upload era o `read_body`/`write` do **cliente** — aplicar só estes dois deixa o defeito vivo. |
| **SK-05** | **ADOTAR** | = O-01. Redação validada (`>request>headers>Cookie delete`). |
| **SK-06** | **ADOTAR** | `allow_url_fopen` não está no `php.ini` (default `On`) e **não há** `file_get_contents` com URL `http` em `src/` ⇒ desligar é seguro e fecha um vetor clássico. |
| **SK-07** | **REJEITAR** | `disable_functions` é bruto; `proc_open` é obrigatório para o `queue`/Symfony Process. Ganho baixo em app containerizada. |
| **SK-08** | **REJEITAR** | `open_basedir` quebra `composer install` (bind mount + `vendor` + `/tmp`) e o build. Risco alto, ganho baixo. |
| **SK-09** | **ADOTAR — 300 s, não 120 s** | 120 s não cobre 110 MiB ÷ 500 B/s (≈231 s). Número derivado em §3. |
| **SK-10** | **ADOTAR** | `queue:work` está **sem `--timeout`** (default 60 s) e **sem `--backoff`**, com `--tries=3` ⇒ um job de 90 s é morto e **reexecutado 3×**. Fix: `--timeout=120 --backoff=3` + `stop_grace_period 150s` coerente + `queue:prune-failed --hours=48` agendado. |
| **SK-11** | **ADOTAR** | = ADR-007 §43: `wal_compression=on`, `max_wal_size`, `checkpoint_timeout` — **os três ausentes** hoje. É conformidade, não otimização. |
| **SK-12** | **ADIAR** | T-tuning de autovacuum depende de medição (`pg_stat_user_tables`) no host real. |
| **SK-13** | **ADOTAR** | O `--backup-only` **já existe** (`up_prod.sh`): um `cron` do host chamando `./up_prod.sh --backup-only` fecha o gap de RPO sem container novo. Também liga o `wal-retention.sh` (R-05). |

### 4.3 `08-rustfs-storage.md` — 14 itens

| ID | Veredito | Motivo |
|---|---|---|
| **RS-01** | **ADIAR — depois do RS-06** | `1.0.2-glibc` existe e o digest do catálogo confere (verificado: `sha256:1617cf459224b161bde874144f96969421ad07b4eb086a44c38d1fe31f36c595`). Mas subir o engine que guarda **toda a mídia** sem backup previo (RS-06) inverte a ordem do próprio `08` (§6: "RS-06 → RS-01 → RS-03"). O release note justifica (correções de scanner/heal — o storage de produção hoje loga `ERROR` "Scanner cycle completed without a durable data usage snapshot" a cada ~5 min). **Ordem: RS-06 → RS-01.** |
| **RS-02** | **ADOTAR AGORA (em 1.0.1)** | **Medido no 1.0.1**: `/health` → 200, `/health/ready` → 200 com JSON de readiness (`read_quorum`, `write_quorum`, `unavailableDrives`, `storage.ready`), `/cluster/health` → **403** (exige auth ⇒ não serve). Hoje o Compose probe `/health` (liveness) ⇒ o app sobe antes do S3 aceitar escrita. Usar `/health/ready` **não exige upgrade**. |
| **RS-03** | **ADOTAR — com correção** | **A recommendation do catálogo está invertida.** Medido: `RUSTFS_OBS_LOGGER_LEVEL=info` **sem** `RUSTFS_OBS_USE_STDOUT` ⇒ **60–63 linhas JSON no stdout**; **com** `RUSTFS_OBS_USE_STDOUT=true` ⇒ **0 linhas**. No 1.0.1 a variável **apaga** o stdout. O container de produção hoje (`level=error`, sem a var) emite JSON no stdout — coerente. Fix: **só** `RUSTFS_OBS_LOGGER_LEVEL=info`; **não** definir `USE_STDOUT` antes do 1.0.2 (lá o release note corrige o "honor" da var). |
| **RS-04** | **ADOTAR PARCIAL** | `stop_grace_period` + `no-new-privileges` (o fsync no shutdown do RustFS pede grace). `cap_drop` só onde tolera (o Postgres quebrou — S-01). |
| **RS-05** | **ADOTAR PARCIAL** | `RUSTFS_DURABILITY_MODE=strict` explícito (é o default, mas explicitar documenta a intenção). **NÃO** usar `RUSTFS_UNSUPPORTED_FS_POLICY=fail`: o filesystem do host WHM é desconhecido e `fail` impede o boot. Manter `warn` + monitorar. |
| **RS-06** | **ADOTAR — P0 de verdade** | Single-drive, EC:0, sem versioning, sem cópia fora do host ⇒ **a mídia some com o host**. O `08` já classifica como P0 e põe a sequência RS-06 antes de tudo; o ADR-030 §9 o remetiu ao backlog. **Implementação sem `mc`** (que é AGPL) e sem binário novo: **comando Artisan no app**, usando o cliente S3 já presente (Flysystem/AWS SDK), copiando objetos para o bucket offsite + manifesto, no mesmo espírito do `core:backup:push` do DB. Fecha ADR-030 §9, ADR-042/F6.I2 e ADR-020 (SBOM/DR). |
| **RS-07** | **ADIAR** | Separação root/workload do RustFS exige mudar o layout do volume no host. |
| **RS-08** | **ADOTAR — depois do RS-06** | Versioning + **PAB** (bucket privado) + lifecycle. O PAB é o que impede vazamento; sem backup, versioning é ILL — por isso depois do RS-06. |
| **RS-09** | **ADIAR** | Medir `docker stats` no host real primeiro (mesma razão que P-08). |
| **RS-10** | **nada** | Single-drive/EC:0 já é decisão registrada (ADR-030). |
| **RS-11** | **ADOTAR** | = S-03 (2 redes; `storage` só na `data`). |
| **RS-12** | **ADIAR** | SSE/KMS: já há `wiki/backlog/cripto-auditoria-avancada.md`. |
| **RS-13** | **REJEITAR (coletor) / ADIAR (métricas)** | Coletor = §6. Métricas do storage: sem coletor, o sinal é o log JSON + `docker stats`. |
| **RS-14** | **REJEITAR** | = S-04. |

---

## 5. U-07 (colisão de nome de projeto) — por que é adiado, não abandonado

O fato é verificável e é real: `docker-compose.yml` usa `name: ${COMPOSE_PROJECT_NAME:-concordia}`
e o resolver (`tools/project-name.sh`) deriva o slug de `APP_NAME`, cujo default no
`.env.example` é `Concordia` ⇒ **`concordia`**. O repositório `cms` irmão também é `Concordia`
⇒ **mesmo projeto Compose** ⇒ `docker compose down --remove-orphans` em um derruba o outro.
O AGENTS.md diz: *"One instance = one client = one Compose (`COMPOSE_PROJECT_NAME`)"*.

Por que não decidir aqui: o nome do projeto é **derivado do `APP_NAME` por decisão do spec 18
§4**, e o `APP_NAME` é também o **nome do tenant** exibido no portal/CMS e no cookie de sessão.
Mudar o default de `APP_NAME` é decisão de produto (o que o chamador do portal vai ver), não de
infraestrutura — e `config/app.php` (`env('APP_NAME','Concordia')`) e `config/cms.php`/title
herdam do mesmo valor.

**Recomendação para a decisão (a ser tomada por quem tem o produto):**
1. `.env.example` do Public V7 passa a declarar `COMPOSE_PROJECT_NAME` **explícito** (o resolver
   já o prefere, §4 do spec 18) — o default de `APP_NAME` continua "Concordia" para o nome exibido;
2. ou `APP_NAME` default distinto por repositório (ex.: `Public V7`), aceitando a troca do título
   exibido;
3. **em qualquer opção**: preflight no `up_prod.sh` que **recusa** subir se o nome do projeto já
   estiver em uso por **outro diretório de trabalho** (o label `com.docker.compose.project.working_dir`
   permite checar exatamente isso) — é a proteção que impede o dano mesmo com nomes iguais.

---

## 6. Vector + Loki + Grafana + Tempo (O-02/O-03) — a resposta direta

**Ganho funcional: nenhum, para este projeto.** O que o catálogo propõe comprar:

| Ganho alegado | Já existe no V7? |
|---|---|
| Log estruturado consultável | Log JSON no stderr (ADR-024) + `docker compose logs` + `json-file` 10 MB×3 |
| Correlação log↔trace | `X-Request-ID` ponta a ponta (ADR-024) — falta só o access log do edge (O-01) |
| Histórico que sobrevive ao restart | `LOG_STACK=json,daily` no host + trilha de auditoria **imutável no Postgres** (ADR-035) |
| Retenção controlada | `LOG_DAILY_DAYS` (O-04) |
| Dashboard de SLO | O V7 é single-tenant, 6 serviços, um operador. Não há SLO a medir. |

**Custo:** Grafana, Loki e Tempo são **AGPL-3.0** (relicensing Grafana Labs 2021; `LICENSING.md`
do Tempo: *"default license for this project is AGPL-3.0-only"*). O **ADR-002** admite apenas
MIT/ISC/Apache-2.0/BSD/OFL e bloqueia copyleft forte "no código e nas imagens"; o Vector (MPL-2.0)
também não está na allowlist. Ou seja: **adotar a stack exige uma emenda ao ADR-002** — decisão
que precisa ser registrada, e que o `05-nao-adotar.md` já rejeita por conta própria.

Além disso: 4 containers, um pipeline de máscara de PII (a parte mais difícil de acertar — a LGPD
manda errar para o lado do vazamento), uma superfície de autenticação e RAM de uma box WHM, para um
produto cujo objetivo declarado é **simplicidade, determinismo e funcionalidade**.

**A alternativa que dá 80% do ganho por ~2% do custo** (e é o que adoto): **O-01** — access log
JSON no Caddy, validado, sem container novo, sem pipeline de PII, sem licença nova, correlacionável
pelo `X-Request-ID` que o app já emite. Se um dia traces exigirem um backend, o caminho
**licença-clean** é o **OpenTelemetry Collector (Apache-2.0)** como receptor OTLP — não
Grafana/Loki/Tempo.

---

## 7. Sequência proposta

**Onda A — defeito de produção (nenhuma mudança de arquitetura)**
A1. Cadeia de timeout de upload (§3) + asserção no `tools/security-check.sh` + spec 16.
A2. RustFS: `RUSTFS_OBS_LOGGER_LEVEL=info` (**sem** `USE_STDOUT`) + healthcheck `/health/ready`.
A3. Telemetria: endpoint vazio em produção ⇒ `NullTelemetry`; `.env.example` documenta a variável.
A4. `wal-retention.sh` ligado no `up_prod.sh` + backup diário via `--backup-only` (cron do host).

**Onda B — conformidade de ADR e higiene**
B1. Uma imagem (P-01) + build único no `up_prod.sh`/CI + purga do toolchain PECL.
B2. PECL pinado + checksum do tarball BLAKE3 (S-09) — **exige build + `make check` completo**.
B3. `postgresql.conf`: `wal_compression`/`max_wal_size`/`checkpoint_timeout` + `shm_size` (ADR-007).
B4. `.env.example` completo (U-01) + `COMPOSE_PROJECT_NAME` (§5).
B5. Segmentação de rede em 2 redes (S-03/RS-11) + `no-new-privileges` + `cap_drop` seletivo (S-01).
B6. Valkey: logfmt + `hide-user-data-from-log` + lazyfree (P-06) + healthcheck sem senha em argv (R-06).
B7. Caddy: access log JSON (O-01) + `grace_period` (P-07) + `allow_url_fopen=Off` (SK-06) + `init`/`stop_grace_period` (R-01/R-02) + `pm` com `request_terminate_timeout` (P-02) + `ping.path` (S-05).
B8. Remover código morto (U-06) + documentar artefato único (U-05) + `queue --timeout/--backoff` (SK-10).

**Onda C — o que exige decisão ou medição**
C1. **Backup da mídia (RS-06)** — comando Artisan, sem `mc` (AGPL), sem binário novo. **P0 real.**
C2. Upgrade do RustFS para 1.0.2 (RS-01) **depois** de C1 + reavaliar `RUSTFS_OBS_USE_STDOUT`.
C3. `mem_limit`/`cpus` (P-08/RS-09) — só com `docker stats` do host WHM.
C4. Nome do projeto / `COMPOSE_PROJECT_NAME` (§5) — decisão de produto + preflight de uso.
C5. `X-Request-ID`: impor UUID (ADR-024) ou registrar a divergência (S-10).
C6. Retenção do log técnico (O-04) e tuning de autovacuum (SK-12) — com medição.

---

## 8. Riscos que este veredito carrega

1. **A cadeia de timeout amplia a janela de retenção de worker** (360 s × 10 filhos do FPM). A
   defesa é a **taxa mínima** do Apache + `read_body_idle` + rate limit da aplicação (ADR-021) —
   os três **precisam** estar no lugar **antes** de subir `read_body` de 10 s para 300 s. Aplicar
   §3 em um único commit, com a asserção de gate, evita a janela intermediária quebrada.
1b. **O `write` do Caddy é um prazo total do request.** Subir só `read_body` troca o 504 por um
   `HTTP=100` com `writing: … i/o timeout` — ainda quebrado, só com outro sintoma. A cadeia tem de
   subir **junta** (`read_body` **e** `write` **e** os timeouts do upstream), e o gate (§3) deve
   verificar a **ordem** entre eles, não a mera presença.
2. **`request_terminate_timeout` novo** muda o comportamento de jobs longos do `queue` — por isso
   `--timeout=120` do SK-10 e `stop_grace_period 150s` entram na mesma Onda B.
3. **S-09 e S-05 exigem rebuild da imagem** ⇒ build mais lento e digest novo ⇒ ADR-031 §"Matriz
   de imagens" precisa ser atualizada junto (a matriz é parte do ADR, não do `docker/`).
4. **Segmentação de rede** pode quebrar egress legítimo (composer, `IndexNow`, coleta de feed).
   A matriz foi medida, mas `composer install` **e** qualquer chamada HTTP de saída do app precisam
   de um teste de regressão no `make check`.
5. **O upgrade do RustFS (C2) toca o engine que guarda a mídia** — por isso C1 é pré-requisito,
   não vizinho.

---

## 9. O que este veredito **não** afirma

- Não mediu p50/p95 de produção real, nem `docker stats` do host WHM — por isso P-08/RS-09/SK-12
  ficam adiados, não aprovados.
- Não validou o build da imagem com PECL pinado (B2) nem com `libfcgi-bin` (S-05): ambos exigem
  um ciclo de build + `make check` completo.
- Não verificou a **mídia** existente (conteúdo, integridade, tamanho) — o RS-06 precisa inventariá-la
  antes de qualquer cópia.
- Não avalia o `cms` irmão além da colisão de nome de projeto (U-07).
- **Nenhuma alteração foi feita no repositório** — este documento é o julgamento; a aplicação é
  uma decisão separada, e todo item marcado **ADOTAR** toca a lista de arquivos indicada no §7.

---

## 10. Como usar este documento

Cada item **ADOTAR** do §4 aponta para arquivo e ação. Para implementar, o caminho é o de sempre:
`kd prime` → `kd task` (epic/issue) → implementação → `make check` 2× (determinismo, ADR-001) →
`kd write` das decisões → `kd sync`. Itens **ADIAR** precisam antes de medição ou de uma decisão
de produto (§5, C3, C5). Itens **REJEITAR** ficam registrados com o motivo para não voltar em
outra sessão.
