round 55~73 review-loop lock per Codex #11 final + Claude #13 6-axis L1~L9. Scope (deterministic only) : - pre-render rank-2/3 fallback via lookup_v4_match_with_fallback (selector only, no calculate_fit migration, no AI, no full planner rerun, no layout topology change, no abort behavior change) - Step 9 informative candidate_evidence schema (additive) — v4_label / phase_z_status / catalog_registered / filtered_for_direct_execution / route_hint / decision / reason - Step 20 qualifier fields (additive) — fallback_used / fallback_selection_count / selection_paths[] — top-level enum unchanged - restructure / reject candidates preserved as non-direct evidence with route hints (design_reference_only / ai_adaptation_required) — deferred actual handlers IMP-29/IMP-31 - catalog 1:1 invariant test (separate file tests/test_catalog_invariant.py) — fails fast if template_id/frame_id 1:1 mapping ever breaks - 6 behavior tests fully synthetic with MOCK_ prefix (no real catalog IDs, no v4_full32_result.yaml dependency) — monkeypatch get_contract + compute_capacity_fit (selector has no DI, function signature unchanged) Deferred to follow-up issues : - IMP-30 first-render invariant + abort bypass (zero-unit + section status filter) - IMP-29 frontend zone-level override (deterministic only) - IMP-31 AI-assisted frame-aware adaptation Guardrails locked : no calculate_fit / no AI / no frontend / no full rerun / no layout topology / no abort behavior change / no 1-2 sample hardcoding. Tests : 8/8 pass (6 selector behavior + 2 catalog invariant). Smoke regression : 11/11 partials pass (IMP-04 F17 calibration intact). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
182 lines
8.2 KiB
Python
182 lines
8.2 KiB
Python
"""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 의 <details> 원칙은 있음, 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)) 으로 종료."
|
|
),
|
|
}
|