Files
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

243 lines
9.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""texts.md 기반 키워드 자동 추출
원칙:
- 실제 Figma 에 있는 텍스트 노드만 사용 (texts.md)
- analysis.md 의 수동 '후보 키워드' 는 사용하지 않음
(구조어·레이아웃 서술어 등 Figma 텍스트에 없는 curator 추가분 포함되어 오염됨)
파이프라인:
texts.md → list item 파싱 → 길이 1자 / 기호 / 숫자만 필터 →
synonyms.yaml 치환 → Kiwi 명사·외국어·숫자 추출 (길이>=2) →
frame 별 키워드 set
산출:
- get_frame_keywords_from_texts(frame_id) → set
- load_frames_from_texts() → {fid: {'keywords': set, 'content': title, ...}}
- FIGMA_KEYWORDS_FROM_TEXTS.md : 생성 리포트 (검수용)
마스터 1171281171 은 전체 Figma 텍스트 통합 메타. 32 프레임(01~32)은
1171281172~1171281213. 마스터는 참고용으로만 사용 (corpus-wide stats).
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from phase_common import load_synonyms, normalize_with_synonyms
from methods import _get_kiwi
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
BLOCKS_DIR = ROOT / "figma_to_html_agent" / "blocks"
HERE = Path(__file__).parent
# 32 프레임 (01~32)
TARGET_FRAME_IDS = sorted([
d.name for d in BLOCKS_DIR.iterdir()
if d.is_dir() and d.name.startswith("1171") and d.name != "1171281171"
])
NOUN_TAGS = {'NNG', 'NNP', 'NR', 'SL', 'SN', 'SH'}
MIN_LEN = 2
# 단일 1글자 숫자/기호 이미 MIN_LEN 으로 배제. 추가 blacklist 는 보수적으로.
STOPWORDS = {
# 범용 수식어 (사용자 명시)
'정의', '특징', '개요', '구성', '결론', '장점', '단점', '고객',
'적극', '구체', '소극',
}
def parse_texts_md(path):
"""texts.md 의 list item 만 추출 (`- item` 라인)."""
text = path.read_text(encoding="utf-8")
items = []
for ln in text.split("\n"):
m = re.match(r'^-\s+(.+)$', ln.strip())
if not m:
continue
item = m.group(1).strip()
# 기호 단독 제거
if item in {"-", "", "—", "(", ")", "/", "*", "**", "."}:
continue
# 단일 한자/문자 (技, 術 같은 장식)
if len(item) <= 1:
continue
items.append(item)
return items
def extract_keywords_from_text_items(items, synonyms):
"""list of text items → set of keywords (Kiwi nouns + synonym norm)."""
joined = "\n".join(items)
normalized = normalize_with_synonyms(joined, synonyms)
kiwi = _get_kiwi()
tokens = kiwi.tokenize(normalized)
out = []
seen = set()
for tok in tokens:
if tok.tag not in NOUN_TAGS:
continue
if len(tok.form) < MIN_LEN:
continue
if tok.form in STOPWORDS:
continue
if tok.form not in seen:
seen.add(tok.form)
out.append(tok.form)
return out
def get_frame_keywords_from_texts(frame_id, synonyms=None):
"""단일 프레임의 texts.md → 키워드 리스트."""
if synonyms is None:
synonyms = load_synonyms()
path = BLOCKS_DIR / frame_id / "texts.md"
if not path.exists():
return []
items = parse_texts_md(path)
return extract_keywords_from_text_items(items, synonyms)
def load_frames_from_texts():
"""32 프레임의 texts.md 기반 키워드 로드.
content 는 analysis.md 에서 재사용 (요약 문장)."""
from phase_common import parse_analysis_md
synonyms = load_synonyms()
frames = {}
for fid in TARGET_FRAME_IDS:
d = BLOCKS_DIR / fid
kws = get_frame_keywords_from_texts(fid, synonyms)
# content (Figma 주제 요약): analysis.md 의 '## 내용' 섹션 재사용
analysis_path = d / "analysis.md"
content = ""
title_original = ""
if analysis_path.exists():
parsed = parse_analysis_md(analysis_path)
content = parsed.get("content", "")
frames[fid] = {"keywords": kws, "content": content}
return frames
def write_report():
"""검수 리포트 생성."""
from phase_common import load_32_frames, compute_df_idf_tier, load_frame_index
synonyms = load_synonyms()
idx_data, frame_to_short = load_frame_index()
# Legacy (analysis.md 후보 키워드)
legacy_frames = load_32_frames()
# New (texts.md 추출)
new_frames = load_frames_from_texts()
lines = []
lines.append("# Figma 키워드 — texts.md 자동 추출 (Phase 23 용)")
lines.append("")
lines.append("**배경**: 기존 analysis.md 의 `후보 키워드` 필드는 curator(Claude Opus 4.7)가 "
"수동 태깅한 것으로, 구조어(3열비교, 표, 다이어그램)와 weak/row label(정의, 특징, 개요 등)이 "
"혼재되어 Phase 23 keyword 축을 오염시킴.")
lines.append("")
lines.append("**해결**: 각 프레임의 `texts.md` (Figma MCP 로 추출된 실제 텍스트 노드) 를 기준으로 "
"Kiwi 명사/외국어/숫자만 자동 추출. 구조어는 애초에 Figma 텍스트에 등장하지 않으므로 "
"자연스럽게 배제됨.")
lines.append("")
lines.append("**파이프라인**:")
lines.append("```")
lines.append("texts.md의 '- item' list →")
lines.append(" 길이 1자 / 단일 기호 필터 →")
lines.append(" synonyms.yaml 정규화 (필수 요건→필수조건, 디지털 전환→DX 등) →")
lines.append(" Kiwi 명사·외국어·숫자 추출 (길이 ≥ 2) →")
lines.append(" STOPWORDS 제거 (정의, 특징, 개요, 결론, 장점, 단점, 고객, 적극, 구체, 소극) →")
lines.append(" 프레임별 키워드 set")
lines.append("```")
lines.append("")
# 전체 통계
all_legacy = set().union(*[set(v['keywords']) for v in legacy_frames.values()])
all_new = set().union(*[set(v['keywords']) for v in new_frames.values()])
lines.append("## 1. 전체 비교")
lines.append("")
lines.append(f"| 구분 | 총 키워드 (중복 포함) | 고유 키워드 | 프레임당 평균 |")
lines.append("|------|--------------------|------------|------------|")
leg_total = sum(len(v['keywords']) for v in legacy_frames.values())
new_total = sum(len(v['keywords']) for v in new_frames.values())
lines.append(f"| Legacy (analysis.md 후보 키워드, 수동) | {leg_total} | {len(all_legacy)} | {leg_total/len(legacy_frames):.1f} |")
lines.append(f"| **New (texts.md 자동 추출)** | **{new_total}** | **{len(all_new)}** | **{new_total/len(new_frames):.1f}** |")
lines.append("")
# 구조어/weak_row가 New 에서 얼마나 제거되는지
from keyword_refinement import classify_keyword
new_structure_leakage = [k for k in all_new if classify_keyword(k) == 'structure']
new_weak_leakage = [k for k in all_new if classify_keyword(k) == 'weak_row']
lines.append(f"**New 에 섞인 구조어 (leakage)**: {len(new_structure_leakage)}개")
if new_structure_leakage:
lines.append(f"- {', '.join(sorted(new_structure_leakage))}")
lines.append("")
lines.append(f"**New 에 섞인 weak_row**: {len(new_weak_leakage)}개")
if new_weak_leakage:
lines.append(f"- {', '.join(sorted(new_weak_leakage))}")
lines.append("")
# 프레임별 before/after
lines.append("## 2. 프레임별 Legacy vs New")
lines.append("")
lines.append("| # | Frame | Legacy 수 | New 수 | 차이 | New에 있지만 Legacy에 없음 (신규) | Legacy에 있지만 New에 없음 (제거) |")
lines.append("|---|-------|----------|--------|------|--------------------------|--------------------------|")
ordered = sorted(new_frames.keys(), key=lambda f: frame_to_short.get(f, "99"))
for fid in ordered:
sid = frame_to_short.get(fid, "?")
legacy_kws = set(legacy_frames.get(fid, {}).get('keywords', []))
new_kws = set(new_frames[fid]['keywords'])
added = new_kws - legacy_kws
removed = legacy_kws - new_kws
diff = len(new_kws) - len(legacy_kws)
added_str = ", ".join(sorted(added)[:8])
if len(added) > 8:
added_str += f"… (+{len(added)-8})"
removed_str = ", ".join(sorted(removed)[:8])
if len(removed) > 8:
removed_str += f"… (+{len(removed)-8})"
lines.append(
f"| {sid} | {fid} | {len(legacy_kws)} | {len(new_kws)} | {diff:+d} | "
f"{added_str or '-'} | {removed_str or '-'} |"
)
lines.append("")
# 프레임별 New 키워드 풀
lines.append("## 3. 프레임별 New 키워드 (텍스트 기반)")
lines.append("")
for fid in ordered:
sid = frame_to_short.get(fid, "?")
kws = new_frames[fid]['keywords']
lines.append(f"### {sid} ({fid}) — {len(kws)}개")
lines.append("")
lines.append(", ".join(f"`{k}`" for k in kws))
lines.append("")
out = HERE / "FIGMA_KEYWORDS_FROM_TEXTS.md"
out.write_text("\n".join(lines), encoding="utf-8")
print(f"완료: {out}")
def main():
write_report()
# 빠른 통계
new_frames = load_frames_from_texts()
from phase_common import load_32_frames
legacy_frames = load_32_frames()
leg_unique = len(set().union(*[set(v['keywords']) for v in legacy_frames.values()]))
new_unique = len(set().union(*[set(v['keywords']) for v in new_frames.values()]))
leg_avg = sum(len(v['keywords']) for v in legacy_frames.values()) / len(legacy_frames)
new_avg = sum(len(v['keywords']) for v in new_frames.values()) / len(new_frames)
print()
print(f"Legacy (analysis.md): 고유 {leg_unique}, 프레임당 평균 {leg_avg:.1f}")
print(f"New (texts.md): 고유 {new_unique}, 프레임당 평균 {new_avg:.1f}")
if __name__ == "__main__":
main()