---
title: "System Architecture Doc — Target State + Phased Roadmap"
project: disclosure-bureau
business: systems-atelier
author: sa-principal (Principal Architect — DNA Fowler + Larson)
baseline_commit: 19d0678
generated_at: 2026-05-17
schema_version: 0.1.0
---

# 4. Arquitetura alvo + roadmap em fases priorizadas

> Fowler: arquitetura evolutiva, patterns como vocabulário. Larson: alavancas de longo prazo, decisões escritas. Cagan: comece pelo problema. Este documento descreve **a arquitetura alvo** (não a atual — essa está no entregável 01) e propõe um **roadmap em 7 fases** que entrega valor incrementalmente, com critérios de aceite verificáveis.

## 4.1 Princípios de arquitetura

São restrições de projeto, não aspirações:

1. **Markdown é a fonte da verdade.** Postgres é cache estruturado; pode ser reconstruído de zero a partir de `wiki/` + `raw/`.
2. **Open-source no data plane.** Postgres, BGE-M3, Sigma/Cytoscape, Nominatim, MapLibre. Sem SaaS proprietário onde dado bruto passa.
3. **Self-hosted via Docker Compose em VPS.** Sem Kubernetes prematuro. Decisão se reabre se chegarmos a multi-tenant pesado (ADR-005).
4. **LLM via Claude Code OAuth ou OpenRouter.** Proibido `ANTHROPIC_API_KEY`. Não-negociável.
5. **Bilíngue EN+PT-BR desde o ingest.** Não tradução tardia. Postgres tem `pt_unaccent`/`en_unaccent`. Frontend tem locale toggle persistente.
6. **Cada claim é citada.** Chunk com `bbox` JSONB resolve para crop visual via `/api/crop`. Sem citação = não vai pro chat.
7. **Procedência Locard sempre.** `chain_of_custody[]` em evidence, `mentioned_in[]` materializado em entities, `source_page` + `bbox` em chunks. Lint bloqueante.
8. **Confidence bands Tetlock.** Toda claim de sumário tem `confidence_band` (high/medium/low/speculation). Linguagem permitida calibrada.
9. **Idempotência.** Reprocessar o mesmo PDF mantém IDs, atualiza `last_ingest`, preserva `created_at`.
10. **Segurança como restrição de projeto.** Veto do sa-security-engineer prevalece. Sem segredo no git. SMTP real antes de signup público.

## 4.2 Arquitetura alvo (diagrama)

```
┌────────────────────────────────────────────────────────────────────────────┐
│  Edge:  Traefik + Let's Encrypt + WAF leve (modsecurity-crs)               │
└────┬──────────────────────────────────────────────────────────────────┬────┘
     │ TLS                                                              │
     ▼                                                                  ▼
┌──────────────────────────┐                                ┌──────────────────────────┐
│  Next.js 15 web (web)    │◄─SSE───────────────────────────┤  AG-UI chat client       │
│  ─ App Router            │                                │  (browser, mobile-ready) │
│  ─ Pages SSR de wiki     │                                └──────────────────────────┘
│  ─ API routes            │
│  ─ Middleware Supabase   │
└──┬───────────────────────┘
   │
   ├──► Postgres 15 + pgvector + pg_trgm + unaccent
   │    schemas:  public (corpora, documents, chunks, entities, mentions, cases,
   │              hypotheses, evidence, witnesses)
   │              auth (gotrue)
   │              storage (storage-api)
   │              tenants (opcional fase 4)
   │
   ├──► embed-service (FastAPI) — BGE-M3 + BGE-Reranker-v2-M3 — warm-pool, queue
   │
   ├──► chat-runtime
   │    ─ provider OpenRouter (Pattern C streaming, tool loop)
   │    ─ provider Claude Code OAuth (subprocess streaming, tool loop) ← Fase 5
   │    ─ contrato AG-UI v1 (events: text_delta, tool_call, tool_result, state,
   │                         artifact, navigate, done, error)
   │
   ├──► case-runtime (cron + workers Python)
   │    ─ hypothesis-tournament
   │    ─ evidence-officer (chain-of-custody)
   │    ─ red-team review (chief-detective)
   │    ─ output: case/**/*.md → indexed in DB
   │
   ├──► geocoding-service (Nominatim self-host)
   │
   └──► observability stack:
        Loki + Promtail + Grafana + Sentry (self-host) + Uptime Kuma

Auth: Supabase GoTrue + PostgREST + Realtime + Studio (Kong gateway)
Storage: Supabase storage-api + Imgproxy
Backups: pg_dump diário + restic para S3-compatible (Backblaze B2 / Wasabi)
```

## 4.3 Fronteiras de serviço

| Serviço | Responsabilidade | Linguagem | Interface |
|---|---|---|---|
| `web` | UI + SSR + chat orquestração + crop on-demand | TS/Next 15 | HTTPS + SSE |
| `db` | Source of truth runtime (cache do MD) | SQL | TCP 5432 internal |
| `embed` | Embedding + rerank | Python/FastAPI | HTTP 8000 internal |
| `case-runtime` | Hypothesis tournament + red team | Python | Cron + Postgres queue |
| `geocoding` | Nominatim self-host | C++ image | HTTP internal |
| `ingest-pipeline` | PDF → chunks → DB index | Python | CLI, manual ou cron |
| `chat-runtime` | Tool-loop server-side (subset de `web/lib/chat/`) | TS, dentro do `web` | API route + SSE |
| `auth/rest/realtime/storage/imgproxy/studio/kong/meilisearch` | Supabase | mixed | internal |
| `observability` | Logs, metrics, errors | Go/Erlang | self-host |
| `traefik` | Edge TLS + roteamento | Go | edge |

**Princípio:** o `web` é o orquestrador, não o trabalhador pesado. Ingest, rerank, case tournament rodam em processos próprios.

## 4.4 Modelo de dados — evolução proposta

### 4.4.1 Tabelas novas (fases 2 e 3)

```sql
-- Casos investigativos (Tetlock + Locard)
CREATE TABLE public.cases (
  case_id        TEXT PRIMARY KEY,        -- e.g. 'CASE-NIMITZ-TIC-TAC'
  canonical_name TEXT NOT NULL,
  scope_summary  TEXT,
  status         TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','closed','dormant')),
  created_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  last_review    TIMESTAMPTZ
);

CREATE TABLE public.hypotheses (
  hypothesis_id   TEXT PRIMARY KEY,       -- 'H-0042'
  case_id         TEXT REFERENCES public.cases(case_id) ON DELETE CASCADE,
  statement_en    TEXT NOT NULL,
  statement_pt    TEXT NOT NULL,
  prior           REAL NOT NULL CHECK (prior BETWEEN 0 AND 1),
  likelihood      REAL CHECK (likelihood BETWEEN 0 AND 1),
  posterior       REAL CHECK (posterior BETWEEN 0 AND 1),
  evidence_for    TEXT[],                 -- evidence_ids
  evidence_against TEXT[],
  red_team_pass   BOOLEAN DEFAULT FALSE,
  confidence_band TEXT NOT NULL CHECK (confidence_band IN ('high','medium','low','speculation')),
  authored_by     TEXT,                   -- 'holmes' | 'poirot' | 'dupin' | 'locard'
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE public.evidence (
  evidence_id      TEXT PRIMARY KEY,      -- 'E-0042'
  case_id          TEXT REFERENCES public.cases(case_id) ON DELETE CASCADE,
  doc_id           TEXT NOT NULL,
  page             INT,
  bbox             JSONB,
  verbatim_excerpt TEXT NOT NULL,
  evidence_grade   CHAR(1) CHECK (evidence_grade IN ('A','B','C')),
  chain_of_custody JSONB NOT NULL,         -- [{step, actor, timestamp}, ...]
  custody_gaps     TEXT[],
  created_at       TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- ... witnesses, gaps, relations, case_reports analogamente
```

### 4.4.2 Mudança crítica em `entities`

Adicionar:

```sql
ALTER TABLE public.entities
  ADD COLUMN narrative_summary_en TEXT,
  ADD COLUMN narrative_summary_pt TEXT,
  ADD COLUMN summary_confidence   TEXT CHECK (summary_confidence IN ('high','medium','low','none')),
  ADD COLUMN summary_status       TEXT NOT NULL DEFAULT 'none'
       CHECK (summary_status IN ('none','synthesized','curated','red_teamed')),
  ADD COLUMN lat                  DOUBLE PRECISION,  -- fase 3
  ADD COLUMN lon                  DOUBLE PRECISION,
  ADD COLUMN geo_confidence       TEXT CHECK (geo_confidence IN ('exact','city','region','unknown'));
```

A coluna `summary_status` substitui o stub textual. Fora do filtro de listings públicos, qualquer linha com `summary_status='none'`.

### 4.4.3 Multi-corpus (fase 4)

```sql
CREATE TABLE public.corpora (
  corpus_id    TEXT PRIMARY KEY,           -- 'war-gov-ufo', 'jfk-files', 'arquivo-nacional-br'
  display_name TEXT NOT NULL,
  description  TEXT,
  visibility   TEXT NOT NULL CHECK (visibility IN ('public','unlisted','private')),
  owner_id     UUID REFERENCES auth.users(id),
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE public.documents ADD COLUMN corpus_id TEXT REFERENCES public.corpora(corpus_id);
-- RLS pass de USING (TRUE) para USING (
--   corpus_id IN (SELECT corpus_id FROM corpora WHERE visibility='public')
--   OR auth.uid() IN (...)
-- )
```

ADR-005 documenta a decisão.

## 4.5 Fronteira do chat — contrato AG-UI

ADR-001 cobre a decisão. Resumo do **contrato alvo**:

- Servidor expõe `POST /api/chat/run` com body `{session_id, message, ctx}`.
- Response = SSE com eventos AG-UI v1: `state.start` → `text.delta` (n×) → `tool.call` → `tool.result` → `artifact.created` (citações, crops, navegação) → `state.end`.
- Cliente assina via `EventSource` ou `fetch+reader.read()`. Reconnect com `Last-Event-ID`.
- Persistência incremental: cada `text.delta` flush num buffer; ao `state.end`, materializa `messages` row.
- Cancelamento: `POST /api/chat/cancel` com `session_id` flagga a sessão; loop checa a cada turno.
- **Artifact tipado**: `citation` (chunk_id+bbox), `crop_image`, `entity_card`, `case_card`, `evidence_card`, `hypothesis_card`, `navigation_offer`. Cada um tem React component dedicado.

## 4.6 Modelo de processamento — pipeline reorganizada

A pipeline atual (`scripts/00–34`) será reorganizada em **estágios canônicos**:

```
Stage 1 — INGEST       (scripts/ingest/)
  01_extract.sh         PDF → PNG@72 + OCR
  02_vision.py          Haiku 4.5 page-level vision pass (EN+PT)
  03_chunk.py           Sonnet 4.6 chunking via subagent (bbox-aware)
  04_synthesize.py      master doc.md assembly per doc

Stage 2 — INDEX        (scripts/index/)
  10_embed.py           BGE-M3 batch embed → chunks.embedding
  11_mentions.py        tsvector-based entity match (substitui ILIKE)
  12_graph_export.py    graph.json (paginado)

Stage 3 — SYNTHESIZE   (scripts/synthesize/)
  20_entity_summary.py  iterate entities with N+ mentions, write narrative_summary
  21_doc_pitch.py       short pitch per doc for listings (já existe — 34-generate-doc-pitches.py)

Stage 4 — CASE         (scripts/case/)
  30_seed.py            create case skeletons from canonical events
  31_hypothesis.py      Tetlock tournament loop
  32_evidence.py        Locard chain-of-custody check
  33_red_team.py        chief-detective review

Stage 5 — MAINTAIN     (scripts/maintain/)
  40_lint.py            full lint (substitui 04-lint.py)
  41_dedup.py           entity dedup (substitui 03 → sem stubs textuais)
  42_geocode.py         Nominatim lookup para entities.lat/lon
  43_log_rotate.py      rotaciona wiki/log.md por mês
```

Cada stage tem `make stage1` / `make stage2` / ... e cada script tem testes pytest.

## 4.7 Roadmap em 7 fases priorizadas

Fases são paralelizáveis dentro do limite de 1 engenheiro sênior + 1 IA-pair. Esforços em homem-dias (`d`).

### **Fase 0 — Limpeza de stubs "Phase X"** (3-4d)

> Esta fase existe porque o usuário está furioso. Não há valor em construir a fase 2 se a fase 0 não roda.

**Objetivo:** zerar a presença textual de "Phase X" / "Will be enriched" no UI.

**Escopo:**

1. Migrar `scripts/03-dedup-entities.py` para nunca emitir stub textual. `narrative_summary` fica `NULL`, `summary_status='none'`.
2. Script de migração `scripts/maintain/41_dedup.py` reescreve os 22.096 arquivos existentes: remove o texto stub, seta `summary_status: none`, mantém YAML válido.
3. UI: páginas de entidade exibem badge "sem síntese ainda — `[Sintetizar]`" em vez do texto stub. `/timeline` exclui eventos com `summary_status='none'` do default (param `?include_unsynthesized=true` força).
4. `wiki/log.md` registra a operação.

**Critério de aceite:**

- `grep -r "Will be enriched in Phase" wiki/` retorna **0**.
- `grep -r "_Stub" wiki/entities/` retorna **0**.
- Página `/timeline` carrega ≥1 evento por default (estado mínimo) — após fase 2 essa lista cresce.
- Página `/e/event/EV-1492-…` mostra layout limpo, sem "Phase 7".

**Esforço:** 3d (1 sênior).

### **Fase 1 — Cleanup técnico inegociável** (5-6d)

> Higiene mínima antes de construir features. Larson: alavancas que se pagam todo dia.

**Objetivo:** secret hygiene, CI, observabilidade mínima.

**Escopo:**

1. **Rotação de segredos vazados** + remoção do `.env.backup.*` do git-history. Documentação no DEPLOY-CHECKLIST.
2. **CI/CD GitHub Actions**: `lint + tsc --noEmit + build + smoke /api/admin/stats`.
3. **Testes piso mínimo**: 30 testes Vitest cobrindo as 19 API routes (fixture com 3 docs). 10 testes pytest cobrindo scripts críticos.
4. **Observabilidade**: Loki + Promtail no compose; dashboard Grafana básico (latência hybrid_search, error rate); GlitchTip (Sentry open-source self-host) para exceptions.
5. **Consolidar scripts ad-hoc**: 28 `rebuild_doc<NN>*.py` → mover para `scripts/_archive/`. README dos canônicos.
6. **GoTrue SMTP** real configurado (provedor à escolha do user; data plane segue intocado). `MAILER_AUTOCONFIRM=false`.

**Critério de aceite:**

- `git log --all -- .env.backup.1778796747` retorna 0 commits (após filter-repo).
- CI passa em PR. Falha bloqueia merge.
- Grafana mostra request rate e error rate em tempo real.
- Signup novo envia e-mail real.
- `scripts/` sem `rebuild_doc<NN>*.py` no root.

**Esforço:** 5d.

### **Fase 2 — Case Bureau MVP (Locard + Tetlock real)** (10-12d)

> O diferencial. Sem isso o produto é "wiki bonita com chat".

**Objetivo:** primeiros 5 casos canônicos rodando com hipóteses concorrentes, evidence chain-of-custody, red-team pass.

**Escopo:**

1. **Schema**: migration `0003_case_bureau.sql` com `cases`, `hypotheses`, `evidence`, `witnesses`, `gaps`, `relations`, `case_reports`, `residual_uncertainty`.
2. **Seed**: `scripts/case/30_seed.py` cria 5 casos âncora — Roswell 1947, Nimitz Tic-Tac 2004, Phoenix Lights 1997, FBI Hottel memo 1950, Apollo Skylab radio anomaly 1973.
3. **Hypothesis tournament**: `scripts/case/31_hypothesis.py` lê evidences, gera 3+ hipóteses via Claude Code OAuth (Sonnet), calcula posterior simples (prior × likelihood normalizado), escreve `H-NNNN.md` + DB row.
4. **Chain of custody**: `scripts/case/32_evidence.py` valida `chain_of_custody[]` ≥ N steps por grade. Grade A precisa ≥3, B ≥2, C ≥1.
5. **Red-team review**: `scripts/case/33_red_team.py` chama Sonnet em modo adversarial: "encontre furos nas premissas das hipóteses dominantes". Output vai para `residual_uncertainty.md`.
6. **UI**: `/case`, `/case/[id]`, `/case/[id]/hypotheses`, `/case/[id]/evidence`. Componentes `CaseTimelineLane`, `HypothesisTournamentCard`, `EvidenceChainCustody`.
7. **Chat tools novos**: `read_case`, `read_hypothesis`, `read_evidence`. Chat passa a citar `[[hypothesis/H-0042]]` resolvido inline.

**Critério de aceite:**

- 5 cases em `case/case-report.md` com quality rubrics threshold 0.85 satisfeitos (chain_of_custody_completeness, confidence_calibration_match, hypothesis_tournament_discipline ≥3, residual_uncertainty_presence, audit_trail_per_claim, red_team_pass).
- Página `/case/case-nimitz-tic-tac` renderiza timeline + 3 hipóteses + 5 evidences com bbox crop inline + red-team box.
- Chat responde "como foi o Nimitz Tic-Tac?" com citações para hypothesis IDs + evidence IDs.

**Esforço:** 12d.

### **Fase 3 — Timeline real + Dashboards investigativos + Mapa** (10-12d)

> Os dashboards faltantes do brief. Aqui o produto começa a parecer "super centro".

**Objetivo:** timeline com narrativas reais; clusters; mapa; padrões.

**Escopo:**

1. **Pipeline de entity summary**: `scripts/synthesize/20_entity_summary.py` itera entidades com `total_mentions ≥ 5`, recupera chunks via `entity_mentions`, escreve `narrative_summary` real. Custo estimado: ~$80-150 corpus inteiro.
2. **Geocoding**: Nominatim self-host no docker-compose. `scripts/maintain/42_geocode.py` popula `entities.lat/lon` para todas as `locations`.
3. **Curadoria timeline**: 50-100 eventos canônicos com narrativa rica escrita pelo case-writer.
4. **Mapa**: rota `/dashboard/map` com MapLibre GL + tiles open-source. Markers de sightings por densidade. Clica → entity card.
5. **Clusters**: `/dashboard/clusters` agrupa eventos por proximidade temporal+geográfica (DBSCAN sobre lat/lon/date_start). Lista clusters como cards.
6. **Atores**: `/dashboard/actors` lista top-50 entities (pessoas + organizações) por menções, sparkline temporal de aparições por década.
7. **Padrões**: `/dashboard/patterns` mostra UFO anomaly por ano, classification heatmap, redaction ratio por collection.

**Critério de aceite:**

- `/timeline` mostra ≥2.000 eventos com narrative_summary real, agrupados por década, com filtro funcionando.
- `/dashboard/map` carrega <2s, marca >500 locations com lat/lon.
- `/dashboard/clusters` mostra ≥20 clusters identificados, cada um com mínimo 3 eventos.
- `/dashboard/actors` lista top-50 com sparkline.
- `/dashboard/patterns` tem 3 gráficos funcionais.

**Esforço:** 12d.

### **Fase 4 — AG-UI nativo + Mobile responsivo + Performance** (8-10d)

> A experiência conversacional vira diferencial.

**Objetivo:** AG-UI v1 real no chat; artefatos tipados; mobile usable; reranker rápido.

**Escopo:**

1. **AG-UI v1**: substituir `web/lib/chat/agui.ts` por implementação AG-UI canônica. Eventos `state.start/end`, `text.delta`, `tool.call`, `tool.result`, `artifact.created`. Last-Event-ID reconnect. ADR-001.
2. **Artefatos tipados**: `citation`, `crop_image`, `entity_card`, `case_card`, `evidence_card`, `hypothesis_card`, `navigation_offer`. Cada um vira `Artifact` clicável no chat.
3. **Persistência incremental**: cada `text.delta` flush em `messages` via Realtime. Aborto via header `x-chat-cancel: <session_id>`.
4. **Refactor `chat-bubble.tsx`** (507 LOC) → quebra em `ChatComposer`, `ChatStream`, `ToolBlock`, `Citation`, `NavigationOffer`. Aumenta testabilidade.
5. **Mobile**: passar Lighthouse mobile ≥85. Bento → grid responsivo. Modais ↔ bottom-sheets. Touch targets ≥44px.
6. **Reranker warm-pool**: ADR-002 — health check warm; cache de rerank `(query_norm, candidate_ids)→scores`; rerank em background com SSE event `rerank_progress`.
7. **Grafo paginado**: endpoint `/api/graph/neighbors?node=&depth=&limit=`. Carregar inicial só hub-and-spoke. Expand-on-click.

**Critério de aceite:**

- Chat aceita reconnect em queda de rede sem perder mensagem.
- Cada citação no chat renderiza crop inline `<img>` (via `/api/crop`).
- Lighthouse mobile ≥85 em 5 páginas-chave.
- p95 hybrid_search warm <800ms; p95 rerank warm <3s.
- `/graph` carrega <1s com hub-and-spoke; expand busca via API.

**Esforço:** 10d.

### **Fase 5 — Multi-corpus (single-tenant) + API REST + MCP server** (12-14d)

> Abertura. Bureau como plataforma.

**Objetivo:** suportar 2-3 corpora além de war.gov/ufo; expor API REST estável; expor MCP server compatível com Claude Code/Cursor.

**Escopo:**

1. **Migration multi-corpus**: tabela `corpora`; FK `corpus_id` em `documents`/`chunks`/`entities`. RLS policy passa a filtrar por `corpus_id`. ADR-005.
2. **Path layout**: `wiki/<corpus_id>/documents/...` (refactor estrutura) **ou** deploy-por-corpus (ADR-005). Recomendação atual: começar com pasta única `wiki/` e coluna `corpus_id` em DB, refactor de paths fica para fase 6.
3. **Onboard de 2 corpora-piloto**: JFK files (pequeno, alto interesse) + Operação Prato 1977 (PT-BR, validação bilíngue real).
4. **Corpus switcher na UI**: navbar tem dropdown; `/c/<corpus_id>` rotas; chat session vincula corpus.
5. **API REST pública**: `/api/v1/{documents,chunks,entities,events,cases,hypotheses}` (read-only) via PostgREST com rate-limit por API key. Auth = Bearer token gerado em `/account/api-keys`.
6. **MCP server**: `apps/mcp-server/` (Node TS) que expõe os mesmos 12 tools do chat como ferramentas MCP. Roda em container próprio na compose.
7. **Documentação**: `/api/docs` (OpenAPI) + `/mcp/docs` (server card).

**Critério de aceite:**

- 3 corpora visíveis no switcher; cada um com >10 docs.
- API key gera; `GET /api/v1/documents?corpus_id=jfk-files&limit=10` retorna 200.
- Claude Code remoto consegue listar tools do MCP server e fazer uma `hybrid_search`.

**Esforço:** 14d.

### **Fase 6 — Hardening + Backups + Multi-tenant verdadeiro (opcional)** (8-15d)

> Operação adulta.

**Objetivo:** sistema sobrevive a queda do VPS, falhas humanas e abertura externa.

**Escopo:**

1. **Backups**: `pg_dump` diário → restic → S3-compatible (Backblaze B2 / Wasabi — self-host adjacent, não SaaS no data plane bruto). Restore testado mensal.
2. **Snapshot do `wiki/` + `raw/`**: rsync mensal para B2.
3. **Disaster recovery runbook** em `infra/DR.md`.
4. **Rate-limit + abuse detection**: nginx-style limits no Kong; Postgres usage_events alimenta heuristic.
5. **Multi-tenant verdadeiro** (opcional): se a fase 5 mostrar sinal de Persona D, escalar. RLS pesado, billing, isolamento. ADR-005 reaberto.

**Critério de aceite:**

- Restore test no staging restaura a infra a partir de zero em <1h.
- Rate-limit aciona em 100 req/min por IP.
- (Opcional) primeiro tenant externo onboarda sem suporte.

**Esforço:** 8-15d (conforme escopo multi-tenant).

## 4.8 Síntese — sequenciamento sugerido

```
Mês 1  [█████░░░] Fase 0 (3-4d) + Fase 1 (5-6d)
Mês 2  [████████] Fase 2 Case Bureau MVP (10-12d)
Mês 3  [████████] Fase 3 Timeline+Dashboards+Mapa (10-12d)
Mês 4  [██████░░] Fase 4 AG-UI + Mobile + Perf (8-10d)
Mês 5  [████████] Fase 5 Multi-corpus + API + MCP (12-14d)
Mês 6  [██████░░] Fase 6 Hardening + Backups (8-10d)
```

Total: ~60d de engenharia sênior + IA-pair em ~6 meses. Espaço para 15-20d de imprevisto e iteração de design.

## 4.9 Decisões type-1 — ADRs documentados

Cada decisão estratégica vira ADR em `/05-adrs/`:

| ADR | Decisão | Documento |
|---|---|---|
| 001 | Pattern do chat: AG-UI v1 vs assistant-ui/react vs continuar com agui.ts caseiro | `05-adrs/ADR-001-chat-pattern-ag-ui.md` |
| 002 | Caching e serving do reranker BGE-Reranker-v2-M3 | `05-adrs/ADR-002-reranker-caching.md` |
| 003 | Pipeline de geração da Timeline (eliminar stubs) | `05-adrs/ADR-003-timeline-no-stubs.md` |
| 004 | Pipeline Case Bureau (Tetlock + Locard) | `05-adrs/ADR-004-case-bureau-pipeline.md` |
| 005 | Multi-tenant vs multi-corpus single-tenant | `05-adrs/ADR-005-multi-tenant-refactor.md` |

(ADRs 6+ ficam para o time de leads conforme avançam — por exemplo, "sigma vs cytoscape", "Nominatim vs Geoapify self-host", "MapLibre tiles source".)

## 4.10 O que **não** fazer agora (anti-roadmap)

Por proporcionalidade Fowler — coisas que tentam o atalho da complexidade e ainda não se pagam:

1. **Kubernetes.** Docker Compose escala para 10x o tráfego atual. Não migrar antes de ≥20k MAU sustentados.
2. **GraphQL.** REST + tipos pré-formados em SQL cobrem 95% dos casos de integração. Reabrir após 3 integradores externos pedirem.
3. **Vector DB dedicado (Pinecone, Qdrant Cloud)** — pgvector + HNSW sustenta 100k chunks tranquilamente. Reabrir se latência p99 dense >500ms.
4. **CDN proprietário (Cloudflare Pro+)** — Imgproxy + Next.js image optimization + Traefik com cache header cobrem. Reabrir após picos sustentados de tráfego.
5. **Re-embed para dimensão maior (BGE-M3 → outro 4096-d)** — não há sinal de que recall esteja teto.
6. **Migrar pipeline para Airflow / Prefect** — `scripts/` com Makefile + cron resolvem. Reabrir após 3 pipelines concorrentes em produção.
7. **Microservizar o `web`** — monólito Next.js é certo para este tamanho. Não fragmentar antes de gargalo medido.

## 4.11 Riscos do roadmap

| Risco | Probabilidade | Mitigação |
|---|---|---|
| Enrichment de 22k entidades estoura orçamento OAuth Max 20x | Média | Limiar por `total_mentions ≥ 5` reduz para ~5-8k. Fila de 200/dia. |
| Case Bureau red-team gera disputa de hipóteses pesada e demora | Baixa | Limita a 3 hipóteses por caso na fase 2. Quality gate threshold relaxado para 0.80 inicialmente. |
| Multi-corpus refactor toca todo o web | Alta | Fase 5 isolada; flag `MULTI_CORPUS=on` permite voltar. |
| MapLibre tiles open-source ficam lentas | Média | Fallback: MapTiler key (não-SaaS no data plane — só tiles). |
| Rotação dos segredos vazados quebra deploy | Baixa | Staged: trocar em staging primeiro, depois prod, com runbook. |

---

_Esta arquitetura alvo e este roadmap são vinculantes para os leads (sa-architecture-lead, sa-engineering-lead, sa-platform-lead, sa-security-engineer, sa-qa-engineer) e ao gate de aprovação do sa-principal. Cada fase abre com um delegation; cada fase fecha com handoff_artifact + self_score._
