# Audit Report: Config Enforcement — Spreading Activation + Reranker Timeout

**Verdict: PASS**

**Spec:** `.vibeflow/specs/config-enforcement-spreading-reranker.md`
**Date:** 2026-03-22

## Tests

```
356 passed, 8 warnings in 0.23s
ruff: All checks passed
mypy: Success, no issues found in 41 source files
```

All tests pass. No regressions.

## DoD Checklist

- [x] **DoD 1: `hops=0` desabilita spreading** — `spreading_activation.py:131-133`: guard `if hops == 0: return SpreadingActivationResult()` no topo de `_spread_activation_impl()`, antes de qualquer DB query. Test: `test_hops_zero_returns_empty` verifica retorno vazio e `session.execute.assert_not_called()`.

- [x] **DoD 2: `spreading_decay_factor` é consumido** — `spreading_activation.py:135`: `decay_factor = config.spreading_decay_factor`. Hop 1 entity (linha 178) e cluster (linha 202) usam `decay_factor`. Hop 2 (linha 268) usa `decay_factor ** 2`. Constantes `HOP1_DECAY`/`HOP2_DECAY` removidas. Test: `test_decay_factor_used_in_scoring`.

- [x] **DoD 3: `spreading_facts_per_entity` atua no Hop 1** — `spreading_activation.py:172`: `limit=config.spreading_facts_per_entity` (antes era `FACTS_PER_ENTITY_HOP1=5`). Constante removida. Test: `test_facts_per_entity_config_respected`.

- [x] **DoD 4: Reranker timeout enforced** — `reranker.py:63-71`: `asyncio.wait_for(llm.complete(...), timeout=config.reranker_timeout_sec)`. Timeout catch: `except TimeoutError` (linha 102) com `logger.warning` e fallback. Test: `test_rerank_timeout_fallback` com `SlowLLMProvider` e `reranker_timeout_sec=0.01`.

- [x] **DoD 5: Testes passam** — 356 passed (era 352). 4 novos testes: `test_hops_zero_returns_empty`, `test_decay_factor_used_in_scoring`, `test_facts_per_entity_config_respected`, `test_rerank_timeout_fallback`.

## Pattern Compliance

- [x] **Error Handling** — Reranker timeout segue fail-safe: `except TimeoutError` → `logger.warning()` → retorna ranking original. Spreading `hops=0` retorna resultado vazio (safe default).
- [x] **Pipeline Orchestration** — Spreading configs controlam sub-stage. pipeline.py não tocado (anti-scope respeitado).
- [x] **Testing** — Mock-based: `AsyncMock` para session no spreading, `SlowLLMProvider` com `asyncio.sleep(10)` para timeout.

## Convention Violations

Nenhuma.

## Anti-scope Verification

- [x] Default values — não mudados
- [x] Configs novas — nenhuma adicionada
- [x] `FACTS_PER_CLUSTER_HOP1` — mantida como constante (linha 35)
- [x] `KG_REL_HOP_DECAY` — mantida como constante (linha 41)
- [x] `META_OBSERVATIONS_LIMIT` — mantida como constante (linha 36)
- [x] Trace step para spreading — não adicionado
- [x] `plan_override` — não implementado
- [x] pipeline.py — não tocado

## Budget

Files changed: 6 / ≤ 6 budget
1. `src/arandu/read/spreading_activation.py` (modified)
2. `src/arandu/read/reranker.py` (modified)
3. `tests/test_spreading_activation.py` (modified)
4. `tests/test_read_pipeline.py` (modified)
5. `docs/en/concepts/read-pipeline.md` (modified)
6. `docs/pt-BR/concepts/read-pipeline.md` (modified)

## Behavioral Change Note

Per spec Risk #1 and #2: defaults agora produzem scores ligeiramente diferentes:
- Hop 1 decay: 0.50 (antes 0.60 hardcoded) — ~17% menor
- Hop 2 decay: 0.25 (antes 0.35 hardcoded) — ~29% menor
- Hop 1 facts per entity: 3 (antes 5 hardcoded)

Isso é intencional — a config agora é a fonte da verdade. Documentado nos docs EN + PT-BR.
