Files
C.E.L_Slide_test2/tests/matching/pipeline_12_generate_templates_v2.py
KyeongminandClaude Opus 4.8 b836e79ee1 wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규
- Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가
- templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신
- tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분)
- tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가
- docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서
- scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸
- .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외

미완성 작업의 보존용 스냅샷 커밋 (2026-07-02)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:03:42 +09:00

696 lines
30 KiB
Python

"""Pipeline Step 12 — templates_v2 draft 생성 (C.2-b AI 일괄 라벨링).
사용자 지침:
1. 원본 structure_ontology.yaml 는 건드리지 않음. structure_ontology_v2.yaml 별도 파일.
2. 각 프레임에 content_affinity / structure_intent / alternative_patterns + evidence/reason.
3. 32개 전부 자동 확정하지 말고 우선 검토 5개 (12, 20, 29, 3, 11) 별도 리포트.
4. 결과 3개 산출물:
(a) structure_ontology_v2.yaml
(b) PRIORITY_5_REVIEW.md/html (우선 5개 상세)
(c) TEMPLATES_V2_DIFF.md (v1 대비 무엇이 달라졌나)
라벨링 전략:
- Layout-default heuristic (fig_layout → 기본 affinity/intent)
- 내용 설명 키워드 refinement (override primary)
- suits/not_suits 에서 교차 검증 신호 추출
- alternative_patterns: _COMPAT 에서 파생 + layout family 유사도
- 각 라벨에 evidence: {field, quote, confidence} 구조
AI 라벨링이지만 결정론적 (재현 가능) — LLM 호출 없이 규칙+키워드로 생성.
"""
import datetime
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
import markdown
import yaml
HERE = Path(__file__).parent
ROOT = HERE.parent.parent
BLOCKS_DIR = ROOT / 'figma_to_html_agent' / 'blocks'
sys.path.insert(0, str(HERE))
from phase_common import _COMPAT
ONTOLOGY_V1_PATH = HERE / 'structure_ontology.yaml'
OUT_V2_PATH = HERE / 'structure_ontology_v2.yaml'
PRIORITY_MD = HERE / 'PRIORITY_5_REVIEW.md'
PRIORITY_HTML = HERE / 'PRIORITY_5_REVIEW.html'
DIFF_MD = HERE / 'TEMPLATES_V2_DIFF.md'
PRIORITY_FRAME_NUMBERS = {3, 11, 12, 20, 29}
# ============================================================
# Vocabularies (schema v2)
# ============================================================
CONTENT_AFFINITY_ENUM = [
'concept_definition', 'concept_comparison', 'goal_axes',
'persona_benefit', 'process_steps', 'before_after_change',
'capability_requirements', 'comparative_matrix',
'stakeholder_roles', 'policy_requirements',
'tool_ecosystem', 'interrelation',
]
STRUCTURE_INTENT_ENUM = [
'binary_compare', 'multi_parallel', 'hierarchy',
'sequence', 'state_transition', 'cycle_interrelation',
'matrix_coverage', 'persona_mapping', 'singleton_emphasis',
]
# ============================================================
# Layout → default content_affinity (primary 먼저)
# ============================================================
# Key 는 original_layout (visual_pattern.layout 또는 source.original_layout)
LAYOUT_DEFAULT_AFFINITY = {
# Compare family
'compare-rows': ['concept_comparison', 'comparative_matrix'],
'compare-2banner-top-2col-bottom': ['before_after_change', 'concept_comparison'],
'compare-2col': ['concept_comparison'],
'compare-2banner': ['before_after_change', 'concept_comparison'],
'banner-top-2col-bottom': ['before_after_change', 'concept_comparison'],
# Table
'table-2col': ['concept_comparison', 'comparative_matrix'],
'table-3col': ['comparative_matrix'],
# Persona / 3-column
'persona-3col': ['persona_benefit', 'stakeholder_roles'],
'3col-parallel': ['goal_axes'],
'3col-cards': ['capability_requirements'],
'3col-compare': ['concept_comparison', 'comparative_matrix'],
'3-column': ['goal_axes'],
'3-category': ['concept_definition'],
'cards-3-category': ['concept_definition'],
'cards-3-compare': ['concept_comparison'],
'cards-3-header': ['capability_requirements'],
# Cards 4+
'cards-4': ['capability_requirements'],
'cards-4-grid': ['capability_requirements'],
'policy-4card-plus-list': ['policy_requirements'],
# Cycle / interrelation
'cycle-3way': ['interrelation', 'goal_axes'],
'cycle-3way-intersection': ['interrelation', 'goal_axes'],
'circular-nodes': ['interrelation'],
'circular-nodes-6': ['interrelation'],
'quadrilateral-relations': ['interrelation'],
# Lists
'list-numbered': ['policy_requirements', 'capability_requirements'],
'list-numbered-4': ['policy_requirements', 'capability_requirements'],
'list-stacked': ['policy_requirements'],
'list-stacked-vertical': ['policy_requirements'],
'bullet-cards': ['capability_requirements'],
'bullet-cards-4-plus-center': ['capability_requirements', 'goal_axes'],
# Paired / Quadrant
'paired-rows': ['comparative_matrix'],
'paired-rows-2x2': ['comparative_matrix'],
'2col-paired': ['persona_benefit'],
'2col-paired-list': ['persona_benefit', 'stakeholder_roles'],
'2-boxes': ['concept_comparison'],
'quadrant-issues': ['policy_requirements', 'singleton_emphasis'],
'quadrant-4': ['comparative_matrix'],
# Diagram / radial
'diagram-5': ['goal_axes'],
'radial-diagram-5': ['goal_axes'],
'diagram-labels': ['concept_definition'],
'central-5-goals': ['goal_axes'],
'central-split': ['concept_comparison'],
'central-split-synthesis': ['concept_comparison'],
# Split panel
'split-panel-diagram': ['concept_comparison'],
'split-panel-numbered': ['process_steps'],
# Side / Sections
'side-card': ['concept_definition'],
'side-card-with-list': ['concept_definition', 'capability_requirements'],
'3-section': ['goal_axes'],
'3-section-framework': ['goal_axes', 'policy_requirements'],
'3-emphasis': ['goal_axes', 'singleton_emphasis'],
'title-plus-3-emphasis': ['goal_axes', 'singleton_emphasis'],
# Full page
'full-page-map': ['policy_requirements', 'stakeholder_roles'],
'full-page-map-banner': ['policy_requirements'],
}
LAYOUT_DEFAULT_INTENT = {
'compare-rows': ['matrix_coverage', 'binary_compare'],
'compare-2banner-top-2col-bottom': ['state_transition', 'binary_compare'],
'compare-2col': ['binary_compare'],
'compare-2banner': ['state_transition', 'binary_compare'],
'banner-top-2col-bottom': ['state_transition', 'binary_compare'],
'table-2col': ['matrix_coverage'],
'table-3col': ['matrix_coverage'],
'persona-3col': ['persona_mapping', 'multi_parallel'],
'3col-parallel': ['multi_parallel'],
'3col-cards': ['multi_parallel'],
'3col-compare': ['binary_compare', 'matrix_coverage'],
'3-column': ['multi_parallel'],
'3-category': ['multi_parallel'],
'cards-3-category': ['multi_parallel'],
'cards-3-compare': ['binary_compare'],
'cards-3-header': ['multi_parallel'],
'cards-4': ['multi_parallel'],
'cards-4-grid': ['multi_parallel'],
'policy-4card-plus-list': ['multi_parallel'],
'cycle-3way': ['cycle_interrelation', 'multi_parallel'],
'cycle-3way-intersection': ['cycle_interrelation', 'multi_parallel'],
'circular-nodes': ['cycle_interrelation'],
'circular-nodes-6': ['cycle_interrelation'],
'quadrilateral-relations': ['cycle_interrelation'],
'list-numbered': ['sequence'],
'list-numbered-4': ['sequence', 'multi_parallel'],
'list-stacked': ['multi_parallel'],
'list-stacked-vertical': ['multi_parallel'],
'bullet-cards': ['multi_parallel'],
'bullet-cards-4-plus-center': ['multi_parallel', 'hierarchy'],
'paired-rows': ['matrix_coverage'],
'paired-rows-2x2': ['matrix_coverage'],
'2col-paired': ['persona_mapping', 'binary_compare'],
'2col-paired-list': ['persona_mapping'],
'2-boxes': ['binary_compare'],
'quadrant-issues': ['matrix_coverage', 'multi_parallel'],
'quadrant-4': ['matrix_coverage'],
'diagram-5': ['hierarchy'],
'radial-diagram-5': ['hierarchy', 'multi_parallel'],
'diagram-labels': ['singleton_emphasis'],
'central-5-goals': ['hierarchy', 'multi_parallel'],
'central-split': ['binary_compare'],
'central-split-synthesis': ['binary_compare'],
'split-panel-diagram': ['binary_compare'],
'split-panel-numbered': ['sequence'],
'side-card': ['singleton_emphasis'],
'side-card-with-list': ['singleton_emphasis', 'multi_parallel'],
'3-section': ['multi_parallel'],
'3-section-framework': ['multi_parallel', 'hierarchy'],
'3-emphasis': ['multi_parallel', 'singleton_emphasis'],
'title-plus-3-emphasis': ['multi_parallel', 'singleton_emphasis'],
'full-page-map': ['singleton_emphasis', 'matrix_coverage'],
'full-page-map-banner': ['singleton_emphasis'],
}
# ============================================================
# 키워드 → affinity 보강 (내용 설명에서 발견 시 primary override)
# ============================================================
AFFINITY_KEYWORDS = {
'concept_definition': ['정의', '이란', '개념', '분류', '구분'],
'concept_comparison': ['비교', '대조', '차이', 'vs', 'VS', '상호관계'],
'goal_axes': ['목표', '궁극적', '비전', '목적', '지향'],
'persona_benefit': ['발주자', '설계자', '시공자', '기대효과', '혜택', '이익'],
'process_steps': ['단계', '순서', 'Step', '1단계', '2단계', '흐름'],
'before_after_change': ['AS-IS', 'TO-BE', '전환', '혁신', '변화', '이중 변환', '과정'],
'capability_requirements': ['필수', '요건', '필요', '역량', 'S/W', '도구'],
'comparative_matrix': ['다면', '다축', '관점별', '여러 관점', '축'],
'stakeholder_roles': ['역할', '책임', '주도', '수행'],
'policy_requirements': ['정책', '제도', '도입', '거버넌스', '전면 도입', '국외'],
'tool_ecosystem': ['Revit', 'Navisworks', 'SketchUp', '소프트웨어', 'S/W 생태'],
'interrelation': ['상호관계', '순환', '조화', '교차', '수렴', '3원'],
}
# ============================================================
# 키워드 → intent 보강
# ============================================================
INTENT_KEYWORDS = {
'binary_compare': ['2개 비교', '2개 개념', '대조', '양분'],
'state_transition': ['AS-IS', 'TO-BE', '전환', '혁신', '이중 Transformation', '과정'],
'cycle_interrelation': ['상호관계', '순환', '조화', '교차', '3원'],
'multi_parallel': ['3개 병렬', '4개 병렬', '카드 3열', '3관점'],
'hierarchy': ['중앙', '상위', '하위', '포함'],
'sequence': ['단계', '순서', '흐름', 'step'],
'matrix_coverage': ['다면', '관점별', '여러 관점', '축별'],
'persona_mapping': ['주체별', '발주자/설계자/시공자', '역할별'],
'singleton_emphasis': ['강조', '문제', '진단', '약점'],
}
# ============================================================
# 핵심 함수
# ============================================================
def parse_analysis_md(path: Path) -> dict:
"""analysis.md 의 주요 섹션 추출."""
text = path.read_text(encoding='utf-8')
# 내용 설명
m = re.search(r'##\s*내용 설명\s*\n+([\s\S]+?)(?=\n##\s|\Z)', text)
content = m.group(1).strip() if m else ''
# suits
m_s = re.search(r'###\s*suits\s*\n([\s\S]+?)(?=\n###|\n##\s|\Z)', text)
suits = []
if m_s:
for ln in m_s.group(1).strip().split('\n'):
ln = ln.strip()
if ln.startswith('-'):
suits.append(ln.lstrip('-').strip())
# not_suits
m_ns = re.search(r'###\s*not_suits\s*\n([\s\S]+?)(?=\n###|\n##\s|\Z)', text)
not_suits = []
if m_ns:
for ln in m_ns.group(1).strip().split('\n'):
ln = ln.strip()
if ln.startswith('-'):
not_suits.append(ln.lstrip('-').strip())
return {'content': content, 'suits': suits, 'not_suits': not_suits}
def find_keyword_hits(text: str, keyword_map: dict) -> list[tuple[str, list[str]]]:
"""text 안에서 각 label 의 키워드 찾기."""
hits = []
for label, kws in keyword_map.items():
found = [kw for kw in kws if kw in text]
if found:
hits.append((label, found))
return hits
def label_content_affinity(layout: str, original_layout: str, content: str,
suits: list, not_suits: list) -> dict:
"""content_affinity primary + secondary + evidence."""
# 1. default
defaults = (LAYOUT_DEFAULT_AFFINITY.get(original_layout)
or LAYOUT_DEFAULT_AFFINITY.get(layout)
or ['concept_definition'])
default_primary = defaults[0]
default_secondary = defaults[1:]
# 2. 키워드 hit
all_text = content + ' ' + ' '.join(suits)
kw_hits = find_keyword_hits(all_text, AFFINITY_KEYWORDS)
# 3. primary 결정: 키워드 가장 강한 것 (개수 기준), 없으면 default
if kw_hits:
kw_hits.sort(key=lambda x: -len(x[1]))
primary = kw_hits[0][0]
primary_kws = kw_hits[0][1]
primary_source = 'keyword_match'
else:
primary = default_primary
primary_kws = []
primary_source = 'layout_default'
# 4. secondary: primary 제외 keyword hit + default secondary
sec_candidates = [l for l, kws in kw_hits[1:] if l != primary]
for d in default_secondary:
if d not in sec_candidates and d != primary:
sec_candidates.append(d)
secondary = sec_candidates[:2]
# 5. evidence 생성
evidence = {}
if primary_source == 'keyword_match':
evidence['primary'] = {
'source': 'keyword_match',
'field': '내용 설명 / suits',
'keywords': primary_kws,
'rule': f"'{primary_kws[0]}' 등 키워드가 {primary} 를 가리킴",
'confidence': min(0.95, 0.6 + 0.1 * len(primary_kws)),
}
else:
evidence['primary'] = {
'source': 'layout_default',
'field': f'original_layout = {original_layout or layout}',
'rule': f'layout → 기본 affinity 매핑',
'confidence': 0.6,
}
for i, s in enumerate(secondary, start=1):
hits_for_s = next((kws for l, kws in kw_hits if l == s), None)
if hits_for_s:
evidence[f'secondary_{i}'] = {
'source': 'keyword_match',
'keywords': hits_for_s,
'confidence': 0.65,
}
else:
evidence[f'secondary_{i}'] = {
'source': 'layout_default',
'confidence': 0.5,
}
return {
'primary': primary,
'secondary': secondary,
'evidence': evidence,
}
def label_structure_intent(layout: str, original_layout: str, content: str,
relation_type: str, cardinality: dict) -> dict:
"""structure_intent primary + secondary + evidence."""
defaults = (LAYOUT_DEFAULT_INTENT.get(original_layout)
or LAYOUT_DEFAULT_INTENT.get(layout)
or ['multi_parallel'])
default_primary = defaults[0]
default_secondary = defaults[1:]
kw_hits = find_keyword_hits(content, INTENT_KEYWORDS)
# relation_type + cardinality 보강
ideal = cardinality.get('ideal') if cardinality else None
if relation_type == 'compare' and ideal == 2:
# state_transition 우선 (내용에 전환/AS-IS 가 있으면)
if 'AS-IS' in content or 'TO-BE' in content or '혁신' in content or '전환' in content:
kw_hits.append(('state_transition', ['AS-IS/TO-BE/혁신/전환']))
else:
kw_hits.append(('binary_compare', ['compare + cardinality.ideal=2']))
elif relation_type == 'parallel' and ideal and ideal >= 3:
kw_hits.append(('multi_parallel', [f'parallel + cardinality.ideal={ideal}']))
elif relation_type == 'sequence':
kw_hits.append(('sequence', ['relation_type=sequence']))
if kw_hits:
kw_hits.sort(key=lambda x: -len(x[1]))
primary = kw_hits[0][0]
primary_kws = kw_hits[0][1]
primary_source = 'keyword_or_structure_match'
else:
primary = default_primary
primary_kws = []
primary_source = 'layout_default'
sec_candidates = []
for l, kws in kw_hits[1:]:
if l != primary and l not in sec_candidates:
sec_candidates.append(l)
for d in default_secondary:
if d not in sec_candidates and d != primary:
sec_candidates.append(d)
secondary = sec_candidates[:2]
evidence = {
'primary': {
'source': primary_source,
'rule': primary_kws[0] if primary_kws else f'layout default → {primary}',
'confidence': 0.7 if primary_source == 'keyword_or_structure_match' else 0.6,
}
}
for i, s in enumerate(secondary, start=1):
evidence[f'secondary_{i}'] = {
'source': 'layout_default' if s in default_secondary else 'keyword_match',
'confidence': 0.55,
}
return {
'primary': primary,
'secondary': secondary,
'evidence': evidence,
}
def derive_alternative_patterns(fig_layout: str) -> list[dict]:
"""_COMPAT 에서 파생 — 이 fig_layout 과 의미적으로 호환 가능한 다른 layout."""
alternatives = {}
# 이 fig_layout 을 높게 평가하는 mdx_layout 들
for mdx_l, fig_dict in _COMPAT.items():
my_compat = fig_dict.get(fig_layout, 0)
if my_compat >= 0.6:
# 같은 mdx_l 에서 compat >= 0.7 인 다른 fig_layout 들 → 대안
for other_fig, c in fig_dict.items():
if other_fig == fig_layout:
continue
if c >= 0.7:
# 가중 합산: 공통으로 호환되는 mdx_l 이 많을수록 강한 대안
alternatives[other_fig] = alternatives.get(other_fig, 0) + my_compat * c
# 정규화 (0~1)
if not alternatives:
return []
max_v = max(alternatives.values())
alts_list = []
for fig_l, v in sorted(alternatives.items(), key=lambda x: -x[1])[:5]:
conf = round(v / max_v, 2)
alts_list.append({
'pattern': fig_l,
'reason': f'_COMPAT 공통 호환 mdx_layout 집합 기반 파생 (정규화 {conf})',
'confidence': conf,
})
return alts_list
def generate_v2_entry(fid: str, tpl_v1: dict) -> dict:
"""v1 엔트리에 v2 필드 추가."""
layout = tpl_v1['visual_pattern']['layout']
original_layout = tpl_v1['source'].get('original_layout', layout)
relation_type = tpl_v1['visual_pattern'].get('relation_type')
cardinality = tpl_v1['visual_pattern'].get('cardinality', {})
# analysis.md 읽기
analysis = parse_analysis_md(BLOCKS_DIR / fid / 'analysis.md')
# v2 필드 생성
affinity = label_content_affinity(
layout, original_layout,
analysis['content'], analysis['suits'], analysis['not_suits'],
)
intent = label_structure_intent(
layout, original_layout, analysis['content'],
relation_type, cardinality,
)
alternatives = derive_alternative_patterns(original_layout)
# v1 엔트리 그대로 복제 + v2 필드 추가
entry = dict(tpl_v1)
entry['content_affinity'] = affinity
entry['structure_intent_v2'] = intent # v1 의 structure_intent 와 구분 위해 이름 변경
entry['alternative_patterns'] = alternatives
entry['v2_meta'] = {
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
'source_analysis': f'{fid}/analysis.md',
'needs_review': tpl_v1.get('short_id') in {'03', '11', '12', '20', '29'},
}
return entry
# ============================================================
# 메인
# ============================================================
def main():
v1 = yaml.safe_load(ONTOLOGY_V1_PATH.read_text(encoding='utf-8'))
templates_v1 = v1['templates_v1']
templates_v2 = {}
for fid, tpl in templates_v1.items():
templates_v2[fid] = generate_v2_entry(fid, tpl)
# 산출물 1: structure_ontology_v2.yaml
out = {
'meta': {
'schema_version': 'template-fit-v2-draft',
'generated_from': 'structure_ontology.yaml (templates_v1)',
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
'generator': 'pipeline_12_generate_templates_v2.py',
'status': 'draft_pending_user_review',
'priority_review_frames': sorted(PRIORITY_FRAME_NUMBERS),
'vocabularies': {
'content_affinity': CONTENT_AFFINITY_ENUM,
'structure_intent': STRUCTURE_INTENT_ENUM,
},
'matching_weights_initial': {
'layout_compat': 0.40,
'content_affinity': 0.35,
'structure_intent': 0.25,
},
'note': (
'AI 초안. 원본 structure_ontology.yaml 는 유지. '
'사용자 검토 우선순위: Frame 12, 20, 29, 3, 11 → 2차 (3col/cycle/table/process 계열) → 3차 (나머지).'
),
},
'templates_v2': templates_v2,
}
OUT_V2_PATH.write_text(
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
encoding='utf-8',
)
# ============================================================
# 산출물 2: PRIORITY_5_REVIEW
# ============================================================
pr_md = []
pr_md.append('# Priority 5 프레임 검토 리포트 (C.2-b AI 초안)')
pr_md.append('')
pr_md.append(
'사용자 검토 우선순위 1순위 — Holdout 평가에서 V3 문제가 드러난 프레임.'
)
pr_md.append('')
pr_md.append(
'각 프레임마다: (a) v1 대비 추가된 v2 필드, (b) 라벨 근거(evidence), '
'(c) 검토 포인트.'
)
pr_md.append('')
short_to_fid = {v['short_id']: k for k, v in templates_v1.items()}
for fn in ['03', '11', '12', '20', '29']:
fid = short_to_fid.get(fn)
if not fid:
continue
v2_entry = templates_v2[fid]
v1_entry = templates_v1[fid]
title = v1_entry['source']['title']
original_layout = v1_entry['source'].get('original_layout')
pr_md.append(f"## Frame {fn}{title}")
pr_md.append('')
pr_md.append(f"- **frame_id**: `{fid}`")
pr_md.append(f"- **layout**: `{v1_entry['visual_pattern']['layout']}` (original: `{original_layout}`)")
pr_md.append(f"- **family / relation_type / cardinality**: "
f"`{v1_entry['visual_pattern']['family']}` / "
f"`{v1_entry['visual_pattern'].get('relation_type')}` / "
f"ideal={v1_entry['visual_pattern'].get('cardinality', {}).get('ideal')}")
pr_md.append('')
pr_md.append('### content_affinity')
aff = v2_entry['content_affinity']
pr_md.append(f"- **primary**: `{aff['primary']}`")
if aff['secondary']:
pr_md.append(f"- **secondary**: {', '.join(f'`{s}`' for s in aff['secondary'])}")
pr_md.append('- **evidence**:')
for k, v in aff['evidence'].items():
extras = []
if 'keywords' in v and v['keywords']:
extras.append(f"keywords={v['keywords']}")
if 'rule' in v:
extras.append(f"rule={v['rule']}")
extras.append(f"conf={v['confidence']}")
pr_md.append(f" - `{k}` (source: {v['source']}) — {' / '.join(extras)}")
pr_md.append('')
pr_md.append('### structure_intent (v2)')
si = v2_entry['structure_intent_v2']
pr_md.append(f"- **primary**: `{si['primary']}`")
if si['secondary']:
pr_md.append(f"- **secondary**: {', '.join(f'`{s}`' for s in si['secondary'])}")
pr_md.append('- **evidence**:')
for k, v in si['evidence'].items():
pr_md.append(f" - `{k}` (source: {v['source']}) — rule={v.get('rule', '—')} / conf={v['confidence']}")
pr_md.append('')
pr_md.append('### alternative_patterns (파생)')
if v2_entry['alternative_patterns']:
pr_md.append('| 대안 layout | confidence | 근거 |')
pr_md.append('|---|---:|---|')
for a in v2_entry['alternative_patterns']:
pr_md.append(f"| `{a['pattern']}` | {a['confidence']} | {a['reason']} |")
else:
pr_md.append('(파생된 대안 없음 — _COMPAT 기반 공통 호환 부재)')
pr_md.append('')
pr_md.append('### 검토 포인트')
pr_md.append(f"- primary content_affinity 가 Frame 의 실제 의도에 맞나?")
pr_md.append(f"- structure_intent primary/secondary 가 layout 의 시각적 메시지를 정확히 기술하나?")
pr_md.append(f"- alternative_patterns 에 **빠진** 의미적 대안이 있나? (예: Frame 12 라면 `3col-parallel` 이 포함되어 있나)")
pr_md.append('')
pr_md.append('---')
pr_md.append('')
pr_text = '\n'.join(pr_md)
PRIORITY_MD.write_text(pr_text, encoding='utf-8')
style = """
body { font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1100px; margin: 2em auto; padding: 0 1.5em 4em; line-height: 1.65; color: #222; background: #f8fafc; }
h1 { border-bottom: 3px solid #2563eb; padding-bottom: 0.25em; }
h2 { margin-top: 2.5em; background: #e0e7ff; padding: 0.6em 0.9em; border-left: 4px solid #0a6; border-radius: 4px; }
h3 { margin-top: 1.3em; color: #1a365d; }
table { border-collapse: collapse; background: #fff; margin: 0.5em 0 1em; }
th, td { border: 1px solid #e2e8f0; padding: 8px 10px; text-align: left; vertical-align: top; }
th { background: #1e293b; color: #fff; }
code { background: #f4f4f4; padding: 1px 6px; border-radius: 3px; font-size: 0.9em; color: #111; }
strong { color: #0a6; }
"""
html_body = markdown.markdown(pr_text, extensions=['tables'])
html = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="utf-8"><title>Priority 5 검토</title><style>{style}</style></head>
<body>{html_body}</body></html>"""
PRIORITY_HTML.write_text(html, encoding='utf-8')
# ============================================================
# 산출물 3: TEMPLATES_V2_DIFF.md (v1 대비 변경 요약)
# ============================================================
d_md = []
d_md.append('# templates_v1 → templates_v2 변경 요약')
d_md.append('')
d_md.append('_v2 는 v1 엔트리를 **복제 + 필드 추가** 방식으로 생성. 기존 필드는 변경 없음._')
d_md.append('')
d_md.append('## 추가된 필드')
d_md.append('')
d_md.append('| 필드 | 타입 | 역할 |')
d_md.append('|---|---|---|')
d_md.append('| `content_affinity` | `{primary, secondary[], evidence{}}` | 프레임이 선호하는 콘텐츠 성격 |')
d_md.append('| `structure_intent_v2` | `{primary, secondary[], evidence{}}` | 레이아웃의 시각 의도 (v1 의 structure_intent 와 별도 축) |')
d_md.append('| `alternative_patterns` | `[{pattern, reason, confidence}]` | 의미적 대안 layout 목록 |')
d_md.append('| `v2_meta` | `{generated_at, source_analysis, needs_review}` | 생성 메타 |')
d_md.append('')
d_md.append('## 유지된 필드 (변경 없음)')
d_md.append('')
d_md.append('- `short_id` / `template_id` / `schema_version` / `source`')
d_md.append('- `description` / `visual_pattern` (전체)')
d_md.append('- `slots` / `suits` / `not_suits` / `adaptation_allowed`')
d_md.append('')
d_md.append('## 통계')
d_md.append('')
# affinity 분포
aff_counter = Counter()
intent_counter = Counter()
alt_counts = []
for fid, e in templates_v2.items():
aff_counter[e['content_affinity']['primary']] += 1
intent_counter[e['structure_intent_v2']['primary']] += 1
alt_counts.append(len(e['alternative_patterns']))
d_md.append('### content_affinity primary 분포')
d_md.append('')
d_md.append('| primary | 프레임 수 |')
d_md.append('|---|---:|')
for lab, c in aff_counter.most_common():
d_md.append(f"| `{lab}` | {c} |")
d_md.append('')
d_md.append('### structure_intent (v2) primary 분포')
d_md.append('')
d_md.append('| primary | 프레임 수 |')
d_md.append('|---|---:|')
for lab, c in intent_counter.most_common():
d_md.append(f"| `{lab}` | {c} |")
d_md.append('')
d_md.append('### alternative_patterns 수')
d_md.append('')
d_md.append(f"- 평균 대안 수: {round(sum(alt_counts)/len(alt_counts), 1)}")
d_md.append(f"- 대안 0개 프레임: {sum(1 for c in alt_counts if c == 0)}")
d_md.append(f"- 대안 5개 프레임: {sum(1 for c in alt_counts if c == 5)}")
d_md.append('')
d_md.append('## v1 과의 관계')
d_md.append('')
d_md.append(
'- `structure_ontology.yaml` 원본 **유지** — v2 는 `structure_ontology_v2.yaml` 별도 파일.'
)
d_md.append(
'- v2 매칭 로직 (pipeline_08_v3_structure_rerank.py 재구현) 에서 `_COMPAT` 대신 '
'`content_affinity + structure_intent_v2 + alternative_patterns` 조합 사용 예정.'
)
d_md.append(
'- v2 최종 승인 전까지 V3 재구현 보류 — 사용자 검토 (priority 5) 후 확정.'
)
d_md.append('')
DIFF_MD.write_text('\n'.join(d_md), encoding='utf-8')
print('=' * 70)
print('templates_v2 draft 생성 완료')
print('=' * 70)
print(f' 1. structure_ontology_v2.yaml: {OUT_V2_PATH}')
print(f' 2. PRIORITY_5_REVIEW.md/html: {PRIORITY_MD}')
print(f' 3. TEMPLATES_V2_DIFF.md: {DIFF_MD}')
print()
print(f' affinity primary 분포: {dict(aff_counter.most_common(5))}')
print(f' intent primary 분포: {dict(intent_counter.most_common(5))}')
print(f' 평균 대안 수: {round(sum(alt_counts)/len(alt_counts), 1)}')
if __name__ == '__main__':
main()