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>
This commit is contained in:
89
src/phase_z2_ai_fallback/router.py
Normal file
89
src/phase_z2_ai_fallback/router.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""IMP-33 u7 — AI fallback router (fallback path only).
|
||||
|
||||
Composes the IMP-33 fallback flow:
|
||||
|
||||
1. flag gate (``settings.ai_fallback_enabled`` default OFF)
|
||||
2. V4 route gate (route must equal ``ai_adaptation_required``)
|
||||
3. cache read (u6 stub returns ``None`` until IMP-46 lands)
|
||||
4. build prompt (u3)
|
||||
5. call client (u4 ``request_proposal``)
|
||||
6. validate (u5 ``validate_proposal``)
|
||||
|
||||
Returns the validated ``AiFallbackProposal``. Save to cache is NOT
|
||||
performed here — it is caller-driven AFTER ``visual_check_passed=True``
|
||||
AND ``user_approved=True``, per the u6 IMP-46 gate. The router does not
|
||||
import ``save_proposal``; this is the structural guarantee that the
|
||||
router cannot persist a proposal before the caller's visual + user
|
||||
checks (`feedback_artifact_status_naming`).
|
||||
|
||||
Guardrails:
|
||||
|
||||
* PZ-1 — normal-path AI call count stays 0: flag-off OR route-mismatch
|
||||
short-circuits BEFORE the prompt builder or client are touched.
|
||||
* ``feedback_ai_isolation_contract`` — MDX READ-ONLY (u3 enforces in
|
||||
prompt; this module never reads or writes MDX).
|
||||
* ``feedback_phase_z_spacing_direction`` — V4 rank-1 protected (u5
|
||||
enforces; router only forwards the contract).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from src.config import settings
|
||||
from src.phase_z2_ai_fallback.cache import read_proposal
|
||||
from src.phase_z2_ai_fallback.client import AiFallbackClient
|
||||
from src.phase_z2_ai_fallback.prompts import (
|
||||
V4_ROUTE_AI_ADAPTATION,
|
||||
build_ai_fallback_prompt,
|
||||
)
|
||||
from src.phase_z2_ai_fallback.schema import AiFallbackProposal
|
||||
from src.phase_z2_ai_fallback.validate import validate_proposal
|
||||
|
||||
|
||||
def route_ai_fallback(
|
||||
*,
|
||||
cache_key: str,
|
||||
v4_result: dict[str, Any],
|
||||
frame_contract: dict[str, Any],
|
||||
frame_visual_html: str,
|
||||
figma_partial_json: dict[str, Any],
|
||||
internal_region: dict[str, Any],
|
||||
mdx_text: str,
|
||||
client: AiFallbackClient | None = None,
|
||||
) -> AiFallbackProposal | None:
|
||||
"""Route a fallback request through cache → prompt → client → validate.
|
||||
|
||||
Returns ``None`` when the master flag is OFF or when the V4 route is
|
||||
not ``ai_adaptation_required`` — both gates short-circuit BEFORE any
|
||||
prompt/client work, so the normal-path AI call count stays at 0
|
||||
(PZ-1).
|
||||
"""
|
||||
if not settings.ai_fallback_enabled:
|
||||
return None
|
||||
route = v4_result.get("route") or v4_result.get("imp05_route_hint")
|
||||
if route != V4_ROUTE_AI_ADAPTATION:
|
||||
return None
|
||||
cached = read_proposal(cache_key)
|
||||
if cached is not None:
|
||||
validate_proposal(
|
||||
cached,
|
||||
frame_contract=frame_contract,
|
||||
internal_region=internal_region,
|
||||
)
|
||||
return cached
|
||||
prompt = build_ai_fallback_prompt(
|
||||
v4_result=v4_result,
|
||||
frame_contract=frame_contract,
|
||||
frame_visual_html=frame_visual_html,
|
||||
figma_partial_json=figma_partial_json,
|
||||
internal_region=internal_region,
|
||||
mdx_text=mdx_text,
|
||||
)
|
||||
active_client = client if client is not None else AiFallbackClient()
|
||||
proposal = active_client.request_proposal(prompt)
|
||||
validate_proposal(
|
||||
proposal,
|
||||
frame_contract=frame_contract,
|
||||
internal_region=internal_region,
|
||||
)
|
||||
return proposal
|
||||
Reference in New Issue
Block a user