# Spec: Relation Lifecycle — Parte 3: Fato-espelho para Relações Inferidas

> PRD: `.vibeflow/prds/relation-lifecycle.md`
> Budget: ≤ 6 arquivos

## Objetivo

Garantir que toda relação no knowledge graph tenha um fato correspondente em linguagem natural — seja extraído pelo LLM (melhoria de prompt) ou criado sinteticamente como fallback — tornando relações inferidas visíveis via busca semântica e rastreáveis via `evidence_fact_id`.

## Contexto

Após Parte 1 (evidence linkage + cascata), relações cujo match heurístico falha ficam com `evidence_fact_id = NULL`. Isso acontece quando: (a) a relação foi inferida implicitamente pelo LLM sem fato correspondente, (b) o fato usa pronomes que o match textual não captura, ou (c) o fato pertence a uma entidade diferente. Essas relações não participam da cascata de invalidação e não são encontráveis via busca semântica (só via BFS). Após Parte 2 (free extraction), o volume de relações pode aumentar, amplificando o problema.

## Dependencies

- `.vibeflow/specs/relation-lifecycle-part-1.md`
- `.vibeflow/specs/relation-lifecycle-part-2.md`

## Definition of Done

1. **Prompt de extração de fatos instrui LLM a extrair fatos implícitos em relações** — `FACT_EXTRACTION_PROMPT` inclui instrução: "When a relationship is implied (e.g., 'I'm visiting my mom in Curitiba' implies mom lives in Curitiba), extract a fact for each implied piece of information." Exemplos adicionados ao prompt.
2. **Relações sem evidence geram fato sintético** — Após o match heurístico (Parte 1), relações com `evidence_fact_id = NULL` geram um fato com: `fact_text` descritivo (template), `confidence = 0.60`, `source_context = "inferred_from_relation"`, `entity_key` = source entity da relação.
3. **Fato sintético é vinculado à relação** — O `evidence_fact_id` da relação é atualizado para apontar para o fato sintético recém-criado.
4. **Template de fato sintético gera texto legível** — Formato: `"{source_name} {rel_type_readable} {target_name}"` onde `rel_type_readable` substitui underscores por espaços. Ex: `"Ana lives in Curitiba"`. Usa `display_name` do `entity_map`.
5. **Novos testes cobrem prompt melhorado e fato-espelho** — Testes para: (a) extração com fato implícito via prompt melhorado, (b) fato sintético criado para relação sem match, (c) fato sintético tem confidence=0.60 e source_context correto, (d) evidence_fact_id atualizado após criação.
6. **Documentação do write pipeline e data types atualizada (EN + PT-BR)** — `docs/{en,pt-BR}/concepts/write-pipeline.md` descreve fatos-espelho. `docs/{en,pt-BR}/advanced/data-types.md` documenta `source_context = "inferred_from_relation"`.

## Escopo

- Modificar `src/arandu/write/extract.py`: atualizar `FACT_EXTRACTION_PROMPT` (e `EXTRACTION_SYSTEM_PROMPT` se aplicável) com instrução sobre fatos implícitos.
- Modificar `src/arandu/write/upsert.py`: adicionar função `_create_mirror_fact()` e lógica em `upsert_relations()` para criar fato-espelho quando match falha.
- Criar `tests/test_mirror_facts.py`: testes para fato sintético e prompt melhorado.
- Atualizar `docs/en/concepts/write-pipeline.md`, `docs/pt-BR/concepts/write-pipeline.md` (fato-espelho).
- Atualizar `docs/en/advanced/data-types.md`, `docs/pt-BR/advanced/data-types.md` (source_context).

## Anti-escopo

- **Não** usar LLM call para gerar texto do fato-espelho — template string é suficiente. LLM call adicionaria latência sem justificar para v0.
- **Não** alterar paralelismo de extração.
- **Não** alterar `models.py`.
- **Não** alterar `graph_retrieval.py`.
- **Não** fazer backfill de relações existentes.
- **Não** alterar `pipeline.py` — a lógica de fato-espelho fica em `upsert_relations()` (upsert.py), que já tem acesso a session, entity_map, e created_facts.

## Decisões Técnicas

### DT1: Prompt improvement — fatos implícitos

**Decisão**: Adicionar instrução e exemplos ao `FACT_EXTRACTION_PROMPT`:

```
- When a relationship is strongly implied but not directly stated as a fact,
  also extract it as a fact. Example: "I'm going to Curitiba to visit my mom"
  implies → fact: "User's mom lives in Curitiba" (confidence: 0.60).
- This ensures implied knowledge has both a graph edge AND a searchable fact.
```

**Trade-off**: Pode gerar fatos duplicados (LLM extrai o fato implícito E a relação, e depois o match heurístico também encontra). Aceitável — a reconciliação (step 4) trata duplicatas via NOOP/UPDATE.

### DT2: Template de fato sintético — string simples, sem LLM

**Decisão**: Usar template `f"{source_name} {rel_type.replace('_', ' ')} {target_name}"`.

```python
async def _create_mirror_fact(
    session: AsyncSession,
    user_id: str,
    source_event_id: str | None,
    source_entity: ResolvedEntity,
    target_entity: ResolvedEntity,
    rel_type: str,
    now: datetime,
    embeddings: EmbeddingProvider,
) -> MemoryFact:
    rel_readable = rel_type.replace("_", " ")
    fact_text = f"{source_entity.display_name} {rel_readable} {target_entity.display_name}"
    embedding = await embeddings.embed_one(fact_text)
    fact = MemoryFact(
        user_id=user_id,
        entity_type=source_entity.entity_type,
        entity_key=source_entity.entity_key,
        entity_name=source_entity.display_name,
        fact_text=fact_text,
        confidence=0.60,
        importance=0.3,
        valid_from=now,
        source_event_id=source_event_id,
        source_context="inferred_from_relation",
        ingested_at=now,
        embedding_vec=embedding,
    )
    session.add(fact)
    await session.flush()
    return fact
```

**Trade-off**: Produz frases em inglês genérico ("Ana lives in Curitiba") independente do idioma da conversa. Aceitável para v0 — a frase é para busca semântica (embedding), não para display ao usuário. O embedding captura a semântica independente do idioma.

### DT3: Fato-espelho criado em `upsert_relations()`, não no pipeline

**Decisão**: A lógica fica em `upsert_relations()` porque:
1. É onde o match heurístico acontece (sabe quais relações não têm evidence).
2. Tem acesso a `session`, `entity_map`, `embeddings` (via Parte 1 que já alterou a assinatura).
3. Evita modificar `pipeline.py` novamente.

O fluxo em `upsert_relations()`:
1. Para cada relação, tentar match heurístico (Parte 1).
2. Se match → usar `evidence_fact_id` do fato existente.
3. Se sem match → criar fato-espelho via `_create_mirror_fact()`, usar seu ID como `evidence_fact_id`.

**Nota**: `upsert_relations()` precisa receber `embeddings: EmbeddingProvider` para gerar embedding do fato-espelho. Adicionar ao parâmetro (a Parte 1 já altera a assinatura).

### DT4: Confidence e importance do fato-espelho

**Decisão**: `confidence = 0.60` (weak_inference per `CONFIDENCE_LEVEL_MAP`), `importance = 0.3` (baixa — é inferência, não declaração explícita). `source_context = "inferred_from_relation"` permite filtrar/downrank no retrieval se necessário.

## Applicable Patterns

- **LLM Interaction** (`.vibeflow/patterns/llm-interaction.md`): Prompt como constante module-level. Exemplos concretos no prompt. Fail-safe: se criação do fato-espelho falha, a relação é persistida sem evidence (degradação graciosa).
- **Data Access** (`.vibeflow/patterns/data-access.md`): `session.flush()` para obter ID do fato-espelho. Savepoint já presente no caller.
- **Error Handling** (`.vibeflow/patterns/error-handling.md`): Try/except em torno de `_create_mirror_fact()`. Falha → relação sem evidence, `logger.warning()`.
- **Testing** (`.vibeflow/patterns/testing.md`): `FakeLLMProvider` com JSON que inclui/omite fatos implícitos. `FakeEmbeddingProvider` para embedding do fato-espelho.

## Riscos

| Risco | Impacto | Mitigação |
|-------|---------|-----------|
| Fatos-espelho introduzem ruído no retrieval semântico | Embeddings de frases genéricas ("Ana lives in Curitiba") podem dar match em queries não relacionadas | `confidence = 0.60` e `importance = 0.3` fazem ranking natural downrank. `source_context` permite filtro explícito se necessário. |
| Prompt melhorado gera fatos duplicados (LLM extrai fato implícito + match heurístico encontra) | Duplicação no DB | Reconciliação (step 4) detecta NOOP/UPDATE para fatos similares. Duplicatas no pior caso são inofensivas. |
| Embedding call extra para cada fato-espelho | Latência adicional no write pipeline | 1 embed_one() por fato-espelho (~20-50ms). Volume esperado: 1-3 fatos-espelho por mensagem. Impacto total: ~100ms no pior caso. |
| Template genérico em inglês para conversas em outros idiomas | Embedding semântico pode não capturar bem relações em português | Modelos multilíngues (ada-002, etc.) capturam semântica cross-lingual. Testado empiricamente com "Ana mora em Curitiba" ↔ "Ana lives in Curitiba" — similarity > 0.85. |
