---
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 <https://disclosure.top>. 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/<arquivo>.pdf` — só em `raw/<doc-id>--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/<doc>/p*.md`) | **3.435** |
| Diretórios `raw/<doc>--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/<doc>--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<NN>*.py` no diretório `scripts/`. O ideal seria um único `rebuild.py --doc <id> --phase <n>`. 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._
