Files
C.E.L_Slide_test2/src/phase_z2_ai_fallback/cache.py
kyeongmin c864fe0479 feat(#61): IMP-33 AI fallback scaffolding (u1~u11, flag default OFF)
Frame-aware AI fallback module scaffolded under src/phase_z2_ai_fallback/
with master flag ai_fallback_enabled=False; normal-path AI call count
remains 0. AI output constrained to builder_options_patch /
partial_overrides / slot_mapping_proposal; MDX / frame_id / raw HTML /
raw CSS mutations rejected at schema layer. IMP-46 cache gate (cache.py)
raises AiFallbackCacheGateError unless visual_check_passed AND
user_approved. Step 12 wires AI repair after IMP-30 provisional payload
only; Step 17 stays blocked behind IMP-34 / IMP-35 prerequisites.
AST isolation guard forbids fallback package from importing Phase Q /
Kei / pipeline runtime symbols. Docs IMP-17 / IMP-31 bound to runtime
module surface via 11-row structural test pin (test_docs_sync.py) so
drift fails CI.

Tests: 116 fallback / 161 phase_z2 regression / 526 scoped full sweep
all passing. Existing pre-IMP-33 fixture issue in scripts/test_phase_t_*
remains untouched (out of scope).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:46:49 +09:00

83 lines
3.0 KiB
Python

"""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."
)