"""Milestone 2 / Step 2: analysis.md 를 structure_ontology.yaml + keyword_base.yaml 기준으로 재작성. 원칙: source of truth → structure_ontology.yaml + keyword_base.yaml human review mirror → figma_to_html_agent/blocks/{fid}/analysis.md 각 frame 의 analysis.md 는 template-fit-v1 정보를 사람이 읽기 좋은 형식으로 반영. 원본은 .pre_milestone2.bak 로 백업. 생성 포맷: # Frame {fid} — {title} ## 내용 설명 (templates_v1[fid].description) ## 후보 키워드 (모든 anchor_sets terms flat) ## 정제 Anchor Sets (set_id + terms) ## 구조 매칭 정보 (visual_pattern + slots) ## 적합/부적합 기준 (fit_notes.suits / not_suits) ## 재구성 허용 (adaptation_allowed) ## 제외된 키워드 (pre_milestone2 대비 제거된 terms + 사유) ## 메타 (schema_version, source, synced_at) """ import sys import shutil from pathlib import Path from datetime import date import yaml sys.path.insert(0, str(Path(__file__).parent)) ROOT = Path(r"d:\ad-hoc\kei\design_agent") BLOCKS_DIR = ROOT / "figma_to_html_agent" / "blocks" HERE = Path(__file__).parent def load_keyword_base_excluded(): """keyword_base.yaml 의 excluded 를 (term → (bucket, reason)) 딕트로.""" path = HERE / "keyword_base.yaml" with open(path, encoding='utf-8') as f: data = yaml.safe_load(f) out = {} for e in data.get('excluded', []): out[e['term']] = {'bucket': e['bucket'], 'reason': e['reason']} return out def load_ontology(path): with open(path, encoding='utf-8') as f: return yaml.safe_load(f) def get_removed_terms_per_frame(current_templates, backup_templates): """Pre-cleanup 대비 각 frame 에서 제거된 anchor terms.""" result = {} for fid, cur_tpl in current_templates.items(): if fid not in backup_templates: continue cur_terms = set() for s in cur_tpl.get('anchor_sets', []): cur_terms.update(s.get('terms', [])) bak_terms = set() for s in backup_templates[fid].get('anchor_sets', []): bak_terms.update(s.get('terms', [])) removed = bak_terms - cur_terms result[fid] = sorted(removed) return result def compose_analysis_md(fid, tpl, removed_terms, excluded_info): """analysis.md 본문 문자열 생성.""" title = tpl.get('source', {}).get('title', '?') original_layout = tpl.get('source', {}).get('original_layout', '?') lines = [] lines.append(f"# Frame {fid} — {title}") lines.append("") # 1. 내용 설명 lines.append("## 내용 설명") lines.append("") desc = tpl.get('description', '').strip() lines.append(desc) lines.append("") # 2. 후보 키워드 (flat) lines.append("## 후보 키워드") lines.append("") all_terms = [] seen = set() for s in tpl.get('anchor_sets', []): for t in s.get('terms', []): if t not in seen: seen.add(t) all_terms.append(t) lines.append(", ".join(all_terms) if all_terms else "_(없음)_") lines.append("") # 3. 정제 Anchor Sets lines.append("## 정제 Anchor Sets") lines.append("") for s in tpl.get('anchor_sets', []): set_id = s.get('id', '?') terms = ", ".join(s.get('terms', [])) extras = [] if 'min_hits' in s: extras.append(f"min_hits={s['min_hits']}") if 'confidence_cap' in s: extras.append(f"cap={s['confidence_cap']}") if 'cap_exempt_if_corroborated_by' in s: extras.append(f"exempt_if≥{s['cap_exempt_if_corroborated_by']}") extra_str = f" _[{', '.join(extras)}]_" if extras else "" lines.append(f"- **{set_id}**: {terms}{extra_str}") if 'note' in s: note = str(s['note']).replace('\n', ' ').strip() lines.append(f" - note: {note[:200]}") lines.append("") # 4. 구조 매칭 정보 lines.append("## 구조 매칭 정보") lines.append("") vp = tpl.get('visual_pattern', {}) card = vp.get('cardinality', {}) lines.append(f"- **family**: {vp.get('family', '?')}") lines.append(f"- **layout**: {vp.get('layout', '?')}") lines.append(f"- **axis**: {vp.get('axis', '?')}") lines.append(f"- **relation_type**: {vp.get('relation_type', '?')}") if card: lines.append(f"- **cardinality**: ideal {card.get('ideal','?')} / " f"min {card.get('min','?')} / max {card.get('max','?')}") slots = tpl.get('slots', []) if slots: slot_ids = ", ".join(s.get('id', '?') for s in slots) required = sum(1 for s in slots if s.get('required')) lines.append(f"- **slots** ({len(slots)}개, required {required}개): {slot_ids}") lines.append(f"- **source title**: {title}") lines.append(f"- **original layout**: {original_layout}") lines.append("") # 5. 적합/부적합 기준 fit = tpl.get('fit_notes', {}) if fit: lines.append("## 적합/부적합 기준") lines.append("") suits = fit.get('suits', []) if suits: lines.append("### suits") for s in suits: lines.append(f"- {s}") lines.append("") not_suits = fit.get('not_suits', []) if not_suits: lines.append("### not_suits") for s in not_suits: lines.append(f"- {s}") lines.append("") # 6. 재구성 허용 adapt = tpl.get('adaptation_allowed', {}) if adapt: lines.append("## 재구성 허용") lines.append("") for k in ['split', 'merge', 'infer_missing_slot', 'rewrite_label', 'rewrite_body']: if k in adapt: lines.append(f"- **{k}**: {adapt[k]}") lines.append("") # 7. 제외된 키워드 (Milestone 2 정리 결과) if removed_terms: lines.append("## 제외된 키워드") lines.append("") lines.append("Milestone 2 anchor_sets 정리 시 이 frame 에서 제거된 term 들. " "구조/레이아웃 서술어·row label·summary label 등 evidence-based 기준으로 제외.") lines.append("") # Frame 20 특수 케이스: 필수성 → 필수 대체 for t in removed_terms: info = excluded_info.get(t, {}) bucket = info.get('bucket', '(대체 or unknown)') reason = info.get('reason', '')[:80] # 필수성 특별 케이스 if fid == '1171281198' and t == '필수성': lines.append(f"- `{t}`: **대체** — Frame 20 실제 텍스트 'S/W가 필수다' 근거로 `필수` 로 치환") else: lines.append(f"- `{t}`: {bucket} — {reason}") lines.append("") # 8. 메타 lines.append("## 메타") lines.append("") lines.append(f"- schema_version: template-fit-v1 mirror") lines.append(f"- source_of_truth: structure_ontology.yaml + keyword_base.yaml") lines.append(f"- structure_content_original_tagged_by: claude-opus-4-7 (2026-04-21)") lines.append(f"- keyword_base_sync_at: {date.today().isoformat()}") lines.append(f"- anchor_sets_cleaned_at: {date.today().isoformat()}") lines.append("") return "\n".join(lines) def main(): print("[1] keyword_base.yaml excluded 로드...") excluded_info = load_keyword_base_excluded() print(f" excluded terms: {len(excluded_info)}") print("[2] 현재 structure_ontology.yaml 로드...") cur_path = HERE / "structure_ontology.yaml" cur_onto = load_ontology(cur_path) cur_templates = cur_onto.get('templates_v1', {}) print(f" templates_v1: {len(cur_templates)}") print("[3] 백업 (pre_milestone2) 로드...") bak_path = HERE / "structure_ontology.yaml.pre_milestone2.bak" if not bak_path.exists(): print(f" WARN: 백업 없음. 제거된 키워드 섹션은 비움") bak_templates = {} else: bak_onto = load_ontology(bak_path) bak_templates = bak_onto.get('templates_v1', {}) print(f" backup templates_v1: {len(bak_templates)}") print("[4] 프레임별 제거된 terms 계산...") removed_map = get_removed_terms_per_frame(cur_templates, bak_templates) total_removed = sum(len(v) for v in removed_map.values()) print(f" 총 제거된 term: {total_removed} (중복 포함)") print("[5] 32 frame analysis.md 재작성...") updated = 0 for fid, tpl in cur_templates.items(): frame_dir = BLOCKS_DIR / fid if not frame_dir.exists(): print(f" WARN: {fid} 폴더 없음, skip") continue analysis_path = frame_dir / "analysis.md" # 백업 if analysis_path.exists(): bak = frame_dir / "analysis.md.pre_milestone2.bak" if not bak.exists(): shutil.copy(analysis_path, bak) # 재작성 content = compose_analysis_md(fid, tpl, removed_map.get(fid, []), excluded_info) analysis_path.write_text(content, encoding='utf-8') updated += 1 print(f" 재작성: {updated}/{len(cur_templates)}") print("\n완료.") print(f"각 frame 의 analysis.md.pre_milestone2.bak 가 원본 보존용으로 생성됨.") if __name__ == "__main__": main()