Phase W + V' 완료: before→filled→after 파이프라인 + 조립 로직 수정

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>
This commit is contained in:
2026-04-06 05:00:52 +09:00
parent 24eb1bc5ad
commit 1f7579cf64
64 changed files with 13955 additions and 696 deletions

View File

@@ -0,0 +1,208 @@
"""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)