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

239 lines
8.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.
"""Phase 23 — Domain Terms Mining (검수용 지식 후보)
목적: corpus 전체에서 도메인 용어 자동 추출 + 관계 힌트 자동 분류.
비목적: synonyms.yaml 업데이트 (synonyms.yaml은 6개 동결 유지).
산출물:
- domain_terms.yaml: 용어 풀 + 분류 힌트 (safe_normalization / abbrev-fullname / semantic / context / hierarchy)
- DOMAIN_TERMS_REPORT.md(.html): 사람 검수용
"""
import sys
import re
import json
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}"), # YY.MM
re.compile(r"^[①②③④⑤⑥⑦⑧⑨⑩]+$"),
re.compile(r"^[가-힣]$"), # 단일 한글
re.compile(r"^[A-Za-z]$"), # 단일 영문
]
_STOP = {"것", "수", "등", "때", "중", "후", "전", "및", "이", "그", "저",
"위", "아래", "바", "뿐", "또", "또한", "그리고", "하지만",
"있", "없", "되", "하", "이", "같"}
def is_noise(token):
if len(token) <= 1: return True
if token in _STOP: return True
for p in _NOISE_PATTERNS:
if p.match(token): return True
return False
def load_corpus():
"""BEPS + 32 Figma (analysis + texts + flat) + MDX 3개"""
sources = {}
# BEPS (1171281171은 32개에 없음 — 별도 취급)
beps = BLOCKS_DIR / "1171281171" / "texts.md"
if beps.exists():
sources["BEPS/texts.md"] = beps.read_text(encoding="utf-8")
# 32 Figma
for d in sorted(BLOCKS_DIR.iterdir()):
if not d.is_dir(): continue
fid = d.name
if not fid.startswith("1171") or fid == "1171281171": continue
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")
# MDX
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")
return sources
def mine_repeated_terms(corpus):
"""2회 이상 등장하는 내용어 추출"""
kiwi = _get_kiwi()
counter = collections.Counter()
term_sources = collections.defaultdict(set) # {term: {source_ids}}
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
def find_spacing_variants(terms, corpus_text):
"""'기술개발' 과 '기술 개발' 처럼 공백만 다른 쌍"""
pairs = []
term_set = set(terms)
seen = set()
for t in terms:
if " " in t: continue
if len(t) < 4: continue # 너무 짧으면 노이즈
# 글자 사이에 공백 삽입한 버전이 corpus에 등장하는지
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
def find_ko_en_paren(corpus_text):
"""'한글 (English)' 또는 'English (한글)'"""
out = []
# 한글 (영문)
p1 = re.compile(r"([가-힣][가-힣\s]{1,15}[가-힣])\s*\(([A-Za-z][A-Za-z\s]{1,30}[A-Za-z])\)")
# 영문 (한글)
p2 = re.compile(r"([A-Za-z][A-Za-z\s]{1,30}[A-Za-z])\s*\(([가-힣][가-힣\s]{1,15}[가-힣])\)")
for m in p1.finditer(corpus_text):
out.append((m.group(1).strip(), m.group(2).strip(), "ko→en"))
for m in p2.finditer(corpus_text):
out.append((m.group(2).strip(), m.group(1).strip(), "en→ko"))
return out
def find_abbrev_expansion(corpus_text):
"""'Long Full Name (SHORT)' 패턴 — 약어 풀네임"""
p = re.compile(r"([A-Z][A-Za-z\s]{5,50})\s*\(([A-Z]{2,6})\)")
out = []
for m in p.finditer(corpus_text):
full = m.group(1).strip()
abbr = m.group(2).strip()
if 2 <= len(abbr) <= 6 and len(full.split()) >= 2:
out.append((abbr, full))
return out
def classify_relation(a, b):
"""두 용어 관계 힌트"""
# 공백만 다름
if a.replace(" ", "") == b.replace(" ", "") and a != b:
return "safe_normalization"
# 영문/한글 혼용
is_en_a = bool(re.match(r"^[A-Za-z\s&\./]+$", a))
is_en_b = bool(re.match(r"^[A-Za-z\s&\./]+$", b))
if is_en_a != is_en_b:
return "ko_en_pair"
# 길이 차이 크면 abbrev-full
if abs(len(a) - len(b)) > 4:
return "abbrev_fullname_candidate"
return "semantic_candidate"
def main():
corpus = load_corpus()
all_text = "\n".join(corpus.values())
print(f"Corpus: {len(corpus)}개 소스 (BEPS 1 + Figma 31×3 + MDX 3)")
print()
# 1. 반복 용어 추출
counter, term_sources = mine_repeated_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()
# 2. 공백 변형 쌍
spacing = find_spacing_variants([t for t, _ in filtered], all_text)
print(f"공백 변형 쌍 (safe_normalization 후보): {len(spacing)}개")
for a, b in spacing[:15]:
print(f" {a}{b}")
print()
# 3. 한영 병기
ko_en = find_ko_en_paren(all_text)
# 빈번한 것 top
ko_en_counter = collections.Counter(
(k, e) for k, e, _ in ko_en
)
print(f"한영 병기 패턴 (unique pairs): {len(ko_en_counter)}개")
for (k, e), c in ko_en_counter.most_common(15):
print(f" '{k}' ↔ '{e}' ({c}회)")
print()
# 4. 약어 풀네임
abbrev = find_abbrev_expansion(all_text)
abbrev_counter = collections.Counter(abbrev)
print(f"약어 풀네임 패턴: {len(abbrev_counter)}개")
for (a, f), c in abbrev_counter.most_common(15):
print(f" '{a}' ≡ '{f}' ({c}회)")
print()
# 5. domain_terms.yaml 조립
safe_norm = [{"canonical": a.replace(" ", ""), "variants": [a, b],
"evidence_count_a": counter.get(a, 0), "evidence_count_b": counter.get(b, 0)}
for a, b in spacing]
# 중복 canonical 병합
canonical_groups = collections.defaultdict(set)
for item in safe_norm:
c = item["canonical"]
canonical_groups[c].update(item["variants"])
safe_norm_clean = [{"canonical": c, "variants": sorted(vs),
"evidence": counter.get(c, 0)}
for c, vs in canonical_groups.items()]
safe_norm_clean.sort(key=lambda x: -x["evidence"])
domain = {
"meta": {
"phase": 23,
"purpose": "검수용 도메인 용어 후보. synonyms.yaml은 별도 동결.",
"corpus": f"BEPS 1 + Figma 31×3 + MDX 3 = {len(corpus)} sources",
"total_unique_terms_2plus": len(filtered),
},
"top_terms_by_frequency": [
{"term": t, "count": c, "sources": len(term_sources[t])}
for t, c in filtered[:50]
],
"classified": {
"safe_normalization": safe_norm_clean,
"ko_en_paren_pairs": [
{"ko": k, "en": e, "count": c}
for (k, e), c in ko_en_counter.most_common(40)
],
"abbrev_fullname_patterns": [
{"abbr": a, "full": f, "count": c}
for (a, f), c in abbrev_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_yaml = HERE / "domain_terms.yaml"
with open(out_yaml, "w", encoding="utf-8") as f:
yaml.safe_dump(domain, f, allow_unicode=True, sort_keys=False, width=120)
print(f"완료: {out_yaml}")
if __name__ == "__main__":
main()