# Spec: Relation Lifecycle — Parte 2: Free Relation Extraction + Resolution

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

## Objetivo

Permitir que o LLM extraia relações com tipos livres (sem restrição a `CANONICAL_REL_TYPES`) e normalizar os tipos extraídos via resolution determinística, enriquecendo o knowledge graph com relações que hoje são descartadas ou forçadas em tipos inadequados.

## Contexto

Os prompts de extração (`EXTRACTION_SYSTEM_PROMPT`, `RELATION_EXTRACTION_PROMPT`) listam explicitamente "Allowed types: works_at, manages, ..." — limitando a extração a 11 tipos canônicos. Relações como `mentored_by`, `inspired_by`, `competed_with` são perdidas ou forçadas em tipos genéricos. A função `normalize_rel_type()` em `constants.py` já trata aliases, mas rejeita tipos desconhecidos retornando o string raw (sem validação). O BFS em `graph_retrieval.py` não filtra por `rel_type` — já funciona com tipos dinâmicos. A unique constraint `uq_entity_relationship` inclui `rel_type`, então tipos novos não causam conflito.

## Dependencies

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

## Definition of Done

1. **Prompts de extração não listam tipos permitidos** — `EXTRACTION_SYSTEM_PROMPT` e `RELATION_EXTRACTION_PROMPT` instruem o LLM a extrair `rel_type` livremente, com exemplos ilustrativos (não restritivos). A instrução é "choose a descriptive relationship type" ao invés de "Allowed types: ...".
2. **`normalize_rel_type()` normaliza sem rejeitar** — Aplica aliases existentes (`_REL_TYPE_ALIASES`), normaliza formatação (lowercase, underscores), e retorna o tipo limpo mesmo que não esteja em `CANONICAL_REL_TYPES`. Tipos novos passam sem alteração após sanitização.
3. **`CANONICAL_REL_TYPES` mantido como referência, não como filtro** — O set continua existindo (usado por consumers, documentação, e como hint), mas não é usado para validação ou rejeição.
4. **Testes cobrem free extraction e normalization** — Testes para: (a) extração com tipo fora do canonical (ex: `mentored_by`), (b) normalização de alias → canônico, (c) tipo novo passa sem alteração, (d) formatação sanitizada (espaços, hífens → underscores).
5. **Documentação de data types atualizada (EN + PT-BR)** — `docs/{en,pt-BR}/advanced/data-types.md` documenta que `rel_type` aceita tipos dinâmicos, lista canônicos como exemplos (não como restrição), e explica o comportamento de normalização.

## Escopo

- Modificar `src/arandu/write/extract.py`: atualizar `EXTRACTION_SYSTEM_PROMPT`, `RELATION_EXTRACTION_PROMPT` (e `FACT_EXTRACTION_PROMPT` se mencionar tipos).
- Modificar `src/arandu/constants.py`: atualizar `normalize_rel_type()`, adicionar docstring explicando que `CANONICAL_REL_TYPES` é referência/não filtro.
- Criar `tests/test_relation_resolution.py`: testes para normalização e free extraction.
- Atualizar `docs/en/advanced/data-types.md` e `docs/pt-BR/advanced/data-types.md`.

## Anti-escopo

- **Não** criar módulo separado `relation_resolution.py` — a resolução é determinística (string normalization + alias mapping), não precisa de LLM. `normalize_rel_type()` em `constants.py` é suficiente.
- **Não** criar LLM call para relation resolution — over-engineering. Aliases cobrem ~90% dos sinônimos.
- **Não** alterar `upsert_relations()` — a unique constraint `uq_entity_relationship` já aceita qualquer `rel_type` string.
- **Não** alterar `graph_retrieval.py` — BFS não filtra por tipo.
- **Não** remover `CANONICAL_REL_TYPES` ou `_REL_TYPE_ALIASES` — mantê-los como referência.
- **Não** alterar `models.py`.
- **Não** alterar o paralelismo de extração.

## Decisões Técnicas

### DT1: Prompt rewrite — exemplos ilustrativos, não restritivos

**Decisão**: Substituir "Allowed types: works_at, manages, ..." por instrução aberta com exemplos:

```
RELATIONS: Connections between entities.
- Each relation has: source, target, rel_type
- Use a short, descriptive snake_case type (e.g., works_at, manages, reports_to, lives_in, mentored_by, inspired_by, competed_with).
- Common types: works_at, manages, reports_to, family_of, friend_of, partner_of, owns, lives_in, member_of, studies_at, works_with.
- You may use other types when they better describe the relationship.
```

**Trade-off**: Tipos livres podem gerar variações inconsistentes (`works_for` vs `employed_at` vs `works_at`). Mitigação: `_REL_TYPE_ALIASES` normaliza os sinônimos mais comuns. Tipos realmente novos (sem alias) passam como estão — melhor ter `mentored_by` no grafo do que perder a informação.

### DT2: `normalize_rel_type()` — pass-through com sanitização

**Decisão**: Manter a lógica atual (check canonical → check aliases → fallback), mas o fallback já retorna `cleaned` (o tipo limpo). Atualmente já faz isso — a mudança é apenas de intenção documentada e adição de sanitização mais robusta:

```python
def normalize_rel_type(raw: str) -> str:
    """Normalize a relationship type string.

    Applies alias mapping for known synonyms. Unknown types pass through
    after sanitization (lowercase, underscores). CANONICAL_REL_TYPES is a
    reference set, not a validation filter.
    """
    cleaned = raw.strip().lower().replace(" ", "_").replace("-", "_")
    if cleaned in CANONICAL_REL_TYPES:
        return cleaned
    return _REL_TYPE_ALIASES.get(cleaned, cleaned)
```

A mudança real é no docstring (intenção) e nos prompts (que deixam de restringir). A função em si já tinha o comportamento correto.

### DT3: Sem relation resolution como pipeline step separado

**Decisão**: Não criar um step "relation resolution" no pipeline. A normalização acontece em `normalize_rel_type()`, chamado de `upsert_relations()` (que já chama via `rel.rel_type`).

**Justificativa**: Entity resolution precisa de LLM porque envolve semântica (é "João" o mesmo que "Joãozinho"?). Relation resolution é string matching determinístico — aliases + sanitização. Não justifica um pipeline step, LLM call, ou módulo separado.

## Applicable Patterns

- **LLM Interaction** (`.vibeflow/patterns/llm-interaction.md`): Prompts como constantes module-level. JSON response format. Exemplos no prompt, não restrições hard-coded.
- **Testing** (`.vibeflow/patterns/testing.md`): `FakeLLMProvider` com JSON canned que inclui tipos não-canônicos. Testar o round-trip: extração → normalização → persistência.

## Riscos

| Risco | Impacto | Mitigação |
|-------|---------|-----------|
| LLM gera tipos inconsistentes (muita variação) | Tipos proliferam no DB sem normalização | `_REL_TYPE_ALIASES` cobre os sinônimos mais comuns. Futuramente pode-se adicionar LLM-based clustering de tipos — mas não agora. |
| Tipos longos ou estranhos (ex: `used_to_work_together_at_the_same_company`) | Unique constraint com strings longas | `rel_type` é `String(64)` no model. Sanitização trunca. O LLM tende a gerar tipos curtos dado o exemplo no prompt. |
| Consumers externos que dependem de `CANONICAL_REL_TYPES` como set fechado | Breaking change para quem filtra por tipo | `CANONICAL_REL_TYPES` continua existindo. Consumers que filtram por ele continuam funcionando — apenas não veem tipos novos. Documentar como non-breaking. |
