Phase W: - weight 비율 초기 배정 (space_allocator header 높이 반영) - block_assembler 공통 조립 함수 (filled/assembled 통합) - filled → Selenium 측정 → context 저장 - sidebar overflow 확장 + body 재배분 - sub_layouts 사전 계산 (이미지 누락 해결) Phase V': - 팝업 링크 우측상단 배치 (인라인 → position:absolute) - 표 내용 Kei 판단 (공란 크기 계산 → 행/열 산출 → Kei 요약) - 출처 라벨 삭제 + 이미지 아래 캡션 배치 - after 공란 제거 (결론 바로 위까지 body/sidebar 채움) 추가: - V-10 bold 키워드: 기계적 추출 → Kei 문맥 판단 - ** 마크다운 → <strong> 변환 - [이미지:] 마커 제거 (bold 변환 전 처리) - grid-template-rows AFTER 크기 반영 (Sonnet final) - assemble_stage2 CSS font-size override, white-space fix - 하드코딩 전수 검토 완료 - 본심 여러 topic 텍스트 합침 Phase X 계획 문서 작성 (동적 역할 구조) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
209 lines
7.9 KiB
Python
209 lines
7.9 KiB
Python
"""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": '<div style="overflow:hidden"><div class="key-msg">test</div></div>',
|
|
"sidebar_html": '<div style="overflow:hidden; padding-left:14px; text-indent:-14px;">side</div>',
|
|
"footer_html": "<div>foot</div>",
|
|
}
|
|
analysis_dict = {
|
|
"topics": [t.model_dump() for t in updated],
|
|
"page_structure": a1.get("page_structure", {}),
|
|
"core_message": a1.get("core_message", ""),
|
|
"title": a1.get("title", ""),
|
|
}
|
|
html = render_slide_from_html(mock_gen, analysis_dict, preset)
|
|
check("Stage 3 render", len(html) > 100, f"len={len(html)}")
|
|
|
|
print("\n-- 9. Stage 2 supplement --")
|
|
from src.html_generator import _build_phase_t_supplement
|
|
phase_t_ctx = {
|
|
"font_hierarchy": fh.model_dump(),
|
|
"container_ratio": ratio,
|
|
"references": {r: v for r, v in refs.items()},
|
|
"design_budgets": {},
|
|
}
|
|
for role in ["배경", "본심", "첨부", "결론"]:
|
|
supp = _build_phase_t_supplement(role, {"phase_t": phase_t_ctx})
|
|
check(f"supplement {role}", len(supp) > 50, f"len={len(supp)}")
|
|
|
|
# 결과
|
|
print()
|
|
if errors:
|
|
print(f"=== FAIL: {len(errors)}건 ===")
|
|
for e in errors:
|
|
print(f" - {e}")
|
|
else:
|
|
print("=== 전수 검사 통과: 오류 0건 ===")
|
|
|
|
sys.exit(1 if errors else 0)
|