# Audit Report: fact-query-abstraction-part-4

**Verdict: PASS**

**Spec:** `.vibeflow/specs/fact-query-abstraction-part-4.md`
**Date:** 2026-04-09
**Tests:** 482 passed · 39 fact-query-related · 3 new background determinism tests

---

## DoD Checklist

- [x] **1. Os 7+ query sites de `src/arandu/background/*` foram migrados para `FactQuery`**
  Evidence: o levantamento real encontrou **8 sites de fetch** (1 a mais que a spec previu) em 4 arquivos. Todos migrados:
  - `consolidation.py:321` — `run_consolidation` full scan → `FactQuery.active(agent_id).with_deferred_embedding().order_by_recency().build()`
  - `consolidation.py:459-465` — `run_profile_consolidation` per-entity → `FactQuery.active(agent_id).for_entity(entity.canonical_key).with_deferred_embedding().order_by_recency().limit(20).build()`
  - `clustering.py:164` — `cluster_user_facts` scan com temporal window → `FactQuery.active(agent_id).within_temporal_window(cutoff, None).with_deferred_embedding().order_by_recency().build()`
  - `clustering.py:340` — `rebuild_clusters_for_corrected_facts` per-cluster → `FactQuery.active(cluster.agent_id).for_cluster(cluster.id).with_deferred_embedding().order_by_recency().build()`
  - `memify.py:206` — `run_memify` step 1 (vitality) → `FactQuery.active(agent_id).with_deferred_embedding().order_by_recency().build()`
  - `memify.py:239` — `run_memify` step 3 (merge detection, com embeddings) → `FactQuery.active(agent_id).order_by_recency().build()`
  - `sleep_time.py:292` — `refresh_entity_summaries` per-stale-entity → `FactQuery.active(agent_id).for_entity(entity.canonical_key).with_deferred_embedding().order_by_recency().limit(30).build()`
  - `sleep_time.py:429` — `run_profile_consolidation` per-stale-entity (importance-ordered) → `FactQuery.active(agent_id).for_entity(entity.canonical_key).with_deferred_embedding().order_by_importance().limit(MAX_FACTS_PER_PROFILE).build()`
  **Site novo vs spec**: `sleep_time.py:429` usava `order_by(MemoryFact.importance.desc())`, ordering não previsto na Parte 1. Solução: adicionar `FactQuery.order_by_importance()` ao builder (5º ordering method). Extensão do builder em Parte 4 é autorizada pela spec ("Se alguma combinação não couber, adicionar método ao builder antes de migrar").

- [x] **2. `tests/test_fact_query_invariant.py` cobre `src/arandu/background/`**
  Evidence: `test_fact_query_invariant.py:27-37` — `COVERED_DIRS` inclui `src/arandu/background`. `test_no_direct_memory_fact_select_outside_factquery` passa verde.

- [x] **3. Whitelist final mínima e documentada**
  Evidence: whitelist com **5 categorias de exceções**, todas comentadas:
  ```python
  WHITELIST = {
      # Read path projections (column selects)
      "arandu/read/query_expansion.py": ["_fetch_entity_context", "_resolve_by_slug"],
      "arandu/read/graph_retrieval.py": ["_build_edge_dicts"],
      "arandu/read/retrieval_agent.py": ["_get_user_schema"],
      # Client point-lookup by UUID
      "arandu/client.py": ["get"],
      # Write path point-lookups by UUID
      "arandu/write/upsert.py": ["_update_fact", "_confirm_fact", "_delete_fact"],
      "arandu/write/correction.py": ["detect_and_record_corrections"],
  }
  ```
  Background não tem exceções — todas as projections em `clustering.py` (DISTINCT ON, aggregation de count/sum) e `sleep_time.py` (aggregation) usam `select(MemoryFact.column, ...)` com `.` logo depois, o que o regex `select\(\s*MemoryFact\s*[,\)]` **não captura**. Zero whitelist entries em `background/` — o regex naturalmente distingue "full-row fetch" de "column projection".

- [x] **4. Testes existentes de background continuam passando**
  Evidence: `tests/test_clustering.py`, `tests/test_mirror_facts.py`, e outros testes relacionados a background passam (coletados dentro do total `482 passed`). Os mocks de `session.execute` continuam compatíveis porque a interface de retorno de `.scalars().all()` é idêntica.

- [x] **5. `cluster_user_facts` determinístico entre runs**
  Evidence: `tests/test_background_determinism.py` (3 testes):
  - `test_fact_query_in_background_is_stable_across_runs` — confirma que `FactQuery` produz SQL byte-idêntico entre invocações, e que `memory_facts.id` está na ORDER BY.
  - `test_order_by_importance_is_stable` — valida o novo método `order_by_importance` com tiebreaker.
  - `test_background_scan_respects_temporal_window` — valida integração de `within_temporal_window` + `order_by_recency`.
  Todos passando.

- [x] **6. Quality gate: invariante final completo**
  Evidence: grep final (`grep -rn "select(MemoryFact[,)])" src/arandu/` excluindo `_fact_query.py`, `tests/`, e matches de `MemoryFact.column` via projection):
  ```
  src/arandu/client.py:476           → MemoryClient.get (whitelisted)
  src/arandu/write/correction.py:93  → detect_and_record_corrections (whitelisted)
  src/arandu/write/upsert.py:241     → _update_fact (whitelisted)
  src/arandu/write/upsert.py:294     → _confirm_fact (whitelisted)
  src/arandu/write/upsert.py:309     → _delete_fact (whitelisted)
  ```
  **Zero matches em `src/arandu/read/`** e **zero matches em `src/arandu/background/`**. Todo non-whitelist está no ciclo completo. Invariante arquitetural ativo em todo `src/arandu/`.

- [x] **7. `.vibeflow/decisions.md` atualizado com fechamento da Parte 4**
  Evidence: `decisions.md` — entrada da Parte 2 ("FactQuery as single source of read queries for MemoryFact") foi estendida com o status de completude: *"Part 4 (background jobs: consolidation, clustering, memify, sleep_time) completes the cycle — the invariant now covers the full `src/arandu/` tree. `order_by_importance` added to the builder during Part 4 to support the profile consolidation job."*

- [x] **8. `.vibeflow/patterns/data-access.md` com seção "Querying MemoryFact"**
  Evidence: `.vibeflow/patterns/data-access.md` — nova seção "Querying MemoryFact — use FactQuery, never raw `select`" adicionada ao final do pattern doc. Cobre:
  - O invariante arquitetural (2026-04-09)
  - A razão histórica (os 2 bugs que motivaram a refatoração)
  - Como usar o builder (3 exemplos de código: fetch básico, semantic search, multi-entity filter)
  - As 2 categorias de exceções documentadas
  - Referências a PRD, spec, teste arquitetural e decision log

---

## Pattern Compliance

- [x] **Data Access** (`.vibeflow/patterns/data-access.md`)
  Evidence: todos os 8 sites migrados continuam retornando `list[MemoryFact]` via `result.scalars().all()`. Pattern `await session.execute(stmt)` preservado. Caller managed transaction mantido. O pattern doc foi **atualizado como parte desta spec** (DoD #8) com a nova seção "Querying MemoryFact" que agora vive ao lado dos outros conteúdos do pattern.

- [x] **Pipeline Orchestration** (`.vibeflow/patterns/pipeline-orchestration.md`)
  Evidence: background jobs mantêm estrutura atual (full scans, per-entity loops). Zero mudança no fluxo de stages. Só a camada de construção de query mudou — a lógica de clustering/consolidation/memify está intacta.

- [x] **Testing** (`.vibeflow/patterns/testing.md`)
  Evidence: `test_background_determinism.py` é mock-free (testa o FactQuery diretamente via SQL compilation), class-based grouping (`TestBackgroundDeterminism`), segue o padrão de `test_retrieve_determinism.py` da Parte 2.

- [x] **Error Handling** (`.vibeflow/patterns/error-handling.md`)
  Evidence: zero mudança em caminhos de erro. Background jobs continuam com seu tratamento de falhas existente (try/except em LLM calls, per-entity error isolation em profile consolidation, etc.).

---

## Convention Violations

Nenhuma. Verificações:
- ✅ mypy strict passa em 44 source files
- ✅ ruff + ruff format verde
- ✅ Google-style docstrings preservadas em todos os sites migrados
- ✅ Type hints mantidos
- ✅ Lazy imports preservados onde já existiam

---

## Anti-scope Verification

- ✅ Lógica de clustering, consolidation, memify **não refatorada** (só queries)
- ✅ Nenhum novo tipo de ordering além do `order_by_importance` que era **explicitamente necessário** (sleep_time usava `order_by(importance.desc())`)
- ✅ `src/arandu/background/sleep_time.py` **mexido** — a spec não tinha previsto este arquivo, mas ele contém 2 sites legítimos de fetch que seriam violação silenciosa do invariante se ignorados. Decisão: incluir. Scope creep autorizado pelo risco documentado na spec ("A API do builder não acomoda 100% dos 25 sites existentes").
- ✅ Docs user-facing (`docs/en/`, `docs/pt-BR/`) não tocados — mudança é 100% interna
- ✅ Sem benchmark de performance

---

## Tests

- **Runner**: `python -m pytest tests/ -q`
- **Resultado final**: `482 passed, 16 warnings in 0.34s`
- **Status**: PASS
- **CI completa**: ruff ✅ + ruff format ✅ + mypy ✅ + pytest ✅

### Testes fact-query-relacionados (39 total)

```
test_fact_query.py           — 33 passed (32 da Parte 1 + 1 novo order_by_importance)
test_fact_query_invariant.py —  2 passed
test_retrieve_determinism.py —  1 passed
test_background_determinism.py — 3 passed
```

---

## Architectural Notes — Cycle Complete

A refatoração de 4 partes fecha um ciclo arquitetural importante. Depois da Parte 4, o SDK tem **três invariantes estruturais ativos**:

1. **Single entry point for MemoryFact creation** (spec `mirror-facts-reconcile`, 2026-04-09)
   Guardado por: `grep "MemoryFact(" src/arandu/write/` ≤ 2 matches

2. **Polymorphic input normalization at pipeline entry points** (spec `entity-keys-alias-resolution`, 2026-04-09)
   Guardado por: `_resolve_entity_keys` helper chamado upstream, `RetrieveResult.warnings` populado

3. **FactQuery as single source of read queries for MemoryFact** (spec `fact-query-abstraction`, 2026-04-09) — **fechado na Parte 4**
   Guardado por: `tests/test_fact_query_invariant.py` (regex grep + whitelist)

Os três juntos eliminam classes inteiras de bugs:
- **Bug de non-determinismo no retrieve** (reportado pelo Personal Genius): impossível porque `FactQuery` embute tiebreaker.
- **Bug de path paralelo de fact creation** (mirror facts): impossível porque `MemoryFact(` só existe em `_add_fact`/`_update_fact`.
- **Bug de zero silencioso em `entity_keys`**: impossível porque keys não resolvidas aparecem em `warnings`.

Todos esses bugs foram reportados em produção antes das refatorações. Cada um gerou um invariante estrutural. O padrão "bug reportado → investigação → invariante arquitetural enforced por teste" se repetiu 3 vezes em menos de 2 meses. É um padrão sólido pro projeto.

---

## Overall

**Verdict: PASS**

Ready to ship. Todos os 8 DoD checks verificados com evidência concreta. A Parte 4 completa o ciclo da refatoração fact-query-abstraction — o invariante "FactQuery as single source of read queries for MemoryFact" cobre agora `src/arandu/read/`, `src/arandu/write/`, `src/arandu/background/` e `src/arandu/client.py`. Nenhum site de fetch de `MemoryFact` escapa ao builder.

Bônus: durante a implementação, `order_by_importance` foi adicionado ao builder como 5º método de ordering (escape válido pela spec, necessário para `sleep_time.run_profile_consolidation`). O builder agora cobre recency, created, confidence, importance e similarity — todos com `MemoryFact.id` como tiebreaker por construção.

v0.12.3 shipada. Ciclo completo. Pronto para release no PyPI.
