# Spec: Verbose Mode — Dados intermediários dos pipelines

> Source PRD: `.vibeflow/prds/verbose-mode.md`

## Objective

Permitir que `write()` e `retrieve()` retornem dados intermediários de cada estágio do pipeline via `verbose=True`, sem alterar o comportamento existente.

## Context

O `run_write_pipeline()` já possui todos os dados intermediários (extraction, entity_map, decisions, upsert_result) como variáveis locais — mas eles são descartados na construção do dict de retorno. O mesmo acontece no `run_read_pipeline()` (plan, candidates, graph_result, scored_facts). O verbose mode captura esses dados já existentes e os expõe no resultado. Não é necessário alterar a lógica dos estágios — apenas capturar o que já existe.

Nota: o write pipeline já salva um `trace_json` parcial no `MemoryEvent` (linhas 214-228 de write/pipeline.py). O verbose mode é mais completo e retornado ao caller, não persistido.

## Definition of Done

1. **PipelineTrace existe** — `src/arandu/tracing.py` contém `PipelineTrace` e `PipelineStep` dataclasses, com `add_step(name, data, duration_ms)` e `to_dict()`.
2. **WriteResult tem campo pipeline** — `WriteResult.pipeline` é `PipelineTrace | None`, default `None`. Quando `verbose=True`, contém steps: `extraction`, `entity_resolution`, `reconciliation`, `upsert`.
3. **RetrieveResult tem campo pipeline** — `RetrieveResult.pipeline` é `PipelineTrace | None`, default `None`. Quando `verbose=True`, contém steps: `planning`, `retrieval`, `reranking`, `scoring`, `compression`.
4. **Zero breaking changes** — `write()` e `retrieve()` sem `verbose` retornam resultados idênticos ao comportamento atual (pipeline=None).
5. **Cada step tem duration_ms** — Cada `PipelineStep` registra o tempo de execução do seu estágio em milissegundos.
6. **Testes passam** — `tests/test_verbose.py` testa write verbose e retrieve verbose com mocks, e `pytest` passa sem regressões.

## Scope

### Novo arquivo: `src/arandu/tracing.py`
```python
@dataclass
class PipelineStep:
    name: str
    duration_ms: float
    data: dict

@dataclass
class PipelineTrace:
    steps: list[PipelineStep]

    def add_step(name, data, duration_ms) -> None
    def to_dict() -> dict
```

### Modificar: `src/arandu/client.py`
- Adicionar `verbose: bool = False` a `write()` e `retrieve()`
- Adicionar `pipeline: PipelineTrace | None = None` a `WriteResult` e `RetrieveResult`
- Quando verbose=True, criar `PipelineTrace()` e passar para `run_write_pipeline()` / `run_read_pipeline()`
- Incluir o trace no resultado

### Modificar: `src/arandu/write/pipeline.py`
- Adicionar parâmetro `trace: PipelineTrace | None = None` a `run_write_pipeline()`
- Após cada estágio, capturar dados e registrar no trace:
  - **extraction:** `{"mode": config.extraction_mode, "entities": [...], "facts": [...], "relations": [...]}`
  - **entity_resolution:** `{"entities": [{"name": ..., "key": ..., "method": ..., "is_new": ..., "fuzzy_score": ...}]}`
  - **reconciliation:** `{"decisions": [{"fact_text": ..., "action": ..., "matched_fact_text": ..., "confidence": ...}]}`
  - **upsert:** `{"added": N, "updated": N, "confirmed": N, "deleted": N}`

### Modificar: `src/arandu/read/pipeline.py`
- Adicionar parâmetro `trace: PipelineTrace | None = None` a `run_read_pipeline()`
- Após cada estágio, capturar dados:
  - **planning:** `{"strategy": ..., "entities": [...], "similarity_query": ..., "broad_query": bool}`
  - **retrieval:** `{"total_candidates": N, "semantic_count": N, "keyword_count": N, "graph_facts": N, "graph_edges_traversed": N}`
  - **reranking:** `{"applied": bool, "candidates_before": N}`
  - **scoring:** `{"top_k": [{"fact_id": ..., "value": ..., "score": ..., "scores": {...}}]}`
  - **compression:** `{"context_length": N, "facts_included": N}`

### Novo arquivo: `tests/test_verbose.py`
- Test write verbose: verificar que pipeline tem 4 steps com nomes corretos
- Test retrieve verbose: verificar que pipeline tem 5 steps com nomes corretos
- Test sem verbose: verificar que pipeline é None
- Usar FakeLLMProvider e FakeEmbeddingProvider existentes

## Anti-scope

- NÃO alterar assinaturas das funções internas (extract_from_message, resolve_entities, etc.) — o trace é adicionado APENAS em run_write_pipeline e run_read_pipeline
- NÃO persistir o trace no banco
- NÃO adicionar logging quando verbose=True
- NÃO instrumentar background jobs
- NÃO criar endpoint FastAPI para verbose
- NÃO alterar prompts LLM
- NÃO alterar __init__.py exports (PipelineTrace é internal, não público)

## Technical Decisions

| Decisão | Escolha | Justificativa |
|---------|---------|---------------|
| Onde fica o trace | `src/arandu/tracing.py` (novo) | Isolado, sem poluir constants.py ou client.py |
| Como passar o trace | Parâmetro opcional `trace: PipelineTrace \| None = None` | Zero overhead quando None; não altera interface existente |
| Onde capturar dados | Dentro de run_write_pipeline e run_read_pipeline | Os dados já existem como variáveis locais; não precisa mudar estágios internos |
| Timing por step | `time.perf_counter()` antes/depois de cada estágio | Mesmo padrão já usado no pipeline total |
| Serialização | `to_dict()` retorna dicts primitivos | JSON-serializável pra consumo por APIs (Inspector) |
| Campo no resultado | `pipeline: PipelineTrace \| None = None` | Opt-in, default None, backward compatible |
| Entity data no extraction step | Serializar ExtractedEntity/Fact/Relation via `.model_dump()` | São Pydantic models, serializam limpo |
| Reconciliation data | Serializar ReconciliationDecision manualmente | É dataclass, não Pydantic; usar dict comprehension |

## Applicable Patterns

Sem `.vibeflow/patterns/`. Convenções inferidas:
- Dataclasses para data types (WriteResult, ScoredFact, ReadResult)
- `time.perf_counter()` para timing
- Lazy imports dentro de métodos (client.py importa pipeline dentro de write/retrieve)
- Pydantic models usam `.model_dump()` para serialização
- Testes usam FakeLLMProvider e FakeEmbeddingProvider de conftest.py

## Risks

| Risco | Mitigação |
|-------|-----------|
| Serialização falha em algum tipo inesperado (UUID, datetime) | `to_dict()` converte UUIDs para str e datetimes para ISO format |
| Overhead de memória com muitos fatos no trace | Dados são dicts pequenos; mesmo 1000 fatos = ~100KB. Aceitável pra debug tool |
| Testes de verbose são frágeis se pipeline mudar | Testes verificam estrutura (nomes dos steps) não valores exatos |

## Budget

6 arquivos: `tracing.py` (novo), `client.py`, `write/pipeline.py`, `read/pipeline.py`, `test_verbose.py` (novo), `__init__.py` (export se necessário)
