"""IMP-91 스냅샷 재생성 절차 (GitHub issue #29). structural.json 의 _doc 계약: "Update only when an intentional pipeline change moves the observed value" — 의도적 변경 후 이 스크립트로 갱신한다. 테스트 모듈의 실행 함수(run_pipeline_for_snapshot — AI OFF 강제)와 shape 헬퍼(_slot_payload_zone_shape 등)를 그대로 import 해 추출 로직 표류를 방지. 실행: python -m tests.integration.scripts.regenerate_snapshots """ from __future__ import annotations import json import re import sys import uuid from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[3] sys.path.insert(0, str(REPO_ROOT)) from tests.integration.test_multi_mdx_regression import ( # noqa: E402 MDX_SET, SNAPSHOTS_DIR, _AI_UNIT_KEYS, _SLIDE_ROOT_RE, _TITLE_RE, _extract_html_zone_topology, _layout_zone_shape, _slot_payload_zone_shape, run_pipeline_for_snapshot, ) def _j(run_dir: Path, name: str) -> dict: return json.loads((run_dir / "steps" / name).read_text(encoding="utf-8")) def extract_structural(run_dir: Path) -> dict: status = _j(run_dir, "step20_slide_status.json")["data"] frame_sel = _j(run_dir, "step09_frame_selection.json")["data"] zones = frame_sel.get("per_zone", []) return { "overall": status.get("overall"), "zone_count": len(zones), "zones": [ {"position": z.get("position"), "selected_template_id": z.get("selected_template_id")} for z in zones ], } def extract_visual(run_dir: Path) -> dict: visual = _j(run_dir, "step14_visual_check.json")["data"] return { "slide_overflowed": visual.get("slide", {}).get("overflowed"), "slide_body_overflowed": visual.get("slide_body", {}).get("overflowed"), "passed": visual.get("passed"), "zones": [ { "position": z.get("position"), "template_id": z.get("template_id"), "overflowed": z.get("overflowed"), "clipped_inner_count": len(z.get("clipped_inner") or []), } for z in visual.get("zones", []) ], } def extract_coverage(run_dir: Path) -> dict: status = _j(run_dir, "step20_slide_status.json")["data"] return { "rendered": status.get("rendered"), "visual_check_passed": status.get("visual_check_passed"), "full_mdx_coverage": status.get("full_mdx_coverage"), "aligned_section_ids": sorted(status.get("aligned_section_ids") or []), "covered_section_ids": sorted(status.get("covered_section_ids") or []), "filtered_section_ids": sorted(status.get("filtered_section_ids") or []), } def extract_normalize(run_dir: Path) -> dict: raw = _j(run_dir, "step02_normalized.json") d = raw["data"] diag = d.get("stage0_adapter_diagnostics", {}) or {} assets = d.get("stage0_normalized_assets", {}) or {} return { "step_num": raw.get("step_num"), "step_status": raw.get("step_status"), "pipeline_path_connected": raw.get("pipeline_path_connected"), "sections_count": d.get("sections_count"), "section_ids": [s.get("section_id") for s in d.get("sections", [])], "orphans_count": len(d.get("orphans") or []), "details_count": len(d.get("details") or []), "adapter_enabled": diag.get("enabled"), "adapter_used": diag.get("used"), "assets_popups_count": len(assets.get("popups") or []), "assets_images_count": len(assets.get("images") or []), "assets_tables_count": len(assets.get("tables") or []), "slide_title_nonempty": bool(d.get("slide_title")), "slide_footer_nonempty": bool(d.get("slide_footer")), } def extract_v4_ranking(run_dir: Path) -> dict: data = _j(run_dir, "step05_v4_evidence.json")["data"] return { "v4_source": str(data.get("v4_source") or "").replace("\\", "/"), "aligned_section_ids": data.get("aligned_section_ids"), "sections": [ { "section_id": ev.get("section_id"), "candidate_status": ev.get("candidate_status"), "candidates": [ { "template_id": c.get("template_id"), "label": c.get("label"), "confidence": c.get("confidence"), } for c in (ev.get("v4_candidates") or []) ], } for ev in (data.get("evidence_per_section") or []) ], } def extract_ai_classifier(run_dir: Path) -> dict: ai = _j(run_dir, "step12_ai_repair.json")["data"] fit = _j(run_dir, "step15_fit_classification.json")["data"] router = _j(run_dir, "step16_router_decision.json")["data"] failure = _j(run_dir, "step18_failure_classification.json")["data"] units = [{k: u.get(k) for k in _AI_UNIT_KEYS} for u in (ai.get("per_unit") or [])] return { "units": units, "coverage_invariant_status": (ai.get("coverage_invariant") or {}).get("status"), "fit_visual_check_passed": fit.get("visual_check_passed"), "fit_classifications_count": len(fit.get("classifications") or []), "fit_categories_seen": fit.get("categories_seen") or [], "router_active": router.get("router_active"), "router_routed_count": router.get("routed_count"), "router_v4_fallback_used_count": (router.get("v4_fallback_summary") or {}).get("fallback_used_count"), "failure_type": failure.get("failure_type"), } def extract_layout(run_dir: Path) -> dict: s7 = _j(run_dir, "step07_layout.json") s8 = _j(run_dir, "step08_zone_region_ratios.json") d7 = s7.get("data") or {} d8 = s8.get("data") or {} css = d7.get("layout_css") or {} return { "step7_step_status": s7.get("step_status"), "step7_pipeline_path_connected": s7.get("pipeline_path_connected"), "layout_preset": d7.get("layout_preset"), "auto_layout_preset": d7.get("auto_layout_preset"), "layout_override_applied": d7.get("layout_override_applied"), "zones_count": d7.get("zones_count"), "unit_count": d7.get("unit_count"), "layout_candidates": d7.get("layout_candidates") or [], "computation": css.get("computation"), "dynamic_rows": css.get("dynamic_rows"), "dynamic_cols": css.get("dynamic_cols"), "heights_px": css.get("heights_px"), "widths_px": css.get("widths_px"), "ratios": css.get("ratios"), "width_ratios": css.get("width_ratios"), "step8_step_status": s8.get("step_status"), "step8_pipeline_path_connected": s8.get("pipeline_path_connected"), "zone_heights_px_planned": d8.get("zone_heights_px_planned"), "zone_widths_px_planned": d8.get("zone_widths_px_planned"), "zone_col_ratios_planned": d8.get("zone_col_ratios_planned"), "per_zone_layout_shape": [ _layout_zone_shape(z) for z in (d8.get("per_zone_plan") or []) ], } def extract_slot_payload(run_dir: Path) -> list: raw = _j(run_dir, "step12_slot_payload.json") return [_slot_payload_zone_shape(z) for z in raw["data"].get("per_zone") or []] def extract_final_html(run_dir: Path) -> dict: raw13 = _j(run_dir, "step13_render.json") d13 = raw13.get("data") or {} ri = d13.get("render_inputs") or {} final_path = run_dir / "final.html" html = final_path.read_text(encoding="utf-8") title_match = _TITLE_RE.search(html) html_title = title_match.group(1).strip() if title_match else "" html_topology = _extract_html_zone_topology(html) return { "step13_status": raw13.get("step_status"), "step13_pipeline_path_connected": raw13.get("pipeline_path_connected"), "render_inputs_zones_count": ri.get("zones_count"), "render_inputs_layout_preset": ri.get("layout_preset"), "render_inputs_slide_title_nonempty": bool((ri.get("slide_title") or "").strip()), "render_inputs_slide_footer_nonempty": bool((ri.get("slide_footer") or "").strip()), "html_title_matches_render_input": html_title == (ri.get("slide_title") or "").strip(), "html_slide_root_count": len(_SLIDE_ROOT_RE.findall(html)), "html_slide_footer_present": '