Add Phase Z runtime foundation
- add visual fit classifier, router, retry, and failure routing modules - add composition planner and catalog-driven mapper - add Phase Z pipeline orchestration and architecture docs
This commit is contained in:
237
src/phase_z2_failure_router.py
Normal file
237
src/phase_z2_failure_router.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""Phase Z-2 retry_failure_classifier + next_action_router (A4 — 분류 / 매핑만).
|
||||
|
||||
A3 (zone_ratio_retry) 의 결과 (retry_trace) 를 받아 :
|
||||
1. **retry_failure_classifier** : 실패 type 을 4 종 중 하나로 분류
|
||||
2. **next_action_router** : failure_type → next_proposed_action 매핑
|
||||
|
||||
본 module 은 ***분류 + 매핑까지만***. layout_adjust / frame_reselect / details_popup
|
||||
실행 X. retry_trace 에 `failure_classification` + `next_action_proposal` 두 필드 추가.
|
||||
|
||||
**잠근 매핑** (사용자 잠금 — 2026-04-29) :
|
||||
|
||||
| failure_type | next_proposed_action |
|
||||
|---|---|
|
||||
| donor_slack_insufficient | layout_adjust |
|
||||
| no_donor_candidates | layout_adjust |
|
||||
| rerender_still_fails | frame_reselect |
|
||||
| not_attempted | none |
|
||||
|
||||
**escalation 단계 hierarchy** (이번 기본 매핑이 따르는 원칙) :
|
||||
```
|
||||
layout_adjust (가장 가벼움 — zone 배치만 변경)
|
||||
↓ 그래도 안 되면
|
||||
frame_reselect (중간 — frame 자체 변경)
|
||||
↓ 그래도 안 되면
|
||||
details_popup_escalation (가장 invasive — content popup, 마지막 resort)
|
||||
```
|
||||
|
||||
`details_popup_escalation` 은 본 매핑에 *없음* — tabular_overflow / structural_major_overflow /
|
||||
frame_reselect 실패 이후 단계에서 다룸 (별 step).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# ─── §A4-1 failure_type registry ──────────────────────────────────
|
||||
|
||||
FAILURE_TYPE_DESCRIPTIONS: dict[str, str] = {
|
||||
"not_attempted": (
|
||||
"retry was not attempted (router_active=False or zone_ratio_retry "
|
||||
"not in proposed actions). 정상 path 의 일부 — 실패 X"
|
||||
),
|
||||
"donor_slack_insufficient": (
|
||||
"primary donor 의 slack 이 target_added_px 보다 작음. 현재 layout 안 "
|
||||
"redistribution 한도 도달"
|
||||
),
|
||||
"no_donor_candidates": (
|
||||
"donor 후보 자체 없음 — single layout / sibling visual fail / capacity "
|
||||
"mismatch / slack 0 등의 이유로 zone redistribution 불가"
|
||||
),
|
||||
"rerender_still_fails": (
|
||||
"redistribution 실행 + rerender 까지 했는데도 visual_check 실패. "
|
||||
"현재 frame/zone 조합이 content 와 맞지 않음"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ─── §A4-2 next_action mapping (사용자 잠금) ──────────────────────
|
||||
|
||||
NEXT_ACTION_BY_FAILURE: dict[str, str] = {
|
||||
"donor_slack_insufficient": "layout_adjust",
|
||||
"no_donor_candidates": "layout_adjust",
|
||||
"rerender_still_fails": "frame_reselect",
|
||||
"not_attempted": "none",
|
||||
}
|
||||
|
||||
NEXT_ACTION_RATIONALE: dict[str, str] = {
|
||||
"donor_slack_insufficient": (
|
||||
"현재 layout 안 redistribution 끝남 → 다른 layout topology 검토 "
|
||||
"(layout_adjust). frame 자체는 아직 의심 대상 X"
|
||||
),
|
||||
"no_donor_candidates": (
|
||||
"donor 자체 없거나 모두 막힘 → layout topology 부터 재구성하여 "
|
||||
"sibling/space 다시 만들어 보는 게 우선 (layout_adjust). frame 변경은 그 다음"
|
||||
),
|
||||
"rerender_still_fails": (
|
||||
"redistribution + rerender 까지 했는데도 visual fail → 현재 "
|
||||
"frame/zone 조합 자체 부적합, V4 top-k 의 다른 frame 평가 (frame_reselect). "
|
||||
"popup 직행은 아직 빠름 (tabular / structural_major 가 아닌 한)"
|
||||
),
|
||||
"not_attempted": (
|
||||
"retry 시도 자체가 없었음 (visual ok 등) — escalation 불필요"
|
||||
),
|
||||
}
|
||||
|
||||
# 본 매핑이 가리키는 next action 들의 *현재 코드* 구현 상태
|
||||
NEXT_ACTION_IMPLEMENTATION_STATUS: dict[str, str] = {
|
||||
"layout_adjust": "MISSING",
|
||||
"frame_reselect": "MISSING",
|
||||
"none": "n/a",
|
||||
}
|
||||
|
||||
|
||||
# ─── classifier ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def classify_retry_failure(retry_trace: dict) -> Optional[dict]:
|
||||
"""retry_trace → failure classification.
|
||||
|
||||
Returns:
|
||||
None : retry 가 *성공* 한 case (retry_passed=True). 분류할 failure 없음.
|
||||
dict : {failure_type, classification_rule}
|
||||
"""
|
||||
# case 0 : retry 성공 — failure 없음
|
||||
if retry_trace.get("retry_passed"):
|
||||
return None
|
||||
|
||||
# case 1 : retry 시도 자체 안 됨 (router_active=False 또는 다른 action)
|
||||
if not retry_trace.get("retry_attempted"):
|
||||
return {
|
||||
"failure_type": "not_attempted",
|
||||
"classification_rule": (
|
||||
"retry_attempted=False — router_active=False or zone_ratio_retry "
|
||||
"not in proposed_actions"
|
||||
),
|
||||
}
|
||||
|
||||
# case 2 : plan 단계 실패 (rerender 안 일어남)
|
||||
plan = retry_trace.get("plan") or {}
|
||||
if plan and not plan.get("feasible"):
|
||||
reason = (plan.get("failure_reason") or "")
|
||||
reason_lower = reason.lower()
|
||||
|
||||
# donor slack insufficient — primary donor 가 있으나 slack 부족
|
||||
if (
|
||||
"primary donor" in reason_lower
|
||||
and "slack" in reason_lower
|
||||
and "target_added_px" in reason_lower
|
||||
):
|
||||
return {
|
||||
"failure_type": "donor_slack_insufficient",
|
||||
"classification_rule": (
|
||||
"plan.feasible=False AND failure_reason matches "
|
||||
"'primary donor ... slack ... target_added_px ...'"
|
||||
),
|
||||
}
|
||||
|
||||
# no donor candidates — sibling 자체 없거나 모두 자격 미달
|
||||
if "no donor candidates" in reason_lower:
|
||||
return {
|
||||
"failure_type": "no_donor_candidates",
|
||||
"classification_rule": (
|
||||
"plan.feasible=False AND failure_reason matches "
|
||||
"'no donor candidates'"
|
||||
),
|
||||
}
|
||||
|
||||
# 위 두 패턴 미매칭 — 보수적으로 no_donor_candidates 로 분류
|
||||
# (donor 가 거의 모두 막힌 경우 와 구조적으로 비슷)
|
||||
return {
|
||||
"failure_type": "no_donor_candidates",
|
||||
"classification_rule": (
|
||||
f"plan.feasible=False, failure_reason did not match known patterns. "
|
||||
f"defaulting to 'no_donor_candidates'. raw failure_reason: {reason!r}"
|
||||
),
|
||||
}
|
||||
|
||||
# case 3 : plan feasible AND rerender 했는데 visual fail
|
||||
if retry_trace.get("rerender_attempted") and not retry_trace.get("retry_passed"):
|
||||
return {
|
||||
"failure_type": "rerender_still_fails",
|
||||
"classification_rule": (
|
||||
"plan.feasible=True AND rerender_attempted=True AND retry_passed=False"
|
||||
),
|
||||
}
|
||||
|
||||
# case 4 (defensive) : 어떤 case 에도 안 잡힘 — 보수적 fallback
|
||||
return {
|
||||
"failure_type": "not_attempted",
|
||||
"classification_rule": (
|
||||
"no failure pattern matched (defensive fallback). retry_trace 구조 "
|
||||
"예상과 다름 — 검토 필요"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ─── router ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def route_retry_failure(failure_type: str) -> dict:
|
||||
"""failure_type → next_proposed_action mapping.
|
||||
|
||||
Returns:
|
||||
dict :
|
||||
next_proposed_action
|
||||
next_action_rationale
|
||||
next_action_implementation_status
|
||||
mapping_source
|
||||
"""
|
||||
next_action = NEXT_ACTION_BY_FAILURE.get(failure_type)
|
||||
if next_action is None:
|
||||
return {
|
||||
"next_proposed_action": None,
|
||||
"next_action_rationale": (
|
||||
f"failure_type '{failure_type}' has no mapping in NEXT_ACTION_BY_FAILURE"
|
||||
),
|
||||
"next_action_implementation_status": "unknown",
|
||||
"mapping_source": "no mapping (unknown failure_type)",
|
||||
}
|
||||
return {
|
||||
"next_proposed_action": next_action,
|
||||
"next_action_rationale": NEXT_ACTION_RATIONALE.get(failure_type, ""),
|
||||
"next_action_implementation_status": NEXT_ACTION_IMPLEMENTATION_STATUS.get(
|
||||
next_action, "unknown"
|
||||
),
|
||||
"mapping_source": "A4 NEXT_ACTION_BY_FAILURE (사용자 잠금 2026-04-29)",
|
||||
}
|
||||
|
||||
|
||||
# ─── enrichment wrapper ──────────────────────────────────────────
|
||||
|
||||
|
||||
def enrich_retry_trace_with_failure_classification(retry_trace: dict) -> dict:
|
||||
"""retry_trace 에 `failure_classification` + `next_action_proposal` 두 필드 추가.
|
||||
|
||||
Mutates retry_trace in place AND returns it.
|
||||
|
||||
retry_passed=True 인 경우 → 두 필드 모두 None (failure 없음, escalation 없음).
|
||||
"""
|
||||
fc = classify_retry_failure(retry_trace)
|
||||
if fc is None:
|
||||
# retry succeeded — no failure to classify
|
||||
retry_trace["failure_classification"] = None
|
||||
retry_trace["next_action_proposal"] = None
|
||||
return retry_trace
|
||||
|
||||
failure_type = fc["failure_type"]
|
||||
nr = route_retry_failure(failure_type)
|
||||
|
||||
retry_trace["failure_classification"] = {
|
||||
"failure_type": failure_type,
|
||||
"failure_type_description": FAILURE_TYPE_DESCRIPTIONS.get(failure_type, ""),
|
||||
"classification_rule": fc["classification_rule"],
|
||||
}
|
||||
retry_trace["next_action_proposal"] = nr
|
||||
return retry_trace
|
||||
Reference in New Issue
Block a user