"""IMP-33 u9 — Step 17 AI repair wiring (BLOCKED until IMP-34 + IMP-35 land). Phase Z Step 17 = retry / salvage cascade (see ``src.phase_z2_pipeline`` section 11.7 ``_attempt_salvage_chain`` and the existing IMP-12 u8/u9 deterministic chain at ``src/phase_z2_pipeline.py:1994`` and ``src/phase_z2_pipeline.py:4948``). Per IMP-17 carve-out (``docs/architecture/IMP-17-CARVE-OUT.md`` lines 16, 40-44), AI repair at Step 17 is permitted ONLY after the full deterministic chain is exhausted AND popup escalation is exhausted AND a user-approved fallback budget remains. IMP-34 (zone resize + compact retry) and IMP-35 (``details_popup_escalation``) are explicit prerequisites under the IMP-33 out-of-scope contract — neither has landed yet. Therefore Step 17 AI repair is STRUCTURALLY BLOCKED at u9. This module: 1. **SPECIFIES** the canonical overflow cascade order via :data:`OVERFLOW_CASCADE_ORDER` — ``deterministic`` → ``popup`` → ``ai_repair`` → ``user_override``. Downstream Step 17 consumers can rely on this single source of truth. 2. **KEEPS** Step 17 AI repair structurally blocked. The entry point :func:`gather_step17_ai_repair_proposals` does NOT import ``route_ai_fallback`` (u7), does NOT instantiate ``AiFallbackClient`` (u4), and does NOT call any Anthropic API. Every unit is recorded with ``skip_reason="step17_ai_blocked_imp_34_35_prerequisites_missing"`` so the caller can distinguish "blocked by carve-out gate" from any other skip path (e.g., u8 ``not_provisional`` / ``design_reference_only_no_ai``). Once IMP-34 + IMP-35 land AND a user-approved fallback budget is granted, this module will gain the actual ``route_ai_fallback`` wiring guarded by the cascade-stage conjunction. Today the gate is closed. """ from __future__ import annotations from enum import Enum from typing import Any, Callable, Iterable class OverflowCascadeStage(str, Enum): """Step 17 overflow cascade stages — canonical order (u9 single source of truth). Members are ordered to match the AI isolation contract: * ``DETERMINISTIC`` — IMP-12 u4/u5/u6 (``cross_zone_redistribute`` / ``glue_compression`` / ``font_step_compression``) + IMP-12 terminal actions (``layout_adjust`` / ``frame_reselect``) + IMP-34 (``zone resize + compact retry``, pending). No AI in any sub-stage. * ``POPUP`` — IMP-35 (``details_popup_escalation``, pending). Content popup escalation as the final deterministic resort before any AI. * ``AI_REPAIR`` — IMP-33 (this carve-out) + IMP-46 cache. Only reachable after DETERMINISTIC and POPUP are both exhausted AND user-approved fallback budget remains. * ``USER_OVERRIDE`` — explicit user override after all auto stages. """ DETERMINISTIC = "deterministic" POPUP = "popup" AI_REPAIR = "ai_repair" USER_OVERRIDE = "user_override" OVERFLOW_CASCADE_ORDER: tuple[OverflowCascadeStage, ...] = ( OverflowCascadeStage.DETERMINISTIC, OverflowCascadeStage.POPUP, OverflowCascadeStage.AI_REPAIR, OverflowCascadeStage.USER_OVERRIDE, ) STEP17_AI_REPAIR_BLOCKED_REASON = ( "step17_ai_blocked_imp_34_35_prerequisites_missing" ) def gather_step17_ai_repair_proposals( units: Iterable[Any], *, route_for_label: Callable[[str | None], str | None], ) -> list[dict]: """Return one BLOCKED record per unit. No AI call is performed at u9. The record schema mirrors :func:`src.phase_z2_ai_fallback.step12 .gather_step12_ai_repair_proposals` so the Step 17 artifact consumer can reuse the same shape, with one addition: ``cascade_stage`` pins the stage this record belongs to (always ``ai_repair`` here). Per Stage 2 contract (IMP-33 u9): Step 17 AI repair is blocked behind IMP-34 + IMP-35. Every unit returns with ``skip_reason=STEP17_AI_REPAIR_BLOCKED_REASON`` and ``ai_called=False``. """ records: list[dict] = [] for index, unit in enumerate(units): label = getattr(unit, "label", None) record: dict = { "unit_index": index, "source_section_ids": list( getattr(unit, "source_section_ids", []) or [] ), "frame_template_id": getattr(unit, "frame_template_id", None), "label": label, "route_hint": route_for_label(label), "provisional": bool(getattr(unit, "provisional", False)), "cascade_stage": OverflowCascadeStage.AI_REPAIR.value, "ai_called": False, "skip_reason": STEP17_AI_REPAIR_BLOCKED_REASON, "proposal": None, "error": None, } records.append(record) return records