# Spec: Write Pipeline — Observability + Empty Message Guard

> Generated from: `.vibeflow/prds/write-pipeline-observability.md`

## Objective

Mensagens vazias retornam imediatamente sem gastar LLM/embedding, e o trace do write pipeline expõe modo efetivo, batch count e se houve retry de relations.

## Context

O trace do extraction step já registra entities/facts/relations count e mode. Faltam: `effective_mode` (multi_pass que caiu pra single_pass), `batch_count`, `relation_retry_triggered`. Além disso, mensagens com só whitespace passam pelo pipeline inteiro desnecessariamente.

## Definition of Done

1. `run_write_pipeline()` retorna imediatamente (sem criar evento, sem chamar LLM/embedding) se `message.strip()` é string vazia — com `success=True` e todos os campos zerados.
2. `_extract_relations()` retorna uma flag `retry_triggered: bool` junto com as relations (tupla ou dataclass).
3. `_multi_pass_extract()` inclui no `ExtractionOutput` (ou retorna separado) os metadados: `effective_mode`, `batch_count`, `relation_retry_triggered`.
4. O trace step "extraction" em `run_write_pipeline()` inclui os campos `effective_mode`, `batch_count`, `relation_retry_triggered` no dict `data`.
5. Testes unitários cobrem: (a) mensagem vazia retorna early, (b) retry de relation sinaliza `relation_retry_triggered=True`, (c) trace contém os novos campos.

## Scope

- Guard clause em `run_write_pipeline()` para mensagem vazia
- `_extract_relations()` retornar tupla `(relations, retry_triggered)`
- `_multi_pass_extract()` retornar metadados extras (via campos em `ExtractionOutput` ou dict separado)
- Trace enrichment no step "extraction"
- Testes

## Anti-scope

- Não mudar `ExtractionOutput` se não for necessário — pode usar dict de metadados separado
- Não mudar interface pública (`WriteResult`, `PipelineTrace`, `PipelineStep`)
- Não adicionar config
- Não mexer no read pipeline
- Não mexer no single-pass extraction (reflexion)
- Não adicionar `input_class` no trace

## Technical Decisions

1. **Guard clause primeiro, antes de qualquer DB/LLM** — mensagem vazia nem cria `MemoryEvent`. Retorna `WritePipelineResult` com defaults.
2. **Tupla em `_extract_relations()`** — retorna `tuple[list[ExtractedRelation], bool]` em vez de criar dataclass. Simples, sem overhead.
3. **Dict de metadados em `_multi_pass_extract()`** — em vez de mudar `ExtractionOutput`, `_multi_pass_extract()` retorna `tuple[ExtractionOutput, dict]` com metadados `{"effective_mode": str, "batch_count": int, "relation_retry_triggered": bool}`.
4. **`extract_from_message()` propaga metadados** — retorna `tuple[ExtractionOutput, dict]`. O pipeline consome o dict no trace.

## Applicable Patterns

- `.vibeflow/patterns/pipeline-orchestration.md` — pipeline stage pattern
- `.vibeflow/patterns/error-handling.md` — fail-safe defaults
- `.vibeflow/patterns/llm-interaction.md` — LLM call patterns

## Risks

- **Breaking change em `extract_from_message()` signature** — agora retorna tupla em vez de `ExtractionOutput`. Qualquer caller precisa se adaptar. Mitigação: só `run_write_pipeline()` chama essa função; verificar se há outro caller.

## Budget

≤ 4 files:
1. `src/arandu/write/extract.py`
2. `src/arandu/write/pipeline.py`
3. `tests/test_write_pipeline.py`
4. `docs/en/concepts/write-pipeline.md` + `docs/pt-BR/concepts/write-pipeline.md` (contam como 1 se são espelho)
