diff --git a/.nirvana/outputs/disclosure-bureau/brief.md b/.nirvana/outputs/disclosure-bureau/brief.md new file mode 100644 index 0000000..cf3f8e0 --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/brief.md @@ -0,0 +1,63 @@ +# Brief + +**Business:** systems-atelier +**Project ID:** disclosure-bureau +**Submitted:** 2026-05-18T01:48:43Z + +## Conteúdo + +Audite e proponha plano de modernização completo do **The Disclosure Bureau** — sistema de investigação sobre documentos declassificados. + +# Contexto + +The Disclosure Bureau (https://disclosure.top) recebe documentos (PDF/imagens), extrai conteúdo via vision LLM, converte em markdown estruturado bbox-aware, gera embeddings densos + esparsos, popula grafo de co-menções entre entidades, e expõe uma wiki investigativa + agente de chat (Sherlock). Corpus atual: 116 PDFs do war.gov/ufo (DOD UAP, FBI Vault, NASA Apollo/Skylab, FOIA) → 3.435 páginas → 20.974 chunks → 34.370 entidades → 248.398 menções. + +**Repo:** `/Users/guto/ufo` (commit baseline `19d0678`) +**Live:** https://disclosure.top +**Instrução canônica:** `/Users/guto/ufo/CLAUDE.md` + `/Users/guto/ufo/CLAUDE-schema-full.md` +**Memory dev:** `/Users/guto/.claude/projects/-Users-guto-ufo/memory/MEMORY.md` + +## Pipeline atual +1. PDF → PNG 72 DPI (pdftoppm) + OCR (pdftotext) +2. Chunking agêntico com Sonnet 4.6 via subagents → raw/--subagent/chunks/cNNNN.md (frontmatter YAML + corpo bilíngue EN+PT-BR) +3. Embedding BGE-M3 (1024d) self-hosted FastAPI; reranker BGE-Reranker-v2-M3 +4. Postgres 15.8 + pgvector 0.8 + pg_trgm + tsvector → hybrid search RRF +5. Entity mentions materializadas via ILIKE +6. Next.js 15 (App Router) + Tailwind + Sigma.js/ForceAtlas2 + +## Stack self-hosted (Docker Compose em VPS) +Next.js 15 / React 19 · Supabase (GoTrue, PostgREST, Realtime, Storage, Studio, Kong 2.8, Imgproxy, Meilisearch) · Postgres + pgvector · BGE-M3/Reranker (PyTorch CPU) · Traefik TLS · Sigma.js graphology. + +## Restrições não-negociáveis +- PROIBIDO ANTHROPIC_API_KEY. Usar CLAUDE_CODE_OAUTH_TOKEN (Claude Code SDK) OU OPENROUTER_API_KEY (fallback). +- Bilíngue EN+PT-BR (português brasileiro, UTF-8 com acentos preservados). +- Identificadores em inglês internacional. +- Markdown puro como source-of-truth. +- VPS self-hosted via Docker Compose. Sem SaaS proprietário no data plane. + +## Funciona hoje +Homepage bento 116 docs · Hybrid search 1.3s · Páginas chunk-a-chunk · Crops bbox auto-tighten · Allowlist image_types · Grafo Sigma.js ForceAtlas2 · Entity pages DB-live · Chat Sherlock (signup/login + Kong corrigido) · /admin/stats, /admin/batch, /admin/indexer. + +## Gaps críticos +1. Timeline (/timeline) — eventos extraídos mas todos com placeholder "_Stub. Will be enriched in Phase 7._" — usuário furioso com esse "Phase X" +2. Case Bureau (case/case-report.md, hypotheses Tetlock, evidence Locard) — schema definido, pipeline nunca rodou +3. Dashboards investigativos reais (não admin técnico) — clusters, geografia, atores, padrões temporais +4. A2UI / AG-UI no chat — streaming tool-calls, artefatos clicáveis, crops bbox inline, multi-turn persistente +5. ELIMINAR todos os "Stubs / Will be enriched in Phase X" + +## Gaps médios +Mobile responsivo precário · Sem onboarding · Sem export (PDF/CSV/PNG) · Sem saved views · Reranker 60s · Grafo trava em corpus grande. + +## Visão longo prazo +Multi-tenant/multi-corpus · API pública REST+GraphQL · MCP server · Visualização temporal-geo. + +## Entregáveis solicitados +1. codebase-analysis-report +2. tech-debt-assessment +3. product-discovery-brief +4. system-architecture-doc +5. ADRs onde necessário + +Modo: zero-human. NÃO implementar agora — produzir análise + plano em fases priorizadas. + +Outputs em ${PROJECTS_OUTPUT_DIR:-/Users/guto/ufo/.projects-outputs}/disclosure-bureau/businesses/systems-atelier/ diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/HANDOFF.json b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/HANDOFF.json new file mode 100644 index 0000000..84a153b --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/HANDOFF.json @@ -0,0 +1,15 @@ +{ + "schema_version": "1.0", + "decisions": [], + "open_questions": [], + "project_id": "disclosure-bureau", + "business_slug": "systems-atelier", + "phase": "plan", + "brief_original": "Audite e proponha plano de modernização completo do **The Disclosure Bureau** — sistema de investigação sobre documentos declassificados.\n\n# Contexto\n\nThe Disclosure Bureau (https://disclosure.top) recebe documentos (PDF/imagens), extrai conteúdo via vision LLM, converte em markdown estruturado bbox-aware, gera embeddings densos + esparsos, popula grafo de co-menções entre entidades, e expõe uma wiki investigativa + agente de chat (Sherlock). Corpus atual: 116 PDFs do war.gov/ufo (DOD UAP, FBI Vault, NASA Apollo/Skylab, FOIA) → 3.435 páginas → 20.974 chunks → 34.370 entidades → 248.398 menções.\n\n**Repo:** `/Users/guto/ufo` (commit baseline `19d0678`)\n**Live:** https://disclosure.top\n**Instrução canônica:** `/Users/guto/ufo/CLAUDE.md` + `/Users/guto/ufo/CLAUDE-schema-full.md`\n**Memory dev:** `/Users/guto/.claude/projects/-Users-guto-ufo/memory/MEMORY.md`\n\n## Pipeline atual\n1. PDF → PNG 72 DPI (pdftoppm) + OCR (pdftotext)\n2. Chunking agêntico com Sonnet 4.6 via subagents → raw/--subagent/chunks/cNNNN.md (frontmatter YAML + corpo bilíngue EN+PT-BR)\n3. Embedding BGE-M3 (1024d) self-hosted FastAPI; reranker BGE-Reranker-v2-M3\n4. Postgres 15.8 + pgvector 0.8 + pg_trgm + tsvector → hybrid search RRF\n5. Entity mentions materializadas via ILIKE\n6. Next.js 15 (App Router) + Tailwind + Sigma.js/ForceAtlas2\n\n## Stack self-hosted (Docker Compose em VPS)\nNext.js 15 / React 19 · Supabase (GoTrue, PostgREST, Realtime, Storage, Studio, Kong 2.8, Imgproxy, Meilisearch) · Postgres + pgvector · BGE-M3/Reranker (PyTorch CPU) · Traefik TLS · Sigma.js graphology.\n\n## Restrições não-negociáveis\n- PROIBIDO ANTHROPIC_API_KEY. Usar CLAUDE_CODE_OAUTH_TOKEN (Claude Code SDK) OU OPENROUTER_API_KEY (fallback).\n- Bilíngue EN+PT-BR (português brasileiro, UTF-8 com acentos preservados).\n- Identificadores em inglês internacional.\n- Markdown puro como source-of-truth.\n- VPS self-hosted via Docker Compose. Sem SaaS proprietário no data plane.\n\n## Funciona hoje\nHomepage bento 116 docs · Hybrid search 1.3s · Páginas chunk-a-chunk · Crops bbox auto-tighten · Allowlist image_types · Grafo Sigma.js ForceAtlas2 · Entity pages DB-live · Chat Sherlock (signup/login + Kong corrigido) · /admin/stats, /admin/batch, /admin/indexer.\n\n## Gaps críticos\n1. Timeline (/timeline) — eventos extraídos mas todos com placeholder \"_Stub. Will be enriched in Phase 7._\" — usuário furioso com esse \"Phase X\"\n2. Case Bureau (case/case-report.md, hypotheses Tetlock, evidence Locard) — schema definido, pipeline nunca rodou\n3. Dashboards investigativos reais (não admin técnico) — clusters, geografia, atores, padrões temporais\n4. A2UI / AG-UI no chat — streaming tool-calls, artefatos clicáveis, crops bbox inline, multi-turn persistente\n5. ELIMINAR todos os \"Stubs / Will be enriched in Phase X\"\n\n## Gaps médios\nMobile responsivo precário · Sem onboarding · Sem export (PDF/CSV/PNG) · Sem saved views · Reranker 60s · Grafo trava em corpus grande.\n\n## Visão longo prazo\nMulti-tenant/multi-corpus · API pública REST+GraphQL · MCP server · Visualização temporal-geo.\n\n## Entregáveis solicitados\n1. codebase-analysis-report\n2. tech-debt-assessment\n3. product-discovery-brief\n4. system-architecture-doc\n5. ADRs onde necessário\n\nModo: zero-human. NÃO implementar agora — produzir análise + plano em fases priorizadas.\n\nOutputs em ${PROJECTS_OUTPUT_DIR:-/Users/guto/ufo/.projects-outputs}/disclosure-bureau/businesses/systems-atelier/", + "last_task_completed": null, + "next_task_id": null, + "audit_log_path": "audit.jsonl", + "resumption_prompt_hint": "Project just received initial brief. Start at the brief_intake employee for systems-atelier.", + "handoff_timestamp": "2026-05-18T01:48:43.851Z", + "dag_snapshot_path": "dag-state.json" +} \ No newline at end of file diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/audit.jsonl b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/audit.jsonl new file mode 100644 index 0000000..1763ec6 --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/audit.jsonl @@ -0,0 +1 @@ +{"ts":"2026-05-18T01:48:43Z","event":"brief_received","project_id":"disclosure-bureau","business_slug":"systems-atelier","brief_chars":3412} diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/01-codebase-analysis-report.md b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/01-codebase-analysis-report.md new file mode 100644 index 0000000..0c07ac8 --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/01-codebase-analysis-report.md @@ -0,0 +1,306 @@ +--- +title: "Codebase Analysis Report — The Disclosure Bureau" +project: disclosure-bureau +business: systems-atelier +author: sa-principal (Principal Architect) +baseline_commit: 19d0678 +generated_at: 2026-05-17 +schema_version: 0.1.0 +--- + +# 1. Codebase Analysis Report — as-is + +Análise do estado atual do repositório `/Users/guto/ufo` no commit baseline `19d0678`, vivo em . Documento factual: o que existe, em que volume, com que qualidade. Recomendações ficam para os entregáveis 02–05. + +## 1.1 Visão de mil pés + +O Disclosure Bureau é um pipeline de ingestão Markdown-first + Next.js 15 + Supabase self-hosted, com retrieval híbrido BGE-M3 sobre 116 PDFs (3.435 páginas, 20.974 chunks bilingue, 34.370 entidades). É um sistema **brownfield maduro no pipeline de ingestão e raso na camada investigativa**: a base do "Karpathy LLM-wiki" funciona, mas as três promessas de produto (Timeline, Case Bureau, Investigative Dashboards) estão em estado de stub. + +## 1.2 Layout físico + +``` +/Users/guto/ufo/ +├── CLAUDE.md contrato vinculante v0.1.0 (10.5 KB, 24 tipos) +├── CLAUDE-schema-full.md schema canônico (36.7 KB) +├── README.md visão geral pública +├── CORPUS-SNAPSHOT.md snapshot estatístico v0.2.0 +├── raw/ 252 entradas 116 PDFs imutáveis + 116 dirs `*--subagent` com chunks v0.2 +├── processing/ 11 subdirs PNG 72 DPI, OCR, vision JSON, crops, vídeos, frames UAP +├── wiki/ 13 subdirs source-of-truth gerado (documents/, pages/, entities/, images-direct/, tables/, videos/) +├── case/ 7 subdirs Investigation Bureau — TODOS VAZIOS (evidence, hypotheses, witnesses, ...) +├── scripts/ 63 arquivos pipeline numerado 00–34 + ~30 scripts ad-hoc de rebuild +├── infra/ 10 subdirs docker-compose disclosure-stack + embed-service + supabase migrations +└── web/ Next.js 15 / React 19 app +``` + +**Regra de ouro respeitada:** nenhum script escreve em `raw/.pdf` — só em `raw/--subagent/`. + +## 1.3 Quantificação + +### 1.3.1 Frontend Next.js (`/Users/guto/ufo/web/`) + +| Métrica | Valor | +|---|---| +| LOC TypeScript/TSX (sem `node_modules`, `.next`) | **12.072** | +| Rotas API (`route.ts`) | **19** | +| Páginas (`page.tsx`) | **12** | +| Componentes (`components/*.tsx`) | **29** | +| Módulos de biblioteca (`lib/**/*.ts`) | **19** | +| Stack runtime | Next.js 15.1 · React 19 · Tailwind 3.4 · TypeScript 5.6 | +| Dependências de produção | 23 (24 com `@assistant-ui/react` instalado mas **não usado em runtime**) | + +### 1.3.2 Pipeline Python (`/Users/guto/ufo/scripts/`) + +| Métrica | Valor | +|---|---| +| LOC Python/Shell/JS | **25.298** | +| Scripts numerados canônicos | 34 (`00-…` a `34-…`) | +| Scripts ad-hoc de "rebuild doc X" | 28+ (`rebuild_doc65_*`, `rebuild_doc_d48.py`, etc.) — sinal de pipeline reativo | +| Documentos processados pelo orquestrador `28-batch-rebuild-all.py` | 116/116 | +| Chunks `c*.md` no disco | **20.974** | +| Custo cumulativo declarado (`CORPUS-SNAPSHOT.md`) | ~$409 USD | + +### 1.3.3 Infraestrutura (`/Users/guto/ufo/infra/`) + +| Componente | Versão | Status | +|---|---|---| +| `db` (Supabase Postgres) | 15.8.1.060 | self-hosted, pgvector + pg_trgm + unaccent | +| `auth` (GoTrue) | v2.170.0 | `MAILER_AUTOCONFIRM=true` (SMTP não configurado) | +| `rest` (PostgREST) | v12.2.8 | OK | +| `realtime` (Supabase) | v2.30.34 | OK | +| `storage` (storage-api) | v1.14.3 | OK | +| `imgproxy` | v3.8.0 | thumbnail/crop server | +| `meta` (postgres-meta) | v0.83.2 | OK | +| `studio` | 20241202 | admin UI | +| `kong` | 2.8.1 | API gateway (corrigido para chat) | +| `meilisearch` | v1.10 | provisionado, **não cabeado ainda ao web** | +| `embed` (FastAPI) | local | BGE-M3 + BGE-Reranker-v2-M3 CPU | +| `web` | Next 15.1 | Dockerfile próprio | +| `traefik-public` | externo | reverse proxy TLS | + +11 serviços + 1 rede externa + 4 volumes. Compose tem **367 linhas**. + +### 1.3.4 Banco de dados — schema observado + +Migrations em `/Users/guto/ufo/infra/supabase/migrations/`: + +| Migration | Tabelas/funções | +|---|---| +| `0001_chat_schema.sql` (221 linhas) | `profiles`, `chat_sessions`, `messages`, `usage_events` + trigger `handle_new_user` | +| `0002_chunks_retrieval.sql` (253 linhas) | `documents`, `chunks` (50 colunas, 8 índices, HNSW vector + 2 GIN tsvector + 2 GIN trgm), `entities`, `entity_mentions`, função `hybrid_search_chunks` (RRF server-side) | + +**8 tabelas públicas + 1 RPC RRF**. RLS habilitada em todas as tabelas de retrieval (`SELECT` público, escrita só via `service_role`). + +### 1.3.5 Corpus — números + +| Indicador | Valor | +|---|---| +| PDFs originais (`raw/*.pdf`) | **116** | +| Páginas (chunks `wiki/pages//p*.md`) | **3.435** | +| Diretórios `raw/--subagent/` | 116 (100% cobertura) | +| Chunks bilíngues `c*.md` | **20.974** | +| Imagens cropadas (`raw/*/images/`) | 752 | +| Entidades únicas (`wiki/entities/**/*.md`) | **34.370** | +| ↳ pessoas | 6.297 | +| ↳ organizações | 3.747 | +| ↳ locais | 4.969 | +| ↳ eventos | **7.091** | +| ↳ uap-objects | 1.775 | +| ↳ veículos | 2.174 | +| ↳ operações | 2.723 | +| ↳ conceitos | 5.594 | +| UFO anomaly chunks | 3.020 (14.4%) | +| Cryptid anomaly chunks | 21 (0.1%) | + +### 1.3.6 Stubs — o problema que enfurece o usuário + +| Métrica | Valor | +|---|---| +| Arquivos `wiki/` com `Will be enriched in Phase X` | **22.096** | +| Eventos com `narrative_summary: _Stub.` | **7.090 / 7.091 (99.99%)** | +| Eventos com summary real | **1** | +| Cobertura agregada de entidades com summary não-stub | ~36% | + +A timeline em `/timeline` lê esses 7.091 eventos via `/api/timeline/route.ts` e os ordena por `date_start`. **O eixo do tempo funciona; o conteúdo não existe.** Cada card mostra `_Stub. Will be enriched in Phase 7._` — exatamente o que o usuário denuncia. + +## 1.4 Arquitetura observada (não a alvo) + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ raw/*.pdf (imutável) │ +└──────────────────────┬─────────────────────────────────────────────────┘ + pdftoppm 72dpi │ pdftotext -layout + ▼ +┌────────────────────────────────────────────────────────────────────────┐ +│ processing/{png,ocr} → scripts/02-vision-page.py (Haiku 4.5) │ +│ → scripts/26-chunk-harness.py (Sonnet 4.6) │ +│ → scripts/28-batch-rebuild-all.py (orchestr.) │ +└──────────────────────┬─────────────────────────────────────────────────┘ + ▼ +┌────────────────────────────────────────────────────────────────────────┐ +│ raw/--subagent/{chunks,images,tables}/ │ +│ wiki/{documents,pages,entities,images-direct}/ (markdown SoT) │ +│ case/{evidence,witnesses,hypotheses,…}/ ← VAZIO │ +└──────────────────────┬─────────────────────────────────────────────────┘ + scripts/30 (BGE-M3 1024d) + scripts/31 (entity_mentions ILIKE) + ▼ +┌────────────────────────────────────────────────────────────────────────┐ +│ Postgres 15.8 + pgvector + pg_trgm + tsvector (pt_unaccent/en_unaccent)│ +│ public.hybrid_search_chunks(text, vec, lang, …) → RRF top-k │ +└──────────────────────┬─────────────────────────────────────────────────┘ + ▼ Stage-2 rerank (BGE-Reranker via FastAPI :8000) +┌────────────────────────────────────────────────────────────────────────┐ +│ Next.js 15 App Router │ +│ ├─ Server: lib/wiki.ts (fs MD) + lib/retrieval/hybrid.ts (pg+embed) │ +│ ├─ Chat: lib/chat/openrouter.ts (Pattern C SSE + tool loop) + │ +│ │ lib/chat/claude-code.ts (OAuth subprocess, no tools) │ +│ └─ Auth: middleware.ts (Supabase SSR) + Kong gateway │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.4.1 Camadas de retrieval + +Três camadas independentes coexistem e **não são unificadas**: + +1. **Filesystem markdown** (`lib/wiki.ts`, 182 LOC) — leitura de `wiki/*.md` via `gray-matter`. Caminho rápido para document, page, entity. Usada pelo SSR das páginas e pelos tools `read_page`, `read_document`, `read_entity`. +2. **Postgres hybrid** (`lib/retrieval/hybrid.ts`, 170 LOC) — RPC `hybrid_search_chunks` + rerank cliente. Usada pelo tool `hybrid_search` no chat e por `/api/search/hybrid`. +3. **Graph** (`lib/retrieval/graph.ts`, 204 LOC) — `entity_mentions` materializada por scripts/31 via ILIKE. Usada pelos tools `entity_neighbors`, `entity_path`, `co_mention_chunks`. + +Não há cache compartilhado entre elas. O grafo Sigma.js carrega o `wiki/graph.json` de **9.2 MB** estático — trava em corpus grande (confirmado pelo gap "Grafo trava em corpus grande" no brief). + +### 1.4.2 Chat — Pattern C ad-hoc + +`web/lib/chat/` (1.922 LOC) implementa um "AG-UI-style" próprio (`agui.ts`, 65 LOC), não o protocolo AG-UI oficial. Eventos SSE: `text_delta`, `tool_start`, `tool_result`, `navigate`, `done`, `error`. O frontend (`chat-bubble.tsx`, 507 LOC) consome SSE manualmente. **Sem retomada de stream, sem persistência intermediária, sem cancelamento limpo.** + +Provedores: + +- `openrouter.ts` (341 LOC) — default. Modelos free DeepSeek v4 / Nemotron. Loop de até 5 turnos de tool-calling client-side. +- `claude-code.ts` (105 LOC) — `spawn("claude", ["-p", "--output-format", "json", …])` em subprocess, sem tools, timeout 90s. Lê `CLAUDE_CODE_OAUTH_TOKEN` do env. **Ainda não tem streaming nem tool calling.** + +12 tools registradas em `tools.ts` (663 LOC), cobrindo retrieval, leitura, navegação. Sólido em escopo, mas o handler `search_corpus` ainda enumera `wiki/entities/*` por `fs.readdir` em loop — barato para 34k arquivos hoje, **não escala** para multi-tenant. + +### 1.4.3 Auth & multi-tenancy + +- GoTrue self-hosted com `GOTRUE_MAILER_AUTOCONFIRM=true` (SMTP não conectado). **Em produção sem SMTP, qualquer e-mail vira conta ativa imediatamente.** Risco aceito mas não trackeado. +- `chat_sessions.user_id` referencia `auth.users(id)`. Sem `tenant_id`, sem `corpus_id`. **Multi-tenant = refactor de schema.** +- RLS em `chunks`/`entities` é `SELECT … USING (TRUE)` — corpus público por design. + +## 1.5 Qualidade do código + +### 1.5.1 Pontos fortes + +- **Comentários cabeçalho ricos em todos os módulos relevantes** (`web/lib/chat/openrouter.ts`, `web/lib/retrieval/hybrid.ts`, scripts numerados). Sinal de cuidado de design. +- **Tipagem TS estrita** com interfaces explícitas em todas as fronteiras (`ChunkHit`, `ToolDefinition`, `AGUIEvent`, …). +- **Idempotência declarada** em scripts críticos (`30-index-chunks-to-db.py --skip-existing`). +- **Bilíngue por construção** desde o vision pass (campo `content_pt`/`content_en` espelhado em todo lugar; tsvector próprio para cada idioma). +- **`CLAUDE.md` + `CLAUDE-schema-full.md` formam um contrato vinculante** auto-suficiente — Fowler aprovaria. Schema das 24 entidades, regex de IDs, naming canônico, idempotência: tudo escrito. +- **Configuração de busca textual cuidadosa**: `pt_unaccent` e `en_unaccent` com `unaccent + portuguese_stem` — não é "ts_default" preguiçoso. +- **`outputFileTracingIncludes` em `next.config.ts`** garante o bundle Docker carregar `wiki/`, `processing/`, `raw/*--subagent/`. Detalhe operacional não-trivial resolvido. + +### 1.5.2 Pontos fracos + +- **Zero testes automatizados.** `find /Users/guto/ufo -name "*.test.*" -o -name "*_test.py"` retorna 0 hits (excluindo `node_modules`). Sem regression suite, refactors são apostas. +- **Sprawl de scripts ad-hoc**: 28+ arquivos `rebuild_doc*.py` no diretório `scripts/`. O ideal seria um único `rebuild.py --doc --phase `. Sinal de pipeline que era para ser robusto mas teve que ser remendado por documento. +- **Acoplamento direto ao filesystem do host**: `UFO_ROOT = process.env.UFO_ROOT ?? "/Users/guto/ufo"` espalhado em `lib/wiki.ts`. O Docker mitiga via volume mount mas o código carrega a premissa de monorepo. +- **`@assistant-ui/react` no `package.json` mas não importado em runtime** (`grep -r "assistant-ui" /Users/guto/ufo/web/{app,components,lib}` não retorna nada relevante). Dependência fantasma. +- **`scripts/03-dedup-entities.py` é o vilão dos stubs.** Linhas 167–169 escrevem literalmente `narrative_summary: _Stub. Will be enriched in Phase 7._` em todo entity sintetizada. Phase 7 nunca foi implementada como passe sistemático. +- **Grafo client-side custoso**: `wiki/graph.json` = 9.2 MB carregado inteiro no browser pelo Sigma.js. Sem viewport-aware lazy load. +- **Reranker 60s** (gap declarado no brief) — `signal: AbortSignal.timeout(60_000)` em `lib/retrieval/embed.ts` é um workaround, não solução. BGE-Reranker-v2-M3 CPU em VPS pequena tem cold start de ~30s + 200-500ms por par query/doc. +- **Sem migrations versionadas para o schema da wiki** — a "evolução" do schema dos 24 tipos é manual via `CLAUDE-schema-full.md`. Lint é manual em `scripts/04-lint.py`. + +### 1.5.3 Hotspots de complexidade + +| Arquivo | LOC | Justificativa | +|---|---|---| +| `web/lib/chat/tools.ts` | **663** | 12 tools + handlers + schemas inline. Candidato número 1 a quebra modular. | +| `web/components/chat-bubble.tsx` | **507** | UI + state + auth + SSE parser + tool rendering em um arquivo. | +| `web/lib/chat/openrouter.ts` | **341** | Loop de tool-call client-side; SSE parser manual de 60+ linhas. | +| `scripts/28-batch-rebuild-all.py` | ~700 | Orquestrador `ThreadPoolExecutor` com retries e quota; razoável dado o domínio. | +| `infra/disclosure-stack/docker-compose.yml` | **367** | 11 serviços; longo mas plano. | + +## 1.6 Dependências externas + +### 1.6.1 Frontend (`web/package.json`) + +- **Hard deps**: `next@^15.1`, `react@^19`, `@supabase/ssr@^0.10`, `@supabase/supabase-js@^2.105`, `pg@^8.13`, `gray-matter@^4.0`, `sharp@^0.33`, `react-markdown@^9`, `remark-wiki-link@^2`. +- **Graph stack**: `sigma@^3`, `graphology@^0.25`, `graphology-layout-forceatlas2@^0.10`, `@react-sigma/core@^5`, `@react-sigma/layout-forceatlas2@^5`, `react-force-graph-2d@^1.27` — **duas libs de grafo coexistem** (sigma e react-force-graph). Decisão pendente. +- **UI**: `@radix-ui/react-dialog@^1.1`, `@radix-ui/react-tooltip@^1.1`, `lucide-react@^0.460`, `framer-motion@^11.11`. +- **Fantasma**: `@assistant-ui/react@^0.14` declarado e não usado. + +### 1.6.2 Backend ML + +- **embed-service** (`infra/embed-service/`, 148 LOC FastAPI): `FlagEmbedding` (BGE-M3 + BGE-Reranker-v2-M3). CPU-only via env `DEVICE=cpu`. Lazy load com `Lock` para thread safety. ~2.5 GB RAM resident. +- Sem GPU declarada. Reranker quente reusa o modelo; cold start carrega ambos os modelos em sequência. + +### 1.6.3 LLM providers + +- **`OPENROUTER_API_KEY`** — chat default (DeepSeek v4 free, Nemotron 3 fallback). +- **`CLAUDE_CODE_OAUTH_TOKEN`** — Claude Code CLI subprocess para scripts Python (vision, chunking, synthesis) **e** para o chat em modo `claude-code`. +- **ANTHROPIC_API_KEY proibido** — verificado: o único hit em código vivo é uma string de aviso "NEVER use ANTHROPIC_API_KEY" em comments. Há referência stale em `infra/coolify/NEXTJS.md:30` que precisa ser removida (documentação antiga). + +### 1.6.4 OS-level + +- `pdftoppm` (Poppler) e `pdftotext -layout` — ambos invocados por `01-convert-pdfs.sh`. +- `claude` CLI no PATH do container — instalação via curl no Dockerfile do web (atualmente **não está no Dockerfile do `web/Dockerfile`** — o `claude-code.ts` falha silenciosamente em container sem o binário; a fallback para OpenRouter cobre, mas a Pattern C com Claude oficial nunca rodou). + +## 1.7 Operacional + +- **Deploy:** Docker Compose. Roteamento via `traefik-public` externo (network já existente do host). `DEPLOY-CHECKLIST.md` em `infra/` mantém runbook. +- **Observabilidade:** `wiki/log.md` (459 KB, append-only) é o único log estruturado da pipeline de ingestão. `raw/_batch-rebuild/progress.jsonl` é lido pela rota `/api/admin/batch`. **Sem Prometheus/Grafana, sem error tracking (Sentry), sem trace distribuído.** +- **Backups:** não declarados. Volume `db-data` do Postgres é o único cofre — sem dump automatizado documentado. +- **CI/CD:** ausente. Push direto via `git`; build local. Não existe `.github/workflows`. +- **Secret hygiene:** `.env.backup.1778796747` em `infra/disclosure-stack/` contém credenciais reais (OAuth, OpenRouter, JWT). **Precisa sair do git-history.** + +## 1.8 Documentação + +- `CLAUDE.md` (10.5 KB) — contrato canônico, atualizado e usado. +- `CLAUDE-schema-full.md` (36.7 KB) — schema dos 24 tipos. Vivo. +- `README.md` (6.3 KB) — visão geral do produto, quickstart, stack. Vivo. +- `CORPUS-SNAPSHOT.md` (3.6 KB) — última snapshot da pipeline. Vivo (gerado 2026-05-17). +- `infra/DEPLOY-CHECKLIST.md` (6.8 KB) — runbook. +- `infra/RETRIEVAL.md` (3.6 KB) — explicação do retrieval híbrido. +- `wiki/log.md` (459 KB) — log append-only de operações de pipeline. Inflado. + +A documentação é desproporcionalmente forte do lado **filosofia/contrato** e fraca do lado **ADR/decisões técnicas históricas**. Não há um ADR único no repo. + +## 1.9 Síntese dos hotspots + +| Hotspot | Localização | Impacto | +|---|---|---| +| **Timeline + 7.091 stubs** | `scripts/03-dedup-entities.py:167-169`, `wiki/entities/events/*.md` | UX bloqueada (gap #1 do brief) | +| **Case Bureau vazio** | `case/{evidence,hypotheses,witnesses,timelines,profiles,connect-the-dots}/` (0 arquivos) | A promessa investigativa do produto não existe (gap #2) | +| **Dashboards = admin técnico** | `web/app/admin/stats/` (272 LOC) | Não há dashboard investigativo (gap #3) | +| **Chat sem AG-UI real** | `web/lib/chat/agui.ts` (65 LOC) | "AG-UI style" caseiro, sem retomada, sem artefato tipado (gap #4) | +| **Sigma.js graph.json 9.2 MB** | `wiki/graph.json` | Trava no browser (gap #5 médio) | +| **Reranker cold start** | `infra/embed-service/app.py` (lazy load) + `web/lib/retrieval/embed.ts` timeout 60s | UX search lenta em primeira query | +| **Stubs em 22.096 arquivos** | `wiki/entities/**/*.md` | Erosão de confiança total | +| **Sem testes** | repo todo | Risco de regressão a cada refactor | +| **Sprawl `rebuild_doc*.py`** | `scripts/rebuild_doc*.py` (28+) | Manutenibilidade do pipeline | +| **`@assistant-ui/react` fantasma** | `web/package.json` | Decisão técnica adiada | +| **`.env.backup.*` versionada** | `infra/disclosure-stack/.env.backup.1778796747` | Secret leak | + +## 1.10 O que está bom e deve ser preservado + +1. **Markdown como source-of-truth** com IDs canônicos regex-validados. O contrato `CLAUDE.md` é uma alavanca de longo prazo (Larson). +2. **Bilíngue desde o vision pass** — não é tradução tardia, é produto. +3. **Hybrid search RRF em SQL pure**: `public.hybrid_search_chunks` evita orquestração de duas queries no app — boa decisão. +4. **OpenRouter + Claude Code OAuth como única política de LLM** — alinhado à restrição inegociável do projeto. +5. **Self-hosted no data plane** (Postgres, pgvector, BGE-M3 local, GoTrue, PostgREST, Storage). Sem SaaS no caminho dos dados. +6. **Bbox-aware chunking**: cada chunk carrega `bbox` JSONB → permite citações com crop visual. + +## 1.11 O que sangra agora e precisa de bisturi + +Em ordem de impacto sobre o usuário (não por dificuldade): + +1. **Fase 0 — Eliminar todos os stubs "Phase X"** (entrega visível em 24h, dor zero técnica). +2. **Timeline real** — pipeline de enrichment ou de filtro: "exibe só eventos com narrative_summary não-stub". Subentendido na fase 0 mas com produto próprio. +3. **Case Bureau pipeline ground-zero** — hypotheses, evidence, witnesses ainda vazios; isso é a alma do produto. +4. **AG-UI verdadeiro no chat** — substituir `agui.ts` caseiro pelo protocolo AG-UI ou por `assistant-ui/react` real (já no `package.json`). +5. **Dashboards investigativos** — clusters, geografia, atores, padrões temporais. Não admin técnico. +6. **Grafo `graph.json` 9.2 MB** — paginar / lazy ou trocar Sigma por Cytoscape com server-side hub-and-spoke. + +Os detalhes de priorização, esforço e fases vão no entregável **04-system-architecture-doc.md**. Os trade-offs vão nos ADRs em **05-adrs/**. + +--- + +_Análise feita em commit `19d0678` baseline. Sem mudanças no código._ diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/02-tech-debt-assessment.md b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/02-tech-debt-assessment.md new file mode 100644 index 0000000..30e0c18 --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/02-tech-debt-assessment.md @@ -0,0 +1,391 @@ +--- +title: "Tech Debt Assessment — The Disclosure Bureau" +project: disclosure-bureau +business: systems-atelier +author: sa-principal (Principal Architect) +baseline_commit: 19d0678 +generated_at: 2026-05-17 +schema_version: 0.1.0 +--- + +# 2. Tech Debt Assessment — priorizado + +Cada débito carrega: **severidade** (`high` / `med` / `low`), **categoria** (UX | Pipeline | Arch | Sec | Ops | DevEx), **localização concreta no repo**, **esforço estimado em homem-dias** (`d`) por engenheiro sênior, e **bloqueio percebido** (o que ele impede). + +A precificação é baseada na escala observada (116 docs, 20.974 chunks, 34.370 entidades) e na ausência de testes — qualquer refactor exige escrever o teste antes. + +## 2.1 Sumário + +| # | Severidade | Débito | Esforço | +|---|---|---|---| +| 1 | **HIGH** | Stubs "Phase X" em 22.096 arquivos de wiki | 3d (saneamento) + 8d (enrichment real, opcional) | +| 2 | **HIGH** | Case Bureau (Tetlock+Locard) nunca rodou | 12d | +| 3 | **HIGH** | Timeline mostra 7.090 eventos com `_Stub. Will be enriched in Phase 7._` | 4d (filtra+narrar) ou rola na fase 0 | +| 4 | **HIGH** | Chat "AG-UI style" caseiro, sem padrão | 5d (adotar AG-UI ou assistant-ui) | +| 5 | **HIGH** | Dashboards investigativos inexistentes (só /admin/stats técnico) | 8d | +| 6 | **HIGH** | Zero testes automatizados | 4d (smoke + retrieval golden set) + ongoing | +| 7 | **MED** | Reranker cold start ~30s + 60s timeout client-side | 3d (warm-pool + queue) | +| 8 | **MED** | Sigma.js consome `graph.json` 9.2 MB inteiro | 4d (server-side neighbors) | +| 9 | **MED** | `entity_mentions` materializada por ILIKE (pode duplicar/perder) | 3d | +| 10 | **MED** | 28+ scripts `rebuild_doc*.py` ad-hoc | 4d (unificar em `rebuild.py --doc`) | +| 11 | **MED** | GoTrue com `MAILER_AUTOCONFIRM=true` sem SMTP | 1d (configurar SMTP) ou 0.5d (gate por allowlist temporária) | +| 12 | **MED** | `.env.backup.1778796747` com credenciais reais commitada | 0.5d + rotação dos segredos | +| 13 | **MED** | `@assistant-ui/react` no `package.json` e não usado | 1d (decisão + remoção ou adoção) | +| 14 | **MED** | Schema sem `tenant_id`/`corpus_id` (multi-tenant futuro) | 4d (schema) + 6d (UI/refactor) | +| 15 | **LOW** | `wiki/log.md` 459 KB monolítico | 1d (rotacionar) | +| 16 | **LOW** | Dois libs de grafo coexistem (sigma + react-force-graph) | 1d (escolher uma, remover outra) | +| 17 | **LOW** | `outputFileTracingIncludes` cobre paths absolutos do dev | 1d (consolidar via env) | +| 18 | **LOW** | Falta CI/CD | 2d (GitHub Actions: lint + build + smoke) | +| 19 | **LOW** | Sem observabilidade (logs, metrics, error tracking) | 3d (Loki + Grafana + Sentry self-hosted) | +| 20 | **LOW** | Documentação ADR ausente | coberto neste deliverable + onging | + +**Total estimado:** ~75 homem-dias para zerar o débito high/med a profundidade média. Quanto disso fica em fases vai no entregável 04. + +--- + +## 2.2 Detalhamento + +### 2.2.1 [HIGH] Stubs "Phase X" — erosão de confiança total + +**Fonte do mal:** `scripts/03-dedup-entities.py`, linhas 167-169, escreve literalmente: + +```python +"narrative_summary": "_Stub. Will be enriched in Phase 7._", +"narrative_summary_pt_br": "_Stub. Será enriquecido na Fase 7._", +"_Stub generated by entity dedup. Will be enriched in Phase 6._\n\n" +``` + +**Distribuição atual:** + +- 22.096 arquivos em `wiki/entities/**/*.md` carregam "Will be enriched" +- 7.090 / 7.091 events com `narrative_summary: _Stub.` +- ~64% das 34.370 entidades sem narrativa real + +**Por que fere o usuário:** o produto promete "wiki investigativa". O leitor entra em qualquer entidade não-trivial (`/e/event/EV-1492-…`) e lê _"Será enriquecido na Fase 7"_. A mensagem comunica: **isto é alpha incompleto**. O usuário declarou furor explícito no brief. + +**Esforço:** + +- **Saneamento básico — 3d.** Reescrever `03-dedup-entities.py` para nunca emitir stub textual; entidades sem narrativa exibem badge "sem síntese ainda" no UI e ficam fora dos listings públicos por default. Rodar um script `scripts/35-strip-stubs.py` que substitui o texto pelo campo vazio + booleano `needs_synthesis: true`. +- **Enrichment real — 8d (opcional, pode ser fila contínua).** Pipeline `scripts/36-enrich-entities.py` que itera entidades com `total_mentions ≥ N`, recupera chunks via `entity_mentions`, sintetiza com Claude Code OAuth, escreve `narrative_summary` real. Custo: ~$80-150 dependendo do limiar. + +**Categoria:** UX + Pipeline. **Bloqueia:** Timeline, Entity pages, Case Bureau. + +--- + +### 2.2.2 [HIGH] Case Bureau — schema definido, pipeline jamais rodou + +**Estado físico:** + +``` +case/ +├── connect-the-dots/ 0 files +├── evidence/ 0 files +├── gaps/ (only 2 placeholder gaps from initial bootstrap) +├── hypotheses/ 0 files +├── profiles/ 0 files +├── timelines/ 0 files +└── witnesses/ 0 files +``` + +**O que o CLAUDE.md promete:** 12 tipos investigativos (evidence, witness, hypothesis, gap, relation, timeline, actor_profile, case_report, residual_uncertainty, …) com chain-of-custody Locard, hypothesis tournament Tetlock, ≥3 hipóteses por caso, posteriores bayesianos, 6 quality rubrics threshold 0.85. + +**O que existe:** nada. Os 8 detetives (Holmes/Poirot/Dupin/Locard + Schneier/Tetlock/Taleb) são copy do README; não há agente rodando, não há output. + +**Esforço — 12d quebrado em 3 etapas:** + +1. **3d — Define-and-seed.** Escrever `scripts/40-case-seed.py` que cria 5 casos âncora (Roswell, Nimitz Tic-Tac, Phoenix Lights, FBI Vault Hottel memo, NASA Apollo radio anomaly). Cada caso = 1 timeline, 3-5 evidence cards com `verbatim_excerpt` + `source_page` + `bbox`, 2-3 witness_analysis com `verdict`, ≥3 hipóteses concorrentes com priors Tetlock. +2. **4d — Hypothesis engine.** `scripts/41-hypothesis-tournament.py` lê evidências, gera 3+ hipóteses via LLM, calcula posterior via Bayes simples (prior × likelihood), ranqueia, escreve `case/hypotheses/H-NNNN.md` + atualiza `case-report.md`. Modelo: Claude Code OAuth, Sonnet para case-writer / red-team review. +3. **5d — UI surface.** Novas rotas Next.js: `/case`, `/case/[id]`, `/case/[id]/hypotheses`, `/case/[id]/evidence`. Componentes: `CaseTimelineLane`, `HypothesisTournamentCard`, `EvidenceChainCustody`, `WitnessVerdictBadge`. Linka para `[[evidence/E-0042]]` resolvido pelo router. + +**Categoria:** Arch + UX + Pipeline. **Bloqueia:** posicionamento como "super centro de investigação" (visão do brief). + +--- + +### 2.2.3 [HIGH] Timeline — funciona mas mostra 99.99% de stubs + +**Localização:** + +- `web/app/api/timeline/route.ts` (119 LOC) — lê `wiki/entities/events/*.md`, ordena por `date_start`. Funciona. +- `web/components/timeline-view.tsx` (143 LOC) — agrupa por década, lista cards. Funciona. +- **Dados:** 7.090 dos 7.091 events com `narrative_summary: _Stub.`. + +**O bug não é a Timeline; é o conteúdo.** Mas o usuário vê a Timeline. Daí ela vira o sintoma. + +**Esforço — 4d (curto-prazo) ou rola junto com 2.2.1:** + +1. **1d** — Filtro server-side em `/api/timeline`: por default só devolve eventos com `narrative_summary` ausente do regex `/^_Stub/`. Query param `?include_stubs=true` para o caso real (ainda 1 evento; vai aumentar conforme enrichment roda). +2. **1d** — UI: separar "Eventos consolidados" (com narrativa) e "Eventos catalogados" (count agregado por década) em duas faixas da Timeline. +3. **2d** — Camada de canonização: 50-100 eventos curados manualmente (Roswell, Nimitz, Phoenix, Aurora, Foo-fighters, Operation Mainbrace, AATIP, etc.) com narrativa rica escrita pelo case-writer. Esses ficam pinned no topo de cada década. + +**Categoria:** UX. **Bloqueia:** primeira impressão da feature. + +--- + +### 2.2.4 [HIGH] Chat — "AG-UI style" caseiro + +**Localização:** + +- `web/lib/chat/agui.ts` (65 LOC) — eventos `text_delta`, `tool_start`, `tool_result`, `navigate`, `done`, `error`. +- `web/lib/chat/openrouter.ts` (341 LOC) — SSE parser + loop de tool-call manual. +- `web/components/chat-bubble.tsx` (507 LOC) — UI consumindo SSE manualmente. + +**Limites:** + +- Sem retomada de stream em queda de conexão. +- Sem persistência intermediária — se o usuário fecha o modal no meio, o assistant message é perdido. +- Sem cancelamento limpo (abort do `fetch` não chega ao backend, que segue executando tools). +- Sem artefatos tipados — citações são strings markdown parseadas no client. +- Sem crops bbox inline no chat (citação aponta para `/d/#` mas o crop só renderiza fora do chat). + +**Esforço — 5d:** + +1. **2d** — Decidir entre adotar [AG-UI](https://github.com/ag-ui-protocol/ag-ui) puro (mais alinhado ao trace "/state/run" do protocolo) **ou** adotar `@assistant-ui/react` (já no `package.json`, ainda não usado). ADR-001 cobre. +2. **2d** — Refactor `chat-bubble.tsx` → componentes pequenos (`ChatComposer`, `ChatStream`, `ToolBlock`, `CitationCrop`, `NavigationOffer`). Cada citação `chunk_id` vira `` com bbox crop inline via `/api/crop`. +3. **1d** — Persistência incremental: cada `text_delta` flush no `messages.content` via Realtime, não ao final. Aborto via `AbortController` chega ao loop server-side via header `x-chat-cancel: `. + +**Categoria:** UX + Arch. **Bloqueia:** experiência conversacional sólida. + +--- + +### 2.2.5 [HIGH] Dashboards investigativos inexistentes + +**Estado:** existe `/admin/stats` que mostra contadores técnicos (docs/chunks/entities/redactions). Não existe **dashboard investigativo**. + +**Falta:** + +- **Clusters** de eventos por proximidade temporal+geográfica (KMeans sobre `date_start` + `primary_location` geocoded). +- **Mapa**: choropleth/scatter geográfico de sightings (precisa geocode em `primary_location`). +- **Atores recorrentes**: top-50 pessoas/organizações por menções, com sparkline temporal. +- **Padrões**: redaction-heavy ratio por collection; UFO anomaly heatmap por ano; cryptid hotspots. +- **Connect-the-dots view**: para um caso aberto, qual o subgrafo mínimo de entidades conectadas. + +**Esforço — 8d:** + +1. **2d** — Geocoding pipeline (`scripts/42-geocode-locations.py`) usando Nominatim self-host (Docker) — sem SaaS. Persistir lat/lon em `entities` via colunas `lat`, `lon`, `geo_confidence`. +2. **3d** — Rotas e componentes Next: `/dashboard/clusters`, `/dashboard/map`, `/dashboard/actors`, `/dashboard/patterns`. Lib: `react-simple-maps` ou MapLibre GL (open-source, self-hosted tiles). +3. **3d** — Conteúdo: queries Postgres dedicadas (views materializadas atualizadas em incremental), componentes de gráfico (Recharts ou Visx). Linka tudo de volta para entities/chunks. + +**Categoria:** UX + Arch. **Bloqueia:** posicionamento "super centro de investigação". + +--- + +### 2.2.6 [HIGH] Zero testes automatizados + +`find /Users/guto/ufo -name "*.test.*" -o -name "*_test.py" -o -name "*.spec.*"` → 0 hits (excluindo `node_modules`). + +**Risco:** + +- Refactors em `scripts/03-dedup-entities.py` (vilão dos stubs) podem corromper 22k arquivos. +- Mudança no schema do chat (`agui.ts`) sem regression test quebra o SSE no browser. +- Migration `0003_*` futura sem teste pode dropar coluna usada por tool handler. + +**Esforço — 4d para um piso mínimo:** + +1. **1d** — Vitest no `web/`, smoke de cada `route.ts` (`/api/timeline`, `/api/search/hybrid`, `/api/chunk`, …). Fixture: subset de 3 docs com 10 chunks. +2. **1d** — Golden retrieval set: 30 queries PT+EN com top-5 chunks esperados; CI valida recall@5 ≥ 0.7. +3. **1d** — pytest para scripts críticos (`03-dedup-entities`, `30-index-chunks-to-db`, `04-lint`). Dataset: 1 PDF de 3 páginas com fixture YAML pronta. +4. **1d** — `.github/workflows/ci.yml`: `pnpm lint && pnpm build && pytest && curl /api/admin/stats`. + +**Categoria:** DevEx + Quality. **Bloqueia:** velocidade de mudança segura. + +--- + +### 2.2.7 [MED] Reranker cold start + timeout 60s + +**Localização:** `web/lib/retrieval/embed.ts:16` (`signal: AbortSignal.timeout(60_000)`). `infra/embed-service/app.py` faz lazy load com `Lock`; primeira chamada após reinício baixa ~2.5 GB de modelo. + +**Sintoma:** primeira query do dia leva 30-60s. Brief lista isso como gap médio. + +**Esforço — 3d:** + +1. **1d** — Warm-pool: container do embed-service ganha um `healthcheck` que dispara `/embed` com `["warmup"]` no startup. `restart: unless-stopped` mantém quente. +2. **1d** — Mover rerank para fila assíncrona com SSE event `rerank_progress` (`text_delta` no chat enquanto roda). Pré-render de top-20 RRF, rerank atualiza inline. +3. **1d** — Cache de rerank: hash `(query_norm, candidate_ids)` → score, TTL 24h. Postgres tabela `rerank_cache(hash TEXT PK, scores REAL[], created_at)`. + +**Categoria:** Performance. **Bloqueia:** UX de busca. + +--- + +### 2.2.8 [MED] Sigma.js `graph.json` 9.2 MB + +**Localização:** `wiki/graph.json` (9.2 MB), carregado por `web/components/sigma-graph-client.tsx` e `force-graph-canvas.tsx` no cliente. + +**Esforço — 4d:** + +1. **2d** — Endpoint server-side `/api/graph/neighbors?node=&depth=1&limit=50`: BFS no `entity_mentions` retorna só vizinhança da query, ~30 KB. +2. **1d** — Carregar inicial: hub-and-spoke dos 100 entities com `total_mentions > N` (≤200 KB), expand-on-click busca vizinhos. +3. **1d** — Decidir uma lib: ADR-006 (sigma vs react-force-graph). Remover a outra. + +**Categoria:** Performance + DevEx. **Bloqueia:** /graph escalar para mais corpora. + +--- + +### 2.2.9 [MED] `entity_mentions` via ILIKE + +**Localização:** `scripts/31-populate-entity-mentions.py`. A materialização do grafo usa ILIKE de `canonical_name` e `aliases` contra `content_en/pt` — frágil para nomes ambíguos ("Hoover", "Park") e perde menções com variações ortográficas. + +**Esforço — 3d:** + +1. **1d** — Substituir ILIKE por `tsvector` match com `pt_unaccent/en_unaccent`, ranqueado. +2. **1d** — Adicionar coluna `mention_confidence REAL` baseado em (alias match exato vs canonical match vs fuzzy via pg_trgm `%`). +3. **1d** — Pipeline diff: nova execução compara contagens com versão anterior e reporta variações > 10% como red flag. + +**Categoria:** Pipeline + Quality. **Bloqueia:** grafo confiável. + +--- + +### 2.2.10 [MED] Sprawl `rebuild_doc*.py` + +**Lista parcial:** `rebuild_doc65.py`, `rebuild_doc65_v2.py`, `rebuild_doc65_full.py`, `rebuild_doc65_gemini.py`, `rebuild_doc65_page_rebuilder.py`, `rebuild_doc65_s2_v2.py`, `rebuild_doc65_s8.py`, `rebuild_doc65_section2..8.py`, `rebuild_doc65_serial130_resume.py`, `rebuild_doc65_suba_final.py`, `rebuild_doc_d48.py`, `rebuild_doc_section3.py`, `rebuild_doc255.py`, `rebuild_doc38.py`, … 28+ arquivos. + +**Esforço — 4d:** + +1. **2d** — Consolidar em `scripts/rebuild.py --doc --phase ` com flags por seção. Parâmetros que hoje viram nomes de arquivo viram args. +2. **1d** — Mover os 28 arquivos para `scripts/_archive/` antes de remover (preservar histórico de hot-fix). +3. **1d** — README dos scripts canônicos: cada `NN-...` ganha purpose, inputs, outputs. + +**Categoria:** DevEx. **Bloqueia:** novos engenheiros entendendo o pipeline. + +--- + +### 2.2.11 [MED] GoTrue `MAILER_AUTOCONFIRM=true` sem SMTP + +**Localização:** `infra/disclosure-stack/docker-compose.yml:81`. + +**Risco:** qualquer e-mail (inclusive throwaway) vira conta ativa. Sem rate-limit no signup. Em produção pública, isso é vetor de spam/abuso e infla `chat_sessions` com lixo. + +**Esforço:** + +- **0.5d** — allowlist temporária: trigger `BEFORE INSERT ON auth.users` rejeita domínios fora de `('disclosure.top', 'plataformainfraredmed.org', allowed_domains_table)`. +- **1d** — Configurar SMTP (Postmark / Mailgun / SES — escolha de provedor é decisão de operação, não data plane). `GOTRUE_MAILER_AUTOCONFIRM=false`. Tela de "confirme seu e-mail" no signup. + +**Categoria:** Sec + Ops. **Bloqueia:** abertura pública controlada. + +--- + +### 2.2.12 [MED] `.env.backup.*` versionado com credenciais + +**Localização:** `infra/disclosure-stack/.env.backup.1778796747` contém `CLAUDE_CODE_OAUTH_TOKEN`, `OPENROUTER_API_KEY`, `POSTGRES_PASSWORD`, `JWT_SECRET`. + +**Esforço — 0.5d + rotação:** + +1. Adicionar `.env.backup.*` ao `.gitignore`. +2. Remover do histórico via `git filter-repo` ou `bfg-repo-cleaner` (operação destrutiva — requer alinhamento com user). +3. **Rotacionar TODOS os segredos vazados** (OAuth token, OpenRouter key, JWT secret, postgres password). Sem essa rotação, o passo 1+2 é teatro. +4. Documentar a rotação no `infra/DEPLOY-CHECKLIST.md`. + +**Categoria:** Sec. **Bloqueia:** higiene básica de produção. + +> **NOTA:** Este débito tem veto implícito de segurança. Não é só "ruim" — é tóxico. Deve ser fixado **antes** de abrir o produto para sign-ups externos. + +--- + +### 2.2.13 [MED] `@assistant-ui/react` fantasma + +**Localização:** `web/package.json:13` declara `"@assistant-ui/react": "^0.14.0"`. `grep -r "@assistant-ui" web/{app,components,lib}` retorna **0 hits** em código importado. + +**Esforço — 1d:** + +- Decisão técnica em ADR-001. Se a escolha for adotar, planeja-se o uso. Se não, remove do `package.json` e do `package-lock.json`. + +**Categoria:** DevEx. **Bloqueia:** clareza de decisão de chat. + +--- + +### 2.2.14 [MED] Schema sem `tenant_id` / `corpus_id` + +**Localização:** todas as tabelas em `infra/supabase/migrations/0002_chunks_retrieval.sql` (`documents`, `chunks`, `entities`, `entity_mentions`) e em `0001_chat_schema.sql` (`chat_sessions`, `messages`). + +**Estado:** monolítico para um único corpus (war.gov/ufo). A visão de longo prazo é multi-tenant / multi-corpus (gap declarado no brief: "Multi-tenant/multi-corpus"). + +**Esforço — 4d (schema) + 6d (UI + refactor):** + +1. **2d** — Migration `0003_multitenant.sql`: `corpora(corpus_id TEXT PK, ...)`, `documents` ganha FK `corpus_id`. RLS policy passa de `USING (TRUE)` para `USING (corpus_id IN (current_user_corpora()))`. +2. **2d** — Wiki paths: `wiki//documents/...` (refactor estrutura) **ou** rotear via env `CORPUS_ID` (single-corpus por deploy — mais simples; perde "single pane of glass"). +3. **6d (UI)** — Routes Next ganham `[corpusId]` segment. Chat sessions vinculam corpus. Switcher de corpus na navbar para usuários com mais de um. + +Trade-offs em **ADR-005**. + +**Categoria:** Arch. **Bloqueia:** visão de longo prazo. + +--- + +### 2.2.15 [LOW] `wiki/log.md` 459 KB monolítico + +`wiki/log.md` é append-only desde o início e está em 459 KB. Eventualmente passa do tamanho razoável de um arquivo MD navegado por humano. + +**Esforço — 1d:** + +- Rotacionar por mês: `wiki/log.md` (current) + `wiki/log/.md` (rotated). Script `scripts/33-compact-progress-log.py` já existe — adaptar para rotação. + +--- + +### 2.2.16 [LOW] Duas libs de grafo + +**Localização:** `web/package.json` declara `sigma`, `graphology`, `@react-sigma/*` **e** `react-force-graph-2d`. + +**Esforço — 1d:** decisão + remoção da não-escolhida (ADR-006). + +--- + +### 2.2.17 [LOW] `outputFileTracingIncludes` com paths relativos absolutos + +`web/next.config.ts` usa `"../wiki/**"`, `"../processing/**"` — assume o layout do monorepo. Em Docker, `WORKDIR /app/web` e o COPY ajusta, mas o desenvolvedor que clona não-monorepo quebra. + +**Esforço — 1d:** envelopar com `process.env.UFO_ROOT_TRACE || "../"`. + +--- + +### 2.2.18 [LOW] Sem CI/CD + +Esforço — 2d: + +1. **1d** — `.github/workflows/ci.yml`: lint + build + smoke. +2. **1d** — Deploy: SSH para VPS, `docker-compose pull && up -d web` orquestrado por workflow `release.yml` em push de tag. + +--- + +### 2.2.19 [LOW] Sem observabilidade + +Esforço — 3d: + +1. **1d** — Loki + Promtail recebem stdout dos serviços compose. +2. **1d** — Grafana dashboard básico: latência hybrid_search, error rate, signup rate. +3. **1d** — Sentry self-hosted (ou GlitchTip open-source) capturando exceptions. + +--- + +### 2.2.20 [LOW] ADRs ausentes + +Este deliverable inicia a prática. Continuação como hábito: cada decisão type-1 (stack, contrato público, fronteira de serviço) escreve um ADR. Pasta `docs/adr/`. Convenção `NNNN-titulo.md`. + +--- + +## 2.3 Mapa débito × arquivos concretos + +| Débito | Arquivo principal | +|---|---| +| Stubs Phase X | `scripts/03-dedup-entities.py:167-169` + `wiki/entities/**/*.md` | +| Case Bureau vazio | `case/{evidence,hypotheses,witnesses,timelines,profiles,connect-the-dots}/` | +| Timeline stubs | `web/app/api/timeline/route.ts` + `wiki/entities/events/EV-*.md` | +| Chat AG-UI caseiro | `web/lib/chat/agui.ts` (65 LOC) | +| Dashboards | `web/app/admin/stats/page.tsx` (único existente) | +| Testes | repo todo | +| Reranker cold | `infra/embed-service/app.py` + `web/lib/retrieval/embed.ts:16` | +| Grafo 9.2 MB | `wiki/graph.json` + `web/components/sigma-graph-client.tsx` | +| Entity mentions ILIKE | `scripts/31-populate-entity-mentions.py` | +| Sprawl rebuild | `scripts/rebuild_doc*.py` (28+) | +| SMTP / autoconfirm | `infra/disclosure-stack/docker-compose.yml:81` | +| .env backup vazado | `infra/disclosure-stack/.env.backup.1778796747` | +| assistant-ui fantasma | `web/package.json:13` | +| Multi-tenant | todas as migrations | +| log.md gigante | `wiki/log.md` | +| Sigma vs force-graph | `web/package.json` | +| Trace includes | `web/next.config.ts:6-13` | +| CI/CD | (ausente) `.github/workflows/` | +| Observabilidade | (ausente) | +| ADRs | `docs/adr/` (a criar) | + +--- + +_Esforços calibrados para 1 engenheiro sênior + 1 IA-pair (Claude Code OAuth ou Cursor) em ritmo zero-human, com a arquitetura alvo do entregável 04 como roadmap._ diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/03-product-discovery-brief.md b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/03-product-discovery-brief.md new file mode 100644 index 0000000..ce9ad4c --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/03-product-discovery-brief.md @@ -0,0 +1,224 @@ +--- +title: "Product Discovery Brief — The Disclosure Bureau" +project: disclosure-bureau +business: systems-atelier +author: sa-principal (Principal Architect — DNA Cagan) +baseline_commit: 19d0678 +generated_at: 2026-05-17 +schema_version: 0.1.0 +--- + +# 3. Product Discovery — por que, para quem, e com que valor + +> **Cagan's first rule:** comece pelo problema e pelo usuário, não pela solução. Antes de escrever uma linha de roadmap, este documento responde: por que o Disclosure Bureau deve existir, quem o usa, o que tentam fazer com ele e onde os riscos de valor / viabilidade / utilizabilidade moram. + +## 3.1 Frase-âncora + +> **O Disclosure Bureau existe para transformar o ruído documental dos arquivos UAP/FOIA em uma investigação navegável, citada e auditável — usado por pesquisadores, jornalistas, ufólogos sérios e cidadãos curiosos —, e tem sucesso quando uma claim feita no chat vem com a página exata, o crop bbox da fonte, e a cadeia de custódia visível.** + +Tudo decorre disso. Se uma feature não serve a esta frase, ela é decoração. + +## 3.2 Por que isso precisa existir + +O domínio UAP/UFO desclassificado tem três problemas estruturais: + +1. **Volume vs leitura humana.** Milhares de páginas FOIA com OCR ruim, layouts inconsistentes, redactions opacas. O leitor médio nunca chega ao parágrafo relevante. +2. **Ruído epistemológico.** Toda fonte secundária (blogs, vídeos, podcasts) cita seletivamente, sem mostrar o documento. O leitor não consegue distinguir fato verificável de inferência criativa. +3. **Falta de cadeia de custódia.** Uma claim viral ("o memo Hottel diz X") raramente vem com a página, o crop, o stamp de classificação. A investigação é estruturalmente irreproduzível. + +O Disclosure Bureau resolve os três simultaneamente: + +| Problema | Solução desenhada | +|---|---| +| Volume | Vision LLM + chunking bbox-aware + retrieval híbrido BGE-M3 | +| Ruído | Sistema de evidence/witness/hypothesis (Locard + Tetlock); confidence bands; verbatim quotes em source-lang | +| Cadeia de custódia | Cada chunk carrega `source_png`, `bbox`, `chain_of_custody[]`, `redaction_code`. Citações no chat abrem crop inline. | + +## 3.3 Quem usa — três personas reais + +### Persona A — **Marina, jornalista investigativa** (35a, fluente PT+EN) + +- Trabalha em série de reportagem longa sobre UAP. Já sabe que existem 116 PDFs no `war.gov/ufo`. Não vai ler tudo. +- Quer: "me mostre todos os encontros do tipo 'Tic-Tac' entre 2004 e 2015 e me dê o documento exato pra cada um". +- Mede sucesso: artigo publicado com 12 citações primárias verificáveis. Sem retração por má-leitura. +- Hoje: faz `Ctrl+F` em PDFs, mantém planilha Excel, perde 4 horas por sessão. + +### Persona B — **Pedro, pesquisador acadêmico** (Sociologia da ciência) + +- Tese sobre "construção institucional do UAP discourse". Precisa de network de atores, cronologia, traçar mudanças de vocabulário. +- Quer: "extraia para mim o grafo de organizações mencionadas em todos os documentos da década de 1950, agrupado por padrão de classificação (SECRET vs CONFIDENTIAL)". +- Mede sucesso: figura publicável + dataset CSV para revisão por pares. +- Hoje: trabalha à mão. Citações no LaTeX vão para `\cite[p.~37]{fbi-vault-1947}`. Sem ferramentas. + +### Persona C — **Carla, cidadã curiosa séria** (público alvo da landing public) + +- Não é especialista. Sabe que algo nesses arquivos importa. Quer entender o argumento sem precisar virar especialista. +- Quer: chat onde pergunta "o que aconteceu em Roswell de verdade?" e recebe uma resposta com 3 fontes primárias, um crop da carta, e um "este é o ponto onde a evidência fica fraca — veja a hipótese alternativa". +- Mede sucesso: confiança no que leu; capacidade de explicar para um amigo. +- Hoje: YouTube, podcasts, posts. Não tem cadeia de custódia. + +### Persona D (latente, fase 2) — **Operador de outro corpus** + +- Pesquisador de outro tema (JFK files, Stargate Project, MK Ultra, OVNIs brasileiros) que quer trazer seus PDFs para o Bureau e ter o mesmo tratamento. **Esta é a expansão multi-tenant / multi-corpus da visão.** + +## 3.4 Jobs-to-be-done + +Em linguagem JTBD ("when … I want to … so that …"): + +1. **Quando** vejo uma claim viral sobre UAP em outro meio, **quero** chegar à fonte primária com bbox crop **para que** eu possa decidir se a claim sustenta a citação. +2. **Quando** estou estudando um período (ex.: 1947-1952), **quero** uma cronologia consolidada de eventos+sightings+operações **para que** eu veja padrões temporais sem montar planilha à mão. +3. **Quando** investigo um ator (pessoa, agência), **quero** ver todos os documentos onde ele aparece, com quem é co-mencionado, e em qual classificação **para que** eu mapeie sua rede de operações. +4. **Quando** leio uma hipótese sobre um caso, **quero** ver hipóteses concorrentes com priors Bayes calibrados **para que** eu evite âncora cognitiva. +5. **Quando** uso o chat, **quero** que cada afirmação venha com `[[doc/p007#c0042]]` clicável → crop inline **para que** eu nunca aceite resposta sem verificar. +6. **Quando** encontro um gap na evidência (página redacted, custody quebrada), **quero** o sistema explicitar `residual_uncertainty` **para que** eu saiba o que falta saber. + +## 3.5 Riscos — taxonomia Cagan + +Risco bom de produto é risco enfrentado cedo. Os quatro: + +### 3.5.1 Risco de **valor** (eles vão usar?) + +| Hipótese | Status | Como derisk | +|---|---|---| +| "Jornalistas vão preferir esta UX a Ctrl+F no PDF" | **Não validado** | 5 entrevistas + landing com waitlist; meta: ≥40% conversão pra signup | +| "Cidadão curioso vai voltar uma segunda vez" | **Não validado** | Cookie de retorno + email opt-in; meta: ≥25% retention semana-1 | +| "Pesquisador acadêmico vai citar o Bureau em paper" | **Aspiracional** | Página `/cite` com BibTeX por chunk; tracking de citações | + +### 3.5.2 Risco de **viabilidade** (faz sentido para a casa?) + +| Variável | Status atual | +|---|---| +| Custo de pipeline | ~$409 acumulado para 116 docs; ~$3.50/doc; escalável | +| Custo de chat run-time | OpenRouter free models default; OAuth do Max 20x absorvido pelo plano | +| Custo de embed-service | $0 (self-hosted CPU); VPS já está rodando | +| Custo de armazenamento | 634 MB raw chunks + 9 MB graph.json — trivial | +| Custo marginal de novo corpus | ~$3-10/doc dependendo do tamanho + algumas horas de operador | + +**Conclusão de viabilidade:** estrutura de custo está calibrada. Risco maior é tempo de engenharia (escopo grande, sem CI, sem testes). Não é dinheiro. + +### 3.5.3 Risco de **utilizabilidade** (eles conseguem usar?) + +| Sintoma observado | Risco | +|---|---| +| Stubs "Phase X" em 22k arquivos | **Alto** — destrói confiança imediatamente | +| Chat sem AG-UI real | **Médio** — falta de feedback streaming faz parecer "travado" | +| Mobile responsivo precário | **Médio** — gap declarado; jornalista lê no celular | +| Grafo 9.2 MB | **Médio** — trava em corpus grande, lá vem o "site quebrado" | +| Sem onboarding | **Médio** — usuário não sabe pedir "list_anomalies" via chat | +| Sem export PDF/CSV/PNG | **Baixo-médio** — pesquisador precisa exportar pra paper | +| Geocoding ausente | **Baixo** — desbloqueia mapa, mas mapa é nice-to-have | + +### 3.5.4 Risco de **utilidade investigativa** (specifically Cagan-adjacent) + +A categoria que mais distingue este produto de "outro chat sobre PDFs": + +| Hipótese | Status | +|---|---| +| "Tetlock-style hypothesis tournament melhora confiança vs LLM-só" | **Não testado** — case bureau ainda vazio | +| "Locard-style chain of custody + bbox crops reduz fabricação" | **Em parte** — citações funcionam; chain_of_custody[] não preenchido | +| "Red-team review é diferencial" | **Não testado** — chief-detective não rodou | + +**Conclusão:** o diferencial do Disclosure Bureau não está no chat. Está no **case bureau**. Se a fase 2 do roadmap (Case Bureau) não rodar, o produto é "Wiki UFO bonita com chat". Comoditizável. + +## 3.6 Posicionamento — "super centro de investigação" + +O brief usa essa frase. Vou traduzi-la em coordenadas de competição: + +### 3.6.1 O que **não** competimos + +- **Não somos** Perplexity / ChatGPT — não fazemos web search ao vivo. Nosso corpus é fixo, curado, auditado. +- **Não somos** Obsidian / Roam — não somos editor pessoal. Somos investigação pública. +- **Não somos** Elicit / Consensus — não somos paper-summarizer. Lidamos com fonte primária bruta, não literatura científica. +- **Não somos** Internet Archive — não somos depositário neutro. Temos curadoria, hipóteses e veredictos. + +### 3.6.2 O que **somos** (em uma linha) + +> **Karpathy LLM-Wiki + Investigation Bureau Locard/Tetlock para arquivos declassificados, com chat AG-UI nativamente citado e auditável.** + +### 3.6.3 Vetor de diferenciação + +| Eixo | Disclosure Bureau | Concorrente médio (Perplexity, Sherlock-no-PDF) | +|---|---|---| +| Citações com bbox visual | **Sim** (`/api/crop`) | Não | +| Bilíngue EN+PT desde ingest | Sim | Tradução tardia, perde precisão | +| Hypothesis tournament Tetlock | Sim (planejado) | Não | +| Chain of custody Locard | Sim (planejado) | Não | +| Red-team review chief-detective | Sim (planejado) | Não | +| Source-of-truth markdown puro | Sim | Banco proprietário | +| Self-hosted, sem SaaS no data plane | Sim | Vendor lock-in | +| Open-source amigável (planejado) | Sim | Não | + +Os "Sim (planejado)" são o roadmap. **Se planejado vira "Sim entregue", o produto fica defensável.** + +## 3.7 Mudanças de visão refinadas a partir do brief + +O brief lista "Visão longo prazo: multi-tenant/multi-corpus · API pública REST+GraphQL · MCP server · Visualização temporal-geo". Reflexão sobre cada uma: + +### 3.7.1 Multi-tenant / multi-corpus — **YES, fase 4** + +- Permite Persona D (operadores de outros corpora). +- Trade-off: explode complexidade de RLS, billing, isolamento. Ver **ADR-005**. +- Estratégia recomendada: começar com **multi-corpus single-tenant** (vários corpora dentro de um Bureau, todos curados pela mesma equipe). Multi-tenant verdadeiro vem depois. + +### 3.7.2 API pública REST + GraphQL — **YES, fase 5** + +- Habilita pesquisadores acadêmicos a integrar em fluxos próprios. +- REST puro via PostgREST já existe (parte do Supabase stack). Pode ser exposto seletivamente. +- GraphQL é luxo. Postpor. REST cobre 90%. + +### 3.7.3 MCP server — **YES, fase 5** + +- Tornar o Bureau acessível como ferramenta para outros agentes (Claude Code, Cursor, Codex). Forte alavanca de adoção. +- Custo: ~3d para expor um MCP server compatível com o protocolo, expondo os mesmos tools do chat (`hybrid_search`, `read_chunk`, `entity_neighbors`, …). +- ROI alto. **Prioridade dentro da fase 5.** + +### 3.7.4 Visualização temporal-geo — **YES, fase 3** + +- Adianta o "super centro de investigação". +- Dependências: geocoding pipeline + timeline real (não-stub). +- Stack open-source: MapLibre GL JS + Nominatim self-host + tiles do MapTiler ou Stamen. + +## 3.8 Critérios de sucesso mensuráveis (1 ano) + +| Métrica | Linha de base atual | Meta 12 meses | +|---|---|---| +| Docs catalogados | 116 | 500+ (com onboard de 3 corpora além de war.gov) | +| Sessões de chat / mês | n.d. (presumir <200) | 5.000+ | +| Citações com crop inline | 100% (já) | manter 100% | +| Eventos com narrativa real (não stub) | 1/7.091 | ≥2.000 (28%+) curados | +| Casos com hypothesis tournament rodado | 0 | ≥15 | +| Hypothesis tournaments com red-team pass | 0 | ≥10 | +| Latência hybrid_search p50 | 1.3s | <800ms | +| Latência rerank p95 (warm) | 60s timeout | <3s | +| Mobile usable score Lighthouse | n.d. (suspeitamente baixo) | ≥85 | +| Retention semana-1 | n.d. | ≥25% | + +## 3.9 Aposta de design — bilíngue PT-BR como ativo + +Decisão estratégica não-óbvia: investir em **PT-BR de primeira linha** (não tradução tardia) é vantagem competitiva sustentável, não custo. + +**Por que:** + +- Mercado lusófono de pesquisa UAP é underserved. Não há concorrente nativo PT-BR. +- O usuário-fundador é brasileiro. Insights de produto vêm em PT-BR primeiro. +- Documentos brasileiros futuros (Operação Prato 1977, Caso Varginha 1996, Marinha-Soure 1959) são corpus natural de expansão de fase 4. + +**Implicação técnica:** já está feito (`pt_unaccent` tsvector, `content_pt` em todo chunk, vision pass dual). Manter. + +## 3.10 Anti-personas — quem não atendemos + +- **Crentes dogmáticos** (de qualquer lado): o produto cita evidência e mostra hipóteses concorrentes; não apoia narrativa fechada. Quem busca confirmação, sai irritado. Aceitável. +- **Conspiracionista de TikTok**: o produto é texto+gráfico, não vídeo curto. Sem TikTok-mode. Aceitável. +- **Pesquisador de domínio adjacente** (cripto-zoologia generalista, paranormal solto): cobrimos `cryptid_anomaly` mas o foco é UAP/UFO. Cryptid é subproduto. Não desviamos roadmap por ele. + +## 3.11 Sinais para validar/ajustar a visão + +Em 90 dias após o roadmap começar, três sinais decidem se a visão de "super centro de investigação" cola: + +1. **Engajamento qualitativo no Case Bureau**: ≥3 cases com ≥30 visitas únicas e tempo médio >3min. Se cair abaixo, o "tournament Tetlock" é teatro, não valor. +2. **Citações externas**: alguém citar `disclosure.top/d/#c0042` em um artigo público ou tweet. Se não acontecer em 6 meses, o produto não atingiu Persona A/B. +3. **Onboard de segundo corpus**: alguém pedir "quero processar meu corpus aqui". Se em 12 meses não acontecer, a aposta multi-corpus é prematura — refoca em war.gov/ufo profundo. + +--- + +_Esta discovery foi conduzida lendo CLAUDE.md (contrato), o brief, o estado atual do repo, MEMORY.md (vinte feedbacks de dev acumulados) e o user-fundador (brasileiro, plano Claude Max 20x). É premissa para o entregável 04._ diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/04-system-architecture-doc.md b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/04-system-architecture-doc.md new file mode 100644 index 0000000..69aeb66 --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/04-system-architecture-doc.md @@ -0,0 +1,450 @@ +--- +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*.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*.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: `. +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 `` (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//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/` 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._ diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-001-chat-pattern-ag-ui.md b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-001-chat-pattern-ag-ui.md new file mode 100644 index 0000000..ac31ded --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-001-chat-pattern-ag-ui.md @@ -0,0 +1,129 @@ +--- +adr: 001 +title: "Chat pattern — substituir 'AG-UI style' caseiro pelo AG-UI v1 + artefatos tipados" +status: Proposed +date: 2026-05-17 +owner: sa-principal +related_phase: Fase 4 +--- + +# ADR-001 — Chat pattern: AG-UI v1 + artefatos tipados + +## Status + +Proposed — aguarda aprovação do `sa-principal` antes da Fase 4 iniciar. + +## Context + +O chat do Disclosure Bureau é o canal primário de uso do produto. Hoje a implementação está dividida entre três módulos: + +- `/Users/guto/ufo/web/lib/chat/agui.ts` (65 LOC) — eventos SSE caseiros (`text_delta`, `tool_start`, `tool_result`, `navigate`, `done`, `error`). +- `/Users/guto/ufo/web/lib/chat/openrouter.ts` (341 LOC) — Pattern C (streaming + tool-call loop client-side) sobre OpenAI-compat da OpenRouter. +- `/Users/guto/ufo/web/components/chat-bubble.tsx` (507 LOC) — UI consumindo SSE manualmente. + +O comentário no topo do `agui.ts` é explícito: _"We don't implement the full AG-UI protocol — we use a simplified event set"_. Isso traz limites concretos: + +1. **Sem reconnect com Last-Event-ID** — queda de rede perde a mensagem inteira. +2. **Sem persistência incremental** — `text_delta` só vira `messages` row após `done`. +3. **Sem cancelamento limpo** — `AbortController` no client não chega ao server; tools continuam executando. +4. **Sem artefatos tipados** — citações são strings markdown parseadas no front, fáceis de errar. +5. **Sem crops bbox inline** — citação aponta para `/d/#`, mas o crop só renderiza fora do chat. + +Adicionalmente, `@assistant-ui/react@^0.14.0` está no `package.json` e **não é importado em runtime** (`grep -r "@assistant-ui" web/{app,components,lib}` → 0 hits). Decisão técnica adiada. + +O brief lista esse problema como gap crítico #4: "A2UI / AG-UI no chat — streaming tool-calls, artefatos clicáveis, crops bbox inline, multi-turn persistente." + +## Opções consideradas + +### Opção A — Continuar com `agui.ts` caseiro, adicionar features + +- Manter o protocolo proprietário, adicionar reconnect, artefatos tipados, cancelamento. +- **Custo:** ~6d. +- **Prós:** zero migração; controle total. +- **Contras:** reinventa AG-UI v1 (que já é open-spec e bem documentado); custo de manutenção cresce; ecossistema não conecta. + +### Opção B — Adotar AG-UI v1 (protocolo aberto) + +- Substituir o protocolo SSE proprietário pela spec [AG-UI v1](https://github.com/ag-ui-protocol/ag-ui): eventos `state.start`, `text.delta`, `tool.call`, `tool.result`, `artifact.created`, `state.end`, `error`. Reconnect via `Last-Event-ID`. +- Client: implementação manual (~150 LOC) ou biblioteca compatível (`@ag-ui/client` em early stage). +- **Custo:** ~5d. +- **Prós:** spec aberta e versionada; outros sistemas (Claude Code, Codex) podem virar clients do mesmo backend; alavanca pra MCP server na Fase 5. +- **Contras:** AG-UI v1 ainda é jovem; spec pode mudar (versionamos a versão exata no chat-runtime). + +### Opção C — Adotar `@assistant-ui/react` + LangGraph runtime + +- Usar a lib já no `package.json`. Trade-off: o `@assistant-ui/react` ofusca o protocolo SSE sob abstrações React; persistência fica acoplada à lib. +- **Custo:** ~4d. +- **Prós:** UI pronta (composer, mensagens, tool blocks); animações grátis. +- **Contras:** opinião pesada na arquitetura; integração com nossos tools custom + Pattern C custa; menos controle do SSE protocol. + +## Decisão + +**Adotar Opção B — AG-UI v1 com client implementado manualmente.** + +Justificativas, em ordem de peso: + +1. **Spec aberta.** AG-UI v1 é versionado e públicó. Não comemos ofuscação proprietária de UI lib. +2. **Alavanca para Fase 5 (MCP server).** Os mesmos artefatos tipados (`citation`, `crop_image`, `entity_card`) são reutilizados como artifacts MCP. Um único contrato serve chat e agent integrations. +3. **Backwards-compatível com nosso `tools.ts`.** Não muda a forma como tools são definidos, só como o stream é emitido. +4. **Controle do SSE protocol.** Reconnect, cancelamento, persistência incremental ficam em nosso código, não em deps. +5. **Remove `@assistant-ui/react` do `package.json`** — débito 2.2.13 fechado. + +## Implementação + +Esquema do contrato (versão 1.0): + +```typescript +type AGUIEvent = + | { kind: "state.start"; session_id: string; turn_id: string } + | { kind: "text.delta"; turn_id: string; delta: string } + | { kind: "tool.call"; turn_id: string; id: string; name: string; args: Record } + | { kind: "tool.result"; turn_id: string; id: string; result: unknown; duration_ms: number } + | { kind: "artifact.created"; turn_id: string; artifact: Artifact } + | { kind: "state.end"; turn_id: string; usage: { tokens_in: number; tokens_out: number } } + | { kind: "error"; turn_id: string; code: string; message: string }; + +type Artifact = + | { type: "citation"; chunk_id: string; doc_id: string; page: number; bbox: BBox } + | { type: "crop_image"; src: string; alt_en: string; alt_pt: string } + | { type: "entity_card"; class: EntityClass; id: string } + | { type: "case_card"; case_id: string } + | { type: "evidence_card"; evidence_id: string } + | { type: "hypothesis_card"; hypothesis_id: string } + | { type: "navigation_offer"; target: string; label_en: string; label_pt: string }; +``` + +Persistência incremental: cada `text.delta` apenda em buffer; ao `state.end`, materializa em `messages`. Reconnect: `EventSource` com header `Last-Event-ID: `; server replay desde o último seq. + +Cancelamento: `POST /api/chat/cancel { session_id }` flag em memória + `chat_sessions.cancel_requested=true`; loop de tool-call checa antes de cada turno. + +## Consequences + +### Positivas + +- Reconnect resiliente em redes móveis ruins (Persona A/C frequentemente lê no celular). +- Artefatos tipados → menos bug de parsing de citação markdown. +- Crops inline no chat (`crop_image` artifact gerado quando tool retorna chunk com bbox). +- Mensagens "salvas em progresso" mesmo se o usuário fecha o modal. +- MCP server (Fase 5) usa o mesmo contrato de artifacts — economia de design. + +### Negativas + +- ~5d de engenharia (versus 0d se ficássemos no caseiro). +- AG-UI v1 spec pode evoluir; precisamos versionar `chat-runtime` com pinning de versão. +- `@assistant-ui/react` removido do package.json — qualquer demo que importou via copy/paste de docs externos quebra (provavelmente nenhum, mas verificar). + +### Neutras + +- Não muda `tools.ts` (12 handlers permanecem). Não muda `OpenRouter`/`Claude Code` providers — só muda o "frame" de eventos emitidos. + +## Validação + +- Smoke test: usuário envia "/test/disconnect" durante streaming → desconecta rede 5s → reconecta → mensagem continua sem perda. +- E2E: tool `hybrid_search` retorna 5 hits → 5 `artifact.created` `citation` aparecem inline no chat; cada citação clica → `/d/.../p007#c0042` com crop visível. + +## Referências + +- AG-UI spec: +- Pattern C streaming: anthropic + openai cookbooks +- Issue: gap #4 do brief original diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-002-reranker-caching.md b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-002-reranker-caching.md new file mode 100644 index 0000000..586cba7 --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-002-reranker-caching.md @@ -0,0 +1,169 @@ +--- +adr: 002 +title: "Caching e serving do reranker BGE-Reranker-v2-M3" +status: Proposed +date: 2026-05-17 +owner: sa-principal +related_phase: Fase 4 +--- + +# ADR-002 — Caching e serving do reranker + +## Status + +Proposed. + +## Context + +O `embed-service` (`/Users/guto/ufo/infra/embed-service/app.py`, FastAPI 148 LOC) carrega BGE-M3 + BGE-Reranker-v2-M3 em CPU. Lazy load via `Lock`. Primeira chamada após restart custa ~30s para carregar ~2.5GB em RAM. + +O cliente em `/Users/guto/ufo/web/lib/retrieval/embed.ts:16` usa `AbortSignal.timeout(60_000)` — workaround, não solução. O brief lista isso como gap médio: "Reranker 60s". + +Cenários observados: + +- **Primeira query do dia:** 30-60s (cold start). UX inaceitável. +- **Query repetida quente:** 200-500ms por par query/doc. Para top-100 candidatos → 20-50s. Inaceitável também. +- **Query única quente, candidatos novos:** ~3-8s para top-20 candidates. + +A causa raiz não é o tamanho do modelo. É (a) cold start sem warm-up, (b) reranker é chamado síncrono dentro do hybrid_search request, (c) nenhum cache compartilha resultados entre usuários. + +## Opções consideradas + +### Opção A — Warm-pool + cache de rerank scores + +- `healthcheck` do `embed` dispara `/embed` no startup (warm-up). +- `restart: unless-stopped` no docker mantém quente. +- Tabela Postgres `rerank_cache(hash TEXT PK, scores REAL[], created_at TIMESTAMPTZ)` com hash = SHA-256 de `(query_normalized, candidate_ids_sorted)`. +- TTL 24h (lru_count <100k). +- **Custo:** 3d. +- **Prós:** simples, sem novo serviço; ataca cold start e queries repetidas. +- **Contras:** queries únicas com candidatos novos continuam 3-8s. + +### Opção B — Warm-pool + cache + rerank assíncrono com SSE progress + +- Mesmo cache da Opção A. +- Hybrid search retorna RRF top-20 imediatamente (já ordenado mas sem rerank). UI mostra resultados. +- Rerank roda em background, emite SSE event `rerank_progress` com top-5 reordenado. UI atualiza inline. +- **Custo:** 5d. +- **Prós:** UX percebida em <1s sempre; rerank vira luxo enriquecedor, não bloqueador. +- **Contras:** complexidade de coordenação client/server; rerank ainda gasta CPU. + +### Opção C — Migrar reranker para GPU (Modal / RunPod / local GPU box) + +- BGE-Reranker-v2-M3 em GPU custa <50ms por par. +- **Custo:** 4d + ongoing $40-150/mês. +- **Prós:** latência ideal. +- **Contras:** **VIOLA restrição "self-hosted Docker Compose em VPS"** se for SaaS; precisa de VPS GPU dedicado (e.g. Hetzner GEX44 ~€100/mês). Não cabe no orçamento atual. + +### Opção D — Trocar o reranker por modelo menor + +- BGE-Reranker-v2-M3 → MiniLM-L-6 (32x menor, 10x mais rápido, recall pior). +- **Custo:** 2d. +- **Prós:** baixa latência sem trocar de host. +- **Contras:** **recall@5 estimado -10 a -20%**. Não validado. Risco de regressão de qualidade. + +### Opção E — Não rerankear; usar só RRF + +- Skip rerank. Devolver direto top-k do `hybrid_search_chunks` RPC. +- **Custo:** 0.5d. +- **Prós:** latência consistente <1.3s. +- **Contras:** perde ~5-15% de precisão@5 dependendo da query (BGE-Reranker melhora especialmente queries semânticas longas). + +## Decisão + +**Adotar Opção B — Warm-pool + cache + rerank assíncrono com SSE progress.** + +Fundamentos: + +1. **Percepção de latência** é o que conta para o usuário, não latência total. Mostrar top-20 RRF em <1s e enriquecer com rerank em background dá UX excelente sem violar restrições. +2. **Cache cobre Persona A/B (jornalistas, pesquisadores)** que repetem queries semelhantes em sessão. +3. **Warm-pool é mandatório de qualquer forma** — o cold start em produção é defeito. +4. **GPU (Opção C) viola política self-hosted Docker Compose** e não cabe no orçamento. Reabrir só se latência rerank ficar reportada como bloqueante mesmo com cache + async. + +## Implementação + +### Warm-pool + +```yaml +# infra/disclosure-stack/docker-compose.yml — embed: +healthcheck: + test: ["CMD", "curl", "-f", "-X", "POST", + "http://localhost:8000/embed", + "-H", "content-type: application/json", + "-d", '{"texts":["warmup"]}'] + interval: 60s + timeout: 30s + retries: 3 + start_period: 90s # tolera carregamento inicial +restart: unless-stopped +``` + +E no `app.py`, um endpoint `/warmup` dedicado que invoca embed + rerank com payload trivial para ambos os modelos. + +### Cache + +```sql +-- migration 0004_rerank_cache.sql +CREATE TABLE public.rerank_cache ( + hash TEXT PRIMARY KEY, + query_normalized TEXT NOT NULL, + candidate_ids INT[] NOT NULL, + scores REAL[] NOT NULL, + hits INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_hit TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_rerank_cache_last_hit ON public.rerank_cache(last_hit); + +-- TTL job: DELETE FROM rerank_cache WHERE last_hit < NOW() - INTERVAL '24 hours'; +-- Roda via cron container leve, 1x/dia. +``` + +`web/lib/retrieval/hybrid.ts`: + +1. Calcula `hash = sha256(normalize(query) + sort(candidate_ids))`. +2. `SELECT scores FROM rerank_cache WHERE hash = $1`. Se hit, devolve direto. +3. Se miss, chama `/rerank`. Insere com `ON CONFLICT (hash) DO UPDATE SET hits = hits + 1, last_hit = NOW()`. + +### Rerank assíncrono + +`web/app/api/search/hybrid/route.ts` vira SSE: + +1. Recebe query, retorna `top-20 RRF` imediatamente como `event: results.initial`. +2. Em background, chama rerank. +3. Emite `event: results.reranked` com nova ordem. +4. UI: estado `results.optimistic` mostra os 20; ao receber `results.reranked`, transição animada. + +Para chat (`hybrid_search` tool), mantém comportamento síncrono — o LLM precisa do top-5 estável. Mas o cache acelera. + +## Consequences + +### Positivas + +- Cold start eliminado pelo warm-pool. +- p50 hybrid_search percebido <800ms. +- p95 rerank warm <3s (com cache hit, instantâneo). +- Cache compartilhado entre usuários — escala viral em queries trending. + +### Negativas + +- Complexidade de coordenação client/server no SSE inicial→reranked. +- Cache pode entupir se `unaccent + normalization` falhar — monitorar tamanho e hits ratio. +- ~3d a mais que a Opção A simples. + +### Neutras + +- Tudo se mantém em CPU local; nada vai para SaaS. +- BGE-Reranker-v2-M3 stays — sem regressão de recall. + +## Validação + +- Latência: medir p50/p95 de `hybrid_search` end-to-end e de `/rerank` isolado, antes e depois. +- Cache hit ratio após 7d de tráfego → meta ≥40%. +- Smoke: matar o container `embed` e verificar que `healthcheck` reativa warm em <90s. + +## Referências + +- BGE-Reranker-v2-M3 perf: +- Gap #5 médio do brief +- `web/lib/retrieval/embed.ts:16` timeout 60s atual diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-003-timeline-no-stubs.md b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-003-timeline-no-stubs.md new file mode 100644 index 0000000..ec5a0ce --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-003-timeline-no-stubs.md @@ -0,0 +1,140 @@ +--- +adr: 003 +title: "Geração da Timeline — eliminar stubs 'Phase X' e materializar narrativa real" +status: Accepted (blocking — Phase 0) +date: 2026-05-17 +owner: sa-principal +related_phase: Fase 0 + Fase 3 +--- + +# ADR-003 — Timeline sem stubs + +## Status + +Accepted. **Bloqueante para entrega.** O usuário está furioso com os stubs "Phase X" e é a primeira coisa que cai na fase 0. + +## Context + +Estado atual: `/Users/guto/ufo/wiki/entities/events/` tem 7.091 eventos. Desses, **7.090 (99.99%) carregam `narrative_summary: _Stub. Will be enriched in Phase 7._`** — só 1 evento tem narrativa real. + +Origem do mal: `/Users/guto/ufo/scripts/03-dedup-entities.py`, linhas 167-169: + +```python +"narrative_summary": "_Stub. Will be enriched in Phase 7._", +"narrative_summary_pt_br": "_Stub. Será enriquecido na Fase 7._", +# (e o body também recebe "_Stub generated by entity dedup. Will be enriched in Phase 6._") +``` + +A página `/timeline` (web/app/timeline/page.tsx + web/components/timeline-view.tsx) carrega esses 7.091 eventos via `/api/timeline/route.ts`, agrupa por década e renderiza cards com `narrative_summary.slice(0, 280)`. Resultado visível: décadas inteiras com "_Stub. Will be enriched in Phase 7._" repetido. + +Impacto: + +- **Promessa de produto destruída.** Quem entra em `/timeline` vê alpha quebrado. +- **Métrica:** 99.99% dos eventos sem narrativa real. +- **22.096 arquivos** em `wiki/entities/**/*.md` carregam variantes desse stub textual. + +## Opções consideradas + +### Opção A — Apagar a Timeline temporariamente + +- Esconde sintoma, não causa. +- **Custo:** 0.5d. +- **Veto:** Timeline é feature de homepage; removê-la em vez de consertar é teatro. + +### Opção B — Filtrar stubs no `/api/timeline` por default + +- Adicionar `WHERE narrative_summary IS NOT NULL AND narrative_summary NOT LIKE '\_Stub%'` na query (ou equivalente filesystem). +- Restante visível com `?include_stubs=true`. +- **Custo:** 1d. +- **Prós:** Timeline carrega 1 evento (estado mínimo honesto) em vez de 7.091 stubs. +- **Contras:** sozinho não resolve o problema — a UI fica vazia. + +### Opção C — Eliminar o texto stub do source + pipeline de enrichment real + +- Patch `03-dedup-entities.py` para nunca emitir stub textual: `narrative_summary: null`, `summary_status: 'none'`. +- Script `scripts/maintain/41_dedup.py` migra os 22.096 arquivos existentes (strip + status field). +- Curadoria de 50-100 eventos canônicos pelo case-writer (Holmes/Watson voz) com narrativa rica. +- Pipeline `scripts/synthesize/20_entity_summary.py` itera eventos com `total_mentions ≥ 5`, gera narrative via Claude Code OAuth, popula DB + escreve no MD. +- **Custo:** 3d (fase 0 — patch + migração + curadoria mínima 20 eventos) + 8d (fase 3 — pipeline enrichment 2k+). +- **Prós:** ataca causa, não sintoma; UI fica honesta + rica. +- **Contras:** custo maior; depende de Claude Code OAuth não estourar quota. + +### Opção D — Auto-gerar narrative no momento da request + +- Lazy enrichment: ao abrir o evento, dispara Claude Code; cache no DB. +- **Custo:** 2d. +- **Veto:** primeiro hit fica 8-20s (péssima UX); custo descontrolado (cada usuário pode bombar 7k eventos); LLM offline = página quebrada. + +## Decisão + +**Adotar Opção C — eliminar fonte + migrar arquivos + curadoria âncora + pipeline enrichment progressivo.** + +Quebra em duas ondas: + +### Onda 1 (Fase 0, 3d — bloqueante) + +1. **Patch `scripts/03-dedup-entities.py`**: substituir as 3 linhas que emitem stub textual por campos vazios + `summary_status: 'none'`. + +2. **Migração `scripts/maintain/41_strip_stubs.py`**: percorre `wiki/entities/**/*.md`. Para cada arquivo com stub textual: + - YAML: seta `narrative_summary: null`, `narrative_summary_pt_br: null`, adiciona `summary_status: none`. + - Body: remove blocos "_Stub generated by entity dedup. Will be enriched in Phase 6._" e mantém o restante. + - Idempotente: já-migrado é detectado por presença de `summary_status` e pulado. + - Append log em `wiki/log.md`. + +3. **Update `/api/timeline/route.ts`**: filtro server-side; default = só eventos com `narrative_summary` não-nulo. Param `?include_unsynthesized=true` libera; UI mostra badge "sem síntese ainda". + +4. **Update UI `timeline-view.tsx` + entity pages**: quando `narrative_summary` ausente, renderiza: + - badge "Sem síntese ainda" + - botão "Sintetizar agora" (visible para `role='admin'` — chama enrichment pontual via `/api/admin/synthesize/`) + - linka para os chunks que mencionam a entidade (já temos `entity_mentions`). + +5. **Curadoria âncora — 20 eventos manuais para a Timeline:** Roswell, Nimitz Tic-Tac, Phoenix Lights, Foo Fighters, Trindade Island BR, Operação Prato, Operação Mainbrace, AATIP disclosure, Belgian Wave, Rendlesham Forest, Cash-Landrum, Westall, Travis Walton, Father Gill, Lonnie Zamora, Maury Island, Kenneth Arnold, Aurora 1897, Foo lights 1944, Mantell crash. Cada um com `narrative_summary_en/pt` curados; `summary_status: 'curated'`. + +### Onda 2 (Fase 3, 8d — enrichment progressivo) + +1. **Pipeline `scripts/synthesize/20_entity_summary.py`**: + - Itera entities com `total_mentions ≥ 5` ordenadas por `total_mentions DESC`. + - Para cada uma, lê chunks via `entity_mentions`, monta prompt com top-10 menções verbatim, chama Claude Code OAuth (Sonnet para qualidade) em modo `--max-turns 1`. + - Output: `narrative_summary_en` (3-6 frases) + `narrative_summary_pt` (3-6 frases) + `summary_status: 'synthesized'` + `summary_confidence: medium`. + - Idempotente: `summary_status` ≠ 'none' pula. + - Custo estimado: ~$80-150 para corpus inteiro a 5+ mentions threshold (~5-8k entidades). + - Throttle: 200 entidades/dia para não estourar quota Max 20x. + +2. **Quality gate**: para `summary_status='curated'`, lint exige `summary_confidence: high`. Para `summary_status='synthesized'`, default `medium` + check de heurística (não pode citar fato sem chunk_id no rationale). + +## Consequences + +### Positivas + +- `grep "Will be enriched in Phase" wiki/` → 0 hits após onda 1. +- `/timeline` mostra ≥20 eventos com narrativa rica de imediato; cresce para ≥2k na fase 3. +- Estado honesto: entidades sem síntese mostram badge claro, não texto enganoso. +- Pipeline progressivo: enrichment não bloqueia release. + +### Negativas + +- Migração toca 22k arquivos — risco de corrupção YAML. Mitigação: dry-run + diff antes de commit; idempotência; backup do `wiki/` antes. +- Custo OAuth ~$150 estimado se rodar enrichment completo. +- 20 eventos curados manualmente = 1d de case-writer; precisa de revisão linguística PT-BR. + +### Neutras + +- Mantém estrutura de 24 tipos de markdown intacta. Só adiciona campo `summary_status` no schema YAML. + +## Compatibilidade com CLAUDE.md + +Schema canônico em `/Users/guto/ufo/CLAUDE-schema-full.md` precisa ganhar `summary_status` em `entity` frontmatter. Update do schema é parte da entrega da fase 0. + +## Validação + +- Antes da fase 0 mergear: `grep -c "Will be enriched" wiki/` precisa ser 0. +- Após onda 1: `/timeline` carrega <2s com ≥20 cards renderizados, cada um com narrativa ≥100 chars. +- Após onda 2: `/timeline` carrega ≥2.000 eventos com narrativa real. +- Lint passa em todas as entidades migradas (regex IDs ainda válidos; YAML válido; backlinks consistentes). + +## Referências + +- `scripts/03-dedup-entities.py` (origem do stub textual). +- `CLAUDE.md §3` (regras bilíngue) — `narrative_summary` precisa ter campos EN e PT-BR. +- `CLAUDE.md §13` (triggers de enrichment) — `≥3 menções OR central claim` → `enrichment_status: deep`. Vamos espelhar essa lógica em `summary_status` (`none` < `synthesized` < `curated` < `red_teamed`). +- Gap #1 do brief (timeline com stubs). diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-004-case-bureau-pipeline.md b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-004-case-bureau-pipeline.md new file mode 100644 index 0000000..4e17958 --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-004-case-bureau-pipeline.md @@ -0,0 +1,218 @@ +--- +adr: 004 +title: "Case Bureau pipeline — Hypothesis tournament (Tetlock) + Evidence chain-of-custody (Locard)" +status: Proposed +date: 2026-05-17 +owner: sa-principal +related_phase: Fase 2 +--- + +# ADR-004 — Case Bureau pipeline + +## Status + +Proposed. **Diferencial competitivo do produto.** Sem Case Bureau o Disclosure Bureau é "wiki bonita com chat" — comoditizável. + +## Context + +O `/Users/guto/ufo/CLAUDE.md` define 12 tipos investigativos (evidence, witness, hypothesis, gap, relation, timeline, actor_profile, case_report, residual_uncertainty, …). O `/Users/guto/ufo/CLAUDE-schema-full.md` formaliza o frontmatter de cada um. + +**Estado físico:** + +``` +case/ +├── connect-the-dots/ 0 files +├── evidence/ 0 files +├── gaps/ 2 placeholder files (bootstrap) +├── hypotheses/ 0 files +├── profiles/ 0 files +├── timelines/ 0 files +└── witnesses/ 0 files +``` + +**Nunca rodou.** O brief lista isso como gap crítico #2. O `case-report.md` referenciado em `wiki/index.md` é um link quebrado. + +O contrato exige: + +- **6 quality rubrics threshold 0.85** em `case_report.md` (chain_of_custody_completeness, confidence_calibration_match, hypothesis_tournament_discipline ≥3 hipóteses, residual_uncertainty_presence, audit_trail_per_claim, red_team_pass). +- **Confidence bands Tetlock**: high (≥0.90) / medium (0.60-0.89) / low (0.30-0.59) / speculation (<0.30). +- **Evidence grades Locard**: A (≥3 custody steps) / B (≥2) / C (≥1). +- **8 detetives**: Holmes/Poirot/Dupin/Locard + Schneier/Tetlock/Taleb (papéis declarados, sem implementação). + +## Opções consideradas + +### Opção A — Pipeline batch sobre todo o corpus + +- Para cada `event` com `total_mentions ≥ 5`, criar case automaticamente; gerar 3 hipóteses; bayesianizar. +- **Custo:** 8d. +- **Prós:** cobertura ampla. +- **Contras:** **viola proporcionalidade Fowler.** 7.091 events × ($0.30 por case) = $2k+ em LLM. Quality cai. Output ilegível. + +### Opção B — Seed manual + pipeline incremental + +- 5 casos âncora seedados manualmente (canonical events). +- Pipeline gera hipóteses para cada caso âncora. +- Próximos casos abrem por demanda (user pede "abra case para Event-X" via chat ou admin). +- **Custo:** 12d (3d seed + 4d hypothesis engine + 5d UI). +- **Prós:** qualidade controlada; ROI por case. +- **Contras:** cobertura inicial pequena. + +### Opção C — Outsource para business parceira + +- Persona desta arquitetura: chamar `juridical-singularity` ou `research-intelligence` via harness para o trabalho investigativo. +- **Custo:** ? (depende da business). +- **Contras:** Case Bureau é **a alma do produto Disclosure Bureau**; outsourcear é estranho. Manter in-house. + +## Decisão + +**Adotar Opção B — 5 casos âncora seedados + pipeline incremental.** + +Justificativas: + +1. **Qualidade > cobertura.** Um Case Bureau com 5 casos exemplares vence 7k casos genéricos. Persona A/B/C aprende o padrão e confia no método. +2. **Proporcional ao orçamento atual** (Claude Max 20x). +3. **Ciclo de feedback rápido.** Cada case rodado refina o prompt do hypothesis engine. + +## Implementação + +### Etapa 1 — Schema (1d) + +Migration `0003_case_bureau.sql` (esboço em entregável 04, §4.4.1). Tabelas: + +- `cases(case_id, canonical_name, scope_summary, status, …)` +- `hypotheses(hypothesis_id, case_id, statement_en, statement_pt, prior, likelihood, posterior, evidence_for[], evidence_against[], red_team_pass, confidence_band, authored_by, …)` +- `evidence(evidence_id, case_id, doc_id, page, bbox, verbatim_excerpt, evidence_grade, chain_of_custody, custody_gaps[], …)` +- `witnesses(witness_id, case_id, witness_name, account_summary, verdict, …)` +- `gaps(gap_id, case_id, description, criticality, …)` +- `case_reports(case_id, body_md, quality_rubrics JSONB, last_review)` + +RLS: `SELECT public USING (TRUE)` (corpus público); writes via `service_role` (pipeline only). + +### Etapa 2 — Seed (2d) + +`scripts/case/30_seed.py` cria 5 casos âncora: + +| `case_id` | Evento | Justificativa | +|---|---|---| +| `CASE-ROSWELL-1947` | Roswell incident, julho 1947 | Canonical UAP case; rico em documentos FBI Vault. | +| `CASE-NIMITZ-TIC-TAC-2004` | USS Nimitz / Princeton Tic-Tac encounter | Caso DoD oficial; vídeo + radar data. | +| `CASE-PHOENIX-LIGHTS-1997` | Phoenix Lights, março 1997 | Multi-witness; corpus tem FAA reports. | +| `CASE-HOTTEL-MEMO-1950` | FBI Hottel memo, Roswell adjacent | Single-document case; mostra leitura crítica. | +| `CASE-APOLLO-SKYLAB-1973` | Skylab IV anomalous photos | NASA corpus; bridge para civilian science angle. | + +Cada seed cria: + +- 1 `case_report` skeleton com `quality_rubrics: null` (será preenchido após review). +- 3-5 `evidence` rows extraídas dos chunks corpus existentes (chunks com `ufo_anomaly=true` para esses docs, ordenados por relevância). +- 2-3 `witness` rows quando aplicável (relatos primários no corpus). +- 1 `timeline` com event_ids relacionados. + +### Etapa 3 — Hypothesis Tournament Engine (4d) + +`scripts/case/31_hypothesis_tournament.py`: + +``` +INPUT: case_id +WORKFLOW: + 1. Load case + evidence + witnesses (DB). + 2. For each detective_role in ['holmes','poirot','dupin']: + prompt = build_prompt(case, role, evidence_summary) + call Claude Code OAuth (Sonnet) --max-turns 3 + parse output → 1 hypothesis with statement_en/pt, prior, likelihood + insert into hypotheses(authored_by=role) + 3. After 3 hypotheses generated: + normalize priors so Σ = 1.0 + compute posterior_i = (prior_i × likelihood_i) / Σ_j(prior_j × likelihood_j) + assign confidence_band by posterior thresholds (Tetlock): + ≥0.70 high, 0.40-0.69 medium, 0.15-0.39 low, <0.15 speculation + 4. Write H-NNNN.md files in case/hypotheses/ + 5. Update case_report.md with leaderboard +``` + +Prompts são versionados em `scripts/case/prompts/{holmes,poirot,dupin}_hypothesis.md`. ADR-007 (futuro) congelará a versão de prompt usada por release. + +### Etapa 4 — Locard Chain-of-Custody Audit (1d como parte da fase 2) + +`scripts/case/32_evidence_audit.py`: para cada evidence row, valida: + +- `verbatim_excerpt` matches a chunk em DB (não fabricado). +- `chain_of_custody` array tem ≥1 step (Grade C), ≥2 (B), ≥3 (A). +- Se gap, popula `custody_gaps[]` com descrição. + +Output: `case//evidence_audit.md` com sumário. + +### Etapa 5 — Red-Team Review (chief-detective) (2d) + +`scripts/case/33_red_team.py`: + +``` +INPUT: case_id +WORKFLOW: + 1. Load hypotheses sorted by posterior DESC. + 2. Take top hypothesis. Build adversarial prompt: + "You are the chief detective. The dominant hypothesis is X with + posterior Y. Find the strongest argument that this hypothesis is + wrong. Cite specific evidence by evidence_id." + 3. Call Claude Code OAuth (Sonnet). + 4. Parse output → residual_uncertainty.md entry. + 5. Quality rubric red_team_pass = (residual_uncertainty has substance). + 6. Update case_report.md quality_rubrics. +``` + +### Etapa 6 — UI (5d, parte da fase 2) + +Rotas novas: + +- `/case` — lista os casos. +- `/case/[case_id]` — overview: status, timeline, 3 hipóteses, evidence count. +- `/case/[case_id]/hypotheses` — tournament leaderboard, cada card com posterior, evidence_for[], red-team box. +- `/case/[case_id]/evidence` — lista evidence; cada card com bbox crop inline + chain_of_custody timeline. + +Componentes: `CaseTimelineLane`, `HypothesisTournamentCard`, `EvidenceChainCustody`, `WitnessVerdictBadge`, `ResidualUncertaintyBox`. + +Tools de chat novos (em `web/lib/chat/tools.ts`): `read_case`, `read_hypothesis`, `read_evidence`, `list_open_cases`. + +### Etapa 7 — Quality gate (incluído na fase 2) + +`scripts/case/34_quality_check.py` valida as 6 rubrics no `case_report.md`. Saída JSON com scores. Gate threshold 0.85 (fase 2 inicial pode relaxar para 0.80 conforme prompts maturam). + +## Consequences + +### Positivas + +- **5 cases âncora exemplares** demonstram o método. Confiança do usuário sobe. +- **Hypothesis tournament real** com posteriores calculados, não decoração. +- **Red-team forçado** evita âncora cognitiva — o produto se desafia. +- **Citações de chat ganham `evidence_card` artifact** (ADR-001) — UI mais rica. +- Quality rubrics no `case_report.md` viram **assinatura epistemológica** do Bureau. + +### Negativas + +- Pipeline LLM-intensivo. Custo estimado: ~$30-50 para os 5 casos âncora, ~$5 por case novo via demanda. +- Quality rubrics threshold 0.85 é alto; primeiros cases podem precisar revisão humana antes do publish. +- 12d de engenharia — fase pesada. + +### Neutras + +- Não toca chat protocol (ADR-001) nem retrieval pipeline. Adiciona camada. +- 24 tipos de markdown do CLAUDE.md ganham 7 com conteúdo real pela primeira vez. + +## Risco mitigado + +- **Hipóteses fabricadas (LLM inventa fatos):** prompt exige citação por `chunk_id` para cada claim; pipeline `32_evidence_audit.py` valida match em DB. Linha fabricada = hypothesis rejeitada. +- **Red-team viciado:** prompt adversarial é separado em outro arquivo, com persona explicitamente cética. +- **Quality rubrics objetivas vs subjetivas:** as 6 rubrics são todas auditáveis (counting custody steps, hypothesis count ≥3, residual entries ≥1, …). Não há "qualidade narrativa" como rubric — isso fica para a curadoria humana opcional. + +## Validação + +- 5 cases na `cases` table com `status='open'` ou `'closed'`. +- Cada case com ≥3 hipóteses; ≥1 com `red_team_pass=true`. +- `case_report.md` por case com `quality_rubrics` JSONB todas ≥0.80. +- UI `/case/case-nimitz-tic-tac-2004` renderiza completo (timeline + hypotheses + evidence + red-team). +- Chat tool `read_case` resolve via inline `case_card` artifact. + +## Referências + +- `CLAUDE.md §4` (24 tipos), `§8` (confidence calibration Tetlock), `§10` (procedência Locard), `§12` (quality gates). +- `CLAUDE-schema-full.md` (schemas YAML de evidence, hypothesis, witness, case_report, residual_uncertainty). +- Gap #2 do brief. diff --git a/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-005-multi-tenant-refactor.md b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-005-multi-tenant-refactor.md new file mode 100644 index 0000000..2c38cb9 --- /dev/null +++ b/.nirvana/outputs/disclosure-bureau/businesses/systems-atelier/deliverables/05-adrs/ADR-005-multi-tenant-refactor.md @@ -0,0 +1,214 @@ +--- +adr: 005 +title: "Multi-tenant vs multi-corpus single-tenant — caminho de evolução" +status: Proposed +date: 2026-05-17 +owner: sa-principal +related_phase: Fase 5 (multi-corpus) e Fase 6 (multi-tenant opcional) +--- + +# ADR-005 — Multi-tenant vs multi-corpus single-tenant + +## Status + +Proposed. + +## Context + +A visão de longo prazo do brief lista "multi-tenant/multi-corpus". Atualmente o sistema é **single-corpus, single-tenant**: + +- Toda tabela de retrieval (`documents`, `chunks`, `entities`, `entity_mentions`) referencia o corpus implícito war.gov/ufo. +- `wiki/` é um único namespace; paths `wiki/documents/.md` colidem se onboardarmos JFK files ou Operação Prato BR. +- `chat_sessions.user_id` aponta `auth.users(id)` direto; sem `tenant_id`. +- RLS policy é `USING (TRUE)` — qualquer signed-up user vê tudo. +- Frontend roteia `/d/` assumindo corpus único. + +A diferença semântica importa: + +- **Multi-corpus** = vários corpora coexistem mesma equipe os opera (war.gov/ufo + JFK + BR). Visibilidade pode variar (`public` / `unlisted` / `private`). +- **Multi-tenant** = vários **operadores independentes** (organizações, indivíduos) com isolamento de dados, billing, gestão. Persona D do entregável 03. + +## Opções consideradas + +### Opção A — Pular direto para multi-tenant verdadeiro + +- Refactor pesado: `tenant_id` em todas as tabelas; RLS por tenant; signup com tenant; billing. +- **Custo:** 18-25d. +- **Veto Cagan:** prematuro. Não temos demanda nem validação. Persona D é latente. + +### Opção B — Multi-corpus single-tenant primeiro; multi-tenant só se houver sinal + +- Adicionar `corpus_id` em `documents`/`chunks`/`entities`. Tabela `corpora` controla visibilidade. +- Single-tenant = a equipe do Bureau opera tudo. Usuários signed-up só leem. +- Multi-tenant fica para fase 6, **opcional**, ativado por flag. +- **Custo:** 14d (fase 5) + 8-15d futuro condicional. +- **Prós:** entrega valor real (3 corpora visíveis); reabre multi-tenant só com sinal. +- **Contras:** schema decision não totalmente future-proof — algumas tabelas precisarão de `tenant_id` extra na fase 6. + +### Opção C — Deploy por corpus (replicar a stack) + +- Cada corpus é um deploy separado (`disclosure.top`, `jfk.disclosure.top`, `prato.disclosure.top`). +- **Custo:** 4d por deploy. +- **Prós:** zero refactor de schema. +- **Contras:** **viola "single pane of glass"**; usuário não pode pesquisar cross-corpus; UX fragmentada; ops complexa (3x containers, 3x backups). + +## Decisão + +**Adotar Opção B — multi-corpus single-tenant na Fase 5; multi-tenant na Fase 6 só se Persona D mostrar sinal.** + +Justificativas: + +1. **Cagan:** valida hipótese com sinal mínimo. Multi-corpus desbloqueia Persona D-light (a equipe do Bureau adicionando corpora curados); multi-tenant verdadeiro requer demanda externa real. +2. **Fowler:** arquitetura evolutiva — não over-engineer. `corpus_id` é refactor barato; `tenant_id` adicionado depois é refactor adicional barato. Não há "muralha" entre B e Multi-tenant na fase 6. +3. **Larson:** decisão escrita. Reabre com critério: ≥3 organizações externas pedindo onboard antes de F6 expandir para multi-tenant. + +## Implementação — Fase 5 (multi-corpus single-tenant) + +### Schema + +```sql +-- migration 0005_multicorpus.sql + +CREATE TABLE public.corpora ( + corpus_id TEXT PRIMARY KEY, -- 'war-gov-ufo', 'jfk-files', 'prato-1977' + display_name TEXT NOT NULL, + description TEXT, + language_primary TEXT NOT NULL DEFAULT 'en', -- 'en' | 'pt' + visibility TEXT NOT NULL DEFAULT 'public' + CHECK (visibility IN ('public','unlisted','private')), + cover_image TEXT, + doc_count INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Seed do corpus existente +INSERT INTO public.corpora (corpus_id, display_name, language_primary, visibility) +VALUES ('war-gov-ufo', 'War.gov/UFO — DoD UAP Declassified', 'en', 'public'); + +-- Adiciona FK em documents +ALTER TABLE public.documents + ADD COLUMN corpus_id TEXT NOT NULL DEFAULT 'war-gov-ufo' + REFERENCES public.corpora(corpus_id) ON DELETE RESTRICT; +ALTER TABLE public.documents ALTER COLUMN corpus_id DROP DEFAULT; + +-- Idem chunks, entities, entity_mentions (via documents.corpus_id derivado ou denormalizado) +-- Para queries grandes, denormalizar é melhor: +ALTER TABLE public.chunks + ADD COLUMN corpus_id TEXT NOT NULL DEFAULT 'war-gov-ufo'; +ALTER TABLE public.entities + ADD COLUMN corpus_id TEXT NOT NULL DEFAULT 'war-gov-ufo'; +-- Drop defaults após backfill + +-- Update RLS +DROP POLICY documents_read ON public.documents; +CREATE POLICY documents_read ON public.documents FOR SELECT USING ( + corpus_id IN ( + SELECT corpus_id FROM public.corpora WHERE visibility = 'public' + ) +); +-- Idem chunks, entities, mentions +``` + +### Paths do `wiki/` + +**Decisão dentro do ADR:** manter `wiki/` em raiz, identificar corpus via DB. Os paths físicos seguem `wiki//documents/...` mas com **link simbólico** `wiki/documents` → `wiki/war-gov-ufo/documents` por compat. + +Trade-off: refactor de paths impacta `web/lib/wiki.ts` e `next.config.ts outputFileTracingIncludes`. Migration script `scripts/maintain/45_relayout_wiki.py` faz a movimentação atômica + cria os symlinks. + +Para corpora futuros (JFK, Prato), paths já nascem `wiki//...`. + +### Frontend + +- Switcher de corpus na navbar: `