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

> Generated from `.vibeflow/prds/reconciliation-attribute-matching.md` on 2026-04-10

## Objective

O write pipeline roda `reconcile_facts()` em 100% dos writes, independente do modo de extração (cego ou informado), garantindo que fatos contraditórios sejam detectados e invalidados.

## Context

A extração informada (`enable_informed_extraction=True`, default) pula o reconcile inteiro. O LLM da extração classifica fatos como NEW/UPDATE, e `build_informed_decisions()` converte direto em `ReconciliationDecision` sem comparar com fatos existentes via `reconcile_facts()`. Resultado: fatos contraditórios nunca são invalidados. Gap mais grave identificado na bateria de testes do Personal Genius (1 FAIL, 4 PARTIALs em 17 cenários).

## Definition of Done

1. **`reconcile_facts()` é chamado em ambos os modos** — o branch `if extraction_meta.get("mode") == "informed"` em `pipeline.py` não existe mais. `reconcile_facts()` roda sempre, independente do valor de `enable_informed_extraction`.

2. **`build_informed_decisions()` e `_find_update_target()` removidos** — as funções não existem mais em `informed_extract.py`. Nenhum outro módulo as importa.

3. **Prompt de extração informada não contém `action` nem `updates`** — o `INFORMED_EXTRACTION_PROMPT` em `informed_extract.py` não pede ao LLM pra classificar NEW/UPDATE. O JSON schema do output não contém os campos `action` nem `updates`. `importance_category` continua.

4. **Testes existentes passam** — `python -m pytest tests/ -q` passa com 0 failures. Testes que dependiam de `build_informed_decisions` ou do branch informed são adaptados ou removidos.

5. **Sem violações de `conventions.md`** — ruff check + ruff format + mypy passam limpo. Fail-safe pattern seguido: se `reconcile_facts` falha, default é ADD (padrão existente, sem mudança).

## Scope

### Modificar `informed_extract.py`
- Remover `build_informed_decisions()` (~linhas 342-411)
- Remover `_find_update_target()` (~linhas 414-453)
- Remover import de `ReconciliationDecision` (não é mais necessário neste módulo)
- Prompt `INFORMED_EXTRACTION_PROMPT`: remover bloco "RULES FOR ACTION" e campos `action`/`updates` do JSON schema
- O prompt continua pedindo `importance_category` (o reconcile não seta isso — precisa propagar)

### Modificar `pipeline.py`
- Remover o branch `if extraction_meta.get("mode") == "informed"` (linhas ~368-376)
- `reconcile_facts()` passa a ser chamado incondicionalmente
- Propagar `importance_category` do `ExtractedFact` pro `ReconciliationDecision` (hoje o reconcile seta importance fixo; precisa preservar o valor da extração informada)

### Adaptar testes
- Testes que mockavam `build_informed_decisions` → remover ou adaptar
- Testes que verificavam o branch informed vs blind → adaptar pro caminho único
- Adicionar teste: extração informada + reconcile roda (não pula)

## Anti-scope

- **`reconcile.py`** — não muda. Threshold, matching, prompt de reconciliation ficam iguais.
- **`upsert.py`** — não muda.
- **`extract.py`** — não muda.
- **`attribute_key`** — não mexe. Se o reconcile não pegar contradições, investiga depois com dados reais.
- **Entity profiles / `updated_profiles`** — continua igual na extração informada.
- **`enable_informed_extraction` config** — flag continua existindo. Controla se extração recebe contexto existente ou não. Não controla mais se reconcile roda.
- **Background jobs** — não muda.

## Technical Decisions

### 1. `importance_category` precisa ser propagado

**Problema:** hoje o `build_informed_decisions` propaga `importance_category` da extração informada pro `ReconciliationDecision` via `IMPORTANCE_CATEGORY_MAP`. O `reconcile_facts()` não faz isso — seta importance fixo (default 0.5).

**Decisão:** adicionar `importance_category` como campo opcional no `ExtractedFact` (já existe — o campo `importance_category` já tá no dataclass). Modificar `reconcile_facts` pra propagar o `importance_category` do `ExtractedFact` pro `ReconciliationDecision.importance` via `IMPORTANCE_CATEGORY_MAP`, igual ao que `build_informed_decisions` fazia. Fallback: se não tiver `importance_category`, usa default.

**Trade-off:** muda uma parte do `reconcile.py` que tava fora do escopo. Mas é necessário pra não regredir: sem isso, o modo informado perde a classificação de importância que tinha antes. São ~5 linhas.

### 2. `action` no `ExtractedFact` fica como está

**Problema:** `ExtractedFact` tem campo `action: str = "NEW"`. A extração cega não usa. A informada usava.

**Decisão:** não remover o campo do dataclass — pode ter consumers externos. Mas o pipeline ignora o campo. O prompt informado para de pedir `action`. Se vier no JSON, é parseado mas ignorado.

**Trade-off:** campo morto no dataclass. Aceitável — remover é breaking change pra consumers que instanciam `ExtractedFact` diretamente. Pode ser removed num major release futuro.

## Applicable Patterns

- **Pipeline Orchestration** (`patterns/pipeline-orchestration.md`) — simplifica o pipeline removendo um branch. `reconcile_facts()` vira stage incondicional entre extração e upsert.
- **Fail-Safe Error Handling** (`patterns/error-handling.md`) — `reconcile_facts` já tem fallback (all ADD on failure). Nenhuma mudança necessária no error handling.
- **LLM Interaction** (`patterns/llm-interaction.md`) — prompt da extração informada muda (remove campos). Parse JSON adapta.

## Risks

| Risco | Probabilidade | Impacto | Mitigação |
|---|---|---|---|
| Reconcile não pega contradição mesmo rodando (threshold ou prompt) | Média | Alto | Fora do escopo — investiga depois com dados reais. Mas pelo menos o reconcile roda, que é melhor que não rodar. |
| LLM call extra no modo informado aumenta latência | Certa | Baixo | ~200ms + tokens. Necessário pra integridade. A extração informada já economiza evitando duplicatas. |
| Testes quebram por depender do branch informado | Certa | Baixo | Adaptar testes é parte do escopo. |
| `importance_category` não propagado corretamente | Baixa | Médio | Testar com mock que verifica importance no ReconciliationDecision. |

## Budget

3 arquivos modificados + testes = **4 arquivos total** (dentro do budget ≤ 6).
