---
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/<entity_id>`)
   - 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).
