"""AnalysisStore — the seam the slow path persists its AnalysisRecord through. The Assembler produces an `AnalysisRecord` (the faithful, structured record of a run — §8.3, INV-4). Persisting it is a separate concern from streaming the answer, so it sits behind this one-method seam. `NullAnalysisStore` is the default: it logs that a record was produced but stores nothing, because the backing table does not exist yet. The plan is to store records in the **same catalog DB** (Neon `dataeyond`, `settings.postgres_connstring`). TODO(persistence): add a Postgres-backed `AnalysisStore` writing an `analysis_records` table in the catalog DB, keyed on (business_context_id, plan_id, created_at), then inject it into ChatHandler. """ from __future__ import annotations from typing import Protocol, runtime_checkable from src.middlewares.logging import get_logger from .schemas import AnalysisRecord logger = get_logger("analysis_store") @runtime_checkable class AnalysisStore(Protocol): """Persist a completed analysis. Implementations must never raise on the caller's path — a persistence failure must not break the user's answer.""" async def save(self, record: AnalysisRecord) -> None: ... class NullAnalysisStore: """Default no-op store: logs the record, persists nothing (no table yet).""" async def save(self, record: AnalysisRecord) -> None: logger.info( "analysis_record produced (not persisted — no store configured)", plan_id=record.plan_id, business_context_id=record.business_context_id, n_tasks=len(record.tasks_run), )