# PRD: Mirror Facts passam por Reconciliação

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

## Problem

O `_create_mirror_fact()` em `src/arandu/write/upsert.py` cria fatos sintéticos pra relações que não encontraram evidence fact match via heurística de string. Esses mirror facts **bypassam completamente** o pipeline de reconciliação (`reconcile_facts`) e `execute_upsert`, resultando em três bugs simultâneos:

1. **Speaker perdido:** mirror facts têm `speaker=None` porque `_create_mirror_fact` nem aceita parâmetro `speaker`.
2. **Duplicatas não detectadas:** mirror facts não passam por `reconcile_facts()` → o LLM nunca vê a chance de marcar como NOOP/UPDATE contra fatos já existentes. Resultado: dois fatos sobre a mesma coisa ("Pedro Menezes lives in Brazil" + "Pedro Menezes lives in Brasil") no mesmo evento.
3. **Inconsistência de idioma:** mirror facts são montados por template (`f"{source} {rel} {target}"`), não pelo LLM — se `display_name` ou `rel_type` vieram em PT (input português) enquanto os outros fatos saíram em EN, o mirror fact polui a memória com idioma divergente.

O caso concreto reportado em produção (v0.11.6):
- Input PT-BR, 5 fatos extraídos em EN, 1 mirror fact em PT duplicando fato 3
- `fact_id=3ec12bfd`: "Pedro Menezes lives in Brasil", speaker=None, confidence=0.60
- Mesma entidade, mesmo attribute, mesmo source_event_id, diferente só no spelling

A raiz é arquitetural: `_create_mirror_fact` existe como um caminho paralelo que **contorna o pipeline canônico** (extract → resolve → reconcile → upsert). Qualquer melhoria futura na reconciliação (melhores prompts, dedup semântica mais apertada, tratamento multilingue) também tem que ser replicada ou rotada pelo mirror path. Débito técnico crescendo.

## Target Audience

- Consumidores do Arandu SDK (especialmente o assistente pessoal do Pedro, que é o primeiro caso de uso real a expor o bug)
- Qualquer dev usando o SDK pra gravar conversas bilíngues ou com entidades cujo nome pode aparecer variado

## Proposed Solution

Unificar mirror fact creation ao pipeline de reconciliação. Em vez de `_create_mirror_fact` instanciar um `MemoryFact` direto via `session.add()`, ele deve:

1. Montar um `ReconciliationDecision` sintético (ação ADD) com o fact_text gerado por template.
2. Passar essa decision pelo mesmo `reconcile_facts()` / `_add_fact()` que os fatos normais usam.
3. Propagar o `speaker_name` desde o `run_write_pipeline()` até a criação do mirror fact.

Assim, mirror facts ganham automaticamente:
- Speaker provenance (via propagação normal)
- Dedup semântica (LLM marca NOOP se já existe algo equivalente)
- Qualquer melhoria futura no reconciler

E de graça: como o reconciler é a única porta de entrada, um único teste valida que todos os caminhos propagam speaker corretamente.

## Success Criteria

1. Reproduzindo o cenário exato do bug report (mesmos dois writes, memória zerada), o segundo write gera **no máximo 5 fatos** — o mirror fact duplicado vira NOOP ou é detectado antes de virar row no banco.
2. Todos os fatos criados num write têm `speaker == speaker_name` (nenhum com `speaker=None`).
3. A relação `(Pedro, lives_in, Brasil)` ainda consegue ter `evidence_fact_id` populado — via matching heurístico melhorado OU via o próprio mirror fact que agora passa por reconcile (cenário onde realmente não existe fato equivalente).
4. Testes unitários existentes de `mirror_facts.py` continuam passando (com ajustes necessários pra nova assinatura).
5. CI verde (ruff + mypy + pytest) sem warnings novos.

## Scope v0

- Refatorar `_create_mirror_fact()` pra retornar um `ReconciliationDecision` em vez de criar `MemoryFact` direto (ou receber `speaker` e chamar `_add_fact` internamente).
- Modificar `upsert_relations()` pra, no caso de fallback, chamar o novo path que passa por reconcile.
- Propagar `speaker_name` desde `run_write_pipeline()` → `upsert_relations()` → mirror fact creation.
- Ajustar testes existentes em `tests/test_mirror_facts.py` pra novo fluxo.
- Atualizar docs EN/PT-BR se o comportamento mudar de forma user-visible (provavelmente só changelog).
- Bump de versão (patch — é bugfix).

## Anti-scope

- **Melhorar heurística de matching entity-vs-fact-text pra tratar "Brasil"≈"Brazil"** — isso é uma toca de coelho (acentos, sinônimos, plurais). Com mirror facts passando por reconcile, o LLM já resolve isso. Se ainda sobrar caso patológico, vira spec separada.
- **Normalização multilingue de `display_name` ou `rel_type`** — mesma coisa, reconcile resolve.
- **Remover `_create_mirror_fact` completamente** — opção B2 que a gente discutiu. Mirror facts têm propósito legítimo (relações sem fato textual explícito). A gente não tá deletando, só rotando pelo reconcile.
- **Dedup cross-event** — se já existe "Pedro vive em Brasil" de uma semana atrás, e agora chega outro, reconcile já cuida. Não é escopo novo.
- **Refatorar `_find_evidence_fact_for_relation`** — função continua fazendo o matching heurístico inicial; só o fallback muda.
- **Cache de embeddings de mirror fact texts** — prematuro.

## Technical Context

### O que já existe

- **`_create_mirror_fact()`** em `src/arandu/write/upsert.py:147-195`: cria `MemoryFact` direto via `session.add()`, com `confidence=0.60` hardcoded. Não aceita `speaker`.
- **`_add_fact()`** em `upsert.py:209`: já aceita e grava `speaker`. Fluxo canônico.
- **`_update_fact()`** em `upsert.py:252`: idem.
- **`reconcile_facts()`** em `src/arandu/write/reconcile.py`: faz dedup semântica via embedding + LLM reasoning.
- **`execute_upsert()`** em `upsert.py:617`: itera sobre `ReconciliationDecision`s chamando `_add_fact`/`_update_fact` conforme ação.
- **`upsert_relations()`** em `upsert.py:617+`: onde o mirror fact fallback é disparado (linha 715 atual). Já tem acesso a `entity_map` e `created_facts`.
- **`run_write_pipeline()`** em `src/arandu/write/pipeline.py:466-475`: passa `speaker_name` pro `execute_upsert()` mas **não passa** pro `upsert_relations()`.
- **Testes existentes**: `tests/test_mirror_facts.py` cobre criação de mirror fact — precisa ajuste.

### Patterns a seguir

- **Data Access** (`.vibeflow/patterns/data-access.md`): `session.add()` + `flush()`, reconciliação antes de persistência.
- **Error Handling** (`.vibeflow/patterns/error-handling.md`): fail-safe — se reconcile falhar no mirror path, degradar pra comportamento atual (mas logar warning).
- **Pipeline Orchestration** (`.vibeflow/patterns/pipeline-orchestration.md`): manter `_create_mirror_fact` como step interno do `upsert_relations`, sem criar novo estágio de pipeline.

### Constraints

- Mirror facts historicamente tinham `confidence=0.60` (abaixo dos 0.95 normais) como flag de "menos confiável". Se passarem por reconcile, o reconcile pode acabar marcando como ADD normal — tudo bem, continua sendo fato. O source_context="inferred_from_relation" preserva a origem rastreada.
- `reconcile_facts` precisa de `ReconciliationDecision` + embedding. Embedding já é computado no mirror fact atual. Decision pode ser montada inline.

## Open Questions

None.
