"""Phase T 전수 검사. 1. 모든 파일 syntax 2. 모든 import chain 3. pipeline.py 내 이름 참조 4. lazy import 유효성 5. catalog.yaml 6. Pydantic 모델 7. 실제 데이터 Stage 0~1.5b 8. Stage 3 render 호출 9. Stage 2 supplement 생성 """ import ast, re, json, sys from pathlib import Path sys.path.insert(0, ".") errors = [] def check(name, condition, detail=""): if condition: print(f" OK {name}") else: print(f" FAIL {name} -- {detail}") errors.append(f"{name}: {detail}") print("-- 1. Syntax --") for f in Path("src").glob("*.py"): try: ast.parse(f.read_text(encoding="utf-8")) print(f" OK {f.name}") except SyntaxError as e: print(f" FAIL {f.name}: {e}") errors.append(f"syntax: {f.name}") print("\n-- 2. Import --") for mod in ["src.pipeline_context", "src.mdx_normalizer", "src.validators", "src.block_reference", "src.space_allocator", "src.html_generator", "src.content_verifier", "src.renderer", "src.kei_client", "src.image_utils", "src.slide_measurer", "src.config", "src.main", "src.pipeline"]: try: __import__(mod) print(f" OK {mod}") except Exception as e: print(f" FAIL {mod}: {e}") errors.append(f"import: {mod}") print("\n-- 3. pipeline.py import 참조 --") psrc = Path("src/pipeline.py").read_text(encoding="utf-8") needed = ["PipelineContext", "Topic", "NormalizedContent", "Analysis", "PageStructure", "ContainerInfo", "TextBudget", "DesignBudget", "FontHierarchy", "BlockReference", "StageFailure", "build_retry_feedback", "create_context"] import_block = re.search(r"from src\.pipeline_context import \((.*?)\)", psrc, re.DOTALL) imported = set() if import_block: imported = {n.strip() for n in import_block.group(1).split(",") if n.strip()} for name in needed: if name in psrc and name not in imported: # 메서드인지 확인 is_method = all(("." + name) in line or name not in line for line in psrc.split("\n") if "from src.pipeline_context" not in line) if not is_method: check(f"import {name}", False, "사용되지만 import 안 됨") else: check(f"import {name}", True) else: check(f"import {name}", name in imported or name not in psrc) print("\n-- 4. lazy import --") for mod_name, func_name in re.findall(r"from (src\.\w+) import (\w+)", psrc): if "pipeline_context" in mod_name: continue try: mod = __import__(mod_name, fromlist=[func_name]) check(f"{mod_name}.{func_name}", hasattr(mod, func_name)) except Exception as e: check(f"{mod_name}.{func_name}", False, str(e)) print("\n-- 5. catalog.yaml --") import yaml data = yaml.safe_load(Path("templates/catalog.yaml").read_text(encoding="utf-8")) blocks = data.get("blocks", []) check("blocks count", len(blocks) == 38, f"got {len(blocks)}") check("schema 38/38", sum(1 for b in blocks if b.get("schema")) == 38) check("visual_diff 20", sum(1 for b in blocks if b.get("visual_diff")) == 20) print("\n-- 6. Pydantic --") from src.pipeline_context import * check("create_context", create_context("test") is not None) check("FontHierarchy OK", FontHierarchy(key_msg=14, core=12, bg=11, sidebar=10) is not None) try: FontHierarchy(key_msg=10, core=12, bg=14, sidebar=9) check("FontHierarchy violation", False, "not caught") except: check("FontHierarchy violation", True) check("Topic no weight", "weight" not in Topic.model_fields) check("DesignBudget", DesignBudget(available_height_px=100) is not None) print("\n-- 7. 실제 데이터 Stage 0~1.5b --") s0 = json.loads(Path("data/runs/20260401_151426/stage_0_context.json").read_text(encoding="utf-8")) a1 = json.loads(Path("data/runs/1774922951020/step1_analysis.json").read_text(encoding="utf-8")) c1b = json.loads(Path("data/runs/1774922951020/step1b_concepts.json").read_text(encoding="utf-8")) # 1A topics = [Topic(**{k: v for k, v in t.items() if k in Topic.model_fields}) for t in a1["topics"]] check("1A Topic 변환", len(topics) == 5) # 1B concepts = c1b.get("concepts", []) updated = [] for t in topics: m = next((c for c in concepts if c.get("id") == t.id), None) if m: updated.append(t.model_copy(update={ "relation_type": m.get("relation_type", ""), "expression_hint": m.get("expression_hint", ""), "source_data": m.get("source_data", ""), })) else: updated.append(t) check("1B 병합", len(updated) == 5) # 검증 from src.validators import validate_stage_1a, validate_stage_1b e1a = validate_stage_1a(a1, s0["normalized"]["clean_text"]) check("1A 검증", not e1a, str(e1a)[:100] if e1a else "") e1b = validate_stage_1b([t.model_dump() for t in updated], s0["normalized"]["clean_text"], raw_content=s0["raw_content"]) check("1B 검증", not e1b, str(e1b)[:100] if e1b else "") # 1.5a from src.space_allocator import calculate_font_hierarchy, calculate_dynamic_ratio, calculate_container_specs, calculate_design_budget from src.design_director import LAYOUT_PRESETS, select_preset from src.block_reference import select_and_generate_references ctx = create_context(s0["raw_content"]) ctx = ctx.model_copy(update={ "normalized": NormalizedContent(**s0["normalized"]), "topics": updated, "page_structure": PageStructure(roles=a1.get("page_structure", {})), "analysis": Analysis(core_message=a1.get("core_message", ""), title=a1.get("title", "")), }) rtl = {role: len(ctx.get_role_content(role)) for role in ["배경", "본심", "첨부", "결론"]} fh_dict = calculate_font_hierarchy(rtl) fh = FontHierarchy(key_msg=fh_dict["핵심"], core=fh_dict["본심"], bg=fh_dict["배경"], sidebar=fh_dict["첨부"]) check("1.5a 폰트위계", fh.key_msg > fh.core >= fh.bg > fh.sidebar) ratio = calculate_dynamic_ratio(rtl, fh_dict) check("1.5a 비율", ratio[0] + ratio[1] == 100) preset_name = select_preset(a1) preset = LAYOUT_PRESETS.get(preset_name, {}) specs = calculate_container_specs(a1.get("page_structure", {}), [t.model_dump() for t in updated], preset) check("1.5a 컨테이너", len(specs) >= 3) # 1.7 refs = select_and_generate_references([t.model_dump() for t in updated], specs, a1.get("page_structure", {})) check("1.7 참고블록", len(refs) >= 3) # 1.5b for role, spec in specs.items(): ref = refs.get(role, {}) schema = ref.get("schema_info", {}) font_map = {"본심": fh.core, "배경": fh.bg, "첨부": fh.sidebar, "결론": fh.core} budget = calculate_design_budget(spec.height_px, spec.width_px, schema, font_map.get(role, 12)) db = DesignBudget(**budget) check(f"1.5b {role}", True) print("\n-- 8. Stage 3 render --") from src.renderer import render_slide_from_html mock_gen = { "body_html": '
', "sidebar_html": '', "footer_html": "