# Audit Report: Living Entity Profiles

**Spec:** `.vibeflow/specs/living-entity-profiles.md`
**Date:** 2026-03-30
**Verdict: PASS**

## Tests
- ruff check: PASS
- ruff format: PASS
- mypy: PASS (0 errors in 43 source files)
- pytest: PASS (404 passed, 0 failed)

## DoD Checklist

- [x] **1. `profile_text` existe no model** — `models.py:244-245` adiciona `profile_text: Mapped[str | None] = mapped_column(Text, nullable=True)` e `profile_refreshed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)` ao `MemoryEntity`. Migration `alembic/versions/003_add_entity_profile.py` com `upgrade()` que faz `ADD COLUMN profile_text TEXT` e `ADD COLUMN profile_refreshed_at TIMESTAMP WITH TIME ZONE`, e `downgrade()` que remove ambas.

- [x] **2. Extração informada recebe perfis como contexto** — `informed_extract.py:181-208` implementa `load_entity_profiles()` que carrega perfis via `SELECT canonical_key, profile_text WHERE profile_text IS NOT NULL`. `build_extraction_context()` (`informed_extract.py:222`) aceita `entity_profiles` param. Quando perfil existe pra uma entidade, usa `"Profile of {entity}:\n  {profile}"` como contexto (`:260-266`). Quando não existe, fallback pra top-K fatos por embedding similarity (`:269-305`). Testado em `test_informed_extract.py::TestEntityProfiles::test_profile_used_as_context_instead_of_facts` e `test_fallback_to_facts_when_no_profile`.

- [x] **3. Extração informada retorna perfis atualizados** — Prompt inclui `"RULES FOR ENTITY PROFILES"` (`:98-104`) e pede `"updated_profiles"` no formato JSON (`:111`). Parser em `informed_extract.py:467-472` extrai `updated_profiles` do output como dict[str, str | None], validando tipos. Campo é OPCIONAL — se ausente, retorna dict vazio. Retornado em `extraction_meta["updated_profiles"]`. Testado em `test_informed_extract.py::TestEntityProfiles::test_updated_profiles_parsed_from_output`, `test_missing_updated_profiles_returns_empty`, `test_null_profile_preserved`.

- [x] **4. Pipeline persiste perfis** — `pipeline.py:432-454` após `execute_upsert`, lê `extraction_meta.get("updated_profiles")`. Pra cada entidade com profile não-null: resolve entity_key via entity_map, SELECT o MemoryEntity, seta `profile_text` e `profile_refreshed_at = now()`, `session.add()`. Tudo dentro do savepoint existente da pipeline. Fail-safe: try/except com `logger.warning`.

- [x] **5. Context compression usa perfis** — `context_compression.py:140-161` injeta perfis como "Tier 0" antes do hot tier. Formato: `"═══ ENTITY PROFILES ═══"` + `"[About {entity}]\n  {profile}"`. Budget-aware: trunca se excede `budget_hot`. `read/pipeline.py:392-393` carrega perfis via `load_entity_profiles()` pras entidades dos top candidates e passa pra `compress_context(entity_profiles=...)`. Testado em `test_context_compression.py::test_profile_injected_in_context`, `test_profile_injected_before_hot_tier`, `test_no_profiles_no_section`, `test_empty_profiles_no_section`.

- [x] **6. Cold start cria perfil inicial** — Com base vazia: `load_entity_profiles` retorna {} → `build_extraction_context` cai no fallback de fatos (que também retorna vazio) → prompt recebe "EXISTING KNOWLEDGE: None" → LLM extrai normalmente e gera perfil inicial via `updated_profiles` → pipeline persiste o perfil. A partir da segunda mensagem, perfil é injetado como contexto. Testado em `test_informed_extract.py::TestInformedExtraction::test_cold_start_works`.

- [x] **7. Segue patterns do projeto** — `updated_profiles` parseado fail-safe (campo ausente → {}). Profile persistence em try/except com `logger.warning`. Pipeline não faz commit. Savepoint ativo. `load_entity_profiles` usa SQLAlchemy `select()` pattern. `read/pipeline.py` carrega perfis com a mesma função `load_entity_profiles` (reutilização). Logging via `logger.info` com structured key=value.

## Pattern Compliance

- [x] **Pipeline Orchestration** — Perfis carregados e persistidos dentro do `run_write_pipeline()`, no savepoint existente. Timing preservado. Trace não afetado (profiles são um sub-step do upsert). Evidência: `pipeline.py:432-454`.

- [x] **LLM Interaction** — `updated_profiles` parseado do mesmo JSON da extração informada. Fail-safe: se `data.get("updated_profiles")` não é dict, retorna {}. Se valor não é str/None, ignora. Prompt como constante module-level. Evidência: `informed_extract.py:98-112,467-472`.

- [x] **Data Access** — Profile load: `select(MemoryEntity.canonical_key, MemoryEntity.profile_text).where(...)` padrão SQLAlchemy. Profile save: `SELECT` + update attrs + `session.add()` dentro de savepoint. Evidência: `informed_extract.py:199-208`, `pipeline.py:443-452`.

- [x] **Error Handling** — Profile persistence fail-safe: try/except envolvendo o loop inteiro, `logger.warning`. Se falhar, fatos já foram persistidos normalmente. `updated_profiles` ausente/malformado → perfil existente mantido. Evidência: `pipeline.py:435,453-454`.

## Convention Violations
Nenhuma.

## Notes
- `read/pipeline.py` recebeu uma modificação mínima (2 linhas: load profiles + pass to compress_context). Não estava explicitamente no scope da spec, mas é requisito implícito para que `compress_context` receba os perfis. O import `load_entity_profiles` reutiliza a função de `write/informed_extract.py`.
- `summary_text` e `refresh_entity_summaries` ficaram intactos — os dois mecanismos coexistem conforme spec.
