"""Phase Z-2 overflow_router v0 (A2 — 정책 매핑 layer 만). fit_classifier 의 출력 (category) 를 spec §4 의 *proposed_action* 으로 매핑하는 layer. 본 module 은 ***매핑까지만***. 실제 action 실행은 별도 step (A3+). 출력 = 각 classification 에 proposed_action 추가 + router 전체 summary. 원칙 : - classifier = 사실 분류 (category 결정) - router = 정책 결정 (그 category 면 무엇을 *제안* 할 것인가) - 본 단계는 *제안 trace* 만. pipeline behavior / abort 정책 / rerender 변경 X - 실행 안 됨 → 현재 코드는 여전히 visual_check_passed=False 시 sys.exit(1) 그러나 debug.json 에 *어떤 action 이 제안됐는지* 가 기록됨 다음 step (별도 — A3) : zone_ratio_retry action 의 *실제 구현* — 지금 spec §4 mapping 의 가장 자주 트리거되는 action. """ from __future__ import annotations from typing import Optional # ─── §4 mapping table (spec PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC §4) ── # category → proposed_action (primary) ACTION_BY_CATEGORY: dict[str, str] = { "minor_overflow": "zone_ratio_retry", "moderate_overflow": "layout_adjust", "structural_minor_overflow": "zone_ratio_retry", "structural_major_overflow": "details_popup_escalation", "tabular_overflow": "details_popup_escalation", "frame_capacity_mismatch": "frame_reselect", "layout_zone_mismatch": "layout_adjust", "hard_visual_fail": "abort", } # 매핑 근거 — *왜 이 category 면 이 action 인가* trace 용 ACTION_RATIONALE: dict[str, str] = { "minor_overflow": "1.5 줄 미만 text/label flow → zone 양보 / spacing 재계산으로 fit 가능", "moderate_overflow": "1.5~4 줄 text/label → layout/zone ratio 재분배 필요", "structural_minor_overflow": "structural unit boundary spill (<1 unit drop) → zone 양보로 fit, 단위 자르기 X", "structural_major_overflow": "1+ structural unit 완전 잘림 → 의미 손실, popup 으로 escalate", "tabular_overflow": "표는 행 단위로 잘리면 의미 손실 → popup escalate (또는 table-friendly frame reselect)", "frame_capacity_mismatch": "composition capacity_fit 가 이미 mismatch 신호 → V4 top-k 의 다른 frame 평가", "layout_zone_mismatch": "frame root 자체 overflow → layout preset 변경 또는 zone 키움", "hard_visual_fail": "위 매핑 모두 미적용 — 마지막 fallback (현재 코드는 sys.exit 으로 abort)", } # 각 action 의 *현재 코드* 구현 상태 (2026-04-29 기준) # A2 단계에서 이 매핑이 *어디까지 자동 처리되고 어디서 막히는지* trace 확보용 ACTION_IMPLEMENTATION_STATUS: dict[str, str] = { "zone_ratio_retry": "IMPLEMENTED", # A3 (2026-04-29) phase_z2_retry.plan_zone_ratio_retry + pipeline orchestration "layout_adjust": "MISSING", "details_popup_escalation": "MISSING", # CLAUDE.md 의
원칙은 있음, runtime 미구현 "frame_reselect": "PARTIAL", # IMP-05 pre-render rank-2/3 fallback implemented; post-render rerender trace-only "adapter_needed": "PARTIAL", # composition v0.1.1 의 mapper FitError catch "abort": "IMPLEMENTED", # sys.exit(1) — pipeline 의 현재 default } # ─── 단일 분류 → routing 결과 ───────────────────────────────────── def route_action(category: str) -> dict: """category → proposed_action mapping 결과 (단일). Returns: dict : proposed_action : action 이름 (또는 None) rationale : *왜* 이 action 인가 implementation_status : implemented / partial / missing / unknown mapping_source : "spec §4 ACTION_BY_CATEGORY" 또는 "no mapping" """ action = ACTION_BY_CATEGORY.get(category) if action is None: return { "proposed_action": None, "rationale": f"category '{category}' has no mapping in ACTION_BY_CATEGORY", "implementation_status": "unknown", "mapping_source": "no mapping (unknown category)", } return { "proposed_action": action, "rationale": ACTION_RATIONALE.get(category, ""), "implementation_status": ACTION_IMPLEMENTATION_STATUS.get(action, "unknown"), "mapping_source": "spec §4 ACTION_BY_CATEGORY", } # ─── fit_classification 전체 → router decision ────────────────── def route_fit_classification(fit_classification: dict) -> dict: """fit_classification 의 모든 classifications 에 proposed_action 추가 + summary. 각 classification 에 다음 필드를 *추가* (기존 필드 보존) : - proposed_action - proposed_action_rationale - proposed_action_implementation_status - proposed_action_mapping_source Returns: router decision summary dict : router_active : True/False (visual_check_passed=False 일 때만 True) proposed_actions_summary : unique action 들 sorted list implementation_status_summary : {status: count} dict routed_count : 처리된 classification 수 routed_details : per-classification routing trace missing_actions_pending_impl : 본 routing 에서 *현재 미구현* 인 action 모음 note : 사용자 안내 텍스트 """ if fit_classification.get("visual_check_passed", True): return { "router_active": False, "proposed_actions_summary": [], "implementation_status_summary": {}, "routed_count": 0, "routed_details": [], "missing_actions_pending_impl": [], "note": "visual check passed — no overflow to route", } classifications = fit_classification.get("classifications", []) or [] routed_details = [] for cls in classifications: category = cls.get("category", "hard_visual_fail") routing = route_action(category) # classification entry 에 proposed_action 정보 *추가* (기존 필드 보존) cls["proposed_action"] = routing["proposed_action"] cls["proposed_action_rationale"] = routing["rationale"] cls["proposed_action_implementation_status"] = routing["implementation_status"] cls["proposed_action_mapping_source"] = routing["mapping_source"] routed_details.append({ "source": cls.get("source"), "zone_position": cls.get("zone_position"), "category": category, "proposed_action": routing["proposed_action"], "implementation_status": routing["implementation_status"], }) # summary actions_seen = sorted({ r["proposed_action"] for r in routed_details if r["proposed_action"] is not None }) status_breakdown: dict[str, int] = {} missing_actions: list[str] = [] for r in routed_details: s = r["implementation_status"] status_breakdown[s] = status_breakdown.get(s, 0) + 1 if s == "MISSING" and r["proposed_action"] not in missing_actions: missing_actions.append(r["proposed_action"]) return { "router_active": True, "proposed_actions_summary": actions_seen, "implementation_status_summary": status_breakdown, "routed_count": len(routed_details), "routed_details": routed_details, "missing_actions_pending_impl": sorted(missing_actions), "note": ( "router 는 category → proposed_action 매핑까지 담당. 실제 action 실행은 " "pipeline 의 별도 orchestrator 가 처리 (예: zone_ratio_retry 는 " "_attempt_zone_ratio_retry 에서 실행). proposed_action 의 implementation_status " "가 IMPLEMENTED 이면 pipeline 이 시도하고 결과는 retry_trace 에 기록, " "MISSING 이면 그 action 은 실행 X 이고 기존 abort/status 흐름 (sys.exit(1)) 으로 종료." ), }