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>
This commit is contained in:
2026-07-02 17:03:42 +09:00
co-authored by Claude Opus 4.8
parent 97b7833a1b
commit b836e79ee1
527 changed files with 673036 additions and 717 deletions
+321
View File
@@ -0,0 +1,321 @@
"""Phase 23b — 23의 결과 정제
개선사항:
1. 메타/리포트 노이즈 확장 필터 (px, Frame, TEXT, 타이틀, 서브헤더, 뱃지 등)
2. Kiwi 일반 어간/조동사 stopword 확장 (통하, 위하, 대하, 가능, 필요 등)
3. safe_normalization 후보를 승격가능/보류/폐기로 자동 분류
4. 약어 풀네임 regex 확장 (한영 혼합, 쉼표 구분, 대문자 접두어 조건 완화)
5. corpus 소스 개수 정확한 설명
산출: domain_terms.yaml (덮어쓰기) + DOMAIN_TERMS_REPORT.md 갱신
"""
import sys
import re
import collections
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).parent))
from methods import _get_kiwi, _extract_content_tokens
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
BLOCKS_DIR = ROOT / "figma_to_html_agent" / "blocks"
MDX_DIR = ROOT / "samples" / "mdx_batch"
HERE = Path(__file__).parent
# ═══ 확장 노이즈 필터 ═══
_NOISE_PATTERNS = [
re.compile(r"^\d+$"),
re.compile(r"^\d+[년월일%]"),
re.compile(r"^\d{2}[\.\-/]\d{2}"),
re.compile(r"^[①②③④⑤⑥⑦⑧⑨⑩]+$"),
re.compile(r"^[가-힣]$"),
re.compile(r"^[A-Za-z]$"),
re.compile(r"^[IVXLCDM]+$"), # 로마숫자
re.compile(r"^\d+[a-zA-Z]$"),
]
# 리포트/Figma/HTML 메타 용어 — 도메인 아님
_META_NOISE = {
# Figma/MCP 추출 메타
"Frame", "frame", "TEXT", "Text", "text", "IMAGE", "image",
"bbox", "nodeId", "node", "Vector", "Group", "Polygon", "Line",
"fill", "stroke", "opacity",
# 레이아웃/스타일
"px", "center", "top", "bottom", "left", "right", "middle",
"padding", "margin", "border", "radius",
# 리포트 구조 라벨 (analysis/texts.md에 반복)
"타이틀", "서브타이틀", "서브헤더", "뱃지", "라벨", "섹션", "본문", "헤더",
"", "", "카드", "박스", "영역", "패널", "컬럼", "텍스트", "아이콘",
# 일반 HTML
"src", "alt", "href", "html", "body",
# analysis.md 내부 구조
"내용", "구조", "후보", "키워드", "메타",
# 위치 기호
"좌측", "우측", "상단", "하단",
}
# Kiwi가 명사/어간으로 잡지만 사실 일반어
_GENERIC_STEM = {
"통하", "위하", "대하", "따라", "의하", "가능", "필요", "다양",
"", "", "", "", "", "", "", "나쁘", "", "어렵",
"", "", "", "", "", "다르",
"대한", "위한", "통한", "따른", "의한",
"", "", "", "", "", "", "", "", "", "또한",
"", "", "", "", "", "",
"내용", "사용", "활용", "적용", "수행", "진행", "수용", "제공",
"방법", "방식", "과정", "결과", "상태", # (context에 따라 도메인어지만 단독 일반)
"관련", "대상", "종류", "종합",
}
def is_noise(token):
if len(token) <= 1: return True
if token in _META_NOISE: return True
if token in _GENERIC_STEM: return True
for p in _NOISE_PATTERNS:
if p.match(token): return True
return False
# ═══ Corpus 로드 (정확한 소스 개수 세기) ═══
def load_corpus():
sources = {}
# BEPS
beps = BLOCKS_DIR / "1171281171" / "texts.md"
if beps.exists():
sources["BEPS/texts.md"] = beps.read_text(encoding="utf-8")
# Figma frames (32개 전체) — 32개 중 1171281171도 포함되는지 확인 필요
figma_frame_count = 0
figma_file_count = 0
for d in sorted(BLOCKS_DIR.iterdir()):
if not d.is_dir(): continue
fid = d.name
if not fid.startswith("1171"): continue
figma_frame_count += 1
if fid == "1171281171":
# BEPS는 texts.md는 이미 BEPS key로 넣었으니 analysis/flat만
for fn in ["analysis.md", "flat.md"]:
p = d / fn
if p.exists():
sources[f"Figma/{fid}/{fn}"] = p.read_text(encoding="utf-8")
figma_file_count += 1
# BEPS texts.md는 위에서 카운트됨 (Figma 로도 1개 추가)
figma_file_count += 1 # texts.md
else:
for fn in ["texts.md", "analysis.md", "flat.md"]:
p = d / fn
if p.exists():
sources[f"Figma/{fid}/{fn}"] = p.read_text(encoding="utf-8")
figma_file_count += 1
# MDX
mdx_count = 0
for mdx in ["01.mdx", "02.mdx", "03.mdx"]:
p = MDX_DIR / mdx
if p.exists():
sources[f"MDX/{mdx}"] = p.read_text(encoding="utf-8")
mdx_count += 1
return sources, figma_frame_count, figma_file_count, mdx_count
# ═══ 용어 추출 ═══
def mine_terms(corpus):
kiwi = _get_kiwi()
counter = collections.Counter()
term_sources = collections.defaultdict(set)
for src_id, text in corpus.items():
tokens = _extract_content_tokens(text, kiwi)
for t in tokens:
if is_noise(t): continue
counter[t] += 1
term_sources[t].add(src_id)
return counter, term_sources
# ═══ Safe normalization 분류 ═══
_META_STRUCTURE_WORDS = {"서브타이틀", "서브헤더", "타이틀", "헤더", "라벨", "뱃지"}
def classify_safe_norm(a, b):
"""공백 변형 쌍을 승격가능/보류/폐기로 분류"""
joined = a if " " not in a else b
spaced = b if " " in b else a
# 노이즈: 마침표/특수문자 포함 (Engn.S)
if "." in joined or "." in spaced:
return "discard", "토큰에 마침표 포함 — Kiwi 분해 노이즈"
# 노이즈: 너무 짧거나 접미사 조각 (rdParty)
if len(joined.replace(" ", "")) < 4:
return "discard", "너무 짧음 — 의미 토큰 아님"
if joined.startswith("rd") or joined.startswith("st") or joined.startswith("nd") or joined.startswith("th"):
return "discard", "서수 접미사 조각 (rdParty 등)"
# 보류: 문서 메타 구조어
if joined in _META_STRUCTURE_WORDS or spaced in _META_STRUCTURE_WORDS:
return "hold", "문서 구조 메타 — synonyms에 넣지 말 것"
# 보류: 고유 제품명/브랜드 (WatchBIM, BIGRoom, TwinHighway 등 CamelCase/PascalCase)
if re.match(r"^[A-Z][a-z]+[A-Z]", joined):
return "hold", "고유 제품명/브랜드 — 치환 위험"
# 승격가능: 도메인 관련 한글 복합어
return "promote", "도메인 일반어 공백 변형"
def find_spacing_variants(terms, corpus_text):
pairs = []
seen = set()
for t in terms:
if " " in t: continue
if len(t) < 4: continue
for split_pos in range(1, len(t)):
spaced = t[:split_pos] + " " + t[split_pos:]
if spaced in corpus_text and (t, spaced) not in seen:
pairs.append((t, spaced))
seen.add((t, spaced)); seen.add((spaced, t))
break
return pairs
# ═══ 약어 풀네임 regex 확장 ═══
def find_abbrev_expansion(corpus_text):
"""여러 패턴 지원:
- `Full Name (ABBR)` — 영문 풀네임 + 대문자 약어
- `ABBR (Full Name)` — 역순
- `한글단어 (ABBR)` / `ABBR (한글단어)` — 한영 혼합
- `ABBR (Full Name, 한글단어)` — 쉼표 구분 병기
"""
results = collections.Counter()
# Pattern 1: Long English (SHORT)
p1 = re.compile(r"([A-Za-z][A-Za-z\s\&\-]{4,50}[A-Za-z])\s*\(([A-Z]{2,8})\)")
for m in p1.finditer(corpus_text):
full, abbr = m.group(1).strip(), m.group(2).strip()
if len(full.split()) >= 2:
results[(abbr, full)] += 1
# Pattern 2: SHORT (Long English or Korean or both comma-separated)
p2 = re.compile(r"([A-Z]{2,8})\s*\(([^()]{5,80})\)")
for m in p2.finditer(corpus_text):
abbr, inside = m.group(1).strip(), m.group(2).strip()
# inside를 쉼표로 분할
parts = [p.strip() for p in inside.split(",")]
for p in parts:
if not p: continue
# 영문 full
if re.match(r"^[A-Za-z][A-Za-z\s\&\-]{3,}$", p) and len(p.split()) >= 2:
results[(abbr, p)] += 1
# 한글 풀네임
elif re.match(r"^[가-힣][가-힣\s]{3,}[가-힣]$", p):
results[(abbr, p)] += 1
# Pattern 3: 한글 (ABBR)
p3 = re.compile(r"([가-힣][가-힣\s]{3,20}[가-힣])\s*\(([A-Z]{2,8})\)")
for m in p3.finditer(corpus_text):
full, abbr = m.group(1).strip(), m.group(2).strip()
results[(abbr, full)] += 1
return results
def main():
corpus, n_figma_frames, n_figma_files, n_mdx = load_corpus()
all_text = "\n".join(corpus.values())
print(f"Corpus 소스 구성:")
print(f" BEPS 본문: 1개")
print(f" Figma 프레임: {n_figma_frames}개 → {n_figma_files}개 파일 (각 프레임 당 texts/analysis/flat.md)")
print(f" MDX 본문: {n_mdx}")
print(f" 총 소스 개수: {len(corpus)}")
print()
# 1. 반복 용어 (확장 노이즈 필터 적용)
counter, term_sources = mine_terms(corpus)
filtered = [(t, c) for t, c in counter.items() if c >= 2]
filtered.sort(key=lambda x: -x[1])
print(f"노이즈 필터 후 2회+ 등장 용어: {len(filtered)}")
print(f"상위 20개: {[t for t, _ in filtered[:20]]}")
print()
# 2. 공백 변형 + 분류
spacing = find_spacing_variants([t for t, _ in filtered], all_text)
print(f"공백 변형 쌍 원본: {len(spacing)}")
classified = {"promote": [], "hold": [], "discard": []}
for a, b in spacing:
verdict, reason = classify_safe_norm(a, b)
classified[verdict].append({
"canonical": a.replace(" ", ""),
"variants": sorted([a, b]),
"reason": reason,
})
for v in ("promote", "hold", "discard"):
print(f" {v}: {len(classified[v])}")
for item in classified[v]:
print(f" {item['variants']}{item['reason']}")
print()
# 3. 약어 풀네임 (확장 regex)
abbrev = find_abbrev_expansion(all_text)
print(f"약어 풀네임 쌍 (확장 regex): {len(abbrev)}")
for (a, f), c in abbrev.most_common(20):
print(f" '{a}''{f}' ({c}회)")
print()
# 4. 한영 병기 (evidence only 태그)
p_keng = re.compile(r"([가-힣][가-힣\s]{1,15}[가-힣])\s*\(([A-Za-z][A-Za-z\s]{1,30}[A-Za-z])\)")
p_engk = re.compile(r"([A-Za-z][A-Za-z\s]{1,30}[A-Za-z])\s*\(([가-힣][가-힣\s]{1,15}[가-힣])\)")
ke_pairs = []
for m in p_keng.finditer(all_text):
ke_pairs.append((m.group(1).strip(), m.group(2).strip()))
for m in p_engk.finditer(all_text):
ke_pairs.append((m.group(2).strip(), m.group(1).strip()))
ke_counter = collections.Counter(ke_pairs)
print(f"한영 병기 쌍: {len(ke_counter)}개 (대부분 evidence-only)")
print()
# 5. domain_terms.yaml 갱신
domain = {
"meta": {
"phase": "23b",
"purpose": "검수용 도메인 용어 카탈로그 (synonyms.yaml/hierarchy.yaml 승격 전 단계)",
"corpus_breakdown": {
"BEPS": "1 (본문 source)",
"Figma_frames": f"{n_figma_frames}개 프레임 → {n_figma_files}개 파일 (texts/analysis/flat)",
"MDX": f"{n_mdx}",
"total_sources": len(corpus),
},
"total_terms_2plus_after_noise_filter": len(filtered),
"note": "synonyms.yaml은 현재 6개 canonical로 동결. 이 파일은 승격 후보만 제공.",
},
"top_terms_by_frequency": [
{"term": t, "count": c, "sources": len(term_sources[t])}
for t, c in filtered[:50]
],
"safe_normalization": {
"promote": classified["promote"],
"hold": classified["hold"],
"discard": classified["discard"],
},
"abbrev_fullname_candidates": [
{"abbr": a, "full": f, "count": c}
for (a, f), c in abbrev.most_common(40)
],
"ko_en_paren_pairs_evidence_only": [
{"ko": k, "en": e, "count": c}
for (k, e), c in ke_counter.most_common(40)
],
"needs_human_review": [
"발주자 vs 발주처 (역할 vs 기관)",
"시공자 vs 시공사 (역할 vs 회사)",
"설계자 vs 설계사 (역할 vs 회사)",
"Engineering S/W vs Engn. S/W (약어 — 동의어 가능)",
"Engineering S/W vs Solution S/W (상하위 or 분류)",
"Engineering S/W vs Application S/W (상하위 or 분류)",
],
}
out = HERE / "domain_terms.yaml"
with open(out, "w", encoding="utf-8") as f:
yaml.safe_dump(domain, f, allow_unicode=True, sort_keys=False, width=120)
print(f"완료: {out}")
if __name__ == "__main__":
main()