feat(#17): generic fallback 탈출 — V4 evidence 확장 + renderable-aware provisional + 게이트 순서 버그 수정

1. V4 evidence 확장 (pipeline_17b_extend_missing_sections.py):
   '결과물이 아니라 프로세스' 원칙 — pipeline_17 과 동일 평가 코드로 누락 3개
   섹션(01-intro/05-1/05-2)만 평가해 v4_full32_result.yaml 병합 (기존 무접촉,
   blind/ANSWER_MAP 불변). 결과: 05-1 → F20 light_edit 0.77 (design-matched!),
   01-intro/05-2 → all-reject (catalog gap 정직 노출 — F19 가 05 주제와 이름까지
   일치하나 partial 없음 → #2 프로모션 최우선 근거)

2. renderable-aware provisional (IMP-30 u1 정밀화):
   rank-1 무조건 승격 → partial 존재 AND (비-reject OR verbatim builder 보유)
   첫 후보 승격. reject+builder 미보유 renderable 의 mapper 네이티브 렌더는
   원문 drop 위험 (F23 1-atom 손실 실측) — 원문 보존 > design 개선 우선순위.

3. 게이트 순서 버그 수정 (_apply_quality_gate_downgrades 추출):
   T28.5d popup 승격 후 재계산 경로에 quality gate 강등 3종(coverage/forbidden/
   consistency) 미적용 → 텍스트 손실이 overall=PASS 로 통과 (mdx05 실측).
   양 경로 공통 헬퍼로 통일 — mdx04 의 #16 시점 PASS 일부가 이 버그 덕이었음을
   정직하게 정정 (현재 PARTIAL + frame mismatch 라벨, 텍스트는 완전).

최종 5-MDX: 전부 missing_atoms=0 (무손실) / 01·03 PASS / 02·04·05 PARTIAL(정직
frame-mismatch 라벨) / mdx01 readiness not_ready→needs_review 개선.
게이트: 1061 passed. SHA baseline 재캡처 (정당 변경).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
kmlee1andClaude Opus 4.8 committed 2026-07-07 13:51:00 +09:00
1 parent 63f4479b05
commit 9b79bf7538
6 files changed
+1868 -58

No files matched your search

+121 -52
View File
@@ -664,6 +664,21 @@ def _build_verbatim_compare_table_2col(unit) -> dict:
}
# issue #17 — verbatim builder 를 보유한 template 집합 (아래 dispatch 와 동기).
# renderable-aware provisional 이 reject 라벨 후보를 승격해도 안전한지(원문
# 무손실 코드 렌더 가능한지) 판단하는 데 사용.
_VERBATIM_BUILDER_TEMPLATE_IDS: frozenset[str] = frozenset({
"three_parallel_requirements",
"three_persona_benefits",
"construction_goals_three_circle_intersection",
"construction_bim_three_usage",
"bim_dx_comparison_table",
"bim_issues_quadrant_four",
"sw_dependency_four_problems",
"pre_construction_model_info_stacked",
})
def _emergency_p4b_build_verbatim_slot_payload(
unit, template_id: str, override_slot_count: Optional[int] = None,
) -> Optional[dict]:
@@ -2797,16 +2812,41 @@ def lookup_v4_match_with_fallback(
# trace entries are left intact (their skip reasons remain accurate).
# Default-off keeps IMP-05 behavior byte-identical.
if allow_provisional:
rank_1_judgment = judgments[0]
# issue #17 — renderable-aware provisional. rank 순서는 유지하되
# 승격 가능 조건: partial HTML 존재 (IMP-95 u6 partial_exists 원칙)
# AND (비-reject 라벨 OR verbatim builder 지원). 근거 실측:
# - partial 없는 frame 승격 → mapper FitError → verbatim 복구 →
# selected_frame_not_applied 라벨 (mdx05 05-2 rank-1 F6)
# - reject 라벨 + verbatim builder 없는 frame 의 mapper 네이티브
# 렌더는 원문 drop 위험 (mdx05 05-2 F23: 1 atom 손실 실측 —
# 원문 보존이 design 개선보다 우선)
# 조건 만족 후보가 없으면 기존대로 rank-1 (동작 보존 — verbatim
# 복구가 텍스트 무손실을 보장).
provisional_judgment = judgments[0]
provisional_rank = 1
for _rank, _judgment in enumerate(judgments, start=1):
_tid = str(_judgment.get("template_id") or "")
if not _b4_partial_exists(_tid):
continue
_label = str(_judgment.get("label") or "")
if _label == "reject" and _tid not in _VERBATIM_BUILDER_TEMPLATE_IDS:
continue
provisional_judgment, provisional_rank = _judgment, _rank
break
provisional_match = _v4_match_from_judgment(
section_id, rank_1_judgment, rank=1
section_id, provisional_judgment, rank=provisional_rank
)
provisional_match.selection_path = "provisional_rank_1"
_prov_path = (
"provisional_rank_1"
if provisional_rank == 1
else f"provisional_renderable_rank_{provisional_rank}"
)
provisional_match.selection_path = _prov_path
provisional_match.fallback_reason = trace["fallback_reason"]
provisional_match.provisional = True
trace.update({
"selection_path": "provisional_rank_1",
"selected_rank": 1,
"selection_path": _prov_path,
"selected_rank": provisional_rank,
"selected_template_id": provisional_match.template_id,
"selected_frame_id": provisional_match.frame_id,
"selected_label": provisional_match.label,
@@ -5870,6 +5910,63 @@ def _compute_density_gate(zones_data: list[dict], overflow: dict) -> dict:
}
def _apply_quality_gate_downgrades(
slide_status: dict,
*,
rendered_text_coverage: dict,
forbidden_rendered_syntax: dict,
applied_render_consistency: dict,
) -> None:
"""issue #17 — quality gate 3종의 overall 강등을 단일 헬퍼로.
두 slide_status 조립 경로(최초 + T28.5d popup 승격 후 재계산)가 동일한
강등 규칙을 공유해야 한다 — 재계산 경로에 이 블록이 없어서 텍스트
손실(rendered_text_coverage fail)이 overall=PASS 로 통과하는 버그가
있었음 (mdx05 05-2 F23 렌더 1-atom 손실 실측).
"""
if not rendered_text_coverage.get("passed"):
slide_status.setdefault("quality_gate_failures", []).append(
"rendered_text_coverage_missing"
)
slide_status.setdefault("visual_fail_reasons", []).append(
"rendered_text_coverage_missing: "
f"{rendered_text_coverage.get('missing_atoms_count')} source text atom(s) "
"not visible in final.html"
)
if slide_status.get("overall") == "PASS":
slide_status["overall"] = "PARTIAL_COVERAGE"
if not forbidden_rendered_syntax.get("passed"):
slide_status.setdefault("quality_gate_failures", []).append(
"forbidden_rendered_syntax"
)
slide_status.setdefault("visual_fail_reasons", []).append(
"forbidden_rendered_syntax: "
f"{forbidden_rendered_syntax.get('forbidden_syntax_count')} JSX/CSS/code "
"fragment(s) visible in final.html"
)
if slide_status.get("overall") == "PASS":
slide_status["overall"] = "PARTIAL_COVERAGE"
if not applied_render_consistency.get("passed"):
for failure in applied_render_consistency.get("failures") or []:
slide_status.setdefault("quality_gate_failures", []).append(failure)
if applied_render_consistency.get("frame_mismatch_count"):
slide_status.setdefault("visual_fail_reasons", []).append(
"selected_frame_not_applied: "
f"{applied_render_consistency.get('frame_mismatch_count')} zone(s) "
"rendered with a different template than Step 6 selected"
)
if "final_layout_positions_mismatch" in (applied_render_consistency.get("failures") or []):
slide_status.setdefault("visual_fail_reasons", []).append(
"final_layout_positions_mismatch: "
f"expected {applied_render_consistency.get('expected_positions')} but "
f"final.html has {applied_render_consistency.get('final_positions')}"
)
if slide_status.get("overall") == "PASS":
slide_status["overall"] = "PARTIAL_COVERAGE"
def _compute_design_readiness(
*,
units: list[CompositionUnit],
@@ -11310,53 +11407,17 @@ def run_phase_z2_mvp1(
density_gate=density_gate,
)
slide_status["design_readiness"] = design_readiness
if not rendered_text_coverage.get("passed"):
slide_status.setdefault("quality_gate_failures", []).append(
"rendered_text_coverage_missing"
)
slide_status.setdefault("visual_fail_reasons", []).append(
"rendered_text_coverage_missing: "
f"{rendered_text_coverage.get('missing_atoms_count')} source text atom(s) "
"not visible in final.html"
)
if slide_status.get("overall") == "PASS":
slide_status["overall"] = "PARTIAL_COVERAGE"
# IMP-47B u8 — Surface Step 12 AI repair outcomes through slide_status.
# Composes u4 gather errors + u5 apply_status + u7 coverage_invariant
# into a single ``ai_repair_status`` axis the frontend (u11) reads to
# render human_review notifications. Auto pipeline first
# ([[feedback_auto_pipeline_first]]) — no review_queue insertion;
# explicit status enum + human_review_required flag.
if not forbidden_rendered_syntax.get("passed"):
slide_status.setdefault("quality_gate_failures", []).append(
"forbidden_rendered_syntax"
)
slide_status.setdefault("visual_fail_reasons", []).append(
"forbidden_rendered_syntax: "
f"{forbidden_rendered_syntax.get('forbidden_syntax_count')} JSX/CSS/code "
"fragment(s) visible in final.html"
)
if slide_status.get("overall") == "PASS":
slide_status["overall"] = "PARTIAL_COVERAGE"
if not applied_render_consistency.get("passed"):
for failure in applied_render_consistency.get("failures") or []:
slide_status.setdefault("quality_gate_failures", []).append(failure)
if applied_render_consistency.get("frame_mismatch_count"):
slide_status.setdefault("visual_fail_reasons", []).append(
"selected_frame_not_applied: "
f"{applied_render_consistency.get('frame_mismatch_count')} zone(s) "
"rendered with a different template than Step 6 selected"
)
if "final_layout_positions_mismatch" in (applied_render_consistency.get("failures") or []):
slide_status.setdefault("visual_fail_reasons", []).append(
"final_layout_positions_mismatch: "
f"expected {applied_render_consistency.get('expected_positions')} but "
f"final.html has {applied_render_consistency.get('final_positions')}"
)
if slide_status.get("overall") == "PASS":
slide_status["overall"] = "PARTIAL_COVERAGE"
# IMP-47B u8 주석 이력 보존 — Surface Step 12 AI repair outcomes through
# slide_status (아래 ai_repair_status 블록). quality gate downgrade 3종은
# issue #17 에서 _apply_quality_gate_downgrades 로 추출 (popup 승격 후
# 재계산 경로에도 동일 적용 — 미적용 시 text 손실이 PASS 로 통과하는
# 게이트 순서 버그가 있었음: mdx05 실측).
_apply_quality_gate_downgrades(
slide_status,
rendered_text_coverage=rendered_text_coverage,
forbidden_rendered_syntax=forbidden_rendered_syntax,
applied_render_consistency=applied_render_consistency,
)
presentation_fit = _build_presentation_fit_report(
overflow=overflow,
@@ -11448,6 +11509,14 @@ def run_phase_z2_mvp1(
slide_status["density_gate"] = density_gate
slide_status["design_readiness"] = design_readiness
slide_status["presentation_fit"] = presentation_fit
# issue #17 — 재계산 경로에도 quality gate 강등 적용 (미적용 시
# text 손실이 overall=PASS 로 통과하던 게이트 순서 버그).
_apply_quality_gate_downgrades(
slide_status,
rendered_text_coverage=rendered_text_coverage,
forbidden_rendered_syntax=forbidden_rendered_syntax,
applied_render_consistency=applied_render_consistency,
)
slide_status["technical_pass"] = bool(
slide_status.get("rendered")
and slide_status.get("full_mdx_coverage")