# PROTOCOLO DE REVISÃO — Concordia / Public V7

| Campo | Valor |
|---|---|
| **Versão** | 2.0 (rigoroso) |
| **Escopo** | Implementação de `wiki/implementation/03-layout-ux.md` → `06-deploy-migracao.md` (F3 → F6), conforme as fichas em `_to_revision/` |
| **Base técnica** | `.agents/skills/php-84-correct/SKILL.md` · `.agents/skills/laravel-12/SKILL.md` |
| **Base normativa** | `AGENTS.md` · `wiki/adr/` · `wiki/specs/` · `CONTRIBUTING.md` · `wiki/DESIGN.md` |
| **Autoridade** | `AGENTS.md` (constituição) > **ADR** > **spec** > plano > skill > código |
| **Frequência** | **Consulta obrigatória no início de cada sessão de revisão.** |
| **Saída** | Achados registrados no `kd`; fichas atualizadas; **nunca** commit (regra §0 do `AGENTS.md`) |

> Conflito entre este protocolo e uma **ADR** = **parar** e abrir ADR (emenda). Nunca desviar em silêncio.

---

## 0. Objetivo, escopo e não-escopo

**Objetivo.** Definir o procedimento **repetível, rastreável e auditável** para revisar a
implementação já feita (F3→F5) contra: (a) a base técnica **PHP 8.4 / Laravel 12**; (b) os
**invariantes** do projeto (simples, determinístico, nativo, poucas deps/permissivas/zero-CDN,
acessível, consistente); (c) a **base normativa** (ADRs/specs); e (d) a **evidência de teste**
(unit + feature + estrutural + **e2e/axe**).

**No escopo.** Código, configuração, migrações, views, CSS/JS, stubs, testes, gates e docs dos
componentes implementados em F3, F4, F5 e **F6** (deploy/operação: `up_prod.sh`, PITR/backup,
comandos de operação e scripts de ensaio).

**Fora do escopo.** F0–F2.5 (base anterior à UI); *operação* em conta real (cPanel/AutoSSL) — não há
código a revisar; trabalho **não** concluído — este protocolo só o cita quando vira ficha.

**Não é** o protocolo de **execução** (isso é `CONTRIBUTING.md` + `wiki/implementation/`), nem a
**revisão de segurança formal** (usa a skill `web-security-review` como apoio, mas a evidência
continua sendo os gates do projeto).

---

## 1. Definições e terminologia

| Termo | Significado |
|---|---|
| **Invariante** | Regra que nunca é exceto por ADR (§2). |
| **Achado** | Divergência entre o que existe e o esperado, com **evidência** (arquivo:linha + gate). |
| **Gate** | Verificação automatizada que **bloqueia** merge (tabela §10). |
| **Evidência** | Saída de gate + teste vermelho→verde + nota no `kd` com `--anchor`. |
| **Token canônico** | Identificador único da feature (ADR-004) que deriva todos os artefatos. |
| **Dívida técnica** | Marcada `// @debt [ID]` e registrada no `kd`; nunca esconde falha de segurança/a11y/dados. |
| **Ficha** | Arquivo `_to_revision/F*-T*.md` correspondente a uma task (`kd task`). |
| **Sessão** | Unidade de trabalho de revisão (uma ou poucas fichas), fechada com `kd rewind`. |
| **Disposição** | Ação tomada sobre um achado (§8). |

---

## 2. Os 6 invariantes (norte inegociável)

| # | Invariante | Operacionalização | Fonte |
|---|---|---|---|
| **1** | **SIMPLES** | Nativo primeiro; **sem `trait`/`abstract`/genéricos/`Result<T>`**; classes só onde o framework exige; interfaces só com porta/DI real; **duplicação > abstração especulativa**. | ADR-001, spec 08 |
| **2** | **DETERMINÍSTICO** | Clock/id-gen **injetados**; **ordem explícita**; dados **idempotentes**; **builds reprodutíveis**; **zero flaky**. | ADR-001, spec 08 §5 |
| **3** | **NATIVO PRIMEIRO** | Esgotar Laravel 12/PHP 8.4 antes de pacote/abstração. | ADR-001 §1 |
| **4** | **POUCAS DEPS · PERMISSIVAS · ZERO CDN** | Licenças MIT/ISC/Apache-2.0/BSD/OFL (uso comercial); tudo **vendorizado**; allowlist externa = **VLibras + GA opt-in**; copyleft forte **bloqueado** (código, imagem, `Process::run`). | ADR-002 |
| **5** | **ACESSÍVEL SEMPRE** | **WCAG 2.2 AA + eMAG** como gate; funciona **sem JS**. | ADR-025, spec 04 |
| **6** | **CONSISTENTE POR TOKEN** | Token canônico em **todas** as camadas; uma busca acha a árvore inteira. | ADR-004, spec 09 |

**Apresentação (transversal):** CSS **`oklch`/`clamp`/`@layer`/`@property`**, unidades `rem`/`fr`/`dvh`/`%`;
**Tailwind 4 + daisyUI 5** (onde cabe) + **Lucide** (único ícone, vendored) + **TipTap** (único editor);
HTML semântico; JS mínimo ES2025, progressivo.

---

## 3. Base normativa (hierarquia e índice de referências)

| Nível | Artefato | Onde |
|---|---|---|
| **Constituição** | Regras invioláveis (git read-only, kd, invariantes) | `AGENTS.md` |
| **ADR (decisão)** | 38 decisões | `wiki/adr/ADR-001…037` |
| **Spec (contrato)** | Contratos vivos por área | `wiki/specs/{04,06,08,09,10,11,12,13,14,16,17,18}-*.md` |
| **Plano (intenção)** | Planos 00–19 | `wiki/plan/` |
| **Design** | Fonte visual canônica | `wiki/DESIGN.md` |
| **DoD** | Definition of Done | `CONTRIBUTING.md` §5 |
| **Skills (técnica)** | PHP 8.4 · Laravel 12 · CSS/HTML/JS modernos · a11y · segurança · perf · daisyUI · Vite · PG18 · PWA · Playwright | `.agents/skills/*/SKILL.md` |

**Precedência de leitura numa sessão:** `AGENTS.md` → ADR do escopo → spec correspondente →
`wiki/DESIGN.md` (se UI/CSS) → skill técnica → arquivos da ficha.

---

## 4. Papéis e responsabilidades

| Papel | Responsabilidade |
|---|---|
| **Revisor (agente ou humano)** | Executar as fases §6; classificar achados; produzir evidência; registrar no `kd`; **não** commitar. |
| **Autor (quem implementou)** | Fornecer a ficha, o racional e responder a dúvidas; a dívida `@debt` deve estar registrada. |
| **Humano (mantenedor)** | Decidir sobre **emenda de ADR**; **commit**; aceitar riscos residuais. |
| **Gates automatizados** | Fonte de verdade executável (tabela §10); verde = passou. |

> O revisor **não reescreve por gosto**. Só corrige violação de invariante/ADR/spec/skill, sempre
> com **teste que falha sem a correção**.

---

## 5. Critérios de entrada (pré-requisitos da sessão)

- [ ] `kd prime` executado (1×/sessão); ambiente dev no ar (`make up`).
- [ ] Ficha selecionada; `kd ask "<tema>"` feito (procurar antes de julgar).
- [ ] ADR/spec/plano do cabeçalho lidos.
- [ ] Fonte de referência conferível: o código da ficha está aplicado (não é stub/pendente).
- [ ] Órbitas de apoio: `wiki/DESIGN.md` (se UI), skill técnica correspondente.

**Não iniciar** a revisão de uma ficha cuja implementação ainda não existe (pendência marcada no README).

---

## 6. Procedimento (fases P0–P6)

### P0 — Preparação (contexto)
1. Releia §2 (invariantes) e §11 (checklist de bolso).
2. `kd prime`; `kd ask "<tema da ficha>"`.
3. Abra a ficha; leia a ADR/spec/plano do cabeçalho.

### P1 — Inventário e leitura
4. Liste os arquivos exatos da ficha; **leia todos** (não amostre).
5. Localize, para cada arquivo, a **dimensão** aplicável (§7) e o **gate** (§10) que o cobre.

### P2 — Análise por dimensão
6. Percorra **D1→D14** (§7). Para cada uma: aplique o checklist, registre o **sinal de violação**
   (arquivo:linha) e rode o **comando de evidência**.
7. Use a **matriz de ADRs** (§7.0) para checar aderência à decisão do escopo e às transversais.

### P3 — Classificação e triagem
8. Classifique cada achado (§8: severidade) e defina a **disposição**.
9. Achado que contradiz uma ADR mas sugere que a ADR está errada → **propor emenda de ADR**
   (é uma disposição válida, não um "desvio").

### P4 — Correção (test-first)
10. Para cada achado **bloqueador/alto**: escreva o **teste que falha** (vermelho), corrija, volte a verde.
11. Bug → teste de regressão obrigatório. Refactor → mudança sem alterar comportamento, com testes verdes antes/depois.

### P5 — Verificação
12. Rode a **matriz de evidências** (§10): sempre `make check` **2×**; se UI/HTML/CSS/rota: `make check E2E=1`
    (e2e + axe + perf); se literal: o gate específico (`tools/arch-check`, `tools/name-check.sh --grep`…).
    O **tempo de cada check** sai no relatório — passou do alvo do §10.1, investigue antes de fechar.
13. Confirmar os **critérios de saída** (§9).

### P6 — Registro e entrega
14. `kd write --anchor <arquivo>` para cada achado/decisão/erro (uma asserção por nota).
15. Atualize a ficha (`## Notas da revisão`) e a tabela do `README.md`; preencha a linha em §15.
16. `kd rewind` (handoff). Entregue ao humano — **sem commit**.

---

## 7. Dimensões de revisão (D1–D14)

### 7.0 Matriz de norteadores (ADR → o que procurar)

| ADR | Regra essencial | Sinal de violação |
|---|---|---|
| **001** | Simples/nativo/DI/determinístico | `trait `, `abstract class`, `@template`, `Result<`, `now()`/`rand()` no domínio, ordem implícita, seed não-idempotente |
| **002** | Zero CDN, deps permissivas, vendorizado | URL externa em asset (fora da allowlist), pacote GPL/AGPL, `Process::run(['gs'…])`, lockfile sem licença |
| **003** | Raiz Laravel; `/src` = domínio | artefato de domínio fora de `/src`; `/src` com pasta ≠ `core/services/layout/modules/tests-e2e` |
| **004** | Token canônico | arquivo/rota/tabela sem o token; exceção sem `@name-check-exception` |
| **005** | Apache/WHM proxy burro; ignora `.user.ini`/`.htaccess` | dependência do cPanel na app; PHP esperando `.htaccess` |
| **006** | Bind-mount; storage configurável | caminho de storage hardcoded; dado volátil fora de volume |
| **007** | PG 18: `text`/`numeric`/`timestamptz`/`jsonb`/`uuidv7()` | `varchar(n)`, `float` p/ dinheiro, `timestamp` sem tz, ramo por driver |
| **008** | Otimização nativa obrigatória | `optimize` vermelho; `env()` fora de `config/`; closure em rota; sem `strict_types` |
| **009** | Blade nativo, sem Livewire/Filament/jQuery | `wire:`, `v-`, `x-data`, `$(`/jQuery, CDN |
| **010** | Web moderna, JS mínimo, sem CDN | `<script>` inline de terceiro; conteúdo público que exige JS |
| **011** | Atkinson `--font-content` | fonte não-vendored; sem fallback métrico; CLS alto |
| **012** | i18n 4 locales; URL é autoridade; jsonb+`LocaleCast` | string fixa; `<html lang>` errado; `url('/x')`; locale por cookie |
| **013** | DDD-lite, fronteira física | `core`→`services/modules`; `services`→`modules`; `modules/*`→`modules/*` |
| **014** | Autodiscovery, 1 `ModuleProvider`, manifest | 2º arquivo-raiz; grupo manual; DB no boot |
| **015** | Services headless/agnósticos | service importa `modules`/`layout`; contrato fora de `services/{N}/Contracts` |
| **016** | Tema/layout = dado; slots ordenados; a11y por último | HTML/CSS no jsonb; `@layer` fora de ordem; grid em PHP |
| **017** | Dados ≠ UI; `down()` não-destrutivo; seeds demo | HTML/CSS no banco; `DROP` no `down()`; seed como dado de referência |
| **018** | Dados abertos first-class | dataset sem DCAT/INDA; export sem checksum/`ETag` |
| **019** | LGPD: classificação, DSAR, retenção, legal hold | PII no log; exclusão que apaga histórico; sem classificação |
| **020** | Segurança em profundidade | CSP com `unsafe-inline`; TLS <1.3; senha fraca |
| **021** | Rate limit day-zero (Valkey) | rota de escrita sem `throttle:`; erro sem RFC 7807/`Retry-After` |
| **022** | Cache de resposta + `ETag`/304; CSRF fora do cache | `ETag` instável; form cacheado `public`; HTML minificado |
| **023** | PHPStan/Larastan **max** + ratchet | baseline crescendo; `phpstan-ignore` sem motivo |
| **024** | JSON logs + RFC 7807 + `X-Request-ID`; telemetria pervasiva | log sem `{module}.{action}`; span com PII; instanciar `Telemetry` |
| **025** | a11y/UX = gate de CI | axe com violação A/AA; `fail` no sniff; alvo < 24px |
| **026** | Mídia/Galeria fluxo único `<picture>` | `<img>` cru novo; sem `width/height`; reescrita de HTML |
| **027** | SEO headless + `@graph` + raiz dinâmica | `<head>` montado no shell; `@graph` sem ordem; GA por padrão |
| **028** | Editor = TipTap; AST jsonb → HTML semântico | HTML no banco; nó HTML livre; `<img>` cru |
| **029** | **BLAKE3** (audit/blind index); SHA-256 só não-cripto | `md5`/`sha1` em hash de auditoria |
| **030** | S3-like desde o dia zero (RustFS) | arquivo de domínio em disco local |
| **031** | Imagens base pinadas `tag@sha256`; não-root | `latest`; imagem sem digest |
| **032** | `APP_ENV` é o único switch | comportamento por flag espalhada; `.env` gordo |
| **033** | Permissões `{module}.{ver|editar|aprovar|arquivar}`, deny-by-default; role única jsonb; `Suporte` reservado | ação sem declaração; herança de role; `Suporte` visível |
| **034** | Ativação = **dado** (Valkey+DB fail-open); guard `module.activation` → 404 | "flags engine"; rotas removidas; manifest sem filtrar inativo |
| **035** | Auditoria = evidência (imutável + BLAKE3); CMS full; portal none | mutação sem trilha; PII na trilha; evento técnico na trilha |
| **036** | Shell CMS = dado; posição CSS | estrutura por JS; `orbital` único caminho; CLS > 0 |
| **037** | Auth CMS nativa; Argon2id; falha genérica; throttle e-mail+IP | JWT/Fortify/Breeze; mensagem que revela existência; `GET` logout |

**Regra de ouro.** Achado que contradiz uma ADR mas indica que a ADR está errada → **emenda de ADR**
(como a emenda de licenças da ADR-002 §"Emenda"). Nunca corrigir por baixo do pano.

---

### D1 — Simplicidade & PHP 8.4
**Objetivo:** código funcional, concreto, moderno e sem abstração especulativa.
**Fonte:** ADR-001; spec 08; `.agents/skills/php-84-correct/SKILL.md`.

- [ ] `declare(strict_types=1)`; tipos em **params e retorno**; `mixed` só quando real.
- [ ] **DI** por construtor/assinatura; corpo **sem** `new`/`resolve`/`app()` escondidos.
- [ ] Sem `trait`, `abstract class`, generics, `@template`, `Result<T>`, *magic methods* desnecessários.
- [ ] Superfície 8.4 usada: `match`, `?->`, enums, `readonly`, **property hooks**, **asymmetric visibility**,
      `array_all/any/find/find_key`, first-class callable, `Random\Randomizer`.
- [ ] Sem removidos: `create_function`, `each(`, `mcrypt_*`, `(real)`/`(unset)`, `define(...,true)`,
      `FILTER_SANITIZE_STRING`, `lcg_value()`, `trigger_error(...,E_USER_ERROR)`, `E_STRICT`.
- [ ] Uma Action por arquivo, foco único, cabeçalho `@intent`/`@see`/`@debt`.
- [ ] Erros por **exceção específica** (nunca silenciar); sem exceção p/ fluxo normal.
- [ ] Segurança PHP: `PASSWORD_ARGON2ID`+rehash, `random_bytes` p/ token, `htmlspecialchars`, SQL preparado, `hash_equals`.
**Sinais de violação (grep):** `trait `, `abstract class`, `@template`, `Result<`, `new [A-Z]\w*(`, ` resolve(`, `each(`, `md5(`, `sha1(`.
**Evidência:** `make check` (`static`); `make check`; leitura.

---

### D2 — Laravel 12
**Objetivo:** usar o framework corretamente (fronteiras de versão + idiomático).
**Fonte:** `.agents/skills/laravel-12/SKILL.md`; ADR-008.

- [ ] Config só em `bootstrap/app.php`; **sem** `Http/Kernel.php`, `Console/Kernel.php`, `Exceptions/Handler.php`.
- [ ] Providers em `bootstrap/providers.php`.
- [ ] **`env()` só em `config/`**; código lê `config()`.
- [ ] Controllers **finos**; validação em **FormRequest**; `$request->validated()` (nunca `->all()` em create/update).
- [ ] **Sem closure em rota** (rota nomeada + controller); `#[Scope]` preferido a `scopeX`.
- [ ] Eloquent: `with()`/`withCount()` contra N+1; casts nativos; `latest`/`oldest` em vez de `orderBy` cru quando cabe.
- [ ] Blade: `{{ }}` (nunca `{!! !!}` com dado não confiável); lógica fora da view.
- [ ] Migração idempotente (`Schema::hasTable`, `IF NOT EXISTS`, `ON CONFLICT`); `down()` não-destrutivo.
**Sinais de violação (grep):** `env(` fora de `config/`, `Kernel.php`, `Handler.php`, `$this->middleware(`, `->all()`, `Route::(get|post|any)\([^)]*fn`, `scope[A-Z]`.
**Evidência:** `make check` (`optimize-check`); `tools/migration-check`; `make check`.

---

### D3 — Determinismo
**Objetivo:** mesma entrada ⇒ mesma saída; zero flaky.
**Fonte:** ADR-001 §0; spec 08 §5.

- [ ] Portfolio: **`Clock`/`IdGen` injetados**; sem `now()`/`rand()`/`mt_rand()`/`random_int()` no domínio.
- [ ] **Ordem explícita** em discovery/manifesto/slots/placements (`sort` determinístico, empate por token).
- [ ] Migrações/seeds **idempotentes**; `migrate:fresh` sem seeder utilizável; `ON CONFLICT`/`insertOrIgnore`.
- [ ] Corpo público **estável** (permite `ETag`/304); assets/precompress **reprodutíveis**.
- [ ] Testes sem dependência de tempo/rede/ordem; `make check` **2×** verde.
**Sinais de violação:** `now()`, `rand(`, `mt_rand(`, `glob(` sem `sort`, factory com valor aleatório não seedado.
**Evidência:** `make check` **2×**; `tools/migration-check`; `make check E2E=1` (`perf`) (ETag/immutable).

---

### D4 — Fronteiras e arquitetura (DDD-lite)
**Objetivo:** fronteira física inviolável (ADR-013/014/015).
**Fonte:** ADR-003/013/014/015; `.agents/skills/...`.

- [ ] `core` **não** importa `services`/`modules`; `services` **não** importa `modules`; `layout` consome `core`/`services`.
- [ ] `modules/*` **nunca** importa `modules/*` — comunicação por **Event/Listener** e **portas do Core**.
- [ ] Um **`ModuleProvider.php`** por módulo (`register()` + `config()`); grupo vem da pasta.
- [ ] Service: `src/services/{Nome}/ServiceProvider.php` implementa `Core\Contracts\Service` (`contracts()`+`config()`);
      módulo depende de `Services\{Nome}\{Contracts,DTOs,Enums}\*`.
- [ ] `/src` só tem `core/services/layout/modules/tests-e2e` (ADR-003).
- [ ] Widget/banner: registro no Core (`WidgetRegistry`); destino referencia o **`id`** (embed ok, import proibido).
**Sinais de violação:** `use Modules\`, `use Services\` em camada proibida; `use` de service dentro de outro service.
**Evidência:** `tools/arch-check`; `tools/arch-check`.

---

### D5 — Dependências, licenças e vendorização
**Objetivo:** supply-chain controlada, comercialmente segura, zero CDN.
**Fonte:** ADR-002 (+ emenda); `tools/vendor-check.sh`; `tools/{licenses-check.php,assets-licenses-check.php}`.

- [ ] Nenhum asset por CDN; allowlist fechada: `vlibras.gov.br`, `googletagmanager.com`, `google-analytics.com`.
- [ ] Licenças na allowlist permissiva (composer + npm + assets + **binários de runtime**).
- [ ] Nada de copyleft forte (Ghostscript/poppler) — nem por pacote, nem por `Process::run`.
- [ ] Toda dependência **justificada** pelo que o nativo não resolve; LGPL só como CLI separado.
- [ ] `THIRD_PARTY_LICENSES` e `THIRD_PARTY_LICENSES_ASSETS` atualizados.
**Sinais de violação:** `https://` em `<script/link/img/iframe>`, `cdnjs|unpkg|jsdelivr|fonts.googleapis|ajax.googleapis`, `Process::run(['gs'`.
**Evidência:** `tools/vendor-check.sh` + `tools/assets-licenses-check.php`; `tools/gates-selftest.sh`; `make check` (`audit`); `make sbom`.

---

### D6 — CSS moderno · Tailwind 4 · daisyUI · Lucide
**Objetivo:** apresentação moderna, determinística e tematizada por tokens.
**Fonte:** ADR-009/010/011/016; `wiki/DESIGN.md`; skills `css-moderno`/`daisyui`; `DesignTokensTest`.

- [ ] **`oklch()` é o padrão**; `hex`/`rgb` só justificado (ex.: manifest PWA).
- [ ] `clamp()` (min/máx em `rem`), `rem`/`fr`/`dvh`/`%`; **`px` = último recurso** (físicas só em print).
- [ ] **Camadas** na ordem `theme, base, components, utilities, overrides, a11y` — **a11y por último**;
      tokens em `@layer theme` (fora de camada vence — proibido).
- [ ] **`@property`** para custom properties animáveis; **subgrid** com fallback `@supports`; container queries.
- [ ] **Sem `!important`**, **sem `style="…"` inline**; propriedades lógicas (`margin-inline`/`padding-block`).
- [ ] Tailwind 4 + **daisyUI 5** como base; daisyUI **tematizado pelos tokens** (`data-theme="concordia"`, `--border: 0.0625rem`).
- [ ] **Lucide** é o único ícone (`<x-ui::icon>`); nome desconhecido **não** gera request; marcas via Simple Icons.
**Sinais de violação:** `!important`, `style="`, `px`/`#hex`/`rgb(` no tema, `@layer` sem `a11y` por último, `CDN`.
**Evidência:** `make check` (`DesignTokensTest`); `tools/gates-selftest.sh`.

---

### D7 — HTML semântico & acessibilidade (eMAG + WCAG 2.2 AA)
**Objetivo:** a11y como **critério de aceite**, não relatório.
**Fonte:** ADR-025; spec 04; `wiki/DESIGN.md` §9; skill `web-accessibility`.

- [ ] **3 camadas de prova**: contrato estrutural (PHPUnit) · jornada (Playwright+axe) · estilo computado (sniffCSS).
- [ ] Semântica: `<header>/<nav>/<main id="main-content" tabindex="-1">/<aside>/<footer>`; **um `<h1>`**; sem salto de nível.
- [ ] **Skip links**; primeiro `Tab` alcança o conteúdo; foco **visível** (`:focus-visible`, ≥0,1875rem), lógico, sem armadilha.
- [ ] **Alvos**: ≥ **24×24 px** sempre (SC 2.5.8); ≥ **44×44 px** em `pointer: coarse`.
- [ ] **Contraste**: 4,5:1 texto / 3:1 grande-UI (árbitro = **axe**, que compõe ancestrais).
- [ ] Imagens com `alt` (obrigatório via Media); formulários com `<label for>` + erro `aria-describedby`/`aria-invalid`/`aria-live`.
- [ ] Nativo primeiro (`<dialog>`, `<details>`, popover); ARIA só quando necessário; `inert` em off-screen de modal.
- [ ] Movimento respeita `prefers-reduced-motion`; sem pisca > 3×/s; nada `transition: all`.
- [ ] Preferências a11y persistem (cookie → SSR, sem FOUC) e **não alteram a marcação**; conteúdo público **sem JS**.
- [ ] **Zero CDN**; VLibras **sob demanda**. Exceções do sniff só na ledger (spec 04 §6), nunca em massa.
**Sinais de violação:** `<div onclick>`, `<img` sem `alt`, campo sem `<label>`, falta de `:focus-visible`, alvo < 24px.
**Evidência:** `make check E2E=1` (axe 0 violações, 3 projetos inclui no-JS); `make check` (`a11y`); `make check` (`coverage`) (`LayoutContractTest`, `ProblemDetailsHtmlTest`).

---

### D8 — i18n (ADR-012 / spec 13)
**Objetivo:** 4 locales, URL como autoridade, sem string fixa.
**Fonte:** ADR-012; spec 13; `ModuleI18nTest`/`I18nTest`/`LocaleUrlInvariantTest`.

- [ ] Locales `pt_BR` (default) + `en/es/fr`; default no root, prefixo nos demais.
- [ ] **Nenhuma string visível fixa**; tradução mora no **dono do fragmento** (módulo/service/layout).
- [ ] Namespace = token canônico; arquivo = seção (`Lang/{locale}/index.php` → `{token}::index.*`).
- [ ] Navegação via **`route()`** (carrega locale) ou **`localized_url()`**; **nunca** `url('/x')`/`href="/x"`.
- [ ] `<html lang>` + `hreflang`/`x-default`; datas via `intl` (`LocalizedDate`).
- [ ] Conteúdo traduzível em **jsonb + `LocaleCast`** (`get` fallback, `set` grava objeto completo); CMS com aba por locale.
- [ ] Manifesto guarda a **chave** (`token::menu.title`), nunca texto.
**Sinais de violação:** texto literal em nó de view, `url('/`, `href="/`, locale por cookie/sessão no portal.
**Evidência:** `make check` (`ModuleI18nTest`, `I18nTest`); e2e com shell localizado.

---

### D9 — Grep-friendly (ADR-004 / spec 09)
**Objetivo:** uma busca acha a feature em todas as camadas.
**Fonte:** ADR-004; spec 09; `Adr004GrepFriendlyTest`; `tools/name-check.sh`.

- [ ] Token único (kebab, sem acento, singular) derivando **todos** os artefatos (tabela da spec 09).
- [ ] Sufixo **`Portal`** para artefatos públicos; `{token}` em classes/rotas/views/tabelas/testes/slots/logs.
- [ ] Exceções documentadas `@name-check-exception: <motivo>` (no provider = feature; no arquivo = arquivo).
- [ ] `bash tools/name-check.sh --grep <token>` retorna ≥1 arquivo por camada.
**Sinais de violação:** arquivo de módulo sem o token; nomes "criativos" por artefato.
**Evidência:** `bash tools/name-check.sh --grep <token>`; `make check` (`Adr004GrepFriendlyTest`).

---

### D10 — Dados / banco (ADR-007/017 / spec 10/12)
**Objetivo:** schema seguro, idempotente, dado desacoplado da UI.
**Fonte:** ADR-007/017; specs 10 e 12; `tools/migration-check`.

- [ ] **PG-only** (sem ramo por driver); `uuidv7()`; `timestamptz`; `numeric` p/ dinheiro; `text`; `NOT NULL`; `jsonb`.
- [ ] Migração: transação + `SET lock_timeout` + log; guardas de idempotência; refs `ON CONFLICT`.
- [ ] **`down()` → `backup_<tabela>_<ts>`**; nunca `DROP`; alteração de coluna em **expand & contract**.
- [ ] Sem HTML/CSS/flag de UI no banco; conteúdo rico em `jsonb`.
- [ ] PII com cast **`encrypted`**; blind index em **BLAKE3** só quando há busca sobre cifrado.
- [ ] Seeds **só demo**; dado de referência na migration; `make db-reset` 2× não duplica.
**Sinais de violação:** `dropColumn`/`DROP` no `down()`, coluna `*_html`/`style`, `varchar(`, `float` p/ preço.
**Evidência:** `tools/migration-check`; `tools/gates-selftest.sh`; `make check` (`coverage`) (`MigrationIdempotencyTest`).

---

### D11 — Segurança & privacidade / LGPD (ADR-019/020/021/022/037)
**Objetivo:** defesa em profundidade + privacidade desde o design.
**Fonte:** ADRs; skill `web-security-review`; specs 14/17(planejada 05).

- [ ] Entrada validada (FormRequest/`filter_*`); saída escapada; SQL por binding; massa por lista (`validated()`).
- [ ] **CSP sem `unsafe-inline`** (hash/nonce), cacheável (ADR-022); headers de segurança completos.
- [ ] Cookies `__Host-` `Secure; HttpOnly; SameSite`; sessão regenerada no login.
- [ ] Rotas de escrita com `throttle:`; `429` **RFC 7807** + `Retry-After`; estado do limite em **Valkey** (sem DB no caminho quente).
- [ ] Login: Argon2id, `session()->regenerate()`, **falha genérica**, logout `POST`+CSRF, throttle e-mail+IP.
- [ ] **LGPD**: dado pessoal classificado/cifrado; DSAR; retenção/`legal hold`; incidente com runbook; **sem PII em log**.
- [ ] Nenhum segredo no repositório (`.env` fora do versionamento; `.env.example`).
**Sinais de violação:** `{!! !!}` com dado não confiável, `Request::all()`, HTML público com token, `SameSite=None` sem `Secure`.
**Evidência:** `make check` (`audit`); `make check` (testes de auth/rate/audit); inspeção de headers.

---

### D12 — Observabilidade, telemetria & auditoria (ADR-024/035 / specs 06/14)
**Objetivo:** log/telemetria/auditoria com o **mesmo vocabulário** e sem PII.
**Fonte:** ADR-024/035; specs 06 e 14.

- [ ] Toda Action emite **span** `{service}.{op}`/`{module}.{action}` **e** `log_event` com o **mesmo** nome.
- [ ] Atributos **`scalar|null`**, **sem PII**; teste usa `RecordingTelemetry`; `Clock`/`IdGen` não são aninhados.
- [ ] Depende **só** de `Core\Contracts\Telemetry` (injeção); nunca instanciar `OtlpTelemetry`; fail-open.
- [ ] Log JSON com campos obrigatórios (`ts`,`level`,`request_id`,`trace_id`,`module`,`action`,`user`,`duration_ms`,`message`,`context`).
- [ ] `X-Request-ID` gerado/preservado e ecoado; erro → **RFC 7807** com `trace_id`.
- [ ] **Auditoria** (só via `AuditWriter::record`): CMS `full` (mutação + leitura sensível + login), portal `none`, api `sensitive`;
      `integrity_hash` **BLAKE3**; imutável (trigger); retenção respeita `legal_hold`.
- [ ] Vocabulário único `{module}.{action}` = permissão = span = log = auditoria.
**Sinais de violação:** `$this->telemetry` instanciado; span com CPF/e-mail; log de domínio frasal; escrita direta na tabela de auditoria.
**Evidência:** `make check` (`*TelemetryTest`, `*Audit*Test`); `make check` (`coverage`).

---

### D13 — Testes e evidência
**Objetivo:** provar comportamento, determinismo e integridade por teste.
**Fonte:** spec 08 §3; spec 04 §7; `.agents/skills/playwright-ts-admin`.

- [ ] Existe teste que **falha sem a mudança** (vermelho→verde); casos de borda cobertos.
- [ ] Camadas: **Unit** (Action/regra) · **Feature** (HTTP/integração) · **Estrutural** (gates transversais) · **E2E**.
- [ ] E2E: `src/tests-e2e/histories/*.md` × `tests/*.spec.ts`, 6 projetos (`cms-serial` roda
      primeiro via `dependencies`, depois `portal-desktop`, `portal-mobile`, `portal-no-js`,
      `cms-desktop`, `cms-mobile`); **axe 0 violações A/AA**.
- [ ] E2E prova **zero request externo**; **paralelismo por arquivo** (`E2E_WORKERS`, default 4,
      `fullyParallel: false` — `true` colide no lote da galeria), `retries: 0`.
- [ ] `make check` verde **2× seguidas** (anti-flaky); `make check E2E=1` ≥ 1× ao fechar o escopo.
- [ ] Nenhum passo novo incondicional e caro no caminho dourado (§10.1).
**Sinais de violação:** teste ausente para o comportamento novo; teste dependente de relógio/rede/ordem;
e2e sem axe; check desligado para "ganhar tempo"; passo de >10 s sem justificativa.
**Evidência:** `make check`; `make check E2E=1` (tempos no relatório).

---

### D14 — Documentação, rastreabilidade e higiene
**Objetivo:** decisão registrada, doc coerente, repositório limpo.
**Fonte:** `CONTRIBUTING.md`; spec 18; `AGENTS.md`.

- [ ] Decisão relevante → **ADR**; spec/plano coerentes; `bash tools/docs-check.sh` verde.
- [ ] Ficha/task ancorada no `wiki/implementation/*.md`; dívida `// @debt [ID]` registrada no `kd`.
- [ ] Sem código morto (mover para `.pi/trash/`, nunca `rm -rf`); `resources/js` boilerplate removido.
- [ ] `.gitignore`/`.gitattributes`/`.dockerignore` corretos; nada de `.env`/segredo versionado.
**Sinais de violação:** doc stale; referência a artefato inexistente; arquivo morto no tree.
**Evidência:** `bash tools/docs-check.sh`; `tools/gates-selftest.sh`; revisão do diff.

---

## 8. Matriz de severidade × disposição

| Severidade | Definição | Disposição padrão |
|---|---|---|
| **Bloqueador** | Violação de invariante/ADR/segurança/boundary; axe vermelho; PHPStan max quebrado; perda de dado | **Corrigir agora** com teste |
| **Alto** | Determinismo; i18n; a11y não coberta por gate; licença; N+1; doc de decisão ausente | Corrigir na sessão **ou** abrir **issue no `kd`** |
| **Médio** | Simplicidade; naming; doc desatualizada; cobertura parcial | Abrir **task no `kd`**, priorizar |
| **Baixo** | Estilo; comentário; oportunidade | Registrar no `kd` |
| **Observação** | Nota/ideia sem ação obrigatória | Registrar no `kd` (não bloqueia ✅) |

**Disposições possíveis:** Corrigir agora · Corrigir na sessão · Abrir issue/task (`kd`) · Registrar
`@debt [ID]` · **Propor emenda de ADR** · Aceitar (com justificativa registrada).

---

## 9. Critérios de saída (DoD da revisão)

- [ ] Todos os arquivos da ficha lidos.
- [ ] Checklists **D1–D14** aplicáveis preenchidos (com evidência).
- [ ] Achados classificados (§8), registrados (`kd write --anchor`) e com disposição definida.
- [ ] Correções com **teste vermelho→verde**; nenhuma regressão.
- [ ] `make check` **2×** verde; `make check E2E=1` + `make check` (`a11y`) verdes se tocou UI/HTML/CSS.
- [ ] `bash tools/docs-check.sh` verde se tocou doc; gates específicos verdes (§10).
- [ ] Ficha atualizada (`✅`/`🔧`) + linha na tabela do `README.md` e em §15.
- [ ] **Sem commit** (regra §0 do `AGENTS.md`); `kd rewind` feito.

---

## 10. Matriz de evidências (gate → comando → critério de aceite)

> **Um caminho só (ADR-038/ADR-040):** não existem alvos `make lint`/`static`/`test`/`coverage`/`e2e`/
> `a11y`/`perf`/`quality-2x` — tudo vive em **`make check`** (e `make check E2E=1` para a cadeia
> completa). A matriz abaixo lista o **check** como aparece no relatório e o script real por trás dele.

| Check no relatório (`make check`) | O que garante | Critério de aceite | Fonte |
|---|---|---|---|
| `prep:test-db` | banco de teste dedicado (`concordia_test`) | existe e é `_test` | CONTRIBUTING §5.1 |
| `prep:migrate`\* | esquema do app em dia antes de semear/medir | sem migração pendente | ADR-017/spec 10 |
| `prep:assets`\* | build Vite presente e fresco | manifesto cobre as entradas | ADR-022 |
| `prep:demo-data` | demonstração idempotente (orquestrador) | 1 boot do framework | ADR-041 |
| `prep:autoload`\* | classmap com as classes atuais | `composer dump-autoload` OK | — |
| `prep:normalize` | Pint + Prettier/ESLint `--fix` (antes/depois) | 0 não-conformidade | spec 08 §4 |
| `docs-check` | specs com dono/status no índice | OK | spec README |
| `lint-php` / `lint-js` | Pint `--test` · Prettier+ESLint | 0 erro | spec 08 §4 |
| `static` | PHPStan/Larastan **max**, sem baseline | 0 erro | ADR-040 |
| `check-boundaries` | fronteiras core/services/layout/modules | 0 violação | ADR-013/015 |
| `check-migrations` | migração segura/idempotente | 0 problema | ADR-017/spec 10 |
| `check-vendor` | zero CDN + licenças (composer/npm/assets/binários) | 0 violação | ADR-002 |
| `check-gates` | selftest dos gates (fixtures) | OK | — |
| `security-check` | borda/`php.ini`/headers/`TrustProxies` | OK | ADR-020/021 |
| `optimize-check` | `env()` só em `config/`; sem closure em rota; `strict_types` | OK | ADR-008 |
| `optimize-verify`\* | config/route/event/view cacheiam | OK | ADR-008 |
| `audit` | `composer audit` + `npm audit` | sem vulnerabilidade conhecida | ADR-002 |
| `coverage` / `coverage-check` | PHPUnit estrito + PCOV | verde e ≥90% de `src/` | ADR-038 |
| `e2e` (E2E=1) | Playwright+axe, 6 projetos (lote global serial primeiro) | **0** violação A/AA | ADR-025/spec 04 |
| `a11y` | sniffCSS (desktop+mobile) | 0 `fail`/`warn` fora da ledger | spec 04 |
| `perf` (E2E=1) | bundle/.br + CLS | dentro do orçamento; CLS ≤ 0,1 | ADR-022/plano 14 |

\* Passo **gated por staleness**: vira `SKIP` quando nada mudou desde o último verde (ver §10.1).

---

## 10.1 Orçamento de ciclo do `make check` (feedback rápido)

**Regra:** alvo **≤ 100 s** sem e2e e **≤ 3 min** com `E2E=1` (12 vCPU). Medido em 2026-10-20
(pós-B9): `make check` **81,93 s / 92,44 s** · `make check E2E=1` **158,83 s**. O relatório imprime
o tempo por check.

Por que é invariante: 50+ módulos e dezenas de services em poucos meses; ciclo lento deixa de ser
usado, e o que não é usado não valida nada (contraria ADR-001/ADR-038 na prática).

| Regra | Anti-pattern (já corrigido) |
|---|---|
| Passo caro **e idempotente** é gated por stamp de mtime, com `SKIP` **visível** | seed demo em 5 chamadas `db:seed --class=` (5 boots, 41 s); `optimize` a cada run (31 s); `dump-autoload` (9 s); `migrate` (8 s) |
| Velocidade vem de **remover repetição** e **paralelizar o independente** | `perf` repetindo o `PerformanceBudgetTest` já coberto no `coverage`; `npm ci` a cada run do runner docker |
| **Nunca** pular verificação para ganhar tempo | desligar a11y/cobertura/e2e "porque demora" (violação, não otimização) |
| Paralelismo só onde é **independente**: e2e **por arquivo** (`E2E_WORKERS`, default 4) | `workers: 1` (174 s) e `--fully-parallel` (colide no lote da galeria: 99/100) |
| Gate pular **só pode falhar, nunca aprovar** | stamps usam mtime de insumo; `bootstrap/cache/*` (escrito pelos checks) não conta |
| Regressão do ciclo é **bug** | `CheckPipelineTest` exige os gates, o seed único e o paralelismo por arquivo |

**Ao adicionar um check:** informe o **custo medido**; > ~10 s exige justificativa ou gate. **Diagnóstico:**
`CHECK_QUIET=1 make check` + tempos do relatório; `CHECK_FORCE_PREP=1` para rodar todo passo caro.

---

## 11. Catálogo de anti-patterns (por dimensão)

**D1 PHP:** `trait `, `abstract class`, `@template`, `Result<`, `new X(` no corpo, `resolve(`/`app(` no corpo,
`each(`, `create_function`, `(real)`/`(unset)`, `lcg_value`, `md5(`/`sha1(` (senha/hash auditoria), `FILTER_SANITIZE_STRING`.

**D2 Laravel:** `env(` fora de `config/`, closure em rota, `$request->all()`, `{!! !!}` com dado não confiável,
`Kernel.php`/`Handler.php`, `$this->middleware()`, `scopeX`, `Model::all()`+filtro em PHP, N+1, `orderBy` cru.

**D3 Determinismo:** `now()`, `rand(`/`mt_rand(`, `glob()` sem `sort`, factory não-idempotente, teste dependente de relógio.

**D4 Fronteiras:** `use Modules\` em `core`/`services`; `use Services\{Nome}\{…}` fora de `{Contracts,DTOs,Enums}` (importar a **implementação**); `use Modules\B` em `Modules\A`; service importando `modules`/`layout`.

**D5 Deps:** `https://` em asset, `cdnjs|unpkg|jsdelivr|fonts.googleapis`, GPL/AGPL, `Process::run(['gs'`.

**D6 CSS:** `!important`, `style="…"`, `px`/`#hex`/`rgb(` no tema, `@layer` sem a11y por último, grid em PHP, daisyUI hardcoded.

**D7 HTML/a11y:** `<div onclick>`, `<img>` sem `alt`, campo sem `<label>`, sem `:focus-visible`, alvo < 24px, exige JS.

**D8 i18n:** string fixa na view, `url('/x')`/`href="/x"`, `<html lang>` errado, locale por cookie, HTML traduzido.

**D9 Grep:** arquivo de módulo sem token, rota/tabela fora do padrão, exceção sem `@name-check-exception`.

**D10 Dados:** `dropColumn`/`DROP` no `down()`, coluna `*_html`/`style`, `varchar(`, `float` p/ preço, seed de dado operacional.

**D11 Segurança:** `unsafe-inline` no CSP, `SameSite=None` sem `Secure`, rota de escrita sem `throttle`, PII em log, segredo no repo.

**D12 Obs/Audit:** `Telemetry` instanciado, span/log com PII, escrita direta em `core_audit_logs`, log frasal.

**D13 Testes:** teste ausente p/ comportamento novo, flaky, e2e sem axe, sem teste vermelho→verde; **passo caro incondicional** no `make check` (§10.1), `fullyParallel: true`, seed por classe.

**D14 Docs:** ADR ausente p/ decisão, doc stale, código morto no tree, arquivo sujo versionado.

**JS (D1/D6):** `var`, `==`, callback-pirâmide, `window.prompt/confirm/alert`, `document.write`, global.

---

## 12. Templates

### 12.1 Achado (registro no `kd`)
```text
[<severidade>] <título curto>
Fonte: <ADR/spec/skill>
Evidência: <arquivo:linha> · <gate e saída>
Esperado: <...>  Atual: <...>
Disposição: <corrigir agora | issue kd <id> | @debt [ID] | emenda ADR | aceitar>
Teste: <arquivo de teste vermelho→verde>
```
Comando: `kd write "<asserção>" --anchor <arquivo>`.

### 12.2 Notas da revisão (ficha `_to_revision/F*-T*.md`)
```md
## Notas da revisão
| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-XX-XX | <agente/humano> | ✅/🔧 | <resumo + ids de achado> |
```

### 12.3 Rastreabilidade
```
kd task <id>  ↔  ficha F{}-I{}-T{}  ↔  ADR/spec  ↔  arquivos  ↔  testes  ↔  gates
```

---

## 13. Roteiro de sessão (resumo operacional)

```
[ ] 0. Relembrar §2 + §11
[ ] 1. kd prime ; kd ask ; abrir ficha + ADR/spec
[ ] 2. Ler TODOS os arquivos da ficha
[ ] 3. D1→D14 (checklist + evidência por dimensão)
[ ] 4. Classificar (§8) e triar
[ ] 5. Corrigir com teste vermelho→verde
[ ] 6. make check 2x (+ E2E=1 se UI/HTML/CSS/rota: e2e + axe + perf)
[ ] 6b. tempos do relatório dentro do alvo (§10.1)
[ ] 7. kd write --anchor ; atualizar ficha + README + §15 (registro de sessões)
[ ] 8. kd rewind ; entregar (sem commit)
```

---

## 14. Métricas e melhoria contínua

| Métrica | Como medir | Meta |
|---|---|---|
| Fichas revisadas / total | §15 | 54/54 |
| Achados por severidade | §15 (contagem) | bloqueadores → 0 |
| Tempo por ficha | §15 | estável/decrescente |
| Reincidência | mesma dimensão/arquivo | decrescente |
| Cobertura de gate | achados cobertos por gate | crescente (achado recorrente vira teste) |
| **Tempo do `make check`** | relatório do próprio comando (tempo por check) | `≤ 100 s` quente · `≤ 3 min` com `E2E=1` (§10.1) |

> Achado recorrente em uma dimensão deve **virar teste estrutural** — para não recorrer.
> Regressão do ciclo de teste é **achado** como qualquer outro: meça, gateie o passo idempotente,
> paralelize o independente — nunca desligue verificação.

---

## 15. Registro de sessões

| Data | Ficha(s) | Achados (bloq/alto/méd/baixo/obs) | Status | Nota |
|---|---|---|---|---|
| 2026-10-17 | F3.I1.T2 (tokens) | 0 / 1 / 1 / 1 / 0 | 🔧 | **A** (alto) `--text-*` sem definição → corrigido + teste de regressão (`fact_00r61m2o`); **B** z-index fora da allowlist (`risk_01dxalee`); **D** alto contraste daisyUI (`risk_00b2hslm`). Evidência: `quality-2x` OK, e2e 77/77, sniff OK. |
| 2026-10-17 | F3.I1.T3 (Atkinson/`--font-content`) | 0 / 1 / 0 / 4 / 0 | 🔧 | **A** (alto) fallback métrico desalinhado (114 vs 130; pior que `system-ui`) → calibrado (`size-adjust` 98,13% / ascent 99,87% / descent 32,61%) + e2e `11-performance` (`fact_013tca1m`); **B–E** observações (`risk_01ve5fta`, `risk_01qtnjqe`, `fact_00im6dq8`). Evidência: `quality-2x` OK, e2e 79/79, sniff OK. |
| 2026-10-17 | F3.I1.T4 (`DESIGN.md`) | 0 / 0 / 1 / 3 / 0 | 🔧 | **A** (médio) medida de leitura 60–75ch documentada mas não imposta (medido 75,5ch) → `.prose { max-inline-size: 65ch }` + teste (`fact_017v23uk`); **B/C** textos corrigidos (`fact_00q61mgq`, `fact_00jyknig`); **D** §10 wording (`risk_00vh5oe7`). Cruzamento 35 tokens doc×código: nenhum ausente. Evidência: `quality-2x` OK, e2e 79/79, sniff OK. |
| 2026-10-17 | F3.I2.T1 (slots/presets) | 0 / 0 / 1 / 3 / 0 | 🔧 | **A** (médio) `SlotConfig::order` prometido pelo plano 10 §3/§5 mas nunca aplicado → **corrigido** (mapa por provedor em `SlotConfig` + `resolve()` com fallback; teste vermelho→verde) e task `error_01bnpvx3` **fechada** (`fact_00zikw2l`); **B** `PresetConfig::label` morto/pt_BR (`risk_00jse6xz`); **C** teste de preset inválido (`fact_0077dcys`); **D** dupla instanciação (`risk_003dknza`). Evidência: `quality-2x` OK (404×2). |
| 2026-10-17 | F3.I2.T2 (tema como dado) | 0 / 1 / 0 / 3 / 0 | 🔧 | **A** (alto) `/theme.css`+`/cms-theme.css` fora de camada venciam `@layer a11y` (ADR-016) → publicados em **`@layer overrides`** + 2 testes vermelho→verde (`fact_003rmj51`); **B** cadeia preset/URL/instância sem fonte (`risk_004981jq`); **C** `defaults()` pode emitir vazio (`risk_00rmgqoq`); **D** 304 estrito no `If-None-Match` (`risk_015naqub`). Evidência: `quality-2x` OK (406×2), e2e 79/79, sniff OK. |
| 2026-10-17 | F3.I2.T3 (shells como componentes) | 0 / 1 / 0 / 3 / 0 | 🔧 | **A** (alto) `<main id="cms-content">` sem `tabindex="-1"` (portal tinha; skip-link não movia foco; spec 04 §4/WCAG 2.4.1) → corrigido + teste PHPUnit e e2e (`fact_00ktzi0u`); **B** `now()->year` no rodapé (`risk_003v3cyz`); **C** `url('/')` + literal ` · CMS` duplicado (`risk_00h2gtga`); **D** card sem heading (`risk_006skhy0`). Evidência: `quality-2x` OK (407×2), e2e 79/79, sniff OK. |
| 2026-10-17 | F3.I2.T4 (camada a11y universal) | 1 / 0 / 0 / 2 / 0 | 🔧 | **A** (bloqueador) alto contraste remapeava só tokens semânticos; módulos usam `--civic-*` e daisyUI `--color-*-content` → axe em HC: h1 1,02, civic-outline 4,3, `btn-primary` 1,07, botão ativo 1,07; sem gate em HC → corrigido (`a11y.css` + axe no e2e `05`, 4 páginas × 2 projetos) (`fact_00phcs0b`, resolve `risk_00b2hslm`); **B** opt-in de movimento reduzido inalcançável (`risk_00lbc67g`); **C** CMS importa `a11y.css` mas não emite `data-a11y-*` (`risk_00j25aw6`). Evidência: `quality-2x` OK (407×2), e2e 81/81, sniff OK. |
| 2026-10-17 | F3.I2.T5 (overrides por schema) | 0 / 0 / 0 / 3 / 2 | ✅ | Sem violação de invariante/ADR/segurança (injeção coberta, `@property` como defesa extra, rota global). **A** (baixo) ficha dizia “nome” mas o schema rejeita cores por nome (correto; `risk_01aws2yx`); **B** shell liga `/theme.css` com overrides inválidos (`risk_01l9ajqq`); **C** `max-age=3600` sem cache-busting (`risk_00qafbph`); **D** resolver “puro” lê `config()` (`fact_01flzjs5`); **E** ADR-016 §47 lista borda/densidade/espaçamento (`risk_01115fpq`). Evidência: `quality-2x` OK (407×2), docs-check OK. |
| 2026-10-17 | F3.I2.T6 (remover legado) | 2 / 2 / 0 / 1 / 0 | 🔧 | O T6 não pegou o legado de CSP/nonce/CSRF no HTML público. **A** (bloqueador) `nonce="…"` por request quebrava `ETag` (ADR-022) → removido (`fact_00yn17bh`); **B** (alto) CSP legada do app (jsdelivr/fonts.google/`unsafe-eval`/nonce) → removida, edge é o dono (spec 16) (`fact_017r3ops`); **C** (bloqueador) `<meta csrf-token>` por request → removido (`fact_00j4lvy1`); **D** (alto) `AllowIframe`/embed ineficaz (Caddy sobrescrevia + X-Frame-Options) → exceção no edge (`fact_01hf6gca`); **E** (baixo) sem gate de CSP → novo `e2e 14-security-headers` (`risk_00b7wcy7`). Evidência: `quality-2x` OK (410×2), e2e 87/87, sniff OK, check-gates OK. |
| 2026-10-18 | F3.I2.T7 (contrato de shell) | 0 / 1 / 4 / 2 / 0 | 🔧 | **A** (alto) saudação do Blueprint era string **fixa pt_BR** (a view renderiza `{{ $mensagem }}`; `ModuleI18nTest` só varre views) → chave `blueprint::index.greeting` nos 4 locales + `__()` na Action + teste vermelho→verde (`fact_01k79vkx`); **B** (médio) `LayoutContractTest` contava o literal do shell em **comentários** (falso negativo) → `countShells()` remove `{{-- --}}` + teste (`fact_00dxj3xq`); **C** (médio) `src/core/Stub/README.md` citava `cms/page.blade.stub` removido (`fact_01y6273z`); **D** (médio) `partials/cover.blade.php` usa `<img>` cru sem `width/height/decoding` na URL externa legada (ADR-026) → task `task_01doc455`; **E** (médio) o gate cobre só `src/modules/*` (views de página de service sem gate) → `task_01kyxn03`; **F/G** (baixo) `app(Hasher)` na view Demo → `task_01hpxxr3` e FQN em Blade → `task_01o3tbg3`. Evidência: 16 testes/537 asserções OK; `make check` 20 OK · 0 FAIL. |
| 2026-10-18 | F3.I2.T8 (home como módulo) | 0 / 1 / 2 / 1 / 1 | 🔧 | **A** (alto) a view da Home tinha um **`>` órfão** renderizado dentro do `<main>` da **página inicial** — nenhum gate pegava → removido + teste `HomeModuleTest::the_home_main_starts_with_an_element_not_stray_text` (`fact_00kmn7oy`); **B** (médio) traduções **FR sem acento** (`Acces`/`simplifie`) → corrigido (`fact_01eqgnmc`); **C** (médio) a Home lê `config('layout.portal.nav')` — o módulo conhecendo a estrutura do layout, contra `config/layout.php` (“o módulo nunca os conhece”) e ADR-016 → task `task_01o6ify3`; **D** (baixo) FQN `\Illuminate\Support\Facades\Route` na view → task `task_0085czqs`; **E** (obs) `app(Registry::class)` no `register()` é padrão **consistente** nos módulos (`fact_00c9jcet`). Novo `tests/Feature/HomeModuleTest.php` (manifesto/rota/4 locales/área central). Evidência: 9 testes/344 asserções OK. |
| 2026-10-18 | F3.I3.T1 (gate a11y/e2e) | 0 / 0 / 3 / 2 / 1 | 🔧 | **A** (médio) `src/tests-e2e/README.md` **stale** (`make e2e`/`make a11y` removidos; 7 de 14 histórias; `cms-desktop` errado) → corrigido (`fact_01dyk5ui`); **B** (médio) histórias **01–06** citavam os mesmos alvos removidos → `make check E2E=1`; **C** (médio) transversal: **ADRs/wiki citam ~30 alvos `make` removidos** → task `task_00tbqx0x` (`.knudge/notas` não se toca); **D** (baixo) `tools/e2e.sh` (docker) roda `npm ci` a cada invocação → task `task_00f8f18w`; **E** (baixo) `03-no-js` fixa o domínio do cookie em `localhost` (`risk_00qfi5yx`); **F** (obs) `sniff/current` acumula snapshots / nome com `date -u` (`risk_0107zknc`). **Novo gate** `E2eDocsTest` (histórias↔specs 1:1, README lista todas, docs E2E sem alvo `make` inexistente) — vermelho→verde (`fact_005j2obd`). Evidência: 3 testes/16 asserções OK. |
| 2026-10-18 | F3.I3.T2 (SNIFF strict + alvos) | 0 / 2 / 2 / 1 / 2 | 🔧 | **A** (médio) `wiki/specs/04-acessibilidade-ux.md` **stale** (§5 com 6 de 14 histórias; ledger citando arquivo removido na F4; alvos `make` removidos) → corrigido (`fact_00xx4dum`); **B** (alto) e2e **não idempotente**: `08-cms-noticias` cria notícias com `Date.now()` sem limpar (**79** linhas `e2e-%` no banco dev) → task `task_00wmwv84`; **C** (alto) a regra “alvo ≥ 2.75rem na paginação” **não tinha gate determinístico** → **novo `PortalPaginationTest`** (vermelho ao remover `min-h-11 min-w-11`) — `fact_008pgd0f`; **D** (baixo) `playwright.config.ts` dizia “três projetos” (são 4) → corrigido (`fact_00yj82cf`); **E** (médio) `KEY_PAGES` omite `/dados-abertos` → task `task_00hzia04`; **F** (obs) `a11y-diff` usa `ls -t` (`risk_00ub9we8`); **G** (obs) `sniff-gate.sh` mede só a home (`risk_00kwwdvz`). O `E2eDocsTest` passou a cobrir a spec 04. Evidência: 7 testes/52 asserções OK; PHPStan `max` limpo. |
| 2026-10-18 | F3.I3.T3 (spec 04) | 1 / 0 / 2 / 2 / 1 | 🔧 | **A** (alto) `tools/sniff.sh` pinava **imagem inexistente** (`sniffcss:0.5.0`, sem digest; o registry publica `v0.5.x`) → fallback Docker do gate a11y quebrado → corrigido (`v0.5.1@sha256:80ff616f…`) + **novo gate** `PinnedToolImagesTest` (`fact_01blx9rk`/`fact_00v7aytv`); **B** (médio) spec 04 §7 dizia que **um** `tools/sniff-gate.sh` cobria desktop+mobile (mede um viewport) → doc corrigida (`fact_01d0ifaq`); **C** (médio) §2 #4 promete foco ≥ 0,1875rem mas o e2e `02` aceitava `>= 2` → alinhado a 3 (vermelho com 0.125rem, verde com 0.1875rem) — `fact_01lt74e7`; **D** (baixo) `ProblemDetailsHtmlTest`/typo “limitacão” → corrigidos (`fact_00xogq40`); **E** (obs) tabela “Prontas” fora de ordem (`risk_003nlw9n`). |
| 2026-10-18 | F3.I4.T1 (Core Lang + locale URL) | 1 / 0 / 2 / 0 / 1 | 🔧 | **A** (**bloqueador**) a view pública do Demo chamava `LocalizedDate::date()` (inexistente) → `/demo/milestones/{id}` respondia **HTTP 500**; PHPStan não analisa Blade e o módulo não tinha teste → corrigido (`long()`, 200 em pt_BR/en) + **gate novo** `BladeStaticCallsTest` (`fact_01b8ggqk`/`fact_00q5rccz`) + regressão `PublicMilestoneTest` (task `task_00nffbim`); **B** (médio) `Locales::normalize()` era código morto encoberto pelo ignore `shipmonk.deadMethod` → removido (`fact_00ieghyi`); **C** (médio) token `strtolower` vs `Str::snake` vs kebab da ADR-004 → task `task_00h25ff0` (`fact_01n2sr6j`); **D** (obs) `app()->getLocale()` em utilitários estáticos (`risk_00avmp3u`). |
| 2026-10-18 | F3.I4.T2 (make:module + stub) | 0 / 1 / 2 / 1 / 1 | 🔧 | **A** (alto) `make:module` sem `--register` não atualizava o manifesto → módulo invisível, views fora da convenção e **PHPStan `view-string` quebrando** (DoD da ficha falhava) → o gerador agora **sempre** publica (`core:module:activate --disable`, inativo), provado ponta a ponta (`fact_00f6l0xr`); **B** (médio) o stub da migração era o único `*_create_*` sem `Schema::hasTable` (as 12 reais têm) → corrigido + teste (`fact_00uemnjh`); **C** (médio) `MODULE.md.stub` citava o removido `make check-grep` → `tools/name-check.sh --grep` (`fact_01owi8do`); **D** (baixo) READMEs do gerador (`fact_0060rvau`); **E** (obs) `ActionTest.stub` com `markTestIncomplete` por design (`risk_01072eae`). |
| 2026-10-18 | F3.I4.T3 (views `__(token::)`) | 1 / 0 / 2 / 1 / 1 | 🔧 | **A** (alto) o corpus `fr` tinha **22 palavras sem diacrítico** (~30 strings, 5 arquivos) — a paridade só compara **chaves**; corrigido + **gate** `translations_keep_their_diacritics` (vermelho 49 → verde) (`fact_00v3mbcq`, `fact_00z47w2y`); **B** (médio) o gate de texto fixo não cobria as **views de service** → estendido e provado vermelho (`fact_00np6o65`); **C** (médio) `LocaleUrlInvariantTest` só cobre `/en`/`/en/noticias` (`task_01b6e7df`); **D** (baixo) `visibleTextNodes()` com parênteses aninhados (`risk_001dbwjw`); **E** (obs) shell Portal sem gate de texto fixo (`risk_007kcri0`). `pt_BR`/`en`/`es` verificados limpos. |
| 2026-10-18 | F4.I1.T1 (shell CMS configurável) | 1 / 0 / 2 / 1 / 2 | 🔧 | **A** (alto) o modo `orbital` lia a **semente** `config('cms.nav')` em vez do **manifesto** → trocar a posição perdia toda a navegação de módulo (viola ADR-036) → fonte única (`nav-items` + `PanelMenu`) + 2 testes vermelho→verde (`fact_003qt7dl`); **B** (médio) `src/layout/Cms/README.md` mandava `@extends('cms::layouts.app')` — view inexistente e padrão proibido (ADR-009/ADR-016) → reescrito (`fact_01by3590`); **C** (baixo) README dos ícones com caminho errado (`fact_00ashft9`); **D** (gate) novo `DocsReferencesTest` (`fact_01cjx6bv`); **E** (obs) orbital sem e2e/axe (`task_01ubnz8t`); **F** (obs) arco limitado a 6 itens (`risk_003a5995`). |
| 2026-10-18 | F4.I1.T2 (editor TipTap) | 3 / 0 / 1 / 2 / 2 | 🔧 | **A** (alto) `underline` chamava comando inexistente (extensão desabilitada e nunca importada) → clique lançava `TypeError`; corrigido + gate `RichTextSchemaTest` (`fact_01bg998i`); **B** (alto) mídia não persistia: bucket ausente + `store()` caindo no local **sem** fallback na leitura → toda mídia 404 e nenhum gate acusava → corrigido + `make up` garante o bucket + e2e com `naturalWidth>0` (`fact_00cw7pqg`); **C** (alto) editor apontava para a origem do **portal** e a CSP do painel bloqueava a imagem → rota `media.stream.cms` + URLs relativas (`fact_002cge80`); **D** (médio) SVG inline confiando só na CSP do edge → `default-src 'none'; sandbox` (`fact_01txq6ym`); **E/F** (baixo) `isAllowedUri` alinhado e `__index` morto removido (`fact_00hse5nh`); **G/H** (obs) `data-indent` sem CSS (`risk_00fao013`), `BASE_URL` default 8080 (`risk_00v9atym`). |
| 2026-10-18 | F4.I1.T3 (sidebar + Ctrl+K) | 2 / 0 / 1 / 1 / 4 | 🔧 | **A** (alto) busca da navegação rápida **sensível a acento** (`noticias` não achava *Notícias*) → normalizada nos dois lados + PHPUnit + e2e nos dois sentidos (`fact_00pxqhk9`); **B** (alto) a regra de alvo em **ponteiro grosso** não chegava ao painel (`layout.css` é só do portal) e nenhum gate percebia → movida para a folha compartilhada + gate `DesignTokensTest` (`fact_001j5p7x`); **C** (médio) `context_key` calculado e **nunca renderizado** → contexto do grupo exibido (`fact_00owvn4i`); **D** (baixo) `url('/')` na marca do painel (`risk_00frr34f`); **E–G** (obs) `aria-current` só no dashboard (`risk_00urwtfp`), lista plana montada na view (`risk_01qasi11`), `Ctrl+K` global no editor (`risk_006noxdx`); **H** (obs) painel sem projeto mobile no e2e nem sniff (`risk_01utjqyo`). |
| 2026-10-18 | F4.I1.T4 (acesso ao painel) | 1 / 0 / 1 / 2 / 5 | 🔧 | **A** (**bloqueador**) 4 migrations declaravam `varchar(n)` contra a **ADR-007**/spec 10 §21 e o `migration-check` não tinha regra (o desvio atravessou F1→F4) → **regra `no-varchar`** (fixture prova) + `create_*` em `text` + 4 migrations de convergência + teste de esquema; zero `varchar` nas 4 tabelas (`fact_018x2miq`); **B** (médio) sem teste para o balde zerando no sucesso nem para logout só-`POST` → 2 testes, o de `405` provado vermelho (`fact_00fkwuys`); **C/D** (baixo) broker `passwords` morto (`risk_00ap1w5z`) e `$request->string()` no lugar de `validated()` (`risk_00aakt4d`); **E–I** (obs) `Notifiable`/`lock_version` sem uso, sem `X-RateLimit-*`, freio só por e-mail+IP (`risk_01i3xldb`), trilha de falha sem identificador. CSRF não é testável em PHPUnit (`fact_001geqqs`). |
| 2026-10-18 | F4.I1.T5 (TipTap × alternativas) | 0 / 0 / 2 / 1 / 2 | 🔧 | **A** (médio) `THIRD_PARTY_LICENSES_ASSETS` **congelado** (287 vs 245 pacotes, sem nota da exceção MPL, Lucide 70 vs 71) e o gate validava a allowlist mas não a frescura → regenerado + gate de frescura, provado vermelho (`fact_002gt3r5`); **B** (médio) a Verificação da ADR-028 atribuía ao `name-check` uma checagem que ele não faz → emenda 6 + `RichTextExclusivityTest`, provado vermelho por injeção (`fact_010yy1g9`); **C** (baixo) índice do `AGENTS.md` sem o bullet da 041; **D/E** (obs) 15 ADRs ainda “Proposto” (`risk_01irkxc8`), npm todo em `devDependencies`. Licenças: zero copyleft forte, MPL só `lightningcss*` (build-only). |
| 2026-10-18 | F4.I1.T6 (`<dialog>` nativo) | 0 / 2 / 2 / 1 / 2 | 🔧 | **A** (alto) o título do diálogo de legenda/alt mostrava a **chave crua** `media_caption` (o componente não publicava `data-i18n-media-caption`) → atributos + gate estrutural + e2e, provados vermelhos (`fact_01pu2gao`); **B** (alto) o `<textarea>` de cada locale sem nome acessível (o rótulo aponta para o radio; com JS o axe o ignora) → `aria-label` + **e2e com o bundle bloqueado rodando axe** no fallback (vermelho `[critical] label`) (`fact_0150v6t6`); **C** (médio) o comentário prometia foco de volta ao gatilho, mas aplicar deixa o foco no editor (rAF) → comentários + e2e dos três caminhos (`fact_014holhc`); **D** (médio) listeners de `[data-dialog-close]` acumulam por abertura (`task_00w29li2`); **E–G** (baixo/obs) `$required` ignorado no multi-locale, inputs dos diálogos no form da página, `maxlength` só no cliente. |
| 2026-10-18 | F4.I2.T1 (make:module) | 0 / 1 / 2 / 3 / 5 | 🔧 | **A** (alto) o módulo gerado **falhava no `--grep` que o próprio `MODULE.md` prescreve** (camada Test sem o token; Migration dependia do índice por acidente) → `{{ module_lower }}` nos 3 stubs + gate, vermelho antes (`fact_00hv5u2r`); **B** (médio) 4 alvos `make` inexistentes no `MODULE.md` gerado (e em 5 docs de `src/`) → comandos reais + gate em `DocsReferencesTest`, vermelho antes (`fact_01jxxyrw`); **C** (médio) e2e de legenda/alt **flaky** (7s num round-trip ao S3) → espera por evento, provado com atraso de 9s (`fact_01rrz8ds`); **D–F** (baixo) README do gerador com exemplos errados/“trait padrão”/botão inexistente, placeholders mortos `{{ view }}`/`{{ resource_lower }}`, rota `.portal.index` fora da convenção (`fact_01gk8ain`); **G–K** (obs) `ActionTest` incompleto por desenho, `authorize()` omitido, chave `example` órfã, `search_path` em exceção, estilo de token (`task_00h25ff0`). Verificado ao vivo: `make stub` gera+ativa+migra, `/stub_probe` 200, rollback → `backup_*`. |
| 2026-10-18 | F4.I2.T2 (Notícias ponta a ponta) | 1 / 0 / 2 / 2 / 9 | 🔧 | **A** (**alto**) o link do título no card de `/noticias` tinha **22px** com título de **uma linha** (mínimo 24px do SC 2.5.8) e **nenhum gate via** — todos os títulos semeados quebravam em duas linhas; apareceu com uma notícia de título curto (`06-alvos-minimos` vermelho em desktop e mobile) → `min-h-6` + `pointer-coarse:min-h-11` **e** notícia curta na semente, vermelho→verde (`fact_00fve5ex`); **B** (médio) rota CMS **de módulo** sem `auth` → 403 ao visitante em vez do login (medido: `/noticias` 403 × `/` 302) → `AreaRegistrar` envolve rotas de módulo (a versão ampla tornou `cms/robots.txt` privado — pego pelo `SeoRootFilesCoverageTest`), teste em `CmsAuthTest` (`fact_019nrozg`); **C** (médio) a **capa** não tinha teste (SaveNoticia em 28% de métodos) → teste com o serviço real + `Storage::fake` e e2e subindo capa real (`fact_01wl0vwn`); **D/E** (baixo) `Request` não usado no controller; `SeedersCoverageTest` acoplado às contagens (`fact_017v9jwm`); **F–N** (obs) `now()`/`Str::random` no model (`risk_0115bt4r`), `{state}` sem restrição, `backfill` sem chunking, sem `throttle` nas escritas do CMS, FQNs em Blade, arquivar sem confirmação. |
| 2026-10-18 | F4.I2.T3 (MODULE.md + token) | 0 / 0 / 3 / 1 / 2 | 🔧 | **A** (médio) `tools/name-check.sh --grep <token>` — o comando que o **`MODULE.md` gerado prescreve** — falhava em módulo sem persistência (`--grep home` acusava Model/Action/Migration; o modo árvore passava) → a camada só é exigida quando a feature do token tem a pasta, token ausente vira ERRO; `home` exit 0, selftest OK (`fact_00iqbhsw`); **B** (médio) nada garantia **sincronia** entre `MODULE.md` e código → 2 gates novos em `DocsReferencesTest` (rotas da tabela e todo `log_event` citado), provados vermelhos (`fact_00hekyfq`); **C** (baixo) `Demo`/`Blueprint`/`Home` sem `MODULE.md` (`task_00v7tsxs`); **D/E** (obs) `new $class` na fábrica de provider e URI com domínio de área (`risk_01angu5b`); **F** (médio) o e2e asseria um **flash** (consumido por qualquer requisição intercalada) → provado no PHPUnit (sessão + render, vermelho ao remover o bloco) (`fact_01lcm8je`). |
| 2026-10-18 | F4.I3.T1 (WidgetRegistry) | 0 / 0 / 1 / 2 / 1 | 🔧 | **A** (médio) o fragmento do widget fixava `id`+`aria-labelledby` e o widget pode ocupar dois slots (a validação do CMS não proíbe repetir) → **dois ids iguais** ao renderizar duas vezes (WCAG 4.1.1) e nenhum gate via → id removido + gate que renderiza todo widget duas vezes, vermelho antes (`fact_00w3iihq`); **B** (baixo) `@props` sem uso no fragmento; **C** (baixo) ordem determinística implementada mas não testada com 2+ widgets → teste novo, vermelho ao remover `ksort`/`sort` (`fact_018n0bu8`); **D** (obs) nenhum gate pega id duplicado *comum* nas páginas (`risk_017spml8`). |
| 2026-10-18 | F4.I3.T2 (banners/perfis) | 0 / 1 / 1 / 2 / 4 | 🔧 | **A** (**alto**) o escopo por **grupo de URL** não valia no **locale padrão**: as rotas sem prefixo carregam o sufixo interno `default.` (ADR-012) e o resolvedor comparava o nome cru → medido: `placements(noticias.index)=1` × `placements(default.noticias.index)=0` (o perfil só aparecia em `/en/noticias`), e o e2e só exercitava o perfil global → normalização + teste PHPUnit + perfil de grupo semeado + e2e novo, todos vermelhos antes (`fact_00ozv3c4`); **B** (médio) o widget declarava `home.aside`, fora do enum canônico → declaração morta + gate novo (`fact_005bch7j`); **C/D** (baixo) cache sem teste (novo teste, vermelho sem o hook `saved`) e `app()` onde cabia injeção no form do CMS (`fact_012px9rt`); **E–H** (obs) widget com cabeçalho em região anterior ao `<main>` quebra a regra “primeiro cabeçalho é h1” (`risk_00hi43ht`), `hasPlacements` sem span, TTL fixo em 300 s, data malformada como fail-open. |
| 2026-10-18 | F4.I3.T3 (admin `/cms`) | 0 / 3 / 2 / 1 / 4 | 🔧 | **A** (**alto**) o formulário de layout **não mostrava erro de validação nenhum** (só a sessão, que era o único gate) → resumo `role="alert"` + `form-group` no escopo + `aria-invalid` nas linhas + `attributes()` + chave `error_summary` nos 4 locales; teste e e2e vermelhos antes (`fact_01dcdocr`); **B** (**alto**) o **texto de erro reprovava contraste** (3.6:1) — o e2e novo é o primeiro a renderizar estado de erro sob axe → `--status-danger` escurecido para ≈5.5:1 (`fact_01uyxbdt`); **C** (**alto, transversal → `task_00inehom`**) o app **não tem `lang/`**: `__(validation.required)` devolve a chave crua em todo formulário (`risk_01shfi4j`); **D** (médio → `task_00lzwcrb`) convidado em admin surface de service recebe **403** e não o login (medido 403 × 302) (`risk_01oagqo4`); **E** (médio) o `PUT` de edição não tinha teste nenhum (ramo *ignore-self* descoberto) → teste novo, cobertura 90,84 → 91,13% (`fact_00r782qf`); **F** (baixo) escopo único reimplementado com closure + `DB::table` em vez de `Rule::unique()->ignore()`; **G–J** (obs) sem entrada de navegação para a admin surface de service, agenda sem fuso no `jsonb`, sem `throttle` nas escritas do CMS, alvo mínimo do CMS não medido por e2e (`risk_009tjwbb`). |

| 2026-10-18 | D6 (higiene/documentação) | 0 / 1 / 3 / 2 / 4 | 🔧 | **A** (**alto**) a **constituição** prescrevia comandos aposentados (`make lint`/`e2e`/`a11y`/`docs-check`) e números velhos (6 histórias/3 projetos × 14/4 reais), sem gate na raiz → corrigido + 2 gates novos (alvos `make` em docs normativos; contagens derivadas da árvore), ambos vermelhos antes (`fact_00ysl5ag`); **B** (médio) o mesmo em `CONTRIBUTING.md`, no índice de implementação e em **12 specs** — 36 substituições, agora cobertas (`fact_00epdbn3`); **C** (médio) tabela **Estado** stale (F5 1/12 × kd 5/12; F4 13/13 × 13/18) → corrigida + regra de contagem (`risk_013jd913`); **D** (médio → `task_00tbqx0x`) registros (`plan`/`adr`/relatos) fora do gate de propósito, inventariados (`risk_00o5fn3y`); **E/F** (baixo) `make psql` inexistente e impossível no `php-fpm` → comando real (`fact_010roqmo`); exemplo da ADR-015 com `Services\SEO`; **G–J** (obs) tasks de revisão sob épico fechado divergem o progresso, gates de alvos sobrepostos, `.pi/trash` fora do padrão, `make psql` útil mas fora da superfície mínima. Verificado OK: sem código morto (`resources/js` ausente, `problem.blade.php` usado), `.pi/` gitignored, zero referências ao descarte. |

| 2026-10-18 | F5.I1.T1 (services/Seo + @graph) | 0 / 1 / 1 / 2 / 5 | 🔧 | **A** (**alto**) o `@graph` declarava `SearchAction` para `/busca`, que responde **404** (medido; o service `Search` é backlog) → `search_path` sem default + nó **condicional** (`searchUrl()` devolve `null`), teste novo vermelho→verde e `SeoTagsTest` afirmando a ausência (`fact_007ofka0`); **B** (médio → `task_01btnk4j`) `alternatesForPath()` gera os 4 locales para qualquer caminho sem checar se a rota existe → `hreflang` para 404 em caminho só-do-default (`risk_01111rdx`); **C/D** (baixo) `url()->current()` (estado de request) num método dito *puro* e a conversão BCP-47 **triplicada** (`risk_01yrqp1i`, `risk_01j5sg9f`); **E–I** (obs) `canonical`/`hreflang` sem a paginação (medido: `/noticias?page=2` canoniza para a página 1, sem `rel=next/prev`), `GovernmentPage.name` com sufixo, falta `og:image:alt`, `description` caindo no nome da entidade, e o caminho `metadata['seo']` só exercitado pelo Demo. Verificado OK: legado (`Services\SEO`/`seo.track`) com zero ocorrências, ordem/`@context` do grafo, casca só incluindo o componente, `JSON_HEX_*`, `x-default`, span+log, `arch-check` OK e aliases de `SeoData` com consumidor/teste (não são código morto). |

| 2026-10-18 | F5.I1.T2 (raiz dinâmica/404/301) | 0 / 3 / 1 / 2 / 2 | 🔧 | **A** (**alto**) a página de erro **não passa pelo resolvedor de locale** (sem rota) → medido: `/en/pagina-inexistente` vinha com `<html lang="pt-BR">` e texto em português ✗ → o renderer resolve `Locales::fromPath()` + `Context('locale')`, teste nos 4 locales (`fact_00egugli`); **B** (**alto**) título/detalhe do erro eram **pt_BR hardcoded no Core** (ADR-012; o `ModuleI18nTest` não lê PHP) → o renderer manda o status e a view resolve `layout::shell.error_{status}_{title,detail}` — 17 chaves × 4 locales (`fact_01djxthk`); **C** (**alto**) o "voltar ao início" usava **`url('/')`** e saía do locale → `localized_url('/')` + teste novo em `LocaleUrlInvariantTest` (`fact_00yv4je5`); **D** (médio) o 404 declarava `canonical`+`hreflang` para 4 URLs que também são 404 (o achado B da T1) → `noindex` não declara canônica nem alternates; o caso geral segue em `task_01btnk4j` (`fact_00p9xgh8`); **E/F** (baixo) `security.txt` com `Expires` de exatamente 1 ano (RFC 9116 exige menos) e `now()` fora do relógio → `Clock` injetado + 6 meses + idiomas do config (`fact_0197rsvh`); `NoticiaSitemapSource` não usava o `publicadas()` que o docblock citava (`fact_00bh3chj`); **G/H** (obs) manifest com o mesmo logo como 192/512 PNG (`task_01kvi19b`) e cores `oklch` hardcoded; `status` do redirect nunca lido, destino relativo, `id()` duplicado sobrescrevendo, DSAR indexável. Verificado OK: ETag/304 + Cache-Control, só URLs públicas no sitemap, rota viva vence, destino same-origin, `down()` → `backup_*`, `X-Robots-Tag` + log sem PII. |

| 2026-10-18 | F5.I1.T3 (CWV budget + precompressed) | 0 / 0 / 3 / 3 / 1 | 🔧 | **A** (médio) o `precompressed` listava **zstd** que o build nunca gera (os docs prometiam `.zst`) → medido: `.br`/`.gz` saem do disco, zstd vinha do `encode` dinâmico → `precompressed br gzip` no Caddyfile e na spec 16, com o texto corrigido (`fact_01y1ketd`); **B** (médio → `task_005ts67s`) o teste de CLS usa `waitForTimeout(500)` → risco de **falso verde** (shift do `font-display: swap` depois da leitura); o determinístico é `document.fonts.ready` + 2 `rAF` (`risk_0109sibj`); **C** (médio) o determinismo era afirmado **sem gate** → medido (dois builds, md5 agregado idêntico, `MTIME=0`/`OS=3`) + gate novo `the_precompressed_variants_carry_no_build_timestamp` (vermelho ao injetar MTIME) (`fact_00ic0lae`); **D** (baixo) o orçamento de 1 folha de estilo quebra com tema do painel (`/theme.css`); **E** (baixo) o plano F5 citava `make perf`, alvo inexistente → corrigido para a etapa `perf` do `make check E2E=1` (`fact_00m0u0t4`); **F/G** (obs) o plugin fixa `outDir='public/build'` e retorna em silêncio se faltar; ordem do `readdirSync`; `.zst` possível mas sem ganho sobre br. Verificado OK: orçamento brotli por entrada, home ≤20 queries, listagem constante 1→10, CLS ≤0,1, zero script bloqueante, paridade métrica do fallback, sem medição de parede no CI. |

| 2026-10-18 | F5.I2.T1 (dados abertos/DCAT) | 0 / 1 / 4 / 2 / 1 | 🔧 | **A** (**alto**) a neutralização de CSV **corrompia número negativo** (`-42` → `'-42`) e **não cobria o cabeçalho** → `is_numeric` fica de fora e o cabeçalho passa pela mesma função, teste vermelho→verde (`fact_00t6jhbl`); **B** (médio → `task_00skpinc`) **toda resposta do portal manda `Set-Cookie`** (XSRF + sessão), inclusive as cacheáveis — a "sessão anônima" que a ADR-022 evitou; falta `Vary: Cookie` (o HTML varia por cookie de a11y) (`risk_01okkydv`); **C** (médio → `task_00vimpys`) predicado de publicação **copiado em 5 pontos** com `now()` e `isPublished()` que **discorda** (`risk_01npkvew`); **D** (médio → `task_012jlp5n`) `metadata` (jsonb) gravada e **nunca lida** — o `dcat:contactPoint` semeado se perde (`risk_00amq05u`); **E** (médio, corrigido) `signIn` duplicado em 3 specs era **flaky** (clique com `actionTimeout` de 10s esperando navegação) e **derrubou o check** → helper compartilhado `support/cms.ts` com `waitForURL` 30s (`fact_01mk7wma`); **F/G** (baixo) rascunho apaga `published_at` enquanto a action de status preserva; colunas duplicadas não são recusadas (JSON colide) e a largura das linhas não é validada; **H** (obs) API com license fixo/sem links/`dcat:` sem `@context`/pretty; export sem throttle; `status` sem CHECK; `dcat:theme` como texto; MIME duplicado; `inLanguage` por locale com conteúdo não localizado; `If-None-Match` por string exata; etc. Verificado OK: determinismo + checksum=ETag=corpo + 304; só publicados em todas as superfícies; API sem PII com `throttle:open-data`; Dataset/DataDownload/DCAT; permissões e i18n. |

| 2026-10-19 | F5.I2.T2 (LGPD: DSAR/retenção) | 0 / 3 / 2 / 2 / 2 | 🔧 | **A** (**alto**, i18n/D8) a política de privacidade renderizava os rótulos de classificação de `config/lgpd.php` em **pt_BR fixo** em todos os locales (`/en/…` mostrava "Dado público"; o `ModuleI18nTest` não pega expressão Blade) → rótulos no `Lang/{locale}/privacy.php` (`classification_label_*`) + teste vermelho→verde (`fact_00m2spdk`); **B** (**alto**, D13/D14) `tools/check.sh` usava `BASE_URL :8090` enquanto `e2e.sh`/`sniff-gate.sh`/Playwright/compose usam `:8080` → `make check E2E=1` **pulava** e2e/a11y/perf e podia reportar verde sem e2e (e `E2E=1` offline virava SKIP) → default `:8080` + `fail_check` offline + gate `CheckPipelineTest` (vermelho 8090×8080) (`error_01oxuv5y`); **C** (**alto**, D7) com o gate consertado, o e2e expôs **contraste AA 1,07:1** no badge `saude` de Notícias em alto contraste (`bg-civic-yellow text-civic-ink` com `--civic-ink` branco) → `text-civic-on-yellow` (`error_01psxo7h`); **D** (médio, D13) testes fracos (só sucesso) → agora provam auditoria da leitura no CMS (`lgpd.view`), protocolo na página de recebido e o alvo `lgpd-dsar` invocado; **E** (médio, D14) `MODULE.md` dizia que o runbook ANPD estava fora do escopo (T2 o entrega) → corrigido; **F** (baixo, D3) `isOverdue()` cai em `Carbon::now()` (`risk_00jg0337`); **G** (baixo, D10) `legal_hold` sem superfície de gestão, `type`/`status` sem CHECK, retenção sem chunking (`risk_000ctztx`); **H** (obs, D12) auditoria ambiente `lgpd.updated` ≠ span/log `lgpd.update_dsar` (sistêmico) (`risk_01vx21pf`); **I** (obs) env drift: `vendor` sem `shipmonk/dead-code-detector`, 9 migrações pendentes, Caddy com Caddyfile antigo (embed sem `frame-ancestors *`) (`fact_01bt2bd1`). Evidência: `make check E2E=1` verde **2× seguidas** (20 OK · 0 FAIL · 1 SKIP; e2e 87/87 + axe no alto contraste). |

| 2026-10-19 | F3.I1.T1 (Tailwind/daisyUI/Lucide vendorizados) | 0 / 1 / 0 / 0 / 1 | 🔧 | **A** (**alto**, D7/D13) **6 ícones** usados em views de módulo/serviço (`pencil`, `archive`, `eye-off`, `send`, `building-2` em Notícias/DadosAbertos/Lgpd + `minus` na toolbar do RichText) **não tinham SVG vendorizado** e renderizavam **nada**; o `IconTest` só varria `src/layout` (ponto cego) → SVGs copiados de `node_modules/lucide-static` (cabeçalho ISC) + `THIRD_PARTY_LICENSES_ASSETS` 71→77 + gate ampliado para todo `src` e para a prop `icon=` da toolbar, vermelho→verde (`pencil`, depois `minus`) (`error_01w8n8et`); **B** (obs) `{!! $icon['inner'] !!}` confia no SVG vendorizado — verificado sem `<script>`/`on*`/`javascript:` (`risk_00flr3qt`). Verificado OK: `vendor-check` (allowlist fechada; só `xmlns`/URLs de licença nos SVG), licenças permissivas (Lucide ISC, Simple Icons CC0, MPL só `lightningcss` build-only), `eslint` ES2025. Evidência: `make check E2E=1` verde **2×** (20 OK · 0 FAIL · 1 SKIP), `IconTest` 8/84. |
| 2026-10-19 | F4.I1.T1 (UX — login/shell do painel) | 0 / 1 / 0 / 0 / 1 | 🔧 | Revisão de UX pedida antes de F5.I3. **A** (**alto**, D6/D7) login desktop comprimido na coluna da sidebar desativada (`data-cms-sidebar` emitido mesmo sem sidebar; medido cartão 160px à esquerda) → `none` + regressão estrutural + e2e de centralização (`error_01yjw6ha`); **B** (**alto**, D14) fundador `suporte@maxima.inf.br` sem usuário/provisionamento (grupo `Suporte` inalcançável) (`risk_00sjrphk`); **C** (obs) breadcrumb repete o título (`risk_011cjiyl`). Evidência: `make check E2E=1` verde 2× (20 OK · 0 FAIL · 1 SKIP). |

| 2026-10-19 | F5.I3.T1 (cache/`ETag`/CSRF/CSP) | 0 / 3 / 2 / 1 / 0 | 🔧 | **A** (**alto**, D11/ADR-022) resposta pública **não-HTML** cacheável (`sitemap.xml`, `robots.txt`, `manifest.webmanifest`, `security.txt`, mídia) saía com `Set-Cookie` de sessão anônima — `StripPublicCookies` só via `cache_public`, que só era marcado para HTML (`fact_00hs9uqw`, corrigido + `ResponseCacheTest` vermelho→verde); **B** (**alto**, D13) `BASE_URL` divergente do `WEB_PORT` do compose podia pular e2e em silêncio → porta **derivada** do `.env` nos 3 scripts + `CheckPipelineTest` (`fact_01jvjp28`); **C** (**alto**, D13) caminho dourado sem `migrate`: `/galeria` **500** e e2e medindo página de erro → `prep:migrate` + gate; **D** (baixo, D2) `Vary` sobrescrito sem `has()` em resposta não-HTML; **E** (**médio**, D14) comentário `:8090` × código `:8080`; **F** (obs) `web` rodando **Caddyfile antigo** (CSP sem Trusted Types) — e2e pegou (`risk_01qke8zj`). Evidência: `make check E2E=1` 22 OK · 0 FAIL · 1 SKIP; e2e 14/15 verdes. |
| 2026-10-19 | F5.I3.T2 (`ETag` estável/`immutable`) | 0 / 1 / 2 / 0 / 0 | 🔧 | **A** (**alto**, D13) manifesto Vite velho quebrava 21 testes no PHPUnit (“Unable to locate file in Vite manifest”) porque `prep:assets` só construía quando o build estava **ausente** → `assets_stale()` por mtime + `AssetsManifestTest` (vermelho provado) (`fact_01jvjp28`); **B** (**médio**, D13/ADR-022) bundle `batch.js` fora do `BUDGET_KB` (sem orçamento nem variante comprimida cobrada) → 8 KB (`fact_01ls8rto`); **C** (**médio**) `Vary`/`Cache-Control` do controller preservados no caminho não-HTML. Verificado OK: CSP sem nonce/`unsafe-inline`, `immutable` no edge (e2e), `.br`/`.gz` imutáveis, FPC descartado com justificativa. |
| 2026-10-19 | F5.I3.T3 + T4 (rate limit · fronteira de storage) | 0 / 1 / 2 / 0 / 2 | 🔧 | **A** (**médio**, D8/ADR-012) títulos de erro de Media/Gallery **fixos em pt_BR** no PHP (o `ModuleI18nTest` só varre views) → `error_*` nos 4 locales + teste vermelho com locale `en` (`fact_01j4nvg7`); **B** (**médio** → `task_01gpf5qd`, D4) `arch-check` aceita `services → services` inteiros: `Gallery` importa `Services\Media\Models\*` (implementação), contra a ADR-015; **C** (obs, D11) gate de borda valida **texto** do arquivo — container com config antigo passa e só o e2e pega (`risk_01qke8zj`); **D** (baixo) `ProcessMediaUpload::failed()` usa `app()` no corpo (`task_01htnzob`). Verificado OK: 3 camadas + `429`/`Retry-After`/`trace_id`, honeypot/time-trap no DSAR, `MediaLimitsTest` amarrando borda×app, `StorageBoundaryTest` + AST + fixtures, quarentena isolada. |
| 2026-10-19 | F5.I4.T1–T4 (media assíncrona · galeria · lote) | 0 / 1 / 2 / 1 / 4 | 🔧 | **A** (**alto**, D11) `GET /cms/media/upload/{upload}` lia o upload de **qualquer** sessão (nome original, `payload`) enquanto as rotas de lote checavam dono → 404 + teste vermelho→verde (`error_01t2yiow`); **B** (**alto**, D13) e2e do lote vermelho por 5 migrações pendentes (`relation "gallery.gallery_media" does not exist`) → `prep:migrate` (`fact_01jvjp28`); **C** (**médio** → `task_01v75xex`, D10) “commit idempotente” sem índice único `(module, path)`; **D** (**médio** → `task_01htnzob`, D3) reentrega da fila sobrescreve `concluido` com `falha`; **E** (**médio** → `task_00zo3k5i`) contrato de leitura por dono no Media em vez de model importado; **F–I** (baixo/obs) `deleted_at` morto em `gallery_media`, `figcaption` duplicando `alt`, `paginate()` listando arquivadas, `<?php // @debt ?>` da sanitização de SVG. Verificado OK: `alt` nas 3 camadas, arquivar preserva o físico, `AuditWriter` em toda escrita, `<picture>` determinístico, MIME real, lote em N requisições com orçamento/cota atômicos, axe na grade e no resultado. |
| 2026-10-19 | F5.I2.T3 (spec 05-segurança/LGPD) | 0 / 0 / 0 / 0 / 1 | ✅ | Doc-only: `docs-check` verde, sem alvo `make` aposentado, fronteira explícita com as specs 14/16 e pendências nomeadas (F5.I3/I4, ROPA/DPIA). |

| 2026-10-19 | F6 — revisão de escopo (plano 15) | 0 / 2 / 3 / 0 / 0 | 🔧 | Revisão **de plano**, não de código: nada de F6 estava implementado. **A** (**alto**, D1/D6) o plano 15 contradizia a constituição ao exigir **imagem de produção separada** (AGENTS: *same image dev/prod*; ADR-031) e ao prometer `legacy_mapping`/`migrate:legacy`/transição de hash para um projeto **novo do zero** → [ADR-042](../wiki/adr/ADR-042-sem-retrocompatibilidade-conteudo-migrado-a-mao.md) (emenda ADR-007) + plano 15 reescrito (`fact_00seq1da`, `decision_01ok0u63`). **B** (**alto**, D13) `up_prod.sh` roda `artisan migrate` **sem `pg_dump` antes** — o backup que o plano prometia não existe (`fact_00yvmv66`) → F6.I2.T1. **C** (médio, D5) não há runbook de deploy/rollback/restore (`wiki/runbooks/` só tem ANPD) (`fact_0117cpka`). **D** (médio, D6) `seo.redirects` funciona mas **sem superfície de operação** (nenhum comando/tela) — com 301 manual, cadastrar URL exigiria SQL em produção (`fact_01th4mno`). **E** (médio) 6 tasks ativas de ETL/PG10 reescritas para o escopo real; paridade deixa de ser contagem de linhas e vira checklist de páginas críticas (`risk_0181ecxw`). Evidência: `make check` 20 OK · 0 FAIL · 3 SKIP; `docs-check` OK. |
| 2026-10-19 | F6 (implementação: I1.T1/T3 · I2.T1/T2 · I3.T1) | 0 / 0 / 0 / 1 / 1 | ✅ | **A** (baixo, D4) o comando novo de backup nasceu em `core/Internal/System/Backup` e o `arch-check` recusou `Storage` fora de `System/Storage`/`services/Media` — **movido** para `System/Storage/Console` (a regra funcionou; nenhuma isenção criada) — `fact_01fq2nyk`. **B** (obs) o schema do `kd` corta statements > 120 escalares: 2 notas tiveram de ser encurtadas. Entregue: **backup antes do `migrate`** (fase 5.9, `pg_dump -Fc` + `pg_restore --list`, `BACKUP_KEEP`, `BACKUP_OFFSITE`→`core:backup:push`, `--backup-only`) provado ao vivo (327 825 bytes; poda 5→2; upload `s3:backups/…`), **restore ensaiado** com RTO medido (`tools/db-restore-drill.sh`: 18 tabelas · 27 migrations · 0 pendentes · 9 s), **`seo:redirect`** (curadoria manual de 301, cache invalidado), runbook `wiki/runbooks/deploy-e-recuperacao.md`, spec 17 atualizada e **gate** `DeployPipelineTest` (ordem backup→migrate, lock/trap/healthcheck, CI único, zero ETL — ADR-042). Evidência: `make check E2E=1` verde **2×** (22 OK · 0 FAIL · 1 SKIP); PHPStan `max` limpo; `arch-check` OK. |
| 2026-10-20 | F6 — ciclo de teste (`make check`) | 0 / 0 / 2 / 1 / 1 | ✅ | **A** (médio, D13/ADR-001) o caminho dourado repetia trabalho **idempotente** a cada run: 5 chamadas `db:seed --class=` (5 boots, **41 s**), `dump-autoload` (9 s), `artisan migrate` (8 s) e `optimize`/`optimize:clear` (**31 s**) — nenhum deles valida nada que já não esteja provado → seed único (orquestrador, ADR-041) e os três últimos **gated por stamp de mtime** (`bootstrap/cache/*` escrito pelos próprios checks não conta — senão o gate nunca fecha) — `fact_01tzeqrz`. **B** (médio) o e2e rodava com `workers: 1` (**174 s**): 100 testes independentes por arquivo → `E2E_WORKERS` (default 4) com `fullyParallel: false`; `--fully-parallel` foi **medido e reprovado** (colide no lote da galeria: 99/100) — isolamento por **arquivo** é o contrato; **71–79 s**, 2 execuções limpas. **C** (baixo) `npm ci` reescrevia `node_modules` em todo run do runner docker e `perf` repetia o `PerformanceBudgetTest` já rodado no `coverage` → gate por lockfile e remoção da repetição. **D** (obs) `coverage` (~44 s) é agora o maior passo: avaliar paratest com merge de clover (`task_018ycvqv`); "rodar só specs afetadas" foi **descartado** (perde regressão). Evidência: `make check` quente **94 s** (era 195 s) e `make check E2E=1` **3m06s** (era ~6m10s, relatado 12 min), verde **2×**; `CheckPipelineTest` (7) passa a exigir os três gates, o seed único e o paralelismo por arquivo. **Normatizado:** `AGENTS.md` (contrato de ciclo + `E2E_WORKERS`/`CHECK_FORCE_PREP`), `CONTRIBUTING.md` §5.1 (alvos, env e tabela de regras) e este protocolo (§10 matriz realinhada ao caminho único, **§10.1** orçamento de ciclo, D13 e §11 com o anti-pattern correspondente). |
| 2026-10-20 | F6 (implementação: I1.T1/T2 · I2.T2/T3 · I3.T2/T3) | 0 / 0 / 2 / 2 / 0 | ✅ | **A** (médio, D10/ADR-042) `up_prod.sh` não tinha **PITR**: só `pg_dump` (volta ao início do dump). Ligado `archive_mode=on`/`wal_level=replica`/`archive_timeout` e criado `tools/pitr-drill.sh` — base backup + `pg_switch_wal` + transação `late` **depois** do alvo + restore por **LSN**. Dois erros medidos e corrigidos: sem `recovery.signal` o PG faz só crash recovery (ignora `recovery_target_*`) e alvo por tempo erra por fuso/corrida do arquivador — `fact_01fa27nj`. **B** (médio, D5) não existia caminho de criação de usuário (sem UI, sem migração — ADR-042): `core:user:create` com Argon2id, um perfil, `suporte` reservado e log sem PII (`fact_01wo8rm3`); retenção remota com `core:backup:prune` (`fact_0027j2jk`); go-live mecanizado em `tools/go-live-check.sh` (`fact_01n4ubi2`). **C** (baixo, D13) `up_prod.sh` **remove as dev deps** do `vendor/` (bind-mount em dev) — registrado como erro + aviso no runbook (`error_00qtwd4q`). **D** (baixo, D14) DoD do plano 06 atualizado (6 itens fechados; 3 dependem de cPanel real). **E** (obs) `DeployPipelineTest` cresceu para 8 testes (PITR/golive/usuários). Evidência: deploy **2×** verde (~117 s, imagens idênticas) + falha injetada → rollback (exit 1, app no ar); PITR 3× (RTO 8–24 s); restore 9 s; `make check E2E=1` verde **2×**. |
| 2026-10-20 | F6 — entrada na fila (fichas 46–54) | — | ⬜ | F6 foi para o pipeline de revisão: **9 fichas** (I1.T1–T3 · I2.T1–T3 · I3.T1–T3), todas `⬜ pendente`, com pontos de revisão próprios de **operação** (idempotência/rollback, segredos e PII em log, retenção/LGPD, `pg_dump`/`pg_restore` na mesma versão, shell `set -Eeuo pipefail`, comandos com `FAILURE` claro e `log_event` sem PII). `README.md` (fila 46–54, ordem de leitura I2→I1→I3) e `_inventario.md` (seção F6) atualizados. Nada foi revisado ainda — é a fila. |
| 2026-10-20 | F6.I1.T1–I3.T3 (9 fichas) | 2 / 1 / 4 / 0 / 3 | 🔧 | **A** (**alto**, D12/ADR-035) `seo:redirect --disable/--remove` usavam **query builder**: `update()`/`delete()` não emitem evento do Eloquent, então desativação e remoção **não entravam na trilha**; o `Cache::forget` manual mascarava a falta do hook do model → passou a operar por model + teste exigindo `created/updated/deleted` (`error_006uxfoa`). **B** (**alto**, D13/D3) a espera do WAL arquivado no `pitr-drill.sh` **expirava em silêncio** e seguia com o restore (ensaio podia passar recuperando menos) e a `cleanup()` existia **nunca chamada** → falha explícita + `trap` (respeita `--keep`) (`error_01udotff`). **C** (**alto**, D11/ADR-020) `core:user:create --password` não validava 12 caracteres (só o prompt) → `MIN_PASSWORD` único (`fact_000raeon`). **D** (médio, D12) `core:backup:prune --dry-run` registrava `core.backup_prune` sem podar nada e `id`=contagem → dry-run não loga, `id`=disco, `delete` falho = `FAILURE` (`fact_000raeon`). **E** (médio, D11/ADR-019) `core:backup:push` mandava **qualquer** arquivo de `storage/` (`logs/…`) ao S3 → só `backups/*.dump` (`fact_000raeon`). **F** (médio, D1/D14) `up_prod.sh` tolerava `storage:ensure-bucket` ausente com texto de 2026-01 (o comando existe) → fallback removido (`error_01udotff`). **G** (médio, D13) `go-live-check.sh` usava `curl -f`+`|| true`: 404/500 virava “sem resposta” → `curl -sS`, relatório mostra `HTTP 404` (`error_01udotff`). **H** (médio, D11) `seo:redirect` aceitava laço (`/a→/a`, `/a→/b`+`/b→/a`) → caminhada limitada a 10 saltos recusa (`error_006uxfoa`). **I** (baixo, D12) `core.user_create` com `action=user_update` → `core.{action}` (`fact_000raeon`). **J/K** (obs) `--from=/a/` não casa com `/a`; `restore_command` fixa o `PGDATA`; `BACKUP_KEEP` cru; `elapsed_ms` GNU-only; `--password` no histórico; `--json` sem escape (`risk_01urn5zn`). Verificado OK em todas: sem CDN/dep nova, sem PII em log, `pg_dump`/`pg_restore` na mesma versão, `set -Eeuo pipefail`, `strict_types`, DI, deny-by-default, idempotência e rollback. Evidência: 9 testes novos (todos vermelhos antes), `make check E2E=1` verde **2×**, PITR 11 s, go-live 9/9. |
| 2026-10-26 | P5.5 (S1–S6.2) | 0 / 3 / 12 / 4 / 1 | ✅ | Revisão paralela com 6 agentes (Core, `OpenData`, dados/export, UI/a11y/e2e, gate/docs, segurança/LGPD). **A** (alto) fonte obsoleta era desvinculada em silêncio ao salvar (`error_01orx61d`); **B** (alto) export curado 500 com slug numérico (`error_00q39uty`); **C** (alto) dataset curado publicava PII sem classificação/base/allowlist (`error_01w5r1yx`). Médios corrigidos: gate aceitava `non_personal` com PII, `source_options`/`metadata` sem validação, erro de colunas oculto, metadados gerados divergentes, toggle publicava fonte morta, `integer` truncava, throttle do export, widget ignorava apresentação, corrida do e2e 33. Parcial: ETag antes da serialização adiado. Adiados como risco no `kd`: contagens (`risk_01s5867q`), custo ETag/throttle (`risk_00k3phqq`), aprovação no save (`risk_00883m9b`). Evidência: 1757 testes/30392 asserções, cobertura 94,19%, `make check` 2× (23 OK/0 FAIL) + `E2E=1` (25 OK/0 FAIL, e2e 154 passed + 1 skip) (`fact_00byu6su`). |

**Legenda:** `✅ revisado` · `🔧 com correções` · `🟡 em revisão` · `⬜ pendente`.

---

## 16. Índice de referências normativas

- **Constituição:** `AGENTS.md` · `CONTRIBUTING.md`
- **ADRs:** `wiki/adr/ADR-001…037` (matriz §7.0)
- **Specs:** `wiki/specs/{04,06,08,09,10,11,12,13,14,16,17,18}-*.md` (índice em `wiki/specs/README.md`)
- **Planos:** `wiki/plan/00-index.md` (00–19)
- **Design:** `wiki/DESIGN.md`
- **Skills:** `.agents/skills/{php-84-correct,laravel-12,css-moderno,html-moderno,javascript-moderno,daisyui,web-accessibility,web-security-review,web-performance,vite,postgresql,pwa,playwright-ts-admin,sniff-css,caddy}/SKILL.md`
- **Gates:** `Makefile` · `tools/*`
- **Fichas:** `_to_revision/F*-T*.md` · índice `_to_revision/README.md` · inventário `_to_revision/_inventario.md`
