"""IMP-33 u6 — AI fallback proposal cache (IMP-46 gate, no persistent storage). This module defines the cache contract that IMP-33 callers use to remember AI fallback proposals across runs. The persistent storage layer itself is out-of-scope for IMP-33 and is owned by IMP-46 (frame transformation cache). Behaviour locked by Stage 2 plan (u6): * ``read_proposal(key)`` always returns ``None`` until IMP-46 lands a persistent backend. Callers MUST handle the cache-miss path. * ``save_proposal(key, proposal, *, visual_check_passed, user_approved)`` enforces the IMP-46 gate before any storage write is attempted: - ``visual_check_passed=False`` -> ``AiFallbackCacheGateError`` - ``user_approved=False`` -> ``AiFallbackCacheGateError`` Only when BOTH gates are True does control reach the storage layer, which currently raises ``NotImplementedError`` (the IMP-46 marker). Guardrails: * No Anthropic import; cache is pure proposal bookkeeping. * No MDX read/write; proposals are u2 ``AiFallbackProposal`` instances. * No silent persistence: gate violations are loud, not skipped writes (`feedback_artifact_status_naming`). """ from __future__ import annotations from src.phase_z2_ai_fallback.schema import AiFallbackProposal class AiFallbackCacheGateError(RuntimeError): """Raised when ``save_proposal`` is called without both IMP-46 gates True.""" def read_proposal(key: str) -> AiFallbackProposal | None: """Look up a previously cached proposal by ``key``. IMP-33 ships without a persistent backend; this stub always returns ``None`` so callers exercise the cache-miss path. The persistent backend will be wired by IMP-46. """ if not isinstance(key, str) or not key: raise ValueError("cache key must be a non-empty string") return None def save_proposal( key: str, proposal: AiFallbackProposal, *, visual_check_passed: bool, user_approved: bool, ) -> None: """Persist ``proposal`` under ``key`` once both IMP-46 gates are True. Raises ``AiFallbackCacheGateError`` if either gate is False — the proposal is NOT written. When both gates are True, storage raises ``NotImplementedError`` (the IMP-46 persistent backend has not landed yet). """ if not isinstance(key, str) or not key: raise ValueError("cache key must be a non-empty string") if not isinstance(proposal, AiFallbackProposal): raise TypeError( "proposal must be an AiFallbackProposal instance " f"(got {type(proposal).__name__})" ) if not visual_check_passed: raise AiFallbackCacheGateError( "IMP-46 gate: visual_check_passed=False; refusing to cache an " "unverified proposal." ) if not user_approved: raise AiFallbackCacheGateError( "IMP-46 gate: user_approved=False; refusing to cache without " "explicit user approval." ) raise NotImplementedError( "IMP-46 persistent cache storage is not implemented yet; " "this is the IMP-33 u6 stub marker." )