# PRD: Unificar reconciliação — rodar reconcile sempre

> Generated via /vibeflow:discover on 2026-04-10

## Problem

O Arandu não invalida fatos contraditórios. "Me mudei pra Curitiba"
não invalida "mora em São Paulo". "Saí da Stone" não invalida
"trabalha na Stone". O retrieve retorna informação factualmente errada.

Nos testes do Personal Genius (Etapas 1-2), este gap causou **1 FAIL
e 4 PARTIALs em 17 cenários**.

### Causa raiz

A extração informada (`enable_informed_extraction=True`, que é o
**default**) pula o reconcile inteiro. O pipeline tem dois caminhos:

```python
# pipeline.py linha 368
if extraction_meta.get("mode") == "informed":
    # Pula reconcile — vai direto pro upsert
    decisions = await build_informed_decisions(...)
else:
    # Passa pelo reconcile
    decisions = await reconcile_facts(...)
```

A extração informada pede ao LLM que classifique cada fato como
`action: "NEW"` ou `action: "UPDATE"` durante a extração. Depois
`build_informed_decisions()` converte isso em `ReconciliationDecision`
e vai direto pro upsert, sem comparar com fatos existentes via
`reconcile_facts()`.

O problema: o LLM da extração recebe contexto existente incompleto
(top-K por embedding ou profile resumido). Se o fato contraditório
não aparece no contexto, o LLM não tem como saber que é UPDATE — e
classifica como NEW. O fato novo é adicionado, o antigo nunca é
tocado.

### Design principle violado

A extração misturou duas responsabilidades: extrair fatos (transformar
texto em conhecimento) e reconciliar fatos (decidir o que fazer com
conhecimento novo vs existente). São trabalhos diferentes que devem
ser de módulos diferentes.

## Target Audience

Qualquer consumer do Arandu que faz writes com informação que evolui.
Caso canônico: Personal Genius.

## Proposed Solution

Rodar `reconcile_facts()` **sempre**, independente do modo de extração.
Remover `action` (NEW/UPDATE) da extração informada — extração extrai,
reconcile reconcilia.

A extração informada continua recebendo contexto existente (isso
melhora qualidade de extração — menos duplicatas, fatos mais precisos).
Mas a decisão ADD/UPDATE/NOOP/DELETE é do reconcile.

## Success Criteria

1. Write "me mudei pra Curitiba" invalida "mora em São Paulo" — fato
   antigo recebe `valid_to`. Retrieve retorna só Curitiba.
2. Write "saí da Stone" invalida "trabalha na Stone".
3. Funciona tanto com `enable_informed_extraction=True` quanto `False`.
4. Cenários 1.4 e 1.5 da bateria de testes do PG passam como PASS
   quando reexecutados.
5. Zero regressão nos testes existentes.

## Scope v0

1. **Remover action/updates da extração informada**
   (`informed_extract.py`)
   - Prompt: remover "RULES FOR ACTION" e campos `action`/`updates`
   - Output passa de `{subject, fact, confidence, action,
     importance_category, updates}` pra `{subject, fact, confidence,
     importance_category}`

2. **Remover `build_informed_decisions()`**
   (`informed_extract.py`)
   - Função removida (~60 linhas)
   - `_find_update_target()` removida (~40 linhas)

3. **Pipeline sempre chama reconcile** (`pipeline.py`)
   - Remover o `if extraction_meta.get("mode") == "informed"` branch
   - `reconcile_facts()` roda SEMPRE

4. **Testes**
   - Test: reconcile roda em ambos os modos
   - Test: contradição detectada no modo informado (end-to-end)

## Anti-scope

1. **attribute_key** — não mexe. Se o reconcile não pegar contradições
   mesmo rodando, aí a gente investiga por quê com dados reais e
   decide o próximo passo.
2. **Threshold de embedding (0.5)** — não mexe. Primeiro vê se o
   reconcile funciona com o threshold atual.
3. **Prompt de reconciliation** — não mexe agora. Primeiro vê se o
   LLM já acerta UPDATE/DELETE quando vê os fatos lado a lado.
4. **Migração de fatos existentes** — fix é prospectivo.
5. **Background jobs** — não muda.
6. **Entity profiles / updated_profiles** — a extração informada
   continua gerando profiles. Não muda.
7. **Remover `enable_informed_extraction` config** — flag continua.
   Informed extraction continua valiosa (contexto melhora extração).
   Só perde a decisão NEW/UPDATE.

## Technical Context

**Arquivos afetados:**

| Arquivo | Mudança |
|---|---|
| `informed_extract.py` | Remover action/updates do prompt, remover `build_informed_decisions` e `_find_update_target` |
| `pipeline.py` | Remover branch informed vs blind, sempre chamar `reconcile_facts` |
| `tests/` | Adaptar testes que esperavam o branch informado |

**Estimativa: ~100 linhas removidas, ~10 linhas alteradas.**

**O que NÃO muda:**

- `reconcile.py` — fica igual
- `upsert.py` — fica igual
- `extract.py` — fica igual
- `canonicalization.py` — fica igual
- Prompt de reconciliation — fica igual
- Config — fica igual

**Patterns aplicáveis:**

- `pipeline-orchestration.md` — simplifica pipeline (remove branch)
- `error-handling.md` — reconcile já tem fallback (all ADD on failure)

**Decisão arquitetural:**

- "Single entry point for MemoryFact creation" — reconcile é o gate
  antes do upsert, agora em 100% dos caminhos

## Open Questions

1. **Q:** O reconcile vai pegar a contradição "mora em SP" vs "mora
   em Curitiba" com o threshold de 0.5?
   **R:** Não sei. Preciso testar com dados reais depois de implementar.
   Se não pegar, a gente investiga o threshold e/ou attribute_key como
   próximo passo — com dados, não com chutes.

2. **Q:** Rodar reconcile no modo informado é uma chamada LLM extra?
   **R:** Sim. Mas é a chamada que garante integridade. O custo é
   ~200ms + tokens do LLM. A alternativa (não reconciliar) é o que
   tá causando o bug.
